11 Commits

Author SHA1 Message Date
Abigail fed97a03b3 update ae7q extension to use ae7qparser
Fixes #195
Fixes #186
Fixes #168
2020-04-22 17:58:27 -04:00
Abigail 5f796d479e bump version to 2.3.1 2020-04-02 23:04:29 -04:00
Abigail Gold 3ba55d4c35 update funetics words list (#218)
Fixes #217
2020-04-02 23:02:19 -04:00
Abigail f4ed93dc76 bump version to 2.3.0 2020-03-30 19:00:38 -04:00
Abigail Gold 2cb4b03532 add phonetic weight command (#215)
Fixes #170

Co-authored-by: 0x5c <dev@0x5c.io>
2020-03-30 18:56:29 -04:00
Abigail Gold bc93462c29 convert all OrderedDicts to dictionaries (#214)
Fixes #184

Co-authored-by: 0x5c <dev@0x5c.io>
2020-03-30 18:54:33 -04:00
Abigail Gold 6867c45c8c add ?standards command for xkcd 927 (#213)
Fixes #187
2020-03-30 18:24:50 -04:00
Abigail dcbb7acab8 bump release to 2.2.3 2020-03-29 12:34:58 -04:00
Abigail Gold 3803ce6045 add git commit hash to file "git_commit" for automatically-built docker images (#211) 2020-03-29 12:34:12 -04:00
Abigail Gold 8ca4911072 make commands case-insensitive (#210)
Fixes #209
2020-03-28 19:07:23 -04:00
Abigail Gold 6e2468f04f create github action for docker image creation and publication (#206) 2020-03-28 18:18:29 -04:00
15 changed files with 687 additions and 44591 deletions
+58
View File
@@ -0,0 +1,58 @@
name: Docker Build and Push
on:
push:
# Publish `master` as Docker `dev` image.
branches:
- master
# Publish `v*` tags as releases and as `latest`.
tags:
- v*
env:
IMAGE_NAME: qrm2
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build image
run: |
echo ${{ github.sha }} > git_commit
docker build . --file Dockerfile -t $IMAGE_NAME
- name: Log into Github Package Registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin
- name: Log into Docker Hub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
- name: Push image to registries
run: |
GITHUB_IMAGE_ID=docker.pkg.github.com/${{ github.repository }}/$IMAGE_NAME
DOCKER_IMAGE_ID=${{ secrets.DOCKER_USERNAME }}/$IMAGE_NAME
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
[[ "$VERSION" == "master" ]] && VERSION=dev
echo GITHUB_IMAGE_ID=$GITHUB_IMAGE_ID
echo DOCKER_IMAGE_ID=$DOCKER_IMAGE_ID
echo VERSION=$VERSION
# tag for Github Registry
docker tag $IMAGE_NAME $GITHUB_IMAGE_ID:$VERSION
[[ "$VERSION" != "dev" ]] && docker tag $IMAGE_NAME $GITHUB_IMAGE_ID:latest
docker push $GITHUB_IMAGE_ID
# tag for Docker Hub
docker tag $IMAGE_NAME $DOCKER_IMAGE_ID:$VERSION
[[ "$VERSION" != "dev" ]] && docker tag $IMAGE_NAME $DOCKER_IMAGE_ID:latest
docker push $DOCKER_IMAGE_ID
+22 -1
View File
@@ -7,6 +7,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [2.3.1] - 2020-04-02
### Fixed
- Wordlist containing innappropriate words.
## [2.3.0] - 2020-03-30
### Added
- `?phoneticweight` command, which calculates a message's length in syllables.
- `?standards` command to display [xkcd 927](https://xkcd.com/927/).
### Changed
- Python>=3.7 now required.
## [2.2.3] - 2020-03-29
### Fixed
- Commands are no longer case-sensitive.
## [2.2.2] - 2020-02-25
### Fixed
- Fixed issue where HamStudy questions with images would cause an error.
@@ -87,7 +105,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 1.0.0 - 2019-07-31 [YANKED]
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.2.2...HEAD
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.3.1...HEAD
[2.3.1]: https://github.com/miaowware/qrm2/releases/tag/v2.3.1
[2.3.0]: https://github.com/miaowware/qrm2/releases/tag/v2.3.0
[2.2.3]: https://github.com/miaowware/qrm2/releases/tag/v2.2.3
[2.2.2]: https://github.com/miaowware/qrm2/releases/tag/v2.2.2
[2.2.1]: https://github.com/miaowware/qrm2/releases/tag/v2.2.1
[2.2.0]: https://github.com/miaowware/qrm2/releases/tag/v2.2.0
+2
View File
@@ -14,6 +14,8 @@ See [README-DOCKER.md](./README-DOCKER.md)
### Without Docker
Requires Python 3.7 or newer.
Prep the environment. For more information on extra options, see the [quick-bot-no-pain Makefile documentation](https://github.com/0x5c/quick-bot-no-pain/blob/master/docs/makefile.md).
```
+357 -296
View File
@@ -16,8 +16,10 @@ the GNU General Public License, version 2.
# KC4USA: reserved, no call history, *but* has application history
import re
import aiohttp
from bs4 import BeautifulSoup
import ae7qparser
import discord.ext.commands as commands
@@ -29,7 +31,7 @@ class AE7QCog(commands.Cog):
self.bot = bot
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
@commands.group(name="ae7q", aliases=["ae"], category=cmn.cat.lookup)
@commands.group(name="ae7q", aliases=["ae"], case_insensitive=True, category=cmn.cat.lookup)
async def _ae7q_lookup(self, ctx: commands.Context):
"""Looks up a callsign, FRN, or Licensee ID on [ae7q.com](http://ae7q.com/)."""
if ctx.invoked_subcommand is None:
@@ -40,64 +42,86 @@ class AE7QCog(commands.Cog):
"""Looks 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:
raise cmn.BotHTTPError(resp)
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}`")
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 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:])
call_data = ae7qparser.get_call(callsign)
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q History for {callsign}"
embed.title = f"AE7Q Callsign History for {callsign}"
embed.url = call_data.query_url
embed.colour = cmn.colours.good
embed.url = base_url + callsign
embed.description = ""
# 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 isinstance(call_data, ae7qparser.Ae7qCallData):
if call_data.conditions:
embed.description = " ".join([
" ".join([y.strip() for y in x]) for x in call_data.conditions
]).replace(callsign, f"`{callsign}`")
if len(table) > 3:
desc += f"\nRecords 1 to 3 of {len(table)}. See ae7q.com for more..."
if not call_data.call_history:
if not call_data.event_callsign_history:
embed.colour = cmn.colours.bad
embed.description += f"\nNo records found for `{callsign}`"
else:
# add the first five rows of the event callsign history to the embed
for row in call_data.event_callsign_history[0:5]:
header = f"**{row.start_date:%Y-%m-%d}-{row.end_date:%Y-%m-%d}**"
body = (f"Requestor: *{row.entity_name} ({row.callsign})*\n"
f"Event: *{row.event_name}*")
embed.add_field(name=header, value=body, inline=False)
embed.description = desc
if len(call_data.event_callsign_history) > 5:
embed.description += (f"\nRecords 1 to 5 of {len(call_data.event_callsign_history)}. "
f"See [ae7q.com]({call_data.query_url}) for more...")
else:
# add the first three rows of the callsign history to the embed
for row in call_data.call_history[0:3]:
header = f"**{row.entity_name}** ({row.applicant_type})"
body = (f"Class: *{row.operator_class}*\n"
f"Region: *{row.region_state}*\n"
f"Status: *{row.license_status}*\n")
if row.grant_date:
body += f"Granted: *{row.grant_date:%Y-%m-%d}*\n"
if row.effective_date:
body += f"Effective: *{row.effective_date:%Y-%m-%d}*\n"
if row.cancel_date:
body += f"Cancelled: *{row.cancel_date:%Y-%m-%d}*\n"
if row.expire_date:
body += f"Expires: *{row.expire_date:%Y-%m-%d}*\n"
embed.add_field(name=header, value=body, inline=False)
if len(call_data.call_history) > 3:
embed.description += (f"\nRecords 1 to 3 of {len(call_data.call_history)}. "
f"See [ae7q.com]({call_data.query_url}) for more...")
elif isinstance(call_data, ae7qparser.Ae7qCanadianCallData):
if not call_data.callsign_data:
embed.colour = cmn.colours.bad
embed.description += f"\nNo records found for `{callsign}`"
else:
if call_data.given_names != "" and call_data.surname != "":
embed.add_field(name="Name", value=f"{call_data.given_names} {call_data.surname}", inline=True)
if call_data.address != "":
embed.add_field(name="Address", value=call_data.address, inline=True)
if call_data.locality != "":
embed.add_field(name="Locality", value=call_data.locality, inline=True)
if call_data.province != "":
embed.add_field(name="Province", value=call_data.province, inline=True)
if call_data.postal_code != "":
embed.add_field(name="Postal Code", value=call_data.postal_code, inline=True)
if call_data.country != "":
embed.add_field(name="Country", value=call_data.country, inline=True)
if call_data.region != "":
embed.add_field(name="Region", value=call_data.region, inline=True)
if call_data.grid_square != "":
embed.add_field(name="Grid Square", value=call_data.grid_square, inline=True)
if call_data.qualifications != "":
embed.add_field(name="License Qualifications", value=call_data.qualifications, inline=True)
else:
embed.colour = cmn.colours.bad
embed.description += f"\nNo records found for `{callsign}`"
await ctx.send(embed=embed)
@@ -106,202 +130,283 @@ class AE7QCog(commands.Cog):
"""Looks 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)
async with self.session.get(base_url + callsign) as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
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")]
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
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"):
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
table = await process_table(table[2:])
call_data = ae7qparser.get_call(callsign)
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q Trustee History for {callsign}"
embed.url = call_data.query_url
embed.colour = cmn.colours.good
embed.url = base_url + callsign
embed.description = ""
# 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 isinstance(call_data, ae7qparser.Ae7qCallData):
if not call_data.trustee_history:
embed.colour = cmn.colours.bad
embed.description += f"\nNo records found for `{callsign}`"
else:
# add the first three rows of the trustee history to the embed
for row in call_data.trustee_history[0:3]:
header = f"**{row.callsign}** ({row.applicant_type})"
body = (f"Name: *{row.entity_name}*\n"
f"Region: *{row.region_state}*\n"
f"Status: *{row.license_status}*\n")
if row.grant_date:
body += f"Granted: *{row.grant_date:%Y-%m-%d}*\n"
if row.effective_date:
body += f"Effective: *{row.effective_date:%Y-%m-%d}*\n"
if row.cancel_date:
body += f"Cancelled: *{row.cancel_date:%Y-%m-%d}*\n"
if row.expire_date:
body += f"Expires: *{row.expire_date:%Y-%m-%d}*\n"
if len(table) > 3:
desc += f"\nRecords 1 to 3 of {len(table)}. See ae7q.com for more..."
embed.add_field(name=header, value=body, inline=False)
embed.description = desc
if len(call_data.trustee_history) > 3:
embed.description += (f"\nRecords 1 to 3 of {len(call_data.trustee_history)}. "
f"See [ae7q.com]({call_data.query_url}) for more...")
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):
"""Looks 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:
raise cmn.BotHTTPError(resp)
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}"
else:
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)
"""
raise NotImplementedError("Application history lookup not yet supported. "
"Check back in a later version of the bot.")
@_ae7q_lookup.command(name="applications", aliases=["a", "apps"], category=cmn.cat.lookup)
async def _ae7q_applications(self, ctx: commands.Context, query: str):
"""Looks up the application history for a callsign, FRN, or Licensee ID on [ae7q.com](http://ae7q.com/)."""
with ctx.typing():
query = query.upper()
# LID
if re.match(r"L\d+", query):
data = ae7qparser.get_licensee_id(query)
# FRN
elif re.match(r"\d{10}", query):
data = ae7qparser.get_frn(query)
# callsign
else:
data = ae7qparser.get_call(query)
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q Application History for {query}"
embed.url = data.query_url
embed.colour = cmn.colours.good
embed.description = ""
if not data.application_history:
embed.colour = cmn.colours.bad
embed.description += f"\nNo records found for `{query}`"
else:
# add the first three rows of the app history to the embed
if isinstance(data.application_history, ae7qparser.ApplicationsHistoryTable):
for row in data.application_history[0:3]:
header = f"**{row.uls_file_number[0]}** ({row.receipt_date:%Y-%m-%d})"
body = (f"Name: *{row.entity_name}*\n"
f"Callsign: *{row.application_callsign}*\n"
f"Region: *{row.region_state}*\n"
f"Purpose: *{row.application_purpose}*\n")
if row.payment_date:
body += f"Payment: *{row.payment_date:%Y-%m-%d}*\n"
if row.last_action_date:
body += f"Last Action: *{row.last_action_date:%Y-%m-%d}*\n"
body += f"Status: *{row.application_status}*"
embed.add_field(name=header, value=body, inline=False)
elif isinstance(data.application_history, ae7qparser.VanityApplicationsHistoryTable):
for row in data.application_history[0:3]:
header = f"**{row.uls_file_number[0]}** ({row.receipt_date:%Y-%m-%d})"
body = (f"Callsign: *{row.application_callsign}*\n"
f"Region: *{row.region_state}*\n"
f"Operator Class: *{row.operator_class}*\n"
f"Purpose: *{row.application_purpose}*\n")
if row.payment_date:
body += f"Payment: *{row.payment_date:%Y-%m-%d}*\n"
if row.last_action_date:
body += f"Last Action: *{row.last_action_date:%Y-%m-%d}*\n"
body += f"Status: *{row.application_status}*\n"
if row.applied_callsigns:
body += (f"Callsign{'s' if len(row.applied_callsigns) > 1 else ''} "
f"Applied For: *{', '.join(row.applied_callsigns)}*")
embed.add_field(name=header, value=body, inline=False)
if len(data.application_history) > 3:
embed.description += (f"\nRecords 1 to 3 of {len(data.application_history)}. "
f"See [ae7q.com]({data.query_url}) for more...")
await ctx.send(embed=embed)
@_ae7q_lookup.command(name="pending_apps", aliases=["pa"], category=cmn.cat.lookup)
async def _ae7q_pending_applications(self, ctx: commands.Context, query: str):
"""Looks up the pending applications for a callsign, FRN, or Licensee ID on [ae7q.com](http://ae7q.com/)."""
with ctx.typing():
query = query.upper()
# LID
if re.match(r"L\d+", query):
data = ae7qparser.get_licensee_id(query)
# FRN
elif re.match(r"\d{10}", query):
data = ae7qparser.get_frn(query)
# callsign
else:
data = ae7qparser.get_call(query)
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q Pending Applications for {query}"
embed.url = data.query_url
embed.colour = cmn.colours.good
embed.description = ""
if not data.pending_applications:
embed.colour = cmn.colours.bad
embed.description += f"\nNo records found for `{query}`"
else:
# add the first three rows of the pending apps to the embed
if isinstance(data.pending_applications, ae7qparser.PendingApplicationsPredictionsTable):
for row in data.pending_applications[0:3]:
header = f"**{row.uls_file_number}** ({row.receipt_date:%Y-%m-%d})"
body = (f"Callsign: *{row.applicant_callsign}*\n"
f"Region: *{row.region_state}*\n"
f"Operator Class: *{row.operator_class}*\n"
f"Vanity Type: *{row.vanity_type}*\n")
if row.process_date:
body += f"Processes On: *{row.process_date:%Y-%m-%d}*\n"
body += (f"Sequential #: *{row.sequential_number}*\n"
f"Vanity Callsign: *{row.vanity_callsign}*\n"
f"Prediction: *{row.prediction}*")
embed.add_field(name=header, value=body, inline=False)
elif isinstance(data.pending_applications, ae7qparser.CallsignPendingApplicationsPredictionsTable):
for row in data.pending_applications[0:3]:
header = f"**{row.uls_file_number}** ({row.receipt_date:%Y-%m-%d})"
body = (f"Callsign: *{row.applicant_callsign}*\n"
f"Region: *{row.region_state}*\n"
f"Operator Class: *{row.operator_class}*\n"
f"Vanity Type: *{row.vanity_type}*\n")
if row.process_date:
body += f"Processes On: *{row.process_date:%Y-%m-%d}*\n"
body += (f"Sequential #: *{row.sequential_number}*\n"
f"Prediction: *{row.prediction}*")
embed.add_field(name=header, value=body, inline=False)
if len(data.pending_applications) > 3:
embed.description += (f"\nRecords 1 to 3 of {len(data.pending_applications)}. "
f"See [ae7q.com]({data.query_url}) for more...")
await ctx.send(embed=embed)
@_ae7q_lookup.command(name="app_detail", aliases=["ad"], category=cmn.cat.lookup)
async def _ae7q_app_detail(self, ctx: commands.Context, ufn: str):
"""Looks up the application data for a ULS file number on [ae7q.com](http://ae7q.com/)."""
with ctx.typing():
app_data = ae7qparser.get_application(ufn)
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q Application Data for {ufn}"
embed.url = app_data.query_url
embed.colour = cmn.colours.good
if not app_data.application_data:
embed.colour = cmn.colours.bad
embed.description += f"\nNo records found for `{ufn}`"
else:
if app_data.frn:
embed.add_field(name="FRN", value=app_data.frn, inline=True)
if app_data.licensee_id:
embed.add_field(name="Licensee ID", value=app_data.licensee_id, inline=True)
if app_data.applicant_type:
embed.add_field(name="Applicant Type", value=app_data.applicant_type, inline=True)
if app_data.entity_type:
embed.add_field(name="Entity Type", value=app_data.entity_type, inline=True)
if app_data.entity_name:
embed.add_field(name="Name", value=app_data.entity_name, inline=True)
if app_data.callsign:
embed.add_field(name="Callsign", value=app_data.callsign, inline=True)
address = f"ATTN: {app_data.attention}\n" if app_data.attention else ""
address += f"{app_data.street_address}\n" if app_data.street_address else ""
address += f"{app_data.po_box}\n" if app_data.po_box else ""
address += (f"{', '.join([app_data.locality, app_data.state.split('-')[0].strip()])}"
f" {app_data.postal_code}")
if address:
embed.add_field(name="Address", value=address, inline=True)
if app_data.county:
embed.add_field(name="County", value=app_data.county, inline=True)
if app_data.maidenhead:
embed.add_field(name="Grid Square", value=app_data.maidenhead, inline=True)
if app_data.uls_geo_region:
embed.add_field(name="Region", value=app_data.uls_geo_region, inline=True)
if app_data.last_action_date:
embed.add_field(name="Last Action", value=f"{app_data.last_action_date:%Y-%m-%d}", inline=True)
if app_data.receipt_date:
embed.add_field(name="Receipt Date", value=f"{app_data.receipt_date:%Y-%m-%d}", inline=True)
if app_data.entered_timestamp:
embed.add_field(name="Entered Time", value=f"{app_data.entered_timestamp:%Y-%m-%d}", inline=True)
if app_data.application_source:
embed.add_field(name="Application Source", value=app_data.application_source, inline=True)
if app_data.application_purpose:
embed.add_field(name="Purpose", value=app_data.application_purpose, inline=True)
if app_data.result:
embed.add_field(name="Result", value=app_data.result, inline=True)
if app_data.fee_control_number:
embed.add_field(name="Fee Control Number", value=app_data.fee_control_number, inline=True)
if app_data.payment_date:
embed.add_field(name="Payment Date", value=f"{app_data.payment_date:%Y-%m-%d}", inline=True)
if app_data.operator_class:
embed.add_field(name="Operator Class", value=app_data.operator_class, inline=True)
if app_data.operator_group:
embed.add_field(name="Operator Group", value=app_data.operator_group, inline=True)
if app_data.uls_group:
embed.add_field(name="ULS Group", value=app_data.uls_group, inline=True)
if app_data.vanity_type:
embed.add_field(name="Vanity Type", value=app_data.vanity_type, inline=True)
if app_data.vanity_relationship:
embed.add_field(name="Vanity Relationship", value=app_data.vanity_relationship, inline=True)
if app_data.trustee_name and app_data.trustee_callsign:
embed.add_field(name="Trustee",
value=f"{app_data.trustee_name} ({app_data.trustee_callsign})",
inline=True)
await ctx.send(embed=embed)
@_ae7q_lookup.command(name="frn", aliases=["f"], category=cmn.cat.lookup)
async def _ae7q_frn(self, ctx: commands.Context, frn: str):
"""Looks 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)
frn = frn.upper()
async with self.session.get(base_url + frn) as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
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:])
frn_data = ae7qparser.get_frn(frn)
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q History for FRN {frn}"
embed.title = f"AE7Q FRN History for {frn}"
embed.url = frn_data.query_url
embed.colour = cmn.colours.good
embed.url = base_url + frn
embed.description = ""
# 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 not frn_data.frn_history:
embed.colour = cmn.colours.bad
embed.description += f"\nNo records found for `{frn}`"
else:
# add the first three rows of the FRN history to the embed
for row in frn_data.frn_history[0:3]:
header = f"**{row.callsign}** ({row.applicant_type})"
body = (f"Name: *{row.entity_name}*\n"
f"Region: *{row.region_state}*\n"
f"Operator Class: *{row.operator_class}*\n"
f"Status: *{row.license_status}*\n")
if row.grant_date:
body += f"Granted: *{row.grant_date:%Y-%m-%d}*\n"
if row.effective_date:
body += f"Effective: *{row.effective_date:%Y-%m-%d}*\n"
if row.cancel_date:
body += f"Cancelled: *{row.cancel_date:%Y-%m-%d}*\n"
if row.expire_date:
body += f"Expires: *{row.expire_date:%Y-%m-%d}*\n"
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..."
if len(frn_data.frn_history) > 3:
embed.description += (f"\nRecords 1 to 3 of {len(frn_data.frn_history)}. "
f"See [ae7q.com]({frn_data.query_url}) for more...")
await ctx.send(embed=embed)
@@ -310,87 +415,43 @@ class AE7QCog(commands.Cog):
"""Looks 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:
raise cmn.BotHTTPError(resp)
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:])
lid_data = ae7qparser.get_licensee_id(licensee_id)
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q History for Licensee {licensee_id}"
embed.url = lid_data.query_url
embed.colour = cmn.colours.good
embed.url = base_url + licensee_id
embed.description = ""
# 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 not lid_data.licensee_id_history:
embed.colour = cmn.colours.bad
embed.description += f"\nNo records found for `{licensee_id}`"
else:
# add the first three rows of the FRN history to the embed
for row in lid_data.licensee_id_history[0:3]:
header = f"**{row.callsign}** ({row.applicant_type})"
body = (f"Name: *{row.entity_name}*\n"
f"Region: *{row.region_state}*\n"
f"Operator Class: *{row.operator_class}*\n"
f"Status: *{row.license_status}*\n")
if row.grant_date:
body += f"Granted: *{row.grant_date:%Y-%m-%d}*\n"
if row.effective_date:
body += f"Effective: *{row.effective_date:%Y-%m-%d}*\n"
if row.cancel_date:
body += f"Cancelled: *{row.cancel_date:%Y-%m-%d}*\n"
if row.expire_date:
body += f"Expires: *{row.expire_date:%Y-%m-%d}*\n"
if len(table) > 3:
embed.description = f"Records 1 to 3 of {len(table)}. See ae7q.com for more..."
embed.add_field(name=header, value=body, inline=False)
if len(lid_data.licensee_id_history) > 3:
embed.description += (f"\nRecords 1 to 3 of {len(lid_data.licensee_id_history)}. "
f"See [ae7q.com]({lid_data.query_url}) 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):
bot.add_cog(AE7QCog(bot))
+1 -2
View File
@@ -10,7 +10,6 @@ the GNU General Public License, version 2.
import random
import re
from collections import OrderedDict
from typing import Union
import discord
@@ -189,7 +188,7 @@ class BaseCog(commands.Cog):
def parse_changelog():
changelog = OrderedDict()
changelog = {}
ver = ""
heading = ""
+5
View File
@@ -31,6 +31,11 @@ class FunCog(commands.Cog):
"""Returns xkcd: tar."""
await ctx.send("http://xkcd.com/1168")
@commands.command(name="standards", category=cmn.cat.fun)
async def _standards(self, ctx: commands.Context):
"""Returns xkcd: Standards."""
await ctx.send("http://xkcd.com/927")
@commands.command(name="xd", hidden=True, category=cmn.cat.fun)
async def _xd(self, ctx: commands.Context):
"""ecks dee"""
+20
View File
@@ -92,6 +92,26 @@ class HamCog(commands.Cog):
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
@commands.command(name="phoneticweight", aliases=["pw"], category=cmn.cat.ref)
async def _weight(self, ctx: commands.Context, *, msg: str):
"""Calculates the phonetic weight of a callsign or message."""
embed = cmn.embed_factory(ctx)
msg = msg.upper()
weight = 0
for char in msg:
try:
weight += phonetics.pweights[char]
except KeyError:
embed.title = "Error in calculation of phonetic weight"
embed.description = f"Unknown character `{char}` in message"
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
embed.title = f"Phonetic Weight of {msg}"
embed.description = f"The phonetic weight is **{weight}**"
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
def setup(bot: commands.Bot):
bot.add_cog(HamCog(bot))
+18 -22
View File
@@ -8,7 +8,6 @@ the GNU General Public License, version 2.
"""
from collections import OrderedDict
from io import BytesIO
import aiohttp
@@ -159,27 +158,24 @@ def qrz_process_info(data: dict):
else:
lotw = "Unknown"
return OrderedDict([("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)),
("CQ Zone", data.get("cqzone", None)),
("ITU Zone", data.get("ituzone", None)),
("IOTA Designator", data.get("iota", None)),
("Born", data.get("born", None))])
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)}
def setup(bot):
+1 -1
View File
@@ -40,7 +40,7 @@ class WeatherCog(commands.Cog):
embed.set_image(url="attachment://condx.png")
await ctx.send(embed=embed, file=discord.File(data, "condx.png"))
@commands.group(name="weather", aliases=["wttr"], category=cmn.cat.weather)
@commands.group(name="weather", aliases=["wttr"], case_insensitive=True, category=cmn.cat.weather)
async def _weather_conditions(self, ctx: commands.Context):
"""Gets local weather conditions from [wttr.in](http://wttr.in/).
+1 -1
View File
@@ -12,5 +12,5 @@ authors = ("@ClassAbbyAmplifier#2229", "@0x5c#0639")
description = """A bot with various useful ham radio-related functions, written in Python."""
license = "Released under the GNU General Public License v2"
contributing = "Check out the source on GitHub, contributions welcome: https://github.com/miaowware/qrm2"
release = "2.2.2"
release = "2.3.1"
bot_server = "https://discord.gg/Ntbg3J4"
+3 -3
View File
@@ -45,8 +45,8 @@ 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(),
case_insensitive=True,
description=info.description, help_command=commands.MinimalHelpCommand(),
loop=loop,
connector=connector)
@@ -82,7 +82,7 @@ async def _shutdown_bot(ctx: commands.Context):
await bot.logout()
@bot.group(name="extctl", aliases=["ex"], category=cmn.cat.admin)
@bot.group(name="extctl", aliases=["ex"], case_insensitive=True, category=cmn.cat.admin)
@commands.check(cmn.check_if_owner)
async def _extctl(ctx: commands.Context):
"""Extension control commands.
+1
View File
@@ -3,3 +3,4 @@ ctyparser
beautifulsoup4
lxml
pytz
ae7qparser
+39 -40
View File
@@ -8,49 +8,48 @@ the GNU General Public License, version 2.
"""
from collections import OrderedDict
us_calls_title = "Valid US Vanity Callsigns"
us_calls_desc = ("#x# is the number of letters in the prefix and suffix of a callsign. "
"E.g., WY4RC would be a 2x2 callsign, with prefix WY and suffix RC.")
us_calls = OrderedDict([("**Group A** (Extra Only)", ("**Any:** K, N, W (1x2)\n"
" AA-AL, KA-KZ, NA-NZ, WA-WZ (2x1)\n"
" AA-AL (2x2)\n"
"*Except*\n"
"**Alaska:** AL, KL, NL, WL (2x1)\n"
"**Caribbean:** KP, NP, WP (2x1)\n"
"**Pacific:** AH, KH, NH, WH (2x1)")),
("**Group B** (Advanced and Extra Only)", ("**Any:** KA-KZ, NA-NZ, WA-WZ (2x2)\n"
"*Except*\n"
"**Alaska:** AL (2x2)\n"
"**Caribbean:** KP (2x2)\n"
"**Pacific:** AH (2x2)")),
("**Group C** (Technician, General, Advanced, Extra Only)", ("**Any Region:** K, N, W (1x3)\n"
"*Except*\n"
"**Alaska:** KL, NL, WL (2x2)\n"
"**Caribbean:** NP, WP (2x2)\n"
"**Pacific:** KH, NH, WH (2x2)")),
("**Group D** (Any License Class)", ("**Any Region:** KA-KZ, WA-WZ (2x3)\n"
"*Except*\n"
"**Alaska:** KL, WL (2x3)\n"
"**Caribbean:** KP, WP (2x3)\n"
"**Pacific:** KH, WH (2x3)")),
("**Unavailable**", ("- KA2AA-KA9ZZ: US Army in Japan\n"
"- KC4AAA-KC4AAF: NSF in Antartica\n"
"- KC4USA-KC4USZ: US Navy in Antartica\n"
"- KG4AA-KG4ZZ: US Navy in Guantanamo Bay\n"
"- KL9KAA-KL9KHZ: US military in Korea\n"
"- KC6AA-KC6ZZ: Former US (Eastern and Western Caroline Islands), "
"now Federated States of Micronesia (V6) and Republic of Palau (T8)\n"
"- KX6AA-KX6ZZ: Former US (Marshall Islands), "
"now Republic of the Marshall Islands (V73)\n"
"- Any suffix SOS or QRA-QUZ\n"
"- Any 2x3 with X as the first suffix letter\n"
"- Any 2x3 with AF, KF, NF, or WF prefix and suffix EMA: FEMA\n"
"- Any 2x3 with AA-AL, NA-NZ, WC, WK, WM, WR, or WT prefix: \"Group X\"\n"
"- Any 2x1, 2x2, or 2x3 with KP, NP, WP prefix and 0, 6, 7, 8, 9 number\n"
"- Any 1x1 callsign: Special Event"))])
us_calls = {
"**Group A** (Extra Only)": ("**Any:** K, N, W (1x2)\n"
" AA-AL, KA-KZ, NA-NZ, WA-WZ (2x1)\n"
" AA-AL (2x2)\n"
"*Except*\n"
"**Alaska:** AL, KL, NL, WL (2x1)\n"
"**Caribbean:** KP, NP, WP (2x1)\n"
"**Pacific:** AH, KH, NH, WH (2x1)"),
"**Group B** (Advanced and Extra Only)": ("**Any:** KA-KZ, NA-NZ, WA-WZ (2x2)\n"
"*Except*\n"
"**Alaska:** AL (2x2)\n"
"**Caribbean:** KP (2x2)\n"
"**Pacific:** AH (2x2)"),
"**Group C** (Technician, General, Advanced, Extra Only)": ("**Any Region:** K, N, W (1x3)\n"
"*Except*\n"
"**Alaska:** KL, NL, WL (2x2)\n"
"**Caribbean:** NP, WP (2x2)\n"
"**Pacific:** KH, NH, WH (2x2)"),
"**Group D** (Any License Class)": ("**Any Region:** KA-KZ, WA-WZ (2x3)\n"
"*Except*\n"
"**Alaska:** KL, WL (2x3)\n"
"**Caribbean:** KP, WP (2x3)\n"
"**Pacific:** KH, WH (2x3)"),
"**Unavailable**": ("- KA2AA-KA9ZZ: US Army in Japan\n"
"- KC4AAA-KC4AAF: NSF in Antartica\n"
"- KC4USA-KC4USZ: US Navy in Antartica\n"
"- KG4AA-KG4ZZ: US Navy in Guantanamo Bay\n"
"- KL9KAA-KL9KHZ: US military in Korea\n"
"- KC6AA-KC6ZZ: Former US (Eastern and Western Caroline Islands), "
"now Federated States of Micronesia (V6) and Republic of Palau (T8)\n"
"- KX6AA-KX6ZZ: Former US (Marshall Islands), "
"now Republic of the Marshall Islands (V73)\n"
"- Any suffix SOS or QRA-QUZ\n"
"- Any 2x3 with X as the first suffix letter\n"
"- Any 2x3 with AF, KF, NF, or WF prefix and suffix EMA: FEMA\n"
"- Any 2x3 with AA-AL, NA-NZ, WC, WK, WM, WR, or WT prefix: \"Group X\"\n"
"- Any 2x1, 2x2, or 2x3 with KP, NP, WP prefix and 0, 6, 7, 8, 9 number\n"
"- Any 1x1 callsign: Special Event")
}
# format: country: (title, description, text)
options = {"us": (us_calls_title, us_calls_desc, us_calls)}
+40
View File
@@ -36,3 +36,43 @@ phonetics = {
"y": "yankee",
"z": "zulu"
}
pweights = {
"A": 2,
"B": 2,
"C": 2,
"D": 2,
"E": 2,
"F": 2,
"G": 1,
"H": 2,
"I": 3,
"J": 3,
"K": 2,
"L": 2,
"M": 1,
"N": 3,
"O": 2,
"P": 2,
"Q": 2,
"R": 3,
"S": 3,
"T": 2,
"U": 3,
"V": 2,
"W": 2,
"X": 2,
"Y": 2,
"Z": 2,
"0": 2,
"1": 1,
"2": 1,
"3": 1,
"4": 1,
"5": 1,
"6": 1,
"7": 2,
"8": 1,
"9": 2,
"/": 1,
}
+119 -44225
View File
File diff suppressed because it is too large Load Diff