Merge branch 'help-checks' of github.com:classabbyamp/discord-qrm2 into help-checks

This commit is contained in:
Abigail 2020-01-28 01:21:40 -05:00
commit 6768a1e01d
23 changed files with 712 additions and 253 deletions

View File

@ -7,12 +7,18 @@ 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`).
- 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.
- All currently-available pools can now be accessed by the `hamstudy` command.
- The `hamstudy` command now uses the syntax `?hamstudy <country> <pool>`.
- 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.
- Fixed warning emoji reaction on messages starting with "??".
## [2.1.0] - 2020-01-04

View File

@ -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.

View File

@ -1,23 +1,19 @@
"""
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.
---
`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 ---
@ -48,7 +45,11 @@ emojis = SimpleNamespace(check_mark='✅',
warning='⚠️',
question='',
no_entry='',
bangbang='‼️')
bangbang='‼️',
a='🇦',
b='🇧',
c='🇨',
d='🇩')
paths = SimpleNamespace(data=Path("./data/"),
resources=Path("./resources/"),
@ -95,6 +96,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:

View File

@ -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.
@ -10,13 +10,13 @@ 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
import aiohttp
from bs4 import BeautifulSoup
import common as cmn
@ -25,7 +25,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):
@ -33,110 +33,380 @@ 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()
desc = ''
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
embed = cmn.embed_factory(ctx)
'''Look up the history of a callsign on [ae7q.com](http://ae7q.com/).'''
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'
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
page = await resp.text()
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 = soup.select("table.Database")
soup = BeautifulSoup(page, features="html.parser")
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:
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}`')
rows = None
table = tables[1]
first_header = ''.join(rows[0].find_all("th")[0].strings)
table_headers = table[0].find_all("th")
first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None
if rows is None or first_header != 'Entity Name':
# 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
table = await process_table(table[1:])
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q History for {callsign}"
embed.colour = cmn.colours.bad
embed.url = f"{base_url}{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
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]
@_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/).'''
with ctx.typing():
callsign = callsign.upper()
desc = ''
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
embed = cmn.embed_factory(ctx)
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q Records for {callsign}"
embed.colour = cmn.colours.good
embed.url = f"{base_url}{callsign}"
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()
for row in table_contents[0:3]:
header = f'**{row[0]}** ({row[1]})'
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)
soup = BeautifulSoup(page, features="html.parser")
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
embed.description = desc
if len(table_contents) > 3:
embed.description += f'\nRecords 1 to 3 of {len(table_contents)}. See ae7q.com for more...'
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
await ctx.send(embed=embed)
table_headers = table[0].find_all("th")
first_header = ''.join(table_headers[0].strings) if len(table_headers) > 0 else None
# TODO: write commands for other AE7Q response types?
# @_ae7q_lookup.command(name="trustee")
# async def _ae7q_trustee(self, ctx: commands.Context, callsign: str):
# pass
# 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
# @_ae7q_lookup.command(name="applications", aliases=['apps'])
# async def _ae7q_applications(self, ctx: commands.Context, callsign: str):
# pass
table = await process_table(table[2:])
# @_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
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q Trustee History for {callsign}"
embed.colour = cmn.colours.good
embed.url = base_url + callsign
# @_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
# 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/).'''
"""
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'
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):
'''Look up the history of an FRN on [ae7q.com](http://ae7q.com/).'''
"""
NOTES:
- 2 tables: callsign history and application history
- If not found: no tables
"""
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'
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
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/).'''
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'
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):
"""Processes tables (*not* including headers) and returns the processed table"""
table_contents = []
for tr in table:
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):

View File

@ -1,23 +1,25 @@
"""
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.
"""
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):
@ -172,15 +174,15 @@ class BaseCog(commands.Cog):
"(https://github.com/classabbyamp/discord-qrm2/issues)!")
await ctx.send(embed=embed)
@commands.command(name="bruce")
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"], category=cmn.cat.admin)
@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)

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.
@ -9,6 +9,8 @@ General Public License, version 2.
import io
import aiohttp
import discord
import discord.ext.commands as commands
@ -16,18 +18,20 @@ 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")
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 = ''):
'''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'
@ -37,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'
@ -65,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):

View File

@ -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.

View File

@ -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.

View File

@ -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.
@ -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)

View File

@ -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.
@ -9,55 +9,106 @@ 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()
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):
'''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
if country in study.pool_names.keys():
if level in study.pool_names[country].keys():
pool_name = study.pool_names[country][level]
if level in ['e', 'ae', 'extra']:
selected_pool = extra_pool
elif level in ("random", "r"):
# select a random level in that country
pool_name = random.choice(list(study.pool_names[country].values()))
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.')
else:
# show list of possible pools
embed.title = "Pool Not Found!"
embed.description = "Possible arguments are:"
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="**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(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 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
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 "_".join(p.split("_")[:-1]) == 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 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:
embed.title = 'Error in HamStudy command'
embed.description = 'Could not load questions'
@ -71,47 +122,66 @@ 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='**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='Answer:', value='Type _?rqa_ for answer', 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='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/{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)
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
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)
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())
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:
if resp.status != 200:
raise ConnectionError
else:
pools_dict = json.loads(await resp.read())
pools = dict()
for ls in pools_dict.values():
for pool in ls:
pools[pool["id"]] = pool
return pools
def setup(bot: commands.Bot):

View File

@ -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.
@ -10,6 +10,8 @@ General Public License, version 2.
import io
import re
import aiohttp
import discord
import discord.ext.commands as commands
@ -21,12 +23,12 @@ 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):
'''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
@ -34,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):
@ -59,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:
@ -83,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:
@ -116,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):

View File

@ -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.

38
main.py
View File

@ -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.
@ -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}.")
@ -74,7 +82,7 @@ async def _shutdown_bot(ctx: commands.Context):
await bot.logout()
@bot.group(name="extctl", category=cmn.cat.admin)
@bot.group(name="extctl", aliases=["ex"], category=cmn.cat.admin)
@commands.check(cmn.check_if_owner)
async def _extctl(ctx: commands.Context):
"""Extension control commands.
@ -84,7 +92,7 @@ async def _extctl(ctx: commands.Context):
await ctx.invoke(cmd)
@_extctl.command(name="list")
@_extctl.command(name="list", aliases=["ls"])
@commands.check(cmn.check_if_owner)
async def _extctl_list(ctx: commands.Context):
"""Lists Extensions."""
@ -94,7 +102,7 @@ async def _extctl_list(ctx: commands.Context):
await ctx.send(embed=embed)
@_extctl.command(name="load")
@_extctl.command(name="load", aliases=["ld"])
@commands.check(cmn.check_if_owner)
async def _extctl_load(ctx: commands.Context, extension: str):
try:
@ -105,7 +113,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"])
@commands.check(cmn.check_if_owner)
async def _extctl_reload(ctx: commands.Context, extension: str):
if ctx.invoked_with == "relaod":
@ -120,7 +128,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"])
@commands.check(cmn.check_if_owner)
async def _extctl_unload(ctx: commands.Context, extension: str):
try:
@ -160,8 +168,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):
@ -250,6 +261,7 @@ except ConnectionResetError as ex:
raise
raise SystemExit("ConnectionResetError: {}".format(ex))
# --- Exit ---
# Codes for the wrapper shell script:
# 0 - Clean exit, don't restart

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

49
resources/study.py Normal file
View File

@ -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': '🇺🇸 🏢'}

3
utils/__init__.py Normal file
View File

@ -0,0 +1,3 @@
"""
Various utilities for the bot.
"""

16
utils/connector.py Normal file
View File

@ -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)