aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar ks129 <[email protected]>2020-11-23 16:45:43 +0200
committerGravatar GitHub <[email protected]>2020-11-23 16:45:43 +0200
commit252ed41579eb4ba9627950d4e225912208e30d00 (patch)
tree60eee5c6b2bd1d469ed305590cd97ad378b1ea49
parentfix typos in docstring (diff)
parentAdd a banner and fix the README a bit. (diff)
Merge branch 'master' into feature/wonder-twins-command
-rw-r--r--.github/ISSUE_TEMPLATE/config.yml2
-rw-r--r--.github/workflows/build.yaml9
-rw-r--r--CONTRIBUTING.md6
-rw-r--r--Pipfile1
-rw-r--r--Pipfile.lock131
-rwxr-xr-xREADME.md18
-rw-r--r--bot/bot.py152
-rw-r--r--bot/constants.py44
-rw-r--r--bot/exts/easter/egg_facts.py8
-rw-r--r--bot/exts/evergreen/branding.py540
-rw-r--r--bot/exts/evergreen/error_handler.py6
-rw-r--r--bot/exts/evergreen/game.py6
-rw-r--r--bot/exts/evergreen/help.py8
-rw-r--r--bot/exts/evergreen/issues.py2
-rw-r--r--bot/exts/evergreen/showprojects.py33
-rw-r--r--bot/exts/evergreen/source.py2
-rw-r--r--bot/exts/evergreen/space.py6
-rw-r--r--bot/exts/halloween/candy_collection.py66
-rw-r--r--bot/exts/halloween/hacktoberstats.py85
-rw-r--r--bot/exts/halloween/spookyreact.py8
-rw-r--r--bot/exts/pride/pride_facts.py8
-rw-r--r--bot/exts/utils/extensions.py2
-rw-r--r--bot/exts/valentines/be_my_valentine.py2
-rw-r--r--bot/resources/evergreen/branding.json3
-rw-r--r--bot/resources/halloween/candy_collection.json1
-rw-r--r--bot/resources/halloween/github_links.json1
-rw-r--r--bot/resources/halloween/spooky_rating.json18
-rw-r--r--bot/seasons.py181
-rw-r--r--bot/utils/checks.py2
-rw-r--r--bot/utils/decorators.py22
-rw-r--r--bot/utils/exceptions.py6
-rw-r--r--deployment.yaml12
-rw-r--r--docker-compose.yml27
-rw-r--r--sir-lancebot-logo.pngbin0 -> 65083 bytes
34 files changed, 303 insertions, 1115 deletions
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 519294a0..4580be2f 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -4,4 +4,4 @@ contact_links:
url: https://discord.gg/python
about: Contributors must be part of the community, so be sure to join!
- name: Contributing Guide
- url: https://pythondiscord.com/pages/contributing/seasonalbot/
+ url: https://pythondiscord.com/pages/contributing/sir-lancebot/
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 404e66f7..20449741 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -44,10 +44,11 @@ jobs:
context: .
file: ./Dockerfile
push: true
- cache-from: type=registry,ref=ghcr.io/python-discord/seasonalbot:latest
+ cache-from: type=registry,ref=ghcr.io/python-discord/sir-lancebot:latest
+ cache-to: type=inline
tags: |
- ghcr.io/python-discord/seasonalbot:latest
- ghcr.io/python-discord/seasonalbot:${{ steps.sha_tag.outputs.tag }}
+ ghcr.io/python-discord/sir-lancebot:latest
+ ghcr.io/python-discord/sir-lancebot:${{ steps.sha_tag.outputs.tag }}
- name: Authenticate with Kubernetes
uses: azure/k8s-set-context@v1
@@ -60,5 +61,5 @@ jobs:
with:
manifests: |
deployment.yaml
- images: 'ghcr.io/python-discord/seasonalbot:${{ steps.sha_tag.outputs.tag }}'
+ images: 'ghcr.io/python-discord/sir-lancebot:${{ steps.sha_tag.outputs.tag }}'
kubectl-version: 'latest'
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e1fa39ec..7cf83db5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,6 @@
-# Contributing to Seasonalbot
+# Contributing to Sir Lancebot
-Seasonalbot is a community project for the Python Discord community over at https://discord.gg/python. We will be providing support for those of you who are new to Git, and this project is to be considered educational.
+Sir Lancebot is a community project for the Python Discord community over at https://discord.gg/python. We will be providing support for those of you who are new to Git, and this project is to be considered educational.
Our projects are open-source and are automatically deployed whenever commits are pushed to the `master` branch on each repository, so we've created a set of guidelines in order to keep everything clean and in working order.
@@ -39,7 +39,7 @@ All projects evolve over time, and this contribution guide is no different. This
## Supplemental Information
### Developer Environment
-Seasonalbot utilizes [Pipenv](https://pipenv.readthedocs.io/en/latest/) for installation and dependency management. For users unfamiliar with the Pipenv workflow, Pipenv's documentation provides a [Basic Usage](https://pipenv.readthedocs.io/en/latest/basics/) tutorial, along with some of the more advanced workflows. A project-specific installation guide can be found in [Seasonalbot's README](https://github.com/python-discord/seasonalbot/blob/master/README.md).
+Sir Lancebot utilizes [Pipenv](https://pipenv.readthedocs.io/en/latest/) for installation and dependency management. For users unfamiliar with the Pipenv workflow, Pipenv's documentation provides a [Basic Usage](https://pipenv.readthedocs.io/en/latest/basics/) tutorial, along with some of the more advanced workflows. A project-specific installation guide can be found in [Sir Lancebot's README](https://github.com/python-discord/sir-lancebot/blob/master/README.md).
When pulling down changes from GitHub, remember to sync your environment using `pipenv sync --dev` to ensure you're using the most up-to-date versions the project's dependencies.
diff --git a/Pipfile b/Pipfile
index c716bb49..100d51a1 100644
--- a/Pipfile
+++ b/Pipfile
@@ -13,6 +13,7 @@ pytz = "~=2019.2"
sentry-sdk = "~=0.14.2"
PyYAML = "~=5.3.1"
"discord.py" = {extras = ["voice"], version = "~=1.5.1"}
+async-rediscache = {extras = ["fakeredis"], version = "~=0.1.4"}
[dev-packages]
flake8 = "~=3.8"
diff --git a/Pipfile.lock b/Pipfile.lock
index c4890fdf..779d986c 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "ef586750d4c2b6d4ad4c2f2bd97510d8a49d90f109aed04f41355e12d3ca981f"
+ "sha256": "c358b14c467cb5ac9f3827e7835ce338ec6750f708bc5a11735163cf4f095f2d"
},
"pipfile-spec": 6,
"requires": {
@@ -43,6 +43,13 @@
"markers": "python_full_version >= '3.5.3'",
"version": "==3.6.3"
},
+ "aioredis": {
+ "hashes": [
+ "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a",
+ "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"
+ ],
+ "version": "==1.3.1"
+ },
"arrow": {
"hashes": [
"sha256:e098abbd9af3665aea81bdd6c869e93af4feb078e98468dd351c383af187aac5",
@@ -51,6 +58,17 @@
"index": "pypi",
"version": "==0.17.0"
},
+ "async-rediscache": {
+ "extras": [
+ "fakeredis"
+ ],
+ "hashes": [
+ "sha256:6be8a657d724ccbcfb1946d29a80c3478c5f9ecd2f78a0a26d2f4013a622258f",
+ "sha256:c25e4fff73f64d20645254783c3224a4c49e083e3fab67c44f17af944c5e26af"
+ ],
+ "index": "pypi",
+ "version": "==0.1.4"
+ },
"async-timeout": {
"hashes": [
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
@@ -61,11 +79,11 @@
},
"attrs": {
"hashes": [
- "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594",
- "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"
+ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
+ "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==20.2.0"
+ "version": "==20.3.0"
},
"beautifulsoup4": {
"hashes": [
@@ -78,10 +96,10 @@
},
"certifi": {
"hashes": [
- "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
- "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
+ "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd",
+ "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"
],
- "version": "==2020.6.20"
+ "version": "==2020.11.8"
},
"cffi": {
"hashes": [
@@ -142,6 +160,13 @@
"index": "pypi",
"version": "==1.5.1"
},
+ "fakeredis": {
+ "hashes": [
+ "sha256:8070b7fce16f828beaef2c757a4354af91698685d5232404f1aeeb233529c7a5",
+ "sha256:f8c8ea764d7b6fd801e7f5486e3edd32ca991d506186f1923a01fc072e33c271"
+ ],
+ "version": "==1.4.4"
+ },
"fuzzywuzzy": {
"hashes": [
"sha256:45016e92264780e58972dca1b3d939ac864b78437422beecebb3095f8efd00e8",
@@ -150,6 +175,58 @@
"index": "pypi",
"version": "==0.18.0"
},
+ "hiredis": {
+ "hashes": [
+ "sha256:06a039208f83744a702279b894c8cf24c14fd63c59cd917dcde168b79eef0680",
+ "sha256:0a909bf501459062aa1552be1461456518f367379fdc9fdb1f2ca5e4a1fdd7c0",
+ "sha256:18402d9e54fb278cb9a8c638df6f1550aca36a009d47ecf5aa263a38600f35b0",
+ "sha256:1e4cbbc3858ec7e680006e5ca590d89a5e083235988f26a004acf7244389ac01",
+ "sha256:23344e3c2177baf6975fbfa361ed92eb7d36d08f454636e5054b3faa7c2aff8a",
+ "sha256:289b31885b4996ce04cadfd5fc03d034dce8e2a8234479f7c9e23b9e245db06b",
+ "sha256:2c1c570ae7bf1bab304f29427e2475fe1856814312c4a1cf1cd0ee133f07a3c6",
+ "sha256:2c227c0ed371771ffda256034427320870e8ea2e4fd0c0a618c766e7c49aad73",
+ "sha256:3bb9b63d319402cead8bbd9dd55dca3b667d2997e9a0d8a1f9b6cc274db4baee",
+ "sha256:3ef2183de67b59930d2db8b8e8d4d58e00a50fcc5e92f4f678f6eed7a1c72d55",
+ "sha256:43b8ed3dbfd9171e44c554cb4acf4ee4505caa84c5e341858b50ea27dd2b6e12",
+ "sha256:47bcf3c5e6c1e87ceb86cdda2ee983fa0fe56a999e6185099b3c93a223f2fa9b",
+ "sha256:5263db1e2e1e8ae30500cdd75a979ff99dcc184201e6b4b820d0de74834d2323",
+ "sha256:5b1451727f02e7acbdf6aae4e06d75f66ee82966ff9114550381c3271a90f56c",
+ "sha256:6996883a8a6ff9117cbb3d6f5b0dcbbae6fb9e31e1a3e4e2f95e0214d9a1c655",
+ "sha256:6c96f64a54f030366657a54bb90b3093afc9c16c8e0dfa29fc0d6dbe169103a5",
+ "sha256:7332d5c3e35154cd234fd79573736ddcf7a0ade7a986db35b6196b9171493e75",
+ "sha256:7885b6f32c4a898e825bb7f56f36a02781ac4a951c63e4169f0afcf9c8c30dfb",
+ "sha256:7b0f63f10a166583ab744a58baad04e0f52cfea1ac27bfa1b0c21a48d1003c23",
+ "sha256:819f95d4eba3f9e484dd115ab7ab72845cf766b84286a00d4ecf76d33f1edca1",
+ "sha256:8968eeaa4d37a38f8ca1f9dbe53526b69628edc9c42229a5b2f56d98bb828c1f",
+ "sha256:89ebf69cb19a33d625db72d2ac589d26e936b8f7628531269accf4a3196e7872",
+ "sha256:8daecd778c1da45b8bd54fd41ffcd471a86beed3d8e57a43acf7a8d63bba4058",
+ "sha256:955ba8ea73cf3ed8bd2f963b4cb9f8f0dcb27becd2f4b3dd536fd24c45533454",
+ "sha256:964f18a59f5a64c0170f684c417f4fe3e695a536612e13074c4dd5d1c6d7c882",
+ "sha256:969843fbdfbf56cdb71da6f0bdf50f9985b8b8aeb630102945306cf10a9c6af2",
+ "sha256:996021ef33e0f50b97ff2d6b5f422a0fe5577de21a8873b58a779a5ddd1c3132",
+ "sha256:9e9c9078a7ce07e6fce366bd818be89365a35d2e4b163268f0ca9ba7e13bb2f6",
+ "sha256:a04901757cb0fb0f5602ac11dda48f5510f94372144d06c2563ba56c480b467c",
+ "sha256:a7bf1492429f18d205f3a818da3ff1f242f60aa59006e53dee00b4ef592a3363",
+ "sha256:aa0af2deb166a5e26e0d554b824605e660039b161e37ed4f01b8d04beec184f3",
+ "sha256:abfb15a6a7822f0fae681785cb38860e7a2cb1616a708d53df557b3d76c5bfd4",
+ "sha256:b253fe4df2afea4dfa6b1fa8c5fef212aff8bcaaeb4207e81eed05cb5e4a7919",
+ "sha256:b27f082f47d23cffc4cf1388b84fdc45c4ef6015f906cd7e0d988d9e35d36349",
+ "sha256:b33aea449e7f46738811fbc6f0b3177c6777a572207412bbbf6f525ffed001ae",
+ "sha256:b44f9421c4505c548435244d74037618f452844c5d3c67719d8a55e2613549da",
+ "sha256:bcc371151d1512201d0214c36c0c150b1dc64f19c2b1a8c9cb1d7c7c15ebd93f",
+ "sha256:c2851deeabd96d3f6283e9c6b26e0bfed4de2dc6fb15edf913e78b79fc5909ed",
+ "sha256:cdfd501c7ac5b198c15df800a3a34c38345f5182e5f80770caf362bccca65628",
+ "sha256:d2c0caffa47606d6d7c8af94ba42547bd2a441f06c74fd90a1ffe328524a6c64",
+ "sha256:dcb2db95e629962db5a355047fb8aefb012df6c8ae608930d391619dbd96fd86",
+ "sha256:e0eeb9c112fec2031927a1745788a181d0eecbacbed941fc5c4f7bc3f7b273bf",
+ "sha256:e154891263306200260d7f3051982774d7b9ef35af3509d5adbbe539afd2610c",
+ "sha256:e2e023a42dcbab8ed31f97c2bcdb980b7fbe0ada34037d87ba9d799664b58ded",
+ "sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390",
+ "sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==1.1.0"
+ },
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
@@ -316,6 +393,14 @@
"index": "pypi",
"version": "==5.3.1"
},
+ "redis": {
+ "hashes": [
+ "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
+ "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==3.5.3"
+ },
"sentry-sdk": {
"hashes": [
"sha256:0e5e947d0f7a969314aa23669a94a9712be5a688ff069ff7b9fc36c66adc160c",
@@ -332,6 +417,13 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
},
+ "sortedcontainers": {
+ "hashes": [
+ "sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f",
+ "sha256:59cc937650cf60d677c16775597c89a960658a09cf7c1a668f86e1e4464b10a1"
+ ],
+ "version": "==2.3.0"
+ },
"soupsieve": {
"hashes": [
"sha256:1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55",
@@ -342,11 +434,11 @@
},
"urllib3": {
"hashes": [
- "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
- "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
+ "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
+ "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
- "version": "==1.25.11"
+ "version": "==1.26.2"
},
"yarl": {
"hashes": [
@@ -382,11 +474,11 @@
},
"attrs": {
"hashes": [
- "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594",
- "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"
+ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
+ "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==20.2.0"
+ "version": "==20.3.0"
},
"cfgv": {
"hashes": [
@@ -482,11 +574,11 @@
},
"identify": {
"hashes": [
- "sha256:3139bf72d81dfd785b0a464e2776bd59bdc725b4cc10e6cf46b56a0db931c82e",
- "sha256:969d844b7a85d32a5f9ac4e163df6e846d73c87c8b75847494ee8f4bd2186421"
+ "sha256:5dd84ac64a9a115b8e0b27d1756b244b882ad264c3c423f42af8235a6e71ca12",
+ "sha256:c9504ba6a043ee2db0a9d69e43246bc138034895f6338d5aed1b41e4a73b1513"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==1.5.6"
+ "version": "==1.5.9"
},
"mccabe": {
"hashes": [
@@ -576,10 +668,11 @@
},
"toml": {
"hashes": [
- "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
- "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
+ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+ "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
- "version": "==0.10.1"
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==0.10.2"
},
"virtualenv": {
"hashes": [
diff --git a/README.md b/README.md
index 0c9d1e7d..3301ccda 100755
--- a/README.md
+++ b/README.md
@@ -1,10 +1,12 @@
-# SeasonalBot
+# Sir Lancebot
[![Lint Badge][1]][2]
[![Build Badge][3]][4]
[![Discord](https://img.shields.io/static/v1?label=Python%20Discord&logo=discord&message=%3E100k%20members&color=%237289DA&logoColor=white)](https://discord.gg/2B963hn)
-A Discord bot for the Python Discord community which changes with the seasons, and provides useful event features.
+![Header](sir-lancebot-logo.png)
+
+A Discord bot built by the Python Discord community, for the Python Discord community.
You can find our community by going to https://discord.gg/python
@@ -12,14 +14,14 @@ You can find our community by going to https://discord.gg/python
We know it can be difficult to get into the whole open source thing at first. To help out, we started the HacktoberBot community project during [Hacktoberfest 2018](https://hacktoberfest.digitalocean.com) to help introduce and encourage members to participate in contributing to open source, providing a calmer and helpful environment for those who want to be part of it.
-This later evolved into a bot that runs all year, providing season-appropriate functionality and issues that beginners can work on.
+This later evolved into a bot designed as a fun and beginner-friendly learning environment for writing bot features and learning open-source.
## Getting started
Before you start, please take some time to read through our [contributing guidelines](CONTRIBUTING.md).
-See [Seasonalbot's Wiki](https://pythondiscord.com/pages/contributing/seasonalbot/) for in-depth guides on getting started with the project!
+See [Sir Lancebot's Wiki](https://pythondiscord.com/pages/contributing/sir-lancebot/) for in-depth guides on getting started with the project!
-[1]:https://github.com/python-discord/seasonalbot/workflows/Lint/badge.svg?branch=master
-[2]:https://github.com/python-discord/seasonalbot/actions?query=workflow%3ALint+branch%3Amaster
-[3]:https://github.com/python-discord/seasonalbot/workflows/Build/badge.svg?branch=master
-[4]:https://github.com/python-discord/seasonalbot/actions?query=workflow%3ABuild+branch%3Amaster
+[1]:https://github.com/python-discord/sir-lancebot/workflows/Lint/badge.svg?branch=master
+[2]:https://github.com/python-discord/sir-lancebot/actions?query=workflow%3ALint+branch%3Amaster
+[3]:https://github.com/python-discord/sir-lancebot/workflows/Build/badge.svg?branch=master
+[4]:https://github.com/python-discord/sir-lancebot/actions?query=workflow%3ABuild+branch%3Amaster
diff --git a/bot/bot.py b/bot/bot.py
index cef4c4d4..97b09243 100644
--- a/bot/bot.py
+++ b/bot/bot.py
@@ -1,36 +1,22 @@
import asyncio
-import enum
import logging
import socket
-from typing import Optional, Union
+from typing import Optional
-import async_timeout
import discord
from aiohttp import AsyncResolver, ClientSession, TCPConnector
-from discord import DiscordException, Embed, Guild, User
+from async_rediscache import RedisSession
+from discord import DiscordException, Embed
from discord.ext import commands
-from bot.constants import Channels, Client, MODERATION_ROLES
-from bot.utils.decorators import mock_in_debug
+from bot import constants
log = logging.getLogger(__name__)
-__all__ = ("AssetType", "SeasonalBot", "bot")
+__all__ = ("Bot", "bot")
-class AssetType(enum.Enum):
- """
- Discord media assets.
-
- The values match exactly the kwarg keys that can be passed to `Guild.edit` or `User.edit`.
- """
-
- BANNER = "banner"
- AVATAR = "avatar"
- SERVER_ICON = "icon"
-
-
-class SeasonalBot(commands.Bot):
+class Bot(commands.Bot):
"""
Base bot instance.
@@ -39,23 +25,36 @@ class SeasonalBot(commands.Bot):
that the upload was successful. See the `mock_in_debug` decorator for further details.
"""
- def __init__(self, **kwargs):
+ name = constants.Client.name
+
+ def __init__(self, redis_session: RedisSession, **kwargs):
super().__init__(**kwargs)
self.http_session = ClientSession(
connector=TCPConnector(resolver=AsyncResolver(), family=socket.AF_INET)
)
self._guild_available = asyncio.Event()
+ self.redis_session = redis_session
- self.loop.create_task(self.send_log("SeasonalBot", "Connected!"))
+ self.loop.create_task(self.send_log(self.name, "Connected!"))
@property
def member(self) -> Optional[discord.Member]:
"""Retrieves the guild member object for the bot."""
- guild = self.get_guild(Client.guild)
+ guild = self.get_guild(constants.Client.guild)
if not guild:
return None
return guild.me
+ async def close(self) -> None:
+ """Close Redis session when bot is shutting down."""
+ await super().close()
+
+ if self.http_session:
+ await self.http_session.close()
+
+ if self.redis_session:
+ await self.redis_session.close()
+
def add_cog(self, cog: commands.Cog) -> None:
"""
Delegate to super to register `cog`.
@@ -72,92 +71,15 @@ class SeasonalBot(commands.Bot):
else:
await super().on_command_error(context, exception)
- async def _fetch_image(self, url: str) -> bytes:
- """Retrieve and read image from `url`."""
- log.debug(f"Getting image from: {url}")
- async with self.http_session.get(url) as resp:
- return await resp.read()
-
- async def _apply_asset(self, target: Union[Guild, User], asset: AssetType, url: str) -> bool:
- """
- Internal method for applying media assets to the guild or the bot.
-
- This shouldn't be called directly. The purpose of this method is mainly generic
- error handling to reduce needless code repetition.
-
- Return True if upload was successful, False otherwise.
- """
- log.info(f"Attempting to set {asset.name}: {url}")
-
- kwargs = {asset.value: await self._fetch_image(url)}
- try:
- async with async_timeout.timeout(5):
- await target.edit(**kwargs)
-
- except asyncio.TimeoutError:
- log.info("Asset upload timed out")
- return False
-
- except discord.HTTPException as discord_error:
- log.exception("Asset upload failed", exc_info=discord_error)
- return False
-
- else:
- log.info("Asset successfully applied")
- return True
-
- @mock_in_debug(return_value=True)
- async def set_banner(self, url: str) -> bool:
- """Set the guild's banner to image at `url`."""
- guild = self.get_guild(Client.guild)
- if guild is None:
- log.info("Failed to get guild instance, aborting asset upload")
- return False
-
- return await self._apply_asset(guild, AssetType.BANNER, url)
-
- @mock_in_debug(return_value=True)
- async def set_icon(self, url: str) -> bool:
- """Sets the guild's icon to image at `url`."""
- guild = self.get_guild(Client.guild)
- if guild is None:
- log.info("Failed to get guild instance, aborting asset upload")
- return False
-
- return await self._apply_asset(guild, AssetType.SERVER_ICON, url)
-
- @mock_in_debug(return_value=True)
- async def set_avatar(self, url: str) -> bool:
- """Set the bot's avatar to image at `url`."""
- return await self._apply_asset(self.user, AssetType.AVATAR, url)
-
- @mock_in_debug(return_value=True)
- async def set_nickname(self, new_name: str) -> bool:
- """Set the bot nickname in the main guild to `new_name`."""
- member = self.member
- if member is None:
- log.info("Failed to get bot member instance, aborting asset upload")
- return False
-
- log.info(f"Attempting to set nickname to {new_name}")
- try:
- await member.edit(nick=new_name)
- except discord.HTTPException as discord_error:
- log.exception("Setting nickname failed", exc_info=discord_error)
- return False
- else:
- log.info("Nickname set successfully")
- return True
-
async def send_log(self, title: str, details: str = None, *, icon: str = None) -> None:
"""Send an embed message to the devlog channel."""
await self.wait_until_guild_available()
- devlog = self.get_channel(Channels.devlog)
+ devlog = self.get_channel(constants.Channels.devlog)
if not devlog:
- log.info(f"Fetching devlog channel as it wasn't found in the cache (ID: {Channels.devlog})")
+ log.info(f"Fetching devlog channel as it wasn't found in the cache (ID: {constants.Channels.devlog})")
try:
- devlog = await self.fetch_channel(Channels.devlog)
+ devlog = await self.fetch_channel(constants.Channels.devlog)
except discord.HTTPException as discord_exc:
log.exception("Fetch failed", exc_info=discord_exc)
return
@@ -177,7 +99,7 @@ class SeasonalBot(commands.Bot):
If the cache appears to still be empty (no members, no channels, or no roles), the event
will not be set.
"""
- if guild.id != Client.guild:
+ if guild.id != constants.Client.guild:
return
if not guild.roles or not guild.members or not guild.channels:
@@ -188,7 +110,7 @@ class SeasonalBot(commands.Bot):
async def on_guild_unavailable(self, guild: discord.Guild) -> None:
"""Clear the internal `_guild_available` event when PyDis guild becomes unavailable."""
- if guild.id != Client.guild:
+ if guild.id != constants.Client.guild:
return
self._guild_available.clear()
@@ -203,7 +125,7 @@ class SeasonalBot(commands.Bot):
await self._guild_available.wait()
-_allowed_roles = [discord.Object(id_) for id_ in MODERATION_ROLES]
+_allowed_roles = [discord.Object(id_) for id_ in constants.MODERATION_ROLES]
_intents = discord.Intents.default() # Default is all intents except for privileged ones (Members, Presences, ...)
_intents.bans = False
@@ -212,9 +134,21 @@ _intents.invites = False
_intents.typing = False
_intents.webhooks = False
-bot = SeasonalBot(
- command_prefix=Client.prefix,
- activity=discord.Game(name=f"Commands: {Client.prefix}help"),
+redis_session = RedisSession(
+ address=(constants.RedisConfig.host, constants.RedisConfig.port),
+ password=constants.RedisConfig.password,
+ minsize=1,
+ maxsize=20,
+ use_fakeredis=constants.RedisConfig.use_fakeredis,
+ global_namespace="sir-lancebot"
+)
+loop = asyncio.get_event_loop()
+loop.run_until_complete(redis_session.connect())
+
+bot = Bot(
+ redis_session=redis_session,
+ command_prefix=constants.Client.prefix,
+ activity=discord.Game(name=f"Commands: {constants.Client.prefix}help"),
allowed_mentions=discord.AllowedMentions(everyone=False, roles=_allowed_roles),
intents=_intents,
)
diff --git a/bot/constants.py b/bot/constants.py
index f1f34886..6999f321 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -17,6 +17,7 @@ __all__ = (
"Roles",
"Tokens",
"Wolfram",
+ "RedisConfig",
"MODERATION_ROLES",
"STAFF_ROLES",
"WHITELISTED_CHANNELS",
@@ -67,13 +68,10 @@ class Channels(NamedTuple):
off_topic_2 = 463035268514185226
python = 267624335836053506
reddit = 458224812528238616
- seasonalbot_commands = int(environ.get("CHANNEL_SEASONALBOT_COMMANDS", 607247579608121354))
- seasonalbot_voice = int(environ.get("CHANNEL_SEASONALBOT_VOICE", 606259004230074378))
+ community_bot_commands = int(environ.get("CHANNEL_COMMUNITY_BOT_COMMANDS", 607247579608121354))
staff_lounge = 464905259261755392
verification = 352442727016693763
python_discussion = 267624335836053506
- show_your_projects = int(environ.get("CHANNEL_SHOW_YOUR_PROJECTS", 303934982764625920))
- show_your_projects_discussion = 360148304664723466
hacktoberfest_2020 = 760857070781071431
voice_chat = 412357430186344448
@@ -96,12 +94,13 @@ class Channels(NamedTuple):
class Client(NamedTuple):
- guild = int(environ.get("SEASONALBOT_GUILD", 267624335836053506))
+ name = "Sir Lancebot"
+ guild = int(environ.get("BOT_GUILD", 267624335836053506))
prefix = environ.get("PREFIX", ".")
- token = environ.get("SEASONALBOT_TOKEN")
- sentry_dsn = environ.get("SEASONALBOT_SENTRY_DSN")
- debug = environ.get("SEASONALBOT_DEBUG", "").lower() == "true"
- github_bot_repo = "https://github.com/python-discord/seasonalbot"
+ token = environ.get("BOT_TOKEN")
+ sentry_dsn = environ.get("BOT_SENTRY_DSN")
+ debug = environ.get("BOT_DEBUG", "").lower() == "true"
+ github_bot_repo = "https://github.com/python-discord/sir-lancebot"
# Override seasonal locks: 1 (January) to 12 (December)
month_override = int(environ["MONTH_OVERRIDE"]) if "MONTH_OVERRIDE" in environ else None
@@ -183,7 +182,7 @@ if Client.month_override is not None:
class Roles(NamedTuple):
- admin = int(environ.get("SEASONALBOT_ADMIN_ROLE_ID", 267628507062992896))
+ admin = int(environ.get("BOT_ADMIN_ROLE_ID", 267628507062992896))
announcements = 463658397560995840
champion = 430492892331769857
contributor = 295488872404484098
@@ -216,6 +215,22 @@ class Wolfram(NamedTuple):
key = environ.get("WOLFRAM_API_KEY")
+class RedisConfig(NamedTuple):
+ host = environ.get("REDIS_HOST", "redis.default.svc.cluster.local")
+ port = environ.get("REDIS_PORT", 6379)
+ password = environ.get("REDIS_PASSWORD")
+ use_fakeredis = environ.get("USE_FAKEREDIS", "false").lower() == "true"
+
+
+class Wikipedia:
+ total_chance = 3
+
+
+class Source:
+ github = "https://github.com/python-discord/sir-lancebot"
+ github_avatar_url = "https://avatars1.githubusercontent.com/u/9919"
+
+
# Default role combinations
MODERATION_ROLES = Roles.moderator, Roles.admin, Roles.owner
STAFF_ROLES = Roles.helpers, Roles.moderator, Roles.admin, Roles.owner
@@ -223,7 +238,7 @@ STAFF_ROLES = Roles.helpers, Roles.moderator, Roles.admin, Roles.owner
# Whitelisted channels
WHITELISTED_CHANNELS = (
Channels.bot,
- Channels.seasonalbot_commands,
+ Channels.community_bot_commands,
Channels.off_topic_0,
Channels.off_topic_1,
Channels.off_topic_2,
@@ -300,10 +315,3 @@ POSITIVE_REPLIES = [
"Aye aye, cap'n!",
"I'll allow it.",
]
-
-class Wikipedia:
- total_chance = 3
-
-class Source:
- github = "https://github.com/python-discord/seasonalbot"
- github_avatar_url = "https://avatars1.githubusercontent.com/u/9919"
diff --git a/bot/exts/easter/egg_facts.py b/bot/exts/easter/egg_facts.py
index 0051aa50..761e9059 100644
--- a/bot/exts/easter/egg_facts.py
+++ b/bot/exts/easter/egg_facts.py
@@ -6,7 +6,7 @@ from pathlib import Path
import discord
from discord.ext import commands
-from bot.bot import SeasonalBot
+from bot.bot import Bot
from bot.constants import Channels, Colours, Month
from bot.utils.decorators import seasonal_task
@@ -20,7 +20,7 @@ class EasterFacts(commands.Cog):
It also contains a background task which sends an easter egg fact in the event channel everyday.
"""
- def __init__(self, bot: SeasonalBot):
+ def __init__(self, bot: Bot):
self.bot = bot
self.facts = self.load_json()
@@ -38,7 +38,7 @@ class EasterFacts(commands.Cog):
"""A background task that sends an easter egg fact in the event channel everyday."""
await self.bot.wait_until_guild_available()
- channel = self.bot.get_channel(Channels.seasonalbot_commands)
+ channel = self.bot.get_channel(Channels.community_bot_commands)
await channel.send(embed=self.make_embed())
@commands.command(name='eggfact', aliases=['fact'])
@@ -56,6 +56,6 @@ class EasterFacts(commands.Cog):
)
-def setup(bot: SeasonalBot) -> None:
+def setup(bot: Bot) -> None:
"""Easter Egg facts cog load."""
bot.add_cog(EasterFacts(bot))
diff --git a/bot/exts/evergreen/branding.py b/bot/exts/evergreen/branding.py
deleted file mode 100644
index fa607270..00000000
--- a/bot/exts/evergreen/branding.py
+++ /dev/null
@@ -1,540 +0,0 @@
-import asyncio
-import itertools
-# import json
-import logging
-import random
-import typing as t
-from datetime import datetime, time, timedelta
-from pathlib import Path
-
-import arrow
-import discord
-from discord.embeds import EmptyEmbed
-from discord.ext import commands
-
-from bot.bot import SeasonalBot
-from bot.constants import Branding, Colours, Emojis, MODERATION_ROLES, Tokens
-from bot.seasons import SeasonBase, get_all_seasons, get_current_season, get_season
-from bot.utils import human_months
-from bot.utils.decorators import with_role
-from bot.utils.exceptions import BrandingError
-# TODO: Implement substitute for current volume persistence requirements # noqa: T000
-# from bot.utils.persist import make_persistent
-
-log = logging.getLogger(__name__)
-
-STATUS_OK = 200 # HTTP status code
-
-FILE_BANNER = "banner.png"
-FILE_AVATAR = "avatar.png"
-SERVER_ICONS = "server_icons"
-
-BRANDING_URL = "https://api.github.com/repos/python-discord/branding/contents"
-
-PARAMS = {"ref": "master"} # Target branch
-HEADERS = {"Accept": "application/vnd.github.v3+json"} # Ensure we use API v3
-
-# A GitHub token is not necessary for the cog to operate,
-# unauthorized requests are however limited to 60 per hour
-if Tokens.github:
- HEADERS["Authorization"] = f"token {Tokens.github}"
-
-
-class GitHubFile(t.NamedTuple):
- """
- Represents a remote file on GitHub.
-
- The `sha` hash is kept so that we can determine that a file has changed,
- despite its filename remaining unchanged.
- """
-
- download_url: str
- path: str
- sha: str
-
-
-def pretty_files(files: t.Iterable[GitHubFile]) -> str:
- """Provide a human-friendly representation of `files`."""
- return "\n".join(file.path for file in files)
-
-
-def time_until_midnight() -> timedelta:
- """
- Determine amount of time until the next-up UTC midnight.
-
- The exact `midnight` moment is actually delayed to 5 seconds after, in order
- to avoid potential problems due to imprecise sleep.
- """
- now = datetime.utcnow()
- tomorrow = now + timedelta(days=1)
- midnight = datetime.combine(tomorrow, time(second=5))
-
- return midnight - now
-
-
-class BrandingManager(commands.Cog):
- """
- Manages the guild's branding.
-
- The purpose of this cog is to help automate the synchronization of the branding
- repository with the guild. It is capable of discovering assets in the repository
- via GitHub's API, resolving download urls for them, and delegating
- to the `bot` instance to upload them to the guild.
-
- BrandingManager is designed to be entirely autonomous. Its `daemon` background task awakens
- once a day (see `time_until_midnight`) to detect new seasons, or to cycle icons within a single
- season. The daemon can be turned on and off via the `daemon` cmd group. The value set via
- its `start` and `stop` commands is persisted across sessions. If turned on, the daemon will
- automatically start on the next bot start-up. Otherwise, it will wait to be started manually.
-
- All supported operations, e.g. setting seasons, applying the branding, or cycling icons, can
- also be invoked manually, via the following API:
-
- branding list
- - Show all available seasons
-
- branding set <season_name>
- - Set the cog's internal state to represent `season_name`, if it exists
- - If no `season_name` is given, set chronologically current season
- - This will not automatically apply the season's branding to the guild,
- the cog's state can be detached from the guild
- - Seasons can therefore be 'previewed' using this command
-
- branding info
- - View detailed information about resolved assets for current season
-
- branding refresh
- - Refresh internal state, i.e. synchronize with branding repository
-
- branding apply
- - Apply the current internal state to the guild, i.e. upload the assets
-
- branding cycle
- - If there are multiple available icons for current season, randomly pick
- and apply the next one
-
- The daemon calls these methods autonomously as appropriate. The use of this cog
- is locked to moderation roles. As it performs media asset uploads, it is prone to
- rate-limits - the `apply` command should be used with caution. The `set` command can,
- however, be used freely to 'preview' seasonal branding and check whether paths have been
- resolved as appropriate.
-
- While the bot is in debug mode, it will 'mock' asset uploads by logging the passed
- download urls and pretending that the upload was successful. Make use of this
- to test this cog's behaviour.
- """
-
- current_season: t.Type[SeasonBase]
-
- banner: t.Optional[GitHubFile]
- avatar: t.Optional[GitHubFile]
-
- available_icons: t.List[GitHubFile]
- remaining_icons: t.List[GitHubFile]
-
- days_since_cycle: t.Iterator
-
- config_file: Path
-
- daemon: t.Optional[asyncio.Task]
-
- def __init__(self, bot: SeasonalBot) -> None:
- """
- Assign safe default values on init.
-
- At this point, we don't have information about currently available branding.
- Most of these attributes will be overwritten once the daemon connects, or once
- the `refresh` command is used.
- """
- self.bot = bot
- self.current_season = get_current_season()
-
- self.banner = None
- self.avatar = None
-
- self.available_icons = []
- self.remaining_icons = []
-
- self.days_since_cycle = itertools.cycle([None])
-
- # self.config_file = make_persistent(Path("bot", "resources", "evergreen", "branding.json"))
-
- # should_run = self._read_config()["daemon_active"]
-
- # if should_run:
- # self.daemon = self.bot.loop.create_task(self._daemon_func())
- # else:
- self.daemon = None
-
- @property
- def _daemon_running(self) -> bool:
- """True if the daemon is currently active, False otherwise."""
- return self.daemon is not None and not self.daemon.done()
-
- def _read_config(self) -> t.Dict[str, bool]:
- """Read and return persistent config file."""
- raise NotImplementedError("read_config functionality requires mounting a persistent volume.")
-
- def _write_config(self, key: str, value: bool) -> None:
- """Write a `key`, `value` pair to persistent config file."""
- raise NotImplementedError("write_config functionality requires mounting a persistent volume.")
-
- async def _daemon_func(self) -> None:
- """
- Manage all automated behaviour of the BrandingManager cog.
-
- Once a day, the daemon will perform the following tasks:
- - Update `current_season`
- - Poll GitHub API to see if the available branding for `current_season` has changed
- - Update assets if changes are detected (banner, guild icon, bot avatar, bot nickname)
- - Check whether it's time to cycle guild icons
-
- The internal loop runs once when activated, then periodically at the time
- given by `time_until_midnight`.
-
- All method calls in the internal loop are considered safe, i.e. no errors propagate
- to the daemon's loop. The daemon itself does not perform any error handling on its own.
- """
- await self.bot.wait_until_guild_available()
-
- while True:
- self.current_season = get_current_season()
- branding_changed = await self.refresh()
-
- if branding_changed:
- await self.apply()
-
- elif next(self.days_since_cycle) == Branding.cycle_frequency:
- await self.cycle()
-
- until_midnight = time_until_midnight()
- await asyncio.sleep(until_midnight.total_seconds())
-
- async def _info_embed(self) -> discord.Embed:
- """Make an informative embed representing current season."""
- info_embed = discord.Embed(description=self.current_season.description, colour=self.current_season.colour)
-
- # If we're in a non-evergreen season, also show active months
- if self.current_season is not SeasonBase:
- title = f"{self.current_season.season_name} ({human_months(self.current_season.months)})"
- else:
- title = self.current_season.season_name
-
- # Use the author field to show the season's name and avatar if available
- info_embed.set_author(name=title, icon_url=self.avatar.download_url if self.avatar else EmptyEmbed)
-
- banner = self.banner.path if self.banner is not None else "Unavailable"
- info_embed.add_field(name="Banner", value=banner, inline=False)
-
- avatar = self.avatar.path if self.avatar is not None else "Unavailable"
- info_embed.add_field(name="Avatar", value=avatar, inline=False)
-
- icons = pretty_files(self.available_icons) or "Unavailable"
- info_embed.add_field(name="Available icons", value=icons, inline=False)
-
- # Only display cycle frequency if we're actually cycling
- if len(self.available_icons) > 1 and Branding.cycle_frequency:
- info_embed.set_footer(text=f"Icon cycle frequency: {Branding.cycle_frequency}")
-
- return info_embed
-
- async def _reset_remaining_icons(self) -> None:
- """Set `remaining_icons` to a shuffled copy of `available_icons`."""
- self.remaining_icons = random.sample(self.available_icons, k=len(self.available_icons))
-
- async def _reset_days_since_cycle(self) -> None:
- """
- Reset the `days_since_cycle` iterator based on configured frequency.
-
- If the current season only has 1 icon, or if `Branding.cycle_frequency` is falsey,
- the iterator will always yield None. This signals that the icon shouldn't be cycled.
-
- Otherwise, it will yield ints in range [1, `Branding.cycle_frequency`] indefinitely.
- When the iterator yields a value equal to `Branding.cycle_frequency`, it is time to cycle.
- """
- if len(self.available_icons) > 1 and Branding.cycle_frequency:
- sequence = range(1, Branding.cycle_frequency + 1)
- else:
- sequence = [None]
-
- self.days_since_cycle = itertools.cycle(sequence)
-
- async def _get_files(self, path: str, include_dirs: bool = False) -> t.Dict[str, GitHubFile]:
- """
- Get files at `path` in the branding repository.
-
- If `include_dirs` is False (default), only returns files at `path`.
- Otherwise, will return both files and directories. Never returns symlinks.
-
- Return dict mapping from filename to corresponding `GitHubFile` instance.
- This may return an empty dict if the response status is non-200,
- or if the target directory is empty.
- """
- url = f"{BRANDING_URL}/{path}"
- async with self.bot.http_session.get(url, headers=HEADERS, params=PARAMS) as resp:
- # Short-circuit if we get non-200 response
- if resp.status != STATUS_OK:
- log.error(f"GitHub API returned non-200 response: {resp}")
- return {}
- directory = await resp.json() # Directory at `path`
-
- allowed_types = {"file", "dir"} if include_dirs else {"file"}
- return {
- file["name"]: GitHubFile(file["download_url"], file["path"], file["sha"])
- for file in directory
- if file["type"] in allowed_types
- }
-
- async def refresh(self) -> bool:
- """
- Synchronize available assets with branding repository.
-
- If the current season is not the evergreen, and lacks at least one asset,
- we use the evergreen seasonal dir as fallback for missing assets.
-
- Finally, if neither the seasonal nor fallback branding directories contain
- an asset, it will simply be ignored.
-
- Return True if the branding has changed. This will be the case when we enter
- a new season, or when something changes in the current seasons's directory
- in the branding repository.
- """
- old_branding = (self.banner, self.avatar, self.available_icons)
- seasonal_dir = await self._get_files(self.current_season.branding_path, include_dirs=True)
-
- # Only make a call to the fallback directory if there is something to be gained
- branding_incomplete = any(
- asset not in seasonal_dir
- for asset in (FILE_BANNER, FILE_AVATAR, SERVER_ICONS)
- )
- if branding_incomplete and self.current_season is not SeasonBase:
- fallback_dir = await self._get_files(SeasonBase.branding_path, include_dirs=True)
- else:
- fallback_dir = {}
-
- # Resolve assets in this directory, None is a safe value
- self.banner = seasonal_dir.get(FILE_BANNER) or fallback_dir.get(FILE_BANNER)
- self.avatar = seasonal_dir.get(FILE_AVATAR) or fallback_dir.get(FILE_AVATAR)
-
- # Now resolve server icons by making a call to the proper sub-directory
- if SERVER_ICONS in seasonal_dir:
- icons_dir = await self._get_files(f"{self.current_season.branding_path}/{SERVER_ICONS}")
- self.available_icons = list(icons_dir.values())
-
- elif SERVER_ICONS in fallback_dir:
- icons_dir = await self._get_files(f"{SeasonBase.branding_path}/{SERVER_ICONS}")
- self.available_icons = list(icons_dir.values())
-
- else:
- self.available_icons = [] # This should never be the case, but an empty list is a safe value
-
- # GitHubFile instances carry a `sha` attr so this will pick up if a file changes
- branding_changed = old_branding != (self.banner, self.avatar, self.available_icons)
-
- if branding_changed:
- log.info(f"New branding detected (season: {self.current_season.season_name})")
- await self._reset_remaining_icons()
- await self._reset_days_since_cycle()
-
- return branding_changed
-
- async def cycle(self) -> bool:
- """
- Apply the next-up server icon.
-
- Returns True if an icon is available and successfully gets applied, False otherwise.
- """
- if not self.available_icons:
- log.info("Cannot cycle: no icons for this season")
- return False
-
- if not self.remaining_icons:
- log.info("Reset & shuffle remaining icons")
- await self._reset_remaining_icons()
-
- next_up = self.remaining_icons.pop(0)
- success = await self.bot.set_icon(next_up.download_url)
-
- return success
-
- async def apply(self) -> t.List[str]:
- """
- Apply current branding to the guild and bot.
-
- This delegates to the bot instance to do all the work. We only provide download urls
- for available assets. Assets unavailable in the branding repo will be ignored.
-
- Returns a list of names of all failed assets. An asset is considered failed
- if it isn't found in the branding repo, or if something goes wrong while the
- bot is trying to apply it.
-
- An empty list denotes that all assets have been applied successfully.
- """
- report = {asset: False for asset in ("banner", "avatar", "nickname", "icon")}
-
- if self.banner is not None:
- report["banner"] = await self.bot.set_banner(self.banner.download_url)
-
- if self.avatar is not None:
- report["avatar"] = await self.bot.set_avatar(self.avatar.download_url)
-
- if self.current_season.bot_name:
- report["nickname"] = await self.bot.set_nickname(self.current_season.bot_name)
-
- report["icon"] = await self.cycle()
-
- failed_assets = [asset for asset, succeeded in report.items() if not succeeded]
- return failed_assets
-
- @with_role(*MODERATION_ROLES)
- @commands.group(name="branding")
- async def branding_cmds(self, ctx: commands.Context) -> None:
- """Manual branding control."""
- if not ctx.invoked_subcommand:
- await ctx.send_help(ctx.command)
-
- @branding_cmds.command(name="list", aliases=["ls"])
- async def branding_list(self, ctx: commands.Context) -> None:
- """List all available seasons and branding sources."""
- embed = discord.Embed(title="Available seasons", colour=Colours.soft_green)
-
- for season in get_all_seasons():
- if season is SeasonBase:
- active_when = "always"
- else:
- active_when = f"in {human_months(season.months)}"
-
- description = (
- f"Active {active_when}\n"
- f"Branding: {season.branding_path}"
- )
- embed.add_field(name=season.season_name, value=description, inline=False)
-
- await ctx.send(embed=embed)
-
- @branding_cmds.command(name="set")
- async def branding_set(self, ctx: commands.Context, *, season_name: t.Optional[str] = None) -> None:
- """
- Manually set season, or reset to current if none given.
-
- Season search is a case-less comparison against both seasonal class name,
- and its `season_name` attr.
-
- This only pre-loads the cog's internal state to the chosen season, but does not
- automatically apply the branding. As that is an expensive operation, the `apply`
- command must be called explicitly after this command finishes.
-
- This means that this command can be used to 'preview' a season gathering info
- about its available assets, without applying them to the guild.
-
- If the daemon is running, it will automatically reset the season to current when
- it wakes up. The season set via this command can therefore remain 'detached' from
- what it should be - the daemon will make sure that it's set back properly.
- """
- if season_name is None:
- new_season = get_current_season()
- else:
- new_season = get_season(season_name)
- if new_season is None:
- raise BrandingError("No such season exists")
-
- if self.current_season is new_season:
- raise BrandingError(f"Season {self.current_season.season_name} already active")
-
- self.current_season = new_season
- await self.branding_refresh(ctx)
-
- @branding_cmds.command(name="info", aliases=["status"])
- async def branding_info(self, ctx: commands.Context) -> None:
- """
- Show available assets for current season.
-
- This can be used to confirm that assets have been resolved properly.
- When `apply` is used, it attempts to upload exactly the assets listed here.
- """
- await ctx.send(embed=await self._info_embed())
-
- @branding_cmds.command(name="refresh")
- async def branding_refresh(self, ctx: commands.Context) -> None:
- """Sync currently available assets with branding repository."""
- async with ctx.typing():
- await self.refresh()
- await self.branding_info(ctx)
-
- @branding_cmds.command(name="apply")
- async def branding_apply(self, ctx: commands.Context) -> None:
- """
- Apply current season's branding to the guild.
-
- Use `info` to check which assets will be applied. Shows which assets have
- failed to be applied, if any.
- """
- async with ctx.typing():
- failed_assets = await self.apply()
- if failed_assets:
- raise BrandingError(f"Failed to apply following assets: {', '.join(failed_assets)}")
-
- response = discord.Embed(description=f"All assets applied {Emojis.ok_hand}", colour=Colours.soft_green)
- await ctx.send(embed=response)
-
- @branding_cmds.command(name="cycle")
- async def branding_cycle(self, ctx: commands.Context) -> None:
- """
- Apply the next-up guild icon, if multiple are available.
-
- The order is random.
- """
- async with ctx.typing():
- success = await self.cycle()
- if not success:
- raise BrandingError("Failed to cycle icon")
-
- response = discord.Embed(description=f"Success {Emojis.ok_hand}", colour=Colours.soft_green)
- await ctx.send(embed=response)
-
- @branding_cmds.group(name="daemon", aliases=["d", "task"])
- async def daemon_group(self, ctx: commands.Context) -> None:
- """Control the background daemon."""
- if not ctx.invoked_subcommand:
- await ctx.send_help(ctx.command)
-
- @daemon_group.command(name="status")
- async def daemon_status(self, ctx: commands.Context) -> None:
- """Check whether daemon is currently active."""
- if self._daemon_running:
- remaining_time = (arrow.utcnow() + time_until_midnight()).humanize()
- response = discord.Embed(description=f"Daemon running {Emojis.ok_hand}", colour=Colours.soft_green)
- response.set_footer(text=f"Next refresh {remaining_time}")
- else:
- response = discord.Embed(description="Daemon not running", colour=Colours.soft_red)
-
- await ctx.send(embed=response)
-
- @daemon_group.command(name="start", enabled=False)
- async def daemon_start(self, ctx: commands.Context) -> None:
- """If the daemon isn't running, start it."""
- if self._daemon_running:
- raise BrandingError("Daemon already running!")
-
- self.daemon = self.bot.loop.create_task(self._daemon_func())
- self._write_config("daemon_active", True)
-
- response = discord.Embed(description=f"Daemon started {Emojis.ok_hand}", colour=Colours.soft_green)
- await ctx.send(embed=response)
-
- @daemon_group.command(name="stop", enabled=False)
- async def daemon_stop(self, ctx: commands.Context) -> None:
- """If the daemon is running, stop it."""
- if not self._daemon_running:
- raise BrandingError("Daemon not running!")
-
- self.daemon.cancel()
- self._write_config("daemon_active", False)
-
- response = discord.Embed(description=f"Daemon stopped {Emojis.ok_hand}", colour=Colours.soft_green)
- await ctx.send(embed=response)
-
-
-def setup(bot: SeasonalBot) -> None:
- """Load BrandingManager cog."""
- bot.add_cog(BrandingManager(bot))
diff --git a/bot/exts/evergreen/error_handler.py b/bot/exts/evergreen/error_handler.py
index 459a2b2d..6e518435 100644
--- a/bot/exts/evergreen/error_handler.py
+++ b/bot/exts/evergreen/error_handler.py
@@ -9,7 +9,7 @@ from sentry_sdk import push_scope
from bot.constants import Colours, ERROR_REPLIES, NEGATIVE_REPLIES
from bot.utils.decorators import InChannelCheckFailure, InMonthCheckFailure
-from bot.utils.exceptions import BrandingError, UserNotPlayingError
+from bot.utils.exceptions import UserNotPlayingError
log = logging.getLogger(__name__)
@@ -57,10 +57,6 @@ class CommandErrorHandler(commands.Cog):
if isinstance(error, commands.CommandNotFound):
return
- if isinstance(error, BrandingError):
- await ctx.send(embed=self.error_embed(str(error)))
- return
-
if isinstance(error, (InChannelCheckFailure, InMonthCheckFailure)):
await ctx.send(embed=self.error_embed(str(error), NEGATIVE_REPLIES), delete_after=7.5)
return
diff --git a/bot/exts/evergreen/game.py b/bot/exts/evergreen/game.py
index 3c8b2725..d0fd7a40 100644
--- a/bot/exts/evergreen/game.py
+++ b/bot/exts/evergreen/game.py
@@ -11,7 +11,7 @@ from discord import Embed
from discord.ext import tasks
from discord.ext.commands import Cog, Context, group
-from bot.bot import SeasonalBot
+from bot.bot import Bot
from bot.constants import STAFF_ROLES, Tokens
from bot.utils.decorators import with_role
from bot.utils.pagination import ImagePaginator, LinePaginator
@@ -130,7 +130,7 @@ class AgeRatings(IntEnum):
class Games(Cog):
"""Games Cog contains commands that collect data from IGDB."""
- def __init__(self, bot: SeasonalBot):
+ def __init__(self, bot: Bot):
self.bot = bot
self.http_session: ClientSession = bot.http_session
@@ -415,7 +415,7 @@ class Games(Cog):
return sorted((item for item in results if item[0] >= 0.60), reverse=True)[:4]
-def setup(bot: SeasonalBot) -> None:
+def setup(bot: Bot) -> None:
"""Add/Load Games cog."""
# Check does IGDB API key exist, if not, log warning and don't load cog
if not Tokens.igdb:
diff --git a/bot/exts/evergreen/help.py b/bot/exts/evergreen/help.py
index ccd76d76..91147243 100644
--- a/bot/exts/evergreen/help.py
+++ b/bot/exts/evergreen/help.py
@@ -12,7 +12,7 @@ from discord.ext.commands import CheckFailure, Cog as DiscordCog, Command, Conte
from fuzzywuzzy import fuzz, process
from bot import constants
-from bot.bot import SeasonalBot
+from bot.bot import Bot
from bot.constants import Emojis
from bot.utils.pagination import (
FIRST_EMOJI, LAST_EMOJI,
@@ -511,7 +511,7 @@ class Help(DiscordCog):
await ctx.send(embed=embed)
-def unload(bot: SeasonalBot) -> None:
+def unload(bot: Bot) -> None:
"""
Reinstates the original help command.
@@ -521,7 +521,7 @@ def unload(bot: SeasonalBot) -> None:
bot.add_command(bot._old_help)
-def setup(bot: SeasonalBot) -> None:
+def setup(bot: Bot) -> None:
"""
The setup for the help extension.
@@ -542,7 +542,7 @@ def setup(bot: SeasonalBot) -> None:
raise
-def teardown(bot: SeasonalBot) -> None:
+def teardown(bot: Bot) -> None:
"""
The teardown for the help extension.
diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py
index 97ee6a12..e419a6f5 100644
--- a/bot/exts/evergreen/issues.py
+++ b/bot/exts/evergreen/issues.py
@@ -33,7 +33,7 @@ class Issues(commands.Cog):
self,
ctx: commands.Context,
numbers: commands.Greedy[int],
- repository: str = "seasonalbot",
+ repository: str = "sir-lancebot",
user: str = "python-discord"
) -> None:
"""Command to retrieve issue(s) from a GitHub repository."""
diff --git a/bot/exts/evergreen/showprojects.py b/bot/exts/evergreen/showprojects.py
deleted file mode 100644
index 328a7aa5..00000000
--- a/bot/exts/evergreen/showprojects.py
+++ /dev/null
@@ -1,33 +0,0 @@
-import logging
-
-from discord import Message
-from discord.ext import commands
-
-from bot.constants import Channels
-
-log = logging.getLogger(__name__)
-
-
-class ShowProjects(commands.Cog):
- """Cog that reacts to posts in the #show-your-projects."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.lastPoster = 0 # Given 0 as the default last poster ID as no user can actually have 0 assigned to them
-
- @commands.Cog.listener()
- async def on_message(self, message: Message) -> None:
- """Adds reactions to posts in #show-your-projects."""
- reactions = ["\U0001f44d", "\U00002764", "\U0001f440", "\U0001f389", "\U0001f680", "\U00002b50", "\U0001f6a9"]
- if (message.channel.id == Channels.show_your_projects
- and message.author.bot is False
- and message.author.id != self.lastPoster):
- for reaction in reactions:
- await message.add_reaction(reaction)
-
- self.lastPoster = message.author.id
-
-
-def setup(bot: commands.Bot) -> None:
- """Show Projects Reaction Cog."""
- bot.add_cog(ShowProjects(bot))
diff --git a/bot/exts/evergreen/source.py b/bot/exts/evergreen/source.py
index 0725714f..cdfe54ec 100644
--- a/bot/exts/evergreen/source.py
+++ b/bot/exts/evergreen/source.py
@@ -38,7 +38,7 @@ class BotSource(commands.Cog):
async def source_command(self, ctx: commands.Context, *, source_item: SourceConverter = None) -> None:
"""Display information and a GitHub link to the source code of a command, tag, or cog."""
if not source_item:
- embed = Embed(title="Seasonal Bot's GitHub Repository")
+ embed = Embed(title="Sir Lancebot's GitHub Repository")
embed.add_field(name="Repository", value=f"[Go to GitHub]({Source.github})")
embed.set_thumbnail(url=Source.github_avatar_url)
await ctx.send(embed=embed)
diff --git a/bot/exts/evergreen/space.py b/bot/exts/evergreen/space.py
index 3587fc00..bc8e3118 100644
--- a/bot/exts/evergreen/space.py
+++ b/bot/exts/evergreen/space.py
@@ -8,7 +8,7 @@ from discord import Embed
from discord.ext import tasks
from discord.ext.commands import BadArgument, Cog, Context, Converter, group
-from bot.bot import SeasonalBot
+from bot.bot import Bot
from bot.constants import Tokens
logger = logging.getLogger(__name__)
@@ -37,7 +37,7 @@ class DateConverter(Converter):
class Space(Cog):
"""Space Cog contains commands, that show images, facts or other information about space."""
- def __init__(self, bot: SeasonalBot):
+ def __init__(self, bot: Bot):
self.bot = bot
self.http_session = bot.http_session
@@ -240,7 +240,7 @@ class Space(Cog):
).set_image(url=image).set_footer(text="Powered by NASA API" + footer)
-def setup(bot: SeasonalBot) -> None:
+def setup(bot: Bot) -> None:
"""Load Space Cog."""
if not Tokens.nasa:
logger.warning("Can't find NASA API key. Not loading Space Cog.")
diff --git a/bot/exts/halloween/candy_collection.py b/bot/exts/halloween/candy_collection.py
index bd0b90cc..0cb37ecd 100644
--- a/bot/exts/halloween/candy_collection.py
+++ b/bot/exts/halloween/candy_collection.py
@@ -1,18 +1,14 @@
-import json
import logging
import random
-# from pathlib import Path
from typing import Union
import discord
+from async_rediscache import RedisCache
from discord.ext import commands
from bot.constants import Channels, Month
from bot.utils.decorators import in_month
-# TODO: Implement substitutes for volume-persistent methods. # noqa: T000
-# from bot.utils.persist import make_persistent
-
log = logging.getLogger(__name__)
# chance is 1 in x range, so 1 in 20 range would give 5% chance (for add candy)
@@ -37,18 +33,15 @@ EMOJIS = dict(
class CandyCollection(commands.Cog):
"""Candy collection game Cog."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- # self.json_file = make_persistent(Path("bot", "resources", "halloween", "candy_collection.json"))
+ # User candy amount records
+ candy_records = RedisCache()
- with self.json_file.open() as fp:
- candy_data = json.load(fp)
+ # Candy and skull messages mapping
+ candy_messages = RedisCache()
+ skull_messages = RedisCache()
- self.candy_records = candy_data.get("records", dict())
-
- # Message ID where bot added the candies/skulls
- self.candy_messages = set()
- self.skull_messages = set()
+ def __init__(self, bot: commands.Bot):
+ self.bot = bot
@in_month(Month.OCTOBER)
@commands.Cog.listener()
@@ -58,16 +51,16 @@ class CandyCollection(commands.Cog):
if message.author.bot:
return
# ensure it's hacktober channel
- if message.channel.id != Channels.seasonalbot_commands:
+ if message.channel.id != Channels.community_bot_commands:
return
# do random check for skull first as it has the lower chance
if random.randint(1, ADD_SKULL_REACTION_CHANCE) == 1:
- self.skull_messages.add(message.id)
+ await self.skull_messages.set(message.id, "skull")
return await message.add_reaction(EMOJIS['SKULL'])
# check for the candy chance next
if random.randint(1, ADD_CANDY_REACTION_CHANCE) == 1:
- self.candy_messages.add(message.id)
+ await self.candy_messages.set(message.id, "candy")
return await message.add_reaction(EMOJIS['CANDY'])
@in_month(Month.OCTOBER)
@@ -80,7 +73,7 @@ class CandyCollection(commands.Cog):
return
# check to ensure it is in correct channel
- if message.channel.id != Channels.seasonalbot_commands:
+ if message.channel.id != Channels.community_bot_commands:
return
# if its not a candy or skull, and it is one of 10 most recent messages,
@@ -94,17 +87,19 @@ class CandyCollection(commands.Cog):
await self.reacted_msg_chance(message)
return
- if message.id in self.candy_messages and str(reaction.emoji) == EMOJIS['CANDY']:
- self.candy_messages.remove(message.id)
- prev_record = self.candy_records.get(str(user.id), 0)
- self.candy_records[str(user.id)] = prev_record + 1
+ if await self.candy_messages.get(message.id) == "candy" and str(reaction.emoji) == EMOJIS['CANDY']:
+ await self.candy_messages.delete(message.id)
+ if await self.candy_records.contains(user.id):
+ await self.candy_records.increment(user.id)
+ else:
+ await self.candy_records.set(user.id, 1)
- elif message.id in self.skull_messages and str(reaction.emoji) == EMOJIS['SKULL']:
- self.skull_messages.remove(message.id)
+ elif await self.skull_messages.get(message.id) == "skull" and str(reaction.emoji) == EMOJIS['SKULL']:
+ await self.skull_messages.delete(message.id)
- if prev_record := self.candy_records.get(str(user.id)):
+ if prev_record := await self.candy_records.get(user.id):
lost = min(random.randint(1, 3), prev_record)
- self.candy_records[str(user.id)] = prev_record - lost
+ await self.candy_records.decrement(user.id, lost)
if lost == prev_record:
await CandyCollection.send_spook_msg(user, message.channel, 'all of your')
@@ -116,7 +111,6 @@ class CandyCollection(commands.Cog):
return # Skip saving
await reaction.clear()
- await self.bot.loop.run_in_executor(None, self.save_to_json)
async def reacted_msg_chance(self, message: discord.Message) -> None:
"""
@@ -126,17 +120,17 @@ class CandyCollection(commands.Cog):
existing reaction.
"""
if random.randint(1, ADD_SKULL_EXISTING_REACTION_CHANCE) == 1:
- self.skull_messages.add(message.id)
+ await self.skull_messages.set(message.id, "skull")
return await message.add_reaction(EMOJIS['SKULL'])
if random.randint(1, ADD_CANDY_EXISTING_REACTION_CHANCE) == 1:
- self.candy_messages.add(message.id)
+ await self.candy_messages.set(message.id, "candy")
return await message.add_reaction(EMOJIS['CANDY'])
@property
def hacktober_channel(self) -> discord.TextChannel:
"""Get #hacktoberbot channel from its ID."""
- return self.bot.get_channel(id=Channels.seasonalbot_commands)
+ return self.bot.get_channel(id=Channels.community_bot_commands)
@staticmethod
async def send_spook_msg(
@@ -159,18 +153,15 @@ class CandyCollection(commands.Cog):
"I tried to take your candies but you had none to begin with!")
await channel.send(embed=embed)
- def save_to_json(self) -> None:
- """Save JSON to a local file."""
- with self.json_file.open('w') as fp:
- json.dump(dict(records=self.candy_records), fp)
-
@in_month(Month.OCTOBER)
@commands.command()
async def candy(self, ctx: commands.Context) -> None:
"""Get the candy leaderboard and save to JSON."""
+ records = await self.candy_records.items()
+
def generate_leaderboard() -> str:
top_sorted = sorted(
- ((user_id, score) for user_id, score in self.candy_records.items() if score > 0),
+ ((user_id, score) for user_id, score in records if score > 0),
key=lambda x: x[1],
reverse=True
)
@@ -199,3 +190,4 @@ class CandyCollection(commands.Cog):
def setup(bot: commands.Bot) -> None:
"""Candy Collection game Cog load."""
+ bot.add_cog(CandyCollection(bot))
diff --git a/bot/exts/halloween/hacktoberstats.py b/bot/exts/halloween/hacktoberstats.py
index 4fd5c324..84b75022 100644
--- a/bot/exts/halloween/hacktoberstats.py
+++ b/bot/exts/halloween/hacktoberstats.py
@@ -1,21 +1,17 @@
-import json
import logging
import re
from collections import Counter
from datetime import datetime, timedelta
-# from pathlib import Path
from typing import List, Tuple, Union
import aiohttp
import discord
+from async_rediscache import RedisCache
from discord.ext import commands
from bot.constants import Channels, Month, Tokens, WHITELISTED_CHANNELS
from bot.utils.decorators import in_month, override_in_channel
-# TODO: Implement substitutes for volume-persistent methods. # noqa: T000
-# from bot.utils.persist import make_persistent
-
log = logging.getLogger(__name__)
CURRENT_YEAR = datetime.now().year # Used to construct GH API query
@@ -39,10 +35,11 @@ GITHUB_NONEXISTENT_USER_MESSAGE = (
class HacktoberStats(commands.Cog):
"""Hacktoberfest statistics Cog."""
+ # Stores mapping of user IDs and GitHub usernames
+ linked_accounts = RedisCache()
+
def __init__(self, bot: commands.Bot):
self.bot = bot
- # self.link_json = make_persistent(Path("bot", "resources", "halloween", "github_links.json"))
- self.linked_accounts = self.load_linked_users()
@in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)
@commands.group(name="hacktoberstats", aliases=("hackstats",), invoke_without_command=True)
@@ -58,8 +55,8 @@ class HacktoberStats(commands.Cog):
if not github_username:
author_id, author_mention = self._author_mention_from_context(ctx)
- if str(author_id) in self.linked_accounts.keys():
- github_username = self.linked_accounts[author_id]["github_username"]
+ if await self.linked_accounts.contains(author_id):
+ github_username = await self.linked_accounts.get(author_id)
logging.info(f"Getting stats for {author_id} linked GitHub account '{github_username}'")
else:
msg = (
@@ -79,30 +76,19 @@ class HacktoberStats(commands.Cog):
"""
Link the invoking user's Github github_username to their Discord ID.
- Linked users are stored as a nested dict:
- {
- Discord_ID: {
- "github_username": str
- "date_added": datetime
- }
- }
+ Linked users are stored in Redis: User ID => GitHub Username.
"""
author_id, author_mention = self._author_mention_from_context(ctx)
if github_username:
- if str(author_id) in self.linked_accounts.keys():
- old_username = self.linked_accounts[author_id]["github_username"]
+ if await self.linked_accounts.contains(author_id):
+ old_username = await self.linked_accounts.get(author_id)
logging.info(f"{author_id} has changed their github link from '{old_username}' to '{github_username}'")
await ctx.send(f"{author_mention}, your GitHub username has been updated to: '{github_username}'")
else:
logging.info(f"{author_id} has added a github link to '{github_username}'")
await ctx.send(f"{author_mention}, your GitHub username has been added")
- self.linked_accounts[author_id] = {
- "github_username": github_username,
- "date_added": datetime.now()
- }
-
- self.save_linked_users()
+ await self.linked_accounts.set(author_id, github_username)
else:
logging.info(f"{author_id} tried to link a GitHub account but didn't provide a username")
await ctx.send(f"{author_mention}, a GitHub username is required to link your account")
@@ -114,7 +100,7 @@ class HacktoberStats(commands.Cog):
"""Remove the invoking user's account link from the log."""
author_id, author_mention = self._author_mention_from_context(ctx)
- stored_user = self.linked_accounts.pop(author_id, None)
+ stored_user = await self.linked_accounts.pop(author_id, None)
if stored_user:
await ctx.send(f"{author_mention}, your GitHub profile has been unlinked")
logging.info(f"{author_id} has unlinked their GitHub account")
@@ -122,48 +108,6 @@ class HacktoberStats(commands.Cog):
await ctx.send(f"{author_mention}, you do not currently have a linked GitHub account")
logging.info(f"{author_id} tried to unlink their GitHub account but no account was linked")
- self.save_linked_users()
-
- def load_linked_users(self) -> dict:
- """
- Load list of linked users from local JSON file.
-
- Linked users are stored as a nested dict:
- {
- Discord_ID: {
- "github_username": str
- "date_added": datetime
- }
- }
- """
- if self.link_json.exists():
- logging.info(f"Loading linked GitHub accounts from '{self.link_json}'")
- with open(self.link_json, 'r', encoding="utf8") as file:
- linked_accounts = json.load(file)
-
- logging.info(f"Loaded {len(linked_accounts)} linked GitHub accounts from '{self.link_json}'")
- return linked_accounts
- else:
- logging.info(f"Linked account log: '{self.link_json}' does not exist")
- return {}
-
- def save_linked_users(self) -> None:
- """
- Save list of linked users to local JSON file.
-
- Linked users are stored as a nested dict:
- {
- Discord_ID: {
- "github_username": str
- "date_added": datetime
- }
- }
- """
- logging.info(f"Saving linked_accounts to '{self.link_json}'")
- with open(self.link_json, 'w', encoding="utf8") as file:
- json.dump(self.linked_accounts, file, default=str)
- logging.info(f"linked_accounts saved to '{self.link_json}'")
-
async def get_stats(self, ctx: commands.Context, github_username: str) -> None:
"""
Query GitHub's API for PRs created by a GitHub user during the month of October.
@@ -249,7 +193,7 @@ class HacktoberStats(commands.Cog):
For each PR:
{
"repo_url": str
- "repo_shortname": str (e.g. "python-discord/seasonalbot")
+ "repo_shortname": str (e.g. "python-discord/sir-lancebot")
"created_at": datetime.datetime
"number": int
}
@@ -416,10 +360,10 @@ class HacktoberStats(commands.Cog):
"""
Extract shortname from https://api.github.com/repos/* URL.
- e.g. "https://api.github.com/repos/python-discord/seasonalbot"
+ e.g. "https://api.github.com/repos/python-discord/sir-lancebot"
|
V
- "python-discord/seasonalbot"
+ "python-discord/sir-lancebot"
"""
exp = r"https?:\/\/api.github.com\/repos\/([/\-\_\.\w]+)"
return re.findall(exp, in_url)[0]
@@ -491,3 +435,4 @@ class HacktoberStats(commands.Cog):
def setup(bot: commands.Bot) -> None:
"""Hacktoberstats Cog load."""
+ bot.add_cog(HacktoberStats(bot))
diff --git a/bot/exts/halloween/spookyreact.py b/bot/exts/halloween/spookyreact.py
index e5945aea..b335df75 100644
--- a/bot/exts/halloween/spookyreact.py
+++ b/bot/exts/halloween/spookyreact.py
@@ -29,13 +29,7 @@ class SpookyReact(Cog):
@in_month(Month.OCTOBER)
@Cog.listener()
async def on_message(self, ctx: discord.Message) -> None:
- """
- A command to send the seasonalbot github project.
-
- Lines that begin with the bot's command prefix are ignored
-
- Seasonalbot's own messages are ignored
- """
+ """Triggered when the bot sees a message in October."""
for trigger in SPOOKY_TRIGGERS.keys():
trigger_test = re.search(SPOOKY_TRIGGERS[trigger][0], ctx.content.lower())
if trigger_test:
diff --git a/bot/exts/pride/pride_facts.py b/bot/exts/pride/pride_facts.py
index 9ff4c9e0..5bd5d0ce 100644
--- a/bot/exts/pride/pride_facts.py
+++ b/bot/exts/pride/pride_facts.py
@@ -9,7 +9,7 @@ import dateutil.parser
import discord
from discord.ext import commands
-from bot.bot import SeasonalBot
+from bot.bot import Bot
from bot.constants import Channels, Colours, Month
from bot.utils.decorators import seasonal_task
@@ -21,7 +21,7 @@ Sendable = Union[commands.Context, discord.TextChannel]
class PrideFacts(commands.Cog):
"""Provides a new fact every day during the Pride season!"""
- def __init__(self, bot: SeasonalBot):
+ def __init__(self, bot: Bot):
self.bot = bot
self.facts = self.load_facts()
@@ -38,7 +38,7 @@ class PrideFacts(commands.Cog):
"""Background task to post the daily pride fact every day."""
await self.bot.wait_until_guild_available()
- channel = self.bot.get_channel(Channels.seasonalbot_commands)
+ channel = self.bot.get_channel(Channels.community_bot_commands)
await self.send_select_fact(channel, datetime.utcnow())
async def send_random_fact(self, ctx: commands.Context) -> None:
@@ -102,6 +102,6 @@ class PrideFacts(commands.Cog):
)
-def setup(bot: SeasonalBot) -> None:
+def setup(bot: Bot) -> None:
"""Cog loader for pride facts."""
bot.add_cog(PrideFacts(bot))
diff --git a/bot/exts/utils/extensions.py b/bot/exts/utils/extensions.py
index 102a0416..bb22c353 100644
--- a/bot/exts/utils/extensions.py
+++ b/bot/exts/utils/extensions.py
@@ -8,7 +8,7 @@ from discord.ext import commands
from discord.ext.commands import Context, group
from bot import exts
-from bot.bot import SeasonalBot as Bot
+from bot.bot import Bot
from bot.constants import Client, Emojis, MODERATION_ROLES, Roles
from bot.utils.checks import with_role_check
from bot.utils.extensions import EXTENSIONS, unqualify
diff --git a/bot/exts/valentines/be_my_valentine.py b/bot/exts/valentines/be_my_valentine.py
index b1258307..4db4d191 100644
--- a/bot/exts/valentines/be_my_valentine.py
+++ b/bot/exts/valentines/be_my_valentine.py
@@ -99,7 +99,7 @@ class BeMyValentine(commands.Cog):
emoji_1, emoji_2 = self.random_emoji()
lovefest_role = discord.utils.get(ctx.guild.roles, id=Lovefest.role_id)
- channel = self.bot.get_channel(Channels.seasonalbot_commands)
+ channel = self.bot.get_channel(Channels.community_bot_commands)
valentine, title = self.valentine_check(valentine_type)
if user is None:
diff --git a/bot/resources/evergreen/branding.json b/bot/resources/evergreen/branding.json
deleted file mode 100644
index 747c0fe8..00000000
--- a/bot/resources/evergreen/branding.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "daemon_active": false
-}
diff --git a/bot/resources/halloween/candy_collection.json b/bot/resources/halloween/candy_collection.json
deleted file mode 100644
index 0967ef42..00000000
--- a/bot/resources/halloween/candy_collection.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/bot/resources/halloween/github_links.json b/bot/resources/halloween/github_links.json
deleted file mode 100644
index 0967ef42..00000000
--- a/bot/resources/halloween/github_links.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/bot/resources/halloween/spooky_rating.json b/bot/resources/halloween/spooky_rating.json
index d9c8dcb7..533e7107 100644
--- a/bot/resources/halloween/spooky_rating.json
+++ b/bot/resources/halloween/spooky_rating.json
@@ -2,46 +2,46 @@
"-1": {
"title": "\uD83D\uDD6F You're not scarin' anyone \uD83D\uDD6F",
"text": "No matter what you say or do, nobody even flinches when you try to scare them. Was your costume this year only a white sheet with holes for eyes? Or did you even bother with a costume at all? Either way, don't expect too many treats when going from door-to-door.",
- "image": "https://raw.githubusercontent.com/python-discord/seasonalbot/master/bot/resources/halloween/spookyrating/candle.jpeg"
+ "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/master/bot/resources/halloween/spookyrating/candle.jpeg"
},
"5": {
"title": "\uD83D\uDC76 Like taking candy from a baby \uD83D\uDC76",
"text": "Your scaring will probably make a baby cry... but that's the limit on your frightening powers. Be careful not to get to the point where everyone's running away from you because they don't like you, not because they're scared of you.",
- "image": "https://raw.githubusercontent.com/python-discord/seasonalbot/master/bot/resources/halloween/spookyrating/baby.jpeg"
+ "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/master/bot/resources/halloween/spookyrating/baby.jpeg"
},
"20": {
"title": "\uD83C\uDFDA You're skills are forming... \uD83C\uDFDA",
"text": "As you become the Devil's apprentice, you begin to make people jump every time you sneak up on them. A good start, but you have to learn not to wear the same costume every year until it doesn't fit you. People will notice you and your prowess will decrease.",
- "image": "https://raw.githubusercontent.com/python-discord/seasonalbot/master/bot/resources/halloween/spookyrating/tiger.jpeg"
+ "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/master/bot/resources/halloween/spookyrating/tiger.jpeg"
},
"30": {
"title": "\uD83D\uDC80 Picture Perfect... \uD83D\uDC80",
"text": "You've nailed the costume this year! You look suuuper scary! Now make sure to play the part and act out your costume and you'll be sure to give a few people a massive fright!",
- "image": "https://raw.githubusercontent.com/python-discord/seasonalbot/master/bot/resources/halloween/spookyrating/costume.jpeg"
+ "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/master/bot/resources/halloween/spookyrating/costume.jpeg"
},
"50": {
"title": "\uD83D\uDC7B Uhm... are you human \uD83D\uDC7B",
"text": "Uhm... you're too good to be human and now you're beginning to sound like a ghost. You're almost invisible when haunting and nobody truly knows where you are at any given time. But they will always scream at the sound of a ghost...",
- "image": "https://raw.githubusercontent.com/python-discord/seasonalbot/master/bot/resources/halloween/spookyrating/ghost.jpeg"
+ "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/master/bot/resources/halloween/spookyrating/ghost.jpeg"
},
"65": {
"title": "\uD83C\uDF83 That potion can't be real \uD83C\uDF83",
"text": "You're carrying... some... unknown liquids and no one knows who they are but yourself. Be careful on who you use these powerful spells on, because no Mage has the power to do any irreversible enchantments because even you won't know what will happen to these mortals.",
- "image": "https://raw.githubusercontent.com/python-discord/seasonalbot/master/bot/resources/halloween/spookyrating/necromancer.jepg"
+ "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/master/bot/resources/halloween/spookyrating/necromancer.jepg"
},
"80": {
"title": "\uD83E\uDD21 The most sinister face \uD83E\uDD21",
"text": "Who knew something intended to be playful could be so menacing... Especially other people seeing you in their nightmares, continuing to haunt them day by day, stuck in their head throughout the entire year. Make sure to pull a face they will never forget.",
- "image": "https://raw.githubusercontent.com/python-discord/seasonalbot/master/bot/resources/halloween/spookyrating/clown.jpeg"
+ "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/master/bot/resources/halloween/spookyrating/clown.jpeg"
},
"95": {
"title": "\uD83D\uDE08 The Devil's Accomplice \uD83D\uDE08",
"text": "Imagine being allies with the most evil character with an aim to scare people to death. Force people to suffer as they proceed straight to hell to meet your boss and best friend. Not even you know the power He has...",
- "image": "https://raw.githubusercontent.com/python-discord/seasonalbot/master/bot/resources/halloween/spookyrating/jackolantern.jpg"
+ "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/master/bot/resources/halloween/spookyrating/jackolantern.jpg"
},
"100": {
"title":"\uD83D\uDC7F The Devil Himself \uD83D\uDC7F",
"text": "You are the evillest creature in existence to scare anyone and everyone humanly possible. The reason your underlings are called mortals is that they die. With your help, they die a lot quicker. With all the evil power in the universe, you know what to do.",
- "image": "https://raw.githubusercontent.com/python-discord/seasonalbot/master/bot/resources/halloween/spookyrating/devil.jpeg"
+ "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/master/bot/resources/halloween/spookyrating/devil.jpeg"
}
}
diff --git a/bot/seasons.py b/bot/seasons.py
deleted file mode 100644
index 55cfef3c..00000000
--- a/bot/seasons.py
+++ /dev/null
@@ -1,181 +0,0 @@
-import logging
-import typing as t
-
-from bot.constants import Colours, Month
-from bot.utils import resolve_current_month
-from bot.utils.exceptions import BrandingError
-
-log = logging.getLogger(__name__)
-
-
-class SeasonBase:
- """
- Base for Seasonal classes.
-
- This serves as the off-season fallback for when no specific
- seasons are active.
-
- Seasons are 'registered' simply by inheriting from `SeasonBase`.
- We discover them by calling `__subclasses__`.
- """
-
- season_name: str = "Evergreen"
- bot_name: str = "SeasonalBot"
-
- colour: str = Colours.soft_green
- description: str = "The default season!"
-
- branding_path: str = "seasonal/evergreen"
-
- months: t.Set[Month] = set(Month)
-
-
-class Christmas(SeasonBase):
- """Branding for December."""
-
- season_name = "Festive season"
- bot_name = "MerryBot"
-
- colour = Colours.soft_red
- description = (
- "The time is here to get into the festive spirit! No matter who you are, where you are, "
- "or what beliefs you may follow, we hope every one of you enjoy this festive season!"
- )
-
- branding_path = "seasonal/christmas"
-
- months = {Month.DECEMBER}
-
-
-class Easter(SeasonBase):
- """Branding for April."""
-
- season_name = "Easter"
- bot_name = "BunnyBot"
-
- colour = Colours.bright_green
- description = (
- "Bunny here, bunny there, bunny everywhere! Here at Python Discord, we celebrate "
- "our version of Easter during the entire month of April."
- )
-
- branding_path = "seasonal/easter"
-
- months = {Month.APRIL}
-
-
-class Halloween(SeasonBase):
- """Branding for October."""
-
- season_name = "Halloween"
- bot_name = "NeonBot"
-
- colour = Colours.orange
- description = "Trick or treat?!"
-
- branding_path = "seasonal/halloween"
-
- months = {Month.OCTOBER}
-
-
-class Pride(SeasonBase):
- """Branding for June."""
-
- season_name = "Pride"
- bot_name = "ProudBot"
-
- colour = Colours.pink
- description = (
- "The month of June is a special month for us at Python Discord. It is very important to us "
- "that everyone feels welcome here, no matter their origin, identity or sexuality. During the "
- "month of June, while some of you are participating in Pride festivals across the world, "
- "we will be celebrating individuality and commemorating the history and challenges "
- "of the LGBTQ+ community with a Pride event of our own!"
- )
-
- branding_path = "seasonal/pride"
-
- months = {Month.JUNE}
-
-
-class Valentines(SeasonBase):
- """Branding for February."""
-
- season_name = "Valentines"
- bot_name = "TenderBot"
-
- colour = Colours.pink
- description = "Love is in the air!"
-
- branding_path = "seasonal/valentines"
-
- months = {Month.FEBRUARY}
-
-
-class Wildcard(SeasonBase):
- """Branding for August."""
-
- season_name = "Wildcard"
- bot_name = "RetroBot"
-
- colour = Colours.purple
- description = "A season full of surprises!"
-
- months = {Month.AUGUST}
-
-
-def get_all_seasons() -> t.List[t.Type[SeasonBase]]:
- """Give all available season classes."""
- return [SeasonBase] + SeasonBase.__subclasses__()
-
-
-def get_current_season() -> t.Type[SeasonBase]:
- """Give active season, based on current UTC month."""
- current_month = resolve_current_month()
-
- active_seasons = tuple(
- season
- for season in SeasonBase.__subclasses__()
- if current_month in season.months
- )
-
- if not active_seasons:
- return SeasonBase
-
- return active_seasons[0]
-
-
-def get_season(name: str) -> t.Optional[t.Type[SeasonBase]]:
- """
- Give season such that its class name or its `season_name` attr match `name` (caseless).
-
- If no such season exists, return None.
- """
- name = name.casefold()
-
- for season in get_all_seasons():
- matches = (season.__name__.casefold(), season.season_name.casefold())
-
- if name in matches:
- return season
-
-
-def _validate_season_overlap() -> None:
- """
- Raise BrandingError if there are any colliding seasons.
-
- This serves as a local test to ensure that seasons haven't been misconfigured.
- """
- month_to_season = {}
-
- for season in SeasonBase.__subclasses__():
- for month in season.months:
- colliding_season = month_to_season.get(month)
-
- if colliding_season:
- raise BrandingError(f"Season {season} collides with {colliding_season} in {month.name}")
- else:
- month_to_season[month] = season
-
-
-_validate_season_overlap()
diff --git a/bot/utils/checks.py b/bot/utils/checks.py
index 3031a271..9dd4dde0 100644
--- a/bot/utils/checks.py
+++ b/bot/utils/checks.py
@@ -39,7 +39,7 @@ def in_whitelist_check(
channels: Container[int] = (),
categories: Container[int] = (),
roles: Container[int] = (),
- redirect: Optional[int] = constants.Channels.seasonalbot_commands,
+ redirect: Optional[int] = constants.Channels.community_bot_commands,
fail_silently: bool = False,
) -> bool:
"""
diff --git a/bot/utils/decorators.py b/bot/utils/decorators.py
index 9e6ef73d..9cdaad3f 100644
--- a/bot/utils/decorators.py
+++ b/bot/utils/decorators.py
@@ -11,7 +11,7 @@ from discord import Colour, Embed
from discord.ext import commands
from discord.ext.commands import CheckFailure, Command, Context
-from bot.constants import Client, ERROR_REPLIES, Month
+from bot.constants import ERROR_REPLIES, Month
from bot.utils import human_months, resolve_current_month
ONE_DAY = 24 * 60 * 60
@@ -298,23 +298,3 @@ def locked() -> t.Union[t.Callable, None]:
return await func(self, ctx, *args, **kwargs)
return inner
return wrap
-
-
-def mock_in_debug(return_value: t.Any) -> t.Callable:
- """
- Short-circuit function execution if in debug mode and return `return_value`.
-
- The original function name, and the incoming args and kwargs are DEBUG level logged
- upon each call. This is useful for expensive operations, i.e. media asset uploads
- that are prone to rate-limits but need to be tested extensively.
- """
- def decorator(func: t.Callable) -> t.Callable:
- @functools.wraps(func)
- async def wrapped(*args, **kwargs) -> t.Any:
- """Short-circuit and log if in debug mode."""
- if Client.debug:
- log.debug(f"Function {func.__name__} called with args: {args}, kwargs: {kwargs}")
- return return_value
- return await func(*args, **kwargs)
- return wrapped
- return decorator
diff --git a/bot/utils/exceptions.py b/bot/utils/exceptions.py
index dc62debe..2b1c1b31 100644
--- a/bot/utils/exceptions.py
+++ b/bot/utils/exceptions.py
@@ -1,9 +1,3 @@
-class BrandingError(Exception):
- """Exception raised by the BrandingManager cog."""
-
- pass
-
-
class UserNotPlayingError(Exception):
"""Will raised when user try to use game commands when not playing."""
diff --git a/deployment.yaml b/deployment.yaml
index b04b528c..cad97c18 100644
--- a/deployment.yaml
+++ b/deployment.yaml
@@ -1,21 +1,21 @@
apiVersion: apps/v1
kind: Deployment
metadata:
- name: seasonalbot
+ name: sir-lancebot
spec:
replicas: 1
selector:
matchLabels:
- app: seasonalbot
+ app: sir-lancebot
template:
metadata:
labels:
- app: seasonalbot
+ app: sir-lancebot
spec:
containers:
- - name: seasonalbot
- image: ghcr.io/python-discord/seasonalbot:latest
+ - name: sir-lancebot
+ image: ghcr.io/python-discord/sir-lancebot:latest
imagePullPolicy: Always
envFrom:
- secretRef:
- name: seasonalbot-env
+ name: sir-lancebot-env
diff --git a/docker-compose.yml b/docker-compose.yml
index 6cf5e9bd..bb6ad6ac 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,22 +1,29 @@
version: "3.7"
services:
- seasonalbot:
+ sir-lancebot:
build:
context: .
dockerfile: Dockerfile
- container_name: seasonalbot
+ container_name: sir-lancebot
init: true
restart: always
+ depends_on:
+ - redis
environment:
- - SEASONALBOT_TOKEN=yourtokenhere
- - SEASONALBOT_DEBUG=true
- # - SEASONALBOT_GUILD=
- # - SEASONALBOT_ADMIN_ROLE_ID=
- # - CHANNEL_ANNOUNCEMENTS=
- # - CHANNEL_DEVLOG=
+ - BOT_TOKEN
+ - BOT_DEBUG
+ - BOT_GUILD
+ - BOT_ADMIN_ROLE_ID
+ - CHANNEL_DEVLOG
+ - CHANNEL_COMMUNITY_BOT_COMMANDS
+ - REDIS_HOST=redis
volumes:
- - /opt/pythondiscord/seasonalbot/log:/bot/bot/log
- - /opt/pythondiscord/seasonalbot/data:/bot/data
+ - .:/bot
+
+ redis:
+ image: redis:latest
+ ports:
+ - "127.0.0.1:6379:6379"
diff --git a/sir-lancebot-logo.png b/sir-lancebot-logo.png
new file mode 100644
index 00000000..fc606bf7
--- /dev/null
+++ b/sir-lancebot-logo.png
Binary files differ