Compare commits
255 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 |
@ -11,3 +11,5 @@ docker-compose.yml
|
||||
Makefile
|
||||
|
||||
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
|
||||
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
flake8_py3:
|
||||
flake8:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/setup-python@v1
|
||||
- name: Setup Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: "3.9"
|
||||
architecture: x64
|
||||
- name: Install flake8
|
||||
run: pip install flake8
|
||||
- name: Run flake8
|
||||
uses: suo/flake8-github-action@releases/v1
|
||||
with:
|
||||
checkName: 'flake8_py3' # NOTE: this needs to be the same as the job name
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: flake8 --format='::error title=flake8,file=%(path)s,line=%(row)d,col=%(col)d::[%(code)s] %(text)s'
|
||||
|
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/
|
||||
/devenv/
|
||||
/data/
|
||||
/data/plugins/
|
||||
|
||||
/docker-compose.yml
|
||||
|
||||
.vscode/
|
||||
|
||||
|
||||
#########################################################
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
203
CHANGELOG.md
@ -7,6 +7,185 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [2.9.2] - 2023-12-15
|
||||
### Added
|
||||
- `?drapmap` command to display NOAA D Region Absorption Predictions map.
|
||||
- Support for the new username format.
|
||||
### Fixed
|
||||
- Issue where `?solarweather` would not show a picture (#474).
|
||||
- Issue where `?metar` and `?taf` failed to fetch data (#475).
|
||||
|
||||
|
||||
## [2.9.1] - 2023-01-29
|
||||
### Fixed
|
||||
- Issue where embeds would not work for users without avatars (#467).
|
||||
- Issue where embeds would show the wrong timezone.
|
||||
- Several issues with `?call` caused by issues in a library (#466).
|
||||
|
||||
|
||||
## [2.9.0] - 2023-01-13
|
||||
### Changed
|
||||
- Migrated to Pycord.
|
||||
### Removed
|
||||
- Long-deprecated aliases for `?solarweather`.
|
||||
### Fixed
|
||||
- Issue where ?hamstudy would not work in direct messages (#442).
|
||||
- Issue where `?solarweather` would not show a picture (#461).
|
||||
|
||||
|
||||
## [2.8.0] - 2022-06-24
|
||||
### Removed
|
||||
- `?ae7q` command (#448).
|
||||
|
||||
|
||||
## [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.
|
||||
@ -76,7 +255,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
## 1.0.0 - 2019-07-31 [YANKED]
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.2.0...HEAD
|
||||
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.9.2...HEAD
|
||||
[2.9.2]: https://github.com/miaowware/qrm2/releases/tag/v2.9.2
|
||||
[2.9.1]: https://github.com/miaowware/qrm2/releases/tag/v2.9.1
|
||||
[2.9.0]: https://github.com/miaowware/qrm2/releases/tag/v2.9.0
|
||||
[2.8.0]: https://github.com/miaowware/qrm2/releases/tag/v2.8.0
|
||||
[2.7.6]: https://github.com/miaowware/qrm2/releases/tag/v2.7.6
|
||||
[2.7.5]: https://github.com/miaowware/qrm2/releases/tag/v2.7.5
|
||||
[2.7.4]: https://github.com/miaowware/qrm2/releases/tag/v2.7.4
|
||||
[2.7.3]: https://github.com/miaowware/qrm2/releases/tag/v2.7.3
|
||||
[2.7.2]: https://github.com/miaowware/qrm2/releases/tag/v2.7.2
|
||||
[2.7.1]: https://github.com/miaowware/qrm2/releases/tag/v2.7.1
|
||||
[2.7.0]: https://github.com/miaowware/qrm2/releases/tag/v2.7.0
|
||||
[2.6.0]: https://github.com/miaowware/qrm2/releases/tag/v2.6.0
|
||||
[2.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
|
||||
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 \
|
||||
echo "**** install build packages ****" && \
|
||||
apk add --no-cache --virtual=build-dependencies \
|
||||
g++ \
|
||||
git \
|
||||
gcc \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
libressl-dev \
|
||||
python3-dev && \
|
||||
echo "**** install runtime packages ****" && \
|
||||
apk add --no-cache \
|
||||
libressl \
|
||||
py3-lxml \
|
||||
py3-pip \
|
||||
python3 && \
|
||||
echo "**** update system ****" && \
|
||||
xbps-install -Suy xbps -R ${REPOSITORY} && \
|
||||
xbps-install -uy -R ${REPOSITORY} && \
|
||||
echo "**** install system packages ****" && \
|
||||
xbps-install -y -R ${REPOSITORY} ${PKGS} python3.11 && \
|
||||
echo "**** install pip packages ****" && \
|
||||
pip3 install -U pip setuptools wheel && \
|
||||
pip3 install -r requirements.txt && \
|
||||
python3.11 -m venv botenv && \
|
||||
botenv/bin/pip install -U pip setuptools wheel && \
|
||||
botenv/bin/pip install -r requirements.txt && \
|
||||
echo "**** clean up ****" && \
|
||||
apk del --purge \
|
||||
build-dependencies && \
|
||||
rm -rf \
|
||||
/root/.cache \
|
||||
/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
|
||||
# at en environment level or as 'make' arguments.
|
||||
BOTENV ?= botenv
|
||||
PYTHON_BIN ?= python3.7
|
||||
PYTHON_BIN ?= python3.11
|
||||
PIP_OUTPUT ?= -q
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ help:
|
||||
|
||||
# Main install target
|
||||
.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
|
||||
$(BOTENV)/success:
|
||||
@ -55,6 +55,11 @@ data:
|
||||
@echo "\033[34;1m--> Creating ./data ...\033[0m"
|
||||
@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
|
||||
data/options.py data/keys.py: ./data
|
||||
@echo "\033[34;1m--> Copying template for ./$@ ...\033[0m"
|
||||
@ -71,6 +76,17 @@ clean:
|
||||
|
||||
|
||||
### 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 ###
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 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)
|
||||
- [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'
|
||||
services:
|
||||
qrm2:
|
||||
image: "classabbyamp/discord-qrm2:latest"
|
||||
image: "ghcr.io/miaowware/qrm2:latest"
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- "./data:/app/data:rw"
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
> 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:
|
||||
qrm2:
|
||||
build: .
|
||||
image: "discord-qrm2:local-latest"
|
||||
image: "qrm2:local-latest"
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- "./data:/app/data:rw"
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
```
|
||||
|
||||
3. Create a subdirectory named `data`.
|
||||
@ -75,10 +71,10 @@ This is the easiest method to run the bot with modifications.
|
||||
|
||||
```none
|
||||
$ 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:
|
||||
|
||||
```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:
|
||||
- `discord-qrm2:local-latest` if you are building your own.
|
||||
- `classabbyamp/discord-qrm2:latest` if you want to use the prebuilt image.
|
||||
- `qrm2:local-latest` if you are building your own.
|
||||
- `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
|
||||
|
||||
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).
|
||||
|
||||
Install `libcairo` and `libjpeg` (package names may vary by distro or OS). Then run:
|
||||
|
||||
```
|
||||
$ make install
|
||||
```
|
||||
@ -26,9 +30,15 @@ Run. For more information on options, see the [quick-bot-no-pain run.sh document
|
||||
$ 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 (C) 2019-2020 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||
|
||||
This program is released under the terms of the GNU General Public License,
|
||||
version 2. See `COPYING` for full license text.
|
||||
This program is released under the terms of the *Québec Free and Open-Source Licence – Strong Reciprocity (LiLiQ-R+)*, version 1.1.
|
||||
See [`LICENCE`](LICENCE) for full license text (Français / English).
|
||||
|
73
common.py
@ -1,30 +1,33 @@
|
||||
"""
|
||||
Common tools for the bot.
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||
"""
|
||||
|
||||
|
||||
import collections
|
||||
import enum
|
||||
import json
|
||||
import re
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Union
|
||||
|
||||
import aiohttp
|
||||
import httpx
|
||||
|
||||
import discord
|
||||
import discord.ext.commands as commands
|
||||
from discord import Emoji, PartialEmoji
|
||||
|
||||
import data.options as opt
|
||||
|
||||
|
||||
__all__ = ["colours", "cat", "emojis", "paths", "ImageMetadata", "ImagesGroup",
|
||||
__all__ = ["colours", "BoltCats", "Cats", "emojis", "paths", "ImageMetadata", "ImagesGroup",
|
||||
"embed_factory", "error_embed_factory", "add_react", "check_if_owner"]
|
||||
|
||||
|
||||
@ -34,18 +37,28 @@ colours = SimpleNamespace(
|
||||
good=0x43B581,
|
||||
neutral=0x7289DA,
|
||||
bad=0xF04747,
|
||||
timeout=0xF26522,
|
||||
)
|
||||
|
||||
|
||||
class BoltCats(enum.Enum):
|
||||
OTHER = "Other"
|
||||
INFO = "Bot Information"
|
||||
ADMIN = "Bot Control"
|
||||
|
||||
|
||||
# meow
|
||||
cat = SimpleNamespace(
|
||||
lookup="Information Lookup",
|
||||
fun="Fun",
|
||||
maps="Mapping",
|
||||
ref="Reference",
|
||||
study="Exam Study",
|
||||
weather="Land and Space Weather",
|
||||
admin="Bot Control",
|
||||
)
|
||||
class Cats(enum.Enum):
|
||||
CALC = "Calculators"
|
||||
CODES = "Code References and Tools"
|
||||
FUN = "Fun"
|
||||
LOOKUP = "Information Lookup"
|
||||
REF = "Reference"
|
||||
STUDY = "Exam Study"
|
||||
TIME = "Time and Time Zones"
|
||||
UTILS = "Utilities"
|
||||
WEATHER = "Land and Space Weather"
|
||||
|
||||
|
||||
emojis = SimpleNamespace(
|
||||
check_mark="✅",
|
||||
@ -54,22 +67,24 @@ emojis = SimpleNamespace(
|
||||
question="❓",
|
||||
no_entry="⛔",
|
||||
bangbang="‼️",
|
||||
clock="🕗",
|
||||
stopwatch="⏱",
|
||||
a="🇦",
|
||||
b="🇧",
|
||||
c="🇨",
|
||||
d="🇩",
|
||||
e="🇪",
|
||||
)
|
||||
|
||||
paths = SimpleNamespace(
|
||||
data=Path("./data/"),
|
||||
resources=Path("./resources/"),
|
||||
bandcharts=Path("./resources/img/bandcharts/"),
|
||||
maps=Path("./resources/img/maps/"),
|
||||
resources=Path("./data/resources/"),
|
||||
)
|
||||
|
||||
|
||||
# --- Classes ---
|
||||
|
||||
|
||||
class ImageMetadata:
|
||||
"""Represents the metadata of a single image."""
|
||||
def __init__(self, metadata: list):
|
||||
@ -96,7 +111,7 @@ class ImagesGroup(collections.abc.Mapping):
|
||||
def __len__(self):
|
||||
return len(self._images)
|
||||
|
||||
def __getitem__(self, key: str):
|
||||
def __getitem__(self, key: str) -> ImageMetadata:
|
||||
return self._images[key]
|
||||
|
||||
def __iter__(self):
|
||||
@ -111,12 +126,16 @@ class ImagesGroup(collections.abc.Mapping):
|
||||
|
||||
class BotHTTPError(Exception):
|
||||
"""Raised whan a requests fails (status != 200) in a command."""
|
||||
def __init__(self, response: aiohttp.ClientResponse):
|
||||
msg = f"Request failed: {response.status} {response.reason}"
|
||||
def __init__(self, response: aiohttp.ClientResponse | httpx.Response):
|
||||
if isinstance(response, aiohttp.ClientResponse):
|
||||
self.status = response.status
|
||||
self.reason = response.reason
|
||||
else:
|
||||
self.status = response.status_code
|
||||
self.reason = response.reason_phrase
|
||||
msg = f"Request failed: {self.status} {self.reason}"
|
||||
super().__init__(msg)
|
||||
self.response = response
|
||||
self.status = response.status
|
||||
self.reason = response.reason
|
||||
|
||||
|
||||
# --- Converters ---
|
||||
@ -146,8 +165,9 @@ class GlobalChannelConverter(commands.IDConverter):
|
||||
|
||||
def embed_factory(ctx: commands.Context) -> discord.Embed:
|
||||
"""Creates an embed with neutral colour and standard footer."""
|
||||
embed = discord.Embed(timestamp=datetime.utcnow(), colour=colours.neutral)
|
||||
embed.set_footer(text=ctx.author, icon_url=str(ctx.author.avatar_url))
|
||||
embed = discord.Embed(timestamp=datetime.now(timezone.utc), colour=colours.neutral)
|
||||
if ctx.author:
|
||||
embed.set_footer(text=str(ctx.author), icon_url=str(ctx.author.display_avatar))
|
||||
return embed
|
||||
|
||||
|
||||
@ -164,11 +184,12 @@ def error_embed_factory(ctx: commands.Context, exception: Exception, debug_mode:
|
||||
return embed
|
||||
|
||||
|
||||
async def add_react(msg: discord.Message, react: str):
|
||||
async def add_react(msg: discord.Message, react: Union[Emoji, PartialEmoji, str]):
|
||||
try:
|
||||
await msg.add_reaction(react)
|
||||
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 ---
|
||||
|
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.
|
@ -20,5 +20,5 @@ Used for grouping info such as name, description, source, and such.
|
||||
| `name` | The name of the file. | `Canada`, `ITU Zones` |
|
||||
| `long_name` | The long name (title) of the file. | `Worldwide map of ITU Zones` |
|
||||
| `description` | The description accompanying the file. | `Full radio allocations chart for all services.` |
|
||||
| `source` | The source of the file. | `Instituto Federal de Telecomunicaciones (IFT)` |
|
||||
| `source` | The source of the file. | `Instituto Federal de Telecomunicaciones (IFT)` |
|
||||
| `emoji` | A Unicode emoji associated with the file. | `📻`, `🇨🇦` |
|
||||
|
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
|
394
exts/ae7q.py
@ -1,396 +1,28 @@
|
||||
"""
|
||||
ae7q extension for qrm
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||
"""
|
||||
|
||||
|
||||
# Test callsigns:
|
||||
# KN8U: active, restricted
|
||||
# AB2EE: expired, restricted
|
||||
# KE8FGB: assigned once, no restrictions
|
||||
# KV4AAA: unassigned, no records
|
||||
# KC4USA: reserved, no call history, *but* has application history
|
||||
|
||||
|
||||
import aiohttp
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
import discord.ext.commands as commands
|
||||
|
||||
import common as cmn
|
||||
from common import embed_factory, colours
|
||||
|
||||
|
||||
class AE7QCog(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
|
||||
|
||||
@commands.group(name="ae7q", aliases=["ae"], category=cmn.cat.lookup)
|
||||
async def _ae7q_lookup(self, ctx: commands.Context):
|
||||
"""Looks up a callsign, FRN, or Licensee ID on [ae7q.com](http://ae7q.com/)."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help(ctx.command)
|
||||
|
||||
@_ae7q_lookup.command(name="call", aliases=["c"], category=cmn.cat.lookup)
|
||||
async def _ae7q_call(self, ctx: commands.Context, callsign: str):
|
||||
"""Looks up the history of a callsign on [ae7q.com](http://ae7q.com/)."""
|
||||
with ctx.typing():
|
||||
callsign = callsign.upper()
|
||||
desc = ""
|
||||
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
|
||||
embed = cmn.embed_factory(ctx)
|
||||
|
||||
async with self.session.get(base_url + callsign) as resp:
|
||||
if resp.status != 200:
|
||||
raise cmn.BotHTTPError(resp)
|
||||
page = await resp.text()
|
||||
|
||||
soup = BeautifulSoup(page, features="html.parser")
|
||||
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
|
||||
|
||||
table = tables[0]
|
||||
|
||||
# find the first table in the page, and use it to make a description
|
||||
if len(table[0]) == 1:
|
||||
for row in table:
|
||||
desc += " ".join(row.getText().split())
|
||||
desc += "\n"
|
||||
desc = desc.replace(callsign, f"`{callsign}`")
|
||||
table = tables[1]
|
||||
|
||||
table_headers = table[0].find_all("th")
|
||||
first_header = "".join(table_headers[0].strings) if len(table_headers) > 0 else None
|
||||
|
||||
# catch if the wrong table was selected
|
||||
if first_header is None or first_header != "Entity Name":
|
||||
embed.title = f"AE7Q History for {callsign}"
|
||||
embed.colour = cmn.colours.bad
|
||||
embed.url = base_url + callsign
|
||||
embed.description = desc
|
||||
embed.description += f"\nNo records found for `{callsign}`"
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
table = await process_table(table[1:])
|
||||
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"AE7Q History for {callsign}"
|
||||
embed.colour = cmn.colours.good
|
||||
embed.url = base_url + callsign
|
||||
|
||||
# add the first three rows of the table to the embed
|
||||
for row in table[0:3]:
|
||||
header = f"**{row[0]}** ({row[1]})" # **Name** (Applicant Type)
|
||||
body = (f"Class: *{row[2]}*\n"
|
||||
f"Region: *{row[3]}*\n"
|
||||
f"Status: *{row[4]}*\n"
|
||||
f"Granted: *{row[5]}*\n"
|
||||
f"Effective: *{row[6]}*\n"
|
||||
f"Cancelled: *{row[7]}*\n"
|
||||
f"Expires: *{row[8]}*")
|
||||
embed.add_field(name=header, value=body, inline=False)
|
||||
|
||||
if len(table) > 3:
|
||||
desc += f"\nRecords 1 to 3 of {len(table)}. See ae7q.com for more..."
|
||||
|
||||
embed.description = desc
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@_ae7q_lookup.command(name="trustee", aliases=["t"], category=cmn.cat.lookup)
|
||||
async def _ae7q_trustee(self, ctx: commands.Context, callsign: str):
|
||||
"""Looks up the licenses for which a licensee is trustee on [ae7q.com](http://ae7q.com/)."""
|
||||
with ctx.typing():
|
||||
callsign = callsign.upper()
|
||||
desc = ""
|
||||
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
|
||||
embed = cmn.embed_factory(ctx)
|
||||
|
||||
async with self.session.get(base_url + callsign) as resp:
|
||||
if resp.status != 200:
|
||||
raise cmn.BotHTTPError(resp)
|
||||
page = await resp.text()
|
||||
|
||||
soup = BeautifulSoup(page, features="html.parser")
|
||||
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
|
||||
|
||||
try:
|
||||
table = tables[2] if len(tables[0][0]) == 1 else tables[1]
|
||||
except IndexError:
|
||||
embed.title = f"AE7Q Trustee History for {callsign}"
|
||||
embed.colour = cmn.colours.bad
|
||||
embed.url = base_url + callsign
|
||||
embed.description = desc
|
||||
embed.description += f"\nNo records found for `{callsign}`"
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
table_headers = table[0].find_all("th")
|
||||
first_header = "".join(table_headers[0].strings) if len(table_headers) > 0 else None
|
||||
|
||||
# catch if the wrong table was selected
|
||||
if first_header is None or not first_header.startswith("With"):
|
||||
embed.title = f"AE7Q Trustee History for {callsign}"
|
||||
embed.colour = cmn.colours.bad
|
||||
embed.url = base_url + callsign
|
||||
embed.description = desc
|
||||
embed.description += f"\nNo records found for `{callsign}`"
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
table = await process_table(table[2:])
|
||||
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"AE7Q Trustee History for {callsign}"
|
||||
embed.colour = cmn.colours.good
|
||||
embed.url = base_url + callsign
|
||||
|
||||
# add the first three rows of the table to the embed
|
||||
for row in table[0:3]:
|
||||
header = f"**{row[0]}** ({row[3]})" # **Name** (Applicant Type)
|
||||
body = (f"Name: *{row[2]}*\n"
|
||||
f"Region: *{row[1]}*\n"
|
||||
f"Status: *{row[4]}*\n"
|
||||
f"Granted: *{row[5]}*\n"
|
||||
f"Effective: *{row[6]}*\n"
|
||||
f"Cancelled: *{row[7]}*\n"
|
||||
f"Expires: *{row[8]}*")
|
||||
embed.add_field(name=header, value=body, inline=False)
|
||||
|
||||
if len(table) > 3:
|
||||
desc += f"\nRecords 1 to 3 of {len(table)}. See ae7q.com for more..."
|
||||
|
||||
embed.description = desc
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@_ae7q_lookup.command(name="applications", aliases=["a"], category=cmn.cat.lookup)
|
||||
async def _ae7q_applications(self, ctx: commands.Context, callsign: str):
|
||||
"""Looks up the application history for a callsign on [ae7q.com](http://ae7q.com/)."""
|
||||
"""
|
||||
with ctx.typing():
|
||||
callsign = callsign.upper()
|
||||
desc = ""
|
||||
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
|
||||
embed = cmn.embed_factory(ctx)
|
||||
|
||||
async with self.session.get(base_url + callsign) as resp:
|
||||
if resp.status != 200:
|
||||
raise cmn.BotHTTPError(resp)
|
||||
page = await resp.text()
|
||||
|
||||
soup = BeautifulSoup(page, features="html.parser")
|
||||
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
|
||||
|
||||
table = tables[0]
|
||||
|
||||
# find the first table in the page, and use it to make a description
|
||||
if len(table[0]) == 1:
|
||||
for row in table:
|
||||
desc += " ".join(row.getText().split())
|
||||
desc += "\n"
|
||||
desc = desc.replace(callsign, f"`{callsign}`")
|
||||
|
||||
# select the last table to get applications
|
||||
table = tables[-1]
|
||||
|
||||
table_headers = table[0].find_all("th")
|
||||
first_header = "".join(table_headers[0].strings) if len(table_headers) > 0 else None
|
||||
|
||||
# catch if the wrong table was selected
|
||||
if first_header is None or not first_header.startswith("Receipt"):
|
||||
embed.title = f"AE7Q Application History for {callsign}"
|
||||
embed.colour = cmn.colours.bad
|
||||
embed.url = base_url + callsign
|
||||
embed.description = desc
|
||||
embed.description += f"\nNo records found for `{callsign}`"
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
table = await process_table(table[1:])
|
||||
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"AE7Q Application History for {callsign}"
|
||||
embed.colour = cmn.colours.good
|
||||
embed.url = base_url + callsign
|
||||
|
||||
# add the first three rows of the table to the embed
|
||||
for row in table[0:3]:
|
||||
header = f"**{row[1]}** ({row[3]})" # **Name** (Callsign)
|
||||
body = (f"Received: *{row[0]}*\n"
|
||||
f"Region: *{row[2]}*\n"
|
||||
f"Purpose: *{row[5]}*\n"
|
||||
f"Last Action: *{row[7]}*\n"
|
||||
f"Application Status: *{row[8]}*\n")
|
||||
embed.add_field(name=header, value=body, inline=False)
|
||||
|
||||
if len(table) > 3:
|
||||
desc += f"\nRecords 1 to 3 of {len(table)}. See ae7q.com for more..."
|
||||
|
||||
embed.description = desc
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
"""
|
||||
raise NotImplementedError("Application history lookup not yet supported. "
|
||||
"Check back in a later version of the bot.")
|
||||
|
||||
@_ae7q_lookup.command(name="frn", aliases=["f"], category=cmn.cat.lookup)
|
||||
async def _ae7q_frn(self, ctx: commands.Context, frn: str):
|
||||
"""Looks up the history of an FRN on [ae7q.com](http://ae7q.com/)."""
|
||||
"""
|
||||
NOTES:
|
||||
- 2 tables: callsign history and application history
|
||||
- If not found: no tables
|
||||
"""
|
||||
with ctx.typing():
|
||||
base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN="
|
||||
embed = cmn.embed_factory(ctx)
|
||||
|
||||
async with self.session.get(base_url + frn) as resp:
|
||||
if resp.status != 200:
|
||||
raise cmn.BotHTTPError(resp)
|
||||
page = await resp.text()
|
||||
|
||||
soup = BeautifulSoup(page, features="html.parser")
|
||||
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
|
||||
|
||||
if not len(tables):
|
||||
embed.title = f"AE7Q History for FRN {frn}"
|
||||
embed.colour = cmn.colours.bad
|
||||
embed.url = base_url + frn
|
||||
embed.description = f"No records found for FRN `{frn}`"
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
table = tables[0]
|
||||
|
||||
table_headers = table[0].find_all("th")
|
||||
first_header = "".join(table_headers[0].strings) if len(table_headers) > 0 else None
|
||||
|
||||
# catch if the wrong table was selected
|
||||
if first_header is None or not first_header.startswith("With Licensee"):
|
||||
embed.title = f"AE7Q History for FRN {frn}"
|
||||
embed.colour = cmn.colours.bad
|
||||
embed.url = base_url + frn
|
||||
embed.description = f"No records found for FRN `{frn}`"
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
table = await process_table(table[2:])
|
||||
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"AE7Q History for FRN {frn}"
|
||||
embed.colour = cmn.colours.good
|
||||
embed.url = base_url + frn
|
||||
|
||||
# add the first three rows of the table to the embed
|
||||
for row in table[0:3]:
|
||||
header = f"**{row[0]}** ({row[3]})" # **Callsign** (Applicant Type)
|
||||
body = (f"Name: *{row[2]}*\n"
|
||||
f"Class: *{row[4]}*\n"
|
||||
f"Region: *{row[1]}*\n"
|
||||
f"Status: *{row[5]}*\n"
|
||||
f"Granted: *{row[6]}*\n"
|
||||
f"Effective: *{row[7]}*\n"
|
||||
f"Cancelled: *{row[8]}*\n"
|
||||
f"Expires: *{row[9]}*")
|
||||
embed.add_field(name=header, value=body, inline=False)
|
||||
|
||||
if len(table) > 3:
|
||||
embed.description = f"Records 1 to 3 of {len(table)}. See ae7q.com for more..."
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@_ae7q_lookup.command(name="licensee", aliases=["l"], category=cmn.cat.lookup)
|
||||
async def _ae7q_licensee(self, ctx: commands.Context, licensee_id: str):
|
||||
"""Looks up the history of a licensee ID on [ae7q.com](http://ae7q.com/)."""
|
||||
with ctx.typing():
|
||||
licensee_id = licensee_id.upper()
|
||||
base_url = "http://ae7q.com/query/data/LicenseeIdHistory.php?ID="
|
||||
embed = cmn.embed_factory(ctx)
|
||||
|
||||
async with self.session.get(base_url + licensee_id) as resp:
|
||||
if resp.status != 200:
|
||||
raise cmn.BotHTTPError(resp)
|
||||
page = await resp.text()
|
||||
|
||||
soup = BeautifulSoup(page, features="html.parser")
|
||||
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
|
||||
|
||||
if not len(tables):
|
||||
embed.title = f"AE7Q History for Licensee {licensee_id}"
|
||||
embed.colour = cmn.colours.bad
|
||||
embed.url = base_url + licensee_id
|
||||
embed.description = f"No records found for Licensee `{licensee_id}`"
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
table = tables[0]
|
||||
|
||||
table_headers = table[0].find_all("th")
|
||||
first_header = "".join(table_headers[0].strings) if len(table_headers) > 0 else None
|
||||
|
||||
# catch if the wrong table was selected
|
||||
if first_header is None or not first_header.startswith("With FCC"):
|
||||
embed.title = f"AE7Q History for Licensee {licensee_id}"
|
||||
embed.colour = cmn.colours.bad
|
||||
embed.url = base_url + licensee_id
|
||||
embed.description = f"No records found for Licensee `{licensee_id}`"
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
table = await process_table(table[2:])
|
||||
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"AE7Q History for Licensee {licensee_id}"
|
||||
embed.colour = cmn.colours.good
|
||||
embed.url = base_url + licensee_id
|
||||
|
||||
# add the first three rows of the table to the embed
|
||||
for row in table[0:3]:
|
||||
header = f"**{row[0]}** ({row[3]})" # **Callsign** (Applicant Type)
|
||||
body = (f"Name: *{row[2]}*\n"
|
||||
f"Class: *{row[4]}*\n"
|
||||
f"Region: *{row[1]}*\n"
|
||||
f"Status: *{row[5]}*\n"
|
||||
f"Granted: *{row[6]}*\n"
|
||||
f"Effective: *{row[7]}*\n"
|
||||
f"Cancelled: *{row[8]}*\n"
|
||||
f"Expires: *{row[9]}*")
|
||||
embed.add_field(name=header, value=body, inline=False)
|
||||
|
||||
if len(table) > 3:
|
||||
embed.description = f"Records 1 to 3 of {len(table)}. See ae7q.com for more..."
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
async def process_table(table: list):
|
||||
"""Processes tables (*not* including headers) and returns the processed table"""
|
||||
table_contents = []
|
||||
for tr in table:
|
||||
row = []
|
||||
for td in tr.find_all("td"):
|
||||
cell_val = td.getText().strip()
|
||||
row.append(cell_val if cell_val else "-")
|
||||
|
||||
# take care of columns that span multiple rows by copying the contents rightward
|
||||
if "colspan" in td.attrs and int(td.attrs["colspan"]) > 1:
|
||||
for i in range(int(td.attrs["colspan"]) - 1):
|
||||
row.append(row[-1])
|
||||
|
||||
# get rid of ditto marks by copying the contents from the previous row
|
||||
for i, cell in enumerate(row):
|
||||
if cell == "\"":
|
||||
row[i] = table_contents[-1][i]
|
||||
# add row to table
|
||||
table_contents += [row]
|
||||
return table_contents
|
||||
@commands.command(name="ae7q", aliases=["ae"], case_insensitive=True)
|
||||
async def _ae7q_lookup(self, ctx: commands.Context, *, _):
|
||||
"""Removed in v2.8.0"""
|
||||
embed = embed_factory(ctx)
|
||||
embed.colour = colours.bad
|
||||
embed.title = "Command removed"
|
||||
embed.description = ("This command was removed in v2.8.0.\n"
|
||||
"For context, see [this Github issue](https://github.com/miaowware/qrm2/issues/448)")
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def setup(bot: commands.Bot):
|
||||
bot.add_cog(AE7QCog(bot))
|
||||
bot.add_cog(AE7QCog())
|
||||
|
159
exts/base.py
@ -1,38 +1,82 @@
|
||||
"""
|
||||
Base extension for qrm
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||
"""
|
||||
|
||||
|
||||
import random
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
from typing import Union
|
||||
from typing import Union, Iterable
|
||||
import pathlib
|
||||
|
||||
import discord
|
||||
import discord.ext.commands as commands
|
||||
from discord.ext.commands import Command, CommandError
|
||||
|
||||
import info
|
||||
import common as cmn
|
||||
|
||||
import data.options as opt
|
||||
from data import options as opt
|
||||
|
||||
|
||||
class QrmHelpCommand(commands.HelpCommand):
|
||||
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
|
||||
|
||||
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
|
||||
mapping = {}
|
||||
|
||||
for cmd in await self.filter_commands(bot.commands, sort=True):
|
||||
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:
|
||||
mapping[cat].append(cmd)
|
||||
else:
|
||||
@ -47,9 +91,9 @@ class QrmHelpCommand(commands.HelpCommand):
|
||||
if parent:
|
||||
fmt = f"{parent} {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}"
|
||||
return f"{opt.prefix}{alias} {command.signature}"
|
||||
return f"{self.context.prefix}{alias} {command.signature}"
|
||||
|
||||
async def send_error_message(self, error):
|
||||
embed = cmn.embed_factory(self.context)
|
||||
@ -61,8 +105,11 @@ class QrmHelpCommand(commands.HelpCommand):
|
||||
async def send_bot_help(self, mapping):
|
||||
embed = cmn.embed_factory(self.context)
|
||||
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.")
|
||||
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():
|
||||
@ -70,7 +117,7 @@ class QrmHelpCommand(commands.HelpCommand):
|
||||
continue
|
||||
names = sorted([cmd.name for cmd in cmds])
|
||||
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:
|
||||
embed.add_field(name="Other", value=", ".join(names), inline=False)
|
||||
await self.context.send(embed=embed)
|
||||
@ -94,7 +141,8 @@ class QrmHelpCommand(commands.HelpCommand):
|
||||
embed.title = await self.get_command_signature(group)
|
||||
embed.description = group.help
|
||||
for cmd in await self.filter_commands(group.commands, sort=True):
|
||||
embed.add_field(name=await self.get_command_signature(cmd), value=cmd.help, inline=False)
|
||||
embed.add_field(name=await self.get_command_signature(cmd), value=cmd.help if cmd.help else "",
|
||||
inline=False)
|
||||
await self.context.send(embed=embed)
|
||||
|
||||
|
||||
@ -102,23 +150,58 @@ class BaseCog(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
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):
|
||||
"""Shows info about qrm."""
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = "About qrm"
|
||||
embed.description = info.description
|
||||
|
||||
embed.add_field(name="Authors", value=", ".join(info.authors))
|
||||
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="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)
|
||||
|
||||
@commands.command(name="ping", aliases=["beep"])
|
||||
@commands.command(name="ping", aliases=["beep"], category=cmn.BoltCats.INFO)
|
||||
async def _ping(self, ctx: commands.Context):
|
||||
"""Shows the current latency to the discord endpoint."""
|
||||
embed = cmn.embed_factory(ctx)
|
||||
@ -131,7 +214,7 @@ class BaseCog(commands.Cog):
|
||||
embed.description = f"Current ping is {self.bot.latency*1000:.1f} ms"
|
||||
await ctx.send(content, embed=embed)
|
||||
|
||||
@commands.command(name="changelog", aliases=["clog"])
|
||||
@commands.command(name="changelog", aliases=["clog"], category=cmn.BoltCats.INFO)
|
||||
async def _changelog(self, ctx: commands.Context, version: str = "latest"):
|
||||
"""Shows what has changed in a bot version. Defaults to the latest version."""
|
||||
embed = cmn.embed_factory(ctx)
|
||||
@ -167,16 +250,40 @@ class BaseCog(commands.Cog):
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="issue")
|
||||
@commands.command(name="issue", category=cmn.BoltCats.INFO)
|
||||
async def _issue(self, ctx: commands.Context):
|
||||
"""Shows how to create a bug report or feature request about the bot."""
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = "Found a bug? Have a feature request?"
|
||||
embed.description = ("Submit an issue on the [issue tracker]"
|
||||
"(https://github.com/miaowware/qrm2/issues)!")
|
||||
embed.description = """Submit an issue on the [issue tracker](https://github.com/miaowware/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)
|
||||
|
||||
@commands.command(name="echo", aliases=["e"], category=cmn.cat.admin)
|
||||
@commands.command(name="donate", aliases=["tip"], category=cmn.BoltCats.INFO)
|
||||
async def _donate(self, ctx: commands.Context):
|
||||
"""Shows ways to help support development of the bot via donations."""
|
||||
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="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)
|
||||
async def _echo(self, ctx: commands.Context,
|
||||
channel: Union[cmn.GlobalChannelConverter, commands.UserConverter], *, msg: str):
|
||||
@ -189,7 +296,7 @@ class BaseCog(commands.Cog):
|
||||
|
||||
|
||||
def parse_changelog():
|
||||
changelog = OrderedDict()
|
||||
changelog = {}
|
||||
ver = ""
|
||||
heading = ""
|
||||
|
||||
|
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()
|
60
exts/fun.py
@ -1,56 +1,72 @@
|
||||
"""
|
||||
Fun extension for qrm
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
import random
|
||||
|
||||
import discord.ext.commands as commands
|
||||
|
||||
import common as cmn
|
||||
|
||||
import data.options as opt
|
||||
|
||||
|
||||
class FunCog(commands.Cog):
|
||||
def __init__(self, bot: commands.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()
|
||||
|
||||
@commands.command(name="xkcd", aliases=["x"], category=cmn.cat.fun)
|
||||
async def _xkcd(self, ctx: commands.Context, number: str):
|
||||
@commands.command(name="xkcd", aliases=["x"], category=cmn.Cats.FUN)
|
||||
async def _xkcd(self, ctx: commands.Context, number: int):
|
||||
"""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):
|
||||
"""Returns xkcd: tar."""
|
||||
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):
|
||||
"""ecks dee"""
|
||||
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):
|
||||
"""Generates fun/wacky phonetics for a word or phrase."""
|
||||
with ctx.typing():
|
||||
result = ""
|
||||
for char in msg.lower():
|
||||
if char.isalpha():
|
||||
result += random.choice([word for word in self.words if word[0] == char])
|
||||
else:
|
||||
result += char
|
||||
result += " "
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Funetics for {msg}"
|
||||
embed.description = result.title()
|
||||
embed.colour = cmn.colours.good
|
||||
result = ""
|
||||
for char in msg.lower():
|
||||
if char.isalpha():
|
||||
result += random.choice([word for word in self.words if word[0] == char])
|
||||
else:
|
||||
result += char
|
||||
result += " "
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Funetics for {msg}"
|
||||
embed.description = result.title()
|
||||
embed.colour = cmn.colours.good
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
|
158
exts/grid.py
@ -1,14 +1,13 @@
|
||||
"""
|
||||
Grid extension for qrm
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||
"""
|
||||
|
||||
|
||||
import math
|
||||
import gridtools
|
||||
|
||||
import discord.ext.commands as commands
|
||||
|
||||
@ -19,127 +18,50 @@ class GridCog(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command(name="grid", category=cmn.cat.maps)
|
||||
async def _grid_sq_lookup(self, ctx: commands.Context, lat: str, lon: str):
|
||||
("""Calculates the grid square for latitude and longitude coordinates, """
|
||||
"""with negative being latitude South and longitude West.""")
|
||||
with ctx.typing():
|
||||
grid = "**"
|
||||
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.title = f"Maidenhead Grid Locator for {float(lat):.6f}, {float(lon):.6f}"
|
||||
embed.description = grid
|
||||
embed.colour = cmn.colours.good
|
||||
else:
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Error generating grid square for {lat}, {lon}."
|
||||
embed.description = """Coordinates out of range.
|
||||
The valid ranges are:
|
||||
- Latitude: `-90` to `+90`
|
||||
- Longitude: `-180` to `+180`"""
|
||||
embed.colour = cmn.colours.bad
|
||||
@commands.command(name="grid", category=cmn.Cats.CALC)
|
||||
async def _grid_sq_lookup(self, ctx: commands.Context, lat: float, lon: float):
|
||||
("""Calculates the grid square for latitude and longitude coordinates."""
|
||||
"""\n\nCoordinates should be in decimal format, with negative being latitude South and longitude West."""
|
||||
"""\n\nTo calculate the latitude and longitude from a grid locator, use `latlong`""")
|
||||
latlong = gridtools.LatLong(lat, lon)
|
||||
grid = gridtools.Grid(latlong)
|
||||
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Maidenhead Grid Locator for {latlong.lat:.5f}, {latlong.long:.5f}"
|
||||
embed.description = f"**{grid}**"
|
||||
embed.colour = cmn.colours.good
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="ungrid", aliases=["loc"], category=cmn.cat.maps)
|
||||
async def _location_lookup(self, ctx: commands.Context, grid: str, grid2: str = None):
|
||||
"""Calculates the latitude and longitude for the center of a grid square.
|
||||
If two grid squares are given, the distance and azimuth between them is calculated."""
|
||||
with ctx.typing():
|
||||
if grid2 is None or grid2 == "":
|
||||
try:
|
||||
grid = grid.upper()
|
||||
loc = get_coords(grid)
|
||||
@commands.command(name="latlong", aliases=["latlon", "loc", "ungrid"], category=cmn.Cats.CALC)
|
||||
async def _location_lookup(self, ctx: commands.Context, grid: str):
|
||||
("""Calculates the latitude and longitude for the center of a grid locator."""
|
||||
"""\n\nTo calculate the grid locator from a latitude and longitude, use `grid`"""
|
||||
"""\n\n*Warning: `ungrid` will be removed soon. Use one of the other names for this command.*""")
|
||||
grid_obj = gridtools.Grid(grid)
|
||||
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Latitude and Longitude for {grid}"
|
||||
embed.colour = cmn.colours.good
|
||||
|
||||
if len(grid) >= 6:
|
||||
embed.description = f"**{loc[0]:.5f}, {loc[1]:.5f}**"
|
||||
embed.url = f"https://www.openstreetmap.org/#map=13/{loc[0]:.5f}/{loc[1]:.5f}"
|
||||
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
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Latitude and Longitude for {grid_obj}"
|
||||
embed.colour = cmn.colours.good
|
||||
embed.description = f"**{grid_obj.lat:.5f}, {grid_obj.long:.5f}**"
|
||||
if ctx.invoked_with == "ungrid":
|
||||
embed.add_field(name="Warning", value=(f"*`{ctx.prefix}ungrid` will be removed soon, use `{ctx.prefix}help "
|
||||
"latlong` to see other names for this command.*"))
|
||||
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):
|
||||
if len(grid) < 3:
|
||||
raise ValueError("The grid locator must be at least 4 characters long.")
|
||||
dist, bearing = gridtools.grid_distance(g1, g2)
|
||||
dist_mi = 0.6214 * dist
|
||||
|
||||
if not grid[0:2].isalpha() or not grid[2:4].isdigit():
|
||||
if len(grid) <= 4:
|
||||
raise ValueError("The grid locator must be of the form AA##.")
|
||||
if len(grid) >= 6 and not grid[5:7].isalpha():
|
||||
raise ValueError("The grid locator must be of the form AA##AA.")
|
||||
|
||||
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)
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Great Circle Distance and Bearing from {g1} to {g2}"
|
||||
embed.description = f"**Distance:** {dist:.1f} km ({dist_mi:.1f} mi)\n**Bearing:** {bearing:.1f}°"
|
||||
embed.colour = cmn.colours.good
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def setup(bot: commands.Bot):
|
||||
|
100
exts/ham.py
@ -1,100 +0,0 @@
|
||||
"""
|
||||
Ham extension for qrm
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of 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):
|
||||
"""Looks up the meaning of 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):
|
||||
"""Returns NATO 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):
|
||||
"""Returns 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"], category=cmn.cat.ref)
|
||||
async def _vanity_prefixes(self, ctx: commands.Context, country: str = None):
|
||||
"""Lists valid callsign prefixes for different 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))
|
106
exts/image.py
@ -1,100 +1,60 @@
|
||||
"""
|
||||
Image extension for qrm
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||
"""
|
||||
|
||||
|
||||
import io
|
||||
|
||||
import aiohttp
|
||||
|
||||
import discord
|
||||
import discord.ext.commands as commands
|
||||
|
||||
import common as cmn
|
||||
|
||||
import data.options as opt
|
||||
|
||||
|
||||
class ImageCog(commands.Cog):
|
||||
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=")
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.bandcharts = cmn.ImagesGroup(cmn.paths.bandcharts / "meta.json")
|
||||
self.maps = cmn.ImagesGroup(cmn.paths.maps / "meta.json")
|
||||
self.bandcharts = cmn.ImagesGroup(cmn.paths.resources / "bandcharts.1.json")
|
||||
self.maps = cmn.ImagesGroup(cmn.paths.resources / "maps.1.json")
|
||||
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
|
||||
|
||||
@commands.command(name="bandplan", aliases=["plan", "bands"], category=cmn.cat.ref)
|
||||
async def _bandplan(self, ctx: commands.Context, region: str = ""):
|
||||
@commands.command(name="bandchart", aliases=["bandplan", "plan", "bands"], category=cmn.Cats.REF)
|
||||
async def _bandcharts(self, ctx: commands.Context, chart_id: str = ""):
|
||||
"""Gets the frequency allocations chart for a given country."""
|
||||
async with ctx.typing():
|
||||
arg = region.lower()
|
||||
embed = cmn.embed_factory(ctx)
|
||||
if arg not in self.bandcharts:
|
||||
desc = "Possible arguments are:\n"
|
||||
for key, img in self.bandcharts.items():
|
||||
desc += f"`{key}`: {img.name}{(' ' + img.emoji if img.emoji else '')}\n"
|
||||
embed.title = "Bandplan Not Found!"
|
||||
embed.description = desc
|
||||
embed.colour = cmn.colours.bad
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
metadata: cmn.ImageMetadata = self.bandcharts[arg]
|
||||
img = discord.File(cmn.paths.bandcharts / 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)
|
||||
await ctx.send(embed=create_embed(ctx, "Bandchart", self.bandcharts, chart_id))
|
||||
|
||||
@commands.command(name="map", category=cmn.cat.maps)
|
||||
@commands.command(name="map", category=cmn.Cats.REF)
|
||||
async def _map(self, ctx: commands.Context, map_id: str = ""):
|
||||
"""Posts a ham-relevant map."""
|
||||
async with ctx.typing():
|
||||
arg = map_id.lower()
|
||||
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)
|
||||
return
|
||||
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)
|
||||
await ctx.send(embed=create_embed(ctx, "Map", self.maps, map_id))
|
||||
|
||||
@commands.command(name="grayline", aliases=["greyline", "grey", "gray", "gl"], category=cmn.cat.maps)
|
||||
async def _grayline(self, ctx: commands.Context):
|
||||
"""Gets a map of the current greyline, where HF propagation is the best."""
|
||||
async with ctx.typing():
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = "Current Greyline Conditions"
|
||||
embed.colour = cmn.colours.good
|
||||
async with self.session.get(self.gl_url) as resp:
|
||||
if resp.status != 200:
|
||||
raise cmn.BotHTTPError(resp)
|
||||
data = io.BytesIO(await resp.read())
|
||||
embed.set_image(url="attachment://greyline.jpg")
|
||||
await ctx.send(embed=embed, file=discord.File(data, "greyline.jpg"))
|
||||
|
||||
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)
|
||||
if img_id not in db:
|
||||
desc = "Possible arguments are:\n"
|
||||
for key, img in db.items():
|
||||
desc += f"`{key}`: {img.name}{(' ' + img.emoji if img.emoji else '')}\n"
|
||||
embed.title = f"{not_found_name} Not Found!"
|
||||
embed.description = desc
|
||||
embed.colour = cmn.colours.bad
|
||||
return embed
|
||||
metadata = db[img_id]
|
||||
if metadata.description:
|
||||
embed.description = metadata.description
|
||||
if metadata.source:
|
||||
embed.add_field(name="Source", value=metadata.source)
|
||||
embed.title = metadata.long_name + (" " + metadata.emoji if metadata.emoji else "")
|
||||
embed.colour = cmn.colours.good
|
||||
embed.set_image(url=opt.resources_url + metadata.filename)
|
||||
return embed
|
||||
|
||||
|
||||
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,84 +0,0 @@
|
||||
"""
|
||||
Lookup extension for qrm
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
|
||||
import threading
|
||||
|
||||
from ctyparser import BigCty
|
||||
|
||||
from discord.ext import commands, tasks
|
||||
|
||||
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 DXCC info about a callsign prefix."""
|
||||
with ctx.typing():
|
||||
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, "./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()
|
103
exts/morse.py
@ -1,81 +1,82 @@
|
||||
"""
|
||||
Morse Code extension for qrm
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import discord.ext.commands as commands
|
||||
|
||||
import common as cmn
|
||||
from resources import morse
|
||||
|
||||
|
||||
class MorseCog(commands.Cog):
|
||||
def __init__(self, bot: commands.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):
|
||||
"""Converts ASCII to international morse code."""
|
||||
with ctx.typing():
|
||||
result = ""
|
||||
for char in msg.upper():
|
||||
try:
|
||||
result += morse.morse[char]
|
||||
except KeyError:
|
||||
result += "<?>"
|
||||
result += " "
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Morse Code for {msg}"
|
||||
embed.description = "**" + result + "**"
|
||||
embed.colour = cmn.colours.good
|
||||
result = ""
|
||||
for char in msg.upper():
|
||||
try:
|
||||
result += self.morse[char]
|
||||
except KeyError:
|
||||
result += "<?>"
|
||||
result += " "
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Morse Code for {msg}"
|
||||
embed.description = "**" + result + "**"
|
||||
embed.colour = cmn.colours.good
|
||||
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):
|
||||
"""Converts international morse code to ASCII."""
|
||||
with ctx.typing():
|
||||
result = ""
|
||||
msg0 = msg
|
||||
msg = msg.split("/")
|
||||
msg = [m.split() for m in msg]
|
||||
for word in msg:
|
||||
for char in word:
|
||||
try:
|
||||
result += morse.ascii[char]
|
||||
except KeyError:
|
||||
result += "<?>"
|
||||
result += " "
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"ASCII for {msg0}"
|
||||
embed.description = result
|
||||
embed.colour = cmn.colours.good
|
||||
result = ""
|
||||
msg0 = msg
|
||||
msg = msg.split("/")
|
||||
msg = [m.split() for m in msg]
|
||||
for word in msg:
|
||||
for char in word:
|
||||
try:
|
||||
result += self.ascii[char]
|
||||
except KeyError:
|
||||
result += "<?>"
|
||||
result += " "
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"ASCII for {msg0}"
|
||||
embed.description = result
|
||||
embed.colour = cmn.colours.good
|
||||
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):
|
||||
"""Calculates the CW weight of a callsign or message."""
|
||||
embed = cmn.embed_factory(ctx)
|
||||
with ctx.typing():
|
||||
msg = msg.upper()
|
||||
weight = 0
|
||||
for char in msg:
|
||||
try:
|
||||
cw_char = morse.morse[char].replace("-", "==")
|
||||
weight += len(cw_char) * 2 + 2
|
||||
except KeyError:
|
||||
embed.title = "Error in calculation of CW weight"
|
||||
embed.description = f"Unknown character `{char}` in message"
|
||||
embed.colour = cmn.colours.bad
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
embed.title = f"CW Weight of {msg}"
|
||||
embed.description = f"The CW weight is **{weight}**"
|
||||
embed.colour = cmn.colours.good
|
||||
msg = msg.upper()
|
||||
weight = 0
|
||||
for char in msg:
|
||||
try:
|
||||
cw_char = self.morse[char].replace("-", "==")
|
||||
weight += len(cw_char) * 2 + 2
|
||||
except KeyError:
|
||||
embed.title = "Error in calculation of CW weight"
|
||||
embed.description = f"Unknown character `{char}` in message"
|
||||
embed.colour = cmn.colours.bad
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
embed.title = f"CW Weight of {msg}"
|
||||
embed.description = f"The CW weight is **{weight}**"
|
||||
embed.colour = cmn.colours.good
|
||||
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))
|
185
exts/qrz.py
@ -1,185 +0,0 @@
|
||||
"""
|
||||
QRZ extension for qrm
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
from io import BytesIO
|
||||
|
||||
import aiohttp
|
||||
from lxml import etree
|
||||
|
||||
from discord.ext import commands, tasks
|
||||
|
||||
import common as cmn
|
||||
|
||||
import data.keys as keys
|
||||
|
||||
|
||||
class QRZCog(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
|
||||
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, *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 keys.qrz_user == "" or keys.qrz_pass == "" or "--link" in flags:
|
||||
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)),
|
||||
("Trustee", data.get("trustee", None)),
|
||||
("eQSL?", eqsl),
|
||||
("Paper QSL?", mqsl),
|
||||
("LotW?", lotw),
|
||||
("QSL Info", data.get("qslmgr", None)),
|
||||
("CQ Zone", data.get("cqzone", None)),
|
||||
("ITU Zone", data.get("ituzone", None)),
|
||||
("IOTA Designator", data.get("iota", None)),
|
||||
("Born", data.get("born", None))])
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(QRZCog(bot))
|
127
exts/study.py
@ -1,10 +1,9 @@
|
||||
"""
|
||||
Study extension for qrm
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||
"""
|
||||
|
||||
|
||||
@ -22,7 +21,8 @@ from resources import study
|
||||
|
||||
|
||||
class StudyCog(commands.Cog):
|
||||
choices = {cmn.emojis.a: "A", cmn.emojis.b: "B", cmn.emojis.c: "C", cmn.emojis.d: "D"}
|
||||
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):
|
||||
self.bot = bot
|
||||
@ -30,14 +30,15 @@ class StudyCog(commands.Cog):
|
||||
self.source = "Data courtesy of [HamStudy.org](https://hamstudy.org/)"
|
||||
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
|
||||
|
||||
@commands.command(name="hamstudy", aliases=["rq", "randomquestion", "randomq"], category=cmn.cat.study)
|
||||
async def _random_question(self, ctx: commands.Context, country: str = "", level: str = ""):
|
||||
@commands.command(name="hamstudy", aliases=["rq", "randomquestion", "randomq"], category=cmn.Cats.STUDY)
|
||||
async def _random_question(self, ctx: commands.Context, country: str = "", level: str = "", element: str = ""):
|
||||
"""Gets a random question from [HamStudy's](https://hamstudy.org) question pools."""
|
||||
with ctx.typing():
|
||||
embed = cmn.embed_factory(ctx)
|
||||
|
||||
country = country.lower()
|
||||
level = level.lower()
|
||||
element = element.upper()
|
||||
|
||||
if country in study.pool_names.keys():
|
||||
if level in study.pool_names[country].keys():
|
||||
@ -88,8 +89,8 @@ class StudyCog(commands.Cog):
|
||||
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] + "+00:00")
|
||||
expires = datetime.fromisoformat(pools[p]["expires"][:-1] + "+00:00")
|
||||
valid_from = datetime.fromisoformat(pools[p]["valid_from"][:-1])
|
||||
expires = datetime.fromisoformat(pools[p]["expires"][:-1])
|
||||
|
||||
if valid_from < datetime.utcnow() < expires:
|
||||
pool = p
|
||||
@ -115,56 +116,106 @@ class StudyCog(commands.Cog):
|
||||
pool = json.loads(await resp.read())["pool"]
|
||||
|
||||
# Select a question
|
||||
pool_section = random.choice(pool)["sections"]
|
||||
if element:
|
||||
els = [el["id"] for el in pool]
|
||||
if element in els:
|
||||
pool_section = pool[els.index(element)]["sections"]
|
||||
else:
|
||||
embed.title = "Element Not Found!"
|
||||
embed.description = f"Possible Elements for Country `{country}` and Level `{level}` are:"
|
||||
embed.colour = cmn.colours.bad
|
||||
embed.description += "\n\n" + "`" + "`, `".join(els) + "`"
|
||||
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=(f"**{cmn.emojis.a}** {question['answers']['A']}"
|
||||
f"\n**{cmn.emojis.b}** {question['answers']['B']}"
|
||||
f"\n**{cmn.emojis.c}** {question['answers']['C']}"
|
||||
f"\n**{cmn.emojis.d}** {question['answers']['D']}"),
|
||||
inline=False)
|
||||
embed.add_field(name="To Answer:",
|
||||
value=("Answer with reactions below. If not answered within 10 minutes,"
|
||||
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/_1330011/images/{pool.split('_',1)[1]}/{question['image']}"
|
||||
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)
|
||||
|
||||
await cmn.add_react(q_msg, cmn.emojis.a)
|
||||
await cmn.add_react(q_msg, cmn.emojis.b)
|
||||
await cmn.add_react(q_msg, cmn.emojis.c)
|
||||
await cmn.add_react(q_msg, cmn.emojis.d)
|
||||
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(reaction, user):
|
||||
return (user.id != self.bot.user.id
|
||||
and reaction.message.id == q_msg.id
|
||||
and str(reaction.emoji) in self.choices.keys())
|
||||
def check(ev):
|
||||
return (ev.user_id != self.bot.user.id
|
||||
and ev.message_id == q_msg.id
|
||||
and (str(ev.emoji) in self.choices.values() or str(ev.emoji) == cmn.emojis.question))
|
||||
|
||||
try:
|
||||
reaction, user = await self.bot.wait_for("reaction_add", timeout=600.0, check=check)
|
||||
ev = await self.bot.wait_for("raw_reaction_add", timeout=300.0, check=check)
|
||||
except asyncio.TimeoutError:
|
||||
embed.remove_field(2)
|
||||
embed.add_field(name="Answer:", value=f"Timed out! The correct answer was **{question['answer']}**.")
|
||||
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 self.choices[str(reaction.emoji)] == question["answer"]:
|
||||
embed.remove_field(2)
|
||||
embed.add_field(name="Answer:", value=f"Correct! The answer was **{question['answer']}**.")
|
||||
embed.colour = cmn.colours.good
|
||||
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:
|
||||
embed.remove_field(2)
|
||||
embed.add_field(name="Answer:", value=f"Incorrect! The correct answer was **{question['answer']}**.")
|
||||
embed.colour = cmn.colours.bad
|
||||
await q_msg.edit(embed=embed)
|
||||
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:
|
||||
|
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))
|
124
exts/weather.py
@ -1,124 +0,0 @@
|
||||
"""
|
||||
Weather extension for qrm
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
|
||||
import io
|
||||
import re
|
||||
|
||||
import aiohttp
|
||||
|
||||
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 = aiohttp.ClientSession(connector=bot.qrm.connector)
|
||||
|
||||
@commands.command(name="bandconditions", aliases=["cond", "condx", "conditions"], category=cmn.cat.weather)
|
||||
async def _band_conditions(self, ctx: commands.Context):
|
||||
"""Gets a solar conditions report."""
|
||||
async 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:
|
||||
raise cmn.BotHTTPError(resp)
|
||||
data = io.BytesIO(await resp.read())
|
||||
embed.set_image(url="attachment://condx.png")
|
||||
await ctx.send(embed=embed, file=discord.File(data, "condx.png"))
|
||||
|
||||
@commands.group(name="weather", aliases=["wttr"], category=cmn.cat.weather)
|
||||
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.cat.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."""
|
||||
async 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:
|
||||
raise cmn.BotHTTPError(resp)
|
||||
data = io.BytesIO(await resp.read())
|
||||
embed.set_image(url="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):
|
||||
"""Gets current local weather conditions from [wttr.in](http://wttr.in/).
|
||||
See help of the `weather` command for possible location types and options."""
|
||||
async 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:
|
||||
raise cmn.BotHTTPError(resp)
|
||||
data = io.BytesIO(await resp.read())
|
||||
embed.set_image(url="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))
|
16
info.py
@ -1,16 +1,18 @@
|
||||
"""
|
||||
Static info about the bot.
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||
"""
|
||||
|
||||
|
||||
authors = ("@ClassAbbyAmplifier#2229", "@0x5c#0639")
|
||||
authors = ("@classabbyamp", "@0x5c.io")
|
||||
description = """A bot with various useful ham radio-related functions, written in Python."""
|
||||
license = "Released under the GNU General Public License v2"
|
||||
contributing = "Check out the source on GitHub, contributions welcome: https://github.com/miaowware/qrm2"
|
||||
release = "2.2.0"
|
||||
license = "Québec Free and Open-Source Licence – Strong Reciprocity (LiLiQ-R+), version 1.1"
|
||||
contributing = """Check out the [source on GitHub](https://github.com/miaowware/qrm2). Contributions are welcome!
|
||||
|
||||
All issues and requests related to resources (including maps, band charts, data) should be added \
|
||||
in [miaowware/qrm-resources](https://github.com/miaowware/qrm-resources)."""
|
||||
release = "2.9.2"
|
||||
bot_server = "https://discord.gg/Ntbg3J4"
|
||||
|
98
main.py
@ -2,10 +2,9 @@
|
||||
"""
|
||||
qrm, a bot for Discord
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||
"""
|
||||
|
||||
|
||||
@ -15,7 +14,9 @@ import sys
|
||||
import traceback
|
||||
from datetime import datetime, time
|
||||
from types import SimpleNamespace
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
import pytz
|
||||
|
||||
import discord
|
||||
@ -24,6 +25,7 @@ from discord.ext import commands, tasks
|
||||
import info
|
||||
import common as cmn
|
||||
import utils.connector as conn
|
||||
from utils.resources_manager import ResourcesManager
|
||||
|
||||
import data.keys as keys
|
||||
import data.options as opt
|
||||
@ -34,6 +36,7 @@ import data.options as opt
|
||||
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.
|
||||
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)
|
||||
|
||||
@ -44,9 +47,20 @@ debug_mode = opt.debug # Separate assignement in-case we define an override (te
|
||||
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,
|
||||
description=info.description,
|
||||
help_command=commands.MinimalHelpCommand(),
|
||||
case_insensitive=True,
|
||||
description=info.description, help_command=commands.MinimalHelpCommand(),
|
||||
intents=intents,
|
||||
member_cache_flags=member_cache,
|
||||
loop=loop,
|
||||
connector=connector)
|
||||
|
||||
@ -56,11 +70,13 @@ bot.qrm = SimpleNamespace()
|
||||
# Let's store stuff here.
|
||||
bot.qrm.connector = connector
|
||||
bot.qrm.debug_mode = debug_mode
|
||||
# TODO: Add code to close the client
|
||||
bot.qrm.httpx_client = httpx.AsyncClient()
|
||||
|
||||
|
||||
# --- Commands ---
|
||||
|
||||
@bot.command(name="restart", aliases=["rs"], category=cmn.cat.admin)
|
||||
@bot.command(name="restart", aliases=["rs"], category=cmn.BoltCats.ADMIN)
|
||||
@commands.check(cmn.check_if_owner)
|
||||
async def _restart_bot(ctx: commands.Context):
|
||||
"""Restarts the bot."""
|
||||
@ -68,10 +84,10 @@ async def _restart_bot(ctx: commands.Context):
|
||||
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
|
||||
print(f"[**] Restarting! Requested by {ctx.author}.")
|
||||
exit_code = 42 # Signals to the wrapper script that the bot needs to be restarted.
|
||||
await bot.logout()
|
||||
await bot.close()
|
||||
|
||||
|
||||
@bot.command(name="shutdown", aliases=["shut"], category=cmn.cat.admin)
|
||||
@bot.command(name="shutdown", aliases=["shut"], category=cmn.BoltCats.ADMIN)
|
||||
@commands.check(cmn.check_if_owner)
|
||||
async def _shutdown_bot(ctx: commands.Context):
|
||||
"""Shuts down the bot."""
|
||||
@ -79,16 +95,16 @@ async def _shutdown_bot(ctx: commands.Context):
|
||||
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
|
||||
print(f"[**] Shutting down! Requested by {ctx.author}.")
|
||||
exit_code = 0 # Signals to the wrapper script that the bot should not be restarted.
|
||||
await bot.logout()
|
||||
await bot.close()
|
||||
|
||||
|
||||
@bot.group(name="extctl", aliases=["ex"], category=cmn.cat.admin)
|
||||
@bot.group(name="extctl", aliases=["ex"], case_insensitive=True, category=cmn.BoltCats.ADMIN)
|
||||
@commands.check(cmn.check_if_owner)
|
||||
async def _extctl(ctx: commands.Context):
|
||||
"""Extension control commands.
|
||||
Defaults to `list` if no subcommand specified"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
cmd = bot.get_command("extctl list")
|
||||
cmd = _extctl_list
|
||||
await ctx.invoke(cmd)
|
||||
|
||||
|
||||
@ -97,14 +113,24 @@ async def _extctl_list(ctx: commands.Context):
|
||||
"""Lists loaded extensions."""
|
||||
embed = cmn.embed_factory(ctx)
|
||||
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)
|
||||
|
||||
|
||||
@_extctl.command(name="load", aliases=["ld"])
|
||||
async def _extctl_load(ctx: commands.Context, extension: str):
|
||||
"""Loads an extension."""
|
||||
bot.load_extension(ext_dir + "." + extension)
|
||||
try:
|
||||
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)
|
||||
|
||||
|
||||
@ -115,14 +141,26 @@ async def _extctl_reload(ctx: commands.Context, extension: str):
|
||||
pika = bot.get_emoji(opt.pika)
|
||||
if pika:
|
||||
await cmn.add_react(ctx.message, pika)
|
||||
bot.reload_extension(ext_dir + "." + extension)
|
||||
try:
|
||||
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)
|
||||
|
||||
|
||||
@_extctl.command(name="unload", aliases=["ul"])
|
||||
async def _extctl_unload(ctx: commands.Context, extension: str):
|
||||
"""Unloads an extension."""
|
||||
bot.unload_extension(ext_dir + "." + extension)
|
||||
try:
|
||||
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)
|
||||
|
||||
|
||||
@ -156,7 +194,7 @@ async def on_command_error(ctx: commands.Context, err: commands.CommandError):
|
||||
await cmn.add_react(ctx.message, cmn.emojis.warning)
|
||||
await ctx.send_help(ctx.command)
|
||||
elif isinstance(err, commands.CommandNotFound):
|
||||
if ctx.invoked_with.startswith(("?", "!")):
|
||||
if ctx.invoked_with and ctx.invoked_with.startswith(("?", "!")):
|
||||
return
|
||||
else:
|
||||
await cmn.add_react(ctx.message, cmn.emojis.question)
|
||||
@ -193,7 +231,10 @@ async def _ensure_activity_time():
|
||||
try:
|
||||
tz = pytz.timezone(opt.status_tz)
|
||||
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
|
||||
|
||||
now = datetime.now(tz=tz).time()
|
||||
@ -203,6 +244,8 @@ async def _ensure_activity_time():
|
||||
end_time = time(hour=sts[2][0], minute=sts[2][1], tzinfo=tz)
|
||||
if start_time < now <= end_time:
|
||||
status = sts[0]
|
||||
if opt.show_help:
|
||||
status += f" | {opt.display_prefix}help"
|
||||
|
||||
await bot.change_presence(activity=discord.Game(name=status))
|
||||
|
||||
@ -210,6 +253,8 @@ async def _ensure_activity_time():
|
||||
@tasks.loop(minutes=5)
|
||||
async def _ensure_activity_random():
|
||||
status = random.choice(opt.statuses)
|
||||
if opt.show_help:
|
||||
status += f" | {opt.display_prefix}help"
|
||||
|
||||
await bot.change_presence(activity=discord.Game(name=status))
|
||||
|
||||
@ -217,15 +262,34 @@ async def _ensure_activity_random():
|
||||
@tasks.loop(minutes=5)
|
||||
async def _ensure_activity_fixed():
|
||||
status = opt.statuses[0]
|
||||
if opt.show_help:
|
||||
status += f" | {opt.display_prefix}help"
|
||||
|
||||
await bot.change_presence(activity=discord.Game(name=status))
|
||||
|
||||
|
||||
# --- 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:
|
||||
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:
|
||||
bot.run(keys.discord_token)
|
||||
|
@ -1,5 +1,9 @@
|
||||
discord.py
|
||||
ctyparser
|
||||
py-cord-dev[speed]==2.5.0rc5
|
||||
ctyparser~=2.0
|
||||
gridtools~=1.0
|
||||
callsignlookuptools[async]~=1.1
|
||||
beautifulsoup4
|
||||
lxml
|
||||
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,56 +1,27 @@
|
||||
"""
|
||||
Information about callsigns for the vanity prefixes command in hamcog.
|
||||
Information about callsigns for the prefixes command in hamcog.
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||
|
||||
This file is part of discord-qrmbot and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||
"""
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .callsigninfos import (us, ca)
|
||||
|
||||
|
||||
us_calls_title = "Valid US Vanity Callsigns"
|
||||
us_calls_desc = ("#x# is the number of letters in the prefix and suffix of a callsign. "
|
||||
"E.g., WY4RC would be a 2x2 callsign, with prefix WY and suffix RC.")
|
||||
us_calls = OrderedDict([("**Group A** (Extra Only)", ("**Any:** K, N, W (1x2)\n"
|
||||
" AA-AL, KA-KZ, NA-NZ, WA-WZ (2x1)\n"
|
||||
" AA-AL (2x2)\n"
|
||||
"*Except*\n"
|
||||
"**Alaska:** AL, KL, NL, WL (2x1)\n"
|
||||
"**Caribbean:** KP, NP, WP (2x1)\n"
|
||||
"**Pacific:** AH, KH, NH, WH (2x1)")),
|
||||
("**Group B** (Advanced and Extra Only)", ("**Any:** KA-KZ, NA-NZ, WA-WZ (2x2)\n"
|
||||
"*Except*\n"
|
||||
"**Alaska:** AL (2x2)\n"
|
||||
"**Caribbean:** KP (2x2)\n"
|
||||
"**Pacific:** AH (2x2)")),
|
||||
("**Group C** (Technician, General, Advanced, Extra Only)", ("**Any Region:** K, N, W (1x3)\n"
|
||||
"*Except*\n"
|
||||
"**Alaska:** KL, NL, WL (2x2)\n"
|
||||
"**Caribbean:** NP, WP (2x2)\n"
|
||||
"**Pacific:** KH, NH, WH (2x2)")),
|
||||
("**Group D** (Any License Class)", ("**Any Region:** KA-KZ, WA-WZ (2x3)\n"
|
||||
"*Except*\n"
|
||||
"**Alaska:** KL, WL (2x3)\n"
|
||||
"**Caribbean:** KP, WP (2x3)\n"
|
||||
"**Pacific:** KH, WH (2x3)")),
|
||||
("**Unavailable**", ("- KA2AA-KA9ZZ: US Army in Japan\n"
|
||||
"- KC4AAA-KC4AAF: NSF in Antartica\n"
|
||||
"- KC4USA-KC4USZ: US Navy in Antartica\n"
|
||||
"- KG4AA-KG4ZZ: US Navy in Guantanamo Bay\n"
|
||||
"- KL9KAA-KL9KHZ: US military in Korea\n"
|
||||
"- KC6AA-KC6ZZ: Former US (Eastern and Western Caroline Islands), "
|
||||
"now Federated States of Micronesia (V6) and Republic of Palau (T8)\n"
|
||||
"- KX6AA-KX6ZZ: Former US (Marshall Islands), "
|
||||
"now Republic of the Marshall Islands (V73)\n"
|
||||
"- Any suffix SOS or QRA-QUZ\n"
|
||||
"- Any 2x3 with X as the first suffix letter\n"
|
||||
"- Any 2x3 with AF, KF, NF, or WF prefix and suffix EMA: FEMA\n"
|
||||
"- Any 2x3 with AA-AL, NA-NZ, WC, WK, WM, WR, or WT prefix: \"Group X\"\n"
|
||||
"- Any 2x1, 2x2, or 2x3 with KP, NP, WP prefix and 0, 6, 7, 8, 9 number\n"
|
||||
"- Any 1x1 callsign: Special Event"))])
|
||||
@dataclass
|
||||
class CallsignInfoData:
|
||||
"""Represents a country's callsign info"""
|
||||
title: str = ""
|
||||
desc: str = ""
|
||||
calls: str = ""
|
||||
emoji: str = ""
|
||||
|
||||
# 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,136 +0,0 @@
|
||||
"""
|
||||
A listing of morse code symbols
|
||||
---
|
||||
Copyright (C) 2019-2020 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,38 +0,0 @@
|
||||
"""
|
||||
A listing of NATO Phonetics
|
||||
---
|
||||
Copyright (C) 2019-2020 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,286 +0,0 @@
|
||||
"""
|
||||
A listing of Q Codes
|
||||
---
|
||||
Copyright (C) 2019-2020 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)."
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
"""
|
||||
A listing of hamstudy command resources
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2023 classabbyamp, 0x5c
|
||||
|
||||
This file is part of discord-qrmbot and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||
"""
|
||||
|
||||
|
||||
|
45402
resources/words
12
run.sh
@ -17,7 +17,7 @@ fi
|
||||
# Argument handling
|
||||
_PASS_ERRORS=0
|
||||
_NO_BOTENV=0
|
||||
while [ ! -z "$1" ]; do
|
||||
while [ -n "$1" ]; do
|
||||
case $1 in
|
||||
--pass-errors)
|
||||
_PASS_ERRORS=1
|
||||
@ -34,9 +34,9 @@ while [ ! -z "$1" ]; do
|
||||
done
|
||||
|
||||
|
||||
# If $PYTHON_BIN is not defined, default to 'python3.7'
|
||||
if [ $_NO_BOTENV -eq 1 -a -z "$PYTHON_BIN" ]; then
|
||||
PYTHON_BIN='python3.7'
|
||||
# If $PYTHON_BIN is not defined, default to 'python3.11'
|
||||
if [ $_NO_BOTENV -eq 1 ] && [ -z "$PYTHON_BIN" ]; then
|
||||
PYTHON_BIN='python3.11'
|
||||
fi
|
||||
|
||||
|
||||
@ -69,9 +69,9 @@ echo "$0: Starting bot..."
|
||||
# The loop
|
||||
while true; do
|
||||
if [ $_NO_BOTENV -eq 1 ]; then
|
||||
"$PYTHON_BIN" main.py $@
|
||||
"$PYTHON_BIN" main.py "$@"
|
||||
else
|
||||
./$BOTENV/bin/python3 main.py $@
|
||||
"./$BOTENV/bin/python3" main.py "$@"
|
||||
fi
|
||||
err=$?
|
||||
_message="$0: The bot exited with [$err]"
|
||||
|
@ -15,7 +15,10 @@ Settings and options for the bot.
|
||||
|
||||
# The prefix for the bot (str). Define a list of stings for multiple prefixes.
|
||||
# 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`,
|
||||
# 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".
|
||||
# * Those users will have full control over the bot.
|
||||
# ! 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.
|
||||
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)
|
||||
status_mode = "fixed"
|
||||
@ -46,9 +81,15 @@ time_statuses = [("with lids on 3.840", (00, 00), (6, 00)),
|
||||
("with lids on 7.200", (18, 00), (20, 00)),
|
||||
("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
|
||||
# Use the format {emoji_id (int): ("tuple", "of", "lowercase", "keywords")}
|
||||
msg_reacts = {}
|
||||
|
||||
# A :pika: emote's ID, None for no emote :c
|
||||
pika = 658733876176355338
|
||||
|
||||
# Base URL to a deployment of rTeX, which performs LaTeX rendering.
|
||||
rtex_instance = "https://rtex.probablyaweb.site/"
|
||||
|
@ -1,10 +1,9 @@
|
||||
"""
|
||||
Wrapper to handle aiohttp connector creation.
|
||||
---
|
||||
Copyright (C) 2020 Abigail Gold, 0x5c
|
||||
Copyright (C) 2020-2023 classabbyamp, 0x5c
|
||||
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
SPDX-License-Identifier: LiLiQ-Rplus-1.1
|
||||
"""
|
||||
|
||||
|
||||
|
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)
|