From a7b4203112fefc78a375a40639f490c40d559de4 Mon Sep 17 00:00:00 2001 From: Abigail G Date: Tue, 16 Mar 2021 22:04:05 -0400 Subject: [PATCH] convert qrzcog to use qrztools fixes #58 fixes #351 --- CHANGELOG.md | 4 + exts/qrz.py | 220 ++++++++++++++------------------------ requirements.txt | 2 +- templates/data/options.py | 4 + 4 files changed, 88 insertions(+), 142 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b40efdf..df3bf89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). - The ability to select an element of a pool in `?hamstudy`. - 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 - New colour theme for `?greyline`. - Moved great circle distance and bearing calculation from `?ungrid` to `?griddistance`. - `?ungrid` to `?latlong`. - Renamed `?cond` to `?solar`. - Reduced `?hamstudy` timeout to 5 minutes. +- Library used for QRZ lookups. ### Fixed - Weird image caching situation for `?greyline` on Discord's side. ### Deprecated - `?ungrid`. - Deprecated old `?solar` aliases (`?cond`, etc). +- Deprecated old `?call` alias (`?qrz`). ## [2.5.1] - 2020-12-10 diff --git a/exts/qrz.py b/exts/qrz.py index 169635e..135729a 100644 --- a/exts/qrz.py +++ b/exts/qrz.py @@ -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 -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 data.options as opt import data.keys as keys class QRZCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.session = aiohttp.ClientSession(connector=bot.qrm.connector) - self._qrz_session_init.start() + self.qrz = None + 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) 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.""" flags = [f.lower() for f in flags] - if not callsign.isalnum(): - embed = cmn.embed_factory(ctx) - embed.title = "QRZ Data for Callsign" - embed.colour = cmn.colours.bad - embed.description = "Not a valid callsign!" - await ctx.send(embed=embed) - return - - if keys.qrz_user == "" or keys.qrz_pass == "" or "--link" in flags: + if self.qrz is None or "--link" in flags: + if ctx.invoked_with == "qrz": + await ctx.send("⚠️ **Deprecated Command Alias**\n" + f"This command has been renamed to `{ctx.prefix}call`!\n" + "This alias will be removed in the next version.") await ctx.send(f"http://qrz.com/db/{callsign}") 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(): try: - await qrz_test_session(self.key, self.session) - except ConnectionError: - await self.get_session() + data = await self.qrz.get_callsign(callsign) + except QrzError as e: + 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}" - 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.title = f"QRZ Data for {data.call}" embed.colour = cmn.colours.good - embed.url = f"http://www.qrz.com/db/{resp_data['call']}" - if "image" in resp_data: - embed.set_thumbnail(url=resp_data["image"]) + embed.url = data.url + if data.image != qrztools.QrzImage(): + embed.set_thumbnail(url=data.image.url) - data = qrz_process_info(resp_data) - - for title, val in data.items(): + for title, val in qrz_process_info(data).items(): if val is not None: embed.add_field(name=title, value=val, inline=True) 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) - async def _qrz_session_init(self): - """Helper task to allow obtaining a session at cog instantiation.""" - try: - with open("data/qrz_session") as qrz_file: - self.key = qrz_file.readline().strip() - await qrz_test_session(self.key, self.session) - except (FileNotFoundError, ConnectionError): - await self.get_session() - - -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"] +def qrz_process_info(data: qrztools.QrzCallsignData) -> Dict: + if data.name != qrztools.Name(): + if opt.qrz_only_nickname: + if data.name.nickname: + name = data.name.nickname + " " + data.name.name + elif data.name.first: + name = data.name.first + " " + data.name.name + else: + name = data.name.name else: - name = data["name"] + name = data.name.formatted_name else: 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, - "Country": data.get("country", None), - "Address": address, - "Grid Square": data.get("grid", None), - "County": data.get("county", None), - "CQ Zone": data.get("cqzone", None), - "ITU Zone": data.get("ituzone", None), - "IOTA Designator": data.get("iota", None), - "Expires": data.get("expdate", None), - "Aliases": data.get("aliases", None), - "Previous Callsign": data.get("p_call", None), - "License Class": data.get("class", None), - "Trustee": data.get("trustee", None), - "eQSL?": eqsl, - "Paper QSL?": mqsl, - "LotW?": lotw, - "QSL Info": data.get("qslmgr", None), - "Born": data.get("born", None)} + if data.address != qrztools.Address(): + state = ", " + data.address.state + " " if data.address.state else "" + address = "\n".join([data.address.attn, data.address.line1, data.address.line2 + state, data.address.zip]) + else: + address = None + + return { + "Name": name, + "Country": data.address.country, + "Address": address, + "Grid Square": data.grid if data.grid != Grid(LatLong(0, 0)) else None, + "County": data.county if data.county else None, + "CQ Zone": data.cq_zone if data.cq_zone else None, + "ITU Zone": data.itu_zone if data.itu_zone else None, + "IOTA Designator": data.iota if data.iota else None, + "Expires": f"{data.expire_date:%Y-%m-%d}" if data.expire_date != datetime.min else None, + "Aliases": ", ".join(data.aliases) if data.aliases else 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): diff --git a/requirements.txt b/requirements.txt index b5f62b8..80d0097 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ discord.py~=1.5.0 ctyparser~=2.0 gridtools~=1.0 +qrztools[async]~=1.0 beautifulsoup4 -lxml pytz cairosvg diff --git a/templates/data/options.py b/templates/data/options.py index e4d66f7..18ee38e 100644 --- a/templates/data/options.py +++ b/templates/data/options.py @@ -46,6 +46,10 @@ exts = [ "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) status_mode = "fixed"