From 8f15feab94d862a319490925002267c63710a5fc Mon Sep 17 00:00:00 2001 From: Abigail Gold Date: Mon, 6 Jan 2020 23:27:48 -0500 Subject: [PATCH 01/18] update copyright for nuevo ans Fixes #150 --- README.md | 2 +- common.py | 2 +- exts/ae7q.py | 2 +- exts/base.py | 2 +- exts/fun.py | 2 +- exts/grid.py | 2 +- exts/ham.py | 2 +- exts/image.py | 2 +- exts/lookup.py | 2 +- exts/morse.py | 2 +- exts/qrz.py | 2 +- exts/study.py | 2 +- exts/weather.py | 2 +- info.py | 2 +- main.py | 2 +- resources/callsign_info.py | 2 +- resources/morse.py | 2 +- resources/phonetics.py | 2 +- resources/qcodes.py | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index e95facd..45736d1 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ $ run.sh ## Copyright -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This program is released under the terms of the GNU General Public License, version 2. See `COPYING` for full license text. diff --git a/common.py b/common.py index bb46f3f..c924498 100644 --- a/common.py +++ b/common.py @@ -1,7 +1,7 @@ """ Common tools for the bot. --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. diff --git a/exts/ae7q.py b/exts/ae7q.py index 6671c7c..cdae837 100644 --- a/exts/ae7q.py +++ b/exts/ae7q.py @@ -1,7 +1,7 @@ """ ae7q extension for qrm --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. diff --git a/exts/base.py b/exts/base.py index d99cbc7..0970cf8 100644 --- a/exts/base.py +++ b/exts/base.py @@ -1,7 +1,7 @@ """ Base extension for qrm --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. diff --git a/exts/fun.py b/exts/fun.py index 136643d..d84f513 100644 --- a/exts/fun.py +++ b/exts/fun.py @@ -1,7 +1,7 @@ """ Fun extension for qrm --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. diff --git a/exts/grid.py b/exts/grid.py index 1a9366f..e8774ec 100644 --- a/exts/grid.py +++ b/exts/grid.py @@ -1,7 +1,7 @@ """ Grid extension for qrm --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. diff --git a/exts/ham.py b/exts/ham.py index a040ceb..71766ba 100644 --- a/exts/ham.py +++ b/exts/ham.py @@ -1,7 +1,7 @@ """ Ham extension for qrm --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. diff --git a/exts/image.py b/exts/image.py index c4f92d9..bcc6cc7 100644 --- a/exts/image.py +++ b/exts/image.py @@ -1,7 +1,7 @@ """ Image extension for qrm --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. diff --git a/exts/lookup.py b/exts/lookup.py index b078bd9..ae22e7a 100644 --- a/exts/lookup.py +++ b/exts/lookup.py @@ -1,7 +1,7 @@ """ Lookup extension for qrm --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. diff --git a/exts/morse.py b/exts/morse.py index 434822b..0f61650 100644 --- a/exts/morse.py +++ b/exts/morse.py @@ -1,7 +1,7 @@ """ Morse Code extension for qrm --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. diff --git a/exts/qrz.py b/exts/qrz.py index 5cc94b8..5492d30 100644 --- a/exts/qrz.py +++ b/exts/qrz.py @@ -1,7 +1,7 @@ """ QRZ extension for qrm --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. diff --git a/exts/study.py b/exts/study.py index faf40a0..3470f80 100644 --- a/exts/study.py +++ b/exts/study.py @@ -1,7 +1,7 @@ """ Study extension for qrm --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. diff --git a/exts/weather.py b/exts/weather.py index 978d057..1063426 100644 --- a/exts/weather.py +++ b/exts/weather.py @@ -1,7 +1,7 @@ """ Weather extension for qrm --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. diff --git a/info.py b/info.py index c429a44..5a442b2 100644 --- a/info.py +++ b/info.py @@ -1,7 +1,7 @@ """ Static info about the bot. --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. diff --git a/main.py b/main.py index c3b4ae4..9bd398c 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ """ qrm, a bot for Discord --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. diff --git a/resources/callsign_info.py b/resources/callsign_info.py index c8f4945..ac671f4 100644 --- a/resources/callsign_info.py +++ b/resources/callsign_info.py @@ -1,7 +1,7 @@ """ Information about callsigns for the vanity prefixes command in hamcog. --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrmbot and is released under the terms of the GNU General Public License, version 2. diff --git a/resources/morse.py b/resources/morse.py index 97181d6..950306c 100644 --- a/resources/morse.py +++ b/resources/morse.py @@ -1,7 +1,7 @@ """ A listing of morse code symbols --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrmbot and is released under the terms of the GNU General Public License, version 2. diff --git a/resources/phonetics.py b/resources/phonetics.py index 7dca02b..45bf2c7 100644 --- a/resources/phonetics.py +++ b/resources/phonetics.py @@ -1,7 +1,7 @@ """ A listing of NATO Phonetics --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrmbot and is released under the terms of the GNU General Public License, version 2. diff --git a/resources/qcodes.py b/resources/qcodes.py index 562a6c1..394472c 100644 --- a/resources/qcodes.py +++ b/resources/qcodes.py @@ -1,7 +1,7 @@ """ A listing of Q Codes --- -Copyright (C) 2019 Abigail Gold, 0x5c +Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrmbot and is released under the terms of the GNU General Public License, version 2. From 30455153ba67ad5e16521e2d584dfcf69fd54d09 Mon Sep 17 00:00:00 2001 From: Abigail Gold Date: Tue, 7 Jan 2020 04:28:51 -0500 Subject: [PATCH 02/18] refactor ae7q call, add ae7q c alias Process on #95 --- CHANGELOG.md | 1 + exts/ae7q.py | 93 +++++++++++++++++++++++++++------------------------- 2 files changed, 50 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8648c6..794d353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added - Added Trustee field to qrz command for club callsigns. +- Added alias for `ae7q call` command (`ae7q c`). ### Changed - Changelog command to accept a version as argument. - The qrz command can now link to a QRZ page instead of embedding the data with the `--link` flag. diff --git a/exts/ae7q.py b/exts/ae7q.py index 6671c7c..de5cee6 100644 --- a/exts/ae7q.py +++ b/exts/ae7q.py @@ -10,9 +10,8 @@ Test callsigns: KN8U: active, restricted AB2EE: expired, restricted KE8FGB: assigned once, no restrictions -NA2AAA: unassigned, no records -KC4USA: reserved but has call history -WF4EMA: " +KV4AAA: unassigned, no records +KC4USA: reserved, no call history, *but* has application history """ import discord.ext.commands as commands @@ -33,7 +32,7 @@ class AE7QCog(commands.Cog): if ctx.invoked_subcommand is None: await ctx.send_help(ctx.command) - @_ae7q_lookup.command(name="call", category=cmn.cat.lookup) + @_ae7q_lookup.command(name="call", aliases=["c"], 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() @@ -51,59 +50,41 @@ class AE7QCog(commands.Cog): page = await resp.text() soup = BeautifulSoup(page, features="html.parser") - tables = soup.select("table.Database") + tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] - for table in tables: - rows = table.find_all("tr") - if len(rows) > 1 and len(rows[0]) > 1: - break - if desc == '': - for row in rows: - desc += " ".join(row.getText().split()) - desc += '\n' - desc = desc.replace(callsign, f'`{callsign}`') - rows = None + table = tables[0] - first_header = ''.join(rows[0].find_all("th")[0].strings) + # find the first table in the page, and use it to make a description + if len(table[0]) == 1: + for row in table: + desc += " ".join(row.getText().split()) + desc += '\n' + desc = desc.replace(callsign, f'`{callsign}`') + table = tables[1] - if rows is None or first_header != 'Entity Name': + table_headers = table[0].find_all("th") + first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None + + # catch if the wrong table was selected + if first_header != 'Entity Name': embed.title = f"AE7Q History for {callsign}" embed.colour = cmn.colours.bad - embed.url = f"{base_url}{callsign}" + embed.url = base_url + callsign embed.description = desc embed.description += f'\nNo records found for `{callsign}`' await ctx.send(embed=embed) return - table_contents = [] # store your table here - for tr in rows: - if rows.index(tr) == 0: - # first_header = ''.join(tr.find_all("th")[0].strings) - # if first_header == 'Entity Name': - # print('yooooo') - continue - row_cells = [] - for td in tr.find_all('td'): - if td.getText().strip() != '': - row_cells.append(td.getText().strip()) - else: - row_cells.append('-') - if 'colspan' in td.attrs and int(td.attrs['colspan']) > 1: - for i in range(int(td.attrs['colspan']) - 1): - row_cells.append(row_cells[-1]) - for i, cell in enumerate(row_cells): - if cell == '"': - row_cells[i] = table_contents[-1][i] - if len(row_cells) > 1: - table_contents += [row_cells] + table = await process_table(table) embed = cmn.embed_factory(ctx) embed.title = f"AE7Q Records for {callsign}" embed.colour = cmn.colours.good - embed.url = f"{base_url}{callsign}" + embed.url = base_url + callsign - for row in table_contents[0:3]: - header = f'**{row[0]}** ({row[1]})' + # add the first three rows of the table to the embed + for row in table[0:3]: + header = f'**{row[0]}** ({row[1]})' # **Name** (Applicant Type) body = (f'Class: *{row[2]}*\n' f'Region: *{row[3]}*\n' f'Status: *{row[4]}*\n' @@ -113,9 +94,10 @@ class AE7QCog(commands.Cog): f'Expires: *{row[8]}*') embed.add_field(name=header, value=body, inline=False) + if len(table) > 3: + desc += f'\nRecords 1 to 3 of {len(table)}. See ae7q.com for more...' + embed.description = desc - if len(table_contents) > 3: - embed.description += f'\nRecords 1 to 3 of {len(table_contents)}. See ae7q.com for more...' await ctx.send(embed=embed) @@ -139,5 +121,28 @@ class AE7QCog(commands.Cog): # pass +async def process_table(table: list): + """Processes tables (including headers) and returns the processed table""" + table_contents = [] + for tr in table[1:]: + row = [] + for td in tr.find_all('td'): + cell_val = td.getText().strip() + row.append(cell_val if cell_val else '-') + + # take care of columns that span multiple rows by copying the contents rightward + if 'colspan' in td.attrs and int(td.attrs['colspan']) > 1: + for i in range(int(td.attrs['colspan']) - 1): + row.append(row[-1]) + + # get rid of ditto marks by copying the contents from the previous row + for i, cell in enumerate(row): + if cell == "\"": + row[i] = table_contents[-1][i] + # add row to table + table_contents += [row] + return table_contents + + def setup(bot: commands.Bot): bot.add_cog(AE7QCog(bot)) From 8bfaaf4af6e76158f3278d3bf8621149584b4433 Mon Sep 17 00:00:00 2001 From: 0x5c Date: Tue, 7 Jan 2020 05:36:09 -0500 Subject: [PATCH 03/18] [FIX] aiohttp DeprecationWarning - Passing a connector to Bot() - Using that connector for sessions in extensions Fixes #141 --- exts/ae7q.py | 3 ++- exts/image.py | 4 +++- exts/qrz.py | 2 +- exts/study.py | 4 +++- exts/weather.py | 4 +++- main.py | 19 ++++++++++++++----- utils/__init__.py | 3 +++ utils/connector.py | 16 ++++++++++++++++ 8 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 utils/__init__.py create mode 100644 utils/connector.py diff --git a/exts/ae7q.py b/exts/ae7q.py index 6671c7c..414349a 100644 --- a/exts/ae7q.py +++ b/exts/ae7q.py @@ -17,6 +17,7 @@ WF4EMA: " import discord.ext.commands as commands +import aiohttp from bs4 import BeautifulSoup import common as cmn @@ -25,7 +26,7 @@ import common as cmn class AE7QCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.session = bot.qrm.session + self.session = aiohttp.ClientSession(connector=bot.qrm.connector) @commands.group(name="ae7q", aliases=["ae"], category=cmn.cat.lookup) async def _ae7q_lookup(self, ctx: commands.Context): diff --git a/exts/image.py b/exts/image.py index c4f92d9..2c871d8 100644 --- a/exts/image.py +++ b/exts/image.py @@ -9,6 +9,8 @@ General Public License, version 2. import io +import aiohttp + import discord import discord.ext.commands as commands @@ -20,7 +22,7 @@ class ImageCog(commands.Cog): self.bot = bot self.bandcharts = cmn.ImagesGroup(cmn.paths.bandcharts / "meta.json") self.maps = cmn.ImagesGroup(cmn.paths.maps / "meta.json") - self.session = bot.qrm.session + self.session = aiohttp.ClientSession(connector=bot.qrm.connector) @commands.command(name="bandplan", aliases=['plan', 'bands'], category=cmn.cat.ref) async def _bandplan(self, ctx: commands.Context, region: str = ''): diff --git a/exts/qrz.py b/exts/qrz.py index 5cc94b8..67ffb22 100644 --- a/exts/qrz.py +++ b/exts/qrz.py @@ -21,7 +21,7 @@ import data.keys as keys class QRZCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.session = bot.qrm.session + self.session = aiohttp.ClientSession(connector=bot.qrm.connector) self._qrz_session_init.start() @commands.command(name="call", aliases=["qrz"], category=cmn.cat.lookup) diff --git a/exts/study.py b/exts/study.py index faf40a0..f94aecf 100644 --- a/exts/study.py +++ b/exts/study.py @@ -10,6 +10,8 @@ General Public License, version 2. import random import json +import aiohttp + import discord.ext.commands as commands import common as cmn @@ -20,7 +22,7 @@ class StudyCog(commands.Cog): self.bot = bot self.lastq = dict() self.source = 'Data courtesy of [HamStudy.org](https://hamstudy.org/)' - self.session = bot.qrm.session + self.session = aiohttp.ClientSession(connector=bot.qrm.connector) @commands.command(name="hamstudy", aliases=['rq', 'randomquestion', 'randomq'], category=cmn.cat.study) async def _random_question(self, ctx: commands.Context, level: str = None): diff --git a/exts/weather.py b/exts/weather.py index 978d057..41ef18b 100644 --- a/exts/weather.py +++ b/exts/weather.py @@ -10,6 +10,8 @@ General Public License, version 2. import io import re +import aiohttp + import discord import discord.ext.commands as commands @@ -21,7 +23,7 @@ class WeatherCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.session = bot.qrm.session + self.session = aiohttp.ClientSession(connector=bot.qrm.connector) @commands.command(name="bandconditions", aliases=['cond', 'condx', 'conditions'], category=cmn.cat.weather) async def _band_conditions(self, ctx: commands.Context): diff --git a/main.py b/main.py index c3b4ae4..573b4f3 100644 --- a/main.py +++ b/main.py @@ -11,17 +11,19 @@ General Public License, version 2. import sys import traceback +import asyncio from datetime import time, datetime import random from types import SimpleNamespace import pytz -import aiohttp import discord from discord.ext import commands, tasks +import utils.connector as conn import common as cmn + import info import data.options as opt import data.keys as keys @@ -38,13 +40,21 @@ debug_mode = opt.debug # Separate assignement in-case we define an override (te # --- Bot setup --- +# Loop/aiohttp stuff +loop = asyncio.get_event_loop() +connector = loop.run_until_complete(conn.new_connector()) + bot = commands.Bot(command_prefix=opt.prefix, description=info.description, - help_command=commands.MinimalHelpCommand()) + help_command=commands.MinimalHelpCommand(), + loop=loop, + connector=connector) +# Simple way to access bot-wide stuff in extensions. bot.qrm = SimpleNamespace() -bot.qrm.session = aiohttp.ClientSession(headers={'User-Agent': f'discord-qrm2/{info.release}'}) +# Let's store stuff here. +bot.qrm.connector = connector bot.qrm.debug_mode = debug_mode @@ -54,7 +64,6 @@ bot.qrm.debug_mode = debug_mode @commands.check(cmn.check_if_owner) async def _restart_bot(ctx: commands.Context): """Restarts the bot.""" - await bot.qrm.session.close() global exit_code await cmn.add_react(ctx.message, cmn.emojis.check_mark) print(f"[**] Restarting! Requested by {ctx.author}.") @@ -66,7 +75,6 @@ async def _restart_bot(ctx: commands.Context): @commands.check(cmn.check_if_owner) async def _shutdown_bot(ctx: commands.Context): """Shuts down the bot.""" - await bot.qrm.session.close() global exit_code await cmn.add_react(ctx.message, cmn.emojis.check_mark) print(f"[**] Shutting down! Requested by {ctx.author}.") @@ -246,6 +254,7 @@ except ConnectionResetError as ex: raise raise SystemExit("ConnectionResetError: {}".format(ex)) + # --- Exit --- # Codes for the wrapper shell script: # 0 - Clean exit, don't restart diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..c2387da --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,3 @@ +""" +Various utilities for the bot. +""" diff --git a/utils/connector.py b/utils/connector.py new file mode 100644 index 0000000..7fce782 --- /dev/null +++ b/utils/connector.py @@ -0,0 +1,16 @@ +""" +Wrapper to handle aiohttp connector creation. +--- +Copyright (C) 2020 Abigail Gold, 0x5c + +This file is part of discord-qrm2 and is released under the terms of the GNU +General Public License, version 2. +""" + + +import aiohttp + + +async def new_connector(*args, **kwargs) -> aiohttp.TCPConnector: + """*Yes, it's just a coro to instantiate a class.*""" + return aiohttp.TCPConnector(*args, **kwargs) From eaa47fc7244e67c174b922ff45626ac2b638ff53 Mon Sep 17 00:00:00 2001 From: Abigail Gold Date: Tue, 7 Jan 2020 15:49:48 -0500 Subject: [PATCH 04/18] add ae7q trustee command, add short aliases for ae7q subcommands, bugfixes in ae7q call Progress on #95 Fixes #153 --- exts/ae7q.py | 117 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 95 insertions(+), 22 deletions(-) diff --git a/exts/ae7q.py b/exts/ae7q.py index de5cee6..505cad8 100644 --- a/exts/ae7q.py +++ b/exts/ae7q.py @@ -34,7 +34,7 @@ class AE7QCog(commands.Cog): @_ae7q_lookup.command(name="call", aliases=["c"], 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/).''' + '''Look up the history of a callsign on [ae7q.com](http://ae7q.com/).''' callsign = callsign.upper() desc = '' base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" @@ -66,7 +66,7 @@ class AE7QCog(commands.Cog): first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None # catch if the wrong table was selected - if first_header != 'Entity Name': + if first_header is None or first_header != 'Entity Name': embed.title = f"AE7Q History for {callsign}" embed.colour = cmn.colours.bad embed.url = base_url + callsign @@ -75,16 +75,16 @@ class AE7QCog(commands.Cog): await ctx.send(embed=embed) return - table = await process_table(table) + table = await process_table(table[1:]) embed = cmn.embed_factory(ctx) - embed.title = f"AE7Q Records for {callsign}" + embed.title = f"AE7Q History for {callsign}" embed.colour = cmn.colours.good embed.url = base_url + callsign # add the first three rows of the table to the embed for row in table[0:3]: - header = f'**{row[0]}** ({row[1]})' # **Name** (Applicant Type) + header = f'**{row[0]}** ({row[1]})' # **Name** (Applicant Type) body = (f'Class: *{row[2]}*\n' f'Region: *{row[3]}*\n' f'Status: *{row[4]}*\n' @@ -101,30 +101,103 @@ class AE7QCog(commands.Cog): await ctx.send(embed=embed) - # TODO: write commands for other AE7Q response types? - # @_ae7q_lookup.command(name="trustee") - # async def _ae7q_trustee(self, ctx: commands.Context, callsign: str): - # pass + @_ae7q_lookup.command(name="trustee", aliases=["t"], category=cmn.cat.lookup) + async def _ae7q_trustee(self, ctx: commands.Context, callsign: str): + '''Look up the licenses for which a licensee is trustee on [ae7q.com](http://ae7q.com/).''' + callsign = callsign.upper() + desc = '' + base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" + embed = cmn.embed_factory(ctx) - # @_ae7q_lookup.command(name="applications", aliases=['apps']) - # async def _ae7q_applications(self, ctx: commands.Context, callsign: str): - # pass + async with self.session.get(base_url + callsign) as resp: + if resp.status != 200: + embed.title = "Error in AE7Q trustee command" + embed.description = 'Could not load AE7Q' + embed.colour = cmn.colours.bad + await ctx.send(embed=embed) + return + page = await resp.text() - # @_ae7q_lookup.command(name="frn") - # async def _ae7q_frn(self, ctx: commands.Context, frn: str): - # base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN=" - # pass + soup = BeautifulSoup(page, features="html.parser") + tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] - # @_ae7q_lookup.command(name="licensee", aliases=["lic"]) - # async def _ae7q_licensee(self, ctx: commands.Context, frn: str): - # base_url = "http://ae7q.com/query/data/LicenseeIdHistory.php?ID=" - # pass + try: + table = tables[2] if len(tables[0][0]) == 1 else tables[1] + except IndexError: + embed.title = f"AE7Q Trustee History for {callsign}" + embed.colour = cmn.colours.bad + embed.url = base_url + callsign + embed.description = desc + embed.description += f'\nNo records found for `{callsign}`' + await ctx.send(embed=embed) + return + + table_headers = table[0].find_all("th") + first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None + + # catch if the wrong table was selected + if first_header is None or not first_header.startswith("With"): + embed.title = f"AE7Q Trustee History for {callsign}" + embed.colour = cmn.colours.bad + embed.url = base_url + callsign + embed.description = desc + embed.description += f'\nNo records found for `{callsign}`' + await ctx.send(embed=embed) + return + + table = await process_table(table[2:]) + + embed = cmn.embed_factory(ctx) + embed.title = f"AE7Q Trustee History for {callsign}" + embed.colour = cmn.colours.good + embed.url = base_url + callsign + + # add the first three rows of the table to the embed + for row in table[0:3]: + header = f'**{row[0]}** ({row[3]})' # **Name** (Applicant Type) + body = (f'Name: *{row[2]}*\n' + f'Region: *{row[1]}*\n' + f'Status: *{row[4]}*\n' + f'Granted: *{row[5]}*\n' + f'Effective: *{row[6]}*\n' + f'Cancelled: *{row[7]}*\n' + f'Expires: *{row[8]}*') + embed.add_field(name=header, value=body, inline=False) + + if len(table) > 3: + desc += f'\nRecords 1 to 3 of {len(table)}. See ae7q.com for more...' + + embed.description = desc + + await ctx.send(embed=embed) + + @_ae7q_lookup.command(name="applications", aliases=["a"], category=cmn.cat.lookup) + async def _ae7q_applications(self, ctx: commands.Context, callsign: str): + '''Look up the applications for a callsign on [ae7q.com](http://ae7q.com/).''' + + @_ae7q_lookup.command(name="frn", aliases=["f"], category=cmn.cat.lookup) + async def _ae7q_frn(self, ctx: commands.Context, frn: str): + '''Look up the history of an FRN on [ae7q.com](http://ae7q.com/).''' + base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN=" + """ + NOTES: + - 2 tables: callsign history and application history + - If not found: no tables + """ + pass + + @_ae7q_lookup.command(name="licensee", aliases=["l"], category=cmn.cat.lookup) + async def _ae7q_licensee(self, ctx: commands.Context, frn: str): + '''Look up the history of a licensee ID on [ae7q.com](http://ae7q.com/).''' + base_url = "http://ae7q.com/query/data/LicenseeIdHistory.php?ID=" + # notes: same as FRN but with different input + pass async def process_table(table: list): - """Processes tables (including headers) and returns the processed table""" + """Processes tables (*not* including headers) and returns the processed table""" table_contents = [] - for tr in table[1:]: + for tr in table: row = [] for td in tr.find_all('td'): cell_val = td.getText().strip() From 0608a74e6cfc0c55098ef4d6a06c2ace3c667ee0 Mon Sep 17 00:00:00 2001 From: 0x5c Date: Wed, 8 Jan 2020 02:19:35 -0500 Subject: [PATCH 05/18] echo: New converters for arguemnts - Created a GlobalChannelConverter - Accepts Union[GlobalChannelConverter, UserConverter] Fixes #145 --- common.py | 34 +++++++++++++++++++++++++++------- exts/base.py | 15 +++++++++++---- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/common.py b/common.py index c924498..20a7d77 100644 --- a/common.py +++ b/common.py @@ -5,19 +5,15 @@ Copyright (C) 2019-2020 Abigail Gold, 0x5c This file is part of discord-qrm2 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. """ import collections import json +import re import traceback -from pathlib import Path from datetime import datetime +from pathlib import Path from types import SimpleNamespace import discord @@ -26,7 +22,8 @@ import discord.ext.commands as commands import data.options as opt -__all__ = ["colours", "cat", "emojis", "embed_factory", "error_embed_factory", "add_react", "check_if_owner"] +__all__ = ["colours", "cat", "emojis", "paths", "ImageMetadata", "ImagesGroup", + "embed_factory", "error_embed_factory", "add_react", "check_if_owner"] # --- Common values --- @@ -94,6 +91,29 @@ class ImagesGroup(collections.abc.Mapping): return str(self._images) +# --- Converters --- + +class GlobalChannelConverter(commands.IDConverter): + """Converter to get any bot-acessible channel by ID/mention (global), or name (in current guild only).""" + async def convert(self, ctx: commands.Context, argument: str): + bot = ctx.bot + guild = ctx.guild + match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument) + result = None + if match is None: + # not a mention/ID + if guild: + result = discord.utils.get(guild.text_channels, name=argument) + else: + raise commands.BadArgument(f"""Channel named "{argument}" not found in this guild.""") + else: + channel_id = int(match.group(1)) + result = bot.get_channel(channel_id) + if not isinstance(result, (discord.TextChannel, discord.abc.PrivateChannel)): + raise commands.BadArgument(f"""Channel "{argument}" not found.""") + return result + + # --- Helper functions --- def embed_factory(ctx: commands.Context) -> discord.Embed: diff --git a/exts/base.py b/exts/base.py index 0970cf8..eee76a2 100644 --- a/exts/base.py +++ b/exts/base.py @@ -7,17 +7,19 @@ This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. """ + +import random import re from collections import OrderedDict -import random +from typing import Union import discord import discord.ext.commands as commands import info +import common as cmn import data.options as opt -import common as cmn class QrmHelpCommand(commands.HelpCommand): @@ -171,8 +173,13 @@ class BaseCog(commands.Cog): @commands.command(name="echo", aliases=["e"], hidden=True) @commands.check(cmn.check_if_owner) - async def _echo(self, ctx: commands.Context, channel: commands.TextChannelConverter, *, msg: str): - """Send a message in a channel as qrm. Only works within a server or DM to server, not between servers.""" + async def _echo(self, ctx: commands.Context, + channel: Union[cmn.GlobalChannelConverter, commands.UserConverter], *, msg: str): + """Send a message in a channel as qrm. Accepts channel/user IDs/mentions. + Channel names are current-guild only. + Does not work with the ID of the bot user.""" + if isinstance(channel, discord.ClientUser): + raise commands.BadArgument("Can't send to the bot user!") await channel.send(msg) From 8db13755bc20523328fff8803693c102cac98301 Mon Sep 17 00:00:00 2001 From: 0x5c Date: Wed, 8 Jan 2020 03:47:00 -0500 Subject: [PATCH 06/18] Added aliases for extctl and subcommands Fixes #164 --- main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 3c90cf7..dbce811 100644 --- a/main.py +++ b/main.py @@ -82,7 +82,7 @@ async def _shutdown_bot(ctx: commands.Context): await bot.logout() -@bot.group(name="extctl", hidden=True) +@bot.group(name="extctl", aliases=["ex"], hidden=True) @commands.check(cmn.check_if_owner) async def _extctl(ctx: commands.Context): """Extension control commands. @@ -92,7 +92,7 @@ async def _extctl(ctx: commands.Context): await ctx.invoke(cmd) -@_extctl.command(name="list") +@_extctl.command(name="list", aliases=["ls"]) async def _extctl_list(ctx: commands.Context): """Lists Extensions.""" embed = cmn.embed_factory(ctx) @@ -101,7 +101,7 @@ async def _extctl_list(ctx: commands.Context): await ctx.send(embed=embed) -@_extctl.command(name="load") +@_extctl.command(name="load", aliases=["ld"]) async def _extctl_load(ctx: commands.Context, extension: str): try: bot.load_extension(ext_dir + "." + extension) @@ -111,7 +111,7 @@ async def _extctl_load(ctx: commands.Context, extension: str): await ctx.send(embed=embed) -@_extctl.command(name="reload", aliases=["relaod"]) +@_extctl.command(name="reload", aliases=["rl", "r", "relaod"]) async def _extctl_reload(ctx: commands.Context, extension: str): if ctx.invoked_with == "relaod": pika = bot.get_emoji(opt.pika) @@ -125,7 +125,7 @@ async def _extctl_reload(ctx: commands.Context, extension: str): await ctx.send(embed=embed) -@_extctl.command(name="unload") +@_extctl.command(name="unload", aliases=["ul"]) async def _extctl_unload(ctx: commands.Context, extension: str): try: bot.unload_extension(ext_dir + "." + extension) From eb5e038624053c0c7d0331e5b3bc78d4a8d1502a Mon Sep 17 00:00:00 2001 From: Abigail Gold Date: Wed, 8 Jan 2020 16:20:52 -0500 Subject: [PATCH 07/18] add ae7q frn command ae7q applications is WIP, and is commented out. There is also an applications table on the FRN page, which could be used in the future Progress on #95 --- exts/ae7q.py | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 2 deletions(-) diff --git a/exts/ae7q.py b/exts/ae7q.py index 505cad8..e255618 100644 --- a/exts/ae7q.py +++ b/exts/ae7q.py @@ -173,7 +173,75 @@ class AE7QCog(commands.Cog): @_ae7q_lookup.command(name="applications", aliases=["a"], category=cmn.cat.lookup) async def _ae7q_applications(self, ctx: commands.Context, callsign: str): - '''Look up the applications for a callsign on [ae7q.com](http://ae7q.com/).''' + '''Look up the application history for a callsign on [ae7q.com](http://ae7q.com/).''' + """ + callsign = callsign.upper() + desc = '' + base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" + embed = cmn.embed_factory(ctx) + + async with self.session.get(base_url + callsign) as resp: + if resp.status != 200: + embed.title = "Error in AE7Q applications command" + embed.description = 'Could not load AE7Q' + embed.colour = cmn.colours.bad + await ctx.send(embed=embed) + return + page = await resp.text() + + soup = BeautifulSoup(page, features="html.parser") + tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] + + table = tables[0] + + # find the first table in the page, and use it to make a description + if len(table[0]) == 1: + for row in table: + desc += " ".join(row.getText().split()) + desc += '\n' + desc = desc.replace(callsign, f'`{callsign}`') + + # select the last table to get applications + table = tables[-1] + + table_headers = table[0].find_all("th") + first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None + + # catch if the wrong table was selected + if first_header is None or not first_header.startswith("Receipt"): + embed.title = f"AE7Q Application History for {callsign}" + embed.colour = cmn.colours.bad + embed.url = base_url + callsign + embed.description = desc + embed.description += f'\nNo records found for `{callsign}`' + await ctx.send(embed=embed) + return + + table = await process_table(table[1:]) + + embed = cmn.embed_factory(ctx) + embed.title = f"AE7Q Application History for {callsign}" + embed.colour = cmn.colours.good + embed.url = base_url + callsign + + # add the first three rows of the table to the embed + for row in table[0:3]: + header = f'**{row[1]}** ({row[3]})' # **Name** (Callsign) + body = (f'Received: *{row[0]}*\n' + f'Region: *{row[2]}*\n' + f'Purpose: *{row[5]}*\n' + f'Last Action: *{row[7]}*\n' + f'Application Status: *{row[8]}*\n') + embed.add_field(name=header, value=body, inline=False) + + if len(table) > 3: + desc += f'\nRecords 1 to 3 of {len(table)}. See ae7q.com for more...' + + embed.description = desc + + await ctx.send(embed=embed) + """ + pass @_ae7q_lookup.command(name="frn", aliases=["f"], category=cmn.cat.lookup) async def _ae7q_frn(self, ctx: commands.Context, frn: str): @@ -184,7 +252,60 @@ class AE7QCog(commands.Cog): - 2 tables: callsign history and application history - If not found: no tables """ - pass + desc = '' + base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN=" + embed = cmn.embed_factory(ctx) + + async with self.session.get(base_url + frn) as resp: + if resp.status != 200: + embed.title = "Error in AE7Q frn command" + embed.description = 'Could not load AE7Q' + embed.colour = cmn.colours.bad + await ctx.send(embed=embed) + return + page = await resp.text() + + soup = BeautifulSoup(page, features="html.parser") + tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] + + table = tables[0] + + + table_headers = table[0].find_all("th") + first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None + + # catch if the wrong table was selected + if first_header is None or not first_header.startswith('With Licensee'): + embed.title = f"AE7Q History for FRN {frn}" + embed.colour = cmn.colours.bad + embed.url = base_url + frn + embed.description = f'No records found for FRN `{frn}`' + await ctx.send(embed=embed) + return + + table = await process_table(table[2:]) + + embed = cmn.embed_factory(ctx) + embed.title = f"AE7Q History for FRN {frn}" + embed.colour = cmn.colours.good + embed.url = base_url + frn + + # add the first three rows of the table to the embed + for row in table[0:3]: + header = f'**{row[0]}** ({row[3]})' # **Callsign** (Applicant Type) + body = (f'Class: *{row[4]}*\n' + f'Region: *{row[1]}*\n' + f'Status: *{row[5]}*\n' + f'Granted: *{row[6]}*\n' + f'Effective: *{row[7]}*\n' + f'Cancelled: *{row[8]}*\n' + f'Expires: *{row[9]}*') + embed.add_field(name=header, value=body, inline=False) + + if len(table) > 3: + embed.description = f'Records 1 to 3 of {len(table)}. See ae7q.com for more...' + + await ctx.send(embed=embed) @_ae7q_lookup.command(name="licensee", aliases=["l"], category=cmn.cat.lookup) async def _ae7q_licensee(self, ctx: commands.Context, frn: str): From 04cbc920ce7802207164772b2ab43857a42b8b6e Mon Sep 17 00:00:00 2001 From: Abigail Gold Date: Wed, 8 Jan 2020 17:07:02 -0500 Subject: [PATCH 08/18] add ae7q licensee command Progress on #95 --- exts/ae7q.py | 79 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/exts/ae7q.py b/exts/ae7q.py index e4c2283..490e2b4 100644 --- a/exts/ae7q.py +++ b/exts/ae7q.py @@ -247,13 +247,11 @@ class AE7QCog(commands.Cog): @_ae7q_lookup.command(name="frn", aliases=["f"], category=cmn.cat.lookup) async def _ae7q_frn(self, ctx: commands.Context, frn: str): '''Look up the history of an FRN on [ae7q.com](http://ae7q.com/).''' - base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN=" """ NOTES: - 2 tables: callsign history and application history - If not found: no tables """ - desc = '' base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN=" embed = cmn.embed_factory(ctx) @@ -269,8 +267,15 @@ class AE7QCog(commands.Cog): soup = BeautifulSoup(page, features="html.parser") tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] - table = tables[0] + if not len(tables): + embed.title = f"AE7Q History for FRN {frn}" + embed.colour = cmn.colours.bad + embed.url = base_url + frn + embed.description = f'No records found for FRN `{frn}`' + await ctx.send(embed=embed) + return + table = tables[0] table_headers = table[0].find_all("th") first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None @@ -294,7 +299,8 @@ class AE7QCog(commands.Cog): # add the first three rows of the table to the embed for row in table[0:3]: header = f'**{row[0]}** ({row[3]})' # **Callsign** (Applicant Type) - body = (f'Class: *{row[4]}*\n' + body = (f'Name: *{row[2]}*\n' + f'Class: *{row[4]}*\n' f'Region: *{row[1]}*\n' f'Status: *{row[5]}*\n' f'Granted: *{row[6]}*\n' @@ -309,11 +315,70 @@ class AE7QCog(commands.Cog): await ctx.send(embed=embed) @_ae7q_lookup.command(name="licensee", aliases=["l"], category=cmn.cat.lookup) - async def _ae7q_licensee(self, ctx: commands.Context, frn: str): + async def _ae7q_licensee(self, ctx: commands.Context, licensee_id: str): '''Look up the history of a licensee ID on [ae7q.com](http://ae7q.com/).''' + licensee_id = licensee_id.upper() base_url = "http://ae7q.com/query/data/LicenseeIdHistory.php?ID=" - # notes: same as FRN but with different input - pass + embed = cmn.embed_factory(ctx) + + async with self.session.get(base_url + licensee_id) as resp: + if resp.status != 200: + embed.title = "Error in AE7Q licensee command" + embed.description = 'Could not load AE7Q' + embed.colour = cmn.colours.bad + await ctx.send(embed=embed) + return + page = await resp.text() + + soup = BeautifulSoup(page, features="html.parser") + tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] + + if not len(tables): + embed.title = f"AE7Q History for Licensee {licensee_id}" + embed.colour = cmn.colours.bad + embed.url = base_url + licensee_id + embed.description = f'No records found for Licensee `{licensee_id}`' + await ctx.send(embed=embed) + return + + table = tables[0] + + table_headers = table[0].find_all("th") + first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None + + # catch if the wrong table was selected + if first_header is None or not first_header.startswith('With FCC'): + embed.title = f"AE7Q History for Licensee {licensee_id}" + embed.colour = cmn.colours.bad + embed.url = base_url + licensee_id + embed.description = f'No records found for Licensee `{licensee_id}`' + await ctx.send(embed=embed) + return + + table = await process_table(table[2:]) + + embed = cmn.embed_factory(ctx) + embed.title = f"AE7Q History for Licensee {licensee_id}" + embed.colour = cmn.colours.good + embed.url = base_url + licensee_id + + # add the first three rows of the table to the embed + for row in table[0:3]: + header = f'**{row[0]}** ({row[3]})' # **Callsign** (Applicant Type) + body = (f'Name: *{row[2]}*\n' + f'Class: *{row[4]}*\n' + f'Region: *{row[1]}*\n' + f'Status: *{row[5]}*\n' + f'Granted: *{row[6]}*\n' + f'Effective: *{row[7]}*\n' + f'Cancelled: *{row[8]}*\n' + f'Expires: *{row[9]}*') + embed.add_field(name=header, value=body, inline=False) + + if len(table) > 3: + embed.description = f'Records 1 to 3 of {len(table)}. See ae7q.com for more...' + + await ctx.send(embed=embed) async def process_table(table: list): From 2c3535d99e968709a66c8db7613815a0a5133bda Mon Sep 17 00:00:00 2001 From: Abigail Gold Date: Wed, 8 Jan 2020 17:18:53 -0500 Subject: [PATCH 09/18] update changelog for additional ae7q commands Fixes #95 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 794d353..8f1c864 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Added Trustee field to qrz command for club callsigns. - Added alias for `ae7q call` command (`ae7q c`). +- Added ae7q lookup by FRN and Licensee ID, and for trustee records (`ae7q frn, licensee, trustee`). ### Changed - Changelog command to accept a version as argument. - The qrz command can now link to a QRZ page instead of embedding the data with the `--link` flag. From 58c69f5aebc2cc0c6a774b3a1986d6b4a5494ea2 Mon Sep 17 00:00:00 2001 From: Abigail Gold Date: Wed, 8 Jan 2020 17:47:34 -0500 Subject: [PATCH 10/18] add typing context to ae7q commands --- exts/ae7q.py | 547 ++++++++++++++++++++++++++------------------------- 1 file changed, 276 insertions(+), 271 deletions(-) diff --git a/exts/ae7q.py b/exts/ae7q.py index 490e2b4..bfcbcb6 100644 --- a/exts/ae7q.py +++ b/exts/ae7q.py @@ -36,211 +36,214 @@ class AE7QCog(commands.Cog): @_ae7q_lookup.command(name="call", aliases=["c"], category=cmn.cat.lookup) async def _ae7q_call(self, ctx: commands.Context, callsign: str): '''Look up the history of a callsign on [ae7q.com](http://ae7q.com/).''' - callsign = callsign.upper() - desc = '' - base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" - embed = cmn.embed_factory(ctx) + with ctx.typing(): + callsign = callsign.upper() + desc = '' + base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" + embed = cmn.embed_factory(ctx) - async with self.session.get(base_url + callsign) as resp: - if resp.status != 200: - embed.title = "Error in AE7Q call command" - embed.description = 'Could not load AE7Q' + async with self.session.get(base_url + callsign) as resp: + if resp.status != 200: + embed.title = "Error in AE7Q call command" + embed.description = 'Could not load AE7Q' + embed.colour = cmn.colours.bad + await ctx.send(embed=embed) + return + page = await resp.text() + + soup = BeautifulSoup(page, features="html.parser") + tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] + + table = tables[0] + + # find the first table in the page, and use it to make a description + if len(table[0]) == 1: + for row in table: + desc += " ".join(row.getText().split()) + desc += '\n' + desc = desc.replace(callsign, f'`{callsign}`') + table = tables[1] + + table_headers = table[0].find_all("th") + first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None + + # catch if the wrong table was selected + if first_header is None or first_header != 'Entity Name': + embed.title = f"AE7Q History for {callsign}" embed.colour = cmn.colours.bad + embed.url = base_url + callsign + embed.description = desc + embed.description += f'\nNo records found for `{callsign}`' await ctx.send(embed=embed) return - page = await resp.text() - soup = BeautifulSoup(page, features="html.parser") - tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] + table = await process_table(table[1:]) - table = tables[0] - - # find the first table in the page, and use it to make a description - if len(table[0]) == 1: - for row in table: - desc += " ".join(row.getText().split()) - desc += '\n' - desc = desc.replace(callsign, f'`{callsign}`') - table = tables[1] - - table_headers = table[0].find_all("th") - first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None - - # catch if the wrong table was selected - if first_header is None or first_header != 'Entity Name': + embed = cmn.embed_factory(ctx) embed.title = f"AE7Q History for {callsign}" - embed.colour = cmn.colours.bad + embed.colour = cmn.colours.good embed.url = base_url + callsign + + # add the first three rows of the table to the embed + for row in table[0:3]: + header = f'**{row[0]}** ({row[1]})' # **Name** (Applicant Type) + body = (f'Class: *{row[2]}*\n' + f'Region: *{row[3]}*\n' + f'Status: *{row[4]}*\n' + f'Granted: *{row[5]}*\n' + f'Effective: *{row[6]}*\n' + f'Cancelled: *{row[7]}*\n' + f'Expires: *{row[8]}*') + embed.add_field(name=header, value=body, inline=False) + + if len(table) > 3: + desc += f'\nRecords 1 to 3 of {len(table)}. See ae7q.com for more...' + embed.description = desc - embed.description += f'\nNo records found for `{callsign}`' + await ctx.send(embed=embed) - return - - table = await process_table(table[1:]) - - embed = cmn.embed_factory(ctx) - embed.title = f"AE7Q History for {callsign}" - embed.colour = cmn.colours.good - embed.url = base_url + callsign - - # add the first three rows of the table to the embed - for row in table[0:3]: - header = f'**{row[0]}** ({row[1]})' # **Name** (Applicant Type) - body = (f'Class: *{row[2]}*\n' - f'Region: *{row[3]}*\n' - f'Status: *{row[4]}*\n' - f'Granted: *{row[5]}*\n' - f'Effective: *{row[6]}*\n' - f'Cancelled: *{row[7]}*\n' - f'Expires: *{row[8]}*') - embed.add_field(name=header, value=body, inline=False) - - if len(table) > 3: - desc += f'\nRecords 1 to 3 of {len(table)}. See ae7q.com for more...' - - embed.description = desc - - await ctx.send(embed=embed) @_ae7q_lookup.command(name="trustee", aliases=["t"], category=cmn.cat.lookup) async def _ae7q_trustee(self, ctx: commands.Context, callsign: str): '''Look up the licenses for which a licensee is trustee on [ae7q.com](http://ae7q.com/).''' - callsign = callsign.upper() - desc = '' - base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" - embed = cmn.embed_factory(ctx) + with ctx.typing(): + callsign = callsign.upper() + desc = '' + base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" + embed = cmn.embed_factory(ctx) - async with self.session.get(base_url + callsign) as resp: - if resp.status != 200: - embed.title = "Error in AE7Q trustee command" - embed.description = 'Could not load AE7Q' + async with self.session.get(base_url + callsign) as resp: + if resp.status != 200: + embed.title = "Error in AE7Q trustee command" + embed.description = 'Could not load AE7Q' + embed.colour = cmn.colours.bad + await ctx.send(embed=embed) + return + page = await resp.text() + + soup = BeautifulSoup(page, features="html.parser") + tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] + + try: + table = tables[2] if len(tables[0][0]) == 1 else tables[1] + except IndexError: + embed.title = f"AE7Q Trustee History for {callsign}" embed.colour = cmn.colours.bad + embed.url = base_url + callsign + embed.description = desc + embed.description += f'\nNo records found for `{callsign}`' await ctx.send(embed=embed) return - page = await resp.text() - soup = BeautifulSoup(page, features="html.parser") - tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] + table_headers = table[0].find_all("th") + first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None - try: - table = tables[2] if len(tables[0][0]) == 1 else tables[1] - except IndexError: + # catch if the wrong table was selected + if first_header is None or not first_header.startswith("With"): + embed.title = f"AE7Q Trustee History for {callsign}" + embed.colour = cmn.colours.bad + embed.url = base_url + callsign + embed.description = desc + embed.description += f'\nNo records found for `{callsign}`' + await ctx.send(embed=embed) + return + + table = await process_table(table[2:]) + + embed = cmn.embed_factory(ctx) embed.title = f"AE7Q Trustee History for {callsign}" - embed.colour = cmn.colours.bad + embed.colour = cmn.colours.good embed.url = base_url + callsign + + # add the first three rows of the table to the embed + for row in table[0:3]: + header = f'**{row[0]}** ({row[3]})' # **Name** (Applicant Type) + body = (f'Name: *{row[2]}*\n' + f'Region: *{row[1]}*\n' + f'Status: *{row[4]}*\n' + f'Granted: *{row[5]}*\n' + f'Effective: *{row[6]}*\n' + f'Cancelled: *{row[7]}*\n' + f'Expires: *{row[8]}*') + embed.add_field(name=header, value=body, inline=False) + + if len(table) > 3: + desc += f'\nRecords 1 to 3 of {len(table)}. See ae7q.com for more...' + embed.description = desc - embed.description += f'\nNo records found for `{callsign}`' + await ctx.send(embed=embed) - return - - table_headers = table[0].find_all("th") - first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None - - # catch if the wrong table was selected - if first_header is None or not first_header.startswith("With"): - embed.title = f"AE7Q Trustee History for {callsign}" - embed.colour = cmn.colours.bad - embed.url = base_url + callsign - embed.description = desc - embed.description += f'\nNo records found for `{callsign}`' - await ctx.send(embed=embed) - return - - table = await process_table(table[2:]) - - embed = cmn.embed_factory(ctx) - embed.title = f"AE7Q Trustee History for {callsign}" - embed.colour = cmn.colours.good - embed.url = base_url + callsign - - # add the first three rows of the table to the embed - for row in table[0:3]: - header = f'**{row[0]}** ({row[3]})' # **Name** (Applicant Type) - body = (f'Name: *{row[2]}*\n' - f'Region: *{row[1]}*\n' - f'Status: *{row[4]}*\n' - f'Granted: *{row[5]}*\n' - f'Effective: *{row[6]}*\n' - f'Cancelled: *{row[7]}*\n' - f'Expires: *{row[8]}*') - embed.add_field(name=header, value=body, inline=False) - - if len(table) > 3: - desc += f'\nRecords 1 to 3 of {len(table)}. See ae7q.com for more...' - - embed.description = desc - - await ctx.send(embed=embed) @_ae7q_lookup.command(name="applications", aliases=["a"], category=cmn.cat.lookup) async def _ae7q_applications(self, ctx: commands.Context, callsign: str): '''Look up the application history for a callsign on [ae7q.com](http://ae7q.com/).''' """ - callsign = callsign.upper() - desc = '' - base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" - embed = cmn.embed_factory(ctx) + with ctx.typing(): + callsign = callsign.upper() + desc = '' + base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" + embed = cmn.embed_factory(ctx) - async with self.session.get(base_url + callsign) as resp: - if resp.status != 200: - embed.title = "Error in AE7Q applications command" - embed.description = 'Could not load AE7Q' + async with self.session.get(base_url + callsign) as resp: + if resp.status != 200: + embed.title = "Error in AE7Q applications command" + embed.description = 'Could not load AE7Q' + embed.colour = cmn.colours.bad + await ctx.send(embed=embed) + return + page = await resp.text() + + soup = BeautifulSoup(page, features="html.parser") + tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] + + table = tables[0] + + # find the first table in the page, and use it to make a description + if len(table[0]) == 1: + for row in table: + desc += " ".join(row.getText().split()) + desc += '\n' + desc = desc.replace(callsign, f'`{callsign}`') + + # select the last table to get applications + table = tables[-1] + + table_headers = table[0].find_all("th") + first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None + + # catch if the wrong table was selected + if first_header is None or not first_header.startswith("Receipt"): + embed.title = f"AE7Q Application History for {callsign}" embed.colour = cmn.colours.bad + embed.url = base_url + callsign + embed.description = desc + embed.description += f'\nNo records found for `{callsign}`' await ctx.send(embed=embed) return - page = await resp.text() - soup = BeautifulSoup(page, features="html.parser") - tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] + table = await process_table(table[1:]) - table = tables[0] - - # find the first table in the page, and use it to make a description - if len(table[0]) == 1: - for row in table: - desc += " ".join(row.getText().split()) - desc += '\n' - desc = desc.replace(callsign, f'`{callsign}`') - - # select the last table to get applications - table = tables[-1] - - table_headers = table[0].find_all("th") - first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None - - # catch if the wrong table was selected - if first_header is None or not first_header.startswith("Receipt"): + embed = cmn.embed_factory(ctx) embed.title = f"AE7Q Application History for {callsign}" - embed.colour = cmn.colours.bad + embed.colour = cmn.colours.good embed.url = base_url + callsign + + # add the first three rows of the table to the embed + for row in table[0:3]: + header = f'**{row[1]}** ({row[3]})' # **Name** (Callsign) + body = (f'Received: *{row[0]}*\n' + f'Region: *{row[2]}*\n' + f'Purpose: *{row[5]}*\n' + f'Last Action: *{row[7]}*\n' + f'Application Status: *{row[8]}*\n') + embed.add_field(name=header, value=body, inline=False) + + if len(table) > 3: + desc += f'\nRecords 1 to 3 of {len(table)}. See ae7q.com for more...' + embed.description = desc - embed.description += f'\nNo records found for `{callsign}`' + await ctx.send(embed=embed) - return - - table = await process_table(table[1:]) - - embed = cmn.embed_factory(ctx) - embed.title = f"AE7Q Application History for {callsign}" - embed.colour = cmn.colours.good - embed.url = base_url + callsign - - # add the first three rows of the table to the embed - for row in table[0:3]: - header = f'**{row[1]}** ({row[3]})' # **Name** (Callsign) - body = (f'Received: *{row[0]}*\n' - f'Region: *{row[2]}*\n' - f'Purpose: *{row[5]}*\n' - f'Last Action: *{row[7]}*\n' - f'Application Status: *{row[8]}*\n') - embed.add_field(name=header, value=body, inline=False) - - if len(table) > 3: - desc += f'\nRecords 1 to 3 of {len(table)}. See ae7q.com for more...' - - embed.description = desc - - await ctx.send(embed=embed) """ pass @@ -252,133 +255,135 @@ class AE7QCog(commands.Cog): - 2 tables: callsign history and application history - If not found: no tables """ - base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN=" - embed = cmn.embed_factory(ctx) + with ctx.typing(): + base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN=" + embed = cmn.embed_factory(ctx) - async with self.session.get(base_url + frn) as resp: - if resp.status != 200: - embed.title = "Error in AE7Q frn command" - embed.description = 'Could not load AE7Q' + async with self.session.get(base_url + frn) as resp: + if resp.status != 200: + embed.title = "Error in AE7Q frn command" + embed.description = 'Could not load AE7Q' + embed.colour = cmn.colours.bad + await ctx.send(embed=embed) + return + page = await resp.text() + + soup = BeautifulSoup(page, features="html.parser") + tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] + + if not len(tables): + embed.title = f"AE7Q History for FRN {frn}" embed.colour = cmn.colours.bad + embed.url = base_url + frn + embed.description = f'No records found for FRN `{frn}`' await ctx.send(embed=embed) return - page = await resp.text() - soup = BeautifulSoup(page, features="html.parser") - tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] + table = tables[0] - if not len(tables): + table_headers = table[0].find_all("th") + first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None + + # catch if the wrong table was selected + if first_header is None or not first_header.startswith('With Licensee'): + embed.title = f"AE7Q History for FRN {frn}" + embed.colour = cmn.colours.bad + embed.url = base_url + frn + embed.description = f'No records found for FRN `{frn}`' + await ctx.send(embed=embed) + return + + table = await process_table(table[2:]) + + embed = cmn.embed_factory(ctx) embed.title = f"AE7Q History for FRN {frn}" - embed.colour = cmn.colours.bad + embed.colour = cmn.colours.good embed.url = base_url + frn - embed.description = f'No records found for FRN `{frn}`' + + # add the first three rows of the table to the embed + for row in table[0:3]: + header = f'**{row[0]}** ({row[3]})' # **Callsign** (Applicant Type) + body = (f'Name: *{row[2]}*\n' + f'Class: *{row[4]}*\n' + f'Region: *{row[1]}*\n' + f'Status: *{row[5]}*\n' + f'Granted: *{row[6]}*\n' + f'Effective: *{row[7]}*\n' + f'Cancelled: *{row[8]}*\n' + f'Expires: *{row[9]}*') + embed.add_field(name=header, value=body, inline=False) + + if len(table) > 3: + embed.description = f'Records 1 to 3 of {len(table)}. See ae7q.com for more...' + await ctx.send(embed=embed) - return - - table = tables[0] - - table_headers = table[0].find_all("th") - first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None - - # catch if the wrong table was selected - if first_header is None or not first_header.startswith('With Licensee'): - embed.title = f"AE7Q History for FRN {frn}" - embed.colour = cmn.colours.bad - embed.url = base_url + frn - embed.description = f'No records found for FRN `{frn}`' - await ctx.send(embed=embed) - return - - table = await process_table(table[2:]) - - embed = cmn.embed_factory(ctx) - embed.title = f"AE7Q History for FRN {frn}" - embed.colour = cmn.colours.good - embed.url = base_url + frn - - # add the first three rows of the table to the embed - for row in table[0:3]: - header = f'**{row[0]}** ({row[3]})' # **Callsign** (Applicant Type) - body = (f'Name: *{row[2]}*\n' - f'Class: *{row[4]}*\n' - f'Region: *{row[1]}*\n' - f'Status: *{row[5]}*\n' - f'Granted: *{row[6]}*\n' - f'Effective: *{row[7]}*\n' - f'Cancelled: *{row[8]}*\n' - f'Expires: *{row[9]}*') - embed.add_field(name=header, value=body, inline=False) - - if len(table) > 3: - embed.description = f'Records 1 to 3 of {len(table)}. See ae7q.com for more...' - - await ctx.send(embed=embed) @_ae7q_lookup.command(name="licensee", aliases=["l"], category=cmn.cat.lookup) async def _ae7q_licensee(self, ctx: commands.Context, licensee_id: str): '''Look up the history of a licensee ID on [ae7q.com](http://ae7q.com/).''' - licensee_id = licensee_id.upper() - base_url = "http://ae7q.com/query/data/LicenseeIdHistory.php?ID=" - embed = cmn.embed_factory(ctx) + with ctx.typing(): + licensee_id = licensee_id.upper() + base_url = "http://ae7q.com/query/data/LicenseeIdHistory.php?ID=" + embed = cmn.embed_factory(ctx) - async with self.session.get(base_url + licensee_id) as resp: - if resp.status != 200: - embed.title = "Error in AE7Q licensee command" - embed.description = 'Could not load AE7Q' + async with self.session.get(base_url + licensee_id) as resp: + if resp.status != 200: + embed.title = "Error in AE7Q licensee command" + embed.description = 'Could not load AE7Q' + embed.colour = cmn.colours.bad + await ctx.send(embed=embed) + return + page = await resp.text() + + soup = BeautifulSoup(page, features="html.parser") + tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] + + if not len(tables): + embed.title = f"AE7Q History for Licensee {licensee_id}" embed.colour = cmn.colours.bad + embed.url = base_url + licensee_id + embed.description = f'No records found for Licensee `{licensee_id}`' await ctx.send(embed=embed) return - page = await resp.text() - soup = BeautifulSoup(page, features="html.parser") - tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")] + table = tables[0] - if not len(tables): + table_headers = table[0].find_all("th") + first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None + + # catch if the wrong table was selected + if first_header is None or not first_header.startswith('With FCC'): + embed.title = f"AE7Q History for Licensee {licensee_id}" + embed.colour = cmn.colours.bad + embed.url = base_url + licensee_id + embed.description = f'No records found for Licensee `{licensee_id}`' + await ctx.send(embed=embed) + return + + table = await process_table(table[2:]) + + embed = cmn.embed_factory(ctx) embed.title = f"AE7Q History for Licensee {licensee_id}" - embed.colour = cmn.colours.bad + embed.colour = cmn.colours.good embed.url = base_url + licensee_id - embed.description = f'No records found for Licensee `{licensee_id}`' + + # add the first three rows of the table to the embed + for row in table[0:3]: + header = f'**{row[0]}** ({row[3]})' # **Callsign** (Applicant Type) + body = (f'Name: *{row[2]}*\n' + f'Class: *{row[4]}*\n' + f'Region: *{row[1]}*\n' + f'Status: *{row[5]}*\n' + f'Granted: *{row[6]}*\n' + f'Effective: *{row[7]}*\n' + f'Cancelled: *{row[8]}*\n' + f'Expires: *{row[9]}*') + embed.add_field(name=header, value=body, inline=False) + + if len(table) > 3: + embed.description = f'Records 1 to 3 of {len(table)}. See ae7q.com for more...' + await ctx.send(embed=embed) - return - - table = tables[0] - - table_headers = table[0].find_all("th") - first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None - - # catch if the wrong table was selected - if first_header is None or not first_header.startswith('With FCC'): - embed.title = f"AE7Q History for Licensee {licensee_id}" - embed.colour = cmn.colours.bad - embed.url = base_url + licensee_id - embed.description = f'No records found for Licensee `{licensee_id}`' - await ctx.send(embed=embed) - return - - table = await process_table(table[2:]) - - embed = cmn.embed_factory(ctx) - embed.title = f"AE7Q History for Licensee {licensee_id}" - embed.colour = cmn.colours.good - embed.url = base_url + licensee_id - - # add the first three rows of the table to the embed - for row in table[0:3]: - header = f'**{row[0]}** ({row[3]})' # **Callsign** (Applicant Type) - body = (f'Name: *{row[2]}*\n' - f'Class: *{row[4]}*\n' - f'Region: *{row[1]}*\n' - f'Status: *{row[5]}*\n' - f'Granted: *{row[6]}*\n' - f'Effective: *{row[7]}*\n' - f'Cancelled: *{row[8]}*\n' - f'Expires: *{row[9]}*') - embed.add_field(name=header, value=body, inline=False) - - if len(table) > 3: - embed.description = f'Records 1 to 3 of {len(table)}. See ae7q.com for more...' - - await ctx.send(embed=embed) async def process_table(table: list): From 776ff7258189f8c549db1a10a2e43135aa405093 Mon Sep 17 00:00:00 2001 From: Abigail Gold Date: Mon, 13 Jan 2020 14:06:26 -0500 Subject: [PATCH 11/18] add ability to use any pool for hamstudy command Fixes #28 --- exts/study.py | 147 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 118 insertions(+), 29 deletions(-) diff --git a/exts/study.py b/exts/study.py index 2f28ae6..6443e07 100644 --- a/exts/study.py +++ b/exts/study.py @@ -9,6 +9,7 @@ General Public License, version 2. import random import json +from datetime import datetime import aiohttp @@ -25,41 +26,115 @@ class StudyCog(commands.Cog): self.session = aiohttp.ClientSession(connector=bot.qrm.connector) @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' - gen_pool = 'E3_2019' - extra_pool = 'E4_2016' - - embed = cmn.embed_factory(ctx) + async def _random_question(self, ctx: commands.Context, country: str = '', level: str = ''): + '''Gets a random question from [HamStudy's](https://hamstudy.org) question pools.''' with ctx.typing(): - selected_pool = None - try: - level = level.lower() - except AttributeError: # no level given (it's None) - pass + embed = cmn.embed_factory(ctx) - if level in ['t', 'technician', 'tech']: - selected_pool = tech_pool + country = country.lower() + level = level.lower() - if level in ['g', 'gen', 'general']: - selected_pool = gen_pool + pool_names = {'us': {'technician': 'E2', + 'tech': 'E2', + 't': 'E2', + 'general': 'E3', + 'gen': 'E3', + 'g': 'E3', + 'extra': 'E4', + 'e': 'E4'}, + 'ca': {'basic': 'CA_B', + 'b': 'CA_B', + 'advanced': 'CA_A', + 'adv': 'CA_A', + 'a': 'CA_A', + 'basic_fr': 'CA_FB', + 'b_fr': 'CA_FB', + 'base': 'CA_FB', + 'advanced_fr': 'CA_FS', + 'adv_fr': 'CA_FS', + 'a_fr': 'CA_FS', + 'supérieure': 'CA_FS', + 'superieure': 'CA_FS', + 's': 'CA_FS'}, + 'us_c': {'c1': 'C1', + 'comm1': 'C1', + 'c3': 'C3', + 'comm3': 'C3', + 'c6': 'C6', + 'comm6': 'C6', + 'c7': 'C7', + 'comm7': 'C7', + 'c7r': 'C7R', + 'comm7r': 'C7R', + 'c8': 'C8', + 'comm8': 'C8', + 'c9': 'C9', + 'comm9': 'C9'}} - if level in ['e', 'ae', 'extra']: - selected_pool = extra_pool + if country in pool_names.keys(): + if level in pool_names[country].keys(): + pool_name = pool_names[country][level] - if (level is None) or (level == 'all'): # no pool given or user wants all, so pick a random pool - selected_pool = random.choice([tech_pool, gen_pool, extra_pool]) - if (level is not None) and (selected_pool is None): # unrecognized pool given by user - embed.title = 'Error in HamStudy command' - embed.description = ('The question pool you gave was unrecognized. ' - 'There are many ways to call up certain question pools - try ?rq t, g, or e. ' - '\n\nNote that currently only the US question pools are available.') + elif level in ("random", "r"): + # select a random level in that country + pool_name = random.choice(list(pool_names[country].values())) + + else: + # show list of possible pools + embed.title = "Pool Not Found!" + embed.description = "Possible arguments are:" + embed.colour = cmn.colours.bad + for cty in pool_names: + levels = '`, `'.join(pool_names[cty].keys()) + embed.add_field(name=f"**Country: `{cty}`**", value=f"Levels: `{levels}`", inline=False) + await ctx.send(embed=embed) + return + + elif country in ("random", "r"): + # select a random country and level + country = random.choice(list(pool_names.keys())) + pool_name = random.choice(list(pool_names[country].values())) + + else: + # show list of possible pools + embed.title = "Pool Not Found!" + embed.description = "Possible arguments are:" embed.colour = cmn.colours.bad + for cty in pool_names: + levels = '`, `'.join(pool_names[cty].keys()) + embed.add_field(name=f"**Country: `{cty}`**", value=f"Levels: `{levels}`", inline=False) await ctx.send(embed=embed) return - async with self.session.get(f'https://hamstudy.org/pools/{selected_pool}') as resp: + pools = await self.hamstudy_get_pools() + + pool_matches = [p for p in pools.keys() if p.startswith(pool_name)] + + if len(pool_matches) > 0: + if len(pool_matches) == 1: + pool = pool_matches[0] + else: + # look at valid_from and expires dates to find the correct one + for p in pool_matches: + valid_from = datetime.fromisoformat(pools[p]["valid_from"][:-1] + "+00:00") + expires = datetime.fromisoformat(pools[p]["expires"][:-1] + "+00:00") + + if valid_from < datetime.utcnow() < expires: + pool = p + break + else: + # show list of possible pools + embed.title = "Pool Not Found!" + embed.description = "Possible arguments are:" + embed.colour = cmn.colours.bad + for cty in pool_names: + levels = '`, `'.join(pool_names[cty].keys()) + embed.add_field(name=f"**Country: `{cty}`**", value=f"Levels: `{levels}`", inline=False) + await ctx.send(embed=embed) + return + + + async with self.session.get(f'https://hamstudy.org/pools/{pool}') as resp: if resp.status != 200: embed.title = 'Error in HamStudy command' embed.description = 'Could not load questions' @@ -83,13 +158,12 @@ class StudyCog(commands.Cog): + '\n**D:** ' + question['answers']['D'], inline=False) embed.add_field(name='Answer:', value='Type _?rqa_ for answer', inline=False) if 'image' in question: - image_url = f'https://hamstudy.org/_1330011/images/{selected_pool.split("_",1)[1]}/{question["image"]}' + image_url = f'https://hamstudy.org/_1330011/images/{pool.split("_",1)[1]}/{question["image"]}' embed.set_image(url=image_url) self.lastq[ctx.message.channel.id] = (question['id'], question['answer']) await ctx.send(embed=embed) - @commands.command(name="hamstudyanswer", aliases=['rqa', 'randomquestionanswer', 'randomqa', 'hamstudya'], - category=cmn.cat.study) + @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(): @@ -115,6 +189,21 @@ class StudyCog(commands.Cog): embed.colour = cmn.colours.neutral await ctx.send(embed=embed) + async def hamstudy_get_pools(self): + async with self.session.get('https://hamstudy.org/pools/') as resp: + if resp.status != 200: + raise ConnectionError + else: + pools_dict = json.loads(await resp.read()) + + pools_list = [] + for l in pools_dict.values(): + pools_list += l + + pools = {p["id"]: p for p in pools_list} + + return pools + def setup(bot: commands.Bot): bot.add_cog(StudyCog(bot)) From 671b0e9ee5eb924c4735148b035db797ddf7776c Mon Sep 17 00:00:00 2001 From: Bruce Yang Date: Fri, 17 Jan 2020 00:30:08 -0500 Subject: [PATCH 12/18] Fixed issue #172 --- exts/base.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/exts/base.py b/exts/base.py index eee76a2..9fea4c5 100644 --- a/exts/base.py +++ b/exts/base.py @@ -166,11 +166,6 @@ class BaseCog(commands.Cog): "(https://github.com/classabbyamp/discord-qrm2/issues)!") await ctx.send(embed=embed) - @commands.command(name="bruce", hidden=True) - async def _b_issue(self, ctx: commands.Context): - """Shows how to create an issue for the bot.""" - await ctx.invoke(self._issue) - @commands.command(name="echo", aliases=["e"], hidden=True) @commands.check(cmn.check_if_owner) async def _echo(self, ctx: commands.Context, From 12886fad89423d17c0ebc7bdc1056dac91eec9d1 Mon Sep 17 00:00:00 2001 From: 0x5c Date: Mon, 20 Jan 2020 03:17:50 -0500 Subject: [PATCH 13/18] Fixed bad file/embed sending logic in multiple commands -> image/*, weather/* - Addressed #167 for those commands - weather/greyline: moved gl_url to the class Fixes #139 Touches #167 --- exts/image.py | 76 ++++++++++++++++++++++++------------------------- exts/weather.py | 33 +++++++++++---------- 2 files changed, 56 insertions(+), 53 deletions(-) diff --git a/exts/image.py b/exts/image.py index b455f17..b5448d0 100644 --- a/exts/image.py +++ b/exts/image.py @@ -18,6 +18,9 @@ import common as cmn class ImageCog(commands.Cog): + 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=') + def __init__(self, bot: commands.Bot): self.bot = bot self.bandcharts = cmn.ImagesGroup(cmn.paths.bandcharts / "meta.json") @@ -27,9 +30,8 @@ class ImageCog(commands.Cog): @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.''' - arg = region.lower() - - with ctx.typing(): + async with ctx.typing(): + arg = region.lower() embed = cmn.embed_factory(ctx) if arg not in self.bandcharts: desc = 'Possible arguments are:\n' @@ -39,25 +41,24 @@ class ImageCog(commands.Cog): embed.description = desc embed.colour = cmn.colours.bad await ctx.send(embed=embed) - else: - metadata: cmn.ImageMetadata = self.bandcharts[arg] - img = discord.File(cmn.paths.bandcharts / metadata.filename, - filename=metadata.filename) - if metadata.description: - embed.description = metadata.description - if metadata.source: - embed.add_field(name="Source", value=metadata.source) - embed.title = metadata.long_name + (" " + metadata.emoji if metadata.emoji else "") - embed.colour = cmn.colours.good - embed.set_image(url='attachment://' + metadata.filename) - await ctx.send(embed=embed, file=img) + return + metadata: cmn.ImageMetadata = self.bandcharts[arg] + img = discord.File(cmn.paths.bandcharts / metadata.filename, + filename=metadata.filename) + if metadata.description: + embed.description = metadata.description + if metadata.source: + embed.add_field(name="Source", value=metadata.source) + embed.title = metadata.long_name + (" " + metadata.emoji if metadata.emoji else "") + embed.colour = cmn.colours.good + embed.set_image(url='attachment://' + metadata.filename) + await ctx.send(embed=embed, file=img) @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.''' - arg = map_id.lower() - - with ctx.typing(): + async with ctx.typing(): + arg = map_id.lower() embed = cmn.embed_factory(ctx) if arg not in self.maps: desc = 'Possible arguments are:\n' @@ -67,36 +68,35 @@ class ImageCog(commands.Cog): embed.description = desc embed.colour = cmn.colours.bad await ctx.send(embed=embed) - else: - metadata: cmn.ImageMetadata = self.maps[arg] - img = discord.File(cmn.paths.maps / metadata.filename, - filename=metadata.filename) - if metadata.description: - embed.description = metadata.description - if metadata.source: - embed.add_field(name="Source", value=metadata.source) - embed.title = metadata.long_name + (" " + metadata.emoji if metadata.emoji else "") - embed.colour = cmn.colours.good - embed.set_image(url='attachment://' + metadata.filename) - await ctx.send(embed=embed, file=img) + return + metadata: cmn.ImageMetadata = self.maps[arg] + img = discord.File(cmn.paths.maps / metadata.filename, + filename=metadata.filename) + if metadata.description: + embed.description = metadata.description + if metadata.source: + embed.add_field(name="Source", value=metadata.source) + embed.title = metadata.long_name + (" " + metadata.emoji if metadata.emoji else "") + embed.colour = cmn.colours.good + embed.set_image(url='attachment://' + metadata.filename) + await ctx.send(embed=embed, file=img) @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(): + async with ctx.typing(): embed = cmn.embed_factory(ctx) embed.title = 'Current Greyline Conditions' embed.colour = cmn.colours.good - async with self.session.get(gl_url) as resp: + async with self.session.get(self.gl_url) as resp: if resp.status != 200: embed.description = 'Could not download file...' embed.colour = cmn.colours.bad - else: - data = io.BytesIO(await resp.read()) - embed.set_image(url=f'attachment://greyline.jpg') - await ctx.send(embed=embed, file=discord.File(data, 'greyline.jpg')) + await ctx.send(embed=embed) + return + data = io.BytesIO(await resp.read()) + embed.set_image(url=f'attachment://greyline.jpg') + await ctx.send(embed=embed, file=discord.File(data, 'greyline.jpg')) def setup(bot: commands.Bot): diff --git a/exts/weather.py b/exts/weather.py index 6e931f9..e877a02 100644 --- a/exts/weather.py +++ b/exts/weather.py @@ -28,7 +28,7 @@ class WeatherCog(commands.Cog): @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(): + async with ctx.typing(): embed = cmn.embed_factory(ctx) embed.title = 'Current Solar Conditions' embed.colour = cmn.colours.good @@ -36,10 +36,11 @@ class WeatherCog(commands.Cog): if resp.status != 200: embed.description = 'Could not download file...' embed.colour = cmn.colours.bad - else: - data = io.BytesIO(await resp.read()) - embed.set_image(url=f'attachment://condx.png') - await ctx.send(embed=embed, file=discord.File(data, 'condx.png')) + await ctx.send(embed=embed) + return + data = io.BytesIO(await resp.read()) + embed.set_image(url=f'attachment://condx.png') + await ctx.send(embed=embed, file=discord.File(data, 'condx.png')) @commands.group(name="weather", aliases=['wttr'], category=cmn.cat.weather) async def _weather_conditions(self, ctx: commands.Context): @@ -61,7 +62,7 @@ class WeatherCog(commands.Cog): 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(): + async with ctx.typing(): try: units_arg = re.search(self.wttr_units_regex, location).group(1) except AttributeError: @@ -85,16 +86,17 @@ See help for weather command for possible location types. Add a `-c` or `-f` to if resp.status != 200: embed.description = 'Could not download file...' embed.colour = cmn.colours.bad - else: - data = io.BytesIO(await resp.read()) - embed.set_image(url=f'attachment://wttr_forecast.png') - await ctx.send(embed=embed, file=discord.File(data, 'wttr_forecast.png')) + await ctx.send(embed=embed) + return + data = io.BytesIO(await resp.read()) + embed.set_image(url=f'attachment://wttr_forecast.png') + await ctx.send(embed=embed, file=discord.File(data, '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): '''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(): + async with ctx.typing(): try: units_arg = re.search(self.wttr_units_regex, location).group(1) except AttributeError: @@ -118,10 +120,11 @@ See help for weather command for possible location types. Add a `-c` or `-f` to if resp.status != 200: embed.description = 'Could not download file...' embed.colour = cmn.colours.bad - else: - data = io.BytesIO(await resp.read()) - embed.set_image(url=f'attachment://wttr_now.png') - await ctx.send(embed=embed, file=discord.File(data, 'wttr_now.png')) + await ctx.send(embed=embed) + return + data = io.BytesIO(await resp.read()) + embed.set_image(url=f'attachment://wttr_now.png') + await ctx.send(embed=embed, file=discord.File(data, 'wttr_now.png')) def setup(bot: commands.Bot): From 925a05aafb0c52b5fceef869d089f07fd0303e9e Mon Sep 17 00:00:00 2001 From: 0x5c Date: Mon, 20 Jan 2020 03:49:03 -0500 Subject: [PATCH 14/18] Mini-fix: extraneous warning emoji reaction on messages starting with "??" - also taken care of: "?!" --- CHANGELOG.md | 1 + main.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f1c864..d4de433 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Fixed ditto marks (") appearing in the ae7q call command. - Fixed issue where incorrect table was parsed in ae7q call command. +- Fixed warning emoji reaction on messages starting with "??". ## [2.1.0] - 2020-01-04 diff --git a/main.py b/main.py index dbce811..01ddb0b 100644 --- a/main.py +++ b/main.py @@ -164,8 +164,11 @@ async def on_command_error(ctx: commands.Context, err: commands.CommandError): if isinstance(err, commands.UserInputError): await cmn.add_react(ctx.message, cmn.emojis.warning) await ctx.send_help(ctx.command) - elif isinstance(err, commands.CommandNotFound) and not ctx.invoked_with.startswith("?"): - await cmn.add_react(ctx.message, cmn.emojis.question) + elif isinstance(err, commands.CommandNotFound): + if ctx.invoked_with.startswith(("?", "!")): + return + else: + await cmn.add_react(ctx.message, cmn.emojis.question) elif isinstance(err, commands.CheckFailure): # Add handling of other subclasses of CheckFailure as needed. if isinstance(err, commands.NotOwner): From b6f6d0408cebd5843c508782f249e614126b4858 Mon Sep 17 00:00:00 2001 From: Abigail Gold Date: Mon, 20 Jan 2020 20:50:55 -0500 Subject: [PATCH 15/18] WIP: answer hamstudy by reaction --- common.py | 6 +++++- exts/study.py | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/common.py b/common.py index 20a7d77..1795850 100644 --- a/common.py +++ b/common.py @@ -44,7 +44,11 @@ emojis = SimpleNamespace(check_mark='✅', warning='⚠️', question='❓', no_entry='⛔', - bangbang='‼️') + bangbang='‼️', + a='🇦', + b='🇧', + c='🇨', + d='🇩') paths = SimpleNamespace(data=Path("./data/"), resources=Path("./resources/"), diff --git a/exts/study.py b/exts/study.py index 6443e07..81168f5 100644 --- a/exts/study.py +++ b/exts/study.py @@ -152,16 +152,24 @@ class StudyCog(commands.Cog): embed.description = self.source embed.colour = cmn.colours.good embed.add_field(name='Question:', value=question['text'], inline=False) - embed.add_field(name='Answers:', value='**A:** ' + question['answers']['A'] - + '\n**B:** ' + question['answers']['B'] - + '\n**C:** ' + question['answers']['C'] - + '\n**D:** ' + question['answers']['D'], inline=False) + embed.add_field(name='Answers:', + value=(f"**{cmn.emojis.a}** {question['answers']['A']}" + f"\n**{cmn.emojis.b}** {question['answers']['B']}" + f"\n**{cmn.emojis.c}** {question['answers']['C']}" + f"\n**{cmn.emojis.d}** {question['answers']['D']}"), + inline=False) embed.add_field(name='Answer:', value='Type _?rqa_ for answer', inline=False) if 'image' in question: image_url = f'https://hamstudy.org/_1330011/images/{pool.split("_",1)[1]}/{question["image"]}' embed.set_image(url=image_url) self.lastq[ctx.message.channel.id] = (question['id'], question['answer']) - await ctx.send(embed=embed) + q_msg = await ctx.send(embed=embed) + + await cmn.add_react(q_msg, cmn.emojis.a) + await cmn.add_react(q_msg, cmn.emojis.b) + await cmn.add_react(q_msg, cmn.emojis.c) + await cmn.add_react(q_msg, cmn.emojis.d) + @commands.command(name="hamstudyanswer", aliases=['rqa', 'randomquestionanswer', 'randomqa', 'hamstudya'], category=cmn.cat.study) async def _q_answer(self, ctx: commands.Context, answer: str = None): From 09c58f9ba2734fdad34ca947e328e497f94f2be2 Mon Sep 17 00:00:00 2001 From: Abigail Date: Tue, 21 Jan 2020 19:48:19 -0500 Subject: [PATCH 16/18] removed hamstudyanswer command and replaced it with using reactions to answer questions. Fixes #169 --- exts/study.py | 137 +++++++++++++++++---------------------------- resources/study.py | 49 ++++++++++++++++ 2 files changed, 100 insertions(+), 86 deletions(-) create mode 100644 resources/study.py diff --git a/exts/study.py b/exts/study.py index 81168f5..1d68e66 100644 --- a/exts/study.py +++ b/exts/study.py @@ -10,15 +10,19 @@ General Public License, version 2. import random import json from datetime import datetime +import asyncio import aiohttp import discord.ext.commands as commands import common as cmn +from resources import study class StudyCog(commands.Cog): + choices = {cmn.emojis.a: 'A', cmn.emojis.b: 'B', cmn.emojis.c: 'C', cmn.emojis.d: 'D'} + def __init__(self, bot: commands.Bot): self.bot = bot self.lastq = dict() @@ -34,81 +38,46 @@ class StudyCog(commands.Cog): country = country.lower() level = level.lower() - pool_names = {'us': {'technician': 'E2', - 'tech': 'E2', - 't': 'E2', - 'general': 'E3', - 'gen': 'E3', - 'g': 'E3', - 'extra': 'E4', - 'e': 'E4'}, - 'ca': {'basic': 'CA_B', - 'b': 'CA_B', - 'advanced': 'CA_A', - 'adv': 'CA_A', - 'a': 'CA_A', - 'basic_fr': 'CA_FB', - 'b_fr': 'CA_FB', - 'base': 'CA_FB', - 'advanced_fr': 'CA_FS', - 'adv_fr': 'CA_FS', - 'a_fr': 'CA_FS', - 'supérieure': 'CA_FS', - 'superieure': 'CA_FS', - 's': 'CA_FS'}, - 'us_c': {'c1': 'C1', - 'comm1': 'C1', - 'c3': 'C3', - 'comm3': 'C3', - 'c6': 'C6', - 'comm6': 'C6', - 'c7': 'C7', - 'comm7': 'C7', - 'c7r': 'C7R', - 'comm7r': 'C7R', - 'c8': 'C8', - 'comm8': 'C8', - 'c9': 'C9', - 'comm9': 'C9'}} - - if country in pool_names.keys(): - if level in pool_names[country].keys(): - pool_name = pool_names[country][level] + if country in study.pool_names.keys(): + if level in study.pool_names[country].keys(): + pool_name = study.pool_names[country][level] elif level in ("random", "r"): # select a random level in that country - pool_name = random.choice(list(pool_names[country].values())) + pool_name = random.choice(list(study.pool_names[country].values())) else: # show list of possible pools embed.title = "Pool Not Found!" embed.description = "Possible arguments are:" embed.colour = cmn.colours.bad - for cty in pool_names: - levels = '`, `'.join(pool_names[cty].keys()) - embed.add_field(name=f"**Country: `{cty}`**", value=f"Levels: `{levels}`", inline=False) + for cty in study.pool_names: + levels = '`, `'.join(study.pool_names[cty].keys()) + embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**", value=f"Levels: `{levels}`", inline=False) + embed.add_field(name="**Random**", value="To select a random pool or country, use `random` or `r`") await ctx.send(embed=embed) return elif country in ("random", "r"): # select a random country and level - country = random.choice(list(pool_names.keys())) - pool_name = random.choice(list(pool_names[country].values())) + country = random.choice(list(study.pool_names.keys())) + pool_name = random.choice(list(study.pool_names[country].values())) else: # show list of possible pools embed.title = "Pool Not Found!" embed.description = "Possible arguments are:" embed.colour = cmn.colours.bad - for cty in pool_names: - levels = '`, `'.join(pool_names[cty].keys()) - embed.add_field(name=f"**Country: `{cty}`**", value=f"Levels: `{levels}`", inline=False) + for cty in study.pool_names: + levels = '`, `'.join(study.pool_names[cty].keys()) + embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**", value=f"Levels: `{levels}`", inline=False) + embed.add_field(name="**Random**", value="To select a random pool or country, use `random` or `r`") await ctx.send(embed=embed) return pools = await self.hamstudy_get_pools() - pool_matches = [p for p in pools.keys() if p.startswith(pool_name)] + pool_matches = [p for p in pools.keys() if "_".join(p.split("_")[:-1]) == pool_name] if len(pool_matches) > 0: if len(pool_matches) == 1: @@ -127,12 +96,14 @@ class StudyCog(commands.Cog): embed.title = "Pool Not Found!" embed.description = "Possible arguments are:" embed.colour = cmn.colours.bad - for cty in pool_names: - levels = '`, `'.join(pool_names[cty].keys()) - embed.add_field(name=f"**Country: `{cty}`**", value=f"Levels: `{levels}`", inline=False) + for cty in study.pool_names: + levels = '`, `'.join(study.pool_names[cty].keys()) + embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**", value=f"Levels: `{levels}`", inline=False) + embed.add_field(name="**Random**", value="To select a random pool or country, use `random` or `r`") await ctx.send(embed=embed) return + pool_meta = pools[pool] async with self.session.get(f'https://hamstudy.org/pools/{pool}') as resp: if resp.status != 200: @@ -148,9 +119,8 @@ class StudyCog(commands.Cog): pool_questions = random.choice(pool_section)['questions'] question = random.choice(pool_questions) - embed.title = question['id'] + embed.title = f"{study.pool_emojis[country]} {pool_meta['class']} {question['id']}" embed.description = self.source - embed.colour = cmn.colours.good embed.add_field(name='Question:', value=question['text'], inline=False) embed.add_field(name='Answers:', value=(f"**{cmn.emojis.a}** {question['answers']['A']}" @@ -158,11 +128,13 @@ class StudyCog(commands.Cog): f"\n**{cmn.emojis.c}** {question['answers']['C']}" f"\n**{cmn.emojis.d}** {question['answers']['D']}"), inline=False) - embed.add_field(name='Answer:', value='Type _?rqa_ for answer', inline=False) + embed.add_field(name='To Answer:', + value='Answer with reactions below. If not answered within 10 minutes, the answer will be revealed.', + inline=False) if 'image' in question: image_url = f'https://hamstudy.org/_1330011/images/{pool.split("_",1)[1]}/{question["image"]}' embed.set_image(url=image_url) - self.lastq[ctx.message.channel.id] = (question['id'], question['answer']) + q_msg = await ctx.send(embed=embed) await cmn.add_react(q_msg, cmn.emojis.a) @@ -170,32 +142,26 @@ class StudyCog(commands.Cog): await cmn.add_react(q_msg, cmn.emojis.c) await cmn.add_react(q_msg, cmn.emojis.d) + def check(reaction, user): + return user.id != self.bot.user.id and reaction.message.id == q_msg.id and str(reaction.emoji) in self.choices.keys() - @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] - embed = cmn.embed_factory(ctx) - if answer is not None: - answer = answer.upper() - if answer == correct_ans: - result = f'Correct! The answer to {q_num} was **{correct_ans}**.' - embed.title = f'{q_num} Answer' - embed.description = f'{self.source}\n\n{result}' - embed.colour = cmn.colours.good - else: - result = f'Incorrect. The answer to {q_num} was **{correct_ans}**, not **{answer}**.' - embed.title = f'{q_num} Answer' - embed.description = f'{self.source}\n\n{result}' - embed.colour = cmn.colours.bad + try: + reaction, user = await self.bot.wait_for('reaction_add', timeout=600.0, check=check) + except asyncio.TimeoutError: + embed.remove_field(2) + embed.add_field(name="Answer:", value=f"Timed out! The correct answer was **{question['answer']}**.") + await q_msg.edit(embed=embed) + else: + if self.choices[str(reaction.emoji)] == question['answer']: + embed.remove_field(2) + embed.add_field(name="Answer:", value=f"Correct! The answer was **{question['answer']}**.") + embed.colour = cmn.colours.good + await q_msg.edit(embed=embed) else: - result = f'The correct answer to {q_num} was **{correct_ans}**.' - embed.title = f'{q_num} Answer' - embed.description = f'{self.source}\n\n{result}' - embed.colour = cmn.colours.neutral - await ctx.send(embed=embed) + embed.remove_field(2) + embed.add_field(name="Answer:", value=f"Incorrect! The correct answer was **{question['answer']}**.") + embed.colour = cmn.colours.bad + await q_msg.edit(embed=embed) async def hamstudy_get_pools(self): async with self.session.get('https://hamstudy.org/pools/') as resp: @@ -204,11 +170,10 @@ class StudyCog(commands.Cog): else: pools_dict = json.loads(await resp.read()) - pools_list = [] - for l in pools_dict.values(): - pools_list += l - - pools = {p["id"]: p for p in pools_list} + pools = dict() + for ls in pools_dict.values(): + for pool in ls: + pools[pool["id"]] = pool return pools diff --git a/resources/study.py b/resources/study.py new file mode 100644 index 0000000..b68f99f --- /dev/null +++ b/resources/study.py @@ -0,0 +1,49 @@ +""" +A listing of hamstudy command resources +--- +Copyright (C) 2019-2020 Abigail Gold, 0x5c + +This file is part of discord-qrmbot and is released under the terms of the GNU +General Public License, version 2. +""" + +pool_names = {'us': {'technician': 'E2', + 'tech': 'E2', + 't': 'E2', + 'general': 'E3', + 'gen': 'E3', + 'g': 'E3', + 'extra': 'E4', + 'e': 'E4'}, + 'ca': {'basic': 'CA_B', + 'b': 'CA_B', + 'advanced': 'CA_A', + 'adv': 'CA_A', + 'a': 'CA_A', + 'basic_fr': 'CA_FB', + 'b_fr': 'CA_FB', + 'base': 'CA_FB', + 'advanced_fr': 'CA_FS', + 'adv_fr': 'CA_FS', + 'a_fr': 'CA_FS', + 'supérieure': 'CA_FS', + 'superieure': 'CA_FS', + 's': 'CA_FS'}, + 'us_c': {'c1': 'C1', + 'comm1': 'C1', + 'c3': 'C3', + 'comm3': 'C3', + 'c6': 'C6', + 'comm6': 'C6', + 'c7': 'C7', + 'comm7': 'C7', + 'c7r': 'C7R', + 'comm7r': 'C7R', + 'c8': 'C8', + 'comm8': 'C8', + 'c9': 'C9', + 'comm9': 'C9'}} + +pool_emojis = {'us': '🇺🇸', + 'ca': '🇨🇦', + 'us_c': '🇺🇸 🏢'} From 528307f22f4d2176d9b0a07ff36c93252d852ed3 Mon Sep 17 00:00:00 2001 From: Abigail Date: Tue, 21 Jan 2020 20:25:37 -0500 Subject: [PATCH 17/18] updated changelog for hamstudy refactor --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8648c6..cb9a35f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Changelog command to accept a version as argument. - The qrz command can now link to a QRZ page instead of embedding the data with the `--link` flag. +- All currently-available pools can now be accessed by the `hamstudy` command. +- The `hamstudy` command now uses the syntax `?hamstudy `. +- Replaced `hamstudyanswer` command with answering by reaction. ### Fixed - Fixed ditto marks (") appearing in the ae7q call command. - Fixed issue where incorrect table was parsed in ae7q call command. From b64c7ee39a5c904755e307a89b4c86730f33ceb2 Mon Sep 17 00:00:00 2001 From: Abigail Date: Tue, 21 Jan 2020 20:31:13 -0500 Subject: [PATCH 18/18] damn you flake8 --- exts/study.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/exts/study.py b/exts/study.py index 1d68e66..b6e11c8 100644 --- a/exts/study.py +++ b/exts/study.py @@ -53,7 +53,8 @@ class StudyCog(commands.Cog): embed.colour = cmn.colours.bad for cty in study.pool_names: levels = '`, `'.join(study.pool_names[cty].keys()) - embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**", value=f"Levels: `{levels}`", inline=False) + embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**", + value=f"Levels: `{levels}`", inline=False) embed.add_field(name="**Random**", value="To select a random pool or country, use `random` or `r`") await ctx.send(embed=embed) return @@ -70,7 +71,8 @@ class StudyCog(commands.Cog): embed.colour = cmn.colours.bad for cty in study.pool_names: levels = '`, `'.join(study.pool_names[cty].keys()) - embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**", value=f"Levels: `{levels}`", inline=False) + embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**", + value=f"Levels: `{levels}`", inline=False) embed.add_field(name="**Random**", value="To select a random pool or country, use `random` or `r`") await ctx.send(embed=embed) return @@ -98,7 +100,8 @@ class StudyCog(commands.Cog): embed.colour = cmn.colours.bad for cty in study.pool_names: levels = '`, `'.join(study.pool_names[cty].keys()) - embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**", value=f"Levels: `{levels}`", inline=False) + embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**", + value=f"Levels: `{levels}`", inline=False) embed.add_field(name="**Random**", value="To select a random pool or country, use `random` or `r`") await ctx.send(embed=embed) return @@ -129,7 +132,8 @@ class StudyCog(commands.Cog): f"\n**{cmn.emojis.d}** {question['answers']['D']}"), inline=False) embed.add_field(name='To Answer:', - value='Answer with reactions below. If not answered within 10 minutes, the answer will be revealed.', + value=('Answer with reactions below. If not answered within 10 minutes,' + ' the answer will be revealed.'), inline=False) if 'image' in question: image_url = f'https://hamstudy.org/_1330011/images/{pool.split("_",1)[1]}/{question["image"]}' @@ -143,7 +147,9 @@ class StudyCog(commands.Cog): await cmn.add_react(q_msg, cmn.emojis.d) def check(reaction, user): - return user.id != self.bot.user.id and reaction.message.id == q_msg.id and str(reaction.emoji) in self.choices.keys() + return (user.id != self.bot.user.id + and reaction.message.id == q_msg.id + and str(reaction.emoji) in self.choices.keys()) try: reaction, user = await self.bot.wait_for('reaction_add', timeout=600.0, check=check)