136 Commits

Author SHA1 Message Date
0x5c c3f002a9df Merge pull request #421 from miaowware/cl
Bump version 2.7.1
2021-04-12 11:40:22 -04:00
0x5c ff40f0caca Bump version 2.7.1 2021-04-12 11:37:43 -04:00
0x5c c0ad8d1108 Merge pull request #420 from miaowware/qrzfix
Fixed ?call crash on empty adress fields
2021-04-12 10:15:47 -04:00
0x5c ce62c93d03 Fixed ?call crash on empty adress fields
Fixes #419
2021-04-12 10:00:11 -04:00
classabbyamp c5c065bd47 Merge pull request #417 from miaowware/help-sort
make help command sort by category
2021-04-12 09:20:10 -04:00
0x5c 8f8b7f87de Merge pull request #418 from miaowware/forms
Fixed issue form for new update
2021-04-11 18:08:56 -04:00
0x5c 144b288f09 Fixed issue form for new update 2021-04-11 17:59:34 -04:00
Abigail G dc1efa7b0c make help command sort by category
fixes #390
2021-04-11 15:20:42 -04:00
thxo 38ba8d9d0c Tex command: more helpful error messages (#416)
* replace error message with common mistakes
* explicitly mention document mode in docstring
* add changelog

fixes #415 

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

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

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

* Update CHANGELOG.md

Co-authored-by: 0x5c <dev@0x5c.io>
2021-03-26 08:04:54 -04:00
classabbyamp bb3ee319b6 Merge pull request #378 from miaowware/dev-deps
add system dependencies to README and DEVELOPING
2021-03-26 03:49:16 -04:00
Abigail G aaec204c43 update readme to add system dependencies 2021-03-24 21:13:10 -04:00
Abigail G 4c276bbc4d add system depedencies to development guide 2021-03-24 21:09:04 -04:00
classabbyamp 89e26dbd2b Merge pull request #375 from miaowware/efix-tex-resource
add latex resource to main.py
2021-03-23 17:56:26 -04:00
Abigail G 52995a8160 add latex resource to main.py 2021-03-22 23:49:34 -04:00
classabbyamp 4e572fa6d8 Merge pull request #371 from thxo/tex
add ?tex command
2021-03-22 23:40:55 -04:00
Howard Xiao 08d611e8c4 reassign authorship to qrm2 authors 2021-03-22 01:44:40 -07:00
thxo a140f74273 address style feedback 2021-03-22 01:44:12 -07:00
Howard Xiao 137b275c78 implement ?tex command and add rTeX server option 2021-03-22 00:35:45 -07:00
0x5c 9f72a63202 Merge pull request #369 from miaowware/correct-readme-links
correct README links
2021-03-19 04:27:44 -04:00
classabbyamp 1eed538d2c correct README link 2021-03-19 02:57:21 -04:00
0x5c 190eef7fa9 Merge pull request #367 from miaowware/cl
Bump changelog v2.6.0
2021-03-18 09:38:52 -04:00
0x5c 38eecf0010 Bump version v2.6.0 2021-03-18 09:37:28 -04:00
0x5c 4faba37c73 Merge pull request #366 from miaowware/thegreatmigration
Added and switched to the Resource Manager
2021-03-18 09:26:48 -04:00
0x5c 0241d1856a Added and switched to the Resource Manager
- Added Resource Manager
- Migrated most exts to it
- Added config entry for the resources URL
- Added my ID to template
- Bumped copyright on edited files

Fixes #246
2021-03-18 09:17:30 -04:00
classabbyamp 54960e8237 Merge pull request #363 from miaowware/plugins
add support for plugins
2021-03-18 01:26:35 -04:00
Abigail G 7ffe821f78 Merge branch 'master' into plugins 2021-03-18 01:22:17 -04:00
classabbyamp 31ca12a048 Merge pull request #364 from miaowware/bump-3.9
bump python to >=3.9, bump d.py to ~=1.6.0
2021-03-18 01:20:02 -04:00
Abigail G 361d4f894e bump python to <=3.9, bump d.py to ~=1.6.0
fixes #361
2021-03-17 23:10:57 -04:00
Abigail G 79545403d8 add support for plugins 2021-03-17 22:05:34 -04:00
classabbyamp 3a5ce9f1c0 Merge pull request #360 from miaowware/public-invite
add failsafe to not show invite if bot is not public
2021-03-17 08:47:26 -04:00
Abigail G 850aa36894 add failsafe to not show invite if bot is not public 2021-03-17 08:41:18 -04:00
0x5c bc2515c9fc Merge pull request #359 from miaowware/qrztools-integration
convert qrzcog to use qrztools
2021-03-17 03:53:34 -04:00
Abigail G 3134c41191 Merge branch 'master' into qrztools-integration 2021-03-16 22:15:49 -04:00
Abigail G a7b4203112 convert qrzcog to use qrztools
fixes #58
fixes #351
2021-03-16 22:04:05 -04:00
classabbyamp 123dff28fa Merge pull request #358 from miaowware/donate-invite
Donate/Invite commands
2021-03-16 21:24:34 -04:00
Abigail G c6581841ab add donation and invite links to ?info
fixes #357
2021-03-16 20:56:52 -04:00
Abigail G df8d258446 add ?invite command
fixes #356
2021-03-16 20:33:45 -04:00
Abigail G fa82610469 add donate command
fixes #355
2021-03-16 20:20:17 -04:00
0x5c 0d5ca5c2fe Merge pull request #354 from miaowware/helppfx
Prefixes and the ?help command
2021-03-13 18:31:29 -05:00
0x5c 950840be60 Merge branch 'master' into helppfx 2021-03-13 18:30:23 -05:00
0x5c 78e34dff63 Added list of available prexifes to ?help
- Only shows when more then one is available.

Fixes #353
2021-03-13 18:16:58 -05:00
0x5c ab73001340 Made help command use the invocation prefix
Fixes #338
2021-03-13 17:48:18 -05:00
classabbyamp 956fc4b02f Merge pull request #349 from miaowware/docker-bs
fix issue with pushing docker images to ghcr
2021-03-07 00:01:48 -05:00
Abigail G be6c78f4de fix issue with pushing docker images to ghcr 2021-03-06 23:55:38 -05:00
classabbyamp 86ab415619 Merge pull request #346 from franrogers/master
Validate inputs for ?ae7q and ?qrz commands
2021-03-06 23:41:37 -05:00
classabbyamp 351b1e2d21 hamstudy improvements (#348)
- added orange colour for timeouts
- added emojis in the last field
- made the correct answer bold after answering/timeout
- minor code optimisations
- added ability to react ? to get the answer

fixes #347
2021-03-06 23:39:50 -05:00
Abigail G 4d64d22ec6 add emojis, question mark, and changelog
fixes #347
2021-03-06 23:38:28 -05:00
Abigail G d7e544edcd hamstudy improvements
- added orange colour for timeouts
- added emojis in the last field
- made the correct answer bold after answering/timeout
- minor code optimisations
2021-02-28 01:51:19 -05:00
Fran Rogers 483a0bad19 Use str.isalnum and str.isdecimal instead of re.match for ?ae7q and ?qrz input validation 2021-01-29 23:43:30 -05:00
Fran Rogers 4ab4748b9f Validate inputs for ?ae7q and ?qrz commands 2021-01-29 22:55:26 -05:00
classabbyamp eb98e295d2 add git commit to info output (#342)
fixes #341
2021-01-25 17:25:26 -05:00
classabbyamp 6dfc05217f add the ability to select an element to hamstudy (#344)
fixes #208
2021-01-25 17:24:45 -05:00
classabbyamp b091b22c17 add resources contrib info to ?info, ?issue, and readme (#343)
fixes #307
2021-01-25 17:24:13 -05:00
classabbyamp 43a24d614b add METAR and TAF commands (#340)
fixes #171
2021-01-20 02:48:38 -05:00
classabbyamp 4139b23fe6 add missing packages to docker dependencies (#337)
fixes #336
2021-01-18 01:18:41 -05:00
0x5c 31a4fca70e Merge pull request #335 from miaowware/glts
Fixed ?greyline caching (set date url param)
2021-01-18 01:12:12 -05:00
classabbyamp 2b3fd59c2d migrate from github packages to github container registry (#327)
fixes #266
2021-01-18 00:38:31 -05:00
0x5c ac183a3e7f Fixed ?greyline caching (set date url param)
Fixed #318
2021-01-18 00:28:16 -05:00
0x5c a25597ec82 Merge pull request #334 from miaowware/cleansessions
Cleaned-up usused aiohttp sessions
2021-01-18 00:08:46 -05:00
0x5c 010e161a46 Removed unused aiohttp import in weather.py 2021-01-18 00:06:11 -05:00
0x5c 5807b54cb3 Merge pull request #333 from miaowware/solar
Renamed ?cond to ?solar
2021-01-18 00:04:16 -05:00
0x5c afe95b912b Cleaned-up usused aiohttp sessions
Fixes #317
2021-01-18 00:03:15 -05:00
0x5c 6092252eaa Renamed ?cond to ?solar
- Deprecated old name and aliases.

Fixes #324
2021-01-17 23:55:23 -05:00
0x5c 32c7029b88 Merge pull request #331 from miaowware/map
Changelog for MUF/foF2 maps
2021-01-17 23:19:52 -05:00
0x5c 5a43495a00 Merge branch 'master' into map 2021-01-17 23:19:14 -05:00
Abigail G e2968bf4a0 add new dependencies 2021-01-17 23:16:54 -05:00
classabbyamp 750fe65fcd 0x5c did an oopsie (#329) 2021-01-17 23:13:14 -05:00
0x5c 362a113e2e Changelog for MUF/foF2 maps 2021-01-17 23:09:11 -05:00
0x5c 3eb6d8b12b Merge pull request #330 from miaowware/map
Added MUF and foF2 maps
2021-01-17 22:38:21 -05:00
0x5c 6c5133f140 Added MUF and foF2 maps
Fixes #311
2021-01-17 22:35:33 -05:00
classabbyamp a93aafaa96 convert GridCog to use gridtools (#326)
* deprecated ?ungrid
* added more help text to commands
* separated distance into its own command

fixes #306
2020-12-22 20:40:24 -05:00
0x5c 39531fd2b0 Merge pull request #319 from miaowware/ngl
Changed the greyline colour theme
2020-12-11 18:28:12 -05:00
0x5c ad96b59b23 Changed the greyline colour theme
Fixes #316
2020-12-11 11:31:11 -05:00
classabbyamp 6074f5bc45 Merge pull request #315 from miaowware/release-2.5.1
Release 2.5.1
2020-12-10 17:46:24 -05:00
Abigail G 23ca74253d prep for release 2.5.1 2020-12-10 17:43:58 -05:00
Abigail G af68be2b2a add changelog for reaction intent fix 2020-12-10 17:41:41 -05:00
0x5c 2ae11058b2 Merge pull request #314 from miaowware/fixgl
Addded a "nonce" to the greyline URL to force discord to reload the c…
2020-12-10 17:40:06 -05:00
0x5c eba8eec5ac Add nonce to greyline URL to force cache bypass
Fixes #308
2020-12-10 17:38:10 -05:00
classabbyamp d6e381efec add reactions intent (#313)
fixes #312
2020-12-10 09:07:30 -05:00
0x5c 940f45f4d4 Merge pull request #305 from miaowware/changelog
Bump version 2.5.0 + changelog
2020-10-31 21:10:52 -04:00
0x5c be7e29b387 Bump version 2.5.0 + changelog 2020-10-31 21:08:44 -04:00
0x5c 518ead9ccd Merge pull request #304 from miaowware/maps-plans-ordering
Re-ordered the maps and plans
2020-10-31 21:05:03 -04:00
0x5c 6e5acba6e9 Re-ordered the maps and plans
Maps:
Followed order (all alphabetic) countrycode, ITU, other organisations.

Plans:
Followed countrycode (alphabetic).

Fixes #292
2020-10-31 20:56:39 -04:00
classabbyamp ad50a86f9d Merge pull request #291 from miaowware/moar-bandcharts
add australian and italian band charts
2020-10-31 20:34:38 -04:00
classabbyamp 0bfa0c6e41 Merge branch 'master' into moar-bandcharts 2020-10-31 20:28:18 -04:00
0x5c ce64d882b8 Merge pull request #303 from miaowware/pr221
[Compressed] Japan and Nederlands Bandplan Update (#221)
2020-10-31 20:26:14 -04:00
0x5c 0aac09f3bc [Compressed] Japan and Nederlands Bandplan Update (#221)
PR originally by MadIceTea

note: will take care of miaowware/qrm-resources#5 once applied there

Co-authored-by: Alexander Wiegman (Taniguchi) <7548448+MadIceTea@users.noreply.github.com>
Co-authored-by: MadIceTea <7548448+MadIceTea@users.noreply.github.com>
2020-10-31 20:08:01 -04:00
0x5c e3534d02d7 Merge pull request #302 from miaowware/cl-gl
Add missing changelog entry for greyline fix
2020-10-30 09:10:11 -04:00
0x5c cdcb0e17d2 Add missing changelog entry for greyline fix 2020-10-30 09:09:13 -04:00
0x5c cd2503c953 Merge pull request #300 from miaowware/paths
Moved paths to pathlib
2020-10-30 08:58:16 -04:00
0x5c b4c165851c Merge pull request #298 from miaowware/intents
Changed the intents and member cache
2020-10-30 08:57:57 -04:00
0x5c 77a5af73bc Merge pull request #297 from miaowware/no-dl
Switch to sending an URL instead of fetching images
2020-10-30 08:57:44 -04:00
0x5c 4b7064cad9 Merge pull request #301 from miaowware/ihaveocd
Yes, this is a PR to remove a newline
2020-10-30 08:49:55 -04:00
0x5c d8fe3cfa02 Yes, this is a PR to remove a newline
Yes, I have OCD

Yes, I made an issue just for that too
Fixes #299
2020-10-30 07:18:58 -04:00
0x5c ffc3be7e24 Moved paths to pathlib
Turns out most paths were already using pathlib, only remained
some in lookup.py and fun.py

Fixes #45
2020-10-30 07:07:56 -04:00
0x5c 5dab93b7d3 Changed the intents and member cache
- Now only intents GUILDS, GUILD_MESSAGES, DIRECT_MESSAGES.
- Member cache now from intents.

Fixes #296
2020-10-30 06:14:14 -04:00
0x5c e660b1a8f5 Switch to sending an URL instead of fetching images
This only applies to images that were downloaded.
Images that are hosted in the bot are not affected.

Fixes #230
Fixes #295
2020-10-30 05:22:16 -04:00
Abigail G 3d96a43c50 add australian and italian band charts
fixes #225
fixes #242
2020-10-29 00:52:44 -04:00
0x5c e8bb18ea8c Merge pull request #290 from miaowware/error
Fixed numerous small potential bugs
2020-10-28 23:33:58 -04:00
0x5c 19952396f2 Fixed numerous small potential bugs
- Most are typing related

Fixes #289
2020-10-28 23:22:28 -04:00
0x5c 1831c56f58 Merge pull request #286 from miaowware/unpin
Unpinned the versions of most dependencies
2020-10-28 21:06:47 -04:00
classabbyamp 77e14a109c convert Unit into a dataclass, move parse to converter (#257)
fixes #256
2020-10-28 21:06:30 -04:00
classabbyamp 2ac13346d4 Merge pull request #276 from miaowware/release-fixes
Release fixes
2020-10-28 21:05:58 -04:00
classabbyamp 2eea7dce23 move CallsignInfoData to resources/callsign_info (#258)
fixes #255
2020-10-28 21:04:14 -04:00
classabbyamp f26a7af928 fixed qsl/lotw status being incorrectly shown (#278)
fixes #277
2020-10-28 21:03:41 -04:00
classabbyamp 786440edcb Merge pull request #281 from miaowware/efix-docker-workflow 2020-10-28 21:01:32 -04:00
0x5c c47d211016 Unpinned the versions of most stuff, made the remaining version pinning less strict 2020-10-28 11:31:10 -04:00
Abigail G 855935a26e outputs can't be shared between jobs :oof: 2020-10-11 14:47:25 -04:00
classabbyamp ff9d46f379 add deploy workflow job (#280)
Fixes #279
2020-10-11 14:42:09 -04:00
classabbyamp 0f0c3bf723 fix variables not working correctly
Fixes #275
2020-10-06 23:44:58 -04:00
classabbyamp 7c818cfb34 fix tag bug in release workflow
progress on #275
2020-10-06 22:58:20 -04:00
67 changed files with 1724 additions and 2692 deletions
+2
View File
@@ -11,3 +11,5 @@ docker-compose.yml
Makefile
README*.md
/resource_schemas.py
+82
View File
@@ -0,0 +1,82 @@
name: Bug Report
description: Report a bug to help us improve this project
labels: [bug, 'needs triage']
body:
- id: description
type: textarea
attributes:
label: Bug description
description: A clear and concise description of what the bug is.
placeholder: There was a crash when...
validations:
required: true
- id: steps
type: textarea
attributes:
label: Steps to reproduce
description: Clear steps to reproduce the bug.
placeholder: |
1. Do the thing
2. Do the other thing
3. ???
4. Crash :(
validations:
required: true
- id: expected
type: textarea
attributes:
label: Expected behaviour
description: A clear and concise description of what you expected to happen.
placeholder: The app is supposed to show that thing.
validations:
required: true
- id: source
type: dropdown
attributes:
label: How did you download qrm?
options:
- Official Docker package
- Git (clone)
- Zip download on the releases package
- I do not know (user of official instance)
validations:
required: true
- id: runtime
type: dropdown
attributes:
label: How are you running qrm?
options:
- docker-compose
- Daemon (systemd, etc)
- Docker
- Command line (./run.sh)
- I do not know (user of official instance)
validations:
required: true
- id: version
type: input
attributes:
label: What qrm version are you running?
description: The version number, or the commit id if `dev` version. You can find both in `?info`.
placeholder: v2.5.2
validations:
required: true
- id: environment
type: textarea
attributes:
label: Environment
description: If relevant, include list of software used and versions
placeholder: |
- Linux 5.8
- Docker 19.04
- id: logs
type: textarea
attributes:
label: Logs
description: If you have a log associated with the bug (tracebacks, etc), paste it directly here.
render: none
- id: context
type: textarea
attributes:
label: Additional context, screenshots, etc
description: Add any other relevant context about the problem here.
+11
View File
@@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Feature Request
url: https://github.com/miaowware/qrm2/discussions/new?category=ideas
about: Suggest an idea for this project
- name: Support
url: https://github.com/miaowware/qrm2/discussions/new?category=support
about: Get help with qrm in GitHub Discussions
- name: Support (Discord)
url: https://discord.gg/SwyjdDN
about: Get help with qrm on Discord
+19 -24
View File
@@ -11,9 +11,6 @@ on:
tags:
- v*
env:
IMAGE_NAME: qrm2
jobs:
docker:
name: Build and push docker images
@@ -28,15 +25,20 @@ jobs:
run: git rev-list -n 1 $GITHUB_REF > ./git_commit
- name: Build image
run: docker build . --file Dockerfile -t $IMAGE_NAME
id: build_image
run: |
IMAGE_NAME=${GITHUB_REPOSITORY#*/}
echo ::set-output name=image_name::$IMAGE_NAME
docker build . --file Dockerfile -t $IMAGE_NAME
- name: Log into Github Package Registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Tag image
id: tag_image
run: |
IMAGE_ID=docker.pkg.github.com/${{ github.repository }}/$IMAGE_NAME
IMAGE_NAME=${{ steps.build_image.outputs.image_name }}
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
echo IMAGE_ID=$IMAGE_ID
echo ::set-output name=image_id::$IMAGE_ID
@@ -55,22 +57,15 @@ jobs:
[[ "$VERSION" != "dev" ]] && docker tag $IMAGE_NAME $IMAGE_ID:latest || true
- name: Push images to registry
run: docker push ${{ steps.tag_image.outputs.image_id }}
run: |
[[ "${{ steps.tag_image.outputs.version }}" != "dev" ]] && docker push ${{ steps.tag_image.outputs.image_id }}:latest || true
docker push ${{ steps.tag_image.outputs.image_id }}:${{ steps.tag_image.outputs.version }}
# deploy:
# name: Deploy new images
# runs-on: ubuntu-20.04
# needs: docker
# steps:
# - name: Deploy official images
# id: deploy_images
# uses: satak/webrequest-action@v1.2.3
# with:
# url: ${{ secrets.DEPLOY_URL }}
# method: POST
# headers: '{"Authentication": "Token ${{ secrets.DEPLOY_TOKEN }}"}'
# payload: |
# '{
# "repository": "${{ github.repository }}",
# "version": "${{ jobs.docker.steps.tag_image.outputs.version }}"
# }'
- 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 -1
View File
@@ -9,7 +9,7 @@ jobs:
- uses: actions/checkout@master
- uses: actions/setup-python@v1
with:
python-version: 3.7
python-version: 3.9
architecture: x64
- name: Install flake8
run: pip install flake8
+14 -10
View File
@@ -25,18 +25,23 @@ jobs:
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 "SUBJECT=$SUBJECT"
echo "BODY=$BODY"
echo ::set-output name=subject::${SUBJECT}
echo ::set-output name=body::${BODY}
echo ::set-output name=current_version::${GITHUB_REF#refs/tags/v}
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: ${{ steps.get_tag.outputs.current_version }}
version: ${{ env.version_num }}
path: ./CHANGELOG.md
- name: Publish Release
@@ -45,14 +50,13 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.changelog_reader.outputs.version }}
release_name: ${{ steps.get_tag.outputs.subject }}
tag_name: ${{ env.tag_version }}
release_name: ${{ env.tag_subject }}
body: |
${{ steps.get_tag.outputs.body }}
${{ env.tag_body }}
## Changelog
${{ steps.changelog_reader.outputs.changes }}
draft: false
prerelease: false
+1
View File
@@ -6,6 +6,7 @@ cty.zip
/botenv/
/devenv/
/data/
/data/plugins/
/docker-compose.yml
+71 -2
View File
@@ -7,6 +7,70 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [2.7.1] - 2021-04-12
### Added
- Helpful LaTeX hints for rendering errors in `?tex`.
### Fixed
- Bug where `?call` would crash if the found profile only had empty address fields.
## [2.7.0] - 2021-04-03
### Added
- `?tex` command to render a LaTeX expression.
- Configuration option to use another rTeX instance for `?tex`.
- `?miltime` command to show the current time in all 25 military timezones.
### Changed
- Main name and aliases of `?bandplan`.
- Recategorized the commands.
### Fixed
- Lack of input sanitisation in `?xkcd`.
- Incorrect capitalisation of the categories in the `?help` command.
## [2.6.0] - 2021-03-18
### Added
- MUF and foF2 maps from [prop.kc2g.com](https://prop.kc2g.com/).
- Commands to show METAR (`?metar`) and TAF (`?taf`) (aeronautical weather conditions).
- The ability to select an element of a pool in `?hamstudy`.
- The ability to answer ❓ to a HamStudy question to get the answer.
- The list of available prefixes to `?help` when there is more than one.
- `?donate` command to show ways to support qrm's development.
- `?invite` command to invite qrm to your server.
- Configuration options to disable showing the `?invite` and set default invite permissions (enabled by default).
- Configuration option to show QRZ nickname in place of first name (enabled by default).
### Changed
- New colour theme for `?greyline`.
- Moved great circle distance and bearing calculation from `?ungrid` to `?griddistance`.
- `?ungrid` to `?latlong`.
- Renamed `?cond` to `?solar`.
- Reduced `?hamstudy` timeout to 5 minutes.
- Library used for QRZ lookups.
- Python>=3.9 now required.
- Most resources are now stored on a website. New indexes are pulled at start and the images linked instead of uploaded.
### Fixed
- Weird image caching situation for `?greyline` on Discord's side.
- The help command was not using the prefix it was invoked with.
### Deprecated
- `?ungrid`.
- Deprecated old `?solar` aliases (`?cond`, etc).
- Deprecated old `?call` alias (`?qrz`).
## [2.5.1] - 2020-12-10
### Fixed
- The result of `?greyline` was cached by discord and would get out of date.
- Broken reaction functionality in `?hamstudy`.
## [2.5.0] - 2020-10-31
### Added
- Italian (`it_hf`, `it_vhf`, `it_shf`), Japanese (`jp`) and Australian (`au`) band charts.
### Fixed
- Details to the Netherlands bandplan command to accurately represent VERNON (Netherlands ARRL equivalent organisation).
- eQSL, paper QSL, and Logbook of the World status in `?qrz` sometimes being shown incorrectly.
- Fixed network error in `?greyline`.
## [2.4.1] - 2020-10-06
### Changed
- Bumped discord.py to 1.5.0
@@ -33,7 +97,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [2.3.1] - 2020-04-02
### Fixed
- Wordlist containing innappropriate words.
- Wordlist containing inappropriate words.
## [2.3.0] - 2020-03-30
@@ -129,7 +193,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 1.0.0 - 2019-07-31 [YANKED]
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.4.1...HEAD
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.7.1...HEAD
[2.7.1]: https://github.com/miaowware/qrm2/releases/tag/v2.7.1
[2.7.0]: https://github.com/miaowware/qrm2/releases/tag/v2.7.0
[2.6.0]: https://github.com/miaowware/qrm2/releases/tag/v2.6.0
[2.5.1]: https://github.com/miaowware/qrm2/releases/tag/v2.5.1
[2.5.0]: https://github.com/miaowware/qrm2/releases/tag/v2.5.0
[2.4.1]: https://github.com/miaowware/qrm2/releases/tag/v2.4.1
[2.4.0]: https://github.com/miaowware/qrm2/releases/tag/v2.4.0
[2.3.2]: https://github.com/miaowware/qrm2/releases/tag/v2.3.2
+2 -1
View File
@@ -7,7 +7,8 @@
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. Install python 3.9 or higher.
1. Install `libcairo` and `libjpeg`. Package names may vary by distro or OS.
1. Run `make dev-install`.
This should install everything you need to develop and run qrm.
1. [Create a bot and token][2], and add it to `data/keys.py`.
+8 -18
View File
@@ -1,4 +1,4 @@
FROM alpine:3.10
FROM python:3.9-slim
COPY . /app
WORKDIR /app
@@ -6,30 +6,20 @@ WORKDIR /app
ENV PYTHON_BIN python3
RUN \
echo "**** install build packages ****" && \
apk add --no-cache --virtual=build-dependencies \
g++ \
git \
gcc \
libxml2-dev \
libxslt-dev \
libressl-dev \
python3-dev && \
apt-get update && \
echo "**** install runtime packages ****" && \
apk add --no-cache \
libressl \
py3-lxml \
py3-pip \
python3 && \
apt-get install -y --no-install-recommends \
libcairo2 \
libjpeg62-turbo \
python-lxml \
&& \
echo "**** install pip packages ****" && \
pip3 install -U pip setuptools wheel && \
pip3 install -r requirements.txt && \
echo "**** clean up ****" && \
apk del --purge \
build-dependencies && \
rm -rf \
/root/.cache \
/tmp/* \
/var/cache/apk/*
/var/lib/apt/lists/*
CMD ["/bin/sh", "run.sh", "--pass-errors", "--no-botenv"]
+8 -3
View File
@@ -12,7 +12,7 @@
# Those are the defaults; they can be over-ridden if specified
# at en environment level or as 'make' arguments.
BOTENV ?= botenv
PYTHON_BIN ?= python3.7
PYTHON_BIN ?= python3.9
PIP_OUTPUT ?= -q
@@ -32,7 +32,7 @@ help:
# Main install target
.PHONY: install
install: $(BOTENV)/req_done data/options.py data/keys.py
install: $(BOTENV)/req_done data/options.py data/keys.py data/plugins
# Virual environment setup
$(BOTENV)/success:
@@ -55,6 +55,11 @@ data:
@echo "\033[34;1m--> Creating ./data ...\033[0m"
@mkdir -p data
# Creating the ./data/plugins subdirectory
data/plugins: ./data
@echo "\033[34;1m--> Creating ./data/plugins ...\033[0m"
@mkdir -p data/plugins
# Copying templates
data/options.py data/keys.py: ./data
@echo "\033[34;1m--> Copying template for ./$@ ...\033[0m"
@@ -72,7 +77,7 @@ clean:
### Dev targets ###
.PHONY: dev-install
dev-install: $(BOTENV)/dev_req_done data/options.py data/keys.py
dev-install: $(BOTENV)/dev_req_done data/options.py data/keys.py data/plugins
# Installing dev requirements
$(BOTENV)/dev_req_done: dev-requirements.txt $(BOTENV)/success
+7 -3
View File
@@ -14,10 +14,12 @@ See [README-DOCKER.md](./README-DOCKER.md)
### Without Docker
Requires Python 3.7 or newer.
Requires Python 3.9 or newer.
Prep the environment. For more information on extra options, see the [quick-bot-no-pain Makefile documentation](https://github.com/0x5c/quick-bot-no-pain/blob/master/docs/makefile.md).
Install `libcairo` and `libjpeg` (package names may vary by distro or OS). Then run:
```
$ make install
```
@@ -30,11 +32,13 @@ $ run.sh
## Contributing
Check out the [contribution guidelines](/CONTRIBUTING.md) for more information about how to contribute to this project.
Check out the [development](/DEVELOPING.md) and [contribution guidelines](https://github.com/miaowware/.github/blob/master/CONTRIBUTING.md) for more information about how to contribute to this project.
All issues and requests related to resources (including maps, band charts, data) should be added in [miaowware/qrm-resources](https://github.com/miaowware/qrm-resources).
## Copyright
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2021 classabbyamp, 0x5c
This program is released under the terms of the GNU General Public License,
version 2. See `COPYING` for full license text.
+33 -28
View File
@@ -1,7 +1,7 @@
"""
Common tools for the bot.
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
@@ -9,22 +9,25 @@ the GNU General Public License, version 2.
import collections
import enum
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", "paths", "ImageMetadata", "ImagesGroup",
__all__ = ["colours", "BoltCats", "Cats", "emojis", "paths", "ImageMetadata", "ImagesGroup",
"embed_factory", "error_embed_factory", "add_react", "check_if_owner"]
@@ -34,18 +37,28 @@ colours = SimpleNamespace(
good=0x43B581,
neutral=0x7289DA,
bad=0xF04747,
timeout=0xF26522,
)
class BoltCats(enum.Enum):
OTHER = "Other"
INFO = "Bot Information"
ADMIN = "Bot Control"
# meow
cat = SimpleNamespace(
lookup="Information Lookup",
fun="Fun",
maps="Mapping",
ref="Reference",
study="Exam Study",
weather="Land and Space Weather",
admin="Bot Control",
)
class Cats(enum.Enum):
CALC = "Calculators"
CODES = "Code References and Tools"
FUN = "Fun"
LOOKUP = "Information Lookup"
REF = "Reference"
STUDY = "Exam Study"
TIME = "Time and Time Zones"
UTILS = "Utilities"
WEATHER = "Land and Space Weather"
emojis = SimpleNamespace(
check_mark="",
@@ -54,33 +67,24 @@ emojis = SimpleNamespace(
question="",
no_entry="",
bangbang="‼️",
clock="🕗",
stopwatch="",
a="🇦",
b="🇧",
c="🇨",
d="🇩",
e="🇪",
)
paths = SimpleNamespace(
data=Path("./data/"),
resources=Path("./resources/"),
img=Path("./resources/img/"),
bandcharts=Path("./resources/img/bandcharts/"),
maps=Path("./resources/img/maps/"),
resources=Path("./data/resources/"),
)
# --- Classes ---
class CallsignInfoData:
"""Represents a country's callsign info"""
def __init__(self, data: list):
self.title: str = data[0]
self.desc: str = data[1]
self.calls: str = data[2]
self.emoji: str = data[3]
class ImageMetadata:
"""Represents the metadata of a single image."""
def __init__(self, metadata: list):
@@ -107,7 +111,7 @@ class ImagesGroup(collections.abc.Mapping):
def __len__(self):
return len(self._images)
def __getitem__(self, key: str):
def __getitem__(self, key: str) -> ImageMetadata:
return self._images[key]
def __iter__(self):
@@ -158,7 +162,7 @@ class GlobalChannelConverter(commands.IDConverter):
def embed_factory(ctx: commands.Context) -> discord.Embed:
"""Creates an embed with neutral colour and standard footer."""
embed = discord.Embed(timestamp=datetime.utcnow(), colour=colours.neutral)
embed.set_footer(text=ctx.author, icon_url=str(ctx.author.avatar_url))
embed.set_footer(text=str(ctx.author), icon_url=str(ctx.author.avatar_url))
return embed
@@ -175,11 +179,12 @@ def error_embed_factory(ctx: commands.Context, exception: Exception, debug_mode:
return embed
async def add_react(msg: discord.Message, react: str):
async def add_react(msg: discord.Message, react: Union[Emoji, 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 ---
+52
View File
@@ -0,0 +1,52 @@
{
"title": "Index",
"type": "object",
"properties": {
"last_updated": {
"title": "Last Updated",
"type": "string",
"format": "date-time"
},
"resources": {
"title": "Resources",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Resource"
}
}
},
"required": [
"last_updated",
"resources"
],
"definitions": {
"File": {
"title": "File",
"type": "object",
"properties": {
"filename": {
"title": "Filename",
"type": "string"
},
"hash": {
"title": "Hash",
"type": "string"
}
},
"required": [
"filename",
"hash"
]
},
"Resource": {
"title": "Resource",
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"$ref": "#/definitions/File"
}
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
from pathlib import Path
from utils.resources_manager import ResourcesManager
path = Path("./data/resources")
url = "https://qrmresources.miaow.io/resources/"
versions = {
"bandcharts": "v1",
"img": "v1",
"maps": "v1",
"morse": "v1",
"phonetics": "v1",
"qcodes": "v1",
"funetics": "v1"
}
rm = ResourcesManager(path, url, versions)
print(rm.index)
+47 -7
View File
@@ -1,7 +1,7 @@
"""
ae7q extension for qrm
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
@@ -29,13 +29,13 @@ class AE7QCog(commands.Cog):
self.bot = bot
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
@commands.group(name="ae7q", aliases=["ae"], case_insensitive=True, category=cmn.cat.lookup)
@commands.group(name="ae7q", aliases=["ae"], case_insensitive=True, category=cmn.Cats.LOOKUP)
async def _ae7q_lookup(self, ctx: commands.Context):
"""Looks up a callsign, FRN, or Licensee ID on [ae7q.com](http://ae7q.com/)."""
if ctx.invoked_subcommand is None:
await ctx.send_help(ctx.command)
@_ae7q_lookup.command(name="call", aliases=["c"], category=cmn.cat.lookup)
@_ae7q_lookup.command(name="call", aliases=["c"], category=cmn.Cats.LOOKUP)
async def _ae7q_call(self, ctx: commands.Context, callsign: str):
"""Looks up the history of a callsign on [ae7q.com](http://ae7q.com/)."""
with ctx.typing():
@@ -44,6 +44,14 @@ class AE7QCog(commands.Cog):
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
embed = cmn.embed_factory(ctx)
if not callsign.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q History for Callsign"
embed.colour = cmn.colours.bad
embed.description = "Not a valid callsign!"
await ctx.send(embed=embed)
return
async with self.session.get(base_url + callsign) as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
@@ -101,7 +109,7 @@ class AE7QCog(commands.Cog):
await ctx.send(embed=embed)
@_ae7q_lookup.command(name="trustee", aliases=["t"], category=cmn.cat.lookup)
@_ae7q_lookup.command(name="trustee", aliases=["t"], category=cmn.Cats.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():
@@ -110,6 +118,14 @@ class AE7QCog(commands.Cog):
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
embed = cmn.embed_factory(ctx)
if not callsign.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q Trustee History for Callsign"
embed.colour = cmn.colours.bad
embed.description = "Not a valid callsign!"
await ctx.send(embed=embed)
return
async with self.session.get(base_url + callsign) as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
@@ -168,7 +184,7 @@ class AE7QCog(commands.Cog):
await ctx.send(embed=embed)
@_ae7q_lookup.command(name="applications", aliases=["a"], category=cmn.cat.lookup)
@_ae7q_lookup.command(name="applications", aliases=["a"], category=cmn.Cats.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/)."""
"""
@@ -178,6 +194,14 @@ class AE7QCog(commands.Cog):
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
embed = cmn.embed_factory(ctx)
if not callsign.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q Application History for Callsign"
embed.colour = cmn.colours.bad
embed.description = "Not a valid callsign!"
await ctx.send(embed=embed)
return
async with self.session.get(base_url + callsign) as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
@@ -238,7 +262,7 @@ class AE7QCog(commands.Cog):
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)
@_ae7q_lookup.command(name="frn", aliases=["f"], category=cmn.Cats.LOOKUP)
async def _ae7q_frn(self, ctx: commands.Context, frn: str):
"""Looks up the history of an FRN on [ae7q.com](http://ae7q.com/)."""
"""
@@ -250,6 +274,14 @@ class AE7QCog(commands.Cog):
base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN="
embed = cmn.embed_factory(ctx)
if not frn.isdecimal():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q History for FRN"
embed.colour = cmn.colours.bad
embed.description = "Not a valid FRN!"
await ctx.send(embed=embed)
return
async with self.session.get(base_url + frn) as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
@@ -305,7 +337,7 @@ class AE7QCog(commands.Cog):
await ctx.send(embed=embed)
@_ae7q_lookup.command(name="licensee", aliases=["l"], category=cmn.cat.lookup)
@_ae7q_lookup.command(name="licensee", aliases=["l"], category=cmn.Cats.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():
@@ -313,6 +345,14 @@ class AE7QCog(commands.Cog):
base_url = "http://ae7q.com/query/data/LicenseeIdHistory.php?ID="
embed = cmn.embed_factory(ctx)
if not licensee_id.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q History for Licensee"
embed.colour = cmn.colours.bad
embed.description = "Not a valid licensee ID!"
await ctx.send(embed=embed)
return
async with self.session.get(base_url + licensee_id) as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
+126 -20
View File
@@ -1,7 +1,7 @@
"""
Base extension for qrm
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
@@ -10,28 +10,74 @@ the GNU General Public License, version 2.
import random
import re
from typing import Union
from typing import Union, Iterable
import pathlib
import discord
import discord.ext.commands as commands
from discord.ext.commands import Command, CommandError
import info
import common as cmn
import data.options as opt
from data import options as opt
class QrmHelpCommand(commands.HelpCommand):
def __init__(self):
super().__init__(command_attrs={"help": "Shows help about qrm or a command", "aliases": ["h"]})
super().__init__(command_attrs={
"help": "Shows help about qrm or a command",
"aliases": ["h"],
"category": cmn.BoltCats.INFO
})
self.verify_checks = True
self.context: commands.Context
async def filter_commands(self, commands: Iterable[Command]) -> list[Command]:
def sort_by_cat(cmds):
ret = []
bolt_cmds = {}
for c in cmds:
cat = c.__original_kwargs__.get("category", cmn.BoltCats.OTHER)
if isinstance(cat, cmn.BoltCats):
if cat in bolt_cmds:
bolt_cmds[cat].append(c)
else:
bolt_cmds[cat] = [c]
cmds.remove(c)
else:
ret.append(c)
ret.sort(key=lambda c: c.__original_kwargs__["category"].name)
for cat in cmn.BoltCats:
ret += sorted(bolt_cmds[cat], key=lambda c: c.name)
return ret
iterator = commands if self.show_hidden else filter(lambda c: not c.hidden, commands)
if not self.verify_checks:
return sort_by_cat(iterator)
async def predicate(cmd):
try:
return await cmd.can_run(self.context)
except CommandError:
return False
cmds = []
for cmd in iterator:
if await predicate(cmd):
cmds.append(cmd)
return sort_by_cat(cmds)
async def get_bot_mapping(self):
bot = self.context.bot
mapping = {}
for cmd in await self.filter_commands(bot.commands, sort=True):
cat = cmd.__original_kwargs__.get("category", None)
for cmd in await self.filter_commands(bot.commands):
cat = cmd.__original_kwargs__.get("category", cmn.BoltCats.OTHER)
if cat in mapping:
mapping[cat].append(cmd)
else:
@@ -46,9 +92,9 @@ class QrmHelpCommand(commands.HelpCommand):
if parent:
fmt = f"{parent} {fmt}"
alias = fmt
return f"{opt.display_prefix}{alias} {command.signature}\n *Aliases:* {aliases}"
return f"{self.context.prefix}{alias} {command.signature}\n *Aliases:* {aliases}"
alias = command.name if not parent else f"{parent} {command.name}"
return f"{opt.display_prefix}{alias} {command.signature}"
return f"{self.context.prefix}{alias} {command.signature}"
async def send_error_message(self, error):
embed = cmn.embed_factory(self.context)
@@ -60,8 +106,11 @@ class QrmHelpCommand(commands.HelpCommand):
async def send_bot_help(self, mapping):
embed = cmn.embed_factory(self.context)
embed.title = "qrm Help"
embed.description = (f"For command-specific help and usage, use `{opt.display_prefix}help [command name]`."
embed.description = (f"For command-specific help and usage, use `{self.context.prefix}help [command name]`."
" Many commands have shorter aliases.")
if isinstance(self.context.bot.command_prefix, list):
embed.description += (" All of the following prefixes work with the bot: `"
+ "`, `".join(self.context.bot.command_prefix) + "`.")
mapping = await mapping
for cat, cmds in mapping.items():
@@ -69,7 +118,7 @@ class QrmHelpCommand(commands.HelpCommand):
continue
names = sorted([cmd.name for cmd in cmds])
if cat is not None:
embed.add_field(name=cat.title(), value=", ".join(names), inline=False)
embed.add_field(name=cat.value, value=", ".join(names), inline=False)
else:
embed.add_field(name="Other", value=", ".join(names), inline=False)
await self.context.send(embed=embed)
@@ -101,23 +150,56 @@ class BaseCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.changelog = parse_changelog()
commit_file = pathlib.Path("git_commit")
dot_git = pathlib.Path(".git")
if commit_file.is_file():
with commit_file.open() as f:
self.commit = f.readline().strip()[:7]
elif dot_git.is_dir():
head_file = pathlib.Path(dot_git, "HEAD")
if head_file.is_file():
with head_file.open() as hf:
head = hf.readline().split(": ")[1].strip()
branch_file = pathlib.Path(dot_git, head)
if branch_file.is_file():
with branch_file.open() as bf:
self.commit = bf.readline().strip()[:7]
else:
self.commit = ""
self.donation_links = {
"Ko-Fi": "https://ko-fi.com/miaowware",
"LiberaPay": "https://liberapay.com/miaowware",
}
self.bot_invite = ""
if self.bot.user:
self.bot_invite = (f"https://discordapp.com/oauth2/authorize?client_id={self.bot.user.id}"
f"&scope=bot&permissions={opt.invite_perms}")
@commands.command(name="info", aliases=["about"])
@commands.Cog.listener()
async def on_ready(self):
if not self.bot_invite:
self.bot_invite = (f"https://discordapp.com/oauth2/authorize?client_id={self.bot.user.id}"
f"&scope=bot&permissions={opt.invite_perms}")
@commands.command(name="info", aliases=["about"], category=cmn.BoltCats.INFO)
async def _info(self, ctx: commands.Context):
"""Shows info about qrm."""
embed = cmn.embed_factory(ctx)
embed.title = "About qrm"
embed.description = info.description
embed.add_field(name="Authors", value=", ".join(info.authors))
embed.add_field(name="License", value=info.license)
embed.add_field(name="Version", value=f"v{info.release}")
embed.add_field(name="Version", value=f"v{info.release} {'(`' + self.commit + '`)' if self.commit else ''}")
embed.add_field(name="Contributing", value=info.contributing, inline=False)
embed.add_field(name="Official Server", value=info.bot_server, inline=False)
embed.add_field(name="Donate", value="\n".join(f"{k}: {v}" for k, v in self.donation_links.items()),
inline=False)
if opt.enable_invite_cmd and (await self.bot.application_info()).bot_public:
embed.add_field(name="Invite qrm to Your Server", value=self.bot_invite, inline=False)
embed.set_thumbnail(url=str(self.bot.user.avatar_url))
await ctx.send(embed=embed)
@commands.command(name="ping", aliases=["beep"])
@commands.command(name="ping", aliases=["beep"], category=cmn.BoltCats.INFO)
async def _ping(self, ctx: commands.Context):
"""Shows the current latency to the discord endpoint."""
embed = cmn.embed_factory(ctx)
@@ -130,7 +212,7 @@ class BaseCog(commands.Cog):
embed.description = f"Current ping is {self.bot.latency*1000:.1f} ms"
await ctx.send(content, embed=embed)
@commands.command(name="changelog", aliases=["clog"])
@commands.command(name="changelog", aliases=["clog"], category=cmn.BoltCats.INFO)
async def _changelog(self, ctx: commands.Context, version: str = "latest"):
"""Shows what has changed in a bot version. Defaults to the latest version."""
embed = cmn.embed_factory(ctx)
@@ -166,16 +248,40 @@ class BaseCog(commands.Cog):
await ctx.send(embed=embed)
@commands.command(name="issue")
@commands.command(name="issue", category=cmn.BoltCats.INFO)
async def _issue(self, ctx: commands.Context):
"""Shows how to create a bug report or feature request about the bot."""
embed = cmn.embed_factory(ctx)
embed.title = "Found a bug? Have a feature request?"
embed.description = ("Submit an issue on the [issue tracker]"
"(https://github.com/miaowware/qrm2/issues)!")
embed.description = """Submit an issue on the [issue tracker](https://github.com/miaowware/qrm2/issues)!
All issues and requests related to resources (including maps, band charts, data) \
should be added in \
[miaowware/qrm-resources](https://github.com/miaowware/qrm-resources/issues)."""
await ctx.send(embed=embed)
@commands.command(name="echo", aliases=["e"], category=cmn.cat.admin)
@commands.command(name="donate", aliases=["tip"], category=cmn.BoltCats.INFO)
async def _donate(self, ctx: commands.Context):
"""Shows ways to help support development of the bot via donations."""
embed = cmn.embed_factory(ctx)
embed.title = "Help Support qrm's Development!"
embed.description = ("Donations are always appreciated, and help with server and infrastructure costs."
"\nThank you for your support!")
for title, url in self.donation_links.items():
embed.add_field(name=title, value=url, inline=False)
await ctx.send(embed=embed)
@commands.command(name="invite", enabled=opt.enable_invite_cmd, category=cmn.BoltCats.INFO)
async def _invite(self, ctx: commands.Context):
"""Generates a link to invite the bot to a server."""
if not (await self.bot.application_info()).bot_public:
raise commands.DisabledCommand
embed = cmn.embed_factory(ctx)
embed.title = "Invite qrm to Your Server!"
embed.description = self.bot_invite
await ctx.send(embed=embed)
@commands.command(name="echo", aliases=["e"], category=cmn.BoltCats.ADMIN)
@commands.check(cmn.check_if_owner)
async def _echo(self, ctx: commands.Context,
channel: Union[cmn.GlobalChannelConverter, commands.UserConverter], *, msg: str):
+133
View File
@@ -0,0 +1,133 @@
"""
Callsign Lookup extension for qrm
---
Copyright (C) 2019-2020 classabbyamp, 0x5c (as qrz.py)
Copyright (C) 2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
from typing import Dict
from datetime import datetime
import aiohttp
from qrztools import qrztools, QrzAsync, QrzError
from gridtools import Grid, LatLong
from discord.ext import commands
import common as cmn
import data.options as opt
import data.keys as keys
class QRZCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.qrz = None
try:
if keys.qrz_user and keys.qrz_pass:
self.qrz = QrzAsync(keys.qrz_user, keys.qrz_pass, useragent="discord-qrm2",
session=aiohttp.ClientSession(connector=bot.qrm.connector))
# seed the qrz object with the previous session key, in case it already works
try:
with open("data/qrz_session") as qrz_file:
self.qrz.session_key = qrz_file.readline().strip()
except FileNotFoundError:
pass
except AttributeError:
pass
@commands.command(name="call", aliases=["qrz"], category=cmn.Cats.LOOKUP)
async def _qrz_lookup(self, ctx: commands.Context, callsign: str, *flags):
"""Looks up a callsign on [QRZ.com](https://www.qrz.com/). Add `--link` to only link the QRZ page."""
flags = [f.lower() for f in flags]
if self.qrz is None or "--link" in flags:
if ctx.invoked_with == "qrz":
await ctx.send("⚠️ **Deprecated Command Alias**\n"
f"This command has been renamed to `{ctx.prefix}call`!\n"
"This alias will be removed in the next version.")
await ctx.send(f"http://qrz.com/db/{callsign}")
return
embed = cmn.embed_factory(ctx)
embed.title = f"QRZ Data for {callsign.upper()}"
if ctx.invoked_with == "qrz":
embed.description = ("⚠️ **Deprecated Command Alias**\n"
f"This command has been renamed to `{ctx.prefix}call`!\n"
"This alias will be removed in the next version.")
async with ctx.typing():
try:
data = await self.qrz.get_callsign(callsign)
except QrzError as e:
embed.colour = cmn.colours.bad
embed.description = str(e)
await ctx.send(embed=embed)
return
embed.title = f"QRZ Data for {data.call}"
embed.colour = cmn.colours.good
embed.url = data.url
if data.image != qrztools.QrzImage():
embed.set_thumbnail(url=data.image.url)
for title, val in qrz_process_info(data).items():
if val:
embed.add_field(name=title, value=val, inline=True)
await ctx.send(embed=embed)
def qrz_process_info(data: qrztools.QrzCallsignData) -> Dict:
if data.name != qrztools.Name():
if opt.qrz_only_nickname:
if data.name.nickname:
name = data.name.nickname + " " + data.name.name
elif data.name.first:
name = data.name.first + " " + data.name.name
else:
name = data.name.name
else:
name = data.name.formatted_name
else:
name = None
if data.address != qrztools.Address():
state = ", " + data.address.state + " " if data.address.state else ""
address = "\n".join(
[x for x
in [data.address.attn, data.address.line1, data.address.line2 + state, data.address.zip]
if x]
)
else:
address = None
return {
"Name": name,
"Country": data.address.country,
"Address": address,
"Grid Square": data.grid if data.grid != Grid(LatLong(0, 0)) else None,
"County": data.county if data.county else None,
"CQ Zone": data.cq_zone if data.cq_zone else None,
"ITU Zone": data.itu_zone if data.itu_zone else None,
"IOTA Designator": data.iota if data.iota else None,
"Expires": f"{data.expire_date:%Y-%m-%d}" if data.expire_date != datetime.min else None,
"Aliases": ", ".join(data.aliases) if data.aliases else None,
"Previous Callsign": data.prev_call if data.prev_call else None,
"License Class": data.lic_class if data.lic_class else None,
"Trustee": data.trustee if data.trustee else None,
"eQSL?": "Yes" if data.eqsl else "No",
"Paper QSL?": "Yes" if data.mail_qsl else "No",
"LotW?": "Yes" if data.lotw_qsl else "No",
"QSL Info": data.qsl_manager if data.qsl_manager else None,
"Born": f"{data.born:%Y-%m-%d}" if data.born != datetime.min else None
}
def setup(bot):
bot.add_cog(QRZCog(bot))
+81
View File
@@ -0,0 +1,81 @@
"""
Codes extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c (as ham.py)
Copyright (C) 2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
import json
import discord.ext.commands as commands
import common as cmn
class HamCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
with open(cmn.paths.resources / "phonetics.1.json") as file:
d = json.load(file)
self.phonetics: dict[str, str] = d["phonetics"]
self.pweights: dict[str, int] = d["pweights"]
with open(cmn.paths.resources / "qcodes.1.json") as file:
self.qcodes: dict = json.load(file)
@commands.command(name="qcode", aliases=["q"], category=cmn.Cats.CODES)
async def _qcode_lookup(self, ctx: commands.Context, qcode: str):
"""Looks up the meaning of a Q Code."""
qcode = qcode.upper()
embed = cmn.embed_factory(ctx)
if qcode in self.qcodes:
embed.title = qcode
embed.description = self.qcodes[qcode]
embed.colour = cmn.colours.good
else:
embed.title = f"Q Code {qcode} not found"
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
@commands.command(name="phonetics", aliases=["ph", "phoneticize", "phoneticise", "phone"], category=cmn.Cats.CODES)
async def _phonetics_lookup(self, ctx: commands.Context, *, msg: str):
"""Returns NATO phonetics for a word or phrase."""
result = ""
for char in msg.lower():
if char.isalpha():
result += self.phonetics[char]
else:
result += char
result += " "
embed = cmn.embed_factory(ctx)
embed.title = f"Phonetics for {msg}"
embed.description = result.title()
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
@commands.command(name="phoneticweight", aliases=["pw"], category=cmn.Cats.CODES)
async def _weight(self, ctx: commands.Context, *, msg: str):
"""Calculates the phonetic weight of a callsign or message."""
embed = cmn.embed_factory(ctx)
msg = msg.upper()
weight = 0
for char in msg:
try:
weight += self.pweights[char]
except KeyError:
embed.title = "Error in calculation of phonetic weight"
embed.description = f"Unknown character `{char}` in message"
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
embed.title = f"Phonetic Weight of {msg}"
embed.description = f"The phonetic weight is **{weight}**"
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
def setup(bot: commands.Bot):
bot.add_cog(HamCog(bot))
+28
View File
@@ -0,0 +1,28 @@
"""
Contest Calendar extension for qrm
---
Copyright (C) 2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
import discord.ext.commands as commands
import common as cmn
class ContestCalendarCog(commands.Cog):
@commands.command(name="contests", aliases=["cc", "tests"], category=cmn.Cats.LOOKUP)
async def _contests(self, ctx: commands.Context):
embed = cmn.embed_factory(ctx)
embed.title = "Contest Calendar"
embed.description = ("*We are currently rewriting the old, Chrome-based `contests` command. In the meantime, "
"use [the website](https://www.contestcalendar.com/weeklycont.php).*")
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
def setup(bot: commands.Bot):
bot.add_cog(ContestCalendarCog(bot))
+34 -35
View File
@@ -1,7 +1,7 @@
"""
Conversion extension for qrm
---
Copyright (C) 2020 Abigail Gold, 0x5c
Copyright (C) 2020-2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
@@ -11,6 +11,7 @@ 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
@@ -21,54 +22,52 @@ 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:
return Unit(argument)
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 Unit:
def __init__(self, raw: str):
self.raw: str = raw
self.unit: str
self.type: UnitType
self.is_db: bool
self.mult: int
self._parse()
def _parse(self):
s = self.raw.lower()
if len(s) > 2 and s[:2] == "db":
self.is_db = True
if s[2:] in units:
u = units[s[2:]]
self.mult = u["mult"]
self.unit = u["log"]
self.type = u["type"]
elif s in units:
self.is_db = False
u = units[s]
self.mult = u["mult"]
self.unit = u["scalar"]
self.type = u["type"]
else:
raise ValueError(f"Invalid unit: {self.raw}")
def __str__(self):
return self.unit
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)
@commands.command(name="dbconv", aliases=["dbc"], category=cmn.Cats.CALC)
async def _db_conv(self, ctx: commands.Context,
value: Optional[float] = None,
unit_from: Optional[UnitConverter] = None,
@@ -85,7 +84,7 @@ class DbConvCog(commands.Cog):
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} = {converted:.3g} {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"
+14 -21
View File
@@ -1,7 +1,8 @@
"""
Lookup extension for qrm
DXCC Prefix Lookup extension for qrm
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2020 classabbyamp, 0x5c (as lookup.py)
Copyright (C) 2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
@@ -9,6 +10,7 @@ the GNU General Public License, version 2.
import threading
from pathlib import Path
from ctyparser import BigCty
@@ -17,27 +19,18 @@ from discord.ext import commands, tasks
import common as cmn
class LookupCog(commands.Cog):
cty_path = Path("./data/cty.json")
class DXCCCog(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")
# else:
# await ctx.send(f"http://www.satmatch.com/satellite/{sat_name}/obs1/{grid1}"
# f"/obs2/{grid2}?search_start_time={now}&duration_hrs=24")
@commands.command(name="dxcc", aliases=["dx"], category=cmn.cat.lookup)
@commands.command(name="dxcc", aliases=["dx"], category=cmn.Cats.LOOKUP)
async def _dxcc_lookup(self, ctx: commands.Context, query: str):
"""Gets DXCC info about a callsign prefix."""
query = query.upper()
@@ -67,7 +60,7 @@ class LookupCog(commands.Cog):
@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()
@@ -78,6 +71,6 @@ def run_update(cty_obj, dump_loc):
def setup(bot: commands.Bot):
lookupcog = LookupCog(bot)
bot.add_cog(lookupcog)
lookupcog._update_cty.start()
dxcccog = DXCCCog(bot)
bot.add_cog(dxcccog)
dxcccog._update_cty.start()
+17 -15
View File
@@ -1,58 +1,60 @@
"""
Fun extension for qrm
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
import json
import random
import discord
import discord.ext.commands as commands
import common as cmn
import data.options as opt
class FunCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
with open("resources/words") as words_file:
with open(cmn.paths.resources / "imgs.1.json") as file:
self.imgs: dict = json.load(file)
with open(cmn.paths.resources / "words.1.txt") as words_file:
self.words = words_file.read().lower().splitlines()
@commands.command(name="xkcd", aliases=["x"], category=cmn.cat.fun)
async def _xkcd(self, ctx: commands.Context, number: str):
@commands.command(name="xkcd", aliases=["x"], category=cmn.Cats.FUN)
async def _xkcd(self, ctx: commands.Context, number: int):
"""Looks up an xkcd comic by number."""
await ctx.send("http://xkcd.com/" + number)
await ctx.send("http://xkcd.com/" + str(number))
@commands.command(name="tar", category=cmn.cat.fun)
@commands.command(name="tar", category=cmn.Cats.FUN)
async def _tar(self, ctx: commands.Context):
"""Returns xkcd: tar."""
await ctx.send("http://xkcd.com/1168")
@commands.command(name="standards", category=cmn.cat.fun)
@commands.command(name="standards", category=cmn.Cats.FUN)
async def _standards(self, ctx: commands.Context):
"""Returns xkcd: Standards."""
await ctx.send("http://xkcd.com/927")
@commands.command(name="worksplit", aliases=["split", "ft8"], category=cmn.cat.fun)
@commands.command(name="worksplit", aliases=["split", "ft8"], category=cmn.Cats.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)
embed.set_image(url=opt.resources_url + self.imgs["worksplit"])
await ctx.send(embed=embed)
@commands.command(name="xd", hidden=True, category=cmn.cat.fun)
@commands.command(name="xd", hidden=True, category=cmn.Cats.FUN)
async def _xd(self, ctx: commands.Context):
"""ecks dee"""
await ctx.send("ECKS DEE :smirk:")
@commands.command(name="funetics", aliases=["fun"], category=cmn.cat.fun)
@commands.command(name="funetics", aliases=["fun"], category=cmn.Cats.FUN)
async def _funetics_lookup(self, ctx: commands.Context, *, msg: str):
"""Generates fun/wacky phonetics for a word or phrase."""
result = ""
+39 -114
View File
@@ -1,14 +1,14 @@
"""
Grid extension for qrm
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
import math
import gridtools
import discord.ext.commands as commands
@@ -19,125 +19,50 @@ class GridCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.command(name="grid", category=cmn.cat.maps)
async def _grid_sq_lookup(self, ctx: commands.Context, lat: str, lon: str):
("""Calculates the grid square for latitude and longitude coordinates, """
"""with negative being latitude South and longitude West.""")
grid = "**"
latf = float(lat) + 90
lonf = float(lon) + 180
if 0 <= latf <= 180 and 0 <= lonf <= 360:
grid += chr(ord("A") + int(lonf / 20))
grid += chr(ord("A") + int(latf / 10))
grid += chr(ord("0") + int((lonf % 20)/2))
grid += chr(ord("0") + int((latf % 10)/1))
grid += chr(ord("a") + int((lonf - (int(lonf/2)*2)) / (5/60)))
grid += chr(ord("a") + int((latf - (int(latf/1)*1)) / (2.5/60)))
grid += "**"
embed = cmn.embed_factory(ctx)
embed.title = f"Maidenhead Grid Locator for {float(lat):.6f}, {float(lon):.6f}"
embed.description = grid
embed.colour = cmn.colours.good
else:
embed = cmn.embed_factory(ctx)
embed.title = f"Error generating grid square for {lat}, {lon}."
embed.description = """Coordinates out of range.
The valid ranges are:
- Latitude: `-90` to `+90`
- Longitude: `-180` to `+180`"""
embed.colour = cmn.colours.bad
@commands.command(name="grid", category=cmn.Cats.CALC)
async def _grid_sq_lookup(self, ctx: commands.Context, lat: float, lon: float):
("""Calculates the grid square for latitude and longitude coordinates."""
"""\n\nCoordinates should be in decimal format, with negative being latitude South and longitude West."""
"""\n\nTo calculate the latitude and longitude from a grid locator, use `latlong`""")
latlong = gridtools.LatLong(lat, lon)
grid = gridtools.Grid(latlong)
embed = cmn.embed_factory(ctx)
embed.title = f"Maidenhead Grid Locator for {latlong.lat:.5f}, {latlong.long:.5f}"
embed.description = f"**{grid}**"
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
@commands.command(name="ungrid", aliases=["loc"], category=cmn.cat.maps)
async def _location_lookup(self, ctx: commands.Context, grid: str, grid2: str = None):
"""Calculates the latitude and longitude for the center of a grid square.
If two grid squares are given, the distance and azimuth between them is calculated."""
if grid2 is None or grid2 == "":
try:
grid = grid.upper()
loc = get_coords(grid)
@commands.command(name="latlong", aliases=["latlon", "loc", "ungrid"], category=cmn.Cats.CALC)
async def _location_lookup(self, ctx: commands.Context, grid: str):
("""Calculates the latitude and longitude for the center of a grid locator."""
"""\n\nTo calculate the grid locator from a latitude and longitude, use `grid`"""
"""\n\n*Warning: `ungrid` will be removed soon. Use one of the other names for this command.*""")
grid_obj = gridtools.Grid(grid)
embed = cmn.embed_factory(ctx)
embed.title = f"Latitude and Longitude for {grid}"
embed.colour = cmn.colours.good
if len(grid) >= 6:
embed.description = f"**{loc[0]:.5f}, {loc[1]:.5f}**"
embed.url = f"https://www.openstreetmap.org/#map=13/{loc[0]:.5f}/{loc[1]:.5f}"
else:
embed.description = f"**{loc[0]:.1f}, {loc[1]:.1f}**"
embed.url = f"https://www.openstreetmap.org/#map=10/{loc[0]:.1f}/{loc[1]:.1f}"
except Exception as e:
embed = cmn.embed_factory(ctx)
embed.title = f"Error generating latitude and longitude for grid {grid}."
embed.description = str(e)
embed.colour = cmn.colours.bad
else:
radius = 6371
try:
grid = grid.upper()
grid2 = grid2.upper()
loc = get_coords(grid)
loc2 = get_coords(grid2)
# Haversine formula
d_lat = math.radians(loc2[0] - loc[0])
d_lon = math.radians(loc2[1] - loc[1])
a = (math.sin(d_lat/2) ** 2
+ math.cos(math.radians(loc[0]))
* math.cos(math.radians(loc2[0]))
* math.sin(d_lon/2) ** 2)
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
d = radius * c
d_mi = 0.6213712 * d
# Bearing
y_dist = math.sin(math.radians(loc2[1]-loc[1])) * math.cos(math.radians(loc2[0]))
x_dist = (math.cos(math.radians(loc[0]))
* math.sin(math.radians(loc2[0]))
- math.sin(math.radians(loc[0]))
* math.cos(math.radians(loc2[0]))
* math.cos(math.radians(loc2[1] - loc[1])))
bearing = (math.degrees(math.atan2(y_dist, x_dist)) + 360) % 360
embed = cmn.embed_factory(ctx)
embed.title = f"Great Circle Distance and Bearing from {grid} to {grid2}"
embed.description = f"**Distance:** {d:.1f} km ({d_mi:.1f} mi)\n**Bearing:** {bearing:.1f}°"
embed.colour = cmn.colours.good
except Exception as e:
embed = cmn.embed_factory(ctx)
embed.title = f"Error generating great circle distance and bearing from {grid} and {grid2}."
embed.description = str(e)
embed.colour = cmn.colours.bad
embed = cmn.embed_factory(ctx)
embed.title = f"Latitude and Longitude for {grid_obj}"
embed.colour = cmn.colours.good
embed.description = f"**{grid_obj.lat:.5f}, {grid_obj.long:.5f}**"
if ctx.invoked_with == "ungrid":
embed.add_field(name="Warning", value=(f"*`{ctx.prefix}ungrid` will be removed soon, use `{ctx.prefix}help "
"latlong` to see other names for this command.*"))
await ctx.send(embed=embed)
@commands.command(name="griddistance", aliases=["griddist", "distance", "dist"], category=cmn.Cats.CALC)
async def _dist_lookup(self, ctx: commands.Context, grid1: str, grid2: str):
"""Calculates the great circle distance and azimuthal bearing between two grid locators."""
g1 = gridtools.Grid(grid1)
g2 = gridtools.Grid(grid2)
def get_coords(grid: str):
if len(grid) < 3:
raise ValueError("The grid locator must be at least 4 characters long.")
dist, bearing = gridtools.grid_distance(g1, g2)
dist_mi = 0.6214 * dist
if not grid[0:2].isalpha() or not grid[2:4].isdigit():
if len(grid) <= 4:
raise ValueError("The grid locator must be of the form AA##.")
if len(grid) >= 6 and not grid[5:7].isalpha():
raise ValueError("The grid locator must be of the form AA##AA.")
lon = ((ord(grid[0]) - ord("A")) * 20) - 180
lat = ((ord(grid[1]) - ord("A")) * 10) - 90
lon += ((ord(grid[2]) - ord("0")) * 2)
lat += ((ord(grid[3]) - ord("0")) * 1)
if len(grid) >= 6:
# have subsquares
lon += ((ord(grid[4])) - ord("A")) * (5/60)
lat += ((ord(grid[5])) - ord("A")) * (2.5/60)
# move to center of subsquare
lon += (2.5/60)
lat += (1.25/60)
return (lat, lon)
# move to center of square
lon += 1
lat += 0.5
return (lat, lon)
embed = cmn.embed_factory(ctx)
embed.title = f"Great Circle Distance and Bearing from {g1} to {g2}"
embed.description = f"**Distance:** {dist:.1f} km ({dist_mi:.1f} mi)\n**Bearing:** {bearing:.1f}°"
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
def setup(bot: commands.Bot):
-122
View File
@@ -1,122 +0,0 @@
"""
Ham extension for qrm
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
from datetime import datetime
import discord.ext.commands as commands
import common as cmn
from resources import callsign_info
from resources import phonetics
from resources import qcodes
class HamCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.pfxs = callsign_info.options
@commands.command(name="qcode", aliases=["q"], category=cmn.cat.ref)
async def _qcode_lookup(self, ctx: commands.Context, qcode: str):
"""Looks up the meaning of a Q Code."""
qcode = qcode.upper()
embed = cmn.embed_factory(ctx)
if qcode in qcodes.qcodes:
embed.title = qcode
embed.description = qcodes.qcodes[qcode]
embed.colour = cmn.colours.good
else:
embed.title = f"Q Code {qcode} not found"
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
@commands.command(name="phonetics", aliases=["ph", "phoneticize", "phoneticise", "phone"], category=cmn.cat.ref)
async def _phonetics_lookup(self, ctx: commands.Context, *, msg: str):
"""Returns NATO phonetics for a word or phrase."""
result = ""
for char in msg.lower():
if char.isalpha():
result += phonetics.phonetics[char]
else:
result += char
result += " "
embed = cmn.embed_factory(ctx)
embed.title = f"Phonetics for {msg}"
embed.description = result.title()
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
@commands.command(name="utc", aliases=["z"], category=cmn.cat.ref)
async def _utc_lookup(self, ctx: commands.Context):
"""Returns the current time in UTC."""
now = datetime.utcnow()
result = "**" + now.strftime("%Y-%m-%d %H:%M") + "Z**"
embed = cmn.embed_factory(ctx)
embed.title = "The current time is:"
embed.description = result
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
@commands.command(name="prefixes", aliases=["vanity", "pfx", "vanities", "prefix"], category=cmn.cat.ref)
async def _vanity_prefixes(self, ctx: commands.Context, country: str = ""):
"""Lists valid callsign prefixes for different countries."""
country = country.lower()
embed = cmn.embed_factory(ctx)
if country not in self.pfxs:
desc = "Possible arguments are:\n"
for key, val in self.pfxs.items():
desc += f"`{key}`: {val.title}{(' ' + val.emoji if val.emoji else '')}\n"
embed.title = f"{country} Not Found!"
embed.description = desc
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
else:
data = self.pfxs[country]
embed.title = data.title + (" " + data.emoji if data.emoji else "")
embed.description = data.desc
embed.colour = cmn.colours.good
for name, val in data.calls.items():
embed.add_field(name=name, value=val, inline=False)
await ctx.send(embed=embed)
@commands.command(name="contests", aliases=["cc", "tests"], category=cmn.cat.ref)
async def _contests(self, ctx: commands.Context):
embed = cmn.embed_factory(ctx)
embed.title = "Contest Calendar"
embed.description = ("*We are currently rewriting the old, Chrome-based `contests` command. In the meantime, "
"use [the website](https://www.contestcalendar.com/weeklycont.php).*")
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
@commands.command(name="phoneticweight", aliases=["pw"], category=cmn.cat.ref)
async def _weight(self, ctx: commands.Context, *, msg: str):
"""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)
def setup(bot: commands.Bot):
bot.add_cog(HamCog(bot))
+32 -71
View File
@@ -1,100 +1,61 @@
"""
Image extension for qrm
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
import io
import aiohttp
import discord
import discord.ext.commands as commands
import common as cmn
import data.options as opt
class ImageCog(commands.Cog):
gl_url = ("http://www.fourmilab.ch/cgi-bin/uncgi/Earth?img=NOAAtopo.evif"
"&imgsize=320&dynimg=y&opt=-p&lat=&lon=&alt=&tle=&date=0&utc=&jd=")
def __init__(self, bot: commands.Bot):
self.bot = bot
self.bandcharts = cmn.ImagesGroup(cmn.paths.bandcharts / "meta.json")
self.maps = cmn.ImagesGroup(cmn.paths.maps / "meta.json")
self.bandcharts = cmn.ImagesGroup(cmn.paths.resources / "bandcharts.1.json")
self.maps = cmn.ImagesGroup(cmn.paths.resources / "maps.1.json")
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
@commands.command(name="bandplan", aliases=["plan", "bands"], category=cmn.cat.ref)
async def _bandplan(self, ctx: commands.Context, region: str = ""):
@commands.command(name="bandchart", aliases=["bandplan", "plan", "bands"], category=cmn.Cats.REF)
async def _bandcharts(self, ctx: commands.Context, chart_id: str = ""):
"""Gets the frequency allocations chart for a given country."""
async with ctx.typing():
arg = region.lower()
embed = cmn.embed_factory(ctx)
if arg not in self.bandcharts:
desc = "Possible arguments are:\n"
for key, img in self.bandcharts.items():
desc += f"`{key}`: {img.name}{(' ' + img.emoji if img.emoji else '')}\n"
embed.title = "Bandplan Not Found!"
embed.description = desc
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
metadata: cmn.ImageMetadata = self.bandcharts[arg]
img = discord.File(cmn.paths.bandcharts / metadata.filename,
filename=metadata.filename)
if metadata.description:
embed.description = metadata.description
if metadata.source:
embed.add_field(name="Source", value=metadata.source)
embed.title = metadata.long_name + (" " + metadata.emoji if metadata.emoji else "")
embed.colour = cmn.colours.good
embed.set_image(url="attachment://" + metadata.filename)
await ctx.send(embed=embed, file=img)
await ctx.send(embed=create_embed(ctx, "Bandchart", self.bandcharts, chart_id))
@commands.command(name="map", category=cmn.cat.maps)
@commands.command(name="map", category=cmn.Cats.REF)
async def _map(self, ctx: commands.Context, map_id: str = ""):
"""Posts a ham-relevant map."""
async with ctx.typing():
arg = map_id.lower()
embed = cmn.embed_factory(ctx)
if arg not in self.maps:
desc = "Possible arguments are:\n"
for key, img in self.maps.items():
desc += f"`{key}`: {img.name}{(' ' + img.emoji if img.emoji else '')}\n"
embed.title = "Map Not Found!"
embed.description = desc
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
metadata: cmn.ImageMetadata = self.maps[arg]
img = discord.File(cmn.paths.maps / metadata.filename,
filename=metadata.filename)
if metadata.description:
embed.description = metadata.description
if metadata.source:
embed.add_field(name="Source", value=metadata.source)
embed.title = metadata.long_name + (" " + metadata.emoji if metadata.emoji else "")
embed.colour = cmn.colours.good
embed.set_image(url="attachment://" + metadata.filename)
await ctx.send(embed=embed, file=img)
await ctx.send(embed=create_embed(ctx, "Map", self.maps, map_id))
@commands.command(name="grayline", aliases=["greyline", "grey", "gray", "gl"], category=cmn.cat.maps)
async def _grayline(self, ctx: commands.Context):
"""Gets a map of the current greyline, where HF propagation is the best."""
async with ctx.typing():
embed = cmn.embed_factory(ctx)
embed.title = "Current Greyline Conditions"
embed.colour = cmn.colours.good
async with self.session.get(self.gl_url) as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
data = io.BytesIO(await resp.read())
embed.set_image(url="attachment://greyline.jpg")
await ctx.send(embed=embed, file=discord.File(data, "greyline.jpg"))
def create_embed(ctx: commands.Context, not_found_name: str, db: cmn.ImagesGroup, img_id: str):
"""Creates an embed for the image and its metadata, or list available images in the group."""
img_id = img_id.lower()
embed = cmn.embed_factory(ctx)
if img_id not in db:
desc = "Possible arguments are:\n"
for key, img in db.items():
desc += f"`{key}`: {img.name}{(' ' + img.emoji if img.emoji else '')}\n"
embed.title = f"{not_found_name} Not Found!"
embed.description = desc
embed.colour = cmn.colours.bad
return embed
metadata = db[img_id]
if metadata.description:
embed.description = metadata.description
if metadata.source:
embed.add_field(name="Source", value=metadata.source)
embed.title = metadata.long_name + (" " + metadata.emoji if metadata.emoji else "")
embed.colour = cmn.colours.good
embed.set_image(url=opt.resources_url + metadata.filename)
return embed
def setup(bot: commands.Bot):
+168
View File
@@ -0,0 +1,168 @@
"""
Land Weather extension for qrm
---
Copyright (C) 2019-2020 classabbyamp, 0x5c (as weather.py)
Copyright (C) 2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
import re
from typing import List
import aiohttp
from discord import Embed
import discord.ext.commands as commands
import common as cmn
class WeatherCog(commands.Cog):
wttr_units_regex = re.compile(r"\B-([cCfF])\b")
def __init__(self, bot: commands.Bot):
self.bot = bot
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
@commands.group(name="weather", aliases=["wttr"], case_insensitive=True, category=cmn.Cats.WEATHER)
async def _weather_conditions(self, ctx: commands.Context):
"""Gets local weather conditions from [wttr.in](http://wttr.in/).
*Supported location types:*
city name: `paris`
any location: `~Eiffel Tower`
Unicode name of any location in any language: `Москва`
airport code (3 letters): `muc`
domain name `@stackoverflow.com`
area codes: `12345`
GPS coordinates: `-78.46,106.79`
Add a `-c` or `-f` to use Celcius or Fahrenheit: `-c YSC`"""
if ctx.invoked_subcommand is None:
await ctx.send_help(ctx.command)
@_weather_conditions.command(name="forecast", aliases=["fc", "future"], category=cmn.Cats.WEATHER)
async def _weather_conditions_forecast(self, ctx: commands.Context, *, location: str):
"""Gets local weather forecast for the next three days from [wttr.in](http://wttr.in/).
See help of the `weather` command for possible location types and options."""
try:
units_arg = re.search(self.wttr_units_regex, location).group(1)
except AttributeError:
units_arg = ""
if units_arg.lower() == "f":
units = "u"
elif units_arg.lower() == "c":
units = "m"
else:
units = ""
loc = self.wttr_units_regex.sub("", location).strip()
embed = cmn.embed_factory(ctx)
embed.title = f"Weather Forecast for {loc}"
embed.description = "Data from [wttr.in](http://wttr.in/)."
embed.colour = cmn.colours.good
loc = loc.replace(" ", "+")
embed.set_image(url=f"http://wttr.in/{loc}_{units}pnFQ.png")
await ctx.send(embed=embed)
@_weather_conditions.command(name="now", aliases=["n"], category=cmn.Cats.WEATHER)
async def _weather_conditions_now(self, ctx: commands.Context, *, location: str):
"""Gets current local weather conditions from [wttr.in](http://wttr.in/).
See help of the `weather` command for possible location types and options."""
try:
units_arg = re.search(self.wttr_units_regex, location).group(1)
except AttributeError:
units_arg = ""
if units_arg.lower() == "f":
units = "u"
elif units_arg.lower() == "c":
units = "m"
else:
units = ""
loc = self.wttr_units_regex.sub("", location).strip()
embed = cmn.embed_factory(ctx)
embed.title = f"Current Weather for {loc}"
embed.description = "Data from [wttr.in](http://wttr.in/)."
embed.colour = cmn.colours.good
loc = loc.replace(" ", "+")
embed.set_image(url=f"http://wttr.in/{loc}_0{units}pnFQ.png")
await ctx.send(embed=embed)
@commands.command(name="metar", category=cmn.Cats.WEATHER)
async def metar(self, ctx: commands.Context, airport: str, hours: int = 0):
"""Gets current raw METAR (Meteorological Terminal Aviation Routine Weather Report) for an airport. \
Optionally, a number of hours can be given to show a number of hours of historical METAR data.
Airports should be given as an \
[ICAO code](https://en.wikipedia.org/wiki/List_of_airports_by_IATA_and_ICAO_code)."""
await ctx.send(embed=await self.gen_metar_taf_embed(ctx, airport, hours, False))
@commands.command(name="taf", category=cmn.Cats.WEATHER)
async def taf(self, ctx: commands.Context, airport: str):
"""Gets forecasted raw TAF (Terminal Aerodrome Forecast) data for an airport. Includes the latest METAR data.
Airports should be given as an \
[ICAO code](https://en.wikipedia.org/wiki/List_of_airports_by_IATA_and_ICAO_code)."""
await ctx.send(embed=await self.gen_metar_taf_embed(ctx, airport, 0, True))
async def gen_metar_taf_embed(self, ctx: commands.Context, airport: str, hours: int, taf: bool) -> Embed:
embed = cmn.embed_factory(ctx)
airport = airport.upper()
if re.fullmatch(r"\w(\w|\d){2,3}", airport):
metar = await self.get_metar_taf_data(airport, hours, taf)
if taf:
embed.title = f"Current TAF for {airport}"
elif hours > 0:
embed.title = f"METAR for {airport} for the last {hours} hour{'s' if hours > 1 else ''}"
else:
embed.title = f"Current METAR for {airport}"
embed.description = "Data from [aviationweather.gov](https://www.aviationweather.gov/metar/data)."
embed.colour = cmn.colours.good
data = "\n".join(metar)
embed.description += f"\n\n```\n{data}\n```"
else:
embed.title = "Invalid airport given!"
embed.colour = cmn.colours.bad
return embed
async def get_metar_taf_data(self, airport: str, hours: int, taf: bool) -> List[str]:
url = (f"https://www.aviationweather.gov/metar/data?ids={airport}&format=raw&hours={hours}"
f"&taf={'on' if taf else 'off'}&layout=off")
async with self.session.get(url) as r:
if r.status != 200:
raise cmn.BotHTTPError(r)
page = await r.text()
# pare down to just the data
page = page.split("<!-- Data starts here -->")[1].split("<!-- Data ends here -->")[0].strip()
# split at <hr>s
data = re.split(r"<hr.*>", page, maxsplit=len(airport))
parsed = []
for sec in data:
if sec.strip():
for line in sec.split("\n"):
line = line.strip()
# remove HTML stuff
line = line.replace("<code>", "").replace("</code>", "")
line = line.replace("<strong>", "").replace("</strong>", "")
line = line.replace("<br/>", "\n").replace("&nbsp;", " ")
line = line.strip("\n")
parsed.append(line)
return parsed
def setup(bot: commands.Bot):
bot.add_cog(WeatherCog(bot))
+13 -8
View File
@@ -1,30 +1,35 @@
"""
Morse Code extension for qrm
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
import json
import discord.ext.commands as commands
import common as cmn
from resources import morse
class MorseCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
with open(cmn.paths.resources / "morse.1.json") as file:
d = json.load(file)
self.morse: dict[str, str] = d["morse"]
self.ascii: dict[str, int] = d["ascii"]
@commands.command(name="morse", aliases=["cw"], category=cmn.cat.ref)
@commands.command(name="morse", aliases=["cw"], category=cmn.Cats.CODES)
async def _morse(self, ctx: commands.Context, *, msg: str):
"""Converts ASCII to international morse code."""
result = ""
for char in msg.upper():
try:
result += morse.morse[char]
result += self.morse[char]
except KeyError:
result += "<?>"
result += " "
@@ -34,7 +39,7 @@ class MorseCog(commands.Cog):
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
@commands.command(name="unmorse", aliases=["demorse", "uncw", "decw"], category=cmn.cat.ref)
@commands.command(name="unmorse", aliases=["demorse", "uncw", "decw"], category=cmn.Cats.CODES)
async def _unmorse(self, ctx: commands.Context, *, msg: str):
"""Converts international morse code to ASCII."""
result = ""
@@ -44,7 +49,7 @@ class MorseCog(commands.Cog):
for word in msg:
for char in word:
try:
result += morse.ascii[char]
result += self.ascii[char]
except KeyError:
result += "<?>"
result += " "
@@ -54,7 +59,7 @@ class MorseCog(commands.Cog):
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
@commands.command(name="cwweight", aliases=["weight", "cww"], category=cmn.cat.ref)
@commands.command(name="cwweight", aliases=["weight", "cww"], category=cmn.Cats.CODES)
async def _weight(self, ctx: commands.Context, *, msg: str):
"""Calculates the CW weight of a callsign or message."""
embed = cmn.embed_factory(ctx)
@@ -62,7 +67,7 @@ class MorseCog(commands.Cog):
weight = 0
for char in msg:
try:
cw_char = morse.morse[char].replace("-", "==")
cw_char = self.morse[char].replace("-", "==")
weight += len(cw_char) * 2 + 2
except KeyError:
embed.title = "Error in calculation of CW weight"
+48
View File
@@ -0,0 +1,48 @@
"""
Prefixes Lookup extension for qrm
---
Copyright (C) 2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
import discord.ext.commands as commands
import common as cmn
from resources import callsign_info
class PrefixesCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.pfxs = callsign_info.options
@commands.command(name="prefixes", aliases=["vanity", "pfx", "vanities", "prefix"], category=cmn.Cats.REF)
async def _vanity_prefixes(self, ctx: commands.Context, country: str = ""):
"""Lists valid callsign prefixes for different countries."""
country = country.lower()
embed = cmn.embed_factory(ctx)
if country not in self.pfxs:
desc = "Possible arguments are:\n"
for key, val in self.pfxs.items():
desc += f"`{key}`: {val.title}{(' ' + val.emoji if val.emoji else '')}\n"
embed.title = f"{country} Not Found!"
embed.description = desc
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
else:
data = self.pfxs[country]
embed.title = data.title + (" " + data.emoji if data.emoji else "")
embed.description = data.desc
embed.colour = cmn.colours.good
for name, val in data.calls.items():
embed.add_field(name=name, value=val, inline=False)
await ctx.send(embed=embed)
def setup(bot: commands.Bot):
bot.add_cog(PrefixesCog(bot))
+87
View File
@@ -0,0 +1,87 @@
"""
Propagation extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
from io import BytesIO
import aiohttp
import cairosvg
from datetime import datetime
import discord
import discord.ext.commands as commands
import common as cmn
class PropagationCog(commands.Cog):
muf_url = "https://prop.kc2g.com/renders/current/mufd-normal-now.svg"
fof2_url = "https://prop.kc2g.com/renders/current/fof2-normal-now.svg"
gl_baseurl = "https://www.fourmilab.ch/cgi-bin/uncgi/Earth?img=ETOPO1_day-m.evif&dynimg=y&opt=-p"
n0nbh_sun_url = "http://www.hamqsl.com/solarsun.php"
def __init__(self, bot):
self.bot = bot
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
@commands.command(name="mufmap", aliases=["muf"], category=cmn.Cats.WEATHER)
async def mufmap(self, ctx: commands.Context):
"""Shows a world map of the Maximum Usable Frequency (MUF)."""
async with ctx.typing():
async with self.session.get(self.muf_url) as r:
svg = await r.read()
out = BytesIO(cairosvg.svg2png(bytestring=svg))
file = discord.File(out, "muf_map.png")
embed = cmn.embed_factory(ctx)
embed.title = "Maximum Usable Frequency Map"
embed.description = "Image from [prop.kc2g.com](https://prop.kc2g.com/)\nData sources listed on the page."
embed.set_image(url="attachment://muf_map.png")
await ctx.send(file=file, embed=embed)
@commands.command(name="fof2map", aliases=["fof2", "critfreq"], category=cmn.Cats.WEATHER)
async def fof2map(self, ctx: commands.Context):
"""Shows a world map of the Critical Frequency (foF2)."""
async with ctx.typing():
async with self.session.get(self.fof2_url) as r:
svg = await r.read()
out = BytesIO(cairosvg.svg2png(bytestring=svg))
file = discord.File(out, "fof2_map.png")
embed = cmn.embed_factory(ctx)
embed.title = "Critical Frequency (foF2) Map"
embed.description = "Image from [prop.kc2g.com](https://prop.kc2g.com/)\nData sources listed on the page."
embed.set_image(url="attachment://fof2_map.png")
await ctx.send(file=file, embed=embed)
@commands.command(name="grayline", aliases=["greyline", "grey", "gray", "gl"], category=cmn.Cats.WEATHER)
async def grayline(self, ctx: commands.Context):
"""Gets a map of the current greyline, where HF propagation is the best."""
embed = cmn.embed_factory(ctx)
embed.title = "Current Greyline Conditions"
embed.colour = cmn.colours.good
date_params = f"&date=1&utc={datetime.utcnow():%Y-%m-%d+%H:%M:%S}"
embed.set_image(url=self.gl_baseurl + date_params)
await ctx.send(embed=embed)
@commands.command(name="solarweather", aliases=["solar", "bandconditions", "cond", "condx", "conditions"],
category=cmn.Cats.WEATHER)
async def solarweather(self, ctx: commands.Context):
"""Gets a solar weather report."""
embed = cmn.embed_factory(ctx)
embed.title = "☀️ Current Solar Weather"
if ctx.invoked_with in ["bandconditions", "cond", "condx", "conditions"]:
embed.add_field(name="⚠️ Deprecated Command Alias",
value=(f"This command has been renamed to `{ctx.prefix}solar`!\n"
"The alias you used will be removed in the next version."))
embed.colour = cmn.colours.good
embed.set_image(url=self.n0nbh_sun_url)
await ctx.send(embed=embed)
def setup(bot: commands.Bot):
bot.add_cog(PropagationCog(bot))
-182
View File
@@ -1,182 +0,0 @@
"""
QRZ extension for qrm
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
from io import BytesIO
import aiohttp
from lxml import etree
from discord.ext import commands, tasks
import common as cmn
import data.keys as keys
class QRZCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
self._qrz_session_init.start()
@commands.command(name="call", aliases=["qrz"], category=cmn.cat.lookup)
async def _qrz_lookup(self, ctx: commands.Context, callsign: str, *flags):
"""Looks up a callsign on [QRZ.com](https://www.qrz.com/). Add `--link` to only link the QRZ page."""
flags = [f.lower() for f in flags]
if keys.qrz_user == "" or keys.qrz_pass == "" or "--link" in flags:
await ctx.send(f"http://qrz.com/db/{callsign}")
return
async with ctx.typing():
try:
await qrz_test_session(self.key, self.session)
except ConnectionError:
await self.get_session()
url = f"http://xmldata.qrz.com/xml/current/?s={self.key};callsign={callsign}"
async with self.session.get(url) as resp:
if resp.status != 200:
raise ConnectionError(f"Unable to connect to QRZ (HTTP Error {resp.status})")
with BytesIO(await resp.read()) as resp_file:
resp_xml = etree.parse(resp_file).getroot()
resp_xml_session = resp_xml.xpath("/x:QRZDatabase/x:Session", namespaces={"x": "http://xmldata.qrz.com"})
resp_session = {el.tag.split("}")[1]: el.text for el in resp_xml_session[0].getiterator()}
if "Error" in resp_session:
if "Session Timeout" in resp_session["Error"]:
await self.get_session()
await self._qrz_lookup(ctx, callsign)
return
if "Not found" in resp_session["Error"]:
embed = cmn.embed_factory(ctx)
embed.title = f"QRZ Data for {callsign.upper()}"
embed.colour = cmn.colours.bad
embed.description = "No data found!"
await ctx.send(embed=embed)
return
raise ValueError(resp_session["Error"])
resp_xml_data = resp_xml.xpath("/x:QRZDatabase/x:Callsign", namespaces={"x": "http://xmldata.qrz.com"})
resp_data = {el.tag.split("}")[1]: el.text for el in resp_xml_data[0].getiterator()}
embed = cmn.embed_factory(ctx)
embed.title = f"QRZ Data for {resp_data['call']}"
embed.colour = cmn.colours.good
embed.url = f"http://www.qrz.com/db/{resp_data['call']}"
if "image" in resp_data:
embed.set_thumbnail(url=resp_data["image"])
data = qrz_process_info(resp_data)
for title, val in data.items():
if val is not None:
embed.add_field(name=title, value=val, inline=True)
await ctx.send(embed=embed)
async def get_session(self):
"""Session creation and caching."""
self.key = await qrz_login(keys.qrz_user, keys.qrz_pass, self.session)
with open("data/qrz_session", "w") as qrz_file:
qrz_file.write(self.key)
@tasks.loop(count=1)
async def _qrz_session_init(self):
"""Helper task to allow obtaining a session at cog instantiation."""
try:
with open("data/qrz_session") as qrz_file:
self.key = qrz_file.readline().strip()
await qrz_test_session(self.key, self.session)
except (FileNotFoundError, ConnectionError):
await self.get_session()
async def qrz_login(user: str, passwd: str, session: aiohttp.ClientSession):
url = f"http://xmldata.qrz.com/xml/current/?username={user};password={passwd};agent=discord-qrm2"
async with session.get(url) as resp:
if resp.status != 200:
raise ConnectionError(f"Unable to connect to QRZ (HTTP Error {resp.status})")
with BytesIO(await resp.read()) as resp_file:
resp_xml = etree.parse(resp_file).getroot()
resp_xml_session = resp_xml.xpath("/x:QRZDatabase/x:Session", namespaces={"x": "http://xmldata.qrz.com"})
resp_session = {el.tag.split("}")[1]: el.text for el in resp_xml_session[0].getiterator()}
if "Error" in resp_session:
raise ConnectionError(resp_session["Error"])
if resp_session["SubExp"] == "non-subscriber":
raise ConnectionError("Invalid QRZ Subscription")
return resp_session["Key"]
async def qrz_test_session(key: str, session: aiohttp.ClientSession):
url = f"http://xmldata.qrz.com/xml/current/?s={key}"
async with session.get(url) as resp:
if resp.status != 200:
raise ConnectionError(f"Unable to connect to QRZ (HTTP Error {resp.status})")
with BytesIO(await resp.read()) as resp_file:
resp_xml = etree.parse(resp_file).getroot()
resp_xml_session = resp_xml.xpath("/x:QRZDatabase/x:Session", namespaces={"x": "http://xmldata.qrz.com"})
resp_session = {el.tag.split("}")[1]: el.text for el in resp_xml_session[0].getiterator()}
if "Error" in resp_session:
raise ConnectionError(resp_session["Error"])
def qrz_process_info(data: dict):
if "name" in data:
if "fname" in data:
name = data["fname"] + " " + data["name"]
else:
name = data["name"]
else:
name = None
if "state" in data:
state = f", {data['state']}"
else:
state = ""
address = data.get("addr1", "") + "\n" + data.get("addr2", "") + state + " " + data.get("zip", "")
address = address.strip()
if address == "":
address = None
if "eqsl" in data:
eqsl = "Yes" if data["eqsl"] == 1 else "No"
else:
eqsl = "Unknown"
if "mqsl" in data:
mqsl = "Yes" if data["mqsl"] == 1 else "No"
else:
mqsl = "Unknown"
if "lotw" in data:
lotw = "Yes" if data["lotw"] == 1 else "No"
else:
lotw = "Unknown"
return {"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):
bot.add_cog(QRZCog(bot))
+76 -30
View File
@@ -1,7 +1,7 @@
"""
Study extension for qrm
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
@@ -22,7 +22,8 @@ from resources import study
class StudyCog(commands.Cog):
choices = {cmn.emojis.a: "A", cmn.emojis.b: "B", cmn.emojis.c: "C", cmn.emojis.d: "D"}
choices = {"A": cmn.emojis.a, "B": cmn.emojis.b, "C": cmn.emojis.c, "D": cmn.emojis.d, "E": cmn.emojis.e}
choices_inv = {y: x for x, y in choices.items()}
def __init__(self, bot: commands.Bot):
self.bot = bot
@@ -30,14 +31,15 @@ class StudyCog(commands.Cog):
self.source = "Data courtesy of [HamStudy.org](https://hamstudy.org/)"
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
@commands.command(name="hamstudy", aliases=["rq", "randomquestion", "randomq"], category=cmn.cat.study)
async def _random_question(self, ctx: commands.Context, country: str = "", level: str = ""):
@commands.command(name="hamstudy", aliases=["rq", "randomquestion", "randomq"], category=cmn.Cats.STUDY)
async def _random_question(self, ctx: commands.Context, country: str = "", level: str = "", element: str = ""):
"""Gets a random question from [HamStudy's](https://hamstudy.org) question pools."""
with ctx.typing():
embed = cmn.embed_factory(ctx)
country = country.lower()
level = level.lower()
element = element.upper()
if country in study.pool_names.keys():
if level in study.pool_names[country].keys():
@@ -115,21 +117,37 @@ class StudyCog(commands.Cog):
pool = json.loads(await resp.read())["pool"]
# Select a question
pool_section = random.choice(pool)["sections"]
if element:
els = [el["id"] for el in pool]
if element in els:
pool_section = pool[els.index(element)]["sections"]
else:
embed.title = "Element Not Found!"
embed.description = f"Possible Elements for Country `{country}` and Level `{level}` are:"
embed.colour = cmn.colours.bad
embed.description += "\n\n" + "`" + "`, `".join(els) + "`"
await ctx.send(embed=embed)
return
else:
pool_section = random.choice(pool)["sections"]
pool_questions = random.choice(pool_section)["questions"]
question = random.choice(pool_questions)
answers = question['answers']
answers_str = ""
answers_str_bolded = ""
for letter, ans in answers.items():
answers_str += f"{self.choices[letter]} {ans}\n"
if letter == question["answer"]:
answers_str_bolded += f"{self.choices[letter]} **{ans}**\n"
else:
answers_str_bolded += f"{self.choices[letter]} {ans}\n"
embed.title = f"{study.pool_emojis[country]} {pool_meta['class']} {question['id']}"
embed.description = self.source
embed.add_field(name="Question:", value=question["text"], inline=False)
embed.add_field(name="Answers:",
value=(f"**{cmn.emojis.a}** {question['answers']['A']}"
f"\n**{cmn.emojis.b}** {question['answers']['B']}"
f"\n**{cmn.emojis.c}** {question['answers']['C']}"
f"\n**{cmn.emojis.d}** {question['answers']['D']}"),
inline=False)
embed.add_field(name="To Answer:",
value=("Answer with reactions below. If not answered within 10 minutes,"
embed.add_field(name="Question", value=question["text"], inline=False)
embed.add_field(name="Answers", value=answers_str, inline=False)
embed.add_field(name="To Answer",
value=("Answer with reactions below. If not answered within 5 minutes,"
" the answer will be revealed."),
inline=False)
if "image" in question:
@@ -138,33 +156,61 @@ class StudyCog(commands.Cog):
q_msg = await ctx.send(embed=embed)
await cmn.add_react(q_msg, cmn.emojis.a)
await cmn.add_react(q_msg, cmn.emojis.b)
await cmn.add_react(q_msg, cmn.emojis.c)
await cmn.add_react(q_msg, cmn.emojis.d)
for i in range(len(answers)):
await cmn.add_react(q_msg, list(self.choices.values())[i])
await cmn.add_react(q_msg, cmn.emojis.question)
def check(reaction, user):
return (user.id != self.bot.user.id
and reaction.message.id == q_msg.id
and str(reaction.emoji) in self.choices.keys())
and (str(reaction.emoji) in self.choices.values() or str(reaction.emoji) == cmn.emojis.question))
try:
reaction, user = await self.bot.wait_for("reaction_add", timeout=600.0, check=check)
reaction, user = await self.bot.wait_for("reaction_add", timeout=300.0, check=check)
except asyncio.TimeoutError:
embed.remove_field(2)
embed.add_field(name="Answer:", value=f"Timed out! The correct answer was **{question['answer']}**.")
embed.set_field_at(1, name="Answers", value=answers_str_bolded, inline=False)
embed.set_field_at(2, name="Answer",
value=(f"{cmn.emojis.stopwatch} "
f"**Timed out!** The correct answer was {self.choices[question['answer']]}"))
embed.colour = cmn.colours.timeout
await q_msg.edit(embed=embed)
else:
if self.choices[str(reaction.emoji)] == question["answer"]:
embed.remove_field(2)
embed.add_field(name="Answer:", value=f"Correct! The answer was **{question['answer']}**.")
embed.colour = cmn.colours.good
if str(reaction.emoji) == cmn.emojis.question:
embed.set_field_at(1, name="Answers", value=answers_str_bolded, inline=False)
embed.set_field_at(2, name="Answer",
value=f"The correct answer was {self.choices[question['answer']]}", inline=False)
embed.add_field(name="Answer Requested By", value=str(user), inline=False)
embed.colour = cmn.colours.timeout
await q_msg.edit(embed=embed)
else:
embed.remove_field(2)
embed.add_field(name="Answer:", value=f"Incorrect! The correct answer was **{question['answer']}**.")
embed.colour = cmn.colours.bad
await q_msg.edit(embed=embed)
answers_str_checked = ""
chosen_ans = self.choices_inv[str(reaction.emoji)]
for letter, ans in answers.items():
answers_str_checked += f"{self.choices[letter]}"
if letter == question["answer"] == chosen_ans:
answers_str_checked += f"{cmn.emojis.check_mark} **{ans}**\n"
elif letter == question["answer"]:
answers_str_checked += f" **{ans}**\n"
elif letter == chosen_ans:
answers_str_checked += f"{cmn.emojis.x} {ans}\n"
else:
answers_str_checked += f" {ans}\n"
if self.choices[question["answer"]] == str(reaction.emoji):
embed.set_field_at(1, name="Answers", value=answers_str_checked, inline=False)
embed.set_field_at(2, name="Answer", value=(f"{cmn.emojis.check_mark} "
f"**Correct!** The answer was {reaction.emoji}"))
embed.add_field(name="Answered By", value=str(user), inline=False)
embed.colour = cmn.colours.good
await q_msg.edit(embed=embed)
else:
embed.set_field_at(1, name="Answers", value=answers_str_checked, inline=False)
embed.set_field_at(2, name="Answer",
value=(f"{cmn.emojis.x} **Incorrect!** The correct answer was "
f"{self.choices[question['answer']]}, not {reaction.emoji}"))
embed.add_field(name="Answered By", value=str(user), inline=False)
embed.colour = cmn.colours.bad
await q_msg.edit(embed=embed)
async def hamstudy_get_pools(self):
async with self.session.get("https://hamstudy.org/pools/") as resp:
+72
View File
@@ -0,0 +1,72 @@
"""
TeX extension for qrm
---
Copyright (C) 2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
import aiohttp
from io import BytesIO
from urllib.parse import urljoin
import discord
import discord.ext.commands as commands
import common as cmn
import data.options as opt
class TexCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
with open(cmn.paths.resources / "template.1.tex") as latex_template:
self.template = latex_template.read()
@commands.command(name="tex", aliases=["latex"], category=cmn.Cats.UTILS)
async def tex(self, ctx: commands.Context, *, expr: str):
"""Renders a LaTeX expression.
In paragraph mode by default. To render math, add `$` around math expressions.
"""
payload = {
"format": "png",
"code": self.template.replace("#CONTENT#", expr),
"quality": 50
}
with ctx.typing():
# ask rTeX to render our expression
async with self.session.post(urljoin(opt.rtex_instance, "api/v2"), json=payload) as r:
if r.status != 200:
raise cmn.BotHTTPError(r)
render_result = await r.json()
if render_result["status"] != "success":
embed = cmn.embed_factory(ctx)
embed.title = "LaTeX Rendering Failed!"
embed.description = ("Here are some common reasons:\n"
"• Did you forget to use math mode? Surround math expressions with `$`,"
" like `$x^3$`.\n"
"• Are you using a command from a package? It might not be available.\n"
"• Are you including the document headers? We already did that for you.")
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
# if rendering went well, download the file given in the response
async with self.session.get(urljoin(opt.rtex_instance, "api/v2/" + render_result["filename"])) as r:
png_buffer = BytesIO(await r.read())
embed = cmn.embed_factory(ctx)
embed.title = "LaTeX Expression"
embed.description = "Rendered by [rTeX](https://rtex.probablyaweb.site/)."
embed.set_image(url="attachment://tex.png")
await ctx.send(file=discord.File(png_buffer, "tex.png"), embed=embed)
def setup(bot: commands.Bot):
bot.add_cog(TexCog(bot))
+80
View File
@@ -0,0 +1,80 @@
"""
Time extension for qrm
---
Copyright (C) 2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
from datetime import datetime, timedelta
import discord.ext.commands as commands
import common as cmn
class TimeCog(commands.Cog):
offsets = [
("Y", "", timedelta(hours=-12)),
("X", "", timedelta(hours=-11)),
("W", "", timedelta(hours=-10)),
("V", "", timedelta(hours=-9)),
("U", "", timedelta(hours=-8)),
("T", "", timedelta(hours=-7)),
("S", "", timedelta(hours=-6)),
("R", "", timedelta(hours=-5)),
("Q", "", timedelta(hours=-4)),
("P", "", timedelta(hours=-3)),
("O", "", timedelta(hours=-2)),
("N", "", timedelta(hours=-1)),
("Z", "UTC", timedelta(hours=0)),
("A", "", timedelta(hours=+1)),
("B", "", timedelta(hours=+2)),
("C", "", timedelta(hours=+3)),
("D", "", timedelta(hours=+4)),
("E", "", timedelta(hours=+5)),
("F", "", timedelta(hours=+6)),
("G", "", timedelta(hours=+7)),
("H", "", timedelta(hours=+8)),
("I", "", timedelta(hours=+9)),
("K", "", timedelta(hours=+10)),
("L", "", timedelta(hours=+11)),
("M", "", timedelta(hours=+12))
]
def __init__(self, bot):
self.bot = bot
@commands.command(name="utc", aliases=["z"], category=cmn.Cats.TIME)
async def _utc_lookup(self, ctx: commands.Context):
"""Returns the current time in UTC."""
now = datetime.utcnow()
result = "**" + now.strftime("%Y-%m-%d %H:%M") + "Z**"
embed = cmn.embed_factory(ctx)
embed.title = "The current time is:"
embed.description = result
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
@commands.command(name="miltime", category=cmn.Cats.TIME)
async def miltime(self, ctx: commands.Context):
"""Prints the current time in all 25 military time zones."""
time = ctx.message.created_at
embed = cmn.embed_factory(ctx)
embed.title = f"{cmn.emojis.clock} Military Time Zones Now"
embed.colour = cmn.colours.good
embed.description = "```"
embed.description += "\n".join([f"{x}: {time + z :%Y-%m-%d %H:%M} {y}" for x, y, z in self.offsets])
embed.description += "```"
embed.add_field(name="Notes", value=(
"**J** is not present in the table, and is used for local time.\n"
"The zones are referenced by their letters, using phonetics.\n"
f"You can check the NATO phonetics for a letter using the `{ctx.prefix}phonetics` command."
))
await ctx.send(embed=embed)
def setup(bot: commands.Bot):
bot.add_cog(TimeCog(bot))
-124
View File
@@ -1,124 +0,0 @@
"""
Weather extension for qrm
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
import io
import re
import aiohttp
import discord
import discord.ext.commands as commands
import common as cmn
class WeatherCog(commands.Cog):
wttr_units_regex = re.compile(r"\B-([cCfF])\b")
def __init__(self, bot: commands.Bot):
self.bot = bot
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
@commands.command(name="bandconditions", aliases=["cond", "condx", "conditions"], category=cmn.cat.weather)
async def _band_conditions(self, ctx: commands.Context):
"""Gets a solar conditions report."""
async with ctx.typing():
embed = cmn.embed_factory(ctx)
embed.title = "Current Solar Conditions"
embed.colour = cmn.colours.good
async with self.session.get("http://www.hamqsl.com/solarsun.php") as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
data = io.BytesIO(await resp.read())
embed.set_image(url="attachment://condx.png")
await ctx.send(embed=embed, file=discord.File(data, "condx.png"))
@commands.group(name="weather", aliases=["wttr"], case_insensitive=True, category=cmn.cat.weather)
async def _weather_conditions(self, ctx: commands.Context):
"""Gets local weather conditions from [wttr.in](http://wttr.in/).
*Supported location types:*
city name: `paris`
any location: `~Eiffel Tower`
Unicode name of any location in any language: `Москва`
airport code (3 letters): `muc`
domain name `@stackoverflow.com`
area codes: `12345`
GPS coordinates: `-78.46,106.79`
Add a `-c` or `-f` to use Celcius or Fahrenheit: `-c YSC`"""
if ctx.invoked_subcommand is None:
await ctx.send_help(ctx.command)
@_weather_conditions.command(name="forecast", aliases=["fc", "future"], category=cmn.cat.weather)
async def _weather_conditions_forecast(self, ctx: commands.Context, *, location: str):
"""Gets local weather forecast for the next three days from [wttr.in](http://wttr.in/).
See help of the `weather` command for possible location types and options."""
async with ctx.typing():
try:
units_arg = re.search(self.wttr_units_regex, location).group(1)
except AttributeError:
units_arg = ""
if units_arg.lower() == "f":
units = "u"
elif units_arg.lower() == "c":
units = "m"
else:
units = ""
loc = self.wttr_units_regex.sub("", location).strip()
embed = cmn.embed_factory(ctx)
embed.title = f"Weather Forecast for {loc}"
embed.description = "Data from [wttr.in](http://wttr.in/)."
embed.colour = cmn.colours.good
loc = loc.replace(" ", "+")
async with self.session.get(f"http://wttr.in/{loc}_{units}pnFQ.png") as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
data = io.BytesIO(await resp.read())
embed.set_image(url="attachment://wttr_forecast.png")
await ctx.send(embed=embed, file=discord.File(data, "wttr_forecast.png"))
@_weather_conditions.command(name="now", aliases=["n"], category=cmn.cat.weather)
async def _weather_conditions_now(self, ctx: commands.Context, *, location: str):
"""Gets current local weather conditions from [wttr.in](http://wttr.in/).
See help of the `weather` command for possible location types and options."""
async with ctx.typing():
try:
units_arg = re.search(self.wttr_units_regex, location).group(1)
except AttributeError:
units_arg = ""
if units_arg.lower() == "f":
units = "u"
elif units_arg.lower() == "c":
units = "m"
else:
units = ""
loc = self.wttr_units_regex.sub("", location).strip()
embed = cmn.embed_factory(ctx)
embed.title = f"Current Weather for {loc}"
embed.description = "Data from [wttr.in](http://wttr.in/)."
embed.colour = cmn.colours.good
loc = loc.replace(" ", "+")
async with self.session.get(f"http://wttr.in/{loc}_0{units}pnFQ.png") as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
data = io.BytesIO(await resp.read())
embed.set_image(url="attachment://wttr_now.png")
await ctx.send(embed=embed, file=discord.File(data, "wttr_now.png"))
def setup(bot: commands.Bot):
bot.add_cog(WeatherCog(bot))
+6 -3
View File
@@ -1,7 +1,7 @@
"""
Static info about the bot.
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
@@ -11,6 +11,9 @@ 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/miaowware/qrm2"
release = "2.4.1"
contributing = """Check out the [source on GitHub](https://github.com/miaowware/qrm2). Contributions are welcome!
All issues and requests related to resources (including maps, band charts, data) should be added \
in [miaowware/qrm-resources](https://github.com/miaowware/qrm-resources)."""
release = "2.7.1"
bot_server = "https://discord.gg/Ntbg3J4"
+60 -19
View File
@@ -2,7 +2,7 @@
"""
qrm, a bot for Discord
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
@@ -15,6 +15,7 @@ import sys
import traceback
from datetime import datetime, time
from types import SimpleNamespace
from pathlib import Path
import pytz
@@ -24,6 +25,7 @@ from discord.ext import commands, tasks
import info
import common as cmn
import utils.connector as conn
from utils.resources_manager import ResourcesManager
import data.keys as keys
import data.options as opt
@@ -34,6 +36,7 @@ import data.options as opt
exit_code = 1 # The default exit code. ?shutdown and ?restart will change it accordingly (fail-safe)
ext_dir = "exts" # The name of the directory where extensions are located.
plugin_dir = "data.plugins" # The name of the directory where plugins are located.
debug_mode = opt.debug # Separate assignement in-case we define an override (ternary operator goes here)
@@ -45,20 +48,19 @@ loop = asyncio.get_event_loop()
connector = loop.run_until_complete(conn.new_connector())
# Defining the intents
intents = discord.Intents.default()
# We don't need those
intents.bans = False
intents.emojis = False
intents.integrations = False
intents.webhooks = False
intents.invites = False
intents.voice_states = False
intents.typing = False
intents = discord.Intents.none()
intents.guilds = True
intents.guild_messages = True
intents.dm_messages = True
intents.reactions = True
member_cache = discord.MemberCacheFlags.from_intents(intents)
bot = commands.Bot(command_prefix=opt.prefix,
case_insensitive=True,
description=info.description, help_command=commands.MinimalHelpCommand(),
intents=intents,
member_cache_flags=member_cache,
loop=loop,
connector=connector)
@@ -72,7 +74,7 @@ bot.qrm.debug_mode = debug_mode
# --- Commands ---
@bot.command(name="restart", aliases=["rs"], category=cmn.cat.admin)
@bot.command(name="restart", aliases=["rs"], category=cmn.BoltCats.ADMIN)
@commands.check(cmn.check_if_owner)
async def _restart_bot(ctx: commands.Context):
"""Restarts the bot."""
@@ -83,7 +85,7 @@ async def _restart_bot(ctx: commands.Context):
await bot.logout()
@bot.command(name="shutdown", aliases=["shut"], category=cmn.cat.admin)
@bot.command(name="shutdown", aliases=["shut"], category=cmn.BoltCats.ADMIN)
@commands.check(cmn.check_if_owner)
async def _shutdown_bot(ctx: commands.Context):
"""Shuts down the bot."""
@@ -94,13 +96,13 @@ async def _shutdown_bot(ctx: commands.Context):
await bot.logout()
@bot.group(name="extctl", aliases=["ex"], case_insensitive=True, category=cmn.cat.admin)
@bot.group(name="extctl", aliases=["ex"], case_insensitive=True, category=cmn.BoltCats.ADMIN)
@commands.check(cmn.check_if_owner)
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)
@@ -109,14 +111,24 @@ async def _extctl_list(ctx: commands.Context):
"""Lists loaded extensions."""
embed = cmn.embed_factory(ctx)
embed.title = "Loaded Extensions"
embed.description = "\n".join(["" + x.split(".")[1] for x in bot.extensions.keys()])
embed.description = "\n".join(
["" + x.split(".")[-1] for x in bot.extensions.keys() if not x.startswith(plugin_dir)]
)
if plugins := ["" + x.split(".")[-1] for x in bot.extensions.keys() if x.startswith(plugin_dir)]:
embed.add_field(name="Loaded Plugins", value="\n".join(plugins))
await ctx.send(embed=embed)
@_extctl.command(name="load", aliases=["ld"])
async def _extctl_load(ctx: commands.Context, extension: str):
"""Loads an extension."""
bot.load_extension(ext_dir + "." + extension)
try:
bot.load_extension(ext_dir + "." + extension)
except commands.ExtensionNotFound as e:
try:
bot.load_extension(plugin_dir + "." + extension)
except commands.ExtensionNotFound:
raise e
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
@@ -127,14 +139,26 @@ async def _extctl_reload(ctx: commands.Context, extension: str):
pika = bot.get_emoji(opt.pika)
if pika:
await cmn.add_react(ctx.message, pika)
bot.reload_extension(ext_dir + "." + extension)
try:
bot.reload_extension(ext_dir + "." + extension)
except commands.ExtensionNotLoaded as e:
try:
bot.reload_extension(plugin_dir + "." + extension)
except commands.ExtensionNotLoaded:
raise e
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
@_extctl.command(name="unload", aliases=["ul"])
async def _extctl_unload(ctx: commands.Context, extension: str):
"""Unloads an extension."""
bot.unload_extension(ext_dir + "." + extension)
try:
bot.unload_extension(ext_dir + "." + extension)
except commands.ExtensionNotLoaded as e:
try:
bot.unload_extension(plugin_dir + "." + extension)
except commands.ExtensionNotLoaded:
raise e
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
@@ -168,7 +192,7 @@ async def on_command_error(ctx: commands.Context, err: commands.CommandError):
await cmn.add_react(ctx.message, cmn.emojis.warning)
await ctx.send_help(ctx.command)
elif isinstance(err, commands.CommandNotFound):
if ctx.invoked_with.startswith(("?", "!")):
if ctx.invoked_with and ctx.invoked_with.startswith(("?", "!")):
return
else:
await cmn.add_react(ctx.message, cmn.emojis.question)
@@ -244,9 +268,26 @@ async def _ensure_activity_fixed():
# --- Run ---
resource_versions = {
"bandcharts": "v1",
"img": "v1",
"maps": "v1",
"morse": "v1",
"phonetics": "v1",
"qcodes": "v1",
"funetics": "v1",
"latex_template": "v1",
}
bot.qrm.rm = ResourcesManager(cmn.paths.resources, opt.resources_url, resource_versions)
for ext in opt.exts:
bot.load_extension(ext_dir + "." + ext)
# load all py files in plugin_dir
for plugin in (f.stem for f in Path(plugin_dir.replace(".", "/")).glob("*.py")):
bot.load_extension(plugin_dir + "." + plugin)
try:
bot.run(keys.discord_token)
+9 -5
View File
@@ -1,5 +1,9 @@
discord.py==1.5.0
ctyparser==2.0.0.post1
beautifulsoup4==4.9.1
lxml==4.5.2
pytz==2020.1
discord.py~=1.6.0
ctyparser~=2.0
gridtools~=1.0
qrztools[async]~=1.0
beautifulsoup4
pytz
cairosvg
requests
pydantic
+18
View File
@@ -0,0 +1,18 @@
"""
Resource schemas generator for qrm2.
---
Copyright (C) 2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
import utils.resources_models as models
print("Generating schema for index.json")
with open("./dev-notes/rs_index_schema.json", "w") as file:
file.write(models.Index.schema_json(indent=4))
print("Done!")
+15 -5
View File
@@ -1,18 +1,28 @@
"""
Information about callsigns for the prefixes command in hamcog.
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2021 classabbyamp, 0x5c
This file is part of discord-qrmbot and is released under the terms of
the GNU General Public License, version 2.
"""
from dataclasses import dataclass
from .callsigninfos import (us, ca)
from common import CallsignInfoData
# format: country: (title, description, text)
@dataclass
class CallsignInfoData:
"""Represents a country's callsign info"""
title: str = ""
desc: str = ""
calls: str = ""
emoji: str = ""
options = {
"us": CallsignInfoData([us.title, us.desc, us.calls, us.emoji]),
"ca": CallsignInfoData([ca.title, ca.desc, ca.calls, ca.emoji]),
"us": CallsignInfoData(us.title, us.desc, us.calls, us.emoji),
"ca": CallsignInfoData(ca.title, ca.desc, ca.calls, ca.emoji),
}
+1 -1
View File
@@ -1,7 +1,7 @@
"""
Information about callsigns for the CA prefixes command in hamcog.
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2021 classabbyamp, 0x5c
This file is part of discord-qrmbot and is released under the terms of
the GNU General Public License, version 2.
+1 -1
View File
@@ -1,7 +1,7 @@
"""
Information about callsigns for the US prefixes command in hamcog.
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2021 classabbyamp, 0x5c
This file is part of discord-qrmbot and is released under the terms of
the GNU General Public License, version 2.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

-7
View File
@@ -1,7 +0,0 @@
{
"ca": ["ca.png", "Canada", "Amateur radio bands in Canada", "**This bandplan is incomplete**; some bands, like 630m, are simply not present. It also does not cover any band above 30MHz.", "[RAC 0-30MHz Band Plan](https://www.rac.ca/wp-content/uploads/files/pdf/RAC%20Bandplan%20December%201%202015.pdf)", "🇨🇦"],
"cn": ["cn.png", "China", "Amateur radio bands in China", "", "Created by KN8U and NY7H", "🇨🇳"],
"mx": ["mx.png", "Mexico", "Radio allocations in Mexico", "Full radio allocations chart for all services. No information specific to amateur radio is shown.", "Secretaría de Comunicaciones y Transportes (SCT) / Instituto Federal de Telecomunicaciones (IFT)", "🇲🇽"],
"nl": ["nl.png", "Netherlands", "Amateur radio bands in the Netherlands", "", "", "🇳🇱"],
"us": ["us.png", "USA", "Amateur radio bands in the USA", "", "*[ARRL Frequency Chart](https://www.arrl.org/shop/ARRL-Frequency-Chart-11-17/)* [[PDF]](http://www.arrl.org/files/file/Regulatory/Band%20Chart/Band%20Chart%20-%2011X17%20Color.pdf)", "🇺🇸"]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

-10
View File
@@ -1,10 +0,0 @@
{
"arrl": ["arrl-rac.png", "ARRL Sections", "ARRL Sections", "", "[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)", "🇨🇦"],
"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)", "🇺🇸"],
"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)", "🇨🇦"],
"ituz": ["itu-zones.png", "ITU Zones", "ITU Zones", "", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🇺🇳"],
"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)", "🇺🇳"],
"cq": ["cq-zones.png", "CQ Zones", "CQ Zones", "These are used for the CQWW contest.", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🌐"]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

-136
View File
@@ -1,136 +0,0 @@
"""
A listing of morse code symbols
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
This file is part of discord-qrmbot and is released under the terms of
the GNU General Public License, version 2.
"""
morse = {
"A": ".-",
"B": "-...",
"C": "-.-.",
"D": "-..",
"E": ".",
"F": "..-.",
"G": "--.",
"H": "....",
"I": "..",
"J": ".---",
"K": "-.-",
"L": ".-..",
"M": "--",
"N": "-.",
"O": "---",
"P": ".--.",
"Q": "--.-",
"R": ".-.",
"S": "...",
"T": "-",
"U": "..-",
"V": "...-",
"W": ".--",
"X": "-..-",
"Y": "-.--",
"Z": "--..",
"1": ".----",
"2": "..---",
"3": "...--",
"4": "....-",
"5": ".....",
"6": "-....",
"7": "--...",
"8": "---..",
"9": "----.",
"0": "-----",
".": ".-.-.-",
",": "--..--",
"?": "..--..",
"'": ".----.",
"!": "-.-.--",
"/": "-..-.",
"(": "-.--.",
")": "-.--.-",
"&": ".-...",
":": "---...",
";": "-.-.-.",
"=": "-...-",
"+": ".-.-.",
"-": "-....-",
"\"": ".-..-.",
"@": ".--.-.",
"Ä": ".-.-",
"Å": ".-.-",
"Ą": ".-.-",
"Æ": ".-.-",
"É": "..-..",
"Ñ": "--.--",
"Ö": "---.",
"Ü": "..--",
"Š": "----",
" ": "/"
}
ascii = {
".-": "A",
"-...": "B",
"-.-.": "C",
"-..": "D",
".": "E",
"..-.": "F",
"--.": "G",
"....": "H",
"..": "I",
".---": "J",
"-.-": "K",
".-..": "L",
"--": "M",
"-.": "N",
"---": "O",
".--.": "P",
"--.-": "Q",
".-.": "R",
"...": "S",
"-": "T",
"..-": "U",
"...-": "V",
".--": "W",
"-..-": "X",
"-.--": "Y",
"--..": "Z",
".----": "1",
"..---": "2",
"...--": "3",
"....-": "4",
".....": "5",
"-....": "6",
"--...": "7",
"---..": "8",
"----.": "9",
"-----": "0",
".-.-.-": ".",
"--..--": ",",
"..--..": "?",
".----.": "'",
"-.-.--": "!",
"-..-.": "/",
"-.--.": "(",
"-.--.-": ")",
".-...": "&",
"---...": ":",
"-.-.-.": ";",
"-...-": "=",
".-.-.": "+",
"-....-": "-",
".-..-.": "\"",
".--.-.": "@",
".-.-": "Ä",
"..-..": "É",
"--.--": "Ñ",
"---.": "Ö",
"..--": "Ü",
"----": "Š",
"/": " "
}
-78
View File
@@ -1,78 +0,0 @@
"""
A listing of NATO Phonetics
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
This file is part of discord-qrmbot and is released under the terms of
the GNU General Public License, version 2.
"""
phonetics = {
"a": "alfa",
"b": "bravo",
"c": "charlie",
"d": "delta",
"e": "echo",
"f": "foxtrot",
"g": "golf",
"h": "hotel",
"i": "india",
"j": "juliett",
"k": "kilo",
"l": "lima",
"m": "mike",
"n": "november",
"o": "oscar",
"p": "papa",
"q": "quebec",
"r": "romeo",
"s": "sierra",
"t": "tango",
"u": "uniform",
"v": "victor",
"w": "whiskey",
"x": "x-ray",
"y": "yankee",
"z": "zulu"
}
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,
}
-286
View File
@@ -1,286 +0,0 @@
"""
A listing of Q Codes
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
This file is part of discord-qrmbot and is released under the terms of
the GNU General Public License, version 2.
"""
# flake8: noqa
qcodes = {
"QAB": "May I have clearance (for ...) from ... (place) to ... (place) at flight level/altitude ... ? / You are cleared (or ... is cleared) by ... from ... (place) to ... (place) at flight level/altitude ...",
"QAF": "Will you advise me when you are (were) at (over) ... (place)? / I am (was) at (over) ... (place) (at ... hours) at flight level/altitude ...",
"QAG": "Arrange your flight in order to arrive over ... (place) at ... hours.",
"QAH": "What is your height above ... (datum)? / I am at .... flight level/altitude ... --or-- Arrange your flight so as to reach flight level/altitude ... at ... (hours or place).",
"QAI": "What is the essential traffic respecting my aircraft? / The essential traffic respecting your aircraft is ...",
"QAK": "Is there any risk of collision? / There is risk of collision.",
"QAL": "Are you going to land at ... (place)? I am going to land at ... (place).",
"QAM": "What is the latest available meteorological observation for ... (place)? / Meteorological observation made at ... (place) at ... hours was as follows ...",
"QAN": "What is the surface wind direction and speed at ... (place)? / The surface wind direction and speed at ... (place) at ... hours is ... (direction) ... (speed).",
"QAO": "What is the wind direction in degrees TRUE and speed at ... (position or zone/s) at each of the ... (figures) ... (units) levels above ... (datum)? / The wind direction and speed at (position or zone/s) at flight level/altitude ... is: ... (vertical distance) ... degrees TRUE ... (speed).",
"QAP": "Shall I listen for you (or for ...) on ... kHz (... MHz)? / Listen for me (or for ...) on ... kHz (... MHz).",
"QAQ": "Am I near a prohibited, restricted or danger area?",
"QAR": "May I stop listening on the watch frequency for ... minutes? / You may stop listening on the watch frequency for ... minutes.",
"QAU": "Where may I jettison fuel? / I am about to jettison fuel.",
"QAW": "I am about to carry out overshoot procedure.",
"QAY": "Will you advise me when you pass (passed) ... (place) bearing 090 (270) degrees relative to your heading? / I passed ... (place) bearing ... degrees relative to my heading at ... hours.",
"QAZ": "Are you experiencing communication difficulties through flying in a storm? / I am experiencing communication difficulties through flying in a storm.",
"QBA": "What is the horizontal visibility at ... (place)? / The horizontal visibility at ... (place) at ... hours is ... (distance figures and units).",
"QBB": "What is the amount, type and height above official aerodrome elevation of the base of the cloud [at ... (place)]? / The amount, type and height above official aerodrome elevation of the base of the cloud at ... (place) at ... hours is: ... eights (... type) at ... (figures and units) height above official aerodrome elevation.",
"QBC": "Report meteorological conditions as observed from your aircraft [at ... (position or zone)] [(at ... hours)]. / The meteorological conditions as observed from my aircraft at ... (position or zone) at ... hours at ... (figures and units) height above ... (datum) are ...",
"QBD": "How much fuel have you remaining (expressed as hours and/or minutes of consumption)? / My fuel endurance is ... (hours and/or minutes).",
"QBE": "I am about to wind in my aerial.",
"QBF": "Are you flying in cloud? / I am flying in cloud at ... flight level/altitude ... [and I am ascending (descending) to flight level/altitude ...].",
"QBG": "Are you flying above cloud? / I am flying above cloud and at flight level/altitude ...",
"QBH": "Are you flying below cloud? / I am flying below cloud and at flight level/altitude ...",
"QBI": "Is flight under IFR compulsory at ... (place) [or from ... to ... (place)]? / Flight under IFR is compulsory at ... (place) [or from ... to ... (place)].",
"QBJ": "What is the amount, type and height above ... (datum) of the top of the cloud [at ... (position or zone)]? / At ... hours at ... (position or zone) the top of the cloud is: amount ... eights (... type) at ... (figures and units) height above ... (datum).",
"QBK": "Are you flying with no cloud in your vicinity? / I am flying with no cloud in my vicinity and at flight level/altitude ...",
"QBM": "Has ... sent any messages for me? / Here is the message sent by ... at ... hours.",
"QBN": "Are you flying between two layers of cloud? / I am flying between two layers of cloud and at flight level/altitude ...",
"QBO": "What is the nearest aerodrome at which flight under VFR is permissible and which would be suitable for my landing? / Flying under VFR is permissible at ... (place) which would be suitable for your landing.",
"QBP": "Are you flying in and out of cloud? / I am flying in and out of cloud and at flight level/altitude ...",
"QBS": "Ascend (or descend) to ... (figures and units) height above ... (datum) before encountering instrument meteorological conditions or if visibility falls below ... (distance figures and units) and advise.",
"QBT": "What is the runway visual range at ... (place)? / The runway visual range at ... (place) at ... hours is ... (distance figures and units).",
"QBV": "Have you reached flight level/altitude ... [or ... (area or place)]? / I have reached ... flight level/altitude ... [or ... (area or place)].",
"QBX": "Have you left ... flight level/altitude ... [or ... (area or place)]? / I have left ... flight level/altitude ... [or ... (area or place)].",
"QBZ": "Report your flying conditions in relation to clouds. / The reply to QBZ ? is given by the appropriate answer form of signals QBF, QBG, QBH, QBK, QBN and QBP.",
"QCA": "May I change my flight level/altitude from ... to ... ? / You may change your flight level/altitude from ... to ...",
"QCB": "Delay is being caused by ...",
"QCE": "When may I expect approach clearance? / Expect approach clearance at ... hours.",
"QCF": "Delay indefinite. Expect approach clearance not later than ... hours.",
"QCH": "May I taxi to ... (place)? / Cleared to taxi to ... (place).",
"QCI": "Make a 360-degree turn immediately (turning to the ...).",
"QCS": "My reception on ... frequency has broken down.",
"QCX": "What is your full call sign? / My full call sign is ...",
"QCY": "I am working on a trailing aerial.",
"QDB": "Have you sent message ... to ... ? / I have sent message ... to ...",
"QDF": "What is your D-Value at ... (position)?",
"QDL": "Do you intend to ask me for a series of bearings? / I intend to ask you for a series of bearings.",
"QDM": "Will you indicate the MAGNETIC heading for me to steer towards you (or ...) with no wind? / The MAGNETIC heading for you to steer to reach me (or ...) with no wind was ... degrees (at ... hours).",
"QDP": "Will you accept control (or responsibility) of (for) ... now (or at ... hours)? / I will accept control (or responsibility) of (for) ... now (or at ... hours).",
"QDR": "What is my MAGNETIC bearing from you (or from ...)? / Your MAGNETIC bearing from me (or from ...) was ... degrees (at ... hours).",
"QDT": "Are you flying in visual meteorological condition? / I am flying in visual meteorological condition.",
"QDU": "Cancelling my IFR flight.",
"QDV": "Are you flying in a flight visibility of less than ... (figures and units)? / I am flying in a flight visibility of less than ... (figures and units) at flight level/altitude ...",
"QEA": "May I cross the runway ahead of me? / You may cross the runway ahead of you.",
"QEB": "May I turn at the intersection? / Taxi as follows at the intersection ...",
"QEC": "May I make a 180-degree turn and return down the runway? / You may make a 180-degree turn and return down the runway.",
"QED": "Shall I follow the pilot vehicle? / Follow the pilot vehicle.",
"QEF": "Have I reached my parking area? / You have reached your parking area.",
"QEG": "May I leave the parking area? / You may leave the parking area.",
"QEH": "May I move to the holding position for runway number ... ? / Cleared to the holding position for runway number ...",
"QEJ": "May I assume position for take-off? / Cleared to hold at take-off position for runway number ...",
"QEK": "Are you ready for immediate take-off? / I am ready for immediate take-off.",
"QEL": "May I take-off (and make a ... hand turn after take-off)? / You are cleared to take-off (turn as follows after take-off ...).",
"QEM": "What is the condition of the landing surface at ... (place)? / The condition of the landing surface at ... (place) is ...",
"QEN": "Shall I hold my position? / Hold your position",
"QEO": "Shall I clear the runway (or landing area)? / Clear the runway (or landing area).",
"QES": "Is a right-hand circuit in force at ... (place)? / A right-hand circuit is in force at ... (place).",
"QFA": "What is the meteorological forecast for ... (flight, route, section of route or zone) for the period ... hours until ... hours? / The meteorological forecast for ... (flight, route, section of route or zone) for the period ... hours until ... hours is ...",
"QFB": "The approach/runway lights are out of order.",
"QFC": "What is the amount, the type and the height above ... (datum) of the base of the cloud at ... (place, position or zone)? / At ... (place, position or zone) the base of the cloud is ... eighths ... type at ... (figures and units) height above ... (datum).",
"QFD": "Is the ... visual beacon [at ... (place)] in operation?",
"QFE": "What should I set on the subscale of my altimeter so that the instrument would indicate its height above the reference elevation being used? / If you set the subscale of your altimeter to read ... millibars, the instrument would indicate its height above aerodrome elevation (above threshold, runway number ...).",
"QFF": "[At ... (place)] what is the present atmospheric pressure converted to mean sea level in accordance with meteorological practice? / At ... (place) the atmospheric pressure converted to mean sea level in accordance with meteorological practice is (or was determined at ... hours to be) ... millibars.",
"QFG": "Am I overhead? / You are overhead.",
"QFH": "May I descend below the clouds? / You may descend below the clouds.",
"QFI": "Are the aerodrome lights lit? / The aerodrome lights are lit.",
"QFL": "Will you send up pyrotechnical lights? / I will send up pyrotechnical lights.",
"QFM": "What flight level/altitude should I maintain?",
"QFO": "May I land immediately? / You may land immediately.",
"QFP": "Will you give me the latest information concerning ... facility [at ... (place)]? / The latest information concerning ... facility [at ... (place)] is as follows ...",
"QFQ": "Are the approach and runway lights lit? / The approach and runway lights are lit.",
"QFR": "Does my landing gear appear damaged? / Your landing gear appears damaged.",
"QFS": "Is the radio facility at ... (place) in operation? / The radio facility at ... (place) is in operation (or will be in operation in ... hours).",
"QFT": "Between what heights above ... (datum) has ice formation been observed [at ... (position or zone)]? / Ice formation has been observed at ... (position or zone) in the type of ... and with an accretion rate of ... between ... (figures and units) and ... (figures and units) heights above ... (datum).",
"QFU": "What is the magnetic direction (or number) of the runway to be used? / The magnetic direction (or number) of the runway to be used is ...",
"QFV": "Are the floodlights switched on? / The floodlights are switched on.",
"QFW": "What is the length of the runway in use in ... (units)? / The length of runway ... now in use is ... (figures and units).",
"QFX": "I am working (or am going to work) on a fixed aerial.",
"QFY": "Please report the present meteorological landing conditions [at ... (place)]. / The present meteorological landing conditions at ... (place) are ...",
"QFZ": "What is the aerodrome meteorological forecast for ... (place) for the period ... hours until ... hours? / The aerodrome meteorological forecast for ... (place) for the period ... hours until ... hours is ...",
"QGC": "There are obstructions to the ... of ... runway ...",
"QGD": "Are there on my track any obstructions whose elevation equals or exceeds my altitude? / There are obstructions on your track ... (figures and units) height above ... (datum).",
"QGE": "What is my distance to your station (or to ...)? / Your distance to my station (or to ...) is ... (distance figures and units).",
"QGH": "May I land using ... (procedure or facility)? / You may land using ... (procedure or facility).",
"QGK": "What track should I make good? / Make good a track from ... (place) on ... degrees ... (true or magnetic).",
"QGL": "May I enter the ... (control area or zone) at ... (place)? / You may enter the ... (control area or zone) at ... (place).",
"QGM": "Leave the ... (control area or zone).",
"QGN": "May I be cleared to land [at ... (place)]? / You are cleared to land [at ... (place)].",
"QGO": "Landing is prohibited at ... (place).",
"QGP": "What is my number for landing? / You are number ... to land.",
"QGQ": "May I hold at ... (place)? / Hold at ... (place) at flight level/altitude ... (datum) and await further clearance.",
"QGT": "Fly for ... minutes on a heading what will enable you to maintain a track reciprocal to your present one.",
"QGU": "Fly for ... minutes on a magnetic heading of ... degrees.",
"QGV": "Do you see me? / Can you see the aerodrome? / Can you see ... (aircraft)? / I see you at ... (cardinal or quadrantal point of direction).",
"QGW": "Does my landing gear appear to be down and in place? / Your landing gear appears to be down and in place.",
"QGZ": "Hold on ... direction of ... facility.",
"QHE": "Will you inform me when you are on ... leg of approach? / I am on ...(leg).. of approach.",
"QHG": "May I enter traffic circuit at flight level/altitude ...? / Cleared to enter traffic circuit at flight level/altitude ...",
"QHH": "Are you making an emergency landing? / I am making an emergency landing.",
"QHI": "Are you (or is ...) ... waterborne / on land? ",
"QHQ": "May I make a ... approach [at ... (place)]? / You may make a ... approach [at ... (place)].",
"QHZ": "Shall I circle the aerodrome (or go around)? / Circle the aerodrome (or go around).",
"QIC": "May I establish communication with ... radio station on ... kHz (or ... MHz.) now (or at ... hours)? / Establish communication with ... radio station on ... kHz. (or MHz.) now (or at ...hours).",
"QIF": "What frequency is ... using? / ... is using ... kHz (or ... MHz.).",
"QJA": "Is my tape / mark and space reversed? / Your tape / mark and space is reversed.",
"QJB": "Will you use radio/cable/telegraph/teletype/telephone/receiver/transmitter/reperforator? / I will use ...",
"QJC": "Will you check your transmitter/autohead/perforator/reperfordator/printer/keyboard/antenna? / I will check my ...",
"QJD": "Am I transmitting letters/figures? / You are transmitting ...",
"QJE": "Is my frequency shift wide/narrow/correct? / Your frequency shift is ... (by ... cycles)",
"QJF": "My signal as checked by monitor locally/as radiated is satisfactory.",
"QJG": "Shall I revert to automatic relay? / Revert to automatic relay.",
"QJH": "Shall I run test tape/sentence? / Run test tape/sentence.",
"QJI": "Will you transmit a continuous mark/space? / I am transmitting a continuous mark/space.",
"QJK": "Are you receiving continuous mark/space / mark/space bias? / I am receiving a continuous mark/space / mark/space bias.",
"QKC": "The sea conditions (at ... position) ... permit alighting but not takeoff / render alihting extremely hazardous.",
"QKF": "May I be relieved (at ... hours)? / You may expect to be relieved at ... hours [by aircraft/vessel/callsign/name].",
"QKG": "Will relief take place when ... (identification) establishes visual/comms contact with survivors? / Relief will take place when ... (identification) establishes visual/comms contact with survivors.",
"QKH": "Report details of the parallel sweep (track) search being (or to be) conducted? --or-- In the parallel sweep (track) search being (or to be) conducted, what is (are) the direction/separation/altitude of sweeps employed in the search pattern? / The parallel sweep (track) search is being (or to be) conducted [with direction of sweeps ... degrees ... (true or magnetic) || with ... (distance figures and units) separation between sweeps || flight level/altitude].",
"QKN": "Aircraft plotted (believed to be you) in position ... on track ... degrees at ... hours.",
"QKO": "What other units are (or will be) taking part in the operation [... (identification of operation)]? / In the operation [... (identification)], the following units are (or will be) taking part ... (name of units). --or-- ... (name) unit is taking part in operation [... (identification] (with effect from ... hours).",
"QKP": "Which pattern of search is being followed? / The search pattern is parallel sweep/square search/creeping line ahead/track crawl/contour search/combined search by aircraft and ship/[other].",
"QLB": "Will you monitor ... station and report regarding range, quality, etc.? / I have monitored ... station and report (briefly) as follows ...",
"QLE": "What is your expected signal? / The expected signal is low...",
"QLF": "Are you sending with your left foot? Try sending with your left foot!",
"QLH": "Will you use simultaneous keying on ... frequency and ... frequency? / I will now key simultaneously on ... frequency and ... frequency.",
"QLV": "Is the ... radio facility still required? / The ... radio facility is still required.",
"QMH": "Shift to transmit and receive on ... kHz (or ... MHz.); if communication is not established within 5 minutes, revert to present frequency.",
"QMI": "Report the vertical distribution of cloud [at ... (position or zone)] as observed from your aircraft. / The vertical distribution of cloud as observed from my aircraft at ... hours at ... (position or zone) is : *lowest layer observed* ... eights (... type) with base of ... (figures and units) and tops of ... (figures and units) [*and similarly in sequence for each of the layers observed.] height above ... (datum).",
"QMU": "What is the surface temperature at ... (place) and what is the dew point temperature at that place? / The surface temperature at ... (place) at ... hours is ... degrees and the dew point temperature at that time and place is ... degrees.",
"QMW": "At ... (position or zone) what is (are) the flight level(s)/altitude(s) ... of the zero Celsius isotherm(s)? / At ... (position or zone) the zero Celsius isotherm(s) is (are) at flight level(s)/altitude(s) ...",
"QMX": "What is the air temperature [at ... (position or zone)] (at ... hours) at flight level/altitude ...? / At ... (position or zone) at ... hours the air temperature is ... (degrees and units) at flight level/altitude ... --Note-- Aircraft reporting QMX information will transmit the temperature figures as corrected for airspeed.",
"QMZ": "Have you any amendments to the flight forecast in respect of section of route yet to be traversed? / The following amendment(s) should be made to the flight forecast ... [If no amendments, signal QMZ NIL]",
"QNE": "What indication will my altimeter give on landing at ... (place) at ... hours, my sub-scale being set to 1013.2 millibars (29.92 inches)? / On landing at ... (place) at ... hours, with your sub-scale being set to 1013.2 millibars (29.92 inches), your altimeter will indicate ... (figures and units).",
"QNH": "What should I set on the subscale of my altimeter so that the instrument would indicate its elevation if my aircraft were on the ground at your station? / If you set the subscale of your altimeter to read ... millibars, the instrument would indicate its elevation if your aircraft were on the ground at my station at ... hours. --Note-- When the setting is given in hundredths of inch the abbreviation INS is used to identify the units.",
"QNI": "May I join the net? / You may check in...",
"QNO": "I am not equipped to give the information (or provide the facility) requested.",
"QNR": "I am approaching my point of no return.",
"QNT": "What is the maximum speed of the surface wind at ... (place)? / The maximum speed of the surface wind at ... (place) at ... hours is ... (speed figures and units).",
"QNY": "What is the present weather and the intensity thereof at ... (place, position or zone)? / The present weather and intensity thereof at ... (place, position or zone) at ... hours is ...",
"QOA": "Can you communicate by radiotelegraphy (500 kHz)? / I can communicate by radiotelegraphy (500 kHz).",
"QOB": "Can you communicate by radiotelephony (2182 kHz)? / I can communicate by radiotelephony (2182 kHz).",
"QOC": "Can you communicate by radiotelephony (channel 16 - frequency 156.80 MHz)? / I can communicate by radiotelephony (channel 16 - frequency 156.80 MHz).",
"QOD": "Can you communicate with me in Dutch/English/French/German/Greek/Italian/Japanese/Norwegian/Russian/Spanish? / I can communicate with you in ...",
"QOE": "Have you received the safety signal sent by ... (name and/or call sign)? / I have received the safety signal sent by ... (name and/or call sign).",
"QOF": "What is the commercial quality of my signals? / The quality of your signals is not commercial/marginally commercial/commercial.",
"QOG": "How many tapes have you to send? / I have ... tapes to send.",
"QOH": "Shall I send a phasing signal for ... seconds? / Send a phasing signal for ... seconds.",
"QOI": "Shall I send my tape? / Send your tape.",
"QOJ": "Will you listen on ... kHz (or MHz) for signals of emergency position-indicating radiobeacons? / I am listening on ... kHz (or MHz) for signals of emergency position-indicating radiobeacons.",
"QOK": "Have you received the signals of an emergency position-indicating radiobeacon on ... kHz (or MHz)? / I have received the signals of an emergency position-indicating radiobeacon on ... kHz (or MHz).",
"QOL": "Is your vessel fitted for reception of selective calls? If so, what is your selective call number or signal? / My vessel is fitted for the reception of selective calls. My selective call number or signal is ...",
"QOM": "On what frequencies can your vessel be reached by a selective call? / My vessel can be reached by a selective call on the following frequency/ies ... (periods of time to be added if necessary).",
"QOO": "Can you send on any working frequency? / I can send on any working frequency.",
"QOT": "Do you hear my call; what is the approximate delay in minutes before we may exchange traffic? / I hear your call; the approximate delay is ... minutes.",
"QRA": "What is the name of your vessel (or station)? / The name of my vessel (or station) is ...",
"QRB": "How far approximately are you from my station? / The approximate distance between our stations is ... nautical miles (or km).",
"QRC": "What is your true bearing? / My true bearing is ____ degrees.",
"QRD": "Where are you bound for? / I am bound for ____.",
"QRE": "What is your estimated time of arrival at ... (or over ...) (place)? / My estimated time of arrival at ... (or over ...) (place) is ... hours.",
"QRF": "Where are you bound from? / I am bound from ____.",
"QRG": "Will you tell me my exact frequency (or that of ...)? / Your exact frequency (or that of ...) is ... kHz (or MHz).",
"QRH": "Does my frequency vary? / Your frequency varies.",
"QRI": "How is the tone of my transmission? / The tone of your transmission is ...",
"QRJ": "How many voice contacts do you want to make? / I want to make ... voice contacts.",
"QRK": "How do you receive me? / I am receiving (1-5).",
"QRL": "Are you busy? / I am busy (or I am busy with ...). Please do not interfere.",
"QRM": "Are you being interfered with? / I am being interfered with.",
"QRN": "Are the atmospherics strong? / Atmospherics (noise) are very strong.",
"QRO": "Shall I increase transmitter power? / Increase transmitter power.",
"QRP": "Shall I decrease transmitter power? / Decrease transmitter power.",
"QRQ": "Shall I send faster? / Send faster (... wpm)",
"QRR": "Are you ready for automatic operation? / I am ready for automatic operation. Send at ... words per minute.",
"QRS": "Shall I send more slowly? / Send more slowly (... words per minute).",
"QRT": "Shall I stop sending? / Stop sending.",
"QRU": "Have you anything for me? / I have nothing for you.",
"QRV": "Are you ready? / I am ready.",
"QRW": "Shall I inform ... that you are calling him on ... kHz (or MHz)? / Please inform ... that I am calling him on ... kHz (or MHz).",
"QRX": "When will you call me again? / I will call you again at ... hours (on ... kHz (or MHz)).",
"QRY": "What is my turn? / Your turn is Number ... (or according to any other indication).",
"QRZ": "Who is calling me? / You are being called by ... (on ... kHz (or MHz)).",
"QSA": "What is the strength of my signals (or those of ...)? / The strength of your signals (or those of ...) is ...",
"QSB": "Are my signals fading? / Your signals are fading.",
"QSC": "Are you a cargo vessel? --or-- Are you a low traffic ship? / I am a cargo vessel. --or-- I am a low traffic ship.",
"QSD": "Is my keying defective? --or-- Are my signals mutilated? / Your keying is defective. --or-- Your signals are mutilated.",
"QSE": "What is the estimated drift of the survival craft? / The estimated drift of the survival craft is ... (figures and units).",
"QSF": "Have you effected rescue? / I have effected rescue and am proceeding to ... base (with ... persons injured requiring ambulance).",
"QSG": "Shall I send ... telegrams at a time? / Send ... telegrams at a time.",
"QSH": "Are you able to home on your direction-finding equipment? / I am able to home on my D/F equipment (on station ...).",
"QSI": "I have been unable to break in on your transmission. --or-- Will you inform ... (call sign) that I have been unable to break in on his transmission (on ... kHz (or MHz)).",
"QSJ": "What is the charge to be collected to ... including your internal charge? / The charge to be collected to ... including my internal charge is ... francs.",
"QSK": "Can you hear me between your signals and if so can I break in on your transmission? / I can hear you between my signals; break in on my transmission.",
"QSL": "Can you acknowledge receipt? / I am acknowledging receipt.",
"QSM": "Shall I repeat the last telegram (message) which I sent you, or some previous telegram (message)? / Repeat the last telegram (message) which you sent me (or telegram(s) (message(s)) numbers(s) ...).",
"QSN": "Did you hear me (or ... (call sign)) on ... kHz (or MHz)? / I did hear you (or ... (call sign)) on ... kHz (or MHz).",
"QSO": "Can you communicate with ... direct (or by relay)? / I can communicate with ... direct (or by relay through ...).",
"QSP": "Will you relay to ... free of charge? / I will relay to ... free of charge.",
"QSQ": "Have you a doctor on board (or is ... (name of person) on board)? / I have a doctor on board (or ... (name of person) is on board).",
"QSR": "Shall I repeat the call on the calling frequency? / Repeat your call on the calling frequency; did not hear you (or have interference).",
"QSS": "What working frequency will you use? / I will use the working frequency ... kHz (or MHz) (in the HF bands normally only the last three figures of the frequency need be given).",
"QST": "Here is a broadcast message to all amateurs.",
"QSU": "Shall I send or reply on this frequency (or on ... kHz (or MHz)) (with emissions of class ...)? / Send or reply on this frequency (or on ... kHz (or MHz)) (with emissions of class ...).",
"QSV": "Shall I send a series of Vs on this frequency (or on ... kHz (or MHz))? / Send a series of Vs on this frequency (or on ... kHz (or MHz)).",
"QSW": "Will you send on this frequency (or on ... kHz (or MHz)) (with emissions of class ...)? / I am going to send on this frequency (or on ... kHz (or MHz)) (with emissions of class ...).",
"QSX": "Will you listen to ... (call sign(s)) on ... kHz (or MHz)? --or-- Will you listen to ... (call sign(s)) on ... kHz (or MHz), or in the bands ... / channels ... ? / I am listening to ... (call sign(s)) on ... kHz (or MHz). --or-- I am listening to ... (call sign(s)) on ... kHz (or MHz), or in the bands ... / channels ...",
"QSY": "Shall I change to transmission on another frequency? / Change to transmission on another frequency (or on ... kHz (or MHz)).",
"QSZ": "Shall I send each word or group more than once? / Send each word or group twice (or ... times).",
"QTA": "Shall I cancel telegram (or message) number ... ? / Cancel telegram (or message) number ...",
"QTB": "Do you agree with my counting of words? / I do not agree with your counting of words; I will repeat the first letter or digit of each word or group.",
"QTC": "How many telegrams have you to send? / I have ... telegrams for you (or for ...).",
"QTD": "What has the rescue vessel or rescue aircraft recovered? / ... (identification) has recovered ... survivors/wreckage/bodies.",
"QTE": "What is my TRUE bearing from you? --or-- What is my TRUE bearing from ... (call sign)? --or-- What is the TRUE bearing of ... (call sign) from ... (call sign)? / Your TRUE bearing from me is ... degrees at ... hours. --or-- Your TRUE bearing from ... (call sign) was ... degrees at ... hours. --or-- The TRUE bearing of ... (call sign) from ... (call sign) was ... degrees at ... hours.",
"QTF": "Will you give me the position of my station according to the bearings taken by the direction-finding stations which you control? / The position of your station according to the bearings taken by the D/F stations which I control was ... latitude, ... longitude (or other indication of position), class ... at ... hours.",
"QTG": "Will you send two dashes of ten seconds each followed by your call sign (repeated ... times) (on ... kHz (or MHz))? --or-- Will you request ... to send two dashes of ten seconds followed by his call sign (repeated ... times) on ... kHz (or MHz)? / I am going to send two dashes of ten seconds each followed by my call sign (repeated ... times) (on ... kHz (or MHz)). --or-- I have requested ... to send two dashes of ten seconds followed by his call sign (repeated ... times) on ... kHz (or MHz).",
"QTHR": "At the registered location; Chiefly British in voice or writing, Historically - the location in the printed Callbook. Modernly - as given in online government records for my callsign",
"QTH": "What is your position in latitude and longitude (or according to any other indication)? / My position is ... latitude, ... longitude (or according to any other indication).",
"QTI": "What is your TRUE course? / My TRUE course is ... degrees.",
"QTJ": "What is your speed? / My speed is ... knots (or ... kilometres per hour or ... statute miles per hour).",
"QTK": "What is the speed of your aircraft in relation to the surface of the Earth? / The speed of my aircraft in relation to the surface of the Earth is ... knots (or ... kilometres per hour or ... statute miles per hour).",
"QTL": "What is your TRUE heading? / My TRUE heading is ... degrees.",
"QTM": "What is your MAGNETIC heading? / My MAGNETIC heading is ... degrees.",
"QTN": "At what time did you depart from ... (place)? / I departed from ... (place) at ... hours.",
"QTO": "Have you left dock (or port)? --or-- Are you airborne? / I have left dock (or port). --or-- I am airborne.",
"QTP": "Are you going to enter dock (or port)? --or-- Are you going to alight (or land)? / I am going to enter dock (or port). --or-- I am going to alight (or land).",
"QTQ": "Can you communicate with my station by means of the International Code of Signals (INTERCO)? / I am going to communicate with your station by means of the International Code of Signals (INTERCO).",
"QTR": "What is the correct time? / The correct time is ... hours.",
"QTS": "Will you send your call sign (and/or name) for ... seconds? / I will send my call sign (and/or name) for ... seconds.",
"QTT": "The identification signal which follows is superimposed on another transmission.",
"QTU": "What are the hours during which your station is open? / My station is open from ... to ... hours.",
"QTV": "Shall I stand guard for you on the frequency of ... kHz (or MHz) (from ... to ... hours)? / Stand guard for me on the frequency of ... kHz (or MHz) (from ... to ... hours).",
"QTW": "What is the condition of survivors? / Survivors are in ... condition and urgently need ...",
"QTX": "Will you keep your station open for further communication with me until further notice (or until ... hours)? / I will keep my station open for further communication with you until further notice (or until ... hours).",
"QTY": "Are you proceeding to the position of incident and if so when do you expect to arrive? / I am proceeding to the position of incident and expect to arrive at ... hours (on ... (date)).",
"QTZ": "Are you continuing the search? / I am continuing the search for ... (aircraft, ship, survival craft, survivors or wreckage).",
"QUA": "Have you news of ... (call sign)? / Here is news of ... (call sign).",
"QUB": "Can you give me in the following order information concerning: the direction in degrees TRUE and speed of the surface wind; visibility; present weather; and amount, type and height of base of cloud above surface elevation at ... (place of observation)? / Here is the information requested: ... (The units used for speed and distances should be indicated.)",
"QUC": "What is the number (or other indication) of the last message you received from me (or from ... (call sign))? / The number (or other indication) of the last message I received from you (or from ... (call sign)) is ...",
"QUD": "Have you received the urgency signal sent by ... (call sign of mobile station)? / I have received the urgency signal sent by ... (call sign of mobile station) at ... hours.",
"QUE": "Can you speak in ... (language), - with interpreter if necessary; if so, on what frequencies? / I can speak in ... (language) on ... kHz (or MHz).",
"QUF": "Have you received the distress signal sent by ... (call sign of mobile station)? / I have received the distress signal sent by ... (call sign of mobile station) at ... hours.",
"QUG": "Will you be forced to alight (or land)? / I am forced to alight (or land) immediately. --or-- I shall be forced to alight (or land) at ... (position or place) at ... hours.",
"QUH": "Will you give me the present barometric pressure at sea level? / The present barometric pressure at sea level is ... (units).",
"QUI": "Are your navigation lights working? / My navigation lights are working.",
"QUJ": "Will you indicate the TRUE track to reach you (or ...)? / The TRUE track to reach me (or ...) is ... degrees at ... hours.",
"QUK": "Can you tell me the condition of the sea observed at ... (place or coordinates)? / The sea at ... (place or coordinates) is ...",
"QUL": "Can you tell me the swell observed at ... (place or coordinates)? / The swell at ... (place or coordinates) is ...",
"QUM": "May I resume normal working? / Normal working may be resumed.",
"QUN": "When directed to all stations: Will vessels in my immediate vicinity ... (or in the vicinity of ... latitude, ... longitude) please indicate their position, TRUE course and speed? --or-- When directed to a single station: please indicate their position, TRUE course and speed? / My position, TRUE course and speed are ...",
"QUO": "Shall I search for aircraft/ship/survival craft in the vicinity of ... latitude, ... longitude (or according to any other indication)? / Please search for aircraft/ship/survival craft in the vicinity of ... latitude, ... longitude (or according to any other indication).",
"QUP": "Will you indicate your position by searchlight/black smoke trail/pyrotechnic lights? / My position is indicated by searchlight/black smoke trail/pyrotechnic lights.",
"QUQ": "Shall I train my searchlight nearly vertical on a cloud, occulting if possible and, if your aircraft is seen, deflect the beam up wind and on the water (or land) to facilitate your landing? / Please train your searchlight on a cloud, occulting if possible and, if my aircraft is seen or heard, deflect the beam up wind and on the water (or land) to facilitate my landing.",
"QUR": "Have survivors received survival equipment/been picked up by rescue vessel/been reached by ground rescue party? / Survivors are in possession of survival equipment dropped by ... / have been picked up by rescue vessel/have been reached by ground rescue party.",
"QUS": "Have you sighted survivors or wreckage? / If so, in what position? / Have sighted ... survivors in water/survivors on rafts/wreckage in position ... latitude, ... longitude (or according to any other indication).",
"QUT": "Is position of incident marked? / Position of incident is marked by flame or smoke float/sea marker/sea marker dye/... (specify other marking).",
"QUU": "Shall I home ship or aircraft to my position? / Home ship or aircraft ... (call sign) ... to your position by transmitting your call sign and long dashes on ... kHz (or MHz)/by transmitting on ... kHz (or MHz) TRUE track to reach you.",
"QUW": "Are you in the search area designated as ... (designator or latitude and longitude)? / I am in the ... (designation) search area.",
"QUX": "Do you have any navigational warnings or gale warnings in force? / I have the following navigational warning(s) or gale warning(s) in force: ...",
"QUY": "Is position of survival craft marked? / Position of survival craft was marked at ... hours by flame or smoke float/sea marker/sea marker dye/... (specify other marking).",
"QUZ": "May I resume restricted working? / Distress phase still in force; restricted working may be resumed.",
"QZZ": "Daily key change about to take place (German WWII usage)."
}
+1 -1
View File
@@ -1,7 +1,7 @@
"""
A listing of hamstudy command resources
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
Copyright (C) 2019-2021 classabbyamp, 0x5c
This file is part of discord-qrmbot and is released under the terms of
the GNU General Public License, version 2.
-1296
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -34,9 +34,9 @@ while [ ! -z "$1" ]; do
done
# If $PYTHON_BIN is not defined, default to 'python3.7'
# If $PYTHON_BIN is not defined, default to 'python3.9'
if [ $_NO_BOTENV -eq 1 -a -z "$PYTHON_BIN" ]; then
PYTHON_BIN='python3.7'
PYTHON_BIN='python3.9'
fi
+37 -2
View File
@@ -27,10 +27,42 @@ debug = False
# A tuple of user IDs that should be considered "bot owners".
# * Those users will have full control over the bot.
# ! This MUST be a tuple of integers. Single element tuple: `(123,)`
owners_uids = (200102491231092736,)
owners_uids = (200102491231092736, 564766093051166729)
# The extensions to load when running the bot.
exts = ["ae7q", "base", "fun", "grid", "ham", "image", "lookup", "morse", "qrz", "study", "weather", "dbconv"]
exts = [
"base",
"ae7q",
"callsign",
"codes",
"contests",
"dbconv",
"dxcc",
"fun",
"grid",
"image",
"land_weather",
"morse",
"prefixes",
"propagation",
"study",
"tex",
"time",
]
# URL to the resources
resources_url = "https://qrmresources.miaow.io/resources/"
# If True (default): when doing QRZ callsign lookups, show the nickname in place of the first name, if it exists
# if False: use QRZ's default name format
qrz_only_nickname = True
# enable a command that provides a link to add the bot to a server
enable_invite_cmd = True
# the default permissions for the bot, to be included in the invite link for ?invite
# this probably does not need to be changed
invite_perms = 67488832
# Either "time", "random", or "fixed" (first item in statuses)
status_mode = "fixed"
@@ -58,3 +90,6 @@ msg_reacts = {}
# A :pika: emote's ID, None for no emote :c
pika = 658733876176355338
# Base URL to a deployment of rTeX, which performs LaTeX rendering.
rtex_instance = "https://rtex.probablyaweb.site/"
+1 -1
View File
@@ -1,7 +1,7 @@
"""
Wrapper to handle aiohttp connector creation.
---
Copyright (C) 2020 Abigail Gold, 0x5c
Copyright (C) 2020-2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
+86
View File
@@ -0,0 +1,86 @@
"""
Resources manager for qrm2.
---
Copyright (C) 2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
from pathlib import Path
import requests
from utils.resources_models import Index
class ResourcesManager:
def __init__(self, basedir: Path, url: str, versions: dict):
self.basedir = basedir
self.url = url
self.versions = versions
self.index: Index = self.sync_start(basedir)
def parse_index(self, index: str):
"""Parses the index."""
return Index.parse_raw(index)
def sync_fetch(self, filepath: str):
"""Fetches files in sync mode."""
self.print_msg(f"Fetching {filepath}", "sync")
with requests.get(self.url + filepath) as resp:
return resp.content
def sync_start(self, basedir: Path) -> Index:
"""Takes cares of constructing the local resources repository and initialising the RM."""
self.print_msg("Initialising ResourceManager", "sync")
self.ensure_dir(basedir)
try:
raw = self.sync_fetch("index.json")
new_index: Index = self.parse_index(raw)
with (basedir / "index.json").open("wb") as file:
file.write(raw)
except (requests.RequestException, OSError) as ex:
self.print_msg(f"There was an issue fetching the index: {ex.__class__.__name__}: {ex}", "sync")
if (basedir / "index.json").exists():
self.print_msg("Old file exist, using old resources", "fallback")
with (basedir / "index.json").open("r") as file:
old_index = self.parse_index(file.read())
for res, ver in self.versions.items():
for file in old_index[res][ver]:
if not (basedir / file.filename).exists():
self.print_msg(f"Error: {file.filename} is missing", "fallback")
raise SystemExit(1)
return old_index
raise SystemExit(1)
for res, ver in self.versions.items():
for file in new_index[res][ver]:
try:
with (basedir / file.filename).open("wb") as f:
f.write(self.sync_fetch(file.filename))
except (requests.RequestException, OSError) as ex:
ex_cls = ex.__class__.__name__
self.print_msg(f"There was an issue fetching {file.filename}: {ex_cls}: {ex}", "sync")
if not (basedir / file.filename).exists():
raise SystemExit(1)
self.print_msg("Old file exists, using it", "fallback")
return new_index
def ensure_dir(self, basedir: Path) -> bool:
"""Ensures that the resources/ directory is present. Creates as necessary."""
if basedir.is_file():
raise FileExistsError(f"'{basedir}' is not a directory!")
if not basedir.exists():
self.print_msg("Creating resources directory")
basedir.mkdir()
return True
return False
def print_msg(self, msg: str, mode: str = None):
"""Formats and prints messages for the resources manager."""
message = "RM: "
message += msg
if mode:
message += f" ({mode})"
print(message)
+63
View File
@@ -0,0 +1,63 @@
"""
Resource index models for qrm2.
---
Copyright (C) 2021 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
from collections.abc import Mapping
from datetime import datetime
from pydantic import BaseModel
class File(BaseModel):
filename: str
hash: str
# For some reason those were not the same???
def __str__(self) -> str:
return repr(self)
class Resource(BaseModel, Mapping):
# 'A Beautiful Hack' https://github.com/samuelcolvin/pydantic/issues/1802
__root__: dict[str, list[File]]
def __getitem__(self, key: str) -> list[File]:
return self.__root__[key]
def __iter__(self):
return iter(self.__root__)
def __len__(self) -> int:
return len(self.__root__)
# For some reason those were not the same???
def __str__(self) -> str:
return repr(self)
# Make the repr more logical (despite the technical inaccuracy)
def __repr_args__(self):
return self.__root__.items()
class Index(BaseModel, Mapping):
last_updated: datetime
resources: dict[str, Resource]
def __getitem__(self, key: str) -> Resource:
return self.resources[key]
def __iter__(self):
return iter(self.resources)
def __len__(self) -> int:
return len(self.resources)
# For some reason those were not the same???
def __str__(self) -> str:
return repr(self)