mirror of
				https://github.com/miaowware/qrm2.git
				synced 2025-11-03 19:50:20 -05:00 
			
		
		
		
	Merge pull request #359 from miaowware/qrztools-integration
convert qrzcog to use qrztools
This commit is contained in:
		
						commit
						bc2515c9fc
					
				@ -13,18 +13,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 | 
				
			|||||||
- The list of available prefixes to `?help` when there is more than one.
 | 
					- The list of available prefixes to `?help` when there is more than one.
 | 
				
			||||||
- `?donate` command to show ways to support qrm's development.
 | 
					- `?donate` command to show ways to support qrm's development.
 | 
				
			||||||
- `?invite` command to invite qrm to your server.
 | 
					- `?invite` command to invite qrm to your server.
 | 
				
			||||||
 | 
					- 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.
 | 
				
			||||||
- The help command was not using the prefix it was invoked with.
 | 
					- The help command was not using the prefix it was invoked with.
 | 
				
			||||||
### 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
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										212
									
								
								exts/qrz.py
									
									
									
									
									
								
							
							
						
						
									
										212
									
								
								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
 | 
					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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        async with ctx.typing():
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                await qrz_test_session(self.key, self.session)
 | 
					 | 
				
			||||||
            except ConnectionError:
 | 
					 | 
				
			||||||
                await self.get_session()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            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 = cmn.embed_factory(ctx)
 | 
				
			||||||
        embed.title = f"QRZ Data for {callsign.upper()}"
 | 
					        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:
 | 
				
			||||||
 | 
					                data = await self.qrz.get_callsign(callsign)
 | 
				
			||||||
 | 
					            except QrzError as e:
 | 
				
			||||||
                embed.colour = cmn.colours.bad
 | 
					                embed.colour = cmn.colours.bad
 | 
				
			||||||
                    embed.description = "No data found!"
 | 
					                embed.description = str(e)
 | 
				
			||||||
                await ctx.send(embed=embed)
 | 
					                await ctx.send(embed=embed)
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
                raise ValueError(resp_session["Error"])
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            resp_xml_data = resp_xml.xpath("/x:QRZDatabase/x:Callsign", namespaces={"x": "http://xmldata.qrz.com"})
 | 
					            embed.title = f"QRZ Data for {data.call}"
 | 
				
			||||||
            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):
 | 
					 | 
				
			||||||
            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"]
 | 
					 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
            name = data["name"]
 | 
					                name = data.name.name
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            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 = "\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,
 | 
					        "Address": address,
 | 
				
			||||||
            "Grid Square": data.get("grid", None),
 | 
					        "Grid Square": data.grid if data.grid != Grid(LatLong(0, 0)) else None,
 | 
				
			||||||
            "County": data.get("county", None),
 | 
					        "County": data.county if data.county else None,
 | 
				
			||||||
            "CQ Zone": data.get("cqzone", None),
 | 
					        "CQ Zone": data.cq_zone if data.cq_zone else None,
 | 
				
			||||||
            "ITU Zone": data.get("ituzone", None),
 | 
					        "ITU Zone": data.itu_zone if data.itu_zone else None,
 | 
				
			||||||
            "IOTA Designator": data.get("iota", None),
 | 
					        "IOTA Designator": data.iota if data.iota else None,
 | 
				
			||||||
            "Expires": data.get("expdate", None),
 | 
					        "Expires": f"{data.expire_date:%Y-%m-%d}" if data.expire_date != datetime.min else None,
 | 
				
			||||||
            "Aliases": data.get("aliases", None),
 | 
					        "Aliases": ", ".join(data.aliases) if data.aliases else None,
 | 
				
			||||||
            "Previous Callsign": data.get("p_call", None),
 | 
					        "Previous Callsign": data.prev_call if data.prev_call else None,
 | 
				
			||||||
            "License Class": data.get("class", None),
 | 
					        "License Class": data.lic_class if data.lic_class else None,
 | 
				
			||||||
            "Trustee": data.get("trustee", None),
 | 
					        "Trustee": data.trustee if data.trustee else None,
 | 
				
			||||||
            "eQSL?": eqsl,
 | 
					        "eQSL?": "Yes" if data.eqsl else "No",
 | 
				
			||||||
            "Paper QSL?": mqsl,
 | 
					        "Paper QSL?": "Yes" if data.mail_qsl else "No",
 | 
				
			||||||
            "LotW?": lotw,
 | 
					        "LotW?": "Yes" if data.lotw_qsl else "No",
 | 
				
			||||||
            "QSL Info": data.get("qslmgr", None),
 | 
					        "QSL Info": data.qsl_manager if data.qsl_manager else None,
 | 
				
			||||||
            "Born": data.get("born", None)}
 | 
					        "Born": f"{data.born:%Y-%m-%d}" if data.born != datetime.min else None
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def setup(bot):
 | 
					def setup(bot):
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# enable a command that provides a link to add the bot to a server
 | 
					# enable a command that provides a link to add the bot to a server
 | 
				
			||||||
enable_invite_cmd = True
 | 
					enable_invite_cmd = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user