Compare commits
325 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4b55ab49b7 | ||
|
cf378a2ef4 | ||
|
13a8a63300 | ||
|
23619949d7 | ||
|
444687bd12 | ||
|
86da8d135a | ||
|
67add85a7a | ||
|
abdc5ebacb | ||
|
a5cbb5a09a | ||
|
ce99cc194e | ||
|
f8d7316071 | ||
|
9feeb01e42 | ||
|
fcb682ec4a | ||
|
c8a1128927 | ||
|
df08cefe25 | ||
|
cf93773a3c | ||
|
642b49041a | ||
|
e95f991300 | ||
|
56ae14a5c3 | ||
|
30c6e96883 | ||
|
44a6905f7b | ||
|
d7de78e582 | ||
|
b000c9173e | ||
|
5460dd811b | ||
|
a00d613430 | ||
|
6b0cdb6249 | ||
|
4eed94b55b | ||
|
3110961a3a | ||
|
a4c8a056ac | ||
|
9368ccd9e2 | ||
|
8efd958314 | ||
|
4803bf89b2 | ||
|
c82216cae6 | ||
|
1650cd50dc | ||
|
1b0b244f99 | ||
|
5db77f78d9 | ||
|
c7ea5e0998 | ||
|
adffd82127 | ||
|
970159e81b | ||
|
f5aeefc934 | ||
|
aac9262469 | ||
|
b472cdfa25 | ||
|
585cae8b97 | ||
|
c3fbd3e719 | ||
|
7eadb50b96 | ||
|
98642c099d | ||
|
ef6f01d1a3 | ||
|
91c5217d24 | ||
|
4659cf2a48 | ||
|
d33dad9f89 | ||
|
be083d2cc8 | ||
|
e2d1d1fc87 | ||
|
68eaeff476 | ||
|
f690ebb357 | ||
|
51e571b97d | ||
|
85ac05c337 | ||
|
718b2a7a80 | ||
|
0189db8792 | ||
|
80d6a989cc | ||
|
8f1782dcc0 | ||
|
aefca97e4f | ||
|
bbd646a7ec | ||
|
8433a7ade0 | ||
|
de0e25b09a | ||
|
4fb1320b2d | ||
|
36acda1666 | ||
|
04ccd807cd | ||
|
aa7b72634b | ||
|
9ee42529e2 | ||
|
74df3ed1f1 | ||
|
e7a1a4e5de | ||
|
2eb183ff08 | ||
|
0cb6ccd285 | ||
|
f55738a8a2 | ||
|
abf79b844e | ||
|
c3f002a9df | ||
|
ff40f0caca | ||
|
c0ad8d1108 | ||
|
ce62c93d03 | ||
|
c5c065bd47 | ||
|
8f8b7f87de | ||
|
144b288f09 | ||
|
dc1efa7b0c | ||
|
38ba8d9d0c | ||
|
62b549619e | ||
|
5910fc5191 | ||
|
2db9dee951 | ||
|
e33d5f7213 | ||
|
0a278fe14f | ||
|
75bf8a638e | ||
|
9e36aab469 | ||
|
cb1aed4278 | ||
|
c27c0b8a48 | ||
|
9d419b159d | ||
|
858223b148 | ||
|
d36702f2b3 | ||
|
6dfa35a8eb | ||
|
9f4bb04364 | ||
|
3de71f7325 | ||
|
22d60ed30f | ||
|
1cdbc66b5c | ||
|
ea7cc7823a | ||
|
22c2cab4d1 | ||
|
63b1d81955 | ||
|
6e54a27f14 | ||
|
f6d69f7498 | ||
|
c569fdb422 | ||
|
98b2f30dcc | ||
|
1cb7ef5861 | ||
|
bb3ee319b6 | ||
|
aaec204c43 | ||
|
4c276bbc4d | ||
|
89e26dbd2b | ||
|
52995a8160 | ||
|
4e572fa6d8 | ||
|
08d611e8c4 | ||
|
a140f74273 | ||
|
137b275c78 | ||
|
9f72a63202 | ||
|
1eed538d2c | ||
|
190eef7fa9 | ||
|
38eecf0010 | ||
|
4faba37c73 | ||
|
0241d1856a | ||
|
54960e8237 | ||
|
7ffe821f78 | ||
|
31ca12a048 | ||
|
361d4f894e | ||
|
79545403d8 | ||
|
3a5ce9f1c0 | ||
|
850aa36894 | ||
|
bc2515c9fc | ||
|
3134c41191 | ||
|
a7b4203112 | ||
|
123dff28fa | ||
|
c6581841ab | ||
|
df8d258446 | ||
|
fa82610469 | ||
|
0d5ca5c2fe | ||
|
950840be60 | ||
|
78e34dff63 | ||
|
ab73001340 | ||
|
956fc4b02f | ||
|
be6c78f4de | ||
|
86ab415619 | ||
|
351b1e2d21 | ||
|
4d64d22ec6 | ||
|
d7e544edcd | ||
|
483a0bad19 | ||
|
4ab4748b9f | ||
|
eb98e295d2 | ||
|
6dfc05217f | ||
|
b091b22c17 | ||
|
43a24d614b | ||
|
4139b23fe6 | ||
|
31a4fca70e | ||
|
2b3fd59c2d | ||
|
ac183a3e7f | ||
|
a25597ec82 | ||
|
010e161a46 | ||
|
5807b54cb3 | ||
|
afe95b912b | ||
|
6092252eaa | ||
|
32c7029b88 | ||
|
5a43495a00 | ||
|
e2968bf4a0 | ||
|
750fe65fcd | ||
|
362a113e2e | ||
|
3eb6d8b12b | ||
|
6c5133f140 | ||
|
a93aafaa96 | ||
|
39531fd2b0 | ||
|
ad96b59b23 | ||
|
6074f5bc45 | ||
|
23ca74253d | ||
|
af68be2b2a | ||
|
2ae11058b2 | ||
|
eba8eec5ac | ||
|
d6e381efec | ||
|
940f45f4d4 | ||
|
be7e29b387 | ||
|
518ead9ccd | ||
|
6e5acba6e9 | ||
|
ad50a86f9d | ||
|
0bfa0c6e41 | ||
|
ce64d882b8 | ||
|
0aac09f3bc | ||
|
e3534d02d7 | ||
|
cdcb0e17d2 | ||
|
cd2503c953 | ||
|
b4c165851c | ||
|
77a5af73bc | ||
|
4b7064cad9 | ||
|
d8fe3cfa02 | ||
|
ffc3be7e24 | ||
|
5dab93b7d3 | ||
|
e660b1a8f5 | ||
|
3d96a43c50 | ||
|
e8bb18ea8c | ||
|
19952396f2 | ||
|
1831c56f58 | ||
|
77e14a109c | ||
|
2ac13346d4 | ||
|
2eea7dce23 | ||
|
f26a7af928 | ||
|
786440edcb | ||
|
c47d211016 | ||
|
855935a26e | ||
|
ff9d46f379 | ||
|
0f0c3bf723 | ||
|
7c818cfb34 | ||
|
27863ae6bf | ||
|
de999bc39d | ||
|
80d80ab718 | ||
|
8cb1a8df15 | ||
|
fa2dded81b | ||
|
bda0540fa8 | ||
|
c9510ad9b9 | ||
|
00f9929deb | ||
|
3597367046 | ||
|
7e35e8949a | ||
|
77b572eb3e | ||
|
be042a9641 | ||
|
488ae6cc98 | ||
|
a65fd04dbd | ||
|
6329718d29 | ||
|
756a15c4c5 | ||
|
b462527211 | ||
|
93b42e64dc | ||
|
f6103ef6f1 | ||
|
090b96482d | ||
|
6d71974ea1 | ||
|
0af82ac241 | ||
|
2ba6249b90 | ||
|
2c11dad358 | ||
|
5f796d479e | ||
|
3ba55d4c35 | ||
|
f4ed93dc76 | ||
|
2cb4b03532 | ||
|
bc93462c29 | ||
|
6867c45c8c | ||
|
dcbb7acab8 | ||
|
3803ce6045 | ||
|
8ca4911072 | ||
|
6e2468f04f | ||
|
b17a8a1749 | ||
|
8dfa7001ef | ||
|
f6ed8430b9 | ||
|
d650cbd6c1 | ||
|
4d9f9d1b19 | ||
|
1c649aacc2 | ||
|
2049ca9fca | ||
|
38416d9050 | ||
|
8dcdc22fe4 | ||
|
c57056e586 | ||
|
b281122d41 | ||
|
fa1cc16c6f | ||
|
dfd9479e0d | ||
|
4b122518f5 | ||
|
0e28366265 | ||
|
9471eaa3cb | ||
|
27c290cff8 | ||
|
58878352aa | ||
|
af136c3d3e | ||
|
9ef4864d15 | ||
|
6803b058d5 | ||
|
e37fdf40fb | ||
|
fdfc8cce06 | ||
|
56b74108eb | ||
|
29d0440d3d | ||
|
29e75c38e1 | ||
|
6c32dcbead | ||
|
74327378b9 | ||
|
c1d3c63503 | ||
|
6768a1e01d | ||
|
ea508d2a59 | ||
|
f56efd3410 | ||
|
976b3e8bf3 | ||
|
0b4204dc13 | ||
|
8686b0ef96 | ||
|
9113638b67 | ||
|
831667ec10 | ||
|
b64c7ee39a | ||
|
528307f22f | ||
|
09c58f9ba2 | ||
|
61f0c9423e | ||
|
b6f6d0408c | ||
|
94f9865103 | ||
|
08ca455895 | ||
|
925a05aafb | ||
|
12886fad89 | ||
|
ce107f5a82 | ||
|
0aa5e6ebd6 | ||
|
671b0e9ee5 | ||
|
776ff72581 | ||
|
58c69f5aeb | ||
|
2c3535d99e | ||
|
04cbc920ce | ||
|
f5a9b0b780 | ||
|
eb5e038624 | ||
|
1a1b4c9329 | ||
|
8db13755bc | ||
|
ac740ee679 | ||
|
0608a74e6c | ||
|
eaa47fc724 | ||
|
60764cd733 | ||
|
8bfaaf4af6 | ||
|
30455153ba | ||
|
d9bd3d354c | ||
|
8f15feab94 | ||
|
4e73fa3734 | ||
|
5e33a6aca6 | ||
|
07fd9255e0 | ||
|
238cdf1c72 | ||
|
25eae1fc0f | ||
|
f4f034abfe | ||
|
ae673f7026 | ||
|
8a9c8cbd43 | ||
|
b94a8d42dd | ||
|
60f8e80b82 | ||
|
6e516a12db | ||
|
9552a7f6d4 | ||
|
e7baca453c | ||
|
673d154b94 | ||
|
118d5e0b63 |
@ -11,3 +11,5 @@ docker-compose.yml
|
|||||||
Makefile
|
Makefile
|
||||||
|
|
||||||
README*.md
|
README*.md
|
||||||
|
|
||||||
|
/resource_schemas.py
|
||||||
|
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,29 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Report a bug to help us improve qrm
|
|
||||||
title: ''
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Run command '...' with input '...'
|
|
||||||
2. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**System (include if related to running the bot):**
|
|
||||||
- OS: [e.g. Linux, Docker]
|
|
||||||
- Version: [e.g. 22]
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
82
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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
@ -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
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for qrm
|
|
||||||
title: ''
|
|
||||||
labels: enhancement
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
12
.github/workflows/checks.yml
vendored
Normal 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
|
68
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# vim: ts=2 sw=2:
|
||||||
|
|
||||||
|
name: Docker Build and Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
push:
|
||||||
|
# Publish `master` as Docker `dev` image.
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
# Publish `v*` tags as x.x.x images and as `latest`.
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
name: Build and push docker images
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: classabbyamp/treeless-checkout-action@v1
|
||||||
|
|
||||||
|
- name: Write ref to file
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
run: git rev-list -n 1 $GITHUB_REF > ./git_commit
|
||||||
|
|
||||||
|
- name: Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
tags: |
|
||||||
|
type=sha,prefix=
|
||||||
|
type=raw,value=dev,enable={{is_default_branch}}
|
||||||
|
type=match,pattern=v(.*),group=1
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.authors=classabbyamp and 0x5c
|
||||||
|
org.opencontainers.image.url=https://github.com/miaowware/qrm2
|
||||||
|
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||||
|
org.opencontainers.image.vendor=miaowware
|
||||||
|
org.opencontainers.image.title=qrm2
|
||||||
|
org.opencontainers.image.description=Discord bot with ham radio functions
|
||||||
|
org.opencontainers.image.licenses=LiLiQ-Rplus-1.1
|
||||||
|
|
||||||
|
- name: Login to Github Container Registry
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
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 }}
|
19
.github/workflows/linting.yml
vendored
@ -1,21 +1,22 @@
|
|||||||
name: Linting
|
name: Linting
|
||||||
|
|
||||||
on: [push]
|
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.7
|
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 }}
|
|
||||||
|
62
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# vim: ts=2 sw=2:
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# Sequence of patterns matched against refs/tags
|
||||||
|
tags:
|
||||||
|
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||||
|
|
||||||
|
name: Create Release
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Create Release
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.ref }}
|
||||||
|
|
||||||
|
- name: Get Version Info
|
||||||
|
id: get_tag
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
SUBJECT=$(/usr/bin/git tag -l ${GITHUB_REF#refs/tags/} --format='%(subject)')
|
||||||
|
BODY=$(/usr/bin/git tag -l ${GITHUB_REF#refs/tags/} --format='%(body)' | sed '/-----BEGIN PGP SIGNATURE-----/,$d')
|
||||||
|
|
||||||
|
echo "SUBJECT=$SUBJECT"
|
||||||
|
echo "BODY=$BODY"
|
||||||
|
|
||||||
|
echo 'tag_subject<<EOS' >> $GITHUB_ENV
|
||||||
|
echo "$SUBJECT" >> $GITHUB_ENV
|
||||||
|
echo 'EOS' >> $GITHUB_ENV
|
||||||
|
echo 'tag_body<<EOB' >> $GITHUB_ENV
|
||||||
|
echo "$BODY" >> $GITHUB_ENV
|
||||||
|
echo 'EOB' >> $GITHUB_ENV
|
||||||
|
echo "tag_version=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
|
echo "version_num=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Get Changelog Content
|
||||||
|
id: changelog_reader
|
||||||
|
uses: mindsers/changelog-reader-action@v2
|
||||||
|
with:
|
||||||
|
version: ${{ env.version_num }}
|
||||||
|
path: ./CHANGELOG.md
|
||||||
|
|
||||||
|
- name: Publish Release
|
||||||
|
id: create_release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
tag: ${{ env.tag_version }}
|
||||||
|
name: ${{ env.tag_subject }}
|
||||||
|
body: |
|
||||||
|
${{ env.tag_body }}
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
${{ steps.changelog_reader.outputs.changes }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
3
.gitignore
vendored
@ -6,9 +6,12 @@ cty.zip
|
|||||||
/botenv/
|
/botenv/
|
||||||
/devenv/
|
/devenv/
|
||||||
/data/
|
/data/
|
||||||
|
/data/plugins/
|
||||||
|
|
||||||
/docker-compose.yml
|
/docker-compose.yml
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
|
||||||
#########################################################
|
#########################################################
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
|
237
CHANGELOG.md
@ -7,7 +7,207 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
|
||||||
## [v2.1.0] - 2020-01-04
|
## [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
|
||||||
|
### Added
|
||||||
|
- MUF and foF2 maps from [prop.kc2g.com](https://prop.kc2g.com/).
|
||||||
|
- Commands to show METAR (`?metar`) and TAF (`?taf`) (aeronautical weather conditions).
|
||||||
|
- The ability to select an element of a pool in `?hamstudy`.
|
||||||
|
- The ability to answer ❓ to a HamStudy question to get the answer.
|
||||||
|
- The list of available prefixes to `?help` when there is more than one.
|
||||||
|
- `?donate` command to show ways to support qrm's development.
|
||||||
|
- `?invite` command to invite qrm to your server.
|
||||||
|
- Configuration options to disable showing the `?invite` and set default invite permissions (enabled by default).
|
||||||
|
- Configuration option to show QRZ nickname in place of first name (enabled by default).
|
||||||
|
### Changed
|
||||||
|
- New colour theme for `?greyline`.
|
||||||
|
- Moved great circle distance and bearing calculation from `?ungrid` to `?griddistance`.
|
||||||
|
- `?ungrid` to `?latlong`.
|
||||||
|
- Renamed `?cond` to `?solar`.
|
||||||
|
- Reduced `?hamstudy` timeout to 5 minutes.
|
||||||
|
- Library used for QRZ lookups.
|
||||||
|
- Python>=3.9 now required.
|
||||||
|
- Most resources are now stored on a website. New indexes are pulled at start and the images linked instead of uploaded.
|
||||||
|
### Fixed
|
||||||
|
- Weird image caching situation for `?greyline` on Discord's side.
|
||||||
|
- The help command was not using the prefix it was invoked with.
|
||||||
|
### Deprecated
|
||||||
|
- `?ungrid`.
|
||||||
|
- Deprecated old `?solar` aliases (`?cond`, etc).
|
||||||
|
- Deprecated old `?call` alias (`?qrz`).
|
||||||
|
|
||||||
|
|
||||||
|
## [2.5.1] - 2020-12-10
|
||||||
|
### Fixed
|
||||||
|
- The result of `?greyline` was cached by discord and would get out of date.
|
||||||
|
- Broken reaction functionality in `?hamstudy`.
|
||||||
|
|
||||||
|
|
||||||
|
## [2.5.0] - 2020-10-31
|
||||||
|
### Added
|
||||||
|
- Italian (`it_hf`, `it_vhf`, `it_shf`), Japanese (`jp`) and Australian (`au`) band charts.
|
||||||
|
### Fixed
|
||||||
|
- Details to the Netherlands bandplan command to accurately represent VERNON (Netherlands ARRL equivalent organisation).
|
||||||
|
- eQSL, paper QSL, and Logbook of the World status in `?qrz` sometimes being shown incorrectly.
|
||||||
|
- Fixed network error in `?greyline`.
|
||||||
|
|
||||||
|
|
||||||
|
## [2.4.1] - 2020-10-06
|
||||||
|
### Changed
|
||||||
|
- Bumped discord.py to 1.5.0
|
||||||
|
|
||||||
|
|
||||||
|
## [2.4.0] - 2020-09-27
|
||||||
|
### Added
|
||||||
|
- Canadian prefix info to the `?prefixes` command.
|
||||||
|
- `?worksplit` command.
|
||||||
|
- Maps for CQ Zones, ITU Zones, ITU Regions, and Canadian prefixes.
|
||||||
|
- Attribution for all maps.
|
||||||
|
- Option to append ` | ?help` to the playing status.
|
||||||
|
- `?dbconv` command to convert voltage, power, and antenna gain values.
|
||||||
|
### Changed
|
||||||
|
- ARRL/RAC section maps to include all current ARRL/RAC sections.
|
||||||
|
### Fixed
|
||||||
|
- Issue where multiple prefixes were not handled properly.
|
||||||
|
|
||||||
|
|
||||||
|
## [2.3.2] - 2020-07-22
|
||||||
|
### Fixed
|
||||||
|
- Dependency issues
|
||||||
|
|
||||||
|
|
||||||
|
## [2.3.1] - 2020-04-02
|
||||||
|
### Fixed
|
||||||
|
- Wordlist containing inappropriate words.
|
||||||
|
|
||||||
|
|
||||||
|
## [2.3.0] - 2020-03-30
|
||||||
|
### Added
|
||||||
|
- `?phoneticweight` command, which calculates a message's length in syllables.
|
||||||
|
- `?standards` command to display [xkcd 927](https://xkcd.com/927/).
|
||||||
|
### Changed
|
||||||
|
- Python>=3.7 now required.
|
||||||
|
|
||||||
|
|
||||||
|
## [2.2.3] - 2020-03-29
|
||||||
|
### Fixed
|
||||||
|
- Commands are no longer case-sensitive.
|
||||||
|
|
||||||
|
|
||||||
|
## [2.2.2] - 2020-02-25
|
||||||
|
### Fixed
|
||||||
|
- Fixed issue where HamStudy questions with images would cause an error.
|
||||||
|
- Added/fixed/removed typing indicators in numerous commands.
|
||||||
|
|
||||||
|
|
||||||
|
## [2.2.1] - 2020-02-20
|
||||||
|
### Fixed
|
||||||
|
- Fixed issue where some HamStudy pools will become unselectable.
|
||||||
|
|
||||||
|
|
||||||
|
## [2.2.0] - 2020-02-15
|
||||||
|
### Added
|
||||||
|
- Added Trustee field to qrz command for club callsigns.
|
||||||
|
- Added alias for `ae7q call` command (`ae7q c`).
|
||||||
|
- Added ae7q lookup by FRN and Licensee ID, and for trustee records (`ae7q frn, licensee, trustee`).
|
||||||
|
### Changed
|
||||||
|
- Changelog command to accept a version as argument.
|
||||||
|
- The qrz command can now link to a QRZ page instead of embedding the data with the `--link` flag.
|
||||||
|
- All currently-available pools can now be accessed by the `hamstudy` command.
|
||||||
|
- The `hamstudy` command now uses the syntax `?hamstudy <country> <pool>`.
|
||||||
|
- Replaced `hamstudyanswer` command with answering by reaction.
|
||||||
|
- Removed all generic error handling from commands.
|
||||||
|
- Cleaned up the description of multiple commands.
|
||||||
|
- Updated repository links from classabbyamp/discord-qrm2 to miaowware/qrm2.
|
||||||
|
### Fixed
|
||||||
|
- Fixed ditto marks (") appearing in the ae7q call command.
|
||||||
|
- Fixed issue where incorrect table was parsed in ae7q call command.
|
||||||
|
- Fixed warning emoji reaction on messages starting with "??".
|
||||||
|
- Fixed issue where `prefixes` would error when given an invalid argument.
|
||||||
|
|
||||||
|
|
||||||
|
## [2.1.0] - 2020-01-04
|
||||||
### Added
|
### Added
|
||||||
- New NATO "phonetics" command.
|
- New NATO "phonetics" command.
|
||||||
- Flag emojis to commands with countries.
|
- Flag emojis to commands with countries.
|
||||||
@ -18,14 +218,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
- Command linking to the issue tracker.
|
- Command linking to the issue tracker.
|
||||||
- New key in options.py: pika.
|
- New key in options.py: pika.
|
||||||
### Changed
|
### Changed
|
||||||
- The "phonetics" command is not called "funetics".
|
- The "phonetics" command is now called "funetics".
|
||||||
- All commands now respond in embeds.
|
- All commands now respond in embeds.
|
||||||
- Playing status can now change on a schedule or randomly from a list.
|
- Playing status can now change on a schedule or randomly from a list.
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fixed incorrect information in the `prefixes` command.
|
- Fixed incorrect information in the `prefixes` command.
|
||||||
|
|
||||||
|
|
||||||
## [v2.0.0] - 2019-12-16
|
## [2.0.0] - 2019-12-16
|
||||||
### Added
|
### Added
|
||||||
- Rich lookup for AE7Q.com (callsigns only, more to come)
|
- Rich lookup for AE7Q.com (callsigns only, more to come)
|
||||||
- Rich lookup for QRZ.com, if a QRZ subscription is present
|
- Rich lookup for QRZ.com, if a QRZ subscription is present
|
||||||
@ -52,9 +252,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
- Issue in morse and unmorse commands where spaces were not interpreted correctly
|
- Issue in morse and unmorse commands where spaces were not interpreted correctly
|
||||||
|
|
||||||
|
|
||||||
## v1.0.0 - 2019-07-31 [YANKED]
|
## 1.0.0 - 2019-07-31 [YANKED]
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/classabbyamp/discord-qrm2/compare/v2.1.0...HEAD
|
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.9.2...HEAD
|
||||||
[v2.1.0]: https://github.com/classabbyamp/discord-qrm2/releases/tag/v2.1.0
|
[2.9.2]: https://github.com/miaowware/qrm2/releases/tag/v2.9.2
|
||||||
[v2.0.0]: https://github.com/classabbyamp/discord-qrm2/releases/tag/v2.0.0
|
[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.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.4.1]: https://github.com/miaowware/qrm2/releases/tag/v2.4.1
|
||||||
|
[2.4.0]: https://github.com/miaowware/qrm2/releases/tag/v2.4.0
|
||||||
|
[2.3.2]: https://github.com/miaowware/qrm2/releases/tag/v2.3.2
|
||||||
|
[2.3.1]: https://github.com/miaowware/qrm2/releases/tag/v2.3.1
|
||||||
|
[2.3.0]: https://github.com/miaowware/qrm2/releases/tag/v2.3.0
|
||||||
|
[2.2.3]: https://github.com/miaowware/qrm2/releases/tag/v2.2.3
|
||||||
|
[2.2.2]: https://github.com/miaowware/qrm2/releases/tag/v2.2.2
|
||||||
|
[2.2.1]: https://github.com/miaowware/qrm2/releases/tag/v2.2.1
|
||||||
|
[2.2.0]: https://github.com/miaowware/qrm2/releases/tag/v2.2.0
|
||||||
|
[2.1.0]: https://github.com/miaowware/qrm2/releases/tag/v2.1.0
|
||||||
|
[2.0.0]: https://github.com/miaowware/qrm2/releases/tag/v2.0.0
|
||||||
|
339
COPYING
@ -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.
|
|
55
DEVELOPING.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Development Guide for qrm
|
||||||
|
|
||||||
|
**Make sure to also read [`CONTRIBUTING.md`][0], everything in there applies here.**
|
||||||
|
|
||||||
|
### Environment Setup
|
||||||
|
|
||||||
|
1. [Fork this repo][1] into your own GitHub namespace.
|
||||||
|
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. Install python 3.9 or higher.
|
||||||
|
1. Install `libcairo` and `libjpeg`. Package names may vary by distro or OS.
|
||||||
|
1. Run `make dev-install`.
|
||||||
|
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`.
|
||||||
|
Also add your [QRZ credentials][3] if needed.
|
||||||
|
1. In `data/options.py`, change values as needed.
|
||||||
|
Some commands require adding your Discord user ID to `owner_uids`.
|
||||||
|
1. To activate the virtual env that was created by `make`, run `source botenv/bin/activate` (or the equivelent for your shell or operating system).
|
||||||
|
|
||||||
|
These instructions are fairly \*NIX-centric, so if you would like to develop on Windows, it is suggested that the Windows Subsystem for Linux be used.
|
||||||
|
|
||||||
|
## While You Develop
|
||||||
|
|
||||||
|
To run qrm, use the command `./run.sh`.
|
||||||
|
We recommend you use the `--pass-errors` flag to avoid perpetual restart loops if you break the bot.
|
||||||
|
It exists because repeatedly mashing [Ctrl+C] at high speed to break a fast loop is not fun.
|
||||||
|
|
||||||
|
Make sure to add [type hints][4] to your code.
|
||||||
|
This is what `mypy` validates in the code.
|
||||||
|
|
||||||
|
Using `dev-notes` for documentation is especially important if you introduce a new json file format (like for maps and bandplans) or to document some development process (like the command to crush the various images in the repository).
|
||||||
|
|
||||||
|
### Test your changes
|
||||||
|
|
||||||
|
In addition to testing functionality, make sure to run `flake8` to ensure your code uses the proper style, and `mypy [files...]` to ensure proper typing.
|
||||||
|
You can also enable them for this project in your IDE if supported.
|
||||||
|
This will give you automatic and continuous linting and type checking.
|
||||||
|
|
||||||
|
### A Note on Style
|
||||||
|
|
||||||
|
qrm tries to keep to PEP 8 style whenever possible.
|
||||||
|
Use the utility `flake8` to check that you follow this style.
|
||||||
|
When you start a PR or push commits, GitHub will automatically run this for you,
|
||||||
|
but we prefer that developers check this before committing and opening PRs.
|
||||||
|
|
||||||
|
Otherwise, try to follow the existing style:
|
||||||
|
- double-quotes except when required to be single,
|
||||||
|
- indentation of mult-line structures matching other examples in the code,
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
[0]: https://github.com/miaowware/.github/blob/master/CONTRIBUTING.md
|
||||||
|
[1]: https://github.com/miaowware/qrm2/fork
|
||||||
|
[2]: https://discordpy.readthedocs.io/en/latest/discord.html
|
||||||
|
[3]: https://www.qrz.com/page/xml_data.html
|
||||||
|
[4]: https://docs.python.org/3/library/typing.html
|
42
Dockerfile
@ -1,35 +1,31 @@
|
|||||||
FROM alpine:3.10
|
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 \
|
||||||
echo "**** install build packages ****" && \
|
echo "**** update system ****" && \
|
||||||
apk add --no-cache --virtual=build-dependencies \
|
xbps-install -Suy xbps -R ${REPOSITORY} && \
|
||||||
g++ \
|
xbps-install -uy -R ${REPOSITORY} && \
|
||||||
git \
|
echo "**** install system packages ****" && \
|
||||||
gcc \
|
xbps-install -y -R ${REPOSITORY} ${PKGS} python3.11 && \
|
||||||
libxml2-dev \
|
|
||||||
libxslt-dev \
|
|
||||||
libressl-dev \
|
|
||||||
python3-dev && \
|
|
||||||
echo "**** install runtime packages ****" && \
|
|
||||||
apk add --no-cache \
|
|
||||||
libressl \
|
|
||||||
py3-lxml \
|
|
||||||
py3-pip \
|
|
||||||
python3 && \
|
|
||||||
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 ****" && \
|
||||||
apk del --purge \
|
|
||||||
build-dependencies && \
|
|
||||||
rm -rf \
|
rm -rf \
|
||||||
/root/.cache \
|
/root/.cache \
|
||||||
/tmp/* \
|
/tmp/* \
|
||||||
/var/cache/apk/*
|
/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
@ -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 d’exercer les droits suivants sur le logiciel :
|
||||||
|
1° Produire ou reproduire la totalité ou une partie importante;
|
||||||
|
2° Exécuter ou représenter la totalité ou une partie importante en public;
|
||||||
|
3° 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 :
|
||||||
|
1° Le logiciel doit être accompagné d'un exemplaire de cette licence;
|
||||||
|
2° 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;
|
||||||
|
3° 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 :
|
||||||
|
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
|
||||||
|
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 software’s 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 licensor’s 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 licensor’s 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.
|
20
Makefile
@ -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.7
|
PYTHON_BIN ?= python3.11
|
||||||
PIP_OUTPUT ?= -q
|
PIP_OUTPUT ?= -q
|
||||||
|
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ help:
|
|||||||
|
|
||||||
# Main install target
|
# Main install target
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
install: $(BOTENV)/req_done data/options.py data/keys.py
|
install: $(BOTENV)/req_done data/options.py data/keys.py data/plugins
|
||||||
|
|
||||||
# Virual environment setup
|
# Virual environment setup
|
||||||
$(BOTENV)/success:
|
$(BOTENV)/success:
|
||||||
@ -55,6 +55,11 @@ data:
|
|||||||
@echo "\033[34;1m--> Creating ./data ...\033[0m"
|
@echo "\033[34;1m--> Creating ./data ...\033[0m"
|
||||||
@mkdir -p data
|
@mkdir -p data
|
||||||
|
|
||||||
|
# Creating the ./data/plugins subdirectory
|
||||||
|
data/plugins: ./data
|
||||||
|
@echo "\033[34;1m--> Creating ./data/plugins ...\033[0m"
|
||||||
|
@mkdir -p data/plugins
|
||||||
|
|
||||||
# Copying templates
|
# Copying templates
|
||||||
data/options.py data/keys.py: ./data
|
data/options.py data/keys.py: ./data
|
||||||
@echo "\033[34;1m--> Copying template for ./$@ ...\033[0m"
|
@echo "\033[34;1m--> Copying template for ./$@ ...\033[0m"
|
||||||
@ -71,6 +76,17 @@ clean:
|
|||||||
|
|
||||||
|
|
||||||
### Dev targets ###
|
### Dev targets ###
|
||||||
|
.PHONY: dev-install
|
||||||
|
dev-install: $(BOTENV)/dev_req_done data/options.py data/keys.py data/plugins
|
||||||
|
|
||||||
|
# Installing dev requirements
|
||||||
|
$(BOTENV)/dev_req_done: dev-requirements.txt $(BOTENV)/success
|
||||||
|
@echo "\033[34;1m--> Installing the dependencies...\033[0m"
|
||||||
|
@. $(BOTENV)/bin/activate; \
|
||||||
|
pip install ${PIP_OUTPUT} -U pip setuptools wheel; \
|
||||||
|
pip install ${PIP_OUTPUT} -U -r dev-requirements.txt
|
||||||
|
@touch $(BOTENV)/dev_req_done
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Special targets ###
|
### Special targets ###
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Docker help for qrm2
|
# Docker help for qrm2
|
||||||
|
|
||||||
You have multiple ways to use docker to run an instance of qrm2
|
You have multiple ways to use docker to run an instance of qrm2.
|
||||||
|
|
||||||
- [Docker help for qrm2](#docker-help-for-qrm2)
|
- [Docker help for qrm2](#docker-help-for-qrm2)
|
||||||
- [Using docker-compose and the prebuilt-image (recommended)](#using-docker-compose-and-the-prebuilt-image-recommended)
|
- [Using docker-compose and the prebuilt-image (recommended)](#using-docker-compose-and-the-prebuilt-image-recommended)
|
||||||
@ -23,12 +23,10 @@ This is the easiest method for running the bot without any modifications.
|
|||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
qrm2:
|
qrm2:
|
||||||
image: "classabbyamp/discord-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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Create a subdirectory named `data`.
|
3. Create a subdirectory named `data`.
|
||||||
@ -42,7 +40,7 @@ This is the easiest method for running the bot without any modifications.
|
|||||||
$ docker-compose up -d
|
$ docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
> Run without "-d" to test the bot. (run in foreground)
|
*Run without "-d" to test the bot (run in foreground).*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -59,12 +57,10 @@ This is the easiest method to run the bot with modifications.
|
|||||||
services:
|
services:
|
||||||
qrm2:
|
qrm2:
|
||||||
build: .
|
build: .
|
||||||
image: "discord-qrm2:local-latest"
|
image: "qrm2:local-latest"
|
||||||
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`.
|
||||||
@ -75,10 +71,10 @@ This is the easiest method to run the bot with modifications.
|
|||||||
|
|
||||||
```none
|
```none
|
||||||
$ docker-compose build --pull
|
$ docker-compose build --pull
|
||||||
$ docker-compose -d
|
$ docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
> Run without "-d" to test the bot. (run in foreground)
|
*Run without "-d" to test the bot (run in foreground).*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -95,7 +91,7 @@ This methods is not very nice to use.
|
|||||||
2. Run docker build:
|
2. Run docker build:
|
||||||
|
|
||||||
```none
|
```none
|
||||||
$ docker build -t discord-qrm2:local-latest .
|
$ docker build -t qrm2:local-latest .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -110,5 +106,5 @@ This methods is not very nice to use.
|
|||||||
```
|
```
|
||||||
|
|
||||||
Where `[image]` is either of:
|
Where `[image]` is either of:
|
||||||
- `discord-qrm2:local-latest` if you are building your own.
|
- `qrm2:local-latest` if you are building your own.
|
||||||
- `classabbyamp/discord-qrm2:latest` if you want to use the prebuilt image.
|
- `ghcr.io/miaowware/qrm2:latest` if you want to use the prebuilt image.
|
||||||
|
16
README.md
@ -14,8 +14,12 @@ See [README-DOCKER.md](./README-DOCKER.md)
|
|||||||
|
|
||||||
### Without Docker
|
### Without Docker
|
||||||
|
|
||||||
|
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
|
||||||
```
|
```
|
||||||
@ -26,9 +30,15 @@ Run. For more information on options, see the [quick-bot-no-pain run.sh document
|
|||||||
$ run.sh
|
$ run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
## Copyright
|
## Copyright
|
||||||
|
|
||||||
Copyright (C) 2019 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).
|
||||||
|
137
common.py
@ -1,62 +1,90 @@
|
|||||||
"""
|
"""
|
||||||
Common tools for the bot.
|
Common tools for the bot.
|
||||||
---
|
---
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
General Public License, version 2.
|
|
||||||
---
|
|
||||||
|
|
||||||
`colours`: Colours used by embeds.
|
|
||||||
|
|
||||||
`cat`: Category names for the HelpCommand.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
import enum
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import httpx
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
import discord.ext.commands as commands
|
import discord.ext.commands as commands
|
||||||
|
from discord import Emoji, PartialEmoji
|
||||||
|
|
||||||
import data.options as opt
|
import data.options as opt
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["colours", "cat", "emojis", "embed_factory", "error_embed_factory", "add_react", "check_if_owner"]
|
__all__ = ["colours", "BoltCats", "Cats", "emojis", "paths", "ImageMetadata", "ImagesGroup",
|
||||||
|
"embed_factory", "error_embed_factory", "add_react", "check_if_owner"]
|
||||||
|
|
||||||
|
|
||||||
# --- Common values ---
|
# --- Common values ---
|
||||||
|
|
||||||
colours = SimpleNamespace(good=0x43B581,
|
colours = SimpleNamespace(
|
||||||
|
good=0x43B581,
|
||||||
neutral=0x7289DA,
|
neutral=0x7289DA,
|
||||||
bad=0xF04747)
|
bad=0xF04747,
|
||||||
|
timeout=0xF26522,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BoltCats(enum.Enum):
|
||||||
|
OTHER = "Other"
|
||||||
|
INFO = "Bot Information"
|
||||||
|
ADMIN = "Bot Control"
|
||||||
|
|
||||||
|
|
||||||
# meow
|
# meow
|
||||||
cat = SimpleNamespace(lookup='Information Lookup',
|
class Cats(enum.Enum):
|
||||||
fun='Fun',
|
CALC = "Calculators"
|
||||||
maps='Mapping',
|
CODES = "Code References and Tools"
|
||||||
ref='Reference',
|
FUN = "Fun"
|
||||||
study='Exam Study',
|
LOOKUP = "Information Lookup"
|
||||||
weather='Land and Space Weather')
|
REF = "Reference"
|
||||||
|
STUDY = "Exam Study"
|
||||||
|
TIME = "Time and Time Zones"
|
||||||
|
UTILS = "Utilities"
|
||||||
|
WEATHER = "Land and Space Weather"
|
||||||
|
|
||||||
emojis = SimpleNamespace(check_mark='✅',
|
|
||||||
x='❌',
|
|
||||||
warning='⚠️',
|
|
||||||
question='❓',
|
|
||||||
no_entry='⛔',
|
|
||||||
bangbang='‼️')
|
|
||||||
|
|
||||||
paths = SimpleNamespace(data=Path("./data/"),
|
emojis = SimpleNamespace(
|
||||||
resources=Path("./resources/"),
|
check_mark="✅",
|
||||||
bandcharts=Path("./resources/img/bandcharts/"),
|
x="❌",
|
||||||
maps=Path("./resources/img/maps/"))
|
warning="⚠️",
|
||||||
|
question="❓",
|
||||||
|
no_entry="⛔",
|
||||||
|
bangbang="‼️",
|
||||||
|
clock="🕗",
|
||||||
|
stopwatch="⏱",
|
||||||
|
a="🇦",
|
||||||
|
b="🇧",
|
||||||
|
c="🇨",
|
||||||
|
d="🇩",
|
||||||
|
e="🇪",
|
||||||
|
)
|
||||||
|
|
||||||
|
paths = SimpleNamespace(
|
||||||
|
data=Path("./data/"),
|
||||||
|
resources=Path("./data/resources/"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# --- Classes ---
|
# --- Classes ---
|
||||||
|
|
||||||
|
|
||||||
class ImageMetadata:
|
class ImageMetadata:
|
||||||
"""Represents the metadata of a single image."""
|
"""Represents the metadata of a single image."""
|
||||||
def __init__(self, metadata: list):
|
def __init__(self, metadata: list):
|
||||||
@ -83,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):
|
||||||
@ -94,12 +122,52 @@ class ImagesGroup(collections.abc.Mapping):
|
|||||||
return str(self._images)
|
return str(self._images)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Exceptions ---
|
||||||
|
|
||||||
|
class BotHTTPError(Exception):
|
||||||
|
"""Raised whan a requests fails (status != 200) in a command."""
|
||||||
|
def __init__(self, response: aiohttp.ClientResponse | httpx.Response):
|
||||||
|
if isinstance(response, aiohttp.ClientResponse):
|
||||||
|
self.status = response.status
|
||||||
|
self.reason = response.reason
|
||||||
|
else:
|
||||||
|
self.status = response.status_code
|
||||||
|
self.reason = response.reason_phrase
|
||||||
|
msg = f"Request failed: {self.status} {self.reason}"
|
||||||
|
super().__init__(msg)
|
||||||
|
self.response = response
|
||||||
|
|
||||||
|
|
||||||
|
# --- Converters ---
|
||||||
|
|
||||||
|
class GlobalChannelConverter(commands.IDConverter):
|
||||||
|
"""Converter to get any bot-acessible channel by ID/mention (global), or name (in current guild only)."""
|
||||||
|
async def convert(self, ctx: commands.Context, argument: str):
|
||||||
|
bot = ctx.bot
|
||||||
|
guild = ctx.guild
|
||||||
|
match = self._get_id_match(argument) or re.match(r"<#([0-9]+)>$", argument)
|
||||||
|
result = None
|
||||||
|
if match is None:
|
||||||
|
# not a mention/ID
|
||||||
|
if guild:
|
||||||
|
result = discord.utils.get(guild.text_channels, name=argument)
|
||||||
|
else:
|
||||||
|
raise commands.BadArgument(f"""Channel named "{argument}" not found in this guild.""")
|
||||||
|
else:
|
||||||
|
channel_id = int(match.group(1))
|
||||||
|
result = bot.get_channel(channel_id)
|
||||||
|
if not isinstance(result, (discord.TextChannel, discord.abc.PrivateChannel)):
|
||||||
|
raise commands.BadArgument(f"""Channel "{argument}" not found.""")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
# --- Helper functions ---
|
# --- Helper functions ---
|
||||||
|
|
||||||
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=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
|
||||||
|
|
||||||
|
|
||||||
@ -111,16 +179,17 @@ def error_embed_factory(ctx: commands.Context, exception: Exception, debug_mode:
|
|||||||
fmtd_ex = traceback.format_exception_only(exception.__class__, exception)
|
fmtd_ex = traceback.format_exception_only(exception.__class__, exception)
|
||||||
embed = embed_factory(ctx)
|
embed = embed_factory(ctx)
|
||||||
embed.title = "⚠️ Error"
|
embed.title = "⚠️ Error"
|
||||||
embed.description = "```\n" + '\n'.join(fmtd_ex) + "```"
|
embed.description = "```\n" + "\n".join(fmtd_ex) + "```"
|
||||||
embed.colour = colours.bad
|
embed.colour = colours.bad
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
|
||||||
async def add_react(msg: discord.Message, react: 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:
|
||||||
print(f"[!!] Missing permissions to add reaction in '{msg.guild.id}/{msg.channel.id}'!")
|
idpath = (f"{msg.guild.id}/" if msg.guild else "") + str(msg.channel.id)
|
||||||
|
print(f"[!!] Missing permissions to add reaction in '{idpath}'!")
|
||||||
|
|
||||||
|
|
||||||
# --- Checks ---
|
# --- Checks ---
|
||||||
|
4
dev-notes/image-processing.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Image processing instructions
|
||||||
|
|
||||||
|
For images like bandplans and maps, first resize the image to a reasonable size, then run `pngquant --quality 30-40` on the images.
|
||||||
|
Do not apply that to non-flat images like actual pictures.
|
52
dev-notes/rs_index_schema.json
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"title": "Index",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"last_updated": {
|
||||||
|
"title": "Last Updated",
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"title": "Resources",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/definitions/Resource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"last_updated",
|
||||||
|
"resources"
|
||||||
|
],
|
||||||
|
"definitions": {
|
||||||
|
"File": {
|
||||||
|
"title": "File",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"filename": {
|
||||||
|
"title": "Filename",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"hash": {
|
||||||
|
"title": "Hash",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"filename",
|
||||||
|
"hash"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Resource": {
|
||||||
|
"title": "Resource",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/File"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
dev-notes/test_pool_E5C14.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"_id": "56956f51f65e5c590272e372",
|
||||||
|
"appears": "2016-04-01T06:00:00.000Z",
|
||||||
|
"class": "Amateur Extra",
|
||||||
|
"subtext": "Expires Jul 1, 2020",
|
||||||
|
"valid_from": "2016-07-01T06:00:00.000Z",
|
||||||
|
"expires": "2020-07-01T06:00:00.000Z",
|
||||||
|
"official_name": "Element 4",
|
||||||
|
"id": "E4_2016",
|
||||||
|
"slug": "extra2016",
|
||||||
|
"passing": 37,
|
||||||
|
"year": 2016,
|
||||||
|
"pool": [{
|
||||||
|
"_id": "5cd63f15910d9b003d545bd7",
|
||||||
|
"qcount": 4,
|
||||||
|
"id": "E5",
|
||||||
|
"name": "ELECTRICAL PRINCIPLES",
|
||||||
|
"sections": [{
|
||||||
|
"_id": "5cd63f15910d9b003d545beb",
|
||||||
|
"id": "E5C",
|
||||||
|
"questions": [{
|
||||||
|
"_id": "5cd63f15910d9b003d545bef",
|
||||||
|
"keywords": ["4"],
|
||||||
|
"answer": "B",
|
||||||
|
"answers": {
|
||||||
|
"A": "Point 2",
|
||||||
|
"B": "Point 4",
|
||||||
|
"C": "Point 5",
|
||||||
|
"D": "Point 6"
|
||||||
|
},
|
||||||
|
"fccpart": "",
|
||||||
|
"id": "E5C14",
|
||||||
|
"image": "E5-2.png",
|
||||||
|
"text": "Which point on Figure E5-2 best represents the impedance of a series circuit consisting of a 400 ohm resistor and a 38 picofarad capacitor at 14 MHz?"
|
||||||
|
}],
|
||||||
|
"summary": "Coordinate systems and phasors in electronics: Rectangular Coordinates; Polar Coordinates; Phasors"
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
"updated": "2019-05-11T03:18:46.121Z",
|
||||||
|
"category": "default",
|
||||||
|
"testIdEnd": 19999,
|
||||||
|
"testIdStart": 10000,
|
||||||
|
"__v": 12,
|
||||||
|
"mat_icon": "flash_on",
|
||||||
|
"tagline": "Serious General operators only! This is the most advanced US license class!",
|
||||||
|
"keywords": ["ham radio extra test prep", "amateur extra class radio study", "amateur extra class ham exam", "ham radio amateur extra test 2016", "2016 amateur extra class", "ham radio license exam", "extra class flash card"],
|
||||||
|
"replaces": "E4_2012"
|
||||||
|
}
|
18
dev-notes/test_resource_manager.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from utils.resources_manager import ResourcesManager
|
||||||
|
|
||||||
|
|
||||||
|
path = Path("./data/resources")
|
||||||
|
url = "https://qrmresources.miaow.io/resources/"
|
||||||
|
versions = {
|
||||||
|
"bandcharts": "v1",
|
||||||
|
"img": "v1",
|
||||||
|
"maps": "v1",
|
||||||
|
"morse": "v1",
|
||||||
|
"phonetics": "v1",
|
||||||
|
"qcodes": "v1",
|
||||||
|
"funetics": "v1"
|
||||||
|
}
|
||||||
|
|
||||||
|
rm = ResourcesManager(path, url, versions)
|
||||||
|
print(rm.index)
|
3
dev-requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
-r requirements.txt
|
||||||
|
flake8
|
||||||
|
mypy
|
134
exts/ae7q.py
@ -1,136 +1,28 @@
|
|||||||
"""
|
"""
|
||||||
ae7q extension for qrm
|
ae7q extension for qrm
|
||||||
---
|
---
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
General Public License, version 2.
|
|
||||||
---
|
|
||||||
Test callsigns:
|
|
||||||
KN8U: active, restricted
|
|
||||||
AB2EE: expired, restricted
|
|
||||||
KE8FGB: assigned once, no restrictions
|
|
||||||
NA2AAA: unassigned, no records
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
import discord.ext.commands as commands
|
import discord.ext.commands as commands
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from common import embed_factory, colours
|
||||||
|
|
||||||
import common as cmn
|
|
||||||
|
|
||||||
|
|
||||||
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 = bot.qrm.session
|
"""Removed in v2.8.0"""
|
||||||
|
embed = embed_factory(ctx)
|
||||||
@commands.group(name="ae7q", aliases=["ae"], category=cmn.cat.lookup)
|
embed.colour = colours.bad
|
||||||
async def _ae7q_lookup(self, ctx: commands.Context):
|
embed.title = "Command removed"
|
||||||
'''Look 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", category=cmn.cat.lookup)
|
|
||||||
async def _ae7q_call(self, ctx: commands.Context, callsign: str):
|
|
||||||
'''Look up the history for a callsign on [ae7q.com](http://ae7q.com/).'''
|
|
||||||
callsign = callsign.upper()
|
|
||||||
desc = ''
|
|
||||||
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
|
|
||||||
async with self.session.get(base_url + callsign) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
embed.title = "Error in AE7Q call command"
|
|
||||||
embed.description = 'Could not load AE7Q'
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
return
|
|
||||||
page = await resp.text()
|
|
||||||
|
|
||||||
soup = BeautifulSoup(page, features="html.parser")
|
|
||||||
tables = soup.select("table.Database")
|
|
||||||
|
|
||||||
for table in tables:
|
|
||||||
rows = table.find_all("tr")
|
|
||||||
if len(rows) > 1 and len(rows[0]) > 1:
|
|
||||||
break
|
|
||||||
if desc == '':
|
|
||||||
for row in rows:
|
|
||||||
desc += " ".join(row.getText().split())
|
|
||||||
desc += '\n'
|
|
||||||
desc = desc.replace(callsign, f'`{callsign}`')
|
|
||||||
rows = None
|
|
||||||
|
|
||||||
if rows is None:
|
|
||||||
embed.title = f"AE7Q History for {callsign}"
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
embed.url = f"{base_url}{callsign}"
|
|
||||||
embed.description = desc
|
|
||||||
embed.description += f'\nNo records found for `{callsign}`'
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
return
|
|
||||||
|
|
||||||
table_contents = [] # store your table here
|
|
||||||
for tr in rows:
|
|
||||||
if rows.index(tr) == 0:
|
|
||||||
continue
|
|
||||||
row_cells = []
|
|
||||||
for td in tr.find_all('td'):
|
|
||||||
if td.getText().strip() != '':
|
|
||||||
row_cells.append(td.getText().strip())
|
|
||||||
else:
|
|
||||||
row_cells.append('-')
|
|
||||||
if 'colspan' in td.attrs and int(td.attrs['colspan']) > 1:
|
|
||||||
for i in range(int(td.attrs['colspan']) - 1):
|
|
||||||
row_cells.append(row_cells[-1])
|
|
||||||
for i, cell in enumerate(row_cells):
|
|
||||||
if cell == '"':
|
|
||||||
cell = table_contents[-1][i]
|
|
||||||
if len(row_cells) > 1:
|
|
||||||
table_contents += [row_cells]
|
|
||||||
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
embed.title = f"AE7Q Records for {callsign}"
|
|
||||||
embed.colour = cmn.colours.good
|
|
||||||
embed.url = f"{base_url}{callsign}"
|
|
||||||
|
|
||||||
for row in table_contents[0:3]:
|
|
||||||
header = f'**{row[0]}** ({row[1]})'
|
|
||||||
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)
|
|
||||||
|
|
||||||
embed.description = desc
|
|
||||||
if len(table_contents) > 3:
|
|
||||||
embed.description += f'\nRecords 1 to 3 of {len(table_contents)}. See ae7q.com for more...'
|
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
|
|
||||||
# TODO: write commands for other AE7Q response types?
|
|
||||||
# @_ae7q_lookup.command(name="trustee")
|
|
||||||
# async def _ae7q_trustee(self, ctx: commands.Context, callsign: str):
|
|
||||||
# pass
|
|
||||||
|
|
||||||
# @_ae7q_lookup.command(name="applications", aliases=['apps'])
|
|
||||||
# async def _ae7q_applications(self, ctx: commands.Context, callsign: str):
|
|
||||||
# pass
|
|
||||||
|
|
||||||
# @_ae7q_lookup.command(name="frn")
|
|
||||||
# async def _ae7q_frn(self, ctx: commands.Context, frn: str):
|
|
||||||
# base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN="
|
|
||||||
# pass
|
|
||||||
|
|
||||||
# @_ae7q_lookup.command(name="licensee", aliases=["lic"])
|
|
||||||
# async def _ae7q_licensee(self, ctx: commands.Context, frn: str):
|
|
||||||
# base_url = "http://ae7q.com/query/data/LicenseeIdHistory.php?ID="
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: commands.Bot):
|
def setup(bot: commands.Bot):
|
||||||
bot.add_cog(AE7QCog(bot))
|
bot.add_cog(AE7QCog())
|
||||||
|
288
exts/base.py
@ -1,88 +1,148 @@
|
|||||||
"""
|
"""
|
||||||
Base extension for qrm
|
Base extension for qrm
|
||||||
---
|
---
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
General Public License, version 2.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
|
||||||
from collections import OrderedDict
|
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
|
from typing import Union, Iterable
|
||||||
|
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 data.options as opt
|
|
||||||
import common as cmn
|
import common as cmn
|
||||||
|
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.context: commands.Context
|
||||||
|
|
||||||
def get_bot_mapping(self):
|
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):
|
||||||
bot = self.context.bot
|
bot = self.context.bot
|
||||||
mapping = {}
|
mapping = {}
|
||||||
for cmd in bot.commands:
|
|
||||||
cat = cmd.__original_kwargs__.get('category', None)
|
for cmd in await self.filter_commands(bot.commands):
|
||||||
|
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:
|
||||||
mapping[cat] = [cmd]
|
mapping[cat] = [cmd]
|
||||||
return mapping
|
return mapping
|
||||||
|
|
||||||
def get_command_signature(self, command):
|
async def get_command_signature(self, command):
|
||||||
parent = command.full_parent_name
|
parent = command.full_parent_name
|
||||||
if command.aliases != []:
|
if command.aliases != []:
|
||||||
aliases = ', '.join(command.aliases)
|
aliases = ", ".join(command.aliases)
|
||||||
fmt = command.name
|
fmt = command.name
|
||||||
if parent:
|
if parent:
|
||||||
fmt = f'{parent} {fmt}'
|
fmt = f"{parent} {fmt}"
|
||||||
alias = fmt
|
alias = fmt
|
||||||
return f'{opt.prefix}{alias} {command.signature}\n *Aliases:* {aliases}'
|
return f"{self.context.prefix}{alias} {command.signature}\n *Aliases:* {aliases}"
|
||||||
alias = command.name if not parent else f'{parent} {command.name}'
|
alias = command.name if not parent else f"{parent} {command.name}"
|
||||||
return f'{opt.prefix}{alias} {command.signature}'
|
return f"{self.context.prefix}{alias} {command.signature}"
|
||||||
|
|
||||||
async def send_error_message(self, error):
|
async def send_error_message(self, error):
|
||||||
embed = cmn.embed_factory(self.context)
|
embed = cmn.embed_factory(self.context)
|
||||||
embed.title = 'qrm Help Error'
|
embed.title = "qrm Help Error"
|
||||||
embed.description = error
|
embed.description = error
|
||||||
embed.colour = cmn.colours.bad
|
embed.colour = cmn.colours.bad
|
||||||
await self.context.send(embed=embed)
|
await self.context.send(embed=embed)
|
||||||
|
|
||||||
async def send_bot_help(self, mapping):
|
async def send_bot_help(self, mapping):
|
||||||
embed = cmn.embed_factory(self.context)
|
embed = cmn.embed_factory(self.context)
|
||||||
embed.title = 'qrm Help'
|
embed.title = "qrm Help"
|
||||||
embed.description = (f'For command-specific help and usage, use `{opt.prefix}help [command name]`'
|
embed.description = (f"For command-specific help and usage, use `{self.context.prefix}help [command name]`."
|
||||||
'. Many commands have shorter aliases.')
|
" Many commands have shorter aliases.")
|
||||||
|
if isinstance(self.context.bot.command_prefix, list):
|
||||||
|
embed.description += (" All of the following prefixes work with the bot: `"
|
||||||
|
+ "`, `".join(self.context.bot.command_prefix) + "`.")
|
||||||
|
mapping = await mapping
|
||||||
|
|
||||||
for cat, cmds in mapping.items():
|
for cat, cmds in mapping.items():
|
||||||
cmds = list(filter(lambda x: not x.hidden, cmds))
|
|
||||||
if cmds == []:
|
if cmds == []:
|
||||||
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)
|
||||||
|
|
||||||
async def send_command_help(self, command):
|
async def send_command_help(self, command):
|
||||||
|
if self.verify_checks:
|
||||||
|
if not await command.can_run(self.context):
|
||||||
|
raise commands.CheckFailure
|
||||||
|
for p in command.parents:
|
||||||
|
if not await p.can_run(self.context):
|
||||||
|
raise commands.CheckFailure
|
||||||
embed = cmn.embed_factory(self.context)
|
embed = cmn.embed_factory(self.context)
|
||||||
embed.title = self.get_command_signature(command)
|
embed.title = await self.get_command_signature(command)
|
||||||
embed.description = command.help
|
embed.description = command.help
|
||||||
await self.context.send(embed=embed)
|
await self.context.send(embed=embed)
|
||||||
|
|
||||||
async def send_group_help(self, group):
|
async def send_group_help(self, group):
|
||||||
|
if self.verify_checks and not await group.can_run(self.context):
|
||||||
|
raise commands.CheckFailure
|
||||||
embed = cmn.embed_factory(self.context)
|
embed = cmn.embed_factory(self.context)
|
||||||
embed.title = self.get_command_signature(group)
|
embed.title = await self.get_command_signature(group)
|
||||||
embed.description = group.help
|
embed.description = group.help
|
||||||
for cmd in group.commands:
|
for cmd in await self.filter_commands(group.commands, sort=True):
|
||||||
embed.add_field(name=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)
|
||||||
|
|
||||||
|
|
||||||
@ -90,111 +150,183 @@ class BaseCog(commands.Cog):
|
|||||||
def __init__(self, bot: commands.Bot):
|
def __init__(self, bot: commands.Bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.changelog = parse_changelog()
|
self.changelog = parse_changelog()
|
||||||
|
commit_file = pathlib.Path("git_commit")
|
||||||
|
dot_git = pathlib.Path(".git")
|
||||||
|
if commit_file.is_file():
|
||||||
|
with commit_file.open() as f:
|
||||||
|
self.commit = f.readline().strip()[:7]
|
||||||
|
elif dot_git.is_dir():
|
||||||
|
head_file = pathlib.Path(dot_git, "HEAD")
|
||||||
|
if head_file.is_file():
|
||||||
|
with head_file.open() as hf:
|
||||||
|
head = hf.readline().split(": ")[1].strip()
|
||||||
|
branch_file = pathlib.Path(dot_git, head)
|
||||||
|
if branch_file.is_file():
|
||||||
|
with branch_file.open() as bf:
|
||||||
|
self.commit = bf.readline().strip()[:7]
|
||||||
|
else:
|
||||||
|
self.commit = ""
|
||||||
|
self.donation_links = {
|
||||||
|
"Ko-Fi": "https://ko-fi.com/miaowware",
|
||||||
|
"LiberaPay": "https://liberapay.com/miaowware",
|
||||||
|
"GitHub Sponsors": "https://github.com/sponsors/classabbyamp",
|
||||||
|
}
|
||||||
|
self.bot_invite = ""
|
||||||
|
if self.bot.user:
|
||||||
|
self.bot_invite = (f"https://discordapp.com/oauth2/authorize?client_id={self.bot.user.id}"
|
||||||
|
f"&scope=bot&permissions={opt.invite_perms}")
|
||||||
|
|
||||||
@commands.command(name="info", aliases=["about"])
|
@commands.Cog.listener()
|
||||||
|
async def on_ready(self):
|
||||||
|
if not self.bot_invite and self.bot.user:
|
||||||
|
self.bot_invite = (f"https://discordapp.com/oauth2/authorize?client_id={self.bot.user.id}"
|
||||||
|
f"&scope=bot&permissions={opt.invite_perms}")
|
||||||
|
|
||||||
|
@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)
|
||||||
embed.title = "About qrm"
|
embed.title = "About qrm"
|
||||||
embed.description = info.description
|
embed.description = info.description
|
||||||
|
|
||||||
embed.add_field(name="Authors", value=", ".join(info.authors))
|
embed.add_field(name="Authors", value=", ".join(info.authors))
|
||||||
embed.add_field(name="License", value=info.license)
|
embed.add_field(name="License", value=info.license)
|
||||||
embed.add_field(name="Version", value=f'v{info.release}')
|
embed.add_field(name="Version", value=f"v{info.release} {'(`' + self.commit + '`)' if self.commit else ''}")
|
||||||
embed.add_field(name="Contributing", value=info.contributing, inline=False)
|
embed.add_field(name="Contributing", value=info.contributing, inline=False)
|
||||||
embed.add_field(name="Official Server", value=info.bot_server, inline=False)
|
embed.add_field(name="Official Server", value=info.bot_server, inline=False)
|
||||||
embed.set_thumbnail(url=str(self.bot.user.avatar_url))
|
embed.add_field(name="Donate", value="\n".join(f"{k}: {v}" for k, v in self.donation_links.items()),
|
||||||
|
inline=False)
|
||||||
|
if opt.enable_invite_cmd and (await self.bot.application_info()).bot_public:
|
||||||
|
embed.add_field(name="Invite qrm to Your Server", value=self.bot_invite, inline=False)
|
||||||
|
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):
|
||||||
"""Show 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)
|
||||||
content = ''
|
content = ""
|
||||||
if ctx.invoked_with == "beep":
|
if ctx.invoked_with == "beep":
|
||||||
embed.title = "**Boop!**"
|
embed.title = "**Boop!**"
|
||||||
else:
|
else:
|
||||||
content = ctx.message.author.mention if random.random() < 0.05 else ''
|
content = ctx.message.author.mention if random.random() < 0.05 else ""
|
||||||
embed.title = "🏓 **Pong!**"
|
embed.title = "🏓 **Pong!**"
|
||||||
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):
|
async def _changelog(self, ctx: commands.Context, version: str = "latest"):
|
||||||
"""Show what has changed in the most recent bot version."""
|
"""Shows what has changed in a bot version. Defaults to the latest version."""
|
||||||
embed = cmn.embed_factory(ctx)
|
embed = cmn.embed_factory(ctx)
|
||||||
embed.title = "qrm Changelog"
|
embed.title = "qrm Changelog"
|
||||||
embed.description = ("For a full listing, visit [Github](https://"
|
embed.description = ("For a full listing, visit [Github](https://"
|
||||||
"github.com/classabbyamp/discord-qrm2/blob/master/CHANGELOG.md).")
|
"github.com/miaowware/qrm2/blob/master/CHANGELOG.md).")
|
||||||
changelog = self.changelog
|
changelog = self.changelog
|
||||||
|
vers = list(changelog.keys())
|
||||||
|
vers.remove("Unreleased")
|
||||||
|
|
||||||
vers = 0
|
version = version.lower()
|
||||||
for ver, log in changelog.items():
|
|
||||||
if ver.lower() != 'unreleased':
|
if version == "latest":
|
||||||
if 'date' in log:
|
version = info.release
|
||||||
embed.description += f'\n\n**{ver}** ({log["date"]})'
|
if version == "unreleased":
|
||||||
|
version = "Unreleased"
|
||||||
|
|
||||||
|
try:
|
||||||
|
log = changelog[version]
|
||||||
|
except KeyError:
|
||||||
|
embed.title += ": Version Not Found"
|
||||||
|
embed.description += "\n\n**Valid versions:** latest, "
|
||||||
|
embed.description += ", ".join(vers)
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
return
|
||||||
|
|
||||||
|
if "date" in log:
|
||||||
|
embed.description += f"\n\n**v{version}** ({log['date']})"
|
||||||
else:
|
else:
|
||||||
embed.description += f'\n\n**{ver}**'
|
embed.description += f"\n\n**v{version}**"
|
||||||
embed = await format_changelog(log, embed)
|
embed = await format_changelog(log, embed)
|
||||||
vers += 1
|
|
||||||
if vers >= 1:
|
|
||||||
break
|
|
||||||
|
|
||||||
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 an issue for 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)
|
||||||
embed.title = "Found a bug? Have a feature request?"
|
embed.title = "Found a bug? Have a feature request?"
|
||||||
embed.description = ("Submit an issue on the [issue tracker]"
|
embed.description = """Submit an issue on the [issue tracker](https://github.com/miaowware/qrm2/issues)!
|
||||||
"(https://github.com/classabbyamp/discord-qrm2/issues)!")
|
|
||||||
|
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/issues)."""
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@commands.command(name="bruce", hidden=True)
|
@commands.command(name="donate", aliases=["tip"], category=cmn.BoltCats.INFO)
|
||||||
async def _b_issue(self, ctx: commands.Context):
|
async def _donate(self, ctx: commands.Context):
|
||||||
"""Shows how to create an issue for the bot."""
|
"""Shows ways to help support development of the bot via donations."""
|
||||||
await ctx.invoke(self._issue)
|
embed = cmn.embed_factory(ctx)
|
||||||
|
embed.title = "Help Support qrm's Development!"
|
||||||
|
embed.description = ("Donations are always appreciated, and help with server and infrastructure costs."
|
||||||
|
"\nThank you for your support!")
|
||||||
|
for title, url in self.donation_links.items():
|
||||||
|
embed.add_field(name=title, value=url, inline=False)
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@commands.command(name="echo", aliases=["e"], hidden=True)
|
@commands.command(name="invite", enabled=opt.enable_invite_cmd, category=cmn.BoltCats.INFO)
|
||||||
|
async def _invite(self, ctx: commands.Context):
|
||||||
|
"""Generates a link to invite the bot to a server."""
|
||||||
|
if not (await self.bot.application_info()).bot_public:
|
||||||
|
raise commands.DisabledCommand
|
||||||
|
embed = cmn.embed_factory(ctx)
|
||||||
|
embed.title = "Invite qrm to Your Server!"
|
||||||
|
embed.description = self.bot_invite
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@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, channel: commands.TextChannelConverter, *, msg: str):
|
async def _echo(self, ctx: commands.Context,
|
||||||
"""Send a message in a channel as qrm. Only works within a server or DM to server, not between servers."""
|
channel: Union[cmn.GlobalChannelConverter, commands.UserConverter], *, msg: str):
|
||||||
|
"""Sends a message in a channel as qrm. Accepts channel/user IDs/mentions.
|
||||||
|
Channel names are current-guild only.
|
||||||
|
Does not work with the ID of the bot user."""
|
||||||
|
if isinstance(channel, discord.ClientUser):
|
||||||
|
raise commands.BadArgument("Can't send to the bot user!")
|
||||||
await channel.send(msg)
|
await channel.send(msg)
|
||||||
|
|
||||||
|
|
||||||
def parse_changelog():
|
def parse_changelog():
|
||||||
changelog = OrderedDict()
|
changelog = {}
|
||||||
ver = ''
|
ver = ""
|
||||||
heading = ''
|
heading = ""
|
||||||
|
|
||||||
with open('CHANGELOG.md') as changelog_file:
|
with open("CHANGELOG.md") as changelog_file:
|
||||||
for line in changelog_file.readlines():
|
for line in changelog_file.readlines():
|
||||||
if line.strip() == '':
|
if line.strip() == "":
|
||||||
continue
|
continue
|
||||||
if re.match(r'##[^#]', line):
|
if re.match(r"##[^#]", line):
|
||||||
ver_match = re.match(r'\[(.+)\](?: - )?(\d{4}-\d{2}-\d{2})?', line.lstrip('#').strip())
|
ver_match = re.match(r"\[(.+)\](?: - )?(\d{4}-\d{2}-\d{2})?", line.lstrip("#").strip())
|
||||||
if ver_match is not None:
|
if ver_match is not None:
|
||||||
ver = ver_match.group(1)
|
ver = ver_match.group(1)
|
||||||
changelog[ver] = dict()
|
changelog[ver] = dict()
|
||||||
if ver_match.group(2):
|
if ver_match.group(2):
|
||||||
changelog[ver]['date'] = ver_match.group(2)
|
changelog[ver]["date"] = ver_match.group(2)
|
||||||
elif re.match(r'###[^#]', line):
|
elif re.match(r"###[^#]", line):
|
||||||
heading = line.lstrip('#').strip()
|
heading = line.lstrip("#").strip()
|
||||||
changelog[ver][heading] = []
|
changelog[ver][heading] = []
|
||||||
elif ver != '' and heading != '':
|
elif ver != "" and heading != "":
|
||||||
if line.startswith('-'):
|
if line.startswith("-"):
|
||||||
changelog[ver][heading].append(line.lstrip('-').strip())
|
changelog[ver][heading].append(line.lstrip("-").strip())
|
||||||
return changelog
|
return changelog
|
||||||
|
|
||||||
|
|
||||||
async def format_changelog(log: dict, embed: discord.Embed):
|
async def format_changelog(log: dict, embed: discord.Embed):
|
||||||
for header, lines in log.items():
|
for header, lines in log.items():
|
||||||
formatted = ''
|
formatted = ""
|
||||||
if header != 'date':
|
if header != "date":
|
||||||
for line in lines:
|
for line in lines:
|
||||||
formatted += f'- {line}\n'
|
formatted += f"- {line}\n"
|
||||||
embed.add_field(name=f'**{header}**', value=formatted, inline=False)
|
embed.add_field(name=f"**{header}**", value=formatted, inline=False)
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
|
||||||
|
128
exts/callsign.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
"""
|
||||||
|
Callsign Lookup extension for qrm
|
||||||
|
---
|
||||||
|
Copyright (C) 2019-2020 classabbyamp, 0x5c (as qrz.py)
|
||||||
|
Copyright (C) 2021-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from callsignlookuptools import QrzAsyncClient, CallsignLookupError, CallsignData
|
||||||
|
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
import common as cmn
|
||||||
|
|
||||||
|
import data.options as opt
|
||||||
|
import data.keys as keys
|
||||||
|
|
||||||
|
|
||||||
|
class QRZCog(commands.Cog):
|
||||||
|
def __init__(self, bot: commands.Bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.qrz = None
|
||||||
|
try:
|
||||||
|
if keys.qrz_user and keys.qrz_pass:
|
||||||
|
# seed the qrz object with the previous session key, in case it already works
|
||||||
|
session_key = ""
|
||||||
|
try:
|
||||||
|
with open("data/qrz_session") as qrz_file:
|
||||||
|
session_key = qrz_file.readline().strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
self.qrz = QrzAsyncClient(username=keys.qrz_user, password=keys.qrz_pass, useragent="discord-qrm2",
|
||||||
|
session_key=session_key,
|
||||||
|
session=aiohttp.ClientSession(connector=bot.qrm.connector))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@commands.command(name="call", aliases=["qrz"], category=cmn.Cats.LOOKUP)
|
||||||
|
async def _qrz_lookup(self, ctx: commands.Context, callsign: str, *flags):
|
||||||
|
"""Looks up a callsign on [QRZ.com](https://www.qrz.com/). Add `--link` to only link the QRZ page."""
|
||||||
|
flags = [f.lower() for f in flags]
|
||||||
|
|
||||||
|
if self.qrz is None or "--link" in flags:
|
||||||
|
if ctx.invoked_with == "qrz":
|
||||||
|
await ctx.send("⚠️ **Deprecated Command Alias**\n"
|
||||||
|
f"This command has been renamed to `{ctx.prefix}call`!\n"
|
||||||
|
"This alias will be removed in the next version.")
|
||||||
|
await ctx.send(f"http://qrz.com/db/{callsign}")
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = cmn.embed_factory(ctx)
|
||||||
|
embed.title = f"QRZ Data for {callsign.upper()}"
|
||||||
|
|
||||||
|
if ctx.invoked_with == "qrz":
|
||||||
|
embed.description = ("⚠️ **Deprecated Command Alias**\n"
|
||||||
|
f"This command has been renamed to `{ctx.prefix}call`!\n"
|
||||||
|
"This alias will be removed in the next version.")
|
||||||
|
|
||||||
|
async with ctx.typing():
|
||||||
|
try:
|
||||||
|
data = await self.qrz.search(callsign)
|
||||||
|
except CallsignLookupError as e:
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
embed.description = str(e)
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
return
|
||||||
|
|
||||||
|
embed.title = f"QRZ Data for {data.callsign}"
|
||||||
|
embed.colour = cmn.colours.good
|
||||||
|
embed.url = data.url
|
||||||
|
if data.image is not None:
|
||||||
|
embed.set_thumbnail(url=data.image.url)
|
||||||
|
|
||||||
|
for title, val in qrz_process_info(data).items():
|
||||||
|
if val is not None and (val := str(val)):
|
||||||
|
embed.add_field(name=title, value=val, inline=True)
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
|
def qrz_process_info(data: CallsignData) -> Dict:
|
||||||
|
if data.name is not None:
|
||||||
|
if opt.qrz_only_nickname:
|
||||||
|
nm = data.name.name if data.name.name is not None else ""
|
||||||
|
if data.name.nickname is not None:
|
||||||
|
name = data.name.nickname + " " + nm
|
||||||
|
elif data.name.first:
|
||||||
|
name = data.name.first + " " + nm
|
||||||
|
else:
|
||||||
|
name = nm
|
||||||
|
else:
|
||||||
|
name = data.name
|
||||||
|
else:
|
||||||
|
name = None
|
||||||
|
|
||||||
|
qsl = dict()
|
||||||
|
if data.qsl is not None:
|
||||||
|
qsl = {
|
||||||
|
"eQSL?": data.qsl.eqsl,
|
||||||
|
"Paper QSL?": data.qsl.mail,
|
||||||
|
"LotW?": data.qsl.lotw,
|
||||||
|
"QSL Info": data.qsl.info,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"Name": name,
|
||||||
|
"Country": data.address.country if data.address is not None else None,
|
||||||
|
"Address": data.address,
|
||||||
|
"Grid Square": data.grid,
|
||||||
|
"County": data.county,
|
||||||
|
"CQ Zone": data.cq_zone,
|
||||||
|
"ITU Zone": data.itu_zone,
|
||||||
|
"IOTA Designator": data.iota,
|
||||||
|
"Expires": f"{data.expire_date:%Y-%m-%d}" if data.expire_date is not None else None,
|
||||||
|
"Aliases": ", ".join(data.aliases) if data.aliases else None,
|
||||||
|
"Previous Callsign": data.prev_call,
|
||||||
|
"License Class": data.lic_class,
|
||||||
|
"Trustee": data.trustee,
|
||||||
|
"Born": data.born,
|
||||||
|
} | qsl
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
bot.add_cog(QRZCog(bot))
|
80
exts/codes.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
"""
|
||||||
|
Codes extension for qrm
|
||||||
|
---
|
||||||
|
Copyright (C) 2019-2021 classabbyamp, 0x5c (as ham.py)
|
||||||
|
Copyright (C) 2021-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import discord.ext.commands as commands
|
||||||
|
|
||||||
|
import common as cmn
|
||||||
|
|
||||||
|
|
||||||
|
class HamCog(commands.Cog):
|
||||||
|
def __init__(self, bot: commands.Bot):
|
||||||
|
self.bot = bot
|
||||||
|
with open(cmn.paths.resources / "phonetics.1.json") as file:
|
||||||
|
d = json.load(file)
|
||||||
|
self.phonetics: dict[str, str] = d["phonetics"]
|
||||||
|
self.pweights: dict[str, int] = d["pweights"]
|
||||||
|
with open(cmn.paths.resources / "qcodes.1.json") as file:
|
||||||
|
self.qcodes: dict = json.load(file)
|
||||||
|
|
||||||
|
@commands.command(name="qcode", aliases=["q"], category=cmn.Cats.CODES)
|
||||||
|
async def _qcode_lookup(self, ctx: commands.Context, qcode: str):
|
||||||
|
"""Looks up the meaning of a Q Code."""
|
||||||
|
qcode = qcode.upper()
|
||||||
|
embed = cmn.embed_factory(ctx)
|
||||||
|
if qcode in self.qcodes:
|
||||||
|
embed.title = qcode
|
||||||
|
embed.description = self.qcodes[qcode]
|
||||||
|
embed.colour = cmn.colours.good
|
||||||
|
else:
|
||||||
|
embed.title = f"Q Code {qcode} not found"
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@commands.command(name="phonetics", aliases=["ph", "phoneticize", "phoneticise", "phone"], category=cmn.Cats.CODES)
|
||||||
|
async def _phonetics_lookup(self, ctx: commands.Context, *, msg: str):
|
||||||
|
"""Returns NATO phonetics for a word or phrase."""
|
||||||
|
result = ""
|
||||||
|
for char in msg.lower():
|
||||||
|
if char.isalpha():
|
||||||
|
result += self.phonetics[char]
|
||||||
|
else:
|
||||||
|
result += char
|
||||||
|
result += " "
|
||||||
|
embed = cmn.embed_factory(ctx)
|
||||||
|
embed.title = f"Phonetics for {msg}"
|
||||||
|
embed.description = result.title()
|
||||||
|
embed.colour = cmn.colours.good
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@commands.command(name="phoneticweight", aliases=["pw"], category=cmn.Cats.CODES)
|
||||||
|
async def _weight(self, ctx: commands.Context, *, msg: str):
|
||||||
|
"""Calculates the phonetic weight of a callsign or message."""
|
||||||
|
embed = cmn.embed_factory(ctx)
|
||||||
|
msg = msg.upper()
|
||||||
|
weight = 0
|
||||||
|
for char in msg:
|
||||||
|
try:
|
||||||
|
weight += self.pweights[char]
|
||||||
|
except KeyError:
|
||||||
|
embed.title = "Error in calculation of phonetic weight"
|
||||||
|
embed.description = f"Unknown character `{char}` in message"
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
return
|
||||||
|
embed.title = f"Phonetic Weight of {msg}"
|
||||||
|
embed.description = f"The phonetic weight is **{weight}**"
|
||||||
|
embed.colour = cmn.colours.good
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot: commands.Bot):
|
||||||
|
bot.add_cog(HamCog(bot))
|
27
exts/contests.py
Normal 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))
|
199
exts/dbconv.py
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
"""
|
||||||
|
Conversion extension for qrm
|
||||||
|
---
|
||||||
|
Copyright (C) 2020-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import math
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import discord.ext.commands as commands
|
||||||
|
|
||||||
|
import common as cmn
|
||||||
|
from data import options as opt
|
||||||
|
|
||||||
|
|
||||||
|
# not sure why but UnitConverter and Unit need to be defined before DbConvCog and convert()
|
||||||
|
class UnitConverter(commands.Converter):
|
||||||
|
async def convert(self, ctx: commands.Context, argument: str):
|
||||||
|
is_db = None
|
||||||
|
mult = None
|
||||||
|
unit = None
|
||||||
|
utype = None
|
||||||
|
try:
|
||||||
|
s = argument.lower()
|
||||||
|
if len(s) > 2 and s[:2] == "db":
|
||||||
|
is_db = True
|
||||||
|
if s[2:] in units:
|
||||||
|
u = units[s[2:]]
|
||||||
|
mult = u["mult"]
|
||||||
|
unit = u["log"]
|
||||||
|
utype = u["type"]
|
||||||
|
elif s in units:
|
||||||
|
is_db = False
|
||||||
|
u = units[s]
|
||||||
|
mult = u["mult"]
|
||||||
|
unit = u["scalar"]
|
||||||
|
utype = u["type"]
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid unit: {argument}")
|
||||||
|
return Unit(argument, unit, utype, is_db, mult)
|
||||||
|
except ValueError as e:
|
||||||
|
raise commands.BadArgument(message=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
class UnitType(Enum):
|
||||||
|
voltage = 1
|
||||||
|
power = 2
|
||||||
|
antenna = 3
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Unit:
|
||||||
|
raw: str
|
||||||
|
unit: str
|
||||||
|
type: UnitType
|
||||||
|
is_db: bool
|
||||||
|
mult: int
|
||||||
|
|
||||||
|
|
||||||
|
class DbConvCog(commands.Cog):
|
||||||
|
def __init__(self, bot: commands.Bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@commands.command(name="dbconv", aliases=["dbc"], category=cmn.Cats.CALC)
|
||||||
|
async def _db_conv(self, ctx: commands.Context,
|
||||||
|
value: Optional[float] = None,
|
||||||
|
unit_from: Optional[UnitConverter] = None,
|
||||||
|
unit_to: Optional[UnitConverter] = None):
|
||||||
|
"""
|
||||||
|
Convert between decibels and scalar values for voltage, power, and antenna gain.
|
||||||
|
|
||||||
|
**Valid Units**
|
||||||
|
*Voltage:* V, mV, µV, uV, dBV, dBmV, dBµV, dBuV
|
||||||
|
*Power:* fW, mW, W, kW, dBf, dBm, dBW, dBk
|
||||||
|
*Antenna Gain:* dBi, dBd, dBq
|
||||||
|
"""
|
||||||
|
embed = cmn.embed_factory(ctx)
|
||||||
|
if value is not None and unit_from is not None and unit_to is not None:
|
||||||
|
converted = convert(value, unit_from, unit_to)
|
||||||
|
|
||||||
|
embed.title = f"{value:.3g} {unit_from.unit} = {converted:.3g} {unit_to.unit}"
|
||||||
|
embed.colour = cmn.colours.good
|
||||||
|
else:
|
||||||
|
embed.title = "Decibel Quick Reference"
|
||||||
|
embed.description = (
|
||||||
|
"Decibels are a great way to easily represent large quantities that are common in electronics. "
|
||||||
|
"There are a few main types that are used often in radio: voltage, power, and antenna gain. "
|
||||||
|
"Here are some commonly-used reference levels for each type:"
|
||||||
|
)
|
||||||
|
v_db_info = ("**dBV** = relative to 1 V\n"
|
||||||
|
"**dBmV** = relative to 1 mV (1e-3 V)\n"
|
||||||
|
"**dBµV** = relative to 1 µV (1e-6 V)")
|
||||||
|
embed.add_field(name="Voltage Decibels", value=v_db_info, inline=False)
|
||||||
|
p_db_info = ("**dBW** = relative to 1 W\n"
|
||||||
|
"**dBk** = relative to 1 kW (1e3 W)\n"
|
||||||
|
"**dBm** = relative to 1 mW (1e-3 W)\n"
|
||||||
|
"**dBf** = relative to 1 fW (1e-15 W)")
|
||||||
|
embed.add_field(name="Power Decibels", value=p_db_info, inline=False)
|
||||||
|
a_db_info = ("**dBi** = relative to a theoretical __i__sotropic radiator in free space "
|
||||||
|
"(equal radiation in all directions)\n"
|
||||||
|
"**dBd** = relative to a dipole in free space (0 dBd = 2.15 dBi)\n"
|
||||||
|
"**dBq** = relative to a quarter-wave antenna in free space (0 dBq = -0.85 dBi)")
|
||||||
|
embed.add_field(name="Antenna Gain Decibels", value=a_db_info, inline=False)
|
||||||
|
embed.add_field(name="Use the bot to do the conversions",
|
||||||
|
value=f"`{opt.display_prefix}dbconv [value] [unit_from] [unit_to]`",
|
||||||
|
inline=False)
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot: commands.Bot):
|
||||||
|
bot.add_cog(DbConvCog(bot))
|
||||||
|
|
||||||
|
|
||||||
|
def convert(initial: float, unit1: Unit, unit2: Unit):
|
||||||
|
if unit1.type == unit2.type:
|
||||||
|
# dB to dB
|
||||||
|
if unit1.is_db and unit2.is_db:
|
||||||
|
if unit1.mult == unit2.mult:
|
||||||
|
return initial
|
||||||
|
elif unit1.type == UnitType.voltage:
|
||||||
|
return _calc_volt_db(_calc_volt(initial, unit1.mult), unit2.mult)
|
||||||
|
elif unit1.type == UnitType.power:
|
||||||
|
return _calc_power_db(_calc_power(initial, unit1.mult), unit2.mult)
|
||||||
|
elif unit1.type == UnitType.antenna:
|
||||||
|
return initial + (unit1.mult - unit2.mult)
|
||||||
|
# V/W to V/W
|
||||||
|
elif not unit1.is_db and not unit2.is_db:
|
||||||
|
if unit1.mult == unit2.mult:
|
||||||
|
return initial
|
||||||
|
return initial * unit1.mult / unit2.mult
|
||||||
|
# dB to V/W
|
||||||
|
elif unit1.is_db and not unit2.is_db:
|
||||||
|
if unit1.type == UnitType.voltage:
|
||||||
|
return _calc_volt(initial, unit1.mult) / unit2.mult
|
||||||
|
elif unit1.type == UnitType.power:
|
||||||
|
return _calc_power(initial, unit1.mult) / unit2.mult
|
||||||
|
# V/W to dB
|
||||||
|
elif not unit1.is_db and unit2.is_db:
|
||||||
|
if unit1.type == UnitType.voltage:
|
||||||
|
return _calc_volt_db(initial * unit1.mult, unit2.mult)
|
||||||
|
elif unit1.type == UnitType.power:
|
||||||
|
return _calc_power_db(initial * unit1.mult, unit2.mult)
|
||||||
|
raise ValueError(f"Can't convert between {unit1} and {unit2}")
|
||||||
|
|
||||||
|
|
||||||
|
units = {
|
||||||
|
# voltage
|
||||||
|
"uv": {"mult": 1e-6, "scalar": "µV", "log": "dBµV", "type": UnitType.voltage},
|
||||||
|
"µv": {"mult": 1e-6, "scalar": "µV", "log": "dBµV", "type": UnitType.voltage},
|
||||||
|
"mv": {"mult": 1e-3, "scalar": "mV", "log": "dBmV", "type": UnitType.voltage},
|
||||||
|
"v": {"mult": 1, "scalar": "V", "log": "dBV", "type": UnitType.voltage},
|
||||||
|
# power
|
||||||
|
"fw": {"mult": 1e-15, "scalar": "fW", "log": "dBf", "type": UnitType.power},
|
||||||
|
"f": {"mult": 1e-15, "scalar": "fW", "log": "dBf", "type": UnitType.power},
|
||||||
|
"mw": {"mult": 1e-3, "scalar": "mW", "log": "dBm", "type": UnitType.power},
|
||||||
|
"m": {"mult": 1e-3, "scalar": "mW", "log": "dBm", "type": UnitType.power},
|
||||||
|
"w": {"mult": 1, "scalar": "W", "log": "dBW", "type": UnitType.power},
|
||||||
|
"kw": {"mult": 1e3, "scalar": "kW", "log": "dBk", "type": UnitType.power},
|
||||||
|
"k": {"mult": 1e3, "scalar": "kW", "log": "dBk", "type": UnitType.power},
|
||||||
|
# antenna
|
||||||
|
"q": {"mult": -0.85, "scalar": None, "log": "dBq", "type": UnitType.antenna},
|
||||||
|
"i": {"mult": 0, "scalar": None, "log": "dBi", "type": UnitType.antenna},
|
||||||
|
"d": {"mult": 2.15, "scalar": None, "log": "dBd", "type": UnitType.antenna},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _calc_power_db(p: float, ref: float):
|
||||||
|
return 10 * math.log10(p / ref)
|
||||||
|
|
||||||
|
|
||||||
|
def _calc_power(db: float, ref: float):
|
||||||
|
return 10 ** (db / 10) * ref
|
||||||
|
|
||||||
|
|
||||||
|
def _calc_volt_db(v: float, ref: float):
|
||||||
|
return 20 * math.log10(v / ref)
|
||||||
|
|
||||||
|
|
||||||
|
def _calc_volt(db: float, ref: float):
|
||||||
|
return 10 ** (db / 20) * ref
|
||||||
|
|
||||||
|
|
||||||
|
# testing code
|
||||||
|
if __name__ == "__main__":
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
ip = input("> ").split()
|
||||||
|
initial = float(ip[0])
|
||||||
|
unit1 = Unit(ip[1])
|
||||||
|
unit2 = Unit(ip[2])
|
||||||
|
conv = convert(initial, unit1, unit2)
|
||||||
|
print(f"{initial:.2f} {unit1} = {conv:.2f} {unit2}")
|
||||||
|
except ValueError as e:
|
||||||
|
print(e)
|
75
exts/dxcc.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
"""
|
||||||
|
DXCC Prefix Lookup extension for qrm
|
||||||
|
---
|
||||||
|
Copyright (C) 2019-2020 classabbyamp, 0x5c (as lookup.py)
|
||||||
|
Copyright (C) 2021-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ctyparser import BigCty
|
||||||
|
|
||||||
|
from discord.ext import commands, tasks
|
||||||
|
|
||||||
|
import common as cmn
|
||||||
|
|
||||||
|
|
||||||
|
cty_path = Path("./data/cty.json")
|
||||||
|
|
||||||
|
|
||||||
|
class DXCCCog(commands.Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
try:
|
||||||
|
self.cty = BigCty(cty_path)
|
||||||
|
except OSError:
|
||||||
|
self.cty = BigCty()
|
||||||
|
|
||||||
|
@commands.command(name="dxcc", aliases=["dx"], category=cmn.Cats.LOOKUP)
|
||||||
|
async def _dxcc_lookup(self, ctx: commands.Context, query: str):
|
||||||
|
"""Gets DXCC info about a callsign prefix."""
|
||||||
|
query = query.upper()
|
||||||
|
full_query = query
|
||||||
|
embed = cmn.embed_factory(ctx)
|
||||||
|
embed.title = "DXCC Info for "
|
||||||
|
embed.description = f"*Last Updated: {self.cty.formatted_version}*"
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
while query:
|
||||||
|
if query in self.cty.keys():
|
||||||
|
data = self.cty[query]
|
||||||
|
embed.add_field(name="Entity", value=data["entity"])
|
||||||
|
embed.add_field(name="CQ Zone", value=data["cq"])
|
||||||
|
embed.add_field(name="ITU Zone", value=data["itu"])
|
||||||
|
embed.add_field(name="Continent", value=data["continent"])
|
||||||
|
embed.add_field(name="Time Zone",
|
||||||
|
value=f"+{data['tz']}" if data["tz"] > 0 else str(data["tz"]))
|
||||||
|
embed.title += query
|
||||||
|
embed.colour = cmn.colours.good
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
query = query[:-1]
|
||||||
|
else:
|
||||||
|
embed.title += full_query + " not found"
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@tasks.loop(hours=24)
|
||||||
|
async def _update_cty(self):
|
||||||
|
update = threading.Thread(target=run_update, args=(self.cty, cty_path))
|
||||||
|
update.start()
|
||||||
|
|
||||||
|
|
||||||
|
def run_update(cty_obj, dump_loc):
|
||||||
|
update = cty_obj.update()
|
||||||
|
if update:
|
||||||
|
cty_obj.dump(dump_loc)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot: commands.Bot):
|
||||||
|
dxcccog = DXCCCog(bot)
|
||||||
|
bot.add_cog(dxcccog)
|
||||||
|
dxcccog._update_cty.start()
|
57
exts/fun.py
@ -1,53 +1,70 @@
|
|||||||
"""
|
"""
|
||||||
Fun extension for qrm
|
Fun extension for qrm
|
||||||
---
|
---
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
General Public License, version 2.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
import random
|
import random
|
||||||
|
|
||||||
import discord.ext.commands as commands
|
import discord.ext.commands as commands
|
||||||
|
|
||||||
import common as cmn
|
import common as cmn
|
||||||
|
|
||||||
|
import data.options as opt
|
||||||
|
|
||||||
|
|
||||||
class FunCog(commands.Cog):
|
class FunCog(commands.Cog):
|
||||||
def __init__(self, bot: commands.Bot):
|
def __init__(self, bot: commands.Bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
with open('resources/words') as words_file:
|
with open(cmn.paths.resources / "imgs.1.json") as file:
|
||||||
|
self.imgs: dict = json.load(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):
|
||||||
'''Look up an xkcd 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 an xkcd about tar.'''
|
"""Returns xkcd: tar."""
|
||||||
await ctx.send('http://xkcd.com/1168')
|
await ctx.send("http://xkcd.com/1168")
|
||||||
|
|
||||||
@commands.command(name="xd", hidden=True, category=cmn.cat.fun)
|
@commands.command(name="standards", category=cmn.Cats.FUN)
|
||||||
|
async def _standards(self, ctx: commands.Context):
|
||||||
|
"""Returns xkcd: Standards."""
|
||||||
|
await ctx.send("http://xkcd.com/927")
|
||||||
|
|
||||||
|
@commands.command(name="worksplit", aliases=["split", "ft8"], category=cmn.Cats.FUN)
|
||||||
|
async def _worksplit(self, ctx: commands.Context):
|
||||||
|
"""Posts "Work split you lids"."""
|
||||||
|
embed = cmn.embed_factory(ctx)
|
||||||
|
embed.title = "Work Split, You Lids!"
|
||||||
|
embed.set_image(url=opt.resources_url + self.imgs["worksplit"])
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@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):
|
||||||
'''Get fun phonetics for a word or phrase.'''
|
"""Generates fun/wacky phonetics for a word or phrase."""
|
||||||
with ctx.typing():
|
result = ""
|
||||||
result = ''
|
|
||||||
for char in msg.lower():
|
for char in msg.lower():
|
||||||
if char.isalpha():
|
if char.isalpha():
|
||||||
result += random.choice([word for word in self.words if word[0] == char])
|
result += random.choice([word for word in self.words if word[0] == char])
|
||||||
else:
|
else:
|
||||||
result += char
|
result += char
|
||||||
result += ' '
|
result += " "
|
||||||
embed = cmn.embed_factory(ctx)
|
embed = cmn.embed_factory(ctx)
|
||||||
embed.title = f'Funetics for {msg}'
|
embed.title = f"Funetics for {msg}"
|
||||||
embed.description = result.title()
|
embed.description = result.title()
|
||||||
embed.colour = cmn.colours.good
|
embed.colour = cmn.colours.good
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
151
exts/grid.py
@ -1,13 +1,13 @@
|
|||||||
"""
|
"""
|
||||||
Grid extension for qrm
|
Grid extension for qrm
|
||||||
---
|
---
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
General Public License, version 2.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import math
|
|
||||||
|
import gridtools
|
||||||
|
|
||||||
import discord.ext.commands as commands
|
import discord.ext.commands as commands
|
||||||
|
|
||||||
@ -18,127 +18,50 @@ 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: str, lon: str):
|
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."""
|
||||||
with negative being latitude South and longitude West.'''
|
"""\n\nCoordinates should be in decimal format, with negative being latitude South and longitude West."""
|
||||||
with ctx.typing():
|
"""\n\nTo calculate the latitude and longitude from a grid locator, use `latlong`""")
|
||||||
grid = "**"
|
latlong = gridtools.LatLong(lat, lon)
|
||||||
try:
|
grid = gridtools.Grid(latlong)
|
||||||
latf = float(lat) + 90
|
|
||||||
lonf = float(lon) + 180
|
|
||||||
if 0 <= latf <= 180 and 0 <= lonf <= 360:
|
|
||||||
grid += chr(ord('A') + int(lonf / 20))
|
|
||||||
grid += chr(ord('A') + int(latf / 10))
|
|
||||||
grid += chr(ord('0') + int((lonf % 20)/2))
|
|
||||||
grid += chr(ord('0') + int((latf % 10)/1))
|
|
||||||
grid += chr(ord('a') + int((lonf - (int(lonf/2)*2)) / (5/60)))
|
|
||||||
grid += chr(ord('a') + int((latf - (int(latf/1)*1)) / (2.5/60)))
|
|
||||||
grid += "**"
|
|
||||||
embed = cmn.embed_factory(ctx)
|
embed = cmn.embed_factory(ctx)
|
||||||
embed.title = f'Maidenhead Grid Locator for {float(lat):.6f}, {float(lon):.6f}'
|
embed.title = f"Maidenhead Grid Locator for {latlong.lat:.5f}, {latlong.long:.5f}"
|
||||||
embed.description = grid
|
embed.description = f"**{grid}**"
|
||||||
embed.colour = cmn.colours.good
|
embed.colour = cmn.colours.good
|
||||||
else:
|
|
||||||
raise ValueError('Out of range.')
|
|
||||||
except ValueError as err:
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
embed.title = f'Error generating grid square for {lat}, {lon}.'
|
|
||||||
embed.description = str(err)
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@commands.command(name="ungrid", aliases=['loc'], 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, grid2: str = None):
|
async def _location_lookup(self, ctx: commands.Context, grid: str):
|
||||||
'''Calculates the latitude and longitude for the center of a grid square.
|
("""Calculates the latitude and longitude for the center of a grid locator."""
|
||||||
If two grid squares are given, the distance and azimuth between them is calculated.'''
|
"""\n\nTo calculate the grid locator from a latitude and longitude, use `grid`"""
|
||||||
with ctx.typing():
|
"""\n\n*Warning: `ungrid` will be removed soon. Use one of the other names for this command.*""")
|
||||||
if grid2 is None or grid2 == '':
|
grid_obj = gridtools.Grid(grid)
|
||||||
try:
|
|
||||||
grid = grid.upper()
|
|
||||||
loc = get_coords(grid)
|
|
||||||
|
|
||||||
embed = cmn.embed_factory(ctx)
|
embed = cmn.embed_factory(ctx)
|
||||||
embed.title = f'Latitude and Longitude for {grid}'
|
embed.title = f"Latitude and Longitude for {grid_obj}"
|
||||||
embed.colour = cmn.colours.good
|
embed.colour = cmn.colours.good
|
||||||
|
embed.description = f"**{grid_obj.lat:.5f}, {grid_obj.long:.5f}**"
|
||||||
if len(grid) >= 6:
|
if ctx.invoked_with == "ungrid":
|
||||||
embed.description = f'**{loc[0]:.5f}, {loc[1]:.5f}**'
|
embed.add_field(name="Warning", value=(f"*`{ctx.prefix}ungrid` will be removed soon, use `{ctx.prefix}help "
|
||||||
embed.url = f'https://www.openstreetmap.org/#map=13/{loc[0]:.5f}/{loc[1]:.5f}'
|
"latlong` to see other names for this command.*"))
|
||||||
else:
|
|
||||||
embed.description = f'**{loc[0]:.1f}, {loc[1]:.1f}**'
|
|
||||||
embed.url = f'https://www.openstreetmap.org/#map=10/{loc[0]:.1f}/{loc[1]:.1f}'
|
|
||||||
except Exception as e:
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
embed.title = f'Error generating latitude and longitude for grid {grid}.'
|
|
||||||
embed.description = str(e)
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
else:
|
|
||||||
radius = 6371
|
|
||||||
try:
|
|
||||||
grid = grid.upper()
|
|
||||||
grid2 = grid2.upper()
|
|
||||||
loc = get_coords(grid)
|
|
||||||
loc2 = get_coords(grid2)
|
|
||||||
# Haversine formula
|
|
||||||
d_lat = math.radians(loc2[0] - loc[0])
|
|
||||||
d_lon = math.radians(loc2[1] - loc[1])
|
|
||||||
a = (math.sin(d_lat/2) ** 2
|
|
||||||
+ math.cos(math.radians(loc[0]))
|
|
||||||
* math.cos(math.radians(loc2[0]))
|
|
||||||
* math.sin(d_lon/2) ** 2)
|
|
||||||
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
|
|
||||||
d = radius * c
|
|
||||||
d_mi = 0.6213712 * d
|
|
||||||
|
|
||||||
# Bearing
|
|
||||||
y_dist = math.sin(math.radians(loc2[1]-loc[1])) * math.cos(math.radians(loc2[0]))
|
|
||||||
x_dist = (math.cos(math.radians(loc[0]))
|
|
||||||
* math.sin(math.radians(loc2[0]))
|
|
||||||
- math.sin(math.radians(loc[0]))
|
|
||||||
* math.cos(math.radians(loc2[0]))
|
|
||||||
* math.cos(math.radians(loc2[1] - loc[1])))
|
|
||||||
bearing = (math.degrees(math.atan2(y_dist, x_dist)) + 360) % 360
|
|
||||||
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
embed.title = f'Great Circle Distance and Bearing from {grid} to {grid2}'
|
|
||||||
embed.description = f'**Distance:** {d:.1f} km ({d_mi:.1f} mi)\n**Bearing:** {bearing:.1f}°'
|
|
||||||
embed.colour = cmn.colours.good
|
|
||||||
except Exception as e:
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
embed.title = f'Error generating great circle distance and bearing from {grid} and {grid2}.'
|
|
||||||
embed.description = str(e)
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@commands.command(name="griddistance", aliases=["griddist", "distance", "dist"], category=cmn.Cats.CALC)
|
||||||
|
async def _dist_lookup(self, ctx: commands.Context, grid1: str, grid2: str):
|
||||||
|
"""Calculates the great circle distance and azimuthal bearing between two grid locators."""
|
||||||
|
g1 = gridtools.Grid(grid1)
|
||||||
|
g2 = gridtools.Grid(grid2)
|
||||||
|
|
||||||
def get_coords(grid: str):
|
dist, bearing = gridtools.grid_distance(g1, g2)
|
||||||
if len(grid) < 3:
|
dist_mi = 0.6214 * dist
|
||||||
raise ValueError('The grid locator must be at least 4 characters long.')
|
|
||||||
|
|
||||||
if not grid[0:2].isalpha() or not grid[2:4].isdigit():
|
embed = cmn.embed_factory(ctx)
|
||||||
if len(grid) <= 4:
|
embed.title = f"Great Circle Distance and Bearing from {g1} to {g2}"
|
||||||
raise ValueError('The grid locator must be of the form AA##.')
|
embed.description = f"**Distance:** {dist:.1f} km ({dist_mi:.1f} mi)\n**Bearing:** {bearing:.1f}°"
|
||||||
if len(grid) >= 6 and not grid[5:7].isalpha():
|
embed.colour = cmn.colours.good
|
||||||
raise ValueError('The grid locator must be of the form AA##AA.')
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
lon = ((ord(grid[0]) - ord('A')) * 20) - 180
|
|
||||||
lat = ((ord(grid[1]) - ord('A')) * 10) - 90
|
|
||||||
lon += ((ord(grid[2]) - ord('0')) * 2)
|
|
||||||
lat += ((ord(grid[3]) - ord('0')) * 1)
|
|
||||||
|
|
||||||
if len(grid) >= 6:
|
|
||||||
# have subsquares
|
|
||||||
lon += ((ord(grid[4])) - ord('A')) * (5/60)
|
|
||||||
lat += ((ord(grid[5])) - ord('A')) * (2.5/60)
|
|
||||||
# move to center of subsquare
|
|
||||||
lon += (2.5/60)
|
|
||||||
lat += (1.25/60)
|
|
||||||
return (lat, lon)
|
|
||||||
# move to center of square
|
|
||||||
lon += 1
|
|
||||||
lat += 0.5
|
|
||||||
return (lat, lon)
|
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: commands.Bot):
|
def setup(bot: commands.Bot):
|
||||||
|
99
exts/ham.py
@ -1,99 +0,0 @@
|
|||||||
"""
|
|
||||||
Ham extension for qrm
|
|
||||||
---
|
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
|
||||||
|
|
||||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
|
||||||
General Public License, version 2.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import discord.ext.commands as commands
|
|
||||||
|
|
||||||
import common as cmn
|
|
||||||
from resources import callsign_info
|
|
||||||
from resources import phonetics
|
|
||||||
from resources import qcodes
|
|
||||||
|
|
||||||
|
|
||||||
class HamCog(commands.Cog):
|
|
||||||
def __init__(self, bot: commands.Bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
@commands.command(name="qcode", aliases=['q'], category=cmn.cat.ref)
|
|
||||||
async def _qcode_lookup(self, ctx: commands.Context, qcode: str):
|
|
||||||
'''Look up a Q Code.'''
|
|
||||||
with ctx.typing():
|
|
||||||
qcode = qcode.upper()
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
if qcode in qcodes.qcodes:
|
|
||||||
embed.title = qcode
|
|
||||||
embed.description = qcodes.qcodes[qcode]
|
|
||||||
embed.colour = cmn.colours.good
|
|
||||||
else:
|
|
||||||
embed.title = f'Q Code {qcode} not found'
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
|
|
||||||
@commands.command(name="phonetics", aliases=['ph', 'phoneticize', 'phoneticise', 'phone'], category=cmn.cat.ref)
|
|
||||||
async def _phonetics_lookup(self, ctx: commands.Context, *, msg: str):
|
|
||||||
'''Get phonetics for a word or phrase.'''
|
|
||||||
with ctx.typing():
|
|
||||||
result = ''
|
|
||||||
for char in msg.lower():
|
|
||||||
if char.isalpha():
|
|
||||||
result += phonetics.phonetics[char]
|
|
||||||
else:
|
|
||||||
result += char
|
|
||||||
result += ' '
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
embed.title = f'Phonetics for {msg}'
|
|
||||||
embed.description = result.title()
|
|
||||||
embed.colour = cmn.colours.good
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
|
|
||||||
@commands.command(name="utc", aliases=['z'], category=cmn.cat.ref)
|
|
||||||
async def _utc_lookup(self, ctx: commands.Context):
|
|
||||||
'''Gets the current time in UTC.'''
|
|
||||||
with ctx.typing():
|
|
||||||
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"])
|
|
||||||
async def _vanity_prefixes(self, ctx: commands.Context, country: str = None):
|
|
||||||
'''Lists valid prefixes for countries.'''
|
|
||||||
if country is None:
|
|
||||||
await ctx.send_help(ctx.command)
|
|
||||||
return
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
if country.lower() not in callsign_info.options:
|
|
||||||
embed.title = f'{country} not found!',
|
|
||||||
embed.description = f'Valid countries: {", ".join(callsign_info.options.keys())}',
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
else:
|
|
||||||
embed.title = callsign_info.options[country.lower()][0]
|
|
||||||
embed.description = callsign_info.options[country.lower()][1]
|
|
||||||
embed.colour = cmn.colours.good
|
|
||||||
|
|
||||||
for name, val in callsign_info.options[country.lower()][2].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)
|
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: commands.Bot):
|
|
||||||
bot.add_cog(HamCog(bot))
|
|
102
exts/image.py
@ -1,100 +1,60 @@
|
|||||||
"""
|
"""
|
||||||
Image extension for qrm
|
Image extension for qrm
|
||||||
---
|
---
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
General Public License, version 2.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
|
||||||
|
|
||||||
import discord
|
import aiohttp
|
||||||
|
|
||||||
import discord.ext.commands as commands
|
import discord.ext.commands as commands
|
||||||
|
|
||||||
import common as cmn
|
import common as cmn
|
||||||
|
|
||||||
|
import data.options as opt
|
||||||
|
|
||||||
|
|
||||||
class ImageCog(commands.Cog):
|
class ImageCog(commands.Cog):
|
||||||
def __init__(self, bot: commands.Bot):
|
def __init__(self, bot: commands.Bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.bandcharts = cmn.ImagesGroup(cmn.paths.bandcharts / "meta.json")
|
self.bandcharts = cmn.ImagesGroup(cmn.paths.resources / "bandcharts.1.json")
|
||||||
self.maps = cmn.ImagesGroup(cmn.paths.maps / "meta.json")
|
self.maps = cmn.ImagesGroup(cmn.paths.resources / "maps.1.json")
|
||||||
self.session = bot.qrm.session
|
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 = ""):
|
||||||
'''Posts an image of Frequency Allocations.'''
|
"""Gets the frequency allocations chart for a given country."""
|
||||||
arg = region.lower()
|
await ctx.send(embed=create_embed(ctx, "Bandchart", self.bandcharts, chart_id))
|
||||||
|
|
||||||
with ctx.typing():
|
@commands.command(name="map", category=cmn.Cats.REF)
|
||||||
|
async def _map(self, ctx: commands.Context, map_id: str = ""):
|
||||||
|
"""Posts a ham-relevant map."""
|
||||||
|
await ctx.send(embed=create_embed(ctx, "Map", self.maps, map_id))
|
||||||
|
|
||||||
|
|
||||||
|
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.bandcharts:
|
if img_id not in db:
|
||||||
desc = 'Possible arguments are:\n'
|
desc = "Possible arguments are:\n"
|
||||||
for key, img in self.bandcharts.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 = f'Bandplan 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
|
||||||
else:
|
metadata = db[img_id]
|
||||||
metadata: cmn.ImageMetadata = self.bandcharts[arg]
|
|
||||||
img = discord.File(cmn.paths.bandcharts / metadata.filename,
|
|
||||||
filename=metadata.filename)
|
|
||||||
if metadata.description:
|
if metadata.description:
|
||||||
embed.description = metadata.description
|
embed.description = metadata.description
|
||||||
if metadata.source:
|
if metadata.source:
|
||||||
embed.add_field(name="Source", value=metadata.source)
|
embed.add_field(name="Source", value=metadata.source)
|
||||||
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='attachment://' + metadata.filename)
|
embed.set_image(url=opt.resources_url + metadata.filename)
|
||||||
await ctx.send(embed=embed, file=img)
|
return embed
|
||||||
|
|
||||||
@commands.command(name="map", category=cmn.cat.maps)
|
|
||||||
async def _map(self, ctx: commands.Context, map_id: str = ''):
|
|
||||||
'''Posts an image of a ham-relevant map.'''
|
|
||||||
arg = map_id.lower()
|
|
||||||
|
|
||||||
with ctx.typing():
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
if arg not in self.maps:
|
|
||||||
desc = 'Possible arguments are:\n'
|
|
||||||
for key, img in self.maps.items():
|
|
||||||
desc += f'`{key}`: {img.name}{(" " + img.emoji if img.emoji else "")}\n'
|
|
||||||
embed.title = 'Map Not Found!'
|
|
||||||
embed.description = desc
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
else:
|
|
||||||
metadata: cmn.ImageMetadata = self.maps[arg]
|
|
||||||
img = discord.File(cmn.paths.maps / metadata.filename,
|
|
||||||
filename=metadata.filename)
|
|
||||||
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='attachment://' + metadata.filename)
|
|
||||||
await ctx.send(embed=embed, file=img)
|
|
||||||
|
|
||||||
@commands.command(name="grayline", aliases=['greyline', 'grey', 'gray', 'gl'], category=cmn.cat.maps)
|
|
||||||
async def _grayline(self, ctx: commands.Context):
|
|
||||||
'''Posts a map of the current greyline, where HF propagation is the best.'''
|
|
||||||
gl_url = ('http://www.fourmilab.ch/cgi-bin/uncgi/Earth?img=NOAAtopo.evif'
|
|
||||||
'&imgsize=320&dynimg=y&opt=-p&lat=&lon=&alt=&tle=&date=0&utc=&jd=')
|
|
||||||
with ctx.typing():
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
embed.title = 'Current Greyline Conditions'
|
|
||||||
embed.colour = cmn.colours.good
|
|
||||||
async with self.session.get(gl_url) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
embed.description = 'Could not download file...'
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
else:
|
|
||||||
data = io.BytesIO(await resp.read())
|
|
||||||
embed.set_image(url=f'attachment://greyline.jpg')
|
|
||||||
await ctx.send(embed=embed, file=discord.File(data, 'greyline.jpg'))
|
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: commands.Bot):
|
def setup(bot: commands.Bot):
|
||||||
|
161
exts/land_weather.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
"""
|
||||||
|
Land Weather extension for qrm
|
||||||
|
---
|
||||||
|
Copyright (C) 2019-2020 classabbyamp, 0x5c (as weather.py)
|
||||||
|
Copyright (C) 2021-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
import discord.ext.commands as commands
|
||||||
|
|
||||||
|
import common as cmn
|
||||||
|
|
||||||
|
|
||||||
|
class WeatherCog(commands.Cog):
|
||||||
|
wttr_units_regex = re.compile(r"\B-([cCfF])\b")
|
||||||
|
|
||||||
|
def __init__(self, bot: commands.Bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
|
||||||
|
|
||||||
|
@commands.group(name="weather", aliases=["wttr"], case_insensitive=True, category=cmn.Cats.WEATHER)
|
||||||
|
async def _weather_conditions(self, ctx: commands.Context):
|
||||||
|
"""Gets local weather conditions from [wttr.in](http://wttr.in/).
|
||||||
|
|
||||||
|
*Supported location types:*
|
||||||
|
city name: `paris`
|
||||||
|
any location: `~Eiffel Tower`
|
||||||
|
Unicode name of any location in any language: `Москва`
|
||||||
|
airport code (3 letters): `muc`
|
||||||
|
domain name `@stackoverflow.com`
|
||||||
|
area codes: `12345`
|
||||||
|
GPS coordinates: `-78.46,106.79`
|
||||||
|
|
||||||
|
Add a `-c` or `-f` to use Celcius or Fahrenheit: `-c YSC`"""
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
await ctx.send_help(ctx.command)
|
||||||
|
|
||||||
|
@_weather_conditions.command(name="forecast", aliases=["fc", "future"], category=cmn.Cats.WEATHER)
|
||||||
|
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/).
|
||||||
|
See help of the `weather` command for possible location types and options."""
|
||||||
|
try:
|
||||||
|
units_arg = re.search(self.wttr_units_regex, location).group(1)
|
||||||
|
except AttributeError:
|
||||||
|
units_arg = ""
|
||||||
|
if units_arg.lower() == "f":
|
||||||
|
units = "u"
|
||||||
|
elif units_arg.lower() == "c":
|
||||||
|
units = "m"
|
||||||
|
else:
|
||||||
|
units = ""
|
||||||
|
|
||||||
|
loc = self.wttr_units_regex.sub("", location).strip()
|
||||||
|
|
||||||
|
embed = cmn.embed_factory(ctx)
|
||||||
|
embed.title = f"Weather Forecast for {loc}"
|
||||||
|
embed.description = "Data from [wttr.in](http://wttr.in/)."
|
||||||
|
embed.colour = cmn.colours.good
|
||||||
|
|
||||||
|
loc = loc.replace(" ", "+")
|
||||||
|
embed.set_image(url=f"http://wttr.in/{loc}_{units}pnFQ.png")
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@_weather_conditions.command(name="now", aliases=["n"], category=cmn.Cats.WEATHER)
|
||||||
|
async def _weather_conditions_now(self, ctx: commands.Context, *, location: str):
|
||||||
|
"""Gets current local weather conditions from [wttr.in](http://wttr.in/).
|
||||||
|
See help of the `weather` command for possible location types and options."""
|
||||||
|
try:
|
||||||
|
units_arg = re.search(self.wttr_units_regex, location).group(1)
|
||||||
|
except AttributeError:
|
||||||
|
units_arg = ""
|
||||||
|
if units_arg.lower() == "f":
|
||||||
|
units = "u"
|
||||||
|
elif units_arg.lower() == "c":
|
||||||
|
units = "m"
|
||||||
|
else:
|
||||||
|
units = ""
|
||||||
|
|
||||||
|
loc = self.wttr_units_regex.sub("", location).strip()
|
||||||
|
|
||||||
|
embed = cmn.embed_factory(ctx)
|
||||||
|
embed.title = f"Current Weather for {loc}"
|
||||||
|
embed.description = "Data from [wttr.in](http://wttr.in/)."
|
||||||
|
embed.colour = cmn.colours.good
|
||||||
|
|
||||||
|
loc = loc.replace(" ", "+")
|
||||||
|
embed.set_image(url=f"http://wttr.in/{loc}_0{units}pnFQ.png")
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@commands.command(name="metar", category=cmn.Cats.WEATHER)
|
||||||
|
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. \
|
||||||
|
Optionally, a number of hours can be given to show a number of hours of historical METAR data.
|
||||||
|
|
||||||
|
Airports should be given as an \
|
||||||
|
[ICAO code](https://en.wikipedia.org/wiki/List_of_airports_by_IATA_and_ICAO_code)."""
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""Gets forecasted raw TAF (Terminal Aerodrome Forecast) data for an airport. Includes the latest METAR data.
|
||||||
|
|
||||||
|
Airports should be given as an \
|
||||||
|
[ICAO code](https://en.wikipedia.org/wiki/List_of_airports_by_IATA_and_ICAO_code)."""
|
||||||
|
|
||||||
|
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/taf?ids={airport}&format=raw&metar=true"
|
||||||
|
async with self.session.get(url) as r:
|
||||||
|
if r.status != 200:
|
||||||
|
raise cmn.BotHTTPError(r)
|
||||||
|
taf = await r.text()
|
||||||
|
|
||||||
|
embed.title = f"Current TAF for {airport}"
|
||||||
|
embed.description = "Data from [aviationweather.gov](https://www.aviationweather.gov/)."
|
||||||
|
embed.colour = cmn.colours.good
|
||||||
|
embed.description += f"\n\n```\n{taf}\n```"
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot: commands.Bot):
|
||||||
|
bot.add_cog(WeatherCog(bot))
|
@ -1,82 +0,0 @@
|
|||||||
"""
|
|
||||||
Lookup extension for qrm
|
|
||||||
---
|
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
|
||||||
|
|
||||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
|
||||||
General Public License, version 2.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from discord.ext import commands, tasks
|
|
||||||
from ctyparser import BigCty
|
|
||||||
|
|
||||||
import common as cmn
|
|
||||||
|
|
||||||
|
|
||||||
class LookupCog(commands.Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
try:
|
|
||||||
self.cty = BigCty('./data/cty.json')
|
|
||||||
except OSError:
|
|
||||||
self.cty = BigCty()
|
|
||||||
|
|
||||||
# TODO: See #107
|
|
||||||
# @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):
|
|
||||||
'''Gets info about a DXCC prefix.'''
|
|
||||||
with ctx.typing():
|
|
||||||
query = query.upper()
|
|
||||||
full_query = query
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
embed.title = f'DXCC Info for '
|
|
||||||
embed.description = f'*Last Updated: {self.cty.formatted_version}*'
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
while query:
|
|
||||||
if query in self.cty.keys():
|
|
||||||
data = self.cty[query]
|
|
||||||
embed.add_field(name="Entity", value=data['entity'])
|
|
||||||
embed.add_field(name="CQ Zone", value=data['cq'])
|
|
||||||
embed.add_field(name="ITU Zone", value=data['itu'])
|
|
||||||
embed.add_field(name="Continent", value=data['continent'])
|
|
||||||
embed.add_field(name="Time Zone",
|
|
||||||
value=f'+{data["tz"]}' if data['tz'] > 0 else str(data['tz']))
|
|
||||||
embed.title += query
|
|
||||||
embed.colour = cmn.colours.good
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
query = query[:-1]
|
|
||||||
else:
|
|
||||||
embed.title += full_query + ' not found'
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
|
|
||||||
@tasks.loop(hours=24)
|
|
||||||
async def _update_cty(self):
|
|
||||||
update = threading.Thread(target=run_update, args=(self.cty, "./data/cty.json"))
|
|
||||||
update.start()
|
|
||||||
|
|
||||||
|
|
||||||
def run_update(cty_obj, dump_loc):
|
|
||||||
update = cty_obj.update()
|
|
||||||
if update:
|
|
||||||
cty_obj.dump(dump_loc)
|
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: commands.Bot):
|
|
||||||
lookupcog = LookupCog(bot)
|
|
||||||
bot.add_cog(lookupcog)
|
|
||||||
lookupcog._update_cty.start()
|
|
@ -1,79 +1,81 @@
|
|||||||
"""
|
"""
|
||||||
Morse Code extension for qrm
|
Morse Code extension for qrm
|
||||||
---
|
---
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
General Public License, version 2.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import discord.ext.commands as commands
|
import discord.ext.commands as commands
|
||||||
|
|
||||||
import common as cmn
|
import common as cmn
|
||||||
from resources import morse
|
|
||||||
|
|
||||||
|
|
||||||
class MorseCog(commands.Cog):
|
class MorseCog(commands.Cog):
|
||||||
def __init__(self, bot: commands.Bot):
|
def __init__(self, bot: commands.Bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
with open(cmn.paths.resources / "morse.1.json") as file:
|
||||||
|
d = json.load(file)
|
||||||
|
self.morse: dict[str, str] = d["morse"]
|
||||||
|
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."""
|
||||||
with ctx.typing():
|
result = ""
|
||||||
result = ''
|
|
||||||
for char in msg.upper():
|
for char in msg.upper():
|
||||||
try:
|
try:
|
||||||
result += morse.morse[char]
|
result += self.morse[char]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
result += '<?>'
|
result += "<?>"
|
||||||
result += ' '
|
result += " "
|
||||||
embed = cmn.embed_factory(ctx)
|
embed = cmn.embed_factory(ctx)
|
||||||
embed.title = f'Morse Code for {msg}'
|
embed.title = f"Morse Code for {msg}"
|
||||||
embed.description = '**' + result + '**'
|
embed.description = "**" + result + "**"
|
||||||
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."""
|
||||||
with ctx.typing():
|
result = ""
|
||||||
result = ''
|
|
||||||
msg0 = msg
|
msg0 = msg
|
||||||
msg = msg.split('/')
|
msg = msg.split("/")
|
||||||
msg = [m.split() for m in msg]
|
msg = [m.split() for m in msg]
|
||||||
for word in msg:
|
for word in msg:
|
||||||
for char in word:
|
for char in word:
|
||||||
try:
|
try:
|
||||||
result += morse.ascii[char]
|
result += self.ascii[char]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
result += '<?>'
|
result += "<?>"
|
||||||
result += ' '
|
result += " "
|
||||||
embed = cmn.embed_factory(ctx)
|
embed = cmn.embed_factory(ctx)
|
||||||
embed.title = f'ASCII for {msg0}'
|
embed.title = f"ASCII for {msg0}"
|
||||||
embed.description = result
|
embed.description = result
|
||||||
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)
|
||||||
with ctx.typing():
|
|
||||||
msg = msg.upper()
|
msg = msg.upper()
|
||||||
weight = 0
|
weight = 0
|
||||||
for char in msg:
|
for char in msg:
|
||||||
try:
|
try:
|
||||||
cw_char = morse.morse[char].replace('-', '==')
|
cw_char = self.morse[char].replace("-", "==")
|
||||||
weight += len(cw_char) * 2 + 2
|
weight += len(cw_char) * 2 + 2
|
||||||
except KeyError:
|
except KeyError:
|
||||||
embed.title = 'Error in calculation of CW weight'
|
embed.title = "Error in calculation of CW weight"
|
||||||
embed.description = f'Unknown character {char} in callsign'
|
embed.description = f"Unknown character `{char}` in message"
|
||||||
embed.colour = cmn.colours.bad
|
embed.colour = cmn.colours.bad
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
return
|
return
|
||||||
embed.title = f'CW Weight of {msg}'
|
embed.title = f"CW Weight of {msg}"
|
||||||
embed.description = f'The CW weight is **{weight}**'
|
embed.description = f"The CW weight is **{weight}**"
|
||||||
embed.colour = cmn.colours.good
|
embed.colour = cmn.colours.good
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
47
exts/prefixes.py
Normal 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))
|
103
exts/propagation.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
"""
|
||||||
|
Propagation extension for qrm
|
||||||
|
---
|
||||||
|
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
import cairosvg
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
import discord
|
||||||
|
import discord.ext.commands as commands
|
||||||
|
|
||||||
|
import common as cmn
|
||||||
|
|
||||||
|
|
||||||
|
class PropagationCog(commands.Cog):
|
||||||
|
muf_url = "https://prop.kc2g.com/renders/current/mufd-normal-now.svg"
|
||||||
|
fof2_url = "https://prop.kc2g.com/renders/current/fof2-normal-now.svg"
|
||||||
|
gl_baseurl = "https://www.fourmilab.ch/cgi-bin/uncgi/Earth?img=ETOPO1_day-m.evif&dynimg=y&opt=-p"
|
||||||
|
n0nbh_sun_url = "https://www.hamqsl.com/solarsun.php"
|
||||||
|
noaa_drap_url = "https://services.swpc.noaa.gov/images/animations/d-rap/global/d-rap/latest.png"
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.httpx_client: httpx.AsyncClient = bot.qrm.httpx_client
|
||||||
|
|
||||||
|
@commands.command(name="mufmap", aliases=["muf"], category=cmn.Cats.WEATHER)
|
||||||
|
async def mufmap(self, ctx: commands.Context):
|
||||||
|
"""Shows a world map of the Maximum Usable Frequency (MUF)."""
|
||||||
|
async with ctx.typing():
|
||||||
|
resp = await self.httpx_client.get(self.muf_url)
|
||||||
|
await resp.aclose()
|
||||||
|
if resp.status_code != 200:
|
||||||
|
raise cmn.BotHTTPError(resp)
|
||||||
|
out = BytesIO(cairosvg.svg2png(bytestring=await resp.aread()))
|
||||||
|
file = discord.File(out, "muf_map.png")
|
||||||
|
embed = cmn.embed_factory(ctx)
|
||||||
|
embed.title = "Maximum Usable Frequency Map"
|
||||||
|
embed.description = "Image from [prop.kc2g.com](https://prop.kc2g.com/)\nData sources listed on the page."
|
||||||
|
embed.set_image(url="attachment://muf_map.png")
|
||||||
|
await ctx.send(file=file, embed=embed)
|
||||||
|
|
||||||
|
@commands.command(name="fof2map", aliases=["fof2", "critfreq"], category=cmn.Cats.WEATHER)
|
||||||
|
async def fof2map(self, ctx: commands.Context):
|
||||||
|
"""Shows a world map of the Critical Frequency (foF2)."""
|
||||||
|
async with ctx.typing():
|
||||||
|
resp = await self.httpx_client.get(self.fof2_url)
|
||||||
|
await resp.aclose()
|
||||||
|
if resp.status_code != 200:
|
||||||
|
raise cmn.BotHTTPError(resp)
|
||||||
|
out = BytesIO(cairosvg.svg2png(bytestring=await resp.aread()))
|
||||||
|
file = discord.File(out, "fof2_map.png")
|
||||||
|
embed = cmn.embed_factory(ctx)
|
||||||
|
embed.title = "Critical Frequency (foF2) Map"
|
||||||
|
embed.description = "Image from [prop.kc2g.com](https://prop.kc2g.com/)\nData sources listed on the page."
|
||||||
|
embed.set_image(url="attachment://fof2_map.png")
|
||||||
|
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):
|
||||||
|
bot.add_cog(PropagationCog(bot))
|
179
exts/qrz.py
@ -1,179 +0,0 @@
|
|||||||
"""
|
|
||||||
QRZ extension for qrm
|
|
||||||
---
|
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
|
||||||
|
|
||||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
|
||||||
General Public License, version 2.
|
|
||||||
"""
|
|
||||||
from collections import OrderedDict
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
from discord.ext import commands, tasks
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
import common as cmn
|
|
||||||
import data.keys as keys
|
|
||||||
|
|
||||||
|
|
||||||
class QRZCog(commands.Cog):
|
|
||||||
def __init__(self, bot: commands.Bot):
|
|
||||||
self.bot = bot
|
|
||||||
self.session = bot.qrm.session
|
|
||||||
self._qrz_session_init.start()
|
|
||||||
|
|
||||||
@commands.command(name="call", aliases=["qrz"], category=cmn.cat.lookup)
|
|
||||||
async def _qrz_lookup(self, ctx: commands.Context, callsign: str):
|
|
||||||
'''Look up a callsign on [QRZ.com](https://www.qrz.com/).'''
|
|
||||||
if keys.qrz_user == '' or keys.qrz_pass == '':
|
|
||||||
await ctx.send(f'http://qrz.com/db/{callsign}')
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
await qrz_test_session(self.key, self.session)
|
|
||||||
except ConnectionError:
|
|
||||||
await self.get_session()
|
|
||||||
|
|
||||||
url = f'http://xmldata.qrz.com/xml/current/?s={self.key};callsign={callsign}'
|
|
||||||
async with self.session.get(url) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
raise ConnectionError(f'Unable to connect to QRZ (HTTP Error {resp.status})')
|
|
||||||
with BytesIO(await resp.read()) as resp_file:
|
|
||||||
resp_xml = etree.parse(resp_file).getroot()
|
|
||||||
|
|
||||||
resp_xml_session = resp_xml.xpath('/x:QRZDatabase/x:Session', namespaces={'x': 'http://xmldata.qrz.com'})
|
|
||||||
resp_session = {el.tag.split('}')[1]: el.text for el in resp_xml_session[0].getiterator()}
|
|
||||||
if 'Error' in resp_session:
|
|
||||||
if 'Session Timeout' in resp_session['Error']:
|
|
||||||
await self.get_session()
|
|
||||||
await self._qrz_lookup(ctx, callsign)
|
|
||||||
return
|
|
||||||
if 'Not found' in resp_session['Error']:
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
embed.title = f"QRZ Data for {callsign.upper()}"
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
embed.description = 'No data found!'
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
return
|
|
||||||
raise ValueError(resp_session['Error'])
|
|
||||||
|
|
||||||
resp_xml_data = resp_xml.xpath('/x:QRZDatabase/x:Callsign', namespaces={'x': 'http://xmldata.qrz.com'})
|
|
||||||
resp_data = {el.tag.split('}')[1]: el.text for el in resp_xml_data[0].getiterator()}
|
|
||||||
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
embed.title = f"QRZ Data for {resp_data['call']}"
|
|
||||||
embed.colour = cmn.colours.good
|
|
||||||
embed.url = f'http://www.qrz.com/db/{resp_data["call"]}'
|
|
||||||
if 'image' in resp_data:
|
|
||||||
embed.set_thumbnail(url=resp_data['image'])
|
|
||||||
|
|
||||||
data = qrz_process_info(resp_data)
|
|
||||||
|
|
||||||
for title, val in data.items():
|
|
||||||
if val is not None:
|
|
||||||
embed.add_field(name=title, value=val, inline=True)
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
|
|
||||||
async def get_session(self):
|
|
||||||
"""Session creation and caching."""
|
|
||||||
self.key = await qrz_login(keys.qrz_user, keys.qrz_pass, self.session)
|
|
||||||
with open('data/qrz_session', 'w') as qrz_file:
|
|
||||||
qrz_file.write(self.key)
|
|
||||||
|
|
||||||
@tasks.loop(count=1)
|
|
||||||
async def _qrz_session_init(self):
|
|
||||||
"""Helper task to allow obtaining a session at cog instantiation."""
|
|
||||||
try:
|
|
||||||
with open('data/qrz_session') as qrz_file:
|
|
||||||
self.key = qrz_file.readline().strip()
|
|
||||||
await qrz_test_session(self.key, self.session)
|
|
||||||
except (FileNotFoundError, ConnectionError):
|
|
||||||
await self.get_session()
|
|
||||||
|
|
||||||
|
|
||||||
async def qrz_login(user: str, passwd: str, session: aiohttp.ClientSession):
|
|
||||||
url = f'http://xmldata.qrz.com/xml/current/?username={user};password={passwd};agent=discord-qrm2'
|
|
||||||
async with session.get(url) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
raise ConnectionError(f'Unable to connect to QRZ (HTTP Error {resp.status})')
|
|
||||||
with BytesIO(await resp.read()) as resp_file:
|
|
||||||
resp_xml = etree.parse(resp_file).getroot()
|
|
||||||
|
|
||||||
resp_xml_session = resp_xml.xpath('/x:QRZDatabase/x:Session', namespaces={'x': 'http://xmldata.qrz.com'})
|
|
||||||
resp_session = {el.tag.split('}')[1]: el.text for el in resp_xml_session[0].getiterator()}
|
|
||||||
if 'Error' in resp_session:
|
|
||||||
raise ConnectionError(resp_session['Error'])
|
|
||||||
if resp_session['SubExp'] == 'non-subscriber':
|
|
||||||
raise ConnectionError('Invalid QRZ Subscription')
|
|
||||||
return resp_session['Key']
|
|
||||||
|
|
||||||
|
|
||||||
async def qrz_test_session(key: str, session: aiohttp.ClientSession):
|
|
||||||
url = f'http://xmldata.qrz.com/xml/current/?s={key}'
|
|
||||||
async with session.get(url) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
raise ConnectionError(f'Unable to connect to QRZ (HTTP Error {resp.status})')
|
|
||||||
with BytesIO(await resp.read()) as resp_file:
|
|
||||||
resp_xml = etree.parse(resp_file).getroot()
|
|
||||||
|
|
||||||
resp_xml_session = resp_xml.xpath('/x:QRZDatabase/x:Session', namespaces={'x': 'http://xmldata.qrz.com'})
|
|
||||||
resp_session = {el.tag.split('}')[1]: el.text for el in resp_xml_session[0].getiterator()}
|
|
||||||
if 'Error' in resp_session:
|
|
||||||
raise ConnectionError(resp_session['Error'])
|
|
||||||
|
|
||||||
|
|
||||||
def qrz_process_info(data: dict):
|
|
||||||
if 'name' in data:
|
|
||||||
if 'fname' in data:
|
|
||||||
name = data['fname'] + ' ' + data['name']
|
|
||||||
else:
|
|
||||||
name = data['name']
|
|
||||||
else:
|
|
||||||
name = None
|
|
||||||
if 'state' in data:
|
|
||||||
state = f', {data["state"]}'
|
|
||||||
else:
|
|
||||||
state = ''
|
|
||||||
address = data.get('addr1', '') + '\n' + data.get('addr2', '') + state + ' ' + data.get('zip', '')
|
|
||||||
address = address.strip()
|
|
||||||
if address == '':
|
|
||||||
address = None
|
|
||||||
if 'eqsl' in data:
|
|
||||||
eqsl = 'Yes' if data['eqsl'] == 1 else 'No'
|
|
||||||
else:
|
|
||||||
eqsl = 'Unknown'
|
|
||||||
if 'mqsl' in data:
|
|
||||||
mqsl = 'Yes' if data['mqsl'] == 1 else 'No'
|
|
||||||
else:
|
|
||||||
mqsl = 'Unknown'
|
|
||||||
if 'lotw' in data:
|
|
||||||
lotw = 'Yes' if data['lotw'] == 1 else 'No'
|
|
||||||
else:
|
|
||||||
lotw = 'Unknown'
|
|
||||||
|
|
||||||
return OrderedDict([('Name', name),
|
|
||||||
('Country', data.get('country', None)),
|
|
||||||
('Address', address),
|
|
||||||
('Grid Square', data.get('grid', None)),
|
|
||||||
('County', data.get('county', None)),
|
|
||||||
('CQ Zone', data.get('cqzone', None)),
|
|
||||||
('ITU Zone', data.get('ituzone', None)),
|
|
||||||
('IOTA Designator', data.get('iota', None)),
|
|
||||||
('Expires', data.get('expdate', None)),
|
|
||||||
('Aliases', data.get('aliases', None)),
|
|
||||||
('Previous Callsign', data.get('p_call', None)),
|
|
||||||
('License Class', data.get('class', None)),
|
|
||||||
('eQSL?', eqsl),
|
|
||||||
('Paper QSL?', mqsl),
|
|
||||||
('LotW?', lotw),
|
|
||||||
('QSL Info', data.get('qslmgr', None)),
|
|
||||||
('CQ Zone', data.get('cqzone', None)),
|
|
||||||
('ITU Zone', data.get('ituzone', None)),
|
|
||||||
('IOTA Designator', data.get('iota', None)),
|
|
||||||
('Born', data.get('born', None))])
|
|
||||||
|
|
||||||
|
|
||||||
def setup(bot):
|
|
||||||
bot.add_cog(QRZCog(bot))
|
|
278
exts/study.py
@ -1,117 +1,235 @@
|
|||||||
"""
|
"""
|
||||||
Study extension for qrm
|
Study extension for qrm
|
||||||
---
|
---
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
General Public License, version 2.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
import discord.ext.commands as commands
|
import discord.ext.commands as commands
|
||||||
|
|
||||||
import common as cmn
|
import common as cmn
|
||||||
|
from resources import study
|
||||||
|
|
||||||
|
|
||||||
class StudyCog(commands.Cog):
|
class StudyCog(commands.Cog):
|
||||||
|
choices = {"A": cmn.emojis.a, "B": cmn.emojis.b, "C": cmn.emojis.c, "D": cmn.emojis.d, "E": cmn.emojis.e}
|
||||||
|
choices_inv = {y: x for x, y in choices.items()}
|
||||||
|
|
||||||
def __init__(self, bot: commands.Bot):
|
def __init__(self, bot: commands.Bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.lastq = dict()
|
self.lastq = dict()
|
||||||
self.source = 'Data courtesy of [HamStudy.org](https://hamstudy.org/)'
|
self.source = "Data courtesy of [HamStudy.org](https://hamstudy.org/)"
|
||||||
self.session = bot.qrm.session
|
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, level: str = None):
|
async def _random_question(self, ctx: commands.Context, country: str = "", level: str = "", element: str = ""):
|
||||||
'''Gets a random question from the Technician, General, and/or Extra question pools.'''
|
"""Gets a random question from [HamStudy's](https://hamstudy.org) question pools."""
|
||||||
tech_pool = 'E2_2018'
|
|
||||||
gen_pool = 'E3_2019'
|
|
||||||
extra_pool = 'E4_2016'
|
|
||||||
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
with ctx.typing():
|
with ctx.typing():
|
||||||
selected_pool = None
|
embed = cmn.embed_factory(ctx)
|
||||||
try:
|
|
||||||
|
country = country.lower()
|
||||||
level = level.lower()
|
level = level.lower()
|
||||||
except AttributeError: # no level given (it's None)
|
element = element.upper()
|
||||||
pass
|
|
||||||
|
|
||||||
if level in ['t', 'technician', 'tech']:
|
if country in study.pool_names.keys():
|
||||||
selected_pool = tech_pool
|
if level in study.pool_names[country].keys():
|
||||||
|
pool_name = study.pool_names[country][level]
|
||||||
|
|
||||||
if level in ['g', 'gen', 'general']:
|
elif level in ("random", "r"):
|
||||||
selected_pool = gen_pool
|
# select a random level in that country
|
||||||
|
pool_name = random.choice(list(study.pool_names[country].values()))
|
||||||
|
|
||||||
if level in ['e', 'ae', 'extra']:
|
else:
|
||||||
selected_pool = extra_pool
|
# show list of possible pools
|
||||||
|
embed.title = "Pool Not Found!"
|
||||||
if (level is None) or (level == 'all'): # no pool given or user wants all, so pick a random pool
|
embed.description = "Possible arguments are:"
|
||||||
selected_pool = random.choice([tech_pool, gen_pool, extra_pool])
|
|
||||||
if (level is not None) and (selected_pool is None): # unrecognized pool given by user
|
|
||||||
embed.title = 'Error in HamStudy command'
|
|
||||||
embed.description = ('The question pool you gave was unrecognized. '
|
|
||||||
'There are many ways to call up certain question pools - try ?rq t, g, or e. '
|
|
||||||
'\n\nNote that currently only the US question pools are available.')
|
|
||||||
embed.colour = cmn.colours.bad
|
embed.colour = cmn.colours.bad
|
||||||
|
for cty in study.pool_names:
|
||||||
|
levels = "`, `".join(study.pool_names[cty].keys())
|
||||||
|
embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**",
|
||||||
|
value=f"Levels: `{levels}`", inline=False)
|
||||||
|
embed.add_field(name="**Random**", value="To select a random pool or country, use `random` or `r`")
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
return
|
return
|
||||||
|
|
||||||
async with self.session.get(f'https://hamstudy.org/pools/{selected_pool}') as resp:
|
elif country in ("random", "r"):
|
||||||
|
# select a random country and level
|
||||||
|
country = random.choice(list(study.pool_names.keys()))
|
||||||
|
pool_name = random.choice(list(study.pool_names[country].values()))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# show list of possible pools
|
||||||
|
embed.title = "Pool Not Found!"
|
||||||
|
embed.description = "Possible arguments are:"
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
for cty in study.pool_names:
|
||||||
|
levels = "`, `".join(study.pool_names[cty].keys())
|
||||||
|
embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**",
|
||||||
|
value=f"Levels: `{levels}`", inline=False)
|
||||||
|
embed.add_field(name="**Random**", value="To select a random pool or country, use `random` or `r`")
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
return
|
||||||
|
|
||||||
|
pools = await self.hamstudy_get_pools()
|
||||||
|
|
||||||
|
pool_matches = [p for p in pools.keys() if "_".join(p.split("_")[:-1]) == pool_name]
|
||||||
|
|
||||||
|
if len(pool_matches) > 0:
|
||||||
|
if len(pool_matches) == 1:
|
||||||
|
pool = pool_matches[0]
|
||||||
|
else:
|
||||||
|
# look at valid_from and expires dates to find the correct one
|
||||||
|
for p in pool_matches:
|
||||||
|
valid_from = datetime.fromisoformat(pools[p]["valid_from"][:-1])
|
||||||
|
expires = datetime.fromisoformat(pools[p]["expires"][:-1])
|
||||||
|
|
||||||
|
if valid_from < datetime.utcnow() < expires:
|
||||||
|
pool = p
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# show list of possible pools
|
||||||
|
embed.title = "Pool Not Found!"
|
||||||
|
embed.description = "Possible arguments are:"
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
for cty in study.pool_names:
|
||||||
|
levels = "`, `".join(study.pool_names[cty].keys())
|
||||||
|
embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**",
|
||||||
|
value=f"Levels: `{levels}`", inline=False)
|
||||||
|
embed.add_field(name="**Random**", value="To select a random pool or country, use `random` or `r`")
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
return
|
||||||
|
|
||||||
|
pool_meta = pools[pool]
|
||||||
|
|
||||||
|
async with self.session.get(f"https://hamstudy.org/pools/{pool}") as resp:
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
embed.title = 'Error in HamStudy command'
|
raise cmn.BotHTTPError(resp)
|
||||||
embed.description = 'Could not load questions'
|
pool = json.loads(await resp.read())["pool"]
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
return
|
|
||||||
pool = json.loads(await resp.read())['pool']
|
|
||||||
|
|
||||||
# Select a question
|
# Select a question
|
||||||
pool_section = random.choice(pool)['sections']
|
if element:
|
||||||
pool_questions = random.choice(pool_section)['questions']
|
els = [el["id"] for el in pool]
|
||||||
question = random.choice(pool_questions)
|
if element in els:
|
||||||
|
pool_section = pool[els.index(element)]["sections"]
|
||||||
embed.title = question['id']
|
|
||||||
embed.description = self.source
|
|
||||||
embed.colour = cmn.colours.good
|
|
||||||
embed.add_field(name='Question:', value=question['text'], inline=False)
|
|
||||||
embed.add_field(name='Answers:', value='**A:** ' + question['answers']['A']
|
|
||||||
+ '\n**B:** ' + question['answers']['B']
|
|
||||||
+ '\n**C:** ' + question['answers']['C']
|
|
||||||
+ '\n**D:** ' + question['answers']['D'], inline=False)
|
|
||||||
embed.add_field(name='Answer:', value='Type _?rqa_ for answer', inline=False)
|
|
||||||
if 'image' in question:
|
|
||||||
image_url = f'https://hamstudy.org/_1330011/images/{selected_pool.split("_",1)[1]}/{question["image"]}'
|
|
||||||
embed.set_image(url=image_url)
|
|
||||||
self.lastq[ctx.message.channel.id] = (question['id'], question['answer'])
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
|
|
||||||
@commands.command(name="hamstudyanswer", aliases=['rqa', 'randomquestionanswer', 'randomqa', 'hamstudya'],
|
|
||||||
category=cmn.cat.study)
|
|
||||||
async def _q_answer(self, ctx: commands.Context, answer: str = None):
|
|
||||||
'''Returns the answer to question last asked (Optional argument: your answer).'''
|
|
||||||
with ctx.typing():
|
|
||||||
correct_ans = self.lastq[ctx.message.channel.id][1]
|
|
||||||
q_num = self.lastq[ctx.message.channel.id][0]
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
if answer is not None:
|
|
||||||
answer = answer.upper()
|
|
||||||
if answer == correct_ans:
|
|
||||||
result = f'Correct! The answer to {q_num} was **{correct_ans}**.'
|
|
||||||
embed.title = f'{q_num} Answer'
|
|
||||||
embed.description = f'{self.source}\n\n{result}'
|
|
||||||
embed.colour = cmn.colours.good
|
|
||||||
else:
|
else:
|
||||||
result = f'Incorrect. The answer to {q_num} was **{correct_ans}**, not **{answer}**.'
|
embed.title = "Element Not Found!"
|
||||||
embed.title = f'{q_num} Answer'
|
embed.description = f"Possible Elements for Country `{country}` and Level `{level}` are:"
|
||||||
embed.description = f'{self.source}\n\n{result}'
|
|
||||||
embed.colour = cmn.colours.bad
|
embed.colour = cmn.colours.bad
|
||||||
else:
|
embed.description += "\n\n" + "`" + "`, `".join(els) + "`"
|
||||||
result = f'The correct answer to {q_num} was **{correct_ans}**.'
|
|
||||||
embed.title = f'{q_num} Answer'
|
|
||||||
embed.description = f'{self.source}\n\n{result}'
|
|
||||||
embed.colour = cmn.colours.neutral
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
pool_section = random.choice(pool)["sections"]
|
||||||
|
pool_questions = random.choice(pool_section)["questions"]
|
||||||
|
question = random.choice(pool_questions)
|
||||||
|
answers = question['answers']
|
||||||
|
answers_str = ""
|
||||||
|
answers_str_bolded = ""
|
||||||
|
for letter, ans in answers.items():
|
||||||
|
answers_str += f"{self.choices[letter]} {ans}\n"
|
||||||
|
if letter == question["answer"]:
|
||||||
|
answers_str_bolded += f"{self.choices[letter]} **{ans}**\n"
|
||||||
|
else:
|
||||||
|
answers_str_bolded += f"{self.choices[letter]} {ans}\n"
|
||||||
|
|
||||||
|
embed.title = f"{study.pool_emojis[country]} {pool_meta['class']} {question['id']}"
|
||||||
|
embed.description = self.source
|
||||||
|
embed.add_field(name="Question", value=question["text"], inline=False)
|
||||||
|
embed.add_field(name="Answers", value=answers_str, inline=False)
|
||||||
|
embed.add_field(name="To Answer",
|
||||||
|
value=("Answer with reactions below. If not answered within 5 minutes,"
|
||||||
|
" the answer will be revealed."),
|
||||||
|
inline=False)
|
||||||
|
if "image" in question:
|
||||||
|
image_url = f"https://hamstudy.org/images/{pool_meta['year']}/{question['image']}"
|
||||||
|
embed.set_image(url=image_url)
|
||||||
|
|
||||||
|
q_msg = await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
for i in range(len(answers)):
|
||||||
|
await cmn.add_react(q_msg, list(self.choices.values())[i])
|
||||||
|
await cmn.add_react(q_msg, cmn.emojis.question)
|
||||||
|
|
||||||
|
def check(ev):
|
||||||
|
return (ev.user_id != self.bot.user.id
|
||||||
|
and ev.message_id == q_msg.id
|
||||||
|
and (str(ev.emoji) in self.choices.values() or str(ev.emoji) == cmn.emojis.question))
|
||||||
|
|
||||||
|
try:
|
||||||
|
ev = await self.bot.wait_for("raw_reaction_add", timeout=300.0, check=check)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
embed.set_field_at(1, name="Answers", value=answers_str_bolded, inline=False)
|
||||||
|
embed.set_field_at(2, name="Answer",
|
||||||
|
value=(f"{cmn.emojis.stopwatch} "
|
||||||
|
f"**Timed out!** The correct answer was {self.choices[question['answer']]}"))
|
||||||
|
embed.colour = cmn.colours.timeout
|
||||||
|
await q_msg.edit(embed=embed)
|
||||||
|
else:
|
||||||
|
if str(ev.emoji) == cmn.emojis.question:
|
||||||
|
embed.set_field_at(1, name="Answers", value=answers_str_bolded, inline=False)
|
||||||
|
embed.set_field_at(2, name="Answer",
|
||||||
|
value=f"The correct answer was {self.choices[question['answer']]}", inline=False)
|
||||||
|
# only available in guilds, but it only makes sense there
|
||||||
|
if ev.member:
|
||||||
|
embed.add_field(name="Answer Requested By", value=str(ev.member), inline=False)
|
||||||
|
embed.colour = cmn.colours.timeout
|
||||||
|
await q_msg.edit(embed=embed)
|
||||||
|
else:
|
||||||
|
answers_str_checked = ""
|
||||||
|
chosen_ans = self.choices_inv[str(ev.emoji)]
|
||||||
|
for letter, ans in answers.items():
|
||||||
|
answers_str_checked += f"{self.choices[letter]}"
|
||||||
|
if letter == question["answer"] == chosen_ans:
|
||||||
|
answers_str_checked += f"{cmn.emojis.check_mark} **{ans}**\n"
|
||||||
|
elif letter == question["answer"]:
|
||||||
|
answers_str_checked += f" **{ans}**\n"
|
||||||
|
elif letter == chosen_ans:
|
||||||
|
answers_str_checked += f"{cmn.emojis.x} {ans}\n"
|
||||||
|
else:
|
||||||
|
answers_str_checked += f" {ans}\n"
|
||||||
|
|
||||||
|
if self.choices[question["answer"]] == str(ev.emoji):
|
||||||
|
embed.set_field_at(1, name="Answers", value=answers_str_checked, inline=False)
|
||||||
|
embed.set_field_at(2, name="Answer", value=(f"{cmn.emojis.check_mark} "
|
||||||
|
f"**Correct!** The answer was {ev.emoji}"))
|
||||||
|
# only available in guilds, but it only makes sense there
|
||||||
|
if ev.member:
|
||||||
|
embed.add_field(name="Answered By", value=str(ev.member), inline=False)
|
||||||
|
embed.colour = cmn.colours.good
|
||||||
|
await q_msg.edit(embed=embed)
|
||||||
|
else:
|
||||||
|
embed.set_field_at(1, name="Answers", value=answers_str_checked, inline=False)
|
||||||
|
embed.set_field_at(2, name="Answer",
|
||||||
|
value=(f"{cmn.emojis.x} **Incorrect!** The correct answer was "
|
||||||
|
f"{self.choices[question['answer']]}, not {ev.emoji}"))
|
||||||
|
# only available in guilds, but it only makes sense there
|
||||||
|
if ev.member:
|
||||||
|
embed.add_field(name="Answered By", value=str(ev.member), inline=False)
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
await q_msg.edit(embed=embed)
|
||||||
|
|
||||||
|
async def hamstudy_get_pools(self):
|
||||||
|
async with self.session.get("https://hamstudy.org/pools/") as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
raise cmn.BotHTTPError(resp)
|
||||||
|
else:
|
||||||
|
pools_dict = json.loads(await resp.read())
|
||||||
|
|
||||||
|
pools = dict()
|
||||||
|
for ls in pools_dict.values():
|
||||||
|
for pool in ls:
|
||||||
|
pools[pool["id"]] = pool
|
||||||
|
|
||||||
|
return pools
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: commands.Bot):
|
def setup(bot: commands.Bot):
|
||||||
|
71
exts/tex.py
Normal 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
@ -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))
|
126
exts/weather.py
@ -1,126 +0,0 @@
|
|||||||
"""
|
|
||||||
Weather extension for qrm
|
|
||||||
---
|
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
|
||||||
|
|
||||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
|
||||||
General Public License, version 2.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import io
|
|
||||||
import re
|
|
||||||
|
|
||||||
import discord
|
|
||||||
import discord.ext.commands as commands
|
|
||||||
|
|
||||||
import common as cmn
|
|
||||||
|
|
||||||
|
|
||||||
class WeatherCog(commands.Cog):
|
|
||||||
wttr_units_regex = re.compile(r"\B-([cCfF])\b")
|
|
||||||
|
|
||||||
def __init__(self, bot: commands.Bot):
|
|
||||||
self.bot = bot
|
|
||||||
self.session = bot.qrm.session
|
|
||||||
|
|
||||||
@commands.command(name="bandconditions", aliases=['cond', 'condx', 'conditions'], category=cmn.cat.weather)
|
|
||||||
async def _band_conditions(self, ctx: commands.Context):
|
|
||||||
'''Posts an image of HF Band Conditions.'''
|
|
||||||
with ctx.typing():
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
embed.title = 'Current Solar Conditions'
|
|
||||||
embed.colour = cmn.colours.good
|
|
||||||
async with self.session.get('http://www.hamqsl.com/solarsun.php') as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
embed.description = 'Could not download file...'
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
else:
|
|
||||||
data = io.BytesIO(await resp.read())
|
|
||||||
embed.set_image(url=f'attachment://condx.png')
|
|
||||||
await ctx.send(embed=embed, file=discord.File(data, 'condx.png'))
|
|
||||||
|
|
||||||
@commands.group(name="weather", aliases=['wttr'], category=cmn.cat.weather)
|
|
||||||
async def _weather_conditions(self, ctx: commands.Context):
|
|
||||||
'''Posts an image of Local Weather Conditions from [wttr.in](http://wttr.in/).
|
|
||||||
|
|
||||||
*Supported location types:*
|
|
||||||
city name: `paris`
|
|
||||||
any location: `~Eiffel Tower`
|
|
||||||
Unicode name of any location in any language: `Москва`
|
|
||||||
airport code (3 letters): `muc`
|
|
||||||
domain name `@stackoverflow.com`
|
|
||||||
area codes: `12345`
|
|
||||||
GPS coordinates: `-78.46,106.79`
|
|
||||||
'''
|
|
||||||
if ctx.invoked_subcommand is None:
|
|
||||||
await ctx.send_help(ctx.command)
|
|
||||||
|
|
||||||
@_weather_conditions.command(name='forecast', aliases=['fc', 'future'], category=cmn.cat.weather)
|
|
||||||
async def _weather_conditions_forecast(self, ctx: commands.Context, *, location: str):
|
|
||||||
'''Posts an image of Local Weather Conditions for the next three days from [wttr.in](http://wttr.in/).
|
|
||||||
See help for weather command for possible location types. Add a `-c` or `-f` to use Celcius or Fahrenheit.'''
|
|
||||||
with ctx.typing():
|
|
||||||
try:
|
|
||||||
units_arg = re.search(self.wttr_units_regex, location).group(1)
|
|
||||||
except AttributeError:
|
|
||||||
units_arg = ''
|
|
||||||
if units_arg.lower() == 'f':
|
|
||||||
units = 'u'
|
|
||||||
elif units_arg.lower() == 'c':
|
|
||||||
units = 'm'
|
|
||||||
else:
|
|
||||||
units = ''
|
|
||||||
|
|
||||||
loc = self.wttr_units_regex.sub('', location).strip()
|
|
||||||
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
embed.title = f'Weather Forecast for {loc}'
|
|
||||||
embed.description = 'Data from [wttr.in](http://wttr.in/).'
|
|
||||||
embed.colour = cmn.colours.good
|
|
||||||
|
|
||||||
loc = loc.replace(' ', '+')
|
|
||||||
async with self.session.get(f'http://wttr.in/{loc}_{units}pnFQ.png') as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
embed.description = 'Could not download file...'
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
else:
|
|
||||||
data = io.BytesIO(await resp.read())
|
|
||||||
embed.set_image(url=f'attachment://wttr_forecast.png')
|
|
||||||
await ctx.send(embed=embed, file=discord.File(data, 'wttr_forecast.png'))
|
|
||||||
|
|
||||||
@_weather_conditions.command(name='now', aliases=['n'], category=cmn.cat.weather)
|
|
||||||
async def _weather_conditions_now(self, ctx: commands.Context, *, location: str):
|
|
||||||
'''Posts an image of current Local Weather Conditions from [wttr.in](http://wttr.in/).
|
|
||||||
See help for weather command for possible location types. Add a `-c` or `-f` to use Celcius or Fahrenheit.'''
|
|
||||||
with ctx.typing():
|
|
||||||
try:
|
|
||||||
units_arg = re.search(self.wttr_units_regex, location).group(1)
|
|
||||||
except AttributeError:
|
|
||||||
units_arg = ''
|
|
||||||
if units_arg.lower() == 'f':
|
|
||||||
units = 'u'
|
|
||||||
elif units_arg.lower() == 'c':
|
|
||||||
units = 'm'
|
|
||||||
else:
|
|
||||||
units = ''
|
|
||||||
|
|
||||||
loc = self.wttr_units_regex.sub('', location).strip()
|
|
||||||
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
embed.title = f'Current Weather for {loc}'
|
|
||||||
embed.description = 'Data from [wttr.in](http://wttr.in/).'
|
|
||||||
embed.colour = cmn.colours.good
|
|
||||||
|
|
||||||
loc = loc.replace(' ', '+')
|
|
||||||
async with self.session.get(f'http://wttr.in/{loc}_0{units}pnFQ.png') as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
embed.description = 'Could not download file...'
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
else:
|
|
||||||
data = io.BytesIO(await resp.read())
|
|
||||||
embed.set_image(url=f'attachment://wttr_now.png')
|
|
||||||
await ctx.send(embed=embed, file=discord.File(data, 'wttr_now.png'))
|
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: commands.Bot):
|
|
||||||
bot.add_cog(WeatherCog(bot))
|
|
32
info.py
@ -1,28 +1,18 @@
|
|||||||
"""
|
"""
|
||||||
Static info about the bot.
|
Static info about the bot.
|
||||||
---
|
---
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
General Public License, version 2.
|
|
||||||
---
|
|
||||||
|
|
||||||
`authors`: The authors of the bot.
|
|
||||||
|
|
||||||
`description`: A description of the bot.
|
|
||||||
|
|
||||||
`license`: The license the bot is released under.
|
|
||||||
|
|
||||||
`contrubuting`: Info on how to contribute to the bot.
|
|
||||||
|
|
||||||
`release`: Current bot version.
|
|
||||||
|
|
||||||
`release_timestamp`: When the bot was last released.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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, contributions welcome: https://github.com/classabbyamp/discord-qrm2"
|
contributing = """Check out the [source on GitHub](https://github.com/miaowware/qrm2). Contributions are welcome!
|
||||||
release = '2.1.0'
|
|
||||||
bot_server = 'https://discord.gg/Ntbg3J4'
|
All issues and requests related to resources (including maps, band charts, data) should be added \
|
||||||
|
in [miaowware/qrm-resources](https://github.com/miaowware/qrm-resources)."""
|
||||||
|
release = "2.9.2"
|
||||||
|
bot_server = "https://discord.gg/Ntbg3J4"
|
||||||
|
145
main.py
@ -2,29 +2,33 @@
|
|||||||
"""
|
"""
|
||||||
qrm, a bot for Discord
|
qrm, a bot for Discord
|
||||||
---
|
---
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
General Public License, version 2.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import time, datetime
|
from datetime import datetime, time
|
||||||
import random
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import httpx
|
||||||
import pytz
|
import pytz
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands, tasks
|
from discord.ext import commands, tasks
|
||||||
|
|
||||||
import common as cmn
|
|
||||||
import info
|
import info
|
||||||
import data.options as opt
|
import common as cmn
|
||||||
|
import utils.connector as conn
|
||||||
|
from utils.resources_manager import ResourcesManager
|
||||||
|
|
||||||
import data.keys as keys
|
import data.keys as keys
|
||||||
|
import data.options as opt
|
||||||
|
|
||||||
|
|
||||||
# --- Settings ---
|
# --- Settings ---
|
||||||
@ -32,99 +36,132 @@ import data.keys as keys
|
|||||||
exit_code = 1 # The default exit code. ?shutdown and ?restart will change it accordingly (fail-safe)
|
exit_code = 1 # The default exit code. ?shutdown and ?restart will change it accordingly (fail-safe)
|
||||||
|
|
||||||
ext_dir = "exts" # The name of the directory where extensions are located.
|
ext_dir = "exts" # The name of the directory where extensions are located.
|
||||||
|
plugin_dir = "data.plugins" # The name of the directory where plugins are located.
|
||||||
|
|
||||||
debug_mode = opt.debug # Separate assignement in-case we define an override (ternary operator goes here)
|
debug_mode = opt.debug # Separate assignement in-case we define an override (ternary operator goes here)
|
||||||
|
|
||||||
|
|
||||||
# --- Bot setup ---
|
# --- Bot setup ---
|
||||||
|
|
||||||
|
# Loop/aiohttp stuff
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
connector = loop.run_until_complete(conn.new_connector())
|
||||||
|
|
||||||
|
# Defining the intents
|
||||||
|
intents = discord.Intents.none()
|
||||||
|
intents.guilds = True
|
||||||
|
intents.messages = True
|
||||||
|
intents.reactions = True
|
||||||
|
intents.message_content = True
|
||||||
|
|
||||||
|
member_cache = discord.MemberCacheFlags.from_intents(intents)
|
||||||
|
|
||||||
bot = commands.Bot(command_prefix=opt.prefix,
|
bot = commands.Bot(command_prefix=opt.prefix,
|
||||||
description=info.description,
|
case_insensitive=True,
|
||||||
help_command=commands.MinimalHelpCommand())
|
description=info.description, help_command=commands.MinimalHelpCommand(),
|
||||||
|
intents=intents,
|
||||||
|
member_cache_flags=member_cache,
|
||||||
|
loop=loop,
|
||||||
|
connector=connector)
|
||||||
|
|
||||||
|
# Simple way to access bot-wide stuff in extensions.
|
||||||
bot.qrm = SimpleNamespace()
|
bot.qrm = SimpleNamespace()
|
||||||
bot.qrm.session = aiohttp.ClientSession(headers={'User-Agent': f'discord-qrm2/{info.release}'})
|
|
||||||
|
|
||||||
|
# Let's store stuff here.
|
||||||
|
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", hidden=True)
|
@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."""
|
||||||
await bot.qrm.session.close()
|
|
||||||
global exit_code
|
global exit_code
|
||||||
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", hidden=True)
|
@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."""
|
||||||
await bot.qrm.session.close()
|
|
||||||
global exit_code
|
global exit_code
|
||||||
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", hidden=True)
|
@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.
|
||||||
Defaults to `list` if no subcommand specified"""
|
Defaults to `list` if no subcommand specified"""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
cmd = bot.get_command("extctl list")
|
cmd = _extctl_list
|
||||||
await ctx.invoke(cmd)
|
await ctx.invoke(cmd)
|
||||||
|
|
||||||
|
|
||||||
@_extctl.command(name="list")
|
@_extctl.command(name="list", aliases=["ls"])
|
||||||
async def _extctl_list(ctx: commands.Context):
|
async def _extctl_list(ctx: commands.Context):
|
||||||
"""Lists Extensions."""
|
"""Lists loaded extensions."""
|
||||||
embed = cmn.embed_factory(ctx)
|
embed = cmn.embed_factory(ctx)
|
||||||
embed.title = "Loaded Extensions"
|
embed.title = "Loaded Extensions"
|
||||||
embed.description = "\n".join(["‣ " + x.split(".")[1] for x in bot.extensions.keys()])
|
embed.description = "\n".join(
|
||||||
|
["‣ " + x.split(".")[-1] for x in bot.extensions.keys() if not x.startswith(plugin_dir)]
|
||||||
|
)
|
||||||
|
if plugins := ["‣ " + x.split(".")[-1] for x in bot.extensions.keys() if x.startswith(plugin_dir)]:
|
||||||
|
embed.add_field(name="Loaded Plugins", value="\n".join(plugins))
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
@_extctl.command(name="load")
|
@_extctl.command(name="load", aliases=["ld"])
|
||||||
async def _extctl_load(ctx: commands.Context, extension: str):
|
async def _extctl_load(ctx: commands.Context, extension: str):
|
||||||
|
"""Loads an extension."""
|
||||||
try:
|
try:
|
||||||
bot.load_extension(ext_dir + "." + extension)
|
bot.load_extension(ext_dir + "." + extension)
|
||||||
|
except discord.errors.ExtensionNotFound as e:
|
||||||
|
try:
|
||||||
|
bot.load_extension(plugin_dir + "." + extension)
|
||||||
|
except discord.errors.ExtensionNotFound:
|
||||||
|
raise e
|
||||||
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
|
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
|
||||||
except commands.ExtensionError as ex:
|
|
||||||
embed = cmn.error_embed_factory(ctx, ex, bot.qrm.debug_mode)
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
|
|
||||||
|
|
||||||
@_extctl.command(name="reload", aliases=["relaod"])
|
@_extctl.command(name="reload", aliases=["rl", "r", "relaod"])
|
||||||
async def _extctl_reload(ctx: commands.Context, extension: str):
|
async def _extctl_reload(ctx: commands.Context, extension: str):
|
||||||
|
"""Reloads an extension."""
|
||||||
if ctx.invoked_with == "relaod":
|
if ctx.invoked_with == "relaod":
|
||||||
pika = bot.get_emoji(opt.pika)
|
pika = bot.get_emoji(opt.pika)
|
||||||
if pika:
|
if pika:
|
||||||
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 discord.errors.ExtensionNotLoaded as e:
|
||||||
|
try:
|
||||||
|
bot.reload_extension(plugin_dir + "." + extension)
|
||||||
|
except discord.errors.ExtensionNotLoaded:
|
||||||
|
raise e
|
||||||
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
|
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
|
||||||
except commands.ExtensionError as ex:
|
|
||||||
embed = cmn.error_embed_factory(ctx, ex, bot.qrm.debug_mode)
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
|
|
||||||
|
|
||||||
@_extctl.command(name="unload")
|
@_extctl.command(name="unload", aliases=["ul"])
|
||||||
async def _extctl_unload(ctx: commands.Context, extension: str):
|
async def _extctl_unload(ctx: commands.Context, extension: str):
|
||||||
|
"""Unloads an extension."""
|
||||||
try:
|
try:
|
||||||
bot.unload_extension(ext_dir + "." + extension)
|
bot.unload_extension(ext_dir + "." + extension)
|
||||||
|
except discord.errors.ExtensionNotLoaded as e:
|
||||||
|
try:
|
||||||
|
bot.unload_extension(plugin_dir + "." + extension)
|
||||||
|
except discord.errors.ExtensionNotLoaded:
|
||||||
|
raise e
|
||||||
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
|
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
|
||||||
except commands.ExtensionError as ex:
|
|
||||||
embed = cmn.error_embed_factory(ctx, ex, bot.qrm.debug_mode)
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Events ---
|
# --- Events ---
|
||||||
@ -156,7 +193,10 @@ async def on_command_error(ctx: commands.Context, err: commands.CommandError):
|
|||||||
if isinstance(err, commands.UserInputError):
|
if isinstance(err, commands.UserInputError):
|
||||||
await cmn.add_react(ctx.message, cmn.emojis.warning)
|
await cmn.add_react(ctx.message, cmn.emojis.warning)
|
||||||
await ctx.send_help(ctx.command)
|
await ctx.send_help(ctx.command)
|
||||||
elif isinstance(err, commands.CommandNotFound) and not ctx.invoked_with.startswith("?"):
|
elif isinstance(err, commands.CommandNotFound):
|
||||||
|
if ctx.invoked_with and ctx.invoked_with.startswith(("?", "!")):
|
||||||
|
return
|
||||||
|
else:
|
||||||
await cmn.add_react(ctx.message, cmn.emojis.question)
|
await cmn.add_react(ctx.message, cmn.emojis.question)
|
||||||
elif isinstance(err, commands.CheckFailure):
|
elif isinstance(err, commands.CheckFailure):
|
||||||
# Add handling of other subclasses of CheckFailure as needed.
|
# Add handling of other subclasses of CheckFailure as needed.
|
||||||
@ -168,7 +208,7 @@ async def on_command_error(ctx: commands.Context, err: commands.CommandError):
|
|||||||
await cmn.add_react(ctx.message, cmn.emojis.bangbang)
|
await cmn.add_react(ctx.message, cmn.emojis.bangbang)
|
||||||
elif isinstance(err, (commands.CommandInvokeError, commands.ConversionError)):
|
elif isinstance(err, (commands.CommandInvokeError, commands.ConversionError)):
|
||||||
# Emulating discord.py's default beaviour.
|
# Emulating discord.py's default beaviour.
|
||||||
print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr)
|
print("Ignoring exception in command {}:".format(ctx.command), file=sys.stderr)
|
||||||
traceback.print_exception(type(err), err, err.__traceback__, file=sys.stderr)
|
traceback.print_exception(type(err), err, err.__traceback__, file=sys.stderr)
|
||||||
|
|
||||||
embed = cmn.error_embed_factory(ctx, err.original, bot.qrm.debug_mode)
|
embed = cmn.error_embed_factory(ctx, err.original, bot.qrm.debug_mode)
|
||||||
@ -177,7 +217,7 @@ async def on_command_error(ctx: commands.Context, err: commands.CommandError):
|
|||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
else:
|
else:
|
||||||
# Emulating discord.py's default beaviour. (safest bet)
|
# Emulating discord.py's default beaviour. (safest bet)
|
||||||
print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr)
|
print("Ignoring exception in command {}:".format(ctx.command), file=sys.stderr)
|
||||||
traceback.print_exception(type(err), err, err.__traceback__, file=sys.stderr)
|
traceback.print_exception(type(err), err, err.__traceback__, file=sys.stderr)
|
||||||
await cmn.add_react(ctx.message, cmn.emojis.warning)
|
await cmn.add_react(ctx.message, cmn.emojis.warning)
|
||||||
|
|
||||||
@ -191,7 +231,10 @@ async def _ensure_activity_time():
|
|||||||
try:
|
try:
|
||||||
tz = pytz.timezone(opt.status_tz)
|
tz = pytz.timezone(opt.status_tz)
|
||||||
except pytz.exceptions.UnknownTimeZoneError:
|
except pytz.exceptions.UnknownTimeZoneError:
|
||||||
await bot.change_presence(activity=discord.Game(name="with invalid timezones."))
|
status = "with invalid timezones"
|
||||||
|
if opt.show_help:
|
||||||
|
status += f" | {opt.display_prefix}help"
|
||||||
|
await bot.change_presence(activity=discord.Game(name=status))
|
||||||
return
|
return
|
||||||
|
|
||||||
now = datetime.now(tz=tz).time()
|
now = datetime.now(tz=tz).time()
|
||||||
@ -201,6 +244,8 @@ async def _ensure_activity_time():
|
|||||||
end_time = time(hour=sts[2][0], minute=sts[2][1], tzinfo=tz)
|
end_time = time(hour=sts[2][0], minute=sts[2][1], tzinfo=tz)
|
||||||
if start_time < now <= end_time:
|
if start_time < now <= end_time:
|
||||||
status = sts[0]
|
status = sts[0]
|
||||||
|
if opt.show_help:
|
||||||
|
status += f" | {opt.display_prefix}help"
|
||||||
|
|
||||||
await bot.change_presence(activity=discord.Game(name=status))
|
await bot.change_presence(activity=discord.Game(name=status))
|
||||||
|
|
||||||
@ -208,6 +253,8 @@ async def _ensure_activity_time():
|
|||||||
@tasks.loop(minutes=5)
|
@tasks.loop(minutes=5)
|
||||||
async def _ensure_activity_random():
|
async def _ensure_activity_random():
|
||||||
status = random.choice(opt.statuses)
|
status = random.choice(opt.statuses)
|
||||||
|
if opt.show_help:
|
||||||
|
status += f" | {opt.display_prefix}help"
|
||||||
|
|
||||||
await bot.change_presence(activity=discord.Game(name=status))
|
await bot.change_presence(activity=discord.Game(name=status))
|
||||||
|
|
||||||
@ -215,14 +262,33 @@ async def _ensure_activity_random():
|
|||||||
@tasks.loop(minutes=5)
|
@tasks.loop(minutes=5)
|
||||||
async def _ensure_activity_fixed():
|
async def _ensure_activity_fixed():
|
||||||
status = opt.statuses[0]
|
status = opt.statuses[0]
|
||||||
|
if opt.show_help:
|
||||||
|
status += f" | {opt.display_prefix}help"
|
||||||
|
|
||||||
await bot.change_presence(activity=discord.Game(name=status))
|
await bot.change_presence(activity=discord.Game(name=status))
|
||||||
|
|
||||||
|
|
||||||
# --- Run ---
|
# --- Run ---
|
||||||
|
|
||||||
|
resource_versions = {
|
||||||
|
"bandcharts": "v1",
|
||||||
|
"img": "v1",
|
||||||
|
"maps": "v1",
|
||||||
|
"morse": "v1",
|
||||||
|
"phonetics": "v1",
|
||||||
|
"qcodes": "v1",
|
||||||
|
"funetics": "v1",
|
||||||
|
"latex_template": "v1",
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.qrm.rm = ResourcesManager(cmn.paths.resources, opt.resources_url, resource_versions)
|
||||||
|
|
||||||
for ext in opt.exts:
|
for ext in opt.exts:
|
||||||
bot.load_extension(ext_dir + '.' + ext)
|
bot.load_extension(ext_dir + "." + ext)
|
||||||
|
|
||||||
|
# load all py files in plugin_dir
|
||||||
|
for plugin in (f.stem for f in Path(plugin_dir.replace(".", "/")).glob("*.py")):
|
||||||
|
bot.load_extension(plugin_dir + "." + plugin)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -246,6 +312,7 @@ except ConnectionResetError as ex:
|
|||||||
raise
|
raise
|
||||||
raise SystemExit("ConnectionResetError: {}".format(ex))
|
raise SystemExit("ConnectionResetError: {}".format(ex))
|
||||||
|
|
||||||
|
|
||||||
# --- Exit ---
|
# --- Exit ---
|
||||||
# Codes for the wrapper shell script:
|
# Codes for the wrapper shell script:
|
||||||
# 0 - Clean exit, don't restart
|
# 0 - Clean exit, don't restart
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
discord.py
|
py-cord-dev[speed]==2.5.0rc5
|
||||||
ctyparser
|
ctyparser~=2.0
|
||||||
|
gridtools~=1.0
|
||||||
|
callsignlookuptools[async]~=1.1
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
lxml
|
|
||||||
pytz
|
pytz
|
||||||
|
cairosvg
|
||||||
|
httpx
|
||||||
|
pydantic~=2.5
|
||||||
|
17
resource_schemas.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
"""
|
||||||
|
Resource schemas generator for qrm2.
|
||||||
|
---
|
||||||
|
Copyright (C) 2021-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import utils.resources_models as models
|
||||||
|
|
||||||
|
|
||||||
|
print("Generating schema for index.json")
|
||||||
|
with open("./dev-notes/rs_index_schema.json", "w") as file:
|
||||||
|
file.write(models.Index.schema_json(indent=4))
|
||||||
|
|
||||||
|
print("Done!")
|
@ -1,55 +1,27 @@
|
|||||||
"""
|
"""
|
||||||
Information about callsigns for the vanity prefixes command in hamcog.
|
Information about callsigns for the prefixes command in hamcog.
|
||||||
---
|
---
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
This file is part of discord-qrmbot and is released under the terms of the GNU
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
General Public License, version 2.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .callsigninfos import (us, ca)
|
||||||
|
|
||||||
|
|
||||||
us_calls_title = "Valid US Vanity Callsigns"
|
@dataclass
|
||||||
us_calls_desc = ('#x# is the number of letters in the prefix and suffix of a callsign. '
|
class CallsignInfoData:
|
||||||
'E.g., WY4RC would be a 2x2 callsign, with prefix WY and suffix RC.')
|
"""Represents a country's callsign info"""
|
||||||
us_calls = OrderedDict([('**Group A** (Extra Only)', ('**Any:** K, N, W (1x2)\n'
|
title: str = ""
|
||||||
' AA-AL, KA-KZ, NA-NZ, WA-WZ (2x1)\n'
|
desc: str = ""
|
||||||
' AA-AL (2x2)\n'
|
calls: str = ""
|
||||||
'*Except*\n'
|
emoji: str = ""
|
||||||
'**Alaska:** AL, KL, NL, WL (2x1)\n'
|
|
||||||
'**Caribbean:** KP, NP, WP (2x1)\n'
|
|
||||||
'**Pacific:** AH, KH, NH, WH (2x1)')),
|
|
||||||
('**Group B** (Advanced and Extra Only)', ('**Any:** KA-KZ, NA-NZ, WA-WZ (2x2)\n'
|
|
||||||
'*Except*\n'
|
|
||||||
'**Alaska:** AL (2x2)\n'
|
|
||||||
'**Caribbean:** KP (2x2)\n'
|
|
||||||
'**Pacific:** AH (2x2)')),
|
|
||||||
('**Group C** (Technician, General, Advanced, Extra Only)', ('**Any Region:** K, N, W (1x3)\n'
|
|
||||||
'*Except*\n'
|
|
||||||
'**Alaska:** KL, NL, WL (2x2)\n'
|
|
||||||
'**Caribbean:** NP, WP (2x2)\n'
|
|
||||||
'**Pacific:** KH, NH, WH (2x2)')),
|
|
||||||
('**Group D** (Any License Class)', ('**Any Region:** KA-KZ, WA-WZ (2x3)\n'
|
|
||||||
'*Except*\n'
|
|
||||||
'**Alaska:** KL, WL (2x3)\n'
|
|
||||||
'**Caribbean:** KP, WP (2x3)\n'
|
|
||||||
'**Pacific:** KH, WH (2x3)')),
|
|
||||||
('**Unavailable**', ('- KA2AA-KA9ZZ: US Army in Japan\n'
|
|
||||||
'- KC4AAA-KC4AAF: NSF in Antartica\n'
|
|
||||||
'- KC4USA-KC4USZ: US Navy in Antartica\n'
|
|
||||||
'- KG4AA-KG4ZZ: US Navy in Guantanamo Bay\n'
|
|
||||||
'- KL9KAA-KL9KHZ: US military in Korea\n'
|
|
||||||
'- KC6AA-KC6ZZ: Former US (Eastern and Western Caroline Islands), '
|
|
||||||
'now Federated States of Micronesia (V6) and Republic of Palau (T8)\n'
|
|
||||||
'- KX6AA-KX6ZZ: Former US (Marshall Islands), '
|
|
||||||
'now Republic of the Marshall Islands (V73)\n'
|
|
||||||
'- Any suffix SOS or QRA-QUZ\n'
|
|
||||||
'- Any 2x3 with X as the first suffix letter\n'
|
|
||||||
'- Any 2x3 with AF, KF, NF, or WF prefix and suffix EMA: FEMA\n'
|
|
||||||
'- Any 2x3 with AA-AL, NA-NZ, WC, WK, WM, WR, or WT prefix: "Group X"\n'
|
|
||||||
'- Any 2x1, 2x2, or 2x3 with KP, NP, WP prefix and 0, 6, 7, 8, 9 number\n'
|
|
||||||
'- Any 1x1 callsign: Special Event'))])
|
|
||||||
|
|
||||||
# format: country: (title, description, text)
|
|
||||||
options = {'us': (us_calls_title, us_calls_desc, us_calls)}
|
options = {
|
||||||
|
"us": CallsignInfoData(us.title, us.desc, us.calls, us.emoji),
|
||||||
|
"ca": CallsignInfoData(ca.title, ca.desc, ca.calls, ca.emoji),
|
||||||
|
}
|
||||||
|
3
resources/callsigninfos/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Callsign info for various countries
|
||||||
|
"""
|
46
resources/callsigninfos/ca.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""
|
||||||
|
Information about callsigns for the CA prefixes command in hamcog.
|
||||||
|
---
|
||||||
|
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
title = "Canadian Callsign Rules"
|
||||||
|
emoji = "🇨🇦"
|
||||||
|
desc = ("Canadian operators are limited to callsigns with the prefixes of their address' province/territory. "
|
||||||
|
"Initially, operators can choose a callsign with a 3-letter suffix. "
|
||||||
|
"Later on, they can apply to change or for additional callsigns. "
|
||||||
|
"Operators can only hold one 2-letter suffix callsign, but many 3-letter suffix callsigns. "
|
||||||
|
"If the number of 2-letter suffix callsigns exceeds 80% of the total available, "
|
||||||
|
"operators can only choose a 2-letter suffix after holding a license for 5 years. "
|
||||||
|
"If the operator is a family member of a deceased operator, they are not bound by this restriction. "
|
||||||
|
"Data from [ISED Canada (RIC-9)](https://www.ic.gc.ca/eic/site/smt-gst.nsf/eng/sf02102.html).")
|
||||||
|
calls = {
|
||||||
|
"Provinces": (
|
||||||
|
"**Nova Scotia:** VE1 and VA1\n"
|
||||||
|
"**Québec:** VE2 and VA2\n"
|
||||||
|
"**Ontario:** VE3 and VA3\n"
|
||||||
|
"**Manitoba:** VE4 and VA4\n"
|
||||||
|
"**Saskatchewan:** VE5 and VA5\n"
|
||||||
|
"**Alberta:** VE6 and VA6\n"
|
||||||
|
"**British Columbia:** VE7 and VA7\n"
|
||||||
|
"**New Brunswick:** VE9\n"
|
||||||
|
"**Newfoundland:** VO1\n"
|
||||||
|
"**Labrador:** VO2\n"
|
||||||
|
"**Prince Edward Island:** VY2\n"
|
||||||
|
),
|
||||||
|
"Territories": (
|
||||||
|
"**Northwest Territories:** VE8\n"
|
||||||
|
"**Nunavut:** VY0\n"
|
||||||
|
"**Yukon:** VY1\n"
|
||||||
|
),
|
||||||
|
"Other": (
|
||||||
|
"**International Waters:** VE0\n"
|
||||||
|
"**Government of Canada:** VY9\n"
|
||||||
|
"**Sable Island:** CY0\n"
|
||||||
|
"**St-Paul Island:** CY9\n"
|
||||||
|
),
|
||||||
|
"Special Event": "Various prefixes in the ranges: CF-CK, CY-CZ, VA-VG, VO, VX-VY, XJ-XO"
|
||||||
|
}
|
52
resources/callsigninfos/us.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"""
|
||||||
|
Information about callsigns for the US prefixes command in hamcog.
|
||||||
|
---
|
||||||
|
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
title = "US Callsign Rules"
|
||||||
|
emoji = "🇺🇸"
|
||||||
|
desc = ("#x# is the number of letters in the prefix and suffix of a callsign. "
|
||||||
|
"E.g., WY4RC would be a 2x2 callsign, with prefix WY and suffix RC.")
|
||||||
|
calls = {
|
||||||
|
"**Group A** (Extra Only)": ("**Any:** K, N, W (1x2)\n"
|
||||||
|
" AA-AL, KA-KZ, NA-NZ, WA-WZ (2x1)\n"
|
||||||
|
" AA-AL (2x2)\n"
|
||||||
|
"*Except*\n"
|
||||||
|
"**Alaska:** AL, KL, NL, WL (2x1)\n"
|
||||||
|
"**Caribbean:** KP, NP, WP (2x1)\n"
|
||||||
|
"**Pacific:** AH, KH, NH, WH (2x1)"),
|
||||||
|
"**Group B** (Advanced and Extra Only)": ("**Any:** KA-KZ, NA-NZ, WA-WZ (2x2)\n"
|
||||||
|
"*Except*\n"
|
||||||
|
"**Alaska:** AL (2x2)\n"
|
||||||
|
"**Caribbean:** KP (2x2)\n"
|
||||||
|
"**Pacific:** AH (2x2)"),
|
||||||
|
"**Group C** (Technician, General, Advanced, Extra Only)": ("**Any Region:** K, N, W (1x3)\n"
|
||||||
|
"*Except*\n"
|
||||||
|
"**Alaska:** KL, NL, WL (2x2)\n"
|
||||||
|
"**Caribbean:** NP, WP (2x2)\n"
|
||||||
|
"**Pacific:** KH, NH, WH (2x2)"),
|
||||||
|
"**Group D** (Any License Class)": ("**Any Region:** KA-KZ, WA-WZ (2x3)\n"
|
||||||
|
"*Except*\n"
|
||||||
|
"**Alaska:** KL, WL (2x3)\n"
|
||||||
|
"**Caribbean:** KP, WP (2x3)\n"
|
||||||
|
"**Pacific:** KH, WH (2x3)"),
|
||||||
|
"**Unavailable**": ("- KA2AA-KA9ZZ: US Army in Japan\n"
|
||||||
|
"- KC4AAA-KC4AAF: NSF in Antartica\n"
|
||||||
|
"- KC4USA-KC4USZ: US Navy in Antartica\n"
|
||||||
|
"- KG4AA-KG4ZZ: US Navy in Guantanamo Bay\n"
|
||||||
|
"- KL9KAA-KL9KHZ: US military in Korea\n"
|
||||||
|
"- KC6AA-KC6ZZ: Former US (Eastern and Western Caroline Islands), "
|
||||||
|
"now Federated States of Micronesia (V6) and Republic of Palau (T8)\n"
|
||||||
|
"- KX6AA-KX6ZZ: Former US (Marshall Islands), "
|
||||||
|
"now Republic of the Marshall Islands (V73)\n"
|
||||||
|
"- Any suffix SOS or QRA-QUZ\n"
|
||||||
|
"- Any 2x3 with X as the first suffix letter\n"
|
||||||
|
"- Any 2x3 with AF, KF, NF, or WF prefix and suffix EMA: FEMA\n"
|
||||||
|
"- Any 2x3 with AA-AL, NA-NZ, WC, WK, WM, WR, or WT prefix: \"Group X\"\n"
|
||||||
|
"- Any 2x1, 2x2, or 2x3 with KP, NP, WP prefix and 0, 6, 7, 8, 9 number\n"
|
||||||
|
"- Any 1x1 callsign: Special Event")
|
||||||
|
}
|
Before Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 153 KiB |
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"ca": ["ca.png", "Canada", "Amateur radio bands in Canada", "**This bandplan is incomplete**; some bands, like 630m, are simply not present. It also does not cover any band above 30MHz.", "[RAC 0-30MHz Band Plan](https://www.rac.ca/wp-content/uploads/files/pdf/RAC%20Bandplan%20December%201%202015.pdf)", "🇨🇦"],
|
|
||||||
"cn": ["cn.png", "China", "Amateur radio bands in China", "", "Created by KN8U and NY7H", "🇨🇳"],
|
|
||||||
"mx": ["mx.png", "Mexico", "Radio allocations in Mexico", "Full radio allocations chart for all services. No information specific to amateur radio is shown.", "Secretaría de Comunicaciones y Transportes (SCT) / Instituto Federal de Telecomunicaciones (IFT)", "🇲🇽"],
|
|
||||||
"nl": ["nl.png", "Netherlands", "Amateur radio bands in the Netherlands", "", "", "🇳🇱"],
|
|
||||||
"us": ["us.png", "USA", "Amateur radio bands in the USA", "", "*[ARRL Frequency Chart](https://www.arrl.org/shop/ARRL-Frequency-Chart-11-17/)* [[PDF]](http://www.arrl.org/files/file/Regulatory/Band%20Chart/Band%20Chart%20-%2011X17%20Color.pdf)", "🇺🇸"]
|
|
||||||
}
|
|
Before Width: | Height: | Size: 1.6 MiB |
Before Width: | Height: | Size: 160 KiB |
Before Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 504 KiB |
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"arrl": ["arrl-rac.png", "ARRL Sections", "ARRL Sections", "", "", "🇺🇸"],
|
|
||||||
"rac": ["arrl-rac.png", "RAC Sections", "RAC Sections", "", "", "🇨🇦"],
|
|
||||||
"cn": ["cn.png", "China's Prefixes", "Map of prefix regions in China", "", "CRAC", "🇨🇳"],
|
|
||||||
"us": ["us.png", "USA's Prefixes", "Map of prefix regions in the USA", "", "*[ARRL WAS Map](https://www.arrl.org/was-forms)* [[PDF]](http://www.arrl.org/files/file/Awards%20Application%20Forms/WASmap_Color.pdf)", "🇺🇸"]
|
|
||||||
}
|
|
Before Width: | Height: | Size: 136 KiB |
@ -1,29 +0,0 @@
|
|||||||
"""
|
|
||||||
A listing of morse code symbols
|
|
||||||
---
|
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
|
||||||
|
|
||||||
This file is part of discord-qrmbot and is released under the terms of the GNU
|
|
||||||
General Public License, version 2.
|
|
||||||
"""
|
|
||||||
morse = {"A": ".-", "B": "-...", "C": "-.-.", "D": "-..", "E": ".", "F": "..-.", "G": "--.",
|
|
||||||
"H": "....", "I": "..", "J": ".---", "K": "-.-", "L": ".-..", "M": "--", "N": "-.",
|
|
||||||
"O": "---", "P": ".--.", "Q": "--.-", "R": ".-.", "S": "...", "T": "-", "U": "..-",
|
|
||||||
"V": "...-", "W": ".--", "X": "-..-", "Y": "-.--", "Z": "--..", "1": ".----",
|
|
||||||
"2": "..---", "3": "...--", "4": "....-", "5": ".....", "6": "-....", "7": "--...",
|
|
||||||
"8": "---..", "9": "----.", "0": "-----", ".": ".-.-.-", ",": "--..--", "?": "..--..",
|
|
||||||
"'": ".----.", "!": "-.-.--", "/": "-..-.", "(": "-.--.", ")": "-.--.-", "&": ".-...",
|
|
||||||
":": "---...", ";": "-.-.-.", "=": "-...-", "+": ".-.-.", "-": "-....-", "\"": ".-..-.",
|
|
||||||
"@": ".--.-.", "Ä": ".-.-", "Å": ".-.-", "Ą": ".-.-", "Æ": ".-.-", "É": "..-..",
|
|
||||||
"Ñ": "--.--", "Ö": "---.", "Ü": "..--", "Š": "----", " ": "/"}
|
|
||||||
|
|
||||||
ascii = {".-": "A", "-...": "B", "-.-.": "C", "-..": "D", ".": "E", "..-.": "F", "--.": "G",
|
|
||||||
"....": "H", "..": "I", ".---": "J", "-.-": "K", ".-..": "L", "--": "M", "-.": "N",
|
|
||||||
"---": "O", ".--.": "P", "--.-": "Q", ".-.": "R", "...": "S", "-": "T", "..-": "U",
|
|
||||||
"...-": "V", ".--": "W", "-..-": "X", "-.--": "Y", "--..": "Z", ".----": "1",
|
|
||||||
"..---": "2", "...--": "3", "....-": "4", ".....": "5", "-....": "6", "--...": "7",
|
|
||||||
"---..": "8", "----.": "9", "-----": "0", ".-.-.-": ".", "--..--": ",", "..--..": "?",
|
|
||||||
".----.": "'", "-.-.--": "!", "-..-.": "/", "-.--.": "(", "-.--.-": ")", ".-...": "&",
|
|
||||||
"---...": ":", "-.-.-.": ";", "-...-": "=", ".-.-.": "+", "-....-": "-", ".-..-.": "\"",
|
|
||||||
".--.-.": "@", ".-.-": "Ä", "..-..": "É", "--.--": "Ñ", "---.": "Ö", "..--": "Ü",
|
|
||||||
"----": "Š", "/": " "}
|
|
@ -1,14 +0,0 @@
|
|||||||
"""
|
|
||||||
A listing of NATO Phonetics
|
|
||||||
---
|
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
|
||||||
|
|
||||||
This file is part of discord-qrmbot and is released under the terms of the GNU
|
|
||||||
General Public License, version 2.
|
|
||||||
"""
|
|
||||||
|
|
||||||
phonetics = {'a': 'alfa', 'b': 'bravo', 'c': 'charlie', 'd': 'delta', 'e': 'echo', 'f': 'foxtrot',
|
|
||||||
'g': 'golf', 'h': 'hotel', 'i': 'india', 'j': 'juliett', 'k': 'kilo', 'l': 'lima',
|
|
||||||
'm': 'mike', 'n': 'november', 'o': 'oscar', 'p': 'papa', 'q': 'quebec', 'r': 'romeo',
|
|
||||||
's': 'sierra', 't': 'tango', 'u': 'uniform', 'v': 'victor', 'w': 'whiskey', 'x': 'x-ray',
|
|
||||||
'y': 'yankee', 'z': 'zulu'}
|
|
@ -1,280 +0,0 @@
|
|||||||
"""
|
|
||||||
A listing of Q Codes
|
|
||||||
---
|
|
||||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
|
||||||
|
|
||||||
This file is part of discord-qrmbot and is released under the terms of the GNU
|
|
||||||
General Public License, version 2.
|
|
||||||
"""
|
|
||||||
# flake8: noqa
|
|
||||||
qcodes = {"QAB": "May I have clearance (for ...) from ... (place) to ... (place) at flight level/altitude ... ? / You are cleared (or ... is cleared) by ... from ... (place) to ... (place) at flight level/altitude ...",
|
|
||||||
"QAF": "Will you advise me when you are (were) at (over) ... (place)? / I am (was) at (over) ... (place) (at ... hours) at flight level/altitude ...",
|
|
||||||
"QAG": "Arrange your flight in order to arrive over ... (place) at ... hours.",
|
|
||||||
"QAH": "What is your height above ... (datum)? / I am at .... flight level/altitude ... --or-- Arrange your flight so as to reach flight level/altitude ... at ... (hours or place).",
|
|
||||||
"QAI": "What is the essential traffic respecting my aircraft? / The essential traffic respecting your aircraft is ...",
|
|
||||||
"QAK": "Is there any risk of collision? / There is risk of collision.",
|
|
||||||
"QAL": "Are you going to land at ... (place)? I am going to land at ... (place).",
|
|
||||||
"QAM": "What is the latest available meteorological observation for ... (place)? / Meteorological observation made at ... (place) at ... hours was as follows ...",
|
|
||||||
"QAN": "What is the surface wind direction and speed at ... (place)? / The surface wind direction and speed at ... (place) at ... hours is ... (direction) ... (speed).",
|
|
||||||
"QAO": "What is the wind direction in degrees TRUE and speed at ... (position or zone/s) at each of the ... (figures) ... (units) levels above ... (datum)? / The wind direction and speed at (position or zone/s) at flight level/altitude ... is: ... (vertical distance) ... degrees TRUE ... (speed).",
|
|
||||||
"QAP": "Shall I listen for you (or for ...) on ... kHz (... MHz)? / Listen for me (or for ...) on ... kHz (... MHz).",
|
|
||||||
"QAQ": "Am I near a prohibited, restricted or danger area?",
|
|
||||||
"QAR": "May I stop listening on the watch frequency for ... minutes? / You may stop listening on the watch frequency for ... minutes.",
|
|
||||||
"QAU": "Where may I jettison fuel? / I am about to jettison fuel.",
|
|
||||||
"QAW": "I am about to carry out overshoot procedure.",
|
|
||||||
"QAY": "Will you advise me when you pass (passed) ... (place) bearing 090 (270) degrees relative to your heading? / I passed ... (place) bearing ... degrees relative to my heading at ... hours.",
|
|
||||||
"QAZ": "Are you experiencing communication difficulties through flying in a storm? / I am experiencing communication difficulties through flying in a storm.",
|
|
||||||
"QBA": "What is the horizontal visibility at ... (place)? / The horizontal visibility at ... (place) at ... hours is ... (distance figures and units).",
|
|
||||||
"QBB": "What is the amount, type and height above official aerodrome elevation of the base of the cloud [at ... (place)]? / The amount, type and height above official aerodrome elevation of the base of the cloud at ... (place) at ... hours is: ... eights (... type) at ... (figures and units) height above official aerodrome elevation.",
|
|
||||||
"QBC": "Report meteorological conditions as observed from your aircraft [at ... (position or zone)] [(at ... hours)]. / The meteorological conditions as observed from my aircraft at ... (position or zone) at ... hours at ... (figures and units) height above ... (datum) are ...",
|
|
||||||
"QBD": "How much fuel have you remaining (expressed as hours and/or minutes of consumption)? / My fuel endurance is ... (hours and/or minutes).",
|
|
||||||
"QBE": "I am about to wind in my aerial.",
|
|
||||||
"QBF": "Are you flying in cloud? / I am flying in cloud at ... flight level/altitude ... [and I am ascending (descending) to flight level/altitude ...].",
|
|
||||||
"QBG": "Are you flying above cloud? / I am flying above cloud and at flight level/altitude ...",
|
|
||||||
"QBH": "Are you flying below cloud? / I am flying below cloud and at flight level/altitude ...",
|
|
||||||
"QBI": "Is flight under IFR compulsory at ... (place) [or from ... to ... (place)]? / Flight under IFR is compulsory at ... (place) [or from ... to ... (place)].",
|
|
||||||
"QBJ": "What is the amount, type and height above ... (datum) of the top of the cloud [at ... (position or zone)]? / At ... hours at ... (position or zone) the top of the cloud is: amount ... eights (... type) at ... (figures and units) height above ... (datum).",
|
|
||||||
"QBK": "Are you flying with no cloud in your vicinity? / I am flying with no cloud in my vicinity and at flight level/altitude ...",
|
|
||||||
"QBM": "Has ... sent any messages for me? / Here is the message sent by ... at ... hours.",
|
|
||||||
"QBN": "Are you flying between two layers of cloud? / I am flying between two layers of cloud and at flight level/altitude ...",
|
|
||||||
"QBO": "What is the nearest aerodrome at which flight under VFR is permissible and which would be suitable for my landing? / Flying under VFR is permissible at ... (place) which would be suitable for your landing.",
|
|
||||||
"QBP": "Are you flying in and out of cloud? / I am flying in and out of cloud and at flight level/altitude ...",
|
|
||||||
"QBS": "Ascend (or descend) to ... (figures and units) height above ... (datum) before encountering instrument meteorological conditions or if visibility falls below ... (distance figures and units) and advise.",
|
|
||||||
"QBT": "What is the runway visual range at ... (place)? / The runway visual range at ... (place) at ... hours is ... (distance figures and units).",
|
|
||||||
"QBV": "Have you reached flight level/altitude ... [or ... (area or place)]? / I have reached ... flight level/altitude ... [or ... (area or place)].",
|
|
||||||
"QBX": "Have you left ... flight level/altitude ... [or ... (area or place)]? / I have left ... flight level/altitude ... [or ... (area or place)].",
|
|
||||||
"QBZ": "Report your flying conditions in relation to clouds. / The reply to QBZ ? is given by the appropriate answer form of signals QBF, QBG, QBH, QBK, QBN and QBP.",
|
|
||||||
"QCA": "May I change my flight level/altitude from ... to ... ? / You may change your flight level/altitude from ... to ...",
|
|
||||||
"QCB": "Delay is being caused by ...",
|
|
||||||
"QCE": "When may I expect approach clearance? / Expect approach clearance at ... hours.",
|
|
||||||
"QCF": "Delay indefinite. Expect approach clearance not later than ... hours.",
|
|
||||||
"QCH": "May I taxi to ... (place)? / Cleared to taxi to ... (place).",
|
|
||||||
"QCI": "Make a 360-degree turn immediately (turning to the ...).",
|
|
||||||
"QCS": "My reception on ... frequency has broken down.",
|
|
||||||
"QCX": "What is your full call sign? / My full call sign is ...",
|
|
||||||
"QCY": "I am working on a trailing aerial.",
|
|
||||||
"QDB": "Have you sent message ... to ... ? / I have sent message ... to ...",
|
|
||||||
"QDF": "What is your D-Value at ... (position)?",
|
|
||||||
"QDL": "Do you intend to ask me for a series of bearings? / I intend to ask you for a series of bearings.",
|
|
||||||
"QDM": "Will you indicate the MAGNETIC heading for me to steer towards you (or ...) with no wind? / The MAGNETIC heading for you to steer to reach me (or ...) with no wind was ... degrees (at ... hours).",
|
|
||||||
"QDP": "Will you accept control (or responsibility) of (for) ... now (or at ... hours)? / I will accept control (or responsibility) of (for) ... now (or at ... hours).",
|
|
||||||
"QDR": "What is my MAGNETIC bearing from you (or from ...)? / Your MAGNETIC bearing from me (or from ...) was ... degrees (at ... hours).",
|
|
||||||
"QDT": "Are you flying in visual meteorological condition? / I am flying in visual meteorological condition.",
|
|
||||||
"QDU": "Cancelling my IFR flight.",
|
|
||||||
"QDV": "Are you flying in a flight visibility of less than ... (figures and units)? / I am flying in a flight visibility of less than ... (figures and units) at flight level/altitude ...",
|
|
||||||
"QEA": "May I cross the runway ahead of me? / You may cross the runway ahead of you.",
|
|
||||||
"QEB": "May I turn at the intersection? / Taxi as follows at the intersection ...",
|
|
||||||
"QEC": "May I make a 180-degree turn and return down the runway? / You may make a 180-degree turn and return down the runway.",
|
|
||||||
"QED": "Shall I follow the pilot vehicle? / Follow the pilot vehicle.",
|
|
||||||
"QEF": "Have I reached my parking area? / You have reached your parking area.",
|
|
||||||
"QEG": "May I leave the parking area? / You may leave the parking area.",
|
|
||||||
"QEH": "May I move to the holding position for runway number ... ? / Cleared to the holding position for runway number ...",
|
|
||||||
"QEJ": "May I assume position for take-off? / Cleared to hold at take-off position for runway number ...",
|
|
||||||
"QEK": "Are you ready for immediate take-off? / I am ready for immediate take-off.",
|
|
||||||
"QEL": "May I take-off (and make a ... hand turn after take-off)? / You are cleared to take-off (turn as follows after take-off ...).",
|
|
||||||
"QEM": "What is the condition of the landing surface at ... (place)? / The condition of the landing surface at ... (place) is ...",
|
|
||||||
"QEN": "Shall I hold my position? / Hold your position",
|
|
||||||
"QEO": "Shall I clear the runway (or landing area)? / Clear the runway (or landing area).",
|
|
||||||
"QES": "Is a right-hand circuit in force at ... (place)? / A right-hand circuit is in force at ... (place).",
|
|
||||||
"QFA": "What is the meteorological forecast for ... (flight, route, section of route or zone) for the period ... hours until ... hours? / The meteorological forecast for ... (flight, route, section of route or zone) for the period ... hours until ... hours is ...",
|
|
||||||
"QFB": "The approach/runway lights are out of order.",
|
|
||||||
"QFC": "What is the amount, the type and the height above ... (datum) of the base of the cloud at ... (place, position or zone)? / At ... (place, position or zone) the base of the cloud is ... eighths ... type at ... (figures and units) height above ... (datum).",
|
|
||||||
"QFD": "Is the ... visual beacon [at ... (place)] in operation?",
|
|
||||||
"QFE": "What should I set on the subscale of my altimeter so that the instrument would indicate its height above the reference elevation being used? / If you set the subscale of your altimeter to read ... millibars, the instrument would indicate its height above aerodrome elevation (above threshold, runway number ...).",
|
|
||||||
"QFF": "[At ... (place)] what is the present atmospheric pressure converted to mean sea level in accordance with meteorological practice? / At ... (place) the atmospheric pressure converted to mean sea level in accordance with meteorological practice is (or was determined at ... hours to be) ... millibars.",
|
|
||||||
"QFG": "Am I overhead? / You are overhead.",
|
|
||||||
"QFH": "May I descend below the clouds? / You may descend below the clouds.",
|
|
||||||
"QFI": "Are the aerodrome lights lit? / The aerodrome lights are lit.",
|
|
||||||
"QFL": "Will you send up pyrotechnical lights? / I will send up pyrotechnical lights.",
|
|
||||||
"QFM": "What flight level/altitude should I maintain?",
|
|
||||||
"QFO": "May I land immediately? / You may land immediately.",
|
|
||||||
"QFP": "Will you give me the latest information concerning ... facility [at ... (place)]? / The latest information concerning ... facility [at ... (place)] is as follows ...",
|
|
||||||
"QFQ": "Are the approach and runway lights lit? / The approach and runway lights are lit.",
|
|
||||||
"QFR": "Does my landing gear appear damaged? / Your landing gear appears damaged.",
|
|
||||||
"QFS": "Is the radio facility at ... (place) in operation? / The radio facility at ... (place) is in operation (or will be in operation in ... hours).",
|
|
||||||
"QFT": "Between what heights above ... (datum) has ice formation been observed [at ... (position or zone)]? / Ice formation has been observed at ... (position or zone) in the type of ... and with an accretion rate of ... between ... (figures and units) and ... (figures and units) heights above ... (datum).",
|
|
||||||
"QFU": "What is the magnetic direction (or number) of the runway to be used? / The magnetic direction (or number) of the runway to be used is ...",
|
|
||||||
"QFV": "Are the floodlights switched on? / The floodlights are switched on.",
|
|
||||||
"QFW": "What is the length of the runway in use in ... (units)? / The length of runway ... now in use is ... (figures and units).",
|
|
||||||
"QFX": "I am working (or am going to work) on a fixed aerial.",
|
|
||||||
"QFY": "Please report the present meteorological landing conditions [at ... (place)]. / The present meteorological landing conditions at ... (place) are ...",
|
|
||||||
"QFZ": "What is the aerodrome meteorological forecast for ... (place) for the period ... hours until ... hours? / The aerodrome meteorological forecast for ... (place) for the period ... hours until ... hours is ...",
|
|
||||||
"QGC": "There are obstructions to the ... of ... runway ...",
|
|
||||||
"QGD": "Are there on my track any obstructions whose elevation equals or exceeds my altitude? / There are obstructions on your track ... (figures and units) height above ... (datum).",
|
|
||||||
"QGE": "What is my distance to your station (or to ...)? / Your distance to my station (or to ...) is ... (distance figures and units).",
|
|
||||||
"QGH": "May I land using ... (procedure or facility)? / You may land using ... (procedure or facility).",
|
|
||||||
"QGK": "What track should I make good? / Make good a track from ... (place) on ... degrees ... (true or magnetic).",
|
|
||||||
"QGL": "May I enter the ... (control area or zone) at ... (place)? / You may enter the ... (control area or zone) at ... (place).",
|
|
||||||
"QGM": "Leave the ... (control area or zone).",
|
|
||||||
"QGN": "May I be cleared to land [at ... (place)]? / You are cleared to land [at ... (place)].",
|
|
||||||
"QGO": "Landing is prohibited at ... (place).",
|
|
||||||
"QGP": "What is my number for landing? / You are number ... to land.",
|
|
||||||
"QGQ": "May I hold at ... (place)? / Hold at ... (place) at flight level/altitude ... (datum) and await further clearance.",
|
|
||||||
"QGT": "Fly for ... minutes on a heading what will enable you to maintain a track reciprocal to your present one.",
|
|
||||||
"QGU": "Fly for ... minutes on a magnetic heading of ... degrees.",
|
|
||||||
"QGV": "Do you see me? / Can you see the aerodrome? / Can you see ... (aircraft)? / I see you at ... (cardinal or quadrantal point of direction).",
|
|
||||||
"QGW": "Does my landing gear appear to be down and in place? / Your landing gear appears to be down and in place.",
|
|
||||||
"QGZ": "Hold on ... direction of ... facility.",
|
|
||||||
"QHE": "Will you inform me when you are on ... leg of approach? / I am on ...(leg).. of approach.",
|
|
||||||
"QHG": "May I enter traffic circuit at flight level/altitude ...? / Cleared to enter traffic circuit at flight level/altitude ...",
|
|
||||||
"QHH": "Are you making an emergency landing? / I am making an emergency landing.",
|
|
||||||
"QHI": "Are you (or is ...) ... waterborne / on land? ",
|
|
||||||
"QHQ": "May I make a ... approach [at ... (place)]? / You may make a ... approach [at ... (place)].",
|
|
||||||
"QHZ": "Shall I circle the aerodrome (or go around)? / Circle the aerodrome (or go around).",
|
|
||||||
"QIC": "May I establish communication with ... radio station on ... kHz (or ... MHz.) now (or at ... hours)? / Establish communication with ... radio station on ... kHz. (or MHz.) now (or at ...hours).",
|
|
||||||
"QIF": "What frequency is ... using? / ... is using ... kHz (or ... MHz.).",
|
|
||||||
"QJA": "Is my tape / mark and space reversed? / Your tape / mark and space is reversed.",
|
|
||||||
"QJB": "Will you use radio/cable/telegraph/teletype/telephone/receiver/transmitter/reperforator? / I will use ...",
|
|
||||||
"QJC": "Will you check your transmitter/autohead/perforator/reperfordator/printer/keyboard/antenna? / I will check my ...",
|
|
||||||
"QJD": "Am I transmitting letters/figures? / You are transmitting ...",
|
|
||||||
"QJE": "Is my frequency shift wide/narrow/correct? / Your frequency shift is ... (by ... cycles)",
|
|
||||||
"QJF": "My signal as checked by monitor locally/as radiated is satisfactory.",
|
|
||||||
"QJG": "Shall I revert to automatic relay? / Revert to automatic relay.",
|
|
||||||
"QJH": "Shall I run test tape/sentence? / Run test tape/sentence.",
|
|
||||||
"QJI": "Will you transmit a continuous mark/space? / I am transmitting a continuous mark/space.",
|
|
||||||
"QJK": "Are you receiving continuous mark/space / mark/space bias? / I am receiving a continuous mark/space / mark/space bias.",
|
|
||||||
"QKC": "The sea conditions (at ... position) ... permit alighting but not takeoff / render alihting extremely hazardous.",
|
|
||||||
"QKF": "May I be relieved (at ... hours)? / You may expect to be relieved at ... hours [by aircraft/vessel/callsign/name].",
|
|
||||||
"QKG": "Will relief take place when ... (identification) establishes visual/comms contact with survivors? / Relief will take place when ... (identification) establishes visual/comms contact with survivors.",
|
|
||||||
"QKH": "Report details of the parallel sweep (track) search being (or to be) conducted? --or-- In the parallel sweep (track) search being (or to be) conducted, what is (are) the direction/separation/altitude of sweeps employed in the search pattern? / The parallel sweep (track) search is being (or to be) conducted [with direction of sweeps ... degrees ... (true or magnetic) || with ... (distance figures and units) separation between sweeps || flight level/altitude].",
|
|
||||||
"QKN": "Aircraft plotted (believed to be you) in position ... on track ... degrees at ... hours.",
|
|
||||||
"QKO": "What other units are (or will be) taking part in the operation [... (identification of operation)]? / In the operation [... (identification)], the following units are (or will be) taking part ... (name of units). --or-- ... (name) unit is taking part in operation [... (identification] (with effect from ... hours).",
|
|
||||||
"QKP": "Which pattern of search is being followed? / The search pattern is parallel sweep/square search/creeping line ahead/track crawl/contour search/combined search by aircraft and ship/[other].",
|
|
||||||
"QLB": "Will you monitor ... station and report regarding range, quality, etc.? / I have monitored ... station and report (briefly) as follows ...",
|
|
||||||
"QLE": "What is your expected signal? / The expected signal is low...",
|
|
||||||
"QLF": "Are you sending with your left foot? Try sending with your left foot!",
|
|
||||||
"QLH": "Will you use simultaneous keying on ... frequency and ... frequency? / I will now key simultaneously on ... frequency and ... frequency.",
|
|
||||||
"QLV": "Is the ... radio facility still required? / The ... radio facility is still required.",
|
|
||||||
"QMH": "Shift to transmit and receive on ... kHz (or ... MHz.); if communication is not established within 5 minutes, revert to present frequency.",
|
|
||||||
"QMI": "Report the vertical distribution of cloud [at ... (position or zone)] as observed from your aircraft. / The vertical distribution of cloud as observed from my aircraft at ... hours at ... (position or zone) is : *lowest layer observed* ... eights (... type) with base of ... (figures and units) and tops of ... (figures and units) [*and similarly in sequence for each of the layers observed.] height above ... (datum).",
|
|
||||||
"QMU": "What is the surface temperature at ... (place) and what is the dew point temperature at that place? / The surface temperature at ... (place) at ... hours is ... degrees and the dew point temperature at that time and place is ... degrees.",
|
|
||||||
"QMW": "At ... (position or zone) what is (are) the flight level(s)/altitude(s) ... of the zero Celsius isotherm(s)? / At ... (position or zone) the zero Celsius isotherm(s) is (are) at flight level(s)/altitude(s) ...",
|
|
||||||
"QMX": "What is the air temperature [at ... (position or zone)] (at ... hours) at flight level/altitude ...? / At ... (position or zone) at ... hours the air temperature is ... (degrees and units) at flight level/altitude ... --Note-- Aircraft reporting QMX information will transmit the temperature figures as corrected for airspeed.",
|
|
||||||
"QMZ": "Have you any amendments to the flight forecast in respect of section of route yet to be traversed? / The following amendment(s) should be made to the flight forecast ... [If no amendments, signal QMZ NIL]",
|
|
||||||
"QNE": "What indication will my altimeter give on landing at ... (place) at ... hours, my sub-scale being set to 1013.2 millibars (29.92 inches)? / On landing at ... (place) at ... hours, with your sub-scale being set to 1013.2 millibars (29.92 inches), your altimeter will indicate ... (figures and units).",
|
|
||||||
"QNH": "What should I set on the subscale of my altimeter so that the instrument would indicate its elevation if my aircraft were on the ground at your station? / If you set the subscale of your altimeter to read ... millibars, the instrument would indicate its elevation if your aircraft were on the ground at my station at ... hours. --Note-- When the setting is given in hundredths of inch the abbreviation INS is used to identify the units.",
|
|
||||||
"QNI": "May I join the net? / You may check in...",
|
|
||||||
"QNO": "I am not equipped to give the information (or provide the facility) requested.",
|
|
||||||
"QNR": "I am approaching my point of no return.",
|
|
||||||
"QNT": "What is the maximum speed of the surface wind at ... (place)? / The maximum speed of the surface wind at ... (place) at ... hours is ... (speed figures and units).",
|
|
||||||
"QNY": "What is the present weather and the intensity thereof at ... (place, position or zone)? / The present weather and intensity thereof at ... (place, position or zone) at ... hours is ...",
|
|
||||||
"QOA": "Can you communicate by radiotelegraphy (500 kHz)? / I can communicate by radiotelegraphy (500 kHz).",
|
|
||||||
"QOB": "Can you communicate by radiotelephony (2182 kHz)? / I can communicate by radiotelephony (2182 kHz).",
|
|
||||||
"QOC": "Can you communicate by radiotelephony (channel 16 - frequency 156.80 MHz)? / I can communicate by radiotelephony (channel 16 - frequency 156.80 MHz).",
|
|
||||||
"QOD": "Can you communicate with me in Dutch/English/French/German/Greek/Italian/Japanese/Norwegian/Russian/Spanish? / I can communicate with you in ...",
|
|
||||||
"QOE": "Have you received the safety signal sent by ... (name and/or call sign)? / I have received the safety signal sent by ... (name and/or call sign).",
|
|
||||||
"QOF": "What is the commercial quality of my signals? / The quality of your signals is not commercial/marginally commercial/commercial.",
|
|
||||||
"QOG": "How many tapes have you to send? / I have ... tapes to send.",
|
|
||||||
"QOH": "Shall I send a phasing signal for ... seconds? / Send a phasing signal for ... seconds.",
|
|
||||||
"QOI": "Shall I send my tape? / Send your tape.",
|
|
||||||
"QOJ": "Will you listen on ... kHz (or MHz) for signals of emergency position-indicating radiobeacons? / I am listening on ... kHz (or MHz) for signals of emergency position-indicating radiobeacons.",
|
|
||||||
"QOK": "Have you received the signals of an emergency position-indicating radiobeacon on ... kHz (or MHz)? / I have received the signals of an emergency position-indicating radiobeacon on ... kHz (or MHz).",
|
|
||||||
"QOL": "Is your vessel fitted for reception of selective calls? If so, what is your selective call number or signal? / My vessel is fitted for the reception of selective calls. My selective call number or signal is ...",
|
|
||||||
"QOM": "On what frequencies can your vessel be reached by a selective call? / My vessel can be reached by a selective call on the following frequency/ies ... (periods of time to be added if necessary).",
|
|
||||||
"QOO": "Can you send on any working frequency? / I can send on any working frequency.",
|
|
||||||
"QOT": "Do you hear my call; what is the approximate delay in minutes before we may exchange traffic? / I hear your call; the approximate delay is ... minutes.",
|
|
||||||
"QRA": "What is the name of your vessel (or station)? / The name of my vessel (or station) is ...",
|
|
||||||
"QRB": "How far approximately are you from my station? / The approximate distance between our stations is ... nautical miles (or km).",
|
|
||||||
"QRC": "What is your true bearing? / My true bearing is ____ degrees.",
|
|
||||||
"QRD": "Where are you bound for? / I am bound for ____.",
|
|
||||||
"QRE": "What is your estimated time of arrival at ... (or over ...) (place)? / My estimated time of arrival at ... (or over ...) (place) is ... hours.",
|
|
||||||
"QRF": "Where are you bound from? / I am bound from ____.",
|
|
||||||
"QRG": "Will you tell me my exact frequency (or that of ...)? / Your exact frequency (or that of ...) is ... kHz (or MHz).",
|
|
||||||
"QRH": "Does my frequency vary? / Your frequency varies.",
|
|
||||||
"QRI": "How is the tone of my transmission? / The tone of your transmission is ...",
|
|
||||||
"QRJ": "How many voice contacts do you want to make? / I want to make ... voice contacts.",
|
|
||||||
"QRK": "How do you receive me? / I am receiving (1-5).",
|
|
||||||
"QRL": "Are you busy? / I am busy (or I am busy with ...). Please do not interfere.",
|
|
||||||
"QRM": "Are you being interfered with? / I am being interfered with.",
|
|
||||||
"QRN": "Are the atmospherics strong? / Atmospherics (noise) are very strong.",
|
|
||||||
"QRO": "Shall I increase transmitter power? / Increase transmitter power.",
|
|
||||||
"QRP": "Shall I decrease transmitter power? / Decrease transmitter power.",
|
|
||||||
"QRQ": "Shall I send faster? / Send faster (... wpm)",
|
|
||||||
"QRR": "Are you ready for automatic operation? / I am ready for automatic operation. Send at ... words per minute.",
|
|
||||||
"QRS": "Shall I send more slowly? / Send more slowly (... words per minute).",
|
|
||||||
"QRT": "Shall I stop sending? / Stop sending.",
|
|
||||||
"QRU": "Have you anything for me? / I have nothing for you.",
|
|
||||||
"QRV": "Are you ready? / I am ready.",
|
|
||||||
"QRW": "Shall I inform ... that you are calling him on ... kHz (or MHz)? / Please inform ... that I am calling him on ... kHz (or MHz).",
|
|
||||||
"QRX": "When will you call me again? / I will call you again at ... hours (on ... kHz (or MHz)).",
|
|
||||||
"QRY": "What is my turn? / Your turn is Number ... (or according to any other indication).",
|
|
||||||
"QRZ": "Who is calling me? / You are being called by ... (on ... kHz (or MHz)).",
|
|
||||||
"QSA": "What is the strength of my signals (or those of ...)? / The strength of your signals (or those of ...) is ...",
|
|
||||||
"QSB": "Are my signals fading? / Your signals are fading.",
|
|
||||||
"QSC": "Are you a cargo vessel? --or-- Are you a low traffic ship? / I am a cargo vessel. --or-- I am a low traffic ship.",
|
|
||||||
"QSD": "Is my keying defective? --or-- Are my signals mutilated? / Your keying is defective. --or-- Your signals are mutilated.",
|
|
||||||
"QSE": "What is the estimated drift of the survival craft? / The estimated drift of the survival craft is ... (figures and units).",
|
|
||||||
"QSF": "Have you effected rescue? / I have effected rescue and am proceeding to ... base (with ... persons injured requiring ambulance).",
|
|
||||||
"QSG": "Shall I send ... telegrams at a time? / Send ... telegrams at a time.",
|
|
||||||
"QSH": "Are you able to home on your direction-finding equipment? / I am able to home on my D/F equipment (on station ...).",
|
|
||||||
"QSI": "I have been unable to break in on your transmission. --or-- Will you inform ... (call sign) that I have been unable to break in on his transmission (on ... kHz (or MHz)).",
|
|
||||||
"QSJ": "What is the charge to be collected to ... including your internal charge? / The charge to be collected to ... including my internal charge is ... francs.",
|
|
||||||
"QSK": "Can you hear me between your signals and if so can I break in on your transmission? / I can hear you between my signals; break in on my transmission.",
|
|
||||||
"QSL": "Can you acknowledge receipt? / I am acknowledging receipt.",
|
|
||||||
"QSM": "Shall I repeat the last telegram (message) which I sent you, or some previous telegram (message)? / Repeat the last telegram (message) which you sent me (or telegram(s) (message(s)) numbers(s) ...).",
|
|
||||||
"QSN": "Did you hear me (or ... (call sign)) on ... kHz (or MHz)? / I did hear you (or ... (call sign)) on ... kHz (or MHz).",
|
|
||||||
"QSO": "Can you communicate with ... direct (or by relay)? / I can communicate with ... direct (or by relay through ...).",
|
|
||||||
"QSP": "Will you relay to ... free of charge? / I will relay to ... free of charge.",
|
|
||||||
"QSQ": "Have you a doctor on board (or is ... (name of person) on board)? / I have a doctor on board (or ... (name of person) is on board).",
|
|
||||||
"QSR": "Shall I repeat the call on the calling frequency? / Repeat your call on the calling frequency; did not hear you (or have interference).",
|
|
||||||
"QSS": "What working frequency will you use? / I will use the working frequency ... kHz (or MHz) (in the HF bands normally only the last three figures of the frequency need be given).",
|
|
||||||
"QST": "Here is a broadcast message to all amateurs.",
|
|
||||||
"QSU": "Shall I send or reply on this frequency (or on ... kHz (or MHz)) (with emissions of class ...)? / Send or reply on this frequency (or on ... kHz (or MHz)) (with emissions of class ...).",
|
|
||||||
"QSV": "Shall I send a series of Vs on this frequency (or on ... kHz (or MHz))? / Send a series of Vs on this frequency (or on ... kHz (or MHz)).",
|
|
||||||
"QSW": "Will you send on this frequency (or on ... kHz (or MHz)) (with emissions of class ...)? / I am going to send on this frequency (or on ... kHz (or MHz)) (with emissions of class ...).",
|
|
||||||
"QSX": "Will you listen to ... (call sign(s)) on ... kHz (or MHz)? --or-- Will you listen to ... (call sign(s)) on ... kHz (or MHz), or in the bands ... / channels ... ? / I am listening to ... (call sign(s)) on ... kHz (or MHz). --or-- I am listening to ... (call sign(s)) on ... kHz (or MHz), or in the bands ... / channels ...",
|
|
||||||
"QSY": "Shall I change to transmission on another frequency? / Change to transmission on another frequency (or on ... kHz (or MHz)).",
|
|
||||||
"QSZ": "Shall I send each word or group more than once? / Send each word or group twice (or ... times).",
|
|
||||||
"QTA": "Shall I cancel telegram (or message) number ... ? / Cancel telegram (or message) number ...",
|
|
||||||
"QTB": "Do you agree with my counting of words? / I do not agree with your counting of words; I will repeat the first letter or digit of each word or group.",
|
|
||||||
"QTC": "How many telegrams have you to send? / I have ... telegrams for you (or for ...).",
|
|
||||||
"QTD": "What has the rescue vessel or rescue aircraft recovered? / ... (identification) has recovered ... survivors/wreckage/bodies.",
|
|
||||||
"QTE": "What is my TRUE bearing from you? --or-- What is my TRUE bearing from ... (call sign)? --or-- What is the TRUE bearing of ... (call sign) from ... (call sign)? / Your TRUE bearing from me is ... degrees at ... hours. --or-- Your TRUE bearing from ... (call sign) was ... degrees at ... hours. --or-- The TRUE bearing of ... (call sign) from ... (call sign) was ... degrees at ... hours.",
|
|
||||||
"QTF": "Will you give me the position of my station according to the bearings taken by the direction-finding stations which you control? / The position of your station according to the bearings taken by the D/F stations which I control was ... latitude, ... longitude (or other indication of position), class ... at ... hours.",
|
|
||||||
"QTG": "Will you send two dashes of ten seconds each followed by your call sign (repeated ... times) (on ... kHz (or MHz))? --or-- Will you request ... to send two dashes of ten seconds followed by his call sign (repeated ... times) on ... kHz (or MHz)? / I am going to send two dashes of ten seconds each followed by my call sign (repeated ... times) (on ... kHz (or MHz)). --or-- I have requested ... to send two dashes of ten seconds followed by his call sign (repeated ... times) on ... kHz (or MHz).",
|
|
||||||
"QTHR": "At the registered location; Chiefly British in voice or writing, Historically - the location in the printed Callbook. Modernly - as given in online government records for my callsign",
|
|
||||||
"QTH": "What is your position in latitude and longitude (or according to any other indication)? / My position is ... latitude, ... longitude (or according to any other indication).",
|
|
||||||
"QTI": "What is your TRUE course? / My TRUE course is ... degrees.",
|
|
||||||
"QTJ": "What is your speed? / My speed is ... knots (or ... kilometres per hour or ... statute miles per hour).",
|
|
||||||
"QTK": "What is the speed of your aircraft in relation to the surface of the Earth? / The speed of my aircraft in relation to the surface of the Earth is ... knots (or ... kilometres per hour or ... statute miles per hour).",
|
|
||||||
"QTL": "What is your TRUE heading? / My TRUE heading is ... degrees.",
|
|
||||||
"QTM": "What is your MAGNETIC heading? / My MAGNETIC heading is ... degrees.",
|
|
||||||
"QTN": "At what time did you depart from ... (place)? / I departed from ... (place) at ... hours.",
|
|
||||||
"QTO": "Have you left dock (or port)? --or-- Are you airborne? / I have left dock (or port). --or-- I am airborne.",
|
|
||||||
"QTP": "Are you going to enter dock (or port)? --or-- Are you going to alight (or land)? / I am going to enter dock (or port). --or-- I am going to alight (or land).",
|
|
||||||
"QTQ": "Can you communicate with my station by means of the International Code of Signals (INTERCO)? / I am going to communicate with your station by means of the International Code of Signals (INTERCO).",
|
|
||||||
"QTR": "What is the correct time? / The correct time is ... hours.",
|
|
||||||
"QTS": "Will you send your call sign (and/or name) for ... seconds? / I will send my call sign (and/or name) for ... seconds.",
|
|
||||||
"QTT": "The identification signal which follows is superimposed on another transmission.",
|
|
||||||
"QTU": "What are the hours during which your station is open? / My station is open from ... to ... hours.",
|
|
||||||
"QTV": "Shall I stand guard for you on the frequency of ... kHz (or MHz) (from ... to ... hours)? / Stand guard for me on the frequency of ... kHz (or MHz) (from ... to ... hours).",
|
|
||||||
"QTW": "What is the condition of survivors? / Survivors are in ... condition and urgently need ...",
|
|
||||||
"QTX": "Will you keep your station open for further communication with me until further notice (or until ... hours)? / I will keep my station open for further communication with you until further notice (or until ... hours).",
|
|
||||||
"QTY": "Are you proceeding to the position of incident and if so when do you expect to arrive? / I am proceeding to the position of incident and expect to arrive at ... hours (on ... (date)).",
|
|
||||||
"QTZ": "Are you continuing the search? / I am continuing the search for ... (aircraft, ship, survival craft, survivors or wreckage).",
|
|
||||||
"QUA": "Have you news of ... (call sign)? / Here is news of ... (call sign).",
|
|
||||||
"QUB": "Can you give me in the following order information concerning: the direction in degrees TRUE and speed of the surface wind; visibility; present weather; and amount, type and height of base of cloud above surface elevation at ... (place of observation)? / Here is the information requested: ... (The units used for speed and distances should be indicated.)",
|
|
||||||
"QUC": "What is the number (or other indication) of the last message you received from me (or from ... (call sign))? / The number (or other indication) of the last message I received from you (or from ... (call sign)) is ...",
|
|
||||||
"QUD": "Have you received the urgency signal sent by ... (call sign of mobile station)? / I have received the urgency signal sent by ... (call sign of mobile station) at ... hours.",
|
|
||||||
"QUE": "Can you speak in ... (language), - with interpreter if necessary; if so, on what frequencies? / I can speak in ... (language) on ... kHz (or MHz).",
|
|
||||||
"QUF": "Have you received the distress signal sent by ... (call sign of mobile station)? / I have received the distress signal sent by ... (call sign of mobile station) at ... hours.",
|
|
||||||
"QUG": "Will you be forced to alight (or land)? / I am forced to alight (or land) immediately. --or-- I shall be forced to alight (or land) at ... (position or place) at ... hours.",
|
|
||||||
"QUH": "Will you give me the present barometric pressure at sea level? / The present barometric pressure at sea level is ... (units).",
|
|
||||||
"QUI": "Are your navigation lights working? / My navigation lights are working.",
|
|
||||||
"QUJ": "Will you indicate the TRUE track to reach you (or ...)? / The TRUE track to reach me (or ...) is ... degrees at ... hours.",
|
|
||||||
"QUK": "Can you tell me the condition of the sea observed at ... (place or coordinates)? / The sea at ... (place or coordinates) is ...",
|
|
||||||
"QUL": "Can you tell me the swell observed at ... (place or coordinates)? / The swell at ... (place or coordinates) is ...",
|
|
||||||
"QUM": "May I resume normal working? / Normal working may be resumed.",
|
|
||||||
"QUN": "When directed to all stations: Will vessels in my immediate vicinity ... (or in the vicinity of ... latitude, ... longitude) please indicate their position, TRUE course and speed? --or-- When directed to a single station: please indicate their position, TRUE course and speed? / My position, TRUE course and speed are ...",
|
|
||||||
"QUO": "Shall I search for aircraft/ship/survival craft in the vicinity of ... latitude, ... longitude (or according to any other indication)? / Please search for aircraft/ship/survival craft in the vicinity of ... latitude, ... longitude (or according to any other indication).",
|
|
||||||
"QUP": "Will you indicate your position by searchlight/black smoke trail/pyrotechnic lights? / My position is indicated by searchlight/black smoke trail/pyrotechnic lights.",
|
|
||||||
"QUQ": "Shall I train my searchlight nearly vertical on a cloud, occulting if possible and, if your aircraft is seen, deflect the beam up wind and on the water (or land) to facilitate your landing? / Please train your searchlight on a cloud, occulting if possible and, if my aircraft is seen or heard, deflect the beam up wind and on the water (or land) to facilitate my landing.",
|
|
||||||
"QUR": "Have survivors received survival equipment/been picked up by rescue vessel/been reached by ground rescue party? / Survivors are in possession of survival equipment dropped by ... / have been picked up by rescue vessel/have been reached by ground rescue party.",
|
|
||||||
"QUS": "Have you sighted survivors or wreckage? / If so, in what position? / Have sighted ... survivors in water/survivors on rafts/wreckage in position ... latitude, ... longitude (or according to any other indication).",
|
|
||||||
"QUT": "Is position of incident marked? / Position of incident is marked by flame or smoke float/sea marker/sea marker dye/... (specify other marking).",
|
|
||||||
"QUU": "Shall I home ship or aircraft to my position? / Home ship or aircraft ... (call sign) ... to your position by transmitting your call sign and long dashes on ... kHz (or MHz)/by transmitting on ... kHz (or MHz) TRUE track to reach you.",
|
|
||||||
"QUW": "Are you in the search area designated as ... (designator or latitude and longitude)? / I am in the ... (designation) search area.",
|
|
||||||
"QUX": "Do you have any navigational warnings or gale warnings in force? / I have the following navigational warning(s) or gale warning(s) in force: ...",
|
|
||||||
"QUY": "Is position of survival craft marked? / Position of survival craft was marked at ... hours by flame or smoke float/sea marker/sea marker dye/... (specify other marking).",
|
|
||||||
"QUZ": "May I resume restricted working? / Distress phase still in force; restricted working may be resumed.",
|
|
||||||
"QZZ": "Daily key change about to take place (German WWII usage)."}
|
|
59
resources/study.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"""
|
||||||
|
A listing of hamstudy command resources
|
||||||
|
---
|
||||||
|
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
pool_names = {
|
||||||
|
"us": {
|
||||||
|
"technician": "E2",
|
||||||
|
"tech": "E2",
|
||||||
|
"t": "E2",
|
||||||
|
"general": "E3",
|
||||||
|
"gen": "E3",
|
||||||
|
"g": "E3",
|
||||||
|
"extra": "E4",
|
||||||
|
"e": "E4",
|
||||||
|
},
|
||||||
|
"ca": {
|
||||||
|
"basic": "CA_B",
|
||||||
|
"b": "CA_B",
|
||||||
|
"advanced": "CA_A",
|
||||||
|
"adv": "CA_A",
|
||||||
|
"a": "CA_A",
|
||||||
|
"basic_fr": "CA_FB",
|
||||||
|
"b_fr": "CA_FB",
|
||||||
|
"base": "CA_FB",
|
||||||
|
"advanced_fr": "CA_FS",
|
||||||
|
"adv_fr": "CA_FS",
|
||||||
|
"a_fr": "CA_FS",
|
||||||
|
"supérieure": "CA_FS",
|
||||||
|
"superieure": "CA_FS",
|
||||||
|
"s": "CA_FS",
|
||||||
|
},
|
||||||
|
"us_c": {
|
||||||
|
"c1": "C1",
|
||||||
|
"comm1": "C1",
|
||||||
|
"c3": "C3",
|
||||||
|
"comm3": "C3",
|
||||||
|
"c6": "C6",
|
||||||
|
"comm6": "C6",
|
||||||
|
"c7": "C7",
|
||||||
|
"comm7": "C7",
|
||||||
|
"c7r": "C7R",
|
||||||
|
"comm7r": "C7R",
|
||||||
|
"c8": "C8",
|
||||||
|
"comm8": "C8",
|
||||||
|
"c9": "C9",
|
||||||
|
"comm9": "C9",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pool_emojis = {
|
||||||
|
"us": "🇺🇸",
|
||||||
|
"ca": "🇨🇦",
|
||||||
|
"us_c": "🇺🇸 🏢",
|
||||||
|
}
|
45402
resources/words
12
run.sh
@ -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.7'
|
# 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.7'
|
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]"
|
||||||
|
@ -15,7 +15,10 @@ Settings and options for the bot.
|
|||||||
|
|
||||||
# The prefix for the bot (str). Define a list of stings for multiple prefixes.
|
# The prefix for the bot (str). Define a list of stings for multiple prefixes.
|
||||||
# ie: `["?", "!", "pls "]`
|
# ie: `["?", "!", "pls "]`
|
||||||
prefix = "?"
|
prefix = ["? ", "?"]
|
||||||
|
|
||||||
|
# The prefix to use for display purposes (ex: status message).
|
||||||
|
display_prefix = "?"
|
||||||
|
|
||||||
# Whether the bot should print full stacktraces for normal exceptions: `True`,
|
# Whether the bot should print full stacktraces for normal exceptions: `True`,
|
||||||
# or be nice and only print small messages: `False` (the default).
|
# or be nice and only print small messages: `False` (the default).
|
||||||
@ -24,10 +27,42 @@ debug = False
|
|||||||
# A tuple of user IDs that should be considered "bot owners".
|
# A tuple of user IDs that should be considered "bot owners".
|
||||||
# * Those users will have full control over the bot.
|
# * Those users will have full control over the bot.
|
||||||
# ! This MUST be a tuple of integers. Single element tuple: `(123,)`
|
# ! This MUST be a tuple of integers. Single element tuple: `(123,)`
|
||||||
owners_uids = (200102491231092736,)
|
owners_uids = (200102491231092736, 564766093051166729)
|
||||||
|
|
||||||
# The extensions to load when running the bot.
|
# The extensions to load when running the bot.
|
||||||
exts = ['ae7q', 'base', 'fun', 'grid', 'ham', 'image', 'lookup', 'morse', 'qrz', 'study', 'weather']
|
exts = [
|
||||||
|
"base",
|
||||||
|
"ae7q",
|
||||||
|
"callsign",
|
||||||
|
"codes",
|
||||||
|
"contests",
|
||||||
|
"dbconv",
|
||||||
|
"dxcc",
|
||||||
|
"fun",
|
||||||
|
"grid",
|
||||||
|
"image",
|
||||||
|
"land_weather",
|
||||||
|
"morse",
|
||||||
|
"prefixes",
|
||||||
|
"propagation",
|
||||||
|
"study",
|
||||||
|
"tex",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
# URL to the resources
|
||||||
|
resources_url = "https://qrmresources.miaow.io/resources/"
|
||||||
|
|
||||||
|
# If True (default): when doing QRZ callsign lookups, show the nickname in place of the first name, if it exists
|
||||||
|
# if False: use QRZ's default name format
|
||||||
|
qrz_only_nickname = True
|
||||||
|
|
||||||
|
# enable a command that provides a link to add the bot to a server
|
||||||
|
enable_invite_cmd = True
|
||||||
|
|
||||||
|
# the default permissions for the bot, to be included in the invite link for ?invite
|
||||||
|
# this probably does not need to be changed
|
||||||
|
invite_perms = 67488832
|
||||||
|
|
||||||
# Either "time", "random", or "fixed" (first item in statuses)
|
# Either "time", "random", or "fixed" (first item in statuses)
|
||||||
status_mode = "fixed"
|
status_mode = "fixed"
|
||||||
@ -37,18 +72,24 @@ statuses = ["with lids on the air", "with fire"]
|
|||||||
|
|
||||||
# Timezone for the status (string)
|
# Timezone for the status (string)
|
||||||
# See https://pythonhosted.org/pytz/ for more info
|
# See https://pythonhosted.org/pytz/ for more info
|
||||||
status_tz = 'US/Eastern'
|
status_tz = "US/Eastern"
|
||||||
|
|
||||||
# The text to put in the "playing" status, with start and stop times
|
# The text to put in the "playing" status, with start and stop times
|
||||||
time_statuses = [('with lids on 3.840', (00, 00), (6, 00)),
|
time_statuses = [("with lids on 3.840", (00, 00), (6, 00)),
|
||||||
('with lids on 7.200', (6, 00), (10, 00)),
|
("with lids on 7.200", (6, 00), (10, 00)),
|
||||||
('with lids on 14.313', (10, 00), (18, 00)),
|
("with lids on 14.313", (10, 00), (18, 00)),
|
||||||
('with lids on 7.200', (18, 00), (20, 00)),
|
("with lids on 7.200", (18, 00), (20, 00)),
|
||||||
('with lids on 3.840', (20, 00), (23, 59))]
|
("with lids on 3.840", (20, 00), (23, 59))]
|
||||||
|
|
||||||
|
# append " | {display_prefix}help" to the Discord playing status
|
||||||
|
show_help = False
|
||||||
|
|
||||||
# Emoji IDs and keywords for emoji reactions
|
# Emoji IDs and keywords for emoji reactions
|
||||||
# Use the format {emoji_id (int): ('tuple', 'of', 'lowercase', 'keywords')}
|
# Use the format {emoji_id (int): ("tuple", "of", "lowercase", "keywords")}
|
||||||
msg_reacts = {}
|
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/"
|
||||||
|
3
utils/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Various utilities for the bot.
|
||||||
|
"""
|
15
utils/connector.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"""
|
||||||
|
Wrapper to handle aiohttp connector creation.
|
||||||
|
---
|
||||||
|
Copyright (C) 2020-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
|
||||||
|
async def new_connector(*args, **kwargs) -> aiohttp.TCPConnector:
|
||||||
|
"""*Yes, it's just a coro to instantiate a class.*"""
|
||||||
|
return aiohttp.TCPConnector(*args, **kwargs)
|
88
utils/resources_manager.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
"""
|
||||||
|
Resources manager for qrm2.
|
||||||
|
---
|
||||||
|
Copyright (C) 2021-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from utils.resources_models import Index
|
||||||
|
|
||||||
|
|
||||||
|
class ResourcesManager:
|
||||||
|
def __init__(self, basedir: Path, url: str, versions: dict):
|
||||||
|
self.basedir = basedir
|
||||||
|
self.url = url
|
||||||
|
self.versions = versions
|
||||||
|
self.index: Index = self.sync_start(basedir)
|
||||||
|
|
||||||
|
def parse_index(self, index: str):
|
||||||
|
"""Parses the index."""
|
||||||
|
return Index.model_validate_json(index)
|
||||||
|
|
||||||
|
def sync_fetch(self, filepath: str):
|
||||||
|
"""Fetches files in sync mode."""
|
||||||
|
self.print_msg(f"Fetching {filepath}", "sync")
|
||||||
|
resp = httpx.get(self.url + filepath)
|
||||||
|
resp.raise_for_status()
|
||||||
|
r = resp.content
|
||||||
|
resp.close()
|
||||||
|
return r
|
||||||
|
|
||||||
|
def sync_start(self, basedir: Path) -> Index:
|
||||||
|
"""Takes cares of constructing the local resources repository and initialising the RM."""
|
||||||
|
self.print_msg("Initialising ResourceManager", "sync")
|
||||||
|
self.ensure_dir(basedir)
|
||||||
|
try:
|
||||||
|
raw = self.sync_fetch("index.json")
|
||||||
|
new_index: Index = self.parse_index(raw)
|
||||||
|
with (basedir / "index.json").open("wb") as file:
|
||||||
|
file.write(raw)
|
||||||
|
except (httpx.RequestError, OSError) as ex:
|
||||||
|
self.print_msg(f"There was an issue fetching the index: {ex.__class__.__name__}: {ex}", "sync")
|
||||||
|
if (basedir / "index.json").exists():
|
||||||
|
self.print_msg("Old file exist, using old resources", "fallback")
|
||||||
|
with (basedir / "index.json").open("r") as file:
|
||||||
|
old_index = self.parse_index(file.read())
|
||||||
|
for res, ver in self.versions.items():
|
||||||
|
for file in old_index[res][ver]:
|
||||||
|
if not (basedir / file.filename).exists():
|
||||||
|
self.print_msg(f"Error: {file.filename} is missing", "fallback")
|
||||||
|
raise SystemExit(1)
|
||||||
|
return old_index
|
||||||
|
raise SystemExit(1)
|
||||||
|
for res, ver in self.versions.items():
|
||||||
|
for file in new_index[res][ver]:
|
||||||
|
try:
|
||||||
|
with (basedir / file.filename).open("wb") as f:
|
||||||
|
f.write(self.sync_fetch(file.filename))
|
||||||
|
except (httpx.RequestError, OSError) as ex:
|
||||||
|
ex_cls = ex.__class__.__name__
|
||||||
|
self.print_msg(f"There was an issue fetching {file.filename}: {ex_cls}: {ex}", "sync")
|
||||||
|
if not (basedir / file.filename).exists():
|
||||||
|
raise SystemExit(1)
|
||||||
|
self.print_msg("Old file exists, using it", "fallback")
|
||||||
|
return new_index
|
||||||
|
|
||||||
|
def ensure_dir(self, basedir: Path) -> bool:
|
||||||
|
"""Ensures that the resources/ directory is present. Creates as necessary."""
|
||||||
|
if basedir.is_file():
|
||||||
|
raise FileExistsError(f"'{basedir}' is not a directory!")
|
||||||
|
if not basedir.exists():
|
||||||
|
self.print_msg("Creating resources directory")
|
||||||
|
basedir.mkdir()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def print_msg(self, msg: str, mode: str = None):
|
||||||
|
"""Formats and prints messages for the resources manager."""
|
||||||
|
message = "RM: "
|
||||||
|
message += msg
|
||||||
|
if mode:
|
||||||
|
message += f" ({mode})"
|
||||||
|
print(message)
|
61
utils/resources_models.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
Resource index models for qrm2.
|
||||||
|
---
|
||||||
|
Copyright (C) 2021-2023 classabbyamp, 0x5c
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, RootModel
|
||||||
|
|
||||||
|
|
||||||
|
class File(BaseModel):
|
||||||
|
filename: str
|
||||||
|
hash: str
|
||||||
|
|
||||||
|
# For some reason those were not the same???
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return repr(self)
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(RootModel, Mapping):
|
||||||
|
root: dict[str, list[File]]
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> list[File]:
|
||||||
|
return self.root[key]
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.root)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self.root)
|
||||||
|
|
||||||
|
# For some reason those were not the same???
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return repr(self)
|
||||||
|
|
||||||
|
# Make the repr more logical (despite the technical inaccuracy)
|
||||||
|
def __repr_args__(self):
|
||||||
|
return self.root.items()
|
||||||
|
|
||||||
|
|
||||||
|
class Index(BaseModel, Mapping):
|
||||||
|
last_updated: datetime
|
||||||
|
resources: dict[str, Resource]
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> Resource:
|
||||||
|
return self.resources[key]
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.resources)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self.resources)
|
||||||
|
|
||||||
|
# For some reason those were not the same???
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return repr(self)
|