Compare commits

...

120 Commits

Author SHA1 Message Date
classabbyamp
4b55ab49b7 .github/workflows/docker.yml: fix labels, again
Some checks failed
Docker Build and Deploy / Build and push docker images (push) Failing after 1m51s
Linting / flake8 (push) Successful in 1m6s
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
Some checks failed
Docker Build and Deploy / Build and push docker images (push) Has been cancelled
Linting / flake8 (push) Has been cancelled
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
Some checks are pending
Docker Build and Deploy / Build and push docker images (push) Waiting to run
Linting / flake8 (push) Waiting to run
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
0x5c
ef6f01d1a3
Merge pull request #450 from miaowware/bump
Bump version to 2.8.0
2022-06-24 17:44:24 -04:00
0x5c
91c5217d24
Bump version to 2.8.0 2022-06-24 17:42:41 -04:00
classabbyamp
4659cf2a48 exts/ae7q: remove extension
fixes #448
2022-06-24 17:17:26 -04:00
0x5c
d33dad9f89 Bump version to 2.7.6 2022-06-13 12:58:18 -04:00
0x5c
be083d2cc8
Merge pull request #446 from miaowware/muf-fof2-bug
Fix aiohttp/apache http2 bug in ?muf and ?fof2
2022-06-13 09:00:52 -04:00
0x5c
e2d1d1fc87
Fix aiohttp/apache http2 bug in ?muf and ?fof2
For more info, https://github.com/aio-libs/aiohttp/issues/3904
It is not possible to fix it by bumping aiohttp since it is pinned by another
dependency.
2022-06-13 08:45:17 -04:00
classabbyamp
68eaeff476 update to 2.7.5 2022-06-08 21:16:14 -07:00
0x5c
f690ebb357
Merge pull request #444 from miaowware/ci-lint
[CI/linting]: Change trigger and flake8 output
2022-05-17 21:23:46 -04:00
0x5c
51e571b97d
[CI/linting]: Change trigger and flake8 output
Fixes #443
2022-05-17 21:20:10 -04:00
classabbyamp
85ac05c337
Merge pull request #438 from miaowware/clog-enforce-workflow
CI updates
2021-11-06 00:22:55 -04:00
classabbyamp
718b2a7a80
add python 3.10 to linting checks 2021-11-05 23:34:26 -04:00
classabbyamp
0189db8792
add workflow to enforce changelog updating before merge 2021-11-05 23:31:14 -04:00
classabbyamp
80d6a989cc
move to new void docker image, clean up dockerfile (#436)
Co-authored-by: 0x5c <dev@0x5c.io>
2021-11-05 17:51:46 -04:00
classabbyamp
8f1782dcc0
ensure docker image id is lowercase (#437)
fixes #413
2021-11-05 17:31:45 -04:00
classabbyamp
aefca97e4f
bump to v2.7.4 and update discord.py (#434) 2021-10-07 03:07:16 -04:00
classabbyamp
bbd646a7ec
move docker image to void linux (#435) 2021-10-07 02:57:36 -04:00
classabbyamp
8433a7ade0
fix function signature of filter_commands() to ignore other args (#433)
discord.py changed the signature to add kwargs which are not used by
qrm's help command

fixes #432
2021-10-07 01:25:02 -04:00
classabbyamp
de0e25b09a
better ghcr login (#431) 2021-07-01 09:58:20 -04:00
classabbyamp
4fb1320b2d
add github sponsor link to donate command (#430) 2021-06-30 01:49:18 -04:00
0x5c
36acda1666
Merge pull request #429 from miaowware/licence
Changed the licence to LiLiQ-Rplus-1.1
2021-06-26 20:30:25 -04:00
0x5c
04ccd807cd
Changed the licence to LiLiQ-Rplus-1.1
Fixes #387
2021-06-26 20:23:55 -04:00
classabbyamp
aa7b72634b
Merge pull request #427 from miaowware/lint-dedup
update linting workflow to not run multiple times unnecessarily
2021-05-25 09:37:02 -04:00
Abigail G
9ee42529e2
update linting workflow to not run multiple times unnecessarily 2021-05-13 20:51:46 -04:00
classabbyamp
74df3ed1f1
Merge pull request #424 from miaowware/273-help-efix
fix issue with help cmd not showing all cmds, bump to 2.7.3
2021-04-12 19:38:27 -04:00
Abigail G
e7a1a4e5de
fix issue with help cmd not showing all cmds, bump to 2.7.3 2021-04-12 19:36:03 -04:00
classabbyamp
2eb183ff08
Merge pull request #423 from miaowware/272-rel
update changelog/version for 2.7.2
2021-04-12 19:06:03 -04:00
Abigail G
0cb6ccd285
update changelog/version for 2.7.2 2021-04-12 19:04:54 -04:00
classabbyamp
f55738a8a2
Merge pull request #422 from miaowware/help-e-fix
fix issue with help command/categories
2021-04-12 19:02:37 -04:00
Abigail G
abf79b844e
fix issue with help command/categories 2021-04-12 18:58:10 -04:00
0x5c
c3f002a9df
Merge pull request #421 from miaowware/cl
Bump version 2.7.1
2021-04-12 11:40:22 -04:00
0x5c
ff40f0caca
Bump version 2.7.1 2021-04-12 11:37:43 -04:00
0x5c
c0ad8d1108
Merge pull request #420 from miaowware/qrzfix
Fixed ?call crash on empty adress fields
2021-04-12 10:15:47 -04:00
0x5c
ce62c93d03
Fixed ?call crash on empty adress fields
Fixes #419
2021-04-12 10:00:11 -04:00
classabbyamp
c5c065bd47
Merge pull request #417 from miaowware/help-sort
make help command sort by category
2021-04-12 09:20:10 -04:00
0x5c
8f8b7f87de
Merge pull request #418 from miaowware/forms
Fixed issue form for new update
2021-04-11 18:08:56 -04:00
0x5c
144b288f09
Fixed issue form for new update 2021-04-11 17:59:34 -04:00
Abigail G
dc1efa7b0c
make help command sort by category
fixes #390
2021-04-11 15:20:42 -04:00
thxo
38ba8d9d0c
Tex command: more helpful error messages (#416)
* replace error message with common mistakes
* explicitly mention document mode in docstring
* add changelog

fixes #415 

Co-authored-by: thxo <thx@uw.edu>
2021-04-09 19:39:46 -04:00
0x5c
62b549619e
Merge pull request #412 from miaowware/bumpcl
Bumpcl
2021-04-03 18:22:45 -04:00
classabbyamp
5910fc5191
Merge pull request #411 from miaowware/copyright-update
bump copyright to include 2021
2021-04-03 18:21:34 -04:00
0x5c
2db9dee951
Bumped version 2.7.0 2021-04-03 18:19:38 -04:00
0x5c
e33d5f7213
Added missing changelog line for new feature 2021-04-03 18:13:54 -04:00
Abigail G
0a278fe14f
bump copyright to include 2021
fixes #384
2021-04-03 18:13:42 -04:00
0x5c
75bf8a638e
Merge pull request #410 from miaowware/clocks
Renamed clock emoji to "stopwatch", added proper clock
2021-04-03 18:08:39 -04:00
0x5c
9e36aab469
Renamed clock emoji to "stopwatch", added proper clock
Fixes #408
2021-04-03 18:06:14 -04:00
0x5c
cb1aed4278
Merge pull request #409 from miaowware/miltime
Added ?miltime command
2021-04-03 17:57:20 -04:00
0x5c
c27c0b8a48
Added ?miltime command
Fixes #377
2021-04-03 17:54:38 -04:00
0x5c
9d419b159d
Merge pull request #400 from miaowware/forms
[meta] Forms: added options for oficial instance users
2021-04-03 12:22:31 -04:00
0x5c
858223b148
[meta] Forms: added options for oficial instance users 2021-04-03 12:20:26 -04:00
0x5c
d36702f2b3
Merge pull request #399 from miaowware/forms
Added issue chooser config, +tag for bug reports
2021-04-02 03:41:59 -04:00
0x5c
6dfa35a8eb
Added issue chooser config, +tag for bug reports 2021-04-02 03:32:40 -04:00
0x5c
9f4bb04364
Merge pull request #397 from miaowware/forms
Fixed? error with bug report issue form
2021-04-02 02:31:53 -04:00
0x5c
3de71f7325
Fixed? error with bug report issue form 2021-04-02 02:29:30 -04:00
0x5c
22d60ed30f
Merge pull request #396 from miaowware/forms
Added bug report issue form, others to follow
2021-04-02 02:16:53 -04:00
0x5c
1cdbc66b5c
Added bug report issue form, others to follow 2021-04-02 02:09:59 -04:00
classabbyamp
ea7cc7823a
Merge pull request #394 from miaowware/copyrighting-some-copywrongs
update copyright name
2021-03-28 14:04:24 -04:00
Abigail G
22c2cab4d1
update copyright name 2021-03-28 13:57:03 -04:00
classabbyamp
63b1d81955
Merge pull request #393 from miaowware/releasethecats
Release the Cats!
2021-03-28 13:28:01 -04:00
0x5c
6e54a27f14
Release the Cats! - Reorganised extensions and recategorised commands
Fixes #389 - Main issue
Fixes #388 - Accidental, Satmatch deadcode removal
2021-03-28 09:50:51 -04:00
0x5c
f6d69f7498
Changed all categories to enums
- Accidentally fixed capitalisation of help command categories

Fixes #383
2021-03-28 02:38:36 -04:00
0x5c
c569fdb422
Merge pull request #381 from miaowware/maplan
Moved map/plan code to a single function
2021-03-27 18:07:14 -04:00
0x5c
98b2f30dcc
Moved map/plan code to a single function
Also changed ?bandplan aliases

Fixes #189
2021-03-27 18:00:28 -04:00
classabbyamp
1cb7ef5861
make ?xkcd only accept ints as arg (#379)
fixes #376

* Update CHANGELOG.md

Co-authored-by: 0x5c <dev@0x5c.io>
2021-03-26 08:04:54 -04:00
classabbyamp
bb3ee319b6
Merge pull request #378 from miaowware/dev-deps
add system dependencies to README and DEVELOPING
2021-03-26 03:49:16 -04:00
Abigail G
aaec204c43
update readme to add system dependencies 2021-03-24 21:13:10 -04:00
Abigail G
4c276bbc4d
add system depedencies to development guide 2021-03-24 21:09:04 -04:00
classabbyamp
89e26dbd2b
Merge pull request #375 from miaowware/efix-tex-resource
add latex resource to main.py
2021-03-23 17:56:26 -04:00
Abigail G
52995a8160
add latex resource to main.py 2021-03-22 23:49:34 -04:00
classabbyamp
4e572fa6d8
Merge pull request #371 from thxo/tex
add ?tex command
2021-03-22 23:40:55 -04:00
Howard Xiao
08d611e8c4
reassign authorship to qrm2 authors 2021-03-22 01:44:40 -07:00
thxo
a140f74273
address style feedback 2021-03-22 01:44:12 -07:00
Howard Xiao
137b275c78
implement ?tex command and add rTeX server option 2021-03-22 00:35:45 -07:00
0x5c
9f72a63202
Merge pull request #369 from miaowware/correct-readme-links
correct README links
2021-03-19 04:27:44 -04:00
classabbyamp
1eed538d2c
correct README link 2021-03-19 02:57:21 -04:00
46 changed files with 1131 additions and 1276 deletions

82
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,82 @@
name: Bug Report
description: Report a bug to help us improve this project
labels: [bug, 'needs triage']
body:
- id: description
type: textarea
attributes:
label: Bug description
description: A clear and concise description of what the bug is.
placeholder: There was a crash when...
validations:
required: true
- id: steps
type: textarea
attributes:
label: Steps to reproduce
description: Clear steps to reproduce the bug.
placeholder: |
1. Do the thing
2. Do the other thing
3. ???
4. Crash :(
validations:
required: true
- id: expected
type: textarea
attributes:
label: Expected behaviour
description: A clear and concise description of what you expected to happen.
placeholder: The app is supposed to show that thing.
validations:
required: true
- id: source
type: dropdown
attributes:
label: How did you download qrm?
options:
- Official Docker package
- Git (clone)
- Zip download on the releases package
- I do not know (user of official instance)
validations:
required: true
- id: runtime
type: dropdown
attributes:
label: How are you running qrm?
options:
- docker-compose
- Daemon (systemd, etc)
- Docker
- Command line (./run.sh)
- I do not know (user of official instance)
validations:
required: true
- id: version
type: input
attributes:
label: What qrm version are you running?
description: The version number, or the commit id if `dev` version. You can find both in `?info`.
placeholder: v2.5.2
validations:
required: true
- id: environment
type: textarea
attributes:
label: Environment
description: If relevant, include list of software used and versions
placeholder: |
- Linux 5.8
- Docker 19.04
- id: logs
type: textarea
attributes:
label: Logs
description: If you have a log associated with the bug (tracebacks, etc), paste it directly here.
render: none
- id: context
type: textarea
attributes:
label: Additional context, screenshots, etc
description: Add any other relevant context about the problem here.

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Feature Request
url: https://github.com/miaowware/qrm2/discussions/new?category=ideas
about: Suggest an idea for this project
- name: Support
url: https://github.com/miaowware/qrm2/discussions/new?category=support
about: Get help with qrm in GitHub Discussions
- name: Support (Discord)
url: https://discord.gg/SwyjdDN
about: Get help with qrm on Discord

12
.github/workflows/checks.yml vendored Normal file
View File

@ -0,0 +1,12 @@
name: "Checks"
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dangoslen/changelog-enforcer@v2

View File

@ -3,6 +3,10 @@
name: Docker Build and Deploy name: Docker Build and Deploy
on: on:
workflow_dispatch:
pull_request:
branches:
- master
push: push:
# Publish `master` as Docker `dev` image. # Publish `master` as Docker `dev` image.
branches: branches:
@ -11,61 +15,54 @@ on:
tags: tags:
- v* - v*
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
docker: docker:
name: Build and push docker images name: Build and push docker images
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
permissions:
packages: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: classabbyamp/treeless-checkout-action@v1
with:
ref: ${{ github.ref }}
- name: Write ref to file - name: Write ref to file
if: ${{ github.event_name != 'pull_request' }}
run: git rev-list -n 1 $GITHUB_REF > ./git_commit run: git rev-list -n 1 $GITHUB_REF > ./git_commit
- name: Build image - name: Docker metadata
id: build_image id: meta
run: | uses: docker/metadata-action@v4
IMAGE_NAME=${GITHUB_REPOSITORY#*/}
echo ::set-output name=image_name::$IMAGE_NAME
docker build . --file Dockerfile -t $IMAGE_NAME
- name: Log into Github Package Registry
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Tag image
id: tag_image
run: |
IMAGE_NAME=${{ steps.build_image.outputs.image_name }}
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
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: |
[[ "${{ steps.tag_image.outputs.version }}" != "dev" ]] && docker push ${{ steps.tag_image.outputs.image_id }}:latest || true
docker push ${{ steps.tag_image.outputs.image_id }}:${{ steps.tag_image.outputs.version }}
- name: Deploy official images
id: deploy_images
uses: satak/webrequest-action@v1
with: with:
url: ${{ secrets.DEPLOY_URL }} images: |
method: POST ghcr.io/${{ github.repository }}
headers: '{"Authentication": "Token ${{ secrets.DEPLOY_TOKEN }}"}' tags: |
payload: '{"version": "${{ steps.tag_image.outputs.version }}"}' 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
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,21 +1,22 @@
name: Linting name: Linting
on: [push,pull_request] on:
push:
branches:
- master
pull_request:
jobs: jobs:
flake8_py3: flake8:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-python@v1 - name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with: with:
python-version: 3.9 python-version: "3.9"
architecture: x64 architecture: x64
- name: Install flake8 - name: Install flake8
run: pip install flake8 run: pip install flake8
- name: Run flake8 - name: Run flake8
uses: suo/flake8-github-action@releases/v1 run: flake8 --format='::error title=flake8,file=%(path)s,line=%(row)d,col=%(col)d::[%(code)s] %(text)s'
with:
checkName: 'flake8_py3' # NOTE: this needs to be the same as the job name
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

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

View File

@ -7,6 +7,88 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [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).
## [2.7.6] - 2022-06-13
### Fixed
- Issue where `?muf` and `?fof2` would fail with an aiohttp error.
## [2.7.5] - 2022-06-08
### Changed
- Bumped ctyparser to 2.2.1.
## [2.7.4] - 2021-10-07
### Added
- a new way to support qrm's development.
### Changed
- Changed the licence to LiLiQ-Rplus-1.1.
- Moved official Docker image to Void Linux.
- Bumped discord.py to 1.7.3.
### Fixed
- Issue where the help command errored.
## [2.7.3] - 2021-04-12
### Fixed
- Issue where `?help` might not display all commands.
## [2.7.2] - 2021-04-12
### Fixed
- Issue where `?help` might not work for all people.
## [2.7.1] - 2021-04-12
### Added
- Helpful LaTeX hints for rendering errors in `?tex`.
### Fixed
- Bug where `?call` would crash if the found profile only had empty address fields.
## [2.7.0] - 2021-04-03
### Added
- `?tex` command to render a LaTeX expression.
- Configuration option to use another rTeX instance for `?tex`.
- `?miltime` command to show the current time in all 25 military timezones.
### Changed
- Main name and aliases of `?bandplan`.
- Recategorized the commands.
### Fixed
- Lack of input sanitisation in `?xkcd`.
- Incorrect capitalisation of the categories in the `?help` command.
## [2.6.0] - 2021-03-18 ## [2.6.0] - 2021-03-18
### Added ### Added
- MUF and foF2 maps from [prop.kc2g.com](https://prop.kc2g.com/). - MUF and foF2 maps from [prop.kc2g.com](https://prop.kc2g.com/).
@ -77,7 +159,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [2.3.1] - 2020-04-02 ## [2.3.1] - 2020-04-02
### Fixed ### Fixed
- Wordlist containing innappropriate words. - Wordlist containing inappropriate words.
## [2.3.0] - 2020-03-30 ## [2.3.0] - 2020-03-30
@ -173,7 +255,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 1.0.0 - 2019-07-31 [YANKED] ## 1.0.0 - 2019-07-31 [YANKED]
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.6.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
[2.7.4]: https://github.com/miaowware/qrm2/releases/tag/v2.7.4
[2.7.3]: https://github.com/miaowware/qrm2/releases/tag/v2.7.3
[2.7.2]: https://github.com/miaowware/qrm2/releases/tag/v2.7.2
[2.7.1]: https://github.com/miaowware/qrm2/releases/tag/v2.7.1
[2.7.0]: https://github.com/miaowware/qrm2/releases/tag/v2.7.0
[2.6.0]: https://github.com/miaowware/qrm2/releases/tag/v2.6.0 [2.6.0]: https://github.com/miaowware/qrm2/releases/tag/v2.6.0
[2.5.1]: https://github.com/miaowware/qrm2/releases/tag/v2.5.1 [2.5.1]: https://github.com/miaowware/qrm2/releases/tag/v2.5.1
[2.5.0]: https://github.com/miaowware/qrm2/releases/tag/v2.5.0 [2.5.0]: https://github.com/miaowware/qrm2/releases/tag/v2.5.0

339
COPYING
View File

@ -1,339 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@ -8,6 +8,7 @@
1. Make sure the `master` branch is up to date, then make yourself a new branch with a descriptive name. 1. Make sure the `master` branch is up to date, then make yourself a new branch with a descriptive name.
1. Once the forked repo is cloned and on the proper branch, you can set up the development environment. 1. Once the forked repo is cloned and on the proper branch, you can set up the development environment.
1. Install python 3.9 or higher. 1. Install python 3.9 or higher.
1. Install `libcairo` and `libjpeg`. Package names may vary by distro or OS.
1. Run `make dev-install`. 1. Run `make dev-install`.
This should install everything you need to develop and run qrm. This should install everything you need to develop and run qrm.
1. [Create a bot and token][2], and add it to `data/keys.py`. 1. [Create a bot and token][2], and add it to `data/keys.py`.

View File

@ -1,25 +1,31 @@
FROM python:3.9-slim FROM ghcr.io/void-linux/void-musl-full
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
ENV PYTHON_BIN python3 ARG REPOSITORY=https://repo-fastly.voidlinux.org/current
ARG PKGS="cairo libjpeg-turbo"
ARG UID 1000
ARG GID 1000
RUN \ RUN \
apt-get update && \ echo "**** update system ****" && \
echo "**** install runtime packages ****" && \ xbps-install -Suy xbps -R ${REPOSITORY} && \
apt-get install -y --no-install-recommends \ xbps-install -uy -R ${REPOSITORY} && \
libcairo2 \ echo "**** install system packages ****" && \
libjpeg62-turbo \ xbps-install -y -R ${REPOSITORY} ${PKGS} python3.11 && \
python-lxml \
&& \
echo "**** install pip packages ****" && \ echo "**** install pip packages ****" && \
pip3 install -U pip setuptools wheel && \ python3.11 -m venv botenv && \
pip3 install -r requirements.txt && \ botenv/bin/pip install -U pip setuptools wheel && \
botenv/bin/pip install -r requirements.txt && \
echo "**** clean up ****" && \ echo "**** clean up ****" && \
rm -rf \ rm -rf \
/root/.cache \ /root/.cache \
/tmp/* \ /tmp/* \
/var/lib/apt/lists/* /var/cache/xbps/*
CMD ["/bin/sh", "run.sh", "--pass-errors", "--no-botenv"] ENV PYTHONUNBUFFERED 1
USER $UID:$GID
CMD ["/bin/sh", "run.sh", "--pass-errors"]

178
LICENCE Normal file
View File

@ -0,0 +1,178 @@
SPDX-License-Identifier: LiLiQ-Rplus-1.1
---- English version follows ----
Licence Libre du Québec Réciprocité forte (LiLiQ-R+)
Version 1.1
1. Préambule
Cette licence s'applique à tout logiciel distribué dont le titulaire du droit d'auteur précise qu'il est sujet aux termes de la Licence Libre du Québec Réciprocité forte (LiLiQ-R+) (ci-après appelée la « licence »).
2. Définitions
Dans la présente licence, à moins que le contexte n'indique un sens différent, on entend par:
« concédant » : le titulaire du droit d'auteur sur le logiciel, ou toute personne dûment autorisée par ce dernier à accorder la présente licence;
« contributeur » : le titulaire du droit d'auteur ou toute personne autorisée par ce dernier à soumettre au concédant une contribution. Un contributeur dont sa contribution est incorporée au logiciel est considéré comme un concédant en regard de sa contribution;
« contribution » : tout logiciel original, ou partie de logiciel original soumis et destiné à être incorporé dans le logiciel;
« distribution » : le fait de délivrer une copie du logiciel;
« licencié » : toute personne qui possède une copie du logiciel et qui exerce les droits concédés par la licence;
« logiciel » : une œuvre protégée par le droit d'auteur, telle qu'un programme d'ordinateur et sa documentation, pour laquelle le titulaire du droit d'auteur a précisé qu'elle est sujette aux termes de la présente licence;
« logiciel dérivé » : tout logiciel original réalisé par un licencié, autre que le logiciel ou un logiciel modifié, qui produit ou reproduit la totalité ou une partie importante du logiciel;
« logiciel modifié » : toute modification par un licencié de l'un des fichiers source du logiciel ou encore tout nouveau fichier source qui incorpore le logiciel ou une partie importante de ce dernier.
3. Licence de droit d'auteur
Sous réserve des termes de la licence, le concédant accorde au licencié une licence non exclusive et libre de redevances lui permettant dexercer les droits suivants sur le logiciel :
 Produire ou reproduire la totalité ou une partie importante;
 Exécuter ou représenter la totalité ou une partie importante en public;
 Publier la totalité ou une partie importante.
Cette licence est accordée sans limite territoriale et sans limite de temps.
L'exercice complet de ces droits est sujet à la distribution par le concédant du code source du logiciel, lequel doit être sous une forme permettant d'y apporter des modifications. Le concédant peut aussi distribuer le logiciel accompagné d'une offre de distribuer le code source du logiciel, sans frais supplémentaires, autres que ceux raisonnables afin de permettre la livraison du code source. Cette offre doit être valide pendant une durée raisonnable.
4. Distribution
Le licencié peut distribuer des copies du logiciel, d'un logiciel modifié ou dérivé, sous réserve de respecter les conditions suivantes :
 Le logiciel doit être accompagné d'un exemplaire de cette licence;
 Si le logiciel a été modifié, le licencié doit en faire la mention, de préférence dans chacun des fichiers modifiés dont la nature permet une telle mention;
 Les étiquettes ou mentions faisant état des droits d'auteur, des marques de commerce, des garanties ou de la paternité concernant le logiciel ne doivent pas être modifiées ou supprimées, à moins que ces étiquettes ou mentions ne soient inapplicables à un logiciel modifié ou dérivé donné.
4.1. Réciprocité
Chaque fois que le licencié distribue le logiciel, le concédant offre au récipiendaire une concession sur le logiciel selon les termes de la présente licence. Le licencié doit offrir une concession selon les termes de la présente licence pour tout logiciel modifié ou dérivé qu'il distribue.
Chaque fois que le licencié distribue le logiciel, un logiciel modifié, ou un logiciel dérivé, ce dernier doit assumer l'obligation d'en distribuer le code source, de la manière prévue au troisième alinéa de l'article 3.
4.2. Compatibilité
Dans la mesure où le licencié souhaite distribuer un logiciel modifié ou dérivé combiné à un logiciel assujetti à une licence compatible, mais dont il ne serait pas possible d'en respecter les termes, le concédant offre, en plus de la présente concession, une concession selon les termes de cette licence compatible.
Un licencié qui est titulaire exclusif du droit d'auteur sur le logiciel assujetti à une licence compatible ne peut pas se prévaloir de cette offre. Il en est de même pour toute autre personne dûment autorisée à sous-licencier par le titulaire exclusif du droit d'auteur sur le logiciel assujetti à une licence compatible.
Est considérée comme une licence compatible toute licence libre approuvée ou certifiée par la Free Software Foundation ou l'Open Source Initiative, dont le niveau de réciprocité est comparable à celui de la présente licence, sans toutefois être moindre, notamment :
 Common Public License Version 1.0 (CPL-1.0)
 Contrat de licence de logiciel libre CeCILL, version 2.1 (CECILL-2.1)
 Eclipse Public License - v 1.0 (EPL-1.0)
 European Union Public License, version 1.1 (EUPL v. 1.1)
 GNU General Public License Version 2 (GNU GPLv2)
 GNU General Public License Version 3 (GNU GPLv3)
5. Contributions
Sous réserve d'une entente distincte, toute contribution soumise par un contributeur au concédant pour inclusion dans le logiciel sera soumise aux termes de cette licence.
6. Marques de commerce
La licence n'accorde aucune permission particulière qui permettrait d'utiliser les marques de commerce du concédant, autre que celle requise permettant d'identifier la provenance du logiciel.
7. Garanties
Sauf mention contraire, le concédant distribue le logiciel sans aucune garantie, aux risques et périls de l'acquéreur de la copie du logiciel, et ce, sans assurer que le logiciel puisse répondre à un besoin particulier ou puisse donner un résultat quelconque.
Sans lier le concédant d'une quelconque manière, rien n'empêche un licencié d'offrir ou d'exclure des garanties ou du support.
8. Responsabilité
Le licencié est responsable de tout préjudice résultant de l'exercice des droits accordés par la licence.
Le concédant ne saurait être tenu responsable du préjudice subi par le licencié ou par des tiers, pour quelque cause que ce soit en lien avec la licence et les droits qui y sont accordés.
9. Résiliation
La présente licence est résiliée de plein droit dès que les droits qui y sont accordés ne sont pas exercés conformément aux termes qui y sont stipulés.
Toutefois, si le défaut est corrigé dans un délai de 30 jours de sa prise de connaissance par la personne en défaut, et qu'il s'agit du premier défaut, la licence est accordée de nouveau.
Pour tout défaut subséquent, le consentement exprès du concédant est nécessaire afin que la licence soit accordée de nouveau.
10. Version de la licence
Le Centre de services partagés du Québec, ses ayants cause ou toute personne qu'il désigne, peuvent diffuser des versions révisées ou modifiées de cette licence. Chaque version recevra un numéro unique. Si un logiciel est déjà soumis aux termes d'une version spécifique, c'est seulement cette version qui liera les parties à la licence.
Le concédant peut aussi choisir de concéder la licence sous la version actuelle ou toute version ultérieure, auquel cas le licencié peut choisir sous quelle version la licence lui est accordée.
11. Divers
Dans la mesure où le concédant est un ministère, un organisme public ou une personne morale de droit public, créés en vertu d'une loi de l'Assemblée nationale du Québec, la licence est régie par le droit applicable au Québec et en cas de contestation, les tribunaux du Québec seront seuls compétents.
La présente licence peut être distribuée sans conditions particulières. Toutefois, une version modifiée doit être distribuée sous un nom différent. Toute référence au Centre de services partagés du Québec, et, le cas échéant, ses ayant cause, doit être retirée, autre que celle permettant d'identifier la provenance de la licence.
----
Québec Free and Open-Source Licence Strong Reciprocity (LiLiQ-R+)
Version 1.1
1. Preamble
This licence applies to any distributed software stipulated by its copyright owner to be subject to the terms of the Québec Free and Open-Source Licence Strong Reciprocity (LiLiQ-R+) (hereinafter referred to as the “licence”).
2. Definitions
Unless the context indicates otherwise, the following terms are used in this licence:
“contribution”: any original software or part of original software submitted and intended to be integrated into the software;
“contributor”: the copyright owner or any person authorized by the copyright owner to submit a contribution to the licensor. A contributor whose contribution is integrated into the software is considered a licensor with respect to that contribution;
“derived software”: any original software developed by a licensee, other than the software or modified software, that produces or reproduces all or a substantial part of the software;
“distribution”: the act of delivering a copy of the software;
“licensee”: any person possessing a copy of the software who exercises the rights granted by the licence;
“licensor”: the software copyright owner or any person duly authorized by the copyright owner to grant this licence;
“modified software”: any modification made by a licensee to one of the softwares source code files, or any new source code file that integrates the software or a substantial part of it;
“software”: a copyright-protected work such as a computer program and its documentation, stipulated by the copyright owner to be subject to the terms of this licence.
3. Copyright licence
Subject to the terms of this licence, the licensor grants the licensee a non-exclusive, royalty-free licence allowing the licensee to exercise the following rights regarding the software:
(1) Produce or reproduce the software or a substantial part thereof;
(2) Perform the software or any substantial part of it in public;
(3) Publish the software or any substantial part of it.
This licence is granted on a world-wide, perpetual basis.
Full exercise of these rights is subject to distribution by the licensor of the software source code in a form allowing it to be modified. The licensor may also distribute the software, along with an offer to distribute the software source code, without additional charges other than reasonable charges for delivery of the source code. That offer must be valid for a reasonable period of time.
4. Distribution
The licensee may distribute copies of the software, modified software or derived software, subject to the following conditions:
(1) The software must be accompanied by a copy of this licence.
(2) If the software has been modified, the licensee must mention this, preferably in every modified file that allows for such a mention.
(3) Software copyright, trademark, warranty or attribution labels or notices must not be modified or removed, unless the labels or notices do not apply to specific modified or derived software.
4.1. Reciprocity
Every time the licensee distributes the software, the licensor grants the recipient an interest in the software in accordance with the terms of this licence. The licensee must grant an interest in accordance with the terms of this licence for any modified or derived software distributed.
Every time the licensee distributes the software, or modified or derived software, the licensee is obliged to distribute its source code in the manner prescribed in the third paragraph of section 3.
4.2. Compatibility
To the extent that the licensee wishes to distribute modified or derived software combined with software subject to a compatible licence whose terms cannot possibly be fulfilled, the licensor offers, in addition to this interest, an interest in compliance with the terms of the compatible licence.
A licensee who is the exclusive copyright owner of the software subject to a compatible licence is not eligible for this offer. The same applies to any other person duly authorized to sub-license by the exclusive copyright owner of the software subject to a compatible licence.
A compatible licence is considered to be any free or open-source licence approved or certified by the Free Software Foundation or the Open Source Initiative, whose level of reciprocity is comparable to that of this licence, without being less so, in particular:
(1) Common Public License Version 1.0 (CPL-1.0)
(2) Contrat de licence de logiciel libre CeCILL, version 2.1 (CECILL-2.1)
(3) Eclipse Public License - v 1.0 (EPL-1.0)
(4) European Union Public License, version 1.1 (EUPL v. 1.1)
(5) GNU General Public License Version 2 (GNU GPLv2)
(6) GNU General Public License Version 3 (GNU GPLv3)
5. Contributions
Subject to a separate agreement, every contribution submitted by a contributor to the licensor for inclusion in the software is subject to the terms of this licence.
6. Trademarks
This licence does not grant any special permission to use the licensors trademarks, except as needed to describe the origin of the software.
7. Warranties
Unless otherwise specified, the licensor distributes the software without any warranty, at the risk of the acquirer of a copy of the software, and without any warranty that the software is suited to any specific need or will yield any specific results.
Without binding the licensor in any way, nothing prevents a licensee from offering or excluding warranties or support.
8. Liability
The licensee is liable for any prejudice resulting from the exercise of the rights granted under the licence.
The licensor cannot be held liable for any prejudice sustained by the licensee or third parties for any reason whatsoever related to the licence and the rights it grants.
9. Termination
This licence is terminated as of right should the rights it grants fail to be exercised in accordance with the terms of the licence.
However, if the failure is remedied within 30 days after its discovery by the person in default and it is the first failure, the licence will be granted once again.
For any subsequent failure, the licensors express consent is required for the licence to be granted once again.
10. Licence version
The Centre de services partagés du Québec, its successors or any person it designates may release revised or modified versions of this licence. Each version will be given a unique number. If software is already subject to the terms of a specific version, the parties to the licence will be bound solely by that version.
The licensor may also choose to grant the licence in its current version or any subsequent version, in which case the licensee may choose the license version to be granted.
11. Miscellaneous
To the extent that the licensor is a government department, public body or legal person established in the public interest and created under a law of the National Assembly of Québec, the licence is governed by the laws applicable in Québec and, in the event of a dispute, the courts of Québec have sole jurisdiction.
This licence may be distributed without any special conditions. However, a modified version must be distributed under a different name. Any reference to the Centre de services partagés du Québec or its successors, where applicable, must be withdrawn, except as needed to describe the origin of the licence.

View File

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

View File

@ -23,14 +23,11 @@ This is the easiest method for running the bot without any modifications.
version: '3' version: '3'
services: services:
qrm2: qrm2:
image: "docker.pkg.github.com/miaowware/qrm2/qrm2:latest" image: "ghcr.io/miaowware/qrm2:latest"
restart: on-failure restart: on-failure
volumes: volumes:
- "./data:/app/data:rw" - "./data:/app/data:rw"
environment:
- PYTHONUNBUFFERED=1
``` ```
*Note that Github's registry requires [a few extra steps](https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages) during the initial setup.*
3. Create a subdirectory named `data`. 3. Create a subdirectory named `data`.
@ -64,8 +61,6 @@ This is the easiest method to run the bot with modifications.
restart: on-failure restart: on-failure
volumes: volumes:
- "./data:/app/data:rw" - "./data:/app/data:rw"
environment:
- PYTHONUNBUFFERED=1
``` ```
3. Create a subdirectory named `data`. 3. Create a subdirectory named `data`.
@ -112,4 +107,4 @@ This methods is not very nice to use.
Where `[image]` is either of: Where `[image]` is either of:
- `qrm2:local-latest` if you are building your own. - `qrm2:local-latest` if you are building your own.
- `docker.pkg.github.com/miaowware/qrm2/qrm2:latest` if you want to use the prebuilt image. - `ghcr.io/miaowware/qrm2:latest` if you want to use the prebuilt image.

View File

@ -18,6 +18,8 @@ Requires Python 3.9 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). 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).
Install `libcairo` and `libjpeg` (package names may vary by distro or OS). Then run:
``` ```
$ make install $ make install
``` ```
@ -30,13 +32,13 @@ $ run.sh
## Contributing ## Contributing
Check out the [contribution guidelines](/CONTRIBUTING.md) for more information about how to contribute to this project. Check out the [development](/DEVELOPING.md) and [contribution guidelines](https://github.com/miaowware/.github/blob/master/CONTRIBUTING.md) for more information about how to contribute to this project.
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). 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).
## Copyright ## Copyright
Copyright (C) 2019-2020 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This program is released under the terms of the GNU General Public License, This program is released under the terms of the *Québec Free and Open-Source Licence Strong Reciprocity (LiLiQ-R+)*, version 1.1.
version 2. See `COPYING` for full license text. See [`LICENCE`](LICENCE) for full license text (Français / English).

View File

@ -1,32 +1,33 @@
""" """
Common tools for the bot. Common tools for the bot.
--- ---
Copyright (C) 2019-2021 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
import collections import collections
import enum
import json import json
import re import re
import traceback import traceback
from datetime import datetime from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
from typing import Union from typing import Union
import aiohttp import aiohttp
import httpx
import discord import discord
import discord.ext.commands as commands import discord.ext.commands as commands
from discord import Emoji, Reaction, PartialEmoji from discord import Emoji, PartialEmoji
import data.options as opt import data.options as opt
__all__ = ["colours", "cat", "emojis", "paths", "ImageMetadata", "ImagesGroup", __all__ = ["colours", "BoltCats", "Cats", "emojis", "paths", "ImageMetadata", "ImagesGroup",
"embed_factory", "error_embed_factory", "add_react", "check_if_owner"] "embed_factory", "error_embed_factory", "add_react", "check_if_owner"]
@ -39,16 +40,25 @@ colours = SimpleNamespace(
timeout=0xF26522, timeout=0xF26522,
) )
class BoltCats(enum.Enum):
OTHER = "Other"
INFO = "Bot Information"
ADMIN = "Bot Control"
# meow # meow
cat = SimpleNamespace( class Cats(enum.Enum):
lookup="Information Lookup", CALC = "Calculators"
fun="Fun", CODES = "Code References and Tools"
maps="Mapping", FUN = "Fun"
ref="Reference", LOOKUP = "Information Lookup"
study="Exam Study", REF = "Reference"
weather="Land and Space Weather", STUDY = "Exam Study"
admin="Bot Control", TIME = "Time and Time Zones"
) UTILS = "Utilities"
WEATHER = "Land and Space Weather"
emojis = SimpleNamespace( emojis = SimpleNamespace(
check_mark="", check_mark="",
@ -57,7 +67,8 @@ emojis = SimpleNamespace(
question="", question="",
no_entry="", no_entry="",
bangbang="‼️", bangbang="‼️",
clock="", clock="🕗",
stopwatch="",
a="🇦", a="🇦",
b="🇧", b="🇧",
c="🇨", c="🇨",
@ -100,7 +111,7 @@ class ImagesGroup(collections.abc.Mapping):
def __len__(self): def __len__(self):
return len(self._images) return len(self._images)
def __getitem__(self, key: str): def __getitem__(self, key: str) -> ImageMetadata:
return self._images[key] return self._images[key]
def __iter__(self): def __iter__(self):
@ -115,12 +126,16 @@ class ImagesGroup(collections.abc.Mapping):
class BotHTTPError(Exception): class BotHTTPError(Exception):
"""Raised whan a requests fails (status != 200) in a command.""" """Raised whan a requests fails (status != 200) in a command."""
def __init__(self, response: aiohttp.ClientResponse): def __init__(self, response: aiohttp.ClientResponse | httpx.Response):
msg = f"Request failed: {response.status} {response.reason}" if isinstance(response, aiohttp.ClientResponse):
super().__init__(msg)
self.response = response
self.status = response.status self.status = response.status
self.reason = response.reason 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
# --- Converters --- # --- Converters ---
@ -150,8 +165,9 @@ class GlobalChannelConverter(commands.IDConverter):
def embed_factory(ctx: commands.Context) -> discord.Embed: def embed_factory(ctx: commands.Context) -> discord.Embed:
"""Creates an embed with neutral colour and standard footer.""" """Creates an embed with neutral colour and standard footer."""
embed = discord.Embed(timestamp=datetime.utcnow(), colour=colours.neutral) embed = discord.Embed(timestamp=datetime.now(timezone.utc), colour=colours.neutral)
embed.set_footer(text=str(ctx.author), icon_url=str(ctx.author.avatar_url)) if ctx.author:
embed.set_footer(text=str(ctx.author), icon_url=str(ctx.author.display_avatar))
return embed return embed
@ -168,7 +184,7 @@ def error_embed_factory(ctx: commands.Context, exception: Exception, debug_mode:
return embed 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: try:
await msg.add_reaction(react) await msg.add_reaction(react)
except discord.Forbidden: except discord.Forbidden:

View File

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

View File

@ -1,436 +1,28 @@
""" """
ae7q extension for qrm ae7q extension for qrm
--- ---
Copyright (C) 2019-2020 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
# Test callsigns:
# KN8U: active, restricted
# AB2EE: expired, restricted
# KE8FGB: assigned once, no restrictions
# KV4AAA: unassigned, no records
# KC4USA: reserved, no call history, *but* has application history
import aiohttp
from bs4 import BeautifulSoup
import discord.ext.commands as commands import discord.ext.commands as commands
import common as cmn from common import embed_factory, colours
class AE7QCog(commands.Cog): class AE7QCog(commands.Cog):
def __init__(self, bot: commands.Bot): @commands.command(name="ae7q", aliases=["ae"], case_insensitive=True)
self.bot = bot async def _ae7q_lookup(self, ctx: commands.Context, *, _):
self.session = aiohttp.ClientSession(connector=bot.qrm.connector) """Removed in v2.8.0"""
embed = embed_factory(ctx)
@commands.group(name="ae7q", aliases=["ae"], case_insensitive=True, category=cmn.cat.lookup) embed.colour = colours.bad
async def _ae7q_lookup(self, ctx: commands.Context): embed.title = "Command removed"
"""Looks up a callsign, FRN, or Licensee ID on [ae7q.com](http://ae7q.com/).""" embed.description = ("This command was removed in v2.8.0.\n"
if ctx.invoked_subcommand is None: "For context, see [this Github issue](https://github.com/miaowware/qrm2/issues/448)")
await ctx.send_help(ctx.command)
@_ae7q_lookup.command(name="call", aliases=["c"], category=cmn.cat.lookup)
async def _ae7q_call(self, ctx: commands.Context, callsign: str):
"""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)
if not callsign.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q History for Callsign"
embed.colour = cmn.colours.bad
embed.description = "Not a valid callsign!"
await ctx.send(embed=embed) await ctx.send(embed=embed)
return
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:])
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q 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[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 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)
@_ae7q_lookup.command(name="trustee", aliases=["t"], category=cmn.cat.lookup)
async def _ae7q_trustee(self, ctx: commands.Context, callsign: str):
"""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)
if not callsign.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q Trustee History for Callsign"
embed.colour = cmn.colours.bad
embed.description = "Not a valid callsign!"
await ctx.send(embed=embed)
return
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:])
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q Trustee 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[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 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)
@_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)
if not callsign.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q Application History for Callsign"
embed.colour = cmn.colours.bad
embed.description = "Not a valid callsign!"
await ctx.send(embed=embed)
return
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}"
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="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)
if not frn.isdecimal():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q History for FRN"
embed.colour = cmn.colours.bad
embed.description = "Not a valid FRN!"
await ctx.send(embed=embed)
return
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:])
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q History for FRN {frn}"
embed.colour = cmn.colours.good
embed.url = base_url + frn
# 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 len(table) > 3:
embed.description = f"Records 1 to 3 of {len(table)}. See ae7q.com for more..."
await ctx.send(embed=embed)
@_ae7q_lookup.command(name="licensee", aliases=["l"], category=cmn.cat.lookup)
async def _ae7q_licensee(self, ctx: commands.Context, licensee_id: str):
"""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)
if not licensee_id.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q History for Licensee"
embed.colour = cmn.colours.bad
embed.description = "Not a valid licensee ID!"
await ctx.send(embed=embed)
return
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:])
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q History for Licensee {licensee_id}"
embed.colour = cmn.colours.good
embed.url = base_url + licensee_id
# 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 len(table) > 3:
embed.description = f"Records 1 to 3 of {len(table)}. See ae7q.com 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): def setup(bot: commands.Bot):
bot.add_cog(AE7QCog(bot)) bot.add_cog(AE7QCog())

View File

@ -1,20 +1,20 @@
""" """
Base extension for qrm Base extension for qrm
--- ---
Copyright (C) 2019-2020 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
import random import random
import re import re
from typing import Union from typing import Union, Iterable
import pathlib import pathlib
import discord import discord
import discord.ext.commands as commands import discord.ext.commands as commands
from discord.ext.commands import Command, CommandError
import info import info
import common as cmn import common as cmn
@ -23,16 +23,60 @@ from data import options as opt
class QrmHelpCommand(commands.HelpCommand): class QrmHelpCommand(commands.HelpCommand):
def __init__(self): def __init__(self):
super().__init__(command_attrs={"help": "Shows help about qrm or a command", "aliases": ["h"]}) super().__init__(command_attrs={
"help": "Shows help about qrm or a command",
"aliases": ["h"],
"category": cmn.BoltCats.INFO
})
self.verify_checks = True self.verify_checks = True
self.context: commands.Context self.context: commands.Context
async def filter_commands(self, commands: Iterable[Command], **kwargs) -> list[Command]:
def sort_by_cat(cmds):
ret = []
bolt_cmds = {}
for c in cmds:
cat = c.__original_kwargs__.get("category", cmn.BoltCats.OTHER)
if isinstance(cat, cmn.BoltCats):
if cat in bolt_cmds:
bolt_cmds[cat].append(c)
else:
bolt_cmds[cat] = [c]
else:
ret.append(c)
ret.sort(key=lambda c: c.__original_kwargs__["category"].name)
for cat in cmn.BoltCats:
if cat in bolt_cmds:
ret += sorted(bolt_cmds[cat], key=lambda c: c.name)
return ret
iterator = commands if self.show_hidden else filter(lambda c: not c.hidden, commands)
if not self.verify_checks:
return sort_by_cat(iterator)
async def predicate(cmd):
try:
return await cmd.can_run(self.context)
except CommandError:
return False
cmds = []
for cmd in iterator:
if await predicate(cmd):
cmds.append(cmd)
return sort_by_cat(cmds)
async def get_bot_mapping(self): async def get_bot_mapping(self):
bot = self.context.bot bot = self.context.bot
mapping = {} mapping = {}
for cmd in await self.filter_commands(bot.commands, sort=True): for cmd in await self.filter_commands(bot.commands):
cat = cmd.__original_kwargs__.get("category", None) cat = cmd.__original_kwargs__.get("category", cmn.BoltCats.OTHER)
if cat in mapping: if cat in mapping:
mapping[cat].append(cmd) mapping[cat].append(cmd)
else: else:
@ -73,7 +117,7 @@ class QrmHelpCommand(commands.HelpCommand):
continue continue
names = sorted([cmd.name for cmd in cmds]) names = sorted([cmd.name for cmd in cmds])
if cat is not None: if cat is not None:
embed.add_field(name=cat.title(), value=", ".join(names), inline=False) embed.add_field(name=cat.value, value=", ".join(names), inline=False)
else: else:
embed.add_field(name="Other", value=", ".join(names), inline=False) embed.add_field(name="Other", value=", ".join(names), inline=False)
await self.context.send(embed=embed) await self.context.send(embed=embed)
@ -97,7 +141,8 @@ class QrmHelpCommand(commands.HelpCommand):
embed.title = await self.get_command_signature(group) embed.title = await self.get_command_signature(group)
embed.description = group.help embed.description = group.help
for cmd in await self.filter_commands(group.commands, sort=True): 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) await self.context.send(embed=embed)
@ -124,6 +169,7 @@ class BaseCog(commands.Cog):
self.donation_links = { self.donation_links = {
"Ko-Fi": "https://ko-fi.com/miaowware", "Ko-Fi": "https://ko-fi.com/miaowware",
"LiberaPay": "https://liberapay.com/miaowware", "LiberaPay": "https://liberapay.com/miaowware",
"GitHub Sponsors": "https://github.com/sponsors/classabbyamp",
} }
self.bot_invite = "" self.bot_invite = ""
if self.bot.user: if self.bot.user:
@ -132,11 +178,11 @@ class BaseCog(commands.Cog):
@commands.Cog.listener() @commands.Cog.listener()
async def on_ready(self): 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}" self.bot_invite = (f"https://discordapp.com/oauth2/authorize?client_id={self.bot.user.id}"
f"&scope=bot&permissions={opt.invite_perms}") f"&scope=bot&permissions={opt.invite_perms}")
@commands.command(name="info", aliases=["about"]) @commands.command(name="info", aliases=["about"], category=cmn.BoltCats.INFO)
async def _info(self, ctx: commands.Context): async def _info(self, ctx: commands.Context):
"""Shows info about qrm.""" """Shows info about qrm."""
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
@ -151,10 +197,11 @@ class BaseCog(commands.Cog):
inline=False) inline=False)
if opt.enable_invite_cmd and (await self.bot.application_info()).bot_public: 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.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) await ctx.send(embed=embed)
@commands.command(name="ping", aliases=["beep"]) @commands.command(name="ping", aliases=["beep"], category=cmn.BoltCats.INFO)
async def _ping(self, ctx: commands.Context): async def _ping(self, ctx: commands.Context):
"""Shows the current latency to the discord endpoint.""" """Shows the current latency to the discord endpoint."""
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
@ -167,7 +214,7 @@ class BaseCog(commands.Cog):
embed.description = f"Current ping is {self.bot.latency*1000:.1f} ms" embed.description = f"Current ping is {self.bot.latency*1000:.1f} ms"
await ctx.send(content, embed=embed) await ctx.send(content, embed=embed)
@commands.command(name="changelog", aliases=["clog"]) @commands.command(name="changelog", aliases=["clog"], category=cmn.BoltCats.INFO)
async def _changelog(self, ctx: commands.Context, version: str = "latest"): async def _changelog(self, ctx: commands.Context, version: str = "latest"):
"""Shows what has changed in a bot version. Defaults to the latest version.""" """Shows what has changed in a bot version. Defaults to the latest version."""
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
@ -203,7 +250,7 @@ class BaseCog(commands.Cog):
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name="issue") @commands.command(name="issue", category=cmn.BoltCats.INFO)
async def _issue(self, ctx: commands.Context): async def _issue(self, ctx: commands.Context):
"""Shows how to create a bug report or feature request about the bot.""" """Shows how to create a bug report or feature request about the bot."""
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
@ -215,7 +262,7 @@ class BaseCog(commands.Cog):
[miaowware/qrm-resources](https://github.com/miaowware/qrm-resources/issues).""" [miaowware/qrm-resources](https://github.com/miaowware/qrm-resources/issues)."""
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name="donate", aliases=["tip"]) @commands.command(name="donate", aliases=["tip"], category=cmn.BoltCats.INFO)
async def _donate(self, ctx: commands.Context): async def _donate(self, ctx: commands.Context):
"""Shows ways to help support development of the bot via donations.""" """Shows ways to help support development of the bot via donations."""
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
@ -226,7 +273,7 @@ class BaseCog(commands.Cog):
embed.add_field(name=title, value=url, inline=False) embed.add_field(name=title, value=url, inline=False)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name="invite", enabled=opt.enable_invite_cmd) @commands.command(name="invite", enabled=opt.enable_invite_cmd, category=cmn.BoltCats.INFO)
async def _invite(self, ctx: commands.Context): async def _invite(self, ctx: commands.Context):
"""Generates a link to invite the bot to a server.""" """Generates a link to invite the bot to a server."""
if not (await self.bot.application_info()).bot_public: if not (await self.bot.application_info()).bot_public:
@ -236,7 +283,7 @@ class BaseCog(commands.Cog):
embed.description = self.bot_invite embed.description = self.bot_invite
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name="echo", aliases=["e"], category=cmn.cat.admin) @commands.command(name="echo", aliases=["e"], category=cmn.BoltCats.ADMIN)
@commands.check(cmn.check_if_owner) @commands.check(cmn.check_if_owner)
async def _echo(self, ctx: commands.Context, async def _echo(self, ctx: commands.Context,
channel: Union[cmn.GlobalChannelConverter, commands.UserConverter], *, msg: str): channel: Union[cmn.GlobalChannelConverter, commands.UserConverter], *, msg: str):

View File

@ -1,19 +1,17 @@
""" """
QRZ extension for qrm Callsign Lookup extension for qrm
--- ---
Copyright (C) 2019-2020 Abigail Gold, 0x5c Copyright (C) 2019-2020 classabbyamp, 0x5c (as qrz.py)
Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
from typing import Dict from typing import Dict
from datetime import datetime
import aiohttp import aiohttp
from qrztools import qrztools, QrzAsync, QrzError from callsignlookuptools import QrzAsyncClient, CallsignLookupError, CallsignData
from gridtools import Grid, LatLong
from discord.ext import commands from discord.ext import commands
@ -29,18 +27,20 @@ class QRZCog(commands.Cog):
self.qrz = None self.qrz = None
try: try:
if keys.qrz_user and keys.qrz_pass: 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 # seed the qrz object with the previous session key, in case it already works
session_key = ""
try: try:
with open("data/qrz_session") as qrz_file: with open("data/qrz_session") as qrz_file:
self.qrz.session_key = qrz_file.readline().strip() session_key = qrz_file.readline().strip()
except FileNotFoundError: except FileNotFoundError:
pass 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: except AttributeError:
pass pass
@commands.command(name="call", aliases=["qrz"], category=cmn.cat.lookup) @commands.command(name="call", aliases=["qrz"], category=cmn.Cats.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]
@ -63,65 +63,65 @@ class QRZCog(commands.Cog):
async with ctx.typing(): async with ctx.typing():
try: try:
data = await self.qrz.get_callsign(callsign) data = await self.qrz.search(callsign)
except QrzError as e: except CallsignLookupError as e:
embed.colour = cmn.colours.bad embed.colour = cmn.colours.bad
embed.description = str(e) embed.description = str(e)
await ctx.send(embed=embed) await ctx.send(embed=embed)
return return
embed.title = f"QRZ Data for {data.call}" embed.title = f"QRZ Data for {data.callsign}"
embed.colour = cmn.colours.good embed.colour = cmn.colours.good
embed.url = data.url embed.url = data.url
if data.image != qrztools.QrzImage(): if data.image is not None:
embed.set_thumbnail(url=data.image.url) embed.set_thumbnail(url=data.image.url)
for title, val in qrz_process_info(data).items(): for title, val in qrz_process_info(data).items():
if val is not None: if val is not None and (val := str(val)):
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)
def qrz_process_info(data: qrztools.QrzCallsignData) -> Dict: def qrz_process_info(data: CallsignData) -> Dict:
if data.name != qrztools.Name(): if data.name is not None:
if opt.qrz_only_nickname: if opt.qrz_only_nickname:
if data.name.nickname: nm = data.name.name if data.name.name is not None else ""
name = data.name.nickname + " " + data.name.name if data.name.nickname is not None:
name = data.name.nickname + " " + nm
elif data.name.first: elif data.name.first:
name = data.name.first + " " + data.name.name name = data.name.first + " " + nm
else: else:
name = data.name.name name = nm
else: else:
name = data.name.formatted_name name = data.name
else: else:
name = None name = None
if data.address != qrztools.Address(): qsl = dict()
state = ", " + data.address.state + " " if data.address.state else "" if data.qsl is not None:
address = "\n".join([data.address.attn, data.address.line1, data.address.line2 + state, data.address.zip]) qsl = {
else: "eQSL?": data.qsl.eqsl,
address = None "Paper QSL?": data.qsl.mail,
"LotW?": data.qsl.lotw,
"QSL Info": data.qsl.info,
}
return { return {
"Name": name, "Name": name,
"Country": data.address.country, "Country": data.address.country if data.address is not None else None,
"Address": address, "Address": data.address,
"Grid Square": data.grid if data.grid != Grid(LatLong(0, 0)) else None, "Grid Square": data.grid,
"County": data.county if data.county else None, "County": data.county,
"CQ Zone": data.cq_zone if data.cq_zone else None, "CQ Zone": data.cq_zone,
"ITU Zone": data.itu_zone if data.itu_zone else None, "ITU Zone": data.itu_zone,
"IOTA Designator": data.iota if data.iota else None, "IOTA Designator": data.iota,
"Expires": f"{data.expire_date:%Y-%m-%d}" if data.expire_date != datetime.min else None, "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, "Aliases": ", ".join(data.aliases) if data.aliases else None,
"Previous Callsign": data.prev_call if data.prev_call else None, "Previous Callsign": data.prev_call,
"License Class": data.lic_class if data.lic_class else None, "License Class": data.lic_class,
"Trustee": data.trustee if data.trustee else None, "Trustee": data.trustee,
"eQSL?": "Yes" if data.eqsl else "No", "Born": data.born,
"Paper QSL?": "Yes" if data.mail_qsl else "No", } | qsl
"LotW?": "Yes" if data.lotw_qsl else "No",
"QSL Info": data.qsl_manager if data.qsl_manager else None,
"Born": f"{data.born:%Y-%m-%d}" if data.born != datetime.min else None
}
def setup(bot): def setup(bot):

View File

@ -1,26 +1,23 @@
""" """
Ham extension for qrm Codes extension for qrm
--- ---
Copyright (C) 2019-2021 Abigail Gold, 0x5c Copyright (C) 2019-2021 classabbyamp, 0x5c (as ham.py)
Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
import json import json
from datetime import datetime
import discord.ext.commands as commands import discord.ext.commands as commands
import common as cmn import common as cmn
from resources import callsign_info
class HamCog(commands.Cog): class HamCog(commands.Cog):
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
self.pfxs = callsign_info.options
with open(cmn.paths.resources / "phonetics.1.json") as file: with open(cmn.paths.resources / "phonetics.1.json") as file:
d = json.load(file) d = json.load(file)
self.phonetics: dict[str, str] = d["phonetics"] self.phonetics: dict[str, str] = d["phonetics"]
@ -28,7 +25,7 @@ class HamCog(commands.Cog):
with open(cmn.paths.resources / "qcodes.1.json") as file: with open(cmn.paths.resources / "qcodes.1.json") as file:
self.qcodes: dict = json.load(file) self.qcodes: dict = json.load(file)
@commands.command(name="qcode", aliases=["q"], category=cmn.cat.ref) @commands.command(name="qcode", aliases=["q"], category=cmn.Cats.CODES)
async def _qcode_lookup(self, ctx: commands.Context, qcode: str): async def _qcode_lookup(self, ctx: commands.Context, qcode: str):
"""Looks up the meaning of a Q Code.""" """Looks up the meaning of a Q Code."""
qcode = qcode.upper() qcode = qcode.upper()
@ -42,7 +39,7 @@ class HamCog(commands.Cog):
embed.colour = cmn.colours.bad embed.colour = cmn.colours.bad
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name="phonetics", aliases=["ph", "phoneticize", "phoneticise", "phone"], category=cmn.cat.ref) @commands.command(name="phonetics", aliases=["ph", "phoneticize", "phoneticise", "phone"], category=cmn.Cats.CODES)
async def _phonetics_lookup(self, ctx: commands.Context, *, msg: str): async def _phonetics_lookup(self, ctx: commands.Context, *, msg: str):
"""Returns NATO phonetics for a word or phrase.""" """Returns NATO phonetics for a word or phrase."""
result = "" result = ""
@ -58,51 +55,7 @@ class HamCog(commands.Cog):
embed.colour = cmn.colours.good embed.colour = cmn.colours.good
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name="utc", aliases=["z"], category=cmn.cat.ref) @commands.command(name="phoneticweight", aliases=["pw"], category=cmn.Cats.CODES)
async def _utc_lookup(self, ctx: commands.Context):
"""Returns the current time in UTC."""
now = datetime.utcnow()
result = "**" + now.strftime("%Y-%m-%d %H:%M") + "Z**"
embed = cmn.embed_factory(ctx)
embed.title = "The current time is:"
embed.description = result
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
@commands.command(name="prefixes", aliases=["vanity", "pfx", "vanities", "prefix"], category=cmn.cat.ref)
async def _vanity_prefixes(self, ctx: commands.Context, country: str = ""):
"""Lists valid callsign prefixes for different countries."""
country = country.lower()
embed = cmn.embed_factory(ctx)
if country not in self.pfxs:
desc = "Possible arguments are:\n"
for key, val in self.pfxs.items():
desc += f"`{key}`: {val.title}{(' ' + val.emoji if val.emoji else '')}\n"
embed.title = f"{country} Not Found!"
embed.description = desc
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
else:
data = self.pfxs[country]
embed.title = data.title + (" " + data.emoji if data.emoji else "")
embed.description = data.desc
embed.colour = cmn.colours.good
for name, val in data.calls.items():
embed.add_field(name=name, value=val, inline=False)
await ctx.send(embed=embed)
@commands.command(name="contests", aliases=["cc", "tests"], category=cmn.cat.ref)
async def _contests(self, ctx: commands.Context):
embed = cmn.embed_factory(ctx)
embed.title = "Contest Calendar"
embed.description = ("*We are currently rewriting the old, Chrome-based `contests` command. In the meantime, "
"use [the website](https://www.contestcalendar.com/weeklycont.php).*")
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): async def _weight(self, ctx: commands.Context, *, msg: str):
"""Calculates the phonetic weight of a callsign or message.""" """Calculates the phonetic weight of a callsign or message."""
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)

27
exts/contests.py Normal file
View File

@ -0,0 +1,27 @@
"""
Contest Calendar extension for qrm
---
Copyright (C) 2021-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
import discord.ext.commands as commands
import common as cmn
class ContestCalendarCog(commands.Cog):
@commands.command(name="contests", aliases=["cc", "tests"], category=cmn.Cats.LOOKUP)
async def _contests(self, ctx: commands.Context):
embed = cmn.embed_factory(ctx)
embed.title = "Contest Calendar"
embed.description = ("*We are currently rewriting the old, Chrome-based `contests` command. In the meantime, "
"use [the website](https://www.contestcalendar.com/weeklycont.php).*")
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
def setup(bot: commands.Bot):
bot.add_cog(ContestCalendarCog(bot))

View File

@ -1,10 +1,9 @@
""" """
Conversion extension for qrm Conversion extension for qrm
--- ---
Copyright (C) 2020 Abigail Gold, 0x5c Copyright (C) 2020-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
@ -67,7 +66,7 @@ class DbConvCog(commands.Cog):
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
@commands.command(name="dbconv", aliases=["dbc"], category=cmn.cat.ref) @commands.command(name="dbconv", aliases=["dbc"], category=cmn.Cats.CALC)
async def _db_conv(self, ctx: commands.Context, async def _db_conv(self, ctx: commands.Context,
value: Optional[float] = None, value: Optional[float] = None,
unit_from: Optional[UnitConverter] = None, unit_from: Optional[UnitConverter] = None,
@ -188,7 +187,7 @@ def _calc_volt(db: float, ref: float):
# testing code # testing code
if __name__ == "__main__": if __name__ == "__main__":
while(True): while True:
try: try:
ip = input("> ").split() ip = input("> ").split()
initial = float(ip[0]) initial = float(ip[0])

View File

@ -1,10 +1,10 @@
""" """
Lookup extension for qrm DXCC Prefix Lookup extension for qrm
--- ---
Copyright (C) 2019-2020 Abigail Gold, 0x5c Copyright (C) 2019-2020 classabbyamp, 0x5c (as lookup.py)
Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
@ -21,7 +21,7 @@ import common as cmn
cty_path = Path("./data/cty.json") cty_path = Path("./data/cty.json")
class LookupCog(commands.Cog): class DXCCCog(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
try: try:
@ -29,19 +29,7 @@ class LookupCog(commands.Cog):
except OSError: except OSError:
self.cty = BigCty() self.cty = BigCty()
# TODO: See #107 @commands.command(name="dxcc", aliases=["dx"], category=cmn.Cats.LOOKUP)
# @commands.command(name="sat", category=cmn.cat.lookup)
# async def _sat_lookup(self, ctx: commands.Context, sat_name: str, grid1: str, grid2: str = None):
# """Links to info about satellite passes on satmatch.com."""
# now = datetime.utcnow().strftime("%Y-%m-%d%%20%H:%M")
# if grid2 is None or grid2 == "":
# await ctx.send(f"http://www.satmatch.com/satellite/{sat_name}/obs1/{grid1}"
# f"?search_start_time={now}&duration_hrs=24")
# else:
# await ctx.send(f"http://www.satmatch.com/satellite/{sat_name}/obs1/{grid1}"
# f"/obs2/{grid2}?search_start_time={now}&duration_hrs=24")
@commands.command(name="dxcc", aliases=["dx"], category=cmn.cat.lookup)
async def _dxcc_lookup(self, ctx: commands.Context, query: str): async def _dxcc_lookup(self, ctx: commands.Context, query: str):
"""Gets DXCC info about a callsign prefix.""" """Gets DXCC info about a callsign prefix."""
query = query.upper() query = query.upper()
@ -82,6 +70,6 @@ def run_update(cty_obj, dump_loc):
def setup(bot: commands.Bot): def setup(bot: commands.Bot):
lookupcog = LookupCog(bot) dxcccog = DXCCCog(bot)
bot.add_cog(lookupcog) bot.add_cog(dxcccog)
lookupcog._update_cty.start() dxcccog._update_cty.start()

View File

@ -1,10 +1,9 @@
""" """
Fun extension for qrm Fun extension for qrm
--- ---
Copyright (C) 2019-2021 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
@ -26,22 +25,22 @@ class FunCog(commands.Cog):
with open(cmn.paths.resources / "words.1.txt") as words_file: with open(cmn.paths.resources / "words.1.txt") as words_file:
self.words = words_file.read().lower().splitlines() self.words = words_file.read().lower().splitlines()
@commands.command(name="xkcd", aliases=["x"], category=cmn.cat.fun) @commands.command(name="xkcd", aliases=["x"], category=cmn.Cats.FUN)
async def _xkcd(self, ctx: commands.Context, number: str): async def _xkcd(self, ctx: commands.Context, number: int):
"""Looks up an xkcd comic by number.""" """Looks up an xkcd comic by number."""
await ctx.send("http://xkcd.com/" + number) await ctx.send("http://xkcd.com/" + str(number))
@commands.command(name="tar", category=cmn.cat.fun) @commands.command(name="tar", category=cmn.Cats.FUN)
async def _tar(self, ctx: commands.Context): async def _tar(self, ctx: commands.Context):
"""Returns xkcd: tar.""" """Returns xkcd: tar."""
await ctx.send("http://xkcd.com/1168") await ctx.send("http://xkcd.com/1168")
@commands.command(name="standards", category=cmn.cat.fun) @commands.command(name="standards", category=cmn.Cats.FUN)
async def _standards(self, ctx: commands.Context): async def _standards(self, ctx: commands.Context):
"""Returns xkcd: Standards.""" """Returns xkcd: Standards."""
await ctx.send("http://xkcd.com/927") await ctx.send("http://xkcd.com/927")
@commands.command(name="worksplit", aliases=["split", "ft8"], category=cmn.cat.fun) @commands.command(name="worksplit", aliases=["split", "ft8"], category=cmn.Cats.FUN)
async def _worksplit(self, ctx: commands.Context): async def _worksplit(self, ctx: commands.Context):
"""Posts "Work split you lids".""" """Posts "Work split you lids"."""
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
@ -49,12 +48,12 @@ class FunCog(commands.Cog):
embed.set_image(url=opt.resources_url + self.imgs["worksplit"]) embed.set_image(url=opt.resources_url + self.imgs["worksplit"])
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name="xd", hidden=True, category=cmn.cat.fun) @commands.command(name="xd", hidden=True, category=cmn.Cats.FUN)
async def _xd(self, ctx: commands.Context): async def _xd(self, ctx: commands.Context):
"""ecks dee""" """ecks dee"""
await ctx.send("ECKS DEE :smirk:") await ctx.send("ECKS DEE :smirk:")
@commands.command(name="funetics", aliases=["fun"], category=cmn.cat.fun) @commands.command(name="funetics", aliases=["fun"], category=cmn.Cats.FUN)
async def _funetics_lookup(self, ctx: commands.Context, *, msg: str): async def _funetics_lookup(self, ctx: commands.Context, *, msg: str):
"""Generates fun/wacky phonetics for a word or phrase.""" """Generates fun/wacky phonetics for a word or phrase."""
result = "" result = ""

View File

@ -1,10 +1,9 @@
""" """
Grid extension for qrm Grid extension for qrm
--- ---
Copyright (C) 2019-2020 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
@ -19,7 +18,7 @@ class GridCog(commands.Cog):
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
@commands.command(name="grid", category=cmn.cat.maps) @commands.command(name="grid", category=cmn.Cats.CALC)
async def _grid_sq_lookup(self, ctx: commands.Context, lat: float, lon: float): async def _grid_sq_lookup(self, ctx: commands.Context, lat: float, lon: float):
("""Calculates the grid square for latitude and longitude coordinates.""" ("""Calculates the grid square for latitude and longitude coordinates."""
"""\n\nCoordinates should be in decimal format, with negative being latitude South and longitude West.""" """\n\nCoordinates should be in decimal format, with negative being latitude South and longitude West."""
@ -33,7 +32,7 @@ class GridCog(commands.Cog):
embed.colour = cmn.colours.good embed.colour = cmn.colours.good
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name="latlong", aliases=["latlon", "loc", "ungrid"], category=cmn.cat.maps) @commands.command(name="latlong", aliases=["latlon", "loc", "ungrid"], category=cmn.Cats.CALC)
async def _location_lookup(self, ctx: commands.Context, grid: str): async def _location_lookup(self, ctx: commands.Context, grid: str):
("""Calculates the latitude and longitude for the center of a grid locator.""" ("""Calculates the latitude and longitude for the center of a grid locator."""
"""\n\nTo calculate the grid locator from a latitude and longitude, use `grid`""" """\n\nTo calculate the grid locator from a latitude and longitude, use `grid`"""
@ -49,7 +48,7 @@ class GridCog(commands.Cog):
"latlong` to see other names for this command.*")) "latlong` to see other names for this command.*"))
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name="griddistance", aliases=["griddist", "distance", "dist"], category=cmn.cat.maps) @commands.command(name="griddistance", aliases=["griddist", "distance", "dist"], category=cmn.Cats.CALC)
async def _dist_lookup(self, ctx: commands.Context, grid1: str, grid2: str): async def _dist_lookup(self, ctx: commands.Context, grid1: str, grid2: str):
"""Calculates the great circle distance and azimuthal bearing between two grid locators.""" """Calculates the great circle distance and azimuthal bearing between two grid locators."""
g1 = gridtools.Grid(grid1) g1 = gridtools.Grid(grid1)

View File

@ -1,15 +1,13 @@
""" """
Image extension for qrm Image extension for qrm
--- ---
Copyright (C) 2019-2021 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
import aiohttp import aiohttp
from datetime import datetime
import discord.ext.commands as commands import discord.ext.commands as commands
@ -19,55 +17,36 @@ import data.options as opt
class ImageCog(commands.Cog): class ImageCog(commands.Cog):
gl_baseurl = "https://www.fourmilab.ch/cgi-bin/uncgi/Earth?img=ETOPO1_day-m.evif&dynimg=y&opt=-p"
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
self.bandcharts = cmn.ImagesGroup(cmn.paths.resources / "bandcharts.1.json") self.bandcharts = cmn.ImagesGroup(cmn.paths.resources / "bandcharts.1.json")
self.maps = cmn.ImagesGroup(cmn.paths.resources / "maps.1.json") self.maps = cmn.ImagesGroup(cmn.paths.resources / "maps.1.json")
self.session = aiohttp.ClientSession(connector=bot.qrm.connector) self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
@commands.command(name="bandplan", aliases=["plan", "bands"], category=cmn.cat.ref) @commands.command(name="bandchart", aliases=["bandplan", "plan", "bands"], category=cmn.Cats.REF)
async def _bandplan(self, ctx: commands.Context, region: str = ""): async def _bandcharts(self, ctx: commands.Context, chart_id: str = ""):
"""Gets the frequency allocations chart for a given country.""" """Gets the frequency allocations chart for a given country."""
async with ctx.typing(): await ctx.send(embed=create_embed(ctx, "Bandchart", self.bandcharts, chart_id))
arg = region.lower()
embed = cmn.embed_factory(ctx)
if arg not in self.bandcharts:
desc = "Possible arguments are:\n"
for key, img in self.bandcharts.items():
desc += f"`{key}`: {img.name}{(' ' + img.emoji if img.emoji else '')}\n"
embed.title = "Bandplan Not Found!"
embed.description = desc
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
metadata: cmn.ImageMetadata = self.bandcharts[arg]
if metadata.description:
embed.description = metadata.description
if metadata.source:
embed.add_field(name="Source", value=metadata.source)
embed.title = metadata.long_name + (" " + metadata.emoji if metadata.emoji else "")
embed.colour = cmn.colours.good
embed.set_image(url=opt.resources_url + metadata.filename)
await ctx.send(embed=embed)
@commands.command(name="map", category=cmn.cat.maps) @commands.command(name="map", category=cmn.Cats.REF)
async def _map(self, ctx: commands.Context, map_id: str = ""): async def _map(self, ctx: commands.Context, map_id: str = ""):
"""Posts a ham-relevant map.""" """Posts a ham-relevant map."""
async with ctx.typing(): await ctx.send(embed=create_embed(ctx, "Map", self.maps, map_id))
arg = map_id.lower()
def create_embed(ctx: commands.Context, not_found_name: str, db: cmn.ImagesGroup, img_id: str):
"""Creates an embed for the image and its metadata, or list available images in the group."""
img_id = img_id.lower()
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
if arg not in self.maps: if img_id not in db:
desc = "Possible arguments are:\n" desc = "Possible arguments are:\n"
for key, img in self.maps.items(): for key, img in db.items():
desc += f"`{key}`: {img.name}{(' ' + img.emoji if img.emoji else '')}\n" desc += f"`{key}`: {img.name}{(' ' + img.emoji if img.emoji else '')}\n"
embed.title = "Map Not Found!" embed.title = f"{not_found_name} Not Found!"
embed.description = desc embed.description = desc
embed.colour = cmn.colours.bad embed.colour = cmn.colours.bad
await ctx.send(embed=embed) return embed
return metadata = db[img_id]
metadata: cmn.ImageMetadata = self.maps[arg]
if metadata.description: if metadata.description:
embed.description = metadata.description embed.description = metadata.description
if metadata.source: if metadata.source:
@ -75,17 +54,7 @@ class ImageCog(commands.Cog):
embed.title = metadata.long_name + (" " + metadata.emoji if metadata.emoji else "") embed.title = metadata.long_name + (" " + metadata.emoji if metadata.emoji else "")
embed.colour = cmn.colours.good embed.colour = cmn.colours.good
embed.set_image(url=opt.resources_url + metadata.filename) embed.set_image(url=opt.resources_url + metadata.filename)
await ctx.send(embed=embed) return embed
@commands.command(name="grayline", aliases=["greyline", "grey", "gray", "gl"], category=cmn.cat.maps)
async def _grayline(self, ctx: commands.Context):
"""Gets a map of the current greyline, where HF propagation is the best."""
embed = cmn.embed_factory(ctx)
embed.title = "Current Greyline Conditions"
embed.colour = cmn.colours.good
date_params = f"&date=1&utc={datetime.utcnow():%Y-%m-%d+%H:%M:%S}"
embed.set_image(url=self.gl_baseurl + date_params)
await ctx.send(embed=embed)
def setup(bot: commands.Bot): def setup(bot: commands.Bot):

View File

@ -1,19 +1,17 @@
""" """
Weather extension for qrm Land Weather extension for qrm
--- ---
Copyright (C) 2019-2020 Abigail Gold, 0x5c Copyright (C) 2019-2020 classabbyamp, 0x5c (as weather.py)
Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
import re import re
from typing import List
import aiohttp import aiohttp
from discord import Embed
import discord.ext.commands as commands import discord.ext.commands as commands
import common as cmn import common as cmn
@ -26,21 +24,7 @@ class WeatherCog(commands.Cog):
self.bot = bot self.bot = bot
self.session = aiohttp.ClientSession(connector=bot.qrm.connector) self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
@commands.command(name="solarweather", aliases=["solar", "bandconditions", "cond", "condx", "conditions"], @commands.group(name="weather", aliases=["wttr"], case_insensitive=True, category=cmn.Cats.WEATHER)
category=cmn.cat.weather)
async def solarweather(self, ctx: commands.Context):
"""Gets a solar weather report."""
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="http://www.hamqsl.com/solarsun.php")
await ctx.send(embed=embed)
@commands.group(name="weather", aliases=["wttr"], case_insensitive=True, category=cmn.cat.weather)
async def _weather_conditions(self, ctx: commands.Context): async def _weather_conditions(self, ctx: commands.Context):
"""Gets local weather conditions from [wttr.in](http://wttr.in/). """Gets local weather conditions from [wttr.in](http://wttr.in/).
@ -57,7 +41,7 @@ class WeatherCog(commands.Cog):
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help(ctx.command) await ctx.send_help(ctx.command)
@_weather_conditions.command(name="forecast", aliases=["fc", "future"], category=cmn.cat.weather) @_weather_conditions.command(name="forecast", aliases=["fc", "future"], category=cmn.Cats.WEATHER)
async def _weather_conditions_forecast(self, ctx: commands.Context, *, location: str): async def _weather_conditions_forecast(self, ctx: commands.Context, *, location: str):
"""Gets local weather forecast for the next three days from [wttr.in](http://wttr.in/). """Gets local weather forecast for the next three days from [wttr.in](http://wttr.in/).
See help of the `weather` command for possible location types and options.""" See help of the `weather` command for possible location types and options."""
@ -83,7 +67,7 @@ class WeatherCog(commands.Cog):
embed.set_image(url=f"http://wttr.in/{loc}_{units}pnFQ.png") embed.set_image(url=f"http://wttr.in/{loc}_{units}pnFQ.png")
await ctx.send(embed=embed) await ctx.send(embed=embed)
@_weather_conditions.command(name="now", aliases=["n"], category=cmn.cat.weather) @_weather_conditions.command(name="now", aliases=["n"], category=cmn.Cats.WEATHER)
async def _weather_conditions_now(self, ctx: commands.Context, *, location: str): async def _weather_conditions_now(self, ctx: commands.Context, *, location: str):
"""Gets current local weather conditions from [wttr.in](http://wttr.in/). """Gets current local weather conditions from [wttr.in](http://wttr.in/).
See help of the `weather` command for possible location types and options.""" See help of the `weather` command for possible location types and options."""
@ -109,72 +93,68 @@ class WeatherCog(commands.Cog):
embed.set_image(url=f"http://wttr.in/{loc}_0{units}pnFQ.png") embed.set_image(url=f"http://wttr.in/{loc}_0{units}pnFQ.png")
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name="metar", category=cmn.cat.weather) @commands.command(name="metar", category=cmn.Cats.WEATHER)
async def metar(self, ctx: commands.Context, airport: str, hours: int = 0): async def metar(self, ctx: commands.Context, airport: str, hours: int = 0):
"""Gets current raw METAR (Meteorological Terminal Aviation Routine Weather Report) for an airport. \ """Gets current raw METAR (Meteorological Terminal Aviation Routine Weather Report) for an airport. \
Optionally, a number of hours can be given to show a number of hours of historical METAR data. Optionally, a number of hours can be given to show a number of hours of historical METAR data.
Airports should be given as an \ Airports should be given as an \
[ICAO code](https://en.wikipedia.org/wiki/List_of_airports_by_IATA_and_ICAO_code).""" [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))
@commands.command(name="taf", category=cmn.cat.weather) 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): async def taf(self, ctx: commands.Context, airport: str):
"""Gets forecasted raw TAF (Terminal Aerodrome Forecast) data for an airport. Includes the latest METAR data. """Gets forecasted raw TAF (Terminal Aerodrome Forecast) data for an airport. Includes the latest METAR data.
Airports should be given as an \ Airports should be given as an \
[ICAO code](https://en.wikipedia.org/wiki/List_of_airports_by_IATA_and_ICAO_code).""" [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) embed = cmn.embed_factory(ctx)
airport = airport.upper() airport = airport.upper()
if re.fullmatch(r"\w(\w|\d){2,3}", airport): if not 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:
embed.title = "Invalid airport given!" embed.title = "Invalid airport given!"
embed.colour = cmn.colours.bad 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://aviationweather.gov/api/data/taf?ids={airport}&format=raw&metar=true"
url = (f"https://www.aviationweather.gov/metar/data?ids={airport}&format=raw&hours={hours}"
f"&taf={'on' if taf else 'off'}&layout=off")
async with self.session.get(url) as r: async with self.session.get(url) as r:
if r.status != 200: if r.status != 200:
raise cmn.BotHTTPError(r) raise cmn.BotHTTPError(r)
page = await r.text() taf = await r.text()
# pare down to just the data embed.title = f"Current TAF for {airport}"
page = page.split("<!-- Data starts here -->")[1].split("<!-- Data ends here -->")[0].strip() embed.description = "Data from [aviationweather.gov](https://www.aviationweather.gov/)."
# split at <hr>s embed.colour = cmn.colours.good
data = re.split(r"<hr.*>", page, maxsplit=len(airport)) embed.description += f"\n\n```\n{taf}\n```"
parsed = [] await ctx.send(embed=embed)
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
def setup(bot: commands.Bot): def setup(bot: commands.Bot):

View File

@ -1,10 +1,9 @@
""" """
Morse Code extension for qrm Morse Code extension for qrm
--- ---
Copyright (C) 2019-2021 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
@ -23,7 +22,7 @@ class MorseCog(commands.Cog):
self.morse: dict[str, str] = d["morse"] self.morse: dict[str, str] = d["morse"]
self.ascii: dict[str, int] = d["ascii"] self.ascii: dict[str, int] = d["ascii"]
@commands.command(name="morse", aliases=["cw"], category=cmn.cat.ref) @commands.command(name="morse", aliases=["cw"], category=cmn.Cats.CODES)
async def _morse(self, ctx: commands.Context, *, msg: str): async def _morse(self, ctx: commands.Context, *, msg: str):
"""Converts ASCII to international morse code.""" """Converts ASCII to international morse code."""
result = "" result = ""
@ -39,7 +38,7 @@ class MorseCog(commands.Cog):
embed.colour = cmn.colours.good embed.colour = cmn.colours.good
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name="unmorse", aliases=["demorse", "uncw", "decw"], category=cmn.cat.ref) @commands.command(name="unmorse", aliases=["demorse", "uncw", "decw"], category=cmn.Cats.CODES)
async def _unmorse(self, ctx: commands.Context, *, msg: str): async def _unmorse(self, ctx: commands.Context, *, msg: str):
"""Converts international morse code to ASCII.""" """Converts international morse code to ASCII."""
result = "" result = ""
@ -59,7 +58,7 @@ class MorseCog(commands.Cog):
embed.colour = cmn.colours.good embed.colour = cmn.colours.good
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name="cwweight", aliases=["weight", "cww"], category=cmn.cat.ref) @commands.command(name="cwweight", aliases=["weight", "cww"], category=cmn.Cats.CODES)
async def _weight(self, ctx: commands.Context, *, msg: str): async def _weight(self, ctx: commands.Context, *, msg: str):
"""Calculates the CW weight of a callsign or message.""" """Calculates the CW weight of a callsign or message."""
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)

47
exts/prefixes.py Normal file
View File

@ -0,0 +1,47 @@
"""
Prefixes Lookup extension for qrm
---
Copyright (C) 2021-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
import discord.ext.commands as commands
import common as cmn
from resources import callsign_info
class PrefixesCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.pfxs = callsign_info.options
@commands.command(name="prefixes", aliases=["vanity", "pfx", "vanities", "prefix"], category=cmn.Cats.REF)
async def _vanity_prefixes(self, ctx: commands.Context, country: str = ""):
"""Lists valid callsign prefixes for different countries."""
country = country.lower()
embed = cmn.embed_factory(ctx)
if country not in self.pfxs:
desc = "Possible arguments are:\n"
for key, val in self.pfxs.items():
desc += f"`{key}`: {val.title}{(' ' + val.emoji if val.emoji else '')}\n"
embed.title = f"{country} Not Found!"
embed.description = desc
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
else:
data = self.pfxs[country]
embed.title = data.title + (" " + data.emoji if data.emoji else "")
embed.description = data.desc
embed.colour = cmn.colours.good
for name, val in data.calls.items():
embed.add_field(name=name, value=val, inline=False)
await ctx.send(embed=embed)
def setup(bot: commands.Bot):
bot.add_cog(PrefixesCog(bot))

View File

@ -1,17 +1,17 @@
""" """
Propagation extension for qrm Propagation extension for qrm
--- ---
Copyright (C) 2019-2020 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
from datetime import datetime
from io import BytesIO from io import BytesIO
import aiohttp
import cairosvg import cairosvg
import httpx
import discord import discord
import discord.ext.commands as commands import discord.ext.commands as commands
@ -22,18 +22,23 @@ import common as cmn
class PropagationCog(commands.Cog): class PropagationCog(commands.Cog):
muf_url = "https://prop.kc2g.com/renders/current/mufd-normal-now.svg" muf_url = "https://prop.kc2g.com/renders/current/mufd-normal-now.svg"
fof2_url = "https://prop.kc2g.com/renders/current/fof2-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 = "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): def __init__(self, bot):
self.bot = 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.cat.weather) @commands.command(name="mufmap", aliases=["muf"], category=cmn.Cats.WEATHER)
async def mufmap(self, ctx: commands.Context): async def mufmap(self, ctx: commands.Context):
"""Shows a world map of the Maximum Usable Frequency (MUF).""" """Shows a world map of the Maximum Usable Frequency (MUF)."""
async with ctx.typing(): async with ctx.typing():
async with self.session.get(self.muf_url) as r: resp = await self.httpx_client.get(self.muf_url)
svg = await r.read() await resp.aclose()
out = BytesIO(cairosvg.svg2png(bytestring=svg)) if resp.status_code != 200:
raise cmn.BotHTTPError(resp)
out = BytesIO(cairosvg.svg2png(bytestring=await resp.aread()))
file = discord.File(out, "muf_map.png") file = discord.File(out, "muf_map.png")
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
embed.title = "Maximum Usable Frequency Map" embed.title = "Maximum Usable Frequency Map"
@ -41,13 +46,15 @@ class PropagationCog(commands.Cog):
embed.set_image(url="attachment://muf_map.png") embed.set_image(url="attachment://muf_map.png")
await ctx.send(file=file, embed=embed) await ctx.send(file=file, embed=embed)
@commands.command(name="fof2map", aliases=["fof2", "critfreq"], category=cmn.cat.weather) @commands.command(name="fof2map", aliases=["fof2", "critfreq"], category=cmn.Cats.WEATHER)
async def fof2map(self, ctx: commands.Context): async def fof2map(self, ctx: commands.Context):
"""Shows a world map of the Critical Frequency (foF2).""" """Shows a world map of the Critical Frequency (foF2)."""
async with ctx.typing(): async with ctx.typing():
async with self.session.get(self.fof2_url) as r: resp = await self.httpx_client.get(self.fof2_url)
svg = await r.read() await resp.aclose()
out = BytesIO(cairosvg.svg2png(bytestring=svg)) if resp.status_code != 200:
raise cmn.BotHTTPError(resp)
out = BytesIO(cairosvg.svg2png(bytestring=await resp.aread()))
file = discord.File(out, "fof2_map.png") file = discord.File(out, "fof2_map.png")
embed = cmn.embed_factory(ctx) embed = cmn.embed_factory(ctx)
embed.title = "Critical Frequency (foF2) Map" embed.title = "Critical Frequency (foF2) Map"
@ -55,6 +62,42 @@ class PropagationCog(commands.Cog):
embed.set_image(url="attachment://fof2_map.png") embed.set_image(url="attachment://fof2_map.png")
await ctx.send(file=file, embed=embed) await ctx.send(file=file, embed=embed)
@commands.command(name="grayline", aliases=["greyline", "grey", "gray", "gl"], category=cmn.Cats.WEATHER)
async def grayline(self, ctx: commands.Context):
"""Gets a map of the current greyline, where HF propagation is the best."""
embed = cmn.embed_factory(ctx)
embed.title = "Current Greyline Conditions"
embed.colour = cmn.colours.good
date_params = f"&date=1&utc={datetime.utcnow():%Y-%m-%d+%H:%M:%S}"
embed.set_image(url=self.gl_baseurl + date_params)
await ctx.send(embed=embed)
@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"
embed.colour = cmn.colours.good
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)
def setup(bot: commands.Bot): def setup(bot: commands.Bot):
bot.add_cog(PropagationCog(bot)) bot.add_cog(PropagationCog(bot))

View File

@ -1,10 +1,9 @@
""" """
Study extension for qrm Study extension for qrm
--- ---
Copyright (C) 2019-2020 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
@ -31,7 +30,7 @@ class StudyCog(commands.Cog):
self.source = "Data courtesy of [HamStudy.org](https://hamstudy.org/)" self.source = "Data courtesy of [HamStudy.org](https://hamstudy.org/)"
self.session = aiohttp.ClientSession(connector=bot.qrm.connector) self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
@commands.command(name="hamstudy", aliases=["rq", "randomquestion", "randomq"], category=cmn.cat.study) @commands.command(name="hamstudy", aliases=["rq", "randomquestion", "randomq"], category=cmn.Cats.STUDY)
async def _random_question(self, ctx: commands.Context, country: str = "", level: str = "", element: str = ""): async def _random_question(self, ctx: commands.Context, country: str = "", level: str = "", element: str = ""):
"""Gets a random question from [HamStudy's](https://hamstudy.org) question pools.""" """Gets a random question from [HamStudy's](https://hamstudy.org) question pools."""
with ctx.typing(): with ctx.typing():
@ -160,31 +159,33 @@ class StudyCog(commands.Cog):
await cmn.add_react(q_msg, list(self.choices.values())[i]) await cmn.add_react(q_msg, list(self.choices.values())[i])
await cmn.add_react(q_msg, cmn.emojis.question) await cmn.add_react(q_msg, cmn.emojis.question)
def check(reaction, user): def check(ev):
return (user.id != self.bot.user.id return (ev.user_id != self.bot.user.id
and reaction.message.id == q_msg.id and ev.message_id == q_msg.id
and (str(reaction.emoji) in self.choices.values() or str(reaction.emoji) == cmn.emojis.question)) and (str(ev.emoji) in self.choices.values() or str(ev.emoji) == cmn.emojis.question))
try: 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: except asyncio.TimeoutError:
embed.set_field_at(1, name="Answers", value=answers_str_bolded, inline=False) embed.set_field_at(1, name="Answers", value=answers_str_bolded, inline=False)
embed.set_field_at(2, name="Answer", embed.set_field_at(2, name="Answer",
value=(f"{cmn.emojis.clock} " value=(f"{cmn.emojis.stopwatch} "
f"**Timed out!** The correct answer was {self.choices[question['answer']]}")) f"**Timed out!** The correct answer was {self.choices[question['answer']]}"))
embed.colour = cmn.colours.timeout embed.colour = cmn.colours.timeout
await q_msg.edit(embed=embed) await q_msg.edit(embed=embed)
else: 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(1, name="Answers", value=answers_str_bolded, inline=False)
embed.set_field_at(2, name="Answer", embed.set_field_at(2, name="Answer",
value=f"The correct answer was {self.choices[question['answer']]}", inline=False) 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 embed.colour = cmn.colours.timeout
await q_msg.edit(embed=embed) await q_msg.edit(embed=embed)
else: else:
answers_str_checked = "" 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(): for letter, ans in answers.items():
answers_str_checked += f"{self.choices[letter]}" answers_str_checked += f"{self.choices[letter]}"
if letter == question["answer"] == chosen_ans: if letter == question["answer"] == chosen_ans:
@ -196,19 +197,23 @@ class StudyCog(commands.Cog):
else: else:
answers_str_checked += f" {ans}\n" 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(1, name="Answers", value=answers_str_checked, inline=False)
embed.set_field_at(2, name="Answer", value=(f"{cmn.emojis.check_mark} " embed.set_field_at(2, name="Answer", value=(f"{cmn.emojis.check_mark} "
f"**Correct!** The answer was {reaction.emoji}")) f"**Correct!** The answer was {ev.emoji}"))
embed.add_field(name="Answered By", value=str(user), inline=False) # 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 embed.colour = cmn.colours.good
await q_msg.edit(embed=embed) await q_msg.edit(embed=embed)
else: else:
embed.set_field_at(1, name="Answers", value=answers_str_checked, inline=False) embed.set_field_at(1, name="Answers", value=answers_str_checked, inline=False)
embed.set_field_at(2, name="Answer", embed.set_field_at(2, name="Answer",
value=(f"{cmn.emojis.x} **Incorrect!** The correct answer was " value=(f"{cmn.emojis.x} **Incorrect!** The correct answer was "
f"{self.choices[question['answer']]}, not {reaction.emoji}")) f"{self.choices[question['answer']]}, not {ev.emoji}"))
embed.add_field(name="Answered By", value=str(user), inline=False) # 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 embed.colour = cmn.colours.bad
await q_msg.edit(embed=embed) await q_msg.edit(embed=embed)

71
exts/tex.py Normal file
View File

@ -0,0 +1,71 @@
"""
TeX extension for qrm
---
Copyright (C) 2021-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
import aiohttp
from io import BytesIO
from urllib.parse import urljoin
import discord
import discord.ext.commands as commands
import common as cmn
import data.options as opt
class TexCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
with open(cmn.paths.resources / "template.1.tex") as latex_template:
self.template = latex_template.read()
@commands.command(name="tex", aliases=["latex"], category=cmn.Cats.UTILS)
async def tex(self, ctx: commands.Context, *, expr: str):
"""Renders a LaTeX expression.
In paragraph mode by default. To render math, add `$` around math expressions.
"""
payload = {
"format": "png",
"code": self.template.replace("#CONTENT#", expr),
"quality": 50
}
with ctx.typing():
# ask rTeX to render our expression
async with self.session.post(urljoin(opt.rtex_instance, "api/v2"), json=payload) as r:
if r.status != 200:
raise cmn.BotHTTPError(r)
render_result = await r.json()
if render_result["status"] != "success":
embed = cmn.embed_factory(ctx)
embed.title = "LaTeX Rendering Failed!"
embed.description = ("Here are some common reasons:\n"
"• Did you forget to use math mode? Surround math expressions with `$`,"
" like `$x^3$`.\n"
"• Are you using a command from a package? It might not be available.\n"
"• Are you including the document headers? We already did that for you.")
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
# if rendering went well, download the file given in the response
async with self.session.get(urljoin(opt.rtex_instance, "api/v2/" + render_result["filename"])) as r:
png_buffer = BytesIO(await r.read())
embed = cmn.embed_factory(ctx)
embed.title = "LaTeX Expression"
embed.description = "Rendered by [rTeX](https://rtex.probablyaweb.site/)."
embed.set_image(url="attachment://tex.png")
await ctx.send(file=discord.File(png_buffer, "tex.png"), embed=embed)
def setup(bot: commands.Bot):
bot.add_cog(TexCog(bot))

79
exts/time.py Normal file
View File

@ -0,0 +1,79 @@
"""
Time extension for qrm
---
Copyright (C) 2021-2023 classabbyamp, 0x5c
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
from datetime import datetime, timedelta
import discord.ext.commands as commands
import common as cmn
class TimeCog(commands.Cog):
offsets = [
("Y", "", timedelta(hours=-12)),
("X", "", timedelta(hours=-11)),
("W", "", timedelta(hours=-10)),
("V", "", timedelta(hours=-9)),
("U", "", timedelta(hours=-8)),
("T", "", timedelta(hours=-7)),
("S", "", timedelta(hours=-6)),
("R", "", timedelta(hours=-5)),
("Q", "", timedelta(hours=-4)),
("P", "", timedelta(hours=-3)),
("O", "", timedelta(hours=-2)),
("N", "", timedelta(hours=-1)),
("Z", "UTC", timedelta(hours=0)),
("A", "", timedelta(hours=+1)),
("B", "", timedelta(hours=+2)),
("C", "", timedelta(hours=+3)),
("D", "", timedelta(hours=+4)),
("E", "", timedelta(hours=+5)),
("F", "", timedelta(hours=+6)),
("G", "", timedelta(hours=+7)),
("H", "", timedelta(hours=+8)),
("I", "", timedelta(hours=+9)),
("K", "", timedelta(hours=+10)),
("L", "", timedelta(hours=+11)),
("M", "", timedelta(hours=+12))
]
def __init__(self, bot):
self.bot = bot
@commands.command(name="utc", aliases=["z"], category=cmn.Cats.TIME)
async def _utc_lookup(self, ctx: commands.Context):
"""Returns the current time in UTC."""
now = datetime.utcnow()
result = "**" + now.strftime("%Y-%m-%d %H:%M") + "Z**"
embed = cmn.embed_factory(ctx)
embed.title = "The current time is:"
embed.description = result
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
@commands.command(name="miltime", category=cmn.Cats.TIME)
async def miltime(self, ctx: commands.Context):
"""Prints the current time in all 25 military time zones."""
time = ctx.message.created_at
embed = cmn.embed_factory(ctx)
embed.title = f"{cmn.emojis.clock} Military Time Zones Now"
embed.colour = cmn.colours.good
embed.description = "```"
embed.description += "\n".join([f"{x}: {time + z :%Y-%m-%d %H:%M} {y}" for x, y, z in self.offsets])
embed.description += "```"
embed.add_field(name="Notes", value=(
"**J** is not present in the table, and is used for local time.\n"
"The zones are referenced by their letters, using phonetics.\n"
f"You can check the NATO phonetics for a letter using the `{ctx.prefix}phonetics` command."
))
await ctx.send(embed=embed)
def setup(bot: commands.Bot):
bot.add_cog(TimeCog(bot))

11
info.py
View File

@ -1,19 +1,18 @@
""" """
Static info about the bot. Static info about the bot.
--- ---
Copyright (C) 2019-2020 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
authors = ("@ClassAbbyAmplifier#2229", "@0x5c#0639") authors = ("@classabbyamp", "@0x5c.io")
description = """A bot with various useful ham radio-related functions, written in Python.""" description = """A bot with various useful ham radio-related functions, written in Python."""
license = "Released under the GNU General Public License v2" 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! 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 \ 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).""" in [miaowware/qrm-resources](https://github.com/miaowware/qrm-resources)."""
release = "2.6.0" release = "2.9.2"
bot_server = "https://discord.gg/Ntbg3J4" bot_server = "https://discord.gg/Ntbg3J4"

37
main.py
View File

@ -2,10 +2,9 @@
""" """
qrm, a bot for Discord qrm, a bot for Discord
--- ---
Copyright (C) 2019-2021 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """
@ -17,6 +16,7 @@ from datetime import datetime, time
from types import SimpleNamespace from types import SimpleNamespace
from pathlib import Path from pathlib import Path
import httpx
import pytz import pytz
import discord import discord
@ -50,9 +50,9 @@ connector = loop.run_until_complete(conn.new_connector())
# Defining the intents # Defining the intents
intents = discord.Intents.none() intents = discord.Intents.none()
intents.guilds = True intents.guilds = True
intents.guild_messages = True intents.messages = True
intents.dm_messages = True
intents.reactions = True intents.reactions = True
intents.message_content = True
member_cache = discord.MemberCacheFlags.from_intents(intents) member_cache = discord.MemberCacheFlags.from_intents(intents)
@ -70,11 +70,13 @@ bot.qrm = SimpleNamespace()
# Let's store stuff here. # Let's store stuff here.
bot.qrm.connector = connector bot.qrm.connector = connector
bot.qrm.debug_mode = debug_mode bot.qrm.debug_mode = debug_mode
# TODO: Add code to close the client
bot.qrm.httpx_client = httpx.AsyncClient()
# --- Commands --- # --- Commands ---
@bot.command(name="restart", aliases=["rs"], category=cmn.cat.admin) @bot.command(name="restart", aliases=["rs"], category=cmn.BoltCats.ADMIN)
@commands.check(cmn.check_if_owner) @commands.check(cmn.check_if_owner)
async def _restart_bot(ctx: commands.Context): async def _restart_bot(ctx: commands.Context):
"""Restarts the bot.""" """Restarts the bot."""
@ -82,10 +84,10 @@ async def _restart_bot(ctx: commands.Context):
await cmn.add_react(ctx.message, cmn.emojis.check_mark) await cmn.add_react(ctx.message, cmn.emojis.check_mark)
print(f"[**] Restarting! Requested by {ctx.author}.") print(f"[**] Restarting! Requested by {ctx.author}.")
exit_code = 42 # Signals to the wrapper script that the bot needs to be restarted. 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.cat.admin) @bot.command(name="shutdown", aliases=["shut"], category=cmn.BoltCats.ADMIN)
@commands.check(cmn.check_if_owner) @commands.check(cmn.check_if_owner)
async def _shutdown_bot(ctx: commands.Context): async def _shutdown_bot(ctx: commands.Context):
"""Shuts down the bot.""" """Shuts down the bot."""
@ -93,10 +95,10 @@ async def _shutdown_bot(ctx: commands.Context):
await cmn.add_react(ctx.message, cmn.emojis.check_mark) await cmn.add_react(ctx.message, cmn.emojis.check_mark)
print(f"[**] Shutting down! Requested by {ctx.author}.") print(f"[**] Shutting down! Requested by {ctx.author}.")
exit_code = 0 # Signals to the wrapper script that the bot should not be restarted. 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.cat.admin) @bot.group(name="extctl", aliases=["ex"], case_insensitive=True, category=cmn.BoltCats.ADMIN)
@commands.check(cmn.check_if_owner) @commands.check(cmn.check_if_owner)
async def _extctl(ctx: commands.Context): async def _extctl(ctx: commands.Context):
"""Extension control commands. """Extension control commands.
@ -124,10 +126,10 @@ async def _extctl_load(ctx: commands.Context, extension: str):
"""Loads an extension.""" """Loads an extension."""
try: try:
bot.load_extension(ext_dir + "." + extension) bot.load_extension(ext_dir + "." + extension)
except commands.ExtensionNotFound as e: except discord.errors.ExtensionNotFound as e:
try: try:
bot.load_extension(plugin_dir + "." + extension) bot.load_extension(plugin_dir + "." + extension)
except commands.ExtensionNotFound: except discord.errors.ExtensionNotFound:
raise e raise e
await cmn.add_react(ctx.message, cmn.emojis.check_mark) await cmn.add_react(ctx.message, cmn.emojis.check_mark)
@ -141,10 +143,10 @@ async def _extctl_reload(ctx: commands.Context, extension: str):
await cmn.add_react(ctx.message, pika) await cmn.add_react(ctx.message, pika)
try: try:
bot.reload_extension(ext_dir + "." + extension) bot.reload_extension(ext_dir + "." + extension)
except commands.ExtensionNotLoaded as e: except discord.errors.ExtensionNotLoaded as e:
try: try:
bot.reload_extension(plugin_dir + "." + extension) bot.reload_extension(plugin_dir + "." + extension)
except commands.ExtensionNotLoaded: except discord.errors.ExtensionNotLoaded:
raise e raise e
await cmn.add_react(ctx.message, cmn.emojis.check_mark) await cmn.add_react(ctx.message, cmn.emojis.check_mark)
@ -154,10 +156,10 @@ async def _extctl_unload(ctx: commands.Context, extension: str):
"""Unloads an extension.""" """Unloads an extension."""
try: try:
bot.unload_extension(ext_dir + "." + extension) bot.unload_extension(ext_dir + "." + extension)
except commands.ExtensionNotLoaded as e: except discord.errors.ExtensionNotLoaded as e:
try: try:
bot.unload_extension(plugin_dir + "." + extension) bot.unload_extension(plugin_dir + "." + extension)
except commands.ExtensionNotLoaded: except discord.errors.ExtensionNotLoaded:
raise e raise e
await cmn.add_react(ctx.message, cmn.emojis.check_mark) await cmn.add_react(ctx.message, cmn.emojis.check_mark)
@ -275,7 +277,8 @@ resource_versions = {
"morse": "v1", "morse": "v1",
"phonetics": "v1", "phonetics": "v1",
"qcodes": "v1", "qcodes": "v1",
"funetics": "v1" "funetics": "v1",
"latex_template": "v1",
} }
bot.qrm.rm = ResourcesManager(cmn.paths.resources, opt.resources_url, resource_versions) bot.qrm.rm = ResourcesManager(cmn.paths.resources, opt.resources_url, resource_versions)

View File

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

View File

@ -1,10 +1,9 @@
""" """
Resource schemas generator for qrm2. Resource schemas generator for qrm2.
--- ---
Copyright (C) 2021 Abigail Gold, 0x5c Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """

View File

@ -1,10 +1,9 @@
""" """
Information about callsigns for the prefixes command in hamcog. Information about callsigns for the prefixes command in hamcog.
--- ---
Copyright (C) 2019-2020 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of discord-qrmbot and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """

View File

@ -1,10 +1,9 @@
""" """
Information about callsigns for the CA prefixes command in hamcog. Information about callsigns for the CA prefixes command in hamcog.
--- ---
Copyright (C) 2019-2020 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of discord-qrmbot and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """

View File

@ -1,10 +1,9 @@
""" """
Information about callsigns for the US prefixes command in hamcog. Information about callsigns for the US prefixes command in hamcog.
--- ---
Copyright (C) 2019-2020 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of discord-qrmbot and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """

View File

@ -1,10 +1,9 @@
""" """
A listing of hamstudy command resources A listing of hamstudy command resources
--- ---
Copyright (C) 2019-2020 Abigail Gold, 0x5c Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of discord-qrmbot and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """

12
run.sh
View File

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

View File

@ -31,19 +31,23 @@ owners_uids = (200102491231092736, 564766093051166729)
# The extensions to load when running the bot. # The extensions to load when running the bot.
exts = [ exts = [
"ae7q",
"base", "base",
"ae7q",
"callsign",
"codes",
"contests",
"dbconv",
"dxcc",
"fun", "fun",
"grid", "grid",
"ham",
"image", "image",
"lookup", "land_weather",
"morse", "morse",
"qrz", "prefixes",
"study",
"weather",
"dbconv",
"propagation", "propagation",
"study",
"tex",
"time",
] ]
# URL to the resources # URL to the resources
@ -86,3 +90,6 @@ msg_reacts = {}
# A :pika: emote's ID, None for no emote :c # A :pika: emote's ID, None for no emote :c
pika = 658733876176355338 pika = 658733876176355338
# Base URL to a deployment of rTeX, which performs LaTeX rendering.
rtex_instance = "https://rtex.probablyaweb.site/"

View File

@ -1,10 +1,9 @@
""" """
Wrapper to handle aiohttp connector creation. Wrapper to handle aiohttp connector creation.
--- ---
Copyright (C) 2020 Abigail Gold, 0x5c Copyright (C) 2020-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of SPDX-License-Identifier: LiLiQ-Rplus-1.1
the GNU General Public License, version 2.
""" """

View File

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

View File

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