82 Commits

Author SHA1 Message Date
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
0x5c 27863ae6bf Merge pull request #274 from miaowware/e-release
Bump version+changelog 2.4.1
2020-10-06 19:36:37 -04:00
classabbyamp de999bc39d fix error in workflow (#273) 2020-10-06 19:31:19 -04:00
0x5c 80d80ab718 Bump version+changelog 2.4.1 2020-10-06 19:26:57 -04:00
classabbyamp 8cb1a8df15 update docker documentation to remove docker hub mentions (#265)
Fixes #263
2020-10-06 19:17:02 -04:00
classabbyamp fa2dded81b Merge pull request #272 from miaowware/worflows-updates
add/update workflows
2020-10-06 19:16:13 -04:00
Abigail G bda0540fa8 add/update workflows
Fixes #270
Fixes #271
2020-10-06 19:13:00 -04:00
0x5c c9510ad9b9 Merge pull request #269 from miaowware/newcrap
Made bot work with new Intents system + only cache needed things
2020-10-06 19:01:46 -04:00
0x5c 00f9929deb Made bot work with new Intents system + only cache needed things
- all in one
- Bumped discord.py to 1.5.0

Fixes #267
Fixes #268
2020-10-06 18:56:45 -04:00
classabbyamp 3597367046 Create developer guide, remove issue/pr templates (#260)
* Create developer guide
* remove issue templates (they are in miaowware/.github now)
2020-10-04 22:29:00 -04:00
classabbyamp 7e35e8949a release v2.4.0 (#254) 2020-09-27 18:03:52 -04:00
classabbyamp 77b572eb3e add a decibel conversion command (#250)
fixes #231
2020-09-27 17:52:52 -04:00
classabbyamp be042a9641 add dev targets to makefile (#252)
fixes #251
2020-09-27 16:52:37 -04:00
classabbyamp 488ae6cc98 fix displaying multiple prefixes, add option to display ?help in status (#249)
* fix displaying multiple prefixes, add option to display ?help in status
Fixes #229

Co-authored-by: 0x5c <dev@0x5c.io>
2020-09-27 16:36:39 -04:00
classabbyamp a65fd04dbd add cq zone, itu zone and region, canadian maps (#248)
* add cq zone, itu zone and region, canadian maps
* update arrl/rac maps
* add attribution to all maps

Fixes #112
Progress on #130

Co-authored-by: 0x5c <dev@0x5c.io>
2020-09-27 16:30:59 -04:00
classabbyamp 6329718d29 Contribution and PR Guidelines (#219)
* Create pull_request_template.md
* add contribution guidelines

fixes #160
2020-09-27 16:18:09 -04:00
classabbyamp 756a15c4c5 added ?worksplit command (#247)
fixes #233
2020-09-27 16:07:21 -04:00
classabbyamp b462527211 add canadian callsign info, improve the prefix info data system
fixes #243
2020-09-24 19:02:12 -04:00
0x5c 93b42e64dc Merge pull request #240 from miaowware/dev-requirements.txt
Add dev-requirements.txt
2020-09-24 10:23:26 -04:00
classabbyamp f6103ef6f1 Merge pull request #245 from miaowware/dockerumentation-v2 2020-09-24 10:18:27 -04:00
Abigail G 090b96482d update docker documentation to mention github registry
also corrected a couple errors
fixes #212
2020-09-24 00:14:45 -04:00
0x5c 6d71974ea1 Add dev-requirements.txt
- Various dev tools like mypy, flake8, and discord.py typing stubs
2020-09-08 03:44:24 -04:00
Abigail Gold 0af82ac241 pin dependency versions (#239) 2020-07-23 00:05:32 -04:00
0x5c 2ba6249b90 Merge pull request #223 from miaowware/action-update
Update linting action
2020-04-19 21:25:08 -04:00
0x5c 2c11dad358 Update linting action
Should now run on PR
2020-04-19 21:10:34 -04:00
Abigail 5f796d479e bump version to 2.3.1 2020-04-02 23:04:29 -04:00
Abigail Gold 3ba55d4c35 update funetics words list (#218)
Fixes #217
2020-04-02 23:02:19 -04:00
Abigail f4ed93dc76 bump version to 2.3.0 2020-03-30 19:00:38 -04:00
Abigail Gold 2cb4b03532 add phonetic weight command (#215)
Fixes #170

Co-authored-by: 0x5c <dev@0x5c.io>
2020-03-30 18:56:29 -04:00
Abigail Gold bc93462c29 convert all OrderedDicts to dictionaries (#214)
Fixes #184

Co-authored-by: 0x5c <dev@0x5c.io>
2020-03-30 18:54:33 -04:00
Abigail Gold 6867c45c8c add ?standards command for xkcd 927 (#213)
Fixes #187
2020-03-30 18:24:50 -04:00
Abigail dcbb7acab8 bump release to 2.2.3 2020-03-29 12:34:58 -04:00
Abigail Gold 3803ce6045 add git commit hash to file "git_commit" for automatically-built docker images (#211) 2020-03-29 12:34:12 -04:00
Abigail Gold 8ca4911072 make commands case-insensitive (#210)
Fixes #209
2020-03-28 19:07:23 -04:00
Abigail Gold 6e2468f04f create github action for docker image creation and publication (#206) 2020-03-28 18:18:29 -04:00
0x5c b17a8a1749 Merge pull request #207 from miaowware/bumpversion
Bump version + changelog (2.2.2)
2020-02-25 21:44:35 -05:00
0x5c 8dfa7001ef Bump version + changelog (2.2.2)
- CL: fixed missing 2.2.1 link
2020-02-25 21:41:36 -05:00
0x5c f6ed8430b9 Merge pull request #205 from miaowware/fix-typing
Added/fixed/removed typing managers
2020-02-25 20:57:07 -05:00
0x5c d650cbd6c1 Removed lint 2020-02-25 20:44:53 -05:00
0x5c 4d9f9d1b19 Added/fixed/removed typing managers
Fixes #167
2020-02-25 20:36:21 -05:00
0x5c 1c649aacc2 Merge pull request #204 from miaowware/fix-studyimg
Fix HamStudy image error
2020-02-25 19:51:03 -05:00
0x5c 2049ca9fca Fix HamStudy image error
- Fixed the image link
- Removed unused import (pytz)
- Added test pool file to dev-notes

Fixes #203
2020-02-25 19:45:32 -05:00
Abigail 38416d9050 whoopsie 2020-02-20 20:02:45 -05:00
Abigail G 8dcdc22fe4 Merge pull request #202 from miaowware/hamstudy-extra-fix
fix offset-naive and offset-aware datetime conflict
2020-02-20 20:02:07 -05:00
Abigail c57056e586 fix offset-naive and offset-aware datetime conflict
Fixes #201
2020-02-20 01:32:17 -05:00
51 changed files with 1235 additions and 44691 deletions
-29
View File
@@ -1,29 +0,0 @@
---
name: Bug report
about: Report a bug to help us improve qrm
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Run command '...' with input '...'
2. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**System (include if related to running the bot):**
- OS: [e.g. Linux, Docker]
- Version: [e.g. 22]
**Additional context**
Add any other context about the problem here.
-20
View File
@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for qrm
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+67
View File
@@ -0,0 +1,67 @@
# vim: ts=2 sw=2:
name: Docker Build and Deploy
on:
push:
# Publish `master` as Docker `dev` image.
branches:
- master
# Publish `v*` tags as x.x.x images and as `latest`.
tags:
- v*
env:
IMAGE_NAME: qrm2
jobs:
docker:
name: Build and push docker images
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{ github.ref }}
- name: Write ref to file
run: git rev-list -n 1 $GITHUB_REF > ./git_commit
- name: Build image
run: docker build . --file Dockerfile -t $IMAGE_NAME
- name: Log into Github Package Registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin
- name: Tag image
id: tag_image
run: |
IMAGE_ID=docker.pkg.github.com/${{ github.repository }}/$IMAGE_NAME
echo IMAGE_ID=$IMAGE_ID
echo ::set-output name=image_id::$IMAGE_ID
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# if version is master, set version to dev
[[ "$VERSION" == "master" ]] && VERSION=dev
echo VERSION=$VERSION
echo ::set-output name=version::$VERSION
# tag dev or x.x.x
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
# tag latest if not a dev release
[[ "$VERSION" != "dev" ]] && docker tag $IMAGE_NAME $IMAGE_ID:latest || true
- name: Push images to registry
run: docker push ${{ steps.tag_image.outputs.image_id }}
- name: Deploy official images
id: deploy_images
uses: satak/webrequest-action@v1
with:
url: ${{ secrets.DEPLOY_URL }}
method: POST
headers: '{"Authentication": "Token ${{ secrets.DEPLOY_TOKEN }}"}'
payload: '{"version": "${{ steps.tag_image.outputs.version }}"}'
+1 -1
View File
@@ -1,6 +1,6 @@
name: Linting
on: [push]
on: [push,pull_request]
jobs:
flake8_py3:
+62
View File
@@ -0,0 +1,62 @@
# vim: ts=2 sw=2:
on:
push:
# Sequence of patterns matched against refs/tags
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
name: Create Release
jobs:
release:
name: Create Release
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{ github.ref }}
- name: Get Version Info
id: get_tag
shell: bash
run: |
SUBJECT=$(/usr/bin/git tag -l ${GITHUB_REF#refs/tags/} --format='%(subject)')
BODY=$(/usr/bin/git tag -l ${GITHUB_REF#refs/tags/} --format='%(body)' | sed '/-----BEGIN PGP SIGNATURE-----/,$d')
echo "SUBJECT=$SUBJECT"
echo "BODY=$BODY"
echo 'tag_subject<<EOS' >> $GITHUB_ENV
echo "$SUBJECT" >> $GITHUB_ENV
echo 'EOS' >> $GITHUB_ENV
echo 'tag_body<<EOB' >> $GITHUB_ENV
echo "$BODY" >> $GITHUB_ENV
echo 'EOB' >> $GITHUB_ENV
echo "tag_version=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
echo "version_num=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Get Changelog Content
id: changelog_reader
uses: mindsers/changelog-reader-action@v2
with:
version: ${{ env.version_num }}
path: ./CHANGELOG.md
- name: Publish Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ env.tag_version }}
release_name: ${{ env.tag_subject }}
body: |
${{ env.tag_body }}
## Changelog
${{ steps.changelog_reader.outputs.changes }}
draft: false
prerelease: false
+2
View File
@@ -9,6 +9,8 @@ cty.zip
/docker-compose.yml
.vscode/
#########################################################
# Byte-compiled / optimized / DLL files
+79 -1
View File
@@ -7,6 +7,74 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [2.5.1] - 2020-12-10
### Fixed
- The result of `?greyline` was cached by discord and would get out of date.
- Broken reaction functionality in `?hamstudy`.
## [2.5.0] - 2020-10-31
### Added
- Italian (`it_hf`, `it_vhf`, `it_shf`), Japanese (`jp`) and Australian (`au`) band charts.
### Fixed
- Details to the Netherlands bandplan command to accurately represent VERNON (Netherlands ARRL equivalent organisation).
- eQSL, paper QSL, and Logbook of the World status in `?qrz` sometimes being shown incorrectly.
- Fixed network error in `?greyline`.
## [2.4.1] - 2020-10-06
### Changed
- Bumped discord.py to 1.5.0
## [2.4.0] - 2020-09-27
### Added
- Canadian prefix info to the `?prefixes` command.
- `?worksplit` command.
- Maps for CQ Zones, ITU Zones, ITU Regions, and Canadian prefixes.
- Attribution for all maps.
- Option to append ` | ?help` to the playing status.
- `?dbconv` command to convert voltage, power, and antenna gain values.
### Changed
- ARRL/RAC section maps to include all current ARRL/RAC sections.
### Fixed
- Issue where multiple prefixes were not handled properly.
## [2.3.2] - 2020-07-22
### Fixed
- Dependency issues
## [2.3.1] - 2020-04-02
### Fixed
- Wordlist containing innappropriate words.
## [2.3.0] - 2020-03-30
### Added
- `?phoneticweight` command, which calculates a message's length in syllables.
- `?standards` command to display [xkcd 927](https://xkcd.com/927/).
### Changed
- Python>=3.7 now required.
## [2.2.3] - 2020-03-29
### Fixed
- Commands are no longer case-sensitive.
## [2.2.2] - 2020-02-25
### Fixed
- Fixed issue where HamStudy questions with images would cause an error.
- Added/fixed/removed typing indicators in numerous commands.
## [2.2.1] - 2020-02-20
### Fixed
- Fixed issue where some HamStudy pools will become unselectable.
## [2.2.0] - 2020-02-15
### Added
- Added Trustee field to qrz command for club callsigns.
@@ -76,7 +144,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 1.0.0 - 2019-07-31 [YANKED]
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.2.0...HEAD
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.5.1...HEAD
[2.5.1]: https://github.com/miaowware/qrm2/releases/tag/v2.5.1
[2.5.0]: https://github.com/miaowware/qrm2/releases/tag/v2.5.0
[2.4.1]: https://github.com/miaowware/qrm2/releases/tag/v2.4.1
[2.4.0]: https://github.com/miaowware/qrm2/releases/tag/v2.4.0
[2.3.2]: https://github.com/miaowware/qrm2/releases/tag/v2.3.2
[2.3.1]: https://github.com/miaowware/qrm2/releases/tag/v2.3.1
[2.3.0]: https://github.com/miaowware/qrm2/releases/tag/v2.3.0
[2.2.3]: https://github.com/miaowware/qrm2/releases/tag/v2.2.3
[2.2.2]: https://github.com/miaowware/qrm2/releases/tag/v2.2.2
[2.2.1]: https://github.com/miaowware/qrm2/releases/tag/v2.2.1
[2.2.0]: https://github.com/miaowware/qrm2/releases/tag/v2.2.0
[2.1.0]: https://github.com/miaowware/qrm2/releases/tag/v2.1.0
[2.0.0]: https://github.com/miaowware/qrm2/releases/tag/v2.0.0
+54
View File
@@ -0,0 +1,54 @@
# Development Guide for qrm
**Make sure to also read [`CONTRIBUTING.md`][0], everything in there applies here.**
### Environment Setup
1. [Fork this repo][1] into your own GitHub namespace.
1. Make sure the `master` branch is up to date, then make yourself a new branch with a descriptive name.
1. Once the forked repo is cloned and on the proper branch, you can set up the development environment.
1. Install python 3.7 or higher.
1. Run `make dev-install`.
This should install everything you need to develop and run qrm.
1. [Create a bot and token][2], and add it to `data/keys.py`.
Also add your [QRZ credentials][3] if needed.
1. In `data/options.py`, change values as needed.
Some commands require adding your Discord user ID to `owner_uids`.
1. To activate the virtual env that was created by `make`, run `source botenv/bin/activate` (or the equivelent for your shell or operating system).
These instructions are fairly \*NIX-centric, so if you would like to develop on Windows, it is suggested that the Windows Subsystem for Linux be used.
## While You Develop
To run qrm, use the command `./run.sh`.
We recommend you use the `--pass-errors` flag to avoid perpetual restart loops if you break the bot.
It exists because repeatedly mashing [Ctrl+C] at high speed to break a fast loop is not fun.
Make sure to add [type hints][4] to your code.
This is what `mypy` validates in the code.
Using `dev-notes` for documentation is especially important if you introduce a new json file format (like for maps and bandplans) or to document some development process (like the command to crush the various images in the repository).
### Test your changes
In addition to testing functionality, make sure to run `flake8` to ensure your code uses the proper style, and `mypy [files...]` to ensure proper typing.
You can also enable them for this project in your IDE if supported.
This will give you automatic and continuous linting and type checking.
### A Note on Style
qrm tries to keep to PEP 8 style whenever possible.
Use the utility `flake8` to check that you follow this style.
When you start a PR or push commits, GitHub will automatically run this for you,
but we prefer that developers check this before committing and opening PRs.
Otherwise, try to follow the existing style:
- double-quotes except when required to be single,
- indentation of mult-line structures matching other examples in the code,
- etc.
[0]: https://github.com/miaowware/.github/blob/master/CONTRIBUTING.md
[1]: https://github.com/miaowware/qrm2/fork
[2]: https://discordpy.readthedocs.io/en/latest/discord.html
[3]: https://www.qrz.com/page/xml_data.html
[4]: https://docs.python.org/3/library/typing.html
+11
View File
@@ -71,6 +71,17 @@ clean:
### Dev targets ###
.PHONY: dev-install
dev-install: $(BOTENV)/dev_req_done data/options.py data/keys.py
# Installing dev requirements
$(BOTENV)/dev_req_done: dev-requirements.txt $(BOTENV)/success
@echo "\033[34;1m--> Installing the dependencies...\033[0m"
@. $(BOTENV)/bin/activate; \
pip install ${PIP_OUTPUT} -U pip setuptools wheel; \
pip install ${PIP_OUTPUT} -U -r dev-requirements.txt
@touch $(BOTENV)/dev_req_done
### Special targets ###
+10 -9
View File
@@ -1,6 +1,6 @@
# Docker help for qrm2
You have multiple ways to use docker to run an instance of qrm2
You have multiple ways to use docker to run an instance of qrm2.
- [Docker help for qrm2](#docker-help-for-qrm2)
- [Using docker-compose and the prebuilt-image (recommended)](#using-docker-compose-and-the-prebuilt-image-recommended)
@@ -23,13 +23,14 @@ This is the easiest method for running the bot without any modifications.
version: '3'
services:
qrm2:
image: "classabbyamp/discord-qrm2:latest"
image: "docker.pkg.github.com/miaowware/qrm2/qrm2:latest"
restart: on-failure
volumes:
- "./data:/app/data:rw"
environment:
- PYTHONUNBUFFERED=1
```
*Note that Github's registry requires [a few extra steps](https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages) during the initial setup.*
3. Create a subdirectory named `data`.
@@ -42,7 +43,7 @@ This is the easiest method for running the bot without any modifications.
$ docker-compose up -d
```
> Run without "-d" to test the bot. (run in foreground)
*Run without "-d" to test the bot (run in foreground).*
@@ -59,7 +60,7 @@ This is the easiest method to run the bot with modifications.
services:
qrm2:
build: .
image: "discord-qrm2:local-latest"
image: "qrm2:local-latest"
restart: on-failure
volumes:
- "./data:/app/data:rw"
@@ -75,10 +76,10 @@ This is the easiest method to run the bot with modifications.
```none
$ docker-compose build --pull
$ docker-compose -d
$ docker-compose up -d
```
> Run without "-d" to test the bot. (run in foreground)
*Run without "-d" to test the bot (run in foreground).*
@@ -95,7 +96,7 @@ This methods is not very nice to use.
2. Run docker build:
```none
$ docker build -t discord-qrm2:local-latest .
$ docker build -t qrm2:local-latest .
```
@@ -110,5 +111,5 @@ This methods is not very nice to use.
```
Where `[image]` is either of:
- `discord-qrm2:local-latest` if you are building your own.
- `classabbyamp/discord-qrm2:latest` if you want to use the prebuilt image.
- `qrm2:local-latest` if you are building your own.
- `docker.pkg.github.com/miaowware/qrm2/qrm2:latest` if you want to use the prebuilt image.
+6
View File
@@ -14,6 +14,8 @@ See [README-DOCKER.md](./README-DOCKER.md)
### Without Docker
Requires Python 3.7 or newer.
Prep the environment. For more information on extra options, see the [quick-bot-no-pain Makefile documentation](https://github.com/0x5c/quick-bot-no-pain/blob/master/docs/makefile.md).
```
@@ -26,6 +28,10 @@ Run. For more information on options, see the [quick-bot-no-pain run.sh document
$ run.sh
```
## Contributing
Check out the [contribution guidelines](/CONTRIBUTING.md) for more information about how to contribute to this project.
## Copyright
Copyright (C) 2019-2020 Abigail Gold, 0x5c
+8 -3
View File
@@ -15,11 +15,13 @@ 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
@@ -63,6 +65,7 @@ emojis = SimpleNamespace(
paths = SimpleNamespace(
data=Path("./data/"),
resources=Path("./resources/"),
img=Path("./resources/img/"),
bandcharts=Path("./resources/img/bandcharts/"),
maps=Path("./resources/img/maps/"),
)
@@ -70,6 +73,7 @@ paths = SimpleNamespace(
# --- Classes ---
class ImageMetadata:
"""Represents the metadata of a single image."""
def __init__(self, metadata: list):
@@ -147,7 +151,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
@@ -164,11 +168,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 ---
+4
View File
@@ -0,0 +1,4 @@
# Image processing instructions
For images like bandplans and maps, first resize the image to a reasonable size, then run `pngquant --quality 30-40` on the images.
Do not apply that to non-flat images like actual pictures.
+1 -1
View File
@@ -20,5 +20,5 @@ Used for grouping info such as name, description, source, and such.
| `name` | The name of the file. | `Canada`, `ITU Zones` |
| `long_name` | The long name (title) of the file. | `Worldwide map of ITU Zones` |
| `description` | The description accompanying the file. | `Full radio allocations chart for all services.` |
| `source` | The source of the file. | `Instituto Federal de Telecomunicaciones (IFT)` |
| `source` | The source of the file. | `Instituto Federal de Telecomunicaciones (IFT)` |
| `emoji` | A Unicode emoji associated with the file. | `📻`, `🇨🇦` |
+48
View File
@@ -0,0 +1,48 @@
{
"_id": "56956f51f65e5c590272e372",
"appears": "2016-04-01T06:00:00.000Z",
"class": "Amateur Extra",
"subtext": "Expires Jul 1, 2020",
"valid_from": "2016-07-01T06:00:00.000Z",
"expires": "2020-07-01T06:00:00.000Z",
"official_name": "Element 4",
"id": "E4_2016",
"slug": "extra2016",
"passing": 37,
"year": 2016,
"pool": [{
"_id": "5cd63f15910d9b003d545bd7",
"qcount": 4,
"id": "E5",
"name": "ELECTRICAL PRINCIPLES",
"sections": [{
"_id": "5cd63f15910d9b003d545beb",
"id": "E5C",
"questions": [{
"_id": "5cd63f15910d9b003d545bef",
"keywords": ["4"],
"answer": "B",
"answers": {
"A": "Point 2",
"B": "Point 4",
"C": "Point 5",
"D": "Point 6"
},
"fccpart": "",
"id": "E5C14",
"image": "E5-2.png",
"text": "Which point on Figure E5-2 best represents the impedance of a series circuit consisting of a 400 ohm resistor and a 38 picofarad capacitor at 14 MHz?"
}],
"summary": "Coordinate systems and phasors in electronics: Rectangular Coordinates; Polar Coordinates; Phasors"
}]
}],
"updated": "2019-05-11T03:18:46.121Z",
"category": "default",
"testIdEnd": 19999,
"testIdStart": 10000,
"__v": 12,
"mat_icon": "flash_on",
"tagline": "Serious General operators only! This is the most advanced US license class!",
"keywords": ["ham radio extra test prep", "amateur extra class radio study", "amateur extra class ham exam", "ham radio amateur extra test 2016", "2016 amateur extra class", "ham radio license exam", "extra class flash card"],
"replaces": "E4_2012"
}
+3
View File
@@ -0,0 +1,3 @@
-r requirements.txt
flake8
discord.py-stubs==1.5.0
+1 -1
View File
@@ -29,7 +29,7 @@ class AE7QCog(commands.Cog):
self.bot = bot
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
@commands.group(name="ae7q", aliases=["ae"], category=cmn.cat.lookup)
@commands.group(name="ae7q", aliases=["ae"], case_insensitive=True, category=cmn.cat.lookup)
async def _ae7q_lookup(self, ctx: commands.Context):
"""Looks up a callsign, FRN, or Licensee ID on [ae7q.com](http://ae7q.com/)."""
if ctx.invoked_subcommand is None:
+4 -6
View File
@@ -10,7 +10,6 @@ the GNU General Public License, version 2.
import random
import re
from collections import OrderedDict
from typing import Union
import discord
@@ -47,9 +46,9 @@ class QrmHelpCommand(commands.HelpCommand):
if parent:
fmt = f"{parent} {fmt}"
alias = fmt
return f"{opt.prefix}{alias} {command.signature}\n *Aliases:* {aliases}"
return f"{opt.display_prefix}{alias} {command.signature}\n *Aliases:* {aliases}"
alias = command.name if not parent else f"{parent} {command.name}"
return f"{opt.prefix}{alias} {command.signature}"
return f"{opt.display_prefix}{alias} {command.signature}"
async def send_error_message(self, error):
embed = cmn.embed_factory(self.context)
@@ -61,7 +60,7 @@ class QrmHelpCommand(commands.HelpCommand):
async def send_bot_help(self, mapping):
embed = cmn.embed_factory(self.context)
embed.title = "qrm Help"
embed.description = (f"For command-specific help and usage, use `{opt.prefix}help [command name]`."
embed.description = (f"For command-specific help and usage, use `{opt.display_prefix}help [command name]`."
" Many commands have shorter aliases.")
mapping = await mapping
@@ -109,7 +108,6 @@ class BaseCog(commands.Cog):
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}")
@@ -189,7 +187,7 @@ class BaseCog(commands.Cog):
def parse_changelog():
changelog = OrderedDict()
changelog = {}
ver = ""
heading = ""
+200
View File
@@ -0,0 +1,200 @@
"""
Conversion extension for qrm
---
Copyright (C) 2020 Abigail Gold, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
"""
import math
from enum import Enum
from typing import Optional
from dataclasses import dataclass
import discord.ext.commands as commands
import common as cmn
from data import options as opt
# not sure why but UnitConverter and Unit need to be defined before DbConvCog and convert()
class UnitConverter(commands.Converter):
async def convert(self, ctx: commands.Context, argument: str):
is_db = None
mult = None
unit = None
utype = None
try:
s = argument.lower()
if len(s) > 2 and s[:2] == "db":
is_db = True
if s[2:] in units:
u = units[s[2:]]
mult = u["mult"]
unit = u["log"]
utype = u["type"]
elif s in units:
is_db = False
u = units[s]
mult = u["mult"]
unit = u["scalar"]
utype = u["type"]
else:
raise ValueError(f"Invalid unit: {argument}")
return Unit(argument, unit, utype, is_db, mult)
except ValueError as e:
raise commands.BadArgument(message=str(e))
class UnitType(Enum):
voltage = 1
power = 2
antenna = 3
@dataclass
class Unit:
raw: str
unit: str
type: UnitType
is_db: bool
mult: int
class DbConvCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.command(name="dbconv", aliases=["dbc"], category=cmn.cat.ref)
async def _db_conv(self, ctx: commands.Context,
value: Optional[float] = None,
unit_from: Optional[UnitConverter] = None,
unit_to: Optional[UnitConverter] = None):
"""
Convert between decibels and scalar values for voltage, power, and antenna gain.
**Valid Units**
*Voltage:* V, mV, µV, uV, dBV, dBmV, dBµV, dBuV
*Power:* fW, mW, W, kW, dBf, dBm, dBW, dBk
*Antenna Gain:* dBi, dBd, dBq
"""
embed = cmn.embed_factory(ctx)
if value is not None and unit_from is not None and unit_to is not None:
converted = convert(value, unit_from, unit_to)
embed.title = f"{value:.3g} {unit_from.unit} = {converted:.3g} {unit_to.unit}"
embed.colour = cmn.colours.good
else:
embed.title = "Decibel Quick Reference"
embed.description = (
"Decibels are a great way to easily represent large quantities that are common in electronics. "
"There are a few main types that are used often in radio: voltage, power, and antenna gain. "
"Here are some commonly-used reference levels for each type:"
)
v_db_info = ("**dBV** = relative to 1 V\n"
"**dBmV** = relative to 1 mV (1e-3 V)\n"
"**dBµV** = relative to 1 µV (1e-6 V)")
embed.add_field(name="Voltage Decibels", value=v_db_info, inline=False)
p_db_info = ("**dBW** = relative to 1 W\n"
"**dBk** = relative to 1 kW (1e3 W)\n"
"**dBm** = relative to 1 mW (1e-3 W)\n"
"**dBf** = relative to 1 fW (1e-15 W)")
embed.add_field(name="Power Decibels", value=p_db_info, inline=False)
a_db_info = ("**dBi** = relative to a theoretical __i__sotropic radiator in free space "
"(equal radiation in all directions)\n"
"**dBd** = relative to a dipole in free space (0 dBd = 2.15 dBi)\n"
"**dBq** = relative to a quarter-wave antenna in free space (0 dBq = -0.85 dBi)")
embed.add_field(name="Antenna Gain Decibels", value=a_db_info, inline=False)
embed.add_field(name="Use the bot to do the conversions",
value=f"`{opt.display_prefix}dbconv [value] [unit_from] [unit_to]`",
inline=False)
await ctx.send(embed=embed)
def setup(bot: commands.Bot):
bot.add_cog(DbConvCog(bot))
def convert(initial: float, unit1: Unit, unit2: Unit):
if unit1.type == unit2.type:
# dB to dB
if unit1.is_db and unit2.is_db:
if unit1.mult == unit2.mult:
return initial
elif unit1.type == UnitType.voltage:
return _calc_volt_db(_calc_volt(initial, unit1.mult), unit2.mult)
elif unit1.type == UnitType.power:
return _calc_power_db(_calc_power(initial, unit1.mult), unit2.mult)
elif unit1.type == UnitType.antenna:
return initial + (unit1.mult - unit2.mult)
# V/W to V/W
elif not unit1.is_db and not unit2.is_db:
if unit1.mult == unit2.mult:
return initial
return initial * unit1.mult / unit2.mult
# dB to V/W
elif unit1.is_db and not unit2.is_db:
if unit1.type == UnitType.voltage:
return _calc_volt(initial, unit1.mult) / unit2.mult
elif unit1.type == UnitType.power:
return _calc_power(initial, unit1.mult) / unit2.mult
# V/W to dB
elif not unit1.is_db and unit2.is_db:
if unit1.type == UnitType.voltage:
return _calc_volt_db(initial * unit1.mult, unit2.mult)
elif unit1.type == UnitType.power:
return _calc_power_db(initial * unit1.mult, unit2.mult)
raise ValueError(f"Can't convert between {unit1} and {unit2}")
units = {
# voltage
"uv": {"mult": 1e-6, "scalar": "µV", "log": "dBµV", "type": UnitType.voltage},
"µv": {"mult": 1e-6, "scalar": "µV", "log": "dBµV", "type": UnitType.voltage},
"mv": {"mult": 1e-3, "scalar": "mV", "log": "dBmV", "type": UnitType.voltage},
"v": {"mult": 1, "scalar": "V", "log": "dBV", "type": UnitType.voltage},
# power
"fw": {"mult": 1e-15, "scalar": "fW", "log": "dBf", "type": UnitType.power},
"f": {"mult": 1e-15, "scalar": "fW", "log": "dBf", "type": UnitType.power},
"mw": {"mult": 1e-3, "scalar": "mW", "log": "dBm", "type": UnitType.power},
"m": {"mult": 1e-3, "scalar": "mW", "log": "dBm", "type": UnitType.power},
"w": {"mult": 1, "scalar": "W", "log": "dBW", "type": UnitType.power},
"kw": {"mult": 1e3, "scalar": "kW", "log": "dBk", "type": UnitType.power},
"k": {"mult": 1e3, "scalar": "kW", "log": "dBk", "type": UnitType.power},
# antenna
"q": {"mult": -0.85, "scalar": None, "log": "dBq", "type": UnitType.antenna},
"i": {"mult": 0, "scalar": None, "log": "dBi", "type": UnitType.antenna},
"d": {"mult": 2.15, "scalar": None, "log": "dBd", "type": UnitType.antenna},
}
def _calc_power_db(p: float, ref: float):
return 10 * math.log10(p / ref)
def _calc_power(db: float, ref: float):
return 10 ** (db / 10) * ref
def _calc_volt_db(v: float, ref: float):
return 20 * math.log10(v / ref)
def _calc_volt(db: float, ref: float):
return 10 ** (db / 20) * ref
# testing code
if __name__ == "__main__":
while(True):
try:
ip = input("> ").split()
initial = float(ip[0])
unit1 = Unit(ip[1])
unit2 = Unit(ip[2])
conv = convert(initial, unit1, unit2)
print(f"{initial:.2f} {unit1} = {conv:.2f} {unit2}")
except ValueError as e:
print(e)
+28 -13
View File
@@ -10,6 +10,7 @@ the GNU General Public License, version 2.
import random
import discord
import discord.ext.commands as commands
import common as cmn
@@ -18,7 +19,7 @@ import common as cmn
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 / "words") as words_file:
self.words = words_file.read().lower().splitlines()
@commands.command(name="xkcd", aliases=["x"], category=cmn.cat.fun)
@@ -31,6 +32,21 @@ class FunCog(commands.Cog):
"""Returns xkcd: tar."""
await ctx.send("http://xkcd.com/1168")
@commands.command(name="standards", category=cmn.cat.fun)
async def _standards(self, ctx: commands.Context):
"""Returns xkcd: Standards."""
await ctx.send("http://xkcd.com/927")
@commands.command(name="worksplit", aliases=["split", "ft8"], category=cmn.cat.fun)
async def _worksplit(self, ctx: commands.Context):
"""Posts "Work split you lids"."""
fn = "worksplit.jpg"
embed = cmn.embed_factory(ctx)
embed.title = "Work Split, You Lids!"
embed.set_image(url="attachment://" + fn)
img = discord.File(cmn.paths.img / fn, filename=fn)
await ctx.send(embed=embed, file=img)
@commands.command(name="xd", hidden=True, category=cmn.cat.fun)
async def _xd(self, ctx: commands.Context):
"""ecks dee"""
@@ -39,18 +55,17 @@ class FunCog(commands.Cog):
@commands.command(name="funetics", aliases=["fun"], category=cmn.cat.fun)
async def _funetics_lookup(self, ctx: commands.Context, *, msg: str):
"""Generates fun/wacky phonetics for a word or phrase."""
with ctx.typing():
result = ""
for char in msg.lower():
if char.isalpha():
result += random.choice([word for word in self.words if word[0] == char])
else:
result += char
result += " "
embed = cmn.embed_factory(ctx)
embed.title = f"Funetics for {msg}"
embed.description = result.title()
embed.colour = cmn.colours.good
result = ""
for char in msg.lower():
if char.isalpha():
result += random.choice([word for word in self.words if word[0] == char])
else:
result += char
result += " "
embed = cmn.embed_factory(ctx)
embed.title = f"Funetics for {msg}"
embed.description = result.title()
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
+75 -77
View File
@@ -23,93 +23,91 @@ class GridCog(commands.Cog):
async def _grid_sq_lookup(self, ctx: commands.Context, lat: str, lon: str):
("""Calculates the grid square for latitude and longitude coordinates, """
"""with negative being latitude South and longitude West.""")
with ctx.typing():
grid = "**"
latf = float(lat) + 90
lonf = float(lon) + 180
if 0 <= latf <= 180 and 0 <= lonf <= 360:
grid += chr(ord("A") + int(lonf / 20))
grid += chr(ord("A") + int(latf / 10))
grid += chr(ord("0") + int((lonf % 20)/2))
grid += chr(ord("0") + int((latf % 10)/1))
grid += chr(ord("a") + int((lonf - (int(lonf/2)*2)) / (5/60)))
grid += chr(ord("a") + int((latf - (int(latf/1)*1)) / (2.5/60)))
grid += "**"
embed = cmn.embed_factory(ctx)
embed.title = f"Maidenhead Grid Locator for {float(lat):.6f}, {float(lon):.6f}"
embed.description = grid
embed.colour = cmn.colours.good
else:
embed = cmn.embed_factory(ctx)
embed.title = f"Error generating grid square for {lat}, {lon}."
embed.description = """Coordinates out of range.
The valid ranges are:
- Latitude: `-90` to `+90`
- Longitude: `-180` to `+180`"""
embed.colour = cmn.colours.bad
grid = "**"
latf = float(lat) + 90
lonf = float(lon) + 180
if 0 <= latf <= 180 and 0 <= lonf <= 360:
grid += chr(ord("A") + int(lonf / 20))
grid += chr(ord("A") + int(latf / 10))
grid += chr(ord("0") + int((lonf % 20)/2))
grid += chr(ord("0") + int((latf % 10)/1))
grid += chr(ord("a") + int((lonf - (int(lonf/2)*2)) / (5/60)))
grid += chr(ord("a") + int((latf - (int(latf/1)*1)) / (2.5/60)))
grid += "**"
embed = cmn.embed_factory(ctx)
embed.title = f"Maidenhead Grid Locator for {float(lat):.6f}, {float(lon):.6f}"
embed.description = grid
embed.colour = cmn.colours.good
else:
embed = cmn.embed_factory(ctx)
embed.title = f"Error generating grid square for {lat}, {lon}."
embed.description = """Coordinates out of range.
The valid ranges are:
- Latitude: `-90` to `+90`
- Longitude: `-180` to `+180`"""
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
@commands.command(name="ungrid", aliases=["loc"], category=cmn.cat.maps)
async def _location_lookup(self, ctx: commands.Context, grid: str, grid2: str = None):
"""Calculates the latitude and longitude for the center of a grid square.
If two grid squares are given, the distance and azimuth between them is calculated."""
with ctx.typing():
if grid2 is None or grid2 == "":
try:
grid = grid.upper()
loc = get_coords(grid)
if grid2 is None or grid2 == "":
try:
grid = grid.upper()
loc = get_coords(grid)
embed = cmn.embed_factory(ctx)
embed.title = f"Latitude and Longitude for {grid}"
embed.colour = cmn.colours.good
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
if len(grid) >= 6:
embed.description = f"**{loc[0]:.5f}, {loc[1]:.5f}**"
embed.url = f"https://www.openstreetmap.org/#map=13/{loc[0]:.5f}/{loc[1]:.5f}"
else:
embed.description = f"**{loc[0]:.1f}, {loc[1]:.1f}**"
embed.url = f"https://www.openstreetmap.org/#map=10/{loc[0]:.1f}/{loc[1]:.1f}"
except Exception as e:
embed = cmn.embed_factory(ctx)
embed.title = f"Error generating latitude and longitude for grid {grid}."
embed.description = str(e)
embed.colour = cmn.colours.bad
else:
radius = 6371
try:
grid = grid.upper()
grid2 = grid2.upper()
loc = get_coords(grid)
loc2 = get_coords(grid2)
# Haversine formula
d_lat = math.radians(loc2[0] - loc[0])
d_lon = math.radians(loc2[1] - loc[1])
a = (math.sin(d_lat/2) ** 2
+ math.cos(math.radians(loc[0]))
* math.cos(math.radians(loc2[0]))
* math.sin(d_lon/2) ** 2)
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
d = radius * c
d_mi = 0.6213712 * d
# Bearing
y_dist = math.sin(math.radians(loc2[1]-loc[1])) * math.cos(math.radians(loc2[0]))
x_dist = (math.cos(math.radians(loc[0]))
* math.sin(math.radians(loc2[0]))
- math.sin(math.radians(loc[0]))
* math.cos(math.radians(loc2[0]))
* math.cos(math.radians(loc2[1] - loc[1])))
bearing = (math.degrees(math.atan2(y_dist, x_dist)) + 360) % 360
# Bearing
y_dist = math.sin(math.radians(loc2[1]-loc[1])) * math.cos(math.radians(loc2[0]))
x_dist = (math.cos(math.radians(loc[0]))
* math.sin(math.radians(loc2[0]))
- math.sin(math.radians(loc[0]))
* math.cos(math.radians(loc2[0]))
* math.cos(math.radians(loc2[1] - loc[1])))
bearing = (math.degrees(math.atan2(y_dist, x_dist)) + 360) % 360
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"Great Circle Distance and Bearing from {grid} to {grid2}"
embed.description = f"**Distance:** {d:.1f} km ({d_mi:.1f} mi)\n**Bearing:** {bearing:.1f}°"
embed.colour = cmn.colours.good
except Exception as e:
embed = cmn.embed_factory(ctx)
embed.title = f"Error generating great circle distance and bearing from {grid} and {grid2}."
embed.description = str(e)
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
+61 -39
View File
@@ -21,68 +21,70 @@ 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."""
with ctx.typing():
qcode = qcode.upper()
embed = cmn.embed_factory(ctx)
if qcode in qcodes.qcodes:
embed.title = qcode
embed.description = qcodes.qcodes[qcode]
embed.colour = cmn.colours.good
else:
embed.title = f"Q Code {qcode} not found"
embed.colour = cmn.colours.bad
qcode = qcode.upper()
embed = cmn.embed_factory(ctx)
if qcode in qcodes.qcodes:
embed.title = qcode
embed.description = qcodes.qcodes[qcode]
embed.colour = cmn.colours.good
else:
embed.title = f"Q Code {qcode} not found"
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
@commands.command(name="phonetics", aliases=["ph", "phoneticize", "phoneticise", "phone"], category=cmn.cat.ref)
async def _phonetics_lookup(self, ctx: commands.Context, *, msg: str):
"""Returns NATO phonetics for a word or phrase."""
with ctx.typing():
result = ""
for char in msg.lower():
if char.isalpha():
result += phonetics.phonetics[char]
else:
result += char
result += " "
embed = cmn.embed_factory(ctx)
embed.title = f"Phonetics for {msg}"
embed.description = result.title()
embed.colour = cmn.colours.good
result = ""
for char in msg.lower():
if char.isalpha():
result += phonetics.phonetics[char]
else:
result += char
result += " "
embed = cmn.embed_factory(ctx)
embed.title = f"Phonetics for {msg}"
embed.description = result.title()
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
@commands.command(name="utc", aliases=["z"], category=cmn.cat.ref)
async def _utc_lookup(self, ctx: commands.Context):
"""Returns the current time in UTC."""
with ctx.typing():
now = datetime.utcnow()
result = "**" + now.strftime("%Y-%m-%d %H:%M") + "Z**"
embed = cmn.embed_factory(ctx)
embed.title = "The current time is:"
embed.description = result
embed.colour = cmn.colours.good
now = datetime.utcnow()
result = "**" + now.strftime("%Y-%m-%d %H:%M") + "Z**"
embed = cmn.embed_factory(ctx)
embed.title = "The current time is:"
embed.description = result
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
@commands.command(name="prefixes", aliases=["vanity", "pfx", "vanities", "prefix"], category=cmn.cat.ref)
async def _vanity_prefixes(self, ctx: commands.Context, country: str = None):
async def _vanity_prefixes(self, ctx: commands.Context, country: str = ""):
"""Lists valid callsign prefixes for different countries."""
if country is None:
await ctx.send_help(ctx.command)
return
country = country.lower()
embed = cmn.embed_factory(ctx)
if country.lower() not in callsign_info.options:
embed.title = f"{country} not found!"
embed.description = f"Valid countries: {', '.join(callsign_info.options.keys())}"
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:
embed.title = callsign_info.options[country.lower()][0]
embed.description = callsign_info.options[country.lower()][1]
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 callsign_info.options[country.lower()][2].items():
for name, val in data.calls.items():
embed.add_field(name=name, value=val, inline=False)
await ctx.send(embed=embed)
@@ -95,6 +97,26 @@ class HamCog(commands.Cog):
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))
+7 -12
View File
@@ -8,8 +8,6 @@ the GNU General Public License, version 2.
"""
import io
import aiohttp
import discord
@@ -85,16 +83,13 @@ class ImageCog(commands.Cog):
@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"))
embed = cmn.embed_factory(ctx)
embed.title = "Current Greyline Conditions"
embed.colour = cmn.colours.good
# Generate a nonce to force discord to recache this
cachenonce = (ctx.message.id >> 22) // 1000 // 600 # nonce will stay the same for ~10min
embed.set_image(url=self.gl_url + f"&cachenonce={cachenonce}")
await ctx.send(embed=embed)
def setup(bot: commands.Bot):
+28 -25
View File
@@ -9,6 +9,7 @@ the GNU General Public License, version 2.
import threading
from pathlib import Path
from ctyparser import BigCty
@@ -17,11 +18,14 @@ from discord.ext import commands, tasks
import common as cmn
cty_path = Path("./data/cty.json")
class LookupCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
try:
self.cty = BigCty("./data/cty.json")
self.cty = BigCty(cty_path)
except OSError:
self.cty = BigCty()
@@ -40,35 +44,34 @@ class LookupCog(commands.Cog):
@commands.command(name="dxcc", aliases=["dx"], category=cmn.cat.lookup)
async def _dxcc_lookup(self, ctx: commands.Context, query: str):
"""Gets DXCC info about a callsign prefix."""
with ctx.typing():
query = query.upper()
full_query = query
embed = cmn.embed_factory(ctx)
embed.title = "DXCC Info for "
embed.description = f"*Last Updated: {self.cty.formatted_version}*"
embed.colour = cmn.colours.bad
while query:
if query in self.cty.keys():
data = self.cty[query]
embed.add_field(name="Entity", value=data["entity"])
embed.add_field(name="CQ Zone", value=data["cq"])
embed.add_field(name="ITU Zone", value=data["itu"])
embed.add_field(name="Continent", value=data["continent"])
embed.add_field(name="Time Zone",
value=f"+{data['tz']}" if data["tz"] > 0 else str(data["tz"]))
embed.title += query
embed.colour = cmn.colours.good
break
else:
query = query[:-1]
query = query.upper()
full_query = query
embed = cmn.embed_factory(ctx)
embed.title = "DXCC Info for "
embed.description = f"*Last Updated: {self.cty.formatted_version}*"
embed.colour = cmn.colours.bad
while query:
if query in self.cty.keys():
data = self.cty[query]
embed.add_field(name="Entity", value=data["entity"])
embed.add_field(name="CQ Zone", value=data["cq"])
embed.add_field(name="ITU Zone", value=data["itu"])
embed.add_field(name="Continent", value=data["continent"])
embed.add_field(name="Time Zone",
value=f"+{data['tz']}" if data["tz"] > 0 else str(data["tz"]))
embed.title += query
embed.colour = cmn.colours.good
break
else:
embed.title += full_query + " not found"
embed.colour = cmn.colours.bad
query = query[:-1]
else:
embed.title += full_query + " not found"
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
@tasks.loop(hours=24)
async def _update_cty(self):
update = threading.Thread(target=run_update, args=(self.cty, "./data/cty.json"))
update = threading.Thread(target=run_update, args=(self.cty, cty_path))
update.start()
+41 -44
View File
@@ -21,61 +21,58 @@ class MorseCog(commands.Cog):
@commands.command(name="morse", aliases=["cw"], category=cmn.cat.ref)
async def _morse(self, ctx: commands.Context, *, msg: str):
"""Converts ASCII to international morse code."""
with ctx.typing():
result = ""
for char in msg.upper():
try:
result += morse.morse[char]
except KeyError:
result += "<?>"
result += " "
embed = cmn.embed_factory(ctx)
embed.title = f"Morse Code for {msg}"
embed.description = "**" + result + "**"
embed.colour = cmn.colours.good
result = ""
for char in msg.upper():
try:
result += morse.morse[char]
except KeyError:
result += "<?>"
result += " "
embed = cmn.embed_factory(ctx)
embed.title = f"Morse Code for {msg}"
embed.description = "**" + result + "**"
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
@commands.command(name="unmorse", aliases=["demorse", "uncw", "decw"], category=cmn.cat.ref)
async def _unmorse(self, ctx: commands.Context, *, msg: str):
"""Converts international morse code to ASCII."""
with ctx.typing():
result = ""
msg0 = msg
msg = msg.split("/")
msg = [m.split() for m in msg]
for word in msg:
for char in word:
try:
result += morse.ascii[char]
except KeyError:
result += "<?>"
result += " "
embed = cmn.embed_factory(ctx)
embed.title = f"ASCII for {msg0}"
embed.description = result
embed.colour = cmn.colours.good
result = ""
msg0 = msg
msg = msg.split("/")
msg = [m.split() for m in msg]
for word in msg:
for char in word:
try:
result += morse.ascii[char]
except KeyError:
result += "<?>"
result += " "
embed = cmn.embed_factory(ctx)
embed.title = f"ASCII for {msg0}"
embed.description = result
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
@commands.command(name="cwweight", aliases=["weight", "cww"], category=cmn.cat.ref)
async def _weight(self, ctx: commands.Context, *, msg: str):
"""Calculates the CW weight of a callsign or message."""
embed = cmn.embed_factory(ctx)
with ctx.typing():
msg = msg.upper()
weight = 0
for char in msg:
try:
cw_char = morse.morse[char].replace("-", "==")
weight += len(cw_char) * 2 + 2
except KeyError:
embed.title = "Error in calculation of CW weight"
embed.description = f"Unknown character `{char}` in message"
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
embed.title = f"CW Weight of {msg}"
embed.description = f"The CW weight is **{weight}**"
embed.colour = cmn.colours.good
msg = msg.upper()
weight = 0
for char in msg:
try:
cw_char = morse.morse[char].replace("-", "==")
weight += len(cw_char) * 2 + 2
except KeyError:
embed.title = "Error in calculation of CW weight"
embed.description = f"Unknown character `{char}` in message"
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
embed.title = f"CW Weight of {msg}"
embed.description = f"The CW weight is **{weight}**"
embed.colour = cmn.colours.good
await ctx.send(embed=embed)
+61 -64
View File
@@ -8,7 +8,6 @@ the GNU General Public License, version 2.
"""
from collections import OrderedDict
from io import BytesIO
import aiohttp
@@ -36,50 +35,51 @@ class QRZCog(commands.Cog):
await ctx.send(f"http://qrz.com/db/{callsign}")
return
try:
await qrz_test_session(self.key, self.session)
except ConnectionError:
await self.get_session()
url = f"http://xmldata.qrz.com/xml/current/?s={self.key};callsign={callsign}"
async with self.session.get(url) as resp:
if resp.status != 200:
raise ConnectionError(f"Unable to connect to QRZ (HTTP Error {resp.status})")
with BytesIO(await resp.read()) as resp_file:
resp_xml = etree.parse(resp_file).getroot()
resp_xml_session = resp_xml.xpath("/x:QRZDatabase/x:Session", namespaces={"x": "http://xmldata.qrz.com"})
resp_session = {el.tag.split("}")[1]: el.text for el in resp_xml_session[0].getiterator()}
if "Error" in resp_session:
if "Session Timeout" in resp_session["Error"]:
async with ctx.typing():
try:
await qrz_test_session(self.key, self.session)
except ConnectionError:
await self.get_session()
await self._qrz_lookup(ctx, callsign)
return
if "Not found" in resp_session["Error"]:
embed = 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()}
url = f"http://xmldata.qrz.com/xml/current/?s={self.key};callsign={callsign}"
async with self.session.get(url) as resp:
if resp.status != 200:
raise ConnectionError(f"Unable to connect to QRZ (HTTP Error {resp.status})")
with BytesIO(await resp.read()) as resp_file:
resp_xml = etree.parse(resp_file).getroot()
embed = 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"])
resp_xml_session = resp_xml.xpath("/x:QRZDatabase/x:Session", namespaces={"x": "http://xmldata.qrz.com"})
resp_session = {el.tag.split("}")[1]: el.text for el in resp_xml_session[0].getiterator()}
if "Error" in resp_session:
if "Session Timeout" in resp_session["Error"]:
await self.get_session()
await self._qrz_lookup(ctx, callsign)
return
if "Not found" in resp_session["Error"]:
embed = cmn.embed_factory(ctx)
embed.title = f"QRZ Data for {callsign.upper()}"
embed.colour = cmn.colours.bad
embed.description = "No data found!"
await ctx.send(embed=embed)
return
raise ValueError(resp_session["Error"])
data = qrz_process_info(resp_data)
resp_xml_data = resp_xml.xpath("/x:QRZDatabase/x:Callsign", namespaces={"x": "http://xmldata.qrz.com"})
resp_data = {el.tag.split("}")[1]: el.text for el in resp_xml_data[0].getiterator()}
for title, val in data.items():
if val is not None:
embed.add_field(name=title, value=val, inline=True)
await ctx.send(embed=embed)
embed = cmn.embed_factory(ctx)
embed.title = f"QRZ Data for {resp_data['call']}"
embed.colour = cmn.colours.good
embed.url = f"http://www.qrz.com/db/{resp_data['call']}"
if "image" in resp_data:
embed.set_thumbnail(url=resp_data["image"])
data = qrz_process_info(resp_data)
for title, val in data.items():
if val is not None:
embed.add_field(name=title, value=val, inline=True)
await ctx.send(embed=embed)
async def get_session(self):
"""Session creation and caching."""
@@ -146,39 +146,36 @@ def qrz_process_info(data: dict):
if address == "":
address = None
if "eqsl" in data:
eqsl = "Yes" if data["eqsl"] == 1 else "No"
eqsl = "Yes" if data["eqsl"] == "1" else "No"
else:
eqsl = "Unknown"
if "mqsl" in data:
mqsl = "Yes" if data["mqsl"] == 1 else "No"
mqsl = "Yes" if data["mqsl"] == "1" else "No"
else:
mqsl = "Unknown"
if "lotw" in data:
lotw = "Yes" if data["lotw"] == 1 else "No"
lotw = "Yes" if data["lotw"] == "1" else "No"
else:
lotw = "Unknown"
return OrderedDict([("Name", name),
("Country", data.get("country", None)),
("Address", address),
("Grid Square", data.get("grid", None)),
("County", data.get("county", None)),
("CQ Zone", data.get("cqzone", None)),
("ITU Zone", data.get("ituzone", None)),
("IOTA Designator", data.get("iota", None)),
("Expires", data.get("expdate", None)),
("Aliases", data.get("aliases", None)),
("Previous Callsign", data.get("p_call", None)),
("License Class", data.get("class", None)),
("Trustee", data.get("trustee", None)),
("eQSL?", eqsl),
("Paper QSL?", mqsl),
("LotW?", lotw),
("QSL Info", data.get("qslmgr", None)),
("CQ Zone", data.get("cqzone", None)),
("ITU Zone", data.get("ituzone", None)),
("IOTA Designator", data.get("iota", None)),
("Born", data.get("born", None))])
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):
+3 -3
View File
@@ -88,8 +88,8 @@ class StudyCog(commands.Cog):
else:
# look at valid_from and expires dates to find the correct one
for p in pool_matches:
valid_from = datetime.fromisoformat(pools[p]["valid_from"][:-1] + "+00:00")
expires = datetime.fromisoformat(pools[p]["expires"][:-1] + "+00:00")
valid_from = datetime.fromisoformat(pools[p]["valid_from"][:-1])
expires = datetime.fromisoformat(pools[p]["expires"][:-1])
if valid_from < datetime.utcnow() < expires:
pool = p
@@ -133,7 +133,7 @@ class StudyCog(commands.Cog):
" the answer will be revealed."),
inline=False)
if "image" in question:
image_url = f"https://hamstudy.org/_1330011/images/{pool.split('_',1)[1]}/{question['image']}"
image_url = f"https://hamstudy.org/images/{pool_meta['year']}/{question['image']}"
embed.set_image(url=image_url)
q_msg = await ctx.send(embed=embed)
+42 -59
View File
@@ -8,12 +8,10 @@ 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
@@ -29,18 +27,13 @@ class WeatherCog(commands.Cog):
@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"))
embed = cmn.embed_factory(ctx)
embed.title = "Current Solar Conditions"
embed.colour = cmn.colours.good
embed.set_image(url="http://www.hamqsl.com/solarsun.php")
await ctx.send(embed=embed)
@commands.group(name="weather", aliases=["wttr"], category=cmn.cat.weather)
@commands.group(name="weather", aliases=["wttr"], case_insensitive=True, category=cmn.cat.weather)
async def _weather_conditions(self, ctx: commands.Context):
"""Gets local weather conditions from [wttr.in](http://wttr.in/).
@@ -61,63 +54,53 @@ class WeatherCog(commands.Cog):
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 = ""
try:
units_arg = re.search(self.wttr_units_regex, location).group(1)
except AttributeError:
units_arg = ""
if units_arg.lower() == "f":
units = "u"
elif units_arg.lower() == "c":
units = "m"
else:
units = ""
loc = self.wttr_units_regex.sub("", location).strip()
loc = self.wttr_units_regex.sub("", location).strip()
embed = 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
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"))
loc = loc.replace(" ", "+")
embed.set_image(url=f"http://wttr.in/{loc}_{units}pnFQ.png")
await ctx.send(embed=embed)
@_weather_conditions.command(name="now", aliases=["n"], category=cmn.cat.weather)
async def _weather_conditions_now(self, ctx: commands.Context, *, location: str):
"""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 = ""
try:
units_arg = re.search(self.wttr_units_regex, location).group(1)
except AttributeError:
units_arg = ""
if units_arg.lower() == "f":
units = "u"
elif units_arg.lower() == "c":
units = "m"
else:
units = ""
loc = self.wttr_units_regex.sub("", location).strip()
loc = self.wttr_units_regex.sub("", location).strip()
embed = 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
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"))
loc = loc.replace(" ", "+")
embed.set_image(url=f"http://wttr.in/{loc}_0{units}pnFQ.png")
await ctx.send(embed=embed)
def setup(bot: commands.Bot):
+1 -1
View File
@@ -12,5 +12,5 @@ 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.2.0"
release = "2.5.1"
bot_server = "https://discord.gg/Ntbg3J4"
+26 -6
View File
@@ -44,9 +44,20 @@ debug_mode = opt.debug # Separate assignement in-case we define an override (te
loop = asyncio.get_event_loop()
connector = loop.run_until_complete(conn.new_connector())
# Defining the intents
intents = discord.Intents.none()
intents.guilds = True
intents.guild_messages = True
intents.dm_messages = True
intents.reactions = True
member_cache = discord.MemberCacheFlags.from_intents(intents)
bot = commands.Bot(command_prefix=opt.prefix,
description=info.description,
help_command=commands.MinimalHelpCommand(),
case_insensitive=True,
description=info.description, help_command=commands.MinimalHelpCommand(),
intents=intents,
member_cache=member_cache,
loop=loop,
connector=connector)
@@ -82,13 +93,13 @@ async def _shutdown_bot(ctx: commands.Context):
await bot.logout()
@bot.group(name="extctl", aliases=["ex"], category=cmn.cat.admin)
@bot.group(name="extctl", aliases=["ex"], case_insensitive=True, category=cmn.cat.admin)
@commands.check(cmn.check_if_owner)
async def _extctl(ctx: commands.Context):
"""Extension control commands.
Defaults to `list` if no subcommand specified"""
if ctx.invoked_subcommand is None:
cmd = bot.get_command("extctl list")
cmd = _extctl_list
await ctx.invoke(cmd)
@@ -156,7 +167,7 @@ async def on_command_error(ctx: commands.Context, err: commands.CommandError):
await cmn.add_react(ctx.message, cmn.emojis.warning)
await ctx.send_help(ctx.command)
elif isinstance(err, commands.CommandNotFound):
if ctx.invoked_with.startswith(("?", "!")):
if ctx.invoked_with and ctx.invoked_with.startswith(("?", "!")):
return
else:
await cmn.add_react(ctx.message, cmn.emojis.question)
@@ -193,7 +204,10 @@ async def _ensure_activity_time():
try:
tz = pytz.timezone(opt.status_tz)
except pytz.exceptions.UnknownTimeZoneError:
await bot.change_presence(activity=discord.Game(name="with invalid timezones."))
status = "with invalid timezones"
if opt.show_help:
status += f" | {opt.display_prefix}help"
await bot.change_presence(activity=discord.Game(name=status))
return
now = datetime.now(tz=tz).time()
@@ -203,6 +217,8 @@ async def _ensure_activity_time():
end_time = time(hour=sts[2][0], minute=sts[2][1], tzinfo=tz)
if start_time < now <= end_time:
status = sts[0]
if opt.show_help:
status += f" | {opt.display_prefix}help"
await bot.change_presence(activity=discord.Game(name=status))
@@ -210,6 +226,8 @@ async def _ensure_activity_time():
@tasks.loop(minutes=5)
async def _ensure_activity_random():
status = random.choice(opt.statuses)
if opt.show_help:
status += f" | {opt.display_prefix}help"
await bot.change_presence(activity=discord.Game(name=status))
@@ -217,6 +235,8 @@ async def _ensure_activity_random():
@tasks.loop(minutes=5)
async def _ensure_activity_fixed():
status = opt.statuses[0]
if opt.show_help:
status += f" | {opt.display_prefix}help"
await bot.change_presence(activity=discord.Game(name=status))
+2 -2
View File
@@ -1,5 +1,5 @@
discord.py
ctyparser
discord.py~=1.5.0
ctyparser~=2.0
beautifulsoup4
lxml
pytz
+16 -44
View File
@@ -1,5 +1,5 @@
"""
Information about callsigns for the vanity prefixes command in hamcog.
Information about callsigns for the prefixes command in hamcog.
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
@@ -8,49 +8,21 @@ the GNU General Public License, version 2.
"""
from collections import OrderedDict
from dataclasses import dataclass
from .callsigninfos import (us, ca)
us_calls_title = "Valid US Vanity Callsigns"
us_calls_desc = ("#x# is the number of letters in the prefix and suffix of a callsign. "
"E.g., WY4RC would be a 2x2 callsign, with prefix WY and suffix RC.")
us_calls = OrderedDict([("**Group A** (Extra Only)", ("**Any:** K, N, W (1x2)\n"
" AA-AL, KA-KZ, NA-NZ, WA-WZ (2x1)\n"
" AA-AL (2x2)\n"
"*Except*\n"
"**Alaska:** AL, KL, NL, WL (2x1)\n"
"**Caribbean:** KP, NP, WP (2x1)\n"
"**Pacific:** AH, KH, NH, WH (2x1)")),
("**Group B** (Advanced and Extra Only)", ("**Any:** KA-KZ, NA-NZ, WA-WZ (2x2)\n"
"*Except*\n"
"**Alaska:** AL (2x2)\n"
"**Caribbean:** KP (2x2)\n"
"**Pacific:** AH (2x2)")),
("**Group C** (Technician, General, Advanced, Extra Only)", ("**Any Region:** K, N, W (1x3)\n"
"*Except*\n"
"**Alaska:** KL, NL, WL (2x2)\n"
"**Caribbean:** NP, WP (2x2)\n"
"**Pacific:** KH, NH, WH (2x2)")),
("**Group D** (Any License Class)", ("**Any Region:** KA-KZ, WA-WZ (2x3)\n"
"*Except*\n"
"**Alaska:** KL, WL (2x3)\n"
"**Caribbean:** KP, WP (2x3)\n"
"**Pacific:** KH, WH (2x3)")),
("**Unavailable**", ("- KA2AA-KA9ZZ: US Army in Japan\n"
"- KC4AAA-KC4AAF: NSF in Antartica\n"
"- KC4USA-KC4USZ: US Navy in Antartica\n"
"- KG4AA-KG4ZZ: US Navy in Guantanamo Bay\n"
"- KL9KAA-KL9KHZ: US military in Korea\n"
"- KC6AA-KC6ZZ: Former US (Eastern and Western Caroline Islands), "
"now Federated States of Micronesia (V6) and Republic of Palau (T8)\n"
"- KX6AA-KX6ZZ: Former US (Marshall Islands), "
"now Republic of the Marshall Islands (V73)\n"
"- Any suffix SOS or QRA-QUZ\n"
"- Any 2x3 with X as the first suffix letter\n"
"- Any 2x3 with AF, KF, NF, or WF prefix and suffix EMA: FEMA\n"
"- Any 2x3 with AA-AL, NA-NZ, WC, WK, WM, WR, or WT prefix: \"Group X\"\n"
"- Any 2x1, 2x2, or 2x3 with KP, NP, WP prefix and 0, 6, 7, 8, 9 number\n"
"- Any 1x1 callsign: Special Event"))])
@dataclass
class CallsignInfoData:
"""Represents a country's callsign info"""
title: str = ""
desc: str = ""
calls: str = ""
emoji: str = ""
# format: country: (title, description, text)
options = {"us": (us_calls_title, us_calls_desc, us_calls)}
options = {
"us": CallsignInfoData(us.title, us.desc, us.calls, us.emoji),
"ca": CallsignInfoData(ca.title, ca.desc, ca.calls, ca.emoji),
}
+3
View File
@@ -0,0 +1,3 @@
"""
Callsign info for various countries
"""
+47
View File
@@ -0,0 +1,47 @@
"""
Information about callsigns for the CA prefixes command in hamcog.
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
This file is part of discord-qrmbot and is released under the terms of
the GNU General Public License, version 2.
"""
title = "Canadian Callsign Rules"
emoji = "🇨🇦"
desc = ("Canadian operators are limited to callsigns with the prefixes of their address' province/territory. "
"Initially, operators can choose a callsign with a 3-letter suffix. "
"Later on, they can apply to change or for additional callsigns. "
"Operators can only hold one 2-letter suffix callsign, but many 3-letter suffix callsigns. "
"If the number of 2-letter suffix callsigns exceeds 80% of the total available, "
"operators can only choose a 2-letter suffix after holding a license for 5 years. "
"If the operator is a family member of a deceased operator, they are not bound by this restriction. "
"Data from [ISED Canada (RIC-9)](https://www.ic.gc.ca/eic/site/smt-gst.nsf/eng/sf02102.html).")
calls = {
"Provinces": (
"**Nova Scotia:** VE1 and VA1\n"
"**Québec:** VE2 and VA2\n"
"**Ontario:** VE3 and VA3\n"
"**Manitoba:** VE4 and VA4\n"
"**Saskatchewan:** VE5 and VA5\n"
"**Alberta:** VE6 and VA6\n"
"**British Columbia:** VE7 and VA7\n"
"**New Brunswick:** VE9\n"
"**Newfoundland:** VO1\n"
"**Labrador:** VO2\n"
"**Prince Edward Island:** VY2\n"
),
"Territories": (
"**Northwest Territories:** VE8\n"
"**Nunavut:** VY0\n"
"**Yukon:** VY1\n"
),
"Other": (
"**International Waters:** VE0\n"
"**Government of Canada:** VY9\n"
"**Sable Island:** CY0\n"
"**St-Paul Island:** CY9\n"
),
"Special Event": "Various prefixes in the ranges: CF-CK, CY-CZ, VA-VG, VO, VX-VY, XJ-XO"
}
+53
View File
@@ -0,0 +1,53 @@
"""
Information about callsigns for the US prefixes command in hamcog.
---
Copyright (C) 2019-2020 Abigail Gold, 0x5c
This file is part of discord-qrmbot and is released under the terms of
the GNU General Public License, version 2.
"""
title = "US Callsign Rules"
emoji = "🇺🇸"
desc = ("#x# is the number of letters in the prefix and suffix of a callsign. "
"E.g., WY4RC would be a 2x2 callsign, with prefix WY and suffix RC.")
calls = {
"**Group A** (Extra Only)": ("**Any:** K, N, W (1x2)\n"
" AA-AL, KA-KZ, NA-NZ, WA-WZ (2x1)\n"
" AA-AL (2x2)\n"
"*Except*\n"
"**Alaska:** AL, KL, NL, WL (2x1)\n"
"**Caribbean:** KP, NP, WP (2x1)\n"
"**Pacific:** AH, KH, NH, WH (2x1)"),
"**Group B** (Advanced and Extra Only)": ("**Any:** KA-KZ, NA-NZ, WA-WZ (2x2)\n"
"*Except*\n"
"**Alaska:** AL (2x2)\n"
"**Caribbean:** KP (2x2)\n"
"**Pacific:** AH (2x2)"),
"**Group C** (Technician, General, Advanced, Extra Only)": ("**Any Region:** K, N, W (1x3)\n"
"*Except*\n"
"**Alaska:** KL, NL, WL (2x2)\n"
"**Caribbean:** NP, WP (2x2)\n"
"**Pacific:** KH, NH, WH (2x2)"),
"**Group D** (Any License Class)": ("**Any Region:** KA-KZ, WA-WZ (2x3)\n"
"*Except*\n"
"**Alaska:** KL, WL (2x3)\n"
"**Caribbean:** KP, WP (2x3)\n"
"**Pacific:** KH, WH (2x3)"),
"**Unavailable**": ("- KA2AA-KA9ZZ: US Army in Japan\n"
"- KC4AAA-KC4AAF: NSF in Antartica\n"
"- KC4USA-KC4USZ: US Navy in Antartica\n"
"- KG4AA-KG4ZZ: US Navy in Guantanamo Bay\n"
"- KL9KAA-KL9KHZ: US military in Korea\n"
"- KC6AA-KC6ZZ: Former US (Eastern and Western Caroline Islands), "
"now Federated States of Micronesia (V6) and Republic of Palau (T8)\n"
"- KX6AA-KX6ZZ: Former US (Marshall Islands), "
"now Republic of the Marshall Islands (V73)\n"
"- Any suffix SOS or QRA-QUZ\n"
"- Any 2x3 with X as the first suffix letter\n"
"- Any 2x3 with AF, KF, NF, or WF prefix and suffix EMA: FEMA\n"
"- Any 2x3 with AA-AL, NA-NZ, WC, WK, WM, WR, or WT prefix: \"Group X\"\n"
"- Any 2x1, 2x2, or 2x3 with KP, NP, WP prefix and 0, 6, 7, 8, 9 number\n"
"- Any 1x1 callsign: Special Event")
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+6 -1
View File
@@ -1,7 +1,12 @@
{
"au": ["au.png", "Australia", "Amateur radio bands in Australia", "", "[Radio Amateur Society of Australia](https://vkradioamateurs.org/its-my-frequency-and-ill-cry-if-i-want-to/) [[PDF]](https://vkradioamateurs.org/wp-content/uploads/2018/05/VK-Band-Plan-Quick-Reference-1.pdf)", "🇦🇺"],
"ca": ["ca.png", "Canada", "Amateur radio bands in Canada", "**This bandplan is incomplete**; some bands, like 630m, are simply not present. It also does not cover any band above 30MHz.", "[RAC 0-30MHz Band Plan](https://www.rac.ca/wp-content/uploads/files/pdf/RAC%20Bandplan%20December%201%202015.pdf)", "🇨🇦"],
"cn": ["cn.png", "China", "Amateur radio bands in China", "", "Created by KN8U and NY7H", "🇨🇳"],
"it_hf": ["it_hf.png", "Italy (HF)", "HF amateur radio bands in Italy", "", "[Associazione Radioamatori Italiani](https://www.arimi.it/download/bandplan/) [[PDF]](http://www.arimi.it/wp-content/Plan/BP%20HF%202013%2032.pdf)", "🇮🇹"],
"it_vhf": ["it_vhf.png", "Italy (VHF/UHF)", "VHF/UHF amateur radio bands in Italy", "", "[Associazione Radioamatori Italiani](https://www.arimi.it/download/bandplan/) [[PDF]](https://www.arimi.it/wp-content/Plan/BP%20V_UHF%202013%2032.pdf)", "🇮🇹"],
"it_shf": ["it_shf.png", "Italy (UHF/SHF)", "UHF/SHF amateur radio bands in Italy", "", "[Associazione Radioamatori Italiani](https://www.arimi.it/download/bandplan/) [[PDF]](https://www.arimi.it/wp-content/Plan/BP%20U-SHF.pdf)", "🇮🇹"],
"jp": ["jp.png", "Japan", "Amateur radio bands in Japan", "Pending checks against the latest update from the 総務省 (MIC) of Japan, March 2020", "[JARL Amateur-Use Bandplans](https://jarl.org/Japanese/A_Shiryo/A-3_Band_Plan/bandplan20150105.pdf)", "🇯🇵"],
"mx": ["mx.png", "Mexico", "Radio allocations in Mexico", "Full radio allocations chart for all services. No information specific to amateur radio is shown.", "Secretaría de Comunicaciones y Transportes (SCT) / Instituto Federal de Telecomunicaciones (IFT)", "🇲🇽"],
"nl": ["nl.png", "Netherlands", "Amateur radio bands in the Netherlands", "", "", "🇳🇱"],
"nl": ["nl.png", "Netherlands", "Amateur radio bands in the Netherlands", "*This version of this bandplan is no longer hosted on VERNON's website and it's accuracy cannot be guaranteed.*", "[VERNON Global Band Plan van de Telecom Agency, Ministerie van Economische Zaken en Klimaat](https://www.veron.nl/downloads/brochures/)", "🇳🇱"],
"us": ["us.png", "USA", "Amateur radio bands in the USA", "", "*[ARRL Frequency Chart](https://www.arrl.org/shop/ARRL-Frequency-Chart-11-17/)* [[PDF]](http://www.arrl.org/files/file/Regulatory/Band%20Chart/Band%20Chart%20-%2011X17%20Color.pdf)", "🇺🇸"]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+7 -3
View File
@@ -1,6 +1,10 @@
{
"arrl": ["arrl-rac.png", "ARRL Sections", "ARRL Sections", "", "", "🇺🇸"],
"rac": ["arrl-rac.png", "RAC Sections", "RAC Sections", "", "", "🇨🇦"],
"ca": ["ca.png", "Canada's Prefixes", "Map of the prefix regions in Canada", "", "[Denelson83 (Wikimedia Commons)](https://commons.wikimedia.org/wiki/File:Amateur_radio_prefixes_in_Canada.svg)", "🇨🇦"],
"cn": ["cn.png", "China's Prefixes", "Map of prefix regions in China", "", "CRAC", "🇨🇳"],
"us": ["us.png", "USA's Prefixes", "Map of prefix regions in the USA", "", "*[ARRL WAS Map](https://www.arrl.org/was-forms)* [[PDF]](http://www.arrl.org/files/file/Awards%20Application%20Forms/WASmap_Color.pdf)", "🇺🇸"]
"us": ["us.png", "USA's Prefixes", "Map of prefix regions in the USA", "", "*[ARRL WAS Map](https://www.arrl.org/was-forms)* [[PDF]](http://www.arrl.org/files/file/Awards%20Application%20Forms/WASmap_Color.pdf)", "🇺🇸"],
"itur": ["itu-regions.png", "ITU Regions", "ITU Regions", "These are also used by the IARU for their regions.", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🇺🇳"],
"ituz": ["itu-zones.png", "ITU Zones", "ITU Zones", "", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🇺🇳"],
"arrl": ["arrl-rac.png", "ARRL Sections", "ARRL Sections", "", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🇺🇸"],
"cq": ["cq-zones.png", "CQ Zones", "CQ Zones", "These are used for the CQWW contest.", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🌐"],
"rac": ["arrl-rac.png", "RAC Sections", "RAC Sections", "", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🇨🇦"]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

+40
View File
@@ -36,3 +36,43 @@ phonetics = {
"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,
}
+119 -44225
View File
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -15,7 +15,10 @@ Settings and options for the bot.
# The prefix for the bot (str). Define a list of stings for multiple prefixes.
# ie: `["?", "!", "pls "]`
prefix = "?"
prefix = ["? ", "?"]
# The prefix to use for display purposes (ex: status message).
display_prefix = "?"
# Whether the bot should print full stacktraces for normal exceptions: `True`,
# or be nice and only print small messages: `False` (the default).
@@ -27,7 +30,7 @@ debug = False
owners_uids = (200102491231092736,)
# The extensions to load when running the bot.
exts = ["ae7q", "base", "fun", "grid", "ham", "image", "lookup", "morse", "qrz", "study", "weather"]
exts = ["ae7q", "base", "fun", "grid", "ham", "image", "lookup", "morse", "qrz", "study", "weather", "dbconv"]
# Either "time", "random", or "fixed" (first item in statuses)
status_mode = "fixed"
@@ -46,6 +49,9 @@ time_statuses = [("with lids on 3.840", (00, 00), (6, 00)),
("with lids on 7.200", (18, 00), (20, 00)),
("with lids on 3.840", (20, 00), (23, 59))]
# append " | {display_prefix}help" to the Discord playing status
show_help = False
# Emoji IDs and keywords for emoji reactions
# Use the format {emoji_id (int): ("tuple", "of", "lowercase", "keywords")}
msg_reacts = {}