diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..2888487 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Report a bug to help us improve qrm +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Run command '...' with input '...' +2. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**System (include if related to running the bot):** + - OS: [e.g. Linux, Docker] + - Version: [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..abd211f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for qrm +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/CHANGELOG.md b/CHANGELOG.md index f44c0cb..38a60a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Rich lookup for QRZ, if a QRZ subscription is present - Timestamp and requester username and avatar are now shown on embeds - Current and 3-Day Forecast terrestrial weather conditions lookup commands +- Extension control commands. - Changelog command ### Changed - Rewrote code to take advantage of discord.py's cogs diff --git a/README-DOCKER.md b/README-DOCKER.md index 45b19d7..ce62610 100644 --- a/README-DOCKER.md +++ b/README-DOCKER.md @@ -4,8 +4,8 @@ A sample `docker-compose.yml` file: version: '3' services: bot: - image: "classabbyamp/discord-qrm-bot:latest" - container_name: "qrmbot" + image: "classabbyamp/discord-qrm2:latest" + container_name: "discord-qrm2" volumes: - "./data:/app/data:rw" ``` diff --git a/README.md b/README.md index b3bfa1d..3671116 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Discord QRM Bot +# qrm, a Bot for Discord A discord bot with ham radio functionalities. diff --git a/common.py b/common.py index 9005238..bb0e052 100644 --- a/common.py +++ b/common.py @@ -3,7 +3,7 @@ Common tools for the bot. --- Copyright (C) 2019 Abigail Gold, 0x5c -This file is part of discord-qrmbot and is released under the terms of the GNU +This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. --- @@ -12,8 +12,22 @@ General Public License, version 2. `cat`: Category names for the HelpCommand. """ + +import traceback +from datetime import datetime from types import SimpleNamespace +import discord +import discord.ext.commands as commands + + +import data.options as opt + + +__all__ = ["colours", "cat", "emojis", "error_embed_factory", "add_react", "check_if_owner"] + + +# --- Common values --- colours = SimpleNamespace(good=0x43B581, neutral=0x7289DA, @@ -25,3 +39,39 @@ cat = SimpleNamespace(lookup='Information Lookup', ref='Reference', study='Exam Study', weather='Land and Space Weather') + +emojis = SimpleNamespace(good='✅', + bad='❌') + + +# --- Helper functions --- + +def error_embed_factory(ctx: commands.Context, exception: Exception, debug_mode: bool) -> discord.Embed: + """Creates an Error embed.""" + if debug_mode: + fmtd_ex = traceback.format_exception(exception.__class__, exception, exception.__traceback__) + else: + fmtd_ex = traceback.format_exception_only(exception.__class__, exception) + embed = discord.Embed(title="Error", + timestamp=datetime.utcnow(), + colour=colours.bad) + embed.set_footer(text=ctx.author, + icon_url=str(ctx.author.avatar_url)) + embed.description = "```\n" + '\n'.join(fmtd_ex) + "```" + return embed + + +async def add_react(msg: discord.Message, react: str): + try: + await msg.add_reaction(react) + except discord.Forbidden: + print(f"[!!] Missing permissions to add reaction in '{msg.guild.id}/{msg.channel.id}'!") + + +# --- Checks --- + +async def check_if_owner(ctx: commands.Context): + if ctx.author.id in opt.owners_uids: + return True + await add_react(ctx.message, emojis.bad) + return False diff --git a/docker-compose.yml b/docker-compose.yml index 183d669..14ae78d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,6 @@ version: '3' services: bot: build: . - container_name: "qrmbot" + container_name: "discord-qrm2" volumes: - "./data:/app/data:rw" diff --git a/cogs/ae7qcog.py b/exts/ae7q.py similarity index 98% rename from cogs/ae7qcog.py rename to exts/ae7q.py index 8e94690..0cb9457 100644 --- a/cogs/ae7qcog.py +++ b/exts/ae7q.py @@ -1,9 +1,9 @@ """ -ae7q cog for qrm +ae7q extension for qrm --- Copyright (C) 2019 Abigail Gold, 0x5c -This file is part of discord-qrmbot and is released under the terms of the GNU +This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. --- Test callsigns: diff --git a/cogs/basecog.py b/exts/base.py similarity index 97% rename from cogs/basecog.py rename to exts/base.py index 290a8cd..4adb6cb 100644 --- a/cogs/basecog.py +++ b/exts/base.py @@ -1,9 +1,9 @@ """ -Base cog for qrm +Base extension for qrm --- Copyright (C) 2019 Abigail Gold, 0x5c -This file is part of discord-qrmbot and is released under the terms of the GNU +This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. """ @@ -17,7 +17,7 @@ import discord.ext.commands as commands import info -from data import options as opt +import data.options as opt import common as cmn @@ -142,7 +142,7 @@ class BaseCog(commands.Cog): """Show what has changed in recent bot versions.""" embed = discord.Embed(title="qrm Changelog", description=("For a full listing, visit [Github](https://" - "github.com/classabbyamp/discord-qrm-bot/blob/master/CHANGELOG.md)."), + "github.com/classabbyamp/discord-qrm2/blob/master/CHANGELOG.md)."), colour=cmn.colours.neutral, timestamp=datetime.utcnow()) embed.set_footer(text=ctx.author.name, diff --git a/cogs/funcog.py b/exts/fun.py similarity index 90% rename from cogs/funcog.py rename to exts/fun.py index 5662b0f..d90d99e 100644 --- a/cogs/funcog.py +++ b/exts/fun.py @@ -1,9 +1,9 @@ """ -Fun cog for qrm +Fun extension for qrm --- Copyright (C) 2019 Abigail Gold, 0x5c -This file is part of discord-qrmbot and is released under the terms of the GNU +This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. """ diff --git a/cogs/gridcog.py b/exts/grid.py similarity index 98% rename from cogs/gridcog.py rename to exts/grid.py index 5e5f9dd..76f4d8e 100644 --- a/cogs/gridcog.py +++ b/exts/grid.py @@ -1,9 +1,9 @@ """ -Grid cog for qrm +Grid extension for qrm --- Copyright (C) 2019 Abigail Gold, 0x5c -This file is part of discord-qrmbot and is released under the terms of the GNU +This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. """ diff --git a/cogs/hamcog.py b/exts/ham.py similarity index 95% rename from cogs/hamcog.py rename to exts/ham.py index 58ede8e..f48872e 100644 --- a/cogs/hamcog.py +++ b/exts/ham.py @@ -1,9 +1,9 @@ """ -Ham cog for qrm +Ham extension for qrm --- Copyright (C) 2019 Abigail Gold, 0x5c -This file is part of discord-qrmbot and is released under the terms of the GNU +This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. """ import json @@ -84,7 +84,7 @@ class HamCog(commands.Cog): if country.lower() not in callsign_info.options: embed = discord.Embed(title=f'{country} not found!', description=f'Valid countries: {", ".join(callsign_info.options.keys())}', - colour=self.gs.colours.bad, + colour=cmn.colours.bad, timestamp=datetime.utcnow()) embed.set_footer(text=ctx.author.name, icon_url=str(ctx.author.avatar_url)) @@ -92,7 +92,7 @@ class HamCog(commands.Cog): return embed = discord.Embed(title=callsign_info.options[country.lower()][0], description=callsign_info.options[country.lower()][1], - colour=self.gs.colours.good, + colour=cmn.colours.good, timestamp=datetime.utcnow()) embed.set_footer(text=ctx.author.name, icon_url=str(ctx.author.avatar_url)) diff --git a/cogs/imagecog.py b/exts/image.py similarity index 98% rename from cogs/imagecog.py rename to exts/image.py index 51d4d7b..e1014b5 100644 --- a/cogs/imagecog.py +++ b/exts/image.py @@ -1,9 +1,9 @@ """ -Image cog for qrm +Image extension for qrm --- Copyright (C) 2019 Abigail Gold, 0x5c -This file is part of discord-qrmbot and is released under the terms of the GNU +This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. """ diff --git a/cogs/lookupcog.py b/exts/lookup.py similarity index 100% rename from cogs/lookupcog.py rename to exts/lookup.py diff --git a/cogs/morsecog.py b/exts/morse.py similarity index 97% rename from cogs/morsecog.py rename to exts/morse.py index 0ba8ab0..8222518 100644 --- a/cogs/morsecog.py +++ b/exts/morse.py @@ -1,9 +1,9 @@ """ -Morse Code cog for qrm +Morse Code extension for qrm --- Copyright (C) 2019 Abigail Gold, 0x5c -This file is part of discord-qrmbot and is released under the terms of the GNU +This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. """ diff --git a/cogs/qrzcog.py b/exts/qrz.py similarity index 97% rename from cogs/qrzcog.py rename to exts/qrz.py index 1a18a94..c09d863 100644 --- a/cogs/qrzcog.py +++ b/exts/qrz.py @@ -1,9 +1,9 @@ """ -QRZ cog for qrm +QRZ extension for qrm --- Copyright (C) 2019 Abigail Gold, 0x5c -This file is part of discord-qrmbot and is released under the terms of the GNU +This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. """ from collections import OrderedDict @@ -17,7 +17,7 @@ import aiohttp from lxml import etree import common as cmn -import keys +import data.keys as keys class QRZCog(commands.Cog): @@ -101,7 +101,7 @@ class QRZCog(commands.Cog): async def qrz_login(user: str, passwd: str, session: aiohttp.ClientSession): - url = f'http://xmldata.qrz.com/xml/current/?username={user};password={passwd};agent=qrmbot' + url = f'http://xmldata.qrz.com/xml/current/?username={user};password={passwd};agent=discord-qrm2' async with session.get(url) as resp: if resp.status != 200: raise ConnectionError(f'Unable to connect to QRZ (HTTP Error {resp.status})') diff --git a/cogs/studycog.py b/exts/study.py similarity index 98% rename from cogs/studycog.py rename to exts/study.py index b72348c..8b85eaa 100644 --- a/cogs/studycog.py +++ b/exts/study.py @@ -1,9 +1,9 @@ """ -Study cog for qrm +Study extension for qrm --- Copyright (C) 2019 Abigail Gold, 0x5c -This file is part of discord-qrmbot and is released under the terms of the GNU +This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. """ diff --git a/cogs/weathercog.py b/exts/weather.py similarity index 88% rename from cogs/weathercog.py rename to exts/weather.py index 5c1faab..f7aed05 100644 --- a/cogs/weathercog.py +++ b/exts/weather.py @@ -1,9 +1,9 @@ """ -Weather cog for qrm +Weather extension for qrm --- Copyright (C) 2019 Abigail Gold, 0x5c -This file is part of discord-qrmbot and is released under the terms of the GNU +This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. """ @@ -90,13 +90,10 @@ See help for weather command for possible location types. Add a `-c` or `-f` to embed.colour = cmn.colours.bad else: data = io.BytesIO(await resp.read()) - loc = loc.replace('+', '') - loc = loc.replace('@', '') - loc = loc.replace('~', '') - embed.set_image(url=f'attachment://{loc}_{units}pnFQ.png') + embed.set_image(url=f'attachment://wttr_forecast.png') embed.set_footer(text=ctx.author.name, icon_url=str(ctx.author.avatar_url)) - await ctx.send(embed=embed, file=discord.File(data, f'{loc}_{units}pnFQ.png')) + await ctx.send(embed=embed, file=discord.File(data, f'wttr_forecast.png')) @_weather_conditions.command(name='now', aliases=['n'], category=cmn.cat.weather) async def _weather_conditions_now(self, ctx: commands.Context, *, location: str): @@ -128,13 +125,10 @@ See help for weather command for possible location types. Add a `-c` or `-f` to embed.colour = cmn.colours.bad else: data = io.BytesIO(await resp.read()) - loc = loc.replace('+', '') - loc = loc.replace('@', '') - loc = loc.replace('~', '') - embed.set_image(url=f'attachment://{loc}_0{units}pnFQ.png') + embed.set_image(url=f'attachment://wttr_now.png') embed.set_footer(text=ctx.author.name, icon_url=str(ctx.author.avatar_url)) - await ctx.send(embed=embed, file=discord.File(data, f'{loc}_0{units}pnFQ.png')) + await ctx.send(embed=embed, file=discord.File(data, 'wttr_now.png')) def setup(bot: commands.Bot): diff --git a/info.py b/info.py index 3ca279b..20a174d 100644 --- a/info.py +++ b/info.py @@ -3,7 +3,7 @@ Static info about the bot. --- Copyright (C) 2019 Abigail Gold, 0x5c -This file is part of discord-qrmbot and is released under the terms of the GNU +This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. --- @@ -23,5 +23,5 @@ General Public License, version 2. authors = ("@ClassAbbyAmplifier#2229", "@0x5c#0639") description = """A bot with various useful ham radio-related functions, written in Python.""" license = "Released under the GNU General Public License v2" -contributing = "Check out the source on GitHub, contributions welcome: https://github.com/classabbyamp/discord-qrm-bot" +contributing = "Check out the source on GitHub, contributions welcome: https://github.com/classabbyamp/discord-qrm2" release = '1.0.0' diff --git a/main.py b/main.py index 9a9ef0b..05a7b08 100644 --- a/main.py +++ b/main.py @@ -4,23 +4,29 @@ qrm, a bot for Discord --- Copyright (C) 2019 Abigail Gold, 0x5c -This file is part of discord-qrmbot and is released under the terms of the GNU +This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. """ + +from datetime import datetime + import discord from discord.ext import commands, tasks +import common as cmn import info -from data import options as opt -from data import keys +import data.options as opt +import data.keys as keys # --- Settings --- exit_code = 1 # The default exit code. ?shutdown and ?restart will change it accordingly (fail-safe) +ext_dir = "exts" # The name of the directory where extensions are located. + debug_mode = opt.debug # Separate assignement in-case we define an override (ternary operator goes here) @@ -31,46 +37,80 @@ bot = commands.Bot(command_prefix=opt.prefix, help_command=commands.MinimalHelpCommand()) -# --- Helper functions --- - -async def add_react(msg: discord.Message, react: str): - try: - await msg.add_reaction(react) - except discord.Forbidden: - print(f"!! Missing permissions to add reaction in '{msg.guild.id}/{msg.channel.id}'!") - - -# --- Checks --- - -async def check_if_owner(ctx: commands.Context): - if ctx.author.id in opt.owners_uids: - return True - await add_react(ctx.message, "❌") - return False - - # --- Commands --- @bot.command(name="restart", hidden=True) -@commands.check(check_if_owner) +@commands.check(cmn.check_if_owner) async def _restart_bot(ctx: commands.Context): """Restarts the bot.""" global exit_code - await add_react(ctx.message, "✅") + await cmn.add_react(ctx.message, cmn.emojis.good) + print(f"[**] Restarting! Requested by {ctx.author}.") exit_code = 42 # Signals to the wrapper script that the bot needs to be restarted. await bot.logout() @bot.command(name="shutdown", hidden=True) -@commands.check(check_if_owner) +@commands.check(cmn.check_if_owner) async def _shutdown_bot(ctx: commands.Context): """Shuts down the bot.""" global exit_code - await add_react(ctx.message, "✅") + await cmn.add_react(ctx.message, cmn.emojis.good) + print(f"[**] Shutting down! Requested by {ctx.author}.") exit_code = 0 # Signals to the wrapper script that the bot should not be restarted. await bot.logout() +@bot.group(name="extctl", hidden=True) +@commands.check(cmn.check_if_owner) +async def _extctl(ctx: commands.Context): + """Extension control commands. + Defaults to `list` if no subcommand specified""" + if ctx.invoked_subcommand is None: + cmd = bot.get_command("extctl list") + await ctx.invoke(cmd) + + +@_extctl.command(name="list") +async def _extctl_list(ctx: commands.Context): + """Lists Extensions.""" + embed = discord.Embed(title="Loaded Extensions", + colour=cmn.colours.neutral, + timestamp=datetime.utcnow()) + embed.description = "\n".join(["‣ " + x.split(".")[1] for x in bot.extensions.keys()]) + await ctx.send(embed=embed) + + +@_extctl.command(name="load") +async def _extctl_load(ctx: commands.Context, extension: str): + try: + bot.load_extension(ext_dir + "." + extension) + await cmn.add_react(ctx.message, cmn.emojis.good) + except commands.ExtensionError as ex: + embed = cmn.error_embed_factory(ctx, ex, debug_mode) + await ctx.send(embed=embed) + + +@_extctl.command(name="reload") +async def _extctl_reload(ctx: commands.Context, extension: str): + try: + bot.reload_extension(ext_dir + "." + extension) + await cmn.add_react(ctx.message, cmn.emojis.good) + except commands.ExtensionError as ex: + embed = cmn.error_embed_factory(ctx, ex, debug_mode) + await ctx.send(embed=embed) + + +@_extctl.command(name="unload") +async def _extctl_unload(ctx: commands.Context, extension: str): + try: + bot.unload_extension(ext_dir + "." + extension) + await cmn.add_react(ctx.message, cmn.emojis.good) + except commands.ExtensionError as ex: + embed = cmn.error_embed_factory(ctx, ex, debug_mode) + await ctx.send(embed=embed) + + # --- Events --- @bot.event @@ -93,9 +133,8 @@ async def _before_ensure_activity(): # --- Run --- -# bot.add_cog(GlobalSettings(bot)) -for cog in opt.cogs: - bot.load_extension(f"cogs.{cog}") +for ext in opt.exts: + bot.load_extension(ext_dir + '.' + ext) _ensure_activity.start() diff --git a/resources/morse.json b/resources/morse.json index d43c9a2..51f7a79 100644 --- a/resources/morse.json +++ b/resources/morse.json @@ -8,5 +8,5 @@ "?": "..--..", "'": ".----.", "!": "-.-.--", "/": "-..-.", "(": "-.--.", ")": "-.--.-", "&": ".-...", ":": "---...", ";": "-.-.-.", "=": "-...-", "+": ".-.-.", "-": "-....-", "\"": ".-..-.", "@": ".--.-.", "Ä": ".-.-", "Å": ".-.-", "Ą": ".-.-", "Æ": ".-.-", -"É": "..-..", "Ñ": "--.--", "Ö": "---.", "Ü": "..--", "Š": "----" +"É": "..-..", "Ñ": "--.--", "Ö": "---.", "Ü": "..--", "Š": "----", " ": "/" } diff --git a/templates/data/options.py b/templates/data/options.py index 8c60b01..81ecc8b 100644 --- a/templates/data/options.py +++ b/templates/data/options.py @@ -26,9 +26,8 @@ debug = False # ! This MUST be a tuple of integers. Single element tuple: `(123,)` owners_uids = (200102491231092736,) -# The cogs to load when running the bot. -cogs = ['basecog', 'morsecog', 'funcog', 'gridcog', 'hamcog', 'imagecog', - 'studycog', 'ae7qcog', 'qrzcog', 'weathercog'] +# The extensions to load when running the bot. +exts = ['ae7q', 'base', 'fun', 'grid', 'ham', 'image', 'morse', 'qrz', 'study', 'weather'] # The text to put in the "playing" status. game = 'with lids on 7.200'