Merge branch 'master' into 5c-metadata

Fix conflicts
This commit is contained in:
0x5c 2019-12-23 21:13:01 -05:00
commit 1b21583d41
No known key found for this signature in database
GPG Key ID: 82039FC95E3FE970
11 changed files with 159 additions and 81 deletions

View File

@ -25,7 +25,8 @@ import discord.ext.commands as commands
import data.options as opt import data.options as opt
__all__ = ["colours", "cat", "emojis", "error_embed_factory", "add_react", "check_if_owner"]
__all__ = ["colours", "cat", "emojis", "embed_factory", "error_embed_factory", "add_react", "check_if_owner"]
# --- Common values --- # --- Common values ---

View File

@ -16,7 +16,6 @@ NA2AAA: unassigned, no records
import discord.ext.commands as commands import discord.ext.commands as commands
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import aiohttp
import common as cmn import common as cmn
@ -24,6 +23,7 @@ import common as cmn
class AE7QCog(commands.Cog): class AE7QCog(commands.Cog):
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
self.session = bot.qrm.session
@commands.group(name="ae7q", aliases=["ae"], category=cmn.cat.lookup) @commands.group(name="ae7q", aliases=["ae"], category=cmn.cat.lookup)
async def _ae7q_lookup(self, ctx: commands.Context): async def _ae7q_lookup(self, ctx: commands.Context):
@ -37,11 +37,15 @@ class AE7QCog(commands.Cog):
callsign = callsign.upper() callsign = callsign.upper()
desc = '' desc = ''
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL=" base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
embed = cmn.embed_factory(ctx)
async with aiohttp.ClientSession() as session: async with self.session.get(base_url + callsign) as resp:
async with session.get(base_url + callsign) as resp:
if resp.status != 200: if resp.status != 200:
return await ctx.send('Could not load AE7Q') 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() page = await resp.text()
soup = BeautifulSoup(page, features="html.parser") soup = BeautifulSoup(page, features="html.parser")
@ -59,7 +63,6 @@ class AE7QCog(commands.Cog):
rows = None rows = None
if rows is None: if rows is None:
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q History for {callsign}" embed.title = f"AE7Q History for {callsign}"
embed.colour = cmn.colours.bad embed.colour = cmn.colours.bad
embed.url = f"{base_url}{callsign}" embed.url = f"{base_url}{callsign}"

View File

@ -99,10 +99,10 @@ class BaseCog(commands.Cog):
embed.title = "About qrm" embed.title = "About qrm"
embed.description = info.description embed.description = info.description
embed = embed.add_field(name="Authors", value=", ".join(info.authors)) embed.add_field(name="Authors", value=", ".join(info.authors))
embed = embed.add_field(name="License", value=info.license) embed.add_field(name="License", value=info.license)
embed = embed.add_field(name="Version", value=f'v{info.release}') embed.add_field(name="Version", value=f'v{info.release}')
embed = embed.add_field(name="Contributing", value=info.contributing, inline=False) embed.add_field(name="Contributing", value=info.contributing, inline=False)
embed.set_thumbnail(url=str(self.bot.user.avatar_url)) embed.set_thumbnail(url=str(self.bot.user.avatar_url))
await ctx.send(embed=embed) await ctx.send(embed=embed)

View File

@ -22,6 +22,7 @@ class ImageCog(commands.Cog):
self.bot = bot self.bot = bot
self.bandcharts = cmn.ImagesGroup(cmn.paths.bandcharts / "meta.json") self.bandcharts = cmn.ImagesGroup(cmn.paths.bandcharts / "meta.json")
self.maps = cmn.ImagesGroup(cmn.paths.maps / "meta.json") self.maps = cmn.ImagesGroup(cmn.paths.maps / "meta.json")
self.session = bot.qrm.session
@commands.command(name="bandplan", aliases=['plan', 'bands'], category=cmn.cat.ref) @commands.command(name="bandplan", aliases=['plan', 'bands'], category=cmn.cat.ref)
async def _bandplan(self, ctx: commands.Context, region: str = ''): async def _bandplan(self, ctx: commands.Context, region: str = ''):
@ -88,8 +89,7 @@ class ImageCog(commands.Cog):
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
embed.title = 'Current Greyline Conditions' embed.title = 'Current Greyline Conditions'
embed.colour = cmn.colours.good embed.colour = cmn.colours.good
async with aiohttp.ClientSession() as session: async with self.session.get(gl_url) as resp:
async with session.get(gl_url) as resp:
if resp.status != 200: if resp.status != 200:
embed.description = 'Could not download file...' embed.description = 'Could not download file...'
embed.colour = cmn.colours.bad embed.colour = cmn.colours.bad

View File

@ -62,6 +62,7 @@ class MorseCog(commands.Cog):
@commands.command(name="cwweight", aliases=["weight", 'cww'], category=cmn.cat.ref) @commands.command(name="cwweight", aliases=["weight", 'cww'], category=cmn.cat.ref)
async def _weight(self, ctx: commands.Context, *, msg: str): async def _weight(self, ctx: commands.Context, *, msg: str):
'''Calculates the CW Weight of a callsign or message.''' '''Calculates the CW Weight of a callsign or message.'''
embed = cmn.embed_factory(ctx)
with ctx.typing(): with ctx.typing():
msg = msg.upper() msg = msg.upper()
weight = 0 weight = 0
@ -70,13 +71,13 @@ class MorseCog(commands.Cog):
cw_char = self.ascii2morse[char].replace('-', '==') cw_char = self.ascii2morse[char].replace('-', '==')
weight += len(cw_char) * 2 + 2 weight += len(cw_char) * 2 + 2
except KeyError: except KeyError:
res = f'Unknown character {char} in callsign' embed.title = 'Error in calculation of CW weight'
await ctx.send(res) embed.description = f'Unknown character {char} in callsign'
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return return
res = f'The CW weight is **{weight}**'
embed = cmn.embed_factory(ctx)
embed.title = f'CW Weight of {msg}' embed.title = f'CW Weight of {msg}'
embed.description = res embed.description = f'The CW weight is **{weight}**'
embed.colour = cmn.colours.good embed.colour = cmn.colours.good
await ctx.send(embed=embed) await ctx.send(embed=embed)

View File

@ -21,7 +21,7 @@ import data.keys as keys
class QRZCog(commands.Cog): class QRZCog(commands.Cog):
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
self.session = aiohttp.ClientSession() self.session = bot.qrm.session
self._qrz_session_init.start() self._qrz_session_init.start()
@commands.command(name="call", aliases=["qrz"], category=cmn.cat.lookup) @commands.command(name="call", aliases=["qrz"], category=cmn.cat.lookup)
@ -139,6 +139,9 @@ def qrz_process_info(data: dict):
state = '' state = ''
address = data.get('addr1', '') + '\n' + data.get('addr2', '') + \ address = data.get('addr1', '') + '\n' + data.get('addr2', '') + \
state + ' ' + data.get('zip', '') state + ' ' + data.get('zip', '')
address = address.strip()
if address == '':
address = None
if 'eqsl' in data: if 'eqsl' in data:
eqsl = 'Yes' if data['eqsl'] == 1 else 'No' eqsl = 'Yes' if data['eqsl'] == 1 else 'No'
else: else:

View File

@ -12,8 +12,6 @@ import json
import discord.ext.commands as commands import discord.ext.commands as commands
import aiohttp
import common as cmn import common as cmn
@ -22,6 +20,7 @@ class StudyCog(commands.Cog):
self.bot = bot self.bot = bot
self.lastq = dict() self.lastq = dict()
self.source = 'Data courtesy of [HamStudy.org](https://hamstudy.org/)' self.source = 'Data courtesy of [HamStudy.org](https://hamstudy.org/)'
self.session = bot.qrm.session
@commands.command(name="hamstudy", aliases=['rq', 'randomquestion', 'randomq'], category=cmn.cat.study) @commands.command(name="hamstudy", aliases=['rq', 'randomquestion', 'randomq'], category=cmn.cat.study)
async def _random_question(self, ctx: commands.Context, level: str = None): async def _random_question(self, ctx: commands.Context, level: str = None):
@ -30,6 +29,7 @@ class StudyCog(commands.Cog):
gen_pool = 'E3_2019' gen_pool = 'E3_2019'
extra_pool = 'E4_2016' extra_pool = 'E4_2016'
embed = cmn.embed_factory(ctx)
with ctx.typing(): with ctx.typing():
selected_pool = None selected_pool = None
try: try:
@ -49,15 +49,21 @@ class StudyCog(commands.Cog):
if (level is None) or (level == 'all'): # no pool given or user wants all, so pick a random pool 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]) 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 if (level is not None) and (selected_pool is None): # unrecognized pool given by user
await ctx.send('The question pool you gave was unrecognized. ' + embed.title = 'Error in HamStudy command'
'There are many ways to call up certain question pools - try ?rq t, g, or e. ' + embed.description = ('The question pool you gave was unrecognized. '
'(Note that only the US question pools are available).') '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.')
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return return
async with aiohttp.ClientSession() as session: async with self.session.get(f'https://hamstudy.org/pools/{selected_pool}') as resp:
async with session.get(f'https://hamstudy.org/pools/{selected_pool}') as resp:
if resp.status != 200: if resp.status != 200:
return await ctx.send('Could not load questions...') embed.title = 'Error in HamStudy command'
embed.description = 'Could not load questions'
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
pool = json.loads(await resp.read())['pool'] pool = json.loads(await resp.read())['pool']
# Select a question # Select a question
@ -65,20 +71,19 @@ class StudyCog(commands.Cog):
pool_questions = random.choice(pool_section)['questions'] pool_questions = random.choice(pool_section)['questions']
question = random.choice(pool_questions) question = random.choice(pool_questions)
embed = cmn.embed_factory(ctx)
embed.title = question['id'] embed.title = question['id']
embed.description = self.source embed.description = self.source
embed.colour = cmn.colours.good embed.colour = cmn.colours.good
embed = embed.add_field(name='Question:', value=question['text'], inline=False) embed.add_field(name='Question:', value=question['text'], inline=False)
embed = embed.add_field(name='Answers:', value='**A:** ' + question['answers']['A'] + embed.add_field(name='Answers:', value='**A:** ' + question['answers']['A'] +
'\n**B:** ' + question['answers']['B'] + '\n**B:** ' + question['answers']['B'] +
'\n**C:** ' + question['answers']['C'] + '\n**C:** ' + question['answers']['C'] +
'\n**D:** ' + question['answers']['D'], '\n**D:** ' + question['answers']['D'],
inline=False) inline=False)
embed = embed.add_field(name='Answer:', value='Type _?rqa_ for answer', inline=False) embed.add_field(name='Answer:', value='Type _?rqa_ for answer', inline=False)
if 'image' in question: 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/{selected_pool.split("_",1)[1]}/{question["image"]}'
embed = embed.set_image(url=image_url) embed.set_image(url=image_url)
self.lastq[ctx.message.channel.id] = (question['id'], question['answer']) self.lastq[ctx.message.channel.id] = (question['id'], question['answer'])
await ctx.send(embed=embed) await ctx.send(embed=embed)

View File

@ -13,8 +13,6 @@ import re
import discord import discord
import discord.ext.commands as commands import discord.ext.commands as commands
import aiohttp
import common as cmn import common as cmn
@ -23,6 +21,7 @@ class WeatherCog(commands.Cog):
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
self.session = bot.qrm.session
@commands.command(name="bandconditions", aliases=['cond', 'condx', 'conditions'], category=cmn.cat.weather) @commands.command(name="bandconditions", aliases=['cond', 'condx', 'conditions'], category=cmn.cat.weather)
async def _band_conditions(self, ctx: commands.Context): async def _band_conditions(self, ctx: commands.Context):
@ -31,8 +30,7 @@ class WeatherCog(commands.Cog):
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
embed.title = 'Current Solar Conditions' embed.title = 'Current Solar Conditions'
embed.colour = cmn.colours.good embed.colour = cmn.colours.good
async with aiohttp.ClientSession() as session: async with self.session.get('http://www.hamqsl.com/solarsun.php') as resp:
async with session.get('http://www.hamqsl.com/solarsun.php') as resp:
if resp.status != 200: if resp.status != 200:
embed.description = 'Could not download file...' embed.description = 'Could not download file...'
embed.colour = cmn.colours.bad embed.colour = cmn.colours.bad
@ -81,8 +79,7 @@ See help for weather command for possible location types. Add a `-c` or `-f` to
embed.colour = cmn.colours.good embed.colour = cmn.colours.good
loc = loc.replace(' ', '+') loc = loc.replace(' ', '+')
async with aiohttp.ClientSession() as session: async with self.session.get(f'http://wttr.in/{loc}_{units}pnFQ.png') as resp:
async with session.get(f'http://wttr.in/{loc}_{units}pnFQ.png') as resp:
if resp.status != 200: if resp.status != 200:
embed.description = 'Could not download file...' embed.description = 'Could not download file...'
embed.colour = cmn.colours.bad embed.colour = cmn.colours.bad
@ -115,8 +112,7 @@ See help for weather command for possible location types. Add a `-c` or `-f` to
embed.colour = cmn.colours.good embed.colour = cmn.colours.good
loc = loc.replace(' ', '+') loc = loc.replace(' ', '+')
async with aiohttp.ClientSession() as session: async with self.session.get(f'http://wttr.in/{loc}_0{units}pnFQ.png') as resp:
async with session.get(f'http://wttr.in/{loc}_0{units}pnFQ.png') as resp:
if resp.status != 200: if resp.status != 200:
embed.description = 'Could not download file...' embed.description = 'Could not download file...'
embed.colour = cmn.colours.bad embed.colour = cmn.colours.bad

66
main.py
View File

@ -8,12 +8,18 @@ This file is part of discord-qrm2 and is released under the terms of the GNU
General Public License, version 2. General Public License, version 2.
""" """
from datetime import time, datetime
import random
from types import SimpleNamespace
import pytz
import aiohttp
import discord import discord
from discord.ext import commands, tasks from discord.ext import commands, tasks
import common as cmn import common as cmn
import info import info
import data.options as opt import data.options as opt
import data.keys as keys import data.keys as keys
@ -33,6 +39,9 @@ bot = commands.Bot(command_prefix=opt.prefix,
description=info.description, description=info.description,
help_command=commands.MinimalHelpCommand()) help_command=commands.MinimalHelpCommand())
bot.qrm = SimpleNamespace()
bot.qrm.session = aiohttp.ClientSession(headers={'User-Agent': f'discord-qrm2/{info.release}'})
# --- Commands --- # --- Commands ---
@ -40,6 +49,7 @@ bot = commands.Bot(command_prefix=opt.prefix,
@commands.check(cmn.check_if_owner) @commands.check(cmn.check_if_owner)
async def _restart_bot(ctx: commands.Context): async def _restart_bot(ctx: commands.Context):
"""Restarts the bot.""" """Restarts the bot."""
await bot.qrm.session.close()
global exit_code global exit_code
await cmn.add_react(ctx.message, cmn.emojis.good) await cmn.add_react(ctx.message, cmn.emojis.good)
print(f"[**] Restarting! Requested by {ctx.author}.") print(f"[**] Restarting! Requested by {ctx.author}.")
@ -51,6 +61,7 @@ async def _restart_bot(ctx: commands.Context):
@commands.check(cmn.check_if_owner) @commands.check(cmn.check_if_owner)
async def _shutdown_bot(ctx: commands.Context): async def _shutdown_bot(ctx: commands.Context):
"""Shuts down the bot.""" """Shuts down the bot."""
await bot.qrm.session.close()
global exit_code global exit_code
await cmn.add_react(ctx.message, cmn.emojis.good) await cmn.add_react(ctx.message, cmn.emojis.good)
print(f"[**] Shutting down! Requested by {ctx.author}.") print(f"[**] Shutting down! Requested by {ctx.author}.")
@ -117,18 +128,59 @@ async def _extctl_unload(ctx: commands.Context, extension: str):
async def on_ready(): async def on_ready():
print(f"Logged in as: {bot.user} - {bot.user.id}") print(f"Logged in as: {bot.user} - {bot.user.id}")
print("------") print("------")
if opt.status_mode == "time":
_ensure_activity_time.start()
elif opt.status_mode == "random":
_ensure_activity_random.start()
else:
_ensure_activity_fixed.start()
@bot.event
async def on_message(message):
msg = message.content.lower()
for emoji, keywords in opt.msg_reacts.items():
if any([keyword in msg for keyword in keywords]):
await message.add_reaction(discord.utils.find(lambda x: x.id == emoji, bot.emojis))
await bot.process_commands(message)
# --- Tasks --- # --- Tasks ---
@tasks.loop(minutes=5) @tasks.loop(minutes=5)
async def _ensure_activity(): async def _ensure_activity_time():
await bot.change_presence(activity=discord.Game(name=opt.game)) status = opt.statuses[0]
try:
tz = pytz.timezone(opt.status_tz)
except pytz.exceptions.UnknownTimeZoneError:
await bot.change_presence(activity=discord.Game(name="with invalid timezones."))
return
now = datetime.now(tz=tz).time()
for sts in opt.time_statuses:
start_time = time(hour=sts[1][0], minute=sts[1][1], tzinfo=tz)
end_time = time(hour=sts[2][0], minute=sts[2][1], tzinfo=tz)
if start_time < now <= end_time:
status = sts[0]
await bot.change_presence(activity=discord.Game(name=status))
@_ensure_activity.before_loop @tasks.loop(minutes=5)
async def _before_ensure_activity(): async def _ensure_activity_random():
await bot.wait_until_ready() status = random.choice(opt.statuses)
await bot.change_presence(activity=discord.Game(name=status))
@tasks.loop(minutes=5)
async def _ensure_activity_fixed():
status = opt.statuses[0]
await bot.change_presence(activity=discord.Game(name=status))
# --- Run --- # --- Run ---
@ -136,8 +188,6 @@ async def _before_ensure_activity():
for ext in opt.exts: for ext in opt.exts:
bot.load_extension(ext_dir + '.' + ext) bot.load_extension(ext_dir + '.' + ext)
_ensure_activity.start()
try: try:
bot.run(keys.discord_token) bot.run(keys.discord_token)

View File

@ -2,3 +2,4 @@ discord.py
ctyparser ctyparser
beautifulsoup4 beautifulsoup4
lxml lxml
pytz

View File

@ -29,8 +29,26 @@ owners_uids = (200102491231092736,)
# The extensions to load when running the bot. # The extensions to load when running the bot.
exts = ['ae7q', 'base', 'fun', 'grid', 'ham', 'image', 'lookup', 'morse', 'qrz', 'study', 'weather'] exts = ['ae7q', 'base', 'fun', 'grid', 'ham', 'image', 'lookup', 'morse', 'qrz', 'study', 'weather']
# The text to put in the "playing" status. # Either "time", "random", or "fixed" (first item in statuses)
game = 'with lids on 7.200' status_mode = "fixed"
# Random statuses pool
statuses = ["with lids on the air", "with fire"]
# Timezone for the status (string)
# See https://pythonhosted.org/pytz/ for more info
status_tz = 'US/Eastern'
# The text to put in the "playing" status, with start and stop times
time_statuses = [('with lids on 3.840', (00, 00), (6, 00)),
('with lids on 7.200', (6, 00), (10, 00)),
('with lids on 14.313', (10, 00), (18, 00)),
('with lids on 7.200', (18, 00), (20, 00)),
('with lids on 3.840', (20, 00), (23, 59))]
# Emoji IDs and keywords for emoji reactions
# Use the format {emoji_id (int): ('tuple', 'of', 'lowercase', 'keywords')}
msg_reacts = {}
# A :pika: emote's ID, None for no emote :c # A :pika: emote's ID, None for no emote :c
pika = 658733876176355338 pika = 658733876176355338