convert qrzcog to use qrztools

fixes #58
fixes #351
This commit is contained in:
Abigail G 2021-03-16 22:04:05 -04:00
parent 956fc4b02f
commit a7b4203112
No known key found for this signature in database
GPG Key ID: 6BE0755918A4C7F5
4 changed files with 88 additions and 142 deletions

View File

@ -10,17 +10,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Commands to show METAR (`?metar`) and TAF (`?taf`) (aeronautical weather conditions). - Commands to show METAR (`?metar`) and TAF (`?taf`) (aeronautical weather conditions).
- The ability to select an element of a pool in `?hamstudy`. - The ability to select an element of a pool in `?hamstudy`.
- The ability to answer ❓ to a HamStudy question to get the answer. - The ability to answer ❓ to a HamStudy question to get the answer.
- Configuration options to disable showing the `?invite` and set default invite permissions (enabled by default).
- Configuration option to show QRZ nickname in place of first name (enabled by default).
### Changed ### Changed
- New colour theme for `?greyline`. - New colour theme for `?greyline`.
- Moved great circle distance and bearing calculation from `?ungrid` to `?griddistance`. - Moved great circle distance and bearing calculation from `?ungrid` to `?griddistance`.
- `?ungrid` to `?latlong`. - `?ungrid` to `?latlong`.
- Renamed `?cond` to `?solar`. - Renamed `?cond` to `?solar`.
- Reduced `?hamstudy` timeout to 5 minutes. - Reduced `?hamstudy` timeout to 5 minutes.
- Library used for QRZ lookups.
### Fixed ### Fixed
- Weird image caching situation for `?greyline` on Discord's side. - Weird image caching situation for `?greyline` on Discord's side.
### Deprecated ### Deprecated
- `?ungrid`. - `?ungrid`.
- Deprecated old `?solar` aliases (`?cond`, etc). - Deprecated old `?solar` aliases (`?cond`, etc).
- Deprecated old `?call` alias (`?qrz`).
## [2.5.1] - 2020-12-10 ## [2.5.1] - 2020-12-10

View File

@ -8,182 +8,120 @@ the GNU General Public License, version 2.
""" """
from io import BytesIO from typing import Dict
from datetime import datetime
import aiohttp import aiohttp
from lxml import etree from qrztools import qrztools, QrzAsync, QrzError
from gridtools import Grid, LatLong
from discord.ext import commands, tasks from discord.ext import commands
import common as cmn import common as cmn
import data.options as opt
import data.keys as keys 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(connector=bot.qrm.connector) self.qrz = None
self._qrz_session_init.start() try:
if keys.qrz_user and keys.qrz_pass:
self.qrz = QrzAsync(keys.qrz_user, keys.qrz_pass, useragent="discord-qrm2",
session=aiohttp.ClientSession(connector=bot.qrm.connector))
# seed the qrz object with the previous session key, in case it already works
try:
with open("data/qrz_session") as qrz_file:
self.qrz.session_key = qrz_file.readline().strip()
except FileNotFoundError:
pass
except AttributeError:
pass
@commands.command(name="call", aliases=["qrz"], category=cmn.cat.lookup) @commands.command(name="call", aliases=["qrz"], category=cmn.cat.lookup)
async def _qrz_lookup(self, ctx: commands.Context, callsign: str, *flags): async def _qrz_lookup(self, ctx: commands.Context, callsign: str, *flags):
"""Looks up a callsign on [QRZ.com](https://www.qrz.com/). Add `--link` to only link the QRZ page.""" """Looks up a callsign on [QRZ.com](https://www.qrz.com/). Add `--link` to only link the QRZ page."""
flags = [f.lower() for f in flags] flags = [f.lower() for f in flags]
if not callsign.isalnum(): if self.qrz is None or "--link" in flags:
embed = cmn.embed_factory(ctx) if ctx.invoked_with == "qrz":
embed.title = "QRZ Data for Callsign" await ctx.send("⚠️ **Deprecated Command Alias**\n"
embed.colour = cmn.colours.bad f"This command has been renamed to `{ctx.prefix}call`!\n"
embed.description = "Not a valid callsign!" "This alias will be removed in the next version.")
await ctx.send(embed=embed)
return
if keys.qrz_user == "" or keys.qrz_pass == "" or "--link" in flags:
await ctx.send(f"http://qrz.com/db/{callsign}") await ctx.send(f"http://qrz.com/db/{callsign}")
return return
embed = cmn.embed_factory(ctx)
embed.title = f"QRZ Data for {callsign.upper()}"
if ctx.invoked_with == "qrz":
embed.description = ("⚠️ **Deprecated Command Alias**\n"
f"This command has been renamed to `{ctx.prefix}call`!\n"
"This alias will be removed in the next version.")
async with ctx.typing(): async with ctx.typing():
try: try:
await qrz_test_session(self.key, self.session) data = await self.qrz.get_callsign(callsign)
except ConnectionError: except QrzError as e:
await self.get_session() embed.colour = cmn.colours.bad
embed.description = str(e)
await ctx.send(embed=embed)
return
url = f"http://xmldata.qrz.com/xml/current/?s={self.key};callsign={callsign}" embed.title = f"QRZ Data for {data.call}"
async with self.session.get(url) as resp:
if resp.status != 200:
raise ConnectionError(f"Unable to connect to QRZ (HTTP Error {resp.status})")
with BytesIO(await resp.read()) as resp_file:
resp_xml = etree.parse(resp_file).getroot()
resp_xml_session = resp_xml.xpath("/x:QRZDatabase/x:Session", namespaces={"x": "http://xmldata.qrz.com"})
resp_session = {el.tag.split("}")[1]: el.text for el in resp_xml_session[0].getiterator()}
if "Error" in resp_session:
if "Session Timeout" in resp_session["Error"]:
await self.get_session()
await self._qrz_lookup(ctx, callsign)
return
if "Not found" in resp_session["Error"]:
embed = cmn.embed_factory(ctx)
embed.title = f"QRZ Data for {callsign.upper()}"
embed.colour = cmn.colours.bad
embed.description = "No data found!"
await ctx.send(embed=embed)
return
raise ValueError(resp_session["Error"])
resp_xml_data = resp_xml.xpath("/x:QRZDatabase/x:Callsign", namespaces={"x": "http://xmldata.qrz.com"})
resp_data = {el.tag.split("}")[1]: el.text for el in resp_xml_data[0].getiterator()}
embed = cmn.embed_factory(ctx)
embed.title = f"QRZ Data for {resp_data['call']}"
embed.colour = cmn.colours.good embed.colour = cmn.colours.good
embed.url = f"http://www.qrz.com/db/{resp_data['call']}" embed.url = data.url
if "image" in resp_data: if data.image != qrztools.QrzImage():
embed.set_thumbnail(url=resp_data["image"]) embed.set_thumbnail(url=data.image.url)
data = qrz_process_info(resp_data) for title, val in qrz_process_info(data).items():
for title, val in data.items():
if val is not None: if val is not None:
embed.add_field(name=title, value=val, inline=True) embed.add_field(name=title, value=val, inline=True)
await ctx.send(embed=embed) await ctx.send(embed=embed)
async def get_session(self):
"""Session creation and caching."""
self.key = await qrz_login(keys.qrz_user, keys.qrz_pass, self.session)
with open("data/qrz_session", "w") as qrz_file:
qrz_file.write(self.key)
@tasks.loop(count=1) def qrz_process_info(data: qrztools.QrzCallsignData) -> Dict:
async def _qrz_session_init(self): if data.name != qrztools.Name():
"""Helper task to allow obtaining a session at cog instantiation.""" if opt.qrz_only_nickname:
try: if data.name.nickname:
with open("data/qrz_session") as qrz_file: name = data.name.nickname + " " + data.name.name
self.key = qrz_file.readline().strip() elif data.name.first:
await qrz_test_session(self.key, self.session) name = data.name.first + " " + data.name.name
except (FileNotFoundError, ConnectionError): else:
await self.get_session() name = data.name.name
async def qrz_login(user: str, passwd: str, session: aiohttp.ClientSession):
url = f"http://xmldata.qrz.com/xml/current/?username={user};password={passwd};agent=discord-qrm2"
async with session.get(url) as resp:
if resp.status != 200:
raise ConnectionError(f"Unable to connect to QRZ (HTTP Error {resp.status})")
with BytesIO(await resp.read()) as resp_file:
resp_xml = etree.parse(resp_file).getroot()
resp_xml_session = resp_xml.xpath("/x:QRZDatabase/x:Session", namespaces={"x": "http://xmldata.qrz.com"})
resp_session = {el.tag.split("}")[1]: el.text for el in resp_xml_session[0].getiterator()}
if "Error" in resp_session:
raise ConnectionError(resp_session["Error"])
if resp_session["SubExp"] == "non-subscriber":
raise ConnectionError("Invalid QRZ Subscription")
return resp_session["Key"]
async def qrz_test_session(key: str, session: aiohttp.ClientSession):
url = f"http://xmldata.qrz.com/xml/current/?s={key}"
async with session.get(url) as resp:
if resp.status != 200:
raise ConnectionError(f"Unable to connect to QRZ (HTTP Error {resp.status})")
with BytesIO(await resp.read()) as resp_file:
resp_xml = etree.parse(resp_file).getroot()
resp_xml_session = resp_xml.xpath("/x:QRZDatabase/x:Session", namespaces={"x": "http://xmldata.qrz.com"})
resp_session = {el.tag.split("}")[1]: el.text for el in resp_xml_session[0].getiterator()}
if "Error" in resp_session:
raise ConnectionError(resp_session["Error"])
def qrz_process_info(data: dict):
if "name" in data:
if "fname" in data:
name = data["fname"] + " " + data["name"]
else: else:
name = data["name"] name = data.name.formatted_name
else: else:
name = None name = None
if "state" in data:
state = f", {data['state']}"
else:
state = ""
address = data.get("addr1", "") + "\n" + data.get("addr2", "") + state + " " + data.get("zip", "")
address = address.strip()
if address == "":
address = None
if "eqsl" in data:
eqsl = "Yes" if data["eqsl"] == "1" else "No"
else:
eqsl = "Unknown"
if "mqsl" in data:
mqsl = "Yes" if data["mqsl"] == "1" else "No"
else:
mqsl = "Unknown"
if "lotw" in data:
lotw = "Yes" if data["lotw"] == "1" else "No"
else:
lotw = "Unknown"
return {"Name": name, if data.address != qrztools.Address():
"Country": data.get("country", None), state = ", " + data.address.state + " " if data.address.state else ""
"Address": address, address = "\n".join([data.address.attn, data.address.line1, data.address.line2 + state, data.address.zip])
"Grid Square": data.get("grid", None), else:
"County": data.get("county", None), address = None
"CQ Zone": data.get("cqzone", None),
"ITU Zone": data.get("ituzone", None), return {
"IOTA Designator": data.get("iota", None), "Name": name,
"Expires": data.get("expdate", None), "Country": data.address.country,
"Aliases": data.get("aliases", None), "Address": address,
"Previous Callsign": data.get("p_call", None), "Grid Square": data.grid if data.grid != Grid(LatLong(0, 0)) else None,
"License Class": data.get("class", None), "County": data.county if data.county else None,
"Trustee": data.get("trustee", None), "CQ Zone": data.cq_zone if data.cq_zone else None,
"eQSL?": eqsl, "ITU Zone": data.itu_zone if data.itu_zone else None,
"Paper QSL?": mqsl, "IOTA Designator": data.iota if data.iota else None,
"LotW?": lotw, "Expires": f"{data.expire_date:%Y-%m-%d}" if data.expire_date != datetime.min else None,
"QSL Info": data.get("qslmgr", None), "Aliases": ", ".join(data.aliases) if data.aliases else None,
"Born": data.get("born", None)} "Previous Callsign": data.prev_call if data.prev_call else None,
"License Class": data.lic_class if data.lic_class else None,
"Trustee": data.trustee if data.trustee else None,
"eQSL?": "Yes" if data.eqsl else "No",
"Paper QSL?": "Yes" if data.mail_qsl else "No",
"LotW?": "Yes" if data.lotw_qsl else "No",
"QSL Info": data.qsl_manager if data.qsl_manager else None,
"Born": f"{data.born:%Y-%m-%d}" if data.born != datetime.min else None
}
def setup(bot): def setup(bot):

View File

@ -1,7 +1,7 @@
discord.py~=1.5.0 discord.py~=1.5.0
ctyparser~=2.0 ctyparser~=2.0
gridtools~=1.0 gridtools~=1.0
qrztools[async]~=1.0
beautifulsoup4 beautifulsoup4
lxml
pytz pytz
cairosvg cairosvg

View File

@ -46,6 +46,10 @@ exts = [
"propagation", "propagation",
] ]
# If True (default): when doing QRZ callsign lookups, show the nickname in place of the first name, if it exists
# if False: use QRZ's default name format
qrz_only_nickname = True
# Either "time", "random", or "fixed" (first item in statuses) # Either "time", "random", or "fixed" (first item in statuses)
status_mode = "fixed" status_mode = "fixed"