Compare commits

...

46 Commits

Author SHA1 Message Date
classabbyamp 4b55ab49b7 .github/workflows/docker.yml: fix labels, again
Docker Build and Deploy / Build and push docker images (push) Has been cancelled Details
Linting / flake8 (push) Has been cancelled Details
2023-12-15 14:47:33 -05:00
classabbyamp cf378a2ef4 CHANGELOG.md: relbump 2023-12-15 14:30:59 -05:00
classabbyamp 13a8a63300 info.py: use pomelo usernames, relbump 2023-12-15 14:30:59 -05:00
classabbyamp 23619949d7 .github/workflows/docker.yml: fix tagging latest
turns out, most of this is handled automatically.
see https://github.com/docker/metadata-action#latest-tag
2023-12-15 14:20:05 -05:00
classabbyamp 444687bd12 .github/workflows/docker.yml: use metadata action, run on PRs
Docker Build and Deploy / Build and push docker images (push) Has been cancelled Details
Linting / flake8 (push) Has been cancelled Details
2023-12-11 08:21:33 -05:00
classabbyamp 86da8d135a Dockerfile: use new void container, python3.11 2023-12-11 08:21:33 -05:00
classabbyamp 67add85a7a run.sh: shellcheck 2023-12-11 08:21:33 -05:00
0x5c abdc5ebacb
Merge pull request #476 from miaowware/fix/metar
Docker Build and Deploy / Build and push docker images (push) Waiting to run Details
Linting / flake8 (push) Waiting to run Details
Fix metar + version bumps
2023-12-10 14:17:35 -05:00
0x5c a5cbb5a09a
exts/land_weather: switch to the new aviationweather.gov API
Fixes #475
2023-12-10 07:47:59 -05:00
0x5c ce99cc194e
bump pydantic to version 2 2023-12-10 07:19:45 -05:00
0x5c f8d7316071
bump pycord to pre-release v5 2023-12-10 07:16:41 -05:00
0x5c 9feeb01e42
Merge pull request #474 from cschmittiey/hamqsl-https
hamqsl.com has moved to https
2023-04-22 02:55:08 -04:00
Caleb Smith fcb682ec4a hamqsl.com has moved to https 2023-04-22 00:53:03 -06:00
Judd West c8a1128927 Add NOAA D-RAP map to propagation plugin 2023-01-30 05:25:01 -05:00
classabbyamp df08cefe25 exts/callsign: fix mail qsl display 2023-01-29 15:40:43 -05:00
0x5c cf93773a3c
Merge pull request #471 from miaowware/rel-2.9.1
rel 2.9.1
2023-01-29 00:52:48 -05:00
classabbyamp 642b49041a
bump version 2.9.1 2023-01-29 00:44:29 -05:00
classabbyamp e95f991300
bump copyright 2023-01-29 00:43:56 -05:00
0x5c 56ae14a5c3
Merge pull request #469 from miaowware/new-clt
exts/callsign: unworkaround some things solved in CLT 1.1.0
2023-01-29 00:34:04 -05:00
classabbyamp 30c6e96883
exts/callsign: unworkaround some things solved in CLT 1.1.0
fixes #466
2023-01-29 00:23:20 -05:00
0x5c 44a6905f7b
Merge pull request #468 from miaowware/embed-avatar
Embed factory/pycord fixes
2023-01-28 23:41:01 -05:00
classabbyamp d7de78e582
common.py: use tz-aware datetime for proper timestamp display
https://docs.pycord.dev/en/stable/api/data_classes.html#discord.Embed.timestamp
2023-01-28 20:11:23 -05:00
classabbyamp b000c9173e
common.py: don't error when creating embeds for users without avatars
behaviour changed in pycord 2.0:
https://docs.pycord.dev/en/stable/api/models.html#discord.User.display_avatar

fixes #467
2023-01-28 20:11:00 -05:00
0x5c 5460dd811b
Merge pull request #465 from miaowware/fix-rel-workflow
.github/workflows/release.yml: move to ncipollo/create-release action
2023-01-13 18:57:54 -05:00
classabbyamp a00d613430
.github/workflows/release.yml: move to ncipollo/create-release action
fixes #464
2023-01-13 12:42:26 -05:00
0x5c 6b0cdb6249
Merge pull request #463 from miaowware/update-changelog
Bump version to 2.9.0
2023-01-13 03:51:01 -05:00
0x5c 4eed94b55b
Bump version to 2.9.0 2023-01-13 03:48:45 -05:00
0x5c 3110961a3a exts/propagation: Fix ?solarweather no image bug
Back to the ugly hack of downloading the image and uploading it to discord.

Fixes #461
2023-01-13 03:44:27 -05:00
0x5c a4c8a056ac First steps for move from aiohttp to httpx 2023-01-13 03:44:27 -05:00
classabbyamp 9368ccd9e2 exts/study: fix in DMs
fixes #442
2023-01-13 01:14:54 -05:00
0x5c 8efd958314
Merge pull request #460 from miaowware/delete-solar-aliases
exts/propagation: Remove deprecated ?solarweather aliases
2023-01-13 01:09:57 -05:00
0x5c 4803bf89b2
exts/propagation: Remove deprecated ?solarweather aliases
Fixes #332
2023-01-13 01:03:37 -05:00
classabbyamp c82216cae6 Revert "update changelog, bump release"
This reverts commit 1b0b244f99.
2023-01-13 00:48:06 -05:00
classabbyamp 1650cd50dc exts/callsign: simplify stringification, fix data validation 2023-01-12 22:24:06 -05:00
classabbyamp 1b0b244f99 update changelog, bump release 2023-01-01 16:22:42 -05:00
classabbyamp 5db77f78d9 exts/callsign: convert to callsignlookuptools (qrz only for now) 2023-01-01 16:22:42 -05:00
classabbyamp c7ea5e0998 migrate to pycord 2023-01-01 16:22:42 -05:00
classabbyamp adffd82127 utils/resources_manager.py: use httpx instead of requests 2023-01-01 16:22:42 -05:00
classabbyamp 970159e81b Makefile: update default python version to 3.11 2023-01-01 16:22:42 -05:00
0x5c f5aeefc934
Merge pull request #454 from miaowware/token-perms
.github/workflows/docker.yml: add package write perms
2022-10-12 01:08:29 -04:00
classabbyamp aac9262469
.github/workflows/docker.yml: add package write perms 2022-10-12 00:56:56 -04:00
0x5c b472cdfa25
Merge pull request #453 from miaowware/xbps-update
Dockerfile: ensure system update works
2022-10-11 19:57:13 -04:00
classabbyamp 585cae8b97
Dockerfile: ensure system update works 2022-10-11 18:17:53 -04:00
0x5c c3fbd3e719
Merge pull request #452 from miaowware/set-output
.github/workflows/docker.yml: remove deprecated set-output
2022-10-11 18:14:12 -04:00
classabbyamp 7eadb50b96
exts/dbconv: fix lint 2022-10-11 18:12:55 -04:00
classabbyamp 98642c099d
.github/workflows/docker.yml: remove deprecated set-output
https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
2022-10-11 14:24:03 -04:00
37 changed files with 305 additions and 262 deletions

View File

@ -3,6 +3,10 @@
name: Docker Build and Deploy
on:
workflow_dispatch:
pull_request:
branches:
- master
push:
# Publish `master` as Docker `dev` image.
branches:
@ -11,27 +15,42 @@ on:
tags:
- v*
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
docker:
name: Build and push docker images
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{ github.ref }}
uses: classabbyamp/treeless-checkout-action@v1
- name: Write ref to file
if: ${{ github.event_name != 'pull_request' }}
run: git rev-list -n 1 $GITHUB_REF > ./git_commit
- name: Build image
id: build_image
run: |
IMAGE_ID=${GITHUB_REPOSITORY,,}
IMAGE_NAME=${IMAGE_ID#*/}
echo ::set-output name=image_id::$IMAGE_ID
echo ::set-output name=image_name::$IMAGE_NAME
docker build . --file Dockerfile -t $IMAGE_NAME
- name: Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/${{ github.repository }}
tags: |
type=sha,prefix=
type=raw,value=dev,enable={{is_default_branch}}
type=match,pattern=v(.*),group=1
labels: |
org.opencontainers.image.authors=classabbyamp and 0x5c
org.opencontainers.image.url=https://github.com/miaowware/qrm2
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.vendor=miaowware
org.opencontainers.image.title=qrm2
org.opencontainers.image.description=Discord bot with ham radio functions
org.opencontainers.image.licenses=LiLiQ-Rplus-1.1
- name: Login to Github Container Registry
uses: docker/login-action@v1
@ -40,41 +59,10 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Tag image
id: tag_image
run: |
IMAGE_NAME=${{ steps.build_image.outputs.image_name }}
IMAGE_ID=ghcr.io/${{ steps.build_image.outputs.image_id }}
echo IMAGE_ID=$IMAGE_ID
echo ::set-output name=image_id::$IMAGE_ID
# 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//')
# if version is master, set version to dev
[[ "$VERSION" == "master" ]] && VERSION=dev
echo VERSION=$VERSION
echo ::set-output name=version::$VERSION
# tag dev or x.x.x
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
# tag latest if not a dev release
[[ "$VERSION" != "dev" ]] && docker tag $IMAGE_NAME $IMAGE_ID:latest || true
- name: Push images to registry
run: |
VERSION=${{ steps.tag_image.outputs.version }}
IMAGE_ID=${{ steps.tag_image.outputs.image_id }}
[[ "$VERSION" != "dev" ]] && docker push $IMAGE_ID:latest || true
docker push $IMAGE_ID:$VERSION
- name: Deploy official images
id: deploy_images
uses: satak/webrequest-action@v1
- name: Build and push
uses: docker/build-push-action@v5
with:
url: ${{ secrets.DEPLOY_URL }}
method: POST
headers: '{"Authentication": "Token ${{ secrets.DEPLOY_TOKEN }}"}'
payload: '{"version": "${{ steps.tag_image.outputs.version }}"}'
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -12,6 +12,8 @@ jobs:
release:
name: Create Release
runs-on: ubuntu-20.04
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v2
@ -46,12 +48,10 @@ jobs:
- name: Publish Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: ncipollo/release-action@v1
with:
tag_name: ${{ env.tag_version }}
release_name: ${{ env.tag_subject }}
tag: ${{ env.tag_version }}
name: ${{ env.tag_subject }}
body: |
${{ env.tag_body }}

View File

@ -7,6 +7,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [2.9.2] - 2023-12-15
### Added
- `?drapmap` command to display NOAA D Region Absorption Predictions map.
- Support for the new username format.
### Fixed
- Issue where `?solarweather` would not show a picture (#474).
- Issue where `?metar` and `?taf` failed to fetch data (#475).
## [2.9.1] - 2023-01-29
### Fixed
- Issue where embeds would not work for users without avatars (#467).
- Issue where embeds would show the wrong timezone.
- Several issues with `?call` caused by issues in a library (#466).
## [2.9.0] - 2023-01-13
### Changed
- Migrated to Pycord.
### Removed
- Long-deprecated aliases for `?solarweather`.
### Fixed
- Issue where ?hamstudy would not work in direct messages (#442).
- Issue where `?solarweather` would not show a picture (#461).
## [2.8.0] - 2022-06-24
### Removed
- `?ae7q` command (#448).
@ -229,7 +255,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.8.0...HEAD
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.9.2...HEAD
[2.9.2]: https://github.com/miaowware/qrm2/releases/tag/v2.9.2
[2.9.1]: https://github.com/miaowware/qrm2/releases/tag/v2.9.1
[2.9.0]: https://github.com/miaowware/qrm2/releases/tag/v2.9.0
[2.8.0]: https://github.com/miaowware/qrm2/releases/tag/v2.8.0
[2.7.6]: https://github.com/miaowware/qrm2/releases/tag/v2.7.6
[2.7.5]: https://github.com/miaowware/qrm2/releases/tag/v2.7.5

View File

@ -1,31 +1,31 @@
FROM ghcr.io/void-linux/void-linux:latest-mini-x86_64
LABEL org.opencontainers.image.source https://github.com/miaowware/qrm2
FROM ghcr.io/void-linux/void-musl-full
COPY . /app
WORKDIR /app
ARG REPOSITORY=https://repo-us.voidlinux.org/current
ARG REPOSITORY=https://repo-fastly.voidlinux.org/current
ARG PKGS="cairo libjpeg-turbo"
ARG UID 1000
ARG GID 1000
RUN \
echo "**** update system ****" && \
xbps-install -SuyM -R ${REPOSITORY} && \
xbps-install -Suy xbps -R ${REPOSITORY} && \
xbps-install -uy -R ${REPOSITORY} && \
echo "**** install system packages ****" && \
xbps-install -yM -R ${REPOSITORY} ${PKGS} python3 python3-pip && \
xbps-install -y -R ${REPOSITORY} ${PKGS} python3.11 && \
echo "**** install pip packages ****" && \
pip3 install -U pip setuptools wheel && \
pip3 install -r requirements.txt && \
python3.11 -m venv botenv && \
botenv/bin/pip install -U pip setuptools wheel && \
botenv/bin/pip install -r requirements.txt && \
echo "**** clean up ****" && \
rm -rf \
/root/.cache \
/tmp/* \
/var/cache/xbps/*
ENV PYTHON_BIN python3
ENV PYTHONUNBUFFERED 1
USER $UID:$GID
CMD ["/bin/sh", "run.sh", "--pass-errors", "--no-botenv"]
CMD ["/bin/sh", "run.sh", "--pass-errors"]

View File

@ -12,7 +12,7 @@
# Those are the defaults; they can be over-ridden if specified
# at en environment level or as 'make' arguments.
BOTENV ?= botenv
PYTHON_BIN ?= python3.9
PYTHON_BIN ?= python3.11
PIP_OUTPUT ?= -q

View File

@ -38,7 +38,7 @@ All issues and requests related to resources (including maps, band charts, data)
## Copyright
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This program is released under the terms of the *Québec Free and Open-Source Licence Strong Reciprocity (LiLiQ-R+)*, version 1.1.
See [`LICENCE`](LICENCE) for full license text (Français / English).

View File

@ -1,7 +1,7 @@
"""
Common tools for the bot.
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
@ -12,16 +12,17 @@ import enum
import json
import re
import traceback
from datetime import datetime
from datetime import datetime, timezone
from pathlib import Path
from types import SimpleNamespace
from typing import Union
import aiohttp
import httpx
import discord
import discord.ext.commands as commands
from discord import Emoji, Reaction, PartialEmoji
from discord import Emoji, PartialEmoji
import data.options as opt
@ -125,12 +126,16 @@ class ImagesGroup(collections.abc.Mapping):
class BotHTTPError(Exception):
"""Raised whan a requests fails (status != 200) in a command."""
def __init__(self, response: aiohttp.ClientResponse):
msg = f"Request failed: {response.status} {response.reason}"
def __init__(self, response: aiohttp.ClientResponse | httpx.Response):
if isinstance(response, aiohttp.ClientResponse):
self.status = response.status
self.reason = response.reason
else:
self.status = response.status_code
self.reason = response.reason_phrase
msg = f"Request failed: {self.status} {self.reason}"
super().__init__(msg)
self.response = response
self.status = response.status
self.reason = response.reason
# --- Converters ---
@ -160,8 +165,9 @@ class GlobalChannelConverter(commands.IDConverter):
def embed_factory(ctx: commands.Context) -> discord.Embed:
"""Creates an embed with neutral colour and standard footer."""
embed = discord.Embed(timestamp=datetime.utcnow(), colour=colours.neutral)
embed.set_footer(text=str(ctx.author), icon_url=str(ctx.author.avatar_url))
embed = discord.Embed(timestamp=datetime.now(timezone.utc), colour=colours.neutral)
if ctx.author:
embed.set_footer(text=str(ctx.author), icon_url=str(ctx.author.display_avatar))
return embed
@ -178,7 +184,7 @@ def error_embed_factory(ctx: commands.Context, exception: Exception, debug_mode:
return embed
async def add_react(msg: discord.Message, react: Union[Emoji, Reaction, PartialEmoji, str]):
async def add_react(msg: discord.Message, react: Union[Emoji, PartialEmoji, str]):
try:
await msg.add_reaction(react)
except discord.Forbidden:

View File

@ -1,3 +1,3 @@
-r requirements.txt
flake8
discord.py-stubs==1.7.3
mypy

View File

@ -1,7 +1,7 @@
"""
ae7q extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,7 +1,7 @@
"""
Base extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
@ -141,7 +141,8 @@ class QrmHelpCommand(commands.HelpCommand):
embed.title = await self.get_command_signature(group)
embed.description = group.help
for cmd in await self.filter_commands(group.commands, sort=True):
embed.add_field(name=await self.get_command_signature(cmd), value=cmd.help, inline=False)
embed.add_field(name=await self.get_command_signature(cmd), value=cmd.help if cmd.help else "",
inline=False)
await self.context.send(embed=embed)
@ -177,7 +178,7 @@ class BaseCog(commands.Cog):
@commands.Cog.listener()
async def on_ready(self):
if not self.bot_invite:
if not self.bot_invite and self.bot.user:
self.bot_invite = (f"https://discordapp.com/oauth2/authorize?client_id={self.bot.user.id}"
f"&scope=bot&permissions={opt.invite_perms}")
@ -196,7 +197,8 @@ class BaseCog(commands.Cog):
inline=False)
if opt.enable_invite_cmd and (await self.bot.application_info()).bot_public:
embed.add_field(name="Invite qrm to Your Server", value=self.bot_invite, inline=False)
embed.set_thumbnail(url=str(self.bot.user.avatar_url))
if self.bot.user and self.bot.user.avatar:
embed.set_thumbnail(url=str(self.bot.user.avatar.url))
await ctx.send(embed=embed)
@commands.command(name="ping", aliases=["beep"], category=cmn.BoltCats.INFO)

View File

@ -2,18 +2,16 @@
Callsign Lookup extension for qrm
---
Copyright (C) 2019-2020 classabbyamp, 0x5c (as qrz.py)
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
from typing import Dict
from datetime import datetime
import aiohttp
from qrztools import qrztools, QrzAsync, QrzError
from gridtools import Grid, LatLong
from callsignlookuptools import QrzAsyncClient, CallsignLookupError, CallsignData
from discord.ext import commands
@ -29,14 +27,16 @@ class QRZCog(commands.Cog):
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
session_key = ""
try:
with open("data/qrz_session") as qrz_file:
self.qrz.session_key = qrz_file.readline().strip()
session_key = qrz_file.readline().strip()
except FileNotFoundError:
pass
self.qrz = QrzAsyncClient(username=keys.qrz_user, password=keys.qrz_pass, useragent="discord-qrm2",
session_key=session_key,
session=aiohttp.ClientSession(connector=bot.qrm.connector))
except AttributeError:
pass
@ -63,69 +63,65 @@ class QRZCog(commands.Cog):
async with ctx.typing():
try:
data = await self.qrz.get_callsign(callsign)
except QrzError as e:
data = await self.qrz.search(callsign)
except CallsignLookupError as e:
embed.colour = cmn.colours.bad
embed.description = str(e)
await ctx.send(embed=embed)
return
embed.title = f"QRZ Data for {data.call}"
embed.title = f"QRZ Data for {data.callsign}"
embed.colour = cmn.colours.good
embed.url = data.url
if data.image != qrztools.QrzImage():
if data.image is not None:
embed.set_thumbnail(url=data.image.url)
for title, val in qrz_process_info(data).items():
if val:
if val is not None and (val := str(val)):
embed.add_field(name=title, value=val, inline=True)
await ctx.send(embed=embed)
def qrz_process_info(data: qrztools.QrzCallsignData) -> Dict:
if data.name != qrztools.Name():
def qrz_process_info(data: CallsignData) -> Dict:
if data.name is not None:
if opt.qrz_only_nickname:
if data.name.nickname:
name = data.name.nickname + " " + data.name.name
nm = data.name.name if data.name.name is not None else ""
if data.name.nickname is not None:
name = data.name.nickname + " " + nm
elif data.name.first:
name = data.name.first + " " + data.name.name
name = data.name.first + " " + nm
else:
name = data.name.name
name = nm
else:
name = data.name.formatted_name
name = data.name
else:
name = None
if data.address != qrztools.Address():
state = ", " + data.address.state + " " if data.address.state else ""
address = "\n".join(
[x for x
in [data.address.attn, data.address.line1, data.address.line2 + state, data.address.zip]
if x]
)
else:
address = None
qsl = dict()
if data.qsl is not None:
qsl = {
"eQSL?": data.qsl.eqsl,
"Paper QSL?": data.qsl.mail,
"LotW?": data.qsl.lotw,
"QSL Info": data.qsl.info,
}
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,
"Country": data.address.country if data.address is not None else None,
"Address": data.address,
"Grid Square": data.grid,
"County": data.county,
"CQ Zone": data.cq_zone,
"ITU Zone": data.itu_zone,
"IOTA Designator": data.iota,
"Expires": f"{data.expire_date:%Y-%m-%d}" if data.expire_date is not None 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
}
"Previous Callsign": data.prev_call,
"License Class": data.lic_class,
"Trustee": data.trustee,
"Born": data.born,
} | qsl
def setup(bot):

View File

@ -2,7 +2,7 @@
Codes extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c (as ham.py)
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,7 +1,7 @@
"""
Contest Calendar extension for qrm
---
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,7 +1,7 @@
"""
Conversion extension for qrm
---
Copyright (C) 2020-2021 classabbyamp, 0x5c
Copyright (C) 2020-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
@ -187,7 +187,7 @@ def _calc_volt(db: float, ref: float):
# testing code
if __name__ == "__main__":
while(True):
while True:
try:
ip = input("> ").split()
initial = float(ip[0])

View File

@ -2,7 +2,7 @@
DXCC Prefix Lookup extension for qrm
---
Copyright (C) 2019-2020 classabbyamp, 0x5c (as lookup.py)
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,7 +1,7 @@
"""
Fun extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,7 +1,7 @@
"""
Grid extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,7 +1,7 @@
"""
Image extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -2,18 +2,16 @@
Land Weather extension for qrm
---
Copyright (C) 2019-2020 classabbyamp, 0x5c (as weather.py)
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
import re
from typing import List
import aiohttp
from discord import Embed
import discord.ext.commands as commands
import common as cmn
@ -102,7 +100,32 @@ class WeatherCog(commands.Cog):
Airports should be given as an \
[ICAO code](https://en.wikipedia.org/wiki/List_of_airports_by_IATA_and_ICAO_code)."""
await ctx.send(embed=await self.gen_metar_taf_embed(ctx, airport, hours, False))
embed = cmn.embed_factory(ctx)
airport = airport.upper()
if not re.fullmatch(r"\w(\w|\d){2,3}", airport):
embed.title = "Invalid airport given!"
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
url = f"https://aviationweather.gov/api/data/metar?ids={airport}&format=raw&taf=false&hours={hours}"
async with self.session.get(url) as r:
if r.status != 200:
raise cmn.BotHTTPError(r)
metar = await r.text()
if hours > 0:
embed.title = f"METAR for {airport} for the last {hours} hour{'s' if hours > 1 else ''}"
else:
embed.title = f"Current METAR for {airport}"
embed.description = "Data from [aviationweather.gov](https://www.aviationweather.gov/)."
embed.colour = cmn.colours.good
embed.description += f"\n\n```\n{metar}\n```"
await ctx.send(embed=embed)
@commands.command(name="taf", category=cmn.Cats.WEATHER)
async def taf(self, ctx: commands.Context, airport: str):
@ -110,57 +133,28 @@ class WeatherCog(commands.Cog):
Airports should be given as an \
[ICAO code](https://en.wikipedia.org/wiki/List_of_airports_by_IATA_and_ICAO_code)."""
await ctx.send(embed=await self.gen_metar_taf_embed(ctx, airport, 0, True))
async def gen_metar_taf_embed(self, ctx: commands.Context, airport: str, hours: int, taf: bool) -> Embed:
embed = cmn.embed_factory(ctx)
airport = airport.upper()
if re.fullmatch(r"\w(\w|\d){2,3}", airport):
metar = await self.get_metar_taf_data(airport, hours, taf)
if taf:
embed.title = f"Current TAF for {airport}"
elif hours > 0:
embed.title = f"METAR for {airport} for the last {hours} hour{'s' if hours > 1 else ''}"
else:
embed.title = f"Current METAR for {airport}"
embed.description = "Data from [aviationweather.gov](https://www.aviationweather.gov/metar/data)."
embed.colour = cmn.colours.good
data = "\n".join(metar)
embed.description += f"\n\n```\n{data}\n```"
else:
if not re.fullmatch(r"\w(\w|\d){2,3}", airport):
embed.title = "Invalid airport given!"
embed.colour = cmn.colours.bad
return embed
await ctx.send(embed=embed)
return
async def get_metar_taf_data(self, airport: str, hours: int, taf: bool) -> List[str]:
url = (f"https://www.aviationweather.gov/metar/data?ids={airport}&format=raw&hours={hours}"
f"&taf={'on' if taf else 'off'}&layout=off")
url = f"https://aviationweather.gov/api/data/taf?ids={airport}&format=raw&metar=true"
async with self.session.get(url) as r:
if r.status != 200:
raise cmn.BotHTTPError(r)
page = await r.text()
taf = await r.text()
# pare down to just the data
page = page.split("<!-- Data starts here -->")[1].split("<!-- Data ends here -->")[0].strip()
# split at <hr>s
data = re.split(r"<hr.*>", page, maxsplit=len(airport))
embed.title = f"Current TAF for {airport}"
embed.description = "Data from [aviationweather.gov](https://www.aviationweather.gov/)."
embed.colour = cmn.colours.good
embed.description += f"\n\n```\n{taf}\n```"
parsed = []
for sec in data:
if sec.strip():
for line in sec.split("\n"):
line = line.strip()
# remove HTML stuff
line = line.replace("<code>", "").replace("</code>", "")
line = line.replace("<strong>", "").replace("</strong>", "")
line = line.replace("<br/>", "\n").replace("&nbsp;", " ")
line = line.strip("\n")
parsed.append(line)
return parsed
await ctx.send(embed=embed)
def setup(bot: commands.Bot):

View File

@ -1,7 +1,7 @@
"""
Morse Code extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,7 +1,7 @@
"""
Prefixes Lookup extension for qrm
---
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,17 +1,17 @@
"""
Propagation extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
from datetime import datetime
from io import BytesIO
import aiohttp
import cairosvg
from datetime import datetime
import httpx
import discord
import discord.ext.commands as commands
@ -23,19 +23,22 @@ class PropagationCog(commands.Cog):
muf_url = "https://prop.kc2g.com/renders/current/mufd-normal-now.svg"
fof2_url = "https://prop.kc2g.com/renders/current/fof2-normal-now.svg"
gl_baseurl = "https://www.fourmilab.ch/cgi-bin/uncgi/Earth?img=ETOPO1_day-m.evif&dynimg=y&opt=-p"
n0nbh_sun_url = "http://www.hamqsl.com/solarsun.php"
n0nbh_sun_url = "https://www.hamqsl.com/solarsun.php"
noaa_drap_url = "https://services.swpc.noaa.gov/images/animations/d-rap/global/d-rap/latest.png"
def __init__(self, bot):
self.bot = bot
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
self.httpx_client: httpx.AsyncClient = bot.qrm.httpx_client
@commands.command(name="mufmap", aliases=["muf"], category=cmn.Cats.WEATHER)
async def mufmap(self, ctx: commands.Context):
"""Shows a world map of the Maximum Usable Frequency (MUF)."""
async with ctx.typing():
async with self.session.get(self.muf_url, headers={"Connection": "Upgrade", "Upgrade": "http/1.1"}) as r:
svg = await r.read()
out = BytesIO(cairosvg.svg2png(bytestring=svg))
resp = await self.httpx_client.get(self.muf_url)
await resp.aclose()
if resp.status_code != 200:
raise cmn.BotHTTPError(resp)
out = BytesIO(cairosvg.svg2png(bytestring=await resp.aread()))
file = discord.File(out, "muf_map.png")
embed = cmn.embed_factory(ctx)
embed.title = "Maximum Usable Frequency Map"
@ -47,9 +50,11 @@ class PropagationCog(commands.Cog):
async def fof2map(self, ctx: commands.Context):
"""Shows a world map of the Critical Frequency (foF2)."""
async with ctx.typing():
async with self.session.get(self.fof2_url, headers={"Connection": "Upgrade", "Upgrade": "http/1.1"}) as r:
svg = await r.read()
out = BytesIO(cairosvg.svg2png(bytestring=svg))
resp = await self.httpx_client.get(self.fof2_url)
await resp.aclose()
if resp.status_code != 200:
raise cmn.BotHTTPError(resp)
out = BytesIO(cairosvg.svg2png(bytestring=await resp.aread()))
file = discord.File(out, "fof2_map.png")
embed = cmn.embed_factory(ctx)
embed.title = "Critical Frequency (foF2) Map"
@ -67,18 +72,30 @@ class PropagationCog(commands.Cog):
embed.set_image(url=self.gl_baseurl + date_params)
await ctx.send(embed=embed)
@commands.command(name="solarweather", aliases=["solar", "bandconditions", "cond", "condx", "conditions"],
category=cmn.Cats.WEATHER)
@commands.command(name="solarweather", aliases=["solar"], category=cmn.Cats.WEATHER)
async def solarweather(self, ctx: commands.Context):
"""Gets a solar weather report."""
resp = await self.httpx_client.get(self.n0nbh_sun_url)
await resp.aclose()
if resp.status_code != 200:
raise cmn.BotHTTPError(resp)
img = BytesIO(await resp.aread())
file = discord.File(img, "solarweather.png")
embed = cmn.embed_factory(ctx)
embed.title = "☀️ Current Solar Weather"
if ctx.invoked_with in ["bandconditions", "cond", "condx", "conditions"]:
embed.add_field(name="⚠️ Deprecated Command Alias",
value=(f"This command has been renamed to `{ctx.prefix}solar`!\n"
"The alias you used will be removed in the next version."))
embed.colour = cmn.colours.good
embed.set_image(url=self.n0nbh_sun_url)
embed.set_image(url="attachment://solarweather.png")
await ctx.send(file=file, embed=embed)
@commands.command(name="drapmap", aliases=["drap"], category=cmn.Cats.WEATHER)
async def drapmap(self, ctx: commands.Context):
"""Gets the current D-RAP map for radio blackouts"""
embed = cmn.embed_factory(ctx)
embed.title = "D Region Absorption Predictions (D-RAP) Map"
embed.colour = cmn.colours.good
embed.description = \
"Image from [swpc.noaa.gov](https://www.swpc.noaa.gov/products/d-region-absorption-predictions-d-rap)"
embed.set_image(url=self.noaa_drap_url)
await ctx.send(embed=embed)

View File

@ -1,7 +1,7 @@
"""
Study extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
@ -159,13 +159,13 @@ class StudyCog(commands.Cog):
await cmn.add_react(q_msg, list(self.choices.values())[i])
await cmn.add_react(q_msg, cmn.emojis.question)
def check(reaction, user):
return (user.id != self.bot.user.id
and reaction.message.id == q_msg.id
and (str(reaction.emoji) in self.choices.values() or str(reaction.emoji) == cmn.emojis.question))
def check(ev):
return (ev.user_id != self.bot.user.id
and ev.message_id == q_msg.id
and (str(ev.emoji) in self.choices.values() or str(ev.emoji) == cmn.emojis.question))
try:
reaction, user = await self.bot.wait_for("reaction_add", timeout=300.0, check=check)
ev = await self.bot.wait_for("raw_reaction_add", timeout=300.0, check=check)
except asyncio.TimeoutError:
embed.set_field_at(1, name="Answers", value=answers_str_bolded, inline=False)
embed.set_field_at(2, name="Answer",
@ -174,16 +174,18 @@ class StudyCog(commands.Cog):
embed.colour = cmn.colours.timeout
await q_msg.edit(embed=embed)
else:
if str(reaction.emoji) == cmn.emojis.question:
if str(ev.emoji) == cmn.emojis.question:
embed.set_field_at(1, name="Answers", value=answers_str_bolded, inline=False)
embed.set_field_at(2, name="Answer",
value=f"The correct answer was {self.choices[question['answer']]}", inline=False)
embed.add_field(name="Answer Requested By", value=str(user), inline=False)
# only available in guilds, but it only makes sense there
if ev.member:
embed.add_field(name="Answer Requested By", value=str(ev.member), inline=False)
embed.colour = cmn.colours.timeout
await q_msg.edit(embed=embed)
else:
answers_str_checked = ""
chosen_ans = self.choices_inv[str(reaction.emoji)]
chosen_ans = self.choices_inv[str(ev.emoji)]
for letter, ans in answers.items():
answers_str_checked += f"{self.choices[letter]}"
if letter == question["answer"] == chosen_ans:
@ -195,19 +197,23 @@ class StudyCog(commands.Cog):
else:
answers_str_checked += f" {ans}\n"
if self.choices[question["answer"]] == str(reaction.emoji):
if self.choices[question["answer"]] == str(ev.emoji):
embed.set_field_at(1, name="Answers", value=answers_str_checked, inline=False)
embed.set_field_at(2, name="Answer", value=(f"{cmn.emojis.check_mark} "
f"**Correct!** The answer was {reaction.emoji}"))
embed.add_field(name="Answered By", value=str(user), inline=False)
f"**Correct!** The answer was {ev.emoji}"))
# only available in guilds, but it only makes sense there
if ev.member:
embed.add_field(name="Answered By", value=str(ev.member), inline=False)
embed.colour = cmn.colours.good
await q_msg.edit(embed=embed)
else:
embed.set_field_at(1, name="Answers", value=answers_str_checked, inline=False)
embed.set_field_at(2, name="Answer",
value=(f"{cmn.emojis.x} **Incorrect!** The correct answer was "
f"{self.choices[question['answer']]}, not {reaction.emoji}"))
embed.add_field(name="Answered By", value=str(user), inline=False)
f"{self.choices[question['answer']]}, not {ev.emoji}"))
# only available in guilds, but it only makes sense there
if ev.member:
embed.add_field(name="Answered By", value=str(ev.member), inline=False)
embed.colour = cmn.colours.bad
await q_msg.edit(embed=embed)

View File

@ -1,7 +1,7 @@
"""
TeX extension for qrm
---
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,7 +1,7 @@
"""
Time extension for qrm
---
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

10
info.py
View File

@ -1,18 +1,18 @@
"""
Static info about the bot.
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
authors = ("@ClassAbbyAmplifier#2229", "@0x5c#0639")
authors = ("@classabbyamp", "@0x5c.io")
description = """A bot with various useful ham radio-related functions, written in Python."""
license = "Québec Free and Open-Source Licence Strong Reciprocity (LiLiQ-R+), version 1.1"
contributing = """Check out the [source on GitHub](https://github.com/miaowware/qrm2). Contributions are welcome!
All issues and requests related to resources (including maps, band charts, data) should be added \
in [miaowware/qrm-resources](https://github.com/miaowware/qrm-resources)."""
release = "2.8.0"
All issues and requests related to resources (including maps, band charts, data) should be added \
in [miaowware/qrm-resources](https://github.com/miaowware/qrm-resources)."""
release = "2.9.2"
bot_server = "https://discord.gg/Ntbg3J4"

25
main.py
View File

@ -2,7 +2,7 @@
"""
qrm, a bot for Discord
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
@ -16,6 +16,7 @@ from datetime import datetime, time
from types import SimpleNamespace
from pathlib import Path
import httpx
import pytz
import discord
@ -49,9 +50,9 @@ connector = loop.run_until_complete(conn.new_connector())
# Defining the intents
intents = discord.Intents.none()
intents.guilds = True
intents.guild_messages = True
intents.dm_messages = True
intents.messages = True
intents.reactions = True
intents.message_content = True
member_cache = discord.MemberCacheFlags.from_intents(intents)
@ -69,6 +70,8 @@ bot.qrm = SimpleNamespace()
# Let's store stuff here.
bot.qrm.connector = connector
bot.qrm.debug_mode = debug_mode
# TODO: Add code to close the client
bot.qrm.httpx_client = httpx.AsyncClient()
# --- Commands ---
@ -81,7 +84,7 @@ async def _restart_bot(ctx: commands.Context):
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
print(f"[**] Restarting! Requested by {ctx.author}.")
exit_code = 42 # Signals to the wrapper script that the bot needs to be restarted.
await bot.logout()
await bot.close()
@bot.command(name="shutdown", aliases=["shut"], category=cmn.BoltCats.ADMIN)
@ -92,7 +95,7 @@ async def _shutdown_bot(ctx: commands.Context):
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
print(f"[**] Shutting down! Requested by {ctx.author}.")
exit_code = 0 # Signals to the wrapper script that the bot should not be restarted.
await bot.logout()
await bot.close()
@bot.group(name="extctl", aliases=["ex"], case_insensitive=True, category=cmn.BoltCats.ADMIN)
@ -123,10 +126,10 @@ async def _extctl_load(ctx: commands.Context, extension: str):
"""Loads an extension."""
try:
bot.load_extension(ext_dir + "." + extension)
except commands.ExtensionNotFound as e:
except discord.errors.ExtensionNotFound as e:
try:
bot.load_extension(plugin_dir + "." + extension)
except commands.ExtensionNotFound:
except discord.errors.ExtensionNotFound:
raise e
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
@ -140,10 +143,10 @@ async def _extctl_reload(ctx: commands.Context, extension: str):
await cmn.add_react(ctx.message, pika)
try:
bot.reload_extension(ext_dir + "." + extension)
except commands.ExtensionNotLoaded as e:
except discord.errors.ExtensionNotLoaded as e:
try:
bot.reload_extension(plugin_dir + "." + extension)
except commands.ExtensionNotLoaded:
except discord.errors.ExtensionNotLoaded:
raise e
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
@ -153,10 +156,10 @@ async def _extctl_unload(ctx: commands.Context, extension: str):
"""Unloads an extension."""
try:
bot.unload_extension(ext_dir + "." + extension)
except commands.ExtensionNotLoaded as e:
except discord.errors.ExtensionNotLoaded as e:
try:
bot.unload_extension(plugin_dir + "." + extension)
except commands.ExtensionNotLoaded:
except discord.errors.ExtensionNotLoaded:
raise e
await cmn.add_react(ctx.message, cmn.emojis.check_mark)

View File

@ -1,9 +1,9 @@
discord.py~=1.7.3
py-cord-dev[speed]==2.5.0rc5
ctyparser~=2.0
gridtools~=1.0
qrztools[async]~=1.0
callsignlookuptools[async]~=1.1
beautifulsoup4
pytz
cairosvg
requests
pydantic
httpx
pydantic~=2.5

View File

@ -1,7 +1,7 @@
"""
Resource schemas generator for qrm2.
---
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,7 +1,7 @@
"""
Information about callsigns for the prefixes command in hamcog.
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,7 +1,7 @@
"""
Information about callsigns for the CA prefixes command in hamcog.
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,7 +1,7 @@
"""
Information about callsigns for the US prefixes command in hamcog.
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,7 +1,7 @@
"""
A listing of hamstudy command resources
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

12
run.sh
View File

@ -17,7 +17,7 @@ fi
# Argument handling
_PASS_ERRORS=0
_NO_BOTENV=0
while [ ! -z "$1" ]; do
while [ -n "$1" ]; do
case $1 in
--pass-errors)
_PASS_ERRORS=1
@ -34,9 +34,9 @@ while [ ! -z "$1" ]; do
done
# If $PYTHON_BIN is not defined, default to 'python3.9'
if [ $_NO_BOTENV -eq 1 -a -z "$PYTHON_BIN" ]; then
PYTHON_BIN='python3.9'
# If $PYTHON_BIN is not defined, default to 'python3.11'
if [ $_NO_BOTENV -eq 1 ] && [ -z "$PYTHON_BIN" ]; then
PYTHON_BIN='python3.11'
fi
@ -69,9 +69,9 @@ echo "$0: Starting bot..."
# The loop
while true; do
if [ $_NO_BOTENV -eq 1 ]; then
"$PYTHON_BIN" main.py $@
"$PYTHON_BIN" main.py "$@"
else
./$BOTENV/bin/python3 main.py $@
"./$BOTENV/bin/python3" main.py "$@"
fi
err=$?
_message="$0: The bot exited with [$err]"

View File

@ -1,7 +1,7 @@
"""
Wrapper to handle aiohttp connector creation.
---
Copyright (C) 2020-2021 classabbyamp, 0x5c
Copyright (C) 2020-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,7 +1,7 @@
"""
Resources manager for qrm2.
---
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
@ -9,7 +9,7 @@ SPDX-License-Identifier: LiLiQ-Rplus-1.1
from pathlib import Path
import requests
import httpx
from utils.resources_models import Index
@ -23,13 +23,16 @@ class ResourcesManager:
def parse_index(self, index: str):
"""Parses the index."""
return Index.parse_raw(index)
return Index.model_validate_json(index)
def sync_fetch(self, filepath: str):
"""Fetches files in sync mode."""
self.print_msg(f"Fetching {filepath}", "sync")
with requests.get(self.url + filepath) as resp:
return resp.content
resp = httpx.get(self.url + filepath)
resp.raise_for_status()
r = resp.content
resp.close()
return r
def sync_start(self, basedir: Path) -> Index:
"""Takes cares of constructing the local resources repository and initialising the RM."""
@ -40,7 +43,7 @@ class ResourcesManager:
new_index: Index = self.parse_index(raw)
with (basedir / "index.json").open("wb") as file:
file.write(raw)
except (requests.RequestException, OSError) as ex:
except (httpx.RequestError, OSError) as ex:
self.print_msg(f"There was an issue fetching the index: {ex.__class__.__name__}: {ex}", "sync")
if (basedir / "index.json").exists():
self.print_msg("Old file exist, using old resources", "fallback")
@ -58,7 +61,7 @@ class ResourcesManager:
try:
with (basedir / file.filename).open("wb") as f:
f.write(self.sync_fetch(file.filename))
except (requests.RequestException, OSError) as ex:
except (httpx.RequestError, OSError) as ex:
ex_cls = ex.__class__.__name__
self.print_msg(f"There was an issue fetching {file.filename}: {ex_cls}: {ex}", "sync")
if not (basedir / file.filename).exists():

View File

@ -1,7 +1,7 @@
"""
Resource index models for qrm2.
---
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
@ -10,7 +10,7 @@ SPDX-License-Identifier: LiLiQ-Rplus-1.1
from collections.abc import Mapping
from datetime import datetime
from pydantic import BaseModel
from pydantic import BaseModel, RootModel
class File(BaseModel):
@ -22,18 +22,17 @@ class File(BaseModel):
return repr(self)
class Resource(BaseModel, Mapping):
# 'A Beautiful Hack' https://github.com/samuelcolvin/pydantic/issues/1802
__root__: dict[str, list[File]]
class Resource(RootModel, Mapping):
root: dict[str, list[File]]
def __getitem__(self, key: str) -> list[File]:
return self.__root__[key]
return self.root[key]
def __iter__(self):
return iter(self.__root__)
return iter(self.root)
def __len__(self) -> int:
return len(self.__root__)
return len(self.root)
# For some reason those were not the same???
def __str__(self) -> str:
@ -41,7 +40,7 @@ class Resource(BaseModel, Mapping):
# Make the repr more logical (despite the technical inaccuracy)
def __repr_args__(self):
return self.__root__.items()
return self.root.items()
class Index(BaseModel, Mapping):