Compare commits
205 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 940f45f4d4 | |||
| be7e29b387 | |||
| 518ead9ccd | |||
| 6e5acba6e9 | |||
| ad50a86f9d | |||
| 0bfa0c6e41 | |||
| ce64d882b8 | |||
| 0aac09f3bc | |||
| e3534d02d7 | |||
| cdcb0e17d2 | |||
| cd2503c953 | |||
| b4c165851c | |||
| 77a5af73bc | |||
| 4b7064cad9 | |||
| d8fe3cfa02 | |||
| ffc3be7e24 | |||
| 5dab93b7d3 | |||
| e660b1a8f5 | |||
| 3d96a43c50 | |||
| e8bb18ea8c | |||
| 19952396f2 | |||
| 1831c56f58 | |||
| 77e14a109c | |||
| 2ac13346d4 | |||
| 2eea7dce23 | |||
| f26a7af928 | |||
| 786440edcb | |||
| c47d211016 | |||
| 855935a26e | |||
| ff9d46f379 | |||
| 0f0c3bf723 | |||
| 7c818cfb34 | |||
| 27863ae6bf | |||
| de999bc39d | |||
| 80d80ab718 | |||
| 8cb1a8df15 | |||
| fa2dded81b | |||
| bda0540fa8 | |||
| c9510ad9b9 | |||
| 00f9929deb | |||
| 3597367046 | |||
| 7e35e8949a | |||
| 77b572eb3e | |||
| be042a9641 | |||
| 488ae6cc98 | |||
| a65fd04dbd | |||
| 6329718d29 | |||
| 756a15c4c5 | |||
| b462527211 | |||
| 93b42e64dc | |||
| f6103ef6f1 | |||
| 090b96482d | |||
| 6d71974ea1 | |||
| 0af82ac241 | |||
| 2ba6249b90 | |||
| 2c11dad358 | |||
| 5f796d479e | |||
| 3ba55d4c35 | |||
| f4ed93dc76 | |||
| 2cb4b03532 | |||
| bc93462c29 | |||
| 6867c45c8c | |||
| dcbb7acab8 | |||
| 3803ce6045 | |||
| 8ca4911072 | |||
| 6e2468f04f | |||
| b17a8a1749 | |||
| 8dfa7001ef | |||
| f6ed8430b9 | |||
| d650cbd6c1 | |||
| 4d9f9d1b19 | |||
| 1c649aacc2 | |||
| 2049ca9fca | |||
| 38416d9050 | |||
| 8dcdc22fe4 | |||
| c57056e586 | |||
| b281122d41 | |||
| fa1cc16c6f | |||
| dfd9479e0d | |||
| 4b122518f5 | |||
| 0e28366265 | |||
| 9471eaa3cb | |||
| 27c290cff8 | |||
| 58878352aa | |||
| af136c3d3e | |||
| 9ef4864d15 | |||
| 6803b058d5 | |||
| e37fdf40fb | |||
| fdfc8cce06 | |||
| 56b74108eb | |||
| 29d0440d3d | |||
| 29e75c38e1 | |||
| 6c32dcbead | |||
| 74327378b9 | |||
| c1d3c63503 | |||
| 6768a1e01d | |||
| ea508d2a59 | |||
| f56efd3410 | |||
| 976b3e8bf3 | |||
| 0b4204dc13 | |||
| 8686b0ef96 | |||
| 9113638b67 | |||
| 831667ec10 | |||
| b64c7ee39a | |||
| 528307f22f | |||
| 09c58f9ba2 | |||
| 61f0c9423e | |||
| b6f6d0408c | |||
| 94f9865103 | |||
| 08ca455895 | |||
| 925a05aafb | |||
| 12886fad89 | |||
| ce107f5a82 | |||
| 0aa5e6ebd6 | |||
| 671b0e9ee5 | |||
| 776ff72581 | |||
| 58c69f5aeb | |||
| 2c3535d99e | |||
| 04cbc920ce | |||
| f5a9b0b780 | |||
| eb5e038624 | |||
| 1a1b4c9329 | |||
| 8db13755bc | |||
| ac740ee679 | |||
| 0608a74e6c | |||
| eaa47fc724 | |||
| 60764cd733 | |||
| 8bfaaf4af6 | |||
| 30455153ba | |||
| d9bd3d354c | |||
| 8f15feab94 | |||
| 4e73fa3734 | |||
| 5e33a6aca6 | |||
| 07fd9255e0 | |||
| 238cdf1c72 | |||
| 25eae1fc0f | |||
| f4f034abfe | |||
| ae673f7026 | |||
| 8a9c8cbd43 | |||
| b94a8d42dd | |||
| 60f8e80b82 | |||
| 6e516a12db | |||
| 9552a7f6d4 | |||
| e7baca453c | |||
| 673d154b94 | |||
| 118d5e0b63 | |||
| c7d0a85464 | |||
| ad11af2865 | |||
| 2f0084d450 | |||
| e1d4b2eab5 | |||
| 09323e54bc | |||
| f974bf1223 | |||
| 0615df0829 | |||
| 70505f10f6 | |||
| 807ef46379 | |||
| f32aa240ce | |||
| 74558c7f27 | |||
| ea8c3a9045 | |||
| b0072f779e | |||
| 69d36fea7f | |||
| 5e250b5e3e | |||
| 48e5040a87 | |||
| aa73f575b5 | |||
| 4368e0b603 | |||
| 2e985209a8 | |||
| 0b292eda10 | |||
| 4bf398a0de | |||
| 3da67ad6a2 | |||
| 02192a7ea9 | |||
| f4806f8a9e | |||
| 3196aa6e40 | |||
| 309f9f7ecb | |||
| 1b21583d41 | |||
| 3ab8fae80a | |||
| 96b2a8f259 | |||
| 7d1a2a561b | |||
| f7aaa467b5 | |||
| f1993c85b2 | |||
| 0bc368877b | |||
| 28eb6d45c1 | |||
| 951dc38992 | |||
| f0805d2095 | |||
| 4d763c2fc1 | |||
| b25b5a95ff | |||
| 9e79eececc | |||
| 2ccb225a5f | |||
| f649d15890 | |||
| e9d2274e64 | |||
| ae67498e75 | |||
| 781a3fa4dd | |||
| c48a4069cb | |||
| e64c9ba2df | |||
| 5c95b9ff12 | |||
| ca224cc744 | |||
| a93f6d7a03 | |||
| d3fd36fb75 | |||
| c47fd0549d | |||
| d7f50bc083 | |||
| e84ba201bd | |||
| 26765e86b6 | |||
| 51cd182c57 | |||
| 7a9d64465e | |||
| 5beb68e1c5 | |||
| 5da9cf0bf2 | |||
| ba1b7e947f |
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,67 @@
|
||||
# vim: ts=2 sw=2:
|
||||
|
||||
name: Docker Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
# Publish `master` as Docker `dev` image.
|
||||
branches:
|
||||
- master
|
||||
# Publish `v*` tags as x.x.x images and as `latest`.
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
IMAGE_NAME: qrm2
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
name: Build and push docker images
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Write ref to file
|
||||
run: git rev-list -n 1 $GITHUB_REF > ./git_commit
|
||||
|
||||
- name: Build image
|
||||
run: docker build . --file Dockerfile -t $IMAGE_NAME
|
||||
|
||||
- name: Log into Github Package Registry
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Tag image
|
||||
id: tag_image
|
||||
run: |
|
||||
IMAGE_ID=docker.pkg.github.com/${{ github.repository }}/$IMAGE_NAME
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo ::set-output name=image_id::$IMAGE_ID
|
||||
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
# if version is master, set version to dev
|
||||
[[ "$VERSION" == "master" ]] && VERSION=dev
|
||||
echo VERSION=$VERSION
|
||||
echo ::set-output name=version::$VERSION
|
||||
|
||||
# tag dev or x.x.x
|
||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||
# tag latest if not a dev release
|
||||
[[ "$VERSION" != "dev" ]] && docker tag $IMAGE_NAME $IMAGE_ID:latest || true
|
||||
|
||||
- name: Push images to registry
|
||||
run: docker push ${{ steps.tag_image.outputs.image_id }}
|
||||
|
||||
- name: Deploy official images
|
||||
id: deploy_images
|
||||
uses: satak/webrequest-action@v1
|
||||
with:
|
||||
url: ${{ secrets.DEPLOY_URL }}
|
||||
method: POST
|
||||
headers: '{"Authentication": "Token ${{ secrets.DEPLOY_TOKEN }}"}'
|
||||
payload: '{"version": "${{ steps.tag_image.outputs.version }}"}'
|
||||
@@ -1,6 +1,6 @@
|
||||
name: Linting
|
||||
|
||||
on: [push]
|
||||
on: [push,pull_request]
|
||||
|
||||
jobs:
|
||||
flake8_py3:
|
||||
|
||||
@@ -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
|
||||
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: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ env.tag_version }}
|
||||
release_name: ${{ env.tag_subject }}
|
||||
body: |
|
||||
${{ env.tag_body }}
|
||||
|
||||
## Changelog
|
||||
|
||||
${{ steps.changelog_reader.outputs.changes }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
@@ -9,6 +9,8 @@ cty.zip
|
||||
|
||||
/docker-compose.yml
|
||||
|
||||
.vscode/
|
||||
|
||||
|
||||
#########################################################
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
||||
@@ -3,9 +3,112 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v2.0.0] - 2019-12-16
|
||||
|
||||
## [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 innappropriate words.
|
||||
|
||||
|
||||
## [2.3.0] - 2020-03-30
|
||||
### Added
|
||||
- `?phoneticweight` command, which calculates a message's length in syllables.
|
||||
- `?standards` command to display [xkcd 927](https://xkcd.com/927/).
|
||||
### Changed
|
||||
- Python>=3.7 now required.
|
||||
|
||||
|
||||
## [2.2.3] - 2020-03-29
|
||||
### Fixed
|
||||
- Commands are no longer case-sensitive.
|
||||
|
||||
|
||||
## [2.2.2] - 2020-02-25
|
||||
### Fixed
|
||||
- Fixed issue where HamStudy questions with images would cause an error.
|
||||
- Added/fixed/removed typing indicators in numerous commands.
|
||||
|
||||
|
||||
## [2.2.1] - 2020-02-20
|
||||
### Fixed
|
||||
- Fixed issue where some HamStudy pools will become unselectable.
|
||||
|
||||
|
||||
## [2.2.0] - 2020-02-15
|
||||
### Added
|
||||
- Added Trustee field to qrz command for club callsigns.
|
||||
- Added alias for `ae7q call` command (`ae7q c`).
|
||||
- Added ae7q lookup by FRN and Licensee ID, and for trustee records (`ae7q frn, licensee, trustee`).
|
||||
### Changed
|
||||
- Changelog command to accept a version as argument.
|
||||
- The qrz command can now link to a QRZ page instead of embedding the data with the `--link` flag.
|
||||
- All currently-available pools can now be accessed by the `hamstudy` command.
|
||||
- The `hamstudy` command now uses the syntax `?hamstudy <country> <pool>`.
|
||||
- Replaced `hamstudyanswer` command with answering by reaction.
|
||||
- Removed all generic error handling from commands.
|
||||
- Cleaned up the description of multiple commands.
|
||||
- Updated repository links from classabbyamp/discord-qrm2 to miaowware/qrm2.
|
||||
### Fixed
|
||||
- Fixed ditto marks (") appearing in the ae7q call command.
|
||||
- Fixed issue where incorrect table was parsed in ae7q call command.
|
||||
- Fixed warning emoji reaction on messages starting with "??".
|
||||
- Fixed issue where `prefixes` would error when given an invalid argument.
|
||||
|
||||
|
||||
## [2.1.0] - 2020-01-04
|
||||
### Added
|
||||
- New NATO "phonetics" command.
|
||||
- Flag emojis to commands with countries.
|
||||
- Image attribution and description to "image" commands.
|
||||
- Better user-facing command error handling.
|
||||
- Reacts with emojis upon specific keywords.
|
||||
- Official server to info command.
|
||||
- Command linking to the issue tracker.
|
||||
- New key in options.py: pika.
|
||||
### Changed
|
||||
- The "phonetics" command is now called "funetics".
|
||||
- All commands now respond in embeds.
|
||||
- Playing status can now change on a schedule or randomly from a list.
|
||||
### Fixed
|
||||
- Fixed incorrect information in the `prefixes` command.
|
||||
|
||||
|
||||
## [2.0.0] - 2019-12-16
|
||||
### Added
|
||||
- Rich lookup for AE7Q.com (callsigns only, more to come)
|
||||
- Rich lookup for QRZ.com, if a QRZ subscription is present
|
||||
@@ -31,7 +134,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Cleaned up code to comply with the PEP8 Standard
|
||||
- Issue in morse and unmorse commands where spaces were not interpreted correctly
|
||||
|
||||
## v1.0.0 - 2019-07-31 [YANKED]
|
||||
|
||||
[Unreleased]: https://github.com/classabbyamp/discord-qrm2/compare/v2.0.0...HEAD
|
||||
[v2.0.0]: https://github.com/classabbyamp/discord-qrm2/releases/tag/v2.0.0
|
||||
## 1.0.0 - 2019-07-31 [YANKED]
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.5.0...HEAD
|
||||
[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
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# 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.7 or higher.
|
||||
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
|
||||
@@ -71,6 +71,17 @@ clean:
|
||||
|
||||
|
||||
### Dev targets ###
|
||||
.PHONY: dev-install
|
||||
dev-install: $(BOTENV)/dev_req_done data/options.py data/keys.py
|
||||
|
||||
# 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,13 +23,14 @@ This is the easiest method for running the bot without any modifications.
|
||||
version: '3'
|
||||
services:
|
||||
qrm2:
|
||||
image: "classabbyamp/discord-qrm2:latest"
|
||||
image: "docker.pkg.github.com/miaowware/qrm2/qrm2:latest"
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- "./data:/app/data:rw"
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
```
|
||||
*Note that Github's registry requires [a few extra steps](https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages) during the initial setup.*
|
||||
|
||||
3. Create a subdirectory named `data`.
|
||||
|
||||
@@ -42,7 +43,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,7 +60,7 @@ 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"
|
||||
@@ -75,10 +76,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 +96,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 +111,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.
|
||||
- `docker.pkg.github.com/miaowware/qrm2/qrm2:latest` if you want to use the prebuilt image.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# qrm, a Bot for Discord
|
||||
|
||||
[](https://discord.gg/SwyjdDN)
|
||||
|
||||
A discord bot with ham radio functionalities.
|
||||
|
||||
An independent rewrite of qrmbot-discord.
|
||||
@@ -12,6 +14,8 @@ See [README-DOCKER.md](./README-DOCKER.md)
|
||||
|
||||
### Without Docker
|
||||
|
||||
Requires Python 3.7 or newer.
|
||||
|
||||
Prep the environment. For more information on extra options, see the [quick-bot-no-pain Makefile documentation](https://github.com/0x5c/quick-bot-no-pain/blob/master/docs/makefile.md).
|
||||
|
||||
```
|
||||
@@ -24,9 +28,13 @@ Run. For more information on options, see the [quick-bot-no-pain run.sh document
|
||||
$ run.sh
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Check out the [contribution guidelines](/CONTRIBUTING.md) for more information about how to contribute to this project.
|
||||
|
||||
## Copyright
|
||||
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This program is released under the terms of the GNU General Public License,
|
||||
version 2. See `COPYING` for full license text.
|
||||
|
||||
@@ -1,71 +1,179 @@
|
||||
"""
|
||||
Common tools for the bot.
|
||||
---
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
||||
General Public License, version 2.
|
||||
---
|
||||
|
||||
`colours`: Colours used by embeds.
|
||||
|
||||
`cat`: Category names for the HelpCommand.
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
|
||||
import collections
|
||||
import json
|
||||
import re
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Union
|
||||
|
||||
import aiohttp
|
||||
|
||||
import discord
|
||||
import discord.ext.commands as commands
|
||||
|
||||
from discord import Emoji, Reaction, PartialEmoji
|
||||
|
||||
import data.options as opt
|
||||
|
||||
|
||||
__all__ = ["colours", "cat", "emojis", "error_embed_factory", "add_react", "check_if_owner"]
|
||||
__all__ = ["colours", "cat", "emojis", "paths", "ImageMetadata", "ImagesGroup",
|
||||
"embed_factory", "error_embed_factory", "add_react", "check_if_owner"]
|
||||
|
||||
|
||||
# --- Common values ---
|
||||
|
||||
colours = SimpleNamespace(good=0x43B581,
|
||||
neutral=0x7289DA,
|
||||
bad=0xF04747)
|
||||
# meow
|
||||
cat = SimpleNamespace(lookup='Information Lookup',
|
||||
fun='Fun',
|
||||
maps='Mapping',
|
||||
ref='Reference',
|
||||
study='Exam Study',
|
||||
weather='Land and Space Weather')
|
||||
colours = SimpleNamespace(
|
||||
good=0x43B581,
|
||||
neutral=0x7289DA,
|
||||
bad=0xF04747,
|
||||
)
|
||||
|
||||
emojis = SimpleNamespace(good='✅',
|
||||
bad='❌')
|
||||
# meow
|
||||
cat = SimpleNamespace(
|
||||
lookup="Information Lookup",
|
||||
fun="Fun",
|
||||
maps="Mapping",
|
||||
ref="Reference",
|
||||
study="Exam Study",
|
||||
weather="Land and Space Weather",
|
||||
admin="Bot Control",
|
||||
)
|
||||
|
||||
emojis = SimpleNamespace(
|
||||
check_mark="✅",
|
||||
x="❌",
|
||||
warning="⚠️",
|
||||
question="❓",
|
||||
no_entry="⛔",
|
||||
bangbang="‼️",
|
||||
a="🇦",
|
||||
b="🇧",
|
||||
c="🇨",
|
||||
d="🇩",
|
||||
)
|
||||
|
||||
paths = SimpleNamespace(
|
||||
data=Path("./data/"),
|
||||
resources=Path("./resources/"),
|
||||
img=Path("./resources/img/"),
|
||||
bandcharts=Path("./resources/img/bandcharts/"),
|
||||
maps=Path("./resources/img/maps/"),
|
||||
)
|
||||
|
||||
|
||||
# --- Classes ---
|
||||
|
||||
|
||||
class ImageMetadata:
|
||||
"""Represents the metadata of a single image."""
|
||||
def __init__(self, metadata: list):
|
||||
self.filename: str = metadata[0]
|
||||
self.name: str = metadata[1]
|
||||
self.long_name: str = metadata[2]
|
||||
self.description: str = metadata[3]
|
||||
self.source: str = metadata[4]
|
||||
self.emoji: str = metadata[5]
|
||||
|
||||
|
||||
class ImagesGroup(collections.abc.Mapping):
|
||||
"""Represents a group of images, loaded from a meta.json file."""
|
||||
def __init__(self, file_path):
|
||||
self._images = {}
|
||||
self.path = file_path
|
||||
|
||||
with open(file_path, "r") as file:
|
||||
images: dict = json.load(file)
|
||||
for key, imgdata in images.items():
|
||||
self._images[key] = ImageMetadata(imgdata)
|
||||
|
||||
# Wrappers to implement dict-like functionality
|
||||
def __len__(self):
|
||||
return len(self._images)
|
||||
|
||||
def __getitem__(self, key: str):
|
||||
return self._images[key]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._images)
|
||||
|
||||
# str(): Simply return what it would be for the underlaying dict
|
||||
def __str__(self):
|
||||
return str(self._images)
|
||||
|
||||
|
||||
# --- Exceptions ---
|
||||
|
||||
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}"
|
||||
super().__init__(msg)
|
||||
self.response = response
|
||||
self.status = response.status
|
||||
self.reason = response.reason
|
||||
|
||||
|
||||
# --- Converters ---
|
||||
|
||||
class GlobalChannelConverter(commands.IDConverter):
|
||||
"""Converter to get any bot-acessible channel by ID/mention (global), or name (in current guild only)."""
|
||||
async def convert(self, ctx: commands.Context, argument: str):
|
||||
bot = ctx.bot
|
||||
guild = ctx.guild
|
||||
match = self._get_id_match(argument) or re.match(r"<#([0-9]+)>$", argument)
|
||||
result = None
|
||||
if match is None:
|
||||
# not a mention/ID
|
||||
if guild:
|
||||
result = discord.utils.get(guild.text_channels, name=argument)
|
||||
else:
|
||||
raise commands.BadArgument(f"""Channel named "{argument}" not found in this guild.""")
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
result = bot.get_channel(channel_id)
|
||||
if not isinstance(result, (discord.TextChannel, discord.abc.PrivateChannel)):
|
||||
raise commands.BadArgument(f"""Channel "{argument}" not found.""")
|
||||
return result
|
||||
|
||||
|
||||
# --- Helper functions ---
|
||||
|
||||
def embed_factory(ctx: commands.Context) -> discord.Embed:
|
||||
"""Creates an embed with neutral colour and standard footer."""
|
||||
embed = discord.Embed(timestamp=datetime.utcnow(), colour=colours.neutral)
|
||||
embed.set_footer(text=str(ctx.author), icon_url=str(ctx.author.avatar_url))
|
||||
return embed
|
||||
|
||||
|
||||
def error_embed_factory(ctx: commands.Context, exception: Exception, debug_mode: bool) -> discord.Embed:
|
||||
"""Creates an Error embed."""
|
||||
if debug_mode:
|
||||
fmtd_ex = traceback.format_exception(exception.__class__, exception, exception.__traceback__)
|
||||
else:
|
||||
fmtd_ex = traceback.format_exception_only(exception.__class__, exception)
|
||||
embed = discord.Embed(title="Error",
|
||||
timestamp=datetime.utcnow(),
|
||||
colour=colours.bad)
|
||||
embed.set_footer(text=ctx.author,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
embed.description = "```\n" + '\n'.join(fmtd_ex) + "```"
|
||||
embed = embed_factory(ctx)
|
||||
embed.title = "⚠️ Error"
|
||||
embed.description = "```\n" + "\n".join(fmtd_ex) + "```"
|
||||
embed.colour = colours.bad
|
||||
return embed
|
||||
|
||||
|
||||
async def add_react(msg: discord.Message, react: str):
|
||||
async def add_react(msg: discord.Message, react: Union[Emoji, Reaction, 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 ---
|
||||
@@ -73,5 +181,4 @@ async def add_react(msg: discord.Message, react: str):
|
||||
async def check_if_owner(ctx: commands.Context):
|
||||
if ctx.author.id in opt.owners_uids:
|
||||
return True
|
||||
await add_react(ctx.message, emojis.bad)
|
||||
return False
|
||||
raise commands.NotOwner
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,24 @@
|
||||
# File metadata format
|
||||
|
||||
Used for grouping info such as name, description, source, and such.
|
||||
|
||||
|
||||
## Format
|
||||
|
||||
*`id` is the dictionary key, and the list items are the attributes for each files.*
|
||||
|
||||
`meta.json`
|
||||
```json
|
||||
{
|
||||
"id": ["filename", "name", "long_name", "description", "source", "emoji"]
|
||||
}
|
||||
```
|
||||
|
||||
| Attribute | Description | Examples |
|
||||
| ------------- | ----------------------------------------------- | ------------------------------------------------ |
|
||||
| `filename` | The file name of the file, without the path. | `ca.png`, `itu.png` |
|
||||
| `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)` |
|
||||
| `emoji` | A Unicode emoji associated with the 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"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
-r requirements.txt
|
||||
flake8
|
||||
discord.py-stubs==1.5.0
|
||||
@@ -1,25 +1,25 @@
|
||||
"""
|
||||
ae7q extension for qrm
|
||||
---
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
||||
General Public License, version 2.
|
||||
---
|
||||
Test callsigns:
|
||||
KN8U: active, restricted
|
||||
AB2EE: expired, restricted
|
||||
KE8FGB: assigned once, no restrictions
|
||||
NA2AAA: unassigned, no records
|
||||
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
|
||||
import discord.ext.commands as commands
|
||||
# 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
|
||||
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
import aiohttp
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
import discord.ext.commands as commands
|
||||
|
||||
import common as cmn
|
||||
|
||||
@@ -27,115 +27,369 @@ import common as cmn
|
||||
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)
|
||||
@commands.group(name="ae7q", aliases=["ae"], case_insensitive=True, category=cmn.cat.lookup)
|
||||
async def _ae7q_lookup(self, ctx: commands.Context):
|
||||
'''Look up a callsign, FRN, or Licensee ID on [ae7q.com](http://ae7q.com/).'''
|
||||
"""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", category=cmn.cat.lookup)
|
||||
@_ae7q_lookup.command(name="call", aliases=["c"], category=cmn.cat.lookup)
|
||||
async def _ae7q_call(self, ctx: commands.Context, callsign: str):
|
||||
'''Look up the history for a callsign on [ae7q.com](http://ae7q.com/).'''
|
||||
callsign = callsign.upper()
|
||||
desc = ''
|
||||
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
|
||||
"""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 aiohttp.ClientSession() as session:
|
||||
async with session.get(base_url + callsign) as resp:
|
||||
async with self.session.get(base_url + callsign) as resp:
|
||||
if resp.status != 200:
|
||||
return await ctx.send('Could not load AE7Q')
|
||||
raise cmn.BotHTTPError(resp)
|
||||
page = await resp.text()
|
||||
|
||||
soup = BeautifulSoup(page, features="html.parser")
|
||||
tables = soup.select("table.Database")
|
||||
soup = BeautifulSoup(page, features="html.parser")
|
||||
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
|
||||
|
||||
for table in tables:
|
||||
rows = table.find_all("tr")
|
||||
if len(rows) > 1 and len(rows[0]) > 1:
|
||||
break
|
||||
if desc == '':
|
||||
for row in rows:
|
||||
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}`')
|
||||
rows = None
|
||||
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..."
|
||||
|
||||
if rows is None:
|
||||
embed = discord.Embed(title=f"AE7Q History for {callsign}",
|
||||
colour=cmn.colours.bad,
|
||||
url=f"{base_url}{callsign}",
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
embed.description = desc
|
||||
embed.description += f'\nNo records found for `{callsign}`'
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
table_contents = [] # store your table here
|
||||
for tr in rows:
|
||||
if rows.index(tr) == 0:
|
||||
continue
|
||||
row_cells = []
|
||||
for td in tr.find_all('td'):
|
||||
if td.getText().strip() != '':
|
||||
row_cells.append(td.getText().strip())
|
||||
else:
|
||||
row_cells.append('-')
|
||||
if 'colspan' in td.attrs and int(td.attrs['colspan']) > 1:
|
||||
for i in range(int(td.attrs['colspan']) - 1):
|
||||
row_cells.append(row_cells[-1])
|
||||
for i, cell in enumerate(row_cells):
|
||||
if cell == '"':
|
||||
cell = table_contents[-1][i]
|
||||
if len(row_cells) > 1:
|
||||
table_contents += [row_cells]
|
||||
@_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)
|
||||
|
||||
embed = discord.Embed(title=f"AE7Q Records for {callsign}",
|
||||
colour=cmn.colours.good,
|
||||
url=f"{base_url}{callsign}",
|
||||
timestamp=datetime.utcnow())
|
||||
async with self.session.get(base_url + callsign) as resp:
|
||||
if resp.status != 200:
|
||||
raise cmn.BotHTTPError(resp)
|
||||
page = await resp.text()
|
||||
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
soup = BeautifulSoup(page, features="html.parser")
|
||||
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
|
||||
|
||||
for row in table_contents[0:3]:
|
||||
header = f'**{row[0]}** ({row[1]})'
|
||||
body = f'Class: *{row[2]}*\n'
|
||||
body += f'Region: *{row[3]}*\n'
|
||||
body += f'Status: *{row[4]}*\n'
|
||||
body += f'Granted: *{row[5]}*\n'
|
||||
body += f'Effective: *{row[6]}*\n'
|
||||
body += f'Cancelled: *{row[7]}*\n'
|
||||
body += f'Expires: *{row[8]}*'
|
||||
embed.add_field(name=header, value=body, inline=False)
|
||||
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
|
||||
|
||||
embed.description = desc
|
||||
if len(table_contents) > 3:
|
||||
embed.description += f'\nRecords 1 to 3 of {len(table_contents)}.'
|
||||
embed.description += ' See ae7q.com for more...'
|
||||
table_headers = table[0].find_all("th")
|
||||
first_header = "".join(table_headers[0].strings) if len(table_headers) > 0 else None
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
# 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
|
||||
|
||||
# TODO: write commands for other AE7Q response types?
|
||||
# @_ae7q_lookup.command(name="trustee")
|
||||
# async def _ae7q_trustee(self, ctx: commands.Context, callsign: str):
|
||||
# pass
|
||||
table = await process_table(table[2:])
|
||||
|
||||
# @_ae7q_lookup.command(name="applications", aliases=['apps'])
|
||||
# async def _ae7q_applications(self, ctx: commands.Context, callsign: str):
|
||||
# pass
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"AE7Q Trustee History for {callsign}"
|
||||
embed.colour = cmn.colours.good
|
||||
embed.url = base_url + callsign
|
||||
|
||||
# @_ae7q_lookup.command(name="frn")
|
||||
# async def _ae7q_frn(self, ctx: commands.Context, frn: str):
|
||||
# base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN="
|
||||
# pass
|
||||
# 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)
|
||||
|
||||
# @_ae7q_lookup.command(name="licensee", aliases=["lic"])
|
||||
# async def _ae7q_licensee(self, ctx: commands.Context, frn: str):
|
||||
# base_url = "http://ae7q.com/query/data/LicenseeIdHistory.php?ID="
|
||||
# pass
|
||||
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
|
||||
|
||||
|
||||
def setup(bot: commands.Bot):
|
||||
|
||||
@@ -1,105 +1,99 @@
|
||||
"""
|
||||
Base extension for qrm
|
||||
---
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
||||
General Public License, version 2.
|
||||
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 re
|
||||
from collections import OrderedDict
|
||||
|
||||
import random
|
||||
import re
|
||||
from typing import Union
|
||||
|
||||
import discord
|
||||
import discord.ext.commands as commands
|
||||
|
||||
import info
|
||||
import common as cmn
|
||||
|
||||
import data.options as opt
|
||||
import common as cmn
|
||||
|
||||
|
||||
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"]})
|
||||
self.verify_checks = True
|
||||
|
||||
def get_bot_mapping(self):
|
||||
async def get_bot_mapping(self):
|
||||
bot = self.context.bot
|
||||
mapping = {}
|
||||
for cmd in bot.commands:
|
||||
cat = cmd.__original_kwargs__.get('category', None)
|
||||
|
||||
for cmd in await self.filter_commands(bot.commands, sort=True):
|
||||
cat = cmd.__original_kwargs__.get("category", None)
|
||||
if cat in mapping:
|
||||
mapping[cat].append(cmd)
|
||||
else:
|
||||
mapping[cat] = [cmd]
|
||||
return mapping
|
||||
|
||||
def get_command_signature(self, command):
|
||||
async def get_command_signature(self, command):
|
||||
parent = command.full_parent_name
|
||||
if command.aliases != []:
|
||||
aliases = ', '.join(command.aliases)
|
||||
aliases = ", ".join(command.aliases)
|
||||
fmt = command.name
|
||||
if parent:
|
||||
fmt = f'{parent} {fmt}'
|
||||
fmt = f"{parent} {fmt}"
|
||||
alias = fmt
|
||||
return f'{opt.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"{opt.display_prefix}{alias} {command.signature}\n *Aliases:* {aliases}"
|
||||
alias = command.name if not parent else f"{parent} {command.name}"
|
||||
return f"{opt.display_prefix}{alias} {command.signature}"
|
||||
|
||||
async def send_error_message(self, error):
|
||||
embed = discord.Embed(title='qrm Help Error',
|
||||
description=error,
|
||||
colour=cmn.colours.bad,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
embed.set_footer(text=self.context.author.name,
|
||||
icon_url=str(self.context.author.avatar_url))
|
||||
embed = cmn.embed_factory(self.context)
|
||||
embed.title = "qrm Help Error"
|
||||
embed.description = error
|
||||
embed.colour = cmn.colours.bad
|
||||
await self.context.send(embed=embed)
|
||||
|
||||
async def send_bot_help(self, mapping):
|
||||
embed = discord.Embed(title='qrm Help',
|
||||
description=(f'For command-specific help and usage, use `{opt.prefix}help [command name]`'
|
||||
'. Many commands have shorter aliases.'),
|
||||
colour=cmn.colours.neutral,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
embed.set_footer(text=self.context.author.name,
|
||||
icon_url=str(self.context.author.avatar_url))
|
||||
embed = cmn.embed_factory(self.context)
|
||||
embed.title = "qrm Help"
|
||||
embed.description = (f"For command-specific help and usage, use `{opt.display_prefix}help [command name]`."
|
||||
" Many commands have shorter aliases.")
|
||||
mapping = await mapping
|
||||
|
||||
for cat, cmds in mapping.items():
|
||||
cmds = list(filter(lambda x: not x.hidden, cmds))
|
||||
if cmds == []:
|
||||
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.title(), value=", ".join(names), inline=False)
|
||||
else:
|
||||
embed.add_field(name='Other', value=', '.join(names), inline=False)
|
||||
embed.add_field(name="Other", value=", ".join(names), inline=False)
|
||||
await self.context.send(embed=embed)
|
||||
|
||||
async def send_command_help(self, command):
|
||||
embed = discord.Embed(title=self.get_command_signature(command),
|
||||
description=command.help,
|
||||
colour=cmn.colours.neutral,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
embed.set_footer(text=self.context.author.name,
|
||||
icon_url=str(self.context.author.avatar_url))
|
||||
if self.verify_checks:
|
||||
if not await command.can_run(self.context):
|
||||
raise commands.CheckFailure
|
||||
for p in command.parents:
|
||||
if not await p.can_run(self.context):
|
||||
raise commands.CheckFailure
|
||||
embed = cmn.embed_factory(self.context)
|
||||
embed.title = await self.get_command_signature(command)
|
||||
embed.description = command.help
|
||||
await self.context.send(embed=embed)
|
||||
|
||||
async def send_group_help(self, group):
|
||||
embed = discord.Embed(title=self.get_command_signature(group),
|
||||
description=group.help,
|
||||
colour=cmn.colours.neutral,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
embed.set_footer(text=self.context.author.name,
|
||||
icon_url=str(self.context.author.avatar_url))
|
||||
for cmd in group.commands:
|
||||
embed.add_field(name=self.get_command_signature(cmd), value=cmd.help, inline=False)
|
||||
if self.verify_checks and not await group.can_run(self.context):
|
||||
raise commands.CheckFailure
|
||||
embed = cmn.embed_factory(self.context)
|
||||
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)
|
||||
await self.context.send(embed=embed)
|
||||
|
||||
|
||||
@@ -111,91 +105,119 @@ class BaseCog(commands.Cog):
|
||||
@commands.command(name="info", aliases=["about"])
|
||||
async def _info(self, ctx: commands.Context):
|
||||
"""Shows info about qrm."""
|
||||
embed = discord.Embed(title="About qrm",
|
||||
description=info.description,
|
||||
colour=cmn.colours.neutral,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
|
||||
embed = embed.add_field(name="Authors", value=", ".join(info.authors))
|
||||
embed = embed.add_field(name="License", value=info.license)
|
||||
embed = embed.add_field(name="Version", value=f'v{info.release}')
|
||||
embed = embed.add_field(name="Contributing", value=info.contributing, inline=False)
|
||||
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="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))
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="ping")
|
||||
@commands.command(name="ping", aliases=["beep"])
|
||||
async def _ping(self, ctx: commands.Context):
|
||||
"""Show the current latency to the discord endpoint."""
|
||||
content = ctx.message.author.mention if random.random() < 0.05 else ''
|
||||
embed = discord.Embed(title="**Pong!**",
|
||||
description=f'Current ping is {self.bot.latency*1000:.1f} ms',
|
||||
colour=cmn.colours.neutral,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
await ctx.send(content=content, embed=embed)
|
||||
"""Shows the current latency to the discord endpoint."""
|
||||
embed = cmn.embed_factory(ctx)
|
||||
content = ""
|
||||
if ctx.invoked_with == "beep":
|
||||
embed.title = "**Boop!**"
|
||||
else:
|
||||
content = ctx.message.author.mention if random.random() < 0.05 else ""
|
||||
embed.title = "🏓 **Pong!**"
|
||||
embed.description = f"Current ping is {self.bot.latency*1000:.1f} ms"
|
||||
await ctx.send(content, embed=embed)
|
||||
|
||||
@commands.command(name="changelog", aliases=["clog"])
|
||||
async def _changelog(self, ctx: commands.Context):
|
||||
"""Show what has changed in the most recent bot version."""
|
||||
embed = discord.Embed(title="qrm Changelog",
|
||||
description=("For a full listing, visit [Github](https://"
|
||||
"github.com/classabbyamp/discord-qrm2/blob/master/CHANGELOG.md)."),
|
||||
colour=cmn.colours.neutral,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
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)
|
||||
embed.title = "qrm Changelog"
|
||||
embed.description = ("For a full listing, visit [Github](https://"
|
||||
"github.com/miaowware/qrm2/blob/master/CHANGELOG.md).")
|
||||
changelog = self.changelog
|
||||
vers = list(changelog.keys())
|
||||
vers.remove("Unreleased")
|
||||
|
||||
vers = 0
|
||||
for ver, log in changelog.items():
|
||||
if ver.lower() != 'unreleased':
|
||||
if 'date' in log:
|
||||
embed.description += f'\n\n**{ver}** ({log["date"]})'
|
||||
else:
|
||||
embed.description += f'\n\n**{ver}**'
|
||||
embed = await format_changelog(log, embed)
|
||||
vers += 1
|
||||
if vers >= 1:
|
||||
break
|
||||
version = version.lower()
|
||||
|
||||
if version == "latest":
|
||||
version = info.release
|
||||
if version == "unreleased":
|
||||
version = "Unreleased"
|
||||
|
||||
try:
|
||||
log = changelog[version]
|
||||
except KeyError:
|
||||
embed.title += ": Version Not Found"
|
||||
embed.description += "\n\n**Valid versions:** latest, "
|
||||
embed.description += ", ".join(vers)
|
||||
embed.colour = cmn.colours.bad
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
if "date" in log:
|
||||
embed.description += f"\n\n**v{version}** ({log['date']})"
|
||||
else:
|
||||
embed.description += f"\n\n**v{version}**"
|
||||
embed = await format_changelog(log, embed)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="issue")
|
||||
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)!")
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="echo", aliases=["e"], category=cmn.cat.admin)
|
||||
@commands.check(cmn.check_if_owner)
|
||||
async def _echo(self, ctx: commands.Context,
|
||||
channel: Union[cmn.GlobalChannelConverter, commands.UserConverter], *, msg: str):
|
||||
"""Sends a message in a channel as qrm. Accepts channel/user IDs/mentions.
|
||||
Channel names are current-guild only.
|
||||
Does not work with the ID of the bot user."""
|
||||
if isinstance(channel, discord.ClientUser):
|
||||
raise commands.BadArgument("Can't send to the bot user!")
|
||||
await channel.send(msg)
|
||||
|
||||
|
||||
def parse_changelog():
|
||||
changelog = OrderedDict()
|
||||
ver = ''
|
||||
heading = ''
|
||||
changelog = {}
|
||||
ver = ""
|
||||
heading = ""
|
||||
|
||||
with open('CHANGELOG.md') as changelog_file:
|
||||
with open("CHANGELOG.md") as changelog_file:
|
||||
for line in changelog_file.readlines():
|
||||
if line.strip() == '':
|
||||
if line.strip() == "":
|
||||
continue
|
||||
if re.match(r'##[^#]', line):
|
||||
ver_match = re.match(r'\[(.+)\](?: - )?(\d{4}-\d{2}-\d{2})?', line.lstrip('#').strip())
|
||||
if re.match(r"##[^#]", line):
|
||||
ver_match = re.match(r"\[(.+)\](?: - )?(\d{4}-\d{2}-\d{2})?", line.lstrip("#").strip())
|
||||
if ver_match is not None:
|
||||
ver = ver_match.group(1)
|
||||
changelog[ver] = dict()
|
||||
if ver_match.group(2):
|
||||
changelog[ver]['date'] = ver_match.group(2)
|
||||
elif re.match(r'###[^#]', line):
|
||||
heading = line.lstrip('#').strip()
|
||||
changelog[ver]["date"] = ver_match.group(2)
|
||||
elif re.match(r"###[^#]", line):
|
||||
heading = line.lstrip("#").strip()
|
||||
changelog[ver][heading] = []
|
||||
elif ver != '' and heading != '':
|
||||
if line.startswith('-'):
|
||||
changelog[ver][heading].append(line.lstrip('-').strip())
|
||||
elif ver != "" and heading != "":
|
||||
if line.startswith("-"):
|
||||
changelog[ver][heading].append(line.lstrip("-").strip())
|
||||
return changelog
|
||||
|
||||
|
||||
async def format_changelog(log: dict, embed: discord.Embed):
|
||||
for header, lines in log.items():
|
||||
formatted = ''
|
||||
if header != 'date':
|
||||
formatted = ""
|
||||
if header != "date":
|
||||
for line in lines:
|
||||
formatted += f'- {line}\n'
|
||||
embed.add_field(name=f'**{header}**', value=formatted, inline=False)
|
||||
formatted += f"- {line}\n"
|
||||
embed.add_field(name=f"**{header}**", value=formatted, inline=False)
|
||||
return embed
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
Conversion extension for qrm
|
||||
---
|
||||
Copyright (C) 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 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.cat.ref)
|
||||
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)
|
||||
@@ -1,12 +1,16 @@
|
||||
"""
|
||||
Fun extension for qrm
|
||||
---
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
||||
General Public License, version 2.
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
|
||||
import random
|
||||
|
||||
import discord
|
||||
import discord.ext.commands as commands
|
||||
|
||||
import common as cmn
|
||||
@@ -15,21 +19,54 @@ import common as cmn
|
||||
class FunCog(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
with open(cmn.paths.resources / "words") as words_file:
|
||||
self.words = words_file.read().lower().splitlines()
|
||||
|
||||
@commands.command(name="xkcd", aliases=['x'], category=cmn.cat.fun)
|
||||
@commands.command(name="xkcd", aliases=["x"], category=cmn.cat.fun)
|
||||
async def _xkcd(self, ctx: commands.Context, number: str):
|
||||
'''Look up an xkcd by number.'''
|
||||
await ctx.send('http://xkcd.com/' + number)
|
||||
"""Looks up an xkcd comic by number."""
|
||||
await ctx.send("http://xkcd.com/" + number)
|
||||
|
||||
@commands.command(name="tar", category=cmn.cat.fun)
|
||||
async def _tar(self, ctx: commands.Context):
|
||||
'''Returns an xkcd about tar.'''
|
||||
await ctx.send('http://xkcd.com/1168')
|
||||
"""Returns xkcd: tar."""
|
||||
await ctx.send("http://xkcd.com/1168")
|
||||
|
||||
@commands.command(name="standards", category=cmn.cat.fun)
|
||||
async def _standards(self, ctx: commands.Context):
|
||||
"""Returns xkcd: Standards."""
|
||||
await ctx.send("http://xkcd.com/927")
|
||||
|
||||
@commands.command(name="worksplit", aliases=["split", "ft8"], category=cmn.cat.fun)
|
||||
async def _worksplit(self, ctx: commands.Context):
|
||||
"""Posts "Work split you lids"."""
|
||||
fn = "worksplit.jpg"
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = "Work Split, You Lids!"
|
||||
embed.set_image(url="attachment://" + fn)
|
||||
img = discord.File(cmn.paths.img / fn, filename=fn)
|
||||
await ctx.send(embed=embed, file=img)
|
||||
|
||||
@commands.command(name="xd", hidden=True, category=cmn.cat.fun)
|
||||
async def _xd(self, ctx: commands.Context):
|
||||
'''ecks dee'''
|
||||
await ctx.send('ECKS DEE :smirk:')
|
||||
"""ecks dee"""
|
||||
await ctx.send("ECKS DEE :smirk:")
|
||||
|
||||
@commands.command(name="funetics", aliases=["fun"], category=cmn.cat.fun)
|
||||
async def _funetics_lookup(self, ctx: commands.Context, *, msg: str):
|
||||
"""Generates fun/wacky phonetics for a word or phrase."""
|
||||
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)
|
||||
|
||||
|
||||
def setup(bot: commands.Bot):
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
"""
|
||||
Grid extension for qrm
|
||||
---
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
||||
General Public License, version 2.
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
import math
|
||||
from datetime import datetime
|
||||
|
||||
import discord
|
||||
import math
|
||||
|
||||
import discord.ext.commands as commands
|
||||
|
||||
import common as cmn
|
||||
@@ -22,130 +21,115 @@ class GridCog(commands.Cog):
|
||||
|
||||
@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 = "**"
|
||||
try:
|
||||
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 = discord.Embed(title=f'Maidenhead Grid Locator for {float(lat):.6f}, {float(lon):.6f}',
|
||||
description=grid,
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
else:
|
||||
raise ValueError('Out of range.')
|
||||
except ValueError as err:
|
||||
msg = f'Error generating grid square for {lat}, {lon}.'
|
||||
embed = discord.Embed(title=msg, description=str(err),
|
||||
colour=cmn.colours.bad,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
("""Calculates the grid square for latitude and longitude coordinates, """
|
||||
"""with negative being latitude South and longitude West.""")
|
||||
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
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="ungrid", aliases=['loc'], category=cmn.cat.maps)
|
||||
@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)
|
||||
"""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."""
|
||||
if grid2 is None or grid2 == "":
|
||||
try:
|
||||
grid = grid.upper()
|
||||
loc = get_coords(grid)
|
||||
|
||||
if len(grid) >= 6:
|
||||
embed = discord.Embed(title=f'Latitude and Longitude for {grid}',
|
||||
description=f'**{loc[0]:.5f}, {loc[1]:.5f}**',
|
||||
colour=cmn.colours.good,
|
||||
url=f'https://www.openstreetmap.org/#map=13/{loc[0]:.5f}/{loc[1]:.5f}',
|
||||
timestamp=datetime.utcnow())
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Latitude and Longitude for {grid}"
|
||||
embed.colour = cmn.colours.good
|
||||
|
||||
else:
|
||||
embed = discord.Embed(title=f'Latitude and Longitude for {grid}',
|
||||
description=f'**{loc[0]:.1f}, {loc[1]:.1f}**',
|
||||
colour=cmn.colours.good,
|
||||
url=f'https://www.openstreetmap.org/#map=10/{loc[0]:.1f}/{loc[1]:.1f}',
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
except Exception as e:
|
||||
msg = f'Error generating latitude and longitude for grid {grid}.'
|
||||
embed = discord.Embed(title=msg, description=str(e),
|
||||
colour=cmn.colours.bad,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
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
|
||||
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
|
||||
# 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
|
||||
|
||||
des = f'**Distance:** {d:.1f} km ({d_mi:.1f} mi)\n**Bearing:** {bearing:.1f}°'
|
||||
embed = discord.Embed(title=f'Great Circle Distance and Bearing from {grid} to {grid2}',
|
||||
description=des,
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
except Exception as e:
|
||||
msg = f'Error generating great circle distance and bearing from {grid} and {grid2}.'
|
||||
embed = discord.Embed(title=msg, description=str(e),
|
||||
colour=cmn.colours.bad,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Great Circle Distance and Bearing from {grid} to {grid2}"
|
||||
embed.description = f"**Distance:** {d:.1f} km ({d_mi:.1f} mi)\n**Bearing:** {bearing:.1f}°"
|
||||
embed.colour = cmn.colours.good
|
||||
except Exception as e:
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Error generating great circle distance and bearing from {grid} and {grid2}."
|
||||
embed.description = str(e)
|
||||
embed.colour = cmn.colours.bad
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def get_coords(grid: str):
|
||||
if len(grid) < 3:
|
||||
raise ValueError('The grid locator must be at least 4 characters long.')
|
||||
raise ValueError("The grid locator must be at least 4 characters long.")
|
||||
|
||||
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##.')
|
||||
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.')
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -1,117 +1,120 @@
|
||||
"""
|
||||
Ham extension for qrm
|
||||
---
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
||||
General Public License, version 2.
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
import json
|
||||
import random
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import discord
|
||||
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
|
||||
with open('resources/qcodes.json') as qcode_file:
|
||||
self.qcodes = json.load(qcode_file)
|
||||
with open('resources/words') as words_file:
|
||||
self.words = words_file.read().lower().splitlines()
|
||||
self.pfxs = callsign_info.options
|
||||
|
||||
@commands.command(name="qcode", aliases=['q'], category=cmn.cat.ref)
|
||||
@commands.command(name="qcode", aliases=["q"], category=cmn.cat.ref)
|
||||
async def _qcode_lookup(self, ctx: commands.Context, qcode: str):
|
||||
'''Look up a Q Code.'''
|
||||
with ctx.typing():
|
||||
qcode = qcode.upper()
|
||||
if qcode in self.qcodes:
|
||||
embed = discord.Embed(title=qcode, description=self.qcodes[qcode],
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
else:
|
||||
embed = discord.Embed(title=f'Q Code {qcode} not found',
|
||||
colour=cmn.colours.bad,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
"""Looks up the meaning of a Q Code."""
|
||||
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.fun)
|
||||
@commands.command(name="phonetics", aliases=["ph", "phoneticize", "phoneticise", "phone"], category=cmn.cat.ref)
|
||||
async def _phonetics_lookup(self, ctx: commands.Context, *, msg: str):
|
||||
'''Get phonetics for a word or phrase.'''
|
||||
with ctx.typing():
|
||||
result = ''
|
||||
for char in msg.lower():
|
||||
if char.isalpha():
|
||||
result += random.choice([word for word in self.words if word[0] == char])
|
||||
else:
|
||||
result += char
|
||||
result += ' '
|
||||
embed = discord.Embed(title=f'Phonetics for {msg}',
|
||||
description=result.title(),
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
"""Returns NATO phonetics for a word or phrase."""
|
||||
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)
|
||||
@commands.command(name="utc", aliases=["z"], category=cmn.cat.ref)
|
||||
async def _utc_lookup(self, ctx: commands.Context):
|
||||
'''Gets the current time in UTC.'''
|
||||
with ctx.typing():
|
||||
now = datetime.utcnow()
|
||||
result = '**' + now.strftime('%Y-%m-%d %H:%M') + 'Z**'
|
||||
embed = discord.Embed(title='The current time is:',
|
||||
description=result,
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
"""Returns the current time in UTC."""
|
||||
now = datetime.utcnow()
|
||||
result = "**" + now.strftime("%Y-%m-%d %H:%M") + "Z**"
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = "The current time is:"
|
||||
embed.description = result
|
||||
embed.colour = cmn.colours.good
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="prefixes", aliases=["vanity", "pfx", "vanities", "prefix"])
|
||||
async def _vanity_prefixes(self, ctx: commands.Context, country: str = None):
|
||||
'''Lists valid prefixes for countries.'''
|
||||
if country is None:
|
||||
await ctx.send_help(ctx.command)
|
||||
return
|
||||
if country.lower() not in callsign_info.options:
|
||||
embed = discord.Embed(title=f'{country} not found!',
|
||||
description=f'Valid countries: {", ".join(callsign_info.options.keys())}',
|
||||
colour=cmn.colours.bad,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
@commands.command(name="prefixes", aliases=["vanity", "pfx", "vanities", "prefix"], category=cmn.cat.ref)
|
||||
async def _vanity_prefixes(self, ctx: commands.Context, country: str = ""):
|
||||
"""Lists valid callsign prefixes for different countries."""
|
||||
country = country.lower()
|
||||
embed = cmn.embed_factory(ctx)
|
||||
if country not in self.pfxs:
|
||||
desc = "Possible arguments are:\n"
|
||||
for key, val in self.pfxs.items():
|
||||
desc += f"`{key}`: {val.title}{(' ' + val.emoji if val.emoji else '')}\n"
|
||||
embed.title = f"{country} Not Found!"
|
||||
embed.description = desc
|
||||
embed.colour = cmn.colours.bad
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
embed = discord.Embed(title=callsign_info.options[country.lower()][0],
|
||||
description=callsign_info.options[country.lower()][1],
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
|
||||
for name, val in callsign_info.options[country.lower()][2].items():
|
||||
embed.add_field(name=name, value=val, inline=False)
|
||||
else:
|
||||
data = self.pfxs[country]
|
||||
embed.title = data.title + (" " + data.emoji if data.emoji else "")
|
||||
embed.description = data.desc
|
||||
embed.colour = cmn.colours.good
|
||||
|
||||
for name, val in data.calls.items():
|
||||
embed.add_field(name=name, value=val, inline=False)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="contests", aliases=["cc", "tests"], category=cmn.cat.ref)
|
||||
async def _contests(self, ctx: commands.Context):
|
||||
embed = discord.Embed(title="Contest Calendar",
|
||||
timestamp=datetime.utcnow(),
|
||||
colour=cmn.colours.good)
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = "Contest Calendar"
|
||||
embed.description = ("*We are currently rewriting the old, Chrome-based `contests` command. In the meantime, "
|
||||
"use [the website](https://www.contestcalendar.com/weeklycont.php).*")
|
||||
embed.colour = cmn.colours.good
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="phoneticweight", aliases=["pw"], category=cmn.cat.ref)
|
||||
async def _weight(self, ctx: commands.Context, *, msg: str):
|
||||
"""Calculates the phonetic weight of a callsign or message."""
|
||||
embed = cmn.embed_factory(ctx)
|
||||
msg = msg.upper()
|
||||
weight = 0
|
||||
for char in msg:
|
||||
try:
|
||||
weight += phonetics.pweights[char]
|
||||
except KeyError:
|
||||
embed.title = "Error in calculation of phonetic weight"
|
||||
embed.description = f"Unknown character `{char}` in message"
|
||||
embed.colour = cmn.colours.bad
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
embed.title = f"Phonetic Weight of {msg}"
|
||||
embed.description = f"The phonetic weight is **{weight}**"
|
||||
embed.colour = cmn.colours.good
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
|
||||
@@ -1,116 +1,93 @@
|
||||
"""
|
||||
Image extension for qrm
|
||||
---
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
||||
General Public License, version 2.
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
import io
|
||||
from datetime import datetime
|
||||
|
||||
import aiohttp
|
||||
|
||||
import discord
|
||||
import discord.ext.commands as commands
|
||||
|
||||
import aiohttp
|
||||
|
||||
import common as cmn
|
||||
|
||||
|
||||
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.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 = ''):
|
||||
'''Posts an image of Frequency Allocations.'''
|
||||
name = {'cn': 'Chinese',
|
||||
'ca': 'Canadian',
|
||||
'nl': 'Dutch',
|
||||
'us': 'US',
|
||||
'mx': 'Mexican'}
|
||||
arg = region.lower()
|
||||
|
||||
with ctx.typing():
|
||||
if arg not in name:
|
||||
desc = 'Possible arguments are:\n'
|
||||
for abbrev, title in name.items():
|
||||
desc += f'`{abbrev}`: {title}\n'
|
||||
embed = discord.Embed(title=f'Bandplan Not Found!',
|
||||
description=desc,
|
||||
colour=cmn.colours.bad,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
@commands.command(name="bandplan", aliases=["plan", "bands"], category=cmn.cat.ref)
|
||||
async def _bandplan(self, ctx: commands.Context, region: 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)
|
||||
else:
|
||||
img = discord.File(f"resources/images/bandchart/{arg}bandchart.png",
|
||||
filename=f'{arg}bandchart.png')
|
||||
embed = discord.Embed(title=f'{name[arg]} Amateur Radio Bands',
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_image(url=f'attachment://{arg}bandchart.png')
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
|
||||
await ctx.send(embed=embed, file=img)
|
||||
|
||||
@commands.command(name="grayline", aliases=['greyline', 'grey', 'gray', 'gl'], category=cmn.cat.maps)
|
||||
async def _grayline(self, ctx: commands.Context):
|
||||
'''Posts a map of the current greyline, where HF propagation is the best.'''
|
||||
gl_url = ('http://www.fourmilab.ch/cgi-bin/uncgi/Earth?img=NOAAtopo.evif'
|
||||
'&imgsize=320&dynimg=y&opt=-p&lat=&lon=&alt=&tle=&date=0&utc=&jd=')
|
||||
with ctx.typing():
|
||||
embed = discord.Embed(title='Current Greyline Conditions',
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(gl_url) as resp:
|
||||
if resp.status != 200:
|
||||
embed.description = 'Could not download file...'
|
||||
embed.colour = cmn.colours.bad
|
||||
else:
|
||||
data = io.BytesIO(await resp.read())
|
||||
embed.set_image(url=f'attachment://greyline.jpg')
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
await ctx.send(embed=embed, file=discord.File(data, 'greyline.jpg'))
|
||||
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)
|
||||
|
||||
@commands.command(name="map", category=cmn.cat.maps)
|
||||
async def _map(self, ctx: commands.Context, map_id: str = ''):
|
||||
'''Posts an image of a ham-relevant map.'''
|
||||
map_titles = {"cq": 'Worldwide CQ Zones Map',
|
||||
"itu": 'Worldwide ITU Zones Map',
|
||||
"arrl": 'ARRL/RAC Section Map',
|
||||
"rac": 'ARRL/RAC Section Map',
|
||||
"cn": 'Chinese Callsign Areas',
|
||||
"us": 'US Callsign Areas'}
|
||||
|
||||
arg = map_id.lower()
|
||||
with ctx.typing():
|
||||
if arg not in map_titles:
|
||||
desc = 'Possible arguments are:\n'
|
||||
for abbrev, title in map_titles.items():
|
||||
desc += f'`{abbrev}`: {title}\n'
|
||||
embed = discord.Embed(title=f'Map Not Found!',
|
||||
description=desc,
|
||||
colour=cmn.colours.bad,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
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)
|
||||
else:
|
||||
img = discord.File(f"resources/images/map/{arg}map.png",
|
||||
filename=f'{arg}map.png')
|
||||
embed = discord.Embed(title=f'{map_titles[arg]} Map',
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_image(url=f'attachment://{arg}map.png')
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
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=embed, file=img)
|
||||
@commands.command(name="grayline", aliases=["greyline", "grey", "gray", "gl"], category=cmn.cat.maps)
|
||||
async def _grayline(self, ctx: commands.Context):
|
||||
"""Gets a map of the current greyline, where HF propagation is the best."""
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = "Current Greyline Conditions"
|
||||
embed.colour = cmn.colours.good
|
||||
embed.set_image(url=self.gl_url)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def setup(bot: commands.Bot):
|
||||
|
||||
@@ -1,80 +1,77 @@
|
||||
"""
|
||||
Lookup extension for qrm
|
||||
---
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
||||
General Public License, version 2.
|
||||
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 threading
|
||||
|
||||
import discord
|
||||
from discord.ext import commands, tasks
|
||||
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 LookupCog(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
try:
|
||||
self.cty = BigCty('./data/cty.json')
|
||||
self.cty = BigCty(cty_path)
|
||||
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')
|
||||
# """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')
|
||||
# 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)
|
||||
@commands.command(name="dxcc", aliases=["dx"], category=cmn.cat.lookup)
|
||||
async def _dxcc_lookup(self, ctx: commands.Context, query: str):
|
||||
'''Gets info about a DXCC prefix.'''
|
||||
with ctx.typing():
|
||||
query = query.upper()
|
||||
full_query = query
|
||||
embed = discord.Embed(title=f'DXCC Info for ',
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=f'{ctx.author.name}',
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
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'])\
|
||||
.add_field(name="CQ Zone",
|
||||
value=data['cq'])\
|
||||
.add_field(name="ITU Zone",
|
||||
value=data['itu'])\
|
||||
.add_field(name="Continent",
|
||||
value=data['continent'])\
|
||||
.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]
|
||||
"""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:
|
||||
embed.title += full_query + ' not found'
|
||||
embed.colour = cmn.colours.bad
|
||||
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 = threading.Thread(target=run_update, args=(self.cty, cty_path))
|
||||
update.start()
|
||||
|
||||
|
||||
|
||||
@@ -1,91 +1,78 @@
|
||||
"""
|
||||
Morse Code extension for qrm
|
||||
---
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
||||
General Public License, version 2.
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
import discord
|
||||
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('resources/morse.json') as morse_file:
|
||||
self.ascii2morse = json.load(morse_file)
|
||||
self.morse2ascii = {v: k for k, v in self.ascii2morse.items()}
|
||||
|
||||
@commands.command(name="morse", aliases=['cw'], category=cmn.cat.ref)
|
||||
@commands.command(name="morse", aliases=["cw"], category=cmn.cat.ref)
|
||||
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 += self.ascii2morse[char]
|
||||
except KeyError:
|
||||
result += '<?>'
|
||||
result += ' '
|
||||
embed = discord.Embed(title=f'Morse Code for {msg}',
|
||||
description=result,
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
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
|
||||
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.cat.ref)
|
||||
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 += self.morse2ascii[char]
|
||||
except KeyError:
|
||||
result += '<?>'
|
||||
result += ' '
|
||||
embed = discord.Embed(title=f'ASCII for {msg0}',
|
||||
description=result,
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
"""Converts international morse code to ASCII."""
|
||||
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
|
||||
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.cat.ref)
|
||||
async def _weight(self, ctx: commands.Context, *, msg: str):
|
||||
'''Calculates the CW Weight of a callsign or message.'''
|
||||
with ctx.typing():
|
||||
msg = msg.upper()
|
||||
weight = 0
|
||||
for char in msg:
|
||||
try:
|
||||
cw_char = self.ascii2morse[char].replace('-', '==')
|
||||
weight += len(cw_char) * 2 + 2
|
||||
except KeyError:
|
||||
res = f'Unknown character {char} in callsign'
|
||||
await ctx.send(res)
|
||||
return
|
||||
res = f'The CW weight is **{weight}**'
|
||||
embed = discord.Embed(title=f'CW Weight of {msg}',
|
||||
description=res,
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
"""Calculates the CW weight of a callsign or message."""
|
||||
embed = cmn.embed_factory(ctx)
|
||||
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
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
|
||||
@@ -1,99 +1,97 @@
|
||||
"""
|
||||
QRZ extension for qrm
|
||||
---
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
||||
General Public License, version 2.
|
||||
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 datetime import datetime
|
||||
from io import BytesIO
|
||||
|
||||
import discord
|
||||
from discord.ext import commands, tasks
|
||||
|
||||
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()
|
||||
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):
|
||||
'''Look up a callsign on [QRZ.com](https://www.qrz.com/).'''
|
||||
if keys.qrz_user == '' or keys.qrz_pass == '':
|
||||
await ctx.send(f'http://qrz.com/db/{callsign}')
|
||||
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})')
|
||||
resp_xml = etree.parse(BytesIO(await resp.read())).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']:
|
||||
async with ctx.typing():
|
||||
try:
|
||||
await qrz_test_session(self.key, self.session)
|
||||
except ConnectionError:
|
||||
await self.get_session()
|
||||
await self._qrz_lookup(ctx, callsign)
|
||||
return
|
||||
if 'Not found' in resp_session['Error']:
|
||||
embed = discord.Embed(title=f"QRZ Data for {callsign.upper()}",
|
||||
colour=cmn.colours.bad,
|
||||
description='No data found!',
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
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()}
|
||||
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()
|
||||
|
||||
embed = discord.Embed(title=f"QRZ Data for {resp_data['call']}",
|
||||
colour=cmn.colours.good,
|
||||
url=f'http://www.qrz.com/db/{resp_data["call"]}',
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
if 'image' in resp_data:
|
||||
embed.set_thumbnail(url=resp_data['image'])
|
||||
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"])
|
||||
|
||||
data = qrz_process_info(resp_data)
|
||||
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()}
|
||||
|
||||
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)
|
||||
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:
|
||||
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:
|
||||
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):
|
||||
@@ -101,84 +99,83 @@ class QRZCog(commands.Cog):
|
||||
|
||||
|
||||
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'
|
||||
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})')
|
||||
resp_xml = etree.parse(BytesIO(await resp.read())).getroot()
|
||||
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']
|
||||
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}'
|
||||
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})')
|
||||
resp_xml = etree.parse(BytesIO(await resp.read())).getroot()
|
||||
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'])
|
||||
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']
|
||||
if "name" in data:
|
||||
if "fname" in data:
|
||||
name = data["fname"] + " " + data["name"]
|
||||
else:
|
||||
name = data['name']
|
||||
name = data["name"]
|
||||
else:
|
||||
name = None
|
||||
if 'state' in data:
|
||||
state = f', {data["state"]}'
|
||||
if "state" in data:
|
||||
state = f", {data['state']}"
|
||||
else:
|
||||
state = ''
|
||||
address = data.get('addr1', '') + '\n' + data.get('addr2', '') + \
|
||||
state + ' ' + data.get('zip', '')
|
||||
if 'eqsl' in data:
|
||||
eqsl = 'Yes' if data['eqsl'] == 1 else 'No'
|
||||
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'
|
||||
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'
|
||||
mqsl = "Unknown"
|
||||
if "lotw" in data:
|
||||
lotw = "Yes" if data["lotw"] == "1" else "No"
|
||||
else:
|
||||
lotw = 'Unknown'
|
||||
lotw = "Unknown"
|
||||
|
||||
return OrderedDict([('Name', name),
|
||||
('Country', data.get('country', None)),
|
||||
('Address', address),
|
||||
('Grid Square', data.get('grid', None)),
|
||||
('County', data.get('county', None)),
|
||||
('CQ Zone', data.get('cqzone', None)),
|
||||
('ITU Zone', data.get('ituzone', None)),
|
||||
('IOTA Designator', data.get('iota', None)),
|
||||
('Expires', data.get('expdate', None)),
|
||||
('Aliases', data.get('aliases', None)),
|
||||
('Previous Callsign', data.get('p_call', None)),
|
||||
('License Class', data.get('class', None)),
|
||||
('eQSL?', eqsl),
|
||||
('Paper QSL?', mqsl),
|
||||
('LotW?', lotw),
|
||||
('QSL Info', data.get('qslmgr', None)),
|
||||
('CQ Zone', data.get('cqzone', None)),
|
||||
('ITU Zone', data.get('ituzone', None)),
|
||||
('IOTA Designator', data.get('iota', None)),
|
||||
('Born', data.get('born', None)),
|
||||
])
|
||||
return {"Name": name,
|
||||
"Country": data.get("country", None),
|
||||
"Address": address,
|
||||
"Grid Square": data.get("grid", None),
|
||||
"County": data.get("county", None),
|
||||
"CQ Zone": data.get("cqzone", None),
|
||||
"ITU Zone": data.get("ituzone", None),
|
||||
"IOTA Designator": data.get("iota", None),
|
||||
"Expires": data.get("expdate", None),
|
||||
"Aliases": data.get("aliases", None),
|
||||
"Previous Callsign": data.get("p_call", None),
|
||||
"License Class": data.get("class", None),
|
||||
"Trustee": data.get("trustee", None),
|
||||
"eQSL?": eqsl,
|
||||
"Paper QSL?": mqsl,
|
||||
"LotW?": lotw,
|
||||
"QSL Info": data.get("qslmgr", None),
|
||||
"Born": data.get("born", None)}
|
||||
|
||||
|
||||
def setup(bot):
|
||||
|
||||
@@ -1,122 +1,184 @@
|
||||
"""
|
||||
Study extension for qrm
|
||||
---
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
||||
General Public License, version 2.
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
|
||||
import random
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
import discord
|
||||
import discord.ext.commands as commands
|
||||
import asyncio
|
||||
|
||||
import aiohttp
|
||||
|
||||
import discord.ext.commands as commands
|
||||
|
||||
import common as cmn
|
||||
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"}
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.lastq = dict()
|
||||
self.source = 'Data courtesy of [HamStudy.org](https://hamstudy.org/)'
|
||||
|
||||
@commands.command(name="hamstudy", aliases=['rq', 'randomquestion', 'randomq'], category=cmn.cat.study)
|
||||
async def _random_question(self, ctx: commands.Context, level: str = None):
|
||||
'''Gets a random question from the Technician, General, and/or Extra question pools.'''
|
||||
tech_pool = 'E2_2018'
|
||||
gen_pool = 'E3_2019'
|
||||
extra_pool = 'E4_2016'
|
||||
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 = ""):
|
||||
"""Gets a random question from [HamStudy's](https://hamstudy.org) question pools."""
|
||||
with ctx.typing():
|
||||
selected_pool = None
|
||||
try:
|
||||
level = level.lower()
|
||||
except AttributeError: # no level given (it's None)
|
||||
pass
|
||||
embed = cmn.embed_factory(ctx)
|
||||
|
||||
if level in ['t', 'technician', 'tech']:
|
||||
selected_pool = tech_pool
|
||||
country = country.lower()
|
||||
level = level.lower()
|
||||
|
||||
if level in ['g', 'gen', 'general']:
|
||||
selected_pool = gen_pool
|
||||
if country in study.pool_names.keys():
|
||||
if level in study.pool_names[country].keys():
|
||||
pool_name = study.pool_names[country][level]
|
||||
|
||||
if level in ['e', 'ae', 'extra']:
|
||||
selected_pool = extra_pool
|
||||
elif level in ("random", "r"):
|
||||
# select a random level in that country
|
||||
pool_name = random.choice(list(study.pool_names[country].values()))
|
||||
|
||||
if (level is None) or (level == 'all'): # no pool given or user wants all, so pick a random pool
|
||||
selected_pool = random.choice([tech_pool, gen_pool, extra_pool])
|
||||
if (level is not None) and (selected_pool is None): # unrecognized pool given by user
|
||||
await ctx.send('The question pool you gave was unrecognized. ' +
|
||||
'There are many ways to call up certain question pools - try ?rq t, g, or e. ' +
|
||||
'(Note that only the US question pools are available).')
|
||||
else:
|
||||
# show list of possible pools
|
||||
embed.title = "Pool Not Found!"
|
||||
embed.description = "Possible arguments are:"
|
||||
embed.colour = cmn.colours.bad
|
||||
for cty in study.pool_names:
|
||||
levels = "`, `".join(study.pool_names[cty].keys())
|
||||
embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**",
|
||||
value=f"Levels: `{levels}`", inline=False)
|
||||
embed.add_field(name="**Random**", value="To select a random pool or country, use `random` or `r`")
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
elif country in ("random", "r"):
|
||||
# select a random country and level
|
||||
country = random.choice(list(study.pool_names.keys()))
|
||||
pool_name = random.choice(list(study.pool_names[country].values()))
|
||||
|
||||
else:
|
||||
# show list of possible pools
|
||||
embed.title = "Pool Not Found!"
|
||||
embed.description = "Possible arguments are:"
|
||||
embed.colour = cmn.colours.bad
|
||||
for cty in study.pool_names:
|
||||
levels = "`, `".join(study.pool_names[cty].keys())
|
||||
embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**",
|
||||
value=f"Levels: `{levels}`", inline=False)
|
||||
embed.add_field(name="**Random**", value="To select a random pool or country, use `random` or `r`")
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f'https://hamstudy.org/pools/{selected_pool}') as resp:
|
||||
if resp.status != 200:
|
||||
return await ctx.send('Could not load questions...')
|
||||
pool = json.loads(await resp.read())['pool']
|
||||
pools = await self.hamstudy_get_pools()
|
||||
|
||||
pool_matches = [p for p in pools.keys() if "_".join(p.split("_")[:-1]) == pool_name]
|
||||
|
||||
if len(pool_matches) > 0:
|
||||
if len(pool_matches) == 1:
|
||||
pool = pool_matches[0]
|
||||
else:
|
||||
# look at valid_from and expires dates to find the correct one
|
||||
for p in pool_matches:
|
||||
valid_from = datetime.fromisoformat(pools[p]["valid_from"][:-1])
|
||||
expires = datetime.fromisoformat(pools[p]["expires"][:-1])
|
||||
|
||||
if valid_from < datetime.utcnow() < expires:
|
||||
pool = p
|
||||
break
|
||||
else:
|
||||
# show list of possible pools
|
||||
embed.title = "Pool Not Found!"
|
||||
embed.description = "Possible arguments are:"
|
||||
embed.colour = cmn.colours.bad
|
||||
for cty in study.pool_names:
|
||||
levels = "`, `".join(study.pool_names[cty].keys())
|
||||
embed.add_field(name=f"**Country: `{cty}` {study.pool_emojis[cty]}**",
|
||||
value=f"Levels: `{levels}`", inline=False)
|
||||
embed.add_field(name="**Random**", value="To select a random pool or country, use `random` or `r`")
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
pool_meta = pools[pool]
|
||||
|
||||
async with self.session.get(f"https://hamstudy.org/pools/{pool}") as resp:
|
||||
if resp.status != 200:
|
||||
raise cmn.BotHTTPError(resp)
|
||||
pool = json.loads(await resp.read())["pool"]
|
||||
|
||||
# Select a question
|
||||
pool_section = random.choice(pool)['sections']
|
||||
pool_questions = random.choice(pool_section)['questions']
|
||||
pool_section = random.choice(pool)["sections"]
|
||||
pool_questions = random.choice(pool_section)["questions"]
|
||||
question = random.choice(pool_questions)
|
||||
|
||||
embed = discord.Embed(title=question['id'],
|
||||
description=self.source,
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
embed = embed.add_field(name='Question:', value=question['text'], inline=False)
|
||||
embed = embed.add_field(name='Answers:', value='**A:** ' + question['answers']['A'] +
|
||||
'\n**B:** ' + question['answers']['B'] +
|
||||
'\n**C:** ' + question['answers']['C'] +
|
||||
'\n**D:** ' + question['answers']['D'],
|
||||
inline=False)
|
||||
embed = embed.add_field(name='Answer:', value='Type _?rqa_ for answer', inline=False)
|
||||
if 'image' in question:
|
||||
image_url = f'https://hamstudy.org/_1330011/images/{selected_pool.split("_",1)[1]}/{question["image"]}'
|
||||
embed = embed.set_image(url=image_url)
|
||||
self.lastq[ctx.message.channel.id] = (question['id'], question['answer'])
|
||||
await ctx.send(embed=embed)
|
||||
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,"
|
||||
" the answer will be revealed."),
|
||||
inline=False)
|
||||
if "image" in question:
|
||||
image_url = f"https://hamstudy.org/images/{pool_meta['year']}/{question['image']}"
|
||||
embed.set_image(url=image_url)
|
||||
|
||||
@commands.command(name="hamstudyanswer", aliases=['rqa', 'randomquestionanswer', 'randomqa', 'hamstudya'],
|
||||
category=cmn.cat.study)
|
||||
async def _q_answer(self, ctx: commands.Context, answer: str = None):
|
||||
'''Returns the answer to question last asked (Optional argument: your answer).'''
|
||||
with ctx.typing():
|
||||
correct_ans = self.lastq[ctx.message.channel.id][1]
|
||||
q_num = self.lastq[ctx.message.channel.id][0]
|
||||
if answer is not None:
|
||||
answer = answer.upper()
|
||||
if answer == correct_ans:
|
||||
result = f'Correct! The answer to {q_num} was **{correct_ans}**.'
|
||||
embed = discord.Embed(title=f'{q_num} Answer',
|
||||
description=f'{self.source}\n\n{result}',
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
else:
|
||||
result = f'Incorrect. The answer to {q_num} was **{correct_ans}**, not **{answer}**.'
|
||||
embed = discord.Embed(title=f'{q_num} Answer',
|
||||
description=f'{self.source}\n\n{result}',
|
||||
colour=cmn.colours.bad,
|
||||
timestamp=datetime.utcnow())
|
||||
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)
|
||||
|
||||
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())
|
||||
|
||||
try:
|
||||
reaction, user = await self.bot.wait_for("reaction_add", timeout=600.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']}**.")
|
||||
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
|
||||
await q_msg.edit(embed=embed)
|
||||
else:
|
||||
result = f'The correct answer to {q_num} was **{correct_ans}**.'
|
||||
embed = discord.Embed(title=f'{q_num} Answer',
|
||||
description=f'{self.source}\n\n{result}',
|
||||
colour=cmn.colours.neutral,
|
||||
timestamp=datetime.utcnow())
|
||||
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)
|
||||
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
await ctx.send(embed=embed)
|
||||
async def hamstudy_get_pools(self):
|
||||
async with self.session.get("https://hamstudy.org/pools/") as resp:
|
||||
if resp.status != 200:
|
||||
raise cmn.BotHTTPError(resp)
|
||||
else:
|
||||
pools_dict = json.loads(await resp.read())
|
||||
|
||||
pools = dict()
|
||||
for ls in pools_dict.values():
|
||||
for pool in ls:
|
||||
pools[pool["id"]] = pool
|
||||
|
||||
return pools
|
||||
|
||||
|
||||
def setup(bot: commands.Bot):
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
"""
|
||||
Weather extension for qrm
|
||||
---
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
||||
General Public License, version 2.
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
import io
|
||||
from datetime import datetime
|
||||
|
||||
import re
|
||||
|
||||
import discord
|
||||
import discord.ext.commands as commands
|
||||
|
||||
import aiohttp
|
||||
|
||||
import discord.ext.commands as commands
|
||||
|
||||
import common as cmn
|
||||
|
||||
|
||||
@@ -24,111 +22,85 @@ class WeatherCog(commands.Cog):
|
||||
|
||||
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)
|
||||
@commands.command(name="bandconditions", aliases=["cond", "condx", "conditions"], category=cmn.cat.weather)
|
||||
async def _band_conditions(self, ctx: commands.Context):
|
||||
'''Posts an image of HF Band Conditions.'''
|
||||
with ctx.typing():
|
||||
embed = discord.Embed(title='Current Solar Conditions',
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get('http://www.hamqsl.com/solarsun.php') as resp:
|
||||
if resp.status != 200:
|
||||
embed.description = 'Could not download file...'
|
||||
embed.colour = cmn.colours.bad
|
||||
else:
|
||||
data = io.BytesIO(await resp.read())
|
||||
embed.set_image(url=f'attachment://condx.png')
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
await ctx.send(embed=embed, file=discord.File(data, 'condx.png'))
|
||||
"""Gets a solar conditions report."""
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = "Current Solar Conditions"
|
||||
embed.colour = cmn.colours.good
|
||||
embed.set_image(url="http://www.hamqsl.com/solarsun.php")
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.group(name="weather", aliases=['wttr'], category=cmn.cat.weather)
|
||||
@commands.group(name="weather", aliases=["wttr"], case_insensitive=True, category=cmn.cat.weather)
|
||||
async def _weather_conditions(self, ctx: commands.Context):
|
||||
'''Posts an image of Local Weather Conditions from [wttr.in](http://wttr.in/).
|
||||
"""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`
|
||||
'''
|
||||
*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)
|
||||
@_weather_conditions.command(name="forecast", aliases=["fc", "future"], category=cmn.cat.weather)
|
||||
async def _weather_conditions_forecast(self, ctx: commands.Context, *, location: str):
|
||||
'''Posts an image of Local Weather Conditions for the next three days from [wttr.in](http://wttr.in/).
|
||||
See help for weather command for possible location types. Add a `-c` or `-f` to use Celcius or Fahrenheit.'''
|
||||
with ctx.typing():
|
||||
try:
|
||||
units_arg = re.search(self.wttr_units_regex, location).group(1)
|
||||
except AttributeError:
|
||||
units_arg = ''
|
||||
if units_arg.lower() == 'f':
|
||||
units = 'u'
|
||||
elif units_arg.lower() == 'c':
|
||||
units = 'm'
|
||||
else:
|
||||
units = ''
|
||||
"""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()
|
||||
loc = self.wttr_units_regex.sub("", location).strip()
|
||||
|
||||
embed = discord.Embed(title=f'Weather Forecast for {loc}',
|
||||
description='Data from [wttr.in](http://wttr.in/).',
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
loc = loc.replace(' ', '+')
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f'http://wttr.in/{loc}_{units}pnFQ.png') as resp:
|
||||
if resp.status != 200:
|
||||
embed.description = 'Could not download file...'
|
||||
embed.colour = cmn.colours.bad
|
||||
else:
|
||||
data = io.BytesIO(await resp.read())
|
||||
embed.set_image(url=f'attachment://wttr_forecast.png')
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
await ctx.send(embed=embed, file=discord.File(data, f'wttr_forecast.png'))
|
||||
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
|
||||
|
||||
@_weather_conditions.command(name='now', aliases=['n'], category=cmn.cat.weather)
|
||||
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.cat.weather)
|
||||
async def _weather_conditions_now(self, ctx: commands.Context, *, location: str):
|
||||
'''Posts an image of current Local Weather Conditions from [wttr.in](http://wttr.in/).
|
||||
See help for weather command for possible location types. Add a `-c` or `-f` to use Celcius or Fahrenheit.'''
|
||||
with ctx.typing():
|
||||
try:
|
||||
units_arg = re.search(self.wttr_units_regex, location).group(1)
|
||||
except AttributeError:
|
||||
units_arg = ''
|
||||
if units_arg.lower() == 'f':
|
||||
units = 'u'
|
||||
elif units_arg.lower() == 'c':
|
||||
units = 'm'
|
||||
else:
|
||||
units = ''
|
||||
"""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()
|
||||
loc = self.wttr_units_regex.sub("", location).strip()
|
||||
|
||||
embed = discord.Embed(title=f'Current Weather for {loc}',
|
||||
description='Data from [wttr.in](http://wttr.in/).',
|
||||
colour=cmn.colours.good,
|
||||
timestamp=datetime.utcnow())
|
||||
loc = loc.replace(' ', '+')
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f'http://wttr.in/{loc}_0{units}pnFQ.png') as resp:
|
||||
if resp.status != 200:
|
||||
embed.description = 'Could not download file...'
|
||||
embed.colour = cmn.colours.bad
|
||||
else:
|
||||
data = io.BytesIO(await resp.read())
|
||||
embed.set_image(url=f'attachment://wttr_now.png')
|
||||
embed.set_footer(text=ctx.author.name,
|
||||
icon_url=str(ctx.author.avatar_url))
|
||||
await ctx.send(embed=embed, file=discord.File(data, 'wttr_now.png'))
|
||||
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)
|
||||
|
||||
|
||||
def setup(bot: commands.Bot):
|
||||
|
||||
@@ -1,27 +1,16 @@
|
||||
"""
|
||||
Static info about the bot.
|
||||
---
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
||||
General Public License, version 2.
|
||||
---
|
||||
|
||||
`authors`: The authors of the bot.
|
||||
|
||||
`description`: A description of the bot.
|
||||
|
||||
`license`: The license the bot is released under.
|
||||
|
||||
`contrubuting`: Info on how to contribute to the bot.
|
||||
|
||||
`release`: Current bot version.
|
||||
|
||||
`release_timestamp`: When the bot was last released.
|
||||
This file is part of qrm2 and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
|
||||
authors = ("@ClassAbbyAmplifier#2229", "@0x5c#0639")
|
||||
description = """A bot with various useful ham radio-related functions, written in Python."""
|
||||
license = "Released under the GNU General Public License v2"
|
||||
contributing = "Check out the source on GitHub, contributions welcome: https://github.com/classabbyamp/discord-qrm2"
|
||||
release = '2.0.0'
|
||||
contributing = "Check out the source on GitHub, contributions welcome: https://github.com/miaowware/qrm2"
|
||||
release = "2.5.0"
|
||||
bot_server = "https://discord.gg/Ntbg3J4"
|
||||
|
||||
@@ -2,23 +2,31 @@
|
||||
"""
|
||||
qrm, a bot for Discord
|
||||
---
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrm2 and is released under the terms of the GNU
|
||||
General Public License, version 2.
|
||||
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 asyncio
|
||||
import random
|
||||
import sys
|
||||
import traceback
|
||||
from datetime import datetime, time
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytz
|
||||
|
||||
import discord
|
||||
from discord.ext import commands, tasks
|
||||
|
||||
import common as cmn
|
||||
import info
|
||||
import common as cmn
|
||||
import utils.connector as conn
|
||||
|
||||
import data.options as opt
|
||||
import data.keys as keys
|
||||
import data.options as opt
|
||||
|
||||
|
||||
# --- Settings ---
|
||||
@@ -32,83 +40,100 @@ debug_mode = opt.debug # Separate assignement in-case we define an override (te
|
||||
|
||||
# --- Bot setup ---
|
||||
|
||||
# Loop/aiohttp stuff
|
||||
loop = asyncio.get_event_loop()
|
||||
connector = loop.run_until_complete(conn.new_connector())
|
||||
|
||||
# Defining the intents
|
||||
intents = discord.Intents.none()
|
||||
intents.guilds = True
|
||||
intents.guild_messages = True
|
||||
intents.dm_messages = 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=member_cache,
|
||||
loop=loop,
|
||||
connector=connector)
|
||||
|
||||
# Simple way to access bot-wide stuff in extensions.
|
||||
bot.qrm = SimpleNamespace()
|
||||
|
||||
# Let's store stuff here.
|
||||
bot.qrm.connector = connector
|
||||
bot.qrm.debug_mode = debug_mode
|
||||
|
||||
|
||||
# --- Commands ---
|
||||
|
||||
@bot.command(name="restart", hidden=True)
|
||||
@bot.command(name="restart", aliases=["rs"], category=cmn.cat.admin)
|
||||
@commands.check(cmn.check_if_owner)
|
||||
async def _restart_bot(ctx: commands.Context):
|
||||
"""Restarts the bot."""
|
||||
global exit_code
|
||||
await cmn.add_react(ctx.message, cmn.emojis.good)
|
||||
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()
|
||||
|
||||
|
||||
@bot.command(name="shutdown", hidden=True)
|
||||
@bot.command(name="shutdown", aliases=["shut"], category=cmn.cat.admin)
|
||||
@commands.check(cmn.check_if_owner)
|
||||
async def _shutdown_bot(ctx: commands.Context):
|
||||
"""Shuts down the bot."""
|
||||
global exit_code
|
||||
await cmn.add_react(ctx.message, cmn.emojis.good)
|
||||
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()
|
||||
|
||||
|
||||
@bot.group(name="extctl", hidden=True)
|
||||
@bot.group(name="extctl", aliases=["ex"], case_insensitive=True, category=cmn.cat.admin)
|
||||
@commands.check(cmn.check_if_owner)
|
||||
async def _extctl(ctx: commands.Context):
|
||||
"""Extension control commands.
|
||||
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)
|
||||
|
||||
|
||||
@_extctl.command(name="list")
|
||||
@_extctl.command(name="list", aliases=["ls"])
|
||||
async def _extctl_list(ctx: commands.Context):
|
||||
"""Lists Extensions."""
|
||||
embed = discord.Embed(title="Loaded Extensions",
|
||||
colour=cmn.colours.neutral,
|
||||
timestamp=datetime.utcnow())
|
||||
"""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()])
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
@_extctl.command(name="load")
|
||||
@_extctl.command(name="load", aliases=["ld"])
|
||||
async def _extctl_load(ctx: commands.Context, extension: str):
|
||||
try:
|
||||
bot.load_extension(ext_dir + "." + extension)
|
||||
await cmn.add_react(ctx.message, cmn.emojis.good)
|
||||
except commands.ExtensionError as ex:
|
||||
embed = cmn.error_embed_factory(ctx, ex, debug_mode)
|
||||
await ctx.send(embed=embed)
|
||||
"""Loads an extension."""
|
||||
bot.load_extension(ext_dir + "." + extension)
|
||||
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
|
||||
|
||||
|
||||
@_extctl.command(name="reload")
|
||||
@_extctl.command(name="reload", aliases=["rl", "r", "relaod"])
|
||||
async def _extctl_reload(ctx: commands.Context, extension: str):
|
||||
try:
|
||||
bot.reload_extension(ext_dir + "." + extension)
|
||||
await cmn.add_react(ctx.message, cmn.emojis.good)
|
||||
except commands.ExtensionError as ex:
|
||||
embed = cmn.error_embed_factory(ctx, ex, debug_mode)
|
||||
await ctx.send(embed=embed)
|
||||
"""Reloads an extension."""
|
||||
if ctx.invoked_with == "relaod":
|
||||
pika = bot.get_emoji(opt.pika)
|
||||
if pika:
|
||||
await cmn.add_react(ctx.message, pika)
|
||||
bot.reload_extension(ext_dir + "." + extension)
|
||||
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
|
||||
|
||||
|
||||
@_extctl.command(name="unload")
|
||||
@_extctl.command(name="unload", aliases=["ul"])
|
||||
async def _extctl_unload(ctx: commands.Context, extension: str):
|
||||
try:
|
||||
bot.unload_extension(ext_dir + "." + extension)
|
||||
await cmn.add_react(ctx.message, cmn.emojis.good)
|
||||
except commands.ExtensionError as ex:
|
||||
embed = cmn.error_embed_factory(ctx, ex, debug_mode)
|
||||
await ctx.send(embed=embed)
|
||||
"""Unloads an extension."""
|
||||
bot.unload_extension(ext_dir + "." + extension)
|
||||
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
|
||||
|
||||
|
||||
# --- Events ---
|
||||
@@ -117,26 +142,108 @@ async def _extctl_unload(ctx: commands.Context, extension: str):
|
||||
async def on_ready():
|
||||
print(f"Logged in as: {bot.user} - {bot.user.id}")
|
||||
print("------")
|
||||
if opt.status_mode == "time":
|
||||
_ensure_activity_time.start()
|
||||
elif opt.status_mode == "random":
|
||||
_ensure_activity_random.start()
|
||||
else:
|
||||
_ensure_activity_fixed.start()
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_message(message):
|
||||
msg = message.content.lower()
|
||||
for emoji, keywords in opt.msg_reacts.items():
|
||||
if any([keyword in msg for keyword in keywords]):
|
||||
await message.add_reaction(discord.utils.find(lambda x: x.id == emoji, bot.emojis))
|
||||
|
||||
await bot.process_commands(message)
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_command_error(ctx: commands.Context, err: commands.CommandError):
|
||||
if isinstance(err, commands.UserInputError):
|
||||
await cmn.add_react(ctx.message, cmn.emojis.warning)
|
||||
await ctx.send_help(ctx.command)
|
||||
elif isinstance(err, commands.CommandNotFound):
|
||||
if ctx.invoked_with and ctx.invoked_with.startswith(("?", "!")):
|
||||
return
|
||||
else:
|
||||
await cmn.add_react(ctx.message, cmn.emojis.question)
|
||||
elif isinstance(err, commands.CheckFailure):
|
||||
# Add handling of other subclasses of CheckFailure as needed.
|
||||
if isinstance(err, commands.NotOwner):
|
||||
await cmn.add_react(ctx.message, cmn.emojis.no_entry)
|
||||
else:
|
||||
await cmn.add_react(ctx.message, cmn.emojis.x)
|
||||
elif isinstance(err, commands.DisabledCommand):
|
||||
await cmn.add_react(ctx.message, cmn.emojis.bangbang)
|
||||
elif isinstance(err, (commands.CommandInvokeError, commands.ConversionError)):
|
||||
# Emulating discord.py's default beaviour.
|
||||
print("Ignoring exception in command {}:".format(ctx.command), file=sys.stderr)
|
||||
traceback.print_exception(type(err), err, err.__traceback__, file=sys.stderr)
|
||||
|
||||
embed = cmn.error_embed_factory(ctx, err.original, bot.qrm.debug_mode)
|
||||
embed.description += f"\n`{type(err).__name__}`"
|
||||
await cmn.add_react(ctx.message, cmn.emojis.warning)
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
# Emulating discord.py's default beaviour. (safest bet)
|
||||
print("Ignoring exception in command {}:".format(ctx.command), file=sys.stderr)
|
||||
traceback.print_exception(type(err), err, err.__traceback__, file=sys.stderr)
|
||||
await cmn.add_react(ctx.message, cmn.emojis.warning)
|
||||
|
||||
|
||||
# --- Tasks ---
|
||||
|
||||
@tasks.loop(minutes=5)
|
||||
async def _ensure_activity():
|
||||
await bot.change_presence(activity=discord.Game(name=opt.game))
|
||||
async def _ensure_activity_time():
|
||||
status = opt.statuses[0]
|
||||
|
||||
try:
|
||||
tz = pytz.timezone(opt.status_tz)
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
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()
|
||||
|
||||
for sts in opt.time_statuses:
|
||||
start_time = time(hour=sts[1][0], minute=sts[1][1], tzinfo=tz)
|
||||
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))
|
||||
|
||||
|
||||
@_ensure_activity.before_loop
|
||||
async def _before_ensure_activity():
|
||||
await bot.wait_until_ready()
|
||||
@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))
|
||||
|
||||
|
||||
@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 ---
|
||||
|
||||
for ext in opt.exts:
|
||||
bot.load_extension(ext_dir + '.' + ext)
|
||||
|
||||
_ensure_activity.start()
|
||||
bot.load_extension(ext_dir + "." + ext)
|
||||
|
||||
|
||||
try:
|
||||
@@ -144,22 +251,23 @@ try:
|
||||
|
||||
except discord.LoginFailure as ex:
|
||||
# Miscellaneous authentications errors: borked token and co
|
||||
if debug_mode:
|
||||
if bot.qrm.debug_mode:
|
||||
raise
|
||||
raise SystemExit("Error: Failed to authenticate: {}".format(ex))
|
||||
|
||||
except discord.ConnectionClosed as ex:
|
||||
# When the connection to the gateway (websocket) is closed
|
||||
if debug_mode:
|
||||
if bot.qrm.debug_mode:
|
||||
raise
|
||||
raise SystemExit("Error: Discord gateway connection closed: [Code {}] {}".format(ex.code, ex.reason))
|
||||
|
||||
except ConnectionResetError as ex:
|
||||
# More generic connection reset error
|
||||
if debug_mode:
|
||||
if bot.qrm.debug_mode:
|
||||
raise
|
||||
raise SystemExit("ConnectionResetError: {}".format(ex))
|
||||
|
||||
|
||||
# --- Exit ---
|
||||
# Codes for the wrapper shell script:
|
||||
# 0 - Clean exit, don't restart
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
discord.py
|
||||
ctyparser
|
||||
discord.py~=1.5.0
|
||||
ctyparser~=2.0
|
||||
beautifulsoup4
|
||||
lxml
|
||||
pytz
|
||||
|
||||
@@ -1,57 +1,28 @@
|
||||
"""
|
||||
Information about callsigns for the vanity prefixes command in hamcog.
|
||||
Information about callsigns for the prefixes command in hamcog.
|
||||
---
|
||||
Copyright (C) 2019 Abigail Gold, 0x5c
|
||||
Copyright (C) 2019-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.
|
||||
This file is part of discord-qrmbot and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .callsigninfos import (us, ca)
|
||||
|
||||
|
||||
us_calls_title = "Valid US Vanity Callsigns"
|
||||
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:** 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 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\n'
|
||||
'- Any 2x3 with AA-AL, NA-NZ, WC, WK, WM, WR, or WT prefix\n'
|
||||
'- Any 2x1, 2x2, or 2x3 with KP, NP, WP prefix and 0, 6, 7, 8, 9 number\n'
|
||||
'- Any 1x1 callsign'))])
|
||||
@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),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Callsign info for various countries
|
||||
"""
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
Information about callsigns for the CA prefixes command in hamcog.
|
||||
---
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
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"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Information about callsigns for the US prefixes command in hamcog.
|
||||
---
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
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: 92 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 416 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 407 KiB |
|
After Width: | Height: | Size: 237 KiB |
|
After Width: | Height: | Size: 441 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"au": ["au.png", "Australia", "Amateur radio bands in Australia", "", "[Radio Amateur Society of Australia](https://vkradioamateurs.org/its-my-frequency-and-ill-cry-if-i-want-to/) [[PDF]](https://vkradioamateurs.org/wp-content/uploads/2018/05/VK-Band-Plan-Quick-Reference-1.pdf)", "🇦🇺"],
|
||||
"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", "🇨🇳"],
|
||||
"it_hf": ["it_hf.png", "Italy (HF)", "HF amateur radio bands in Italy", "", "[Associazione Radioamatori Italiani](https://www.arimi.it/download/bandplan/) [[PDF]](http://www.arimi.it/wp-content/Plan/BP%20HF%202013%2032.pdf)", "🇮🇹"],
|
||||
"it_vhf": ["it_vhf.png", "Italy (VHF/UHF)", "VHF/UHF amateur radio bands in Italy", "", "[Associazione Radioamatori Italiani](https://www.arimi.it/download/bandplan/) [[PDF]](https://www.arimi.it/wp-content/Plan/BP%20V_UHF%202013%2032.pdf)", "🇮🇹"],
|
||||
"it_shf": ["it_shf.png", "Italy (UHF/SHF)", "UHF/SHF amateur radio bands in Italy", "", "[Associazione Radioamatori Italiani](https://www.arimi.it/download/bandplan/) [[PDF]](https://www.arimi.it/wp-content/Plan/BP%20U-SHF.pdf)", "🇮🇹"],
|
||||
"jp": ["jp.png", "Japan", "Amateur radio bands in Japan", "Pending checks against the latest update from the 総務省 (MIC) of Japan, March 2020", "[JARL Amateur-Use Bandplans](https://jarl.org/Japanese/A_Shiryo/A-3_Band_Plan/bandplan20150105.pdf)", "🇯🇵"],
|
||||
"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", "*This version of this bandplan is no longer hosted on VERNON's website and it's accuracy cannot be guaranteed.*", "[VERNON Global Band Plan van de Telecom Agency, Ministerie van Economische Zaken en Klimaat](https://www.veron.nl/downloads/brochures/)", "🇳🇱"],
|
||||
"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 After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 504 KiB After Width: | Height: | Size: 504 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"ca": ["ca.png", "Canada's Prefixes", "Map of the prefix regions in Canada", "", "[Denelson83 (Wikimedia Commons)](https://commons.wikimedia.org/wiki/File:Amateur_radio_prefixes_in_Canada.svg)", "🇨🇦"],
|
||||
"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)", "🇺🇸"],
|
||||
"itur": ["itu-regions.png", "ITU Regions", "ITU Regions", "These are also used by the IARU for their regions.", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🇺🇳"],
|
||||
"ituz": ["itu-zones.png", "ITU Zones", "ITU Zones", "", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🇺🇳"],
|
||||
"arrl": ["arrl-rac.png", "ARRL Sections", "ARRL Sections", "", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🇺🇸"],
|
||||
"cq": ["cq-zones.png", "CQ Zones", "CQ Zones", "These are used for the CQWW contest.", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🌐"],
|
||||
"rac": ["arrl-rac.png", "RAC Sections", "RAC Sections", "", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🇨🇦"]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 304 KiB |
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"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": "-----", " ": " ", ".": ".-.-.-", ",": "--..--",
|
||||
"?": "..--..", "'": ".----.", "!": "-.-.--", "/": "-..-.", "(": "-.--.", ")": "-.--.-",
|
||||
"&": ".-...", ":": "---...", ";": "-.-.-.", "=": "-...-", "+": ".-.-.", "-": "-....-",
|
||||
"\"": ".-..-.", "@": ".--.-.", "Ä": ".-.-", "Å": ".-.-", "Ą": ".-.-", "Æ": ".-.-",
|
||||
"É": "..-..", "Ñ": "--.--", "Ö": "---.", "Ü": "..--", "Š": "----", " ": "/"
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
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",
|
||||
".-.-.-": ".",
|
||||
"--..--": ",",
|
||||
"..--..": "?",
|
||||
".----.": "'",
|
||||
"-.-.--": "!",
|
||||
"-..-.": "/",
|
||||
"-.--.": "(",
|
||||
"-.--.-": ")",
|
||||
".-...": "&",
|
||||
"---...": ":",
|
||||
"-.-.-.": ";",
|
||||
"-...-": "=",
|
||||
".-.-.": "+",
|
||||
"-....-": "-",
|
||||
".-..-.": "\"",
|
||||
".--.-.": "@",
|
||||
".-.-": "Ä",
|
||||
"..-..": "É",
|
||||
"--.--": "Ñ",
|
||||
"---.": "Ö",
|
||||
"..--": "Ü",
|
||||
"----": "Š",
|
||||
"/": " "
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
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"
|
||||
}
|
||||
|
||||
pweights = {
|
||||
"A": 2,
|
||||
"B": 2,
|
||||
"C": 2,
|
||||
"D": 2,
|
||||
"E": 2,
|
||||
"F": 2,
|
||||
"G": 1,
|
||||
"H": 2,
|
||||
"I": 3,
|
||||
"J": 3,
|
||||
"K": 2,
|
||||
"L": 2,
|
||||
"M": 1,
|
||||
"N": 3,
|
||||
"O": 2,
|
||||
"P": 2,
|
||||
"Q": 2,
|
||||
"R": 3,
|
||||
"S": 3,
|
||||
"T": 2,
|
||||
"U": 3,
|
||||
"V": 2,
|
||||
"W": 2,
|
||||
"X": 2,
|
||||
"Y": 2,
|
||||
"Z": 2,
|
||||
"0": 2,
|
||||
"1": 1,
|
||||
"2": 1,
|
||||
"3": 1,
|
||||
"4": 1,
|
||||
"5": 1,
|
||||
"6": 1,
|
||||
"7": 2,
|
||||
"8": 1,
|
||||
"9": 2,
|
||||
"/": 1,
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
{
|
||||
"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": "Change to transmission on another frequency",
|
||||
"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).",
|
||||
"QTH": "My location is",
|
||||
"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)."
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
"""
|
||||
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)."
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
A listing of hamstudy command resources
|
||||
---
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
pool_names = {
|
||||
"us": {
|
||||
"technician": "E2",
|
||||
"tech": "E2",
|
||||
"t": "E2",
|
||||
"general": "E3",
|
||||
"gen": "E3",
|
||||
"g": "E3",
|
||||
"extra": "E4",
|
||||
"e": "E4",
|
||||
},
|
||||
"ca": {
|
||||
"basic": "CA_B",
|
||||
"b": "CA_B",
|
||||
"advanced": "CA_A",
|
||||
"adv": "CA_A",
|
||||
"a": "CA_A",
|
||||
"basic_fr": "CA_FB",
|
||||
"b_fr": "CA_FB",
|
||||
"base": "CA_FB",
|
||||
"advanced_fr": "CA_FS",
|
||||
"adv_fr": "CA_FS",
|
||||
"a_fr": "CA_FS",
|
||||
"supérieure": "CA_FS",
|
||||
"superieure": "CA_FS",
|
||||
"s": "CA_FS",
|
||||
},
|
||||
"us_c": {
|
||||
"c1": "C1",
|
||||
"comm1": "C1",
|
||||
"c3": "C3",
|
||||
"comm3": "C3",
|
||||
"c6": "C6",
|
||||
"comm6": "C6",
|
||||
"c7": "C7",
|
||||
"comm7": "C7",
|
||||
"c7r": "C7R",
|
||||
"comm7r": "C7R",
|
||||
"c8": "C8",
|
||||
"comm8": "C8",
|
||||
"c9": "C9",
|
||||
"comm9": "C9",
|
||||
},
|
||||
}
|
||||
|
||||
pool_emojis = {
|
||||
"us": "🇺🇸",
|
||||
"ca": "🇨🇦",
|
||||
"us_c": "🇺🇸 🏢",
|
||||
}
|
||||
@@ -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).
|
||||
@@ -27,7 +30,31 @@ debug = False
|
||||
owners_uids = (200102491231092736,)
|
||||
|
||||
# The extensions to load when running the bot.
|
||||
exts = ['ae7q', 'base', 'fun', 'grid', 'ham', 'image', 'lookup', 'morse', 'qrz', 'study', 'weather']
|
||||
exts = ["ae7q", "base", "fun", "grid", "ham", "image", "lookup", "morse", "qrz", "study", "weather", "dbconv"]
|
||||
|
||||
# The text to put in the "playing" status.
|
||||
game = 'with lids on 7.200'
|
||||
# Either "time", "random", or "fixed" (first item in statuses)
|
||||
status_mode = "fixed"
|
||||
|
||||
# Random statuses pool
|
||||
statuses = ["with lids on the air", "with fire"]
|
||||
|
||||
# Timezone for the status (string)
|
||||
# See https://pythonhosted.org/pytz/ for more info
|
||||
status_tz = "US/Eastern"
|
||||
|
||||
# The text to put in the "playing" status, with start and stop times
|
||||
time_statuses = [("with lids on 3.840", (00, 00), (6, 00)),
|
||||
("with lids on 7.200", (6, 00), (10, 00)),
|
||||
("with lids on 14.313", (10, 00), (18, 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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Various utilities for the bot.
|
||||
"""
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Wrapper to handle aiohttp connector creation.
|
||||
---
|
||||
Copyright (C) 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 aiohttp
|
||||
|
||||
|
||||
async def new_connector(*args, **kwargs) -> aiohttp.TCPConnector:
|
||||
"""*Yes, it's just a coro to instantiate a class.*"""
|
||||
return aiohttp.TCPConnector(*args, **kwargs)
|
||||