Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fed97a03b3 |
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
# vim: ts=2 sw=2:
|
name: Docker Build and Push
|
||||||
|
|
||||||
name: Docker Build and Deploy
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
# Publish `master` as Docker `dev` image.
|
# Publish `master` as Docker `dev` image.
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
# Publish `v*` tags as x.x.x images and as `latest`.
|
# Publish `v*` tags as releases and as `latest`.
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
|
|
||||||
@@ -15,53 +13,46 @@ env:
|
|||||||
IMAGE_NAME: qrm2
|
IMAGE_NAME: qrm2
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
build-and-push:
|
||||||
name: Build and push docker images
|
runs-on: ubuntu-latest
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
ref: ${{ github.ref }}
|
|
||||||
|
|
||||||
- name: Write ref to file
|
steps:
|
||||||
run: git rev-list -n 1 $GITHUB_REF > ./git_commit
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Build image
|
- name: Build image
|
||||||
run: docker build . --file Dockerfile -t $IMAGE_NAME
|
run: |
|
||||||
|
echo ${{ github.sha }} > git_commit
|
||||||
|
docker build . --file Dockerfile -t $IMAGE_NAME
|
||||||
|
|
||||||
- name: Log into Github Package Registry
|
- 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.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin
|
||||||
|
|
||||||
- name: Tag image
|
- name: Log into Docker Hub
|
||||||
id: tag_image
|
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
|
||||||
|
|
||||||
|
- name: Push image to registries
|
||||||
run: |
|
run: |
|
||||||
IMAGE_ID=docker.pkg.github.com/${{ github.repository }}/$IMAGE_NAME
|
GITHUB_IMAGE_ID=docker.pkg.github.com/${{ github.repository }}/$IMAGE_NAME
|
||||||
echo IMAGE_ID=$IMAGE_ID
|
DOCKER_IMAGE_ID=${{ secrets.DOCKER_USERNAME }}/$IMAGE_NAME
|
||||||
echo ::set-output name=image_id::$IMAGE_ID
|
|
||||||
|
|
||||||
# Strip git ref prefix from version
|
# Strip git ref prefix from version
|
||||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||||
|
|
||||||
# Strip "v" prefix from tag name
|
# Strip "v" prefix from tag name
|
||||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||||
# if version is master, set version to dev
|
|
||||||
[[ "$VERSION" == "master" ]] && VERSION=dev
|
[[ "$VERSION" == "master" ]] && VERSION=dev
|
||||||
|
|
||||||
|
echo GITHUB_IMAGE_ID=$GITHUB_IMAGE_ID
|
||||||
|
echo DOCKER_IMAGE_ID=$DOCKER_IMAGE_ID
|
||||||
echo VERSION=$VERSION
|
echo VERSION=$VERSION
|
||||||
echo ::set-output name=version::$VERSION
|
|
||||||
|
|
||||||
# tag dev or x.x.x
|
# tag for Github Registry
|
||||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
docker tag $IMAGE_NAME $GITHUB_IMAGE_ID:$VERSION
|
||||||
# tag latest if not a dev release
|
[[ "$VERSION" != "dev" ]] && docker tag $IMAGE_NAME $GITHUB_IMAGE_ID:latest
|
||||||
[[ "$VERSION" != "dev" ]] && docker tag $IMAGE_NAME $IMAGE_ID:latest || true
|
docker push $GITHUB_IMAGE_ID
|
||||||
|
|
||||||
- name: Push images to registry
|
# tag for Docker Hub
|
||||||
run: docker push ${{ steps.tag_image.outputs.image_id }}
|
docker tag $IMAGE_NAME $DOCKER_IMAGE_ID:$VERSION
|
||||||
|
[[ "$VERSION" != "dev" ]] && docker tag $IMAGE_NAME $DOCKER_IMAGE_ID:latest
|
||||||
- name: Deploy official images
|
docker push $DOCKER_IMAGE_ID
|
||||||
id: deploy_images
|
|
||||||
uses: satak/webrequest-action@v1
|
|
||||||
with:
|
|
||||||
url: ${{ secrets.DEPLOY_URL }}
|
|
||||||
method: POST
|
|
||||||
headers: '{"Authentication": "Token ${{ secrets.DEPLOY_TOKEN }}"}'
|
|
||||||
payload: '{"version": "${{ steps.tag_image.outputs.version }}"}'
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Linting
|
name: Linting
|
||||||
|
|
||||||
on: [push,pull_request]
|
on: [push]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
flake8_py3:
|
flake8_py3:
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
# vim: ts=2 sw=2:
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
# Sequence of patterns matched against refs/tags
|
|
||||||
tags:
|
|
||||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
|
||||||
|
|
||||||
name: Create Release
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
name: Create Release
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
ref: ${{ github.ref }}
|
|
||||||
|
|
||||||
- name: Get Version Info
|
|
||||||
id: get_tag
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
SUBJECT=$(/usr/bin/git tag -l ${GITHUB_REF#refs/tags/} --format='%(subject)')
|
|
||||||
BODY=$(/usr/bin/git tag -l ${GITHUB_REF#refs/tags/} --format='%(body)' | sed '/-----BEGIN PGP SIGNATURE-----/,$d')
|
|
||||||
|
|
||||||
echo "SUBJECT=$SUBJECT"
|
|
||||||
echo "BODY=$BODY"
|
|
||||||
|
|
||||||
echo 'tag_subject<<EOS' >> $GITHUB_ENV
|
|
||||||
echo "$SUBJECT" >> $GITHUB_ENV
|
|
||||||
echo 'EOS' >> $GITHUB_ENV
|
|
||||||
echo 'tag_body<<EOB' >> $GITHUB_ENV
|
|
||||||
echo "$BODY" >> $GITHUB_ENV
|
|
||||||
echo 'EOB' >> $GITHUB_ENV
|
|
||||||
echo "tag_version=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
|
||||||
echo "version_num=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Get Changelog Content
|
|
||||||
id: changelog_reader
|
|
||||||
uses: mindsers/changelog-reader-action@v2
|
|
||||||
with:
|
|
||||||
version: ${{ env.version_num }}
|
|
||||||
path: ./CHANGELOG.md
|
|
||||||
|
|
||||||
- name: Publish Release
|
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: ${{ env.tag_version }}
|
|
||||||
release_name: ${{ env.tag_subject }}
|
|
||||||
body: |
|
|
||||||
${{ env.tag_body }}
|
|
||||||
|
|
||||||
## Changelog
|
|
||||||
|
|
||||||
${{ steps.changelog_reader.outputs.changes }}
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
@@ -9,8 +9,6 @@ cty.zip
|
|||||||
|
|
||||||
/docker-compose.yml
|
/docker-compose.yml
|
||||||
|
|
||||||
.vscode/
|
|
||||||
|
|
||||||
|
|
||||||
#########################################################
|
#########################################################
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
|
|||||||
@@ -7,39 +7,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
|
||||||
## [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
|
## [2.3.1] - 2020-04-02
|
||||||
### Fixed
|
### Fixed
|
||||||
- Wordlist containing innappropriate words.
|
- Wordlist containing innappropriate words.
|
||||||
@@ -138,11 +105,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
## 1.0.0 - 2019-07-31 [YANKED]
|
## 1.0.0 - 2019-07-31 [YANKED]
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.5.0...HEAD
|
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.3.1...HEAD
|
||||||
[2.5.0]: https://github.com/miaowware/qrm2/releases/tag/v2.5.0
|
|
||||||
[2.4.1]: https://github.com/miaowware/qrm2/releases/tag/v2.4.1
|
|
||||||
[2.4.0]: https://github.com/miaowware/qrm2/releases/tag/v2.4.0
|
|
||||||
[2.3.2]: https://github.com/miaowware/qrm2/releases/tag/v2.3.2
|
|
||||||
[2.3.1]: https://github.com/miaowware/qrm2/releases/tag/v2.3.1
|
[2.3.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.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.3]: https://github.com/miaowware/qrm2/releases/tag/v2.2.3
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
# Development Guide for qrm
|
|
||||||
|
|
||||||
**Make sure to also read [`CONTRIBUTING.md`][0], everything in there applies here.**
|
|
||||||
|
|
||||||
### Environment Setup
|
|
||||||
|
|
||||||
1. [Fork this repo][1] into your own GitHub namespace.
|
|
||||||
1. Make sure the `master` branch is up to date, then make yourself a new branch with a descriptive name.
|
|
||||||
1. Once the forked repo is cloned and on the proper branch, you can set up the development environment.
|
|
||||||
1. Install python 3.7 or higher.
|
|
||||||
1. Run `make dev-install`.
|
|
||||||
This should install everything you need to develop and run qrm.
|
|
||||||
1. [Create a bot and token][2], and add it to `data/keys.py`.
|
|
||||||
Also add your [QRZ credentials][3] if needed.
|
|
||||||
1. In `data/options.py`, change values as needed.
|
|
||||||
Some commands require adding your Discord user ID to `owner_uids`.
|
|
||||||
1. To activate the virtual env that was created by `make`, run `source botenv/bin/activate` (or the equivelent for your shell or operating system).
|
|
||||||
|
|
||||||
These instructions are fairly \*NIX-centric, so if you would like to develop on Windows, it is suggested that the Windows Subsystem for Linux be used.
|
|
||||||
|
|
||||||
## While You Develop
|
|
||||||
|
|
||||||
To run qrm, use the command `./run.sh`.
|
|
||||||
We recommend you use the `--pass-errors` flag to avoid perpetual restart loops if you break the bot.
|
|
||||||
It exists because repeatedly mashing [Ctrl+C] at high speed to break a fast loop is not fun.
|
|
||||||
|
|
||||||
Make sure to add [type hints][4] to your code.
|
|
||||||
This is what `mypy` validates in the code.
|
|
||||||
|
|
||||||
Using `dev-notes` for documentation is especially important if you introduce a new json file format (like for maps and bandplans) or to document some development process (like the command to crush the various images in the repository).
|
|
||||||
|
|
||||||
### Test your changes
|
|
||||||
|
|
||||||
In addition to testing functionality, make sure to run `flake8` to ensure your code uses the proper style, and `mypy [files...]` to ensure proper typing.
|
|
||||||
You can also enable them for this project in your IDE if supported.
|
|
||||||
This will give you automatic and continuous linting and type checking.
|
|
||||||
|
|
||||||
### A Note on Style
|
|
||||||
|
|
||||||
qrm tries to keep to PEP 8 style whenever possible.
|
|
||||||
Use the utility `flake8` to check that you follow this style.
|
|
||||||
When you start a PR or push commits, GitHub will automatically run this for you,
|
|
||||||
but we prefer that developers check this before committing and opening PRs.
|
|
||||||
|
|
||||||
Otherwise, try to follow the existing style:
|
|
||||||
- double-quotes except when required to be single,
|
|
||||||
- indentation of mult-line structures matching other examples in the code,
|
|
||||||
- etc.
|
|
||||||
|
|
||||||
[0]: https://github.com/miaowware/.github/blob/master/CONTRIBUTING.md
|
|
||||||
[1]: https://github.com/miaowware/qrm2/fork
|
|
||||||
[2]: https://discordpy.readthedocs.io/en/latest/discord.html
|
|
||||||
[3]: https://www.qrz.com/page/xml_data.html
|
|
||||||
[4]: https://docs.python.org/3/library/typing.html
|
|
||||||
@@ -71,17 +71,6 @@ clean:
|
|||||||
|
|
||||||
|
|
||||||
### Dev targets ###
|
### 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 ###
|
### Special targets ###
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Docker help for qrm2
|
# Docker help for qrm2
|
||||||
|
|
||||||
You have multiple ways to use docker to run an instance of qrm2.
|
You have multiple ways to use docker to run an instance of qrm2
|
||||||
|
|
||||||
- [Docker help for qrm2](#docker-help-for-qrm2)
|
- [Docker help for qrm2](#docker-help-for-qrm2)
|
||||||
- [Using docker-compose and the prebuilt-image (recommended)](#using-docker-compose-and-the-prebuilt-image-recommended)
|
- [Using docker-compose and the prebuilt-image (recommended)](#using-docker-compose-and-the-prebuilt-image-recommended)
|
||||||
@@ -23,14 +23,13 @@ This is the easiest method for running the bot without any modifications.
|
|||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
qrm2:
|
qrm2:
|
||||||
image: "docker.pkg.github.com/miaowware/qrm2/qrm2:latest"
|
image: "classabbyamp/discord-qrm2:latest"
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
volumes:
|
volumes:
|
||||||
- "./data:/app/data:rw"
|
- "./data:/app/data:rw"
|
||||||
environment:
|
environment:
|
||||||
- PYTHONUNBUFFERED=1
|
- 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`.
|
3. Create a subdirectory named `data`.
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ This is the easiest method for running the bot without any modifications.
|
|||||||
$ docker-compose up -d
|
$ docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
*Run without "-d" to test the bot (run in foreground).*
|
> Run without "-d" to test the bot. (run in foreground)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@ This is the easiest method to run the bot with modifications.
|
|||||||
services:
|
services:
|
||||||
qrm2:
|
qrm2:
|
||||||
build: .
|
build: .
|
||||||
image: "qrm2:local-latest"
|
image: "discord-qrm2:local-latest"
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
volumes:
|
volumes:
|
||||||
- "./data:/app/data:rw"
|
- "./data:/app/data:rw"
|
||||||
@@ -76,10 +75,10 @@ This is the easiest method to run the bot with modifications.
|
|||||||
|
|
||||||
```none
|
```none
|
||||||
$ docker-compose build --pull
|
$ docker-compose build --pull
|
||||||
$ docker-compose up -d
|
$ docker-compose -d
|
||||||
```
|
```
|
||||||
|
|
||||||
*Run without "-d" to test the bot (run in foreground).*
|
> Run without "-d" to test the bot. (run in foreground)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -96,7 +95,7 @@ This methods is not very nice to use.
|
|||||||
2. Run docker build:
|
2. Run docker build:
|
||||||
|
|
||||||
```none
|
```none
|
||||||
$ docker build -t qrm2:local-latest .
|
$ docker build -t discord-qrm2:local-latest .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -111,5 +110,5 @@ This methods is not very nice to use.
|
|||||||
```
|
```
|
||||||
|
|
||||||
Where `[image]` is either of:
|
Where `[image]` is either of:
|
||||||
- `qrm2:local-latest` if you are building your own.
|
- `discord-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.
|
- `classabbyamp/discord-qrm2:latest` if you want to use the prebuilt image.
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ Run. For more information on options, see the [quick-bot-no-pain run.sh document
|
|||||||
$ run.sh
|
$ run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Check out the [contribution guidelines](/CONTRIBUTING.md) for more information about how to contribute to this project.
|
|
||||||
|
|
||||||
## Copyright
|
## Copyright
|
||||||
|
|
||||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||||
|
|||||||
@@ -15,13 +15,11 @@ import traceback
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
import discord.ext.commands as commands
|
import discord.ext.commands as commands
|
||||||
from discord import Emoji, Reaction, PartialEmoji
|
|
||||||
|
|
||||||
import data.options as opt
|
import data.options as opt
|
||||||
|
|
||||||
@@ -65,7 +63,6 @@ emojis = SimpleNamespace(
|
|||||||
paths = SimpleNamespace(
|
paths = SimpleNamespace(
|
||||||
data=Path("./data/"),
|
data=Path("./data/"),
|
||||||
resources=Path("./resources/"),
|
resources=Path("./resources/"),
|
||||||
img=Path("./resources/img/"),
|
|
||||||
bandcharts=Path("./resources/img/bandcharts/"),
|
bandcharts=Path("./resources/img/bandcharts/"),
|
||||||
maps=Path("./resources/img/maps/"),
|
maps=Path("./resources/img/maps/"),
|
||||||
)
|
)
|
||||||
@@ -73,7 +70,6 @@ paths = SimpleNamespace(
|
|||||||
|
|
||||||
# --- Classes ---
|
# --- Classes ---
|
||||||
|
|
||||||
|
|
||||||
class ImageMetadata:
|
class ImageMetadata:
|
||||||
"""Represents the metadata of a single image."""
|
"""Represents the metadata of a single image."""
|
||||||
def __init__(self, metadata: list):
|
def __init__(self, metadata: list):
|
||||||
@@ -151,7 +147,7 @@ class GlobalChannelConverter(commands.IDConverter):
|
|||||||
def embed_factory(ctx: commands.Context) -> discord.Embed:
|
def embed_factory(ctx: commands.Context) -> discord.Embed:
|
||||||
"""Creates an embed with neutral colour and standard footer."""
|
"""Creates an embed with neutral colour and standard footer."""
|
||||||
embed = discord.Embed(timestamp=datetime.utcnow(), colour=colours.neutral)
|
embed = discord.Embed(timestamp=datetime.utcnow(), colour=colours.neutral)
|
||||||
embed.set_footer(text=str(ctx.author), icon_url=str(ctx.author.avatar_url))
|
embed.set_footer(text=ctx.author, icon_url=str(ctx.author.avatar_url))
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
|
||||||
@@ -168,12 +164,11 @@ def error_embed_factory(ctx: commands.Context, exception: Exception, debug_mode:
|
|||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
|
||||||
async def add_react(msg: discord.Message, react: Union[Emoji, Reaction, PartialEmoji, str]):
|
async def add_react(msg: discord.Message, react: str):
|
||||||
try:
|
try:
|
||||||
await msg.add_reaction(react)
|
await msg.add_reaction(react)
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
idpath = (f"{msg.guild.id}/" if msg.guild else "") + str(msg.channel.id)
|
print(f"[!!] Missing permissions to add reaction in '{msg.guild.id}/{msg.channel.id}'!")
|
||||||
print(f"[!!] Missing permissions to add reaction in '{idpath}'!")
|
|
||||||
|
|
||||||
|
|
||||||
# --- Checks ---
|
# --- Checks ---
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
# 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,3 +0,0 @@
|
|||||||
-r requirements.txt
|
|
||||||
flake8
|
|
||||||
discord.py-stubs==1.5.0
|
|
||||||
@@ -16,8 +16,10 @@ the GNU General Public License, version 2.
|
|||||||
# KC4USA: reserved, no call history, *but* has application history
|
# KC4USA: reserved, no call history, *but* has application history
|
||||||
|
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from bs4 import BeautifulSoup
|
import ae7qparser
|
||||||
|
|
||||||
import discord.ext.commands as commands
|
import discord.ext.commands as commands
|
||||||
|
|
||||||
@@ -40,64 +42,86 @@ class AE7QCog(commands.Cog):
|
|||||||
"""Looks up the history of a callsign on [ae7q.com](http://ae7q.com/)."""
|
"""Looks up the history of a callsign on [ae7q.com](http://ae7q.com/)."""
|
||||||
with ctx.typing():
|
with ctx.typing():
|
||||||
callsign = callsign.upper()
|
callsign = callsign.upper()
|
||||||
desc = ""
|
|
||||||
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
|
|
||||||
async with self.session.get(base_url + callsign) as resp:
|
call_data = ae7qparser.get_call(callsign)
|
||||||
if resp.status != 200:
|
|
||||||
raise cmn.BotHTTPError(resp)
|
|
||||||
page = await resp.text()
|
|
||||||
|
|
||||||
soup = BeautifulSoup(page, features="html.parser")
|
|
||||||
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
|
|
||||||
|
|
||||||
table = tables[0]
|
|
||||||
|
|
||||||
# find the first table in the page, and use it to make a description
|
|
||||||
if len(table[0]) == 1:
|
|
||||||
for row in table:
|
|
||||||
desc += " ".join(row.getText().split())
|
|
||||||
desc += "\n"
|
|
||||||
desc = desc.replace(callsign, f"`{callsign}`")
|
|
||||||
table = tables[1]
|
|
||||||
|
|
||||||
table_headers = table[0].find_all("th")
|
|
||||||
first_header = "".join(table_headers[0].strings) if len(table_headers) > 0 else None
|
|
||||||
|
|
||||||
# catch if the wrong table was selected
|
|
||||||
if first_header is None or first_header != "Entity Name":
|
|
||||||
embed.title = f"AE7Q History for {callsign}"
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
embed.url = base_url + callsign
|
|
||||||
embed.description = desc
|
|
||||||
embed.description += f"\nNo records found for `{callsign}`"
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
return
|
|
||||||
|
|
||||||
table = await process_table(table[1:])
|
|
||||||
|
|
||||||
embed = cmn.embed_factory(ctx)
|
embed = cmn.embed_factory(ctx)
|
||||||
embed.title = f"AE7Q History for {callsign}"
|
embed.title = f"AE7Q Callsign History for {callsign}"
|
||||||
|
embed.url = call_data.query_url
|
||||||
embed.colour = cmn.colours.good
|
embed.colour = cmn.colours.good
|
||||||
embed.url = base_url + callsign
|
embed.description = ""
|
||||||
|
|
||||||
# add the first three rows of the table to the embed
|
if isinstance(call_data, ae7qparser.Ae7qCallData):
|
||||||
for row in table[0:3]:
|
if call_data.conditions:
|
||||||
header = f"**{row[0]}** ({row[1]})" # **Name** (Applicant Type)
|
embed.description = " ".join([
|
||||||
body = (f"Class: *{row[2]}*\n"
|
" ".join([y.strip() for y in x]) for x in call_data.conditions
|
||||||
f"Region: *{row[3]}*\n"
|
]).replace(callsign, f"`{callsign}`")
|
||||||
f"Status: *{row[4]}*\n"
|
|
||||||
f"Granted: *{row[5]}*\n"
|
if not call_data.call_history:
|
||||||
f"Effective: *{row[6]}*\n"
|
if not call_data.event_callsign_history:
|
||||||
f"Cancelled: *{row[7]}*\n"
|
embed.colour = cmn.colours.bad
|
||||||
f"Expires: *{row[8]}*")
|
embed.description += f"\nNo records found for `{callsign}`"
|
||||||
|
else:
|
||||||
|
# add the first five rows of the event callsign history to the embed
|
||||||
|
for row in call_data.event_callsign_history[0:5]:
|
||||||
|
header = f"**{row.start_date:%Y-%m-%d}-{row.end_date:%Y-%m-%d}**"
|
||||||
|
body = (f"Requestor: *{row.entity_name} ({row.callsign})*\n"
|
||||||
|
f"Event: *{row.event_name}*")
|
||||||
embed.add_field(name=header, value=body, inline=False)
|
embed.add_field(name=header, value=body, inline=False)
|
||||||
|
|
||||||
if len(table) > 3:
|
if len(call_data.event_callsign_history) > 5:
|
||||||
desc += f"\nRecords 1 to 3 of {len(table)}. See ae7q.com for more..."
|
embed.description += (f"\nRecords 1 to 5 of {len(call_data.event_callsign_history)}. "
|
||||||
|
f"See [ae7q.com]({call_data.query_url}) for more...")
|
||||||
|
|
||||||
embed.description = desc
|
else:
|
||||||
|
# add the first three rows of the callsign history to the embed
|
||||||
|
for row in call_data.call_history[0:3]:
|
||||||
|
header = f"**{row.entity_name}** ({row.applicant_type})"
|
||||||
|
body = (f"Class: *{row.operator_class}*\n"
|
||||||
|
f"Region: *{row.region_state}*\n"
|
||||||
|
f"Status: *{row.license_status}*\n")
|
||||||
|
if row.grant_date:
|
||||||
|
body += f"Granted: *{row.grant_date:%Y-%m-%d}*\n"
|
||||||
|
if row.effective_date:
|
||||||
|
body += f"Effective: *{row.effective_date:%Y-%m-%d}*\n"
|
||||||
|
if row.cancel_date:
|
||||||
|
body += f"Cancelled: *{row.cancel_date:%Y-%m-%d}*\n"
|
||||||
|
if row.expire_date:
|
||||||
|
body += f"Expires: *{row.expire_date:%Y-%m-%d}*\n"
|
||||||
|
|
||||||
|
embed.add_field(name=header, value=body, inline=False)
|
||||||
|
|
||||||
|
if len(call_data.call_history) > 3:
|
||||||
|
embed.description += (f"\nRecords 1 to 3 of {len(call_data.call_history)}. "
|
||||||
|
f"See [ae7q.com]({call_data.query_url}) for more...")
|
||||||
|
|
||||||
|
elif isinstance(call_data, ae7qparser.Ae7qCanadianCallData):
|
||||||
|
if not call_data.callsign_data:
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
embed.description += f"\nNo records found for `{callsign}`"
|
||||||
|
else:
|
||||||
|
if call_data.given_names != "" and call_data.surname != "":
|
||||||
|
embed.add_field(name="Name", value=f"{call_data.given_names} {call_data.surname}", inline=True)
|
||||||
|
if call_data.address != "":
|
||||||
|
embed.add_field(name="Address", value=call_data.address, inline=True)
|
||||||
|
if call_data.locality != "":
|
||||||
|
embed.add_field(name="Locality", value=call_data.locality, inline=True)
|
||||||
|
if call_data.province != "":
|
||||||
|
embed.add_field(name="Province", value=call_data.province, inline=True)
|
||||||
|
if call_data.postal_code != "":
|
||||||
|
embed.add_field(name="Postal Code", value=call_data.postal_code, inline=True)
|
||||||
|
if call_data.country != "":
|
||||||
|
embed.add_field(name="Country", value=call_data.country, inline=True)
|
||||||
|
if call_data.region != "":
|
||||||
|
embed.add_field(name="Region", value=call_data.region, inline=True)
|
||||||
|
if call_data.grid_square != "":
|
||||||
|
embed.add_field(name="Grid Square", value=call_data.grid_square, inline=True)
|
||||||
|
if call_data.qualifications != "":
|
||||||
|
embed.add_field(name="License Qualifications", value=call_data.qualifications, inline=True)
|
||||||
|
|
||||||
|
else:
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
embed.description += f"\nNo records found for `{callsign}`"
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@@ -106,202 +130,283 @@ class AE7QCog(commands.Cog):
|
|||||||
"""Looks up the licenses for which a licensee is trustee on [ae7q.com](http://ae7q.com/)."""
|
"""Looks up the licenses for which a licensee is trustee on [ae7q.com](http://ae7q.com/)."""
|
||||||
with ctx.typing():
|
with ctx.typing():
|
||||||
callsign = callsign.upper()
|
callsign = callsign.upper()
|
||||||
desc = ""
|
|
||||||
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
|
|
||||||
async with self.session.get(base_url + callsign) as resp:
|
call_data = ae7qparser.get_call(callsign)
|
||||||
if resp.status != 200:
|
|
||||||
raise cmn.BotHTTPError(resp)
|
|
||||||
page = await resp.text()
|
|
||||||
|
|
||||||
soup = BeautifulSoup(page, features="html.parser")
|
|
||||||
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
|
|
||||||
|
|
||||||
try:
|
|
||||||
table = tables[2] if len(tables[0][0]) == 1 else tables[1]
|
|
||||||
except IndexError:
|
|
||||||
embed.title = f"AE7Q Trustee History for {callsign}"
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
embed.url = base_url + callsign
|
|
||||||
embed.description = desc
|
|
||||||
embed.description += f"\nNo records found for `{callsign}`"
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
return
|
|
||||||
|
|
||||||
table_headers = table[0].find_all("th")
|
|
||||||
first_header = "".join(table_headers[0].strings) if len(table_headers) > 0 else None
|
|
||||||
|
|
||||||
# catch if the wrong table was selected
|
|
||||||
if first_header is None or not first_header.startswith("With"):
|
|
||||||
embed.title = f"AE7Q Trustee History for {callsign}"
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
embed.url = base_url + callsign
|
|
||||||
embed.description = desc
|
|
||||||
embed.description += f"\nNo records found for `{callsign}`"
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
return
|
|
||||||
|
|
||||||
table = await process_table(table[2:])
|
|
||||||
|
|
||||||
embed = cmn.embed_factory(ctx)
|
embed = cmn.embed_factory(ctx)
|
||||||
embed.title = f"AE7Q Trustee History for {callsign}"
|
embed.title = f"AE7Q Trustee History for {callsign}"
|
||||||
|
embed.url = call_data.query_url
|
||||||
embed.colour = cmn.colours.good
|
embed.colour = cmn.colours.good
|
||||||
embed.url = base_url + callsign
|
embed.description = ""
|
||||||
|
|
||||||
|
if isinstance(call_data, ae7qparser.Ae7qCallData):
|
||||||
|
if not call_data.trustee_history:
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
embed.description += f"\nNo records found for `{callsign}`"
|
||||||
|
else:
|
||||||
|
# add the first three rows of the trustee history to the embed
|
||||||
|
for row in call_data.trustee_history[0:3]:
|
||||||
|
header = f"**{row.callsign}** ({row.applicant_type})"
|
||||||
|
body = (f"Name: *{row.entity_name}*\n"
|
||||||
|
f"Region: *{row.region_state}*\n"
|
||||||
|
f"Status: *{row.license_status}*\n")
|
||||||
|
if row.grant_date:
|
||||||
|
body += f"Granted: *{row.grant_date:%Y-%m-%d}*\n"
|
||||||
|
if row.effective_date:
|
||||||
|
body += f"Effective: *{row.effective_date:%Y-%m-%d}*\n"
|
||||||
|
if row.cancel_date:
|
||||||
|
body += f"Cancelled: *{row.cancel_date:%Y-%m-%d}*\n"
|
||||||
|
if row.expire_date:
|
||||||
|
body += f"Expires: *{row.expire_date:%Y-%m-%d}*\n"
|
||||||
|
|
||||||
# add the first three rows of the table to the embed
|
|
||||||
for row in table[0:3]:
|
|
||||||
header = f"**{row[0]}** ({row[3]})" # **Name** (Applicant Type)
|
|
||||||
body = (f"Name: *{row[2]}*\n"
|
|
||||||
f"Region: *{row[1]}*\n"
|
|
||||||
f"Status: *{row[4]}*\n"
|
|
||||||
f"Granted: *{row[5]}*\n"
|
|
||||||
f"Effective: *{row[6]}*\n"
|
|
||||||
f"Cancelled: *{row[7]}*\n"
|
|
||||||
f"Expires: *{row[8]}*")
|
|
||||||
embed.add_field(name=header, value=body, inline=False)
|
embed.add_field(name=header, value=body, inline=False)
|
||||||
|
|
||||||
if len(table) > 3:
|
if len(call_data.trustee_history) > 3:
|
||||||
desc += f"\nRecords 1 to 3 of {len(table)}. See ae7q.com for more..."
|
embed.description += (f"\nRecords 1 to 3 of {len(call_data.trustee_history)}. "
|
||||||
|
f"See [ae7q.com]({call_data.query_url}) for more...")
|
||||||
|
|
||||||
embed.description = desc
|
else:
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
embed.description += f"\nNo records found for `{callsign}`"
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@_ae7q_lookup.command(name="applications", aliases=["a"], category=cmn.cat.lookup)
|
@_ae7q_lookup.command(name="applications", aliases=["a", "apps"], category=cmn.cat.lookup)
|
||||||
async def _ae7q_applications(self, ctx: commands.Context, callsign: str):
|
async def _ae7q_applications(self, ctx: commands.Context, query: str):
|
||||||
"""Looks up the application history for a callsign on [ae7q.com](http://ae7q.com/)."""
|
"""Looks up the application history for a callsign, FRN, or Licensee ID on [ae7q.com](http://ae7q.com/)."""
|
||||||
"""
|
|
||||||
with ctx.typing():
|
with ctx.typing():
|
||||||
callsign = callsign.upper()
|
query = query.upper()
|
||||||
desc = ""
|
|
||||||
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
|
|
||||||
async with self.session.get(base_url + callsign) as resp:
|
# LID
|
||||||
if resp.status != 200:
|
if re.match(r"L\d+", query):
|
||||||
raise cmn.BotHTTPError(resp)
|
data = ae7qparser.get_licensee_id(query)
|
||||||
page = await resp.text()
|
# FRN
|
||||||
|
elif re.match(r"\d{10}", query):
|
||||||
soup = BeautifulSoup(page, features="html.parser")
|
data = ae7qparser.get_frn(query)
|
||||||
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
|
# callsign
|
||||||
|
else:
|
||||||
table = tables[0]
|
data = ae7qparser.get_call(query)
|
||||||
|
|
||||||
# find the first table in the page, and use it to make a description
|
|
||||||
if len(table[0]) == 1:
|
|
||||||
for row in table:
|
|
||||||
desc += " ".join(row.getText().split())
|
|
||||||
desc += "\n"
|
|
||||||
desc = desc.replace(callsign, f"`{callsign}`")
|
|
||||||
|
|
||||||
# select the last table to get applications
|
|
||||||
table = tables[-1]
|
|
||||||
|
|
||||||
table_headers = table[0].find_all("th")
|
|
||||||
first_header = "".join(table_headers[0].strings) if len(table_headers) > 0 else None
|
|
||||||
|
|
||||||
# catch if the wrong table was selected
|
|
||||||
if first_header is None or not first_header.startswith("Receipt"):
|
|
||||||
embed.title = f"AE7Q Application History for {callsign}"
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
embed.url = base_url + callsign
|
|
||||||
embed.description = desc
|
|
||||||
embed.description += f"\nNo records found for `{callsign}`"
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
return
|
|
||||||
|
|
||||||
table = await process_table(table[1:])
|
|
||||||
|
|
||||||
embed = cmn.embed_factory(ctx)
|
embed = cmn.embed_factory(ctx)
|
||||||
embed.title = f"AE7Q Application History for {callsign}"
|
embed.title = f"AE7Q Application History for {query}"
|
||||||
|
embed.url = data.query_url
|
||||||
embed.colour = cmn.colours.good
|
embed.colour = cmn.colours.good
|
||||||
embed.url = base_url + callsign
|
embed.description = ""
|
||||||
|
|
||||||
# add the first three rows of the table to the embed
|
if not data.application_history:
|
||||||
for row in table[0:3]:
|
embed.colour = cmn.colours.bad
|
||||||
header = f"**{row[1]}** ({row[3]})" # **Name** (Callsign)
|
embed.description += f"\nNo records found for `{query}`"
|
||||||
body = (f"Received: *{row[0]}*\n"
|
else:
|
||||||
f"Region: *{row[2]}*\n"
|
# add the first three rows of the app history to the embed
|
||||||
f"Purpose: *{row[5]}*\n"
|
if isinstance(data.application_history, ae7qparser.ApplicationsHistoryTable):
|
||||||
f"Last Action: *{row[7]}*\n"
|
for row in data.application_history[0:3]:
|
||||||
f"Application Status: *{row[8]}*\n")
|
header = f"**{row.uls_file_number[0]}** ({row.receipt_date:%Y-%m-%d})"
|
||||||
|
body = (f"Name: *{row.entity_name}*\n"
|
||||||
|
f"Callsign: *{row.application_callsign}*\n"
|
||||||
|
f"Region: *{row.region_state}*\n"
|
||||||
|
f"Purpose: *{row.application_purpose}*\n")
|
||||||
|
if row.payment_date:
|
||||||
|
body += f"Payment: *{row.payment_date:%Y-%m-%d}*\n"
|
||||||
|
if row.last_action_date:
|
||||||
|
body += f"Last Action: *{row.last_action_date:%Y-%m-%d}*\n"
|
||||||
|
body += f"Status: *{row.application_status}*"
|
||||||
embed.add_field(name=header, value=body, inline=False)
|
embed.add_field(name=header, value=body, inline=False)
|
||||||
|
|
||||||
if len(table) > 3:
|
elif isinstance(data.application_history, ae7qparser.VanityApplicationsHistoryTable):
|
||||||
desc += f"\nRecords 1 to 3 of {len(table)}. See ae7q.com for more..."
|
for row in data.application_history[0:3]:
|
||||||
|
header = f"**{row.uls_file_number[0]}** ({row.receipt_date:%Y-%m-%d})"
|
||||||
|
body = (f"Callsign: *{row.application_callsign}*\n"
|
||||||
|
f"Region: *{row.region_state}*\n"
|
||||||
|
f"Operator Class: *{row.operator_class}*\n"
|
||||||
|
f"Purpose: *{row.application_purpose}*\n")
|
||||||
|
if row.payment_date:
|
||||||
|
body += f"Payment: *{row.payment_date:%Y-%m-%d}*\n"
|
||||||
|
if row.last_action_date:
|
||||||
|
body += f"Last Action: *{row.last_action_date:%Y-%m-%d}*\n"
|
||||||
|
body += f"Status: *{row.application_status}*\n"
|
||||||
|
if row.applied_callsigns:
|
||||||
|
body += (f"Callsign{'s' if len(row.applied_callsigns) > 1 else ''} "
|
||||||
|
f"Applied For: *{', '.join(row.applied_callsigns)}*")
|
||||||
|
embed.add_field(name=header, value=body, inline=False)
|
||||||
|
|
||||||
embed.description = desc
|
if len(data.application_history) > 3:
|
||||||
|
embed.description += (f"\nRecords 1 to 3 of {len(data.application_history)}. "
|
||||||
|
f"See [ae7q.com]({data.query_url}) for more...")
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@_ae7q_lookup.command(name="pending_apps", aliases=["pa"], category=cmn.cat.lookup)
|
||||||
|
async def _ae7q_pending_applications(self, ctx: commands.Context, query: str):
|
||||||
|
"""Looks up the pending applications for a callsign, FRN, or Licensee ID on [ae7q.com](http://ae7q.com/)."""
|
||||||
|
with ctx.typing():
|
||||||
|
query = query.upper()
|
||||||
|
|
||||||
|
# LID
|
||||||
|
if re.match(r"L\d+", query):
|
||||||
|
data = ae7qparser.get_licensee_id(query)
|
||||||
|
# FRN
|
||||||
|
elif re.match(r"\d{10}", query):
|
||||||
|
data = ae7qparser.get_frn(query)
|
||||||
|
# callsign
|
||||||
|
else:
|
||||||
|
data = ae7qparser.get_call(query)
|
||||||
|
|
||||||
|
embed = cmn.embed_factory(ctx)
|
||||||
|
embed.title = f"AE7Q Pending Applications for {query}"
|
||||||
|
embed.url = data.query_url
|
||||||
|
embed.colour = cmn.colours.good
|
||||||
|
embed.description = ""
|
||||||
|
|
||||||
|
if not data.pending_applications:
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
embed.description += f"\nNo records found for `{query}`"
|
||||||
|
else:
|
||||||
|
# add the first three rows of the pending apps to the embed
|
||||||
|
if isinstance(data.pending_applications, ae7qparser.PendingApplicationsPredictionsTable):
|
||||||
|
for row in data.pending_applications[0:3]:
|
||||||
|
header = f"**{row.uls_file_number}** ({row.receipt_date:%Y-%m-%d})"
|
||||||
|
body = (f"Callsign: *{row.applicant_callsign}*\n"
|
||||||
|
f"Region: *{row.region_state}*\n"
|
||||||
|
f"Operator Class: *{row.operator_class}*\n"
|
||||||
|
f"Vanity Type: *{row.vanity_type}*\n")
|
||||||
|
if row.process_date:
|
||||||
|
body += f"Processes On: *{row.process_date:%Y-%m-%d}*\n"
|
||||||
|
body += (f"Sequential #: *{row.sequential_number}*\n"
|
||||||
|
f"Vanity Callsign: *{row.vanity_callsign}*\n"
|
||||||
|
f"Prediction: *{row.prediction}*")
|
||||||
|
embed.add_field(name=header, value=body, inline=False)
|
||||||
|
|
||||||
|
elif isinstance(data.pending_applications, ae7qparser.CallsignPendingApplicationsPredictionsTable):
|
||||||
|
for row in data.pending_applications[0:3]:
|
||||||
|
header = f"**{row.uls_file_number}** ({row.receipt_date:%Y-%m-%d})"
|
||||||
|
body = (f"Callsign: *{row.applicant_callsign}*\n"
|
||||||
|
f"Region: *{row.region_state}*\n"
|
||||||
|
f"Operator Class: *{row.operator_class}*\n"
|
||||||
|
f"Vanity Type: *{row.vanity_type}*\n")
|
||||||
|
if row.process_date:
|
||||||
|
body += f"Processes On: *{row.process_date:%Y-%m-%d}*\n"
|
||||||
|
body += (f"Sequential #: *{row.sequential_number}*\n"
|
||||||
|
f"Prediction: *{row.prediction}*")
|
||||||
|
embed.add_field(name=header, value=body, inline=False)
|
||||||
|
|
||||||
|
if len(data.pending_applications) > 3:
|
||||||
|
embed.description += (f"\nRecords 1 to 3 of {len(data.pending_applications)}. "
|
||||||
|
f"See [ae7q.com]({data.query_url}) for more...")
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@_ae7q_lookup.command(name="app_detail", aliases=["ad"], category=cmn.cat.lookup)
|
||||||
|
async def _ae7q_app_detail(self, ctx: commands.Context, ufn: str):
|
||||||
|
"""Looks up the application data for a ULS file number on [ae7q.com](http://ae7q.com/)."""
|
||||||
|
with ctx.typing():
|
||||||
|
app_data = ae7qparser.get_application(ufn)
|
||||||
|
|
||||||
|
embed = cmn.embed_factory(ctx)
|
||||||
|
embed.title = f"AE7Q Application Data for {ufn}"
|
||||||
|
embed.url = app_data.query_url
|
||||||
|
embed.colour = cmn.colours.good
|
||||||
|
|
||||||
|
if not app_data.application_data:
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
embed.description += f"\nNo records found for `{ufn}`"
|
||||||
|
else:
|
||||||
|
if app_data.frn:
|
||||||
|
embed.add_field(name="FRN", value=app_data.frn, inline=True)
|
||||||
|
if app_data.licensee_id:
|
||||||
|
embed.add_field(name="Licensee ID", value=app_data.licensee_id, inline=True)
|
||||||
|
if app_data.applicant_type:
|
||||||
|
embed.add_field(name="Applicant Type", value=app_data.applicant_type, inline=True)
|
||||||
|
if app_data.entity_type:
|
||||||
|
embed.add_field(name="Entity Type", value=app_data.entity_type, inline=True)
|
||||||
|
if app_data.entity_name:
|
||||||
|
embed.add_field(name="Name", value=app_data.entity_name, inline=True)
|
||||||
|
if app_data.callsign:
|
||||||
|
embed.add_field(name="Callsign", value=app_data.callsign, inline=True)
|
||||||
|
|
||||||
|
address = f"ATTN: {app_data.attention}\n" if app_data.attention else ""
|
||||||
|
address += f"{app_data.street_address}\n" if app_data.street_address else ""
|
||||||
|
address += f"{app_data.po_box}\n" if app_data.po_box else ""
|
||||||
|
address += (f"{', '.join([app_data.locality, app_data.state.split('-')[0].strip()])}"
|
||||||
|
f" {app_data.postal_code}")
|
||||||
|
if address:
|
||||||
|
embed.add_field(name="Address", value=address, inline=True)
|
||||||
|
if app_data.county:
|
||||||
|
embed.add_field(name="County", value=app_data.county, inline=True)
|
||||||
|
if app_data.maidenhead:
|
||||||
|
embed.add_field(name="Grid Square", value=app_data.maidenhead, inline=True)
|
||||||
|
if app_data.uls_geo_region:
|
||||||
|
embed.add_field(name="Region", value=app_data.uls_geo_region, inline=True)
|
||||||
|
if app_data.last_action_date:
|
||||||
|
embed.add_field(name="Last Action", value=f"{app_data.last_action_date:%Y-%m-%d}", inline=True)
|
||||||
|
if app_data.receipt_date:
|
||||||
|
embed.add_field(name="Receipt Date", value=f"{app_data.receipt_date:%Y-%m-%d}", inline=True)
|
||||||
|
if app_data.entered_timestamp:
|
||||||
|
embed.add_field(name="Entered Time", value=f"{app_data.entered_timestamp:%Y-%m-%d}", inline=True)
|
||||||
|
if app_data.application_source:
|
||||||
|
embed.add_field(name="Application Source", value=app_data.application_source, inline=True)
|
||||||
|
if app_data.application_purpose:
|
||||||
|
embed.add_field(name="Purpose", value=app_data.application_purpose, inline=True)
|
||||||
|
if app_data.result:
|
||||||
|
embed.add_field(name="Result", value=app_data.result, inline=True)
|
||||||
|
if app_data.fee_control_number:
|
||||||
|
embed.add_field(name="Fee Control Number", value=app_data.fee_control_number, inline=True)
|
||||||
|
if app_data.payment_date:
|
||||||
|
embed.add_field(name="Payment Date", value=f"{app_data.payment_date:%Y-%m-%d}", inline=True)
|
||||||
|
if app_data.operator_class:
|
||||||
|
embed.add_field(name="Operator Class", value=app_data.operator_class, inline=True)
|
||||||
|
if app_data.operator_group:
|
||||||
|
embed.add_field(name="Operator Group", value=app_data.operator_group, inline=True)
|
||||||
|
if app_data.uls_group:
|
||||||
|
embed.add_field(name="ULS Group", value=app_data.uls_group, inline=True)
|
||||||
|
if app_data.vanity_type:
|
||||||
|
embed.add_field(name="Vanity Type", value=app_data.vanity_type, inline=True)
|
||||||
|
if app_data.vanity_relationship:
|
||||||
|
embed.add_field(name="Vanity Relationship", value=app_data.vanity_relationship, inline=True)
|
||||||
|
if app_data.trustee_name and app_data.trustee_callsign:
|
||||||
|
embed.add_field(name="Trustee",
|
||||||
|
value=f"{app_data.trustee_name} ({app_data.trustee_callsign})",
|
||||||
|
inline=True)
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
"""
|
|
||||||
raise NotImplementedError("Application history lookup not yet supported. "
|
|
||||||
"Check back in a later version of the bot.")
|
|
||||||
|
|
||||||
@_ae7q_lookup.command(name="frn", aliases=["f"], category=cmn.cat.lookup)
|
@_ae7q_lookup.command(name="frn", aliases=["f"], category=cmn.cat.lookup)
|
||||||
async def _ae7q_frn(self, ctx: commands.Context, frn: str):
|
async def _ae7q_frn(self, ctx: commands.Context, frn: str):
|
||||||
"""Looks up the history of an FRN on [ae7q.com](http://ae7q.com/)."""
|
"""Looks up the history of an FRN on [ae7q.com](http://ae7q.com/)."""
|
||||||
"""
|
|
||||||
NOTES:
|
|
||||||
- 2 tables: callsign history and application history
|
|
||||||
- If not found: no tables
|
|
||||||
"""
|
|
||||||
with ctx.typing():
|
with ctx.typing():
|
||||||
base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN="
|
frn = frn.upper()
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
|
|
||||||
async with self.session.get(base_url + frn) as resp:
|
frn_data = ae7qparser.get_frn(frn)
|
||||||
if resp.status != 200:
|
|
||||||
raise cmn.BotHTTPError(resp)
|
|
||||||
page = await resp.text()
|
|
||||||
|
|
||||||
soup = BeautifulSoup(page, features="html.parser")
|
|
||||||
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
|
|
||||||
|
|
||||||
if not len(tables):
|
|
||||||
embed.title = f"AE7Q History for FRN {frn}"
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
embed.url = base_url + frn
|
|
||||||
embed.description = f"No records found for FRN `{frn}`"
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
return
|
|
||||||
|
|
||||||
table = tables[0]
|
|
||||||
|
|
||||||
table_headers = table[0].find_all("th")
|
|
||||||
first_header = "".join(table_headers[0].strings) if len(table_headers) > 0 else None
|
|
||||||
|
|
||||||
# catch if the wrong table was selected
|
|
||||||
if first_header is None or not first_header.startswith("With Licensee"):
|
|
||||||
embed.title = f"AE7Q History for FRN {frn}"
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
embed.url = base_url + frn
|
|
||||||
embed.description = f"No records found for FRN `{frn}`"
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
return
|
|
||||||
|
|
||||||
table = await process_table(table[2:])
|
|
||||||
|
|
||||||
embed = cmn.embed_factory(ctx)
|
embed = cmn.embed_factory(ctx)
|
||||||
embed.title = f"AE7Q History for FRN {frn}"
|
embed.title = f"AE7Q FRN History for {frn}"
|
||||||
|
embed.url = frn_data.query_url
|
||||||
embed.colour = cmn.colours.good
|
embed.colour = cmn.colours.good
|
||||||
embed.url = base_url + frn
|
embed.description = ""
|
||||||
|
|
||||||
# add the first three rows of the table to the embed
|
if not frn_data.frn_history:
|
||||||
for row in table[0:3]:
|
embed.colour = cmn.colours.bad
|
||||||
header = f"**{row[0]}** ({row[3]})" # **Callsign** (Applicant Type)
|
embed.description += f"\nNo records found for `{frn}`"
|
||||||
body = (f"Name: *{row[2]}*\n"
|
else:
|
||||||
f"Class: *{row[4]}*\n"
|
# add the first three rows of the FRN history to the embed
|
||||||
f"Region: *{row[1]}*\n"
|
for row in frn_data.frn_history[0:3]:
|
||||||
f"Status: *{row[5]}*\n"
|
header = f"**{row.callsign}** ({row.applicant_type})"
|
||||||
f"Granted: *{row[6]}*\n"
|
body = (f"Name: *{row.entity_name}*\n"
|
||||||
f"Effective: *{row[7]}*\n"
|
f"Region: *{row.region_state}*\n"
|
||||||
f"Cancelled: *{row[8]}*\n"
|
f"Operator Class: *{row.operator_class}*\n"
|
||||||
f"Expires: *{row[9]}*")
|
f"Status: *{row.license_status}*\n")
|
||||||
|
if row.grant_date:
|
||||||
|
body += f"Granted: *{row.grant_date:%Y-%m-%d}*\n"
|
||||||
|
if row.effective_date:
|
||||||
|
body += f"Effective: *{row.effective_date:%Y-%m-%d}*\n"
|
||||||
|
if row.cancel_date:
|
||||||
|
body += f"Cancelled: *{row.cancel_date:%Y-%m-%d}*\n"
|
||||||
|
if row.expire_date:
|
||||||
|
body += f"Expires: *{row.expire_date:%Y-%m-%d}*\n"
|
||||||
embed.add_field(name=header, value=body, inline=False)
|
embed.add_field(name=header, value=body, inline=False)
|
||||||
|
|
||||||
if len(table) > 3:
|
if len(frn_data.frn_history) > 3:
|
||||||
embed.description = f"Records 1 to 3 of {len(table)}. See ae7q.com for more..."
|
embed.description += (f"\nRecords 1 to 3 of {len(frn_data.frn_history)}. "
|
||||||
|
f"See [ae7q.com]({frn_data.query_url}) for more...")
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@@ -310,87 +415,43 @@ class AE7QCog(commands.Cog):
|
|||||||
"""Looks up the history of a licensee ID on [ae7q.com](http://ae7q.com/)."""
|
"""Looks up the history of a licensee ID on [ae7q.com](http://ae7q.com/)."""
|
||||||
with ctx.typing():
|
with ctx.typing():
|
||||||
licensee_id = licensee_id.upper()
|
licensee_id = licensee_id.upper()
|
||||||
base_url = "http://ae7q.com/query/data/LicenseeIdHistory.php?ID="
|
|
||||||
embed = cmn.embed_factory(ctx)
|
|
||||||
|
|
||||||
async with self.session.get(base_url + licensee_id) as resp:
|
lid_data = ae7qparser.get_licensee_id(licensee_id)
|
||||||
if resp.status != 200:
|
|
||||||
raise cmn.BotHTTPError(resp)
|
|
||||||
page = await resp.text()
|
|
||||||
|
|
||||||
soup = BeautifulSoup(page, features="html.parser")
|
|
||||||
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
|
|
||||||
|
|
||||||
if not len(tables):
|
|
||||||
embed.title = f"AE7Q History for Licensee {licensee_id}"
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
embed.url = base_url + licensee_id
|
|
||||||
embed.description = f"No records found for Licensee `{licensee_id}`"
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
return
|
|
||||||
|
|
||||||
table = tables[0]
|
|
||||||
|
|
||||||
table_headers = table[0].find_all("th")
|
|
||||||
first_header = "".join(table_headers[0].strings) if len(table_headers) > 0 else None
|
|
||||||
|
|
||||||
# catch if the wrong table was selected
|
|
||||||
if first_header is None or not first_header.startswith("With FCC"):
|
|
||||||
embed.title = f"AE7Q History for Licensee {licensee_id}"
|
|
||||||
embed.colour = cmn.colours.bad
|
|
||||||
embed.url = base_url + licensee_id
|
|
||||||
embed.description = f"No records found for Licensee `{licensee_id}`"
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
return
|
|
||||||
|
|
||||||
table = await process_table(table[2:])
|
|
||||||
|
|
||||||
embed = cmn.embed_factory(ctx)
|
embed = cmn.embed_factory(ctx)
|
||||||
embed.title = f"AE7Q History for Licensee {licensee_id}"
|
embed.title = f"AE7Q History for Licensee {licensee_id}"
|
||||||
|
embed.url = lid_data.query_url
|
||||||
embed.colour = cmn.colours.good
|
embed.colour = cmn.colours.good
|
||||||
embed.url = base_url + licensee_id
|
embed.description = ""
|
||||||
|
|
||||||
|
if not lid_data.licensee_id_history:
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
|
embed.description += f"\nNo records found for `{licensee_id}`"
|
||||||
|
else:
|
||||||
|
# add the first three rows of the FRN history to the embed
|
||||||
|
for row in lid_data.licensee_id_history[0:3]:
|
||||||
|
header = f"**{row.callsign}** ({row.applicant_type})"
|
||||||
|
body = (f"Name: *{row.entity_name}*\n"
|
||||||
|
f"Region: *{row.region_state}*\n"
|
||||||
|
f"Operator Class: *{row.operator_class}*\n"
|
||||||
|
f"Status: *{row.license_status}*\n")
|
||||||
|
if row.grant_date:
|
||||||
|
body += f"Granted: *{row.grant_date:%Y-%m-%d}*\n"
|
||||||
|
if row.effective_date:
|
||||||
|
body += f"Effective: *{row.effective_date:%Y-%m-%d}*\n"
|
||||||
|
if row.cancel_date:
|
||||||
|
body += f"Cancelled: *{row.cancel_date:%Y-%m-%d}*\n"
|
||||||
|
if row.expire_date:
|
||||||
|
body += f"Expires: *{row.expire_date:%Y-%m-%d}*\n"
|
||||||
|
|
||||||
# add the first three rows of the table to the embed
|
|
||||||
for row in table[0:3]:
|
|
||||||
header = f"**{row[0]}** ({row[3]})" # **Callsign** (Applicant Type)
|
|
||||||
body = (f"Name: *{row[2]}*\n"
|
|
||||||
f"Class: *{row[4]}*\n"
|
|
||||||
f"Region: *{row[1]}*\n"
|
|
||||||
f"Status: *{row[5]}*\n"
|
|
||||||
f"Granted: *{row[6]}*\n"
|
|
||||||
f"Effective: *{row[7]}*\n"
|
|
||||||
f"Cancelled: *{row[8]}*\n"
|
|
||||||
f"Expires: *{row[9]}*")
|
|
||||||
embed.add_field(name=header, value=body, inline=False)
|
embed.add_field(name=header, value=body, inline=False)
|
||||||
|
|
||||||
if len(table) > 3:
|
if len(lid_data.licensee_id_history) > 3:
|
||||||
embed.description = f"Records 1 to 3 of {len(table)}. See ae7q.com for more..."
|
embed.description += (f"\nRecords 1 to 3 of {len(lid_data.licensee_id_history)}. "
|
||||||
|
f"See [ae7q.com]({lid_data.query_url}) for more...")
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
async def process_table(table: list):
|
|
||||||
"""Processes tables (*not* including headers) and returns the processed table"""
|
|
||||||
table_contents = []
|
|
||||||
for tr in table:
|
|
||||||
row = []
|
|
||||||
for td in tr.find_all("td"):
|
|
||||||
cell_val = td.getText().strip()
|
|
||||||
row.append(cell_val if cell_val else "-")
|
|
||||||
|
|
||||||
# take care of columns that span multiple rows by copying the contents rightward
|
|
||||||
if "colspan" in td.attrs and int(td.attrs["colspan"]) > 1:
|
|
||||||
for i in range(int(td.attrs["colspan"]) - 1):
|
|
||||||
row.append(row[-1])
|
|
||||||
|
|
||||||
# get rid of ditto marks by copying the contents from the previous row
|
|
||||||
for i, cell in enumerate(row):
|
|
||||||
if cell == "\"":
|
|
||||||
row[i] = table_contents[-1][i]
|
|
||||||
# add row to table
|
|
||||||
table_contents += [row]
|
|
||||||
return table_contents
|
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: commands.Bot):
|
def setup(bot: commands.Bot):
|
||||||
bot.add_cog(AE7QCog(bot))
|
bot.add_cog(AE7QCog(bot))
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ class QrmHelpCommand(commands.HelpCommand):
|
|||||||
if parent:
|
if parent:
|
||||||
fmt = f"{parent} {fmt}"
|
fmt = f"{parent} {fmt}"
|
||||||
alias = fmt
|
alias = fmt
|
||||||
return f"{opt.display_prefix}{alias} {command.signature}\n *Aliases:* {aliases}"
|
return f"{opt.prefix}{alias} {command.signature}\n *Aliases:* {aliases}"
|
||||||
alias = command.name if not parent else f"{parent} {command.name}"
|
alias = command.name if not parent else f"{parent} {command.name}"
|
||||||
return f"{opt.display_prefix}{alias} {command.signature}"
|
return f"{opt.prefix}{alias} {command.signature}"
|
||||||
|
|
||||||
async def send_error_message(self, error):
|
async def send_error_message(self, error):
|
||||||
embed = cmn.embed_factory(self.context)
|
embed = cmn.embed_factory(self.context)
|
||||||
@@ -60,7 +60,7 @@ class QrmHelpCommand(commands.HelpCommand):
|
|||||||
async def send_bot_help(self, mapping):
|
async def send_bot_help(self, mapping):
|
||||||
embed = cmn.embed_factory(self.context)
|
embed = cmn.embed_factory(self.context)
|
||||||
embed.title = "qrm Help"
|
embed.title = "qrm Help"
|
||||||
embed.description = (f"For command-specific help and usage, use `{opt.display_prefix}help [command name]`."
|
embed.description = (f"For command-specific help and usage, use `{opt.prefix}help [command name]`."
|
||||||
" Many commands have shorter aliases.")
|
" Many commands have shorter aliases.")
|
||||||
mapping = await mapping
|
mapping = await mapping
|
||||||
|
|
||||||
@@ -108,6 +108,7 @@ class BaseCog(commands.Cog):
|
|||||||
embed = cmn.embed_factory(ctx)
|
embed = cmn.embed_factory(ctx)
|
||||||
embed.title = "About qrm"
|
embed.title = "About qrm"
|
||||||
embed.description = info.description
|
embed.description = info.description
|
||||||
|
|
||||||
embed.add_field(name="Authors", value=", ".join(info.authors))
|
embed.add_field(name="Authors", value=", ".join(info.authors))
|
||||||
embed.add_field(name="License", value=info.license)
|
embed.add_field(name="License", value=info.license)
|
||||||
embed.add_field(name="Version", value=f"v{info.release}")
|
embed.add_field(name="Version", value=f"v{info.release}")
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
"""
|
|
||||||
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)
|
|
||||||
@@ -10,7 +10,6 @@ the GNU General Public License, version 2.
|
|||||||
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
import discord
|
|
||||||
import discord.ext.commands as commands
|
import discord.ext.commands as commands
|
||||||
|
|
||||||
import common as cmn
|
import common as cmn
|
||||||
@@ -19,7 +18,7 @@ import common as cmn
|
|||||||
class FunCog(commands.Cog):
|
class FunCog(commands.Cog):
|
||||||
def __init__(self, bot: commands.Bot):
|
def __init__(self, bot: commands.Bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
with open(cmn.paths.resources / "words") as words_file:
|
with open("resources/words") as words_file:
|
||||||
self.words = words_file.read().lower().splitlines()
|
self.words = words_file.read().lower().splitlines()
|
||||||
|
|
||||||
@commands.command(name="xkcd", aliases=["x"], category=cmn.cat.fun)
|
@commands.command(name="xkcd", aliases=["x"], category=cmn.cat.fun)
|
||||||
@@ -37,16 +36,6 @@ class FunCog(commands.Cog):
|
|||||||
"""Returns xkcd: Standards."""
|
"""Returns xkcd: Standards."""
|
||||||
await ctx.send("http://xkcd.com/927")
|
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)
|
@commands.command(name="xd", hidden=True, category=cmn.cat.fun)
|
||||||
async def _xd(self, ctx: commands.Context):
|
async def _xd(self, ctx: commands.Context):
|
||||||
"""ecks dee"""
|
"""ecks dee"""
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ from resources import qcodes
|
|||||||
class HamCog(commands.Cog):
|
class HamCog(commands.Cog):
|
||||||
def __init__(self, bot: commands.Bot):
|
def __init__(self, bot: commands.Bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.pfxs = callsign_info.options
|
|
||||||
|
|
||||||
@commands.command(name="qcode", aliases=["q"], category=cmn.cat.ref)
|
@commands.command(name="qcode", aliases=["q"], category=cmn.cat.ref)
|
||||||
async def _qcode_lookup(self, ctx: commands.Context, qcode: str):
|
async def _qcode_lookup(self, ctx: commands.Context, qcode: str):
|
||||||
@@ -65,26 +64,22 @@ class HamCog(commands.Cog):
|
|||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@commands.command(name="prefixes", aliases=["vanity", "pfx", "vanities", "prefix"], category=cmn.cat.ref)
|
@commands.command(name="prefixes", aliases=["vanity", "pfx", "vanities", "prefix"], category=cmn.cat.ref)
|
||||||
async def _vanity_prefixes(self, ctx: commands.Context, country: str = ""):
|
async def _vanity_prefixes(self, ctx: commands.Context, country: str = None):
|
||||||
"""Lists valid callsign prefixes for different countries."""
|
"""Lists valid callsign prefixes for different countries."""
|
||||||
country = country.lower()
|
if country is None:
|
||||||
embed = cmn.embed_factory(ctx)
|
await ctx.send_help(ctx.command)
|
||||||
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
|
return
|
||||||
|
embed = cmn.embed_factory(ctx)
|
||||||
|
if country.lower() not in callsign_info.options:
|
||||||
|
embed.title = f"{country} not found!"
|
||||||
|
embed.description = f"Valid countries: {', '.join(callsign_info.options.keys())}"
|
||||||
|
embed.colour = cmn.colours.bad
|
||||||
else:
|
else:
|
||||||
data = self.pfxs[country]
|
embed.title = callsign_info.options[country.lower()][0]
|
||||||
embed.title = data.title + (" " + data.emoji if data.emoji else "")
|
embed.description = callsign_info.options[country.lower()][1]
|
||||||
embed.description = data.desc
|
|
||||||
embed.colour = cmn.colours.good
|
embed.colour = cmn.colours.good
|
||||||
|
|
||||||
for name, val in data.calls.items():
|
for name, val in callsign_info.options[country.lower()][2].items():
|
||||||
embed.add_field(name=name, value=val, inline=False)
|
embed.add_field(name=name, value=val, inline=False)
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ the GNU General Public License, version 2.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import io
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
@@ -83,11 +85,16 @@ class ImageCog(commands.Cog):
|
|||||||
@commands.command(name="grayline", aliases=["greyline", "grey", "gray", "gl"], category=cmn.cat.maps)
|
@commands.command(name="grayline", aliases=["greyline", "grey", "gray", "gl"], category=cmn.cat.maps)
|
||||||
async def _grayline(self, ctx: commands.Context):
|
async def _grayline(self, ctx: commands.Context):
|
||||||
"""Gets a map of the current greyline, where HF propagation is the best."""
|
"""Gets a map of the current greyline, where HF propagation is the best."""
|
||||||
|
async with ctx.typing():
|
||||||
embed = cmn.embed_factory(ctx)
|
embed = cmn.embed_factory(ctx)
|
||||||
embed.title = "Current Greyline Conditions"
|
embed.title = "Current Greyline Conditions"
|
||||||
embed.colour = cmn.colours.good
|
embed.colour = cmn.colours.good
|
||||||
embed.set_image(url=self.gl_url)
|
async with self.session.get(self.gl_url) as resp:
|
||||||
await ctx.send(embed=embed)
|
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 setup(bot: commands.Bot):
|
def setup(bot: commands.Bot):
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ the GNU General Public License, version 2.
|
|||||||
|
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from ctyparser import BigCty
|
from ctyparser import BigCty
|
||||||
|
|
||||||
@@ -18,14 +17,11 @@ from discord.ext import commands, tasks
|
|||||||
import common as cmn
|
import common as cmn
|
||||||
|
|
||||||
|
|
||||||
cty_path = Path("./data/cty.json")
|
|
||||||
|
|
||||||
|
|
||||||
class LookupCog(commands.Cog):
|
class LookupCog(commands.Cog):
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
try:
|
try:
|
||||||
self.cty = BigCty(cty_path)
|
self.cty = BigCty("./data/cty.json")
|
||||||
except OSError:
|
except OSError:
|
||||||
self.cty = BigCty()
|
self.cty = BigCty()
|
||||||
|
|
||||||
@@ -71,7 +67,7 @@ class LookupCog(commands.Cog):
|
|||||||
|
|
||||||
@tasks.loop(hours=24)
|
@tasks.loop(hours=24)
|
||||||
async def _update_cty(self):
|
async def _update_cty(self):
|
||||||
update = threading.Thread(target=run_update, args=(self.cty, cty_path))
|
update = threading.Thread(target=run_update, args=(self.cty, "./data/cty.json"))
|
||||||
update.start()
|
update.start()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -146,15 +146,15 @@ def qrz_process_info(data: dict):
|
|||||||
if address == "":
|
if address == "":
|
||||||
address = None
|
address = None
|
||||||
if "eqsl" in data:
|
if "eqsl" in data:
|
||||||
eqsl = "Yes" if data["eqsl"] == "1" else "No"
|
eqsl = "Yes" if data["eqsl"] == 1 else "No"
|
||||||
else:
|
else:
|
||||||
eqsl = "Unknown"
|
eqsl = "Unknown"
|
||||||
if "mqsl" in data:
|
if "mqsl" in data:
|
||||||
mqsl = "Yes" if data["mqsl"] == "1" else "No"
|
mqsl = "Yes" if data["mqsl"] == 1 else "No"
|
||||||
else:
|
else:
|
||||||
mqsl = "Unknown"
|
mqsl = "Unknown"
|
||||||
if "lotw" in data:
|
if "lotw" in data:
|
||||||
lotw = "Yes" if data["lotw"] == "1" else "No"
|
lotw = "Yes" if data["lotw"] == 1 else "No"
|
||||||
else:
|
else:
|
||||||
lotw = "Unknown"
|
lotw = "Unknown"
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ the GNU General Public License, version 2.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import io
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
|
import discord
|
||||||
import discord.ext.commands as commands
|
import discord.ext.commands as commands
|
||||||
|
|
||||||
import common as cmn
|
import common as cmn
|
||||||
@@ -27,11 +29,16 @@ class WeatherCog(commands.Cog):
|
|||||||
@commands.command(name="bandconditions", aliases=["cond", "condx", "conditions"], category=cmn.cat.weather)
|
@commands.command(name="bandconditions", aliases=["cond", "condx", "conditions"], category=cmn.cat.weather)
|
||||||
async def _band_conditions(self, ctx: commands.Context):
|
async def _band_conditions(self, ctx: commands.Context):
|
||||||
"""Gets a solar conditions report."""
|
"""Gets a solar conditions report."""
|
||||||
|
async with ctx.typing():
|
||||||
embed = cmn.embed_factory(ctx)
|
embed = cmn.embed_factory(ctx)
|
||||||
embed.title = "Current Solar Conditions"
|
embed.title = "Current Solar Conditions"
|
||||||
embed.colour = cmn.colours.good
|
embed.colour = cmn.colours.good
|
||||||
embed.set_image(url="http://www.hamqsl.com/solarsun.php")
|
async with self.session.get("http://www.hamqsl.com/solarsun.php") as resp:
|
||||||
await ctx.send(embed=embed)
|
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)
|
@commands.group(name="weather", aliases=["wttr"], case_insensitive=True, category=cmn.cat.weather)
|
||||||
async def _weather_conditions(self, ctx: commands.Context):
|
async def _weather_conditions(self, ctx: commands.Context):
|
||||||
@@ -54,6 +61,7 @@ class WeatherCog(commands.Cog):
|
|||||||
async def _weather_conditions_forecast(self, ctx: commands.Context, *, location: str):
|
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/).
|
"""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."""
|
See help of the `weather` command for possible location types and options."""
|
||||||
|
async with ctx.typing():
|
||||||
try:
|
try:
|
||||||
units_arg = re.search(self.wttr_units_regex, location).group(1)
|
units_arg = re.search(self.wttr_units_regex, location).group(1)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -73,13 +81,18 @@ class WeatherCog(commands.Cog):
|
|||||||
embed.colour = cmn.colours.good
|
embed.colour = cmn.colours.good
|
||||||
|
|
||||||
loc = loc.replace(" ", "+")
|
loc = loc.replace(" ", "+")
|
||||||
embed.set_image(url=f"http://wttr.in/{loc}_{units}pnFQ.png")
|
async with self.session.get(f"http://wttr.in/{loc}_{units}pnFQ.png") as resp:
|
||||||
await ctx.send(embed=embed)
|
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)
|
@_weather_conditions.command(name="now", aliases=["n"], category=cmn.cat.weather)
|
||||||
async def _weather_conditions_now(self, ctx: commands.Context, *, location: str):
|
async def _weather_conditions_now(self, ctx: commands.Context, *, location: str):
|
||||||
"""Gets current local weather conditions from [wttr.in](http://wttr.in/).
|
"""Gets current local weather conditions from [wttr.in](http://wttr.in/).
|
||||||
See help of the `weather` command for possible location types and options."""
|
See help of the `weather` command for possible location types and options."""
|
||||||
|
async with ctx.typing():
|
||||||
try:
|
try:
|
||||||
units_arg = re.search(self.wttr_units_regex, location).group(1)
|
units_arg = re.search(self.wttr_units_regex, location).group(1)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -99,8 +112,12 @@ class WeatherCog(commands.Cog):
|
|||||||
embed.colour = cmn.colours.good
|
embed.colour = cmn.colours.good
|
||||||
|
|
||||||
loc = loc.replace(" ", "+")
|
loc = loc.replace(" ", "+")
|
||||||
embed.set_image(url=f"http://wttr.in/{loc}_0{units}pnFQ.png")
|
async with self.session.get(f"http://wttr.in/{loc}_0{units}pnFQ.png") as resp:
|
||||||
await ctx.send(embed=embed)
|
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):
|
def setup(bot: commands.Bot):
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ authors = ("@ClassAbbyAmplifier#2229", "@0x5c#0639")
|
|||||||
description = """A bot with various useful ham radio-related functions, written in Python."""
|
description = """A bot with various useful ham radio-related functions, written in Python."""
|
||||||
license = "Released under the GNU General Public License v2"
|
license = "Released under the GNU General Public License v2"
|
||||||
contributing = "Check out the source on GitHub, contributions welcome: https://github.com/miaowware/qrm2"
|
contributing = "Check out the source on GitHub, contributions welcome: https://github.com/miaowware/qrm2"
|
||||||
release = "2.5.0"
|
release = "2.3.1"
|
||||||
bot_server = "https://discord.gg/Ntbg3J4"
|
bot_server = "https://discord.gg/Ntbg3J4"
|
||||||
|
|||||||
@@ -44,19 +44,9 @@ debug_mode = opt.debug # Separate assignement in-case we define an override (te
|
|||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
connector = loop.run_until_complete(conn.new_connector())
|
connector = loop.run_until_complete(conn.new_connector())
|
||||||
|
|
||||||
# Defining the intents
|
|
||||||
intents = discord.Intents.none()
|
|
||||||
intents.guilds = True
|
|
||||||
intents.guild_messages = True
|
|
||||||
intents.dm_messages = True
|
|
||||||
|
|
||||||
member_cache = discord.MemberCacheFlags.from_intents(intents)
|
|
||||||
|
|
||||||
bot = commands.Bot(command_prefix=opt.prefix,
|
bot = commands.Bot(command_prefix=opt.prefix,
|
||||||
case_insensitive=True,
|
case_insensitive=True,
|
||||||
description=info.description, help_command=commands.MinimalHelpCommand(),
|
description=info.description, help_command=commands.MinimalHelpCommand(),
|
||||||
intents=intents,
|
|
||||||
member_cache=member_cache,
|
|
||||||
loop=loop,
|
loop=loop,
|
||||||
connector=connector)
|
connector=connector)
|
||||||
|
|
||||||
@@ -98,7 +88,7 @@ async def _extctl(ctx: commands.Context):
|
|||||||
"""Extension control commands.
|
"""Extension control commands.
|
||||||
Defaults to `list` if no subcommand specified"""
|
Defaults to `list` if no subcommand specified"""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
cmd = _extctl_list
|
cmd = bot.get_command("extctl list")
|
||||||
await ctx.invoke(cmd)
|
await ctx.invoke(cmd)
|
||||||
|
|
||||||
|
|
||||||
@@ -166,7 +156,7 @@ async def on_command_error(ctx: commands.Context, err: commands.CommandError):
|
|||||||
await cmn.add_react(ctx.message, cmn.emojis.warning)
|
await cmn.add_react(ctx.message, cmn.emojis.warning)
|
||||||
await ctx.send_help(ctx.command)
|
await ctx.send_help(ctx.command)
|
||||||
elif isinstance(err, commands.CommandNotFound):
|
elif isinstance(err, commands.CommandNotFound):
|
||||||
if ctx.invoked_with and ctx.invoked_with.startswith(("?", "!")):
|
if ctx.invoked_with.startswith(("?", "!")):
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
await cmn.add_react(ctx.message, cmn.emojis.question)
|
await cmn.add_react(ctx.message, cmn.emojis.question)
|
||||||
@@ -203,10 +193,7 @@ async def _ensure_activity_time():
|
|||||||
try:
|
try:
|
||||||
tz = pytz.timezone(opt.status_tz)
|
tz = pytz.timezone(opt.status_tz)
|
||||||
except pytz.exceptions.UnknownTimeZoneError:
|
except pytz.exceptions.UnknownTimeZoneError:
|
||||||
status = "with invalid timezones"
|
await bot.change_presence(activity=discord.Game(name="with invalid timezones."))
|
||||||
if opt.show_help:
|
|
||||||
status += f" | {opt.display_prefix}help"
|
|
||||||
await bot.change_presence(activity=discord.Game(name=status))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
now = datetime.now(tz=tz).time()
|
now = datetime.now(tz=tz).time()
|
||||||
@@ -216,8 +203,6 @@ async def _ensure_activity_time():
|
|||||||
end_time = time(hour=sts[2][0], minute=sts[2][1], tzinfo=tz)
|
end_time = time(hour=sts[2][0], minute=sts[2][1], tzinfo=tz)
|
||||||
if start_time < now <= end_time:
|
if start_time < now <= end_time:
|
||||||
status = sts[0]
|
status = sts[0]
|
||||||
if opt.show_help:
|
|
||||||
status += f" | {opt.display_prefix}help"
|
|
||||||
|
|
||||||
await bot.change_presence(activity=discord.Game(name=status))
|
await bot.change_presence(activity=discord.Game(name=status))
|
||||||
|
|
||||||
@@ -225,8 +210,6 @@ async def _ensure_activity_time():
|
|||||||
@tasks.loop(minutes=5)
|
@tasks.loop(minutes=5)
|
||||||
async def _ensure_activity_random():
|
async def _ensure_activity_random():
|
||||||
status = random.choice(opt.statuses)
|
status = random.choice(opt.statuses)
|
||||||
if opt.show_help:
|
|
||||||
status += f" | {opt.display_prefix}help"
|
|
||||||
|
|
||||||
await bot.change_presence(activity=discord.Game(name=status))
|
await bot.change_presence(activity=discord.Game(name=status))
|
||||||
|
|
||||||
@@ -234,8 +217,6 @@ async def _ensure_activity_random():
|
|||||||
@tasks.loop(minutes=5)
|
@tasks.loop(minutes=5)
|
||||||
async def _ensure_activity_fixed():
|
async def _ensure_activity_fixed():
|
||||||
status = opt.statuses[0]
|
status = opt.statuses[0]
|
||||||
if opt.show_help:
|
|
||||||
status += f" | {opt.display_prefix}help"
|
|
||||||
|
|
||||||
await bot.change_presence(activity=discord.Game(name=status))
|
await bot.change_presence(activity=discord.Game(name=status))
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
discord.py~=1.5.0
|
discord.py
|
||||||
ctyparser~=2.0
|
ctyparser
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
lxml
|
lxml
|
||||||
pytz
|
pytz
|
||||||
|
ae7qparser
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Information about callsigns for the prefixes command in hamcog.
|
Information about callsigns for the vanity prefixes command in hamcog.
|
||||||
---
|
---
|
||||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||||
|
|
||||||
@@ -8,21 +8,48 @@ the GNU General Public License, version 2.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
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 = {
|
||||||
|
"**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")
|
||||||
|
}
|
||||||
|
|
||||||
from .callsigninfos import (us, ca)
|
# format: country: (title, description, text)
|
||||||
|
options = {"us": (us_calls_title, us_calls_desc, us_calls)}
|
||||||
|
|
||||||
@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),
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Callsign info for various countries
|
|
||||||
"""
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
"""
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
"""
|
|
||||||
Information about callsigns for the US prefixes command in hamcog.
|
|
||||||
---
|
|
||||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
|
||||||
|
|
||||||
This file is part of discord-qrmbot and is released under the terms of
|
|
||||||
the GNU General Public License, version 2.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
title = "US Callsign Rules"
|
|
||||||
emoji = "🇺🇸"
|
|
||||||
desc = ("#x# is the number of letters in the prefix and suffix of a callsign. "
|
|
||||||
"E.g., WY4RC would be a 2x2 callsign, with prefix WY and suffix RC.")
|
|
||||||
calls = {
|
|
||||||
"**Group A** (Extra Only)": ("**Any:** K, N, W (1x2)\n"
|
|
||||||
" AA-AL, KA-KZ, NA-NZ, WA-WZ (2x1)\n"
|
|
||||||
" AA-AL (2x2)\n"
|
|
||||||
"*Except*\n"
|
|
||||||
"**Alaska:** AL, KL, NL, WL (2x1)\n"
|
|
||||||
"**Caribbean:** KP, NP, WP (2x1)\n"
|
|
||||||
"**Pacific:** AH, KH, NH, WH (2x1)"),
|
|
||||||
"**Group B** (Advanced and Extra Only)": ("**Any:** KA-KZ, NA-NZ, WA-WZ (2x2)\n"
|
|
||||||
"*Except*\n"
|
|
||||||
"**Alaska:** AL (2x2)\n"
|
|
||||||
"**Caribbean:** KP (2x2)\n"
|
|
||||||
"**Pacific:** AH (2x2)"),
|
|
||||||
"**Group C** (Technician, General, Advanced, Extra Only)": ("**Any Region:** K, N, W (1x3)\n"
|
|
||||||
"*Except*\n"
|
|
||||||
"**Alaska:** KL, NL, WL (2x2)\n"
|
|
||||||
"**Caribbean:** NP, WP (2x2)\n"
|
|
||||||
"**Pacific:** KH, NH, WH (2x2)"),
|
|
||||||
"**Group D** (Any License Class)": ("**Any Region:** KA-KZ, WA-WZ (2x3)\n"
|
|
||||||
"*Except*\n"
|
|
||||||
"**Alaska:** KL, WL (2x3)\n"
|
|
||||||
"**Caribbean:** KP, WP (2x3)\n"
|
|
||||||
"**Pacific:** KH, WH (2x3)"),
|
|
||||||
"**Unavailable**": ("- KA2AA-KA9ZZ: US Army in Japan\n"
|
|
||||||
"- KC4AAA-KC4AAF: NSF in Antartica\n"
|
|
||||||
"- KC4USA-KC4USZ: US Navy in Antartica\n"
|
|
||||||
"- KG4AA-KG4ZZ: US Navy in Guantanamo Bay\n"
|
|
||||||
"- KL9KAA-KL9KHZ: US military in Korea\n"
|
|
||||||
"- KC6AA-KC6ZZ: Former US (Eastern and Western Caroline Islands), "
|
|
||||||
"now Federated States of Micronesia (V6) and Republic of Palau (T8)\n"
|
|
||||||
"- KX6AA-KX6ZZ: Former US (Marshall Islands), "
|
|
||||||
"now Republic of the Marshall Islands (V73)\n"
|
|
||||||
"- Any suffix SOS or QRA-QUZ\n"
|
|
||||||
"- Any 2x3 with X as the first suffix letter\n"
|
|
||||||
"- Any 2x3 with AF, KF, NF, or WF prefix and suffix EMA: FEMA\n"
|
|
||||||
"- Any 2x3 with AA-AL, NA-NZ, WC, WK, WM, WR, or WT prefix: \"Group X\"\n"
|
|
||||||
"- Any 2x1, 2x2, or 2x3 with KP, NP, WP prefix and 0, 6, 7, 8, 9 number\n"
|
|
||||||
"- Any 1x1 callsign: Special Event")
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 416 KiB |
|
Before Width: | Height: | Size: 407 KiB |
|
Before Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 441 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -1,12 +1,7 @@
|
|||||||
{
|
{
|
||||||
"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)", "🇨🇦"],
|
"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", "🇨🇳"],
|
"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)", "🇲🇽"],
|
"mx": ["mx.png", "Mexico", "Radio allocations in Mexico", "Full radio allocations chart for all services. No information specific to amateur radio is shown.", "Secretaría de Comunicaciones y Transportes (SCT) / Instituto Federal de Telecomunicaciones (IFT)", "🇲🇽"],
|
||||||
"nl": ["nl.png", "Netherlands", "Amateur radio bands in the Netherlands", "*This version of this bandplan is no longer hosted on VERNON's website and it's accuracy cannot be guaranteed.*", "[VERNON Global Band Plan van de Telecom Agency, Ministerie van Economische Zaken en Klimaat](https://www.veron.nl/downloads/brochures/)", "🇳🇱"],
|
"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)", "🇺🇸"]
|
"us": ["us.png", "USA", "Amateur radio bands in the USA", "", "*[ARRL Frequency Chart](https://www.arrl.org/shop/ARRL-Frequency-Chart-11-17/)* [[PDF]](http://www.arrl.org/files/file/Regulatory/Band%20Chart/Band%20Chart%20-%2011X17%20Color.pdf)", "🇺🇸"]
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 27 KiB |
@@ -1,10 +1,6 @@
|
|||||||
{
|
{
|
||||||
"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)", "🇨🇦"],
|
"arrl": ["arrl-rac.png", "ARRL Sections", "ARRL Sections", "", "", "🇺🇸"],
|
||||||
|
"rac": ["arrl-rac.png", "RAC Sections", "RAC Sections", "", "", "🇨🇦"],
|
||||||
"cn": ["cn.png", "China's Prefixes", "Map of prefix regions in China", "", "CRAC", "🇨🇳"],
|
"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)", "🇨🇦"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 304 KiB |
@@ -15,10 +15,7 @@ Settings and options for the bot.
|
|||||||
|
|
||||||
# The prefix for the bot (str). Define a list of stings for multiple prefixes.
|
# The prefix for the bot (str). Define a list of stings for multiple prefixes.
|
||||||
# ie: `["?", "!", "pls "]`
|
# ie: `["?", "!", "pls "]`
|
||||||
prefix = ["? ", "?"]
|
prefix = "?"
|
||||||
|
|
||||||
# The prefix to use for display purposes (ex: status message).
|
|
||||||
display_prefix = "?"
|
|
||||||
|
|
||||||
# Whether the bot should print full stacktraces for normal exceptions: `True`,
|
# Whether the bot should print full stacktraces for normal exceptions: `True`,
|
||||||
# or be nice and only print small messages: `False` (the default).
|
# or be nice and only print small messages: `False` (the default).
|
||||||
@@ -30,7 +27,7 @@ debug = False
|
|||||||
owners_uids = (200102491231092736,)
|
owners_uids = (200102491231092736,)
|
||||||
|
|
||||||
# The extensions to load when running the bot.
|
# The extensions to load when running the bot.
|
||||||
exts = ["ae7q", "base", "fun", "grid", "ham", "image", "lookup", "morse", "qrz", "study", "weather", "dbconv"]
|
exts = ["ae7q", "base", "fun", "grid", "ham", "image", "lookup", "morse", "qrz", "study", "weather"]
|
||||||
|
|
||||||
# Either "time", "random", or "fixed" (first item in statuses)
|
# Either "time", "random", or "fixed" (first item in statuses)
|
||||||
status_mode = "fixed"
|
status_mode = "fixed"
|
||||||
@@ -49,9 +46,6 @@ time_statuses = [("with lids on 3.840", (00, 00), (6, 00)),
|
|||||||
("with lids on 7.200", (18, 00), (20, 00)),
|
("with lids on 7.200", (18, 00), (20, 00)),
|
||||||
("with lids on 3.840", (20, 00), (23, 59))]
|
("with lids on 3.840", (20, 00), (23, 59))]
|
||||||
|
|
||||||
# append " | {display_prefix}help" to the Discord playing status
|
|
||||||
show_help = False
|
|
||||||
|
|
||||||
# Emoji IDs and keywords for emoji reactions
|
# Emoji IDs and keywords for emoji reactions
|
||||||
# Use the format {emoji_id (int): ("tuple", "of", "lowercase", "keywords")}
|
# Use the format {emoji_id (int): ("tuple", "of", "lowercase", "keywords")}
|
||||||
msg_reacts = {}
|
msg_reacts = {}
|
||||||
|
|||||||