Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e35e8949a | |||
| 77b572eb3e | |||
| be042a9641 | |||
| 488ae6cc98 | |||
| a65fd04dbd | |||
| 6329718d29 | |||
| 756a15c4c5 | |||
| b462527211 | |||
| 93b42e64dc | |||
| f6103ef6f1 | |||
| 090b96482d | |||
| 6d71974ea1 | |||
| 0af82ac241 | |||
| 2ba6249b90 | |||
| 2c11dad358 | |||
| 5f796d479e | |||
| 3ba55d4c35 | |||
| f4ed93dc76 | |||
| 2cb4b03532 | |||
| bc93462c29 | |||
| 6867c45c8c | |||
| dcbb7acab8 | |||
| 3803ce6045 | |||
| 8ca4911072 | |||
| 6e2468f04f | |||
| b17a8a1749 | |||
| 8dfa7001ef | |||
| f6ed8430b9 | |||
| d650cbd6c1 | |||
| 4d9f9d1b19 | |||
| 1c649aacc2 | |||
| 2049ca9fca | |||
| 38416d9050 | |||
| 8dcdc22fe4 | |||
| c57056e586 |
@@ -0,0 +1,29 @@
|
||||
### Description
|
||||
|
||||
*Describe the changes you made here.*
|
||||
|
||||
Fixes #{issue}
|
||||
|
||||
### Type of change
|
||||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- Bug fix (non-breaking change which fixes an issue)
|
||||
- New feature (non-breaking change which adds functionality)
|
||||
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- This change requires a documentation update
|
||||
|
||||
### How has this been tested?
|
||||
|
||||
*Describe the procedure used for verifying your changes here.*
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Issue exists for PR
|
||||
- [ ] Code reviewed by the author
|
||||
- [ ] Code documented (comments or other documentation)
|
||||
- [ ] Changes tested
|
||||
- [ ] `flake8` passes
|
||||
- [ ] `CHANGELOG.md` updated
|
||||
- [ ] Informative commit messages
|
||||
- [ ] Descriptive PR title
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Docker Build and Push
|
||||
|
||||
on:
|
||||
push:
|
||||
# Publish `master` as Docker `dev` image.
|
||||
branches:
|
||||
- master
|
||||
# Publish `v*` tags as releases and as `latest`.
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
IMAGE_NAME: qrm2
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
echo ${{ github.sha }} > git_commit
|
||||
docker build . --file Dockerfile -t $IMAGE_NAME
|
||||
|
||||
- name: Log into Github Package Registry
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Log into Docker Hub
|
||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
|
||||
|
||||
- name: Push image to registries
|
||||
run: |
|
||||
GITHUB_IMAGE_ID=docker.pkg.github.com/${{ github.repository }}/$IMAGE_NAME
|
||||
DOCKER_IMAGE_ID=${{ secrets.DOCKER_USERNAME }}/$IMAGE_NAME
|
||||
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
|
||||
[[ "$VERSION" == "master" ]] && VERSION=dev
|
||||
|
||||
echo GITHUB_IMAGE_ID=$GITHUB_IMAGE_ID
|
||||
echo DOCKER_IMAGE_ID=$DOCKER_IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
|
||||
# tag for Github Registry
|
||||
docker tag $IMAGE_NAME $GITHUB_IMAGE_ID:$VERSION
|
||||
[[ "$VERSION" != "dev" ]] && docker tag $IMAGE_NAME $GITHUB_IMAGE_ID:latest
|
||||
docker push $GITHUB_IMAGE_ID
|
||||
|
||||
# tag for Docker Hub
|
||||
docker tag $IMAGE_NAME $DOCKER_IMAGE_ID:$VERSION
|
||||
[[ "$VERSION" != "dev" ]] && docker tag $IMAGE_NAME $DOCKER_IMAGE_ID:latest
|
||||
docker push $DOCKER_IMAGE_ID
|
||||
@@ -1,6 +1,6 @@
|
||||
name: Linting
|
||||
|
||||
on: [push]
|
||||
on: [push,pull_request]
|
||||
|
||||
jobs:
|
||||
flake8_py3:
|
||||
|
||||
@@ -9,6 +9,8 @@ cty.zip
|
||||
|
||||
/docker-compose.yml
|
||||
|
||||
.vscode/
|
||||
|
||||
|
||||
#########################################################
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
||||
@@ -7,6 +7,54 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [2.4.0] - 2020-09-27
|
||||
### Added
|
||||
- Canadian prefix info to the `?prefixes` command.
|
||||
- `?worksplit` command.
|
||||
- Maps for CQ Zones, ITU Zones, ITU Regions, and Canadian prefixes.
|
||||
- Attribution for all maps.
|
||||
- Option to append ` | ?help` to the playing status.
|
||||
- `?dbconv` command to convert voltage, power, and antenna gain values.
|
||||
### Changed
|
||||
- ARRL/RAC section maps to include all current ARRL/RAC sections.
|
||||
### Fixed
|
||||
- Issue where multiple prefixes were not handled properly.
|
||||
|
||||
|
||||
## [2.3.2] - 2020-07-22
|
||||
### Fixed
|
||||
- Dependency issues
|
||||
|
||||
|
||||
## [2.3.1] - 2020-04-02
|
||||
### Fixed
|
||||
- Wordlist containing innappropriate words.
|
||||
|
||||
|
||||
## [2.3.0] - 2020-03-30
|
||||
### Added
|
||||
- `?phoneticweight` command, which calculates a message's length in syllables.
|
||||
- `?standards` command to display [xkcd 927](https://xkcd.com/927/).
|
||||
### Changed
|
||||
- Python>=3.7 now required.
|
||||
|
||||
|
||||
## [2.2.3] - 2020-03-29
|
||||
### Fixed
|
||||
- Commands are no longer case-sensitive.
|
||||
|
||||
|
||||
## [2.2.2] - 2020-02-25
|
||||
### Fixed
|
||||
- Fixed issue where HamStudy questions with images would cause an error.
|
||||
- Added/fixed/removed typing indicators in numerous commands.
|
||||
|
||||
|
||||
## [2.2.1] - 2020-02-20
|
||||
### Fixed
|
||||
- Fixed issue where some HamStudy pools will become unselectable.
|
||||
|
||||
|
||||
## [2.2.0] - 2020-02-15
|
||||
### Added
|
||||
- Added Trustee field to qrz command for club callsigns.
|
||||
@@ -76,7 +124,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
## 1.0.0 - 2019-07-31 [YANKED]
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.2.0...HEAD
|
||||
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.4.0...HEAD
|
||||
[2.4.0]: https://github.com/miaowware/qrm2/releases/tag/v2.4.0
|
||||
[2.3.2]: https://github.com/miaowware/qrm2/releases/tag/v2.3.2
|
||||
[2.3.1]: https://github.com/miaowware/qrm2/releases/tag/v2.3.1
|
||||
[2.3.0]: https://github.com/miaowware/qrm2/releases/tag/v2.3.0
|
||||
[2.2.3]: https://github.com/miaowware/qrm2/releases/tag/v2.2.3
|
||||
[2.2.2]: https://github.com/miaowware/qrm2/releases/tag/v2.2.2
|
||||
[2.2.1]: https://github.com/miaowware/qrm2/releases/tag/v2.2.1
|
||||
[2.2.0]: https://github.com/miaowware/qrm2/releases/tag/v2.2.0
|
||||
[2.1.0]: https://github.com/miaowware/qrm2/releases/tag/v2.1.0
|
||||
[2.0.0]: https://github.com/miaowware/qrm2/releases/tag/v2.0.0
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Contributing to qrm
|
||||
|
||||
## Before You Start
|
||||
|
||||
- Make sure there's an issue for the feature, bugfix, or other improvement you want to make.
|
||||
- Make sure it's something that the project maintainers want.
|
||||
We can discuss it and assign the issue to you.
|
||||
- Make sure work isn't already being done on the issue.
|
||||
|
||||
### Environment Setup
|
||||
|
||||
Once all of the above is done, you can get started by setting up your development envronment.
|
||||
|
||||
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).
|
||||
|
||||
## While You Develop
|
||||
|
||||
To run qrm, use the command `./run.sh`.
|
||||
We recommend you use the `--pass-errors` flags 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.
|
||||
|
||||
Commit messages should be descriptive and mention issues that they fix ("fixes #123") or contain progress on ("progress on #123").
|
||||
Make commits as needed, but try to keep it reasonable.
|
||||
If there are too many, your contribution may be squashed when merged.
|
||||
You may want to squash your commits locally yourself:
|
||||
|
||||
```sh
|
||||
git reset --soft [commit before your changes]
|
||||
git commit
|
||||
```
|
||||
|
||||
Make sure to document your code as you go, in both comments and external documentation (in `/dev-notes/`) as needed.
|
||||
`dev-notes` is especially important if you introduce a new json file format or to document some development process (like the command to crush the various images in the repository).
|
||||
|
||||
**Test your changes.**
|
||||
If your code doesn't work, it's not ready for merging.
|
||||
Make sure you not only test intended behaviour, but also edge cases and error cases.
|
||||
Make sure to run `flake8` to ensure your code uses the proper style, and `mypy [files...]` to ensure proper typing.
|
||||
|
||||
If you're making a user-facing change, put a quick summary in `CHANGELOG.md` under the `[Unreleased]` heading.
|
||||
Follow the [Keep a Changelog][4] format.
|
||||
|
||||
### 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;
|
||||
if that fails, you will be expected to fix those errors before merge.
|
||||
|
||||
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,
|
||||
add type hints,
|
||||
etc.
|
||||
|
||||
## When You're Ready to Merge
|
||||
|
||||
1. When you have finished working on your contribution, create a pull request from your fork's branch into the master branch of this repository.
|
||||
1. Read through and complete the pull request template.
|
||||
If the checklist is not complete, your contribution will not be merged.
|
||||
1. Your pull request will get reviewed by at least one maintainer.
|
||||
1. If approved, another maintainer may merge the pull request if everything looks good.
|
||||
|
||||
[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://keepachangelog.com/en/1.0.0/
|
||||
@@ -71,6 +71,17 @@ clean:
|
||||
|
||||
|
||||
### Dev targets ###
|
||||
.PHONY: dev-install
|
||||
dev-install: $(BOTENV)/dev_req_done data/options.py data/keys.py
|
||||
|
||||
# Installing dev requirements
|
||||
$(BOTENV)/dev_req_done: dev-requirements.txt $(BOTENV)/success
|
||||
@echo "\033[34;1m--> Installing the dependencies...\033[0m"
|
||||
@. $(BOTENV)/bin/activate; \
|
||||
pip install ${PIP_OUTPUT} -U pip setuptools wheel; \
|
||||
pip install ${PIP_OUTPUT} -U -r dev-requirements.txt
|
||||
@touch $(BOTENV)/dev_req_done
|
||||
|
||||
|
||||
|
||||
### Special targets ###
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Docker help for qrm2
|
||||
|
||||
You have multiple ways to use docker to run an instance of qrm2
|
||||
You have multiple ways to use docker to run an instance of qrm2.
|
||||
|
||||
- [Docker help for qrm2](#docker-help-for-qrm2)
|
||||
- [Using docker-compose and the prebuilt-image (recommended)](#using-docker-compose-and-the-prebuilt-image-recommended)
|
||||
@@ -23,13 +23,16 @@ This is the easiest method for running the bot without any modifications.
|
||||
version: '3'
|
||||
services:
|
||||
qrm2:
|
||||
image: "classabbyamp/discord-qrm2:latest"
|
||||
image: "classabbyamp/qrm2:latest"
|
||||
# OR
|
||||
# image: "docker.pkg.github.com/miaowware/qrm2/qrm2:latest"
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- "./data:/app/data:rw"
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
```
|
||||
> Note that there are two possible sources for the image: docker's and github's registry. 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`.
|
||||
|
||||
@@ -59,7 +62,7 @@ This is the easiest method to run the bot with modifications.
|
||||
services:
|
||||
qrm2:
|
||||
build: .
|
||||
image: "discord-qrm2:local-latest"
|
||||
image: "qrm2:local-latest"
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- "./data:/app/data:rw"
|
||||
@@ -75,7 +78,7 @@ This is the easiest method to run the bot with modifications.
|
||||
|
||||
```none
|
||||
$ docker-compose build --pull
|
||||
$ docker-compose -d
|
||||
$ docker-compose up -d
|
||||
```
|
||||
|
||||
> Run without "-d" to test the bot. (run in foreground)
|
||||
@@ -95,7 +98,7 @@ This methods is not very nice to use.
|
||||
2. Run docker build:
|
||||
|
||||
```none
|
||||
$ docker build -t discord-qrm2:local-latest .
|
||||
$ docker build -t qrm2:local-latest .
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ See [README-DOCKER.md](./README-DOCKER.md)
|
||||
|
||||
### Without Docker
|
||||
|
||||
Requires Python 3.7 or newer.
|
||||
|
||||
Prep the environment. For more information on extra options, see the [quick-bot-no-pain Makefile documentation](https://github.com/0x5c/quick-bot-no-pain/blob/master/docs/makefile.md).
|
||||
|
||||
```
|
||||
@@ -26,6 +28,10 @@ Run. For more information on options, see the [quick-bot-no-pain run.sh document
|
||||
$ run.sh
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Check out the [contribution guidelines](/CONTRIBUTING.md) for more information about how to contribute to this project.
|
||||
|
||||
## Copyright
|
||||
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
@@ -63,6 +63,7 @@ emojis = SimpleNamespace(
|
||||
paths = SimpleNamespace(
|
||||
data=Path("./data/"),
|
||||
resources=Path("./resources/"),
|
||||
img=Path("./resources/img/"),
|
||||
bandcharts=Path("./resources/img/bandcharts/"),
|
||||
maps=Path("./resources/img/maps/"),
|
||||
)
|
||||
@@ -70,6 +71,16 @@ paths = SimpleNamespace(
|
||||
|
||||
# --- Classes ---
|
||||
|
||||
|
||||
class CallsignInfoData:
|
||||
"""Represents a country's callsign info"""
|
||||
def __init__(self, data: list):
|
||||
self.title: str = data[0]
|
||||
self.desc: str = data[1]
|
||||
self.calls: str = data[2]
|
||||
self.emoji: str = data[3]
|
||||
|
||||
|
||||
class ImageMetadata:
|
||||
"""Represents the metadata of a single image."""
|
||||
def __init__(self, metadata: list):
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Image processing instructions
|
||||
|
||||
For images like bandplans and maps, first resize the image to a reasonable size, then run `pngquant --quality 30-40` on the images.
|
||||
Do not apply that to non-flat images like actual pictures.
|
||||
@@ -20,5 +20,5 @@ Used for grouping info such as name, description, source, and such.
|
||||
| `name` | The name of the file. | `Canada`, `ITU Zones` |
|
||||
| `long_name` | The long name (title) of the file. | `Worldwide map of ITU Zones` |
|
||||
| `description` | The description accompanying the file. | `Full radio allocations chart for all services.` |
|
||||
| `source` | The source of the file. | `Instituto Federal de Telecomunicaciones (IFT)` |
|
||||
| `source` | The source of the file. | `Instituto Federal de Telecomunicaciones (IFT)` |
|
||||
| `emoji` | A Unicode emoji associated with the file. | `📻`, `🇨🇦` |
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"_id": "56956f51f65e5c590272e372",
|
||||
"appears": "2016-04-01T06:00:00.000Z",
|
||||
"class": "Amateur Extra",
|
||||
"subtext": "Expires Jul 1, 2020",
|
||||
"valid_from": "2016-07-01T06:00:00.000Z",
|
||||
"expires": "2020-07-01T06:00:00.000Z",
|
||||
"official_name": "Element 4",
|
||||
"id": "E4_2016",
|
||||
"slug": "extra2016",
|
||||
"passing": 37,
|
||||
"year": 2016,
|
||||
"pool": [{
|
||||
"_id": "5cd63f15910d9b003d545bd7",
|
||||
"qcount": 4,
|
||||
"id": "E5",
|
||||
"name": "ELECTRICAL PRINCIPLES",
|
||||
"sections": [{
|
||||
"_id": "5cd63f15910d9b003d545beb",
|
||||
"id": "E5C",
|
||||
"questions": [{
|
||||
"_id": "5cd63f15910d9b003d545bef",
|
||||
"keywords": ["4"],
|
||||
"answer": "B",
|
||||
"answers": {
|
||||
"A": "Point 2",
|
||||
"B": "Point 4",
|
||||
"C": "Point 5",
|
||||
"D": "Point 6"
|
||||
},
|
||||
"fccpart": "",
|
||||
"id": "E5C14",
|
||||
"image": "E5-2.png",
|
||||
"text": "Which point on Figure E5-2 best represents the impedance of a series circuit consisting of a 400 ohm resistor and a 38 picofarad capacitor at 14 MHz?"
|
||||
}],
|
||||
"summary": "Coordinate systems and phasors in electronics: Rectangular Coordinates; Polar Coordinates; Phasors"
|
||||
}]
|
||||
}],
|
||||
"updated": "2019-05-11T03:18:46.121Z",
|
||||
"category": "default",
|
||||
"testIdEnd": 19999,
|
||||
"testIdStart": 10000,
|
||||
"__v": 12,
|
||||
"mat_icon": "flash_on",
|
||||
"tagline": "Serious General operators only! This is the most advanced US license class!",
|
||||
"keywords": ["ham radio extra test prep", "amateur extra class radio study", "amateur extra class ham exam", "ham radio amateur extra test 2016", "2016 amateur extra class", "ham radio license exam", "extra class flash card"],
|
||||
"replaces": "E4_2012"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
-r requirements.txt
|
||||
flake8
|
||||
discord.py-stubs
|
||||
@@ -29,7 +29,7 @@ class AE7QCog(commands.Cog):
|
||||
self.bot = bot
|
||||
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
|
||||
|
||||
@commands.group(name="ae7q", aliases=["ae"], category=cmn.cat.lookup)
|
||||
@commands.group(name="ae7q", aliases=["ae"], case_insensitive=True, category=cmn.cat.lookup)
|
||||
async def _ae7q_lookup(self, ctx: commands.Context):
|
||||
"""Looks up a callsign, FRN, or Licensee ID on [ae7q.com](http://ae7q.com/)."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
|
||||
@@ -10,7 +10,6 @@ the GNU General Public License, version 2.
|
||||
|
||||
import random
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
from typing import Union
|
||||
|
||||
import discord
|
||||
@@ -47,9 +46,9 @@ class QrmHelpCommand(commands.HelpCommand):
|
||||
if parent:
|
||||
fmt = f"{parent} {fmt}"
|
||||
alias = fmt
|
||||
return f"{opt.prefix}{alias} {command.signature}\n *Aliases:* {aliases}"
|
||||
return f"{opt.display_prefix}{alias} {command.signature}\n *Aliases:* {aliases}"
|
||||
alias = command.name if not parent else f"{parent} {command.name}"
|
||||
return f"{opt.prefix}{alias} {command.signature}"
|
||||
return f"{opt.display_prefix}{alias} {command.signature}"
|
||||
|
||||
async def send_error_message(self, error):
|
||||
embed = cmn.embed_factory(self.context)
|
||||
@@ -61,7 +60,7 @@ class QrmHelpCommand(commands.HelpCommand):
|
||||
async def send_bot_help(self, mapping):
|
||||
embed = cmn.embed_factory(self.context)
|
||||
embed.title = "qrm Help"
|
||||
embed.description = (f"For command-specific help and usage, use `{opt.prefix}help [command name]`."
|
||||
embed.description = (f"For command-specific help and usage, use `{opt.display_prefix}help [command name]`."
|
||||
" Many commands have shorter aliases.")
|
||||
mapping = await mapping
|
||||
|
||||
@@ -189,7 +188,7 @@ class BaseCog(commands.Cog):
|
||||
|
||||
|
||||
def parse_changelog():
|
||||
changelog = OrderedDict()
|
||||
changelog = {}
|
||||
ver = ""
|
||||
heading = ""
|
||||
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
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
|
||||
|
||||
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):
|
||||
try:
|
||||
return Unit(argument)
|
||||
except ValueError as e:
|
||||
raise commands.BadArgument(message=str(e))
|
||||
|
||||
|
||||
class Unit:
|
||||
def __init__(self, raw: str):
|
||||
self.raw: str = raw
|
||||
self.unit: str
|
||||
self.type: UnitType
|
||||
self.is_db: bool
|
||||
self.mult: int
|
||||
self._parse()
|
||||
|
||||
def _parse(self):
|
||||
s = self.raw.lower()
|
||||
if len(s) > 2 and s[:2] == "db":
|
||||
self.is_db = True
|
||||
if s[2:] in units:
|
||||
u = units[s[2:]]
|
||||
self.mult = u["mult"]
|
||||
self.unit = u["log"]
|
||||
self.type = u["type"]
|
||||
elif s in units:
|
||||
self.is_db = False
|
||||
u = units[s]
|
||||
self.mult = u["mult"]
|
||||
self.unit = u["scalar"]
|
||||
self.type = u["type"]
|
||||
else:
|
||||
raise ValueError(f"Invalid unit: {self.raw}")
|
||||
|
||||
def __str__(self):
|
||||
return self.unit
|
||||
|
||||
|
||||
class UnitType(Enum):
|
||||
voltage = 1
|
||||
power = 2
|
||||
antenna = 3
|
||||
|
||||
|
||||
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} = {converted:.3g} {unit_to}"
|
||||
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,6 +10,7 @@ the GNU General Public License, version 2.
|
||||
|
||||
import random
|
||||
|
||||
import discord
|
||||
import discord.ext.commands as commands
|
||||
|
||||
import common as cmn
|
||||
@@ -31,6 +32,21 @@ class FunCog(commands.Cog):
|
||||
"""Returns xkcd: tar."""
|
||||
await ctx.send("http://xkcd.com/1168")
|
||||
|
||||
@commands.command(name="standards", category=cmn.cat.fun)
|
||||
async def _standards(self, ctx: commands.Context):
|
||||
"""Returns xkcd: Standards."""
|
||||
await ctx.send("http://xkcd.com/927")
|
||||
|
||||
@commands.command(name="worksplit", aliases=["split", "ft8"], category=cmn.cat.fun)
|
||||
async def _worksplit(self, ctx: commands.Context):
|
||||
"""Posts "Work split you lids"."""
|
||||
fn = "worksplit.jpg"
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = "Work Split, You Lids!"
|
||||
embed.set_image(url="attachment://" + fn)
|
||||
img = discord.File(cmn.paths.img / fn, filename=fn)
|
||||
await ctx.send(embed=embed, file=img)
|
||||
|
||||
@commands.command(name="xd", hidden=True, category=cmn.cat.fun)
|
||||
async def _xd(self, ctx: commands.Context):
|
||||
"""ecks dee"""
|
||||
@@ -39,18 +55,17 @@ class FunCog(commands.Cog):
|
||||
@commands.command(name="funetics", aliases=["fun"], category=cmn.cat.fun)
|
||||
async def _funetics_lookup(self, ctx: commands.Context, *, msg: str):
|
||||
"""Generates fun/wacky phonetics for a word or phrase."""
|
||||
with ctx.typing():
|
||||
result = ""
|
||||
for char in msg.lower():
|
||||
if char.isalpha():
|
||||
result += random.choice([word for word in self.words if word[0] == char])
|
||||
else:
|
||||
result += char
|
||||
result += " "
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Funetics for {msg}"
|
||||
embed.description = result.title()
|
||||
embed.colour = cmn.colours.good
|
||||
result = ""
|
||||
for char in msg.lower():
|
||||
if char.isalpha():
|
||||
result += random.choice([word for word in self.words if word[0] == char])
|
||||
else:
|
||||
result += char
|
||||
result += " "
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Funetics for {msg}"
|
||||
embed.description = result.title()
|
||||
embed.colour = cmn.colours.good
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
|
||||
@@ -23,93 +23,91 @@ class GridCog(commands.Cog):
|
||||
async def _grid_sq_lookup(self, ctx: commands.Context, lat: str, lon: str):
|
||||
("""Calculates the grid square for latitude and longitude coordinates, """
|
||||
"""with negative being latitude South and longitude West.""")
|
||||
with ctx.typing():
|
||||
grid = "**"
|
||||
latf = float(lat) + 90
|
||||
lonf = float(lon) + 180
|
||||
if 0 <= latf <= 180 and 0 <= lonf <= 360:
|
||||
grid += chr(ord("A") + int(lonf / 20))
|
||||
grid += chr(ord("A") + int(latf / 10))
|
||||
grid += chr(ord("0") + int((lonf % 20)/2))
|
||||
grid += chr(ord("0") + int((latf % 10)/1))
|
||||
grid += chr(ord("a") + int((lonf - (int(lonf/2)*2)) / (5/60)))
|
||||
grid += chr(ord("a") + int((latf - (int(latf/1)*1)) / (2.5/60)))
|
||||
grid += "**"
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Maidenhead Grid Locator for {float(lat):.6f}, {float(lon):.6f}"
|
||||
embed.description = grid
|
||||
embed.colour = cmn.colours.good
|
||||
else:
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Error generating grid square for {lat}, {lon}."
|
||||
embed.description = """Coordinates out of range.
|
||||
The valid ranges are:
|
||||
- Latitude: `-90` to `+90`
|
||||
- Longitude: `-180` to `+180`"""
|
||||
embed.colour = cmn.colours.bad
|
||||
grid = "**"
|
||||
latf = float(lat) + 90
|
||||
lonf = float(lon) + 180
|
||||
if 0 <= latf <= 180 and 0 <= lonf <= 360:
|
||||
grid += chr(ord("A") + int(lonf / 20))
|
||||
grid += chr(ord("A") + int(latf / 10))
|
||||
grid += chr(ord("0") + int((lonf % 20)/2))
|
||||
grid += chr(ord("0") + int((latf % 10)/1))
|
||||
grid += chr(ord("a") + int((lonf - (int(lonf/2)*2)) / (5/60)))
|
||||
grid += chr(ord("a") + int((latf - (int(latf/1)*1)) / (2.5/60)))
|
||||
grid += "**"
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Maidenhead Grid Locator for {float(lat):.6f}, {float(lon):.6f}"
|
||||
embed.description = grid
|
||||
embed.colour = cmn.colours.good
|
||||
else:
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Error generating grid square for {lat}, {lon}."
|
||||
embed.description = """Coordinates out of range.
|
||||
The valid ranges are:
|
||||
- Latitude: `-90` to `+90`
|
||||
- Longitude: `-180` to `+180`"""
|
||||
embed.colour = cmn.colours.bad
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="ungrid", aliases=["loc"], category=cmn.cat.maps)
|
||||
async def _location_lookup(self, ctx: commands.Context, grid: str, grid2: str = None):
|
||||
"""Calculates the latitude and longitude for the center of a grid square.
|
||||
If two grid squares are given, the distance and azimuth between them is calculated."""
|
||||
with ctx.typing():
|
||||
if grid2 is None or grid2 == "":
|
||||
try:
|
||||
grid = grid.upper()
|
||||
loc = get_coords(grid)
|
||||
if grid2 is None or grid2 == "":
|
||||
try:
|
||||
grid = grid.upper()
|
||||
loc = get_coords(grid)
|
||||
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Latitude and Longitude for {grid}"
|
||||
embed.colour = cmn.colours.good
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Latitude and Longitude for {grid}"
|
||||
embed.colour = cmn.colours.good
|
||||
|
||||
if len(grid) >= 6:
|
||||
embed.description = f"**{loc[0]:.5f}, {loc[1]:.5f}**"
|
||||
embed.url = f"https://www.openstreetmap.org/#map=13/{loc[0]:.5f}/{loc[1]:.5f}"
|
||||
else:
|
||||
embed.description = f"**{loc[0]:.1f}, {loc[1]:.1f}**"
|
||||
embed.url = f"https://www.openstreetmap.org/#map=10/{loc[0]:.1f}/{loc[1]:.1f}"
|
||||
except Exception as e:
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Error generating latitude and longitude for grid {grid}."
|
||||
embed.description = str(e)
|
||||
embed.colour = cmn.colours.bad
|
||||
else:
|
||||
radius = 6371
|
||||
try:
|
||||
grid = grid.upper()
|
||||
grid2 = grid2.upper()
|
||||
loc = get_coords(grid)
|
||||
loc2 = get_coords(grid2)
|
||||
# Haversine formula
|
||||
d_lat = math.radians(loc2[0] - loc[0])
|
||||
d_lon = math.radians(loc2[1] - loc[1])
|
||||
a = (math.sin(d_lat/2) ** 2
|
||||
+ math.cos(math.radians(loc[0]))
|
||||
* math.cos(math.radians(loc2[0]))
|
||||
* math.sin(d_lon/2) ** 2)
|
||||
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
|
||||
d = radius * c
|
||||
d_mi = 0.6213712 * d
|
||||
if len(grid) >= 6:
|
||||
embed.description = f"**{loc[0]:.5f}, {loc[1]:.5f}**"
|
||||
embed.url = f"https://www.openstreetmap.org/#map=13/{loc[0]:.5f}/{loc[1]:.5f}"
|
||||
else:
|
||||
embed.description = f"**{loc[0]:.1f}, {loc[1]:.1f}**"
|
||||
embed.url = f"https://www.openstreetmap.org/#map=10/{loc[0]:.1f}/{loc[1]:.1f}"
|
||||
except Exception as e:
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Error generating latitude and longitude for grid {grid}."
|
||||
embed.description = str(e)
|
||||
embed.colour = cmn.colours.bad
|
||||
else:
|
||||
radius = 6371
|
||||
try:
|
||||
grid = grid.upper()
|
||||
grid2 = grid2.upper()
|
||||
loc = get_coords(grid)
|
||||
loc2 = get_coords(grid2)
|
||||
# Haversine formula
|
||||
d_lat = math.radians(loc2[0] - loc[0])
|
||||
d_lon = math.radians(loc2[1] - loc[1])
|
||||
a = (math.sin(d_lat/2) ** 2
|
||||
+ math.cos(math.radians(loc[0]))
|
||||
* math.cos(math.radians(loc2[0]))
|
||||
* math.sin(d_lon/2) ** 2)
|
||||
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
|
||||
d = radius * c
|
||||
d_mi = 0.6213712 * d
|
||||
|
||||
# Bearing
|
||||
y_dist = math.sin(math.radians(loc2[1]-loc[1])) * math.cos(math.radians(loc2[0]))
|
||||
x_dist = (math.cos(math.radians(loc[0]))
|
||||
* math.sin(math.radians(loc2[0]))
|
||||
- math.sin(math.radians(loc[0]))
|
||||
* math.cos(math.radians(loc2[0]))
|
||||
* math.cos(math.radians(loc2[1] - loc[1])))
|
||||
bearing = (math.degrees(math.atan2(y_dist, x_dist)) + 360) % 360
|
||||
# Bearing
|
||||
y_dist = math.sin(math.radians(loc2[1]-loc[1])) * math.cos(math.radians(loc2[0]))
|
||||
x_dist = (math.cos(math.radians(loc[0]))
|
||||
* math.sin(math.radians(loc2[0]))
|
||||
- math.sin(math.radians(loc[0]))
|
||||
* math.cos(math.radians(loc2[0]))
|
||||
* math.cos(math.radians(loc2[1] - loc[1])))
|
||||
bearing = (math.degrees(math.atan2(y_dist, x_dist)) + 360) % 360
|
||||
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Great Circle Distance and Bearing from {grid} to {grid2}"
|
||||
embed.description = f"**Distance:** {d:.1f} km ({d_mi:.1f} mi)\n**Bearing:** {bearing:.1f}°"
|
||||
embed.colour = cmn.colours.good
|
||||
except Exception as e:
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Error generating great circle distance and bearing from {grid} and {grid2}."
|
||||
embed.description = str(e)
|
||||
embed.colour = cmn.colours.bad
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Great Circle Distance and Bearing from {grid} to {grid2}"
|
||||
embed.description = f"**Distance:** {d:.1f} km ({d_mi:.1f} mi)\n**Bearing:** {bearing:.1f}°"
|
||||
embed.colour = cmn.colours.good
|
||||
except Exception as e:
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Error generating great circle distance and bearing from {grid} and {grid2}."
|
||||
embed.description = str(e)
|
||||
embed.colour = cmn.colours.bad
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
|
||||
@@ -21,68 +21,70 @@ from resources import qcodes
|
||||
class HamCog(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.pfxs = callsign_info.options
|
||||
|
||||
@commands.command(name="qcode", aliases=["q"], category=cmn.cat.ref)
|
||||
async def _qcode_lookup(self, ctx: commands.Context, qcode: str):
|
||||
"""Looks up the meaning of a Q Code."""
|
||||
with ctx.typing():
|
||||
qcode = qcode.upper()
|
||||
embed = cmn.embed_factory(ctx)
|
||||
if qcode in qcodes.qcodes:
|
||||
embed.title = qcode
|
||||
embed.description = qcodes.qcodes[qcode]
|
||||
embed.colour = cmn.colours.good
|
||||
else:
|
||||
embed.title = f"Q Code {qcode} not found"
|
||||
embed.colour = cmn.colours.bad
|
||||
qcode = qcode.upper()
|
||||
embed = cmn.embed_factory(ctx)
|
||||
if qcode in qcodes.qcodes:
|
||||
embed.title = qcode
|
||||
embed.description = qcodes.qcodes[qcode]
|
||||
embed.colour = cmn.colours.good
|
||||
else:
|
||||
embed.title = f"Q Code {qcode} not found"
|
||||
embed.colour = cmn.colours.bad
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="phonetics", aliases=["ph", "phoneticize", "phoneticise", "phone"], category=cmn.cat.ref)
|
||||
async def _phonetics_lookup(self, ctx: commands.Context, *, msg: str):
|
||||
"""Returns NATO phonetics for a word or phrase."""
|
||||
with ctx.typing():
|
||||
result = ""
|
||||
for char in msg.lower():
|
||||
if char.isalpha():
|
||||
result += phonetics.phonetics[char]
|
||||
else:
|
||||
result += char
|
||||
result += " "
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Phonetics for {msg}"
|
||||
embed.description = result.title()
|
||||
embed.colour = cmn.colours.good
|
||||
result = ""
|
||||
for char in msg.lower():
|
||||
if char.isalpha():
|
||||
result += phonetics.phonetics[char]
|
||||
else:
|
||||
result += char
|
||||
result += " "
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Phonetics for {msg}"
|
||||
embed.description = result.title()
|
||||
embed.colour = cmn.colours.good
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="utc", aliases=["z"], category=cmn.cat.ref)
|
||||
async def _utc_lookup(self, ctx: commands.Context):
|
||||
"""Returns the current time in UTC."""
|
||||
with ctx.typing():
|
||||
now = datetime.utcnow()
|
||||
result = "**" + now.strftime("%Y-%m-%d %H:%M") + "Z**"
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = "The current time is:"
|
||||
embed.description = result
|
||||
embed.colour = cmn.colours.good
|
||||
now = datetime.utcnow()
|
||||
result = "**" + now.strftime("%Y-%m-%d %H:%M") + "Z**"
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = "The current time is:"
|
||||
embed.description = result
|
||||
embed.colour = cmn.colours.good
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="prefixes", aliases=["vanity", "pfx", "vanities", "prefix"], category=cmn.cat.ref)
|
||||
async def _vanity_prefixes(self, ctx: commands.Context, country: str = None):
|
||||
async def _vanity_prefixes(self, ctx: commands.Context, country: str = ""):
|
||||
"""Lists valid callsign prefixes for different countries."""
|
||||
if country is None:
|
||||
await ctx.send_help(ctx.command)
|
||||
return
|
||||
country = country.lower()
|
||||
embed = cmn.embed_factory(ctx)
|
||||
if country.lower() not in callsign_info.options:
|
||||
embed.title = f"{country} not found!"
|
||||
embed.description = f"Valid countries: {', '.join(callsign_info.options.keys())}"
|
||||
if country not in self.pfxs:
|
||||
desc = "Possible arguments are:\n"
|
||||
for key, val in self.pfxs.items():
|
||||
desc += f"`{key}`: {val.title}{(' ' + val.emoji if val.emoji else '')}\n"
|
||||
embed.title = f"{country} Not Found!"
|
||||
embed.description = desc
|
||||
embed.colour = cmn.colours.bad
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
else:
|
||||
embed.title = callsign_info.options[country.lower()][0]
|
||||
embed.description = callsign_info.options[country.lower()][1]
|
||||
data = self.pfxs[country]
|
||||
embed.title = data.title + (" " + data.emoji if data.emoji else "")
|
||||
embed.description = data.desc
|
||||
embed.colour = cmn.colours.good
|
||||
|
||||
for name, val in callsign_info.options[country.lower()][2].items():
|
||||
for name, val in data.calls.items():
|
||||
embed.add_field(name=name, value=val, inline=False)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@@ -95,6 +97,26 @@ class HamCog(commands.Cog):
|
||||
embed.colour = cmn.colours.good
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="phoneticweight", aliases=["pw"], category=cmn.cat.ref)
|
||||
async def _weight(self, ctx: commands.Context, *, msg: str):
|
||||
"""Calculates the phonetic weight of a callsign or message."""
|
||||
embed = cmn.embed_factory(ctx)
|
||||
msg = msg.upper()
|
||||
weight = 0
|
||||
for char in msg:
|
||||
try:
|
||||
weight += phonetics.pweights[char]
|
||||
except KeyError:
|
||||
embed.title = "Error in calculation of phonetic weight"
|
||||
embed.description = f"Unknown character `{char}` in message"
|
||||
embed.colour = cmn.colours.bad
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
embed.title = f"Phonetic Weight of {msg}"
|
||||
embed.description = f"The phonetic weight is **{weight}**"
|
||||
embed.colour = cmn.colours.good
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def setup(bot: commands.Bot):
|
||||
bot.add_cog(HamCog(bot))
|
||||
|
||||
@@ -40,30 +40,29 @@ class LookupCog(commands.Cog):
|
||||
@commands.command(name="dxcc", aliases=["dx"], category=cmn.cat.lookup)
|
||||
async def _dxcc_lookup(self, ctx: commands.Context, query: str):
|
||||
"""Gets DXCC info about a callsign prefix."""
|
||||
with ctx.typing():
|
||||
query = query.upper()
|
||||
full_query = query
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = "DXCC Info for "
|
||||
embed.description = f"*Last Updated: {self.cty.formatted_version}*"
|
||||
embed.colour = cmn.colours.bad
|
||||
while query:
|
||||
if query in self.cty.keys():
|
||||
data = self.cty[query]
|
||||
embed.add_field(name="Entity", value=data["entity"])
|
||||
embed.add_field(name="CQ Zone", value=data["cq"])
|
||||
embed.add_field(name="ITU Zone", value=data["itu"])
|
||||
embed.add_field(name="Continent", value=data["continent"])
|
||||
embed.add_field(name="Time Zone",
|
||||
value=f"+{data['tz']}" if data["tz"] > 0 else str(data["tz"]))
|
||||
embed.title += query
|
||||
embed.colour = cmn.colours.good
|
||||
break
|
||||
else:
|
||||
query = query[:-1]
|
||||
query = query.upper()
|
||||
full_query = query
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = "DXCC Info for "
|
||||
embed.description = f"*Last Updated: {self.cty.formatted_version}*"
|
||||
embed.colour = cmn.colours.bad
|
||||
while query:
|
||||
if query in self.cty.keys():
|
||||
data = self.cty[query]
|
||||
embed.add_field(name="Entity", value=data["entity"])
|
||||
embed.add_field(name="CQ Zone", value=data["cq"])
|
||||
embed.add_field(name="ITU Zone", value=data["itu"])
|
||||
embed.add_field(name="Continent", value=data["continent"])
|
||||
embed.add_field(name="Time Zone",
|
||||
value=f"+{data['tz']}" if data["tz"] > 0 else str(data["tz"]))
|
||||
embed.title += query
|
||||
embed.colour = cmn.colours.good
|
||||
break
|
||||
else:
|
||||
embed.title += full_query + " not found"
|
||||
embed.colour = cmn.colours.bad
|
||||
query = query[:-1]
|
||||
else:
|
||||
embed.title += full_query + " not found"
|
||||
embed.colour = cmn.colours.bad
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@tasks.loop(hours=24)
|
||||
|
||||
@@ -21,61 +21,58 @@ class MorseCog(commands.Cog):
|
||||
@commands.command(name="morse", aliases=["cw"], category=cmn.cat.ref)
|
||||
async def _morse(self, ctx: commands.Context, *, msg: str):
|
||||
"""Converts ASCII to international morse code."""
|
||||
with ctx.typing():
|
||||
result = ""
|
||||
for char in msg.upper():
|
||||
try:
|
||||
result += morse.morse[char]
|
||||
except KeyError:
|
||||
result += "<?>"
|
||||
result += " "
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Morse Code for {msg}"
|
||||
embed.description = "**" + result + "**"
|
||||
embed.colour = cmn.colours.good
|
||||
result = ""
|
||||
for char in msg.upper():
|
||||
try:
|
||||
result += morse.morse[char]
|
||||
except KeyError:
|
||||
result += "<?>"
|
||||
result += " "
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"Morse Code for {msg}"
|
||||
embed.description = "**" + result + "**"
|
||||
embed.colour = cmn.colours.good
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="unmorse", aliases=["demorse", "uncw", "decw"], category=cmn.cat.ref)
|
||||
async def _unmorse(self, ctx: commands.Context, *, msg: str):
|
||||
"""Converts international morse code to ASCII."""
|
||||
with ctx.typing():
|
||||
result = ""
|
||||
msg0 = msg
|
||||
msg = msg.split("/")
|
||||
msg = [m.split() for m in msg]
|
||||
for word in msg:
|
||||
for char in word:
|
||||
try:
|
||||
result += morse.ascii[char]
|
||||
except KeyError:
|
||||
result += "<?>"
|
||||
result += " "
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"ASCII for {msg0}"
|
||||
embed.description = result
|
||||
embed.colour = cmn.colours.good
|
||||
result = ""
|
||||
msg0 = msg
|
||||
msg = msg.split("/")
|
||||
msg = [m.split() for m in msg]
|
||||
for word in msg:
|
||||
for char in word:
|
||||
try:
|
||||
result += morse.ascii[char]
|
||||
except KeyError:
|
||||
result += "<?>"
|
||||
result += " "
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"ASCII for {msg0}"
|
||||
embed.description = result
|
||||
embed.colour = cmn.colours.good
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="cwweight", aliases=["weight", "cww"], category=cmn.cat.ref)
|
||||
async def _weight(self, ctx: commands.Context, *, msg: str):
|
||||
"""Calculates the CW weight of a callsign or message."""
|
||||
embed = cmn.embed_factory(ctx)
|
||||
with ctx.typing():
|
||||
msg = msg.upper()
|
||||
weight = 0
|
||||
for char in msg:
|
||||
try:
|
||||
cw_char = morse.morse[char].replace("-", "==")
|
||||
weight += len(cw_char) * 2 + 2
|
||||
except KeyError:
|
||||
embed.title = "Error in calculation of CW weight"
|
||||
embed.description = f"Unknown character `{char}` in message"
|
||||
embed.colour = cmn.colours.bad
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
embed.title = f"CW Weight of {msg}"
|
||||
embed.description = f"The CW weight is **{weight}**"
|
||||
embed.colour = cmn.colours.good
|
||||
msg = msg.upper()
|
||||
weight = 0
|
||||
for char in msg:
|
||||
try:
|
||||
cw_char = morse.morse[char].replace("-", "==")
|
||||
weight += len(cw_char) * 2 + 2
|
||||
except KeyError:
|
||||
embed.title = "Error in calculation of CW weight"
|
||||
embed.description = f"Unknown character `{char}` in message"
|
||||
embed.colour = cmn.colours.bad
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
embed.title = f"CW Weight of {msg}"
|
||||
embed.description = f"The CW weight is **{weight}**"
|
||||
embed.colour = cmn.colours.good
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
from io import BytesIO
|
||||
|
||||
import aiohttp
|
||||
@@ -36,50 +35,51 @@ class QRZCog(commands.Cog):
|
||||
await ctx.send(f"http://qrz.com/db/{callsign}")
|
||||
return
|
||||
|
||||
try:
|
||||
await qrz_test_session(self.key, self.session)
|
||||
except ConnectionError:
|
||||
await self.get_session()
|
||||
|
||||
url = f"http://xmldata.qrz.com/xml/current/?s={self.key};callsign={callsign}"
|
||||
async with self.session.get(url) as resp:
|
||||
if resp.status != 200:
|
||||
raise ConnectionError(f"Unable to connect to QRZ (HTTP Error {resp.status})")
|
||||
with BytesIO(await resp.read()) as resp_file:
|
||||
resp_xml = etree.parse(resp_file).getroot()
|
||||
|
||||
resp_xml_session = resp_xml.xpath("/x:QRZDatabase/x:Session", namespaces={"x": "http://xmldata.qrz.com"})
|
||||
resp_session = {el.tag.split("}")[1]: el.text for el in resp_xml_session[0].getiterator()}
|
||||
if "Error" in resp_session:
|
||||
if "Session Timeout" in resp_session["Error"]:
|
||||
async with ctx.typing():
|
||||
try:
|
||||
await qrz_test_session(self.key, self.session)
|
||||
except ConnectionError:
|
||||
await self.get_session()
|
||||
await self._qrz_lookup(ctx, callsign)
|
||||
return
|
||||
if "Not found" in resp_session["Error"]:
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"QRZ Data for {callsign.upper()}"
|
||||
embed.colour = cmn.colours.bad
|
||||
embed.description = "No data found!"
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
raise ValueError(resp_session["Error"])
|
||||
|
||||
resp_xml_data = resp_xml.xpath("/x:QRZDatabase/x:Callsign", namespaces={"x": "http://xmldata.qrz.com"})
|
||||
resp_data = {el.tag.split("}")[1]: el.text for el in resp_xml_data[0].getiterator()}
|
||||
url = f"http://xmldata.qrz.com/xml/current/?s={self.key};callsign={callsign}"
|
||||
async with self.session.get(url) as resp:
|
||||
if resp.status != 200:
|
||||
raise ConnectionError(f"Unable to connect to QRZ (HTTP Error {resp.status})")
|
||||
with BytesIO(await resp.read()) as resp_file:
|
||||
resp_xml = etree.parse(resp_file).getroot()
|
||||
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"QRZ Data for {resp_data['call']}"
|
||||
embed.colour = cmn.colours.good
|
||||
embed.url = f"http://www.qrz.com/db/{resp_data['call']}"
|
||||
if "image" in resp_data:
|
||||
embed.set_thumbnail(url=resp_data["image"])
|
||||
resp_xml_session = resp_xml.xpath("/x:QRZDatabase/x:Session", namespaces={"x": "http://xmldata.qrz.com"})
|
||||
resp_session = {el.tag.split("}")[1]: el.text for el in resp_xml_session[0].getiterator()}
|
||||
if "Error" in resp_session:
|
||||
if "Session Timeout" in resp_session["Error"]:
|
||||
await self.get_session()
|
||||
await self._qrz_lookup(ctx, callsign)
|
||||
return
|
||||
if "Not found" in resp_session["Error"]:
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"QRZ Data for {callsign.upper()}"
|
||||
embed.colour = cmn.colours.bad
|
||||
embed.description = "No data found!"
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
raise ValueError(resp_session["Error"])
|
||||
|
||||
data = qrz_process_info(resp_data)
|
||||
resp_xml_data = resp_xml.xpath("/x:QRZDatabase/x:Callsign", namespaces={"x": "http://xmldata.qrz.com"})
|
||||
resp_data = {el.tag.split("}")[1]: el.text for el in resp_xml_data[0].getiterator()}
|
||||
|
||||
for title, val in data.items():
|
||||
if val is not None:
|
||||
embed.add_field(name=title, value=val, inline=True)
|
||||
await ctx.send(embed=embed)
|
||||
embed = cmn.embed_factory(ctx)
|
||||
embed.title = f"QRZ Data for {resp_data['call']}"
|
||||
embed.colour = cmn.colours.good
|
||||
embed.url = f"http://www.qrz.com/db/{resp_data['call']}"
|
||||
if "image" in resp_data:
|
||||
embed.set_thumbnail(url=resp_data["image"])
|
||||
|
||||
data = qrz_process_info(resp_data)
|
||||
|
||||
for title, val in data.items():
|
||||
if val is not None:
|
||||
embed.add_field(name=title, value=val, inline=True)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
async def get_session(self):
|
||||
"""Session creation and caching."""
|
||||
@@ -158,27 +158,24 @@ def qrz_process_info(data: dict):
|
||||
else:
|
||||
lotw = "Unknown"
|
||||
|
||||
return OrderedDict([("Name", name),
|
||||
("Country", data.get("country", None)),
|
||||
("Address", address),
|
||||
("Grid Square", data.get("grid", None)),
|
||||
("County", data.get("county", None)),
|
||||
("CQ Zone", data.get("cqzone", None)),
|
||||
("ITU Zone", data.get("ituzone", None)),
|
||||
("IOTA Designator", data.get("iota", None)),
|
||||
("Expires", data.get("expdate", None)),
|
||||
("Aliases", data.get("aliases", None)),
|
||||
("Previous Callsign", data.get("p_call", None)),
|
||||
("License Class", data.get("class", None)),
|
||||
("Trustee", data.get("trustee", None)),
|
||||
("eQSL?", eqsl),
|
||||
("Paper QSL?", mqsl),
|
||||
("LotW?", lotw),
|
||||
("QSL Info", data.get("qslmgr", None)),
|
||||
("CQ Zone", data.get("cqzone", None)),
|
||||
("ITU Zone", data.get("ituzone", None)),
|
||||
("IOTA Designator", data.get("iota", None)),
|
||||
("Born", data.get("born", None))])
|
||||
return {"Name": name,
|
||||
"Country": data.get("country", None),
|
||||
"Address": address,
|
||||
"Grid Square": data.get("grid", None),
|
||||
"County": data.get("county", None),
|
||||
"CQ Zone": data.get("cqzone", None),
|
||||
"ITU Zone": data.get("ituzone", None),
|
||||
"IOTA Designator": data.get("iota", None),
|
||||
"Expires": data.get("expdate", None),
|
||||
"Aliases": data.get("aliases", None),
|
||||
"Previous Callsign": data.get("p_call", None),
|
||||
"License Class": data.get("class", None),
|
||||
"Trustee": data.get("trustee", None),
|
||||
"eQSL?": eqsl,
|
||||
"Paper QSL?": mqsl,
|
||||
"LotW?": lotw,
|
||||
"QSL Info": data.get("qslmgr", None),
|
||||
"Born": data.get("born", None)}
|
||||
|
||||
|
||||
def setup(bot):
|
||||
|
||||
@@ -88,8 +88,8 @@ class StudyCog(commands.Cog):
|
||||
else:
|
||||
# look at valid_from and expires dates to find the correct one
|
||||
for p in pool_matches:
|
||||
valid_from = datetime.fromisoformat(pools[p]["valid_from"][:-1] + "+00:00")
|
||||
expires = datetime.fromisoformat(pools[p]["expires"][:-1] + "+00:00")
|
||||
valid_from = datetime.fromisoformat(pools[p]["valid_from"][:-1])
|
||||
expires = datetime.fromisoformat(pools[p]["expires"][:-1])
|
||||
|
||||
if valid_from < datetime.utcnow() < expires:
|
||||
pool = p
|
||||
@@ -133,7 +133,7 @@ class StudyCog(commands.Cog):
|
||||
" the answer will be revealed."),
|
||||
inline=False)
|
||||
if "image" in question:
|
||||
image_url = f"https://hamstudy.org/_1330011/images/{pool.split('_',1)[1]}/{question['image']}"
|
||||
image_url = f"https://hamstudy.org/images/{pool_meta['year']}/{question['image']}"
|
||||
embed.set_image(url=image_url)
|
||||
|
||||
q_msg = await ctx.send(embed=embed)
|
||||
|
||||
@@ -40,7 +40,7 @@ class WeatherCog(commands.Cog):
|
||||
embed.set_image(url="attachment://condx.png")
|
||||
await ctx.send(embed=embed, file=discord.File(data, "condx.png"))
|
||||
|
||||
@commands.group(name="weather", aliases=["wttr"], category=cmn.cat.weather)
|
||||
@commands.group(name="weather", aliases=["wttr"], case_insensitive=True, category=cmn.cat.weather)
|
||||
async def _weather_conditions(self, ctx: commands.Context):
|
||||
"""Gets local weather conditions from [wttr.in](http://wttr.in/).
|
||||
|
||||
|
||||
@@ -12,5 +12,5 @@ authors = ("@ClassAbbyAmplifier#2229", "@0x5c#0639")
|
||||
description = """A bot with various useful ham radio-related functions, written in Python."""
|
||||
license = "Released under the GNU General Public License v2"
|
||||
contributing = "Check out the source on GitHub, contributions welcome: https://github.com/miaowware/qrm2"
|
||||
release = "2.2.0"
|
||||
release = "2.4.0"
|
||||
bot_server = "https://discord.gg/Ntbg3J4"
|
||||
|
||||
@@ -45,8 +45,8 @@ loop = asyncio.get_event_loop()
|
||||
connector = loop.run_until_complete(conn.new_connector())
|
||||
|
||||
bot = commands.Bot(command_prefix=opt.prefix,
|
||||
description=info.description,
|
||||
help_command=commands.MinimalHelpCommand(),
|
||||
case_insensitive=True,
|
||||
description=info.description, help_command=commands.MinimalHelpCommand(),
|
||||
loop=loop,
|
||||
connector=connector)
|
||||
|
||||
@@ -82,7 +82,7 @@ async def _shutdown_bot(ctx: commands.Context):
|
||||
await bot.logout()
|
||||
|
||||
|
||||
@bot.group(name="extctl", aliases=["ex"], category=cmn.cat.admin)
|
||||
@bot.group(name="extctl", aliases=["ex"], case_insensitive=True, category=cmn.cat.admin)
|
||||
@commands.check(cmn.check_if_owner)
|
||||
async def _extctl(ctx: commands.Context):
|
||||
"""Extension control commands.
|
||||
@@ -193,7 +193,10 @@ async def _ensure_activity_time():
|
||||
try:
|
||||
tz = pytz.timezone(opt.status_tz)
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
await bot.change_presence(activity=discord.Game(name="with invalid timezones."))
|
||||
status = "with invalid timezones"
|
||||
if opt.show_help:
|
||||
status += f" | {opt.display_prefix}help"
|
||||
await bot.change_presence(activity=discord.Game(name=status))
|
||||
return
|
||||
|
||||
now = datetime.now(tz=tz).time()
|
||||
@@ -203,6 +206,8 @@ async def _ensure_activity_time():
|
||||
end_time = time(hour=sts[2][0], minute=sts[2][1], tzinfo=tz)
|
||||
if start_time < now <= end_time:
|
||||
status = sts[0]
|
||||
if opt.show_help:
|
||||
status += f" | {opt.display_prefix}help"
|
||||
|
||||
await bot.change_presence(activity=discord.Game(name=status))
|
||||
|
||||
@@ -210,6 +215,8 @@ async def _ensure_activity_time():
|
||||
@tasks.loop(minutes=5)
|
||||
async def _ensure_activity_random():
|
||||
status = random.choice(opt.statuses)
|
||||
if opt.show_help:
|
||||
status += f" | {opt.display_prefix}help"
|
||||
|
||||
await bot.change_presence(activity=discord.Game(name=status))
|
||||
|
||||
@@ -217,6 +224,8 @@ async def _ensure_activity_random():
|
||||
@tasks.loop(minutes=5)
|
||||
async def _ensure_activity_fixed():
|
||||
status = opt.statuses[0]
|
||||
if opt.show_help:
|
||||
status += f" | {opt.display_prefix}help"
|
||||
|
||||
await bot.change_presence(activity=discord.Game(name=status))
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
discord.py
|
||||
ctyparser
|
||||
beautifulsoup4
|
||||
lxml
|
||||
pytz
|
||||
discord.py==1.3.4
|
||||
ctyparser==2.0.0.post1
|
||||
beautifulsoup4==4.9.1
|
||||
lxml==4.5.2
|
||||
pytz==2020.1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Information about callsigns for the vanity prefixes command in hamcog.
|
||||
Information about callsigns for the prefixes command in hamcog.
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
@@ -7,50 +7,12 @@ This file is part of discord-qrmbot and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
from .callsigninfos import (us, ca)
|
||||
from common import CallsignInfoData
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
us_calls_title = "Valid US Vanity Callsigns"
|
||||
us_calls_desc = ("#x# is the number of letters in the prefix and suffix of a callsign. "
|
||||
"E.g., WY4RC would be a 2x2 callsign, with prefix WY and suffix RC.")
|
||||
us_calls = OrderedDict([("**Group A** (Extra Only)", ("**Any:** K, N, W (1x2)\n"
|
||||
" AA-AL, KA-KZ, NA-NZ, WA-WZ (2x1)\n"
|
||||
" AA-AL (2x2)\n"
|
||||
"*Except*\n"
|
||||
"**Alaska:** AL, KL, NL, WL (2x1)\n"
|
||||
"**Caribbean:** KP, NP, WP (2x1)\n"
|
||||
"**Pacific:** AH, KH, NH, WH (2x1)")),
|
||||
("**Group B** (Advanced and Extra Only)", ("**Any:** KA-KZ, NA-NZ, WA-WZ (2x2)\n"
|
||||
"*Except*\n"
|
||||
"**Alaska:** AL (2x2)\n"
|
||||
"**Caribbean:** KP (2x2)\n"
|
||||
"**Pacific:** AH (2x2)")),
|
||||
("**Group C** (Technician, General, Advanced, Extra Only)", ("**Any Region:** K, N, W (1x3)\n"
|
||||
"*Except*\n"
|
||||
"**Alaska:** KL, NL, WL (2x2)\n"
|
||||
"**Caribbean:** NP, WP (2x2)\n"
|
||||
"**Pacific:** KH, NH, WH (2x2)")),
|
||||
("**Group D** (Any License Class)", ("**Any Region:** KA-KZ, WA-WZ (2x3)\n"
|
||||
"*Except*\n"
|
||||
"**Alaska:** KL, WL (2x3)\n"
|
||||
"**Caribbean:** KP, WP (2x3)\n"
|
||||
"**Pacific:** KH, WH (2x3)")),
|
||||
("**Unavailable**", ("- KA2AA-KA9ZZ: US Army in Japan\n"
|
||||
"- KC4AAA-KC4AAF: NSF in Antartica\n"
|
||||
"- KC4USA-KC4USZ: US Navy in Antartica\n"
|
||||
"- KG4AA-KG4ZZ: US Navy in Guantanamo Bay\n"
|
||||
"- KL9KAA-KL9KHZ: US military in Korea\n"
|
||||
"- KC6AA-KC6ZZ: Former US (Eastern and Western Caroline Islands), "
|
||||
"now Federated States of Micronesia (V6) and Republic of Palau (T8)\n"
|
||||
"- KX6AA-KX6ZZ: Former US (Marshall Islands), "
|
||||
"now Republic of the Marshall Islands (V73)\n"
|
||||
"- Any suffix SOS or QRA-QUZ\n"
|
||||
"- Any 2x3 with X as the first suffix letter\n"
|
||||
"- Any 2x3 with AF, KF, NF, or WF prefix and suffix EMA: FEMA\n"
|
||||
"- Any 2x3 with AA-AL, NA-NZ, WC, WK, WM, WR, or WT prefix: \"Group X\"\n"
|
||||
"- Any 2x1, 2x2, or 2x3 with KP, NP, WP prefix and 0, 6, 7, 8, 9 number\n"
|
||||
"- Any 1x1 callsign: Special Event"))])
|
||||
|
||||
# format: country: (title, description, text)
|
||||
options = {"us": (us_calls_title, us_calls_desc, us_calls)}
|
||||
options = {
|
||||
"us": CallsignInfoData([us.title, us.desc, us.calls, us.emoji]),
|
||||
"ca": CallsignInfoData([ca.title, ca.desc, ca.calls, ca.emoji]),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Callsign info for various countries
|
||||
"""
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
Information about callsigns for the CA prefixes command in hamcog.
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrmbot and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
|
||||
title = "Canadian Callsign Rules"
|
||||
emoji = "🇨🇦"
|
||||
desc = ("Canadian operators are limited to callsigns with the prefixes of their address' province/territory. "
|
||||
"Initially, operators can choose a callsign with a 3-letter suffix. "
|
||||
"Later on, they can apply to change or for additional callsigns. "
|
||||
"Operators can only hold one 2-letter suffix callsign, but many 3-letter suffix callsigns. "
|
||||
"If the number of 2-letter suffix callsigns exceeds 80% of the total available, "
|
||||
"operators can only choose a 2-letter suffix after holding a license for 5 years. "
|
||||
"If the operator is a family member of a deceased operator, they are not bound by this restriction. "
|
||||
"Data from [ISED Canada (RIC-9)](https://www.ic.gc.ca/eic/site/smt-gst.nsf/eng/sf02102.html).")
|
||||
calls = {
|
||||
"Provinces": (
|
||||
"**Nova Scotia:** VE1 and VA1\n"
|
||||
"**Québec:** VE2 and VA2\n"
|
||||
"**Ontario:** VE3 and VA3\n"
|
||||
"**Manitoba:** VE4 and VA4\n"
|
||||
"**Saskatchewan:** VE5 and VA5\n"
|
||||
"**Alberta:** VE6 and VA6\n"
|
||||
"**British Columbia:** VE7 and VA7\n"
|
||||
"**New Brunswick:** VE9\n"
|
||||
"**Newfoundland:** VO1\n"
|
||||
"**Labrador:** VO2\n"
|
||||
"**Prince Edward Island:** VY2\n"
|
||||
),
|
||||
"Territories": (
|
||||
"**Northwest Territories:** VE8\n"
|
||||
"**Nunavut:** VY0\n"
|
||||
"**Yukon:** VY1\n"
|
||||
),
|
||||
"Other": (
|
||||
"**International Waters:** VE0\n"
|
||||
"**Government of Canada:** VY9\n"
|
||||
"**Sable Island:** CY0\n"
|
||||
"**St-Paul Island:** CY9\n"
|
||||
),
|
||||
"Special Event": "Various prefixes in the ranges: CF-CK, CY-CZ, VA-VG, VO, VX-VY, XJ-XO"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Information about callsigns for the US prefixes command in hamcog.
|
||||
---
|
||||
Copyright (C) 2019-2020 Abigail Gold, 0x5c
|
||||
|
||||
This file is part of discord-qrmbot and is released under the terms of
|
||||
the GNU General Public License, version 2.
|
||||
"""
|
||||
|
||||
|
||||
title = "US Callsign Rules"
|
||||
emoji = "🇺🇸"
|
||||
desc = ("#x# is the number of letters in the prefix and suffix of a callsign. "
|
||||
"E.g., WY4RC would be a 2x2 callsign, with prefix WY and suffix RC.")
|
||||
calls = {
|
||||
"**Group A** (Extra Only)": ("**Any:** K, N, W (1x2)\n"
|
||||
" AA-AL, KA-KZ, NA-NZ, WA-WZ (2x1)\n"
|
||||
" AA-AL (2x2)\n"
|
||||
"*Except*\n"
|
||||
"**Alaska:** AL, KL, NL, WL (2x1)\n"
|
||||
"**Caribbean:** KP, NP, WP (2x1)\n"
|
||||
"**Pacific:** AH, KH, NH, WH (2x1)"),
|
||||
"**Group B** (Advanced and Extra Only)": ("**Any:** KA-KZ, NA-NZ, WA-WZ (2x2)\n"
|
||||
"*Except*\n"
|
||||
"**Alaska:** AL (2x2)\n"
|
||||
"**Caribbean:** KP (2x2)\n"
|
||||
"**Pacific:** AH (2x2)"),
|
||||
"**Group C** (Technician, General, Advanced, Extra Only)": ("**Any Region:** K, N, W (1x3)\n"
|
||||
"*Except*\n"
|
||||
"**Alaska:** KL, NL, WL (2x2)\n"
|
||||
"**Caribbean:** NP, WP (2x2)\n"
|
||||
"**Pacific:** KH, NH, WH (2x2)"),
|
||||
"**Group D** (Any License Class)": ("**Any Region:** KA-KZ, WA-WZ (2x3)\n"
|
||||
"*Except*\n"
|
||||
"**Alaska:** KL, WL (2x3)\n"
|
||||
"**Caribbean:** KP, WP (2x3)\n"
|
||||
"**Pacific:** KH, WH (2x3)"),
|
||||
"**Unavailable**": ("- KA2AA-KA9ZZ: US Army in Japan\n"
|
||||
"- KC4AAA-KC4AAF: NSF in Antartica\n"
|
||||
"- KC4USA-KC4USZ: US Navy in Antartica\n"
|
||||
"- KG4AA-KG4ZZ: US Navy in Guantanamo Bay\n"
|
||||
"- KL9KAA-KL9KHZ: US military in Korea\n"
|
||||
"- KC6AA-KC6ZZ: Former US (Eastern and Western Caroline Islands), "
|
||||
"now Federated States of Micronesia (V6) and Republic of Palau (T8)\n"
|
||||
"- KX6AA-KX6ZZ: Former US (Marshall Islands), "
|
||||
"now Republic of the Marshall Islands (V73)\n"
|
||||
"- Any suffix SOS or QRA-QUZ\n"
|
||||
"- Any 2x3 with X as the first suffix letter\n"
|
||||
"- Any 2x3 with AF, KF, NF, or WF prefix and suffix EMA: FEMA\n"
|
||||
"- Any 2x3 with AA-AL, NA-NZ, WC, WK, WM, WR, or WT prefix: \"Group X\"\n"
|
||||
"- Any 2x1, 2x2, or 2x3 with KP, NP, WP prefix and 0, 6, 7, 8, 9 number\n"
|
||||
"- Any 1x1 callsign: Special Event")
|
||||
}
|
||||
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"arrl": ["arrl-rac.png", "ARRL Sections", "ARRL Sections", "", "", "🇺🇸"],
|
||||
"rac": ["arrl-rac.png", "RAC Sections", "RAC Sections", "", "", "🇨🇦"],
|
||||
"arrl": ["arrl-rac.png", "ARRL Sections", "ARRL Sections", "", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🇺🇸"],
|
||||
"rac": ["arrl-rac.png", "RAC Sections", "RAC Sections", "", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🇨🇦"],
|
||||
"cn": ["cn.png", "China's Prefixes", "Map of prefix regions in China", "", "CRAC", "🇨🇳"],
|
||||
"us": ["us.png", "USA's Prefixes", "Map of prefix regions in the USA", "", "*[ARRL WAS Map](https://www.arrl.org/was-forms)* [[PDF]](http://www.arrl.org/files/file/Awards%20Application%20Forms/WASmap_Color.pdf)", "🇺🇸"]
|
||||
"us": ["us.png", "USA's Prefixes", "Map of prefix regions in the USA", "", "*[ARRL WAS Map](https://www.arrl.org/was-forms)* [[PDF]](http://www.arrl.org/files/file/Awards%20Application%20Forms/WASmap_Color.pdf)", "🇺🇸"],
|
||||
"ca": ["ca.png", "Canada's Prefixes", "Map of the prefix regions in Canada", "", "[Denelson83 (Wikimedia Commons)](https://commons.wikimedia.org/wiki/File:Amateur_radio_prefixes_in_Canada.svg)", "🇨🇦"],
|
||||
"ituz": ["itu-zones.png", "ITU Zones", "ITU Zones", "", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🇺🇳"],
|
||||
"itur": ["itu-regions.png", "ITU Regions", "ITU Regions", "These are also used by the IARU for their regions.", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🇺🇳"],
|
||||
"cq": ["cq-zones.png", "CQ Zones", "CQ Zones", "These are used for the CQWW contest.", "[EI8IC](https://www.mapability.com/ei8ic/maps/maps.php)", "🌐"]
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 304 KiB |
@@ -36,3 +36,43 @@ phonetics = {
|
||||
"y": "yankee",
|
||||
"z": "zulu"
|
||||
}
|
||||
|
||||
pweights = {
|
||||
"A": 2,
|
||||
"B": 2,
|
||||
"C": 2,
|
||||
"D": 2,
|
||||
"E": 2,
|
||||
"F": 2,
|
||||
"G": 1,
|
||||
"H": 2,
|
||||
"I": 3,
|
||||
"J": 3,
|
||||
"K": 2,
|
||||
"L": 2,
|
||||
"M": 1,
|
||||
"N": 3,
|
||||
"O": 2,
|
||||
"P": 2,
|
||||
"Q": 2,
|
||||
"R": 3,
|
||||
"S": 3,
|
||||
"T": 2,
|
||||
"U": 3,
|
||||
"V": 2,
|
||||
"W": 2,
|
||||
"X": 2,
|
||||
"Y": 2,
|
||||
"Z": 2,
|
||||
"0": 2,
|
||||
"1": 1,
|
||||
"2": 1,
|
||||
"3": 1,
|
||||
"4": 1,
|
||||
"5": 1,
|
||||
"6": 1,
|
||||
"7": 2,
|
||||
"8": 1,
|
||||
"9": 2,
|
||||
"/": 1,
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ Settings and options for the bot.
|
||||
|
||||
# The prefix for the bot (str). Define a list of stings for multiple prefixes.
|
||||
# ie: `["?", "!", "pls "]`
|
||||
prefix = "?"
|
||||
prefix = ["? ", "?"]
|
||||
|
||||
# The prefix to use for display purposes (ex: status message).
|
||||
display_prefix = "?"
|
||||
|
||||
# Whether the bot should print full stacktraces for normal exceptions: `True`,
|
||||
# or be nice and only print small messages: `False` (the default).
|
||||
@@ -27,7 +30,7 @@ debug = False
|
||||
owners_uids = (200102491231092736,)
|
||||
|
||||
# The extensions to load when running the bot.
|
||||
exts = ["ae7q", "base", "fun", "grid", "ham", "image", "lookup", "morse", "qrz", "study", "weather"]
|
||||
exts = ["ae7q", "base", "fun", "grid", "ham", "image", "lookup", "morse", "qrz", "study", "weather", "dbconv"]
|
||||
|
||||
# Either "time", "random", or "fixed" (first item in statuses)
|
||||
status_mode = "fixed"
|
||||
@@ -46,6 +49,9 @@ time_statuses = [("with lids on 3.840", (00, 00), (6, 00)),
|
||||
("with lids on 7.200", (18, 00), (20, 00)),
|
||||
("with lids on 3.840", (20, 00), (23, 59))]
|
||||
|
||||
# append " | {display_prefix}help" to the Discord playing status
|
||||
show_help = False
|
||||
|
||||
# Emoji IDs and keywords for emoji reactions
|
||||
# Use the format {emoji_id (int): ("tuple", "of", "lowercase", "keywords")}
|
||||
msg_reacts = {}
|
||||
|
||||