From ad2c559a0fd53edb7406944bd22c16a08a424add Mon Sep 17 00:00:00 2001 From: Abigail Gold <5366828+classabbyamp@users.noreply.github.com> Date: Fri, 6 Dec 2019 01:19:42 -0500 Subject: [PATCH] Custom Help Command (#55) * subclasses HelpCommand to implement a custom help command. add cog names for display in the help command. Globalsettings is no longer a cog. HelpCommand uses the custom attribute category for grouping commands. * PEP8 fixes * improve variable names for help command clarity. Fixes #70 * improve help command formatting add aliases for weather commands add help text for ae7q call and qrz * move global_settings to common * rename import alias * fix loading/unloading of help command * fix info command * fix options import * changed canonical command names to be more descriptive * remove cog names and revert map/bandplan error listing * add link to hamstudy references --- cogs/ae7qcog.py | 22 +++++---- cogs/basecog.py | 112 +++++++++++++++++++++++++++++++++++++++++---- cogs/funcog.py | 13 +++--- cogs/gridcog.py | 28 ++++++------ cogs/hamcog.py | 18 ++++---- cogs/imagecog.py | 36 +++++++-------- cogs/morsecog.py | 19 ++++---- cogs/qrzcog.py | 25 +++++----- cogs/studycog.py | 34 ++++++++------ cogs/weathercog.py | 35 +++++++------- common.py | 27 +++++++++++ info.py | 6 ++- main.py | 22 ++------- 13 files changed, 257 insertions(+), 140 deletions(-) create mode 100644 common.py diff --git a/cogs/ae7qcog.py b/cogs/ae7qcog.py index aa7eea6..8e94690 100644 --- a/cogs/ae7qcog.py +++ b/cogs/ae7qcog.py @@ -21,20 +21,22 @@ import discord.ext.commands as commands from bs4 import BeautifulSoup import aiohttp +import common as cmn + class AE7QCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.gs = bot.get_cog("GlobalSettings") - @commands.group(name="ae7q", aliases=["ae"]) + @commands.group(name="ae7q", aliases=["ae"], category=cmn.cat.lookup) async def _ae7q_lookup(self, ctx: commands.Context): - '''Look up a callsign, FRN, or Licensee ID on ae7q.com''' + '''Look up a callsign, FRN, or Licensee ID on [ae7q.com](http://ae7q.com/).''' if ctx.invoked_subcommand is None: await ctx.send_help(ctx.command) - @_ae7q_lookup.command(name="call") + @_ae7q_lookup.command(name="call", category=cmn.cat.lookup) async def _ae7q_call(self, ctx: commands.Context, callsign: str): + '''Look up the history for a callsign on [ae7q.com](http://ae7q.com/).''' callsign = callsign.upper() desc = '' base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" @@ -61,7 +63,7 @@ class AE7QCog(commands.Cog): if rows is None: embed = discord.Embed(title=f"AE7Q History for {callsign}", - colour=self.gs.colours.bad, + colour=cmn.colours.bad, url=f"{base_url}{callsign}", timestamp=datetime.utcnow()) embed.set_footer(text=ctx.author.name, @@ -91,7 +93,7 @@ class AE7QCog(commands.Cog): table_contents += [row_cells] embed = discord.Embed(title=f"AE7Q Records for {callsign}", - colour=self.gs.colours.good, + colour=cmn.colours.good, url=f"{base_url}{callsign}", timestamp=datetime.utcnow()) @@ -118,20 +120,20 @@ class AE7QCog(commands.Cog): # TODO: write commands for other AE7Q response types? # @_ae7q_lookup.command(name="trustee") - # async def _ae7q_trustee(self, ctx, callsign: str): + # async def _ae7q_trustee(self, ctx: commands.Context, callsign: str): # pass # @_ae7q_lookup.command(name="applications", aliases=['apps']) - # async def _ae7q_applications(self, ctx, callsign: str): + # async def _ae7q_applications(self, ctx: commands.Context, callsign: str): # pass # @_ae7q_lookup.command(name="frn") - # async def _ae7q_frn(self, ctx, frn: str): + # async def _ae7q_frn(self, ctx: commands.Context, frn: str): # base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN=" # pass # @_ae7q_lookup.command(name="licensee", aliases=["lic"]) - # async def _ae7q_licensee(self, ctx, frn: str): + # async def _ae7q_licensee(self, ctx: commands.Context, frn: str): # base_url = "http://ae7q.com/query/data/LicenseeIdHistory.php?ID=" # pass diff --git a/cogs/basecog.py b/cogs/basecog.py index 9485142..290a8cd 100644 --- a/cogs/basecog.py +++ b/cogs/basecog.py @@ -15,26 +15,114 @@ import random import discord import discord.ext.commands as commands +import info + +from data import options as opt +import common as cmn + + +class QrmHelpCommand(commands.HelpCommand): + def __init__(self): + super().__init__(command_attrs={'help': 'Shows help about qrm or a command', + 'aliases': ['h']}) + + def get_bot_mapping(self): + bot = self.context.bot + mapping = {} + for cmd in bot.commands: + cat = cmd.__original_kwargs__.get('category', None) + if cat in mapping: + mapping[cat].append(cmd) + else: + mapping[cat] = [cmd] + return mapping + + def get_command_signature(self, command): + parent = command.full_parent_name + if command.aliases != []: + aliases = ', '.join(command.aliases) + fmt = command.name + if parent: + fmt = f'{parent} {fmt}' + alias = fmt + return f'{opt.prefix}{alias} {command.signature}\n *Aliases:* {aliases}' + alias = command.name if not parent else f'{parent} {command.name}' + return f'{opt.prefix}{alias} {command.signature}' + + async def send_error_message(self, error): + embed = discord.Embed(title='qrm Help Error', + description=error, + colour=cmn.colours.bad, + timestamp=datetime.utcnow() + ) + embed.set_footer(text=self.context.author.name, + icon_url=str(self.context.author.avatar_url)) + await self.context.send(embed=embed) + + async def send_bot_help(self, mapping): + embed = discord.Embed(title='qrm Help', + description=(f'For command-specific help and usage, use `{opt.prefix}help [command name]`' + '. Many commands have shorter aliases.'), + colour=cmn.colours.neutral, + timestamp=datetime.utcnow() + ) + embed.set_footer(text=self.context.author.name, + icon_url=str(self.context.author.avatar_url)) + + for cat, cmds in mapping.items(): + cmds = list(filter(lambda x: not x.hidden, cmds)) + if cmds == []: + continue + names = sorted([cmd.name for cmd in cmds]) + if cat is not None: + embed.add_field(name=cat.title(), value=', '.join(names), inline=False) + else: + embed.add_field(name='Other', value=', '.join(names), inline=False) + await self.context.send(embed=embed) + + async def send_command_help(self, command): + embed = discord.Embed(title=self.get_command_signature(command), + description=command.help, + colour=cmn.colours.neutral, + timestamp=datetime.utcnow() + ) + embed.set_footer(text=self.context.author.name, + icon_url=str(self.context.author.avatar_url)) + await self.context.send(embed=embed) + + async def send_group_help(self, group): + embed = discord.Embed(title=self.get_command_signature(group), + description=group.help, + colour=cmn.colours.neutral, + timestamp=datetime.utcnow() + ) + embed.set_footer(text=self.context.author.name, + icon_url=str(self.context.author.avatar_url)) + for cmd in group.commands: + embed.add_field(name=self.get_command_signature(cmd), value=cmd.help, inline=False) + await self.context.send(embed=embed) + class BaseCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.gs = bot.get_cog("GlobalSettings") self.changelog = parse_changelog() @commands.command(name="info", aliases=["about"]) - async def _info(self, ctx): + async def _info(self, ctx: commands.Context): """Shows info about qrm.""" embed = discord.Embed(title="About qrm", - description=self.gs.info.description, - colour=self.gs.colours.neutral, + description=info.description, + colour=cmn.colours.neutral, timestamp=datetime.utcnow()) embed.set_footer(text=ctx.author.name, icon_url=str(ctx.author.avatar_url)) + + embed = embed.add_field(name="Authors", value=", ".join(info.authors)) + embed = embed.add_field(name="License", value=info.license) + embed = embed.add_field(name="Version", value=f'v{info.release}') + embed = embed.add_field(name="Contributing", value=info.contributing, inline=False) embed.set_thumbnail(url=str(self.bot.user.avatar_url)) - embed = embed.add_field(name="Authors", value=", ".join(self.gs.info.authors)) - embed = embed.add_field(name="Contributing", value=self.gs.info.contributing) - embed = embed.add_field(name="License", value=self.gs.info.license) await ctx.send(embed=embed) @commands.command(name="ping") @@ -43,7 +131,7 @@ class BaseCog(commands.Cog): content = ctx.message.author.mention if random.random() < 0.05 else '' embed = discord.Embed(title="**Pong!**", description=f'Current ping is {self.bot.latency*1000:.1f} ms', - colour=self.gs.colours.neutral, + colour=cmn.colours.neutral, timestamp=datetime.utcnow()) embed.set_footer(text=ctx.author.name, icon_url=str(ctx.author.avatar_url)) @@ -55,7 +143,7 @@ class BaseCog(commands.Cog): embed = discord.Embed(title="qrm Changelog", description=("For a full listing, visit [Github](https://" "github.com/classabbyamp/discord-qrm-bot/blob/master/CHANGELOG.md)."), - colour=self.gs.colours.neutral, + colour=cmn.colours.neutral, timestamp=datetime.utcnow()) embed.set_footer(text=ctx.author.name, icon_url=str(ctx.author.avatar_url)) @@ -111,3 +199,9 @@ async def format_changelog(log: dict): def setup(bot: commands.Bot): bot.add_cog(BaseCog(bot)) + bot._original_help_command = bot.help_command + bot.help_command = QrmHelpCommand() + + +def teardown(bot: commands.Bot): + bot.help_command = bot._original_help_command diff --git a/cogs/funcog.py b/cogs/funcog.py index 8045c54..5662b0f 100644 --- a/cogs/funcog.py +++ b/cogs/funcog.py @@ -9,23 +9,24 @@ General Public License, version 2. import discord.ext.commands as commands +import common as cmn + class FunCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.gs = bot.get_cog("GlobalSettings") - @commands.command(name="xkcd", aliases=['x']) - async def _xkcd(self, ctx: commands.Context, num: str): + @commands.command(name="xkcd", aliases=['x'], category=cmn.cat.fun) + async def _xkcd(self, ctx: commands.Context, number: str): '''Look up an xkcd by number.''' - await ctx.send('http://xkcd.com/' + num) + await ctx.send('http://xkcd.com/' + number) - @commands.command(name="tar") + @commands.command(name="tar", category=cmn.cat.fun) async def _tar(self, ctx: commands.Context): '''Returns an xkcd about tar.''' await ctx.send('http://xkcd.com/1168') - @commands.command(name="xd") + @commands.command(name="xd", hidden=True, category=cmn.cat.fun) async def _xd(self, ctx: commands.Context): '''ecks dee''' await ctx.send('ECKS DEE :smirk:') diff --git a/cogs/gridcog.py b/cogs/gridcog.py index f86b02a..5e5f9dd 100644 --- a/cogs/gridcog.py +++ b/cogs/gridcog.py @@ -13,17 +13,17 @@ from datetime import datetime import discord import discord.ext.commands as commands +import common as cmn + class GridCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.gs = bot.get_cog("GlobalSettings") - @commands.command(name="grid") + @commands.command(name="grid", category=cmn.cat.maps) async def _grid_sq_lookup(self, ctx: commands.Context, lat: str, lon: str): - '''Calculates the grid square for latitude and longitude coordinates. - Usage: `?grid ` - `lat` and `lon` are decimal coordinates, with negative being latitude South and longitude West.''' + '''Calculates the grid square for latitude and longitude coordinates, +with negative being latitude South and longitude West.''' with ctx.typing(): grid = "**" try: @@ -39,7 +39,7 @@ class GridCog(commands.Cog): grid += "**" embed = discord.Embed(title=f'Maidenhead Grid Locator for {float(lat):.6f}, {float(lon):.6f}', description=grid, - 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)) @@ -48,16 +48,16 @@ class GridCog(commands.Cog): except ValueError as err: msg = f'Error generating grid square for {lat}, {lon}.' embed = discord.Embed(title=msg, description=str(err), - 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)) await ctx.send(embed=embed) - @commands.command(name="ungrid", aliases=['loc']) + @commands.command(name="ungrid", aliases=['loc'], category=cmn.cat.maps) async def _location_lookup(self, ctx: commands.Context, grid: str, grid2: str = None): '''Calculates the latitude and longitude for the center of a grid square. - If two grid squares are given, the distance and azimuth between them is calculated.''' +If two grid squares are given, the distance and azimuth between them is calculated.''' with ctx.typing(): if grid2 is None or grid2 == '': try: @@ -67,14 +67,14 @@ class GridCog(commands.Cog): if len(grid) >= 6: embed = discord.Embed(title=f'Latitude and Longitude for {grid}', description=f'**{loc[0]:.5f}, {loc[1]:.5f}**', - colour=self.gs.colours.good, + colour=cmn.colours.good, url=f'https://www.openstreetmap.org/#map=13/{loc[0]:.5f}/{loc[1]:.5f}', timestamp=datetime.utcnow()) else: embed = discord.Embed(title=f'Latitude and Longitude for {grid}', description=f'**{loc[0]:.1f}, {loc[1]:.1f}**', - colour=self.gs.colours.good, + colour=cmn.colours.good, url=f'https://www.openstreetmap.org/#map=10/{loc[0]:.1f}/{loc[1]:.1f}', timestamp=datetime.utcnow()) embed.set_footer(text=ctx.author.name, @@ -82,7 +82,7 @@ class GridCog(commands.Cog): except Exception as e: msg = f'Error generating latitude and longitude for grid {grid}.' embed = discord.Embed(title=msg, description=str(e), - 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)) @@ -113,14 +113,14 @@ class GridCog(commands.Cog): des = f'**Distance:** {d:.1f} km ({d_mi:.1f} mi)\n**Bearing:** {bearing:.1f}°' embed = discord.Embed(title=f'Great Circle Distance and Bearing from {grid} to {grid2}', description=des, - 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)) except Exception as e: msg = f'Error generating great circle distance and bearing from {grid} and {grid2}.' embed = discord.Embed(title=msg, description=str(e), - 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)) diff --git a/cogs/hamcog.py b/cogs/hamcog.py index db1a96d..58ede8e 100644 --- a/cogs/hamcog.py +++ b/cogs/hamcog.py @@ -13,36 +13,36 @@ from datetime import datetime import discord import discord.ext.commands as commands +import common as cmn from resources import callsign_info class HamCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.gs = bot.get_cog("GlobalSettings") with open('resources/qcodes.json') as qcode_file: self.qcodes = json.load(qcode_file) with open('resources/words') as words_file: self.words = words_file.read().lower().splitlines() - @commands.command(name="qcode", aliases=['q']) + @commands.command(name="qcode", aliases=['q'], category=cmn.cat.ref) async def _qcode_lookup(self, ctx: commands.Context, qcode: str): '''Look up a Q Code.''' with ctx.typing(): qcode = qcode.upper() if qcode in self.qcodes: embed = discord.Embed(title=qcode, description=self.qcodes[qcode], - colour=self.gs.colours.good, + colour=cmn.colours.good, timestamp=datetime.utcnow()) else: embed = discord.Embed(title=f'Q Code {qcode} not found', - 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)) await ctx.send(embed=embed) - @commands.command(name="phonetics", aliases=['ph', 'phoneticize', 'phoneticise', 'phone']) + @commands.command(name="phonetics", aliases=['ph', 'phoneticize', 'phoneticise', 'phone'], category=cmn.cat.fun) async def _phonetics_lookup(self, ctx: commands.Context, *, msg: str): '''Get phonetics for a word or phrase.''' with ctx.typing(): @@ -55,13 +55,13 @@ class HamCog(commands.Cog): result += ' ' embed = discord.Embed(title=f'Phonetics for {msg}', description=result.title(), - 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)) await ctx.send(embed=embed) - @commands.command(name="utc", aliases=['z']) + @commands.command(name="utc", aliases=['z'], category=cmn.cat.ref) async def _utc_lookup(self, ctx: commands.Context): '''Gets the current time in UTC.''' with ctx.typing(): @@ -69,13 +69,13 @@ class HamCog(commands.Cog): result = '**' + now.strftime('%Y-%m-%d %H:%M') + 'Z**' embed = discord.Embed(title='The current time is:', description=result, - 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)) await ctx.send(embed=embed) - @commands.command(name="vanities", aliases=["vanity", "pfx", "prefixes", "prefix"]) + @commands.command(name="prefixes", aliases=["vanity", "pfx", "vanities", "prefix"]) async def _vanity_prefixes(self, ctx: commands.Context, country: str = None): '''Lists valid prefixes for countries.''' if country is None: diff --git a/cogs/imagecog.py b/cogs/imagecog.py index 922296f..51d4d7b 100644 --- a/cogs/imagecog.py +++ b/cogs/imagecog.py @@ -15,22 +15,22 @@ import discord.ext.commands as commands import aiohttp +import common as cmn + class ImageCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.gs = bot.get_cog("GlobalSettings") - @commands.command(name="plan", aliases=['bands']) - async def _bandplan(self, ctx: commands.Context, msg: str = ''): - '''Posts an image of Frequency Allocations. - Optional argument: `cn`, `ca`, `nl`, `us`, `mx`.''' + @commands.command(name="bandplan", aliases=['plan', 'bands'], category=cmn.cat.ref) + async def _bandplan(self, ctx: commands.Context, region: str = ''): + '''Posts an image of Frequency Allocations.''' name = {'cn': 'Chinese', 'ca': 'Canadian', 'nl': 'Dutch', 'us': 'US', 'mx': 'Mexican'} - arg = msg.lower() + arg = region.lower() with ctx.typing(): if arg not in name: @@ -39,7 +39,7 @@ class ImageCog(commands.Cog): desc += f'`{abbrev}`: {title}\n' embed = discord.Embed(title=f'Bandplan Not Found!', description=desc, - 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)) @@ -48,7 +48,7 @@ class ImageCog(commands.Cog): img = discord.File(f"resources/images/bandchart/{arg}bandchart.png", filename=f'{arg}bandchart.png') embed = discord.Embed(title=f'{name[arg]} Amateur Radio Bands', - colour=self.gs.colours.good, + colour=cmn.colours.good, timestamp=datetime.utcnow()) embed.set_image(url=f'attachment://{arg}bandchart.png') embed.set_footer(text=ctx.author.name, @@ -56,20 +56,20 @@ class ImageCog(commands.Cog): await ctx.send(embed=embed, file=img) - @commands.command(name="grayline", aliases=['greyline', 'grey', 'gray', 'gl']) + @commands.command(name="grayline", aliases=['greyline', 'grey', 'gray', 'gl'], category=cmn.cat.maps) async def _grayline(self, ctx: commands.Context): '''Posts a map of the current greyline, where HF propagation is the best.''' gl_url = ('http://www.fourmilab.ch/cgi-bin/uncgi/Earth?img=NOAAtopo.evif' '&imgsize=320&dynimg=y&opt=-p&lat=&lon=&alt=&tle=&date=0&utc=&jd=') with ctx.typing(): embed = discord.Embed(title='Current Greyline Conditions', - colour=self.gs.colours.good, + colour=cmn.colours.good, timestamp=datetime.utcnow()) async with aiohttp.ClientSession() as session: async with session.get(gl_url) as resp: if resp.status != 200: embed.description = 'Could not download file...' - embed.colour = self.gs.colours.bad + embed.colour = cmn.colours.bad else: data = io.BytesIO(await resp.read()) embed.set_image(url=f'attachment://greyline.jpg') @@ -77,11 +77,9 @@ class ImageCog(commands.Cog): icon_url=str(ctx.author.avatar_url)) await ctx.send(embed=embed, file=discord.File(data, 'greyline.jpg')) - @commands.command(name="map") - async def _map(self, ctx: commands.Context, msg: str = ''): - '''Posts an image of Frequency Allocations. - Optional argument:`cq` = CQ Zones, `itu` = ITU Zones, `arrl` or `rac` = - ARRL/RAC sections, `cn` = Chinese Callsign Areas, `us` = US Callsign Areas.''' + @commands.command(name="map", category=cmn.cat.maps) + async def _map(self, ctx: commands.Context, map_id: str = ''): + '''Posts an image of a ham-relevant map.''' map_titles = {"cq": 'Worldwide CQ Zones Map', "itu": 'Worldwide ITU Zones Map', "arrl": 'ARRL/RAC Section Map', @@ -89,7 +87,7 @@ class ImageCog(commands.Cog): "cn": 'Chinese Callsign Areas', "us": 'US Callsign Areas'} - arg = msg.lower() + arg = map_id.lower() with ctx.typing(): if arg not in map_titles: desc = 'Possible arguments are:\n' @@ -97,7 +95,7 @@ class ImageCog(commands.Cog): desc += f'`{abbrev}`: {title}\n' embed = discord.Embed(title=f'Map Not Found!', description=desc, - 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)) @@ -106,7 +104,7 @@ class ImageCog(commands.Cog): img = discord.File(f"resources/images/map/{arg}map.png", filename=f'{arg}map.png') embed = discord.Embed(title=f'{map_titles[arg]} Map', - colour=self.gs.colours.good, + colour=cmn.colours.good, timestamp=datetime.utcnow()) embed.set_image(url=f'attachment://{arg}map.png') embed.set_footer(text=ctx.author.name, diff --git a/cogs/morsecog.py b/cogs/morsecog.py index 9b8f9e7..0ba8ab0 100644 --- a/cogs/morsecog.py +++ b/cogs/morsecog.py @@ -13,16 +13,17 @@ from datetime import datetime import discord import discord.ext.commands as commands +import common as cmn + class MorseCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.gs = bot.get_cog("GlobalSettings") with open('resources/morse.json') as morse_file: self.ascii2morse = json.load(morse_file) self.morse2ascii = {v: k for k, v in self.ascii2morse.items()} - @commands.command(name="morse", aliases=['cw']) + @commands.command(name="morse", aliases=['cw'], category=cmn.cat.ref) async def _morse(self, ctx: commands.Context, *, msg: str): """Converts ASCII to international morse code.""" with ctx.typing(): @@ -35,13 +36,13 @@ class MorseCog(commands.Cog): result += ' ' embed = discord.Embed(title=f'Morse Code for {msg}', description=result, - 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)) await ctx.send(embed=embed) - @commands.command(name="unmorse", aliases=['demorse', 'uncw', 'decw']) + @commands.command(name="unmorse", aliases=['demorse', 'uncw', 'decw'], category=cmn.cat.ref) async def _unmorse(self, ctx: commands.Context, *, msg: str): '''Converts international morse code to ASCII.''' with ctx.typing(): @@ -58,15 +59,15 @@ class MorseCog(commands.Cog): result += ' ' embed = discord.Embed(title=f'ASCII for {msg0}', description=result, - 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)) await ctx.send(embed=embed) - @commands.command(name="weight", aliases=["cwweight", 'cww']) - async def _weight(self, ctx: commands.Context, msg: str): - '''Calculates the CW Weight of a callsign.''' + @commands.command(name="cwweight", aliases=["weight", 'cww'], category=cmn.cat.ref) + async def _weight(self, ctx: commands.Context, *, msg: str): + '''Calculates the CW Weight of a callsign or message.''' with ctx.typing(): msg = msg.upper() weight = 0 @@ -81,7 +82,7 @@ class MorseCog(commands.Cog): res = f'The CW weight is **{weight}**' embed = discord.Embed(title=f'CW Weight of {msg}', description=res, - 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/qrzcog.py b/cogs/qrzcog.py index d20f312..1a18a94 100644 --- a/cogs/qrzcog.py +++ b/cogs/qrzcog.py @@ -16,18 +16,21 @@ from discord.ext import commands, tasks import aiohttp from lxml import etree +import common as cmn +import keys + class QRZCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.gs = bot.get_cog("GlobalSettings") self.session = aiohttp.ClientSession() self._qrz_session_init.start() - @commands.command(name="qrz", aliases=["call"]) - async def _qrz_lookup(self, ctx: commands.Context, call: str): - if self.gs.keys.qrz_user == '' or self.gs.keys.qrz_pass == '': - await ctx.send(f'http://qrz.com/db/{call}') + @commands.command(name="call", aliases=["qrz"], category=cmn.cat.lookup) + async def _qrz_lookup(self, ctx: commands.Context, callsign: str): + '''Look up a callsign on [QRZ.com](https://www.qrz.com/).''' + if keys.qrz_user == '' or keys.qrz_pass == '': + await ctx.send(f'http://qrz.com/db/{callsign}') return try: @@ -35,7 +38,7 @@ class QRZCog(commands.Cog): except ConnectionError: await self.get_session() - url = f'http://xmldata.qrz.com/xml/current/?s={self.key};callsign={call}' + url = f'http://xmldata.qrz.com/xml/current/?s={self.key};callsign={callsign}' async with self.session.get(url) as resp: if resp.status != 200: raise ConnectionError(f'Unable to connect to QRZ (HTTP Error {resp.status})') @@ -47,11 +50,11 @@ class QRZCog(commands.Cog): if 'Error' in resp_session: if 'Session Timeout' in resp_session['Error']: await self.get_session() - await self._qrz_lookup(ctx, call) + await self._qrz_lookup(ctx, callsign) return if 'Not found' in resp_session['Error']: - embed = discord.Embed(title=f"QRZ Data for {call.upper()}", - colour=self.gs.colours.bad, + embed = discord.Embed(title=f"QRZ Data for {callsign.upper()}", + colour=cmn.colours.bad, description='No data found!', timestamp=datetime.utcnow()) embed.set_footer(text=ctx.author.name, @@ -65,7 +68,7 @@ class QRZCog(commands.Cog): resp_data = {el.tag.split('}')[1]: el.text for el in resp_xml_data[0].getiterator()} embed = discord.Embed(title=f"QRZ Data for {resp_data['call']}", - colour=self.gs.colours.good, + colour=cmn.colours.good, url=f'http://www.qrz.com/db/{resp_data["call"]}', timestamp=datetime.utcnow()) embed.set_footer(text=ctx.author.name, @@ -82,7 +85,7 @@ class QRZCog(commands.Cog): async def get_session(self): """Session creation and caching.""" - self.key = await qrz_login(self.gs.keys.qrz_user, self.gs.keys.qrz_pass, self.session) + self.key = await qrz_login(keys.qrz_user, keys.qrz_pass, self.session) with open('data/qrz_session', 'w') as qrz_file: qrz_file.write(self.key) diff --git a/cogs/studycog.py b/cogs/studycog.py index 4c80103..b72348c 100644 --- a/cogs/studycog.py +++ b/cogs/studycog.py @@ -16,14 +16,16 @@ import discord.ext.commands as commands import aiohttp +import common as cmn + class StudyCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.gs = bot.get_cog("GlobalSettings") self.lastq = dict() + self.source = 'Data courtesy of [HamStudy.org](https://hamstudy.org/)' - @commands.command(name="rq", aliases=['randomq']) + @commands.command(name="hamstudy", aliases=['rq', 'randomquestion', 'randomq'], category=cmn.cat.study) async def _random_question(self, ctx: commands.Context, level: str = None): '''Gets a random question from the Technician, General, and/or Extra question pools.''' tech_pool = 'E2_2018' @@ -66,7 +68,8 @@ class StudyCog(commands.Cog): question = random.choice(pool_questions) embed = discord.Embed(title=question['id'], - colour=self.gs.colours.good, + description=self.source, + colour=cmn.colours.good, timestamp=datetime.utcnow()) embed.set_footer(text=ctx.author.name, icon_url=str(ctx.author.avatar_url)) @@ -83,31 +86,32 @@ class StudyCog(commands.Cog): self.lastq[ctx.message.channel.id] = (question['id'], question['answer']) await ctx.send(embed=embed) - @commands.command(name="rqa") - async def _q_answer(self, ctx: commands.Context, ans: str = None): + @commands.command(name="hamstudyanswer", aliases=['rqa', 'randomquestionanswer', 'randomqa', 'hamstudya'], + category=cmn.cat.study) + async def _q_answer(self, ctx: commands.Context, answer: str = None): '''Returns the answer to question last asked (Optional argument: your answer).''' with ctx.typing(): correct_ans = self.lastq[ctx.message.channel.id][1] q_num = self.lastq[ctx.message.channel.id][0] - if ans is not None: - ans = ans.upper() - if ans == correct_ans: + if answer is not None: + answer = answer.upper() + if answer == correct_ans: result = f'Correct! The answer to {q_num} was **{correct_ans}**.' embed = discord.Embed(title=f'{q_num} Answer', - description=result, - colour=self.gs.colours.good, + description=f'{self.source}\n\n{result}', + colour=cmn.colours.good, timestamp=datetime.utcnow()) else: - result = f'Incorrect. The answer to {q_num} was **{correct_ans}**, not **{ans}**.' + result = f'Incorrect. The answer to {q_num} was **{correct_ans}**, not **{answer}**.' embed = discord.Embed(title=f'{q_num} Answer', - description=result, - colour=self.gs.colours.bad, + description=f'{self.source}\n\n{result}', + colour=cmn.colours.bad, timestamp=datetime.utcnow()) else: result = f'The correct answer to {q_num} was **{correct_ans}**.' embed = discord.Embed(title=f'{q_num} Answer', - description=result, - colour=self.gs.colours.neutral, + description=f'{self.source}\n\n{result}', + colour=cmn.colours.neutral, timestamp=datetime.utcnow()) embed.set_footer(text=ctx.author.name, diff --git a/cogs/weathercog.py b/cogs/weathercog.py index 9217bdf..5c1faab 100644 --- a/cogs/weathercog.py +++ b/cogs/weathercog.py @@ -16,26 +16,27 @@ import discord.ext.commands as commands import aiohttp +import common as cmn + class WeatherCog(commands.Cog): wttr_units_regex = re.compile(r"\B-([cCfF])\b") def __init__(self, bot: commands.Bot): self.bot = bot - self.gs = bot.get_cog("GlobalSettings") - @commands.command(name="cond", aliases=['condx']) + @commands.command(name="bandconditions", aliases=['cond', 'condx', 'conditions'], category=cmn.cat.weather) async def _band_conditions(self, ctx: commands.Context): '''Posts an image of HF Band Conditions.''' with ctx.typing(): embed = discord.Embed(title='Current Solar Conditions', - colour=self.gs.colours.good, + colour=cmn.colours.good, timestamp=datetime.utcnow()) async with aiohttp.ClientSession() as session: async with session.get('http://www.hamqsl.com/solarsun.php') as resp: if resp.status != 200: embed.description = 'Could not download file...' - embed.colour = self.gs.colours.bad + embed.colour = cmn.colours.bad else: data = io.BytesIO(await resp.read()) embed.set_image(url=f'attachment://condx.png') @@ -43,7 +44,7 @@ class WeatherCog(commands.Cog): icon_url=str(ctx.author.avatar_url)) await ctx.send(embed=embed, file=discord.File(data, 'condx.png')) - @commands.group(name="weather", aliases=['wttr']) + @commands.group(name="weather", aliases=['wttr'], category=cmn.cat.weather) async def _weather_conditions(self, ctx: commands.Context): '''Posts an image of Local Weather Conditions from [wttr.in](http://wttr.in/). @@ -59,13 +60,13 @@ class WeatherCog(commands.Cog): if ctx.invoked_subcommand is None: await ctx.send_help(ctx.command) - @_weather_conditions.command(name='forecast') - async def _weather_conditions_forecast(self, ctx: commands.Context, *, args: str): + @_weather_conditions.command(name='forecast', aliases=['fc', 'future'], category=cmn.cat.weather) + async def _weather_conditions_forecast(self, ctx: commands.Context, *, location: str): '''Posts an image of Local Weather Conditions for the next three days from [wttr.in](http://wttr.in/). See help for weather command for possible location types. Add a `-c` or `-f` to use Celcius or Fahrenheit.''' with ctx.typing(): try: - units_arg = re.search(self.wttr_units_regex, args).group(1) + units_arg = re.search(self.wttr_units_regex, location).group(1) except AttributeError: units_arg = '' if units_arg.lower() == 'f': @@ -75,18 +76,18 @@ See help for weather command for possible location types. Add a `-c` or `-f` to else: units = '' - loc = self.wttr_units_regex.sub('', args).strip() + loc = self.wttr_units_regex.sub('', location).strip() embed = discord.Embed(title=f'Weather Forecast for {loc}', description='Data from [wttr.in](http://wttr.in/).', - colour=self.gs.colours.good, + colour=cmn.colours.good, timestamp=datetime.utcnow()) loc = loc.replace(' ', '+') async with aiohttp.ClientSession() as session: async with session.get(f'http://wttr.in/{loc}_{units}pnFQ.png') as resp: if resp.status != 200: embed.description = 'Could not download file...' - embed.colour = self.gs.colours.bad + embed.colour = cmn.colours.bad else: data = io.BytesIO(await resp.read()) loc = loc.replace('+', '') @@ -97,13 +98,13 @@ See help for weather command for possible location types. Add a `-c` or `-f` to icon_url=str(ctx.author.avatar_url)) await ctx.send(embed=embed, file=discord.File(data, f'{loc}_{units}pnFQ.png')) - @_weather_conditions.command(name='now') - async def _weather_conditions_now(self, ctx: commands.Context, *, args: str): + @_weather_conditions.command(name='now', aliases=['n'], category=cmn.cat.weather) + async def _weather_conditions_now(self, ctx: commands.Context, *, location: str): '''Posts an image of current Local Weather Conditions from [wttr.in](http://wttr.in/). See help for weather command for possible location types. Add a `-c` or `-f` to use Celcius or Fahrenheit.''' with ctx.typing(): try: - units_arg = re.search(self.wttr_units_regex, args).group(1) + units_arg = re.search(self.wttr_units_regex, location).group(1) except AttributeError: units_arg = '' if units_arg.lower() == 'f': @@ -113,18 +114,18 @@ See help for weather command for possible location types. Add a `-c` or `-f` to else: units = '' - loc = self.wttr_units_regex.sub('', args).strip() + loc = self.wttr_units_regex.sub('', location).strip() embed = discord.Embed(title=f'Current Weather for {loc}', description='Data from [wttr.in](http://wttr.in/).', - colour=self.gs.colours.good, + colour=cmn.colours.good, timestamp=datetime.utcnow()) loc = loc.replace(' ', '+') async with aiohttp.ClientSession() as session: async with session.get(f'http://wttr.in/{loc}_0{units}pnFQ.png') as resp: if resp.status != 200: embed.description = 'Could not download file...' - embed.colour = self.gs.colours.bad + embed.colour = cmn.colours.bad else: data = io.BytesIO(await resp.read()) loc = loc.replace('+', '') diff --git a/common.py b/common.py new file mode 100644 index 0000000..9005238 --- /dev/null +++ b/common.py @@ -0,0 +1,27 @@ +""" +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 +General Public License, version 2. +--- + +`colours`: Colours used by embeds. + +`cat`: Category names for the HelpCommand. +""" + +from types import SimpleNamespace + + +colours = SimpleNamespace(good=0x43B581, + neutral=0x7289DA, + bad=0xF04747) +# meow +cat = SimpleNamespace(lookup='Information Lookup', + fun='Fun', + maps='Mapping', + ref='Reference', + study='Exam Study', + weather='Land and Space Weather') diff --git a/info.py b/info.py index 6ba9033..3ca279b 100644 --- a/info.py +++ b/info.py @@ -15,11 +15,13 @@ General Public License, version 2. `contrubuting`: Info on how to contribute to the bot. +`release`: Current bot version. + `release_timestamp`: When the bot was last released. """ -authors = ("@ClassAbbyAmplifier#2229", "@0x5c") +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" -release_timestamp = "not yet :P" +release = '1.0.0' diff --git a/main.py b/main.py index 56b89ab..9a9ef0b 100644 --- a/main.py +++ b/main.py @@ -8,8 +8,6 @@ This file is part of discord-qrmbot and is released under the terms of the GNU General Public License, version 2. """ -from types import SimpleNamespace - import discord from discord.ext import commands, tasks @@ -26,20 +24,6 @@ exit_code = 1 # The default exit code. ?shutdown and ?restart will change it ac debug_mode = opt.debug # Separate assignement in-case we define an override (ternary operator goes here) -class GlobalSettings(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - - self.opt = opt - self.keys = keys - self.info = info - - self.colours = SimpleNamespace(good=0x43B581, - neutral=0x7289DA, - bad=0xF04747) - self.debug = debug_mode - - # --- Bot setup --- bot = commands.Bot(command_prefix=opt.prefix, @@ -67,7 +51,7 @@ async def check_if_owner(ctx: commands.Context): # --- Commands --- -@bot.command(name="restart") +@bot.command(name="restart", hidden=True) @commands.check(check_if_owner) async def _restart_bot(ctx: commands.Context): """Restarts the bot.""" @@ -77,7 +61,7 @@ async def _restart_bot(ctx: commands.Context): await bot.logout() -@bot.command(name="shutdown") +@bot.command(name="shutdown", hidden=True) @commands.check(check_if_owner) async def _shutdown_bot(ctx: commands.Context): """Shuts down the bot.""" @@ -109,7 +93,7 @@ async def _before_ensure_activity(): # --- Run --- -bot.add_cog(GlobalSettings(bot)) +# bot.add_cog(GlobalSettings(bot)) for cog in opt.cogs: bot.load_extension(f"cogs.{cog}")