Compare commits

...

71 Commits

Author SHA1 Message Date
classabbyamp 4b55ab49b7 .github/workflows/docker.yml: fix labels, again
Docker Build and Deploy / Build and push docker images (push) Has been cancelled Details
Linting / flake8 (push) Has been cancelled Details
2023-12-15 14:47:33 -05:00
classabbyamp cf378a2ef4 CHANGELOG.md: relbump 2023-12-15 14:30:59 -05:00
classabbyamp 13a8a63300 info.py: use pomelo usernames, relbump 2023-12-15 14:30:59 -05:00
classabbyamp 23619949d7 .github/workflows/docker.yml: fix tagging latest
turns out, most of this is handled automatically.
see https://github.com/docker/metadata-action#latest-tag
2023-12-15 14:20:05 -05:00
classabbyamp 444687bd12 .github/workflows/docker.yml: use metadata action, run on PRs
Docker Build and Deploy / Build and push docker images (push) Has been cancelled Details
Linting / flake8 (push) Has been cancelled Details
2023-12-11 08:21:33 -05:00
classabbyamp 86da8d135a Dockerfile: use new void container, python3.11 2023-12-11 08:21:33 -05:00
classabbyamp 67add85a7a run.sh: shellcheck 2023-12-11 08:21:33 -05:00
0x5c abdc5ebacb
Merge pull request #476 from miaowware/fix/metar
Docker Build and Deploy / Build and push docker images (push) Waiting to run Details
Linting / flake8 (push) Waiting to run Details
Fix metar + version bumps
2023-12-10 14:17:35 -05:00
0x5c a5cbb5a09a
exts/land_weather: switch to the new aviationweather.gov API
Fixes #475
2023-12-10 07:47:59 -05:00
0x5c ce99cc194e
bump pydantic to version 2 2023-12-10 07:19:45 -05:00
0x5c f8d7316071
bump pycord to pre-release v5 2023-12-10 07:16:41 -05:00
0x5c 9feeb01e42
Merge pull request #474 from cschmittiey/hamqsl-https
hamqsl.com has moved to https
2023-04-22 02:55:08 -04:00
Caleb Smith fcb682ec4a hamqsl.com has moved to https 2023-04-22 00:53:03 -06:00
Judd West c8a1128927 Add NOAA D-RAP map to propagation plugin 2023-01-30 05:25:01 -05:00
classabbyamp df08cefe25 exts/callsign: fix mail qsl display 2023-01-29 15:40:43 -05:00
0x5c cf93773a3c
Merge pull request #471 from miaowware/rel-2.9.1
rel 2.9.1
2023-01-29 00:52:48 -05:00
classabbyamp 642b49041a
bump version 2.9.1 2023-01-29 00:44:29 -05:00
classabbyamp e95f991300
bump copyright 2023-01-29 00:43:56 -05:00
0x5c 56ae14a5c3
Merge pull request #469 from miaowware/new-clt
exts/callsign: unworkaround some things solved in CLT 1.1.0
2023-01-29 00:34:04 -05:00
classabbyamp 30c6e96883
exts/callsign: unworkaround some things solved in CLT 1.1.0
fixes #466
2023-01-29 00:23:20 -05:00
0x5c 44a6905f7b
Merge pull request #468 from miaowware/embed-avatar
Embed factory/pycord fixes
2023-01-28 23:41:01 -05:00
classabbyamp d7de78e582
common.py: use tz-aware datetime for proper timestamp display
https://docs.pycord.dev/en/stable/api/data_classes.html#discord.Embed.timestamp
2023-01-28 20:11:23 -05:00
classabbyamp b000c9173e
common.py: don't error when creating embeds for users without avatars
behaviour changed in pycord 2.0:
https://docs.pycord.dev/en/stable/api/models.html#discord.User.display_avatar

fixes #467
2023-01-28 20:11:00 -05:00
0x5c 5460dd811b
Merge pull request #465 from miaowware/fix-rel-workflow
.github/workflows/release.yml: move to ncipollo/create-release action
2023-01-13 18:57:54 -05:00
classabbyamp a00d613430
.github/workflows/release.yml: move to ncipollo/create-release action
fixes #464
2023-01-13 12:42:26 -05:00
0x5c 6b0cdb6249
Merge pull request #463 from miaowware/update-changelog
Bump version to 2.9.0
2023-01-13 03:51:01 -05:00
0x5c 4eed94b55b
Bump version to 2.9.0 2023-01-13 03:48:45 -05:00
0x5c 3110961a3a exts/propagation: Fix ?solarweather no image bug
Back to the ugly hack of downloading the image and uploading it to discord.

Fixes #461
2023-01-13 03:44:27 -05:00
0x5c a4c8a056ac First steps for move from aiohttp to httpx 2023-01-13 03:44:27 -05:00
classabbyamp 9368ccd9e2 exts/study: fix in DMs
fixes #442
2023-01-13 01:14:54 -05:00
0x5c 8efd958314
Merge pull request #460 from miaowware/delete-solar-aliases
exts/propagation: Remove deprecated ?solarweather aliases
2023-01-13 01:09:57 -05:00
0x5c 4803bf89b2
exts/propagation: Remove deprecated ?solarweather aliases
Fixes #332
2023-01-13 01:03:37 -05:00
classabbyamp c82216cae6 Revert "update changelog, bump release"
This reverts commit 1b0b244f99.
2023-01-13 00:48:06 -05:00
classabbyamp 1650cd50dc exts/callsign: simplify stringification, fix data validation 2023-01-12 22:24:06 -05:00
classabbyamp 1b0b244f99 update changelog, bump release 2023-01-01 16:22:42 -05:00
classabbyamp 5db77f78d9 exts/callsign: convert to callsignlookuptools (qrz only for now) 2023-01-01 16:22:42 -05:00
classabbyamp c7ea5e0998 migrate to pycord 2023-01-01 16:22:42 -05:00
classabbyamp adffd82127 utils/resources_manager.py: use httpx instead of requests 2023-01-01 16:22:42 -05:00
classabbyamp 970159e81b Makefile: update default python version to 3.11 2023-01-01 16:22:42 -05:00
0x5c f5aeefc934
Merge pull request #454 from miaowware/token-perms
.github/workflows/docker.yml: add package write perms
2022-10-12 01:08:29 -04:00
classabbyamp aac9262469
.github/workflows/docker.yml: add package write perms 2022-10-12 00:56:56 -04:00
0x5c b472cdfa25
Merge pull request #453 from miaowware/xbps-update
Dockerfile: ensure system update works
2022-10-11 19:57:13 -04:00
classabbyamp 585cae8b97
Dockerfile: ensure system update works 2022-10-11 18:17:53 -04:00
0x5c c3fbd3e719
Merge pull request #452 from miaowware/set-output
.github/workflows/docker.yml: remove deprecated set-output
2022-10-11 18:14:12 -04:00
classabbyamp 7eadb50b96
exts/dbconv: fix lint 2022-10-11 18:12:55 -04:00
classabbyamp 98642c099d
.github/workflows/docker.yml: remove deprecated set-output
https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
2022-10-11 14:24:03 -04:00
0x5c ef6f01d1a3
Merge pull request #450 from miaowware/bump
Bump version to 2.8.0
2022-06-24 17:44:24 -04:00
0x5c 91c5217d24
Bump version to 2.8.0 2022-06-24 17:42:41 -04:00
classabbyamp 4659cf2a48 exts/ae7q: remove extension
fixes #448
2022-06-24 17:17:26 -04:00
0x5c d33dad9f89 Bump version to 2.7.6 2022-06-13 12:58:18 -04:00
0x5c be083d2cc8
Merge pull request #446 from miaowware/muf-fof2-bug
Fix aiohttp/apache http2 bug in ?muf and ?fof2
2022-06-13 09:00:52 -04:00
0x5c e2d1d1fc87
Fix aiohttp/apache http2 bug in ?muf and ?fof2
For more info, https://github.com/aio-libs/aiohttp/issues/3904
It is not possible to fix it by bumping aiohttp since it is pinned by another
dependency.
2022-06-13 08:45:17 -04:00
classabbyamp 68eaeff476 update to 2.7.5 2022-06-08 21:16:14 -07:00
0x5c f690ebb357
Merge pull request #444 from miaowware/ci-lint
[CI/linting]: Change trigger and flake8 output
2022-05-17 21:23:46 -04:00
0x5c 51e571b97d
[CI/linting]: Change trigger and flake8 output
Fixes #443
2022-05-17 21:20:10 -04:00
classabbyamp 85ac05c337
Merge pull request #438 from miaowware/clog-enforce-workflow
CI updates
2021-11-06 00:22:55 -04:00
classabbyamp 718b2a7a80
add python 3.10 to linting checks 2021-11-05 23:34:26 -04:00
classabbyamp 0189db8792
add workflow to enforce changelog updating before merge 2021-11-05 23:31:14 -04:00
classabbyamp 80d6a989cc
move to new void docker image, clean up dockerfile (#436)
Co-authored-by: 0x5c <dev@0x5c.io>
2021-11-05 17:51:46 -04:00
classabbyamp 8f1782dcc0
ensure docker image id is lowercase (#437)
fixes #413
2021-11-05 17:31:45 -04:00
classabbyamp aefca97e4f
bump to v2.7.4 and update discord.py (#434) 2021-10-07 03:07:16 -04:00
classabbyamp bbd646a7ec
move docker image to void linux (#435) 2021-10-07 02:57:36 -04:00
classabbyamp 8433a7ade0
fix function signature of filter_commands() to ignore other args (#433)
discord.py changed the signature to add kwargs which are not used by
qrm's help command

fixes #432
2021-10-07 01:25:02 -04:00
classabbyamp de0e25b09a
better ghcr login (#431) 2021-07-01 09:58:20 -04:00
classabbyamp 4fb1320b2d
add github sponsor link to donate command (#430) 2021-06-30 01:49:18 -04:00
0x5c 36acda1666
Merge pull request #429 from miaowware/licence
Changed the licence to LiLiQ-Rplus-1.1
2021-06-26 20:30:25 -04:00
0x5c 04ccd807cd
Changed the licence to LiLiQ-Rplus-1.1
Fixes #387
2021-06-26 20:23:55 -04:00
classabbyamp aa7b72634b
Merge pull request #427 from miaowware/lint-dedup
update linting workflow to not run multiple times unnecessarily
2021-05-25 09:37:02 -04:00
Abigail G 9ee42529e2
update linting workflow to not run multiple times unnecessarily 2021-05-13 20:51:46 -04:00
classabbyamp 74df3ed1f1
Merge pull request #424 from miaowware/273-help-efix
fix issue with help cmd not showing all cmds, bump to 2.7.3
2021-04-12 19:38:27 -04:00
Abigail G e7a1a4e5de
fix issue with help cmd not showing all cmds, bump to 2.7.3 2021-04-12 19:36:03 -04:00
42 changed files with 605 additions and 1099 deletions

12
.github/workflows/checks.yml vendored Normal file
View File

@ -0,0 +1,12 @@
name: "Checks"
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dangoslen/changelog-enforcer@v2

View File

@ -3,6 +3,10 @@
name: Docker Build and Deploy
on:
workflow_dispatch:
pull_request:
branches:
- master
push:
# Publish `master` as Docker `dev` image.
branches:
@ -11,61 +15,54 @@ on:
tags:
- v*
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
docker:
name: Build and push docker images
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{ github.ref }}
uses: classabbyamp/treeless-checkout-action@v1
- name: Write ref to file
if: ${{ github.event_name != 'pull_request' }}
run: git rev-list -n 1 $GITHUB_REF > ./git_commit
- name: Build image
id: build_image
run: |
IMAGE_NAME=${GITHUB_REPOSITORY#*/}
echo ::set-output name=image_name::$IMAGE_NAME
docker build . --file Dockerfile -t $IMAGE_NAME
- name: Log into Github Package Registry
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Tag image
id: tag_image
run: |
IMAGE_NAME=${{ steps.build_image.outputs.image_name }}
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
echo IMAGE_ID=$IMAGE_ID
echo ::set-output name=image_id::$IMAGE_ID
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# if version is master, set version to dev
[[ "$VERSION" == "master" ]] && VERSION=dev
echo VERSION=$VERSION
echo ::set-output name=version::$VERSION
# tag dev or x.x.x
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
# tag latest if not a dev release
[[ "$VERSION" != "dev" ]] && docker tag $IMAGE_NAME $IMAGE_ID:latest || true
- name: Push images to registry
run: |
[[ "${{ steps.tag_image.outputs.version }}" != "dev" ]] && docker push ${{ steps.tag_image.outputs.image_id }}:latest || true
docker push ${{ steps.tag_image.outputs.image_id }}:${{ steps.tag_image.outputs.version }}
- name: Deploy official images
id: deploy_images
uses: satak/webrequest-action@v1
- name: Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
url: ${{ secrets.DEPLOY_URL }}
method: POST
headers: '{"Authentication": "Token ${{ secrets.DEPLOY_TOKEN }}"}'
payload: '{"version": "${{ steps.tag_image.outputs.version }}"}'
images: |
ghcr.io/${{ github.repository }}
tags: |
type=sha,prefix=
type=raw,value=dev,enable={{is_default_branch}}
type=match,pattern=v(.*),group=1
labels: |
org.opencontainers.image.authors=classabbyamp and 0x5c
org.opencontainers.image.url=https://github.com/miaowware/qrm2
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.vendor=miaowware
org.opencontainers.image.title=qrm2
org.opencontainers.image.description=Discord bot with ham radio functions
org.opencontainers.image.licenses=LiLiQ-Rplus-1.1
- name: Login to Github Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,21 +1,22 @@
name: Linting
on: [push,pull_request]
on:
push:
branches:
- master
pull_request:
jobs:
flake8_py3:
flake8:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-python@v1
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: 3.9
python-version: "3.9"
architecture: x64
- name: Install flake8
run: pip install flake8
- name: Run flake8
uses: suo/flake8-github-action@releases/v1
with:
checkName: 'flake8_py3' # NOTE: this needs to be the same as the job name
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: flake8 --format='::error title=flake8,file=%(path)s,line=%(row)d,col=%(col)d::[%(code)s] %(text)s'

View File

@ -12,6 +12,8 @@ jobs:
release:
name: Create Release
runs-on: ubuntu-20.04
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v2
@ -46,12 +48,10 @@ jobs:
- name: Publish Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: ncipollo/release-action@v1
with:
tag_name: ${{ env.tag_version }}
release_name: ${{ env.tag_subject }}
tag: ${{ env.tag_version }}
name: ${{ env.tag_subject }}
body: |
${{ env.tag_body }}

View File

@ -7,6 +7,63 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [2.9.2] - 2023-12-15
### Added
- `?drapmap` command to display NOAA D Region Absorption Predictions map.
- Support for the new username format.
### Fixed
- Issue where `?solarweather` would not show a picture (#474).
- Issue where `?metar` and `?taf` failed to fetch data (#475).
## [2.9.1] - 2023-01-29
### Fixed
- Issue where embeds would not work for users without avatars (#467).
- Issue where embeds would show the wrong timezone.
- Several issues with `?call` caused by issues in a library (#466).
## [2.9.0] - 2023-01-13
### Changed
- Migrated to Pycord.
### Removed
- Long-deprecated aliases for `?solarweather`.
### Fixed
- Issue where ?hamstudy would not work in direct messages (#442).
- Issue where `?solarweather` would not show a picture (#461).
## [2.8.0] - 2022-06-24
### Removed
- `?ae7q` command (#448).
## [2.7.6] - 2022-06-13
### Fixed
- Issue where `?muf` and `?fof2` would fail with an aiohttp error.
## [2.7.5] - 2022-06-08
### Changed
- Bumped ctyparser to 2.2.1.
## [2.7.4] - 2021-10-07
### Added
- a new way to support qrm's development.
### Changed
- Changed the licence to LiLiQ-Rplus-1.1.
- Moved official Docker image to Void Linux.
- Bumped discord.py to 1.7.3.
### Fixed
- Issue where the help command errored.
## [2.7.3] - 2021-04-12
### Fixed
- Issue where `?help` might not display all commands.
## [2.7.2] - 2021-04-12
### Fixed
- Issue where `?help` might not work for all people.
@ -198,7 +255,15 @@ 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.7.2...HEAD
[Unreleased]: https://github.com/miaowware/qrm2/compare/v2.9.2...HEAD
[2.9.2]: https://github.com/miaowware/qrm2/releases/tag/v2.9.2
[2.9.1]: https://github.com/miaowware/qrm2/releases/tag/v2.9.1
[2.9.0]: https://github.com/miaowware/qrm2/releases/tag/v2.9.0
[2.8.0]: https://github.com/miaowware/qrm2/releases/tag/v2.8.0
[2.7.6]: https://github.com/miaowware/qrm2/releases/tag/v2.7.6
[2.7.5]: https://github.com/miaowware/qrm2/releases/tag/v2.7.5
[2.7.4]: https://github.com/miaowware/qrm2/releases/tag/v2.7.4
[2.7.3]: https://github.com/miaowware/qrm2/releases/tag/v2.7.3
[2.7.2]: https://github.com/miaowware/qrm2/releases/tag/v2.7.2
[2.7.1]: https://github.com/miaowware/qrm2/releases/tag/v2.7.1
[2.7.0]: https://github.com/miaowware/qrm2/releases/tag/v2.7.0

339
COPYING
View File

@ -1,339 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@ -1,25 +1,31 @@
FROM python:3.9-slim
FROM ghcr.io/void-linux/void-musl-full
COPY . /app
WORKDIR /app
ENV PYTHON_BIN python3
ARG REPOSITORY=https://repo-fastly.voidlinux.org/current
ARG PKGS="cairo libjpeg-turbo"
ARG UID 1000
ARG GID 1000
RUN \
apt-get update && \
echo "**** install runtime packages ****" && \
apt-get install -y --no-install-recommends \
libcairo2 \
libjpeg62-turbo \
python-lxml \
&& \
echo "**** update system ****" && \
xbps-install -Suy xbps -R ${REPOSITORY} && \
xbps-install -uy -R ${REPOSITORY} && \
echo "**** install system packages ****" && \
xbps-install -y -R ${REPOSITORY} ${PKGS} python3.11 && \
echo "**** install pip packages ****" && \
pip3 install -U pip setuptools wheel && \
pip3 install -r requirements.txt && \
python3.11 -m venv botenv && \
botenv/bin/pip install -U pip setuptools wheel && \
botenv/bin/pip install -r requirements.txt && \
echo "**** clean up ****" && \
rm -rf \
/root/.cache \
/tmp/* \
/var/lib/apt/lists/*
/var/cache/xbps/*
CMD ["/bin/sh", "run.sh", "--pass-errors", "--no-botenv"]
ENV PYTHONUNBUFFERED 1
USER $UID:$GID
CMD ["/bin/sh", "run.sh", "--pass-errors"]

178
LICENCE Normal file
View File

@ -0,0 +1,178 @@
SPDX-License-Identifier: LiLiQ-Rplus-1.1
---- English version follows ----
Licence Libre du Québec Réciprocité forte (LiLiQ-R+)
Version 1.1
1. Préambule
Cette licence s'applique à tout logiciel distribué dont le titulaire du droit d'auteur précise qu'il est sujet aux termes de la Licence Libre du Québec Réciprocité forte (LiLiQ-R+) (ci-après appelée la « licence »).
2. Définitions
Dans la présente licence, à moins que le contexte n'indique un sens différent, on entend par:
« concédant » : le titulaire du droit d'auteur sur le logiciel, ou toute personne dûment autorisée par ce dernier à accorder la présente licence;
« contributeur » : le titulaire du droit d'auteur ou toute personne autorisée par ce dernier à soumettre au concédant une contribution. Un contributeur dont sa contribution est incorporée au logiciel est considéré comme un concédant en regard de sa contribution;
« contribution » : tout logiciel original, ou partie de logiciel original soumis et destiné à être incorporé dans le logiciel;
« distribution » : le fait de délivrer une copie du logiciel;
« licencié » : toute personne qui possède une copie du logiciel et qui exerce les droits concédés par la licence;
« logiciel » : une œuvre protégée par le droit d'auteur, telle qu'un programme d'ordinateur et sa documentation, pour laquelle le titulaire du droit d'auteur a précisé qu'elle est sujette aux termes de la présente licence;
« logiciel dérivé » : tout logiciel original réalisé par un licencié, autre que le logiciel ou un logiciel modifié, qui produit ou reproduit la totalité ou une partie importante du logiciel;
« logiciel modifié » : toute modification par un licencié de l'un des fichiers source du logiciel ou encore tout nouveau fichier source qui incorpore le logiciel ou une partie importante de ce dernier.
3. Licence de droit d'auteur
Sous réserve des termes de la licence, le concédant accorde au licencié une licence non exclusive et libre de redevances lui permettant dexercer les droits suivants sur le logiciel :
 Produire ou reproduire la totalité ou une partie importante;
 Exécuter ou représenter la totalité ou une partie importante en public;
 Publier la totalité ou une partie importante.
Cette licence est accordée sans limite territoriale et sans limite de temps.
L'exercice complet de ces droits est sujet à la distribution par le concédant du code source du logiciel, lequel doit être sous une forme permettant d'y apporter des modifications. Le concédant peut aussi distribuer le logiciel accompagné d'une offre de distribuer le code source du logiciel, sans frais supplémentaires, autres que ceux raisonnables afin de permettre la livraison du code source. Cette offre doit être valide pendant une durée raisonnable.
4. Distribution
Le licencié peut distribuer des copies du logiciel, d'un logiciel modifié ou dérivé, sous réserve de respecter les conditions suivantes :
 Le logiciel doit être accompagné d'un exemplaire de cette licence;
 Si le logiciel a été modifié, le licencié doit en faire la mention, de préférence dans chacun des fichiers modifiés dont la nature permet une telle mention;
 Les étiquettes ou mentions faisant état des droits d'auteur, des marques de commerce, des garanties ou de la paternité concernant le logiciel ne doivent pas être modifiées ou supprimées, à moins que ces étiquettes ou mentions ne soient inapplicables à un logiciel modifié ou dérivé donné.
4.1. Réciprocité
Chaque fois que le licencié distribue le logiciel, le concédant offre au récipiendaire une concession sur le logiciel selon les termes de la présente licence. Le licencié doit offrir une concession selon les termes de la présente licence pour tout logiciel modifié ou dérivé qu'il distribue.
Chaque fois que le licencié distribue le logiciel, un logiciel modifié, ou un logiciel dérivé, ce dernier doit assumer l'obligation d'en distribuer le code source, de la manière prévue au troisième alinéa de l'article 3.
4.2. Compatibilité
Dans la mesure où le licencié souhaite distribuer un logiciel modifié ou dérivé combiné à un logiciel assujetti à une licence compatible, mais dont il ne serait pas possible d'en respecter les termes, le concédant offre, en plus de la présente concession, une concession selon les termes de cette licence compatible.
Un licencié qui est titulaire exclusif du droit d'auteur sur le logiciel assujetti à une licence compatible ne peut pas se prévaloir de cette offre. Il en est de même pour toute autre personne dûment autorisée à sous-licencier par le titulaire exclusif du droit d'auteur sur le logiciel assujetti à une licence compatible.
Est considérée comme une licence compatible toute licence libre approuvée ou certifiée par la Free Software Foundation ou l'Open Source Initiative, dont le niveau de réciprocité est comparable à celui de la présente licence, sans toutefois être moindre, notamment :
 Common Public License Version 1.0 (CPL-1.0)
 Contrat de licence de logiciel libre CeCILL, version 2.1 (CECILL-2.1)
 Eclipse Public License - v 1.0 (EPL-1.0)
 European Union Public License, version 1.1 (EUPL v. 1.1)
 GNU General Public License Version 2 (GNU GPLv2)
 GNU General Public License Version 3 (GNU GPLv3)
5. Contributions
Sous réserve d'une entente distincte, toute contribution soumise par un contributeur au concédant pour inclusion dans le logiciel sera soumise aux termes de cette licence.
6. Marques de commerce
La licence n'accorde aucune permission particulière qui permettrait d'utiliser les marques de commerce du concédant, autre que celle requise permettant d'identifier la provenance du logiciel.
7. Garanties
Sauf mention contraire, le concédant distribue le logiciel sans aucune garantie, aux risques et périls de l'acquéreur de la copie du logiciel, et ce, sans assurer que le logiciel puisse répondre à un besoin particulier ou puisse donner un résultat quelconque.
Sans lier le concédant d'une quelconque manière, rien n'empêche un licencié d'offrir ou d'exclure des garanties ou du support.
8. Responsabilité
Le licencié est responsable de tout préjudice résultant de l'exercice des droits accordés par la licence.
Le concédant ne saurait être tenu responsable du préjudice subi par le licencié ou par des tiers, pour quelque cause que ce soit en lien avec la licence et les droits qui y sont accordés.
9. Résiliation
La présente licence est résiliée de plein droit dès que les droits qui y sont accordés ne sont pas exercés conformément aux termes qui y sont stipulés.
Toutefois, si le défaut est corrigé dans un délai de 30 jours de sa prise de connaissance par la personne en défaut, et qu'il s'agit du premier défaut, la licence est accordée de nouveau.
Pour tout défaut subséquent, le consentement exprès du concédant est nécessaire afin que la licence soit accordée de nouveau.
10. Version de la licence
Le Centre de services partagés du Québec, ses ayants cause ou toute personne qu'il désigne, peuvent diffuser des versions révisées ou modifiées de cette licence. Chaque version recevra un numéro unique. Si un logiciel est déjà soumis aux termes d'une version spécifique, c'est seulement cette version qui liera les parties à la licence.
Le concédant peut aussi choisir de concéder la licence sous la version actuelle ou toute version ultérieure, auquel cas le licencié peut choisir sous quelle version la licence lui est accordée.
11. Divers
Dans la mesure où le concédant est un ministère, un organisme public ou une personne morale de droit public, créés en vertu d'une loi de l'Assemblée nationale du Québec, la licence est régie par le droit applicable au Québec et en cas de contestation, les tribunaux du Québec seront seuls compétents.
La présente licence peut être distribuée sans conditions particulières. Toutefois, une version modifiée doit être distribuée sous un nom différent. Toute référence au Centre de services partagés du Québec, et, le cas échéant, ses ayant cause, doit être retirée, autre que celle permettant d'identifier la provenance de la licence.
----
Québec Free and Open-Source Licence Strong Reciprocity (LiLiQ-R+)
Version 1.1
1. Preamble
This licence applies to any distributed software stipulated by its copyright owner to be subject to the terms of the Québec Free and Open-Source Licence Strong Reciprocity (LiLiQ-R+) (hereinafter referred to as the “licence”).
2. Definitions
Unless the context indicates otherwise, the following terms are used in this licence:
“contribution”: any original software or part of original software submitted and intended to be integrated into the software;
“contributor”: the copyright owner or any person authorized by the copyright owner to submit a contribution to the licensor. A contributor whose contribution is integrated into the software is considered a licensor with respect to that contribution;
“derived software”: any original software developed by a licensee, other than the software or modified software, that produces or reproduces all or a substantial part of the software;
“distribution”: the act of delivering a copy of the software;
“licensee”: any person possessing a copy of the software who exercises the rights granted by the licence;
“licensor”: the software copyright owner or any person duly authorized by the copyright owner to grant this licence;
“modified software”: any modification made by a licensee to one of the softwares source code files, or any new source code file that integrates the software or a substantial part of it;
“software”: a copyright-protected work such as a computer program and its documentation, stipulated by the copyright owner to be subject to the terms of this licence.
3. Copyright licence
Subject to the terms of this licence, the licensor grants the licensee a non-exclusive, royalty-free licence allowing the licensee to exercise the following rights regarding the software:
(1) Produce or reproduce the software or a substantial part thereof;
(2) Perform the software or any substantial part of it in public;
(3) Publish the software or any substantial part of it.
This licence is granted on a world-wide, perpetual basis.
Full exercise of these rights is subject to distribution by the licensor of the software source code in a form allowing it to be modified. The licensor may also distribute the software, along with an offer to distribute the software source code, without additional charges other than reasonable charges for delivery of the source code. That offer must be valid for a reasonable period of time.
4. Distribution
The licensee may distribute copies of the software, modified software or derived software, subject to the following conditions:
(1) The software must be accompanied by a copy of this licence.
(2) If the software has been modified, the licensee must mention this, preferably in every modified file that allows for such a mention.
(3) Software copyright, trademark, warranty or attribution labels or notices must not be modified or removed, unless the labels or notices do not apply to specific modified or derived software.
4.1. Reciprocity
Every time the licensee distributes the software, the licensor grants the recipient an interest in the software in accordance with the terms of this licence. The licensee must grant an interest in accordance with the terms of this licence for any modified or derived software distributed.
Every time the licensee distributes the software, or modified or derived software, the licensee is obliged to distribute its source code in the manner prescribed in the third paragraph of section 3.
4.2. Compatibility
To the extent that the licensee wishes to distribute modified or derived software combined with software subject to a compatible licence whose terms cannot possibly be fulfilled, the licensor offers, in addition to this interest, an interest in compliance with the terms of the compatible licence.
A licensee who is the exclusive copyright owner of the software subject to a compatible licence is not eligible for this offer. The same applies to any other person duly authorized to sub-license by the exclusive copyright owner of the software subject to a compatible licence.
A compatible licence is considered to be any free or open-source licence approved or certified by the Free Software Foundation or the Open Source Initiative, whose level of reciprocity is comparable to that of this licence, without being less so, in particular:
(1) Common Public License Version 1.0 (CPL-1.0)
(2) Contrat de licence de logiciel libre CeCILL, version 2.1 (CECILL-2.1)
(3) Eclipse Public License - v 1.0 (EPL-1.0)
(4) European Union Public License, version 1.1 (EUPL v. 1.1)
(5) GNU General Public License Version 2 (GNU GPLv2)
(6) GNU General Public License Version 3 (GNU GPLv3)
5. Contributions
Subject to a separate agreement, every contribution submitted by a contributor to the licensor for inclusion in the software is subject to the terms of this licence.
6. Trademarks
This licence does not grant any special permission to use the licensors trademarks, except as needed to describe the origin of the software.
7. Warranties
Unless otherwise specified, the licensor distributes the software without any warranty, at the risk of the acquirer of a copy of the software, and without any warranty that the software is suited to any specific need or will yield any specific results.
Without binding the licensor in any way, nothing prevents a licensee from offering or excluding warranties or support.
8. Liability
The licensee is liable for any prejudice resulting from the exercise of the rights granted under the licence.
The licensor cannot be held liable for any prejudice sustained by the licensee or third parties for any reason whatsoever related to the licence and the rights it grants.
9. Termination
This licence is terminated as of right should the rights it grants fail to be exercised in accordance with the terms of the licence.
However, if the failure is remedied within 30 days after its discovery by the person in default and it is the first failure, the licence will be granted once again.
For any subsequent failure, the licensors express consent is required for the licence to be granted once again.
10. Licence version
The Centre de services partagés du Québec, its successors or any person it designates may release revised or modified versions of this licence. Each version will be given a unique number. If software is already subject to the terms of a specific version, the parties to the licence will be bound solely by that version.
The licensor may also choose to grant the licence in its current version or any subsequent version, in which case the licensee may choose the license version to be granted.
11. Miscellaneous
To the extent that the licensor is a government department, public body or legal person established in the public interest and created under a law of the National Assembly of Québec, the licence is governed by the laws applicable in Québec and, in the event of a dispute, the courts of Québec have sole jurisdiction.
This licence may be distributed without any special conditions. However, a modified version must be distributed under a different name. Any reference to the Centre de services partagés du Québec or its successors, where applicable, must be withdrawn, except as needed to describe the origin of the licence.

View File

@ -12,7 +12,7 @@
# Those are the defaults; they can be over-ridden if specified
# at en environment level or as 'make' arguments.
BOTENV ?= botenv
PYTHON_BIN ?= python3.9
PYTHON_BIN ?= python3.11
PIP_OUTPUT ?= -q

View File

@ -23,14 +23,11 @@ This is the easiest method for running the bot without any modifications.
version: '3'
services:
qrm2:
image: "docker.pkg.github.com/miaowware/qrm2/qrm2:latest"
image: "ghcr.io/miaowware/qrm2:latest"
restart: on-failure
volumes:
- "./data:/app/data:rw"
environment:
- PYTHONUNBUFFERED=1
```
*Note that Github's registry requires [a few extra steps](https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages) during the initial setup.*
3. Create a subdirectory named `data`.
@ -64,8 +61,6 @@ This is the easiest method to run the bot with modifications.
restart: on-failure
volumes:
- "./data:/app/data:rw"
environment:
- PYTHONUNBUFFERED=1
```
3. Create a subdirectory named `data`.
@ -112,4 +107,4 @@ This methods is not very nice to use.
Where `[image]` is either of:
- `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.
- `ghcr.io/miaowware/qrm2:latest` if you want to use the prebuilt image.

View File

@ -38,7 +38,7 @@ All issues and requests related to resources (including maps, band charts, data)
## Copyright
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This program is released under the terms of the GNU General Public License,
version 2. See `COPYING` for full license text.
This program is released under the terms of the *Québec Free and Open-Source Licence Strong Reciprocity (LiLiQ-R+)*, version 1.1.
See [`LICENCE`](LICENCE) for full license text (Français / English).

View File

@ -1,10 +1,9 @@
"""
Common tools for the bot.
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
@ -13,16 +12,17 @@ import enum
import json
import re
import traceback
from datetime import datetime
from datetime import datetime, timezone
from pathlib import Path
from types import SimpleNamespace
from typing import Union
import aiohttp
import httpx
import discord
import discord.ext.commands as commands
from discord import Emoji, Reaction, PartialEmoji
from discord import Emoji, PartialEmoji
import data.options as opt
@ -126,12 +126,16 @@ class ImagesGroup(collections.abc.Mapping):
class BotHTTPError(Exception):
"""Raised whan a requests fails (status != 200) in a command."""
def __init__(self, response: aiohttp.ClientResponse):
msg = f"Request failed: {response.status} {response.reason}"
def __init__(self, response: aiohttp.ClientResponse | httpx.Response):
if isinstance(response, aiohttp.ClientResponse):
self.status = response.status
self.reason = response.reason
else:
self.status = response.status_code
self.reason = response.reason_phrase
msg = f"Request failed: {self.status} {self.reason}"
super().__init__(msg)
self.response = response
self.status = response.status
self.reason = response.reason
# --- Converters ---
@ -161,8 +165,9 @@ class GlobalChannelConverter(commands.IDConverter):
def embed_factory(ctx: commands.Context) -> discord.Embed:
"""Creates an embed with neutral colour and standard footer."""
embed = discord.Embed(timestamp=datetime.utcnow(), colour=colours.neutral)
embed.set_footer(text=str(ctx.author), icon_url=str(ctx.author.avatar_url))
embed = discord.Embed(timestamp=datetime.now(timezone.utc), colour=colours.neutral)
if ctx.author:
embed.set_footer(text=str(ctx.author), icon_url=str(ctx.author.display_avatar))
return embed
@ -179,7 +184,7 @@ def error_embed_factory(ctx: commands.Context, exception: Exception, debug_mode:
return embed
async def add_react(msg: discord.Message, react: Union[Emoji, Reaction, PartialEmoji, str]):
async def add_react(msg: discord.Message, react: Union[Emoji, PartialEmoji, str]):
try:
await msg.add_reaction(react)
except discord.Forbidden:

View File

@ -1,3 +1,3 @@
-r requirements.txt
flake8
discord.py-stubs==1.5.0
mypy

View File

@ -1,436 +1,28 @@
"""
ae7q extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
# Test callsigns:
# KN8U: active, restricted
# AB2EE: expired, restricted
# KE8FGB: assigned once, no restrictions
# KV4AAA: unassigned, no records
# KC4USA: reserved, no call history, *but* has application history
import aiohttp
from bs4 import BeautifulSoup
import discord.ext.commands as commands
import common as cmn
from common import embed_factory, colours
class AE7QCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
@commands.group(name="ae7q", aliases=["ae"], case_insensitive=True, category=cmn.Cats.LOOKUP)
async def _ae7q_lookup(self, ctx: commands.Context):
"""Looks up a callsign, FRN, or Licensee ID on [ae7q.com](http://ae7q.com/)."""
if ctx.invoked_subcommand is None:
await ctx.send_help(ctx.command)
@_ae7q_lookup.command(name="call", aliases=["c"], category=cmn.Cats.LOOKUP)
async def _ae7q_call(self, ctx: commands.Context, callsign: str):
"""Looks up the history of a callsign on [ae7q.com](http://ae7q.com/)."""
with ctx.typing():
callsign = callsign.upper()
desc = ""
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
embed = cmn.embed_factory(ctx)
if not callsign.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q History for Callsign"
embed.colour = cmn.colours.bad
embed.description = "Not a valid callsign!"
await ctx.send(embed=embed)
return
async with self.session.get(base_url + callsign) as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
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.title = f"AE7Q History for {callsign}"
embed.colour = cmn.colours.good
embed.url = base_url + callsign
# add the first three rows of the table to the embed
for row in table[0:3]:
header = f"**{row[0]}** ({row[1]})" # **Name** (Applicant Type)
body = (f"Class: *{row[2]}*\n"
f"Region: *{row[3]}*\n"
f"Status: *{row[4]}*\n"
f"Granted: *{row[5]}*\n"
f"Effective: *{row[6]}*\n"
f"Cancelled: *{row[7]}*\n"
f"Expires: *{row[8]}*")
embed.add_field(name=header, value=body, inline=False)
if len(table) > 3:
desc += f"\nRecords 1 to 3 of {len(table)}. See ae7q.com for more..."
embed.description = desc
await ctx.send(embed=embed)
@_ae7q_lookup.command(name="trustee", aliases=["t"], category=cmn.Cats.LOOKUP)
async def _ae7q_trustee(self, ctx: commands.Context, callsign: str):
"""Looks up the licenses for which a licensee is trustee on [ae7q.com](http://ae7q.com/)."""
with ctx.typing():
callsign = callsign.upper()
desc = ""
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
embed = cmn.embed_factory(ctx)
if not callsign.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q Trustee History for Callsign"
embed.colour = cmn.colours.bad
embed.description = "Not a valid callsign!"
await ctx.send(embed=embed)
return
async with self.session.get(base_url + callsign) as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
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.title = f"AE7Q Trustee History for {callsign}"
embed.colour = cmn.colours.good
embed.url = base_url + callsign
# add the first three rows of the table to the embed
for row in table[0:3]:
header = f"**{row[0]}** ({row[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)
if len(table) > 3:
desc += f"\nRecords 1 to 3 of {len(table)}. See ae7q.com for more..."
embed.description = desc
await ctx.send(embed=embed)
@_ae7q_lookup.command(name="applications", aliases=["a"], category=cmn.Cats.LOOKUP)
async def _ae7q_applications(self, ctx: commands.Context, callsign: str):
"""Looks up the application history for a callsign on [ae7q.com](http://ae7q.com/)."""
"""
with ctx.typing():
callsign = callsign.upper()
desc = ""
base_url = "http://ae7q.com/query/data/CallHistory.php?CALL="
embed = cmn.embed_factory(ctx)
if not callsign.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q Application History for Callsign"
embed.colour = cmn.colours.bad
embed.description = "Not a valid callsign!"
await ctx.send(embed=embed)
return
async with self.session.get(base_url + callsign) as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
page = await resp.text()
soup = BeautifulSoup(page, features="html.parser")
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
table = tables[0]
# find the first table in the page, and use it to make a description
if len(table[0]) == 1:
for row in table:
desc += " ".join(row.getText().split())
desc += "\n"
desc = desc.replace(callsign, f"`{callsign}`")
# select the last table to get applications
table = tables[-1]
table_headers = table[0].find_all("th")
first_header = "".join(table_headers[0].strings) if len(table_headers) > 0 else None
# catch if the wrong table was selected
if first_header is None or not first_header.startswith("Receipt"):
embed.title = f"AE7Q Application History for {callsign}"
embed.colour = cmn.colours.bad
embed.url = base_url + callsign
embed.description = desc
embed.description += f"\nNo records found for `{callsign}`"
await ctx.send(embed=embed)
return
table = await process_table(table[1:])
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q Application History for {callsign}"
embed.colour = cmn.colours.good
embed.url = base_url + callsign
# add the first three rows of the table to the embed
for row in table[0:3]:
header = f"**{row[1]}** ({row[3]})" # **Name** (Callsign)
body = (f"Received: *{row[0]}*\n"
f"Region: *{row[2]}*\n"
f"Purpose: *{row[5]}*\n"
f"Last Action: *{row[7]}*\n"
f"Application Status: *{row[8]}*\n")
embed.add_field(name=header, value=body, inline=False)
if len(table) > 3:
desc += f"\nRecords 1 to 3 of {len(table)}. See ae7q.com for more..."
embed.description = desc
await ctx.send(embed=embed)
"""
raise NotImplementedError("Application history lookup not yet supported. "
"Check back in a later version of the bot.")
@_ae7q_lookup.command(name="frn", aliases=["f"], category=cmn.Cats.LOOKUP)
async def _ae7q_frn(self, ctx: commands.Context, frn: str):
"""Looks up the history of an FRN on [ae7q.com](http://ae7q.com/)."""
"""
NOTES:
- 2 tables: callsign history and application history
- If not found: no tables
"""
with ctx.typing():
base_url = "http://ae7q.com/query/data/FrnHistory.php?FRN="
embed = cmn.embed_factory(ctx)
if not frn.isdecimal():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q History for FRN"
embed.colour = cmn.colours.bad
embed.description = "Not a valid FRN!"
await ctx.send(embed=embed)
return
async with self.session.get(base_url + frn) as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
page = await resp.text()
soup = BeautifulSoup(page, features="html.parser")
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
if not len(tables):
embed.title = f"AE7Q History for FRN {frn}"
embed.colour = cmn.colours.bad
embed.url = base_url + frn
embed.description = f"No records found for FRN `{frn}`"
await ctx.send(embed=embed)
return
table = tables[0]
table_headers = table[0].find_all("th")
first_header = "".join(table_headers[0].strings) if len(table_headers) > 0 else None
# catch if the wrong table was selected
if first_header is None or not first_header.startswith("With Licensee"):
embed.title = f"AE7Q History for FRN {frn}"
embed.colour = cmn.colours.bad
embed.url = base_url + frn
embed.description = f"No records found for FRN `{frn}`"
await ctx.send(embed=embed)
return
table = await process_table(table[2:])
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q History for FRN {frn}"
embed.colour = cmn.colours.good
embed.url = base_url + frn
# add the first three rows of the table to the embed
for row in table[0:3]:
header = f"**{row[0]}** ({row[3]})" # **Callsign** (Applicant Type)
body = (f"Name: *{row[2]}*\n"
f"Class: *{row[4]}*\n"
f"Region: *{row[1]}*\n"
f"Status: *{row[5]}*\n"
f"Granted: *{row[6]}*\n"
f"Effective: *{row[7]}*\n"
f"Cancelled: *{row[8]}*\n"
f"Expires: *{row[9]}*")
embed.add_field(name=header, value=body, inline=False)
if len(table) > 3:
embed.description = f"Records 1 to 3 of {len(table)}. See ae7q.com for more..."
await ctx.send(embed=embed)
@_ae7q_lookup.command(name="licensee", aliases=["l"], category=cmn.Cats.LOOKUP)
async def _ae7q_licensee(self, ctx: commands.Context, licensee_id: str):
"""Looks up the history of a licensee ID on [ae7q.com](http://ae7q.com/)."""
with ctx.typing():
licensee_id = licensee_id.upper()
base_url = "http://ae7q.com/query/data/LicenseeIdHistory.php?ID="
embed = cmn.embed_factory(ctx)
if not licensee_id.isalnum():
embed = cmn.embed_factory(ctx)
embed.title = "AE7Q History for Licensee"
embed.colour = cmn.colours.bad
embed.description = "Not a valid licensee ID!"
await ctx.send(embed=embed)
return
async with self.session.get(base_url + licensee_id) as resp:
if resp.status != 200:
raise cmn.BotHTTPError(resp)
page = await resp.text()
soup = BeautifulSoup(page, features="html.parser")
tables = [[row for row in table.find_all("tr")] for table in soup.select("table.Database")]
if not len(tables):
embed.title = f"AE7Q History for Licensee {licensee_id}"
embed.colour = cmn.colours.bad
embed.url = base_url + licensee_id
embed.description = f"No records found for Licensee `{licensee_id}`"
await ctx.send(embed=embed)
return
table = tables[0]
table_headers = table[0].find_all("th")
first_header = "".join(table_headers[0].strings) if len(table_headers) > 0 else None
# catch if the wrong table was selected
if first_header is None or not first_header.startswith("With FCC"):
embed.title = f"AE7Q History for Licensee {licensee_id}"
embed.colour = cmn.colours.bad
embed.url = base_url + licensee_id
embed.description = f"No records found for Licensee `{licensee_id}`"
await ctx.send(embed=embed)
return
table = await process_table(table[2:])
embed = cmn.embed_factory(ctx)
embed.title = f"AE7Q History for Licensee {licensee_id}"
embed.colour = cmn.colours.good
embed.url = base_url + licensee_id
# add the first three rows of the table to the embed
for row in table[0:3]:
header = f"**{row[0]}** ({row[3]})" # **Callsign** (Applicant Type)
body = (f"Name: *{row[2]}*\n"
f"Class: *{row[4]}*\n"
f"Region: *{row[1]}*\n"
f"Status: *{row[5]}*\n"
f"Granted: *{row[6]}*\n"
f"Effective: *{row[7]}*\n"
f"Cancelled: *{row[8]}*\n"
f"Expires: *{row[9]}*")
embed.add_field(name=header, value=body, inline=False)
if len(table) > 3:
embed.description = f"Records 1 to 3 of {len(table)}. See ae7q.com for more..."
await ctx.send(embed=embed)
async def process_table(table: list):
"""Processes tables (*not* including headers) and returns the processed table"""
table_contents = []
for tr in table:
row = []
for td in tr.find_all("td"):
cell_val = td.getText().strip()
row.append(cell_val if cell_val else "-")
# take care of columns that span multiple rows by copying the contents rightward
if "colspan" in td.attrs and int(td.attrs["colspan"]) > 1:
for i in range(int(td.attrs["colspan"]) - 1):
row.append(row[-1])
# get rid of ditto marks by copying the contents from the previous row
for i, cell in enumerate(row):
if cell == "\"":
row[i] = table_contents[-1][i]
# add row to table
table_contents += [row]
return table_contents
@commands.command(name="ae7q", aliases=["ae"], case_insensitive=True)
async def _ae7q_lookup(self, ctx: commands.Context, *, _):
"""Removed in v2.8.0"""
embed = embed_factory(ctx)
embed.colour = colours.bad
embed.title = "Command removed"
embed.description = ("This command was removed in v2.8.0.\n"
"For context, see [this Github issue](https://github.com/miaowware/qrm2/issues/448)")
await ctx.send(embed=embed)
def setup(bot: commands.Bot):
bot.add_cog(AE7QCog(bot))
bot.add_cog(AE7QCog())

View File

@ -1,10 +1,9 @@
"""
Base extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
@ -32,7 +31,7 @@ class QrmHelpCommand(commands.HelpCommand):
self.verify_checks = True
self.context: commands.Context
async def filter_commands(self, commands: Iterable[Command]) -> list[Command]:
async def filter_commands(self, commands: Iterable[Command], **kwargs) -> list[Command]:
def sort_by_cat(cmds):
ret = []
bolt_cmds = {}
@ -43,7 +42,6 @@ class QrmHelpCommand(commands.HelpCommand):
bolt_cmds[cat].append(c)
else:
bolt_cmds[cat] = [c]
cmds.remove(c)
else:
ret.append(c)
@ -143,7 +141,8 @@ class QrmHelpCommand(commands.HelpCommand):
embed.title = await self.get_command_signature(group)
embed.description = group.help
for cmd in await self.filter_commands(group.commands, sort=True):
embed.add_field(name=await self.get_command_signature(cmd), value=cmd.help, inline=False)
embed.add_field(name=await self.get_command_signature(cmd), value=cmd.help if cmd.help else "",
inline=False)
await self.context.send(embed=embed)
@ -170,6 +169,7 @@ class BaseCog(commands.Cog):
self.donation_links = {
"Ko-Fi": "https://ko-fi.com/miaowware",
"LiberaPay": "https://liberapay.com/miaowware",
"GitHub Sponsors": "https://github.com/sponsors/classabbyamp",
}
self.bot_invite = ""
if self.bot.user:
@ -178,7 +178,7 @@ class BaseCog(commands.Cog):
@commands.Cog.listener()
async def on_ready(self):
if not self.bot_invite:
if not self.bot_invite and self.bot.user:
self.bot_invite = (f"https://discordapp.com/oauth2/authorize?client_id={self.bot.user.id}"
f"&scope=bot&permissions={opt.invite_perms}")
@ -197,7 +197,8 @@ class BaseCog(commands.Cog):
inline=False)
if opt.enable_invite_cmd and (await self.bot.application_info()).bot_public:
embed.add_field(name="Invite qrm to Your Server", value=self.bot_invite, inline=False)
embed.set_thumbnail(url=str(self.bot.user.avatar_url))
if self.bot.user and self.bot.user.avatar:
embed.set_thumbnail(url=str(self.bot.user.avatar.url))
await ctx.send(embed=embed)
@commands.command(name="ping", aliases=["beep"], category=cmn.BoltCats.INFO)

View File

@ -2,19 +2,16 @@
Callsign Lookup extension for qrm
---
Copyright (C) 2019-2020 classabbyamp, 0x5c (as qrz.py)
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
from typing import Dict
from datetime import datetime
import aiohttp
from qrztools import qrztools, QrzAsync, QrzError
from gridtools import Grid, LatLong
from callsignlookuptools import QrzAsyncClient, CallsignLookupError, CallsignData
from discord.ext import commands
@ -30,14 +27,16 @@ class QRZCog(commands.Cog):
self.qrz = None
try:
if keys.qrz_user and keys.qrz_pass:
self.qrz = QrzAsync(keys.qrz_user, keys.qrz_pass, useragent="discord-qrm2",
session=aiohttp.ClientSession(connector=bot.qrm.connector))
# seed the qrz object with the previous session key, in case it already works
session_key = ""
try:
with open("data/qrz_session") as qrz_file:
self.qrz.session_key = qrz_file.readline().strip()
session_key = qrz_file.readline().strip()
except FileNotFoundError:
pass
self.qrz = QrzAsyncClient(username=keys.qrz_user, password=keys.qrz_pass, useragent="discord-qrm2",
session_key=session_key,
session=aiohttp.ClientSession(connector=bot.qrm.connector))
except AttributeError:
pass
@ -64,69 +63,65 @@ class QRZCog(commands.Cog):
async with ctx.typing():
try:
data = await self.qrz.get_callsign(callsign)
except QrzError as e:
data = await self.qrz.search(callsign)
except CallsignLookupError as e:
embed.colour = cmn.colours.bad
embed.description = str(e)
await ctx.send(embed=embed)
return
embed.title = f"QRZ Data for {data.call}"
embed.title = f"QRZ Data for {data.callsign}"
embed.colour = cmn.colours.good
embed.url = data.url
if data.image != qrztools.QrzImage():
if data.image is not None:
embed.set_thumbnail(url=data.image.url)
for title, val in qrz_process_info(data).items():
if val:
if val is not None and (val := str(val)):
embed.add_field(name=title, value=val, inline=True)
await ctx.send(embed=embed)
def qrz_process_info(data: qrztools.QrzCallsignData) -> Dict:
if data.name != qrztools.Name():
def qrz_process_info(data: CallsignData) -> Dict:
if data.name is not None:
if opt.qrz_only_nickname:
if data.name.nickname:
name = data.name.nickname + " " + data.name.name
nm = data.name.name if data.name.name is not None else ""
if data.name.nickname is not None:
name = data.name.nickname + " " + nm
elif data.name.first:
name = data.name.first + " " + data.name.name
name = data.name.first + " " + nm
else:
name = data.name.name
name = nm
else:
name = data.name.formatted_name
name = data.name
else:
name = None
if data.address != qrztools.Address():
state = ", " + data.address.state + " " if data.address.state else ""
address = "\n".join(
[x for x
in [data.address.attn, data.address.line1, data.address.line2 + state, data.address.zip]
if x]
)
else:
address = None
qsl = dict()
if data.qsl is not None:
qsl = {
"eQSL?": data.qsl.eqsl,
"Paper QSL?": data.qsl.mail,
"LotW?": data.qsl.lotw,
"QSL Info": data.qsl.info,
}
return {
"Name": name,
"Country": data.address.country,
"Address": address,
"Grid Square": data.grid if data.grid != Grid(LatLong(0, 0)) else None,
"County": data.county if data.county else None,
"CQ Zone": data.cq_zone if data.cq_zone else None,
"ITU Zone": data.itu_zone if data.itu_zone else None,
"IOTA Designator": data.iota if data.iota else None,
"Expires": f"{data.expire_date:%Y-%m-%d}" if data.expire_date != datetime.min else None,
"Country": data.address.country if data.address is not None else None,
"Address": data.address,
"Grid Square": data.grid,
"County": data.county,
"CQ Zone": data.cq_zone,
"ITU Zone": data.itu_zone,
"IOTA Designator": data.iota,
"Expires": f"{data.expire_date:%Y-%m-%d}" if data.expire_date is not None else None,
"Aliases": ", ".join(data.aliases) if data.aliases else None,
"Previous Callsign": data.prev_call if data.prev_call else None,
"License Class": data.lic_class if data.lic_class else None,
"Trustee": data.trustee if data.trustee else None,
"eQSL?": "Yes" if data.eqsl else "No",
"Paper QSL?": "Yes" if data.mail_qsl else "No",
"LotW?": "Yes" if data.lotw_qsl else "No",
"QSL Info": data.qsl_manager if data.qsl_manager else None,
"Born": f"{data.born:%Y-%m-%d}" if data.born != datetime.min else None
}
"Previous Callsign": data.prev_call,
"License Class": data.lic_class,
"Trustee": data.trustee,
"Born": data.born,
} | qsl
def setup(bot):

View File

@ -2,10 +2,9 @@
Codes extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c (as ham.py)
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,10 +1,9 @@
"""
Contest Calendar extension for qrm
---
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,10 +1,9 @@
"""
Conversion extension for qrm
---
Copyright (C) 2020-2021 classabbyamp, 0x5c
Copyright (C) 2020-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
@ -188,7 +187,7 @@ def _calc_volt(db: float, ref: float):
# testing code
if __name__ == "__main__":
while(True):
while True:
try:
ip = input("> ").split()
initial = float(ip[0])

View File

@ -2,10 +2,9 @@
DXCC Prefix Lookup extension for qrm
---
Copyright (C) 2019-2020 classabbyamp, 0x5c (as lookup.py)
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,10 +1,9 @@
"""
Fun extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,10 +1,9 @@
"""
Grid extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,10 +1,9 @@
"""
Image extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -2,19 +2,16 @@
Land Weather extension for qrm
---
Copyright (C) 2019-2020 classabbyamp, 0x5c (as weather.py)
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
import re
from typing import List
import aiohttp
from discord import Embed
import discord.ext.commands as commands
import common as cmn
@ -103,7 +100,32 @@ class WeatherCog(commands.Cog):
Airports should be given as an \
[ICAO code](https://en.wikipedia.org/wiki/List_of_airports_by_IATA_and_ICAO_code)."""
await ctx.send(embed=await self.gen_metar_taf_embed(ctx, airport, hours, False))
embed = cmn.embed_factory(ctx)
airport = airport.upper()
if not re.fullmatch(r"\w(\w|\d){2,3}", airport):
embed.title = "Invalid airport given!"
embed.colour = cmn.colours.bad
await ctx.send(embed=embed)
return
url = f"https://aviationweather.gov/api/data/metar?ids={airport}&format=raw&taf=false&hours={hours}"
async with self.session.get(url) as r:
if r.status != 200:
raise cmn.BotHTTPError(r)
metar = await r.text()
if hours > 0:
embed.title = f"METAR for {airport} for the last {hours} hour{'s' if hours > 1 else ''}"
else:
embed.title = f"Current METAR for {airport}"
embed.description = "Data from [aviationweather.gov](https://www.aviationweather.gov/)."
embed.colour = cmn.colours.good
embed.description += f"\n\n```\n{metar}\n```"
await ctx.send(embed=embed)
@commands.command(name="taf", category=cmn.Cats.WEATHER)
async def taf(self, ctx: commands.Context, airport: str):
@ -111,57 +133,28 @@ class WeatherCog(commands.Cog):
Airports should be given as an \
[ICAO code](https://en.wikipedia.org/wiki/List_of_airports_by_IATA_and_ICAO_code)."""
await ctx.send(embed=await self.gen_metar_taf_embed(ctx, airport, 0, True))
async def gen_metar_taf_embed(self, ctx: commands.Context, airport: str, hours: int, taf: bool) -> Embed:
embed = cmn.embed_factory(ctx)
airport = airport.upper()
if re.fullmatch(r"\w(\w|\d){2,3}", airport):
metar = await self.get_metar_taf_data(airport, hours, taf)
if taf:
embed.title = f"Current TAF for {airport}"
elif hours > 0:
embed.title = f"METAR for {airport} for the last {hours} hour{'s' if hours > 1 else ''}"
else:
embed.title = f"Current METAR for {airport}"
embed.description = "Data from [aviationweather.gov](https://www.aviationweather.gov/metar/data)."
embed.colour = cmn.colours.good
data = "\n".join(metar)
embed.description += f"\n\n```\n{data}\n```"
else:
if not re.fullmatch(r"\w(\w|\d){2,3}", airport):
embed.title = "Invalid airport given!"
embed.colour = cmn.colours.bad
return embed
await ctx.send(embed=embed)
return
async def get_metar_taf_data(self, airport: str, hours: int, taf: bool) -> List[str]:
url = (f"https://www.aviationweather.gov/metar/data?ids={airport}&format=raw&hours={hours}"
f"&taf={'on' if taf else 'off'}&layout=off")
url = f"https://aviationweather.gov/api/data/taf?ids={airport}&format=raw&metar=true"
async with self.session.get(url) as r:
if r.status != 200:
raise cmn.BotHTTPError(r)
page = await r.text()
taf = await r.text()
# pare down to just the data
page = page.split("<!-- Data starts here -->")[1].split("<!-- Data ends here -->")[0].strip()
# split at <hr>s
data = re.split(r"<hr.*>", page, maxsplit=len(airport))
embed.title = f"Current TAF for {airport}"
embed.description = "Data from [aviationweather.gov](https://www.aviationweather.gov/)."
embed.colour = cmn.colours.good
embed.description += f"\n\n```\n{taf}\n```"
parsed = []
for sec in data:
if sec.strip():
for line in sec.split("\n"):
line = line.strip()
# remove HTML stuff
line = line.replace("<code>", "").replace("</code>", "")
line = line.replace("<strong>", "").replace("</strong>", "")
line = line.replace("<br/>", "\n").replace("&nbsp;", " ")
line = line.strip("\n")
parsed.append(line)
return parsed
await ctx.send(embed=embed)
def setup(bot: commands.Bot):

View File

@ -1,10 +1,9 @@
"""
Morse Code extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,10 +1,9 @@
"""
Prefixes Lookup extension for qrm
---
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,18 +1,17 @@
"""
Propagation extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
from datetime import datetime
from io import BytesIO
import aiohttp
import cairosvg
from datetime import datetime
import httpx
import discord
import discord.ext.commands as commands
@ -24,19 +23,22 @@ class PropagationCog(commands.Cog):
muf_url = "https://prop.kc2g.com/renders/current/mufd-normal-now.svg"
fof2_url = "https://prop.kc2g.com/renders/current/fof2-normal-now.svg"
gl_baseurl = "https://www.fourmilab.ch/cgi-bin/uncgi/Earth?img=ETOPO1_day-m.evif&dynimg=y&opt=-p"
n0nbh_sun_url = "http://www.hamqsl.com/solarsun.php"
n0nbh_sun_url = "https://www.hamqsl.com/solarsun.php"
noaa_drap_url = "https://services.swpc.noaa.gov/images/animations/d-rap/global/d-rap/latest.png"
def __init__(self, bot):
self.bot = bot
self.session = aiohttp.ClientSession(connector=bot.qrm.connector)
self.httpx_client: httpx.AsyncClient = bot.qrm.httpx_client
@commands.command(name="mufmap", aliases=["muf"], category=cmn.Cats.WEATHER)
async def mufmap(self, ctx: commands.Context):
"""Shows a world map of the Maximum Usable Frequency (MUF)."""
async with ctx.typing():
async with self.session.get(self.muf_url) as r:
svg = await r.read()
out = BytesIO(cairosvg.svg2png(bytestring=svg))
resp = await self.httpx_client.get(self.muf_url)
await resp.aclose()
if resp.status_code != 200:
raise cmn.BotHTTPError(resp)
out = BytesIO(cairosvg.svg2png(bytestring=await resp.aread()))
file = discord.File(out, "muf_map.png")
embed = cmn.embed_factory(ctx)
embed.title = "Maximum Usable Frequency Map"
@ -48,9 +50,11 @@ class PropagationCog(commands.Cog):
async def fof2map(self, ctx: commands.Context):
"""Shows a world map of the Critical Frequency (foF2)."""
async with ctx.typing():
async with self.session.get(self.fof2_url) as r:
svg = await r.read()
out = BytesIO(cairosvg.svg2png(bytestring=svg))
resp = await self.httpx_client.get(self.fof2_url)
await resp.aclose()
if resp.status_code != 200:
raise cmn.BotHTTPError(resp)
out = BytesIO(cairosvg.svg2png(bytestring=await resp.aread()))
file = discord.File(out, "fof2_map.png")
embed = cmn.embed_factory(ctx)
embed.title = "Critical Frequency (foF2) Map"
@ -68,18 +72,30 @@ class PropagationCog(commands.Cog):
embed.set_image(url=self.gl_baseurl + date_params)
await ctx.send(embed=embed)
@commands.command(name="solarweather", aliases=["solar", "bandconditions", "cond", "condx", "conditions"],
category=cmn.Cats.WEATHER)
@commands.command(name="solarweather", aliases=["solar"], category=cmn.Cats.WEATHER)
async def solarweather(self, ctx: commands.Context):
"""Gets a solar weather report."""
resp = await self.httpx_client.get(self.n0nbh_sun_url)
await resp.aclose()
if resp.status_code != 200:
raise cmn.BotHTTPError(resp)
img = BytesIO(await resp.aread())
file = discord.File(img, "solarweather.png")
embed = cmn.embed_factory(ctx)
embed.title = "☀️ Current Solar Weather"
if ctx.invoked_with in ["bandconditions", "cond", "condx", "conditions"]:
embed.add_field(name="⚠️ Deprecated Command Alias",
value=(f"This command has been renamed to `{ctx.prefix}solar`!\n"
"The alias you used will be removed in the next version."))
embed.colour = cmn.colours.good
embed.set_image(url=self.n0nbh_sun_url)
embed.set_image(url="attachment://solarweather.png")
await ctx.send(file=file, embed=embed)
@commands.command(name="drapmap", aliases=["drap"], category=cmn.Cats.WEATHER)
async def drapmap(self, ctx: commands.Context):
"""Gets the current D-RAP map for radio blackouts"""
embed = cmn.embed_factory(ctx)
embed.title = "D Region Absorption Predictions (D-RAP) Map"
embed.colour = cmn.colours.good
embed.description = \
"Image from [swpc.noaa.gov](https://www.swpc.noaa.gov/products/d-region-absorption-predictions-d-rap)"
embed.set_image(url=self.noaa_drap_url)
await ctx.send(embed=embed)

View File

@ -1,10 +1,9 @@
"""
Study extension for qrm
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
@ -160,13 +159,13 @@ class StudyCog(commands.Cog):
await cmn.add_react(q_msg, list(self.choices.values())[i])
await cmn.add_react(q_msg, cmn.emojis.question)
def check(reaction, user):
return (user.id != self.bot.user.id
and reaction.message.id == q_msg.id
and (str(reaction.emoji) in self.choices.values() or str(reaction.emoji) == cmn.emojis.question))
def check(ev):
return (ev.user_id != self.bot.user.id
and ev.message_id == q_msg.id
and (str(ev.emoji) in self.choices.values() or str(ev.emoji) == cmn.emojis.question))
try:
reaction, user = await self.bot.wait_for("reaction_add", timeout=300.0, check=check)
ev = await self.bot.wait_for("raw_reaction_add", timeout=300.0, check=check)
except asyncio.TimeoutError:
embed.set_field_at(1, name="Answers", value=answers_str_bolded, inline=False)
embed.set_field_at(2, name="Answer",
@ -175,16 +174,18 @@ class StudyCog(commands.Cog):
embed.colour = cmn.colours.timeout
await q_msg.edit(embed=embed)
else:
if str(reaction.emoji) == cmn.emojis.question:
if str(ev.emoji) == cmn.emojis.question:
embed.set_field_at(1, name="Answers", value=answers_str_bolded, inline=False)
embed.set_field_at(2, name="Answer",
value=f"The correct answer was {self.choices[question['answer']]}", inline=False)
embed.add_field(name="Answer Requested By", value=str(user), inline=False)
# only available in guilds, but it only makes sense there
if ev.member:
embed.add_field(name="Answer Requested By", value=str(ev.member), inline=False)
embed.colour = cmn.colours.timeout
await q_msg.edit(embed=embed)
else:
answers_str_checked = ""
chosen_ans = self.choices_inv[str(reaction.emoji)]
chosen_ans = self.choices_inv[str(ev.emoji)]
for letter, ans in answers.items():
answers_str_checked += f"{self.choices[letter]}"
if letter == question["answer"] == chosen_ans:
@ -196,19 +197,23 @@ class StudyCog(commands.Cog):
else:
answers_str_checked += f" {ans}\n"
if self.choices[question["answer"]] == str(reaction.emoji):
if self.choices[question["answer"]] == str(ev.emoji):
embed.set_field_at(1, name="Answers", value=answers_str_checked, inline=False)
embed.set_field_at(2, name="Answer", value=(f"{cmn.emojis.check_mark} "
f"**Correct!** The answer was {reaction.emoji}"))
embed.add_field(name="Answered By", value=str(user), inline=False)
f"**Correct!** The answer was {ev.emoji}"))
# only available in guilds, but it only makes sense there
if ev.member:
embed.add_field(name="Answered By", value=str(ev.member), inline=False)
embed.colour = cmn.colours.good
await q_msg.edit(embed=embed)
else:
embed.set_field_at(1, name="Answers", value=answers_str_checked, inline=False)
embed.set_field_at(2, name="Answer",
value=(f"{cmn.emojis.x} **Incorrect!** The correct answer was "
f"{self.choices[question['answer']]}, not {reaction.emoji}"))
embed.add_field(name="Answered By", value=str(user), inline=False)
f"{self.choices[question['answer']]}, not {ev.emoji}"))
# only available in guilds, but it only makes sense there
if ev.member:
embed.add_field(name="Answered By", value=str(ev.member), inline=False)
embed.colour = cmn.colours.bad
await q_msg.edit(embed=embed)

View File

@ -1,10 +1,9 @@
"""
TeX extension for qrm
---
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,10 +1,9 @@
"""
Time extension for qrm
---
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

15
info.py
View File

@ -1,19 +1,18 @@
"""
Static info about the bot.
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
authors = ("@ClassAbbyAmplifier#2229", "@0x5c#0639")
authors = ("@classabbyamp", "@0x5c.io")
description = """A bot with various useful ham radio-related functions, written in Python."""
license = "Released under the GNU General Public License v2"
license = "Québec Free and Open-Source Licence Strong Reciprocity (LiLiQ-R+), version 1.1"
contributing = """Check out the [source on GitHub](https://github.com/miaowware/qrm2). Contributions are welcome!
All issues and requests related to resources (including maps, band charts, data) should be added \
in [miaowware/qrm-resources](https://github.com/miaowware/qrm-resources)."""
release = "2.7.2"
All issues and requests related to resources (including maps, band charts, data) should be added \
in [miaowware/qrm-resources](https://github.com/miaowware/qrm-resources)."""
release = "2.9.2"
bot_server = "https://discord.gg/Ntbg3J4"

28
main.py
View File

@ -2,10 +2,9 @@
"""
qrm, a bot for Discord
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
@ -17,6 +16,7 @@ from datetime import datetime, time
from types import SimpleNamespace
from pathlib import Path
import httpx
import pytz
import discord
@ -50,9 +50,9 @@ connector = loop.run_until_complete(conn.new_connector())
# Defining the intents
intents = discord.Intents.none()
intents.guilds = True
intents.guild_messages = True
intents.dm_messages = True
intents.messages = True
intents.reactions = True
intents.message_content = True
member_cache = discord.MemberCacheFlags.from_intents(intents)
@ -70,6 +70,8 @@ bot.qrm = SimpleNamespace()
# Let's store stuff here.
bot.qrm.connector = connector
bot.qrm.debug_mode = debug_mode
# TODO: Add code to close the client
bot.qrm.httpx_client = httpx.AsyncClient()
# --- Commands ---
@ -82,7 +84,7 @@ async def _restart_bot(ctx: commands.Context):
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
print(f"[**] Restarting! Requested by {ctx.author}.")
exit_code = 42 # Signals to the wrapper script that the bot needs to be restarted.
await bot.logout()
await bot.close()
@bot.command(name="shutdown", aliases=["shut"], category=cmn.BoltCats.ADMIN)
@ -93,7 +95,7 @@ async def _shutdown_bot(ctx: commands.Context):
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
print(f"[**] Shutting down! Requested by {ctx.author}.")
exit_code = 0 # Signals to the wrapper script that the bot should not be restarted.
await bot.logout()
await bot.close()
@bot.group(name="extctl", aliases=["ex"], case_insensitive=True, category=cmn.BoltCats.ADMIN)
@ -124,10 +126,10 @@ async def _extctl_load(ctx: commands.Context, extension: str):
"""Loads an extension."""
try:
bot.load_extension(ext_dir + "." + extension)
except commands.ExtensionNotFound as e:
except discord.errors.ExtensionNotFound as e:
try:
bot.load_extension(plugin_dir + "." + extension)
except commands.ExtensionNotFound:
except discord.errors.ExtensionNotFound:
raise e
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
@ -141,10 +143,10 @@ async def _extctl_reload(ctx: commands.Context, extension: str):
await cmn.add_react(ctx.message, pika)
try:
bot.reload_extension(ext_dir + "." + extension)
except commands.ExtensionNotLoaded as e:
except discord.errors.ExtensionNotLoaded as e:
try:
bot.reload_extension(plugin_dir + "." + extension)
except commands.ExtensionNotLoaded:
except discord.errors.ExtensionNotLoaded:
raise e
await cmn.add_react(ctx.message, cmn.emojis.check_mark)
@ -154,10 +156,10 @@ async def _extctl_unload(ctx: commands.Context, extension: str):
"""Unloads an extension."""
try:
bot.unload_extension(ext_dir + "." + extension)
except commands.ExtensionNotLoaded as e:
except discord.errors.ExtensionNotLoaded as e:
try:
bot.unload_extension(plugin_dir + "." + extension)
except commands.ExtensionNotLoaded:
except discord.errors.ExtensionNotLoaded:
raise e
await cmn.add_react(ctx.message, cmn.emojis.check_mark)

View File

@ -1,9 +1,9 @@
discord.py~=1.6.0
py-cord-dev[speed]==2.5.0rc5
ctyparser~=2.0
gridtools~=1.0
qrztools[async]~=1.0
callsignlookuptools[async]~=1.1
beautifulsoup4
pytz
cairosvg
requests
pydantic
httpx
pydantic~=2.5

View File

@ -1,10 +1,9 @@
"""
Resource schemas generator for qrm2.
---
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,10 +1,9 @@
"""
Information about callsigns for the prefixes command in hamcog.
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of discord-qrmbot and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,10 +1,9 @@
"""
Information about callsigns for the CA prefixes command in hamcog.
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of discord-qrmbot and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,10 +1,9 @@
"""
Information about callsigns for the US prefixes command in hamcog.
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of discord-qrmbot and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,10 +1,9 @@
"""
A listing of hamstudy command resources
---
Copyright (C) 2019-2021 classabbyamp, 0x5c
Copyright (C) 2019-2023 classabbyamp, 0x5c
This file is part of discord-qrmbot and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

12
run.sh
View File

@ -17,7 +17,7 @@ fi
# Argument handling
_PASS_ERRORS=0
_NO_BOTENV=0
while [ ! -z "$1" ]; do
while [ -n "$1" ]; do
case $1 in
--pass-errors)
_PASS_ERRORS=1
@ -34,9 +34,9 @@ while [ ! -z "$1" ]; do
done
# If $PYTHON_BIN is not defined, default to 'python3.9'
if [ $_NO_BOTENV -eq 1 -a -z "$PYTHON_BIN" ]; then
PYTHON_BIN='python3.9'
# If $PYTHON_BIN is not defined, default to 'python3.11'
if [ $_NO_BOTENV -eq 1 ] && [ -z "$PYTHON_BIN" ]; then
PYTHON_BIN='python3.11'
fi
@ -69,9 +69,9 @@ echo "$0: Starting bot..."
# The loop
while true; do
if [ $_NO_BOTENV -eq 1 ]; then
"$PYTHON_BIN" main.py $@
"$PYTHON_BIN" main.py "$@"
else
./$BOTENV/bin/python3 main.py $@
"./$BOTENV/bin/python3" main.py "$@"
fi
err=$?
_message="$0: The bot exited with [$err]"

View File

@ -1,10 +1,9 @@
"""
Wrapper to handle aiohttp connector creation.
---
Copyright (C) 2020-2021 classabbyamp, 0x5c
Copyright (C) 2020-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""

View File

@ -1,16 +1,15 @@
"""
Resources manager for qrm2.
---
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
from pathlib import Path
import requests
import httpx
from utils.resources_models import Index
@ -24,13 +23,16 @@ class ResourcesManager:
def parse_index(self, index: str):
"""Parses the index."""
return Index.parse_raw(index)
return Index.model_validate_json(index)
def sync_fetch(self, filepath: str):
"""Fetches files in sync mode."""
self.print_msg(f"Fetching {filepath}", "sync")
with requests.get(self.url + filepath) as resp:
return resp.content
resp = httpx.get(self.url + filepath)
resp.raise_for_status()
r = resp.content
resp.close()
return r
def sync_start(self, basedir: Path) -> Index:
"""Takes cares of constructing the local resources repository and initialising the RM."""
@ -41,7 +43,7 @@ class ResourcesManager:
new_index: Index = self.parse_index(raw)
with (basedir / "index.json").open("wb") as file:
file.write(raw)
except (requests.RequestException, OSError) as ex:
except (httpx.RequestError, OSError) as ex:
self.print_msg(f"There was an issue fetching the index: {ex.__class__.__name__}: {ex}", "sync")
if (basedir / "index.json").exists():
self.print_msg("Old file exist, using old resources", "fallback")
@ -59,7 +61,7 @@ class ResourcesManager:
try:
with (basedir / file.filename).open("wb") as f:
f.write(self.sync_fetch(file.filename))
except (requests.RequestException, OSError) as ex:
except (httpx.RequestError, OSError) as ex:
ex_cls = ex.__class__.__name__
self.print_msg(f"There was an issue fetching {file.filename}: {ex_cls}: {ex}", "sync")
if not (basedir / file.filename).exists():

View File

@ -1,17 +1,16 @@
"""
Resource index models for qrm2.
---
Copyright (C) 2021 classabbyamp, 0x5c
Copyright (C) 2021-2023 classabbyamp, 0x5c
This file is part of qrm2 and is released under the terms of
the GNU General Public License, version 2.
SPDX-License-Identifier: LiLiQ-Rplus-1.1
"""
from collections.abc import Mapping
from datetime import datetime
from pydantic import BaseModel
from pydantic import BaseModel, RootModel
class File(BaseModel):
@ -23,18 +22,17 @@ class File(BaseModel):
return repr(self)
class Resource(BaseModel, Mapping):
# 'A Beautiful Hack' https://github.com/samuelcolvin/pydantic/issues/1802
__root__: dict[str, list[File]]
class Resource(RootModel, Mapping):
root: dict[str, list[File]]
def __getitem__(self, key: str) -> list[File]:
return self.__root__[key]
return self.root[key]
def __iter__(self):
return iter(self.__root__)
return iter(self.root)
def __len__(self) -> int:
return len(self.__root__)
return len(self.root)
# For some reason those were not the same???
def __str__(self) -> str:
@ -42,7 +40,7 @@ class Resource(BaseModel, Mapping):
# Make the repr more logical (despite the technical inaccuracy)
def __repr_args__(self):
return self.__root__.items()
return self.root.items()
class Index(BaseModel, Mapping):