diff options
author | 2021-03-19 15:19:23 +0100 | |
---|---|---|
committer | 2021-03-19 15:19:23 +0100 | |
commit | 25b2e4c8412cceffe1ec528a9894586599f5f25d (patch) | |
tree | e3387dc9441445184947c88b214406a3b4f9aaa7 | |
parent | First pass of easy to produce errors (diff) | |
parent | Merge pull request #631 from python-discord/dependabot/pip/pillow-8.1.1 (diff) |
Merge branch 'main' into Handle-DMChannels
47 files changed, 1226 insertions, 511 deletions
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..3f3c4fd6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +*.png binary diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9d12cd10..0a006eb9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -4,14 +4,14 @@ on: workflow_run: workflows: ["Lint"] branches: - - master + - main types: - completed jobs: build: if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' - name: Build, Push, & Deploy Container + name: Build & Push runs-on: ubuntu-latest steps: @@ -28,15 +28,6 @@ jobs: with: path: sir-lancebot - # Check out the private "kubernetes" repository in the `kubernetes` - # subdirectory using a GitHub Personal Access Token - - name: Checkout code - uses: actions/checkout@v2 - with: - repository: python-discord/kubernetes - token: ${{ secrets.REPO_TOKEN }} - path: kubernetes - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 @@ -64,6 +55,29 @@ jobs: build-args: | git_sha=${{ github.sha }} + deploy: + needs: build + name: Deploy + runs-on: ubuntu-latest + environment: production + + steps: + # Create a commit SHA-based tag for the container repositories + - name: Create SHA Container Tag + id: sha_tag + run: | + tag=$(cut -c 1-7 <<< $GITHUB_SHA) + echo "::set-output name=tag::$tag" + + # Check out the private "kubernetes" repository in the `kubernetes` + # subdirectory using a GitHub Personal Access Token + - name: Checkout code + uses: actions/checkout@v2 + with: + repository: python-discord/kubernetes + token: ${{ secrets.REPO_TOKEN }} + path: kubernetes + - name: Authenticate with Kubernetes uses: azure/k8s-set-context@v1 with: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index a5f45255..7f157da3 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -3,7 +3,7 @@ name: Lint on: push: branches: - - master + - main pull_request: diff --git a/.github/workflows/sentry_release.yaml b/.github/workflows/sentry_release.yaml index 0e02dd0c..3d15e01e 100644 --- a/.github/workflows/sentry_release.yaml +++ b/.github/workflows/sentry_release.yaml @@ -3,14 +3,14 @@ name: Create Sentry release on: push: branches: - - master + - main jobs: create_sentry_release: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@master + uses: actions/checkout@main - name: Create a Sentry.io release uses: tclindner/[email protected] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a66bf97c..6c94375c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,8 +6,6 @@ repos: - id: check-toml - id: check-yaml - id: end-of-file-fixer - - id: mixed-line-ending - args: [--fix=lf] - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - repo: https://github.com/pre-commit/pygrep-hooks diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7cf83db5..3a1803e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ 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. +Our projects are open-source and are automatically deployed whenever commits are pushed to the `main` branch on each repository, so we've created a set of guidelines in order to keep everything clean and in working order. Note that contributions may be rejected on the basis of a contributor failing to follow these guidelines. @@ -12,7 +12,7 @@ Note that contributions may be rejected on the basis of a contributor failing to 2. Your pull request must solve an issue created or approved by a staff member. These will be labeled with the `approved` label. Feel free to suggest issues of your own, which staff can review for approval. 3. **No force-pushes** or modifying the Git history in any way. 4. If you have direct access to the repository, **create a branch for your changes** and create a pull request for that branch. If not, create a branch on a fork of the repository and create a pull request from there. - * It's common practice for a repository to reject direct pushes to `master`, so make branching a habit! + * It's common practice for a repository to reject direct pushes to `main`, so make branching a habit! * If PRing from your own fork, **ensure that "Allow edits from maintainers" is checked**. This gives permission for maintainers to commit changes directly to your fork, speeding up the review process. 5. **Adhere to the prevailing code style**, which we enforce using [`flake8`](http://flake8.pycqa.org/en/latest/index.html) and [`pre-commit`](https://pre-commit.com/). * Run `flake8` and `pre-commit` against your code [**before** you push it](https://soundcloud.com/lemonsaurusrex/lint-before-you-push). Your commit will be rejected by the build server if it fails to lint. @@ -22,7 +22,7 @@ Note that contributions may be rejected on the basis of a contributor failing to * Avoid making minor commits for fixing typos or linting errors. Since you've already set up a `pre-commit` hook to run the linting pipeline before a commit, you shouldn't be committing linting issues anyway. * A more in-depth guide to writing great commit messages can be found in Chris Beam's [*How to Write a Git Commit Message*](https://chris.beams.io/posts/git-commit/) 7. **Avoid frequent pushes to the main repository**. This goes for PRs opened against your fork as well. Our test build pipelines are triggered every time a push to the repository (or PR) is made. Try to batch your commits until you've finished working for that session, or you've reached a point where collaborators need your commits to continue their own work. This also provides you the opportunity to amend commits for minor changes rather than having to commit them on their own because you've already pushed. - * This includes merging master into your branch. Try to leave merging from master for after your PR passes review; a maintainer will bring your PR up to date before merging. Exceptions to this include: resolving merge conflicts, needing something that was pushed to master for your branch, or something was pushed to master that could potentionally affect the functionality of what you're writing. + * This includes merging main into your branch. Try to leave merging from main for after your PR passes review; a maintainer will bring your PR up to date before merging. Exceptions to this include: resolving merge conflicts, needing something that was pushed to main for your branch, or something was pushed to main that could potentionally affect the functionality of what you're writing. 8. **Don't fight the framework**. Every framework has its flaws, but the frameworks we've picked out have been carefully chosen for their particular merits. If you can avoid it, please resist reimplementing swathes of framework logic - the work has already been done for you! 9. If someone is working on an issue or pull request, **do not open your own pull request for the same task**. Instead, collaborate with the author(s) of the existing pull request. Duplicate PRs opened without communicating with the other author(s) and/or PyDis staff will be closed. Communication is key, and there's no point in two separate implementations of the same thing. * One option is to fork the other contributor's repository and submit your changes to their branch with your own pull request. We suggest following these guidelines when interacting with their repository as well. @@ -39,7 +39,7 @@ All projects evolve over time, and this contribution guide is no different. This ## Supplemental Information ### Developer Environment -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). +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/main/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. @@ -1,14 +1,10 @@ FROM python:3.8-slim -# Set SHA build argument -ARG git_sha="development" - # Set pip to have cleaner logs and no saved cache ENV PIP_NO_CACHE_DIR=false \ PIPENV_HIDE_EMOJIS=1 \ PIPENV_IGNORE_VIRTUALENVS=1 \ - PIPENV_NOSPIN=1 \ - GIT_SHA=$git_sha + PIPENV_NOSPIN=1 # Install git to be able to dowload git dependencies in the Pipfile RUN apt-get -y update \ @@ -21,11 +17,20 @@ RUN pip install -U pipenv # Copy the project files into working directory WORKDIR /bot -COPY . . + +# Copy dependency files +COPY Pipfile Pipfile.lock ./ # Install project dependencies RUN pipenv install --deploy --system +# Copy project code +COPY . . + +# Set Git SHA enviroment variable +ARG git_sha="development" +ENV GIT_SHA=$git_sha + ENTRYPOINT ["python"] CMD ["-m", "bot"] @@ -8,12 +8,13 @@ aiodns = "~=2.0" arrow = "~=0.14" beautifulsoup4 = "~=4.8" fuzzywuzzy = "~=0.17" -pillow = "~=7.2" +pillow = "~=8.1" pytz = "~=2019.2" sentry-sdk = "~=0.19" PyYAML = "~=5.3.1" "discord.py" = {extras = ["voice"], version = "~=1.5.1"} async-rediscache = {extras = ["fakeredis"], version = "~=0.1.4"} +emojis = "~=0.6.0" [dev-packages] flake8 = "~=3.8" diff --git a/Pipfile.lock b/Pipfile.lock index be6f9574..d09588cf 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9be419062bd9db364ac9dddfcd50aef9c932384b45850363e482591fe7d12403" + "sha256": "cf7535796e0d4f4dd6a414d8db4781caaab4a624380d29abea17577209043742" }, "pipfile-spec": 6, "requires": { @@ -40,7 +40,6 @@ "sha256:c506853ba52e516b264b106321c424d03f3ddef2813246432fa9d1cefd361c81", "sha256:fb83326d8295e8840e4ba774edf346e87eca78ba8a89c55d2690352842c15ba5" ], - "markers": "python_full_version >= '3.5.3'", "version": "==3.6.3" }, "aioredis": { @@ -74,7 +73,6 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], - "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -82,7 +80,6 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "beautifulsoup4": { @@ -103,44 +100,45 @@ }, "cffi": { "hashes": [ - "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e", - "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d", - "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a", - "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec", - "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362", - "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668", - "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c", - "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b", - "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06", - "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698", - "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2", - "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c", - "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7", - "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009", - "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03", - "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b", - "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909", - "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53", - "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35", - "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26", - "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b", - "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01", - "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb", - "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293", - "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd", - "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d", - "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3", - "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d", - "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e", - "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca", - "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d", - "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775", - "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375", - "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b", - "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b", - "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f" - ], - "version": "==1.14.4" + "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813", + "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06", + "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea", + "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee", + "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396", + "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73", + "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315", + "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1", + "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49", + "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892", + "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482", + "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058", + "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5", + "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53", + "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045", + "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3", + "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5", + "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e", + "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c", + "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369", + "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827", + "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053", + "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa", + "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4", + "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322", + "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132", + "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62", + "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa", + "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0", + "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396", + "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e", + "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991", + "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6", + "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1", + "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406", + "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d", + "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c" + ], + "version": "==1.14.5" }, "chardet": { "hashes": [ @@ -160,6 +158,14 @@ "index": "pypi", "version": "==1.5.1" }, + "emojis": { + "hashes": [ + "sha256:7da34c8a78ae262fd68cef9e2c78a3c1feb59784489eeea0f54ba1d4b7111c7c", + "sha256:bf605d1f1a27a81cd37fe82eb65781c904467f569295a541c33710b97e4225ec" + ], + "index": "pypi", + "version": "==0.6.0" + }, "fakeredis": { "hashes": [ "sha256:01cb47d2286825a171fb49c0e445b1fa9307087e07cbb3d027ea10dbff108b6a", @@ -224,16 +230,14 @@ "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", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", + "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.10" + "version": "==3.1" }, "multidict": { "hashes": [ @@ -255,42 +259,46 @@ "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" ], - "markers": "python_version >= '3.5'", "version": "==4.7.6" }, "pillow": { "hashes": [ - "sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f", - "sha256:06aba4169e78c439d528fdeb34762c3b61a70813527a2c57f0540541e9f433a8", - "sha256:09d7f9e64289cb40c2c8d7ad674b2ed6105f55dc3b09aa8e4918e20a0311e7ad", - "sha256:0a80dd307a5d8440b0a08bd7b81617e04d870e40a3e46a32d9c246e54705e86f", - "sha256:1ca594126d3c4def54babee699c055a913efb01e106c309fa6b04405d474d5ae", - "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d", - "sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5", - "sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b", - "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8", - "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233", - "sha256:6d7741e65835716ceea0fd13a7d0192961212fd59e741a46bbed7a473c634ed6", - "sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727", - "sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f", - "sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38", - "sha256:94cf49723928eb6070a892cb39d6c156f7b5a2db4e8971cb958f7b6b104fb4c4", - "sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626", - "sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d", - "sha256:9c87ef410a58dd54b92424ffd7e28fd2ec65d2f7fc02b76f5e9b2067e355ebf6", - "sha256:a060cf8aa332052df2158e5a119303965be92c3da6f2d93b6878f0ebca80b2f6", - "sha256:c79f9c5fb846285f943aafeafda3358992d64f0ef58566e23484132ecd8d7d63", - "sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f", - "sha256:d08b23fdb388c0715990cbc06866db554e1822c4bdcf6d4166cf30ac82df8c41", - "sha256:d350f0f2c2421e65fbc62690f26b59b0bcda1b614beb318c81e38647e0f673a1", - "sha256:e901964262a56d9ea3c2693df68bc9860b8bdda2b04768821e4c44ae797de117", - "sha256:ec29604081f10f16a7aea809ad42e27764188fc258b02259a03a8ff7ded3808d", - "sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9", - "sha256:f7e30c27477dffc3e85c2463b3e649f751789e0f6c8456099eea7ddd53be4a8a", - "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce" + "sha256:01bb0a34f1a6689b138c0089d670ae2e8f886d2666a9b2f2019031abdea673c4", + "sha256:07872f1d8421db5a3fe770f7480835e5e90fddb58f36c216d4a2ac0d594de474", + "sha256:1022f8f6dc3c5b0dcf928f1c49ba2ac73051f576af100d57776e2b65c1f76a8d", + "sha256:14415e9e28410232370615dbde0cf0a00e526f522f665460344a5b96973a3086", + "sha256:172acfaf00434a28dddfe592d83f2980e22e63c769ff4a448ddf7b7a38ffd165", + "sha256:1c5e3c36f02c815766ae9dd91899b1c5b4652f2a37b7a51609f3bd467c0f11fb", + "sha256:292f2aa1ae5c5c1451cb4b558addb88c257411d3fd71c6cf45562911baffc979", + "sha256:2a40d7d4b17db87f5b9a1efc0aff56000e1d0d5ece415090c102aafa0ccbe858", + "sha256:2f0d7034d5faae9a8d1019d152ede924f653df2ce77d3bba4ce62cd21b5f94ae", + "sha256:33fdbd4f5608c852d97264f9d2e3b54e9e9959083d008145175b86100b275e5b", + "sha256:3b13d89d97b551e02549d1f0edf22bed6acfd6fd2e888cd1e9a953bf215f0e81", + "sha256:3e759bcc03d6f39bc751e56d86bc87252b9a21c689a27c5ed753717a87d53a5b", + "sha256:3ec87bd1248b23a2e4e19e774367fbe30fddc73913edc5f9b37470624f55dc1f", + "sha256:436b0a2dd9fe3f7aa6a444af6bdf53c1eb8f5ced9ea3ef104daa83f0ea18e7bc", + "sha256:43b3c859912e8bf754b3c5142df624794b18eb7ae07cfeddc917e1a9406a3ef2", + "sha256:4fe74636ee71c57a7f65d7b21a9f127d842b4fb75511e5d256ace258826eb352", + "sha256:59445af66b59cc39530b4f810776928d75e95f41e945f0c32a3de4aceb93c15d", + "sha256:69da5b1d7102a61ce9b45deb2920a2012d52fd8f4201495ea9411d0071b0ec22", + "sha256:7094bbdecb95ebe53166e4c12cf5e28310c2b550b08c07c5dc15433898e2238e", + "sha256:8211cac9bf10461f9e33fe9a3af6c5131f3fdd0d10672afc2abb2c70cf95c5ca", + "sha256:8cf77e458bd996dc85455f10fe443c0c946f5b13253773439bcbec08aa1aebc2", + "sha256:924fc33cb4acaf6267b8ca3b8f1922620d57a28470d5e4f49672cea9a841eb08", + "sha256:99ce3333b40b7a4435e0a18baad468d44ab118a4b1da0af0a888893d03253f1d", + "sha256:a7d690b2c5f7e4a932374615fedceb1e305d2dd5363c1de15961725fe10e7d16", + "sha256:b9af590adc1e46898a1276527f3cfe2da8048ae43fbbf9b1bf9395f6c99d9b47", + "sha256:bb18422ad00c1fecc731d06592e99c3be2c634da19e26942ba2f13d805005cf2", + "sha256:c10af40ee2f1a99e1ae755ab1f773916e8bca3364029a042cd9161c400416bd8", + "sha256:c143c409e7bc1db784471fe9d0bf95f37c4458e879ad84cfae640cb74ee11a26", + "sha256:c448d2b335e21951416a30cd48d35588d122a912d5fe9e41900afacecc7d21a1", + "sha256:d30f30c044bdc0ab8f3924e1eeaac87e0ff8a27e87369c5cac4064b6ec78fd83", + "sha256:df534e64d4f3e84e8f1e1a37da3f541555d947c1c1c09b32178537f0f243f69d", + "sha256:f6fc18f9c9c7959bf58e6faf801d14fafb6d4717faaf6f79a68c8bb2a13dcf20", + "sha256:ff83dfeb04c98bb3e7948f876c17513a34e9a19fd92e292288649164924c1b39" ], "index": "pypi", - "version": "==7.2.0" + "version": "==8.1.1" }, "pycares": { "hashes": [ @@ -331,7 +339,6 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pynacl": { @@ -365,7 +372,6 @@ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.1" }, "pytz": { @@ -400,23 +406,21 @@ "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:0a711ec952441c2ec89b8f5d226c33bc697914f46e876b44a4edd3e7864cf4d0", - "sha256:737a094e49a529dd0fdcaafa9e97cf7c3d5eb964bd229821d640bc77f3502b3f" + "sha256:4ae8d1ced6c67f1c8ea51d82a16721c166c489b76876c9f2c202b8a50334b237", + "sha256:e75c8c58932bda8cd293ea8e4b242527129e1caaec91433d21b8b2f20fee030b" ], "index": "pypi", - "version": "==0.19.5" + "version": "==0.20.3" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "sortedcontainers": { @@ -428,19 +432,18 @@ }, "soupsieve": { "hashes": [ - "sha256:4bb21a6ee4707bf43b61230e80740e71bfe56e55d1f1f50924b087bb2975c851", - "sha256:6dc52924dc0bc710a5d16794e6b3480b2c7c08b07729505feab2b2c16661ff6e" + "sha256:407fa1e8eb3458d1b5614df51d9651a1180ea5fedf07feb46e45d7e25e6d6cdd", + "sha256:d3a5ea5b350423f47d07639f74475afedad48cf41c0ad7a82ca13a3928af34f6" ], "markers": "python_version >= '3.0'", - "version": "==2.1" + "version": "==2.2" }, "urllib3": { "hashes": [ - "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", - "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" + "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", + "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" ], - "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.26.2" + "version": "==1.26.4" }, "yarl": { "hashes": [ @@ -462,7 +465,6 @@ "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" ], - "markers": "python_version >= '3.5'", "version": "==1.5.1" } }, @@ -479,7 +481,6 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "cfgv": { @@ -487,7 +488,6 @@ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], - "markers": "python_full_version >= '3.6.1'", "version": "==3.2.0" }, "distlib": { @@ -514,11 +514,11 @@ }, "flake8-annotations": { "hashes": [ - "sha256:0bcebb0792f1f96d617ded674dca7bf64181870bfe5dace353a1483551f8e5f1", - "sha256:bebd11a850f6987a943ce8cdff4159767e0f5f89b3c88aca64680c2175ee02df" + "sha256:3a377140556aecf11fa9f3bb18c10db01f5ea56dc79a730e2ec9b4f1f49e2055", + "sha256:e17947a48a5b9f632fe0c72682fc797c385e451048e7dfb20139f448a074cb3e" ], "index": "pypi", - "version": "==2.4.1" + "version": "==2.5.0" }, "flake8-bugbear": { "hashes": [ @@ -576,11 +576,10 @@ }, "identify": { "hashes": [ - "sha256:943cd299ac7f5715fcb3f684e2fc1594c1e0f22a90d15398e5888143bd4144b5", - "sha256:cc86e6a9a390879dcc2976cef169dd9cc48843ed70b7380f321d1b118163c60e" + "sha256:46d1816c6a4fc2d1e8758f293a5dcc1ae6404ab344179d7c1e73637bf283beb1", + "sha256:ed4a05fb80e3cbd12e83c959f9ff7f729ba6b66ab8d6178850fd5cb4c1cf6c5d" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.5.10" + "version": "==2.1.3" }, "mccabe": { "hashes": [ @@ -606,18 +605,17 @@ }, "pre-commit": { "hashes": [ - "sha256:6c86d977d00ddc8a60d68eec19f51ef212d9462937acf3ea37c7adec32284ac0", - "sha256:ee784c11953e6d8badb97d19bc46b997a3a9eded849881ec587accd8608d74a4" + "sha256:16212d1fde2bed88159287da88ff03796863854b04dc9f838a55979325a3d20e", + "sha256:399baf78f13f4de82a29b649afd74bef2c4e28eb4f021661fc7f29246e8c7a3a" ], "index": "pypi", - "version": "==2.9.3" + "version": "==2.10.1" }, "pycodestyle": { "hashes": [ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pydocstyle": { @@ -625,7 +623,6 @@ "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325", "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678" ], - "markers": "python_version >= '3.5'", "version": "==5.1.1" }, "pyflakes": { @@ -633,7 +630,6 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pyyaml": { @@ -660,31 +656,28 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "snowballstemmer": { "hashes": [ - "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", - "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52" + "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", + "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914" ], - "version": "==2.0.0" + "version": "==2.1.0" }, "toml": { "hashes": [ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, "virtualenv": { "hashes": [ - "sha256:54b05fc737ea9c9ee9f8340f579e5da5b09fb64fd010ab5757eb90268616907c", - "sha256:b7a8ec323ee02fb2312f098b6b4c9de99559b462775bc8fe3627a73706603c1b" + "sha256:49ec4eb4c224c6f7dd81bb6d0a28a09ecae5894f4e593c89b0db0885f565a107", + "sha256:83f95875d382c7abafe06bd2a4cdd1b363e1bb77e02f155ebe8ac082a916b37c" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.2.2" + "version": "==20.4.3" } } } @@ -22,9 +22,9 @@ Before you start, please take some time to read through our [contributing guidel 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/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 -[5]: https://raw.githubusercontent.com/python-discord/branding/master/logos/badge/badge_github.svg +[1]:https://github.com/python-discord/sir-lancebot/workflows/Lint/badge.svg?branch=main +[2]:https://github.com/python-discord/sir-lancebot/actions?query=workflow%3ALint+branch%3Amain +[3]:https://github.com/python-discord/sir-lancebot/workflows/Build/badge.svg?branch=main +[4]:https://github.com/python-discord/sir-lancebot/actions?query=workflow%3ABuild+branch%3Amain +[5]: https://raw.githubusercontent.com/python-discord/branding/main/logos/badge/badge_github.svg [6]: https://discord.gg/python diff --git a/bot/__main__.py b/bot/__main__.py index e9b14a53..c6e5fa57 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -6,10 +6,9 @@ from sentry_sdk.integrations.redis import RedisIntegration from bot.bot import bot from bot.constants import Client, GIT_SHA, STAFF_ROLES, WHITELISTED_CHANNELS -from bot.utils.decorators import in_channel_check +from bot.utils.decorators import whitelist_check from bot.utils.extensions import walk_extensions - sentry_logging = LoggingIntegration( level=logging.DEBUG, event_level=logging.WARNING @@ -26,7 +25,7 @@ sentry_sdk.init( log = logging.getLogger(__name__) -bot.add_check(in_channel_check(*WHITELISTED_CHANNELS, bypass_roles=STAFF_ROLES)) +bot.add_check(whitelist_check(channels=WHITELISTED_CHANNELS, roles=STAFF_ROLES)) for ext in walk_extensions(): bot.load_extension(ext) @@ -34,7 +34,7 @@ class Bot(commands.Bot): ) self._guild_available = asyncio.Event() self.redis_session = redis_session - + self.loop.create_task(self.check_channels()) self.loop.create_task(self.send_log(self.name, "Connected!")) @property @@ -71,6 +71,21 @@ class Bot(commands.Bot): else: await super().on_command_error(context, exception) + async def check_channels(self) -> None: + """Verifies that all channel constants refer to channels which exist.""" + await self.wait_until_guild_available() + + if constants.Client.debug: + log.info("Skipping Channels Check.") + return + + all_channels_ids = [channel.id for channel in self.get_all_channels()] + for name, channel_id in vars(constants.Channels).items(): + if name.startswith('_'): + continue + if channel_id not in all_channels_ids: + log.error(f'Channel "{name}" with ID {channel_id} missing') + 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() diff --git a/bot/constants.py b/bot/constants.py index bb538487..416dd0e7 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -94,36 +94,22 @@ class Branding: class Channels(NamedTuple): - admins = 365960823622991872 advent_of_code = int(environ.get("AOC_CHANNEL_ID", 782715290437943306)) advent_of_code_commands = int(environ.get("AOC_COMMANDS_CHANNEL_ID", 607247579608121354)) - announcements = int(environ.get("CHANNEL_ANNOUNCEMENTS", 354619224620138496)) - big_brother_logs = 468507907357409333 bot = 267659945086812160 - checkpoint_test = 422077681434099723 organisation = 551789653284356126 - devalerts = 460181980097675264 devlog = int(environ.get("CHANNEL_DEVLOG", 622895325144940554)) dev_contrib = 635950537262759947 - dev_branding = 753252897059373066 - helpers = 385474242440986624 - message_log = 467752170159079424 - mod_alerts = 473092532147060736 - modlog = 282638479504965634 mod_meta = 775412552795947058 mod_tools = 775413915391098921 off_topic_0 = 291284109232308226 off_topic_1 = 463035241142026251 off_topic_2 = 463035268514185226 - python = 267624335836053506 - reddit = 458224812528238616 community_bot_commands = int(environ.get("CHANNEL_COMMUNITY_BOT_COMMANDS", 607247579608121354)) - staff_lounge = 464905259261755392 - verification = 352442727016693763 - python_discussion = 267624335836053506 hacktoberfest_2020 = 760857070781071431 voice_chat_0 = 412357430186344448 voice_chat_1 = 799647045886541885 + staff_voice = 541638762007101470 class Categories(NamedTuple): @@ -131,6 +117,7 @@ class Categories(NamedTuple): development = 411199786025484308 devprojects = 787641585624940544 media = 799054581991997460 + staff = 364918151625965579 class Client(NamedTuple): @@ -156,6 +143,9 @@ class Colours: soft_orange = 0xf9cb54 soft_red = 0xcd6d6d yellow = 0xf9f586 + python_blue = 0x4B8BBE + python_yellow = 0xFFD43B + grass_green = 0x66ff00 class Emojis: @@ -165,6 +155,7 @@ class Emojis: envelope = "\U0001F4E8" trashcan = "<:trashcan:637136429717389331>" ok_hand = ":ok_hand:" + hand_raised = "\U0001f64b" dice_1 = "<:dice_1:755891608859443290>" dice_2 = "<:dice_2:755891608741740635>" @@ -179,7 +170,6 @@ class Emojis: pull_request_closed = "<:PRClosed:629695470519713818>" merge = "<:PRMerged:629695470570176522>" - # TicTacToe Emojis number_emojis = { 1: "\u0031\ufe0f\u20e3", 2: "\u0032\ufe0f\u20e3", @@ -191,8 +181,11 @@ class Emojis: 8: "\u0038\ufe0f\u20e3", 9: "\u0039\ufe0f\u20e3" } + confirmation = "\u2705" decline = "\u274c" + incident_unactioned = "<:incident_unactioned:719645583245180960>" + x = "\U0001f1fd" o = "\U0001f1f4" @@ -240,20 +233,10 @@ if Client.month_override is not None: class Roles(NamedTuple): admin = int(environ.get("BOT_ADMIN_ROLE_ID", 267628507062992896)) - announcements = 463658397560995840 - champion = 430492892331769857 - contributor = 295488872404484098 - devops = 409416496733880320 - jammer = 423054537079783434 moderator = 267629731250176001 - muted = 277914926603829249 owner = 267627879762755584 - verified = 352427296948486144 helpers = int(environ.get("ROLE_HELPERS", 267630620367257601)) - rockstars = 458226413825294336 core_developers = 587606783669829632 - events_lead = 778361735739998228 - everyone_role = 267624335836053506 class Tokens(NamedTuple): @@ -266,6 +249,7 @@ class Tokens(NamedTuple): igdb_client_id = environ.get("IGDB_CLIENT_ID") igdb_client_secret = environ.get("IGDB_CLIENT_SECRET") github = environ.get("GITHUB_TOKEN") + unsplash_access_key = environ.get("UNSPLASH_KEY") class Wolfram(NamedTuple): @@ -281,10 +265,6 @@ class RedisConfig(NamedTuple): 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" diff --git a/bot/exts/christmas/advent_of_code/_cog.py b/bot/exts/christmas/advent_of_code/_cog.py index c3b87f96..8376987d 100644 --- a/bot/exts/christmas/advent_of_code/_cog.py +++ b/bot/exts/christmas/advent_of_code/_cog.py @@ -11,7 +11,8 @@ from bot.constants import ( AdventOfCode as AocConfig, Channels, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS, ) from bot.exts.christmas.advent_of_code import _helpers -from bot.utils.decorators import InChannelCheckFailure, in_month, override_in_channel, with_role +from bot.utils.decorators import InChannelCheckFailure, in_month, whitelist_override, with_role +from bot.utils.extensions import invoke_help_command log = logging.getLogger(__name__) @@ -36,9 +37,6 @@ class AdventOfCode(commands.Cog): self.about_aoc_filepath = Path("./bot/resources/advent_of_code/about.json") self.cached_about_aoc = self._build_about_embed() - self.countdown_task = None - self.status_task = None - notification_coro = _helpers.new_puzzle_notification(self.bot) self.notification_task = self.bot.loop.create_task(notification_coro) self.notification_task.set_name("Daily AoC Notification") @@ -50,18 +48,18 @@ class AdventOfCode(commands.Cog): self.status_task.add_done_callback(_helpers.background_task_callback) @commands.group(name="adventofcode", aliases=("aoc",)) - @override_in_channel(AOC_WHITELIST) + @whitelist_override(channels=AOC_WHITELIST) async def adventofcode_group(self, ctx: commands.Context) -> None: """All of the Advent of Code commands.""" if not ctx.invoked_subcommand: - await ctx.send_help(ctx.command) + await invoke_help_command(ctx) @adventofcode_group.command( name="subscribe", aliases=("sub", "notifications", "notify", "notifs"), brief="Notifications for new days" ) - @override_in_channel(AOC_WHITELIST) + @whitelist_override(channels=AOC_WHITELIST) async def aoc_subscribe(self, ctx: commands.Context) -> None: """Assign the role for notifications about new days being ready.""" current_year = datetime.now().year @@ -82,7 +80,7 @@ class AdventOfCode(commands.Cog): @in_month(Month.DECEMBER) @adventofcode_group.command(name="unsubscribe", aliases=("unsub",), brief="Notifications for new days") - @override_in_channel(AOC_WHITELIST) + @whitelist_override(channels=AOC_WHITELIST) async def aoc_unsubscribe(self, ctx: commands.Context) -> None: """Remove the role for notifications about new days being ready.""" role = ctx.guild.get_role(AocConfig.role_id) @@ -94,7 +92,7 @@ class AdventOfCode(commands.Cog): await ctx.send("Hey, you don't even get any notifications about new Advent of Code tasks currently anyway.") @adventofcode_group.command(name="countdown", aliases=("count", "c"), brief="Return time left until next day") - @override_in_channel(AOC_WHITELIST) + @whitelist_override(channels=AOC_WHITELIST) async def aoc_countdown(self, ctx: commands.Context) -> None: """Return time left until next day.""" if not _helpers.is_in_advent(): @@ -123,13 +121,13 @@ class AdventOfCode(commands.Cog): await ctx.send(f"There are {hours} hours and {minutes} minutes left until day {tomorrow.day}.") @adventofcode_group.command(name="about", aliases=("ab", "info"), brief="Learn about Advent of Code") - @override_in_channel(AOC_WHITELIST) + @whitelist_override(channels=AOC_WHITELIST) async def about_aoc(self, ctx: commands.Context) -> None: """Respond with an explanation of all things Advent of Code.""" await ctx.send("", embed=self.cached_about_aoc) @adventofcode_group.command(name="join", aliases=("j",), brief="Learn how to join the leaderboard (via DM)") - @override_in_channel(AOC_WHITELIST) + @whitelist_override(channels=AOC_WHITELIST) async def join_leaderboard(self, ctx: commands.Context) -> None: """DM the user the information for joining the Python Discord leaderboard.""" current_year = datetime.now().year @@ -173,12 +171,13 @@ class AdventOfCode(commands.Cog): else: await ctx.message.add_reaction(Emojis.envelope) + @in_month(Month.DECEMBER) @adventofcode_group.command( name="leaderboard", aliases=("board", "lb"), brief="Get a snapshot of the PyDis private AoC leaderboard", ) - @override_in_channel(AOC_WHITELIST_RESTRICTED) + @whitelist_override(channels=AOC_WHITELIST_RESTRICTED) async def aoc_leaderboard(self, ctx: commands.Context) -> None: """Get the current top scorers of the Python Discord Leaderboard.""" async with ctx.typing(): @@ -198,12 +197,13 @@ class AdventOfCode(commands.Cog): await ctx.send(content=f"{header}\n\n{table}", embed=info_embed) + @in_month(Month.DECEMBER) @adventofcode_group.command( name="global", aliases=("globalboard", "gb"), brief="Get a link to the global leaderboard", ) - @override_in_channel(AOC_WHITELIST_RESTRICTED) + @whitelist_override(channels=AOC_WHITELIST_RESTRICTED) async def aoc_global_leaderboard(self, ctx: commands.Context) -> None: """Get a link to the global Advent of Code leaderboard.""" url = self.global_leaderboard_url @@ -219,7 +219,7 @@ class AdventOfCode(commands.Cog): aliases=("dailystats", "ds"), brief="Get daily statistics for the Python Discord leaderboard" ) - @override_in_channel(AOC_WHITELIST_RESTRICTED) + @whitelist_override(channels=AOC_WHITELIST_RESTRICTED) async def private_leaderboard_daily_stats(self, ctx: commands.Context) -> None: """Send an embed with daily completion statistics for the Python Discord leaderboard.""" try: @@ -244,7 +244,7 @@ class AdventOfCode(commands.Cog): info_embed = _helpers.get_summary_embed(leaderboard) await ctx.send(f"```\n{table}\n```", embed=info_embed) - @with_role(Roles.admin, Roles.events_lead) + @with_role(Roles.admin) @adventofcode_group.command( name="refresh", aliases=("fetch",), @@ -268,7 +268,7 @@ class AdventOfCode(commands.Cog): def cog_unload(self) -> None: """Cancel season-related tasks on cog unload.""" log.debug("Unloading the cog and canceling the background task.") - self.countdown_task.cancel() + self.notification_task.cancel() self.status_task.cancel() def _build_about_embed(self) -> discord.Embed: diff --git a/bot/exts/christmas/advent_of_code/_helpers.py b/bot/exts/christmas/advent_of_code/_helpers.py index b7adc895..a16a4871 100644 --- a/bot/exts/christmas/advent_of_code/_helpers.py +++ b/bot/exts/christmas/advent_of_code/_helpers.py @@ -44,7 +44,7 @@ REQUIRED_CACHE_KEYS = ( AOC_EMBED_THUMBNAIL = ( "https://raw.githubusercontent.com/python-discord" - "/branding/master/seasonal/christmas/server_icons/festive_256.gif" + "/branding/main/seasonal/christmas/server_icons/festive_256.gif" ) # Create an easy constant for the EST timezone diff --git a/bot/exts/easter/earth_photos.py b/bot/exts/easter/earth_photos.py new file mode 100644 index 00000000..bf658391 --- /dev/null +++ b/bot/exts/easter/earth_photos.py @@ -0,0 +1,63 @@ +import logging + +import discord +from discord.ext import commands + +from bot.constants import Colours +from bot.constants import Tokens + +log = logging.getLogger(__name__) + + +class EarthPhotos(commands.Cog): + """This cog contains the command for earth photos.""" + + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.command(aliases=["earth"]) + async def earth_photos(self, ctx: commands.Context) -> None: + """Returns a random photo of earth, sourced from Unsplash.""" + async with ctx.typing(): + async with self.bot.http_session.get( + 'https://api.unsplash.com/photos/random', + params={"query": "planet_earth", "client_id": Tokens.unsplash_access_key} + ) as r: + jsondata = await r.json() + linksdata = jsondata.get("urls") + embedlink = linksdata.get("regular") + downloadlinksdata = jsondata.get("links") + userdata = jsondata.get("user") + username = userdata.get("name") + userlinks = userdata.get("links") + profile = userlinks.get("html") + # Referral flags + rf = "?utm_source=Sir%20Lancebot&utm_medium=referral" + async with self.bot.http_session.get( + downloadlinksdata.get("download_location"), + params={"client_id": Tokens.unsplash_access_key} + ) as _: + pass + + embed = discord.Embed( + title="Earth Photo", + description="A photo of Earth 🌎 from Unsplash.", + color=Colours.grass_green + ) + embed.set_image(url=embedlink) + embed.add_field( + name="Author", + value=( + f"Photo by [{username}]({profile}{rf}) " + f"on [Unsplash](https://unsplash.com{rf})." + ) + ) + await ctx.send(embed=embed) + + +def setup(bot: commands.Bot) -> None: + """Load the Earth Photos cog.""" + if not Tokens.unsplash_access_key: + log.warning("No Unsplash access key found. Cog not loading.") + return + bot.add_cog(EarthPhotos(bot)) diff --git a/bot/exts/evergreen/cheatsheet.py b/bot/exts/evergreen/cheatsheet.py index 97485365..3fe709d5 100644 --- a/bot/exts/evergreen/cheatsheet.py +++ b/bot/exts/evergreen/cheatsheet.py @@ -8,8 +8,8 @@ from discord.ext import commands from discord.ext.commands import BucketType, Context from bot import constants -from bot.constants import Categories, Channels, Colours, ERROR_REPLIES, Roles, WHITELISTED_CHANNELS -from bot.utils.decorators import with_role +from bot.constants import Categories, Channels, Colours, ERROR_REPLIES +from bot.utils.decorators import whitelist_override ERROR_MESSAGE = f""" Unknown cheat sheet. Please try to reformulate your query. @@ -26,6 +26,8 @@ If the problem persists send a message in <#{Channels.dev_contrib}> URL = 'https://cheat.sh/python/{search}' ESCAPE_TT = str.maketrans({"`": "\\`"}) ANSI_RE = re.compile(r"\x1b\[.*?m") +# We need to pass headers as curl otherwise it would default to aiohttp which would return raw html. +HEADERS = {'User-Agent': 'curl/7.68.0'} class CheatSheet(commands.Cog): @@ -73,7 +75,7 @@ class CheatSheet(commands.Cog): aliases=("cht.sh", "cheatsheet", "cheat-sheet", "cht"), ) @commands.cooldown(1, 10, BucketType.user) - @with_role(Roles.everyone_role) + @whitelist_override(categories=[Categories.help_in_use]) async def cheat_sheet(self, ctx: Context, *search_terms: str) -> None: """ Search cheat.sh. @@ -82,17 +84,11 @@ class CheatSheet(commands.Cog): Usage: --> .cht read json """ - if not ( - ctx.channel.category.id == Categories.help_in_use - or ctx.channel.id in WHITELISTED_CHANNELS - ): - return - async with ctx.typing(): search_string = quote_plus(" ".join(search_terms)) async with self.bot.http_session.get( - URL.format(search=search_string) + URL.format(search=search_string), headers=HEADERS ) as response: result = ANSI_RE.sub("", await response.text()).translate(ESCAPE_TT) diff --git a/bot/exts/evergreen/connect_four.py b/bot/exts/evergreen/connect_four.py new file mode 100644 index 00000000..7e3ec42b --- /dev/null +++ b/bot/exts/evergreen/connect_four.py @@ -0,0 +1,450 @@ +import asyncio +import random +import typing +from functools import partial + +import discord +import emojis +from discord.ext import commands +from discord.ext.commands import guild_only + +from bot.constants import Emojis + +NUMBERS = list(Emojis.number_emojis.values()) +CROSS_EMOJI = Emojis.incident_unactioned + +Coordinate = typing.Optional[typing.Tuple[int, int]] +EMOJI_CHECK = typing.Union[discord.Emoji, str] + + +class Game: + """A Connect 4 Game.""" + + def __init__( + self, + bot: commands.Bot, + channel: discord.TextChannel, + player1: discord.Member, + player2: typing.Optional[discord.Member], + tokens: typing.List[str], + size: int = 7 + ) -> None: + + self.bot = bot + self.channel = channel + self.player1 = player1 + self.player2 = player2 or AI(self.bot, game=self) + self.tokens = tokens + + self.grid = self.generate_board(size) + self.grid_size = size + + self.unicode_numbers = NUMBERS[:self.grid_size] + + self.message = None + + self.player_active = None + self.player_inactive = None + + @staticmethod + def generate_board(size: int) -> typing.List[typing.List[int]]: + """Generate the connect 4 board.""" + return [[0 for _ in range(size)] for _ in range(size)] + + async def print_grid(self) -> None: + """Formats and outputs the Connect Four grid to the channel.""" + title = ( + f'Connect 4: {self.player1.display_name}' + f' VS {self.bot.user.display_name if isinstance(self.player2, AI) else self.player2.display_name}' + ) + + rows = [" ".join(self.tokens[s] for s in row) for row in self.grid] + first_row = " ".join(x for x in NUMBERS[:self.grid_size]) + formatted_grid = "\n".join([first_row] + rows) + embed = discord.Embed(title=title, description=formatted_grid) + + if self.message: + await self.message.edit(embed=embed) + else: + self.message = await self.channel.send(content='Loading...') + for emoji in self.unicode_numbers: + await self.message.add_reaction(emoji) + await self.message.add_reaction(CROSS_EMOJI) + await self.message.edit(content=None, embed=embed) + + async def game_over(self, action: str, player1: discord.user, player2: discord.user) -> None: + """Announces to public chat.""" + if action == "win": + await self.channel.send(f"Game Over! {player1.mention} won against {player2.mention}") + elif action == "draw": + await self.channel.send(f"Game Over! {player1.mention} {player2.mention} It's A Draw :tada:") + elif action == "quit": + await self.channel.send(f"{self.player1.mention} surrendered. Game over!") + await self.print_grid() + + async def start_game(self) -> None: + """Begins the game.""" + self.player_active, self.player_inactive = self.player1, self.player2 + + while True: + await self.print_grid() + + if isinstance(self.player_active, AI): + coords = self.player_active.play() + if not coords: + await self.game_over( + "draw", + self.bot.user if isinstance(self.player_active, AI) else self.player_active, + self.bot.user if isinstance(self.player_inactive, AI) else self.player_inactive, + ) + else: + coords = await self.player_turn() + + if not coords: + return + + if self.check_win(coords, 1 if self.player_active == self.player1 else 2): + await self.game_over( + "win", + self.bot.user if isinstance(self.player_active, AI) else self.player_active, + self.bot.user if isinstance(self.player_inactive, AI) else self.player_inactive, + ) + return + + self.player_active, self.player_inactive = self.player_inactive, self.player_active + + def predicate(self, reaction: discord.Reaction, user: discord.Member) -> bool: + """The predicate to check for the player's reaction.""" + return ( + reaction.message.id == self.message.id + and user.id == self.player_active.id + and str(reaction.emoji) in (*self.unicode_numbers, CROSS_EMOJI) + ) + + async def player_turn(self) -> Coordinate: + """Initiate the player's turn.""" + message = await self.channel.send( + f"{self.player_active.mention}, it's your turn! React with the column you want to place your token in." + ) + player_num = 1 if self.player_active == self.player1 else 2 + while True: + try: + reaction, user = await self.bot.wait_for("reaction_add", check=self.predicate, timeout=30.0) + except asyncio.TimeoutError: + await self.channel.send(f"{self.player_active.mention}, you took too long. Game over!") + return + else: + await message.delete() + if str(reaction.emoji) == CROSS_EMOJI: + await self.game_over("quit", self.player_active, self.player_inactive) + return + + await self.message.remove_reaction(reaction, user) + + column_num = self.unicode_numbers.index(str(reaction.emoji)) + column = [row[column_num] for row in self.grid] + + for row_num, square in reversed(list(enumerate(column))): + if not square: + self.grid[row_num][column_num] = player_num + return row_num, column_num + message = await self.channel.send(f"Column {column_num + 1} is full. Try again") + + def check_win(self, coords: Coordinate, player_num: int) -> bool: + """Check that placing a counter here would cause the player to win.""" + vertical = [(-1, 0), (1, 0)] + horizontal = [(0, 1), (0, -1)] + forward_diag = [(-1, 1), (1, -1)] + backward_diag = [(-1, -1), (1, 1)] + axes = [vertical, horizontal, forward_diag, backward_diag] + + for axis in axes: + counters_in_a_row = 1 # The initial counter that is compared to + for (row_incr, column_incr) in axis: + row, column = coords + row += row_incr + column += column_incr + + while 0 <= row < self.grid_size and 0 <= column < self.grid_size: + if self.grid[row][column] == player_num: + counters_in_a_row += 1 + row += row_incr + column += column_incr + else: + break + if counters_in_a_row >= 4: + return True + return False + + +class AI: + """The Computer Player for Single-Player games.""" + + def __init__(self, bot: commands.Bot, game: Game) -> None: + self.game = game + self.mention = bot.user.mention + + def get_possible_places(self) -> typing.List[Coordinate]: + """Gets all the coordinates where the AI could possibly place a counter.""" + possible_coords = [] + for column_num in range(self.game.grid_size): + column = [row[column_num] for row in self.game.grid] + for row_num, square in reversed(list(enumerate(column))): + if not square: + possible_coords.append((row_num, column_num)) + break + return possible_coords + + def check_ai_win(self, coord_list: typing.List[Coordinate]) -> typing.Optional[Coordinate]: + """ + Check AI win. + + Check if placing a counter in any possible coordinate would cause the AI to win + with 10% chance of not winning and returning None + """ + if random.randint(1, 10) == 1: + return + for coords in coord_list: + if self.game.check_win(coords, 2): + return coords + + def check_player_win(self, coord_list: typing.List[Coordinate]) -> typing.Optional[Coordinate]: + """ + Check Player win. + + Check if placing a counter in possible coordinates would stop the player + from winning with 25% of not blocking them and returning None. + """ + if random.randint(1, 4) == 1: + return + for coords in coord_list: + if self.game.check_win(coords, 1): + return coords + + @staticmethod + def random_coords(coord_list: typing.List[Coordinate]) -> Coordinate: + """Picks a random coordinate from the possible ones.""" + return random.choice(coord_list) + + def play(self) -> typing.Union[Coordinate, bool]: + """ + Plays for the AI. + + Gets all possible coords, and determins the move: + 1. coords where it can win. + 2. coords where the player can win. + 3. Random coord + The first possible value is choosen. + """ + possible_coords = self.get_possible_places() + + if not possible_coords: + return False + + coords = ( + self.check_ai_win(possible_coords) + or self.check_player_win(possible_coords) + or self.random_coords(possible_coords) + ) + + row, column = coords + self.game.grid[row][column] = 2 + return coords + + +class ConnectFour(commands.Cog): + """Connect Four. The Classic Vertical Four-in-a-row Game!""" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + self.games: typing.List[Game] = [] + self.waiting: typing.List[discord.Member] = [] + + self.tokens = [":white_circle:", ":blue_circle:", ":red_circle:"] + + self.max_board_size = 9 + self.min_board_size = 5 + + async def check_author(self, ctx: commands.Context, board_size: int) -> bool: + """Check if the requester is free and the board size is correct.""" + if self.already_playing(ctx.author): + await ctx.send("You're already playing a game!") + return False + + if ctx.author in self.waiting: + await ctx.send("You've already sent out a request for a player 2") + return False + + if not self.min_board_size <= board_size <= self.max_board_size: + await ctx.send(f"{board_size} is not a valid board size. A valid board size is " + f"between `{self.min_board_size}` and `{self.max_board_size}`.") + return False + + return True + + def get_player( + self, + ctx: commands.Context, + announcement: discord.Message, + reaction: discord.Reaction, + user: discord.Member + ) -> bool: + """Predicate checking the criteria for the announcement message.""" + if self.already_playing(ctx.author): # If they've joined a game since requesting a player 2 + return True # Is dealt with later on + + if ( + user.id not in (ctx.me.id, ctx.author.id) + and str(reaction.emoji) == Emojis.hand_raised + and reaction.message.id == announcement.id + ): + if self.already_playing(user): + self.bot.loop.create_task(ctx.send(f"{user.mention} You're already playing a game!")) + self.bot.loop.create_task(announcement.remove_reaction(reaction, user)) + return False + + if user in self.waiting: + self.bot.loop.create_task(ctx.send( + f"{user.mention} Please cancel your game first before joining another one." + )) + self.bot.loop.create_task(announcement.remove_reaction(reaction, user)) + return False + + return True + + if ( + user.id == ctx.author.id + and str(reaction.emoji) == CROSS_EMOJI + and reaction.message.id == announcement.id + ): + return True + return False + + def already_playing(self, player: discord.Member) -> bool: + """Check if someone is already in a game.""" + return any(player in (game.player1, game.player2) for game in self.games) + + @staticmethod + def check_emojis( + e1: EMOJI_CHECK, e2: EMOJI_CHECK + ) -> typing.Tuple[bool, typing.Optional[str]]: + """Validate the emojis, the user put.""" + if isinstance(e1, str) and emojis.count(e1) != 1: + return False, e1 + if isinstance(e2, str) and emojis.count(e2) != 1: + return False, e2 + return True, None + + async def _play_game( + self, + ctx: commands.Context, + user: typing.Optional[discord.Member], + board_size: int, + emoji1: str, + emoji2: str + ) -> None: + """Helper for playing a game of connect four.""" + self.tokens = [":white_circle:", str(emoji1), str(emoji2)] + game = None # if game fails to intialize in try...except + + try: + game = Game(self.bot, ctx.channel, ctx.author, user, self.tokens, size=board_size) + self.games.append(game) + await game.start_game() + self.games.remove(game) + except Exception: + # End the game in the event of an unforeseen error so the players aren't stuck in a game + await ctx.send(f"{ctx.author.mention} {user.mention if user else ''} An error occurred. Game failed") + if game in self.games: + self.games.remove(game) + raise + + @guild_only() + @commands.group( + invoke_without_command=True, + aliases=["4inarow", "connect4", "connectfour", "c4"], + case_insensitive=True + ) + async def connect_four( + self, + ctx: commands.Context, + board_size: int = 7, + emoji1: EMOJI_CHECK = "\U0001f535", + emoji2: EMOJI_CHECK = "\U0001f534" + ) -> None: + """ + Play the classic game of Connect Four with someone! + + Sets up a message waiting for someone else to react and play along. + The game will start once someone has reacted. + All inputs will be through reactions. + """ + check, emoji = self.check_emojis(emoji1, emoji2) + if not check: + raise commands.EmojiNotFound(emoji) + + check_author_result = await self.check_author(ctx, board_size) + if not check_author_result: + return + + announcement = await ctx.send( + "**Connect Four**: A new game is about to start!\n" + f"Press {Emojis.hand_raised} to play against {ctx.author.mention}!\n" + f"(Cancel the game with {CROSS_EMOJI}.)" + ) + self.waiting.append(ctx.author) + await announcement.add_reaction(Emojis.hand_raised) + await announcement.add_reaction(CROSS_EMOJI) + + try: + reaction, user = await self.bot.wait_for( + "reaction_add", + check=partial(self.get_player, ctx, announcement), + timeout=60.0 + ) + except asyncio.TimeoutError: + self.waiting.remove(ctx.author) + await announcement.delete() + await ctx.send( + f"{ctx.author.mention} Seems like there's no one here to play. " + f"Use `{ctx.prefix}{ctx.invoked_with} ai` to play against a computer." + ) + return + + if str(reaction.emoji) == CROSS_EMOJI: + self.waiting.remove(ctx.author) + await announcement.delete() + await ctx.send(f"{ctx.author.mention} Game cancelled.") + return + + await announcement.delete() + self.waiting.remove(ctx.author) + if self.already_playing(ctx.author): + return + + await self._play_game(ctx, user, board_size, str(emoji1), str(emoji2)) + + @guild_only() + @connect_four.command(aliases=["bot", "computer", "cpu"]) + async def ai( + self, + ctx: commands.Context, + board_size: int = 7, + emoji1: EMOJI_CHECK = "\U0001f535", + emoji2: EMOJI_CHECK = "\U0001f534" + ) -> None: + """Play Connect Four against a computer player.""" + check, emoji = self.check_emojis(emoji1, emoji2) + if not check: + raise commands.EmojiNotFound(emoji) + + check_author_result = await self.check_author(ctx, board_size) + if not check_author_result: + return + + await self._play_game(ctx, None, board_size, str(emoji1), str(emoji2)) + + +def setup(bot: commands.Bot) -> None: + """Load ConnectFour Cog.""" + bot.add_cog(ConnectFour(bot)) diff --git a/bot/exts/evergreen/conversationstarters.py b/bot/exts/evergreen/conversationstarters.py index 576b8d76..e7058961 100644 --- a/bot/exts/evergreen/conversationstarters.py +++ b/bot/exts/evergreen/conversationstarters.py @@ -5,7 +5,7 @@ from discord import Color, Embed from discord.ext import commands from bot.constants import WHITELISTED_CHANNELS -from bot.utils.decorators import override_in_channel +from bot.utils.decorators import whitelist_override from bot.utils.randomization import RandomCycle SUGGESTION_FORM = 'https://forms.gle/zw6kkJqv8U43Nfjg9' @@ -38,7 +38,7 @@ class ConvoStarters(commands.Cog): self.bot = bot @commands.command() - @override_in_channel(ALL_ALLOWED_CHANNELS) + @whitelist_override(channels=ALL_ALLOWED_CHANNELS) async def topic(self, ctx: commands.Context) -> None: """ Responds with a random topic to start a conversation. diff --git a/bot/exts/evergreen/emoji_count.py b/bot/exts/evergreen/emoji.py index cc43e9ab..fa3044e3 100644 --- a/bot/exts/evergreen/emoji_count.py +++ b/bot/exts/evergreen/emoji.py @@ -1,49 +1,52 @@ -import datetime import logging import random +import textwrap from collections import defaultdict -from typing import List, Tuple +from datetime import datetime +from typing import List, Optional, Tuple -import discord +from discord import Color, Embed, Emoji from discord.ext import commands from bot.constants import Colours, ERROR_REPLIES +from bot.utils.extensions import invoke_help_command from bot.utils.pagination import LinePaginator +from bot.utils.time import time_since log = logging.getLogger(__name__) -class EmojiCount(commands.Cog): - """Command that give random emoji based on category.""" +class Emojis(commands.Cog): + """A collection of commands related to emojis in the server.""" def __init__(self, bot: commands.Bot): self.bot = bot @staticmethod - def embed_builder(emoji: dict) -> Tuple[discord.Embed, List[str]]: + def embed_builder(emoji: dict) -> Tuple[Embed, List[str]]: """Generates an embed with the emoji names and count.""" - embed = discord.Embed( + embed = Embed( color=Colours.orange, title="Emoji Count", - timestamp=datetime.datetime.utcnow() + timestamp=datetime.utcnow() ) msg = [] if len(emoji) == 1: for category_name, category_emojis in emoji.items(): if len(category_emojis) == 1: - msg.append(f"There is **{len(category_emojis)}** emoji in **{category_name}** category") + msg.append(f"There is **{len(category_emojis)}** emoji in the **{category_name}** category.") else: - msg.append(f"There are **{len(category_emojis)}** emojis in **{category_name}** category") + msg.append(f"There are **{len(category_emojis)}** emojis in the **{category_name}** category.") embed.set_thumbnail(url=random.choice(category_emojis).url) else: for category_name, category_emojis in emoji.items(): emoji_choice = random.choice(category_emojis) if len(category_emojis) > 1: - emoji_info = f"There are **{len(category_emojis)}** emojis in **{category_name}** category" + emoji_info = f"There are **{len(category_emojis)}** emojis in the **{category_name}** category." else: - emoji_info = f"There is **{len(category_emojis)}** emoji in **{category_name}** category" + emoji_info = f"There is **{len(category_emojis)}** emoji in the **{category_name}** category." if emoji_choice.animated: msg.append(f'<a:{emoji_choice.name}:{emoji_choice.id}> {emoji_info}') else: @@ -51,9 +54,9 @@ class EmojiCount(commands.Cog): return embed, msg @staticmethod - def generate_invalid_embed(emojis: list) -> Tuple[discord.Embed, List[str]]: - """Generates error embed.""" - embed = discord.Embed( + def generate_invalid_embed(emojis: list) -> Tuple[Embed, List[str]]: + """Generates error embed for invalid emoji categories.""" + embed = Embed( color=Colours.soft_red, title=random.choice(ERROR_REPLIES) ) @@ -64,11 +67,19 @@ class EmojiCount(commands.Cog): emoji_dict[emoji.name.split("_")[0]].append(emoji) error_comp = ', '.join(emoji_dict) - msg.append(f"These are the valid categories\n```{error_comp}```") + msg.append(f"These are the valid emoji categories:\n```{error_comp}```") return embed, msg - @commands.command(name="emojicount", aliases=["ec", "emojis"]) - async def emoji_count(self, ctx: commands.Context, *, category_query: str = None) -> None: + @commands.group(name="emoji", invoke_without_command=True) + async def emoji_group(self, ctx: commands.Context, emoji: Optional[Emoji]) -> None: + """A group of commands related to emojis.""" + if emoji is not None: + await ctx.invoke(self.info_command, emoji) + else: + await invoke_help_command(ctx) + + @emoji_group.command(name="count", aliases=("c",)) + async def count_command(self, ctx: commands.Context, *, category_query: str = None) -> None: """Returns embed with emoji category and info given by the user.""" emoji_dict = defaultdict(list) @@ -91,7 +102,24 @@ class EmojiCount(commands.Cog): embed, msg = self.embed_builder(emoji_dict) await LinePaginator.paginate(lines=msg, ctx=ctx, embed=embed) + @emoji_group.command(name="info", aliases=("i",)) + async def info_command(self, ctx: commands.Context, emoji: Emoji) -> None: + """Returns relevant information about a Discord Emoji.""" + emoji_information = Embed( + title=f"Emoji Information: {emoji.name}", + description=textwrap.dedent(f""" + **Name:** {emoji.name} + **Created:** {time_since(emoji.created_at, precision="hours")} + **Date:** {datetime.strftime(emoji.created_at, "%d/%m/%Y")} + **ID:** {emoji.id} + """), + color=Color.blurple(), + url=str(emoji.url), + ).set_thumbnail(url=emoji.url) + + await ctx.send(embed=emoji_information) + def setup(bot: commands.Bot) -> None: - """Emoji Count Cog load.""" - bot.add_cog(EmojiCount(bot)) + """Add the Emojis cog into the bot.""" + bot.add_cog(Emojis(bot)) diff --git a/bot/exts/evergreen/error_handler.py b/bot/exts/evergreen/error_handler.py index 99af1519..28902503 100644 --- a/bot/exts/evergreen/error_handler.py +++ b/bot/exts/evergreen/error_handler.py @@ -7,7 +7,7 @@ from discord import Embed, Message from discord.ext import commands from sentry_sdk import push_scope -from bot.constants import Colours, ERROR_REPLIES, NEGATIVE_REPLIES +from bot.constants import Channels, Colours, ERROR_REPLIES, NEGATIVE_REPLIES from bot.utils.decorators import InChannelCheckFailure, InMonthCheckFailure from bot.utils.exceptions import UserNotPlayingError @@ -83,7 +83,12 @@ class CommandErrorHandler(commands.Cog): return if isinstance(error, commands.NoPrivateMessage): - await ctx.send(embed=self.error_embed("This command can only be used in the server.", NEGATIVE_REPLIES)) + await ctx.send( + embed=self.error_embed( + f"This command can only be used in the server. Go to <#{Channels.community_bot_commands}> instead!", + NEGATIVE_REPLIES + ) + ) return if isinstance(error, commands.BadArgument): diff --git a/bot/exts/evergreen/game.py b/bot/exts/evergreen/game.py index d37be0e2..068d3f68 100644 --- a/bot/exts/evergreen/game.py +++ b/bot/exts/evergreen/game.py @@ -15,6 +15,7 @@ from discord.ext.commands import Cog, Context, group from bot.bot import Bot from bot.constants import STAFF_ROLES, Tokens from bot.utils.decorators import with_role +from bot.utils.extensions import invoke_help_command from bot.utils.pagination import ImagePaginator, LinePaginator # Base URL of IGDB API @@ -234,7 +235,7 @@ class Games(Cog): """ # When user didn't specified genre, send help message if genre is None: - await ctx.send_help("games") + await invoke_help_command(ctx) return # Capitalize genre for check diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 1f22f287..d877ac00 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -24,9 +24,11 @@ if GITHUB_TOKEN := Tokens.github: REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" WHITELISTED_CATEGORIES = ( - Categories.devprojects, Categories.media, Categories.development + Categories.development, Categories.devprojects, Categories.media, Categories.staff +) +WHITELISTED_CHANNELS_ON_MESSAGE = ( + Channels.organisation, Channels.mod_meta, Channels.mod_tools, Channels.staff_voice ) -WHITELISTED_CHANNELS_ON_MESSAGE = (Channels.organisation, Channels.mod_meta, Channels.mod_tools) CODE_BLOCK_RE = re.compile( r"^`([^`\n]+)`" # Inline codeblock diff --git a/bot/exts/evergreen/minesweeper.py b/bot/exts/evergreen/minesweeper.py index 286ac7a5..3031debc 100644 --- a/bot/exts/evergreen/minesweeper.py +++ b/bot/exts/evergreen/minesweeper.py @@ -8,6 +8,7 @@ from discord.ext import commands from bot.constants import Client from bot.utils.exceptions import UserNotPlayingError +from bot.utils.extensions import invoke_help_command MESSAGE_MAPPING = { 0: ":stop_button:", @@ -83,7 +84,7 @@ class Minesweeper(commands.Cog): @commands.group(name='minesweeper', aliases=('ms',), invoke_without_command=True) async def minesweeper_group(self, ctx: commands.Context) -> None: """Commands for Playing Minesweeper.""" - await ctx.send_help(ctx.command) + await invoke_help_command(ctx) @staticmethod def get_neighbours(x: int, y: int) -> typing.Generator[typing.Tuple[int, int], None, None]: diff --git a/bot/exts/evergreen/movie.py b/bot/exts/evergreen/movie.py index 340a5724..b3bfe998 100644 --- a/bot/exts/evergreen/movie.py +++ b/bot/exts/evergreen/movie.py @@ -9,6 +9,7 @@ from discord import Embed from discord.ext.commands import Bot, Cog, Context, group from bot.constants import Tokens +from bot.utils.extensions import invoke_help_command from bot.utils.pagination import ImagePaginator # Define base URL of TMDB @@ -73,7 +74,7 @@ class Movie(Cog): try: result = await self.get_movies_list(self.http_session, MovieGenres[genre].value, 1) except KeyError: - await ctx.send_help('movies') + await invoke_help_command(ctx) return # Check if "results" is in result. If not, throw error. diff --git a/bot/exts/evergreen/pythonfacts.py b/bot/exts/evergreen/pythonfacts.py new file mode 100644 index 00000000..457c2fd3 --- /dev/null +++ b/bot/exts/evergreen/pythonfacts.py @@ -0,0 +1,33 @@ +import itertools + +import discord +from discord.ext import commands + +from bot.constants import Colours + +with open('bot/resources/evergreen/python_facts.txt') as file: + FACTS = itertools.cycle(list(file)) + +COLORS = itertools.cycle([Colours.python_blue, Colours.python_yellow]) + + +class PythonFacts(commands.Cog): + """Sends a random fun fact about Python.""" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + @commands.command(name='pythonfact', aliases=['pyfact']) + async def get_python_fact(self, ctx: commands.Context) -> None: + """Sends a Random fun fact about Python.""" + embed = discord.Embed(title='Python Facts', + description=next(FACTS), + colour=next(COLORS)) + embed.add_field(name='Suggestions', + value="Suggest more facts [here!](https://github.com/python-discord/meta/discussions/93)") + await ctx.send(embed=embed) + + +def setup(bot: commands.Bot) -> None: + """Load PythonFacts Cog.""" + bot.add_cog(PythonFacts(bot)) diff --git a/bot/exts/evergreen/snakes/_snakes_cog.py b/bot/exts/evergreen/snakes/_snakes_cog.py index d5e4f206..3732b559 100644 --- a/bot/exts/evergreen/snakes/_snakes_cog.py +++ b/bot/exts/evergreen/snakes/_snakes_cog.py @@ -22,6 +22,7 @@ from bot.constants import ERROR_REPLIES, Tokens from bot.exts.evergreen.snakes import _utils as utils from bot.exts.evergreen.snakes._converter import Snake from bot.utils.decorators import locked +from bot.utils.extensions import invoke_help_command log = logging.getLogger(__name__) @@ -440,7 +441,7 @@ class Snakes(Cog): @group(name='snakes', aliases=('snake',), invoke_without_command=True) async def snakes_group(self, ctx: Context) -> None: """Commands from our first code jam.""" - await ctx.send_help(ctx.command) + await invoke_help_command(ctx) @bot_has_permissions(manage_messages=True) @snakes_group.command(name='antidote') diff --git a/bot/exts/evergreen/source.py b/bot/exts/evergreen/source.py index cdfe54ec..45752bf9 100644 --- a/bot/exts/evergreen/source.py +++ b/bot/exts/evergreen/source.py @@ -76,7 +76,7 @@ class BotSource(commands.Cog): file_location = Path(filename).relative_to(Path.cwd()).as_posix() - url = f"{Source.github}/blob/master/{file_location}{lines_extension}" + url = f"{Source.github}/blob/main/{file_location}{lines_extension}" return url, file_location, first_line_no or None diff --git a/bot/exts/evergreen/space.py b/bot/exts/evergreen/space.py index bc8e3118..323ff659 100644 --- a/bot/exts/evergreen/space.py +++ b/bot/exts/evergreen/space.py @@ -10,6 +10,7 @@ from discord.ext.commands import BadArgument, Cog, Context, Converter, group from bot.bot import Bot from bot.constants import Tokens +from bot.utils.extensions import invoke_help_command logger = logging.getLogger(__name__) @@ -63,7 +64,7 @@ class Space(Cog): @group(name="space", invoke_without_command=True) async def space(self, ctx: Context) -> None: """Head command that contains commands about space.""" - await ctx.send_help("space") + await invoke_help_command(ctx) @space.command(name="apod") async def apod(self, ctx: Context, date: Optional[str] = None) -> None: diff --git a/bot/exts/evergreen/status_codes.py b/bot/exts/evergreen/status_codes.py index 874c87eb..7c00fe20 100644 --- a/bot/exts/evergreen/status_codes.py +++ b/bot/exts/evergreen/status_codes.py @@ -3,6 +3,8 @@ from http import HTTPStatus import discord from discord.ext import commands +from bot.utils.extensions import invoke_help_command + HTTP_DOG_URL = "https://httpstatusdogs.com/img/{code}.jpg" HTTP_CAT_URL = "https://http.cat/{code}.jpg" @@ -17,7 +19,7 @@ class HTTPStatusCodes(commands.Cog): async def http_status_group(self, ctx: commands.Context) -> None: """Group containing dog and cat http status code commands.""" if not ctx.invoked_subcommand: - await ctx.send_help(ctx.command) + await invoke_help_command(ctx) @http_status_group.command(name='cat') async def http_cat(self, ctx: commands.Context, code: int) -> None: diff --git a/bot/exts/evergreen/tic_tac_toe.py b/bot/exts/evergreen/tic_tac_toe.py index e1190502..6e21528e 100644 --- a/bot/exts/evergreen/tic_tac_toe.py +++ b/bot/exts/evergreen/tic_tac_toe.py @@ -10,8 +10,8 @@ from bot.constants import Emojis from bot.utils.pagination import LinePaginator CONFIRMATION_MESSAGE = ( - "{opponent}, {requester} wants to play Tic-Tac-Toe against you. React to this message with " - f"{Emojis.confirmation} to accept or with {Emojis.decline} to decline." + "{opponent}, {requester} wants to play Tic-Tac-Toe against you." + f"\nReact to this message with {Emojis.confirmation} to accept or with {Emojis.decline} to decline." ) @@ -253,7 +253,7 @@ class TicTacToe(Cog): @guild_only() @is_channel_free() @is_requester_free() - @group(name="tictactoe", aliases=("ttt",), invoke_without_command=True) + @group(name="tictactoe", aliases=("ttt", "tic"), invoke_without_command=True) async def tic_tac_toe(self, ctx: Context, opponent: t.Optional[discord.User]) -> None: """Tic Tac Toe game. Play against friends or AI. Use reactions to add your mark to field.""" if opponent == ctx.author: @@ -276,6 +276,10 @@ class TicTacToe(Cog): ) self.games.append(game) if opponent is not None: + if opponent.bot: # check whether the opponent is a bot or not + await ctx.send("You can't play Tic-Tac-Toe with bots!") + return + confirmed, msg = await game.get_confirmation() if not confirmed: diff --git a/bot/exts/evergreen/wikipedia.py b/bot/exts/evergreen/wikipedia.py index be36e2c4..068c4f43 100644 --- a/bot/exts/evergreen/wikipedia.py +++ b/bot/exts/evergreen/wikipedia.py @@ -1,114 +1,94 @@ -import asyncio -import datetime import logging +import re +from datetime import datetime +from html import unescape from typing import List, Optional -from aiohttp import client_exceptions -from discord import Color, Embed, Message +from discord import Color, Embed, TextChannel from discord.ext import commands -from bot.constants import Wikipedia +from bot.bot import Bot +from bot.utils import LinePaginator log = logging.getLogger(__name__) -SEARCH_API = "https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch={search_term}&format=json" -WIKIPEDIA_URL = "https://en.wikipedia.org/wiki/{title}" +SEARCH_API = ( + "https://en.wikipedia.org/w/api.php?action=query&list=search&prop=info&inprop=url&utf8=&" + "format=json&origin=*&srlimit={number_of_results}&srsearch={string}" +) +WIKI_THUMBNAIL = ( + "https://upload.wikimedia.org/wikipedia/en/thumb/8/80/Wikipedia-logo-v2.svg" + "/330px-Wikipedia-logo-v2.svg.png" +) +WIKI_SNIPPET_REGEX = r'(<!--.*?-->|<[^>]*>)' +WIKI_SEARCH_RESULT = ( + "**[{name}]({url})**\n" + "{description}\n" +) class WikipediaSearch(commands.Cog): """Get info from wikipedia.""" - def __init__(self, bot: commands.Bot): + def __init__(self, bot: Bot): self.bot = bot - self.http_session = bot.http_session - @staticmethod - def formatted_wiki_url(index: int, title: str) -> str: - """Formating wikipedia link with index and title.""" - return f'`{index}` [{title}]({WIKIPEDIA_URL.format(title=title.replace(" ", "_"))})' + async def wiki_request(self, channel: TextChannel, search: str) -> Optional[List[str]]: + """Search wikipedia search string and return formatted first 10 pages found.""" + url = SEARCH_API.format(number_of_results=10, string=search) + async with self.bot.http_session.get(url=url) as resp: + if resp.status == 200: + raw_data = await resp.json() + number_of_results = raw_data['query']['searchinfo']['totalhits'] + + if number_of_results: + results = raw_data['query']['search'] + lines = [] + + for article in results: + line = WIKI_SEARCH_RESULT.format( + name=article['title'], + description=unescape( + re.sub( + WIKI_SNIPPET_REGEX, '', article['snippet'] + ) + ), + url=f"https://en.wikipedia.org/?curid={article['pageid']}" + ) + lines.append(line) + + return lines - async def search_wikipedia(self, search_term: str) -> Optional[List[str]]: - """Search wikipedia and return the first 10 pages found.""" - pages = [] - async with self.http_session.get(SEARCH_API.format(search_term=search_term)) as response: - try: - data = await response.json() - - search_results = data["query"]["search"] - - # Ignore pages with "may refer to" - for search_result in search_results: - log.info("trying to append titles") - if "may refer to" not in search_result["snippet"]: - pages.append(search_result["title"]) - except client_exceptions.ContentTypeError: - pages = None - - log.info("Finished appending titles") - return pages + else: + await channel.send( + "Sorry, we could not find a wikipedia article using that search term." + ) + return + else: + log.info(f"Unexpected response `{resp.status}` while searching wikipedia for `{search}`") + await channel.send( + "Whoops, the Wikipedia API is having some issues right now. Try again later." + ) + return @commands.cooldown(1, 10, commands.BucketType.user) @commands.command(name="wikipedia", aliases=["wiki"]) async def wikipedia_search_command(self, ctx: commands.Context, *, search: str) -> None: - """Return list of results containing your search query from wikipedia.""" - titles = await self.search_wikipedia(search) - - def check(message: Message) -> bool: - return message.author.id == ctx.author.id and message.channel == ctx.channel - - if not titles: - await ctx.send("Sorry, we could not find a wikipedia article using that search term") - return - - async with ctx.typing(): - log.info("Finished appending titles to titles_no_underscore list") - - s_desc = "\n".join(self.formatted_wiki_url(index, title) for index, title in enumerate(titles, start=1)) - embed = Embed(colour=Color.blue(), title=f"Wikipedia results for `{search}`", description=s_desc) - embed.timestamp = datetime.datetime.utcnow() - await ctx.send(embed=embed) - embed = Embed(colour=Color.green(), description="Enter number to choose") - msg = await ctx.send(embed=embed) - titles_len = len(titles) # getting length of list - - for retry_count in range(1, Wikipedia.total_chance + 1): - retries_left = Wikipedia.total_chance - retry_count - if retry_count < Wikipedia.total_chance: - error_msg = f"You have `{retries_left}/{Wikipedia.total_chance}` chances left" - else: - error_msg = 'Please try again by using `.wiki` command' - try: - message = await ctx.bot.wait_for('message', timeout=60.0, check=check) - response_from_user = await self.bot.get_context(message) - - if response_from_user.command: - return - - response = int(message.content) - if response < 0: - await ctx.send(f"Sorry, but you can't give negative index, {error_msg}") - elif response == 0: - await ctx.send(f"Sorry, please give an integer between `1` to `{titles_len}`, {error_msg}") - else: - await ctx.send(WIKIPEDIA_URL.format(title=titles[response - 1].replace(" ", "_"))) - break - - except asyncio.TimeoutError: - embed = Embed(colour=Color.red(), description=f"Time's up {ctx.author.mention}") - await msg.edit(embed=embed) - break - - except ValueError: - await ctx.send(f"Sorry, but you cannot do that, I will only accept an positive integer, {error_msg}") - - except IndexError: - await ctx.send(f"Sorry, please give an integer between `1` to `{titles_len}`, {error_msg}") - - except Exception as e: - log.info(f"Caught exception {e}, breaking out of retry loop") - break - - -def setup(bot: commands.Bot) -> None: + """Sends paginated top 10 results of Wikipedia search..""" + contents = await self.wiki_request(ctx.channel, search) + + if contents: + embed = Embed( + title="Wikipedia Search Results", + colour=Color.blurple() + ) + embed.set_thumbnail(url=WIKI_THUMBNAIL) + embed.timestamp = datetime.utcnow() + await LinePaginator.paginate( + contents, ctx, embed + ) + + +def setup(bot: Bot) -> None: """Wikipedia Cog load.""" bot.add_cog(WikipediaSearch(bot)) diff --git a/bot/exts/evergreen/wolfram.py b/bot/exts/evergreen/wolfram.py index 077a99f5..14ec1041 100644 --- a/bot/exts/evergreen/wolfram.py +++ b/bot/exts/evergreen/wolfram.py @@ -109,7 +109,10 @@ async def get_pod_pages(ctx: Context, bot: commands.Bot, query: str) -> Optional "input": query, "appid": APPID, "output": DEFAULT_OUTPUT_FORMAT, - "format": "image,plaintext" + "format": "image,plaintext", + "location": "the moon", + "latlong": "0.0,0.0", + "ip": "1.1.1.1" }) request_url = QUERY.format(request="query", data=url_str) @@ -169,6 +172,9 @@ class Wolfram(Cog): url_str = parse.urlencode({ "i": query, "appid": APPID, + "location": "the moon", + "latlong": "0.0,0.0", + "ip": "1.1.1.1" }) query = QUERY.format(request="simple", data=url_str) @@ -249,6 +255,9 @@ class Wolfram(Cog): url_str = parse.urlencode({ "i": query, "appid": APPID, + "location": "the moon", + "latlong": "0.0,0.0", + "ip": "1.1.1.1" }) query = QUERY.format(request="result", data=url_str) diff --git a/bot/exts/evergreen/xkcd.py b/bot/exts/evergreen/xkcd.py index d3224bfe..1ff98ca2 100644 --- a/bot/exts/evergreen/xkcd.py +++ b/bot/exts/evergreen/xkcd.py @@ -69,6 +69,8 @@ class XKCD(Cog): return embed.title = f"XKCD comic #{info['num']}" + embed.description = info['alt'] + embed.url = f"{BASE_URL}/{info['num']}" if info["img"][-3:] in ("jpg", "png", "gif"): embed.set_image(url=info["img"]) diff --git a/bot/exts/halloween/hacktoberstats.py b/bot/exts/halloween/hacktoberstats.py index a1c55922..d9fc0e8a 100644 --- a/bot/exts/halloween/hacktoberstats.py +++ b/bot/exts/halloween/hacktoberstats.py @@ -11,7 +11,7 @@ from async_rediscache import RedisCache from discord.ext import commands from bot.constants import Channels, Month, NEGATIVE_REPLIES, Tokens, WHITELISTED_CHANNELS -from bot.utils.decorators import in_month, override_in_channel +from bot.utils.decorators import in_month, whitelist_override log = logging.getLogger(__name__) @@ -44,7 +44,7 @@ class HacktoberStats(commands.Cog): @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER) @commands.group(name="hacktoberstats", aliases=("hackstats",), invoke_without_command=True) - @override_in_channel(HACKTOBER_WHITELIST) + @whitelist_override(channels=HACKTOBER_WHITELIST) async def hacktoberstats_group(self, ctx: commands.Context, github_username: str = None) -> None: """ Display an embed for a user's Hacktoberfest contributions. @@ -72,7 +72,7 @@ class HacktoberStats(commands.Cog): @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER) @hacktoberstats_group.command(name="link") - @override_in_channel(HACKTOBER_WHITELIST) + @whitelist_override(channels=HACKTOBER_WHITELIST) async def link_user(self, ctx: commands.Context, github_username: str = None) -> None: """ Link the invoking user's Github github_username to their Discord ID. @@ -96,7 +96,7 @@ class HacktoberStats(commands.Cog): @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER) @hacktoberstats_group.command(name="unlink") - @override_in_channel(HACKTOBER_WHITELIST) + @whitelist_override(channels=HACKTOBER_WHITELIST) async def unlink_user(self, ctx: commands.Context) -> None: """Remove the invoking user's account link from the log.""" author_id, author_mention = self._author_mention_from_context(ctx) diff --git a/bot/exts/utils/extensions.py b/bot/exts/utils/extensions.py index bb22c353..64e404d2 100644 --- a/bot/exts/utils/extensions.py +++ b/bot/exts/utils/extensions.py @@ -11,7 +11,7 @@ from bot import exts 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 +from bot.utils.extensions import EXTENSIONS, invoke_help_command, unqualify from bot.utils.pagination import LinePaginator log = logging.getLogger(__name__) @@ -77,7 +77,7 @@ class Extensions(commands.Cog): @group(name="extensions", aliases=("ext", "exts", "c", "cogs"), invoke_without_command=True) async def extensions_group(self, ctx: Context) -> None: """Load, unload, reload, and list loaded extensions.""" - await ctx.send_help(ctx.command) + await invoke_help_command(ctx) @extensions_group.command(name="load", aliases=("l",)) async def load_command(self, ctx: Context, *extensions: Extension) -> None: @@ -87,7 +87,7 @@ class Extensions(commands.Cog): If '\*' or '\*\*' is given as the name, all unloaded extensions will be loaded. """ # noqa: W605 if not extensions: - await ctx.send_help(ctx.command) + await invoke_help_command(ctx) return if "*" in extensions or "**" in extensions: @@ -104,7 +104,7 @@ class Extensions(commands.Cog): If '\*' or '\*\*' is given as the name, all loaded extensions will be unloaded. """ # noqa: W605 if not extensions: - await ctx.send_help(ctx.command) + await invoke_help_command(ctx) return blacklisted = "\n".join(UNLOAD_BLACKLIST & set(extensions)) @@ -130,7 +130,7 @@ class Extensions(commands.Cog): If '\*\*' is given as the name, all extensions, including unloaded ones, will be reloaded. """ # noqa: W605 if not extensions: - await ctx.send_help(ctx.command) + await invoke_help_command(ctx) return if "**" in extensions: diff --git a/bot/exts/valentines/be_my_valentine.py b/bot/exts/valentines/be_my_valentine.py index 4db4d191..09591cf8 100644 --- a/bot/exts/valentines/be_my_valentine.py +++ b/bot/exts/valentines/be_my_valentine.py @@ -2,14 +2,15 @@ import logging import random from json import load from pathlib import Path -from typing import Optional, Tuple +from typing import Tuple import discord from discord.ext import commands from discord.ext.commands.cooldowns import BucketType -from bot.constants import Channels, Client, Colours, Lovefest, Month +from bot.constants import Channels, Colours, Lovefest, Month from bot.utils.decorators import in_month +from bot.utils.extensions import invoke_help_command log = logging.getLogger(__name__) @@ -43,7 +44,7 @@ class BeMyValentine(commands.Cog): 2) use the command \".lovefest unsub\" to get rid of the lovefest role. """ if not ctx.invoked_subcommand: - await ctx.send_help(ctx.command) + await invoke_help_command(ctx) @lovefest_role.command(name="sub") async def add_role(self, ctx: commands.Context) -> None: @@ -70,44 +71,35 @@ class BeMyValentine(commands.Cog): @commands.cooldown(1, 1800, BucketType.user) @commands.group(name='bemyvalentine', invoke_without_command=True) async def send_valentine( - self, ctx: commands.Context, user: Optional[discord.Member] = None, *, valentine_type: str = None + self, ctx: commands.Context, user: discord.Member, *, valentine_type: str = None ) -> None: """ - Send a valentine to user, if specified, or to a random user with the lovefest role. + Send a valentine to a specified user with the lovefest role. - syntax: .bemyvalentine [user](optional) [p/poem/c/compliment/or you can type your own valentine message] + syntax: .bemyvalentine [user] [p/poem/c/compliment/or you can type your own valentine message] (optional) - example: .bemyvalentine (sends valentine as a poem or a compliment to a random user) example: .bemyvalentine Iceman#6508 p (sends a poem to Iceman) example: .bemyvalentine Iceman Hey I love you, wanna hang around ? (sends the custom message to Iceman) NOTE : AVOID TAGGING THE USER MOST OF THE TIMES.JUST TRIM THE '@' when using this command. """ if ctx.guild is None: # This command should only be used in the server - msg = "You are supposed to use this command in the server." - return await ctx.send(msg) + raise commands.UserInputError("You are supposed to use this command in the server.") - if user: - if Lovefest.role_id not in [role.id for role in user.roles]: - message = f"You cannot send a valentine to {user} as he/she does not have the lovefest role!" - return await ctx.send(message) + if Lovefest.role_id not in [role.id for role in user.roles]: + raise commands.UserInputError( + f"You cannot send a valentine to {user} as they do not have the lovefest role!" + ) if user == ctx.author: # Well a user can't valentine himself/herself. - return await ctx.send("Come on dude, you can't send a valentine to yourself :expressionless:") + raise commands.UserInputError("Come on, you can't send a valentine to yourself :expressionless:") 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.community_bot_commands) valentine, title = self.valentine_check(valentine_type) - if user is None: - author = ctx.author - user = self.random_user(author, lovefest_role.members) - if user is None: - return await ctx.send("There are no users avilable to whome your valentine can be sent.") - embed = discord.Embed( title=f'{emoji_1} {title} {user.display_name} {emoji_2}', description=f'{valentine} \n **{emoji_2}From {ctx.author}{emoji_1}**', @@ -118,56 +110,41 @@ class BeMyValentine(commands.Cog): @commands.cooldown(1, 1800, BucketType.user) @send_valentine.command(name='secret') async def anonymous( - self, ctx: commands.Context, user: Optional[discord.Member] = None, *, valentine_type: str = None + self, ctx: commands.Context, user: discord.Member, *, valentine_type: str = None ) -> None: """ - Send an anonymous Valentine via DM to to a user, if specified, or to a random with the lovefest role. - - **This command should be DMed to the bot.** + Send an anonymous Valentine via DM to to a specified user with the lovefest role. - syntax : .bemyvalentine secret [user](optional) [p/poem/c/compliment/or you can type your own valentine message] + syntax : .bemyvalentine secret [user] [p/poem/c/compliment/or you can type your own valentine message] (optional) - example : .bemyvalentine secret (sends valentine as a poem or a compliment to a random user in DM making you - anonymous) example : .bemyvalentine secret Iceman#6508 p (sends a poem to Iceman in DM making you anonymous) example : .bemyvalentine secret Iceman#6508 Hey I love you, wanna hang around ? (sends the custom message to Iceman in DM making you anonymous) """ - if ctx.guild is not None: - # This command is only DM specific - msg = "You are not supposed to use this command in the server, DM the command to the bot." - return await ctx.send(msg) - - if user: - if Lovefest.role_id not in [role.id for role in user.roles]: - message = f"You cannot send a valentine to {user} as he/she does not have the lovefest role!" - return await ctx.send(message) + if Lovefest.role_id not in [role.id for role in user.roles]: + await ctx.message.delete() + raise commands.UserInputError( + f"You cannot send a valentine to {user} as they do not have the lovefest role!" + ) if user == ctx.author: # Well a user cant valentine himself/herself. - return await ctx.send('Come on dude, you cant send a valentine to yourself :expressionless:') + raise commands.UserInputError("Come on, you can't send a valentine to yourself :expressionless:") - guild = self.bot.get_guild(id=Client.guild) emoji_1, emoji_2 = self.random_emoji() - lovefest_role = discord.utils.get(guild.roles, id=Lovefest.role_id) valentine, title = self.valentine_check(valentine_type) - if user is None: - author = ctx.author - user = self.random_user(author, lovefest_role.members) - if user is None: - return await ctx.send("There are no users avilable to whome your valentine can be sent.") - embed = discord.Embed( title=f'{emoji_1}{title} {user.display_name}{emoji_2}', description=f'{valentine} \n **{emoji_2}From anonymous{emoji_1}**', color=Colours.pink ) + await ctx.message.delete() try: await user.send(embed=embed) except discord.Forbidden: - await ctx.author.send(f"{user} has DMs disabled, so I couldn't send the message. Sorry!") + raise commands.UserInputError(f"{user} has DMs disabled, so I couldn't send the message. Sorry!") else: await ctx.author.send(f"Your message has been sent to {user}") @@ -191,18 +168,6 @@ class BeMyValentine(commands.Cog): return valentine, title @staticmethod - def random_user(author: discord.Member, members: discord.Member) -> None: - """ - Picks a random member from the list provided in `members`. - - The invoking author is ignored. - """ - if author in members: - members.remove(author) - - return random.choice(members) if members else None - - @staticmethod def random_emoji() -> Tuple[str, str]: """Return two random emoji from the module-defined constants.""" emoji_1 = random.choice(HEART_EMOJIS) diff --git a/bot/exts/valentines/lovecalculator.py b/bot/exts/valentines/lovecalculator.py index c75ea6cf..966acc82 100644 --- a/bot/exts/valentines/lovecalculator.py +++ b/bot/exts/valentines/lovecalculator.py @@ -4,15 +4,13 @@ import json import logging import random from pathlib import Path -from typing import Union +from typing import Coroutine, Union import discord from discord import Member from discord.ext import commands from discord.ext.commands import BadArgument, Cog, clean_content -from bot.constants import Roles - log = logging.getLogger(__name__) with Path("bot/resources/valentines/love_matches.json").open(encoding="utf8") as file: @@ -46,14 +44,11 @@ class LoveCalculator(Cog): If you want to use multiple words for one argument, you must include quotes. .love "Zes Vappa" "morning coffee" - - If only one argument is provided, the subject will become one of the helpers at random. """ if whom is None: - staff = ctx.guild.get_role(Roles.helpers).members - whom = random.choice(staff) + whom = ctx.author - def normalize(arg: Union[Member, str]) -> str: + def normalize(arg: Union[Member, str]) -> Coroutine: if isinstance(arg, Member): # If we are given a member, return name#discrim without any extra changes arg = str(arg) diff --git a/bot/resources/evergreen/py_topics.yaml b/bot/resources/evergreen/py_topics.yaml index 1e53429a..f3b2eaa3 100644 --- a/bot/resources/evergreen/py_topics.yaml +++ b/bot/resources/evergreen/py_topics.yaml @@ -3,8 +3,6 @@ # python-general 267624335836053506: - What's your favorite PEP? - - What's your current text editor/IDE, and what functionality do you like about it the most when programming in Python? - - What functionality is your text editor/IDE missing for programming Python? - What parts of your life has Python automated, if any? - Which Python project are you the most proud of making? - What made you want to learn Python? @@ -16,23 +14,34 @@ - What feature do you think should be added to Python? - Has Python helped you in school? If so, how? - What was the first thing you created with Python? + - What is your favorite Python package? + - What standard library module is really underrated? + - Have you published any packages on PyPi? If so, what are they? + - What are you currently working on in Python? + - What's your favorite script and how has it helped you in day to day activities? + - When you were first learning, what is something that stumped you? + - When you were first learning, what is a resource you wish you had? + - What is something you know now, that you wish you knew when starting out? + - What is something simple that you still error on today? + +# algos-and-data-structs +650401909852864553: + - # async 630504881542791169: - Are there any frameworks you wish were async? - How have coroutines changed the way you write Python? + - What is your favorite async library? # c-extensions 728390945384431688: - -# computer-science -650401909852864553: - - - # databases 342318764227821568: - Where do you get your best data? + - What is your preferred database and for what use? # data-science 366673247892275221: @@ -45,11 +54,18 @@ - What feature would you be the most interested in making? - What feature would you like to see added to the library? what feature in the library do you think is redundant? - Do you think there's a way in which Discord could handle bots better? + - What's one feature you wish more developers had in their bots? + +# editors-ides +813178633006350366: + - What's your current text editor/IDE, and what functionality do you like about it the most when programming in Python? + - What functionality is your text editor/IDE missing for programming Python? # esoteric-python 470884583684964352: - What's a common part of programming we can make harder? - What are the pros and cons of messing with __magic__()? + - What's your favorite Python hack? # game-development 660625198390837248: @@ -57,7 +73,7 @@ # microcontrollers 545603026732318730: - - + - What is your favorite version of the Raspberry Pi? # networking 716325106619777044: @@ -67,23 +83,40 @@ 366674035876167691: - If you could wish for a library involving net-sec, what would it be? -# software-testing -463035728335732738: +# software-design +782713858615017503: - # tools-and-devops 463035462760792066: - What editor would you recommend to a beginner? Why? - What editor would you recommend to be the most efficient? Why? + - How often do you use GitHub Actions and workflows to automate your repositories? + - What's your favorite app on GitHub? + +# unit-testing +463035728335732738: + - # unix 491523972836360192: - - + - What's your favorite Bash command? + - What's your most used Bash command? + - How often do you update your Unix machine? + - How often do you upgrade on production? # user-interfaces 338993628049571840: - What's the most impressive Desktop Application you've made with Python so far? + - Have you ever made your own GUI? If so, how? + - Do you perfer Command Line Interfaces (CLI) or Graphic User Interfaces (GUI)? + - What's your favorite CLI (Command Line Interface) or TUI (Terminal Line Interface)? + - What's your best GUI project? # web-development 366673702533988363: - How has Python helped you in web development? + - What tools do you use for web development? + - What is your favorite API library? + - What do you use for your frontend? + - What does your stack look like? diff --git a/bot/resources/evergreen/python_facts.txt b/bot/resources/evergreen/python_facts.txt new file mode 100644 index 00000000..0abd971b --- /dev/null +++ b/bot/resources/evergreen/python_facts.txt @@ -0,0 +1,3 @@ +Python was named after Monty Python, a British Comedy Troupe, which Guido van Rossum likes. +If you type `import this` in the Python REPL, you'll get a poem about the philosophies about Python. (check it out by doing !zen in <#267659945086812160>) +If you type `import antigravity` in the Python REPL, you'll be directed to an [xkcd comic](https://xkcd.com/353/) about how easy Python is. diff --git a/bot/resources/evergreen/starter.yaml b/bot/resources/evergreen/starter.yaml index 53c89364..949220f9 100644 --- a/bot/resources/evergreen/starter.yaml +++ b/bot/resources/evergreen/starter.yaml @@ -20,3 +20,14 @@ - If you had $100 bill in your Easter Basket, what would you do with it? - What would you do if you know you could succeed at anything you chose to do? - If you could take only three things from your house, what would they be? +- What's the best pastry? +- What's your favourite kind of soup? +- What is the most useless talent that you have? +- Would you rather fight 100 duck sized horses or one horse sized duck? +- What is your favourite color? +- What's your favourite type of weather? +- Tea or coffee? What about milk? +- Do you speak a language other than English? +- What is your favorite TV show? +- What is your favorite media genre? +- How many years have you spent coding? diff --git a/bot/resources/evergreen/trivia_quiz.json b/bot/resources/evergreen/trivia_quiz.json index faa3bc3b..a4225eb1 100644 --- a/bot/resources/evergreen/trivia_quiz.json +++ b/bot/resources/evergreen/trivia_quiz.json @@ -2,36 +2,51 @@ "retro": [ { "id": 1, - "hints": ["It is not a mainline Mario Game, although the plumber is present.", "It is not a mainline Zelda Game, although Link is present."], + "hints": [ + "It is not a mainline Mario Game, although the plumber is present.", + "It is not a mainline Zelda Game, although Link is present." + ], "question": "What is the best selling game on the Nintendo GameCube?", "answer": "Super Smash Bros" }, { "id": 2, - "hints": ["It was released before the 90's.", "It was released after 1980."], + "hints": [ + "It was released before the 90's.", + "It was released after 1980." + ], "question": "What year was Tetris released?", "answer": "1984" }, { "id": 3, - "hints": ["The occupation was in construction", "He appeared as this kind of worker in 1981's Donkey Kong"], + "hints": [ + "The occupation was in construction", + "He appeared as this kind of worker in 1981's Donkey Kong" + ], "question": "What was Mario's original occupation?", "answer": "Carpenter" }, { "id": 4, - "hints": ["It was revealed in the Nintendo Character Guide in 1993.", "His last name has to do with eating Mario's enemies."], + "hints": [ + "It was revealed in the Nintendo Character Guide in 1993.", + "His last name has to do with eating Mario's enemies." + ], "question": "What is Yoshi's (from Mario Bros.) full name?", "answer": "Yoshisaur Munchakoopas" }, { "id": 5, - "hints": ["The game was released in 1990.", "It was released on the SNES."], + "hints": [ + "The game was released in 1990.", + "It was released on the SNES." + ], "question": "What was the first game Yoshi appeared in?", "answer": "Super Mario World" } ], - "general":[ + "general": [ { "id": 100, "question": "Name \"the land of a thousand lakes\"", @@ -114,7 +129,7 @@ "id": 113, "question": "What's the name of the tallest waterfall in the world.", "answer": "Angel Falls", - "info": "Angel Falls (Salto Ángel) in Venezuela is the highest waterfall in the world. The falls are 3230 feet in height, with an uninterrupted drop of 2647 feet. Angel Falls is located on a tributary of the Rio Caroni." + "info": "Angel Falls (Salto \u00c1ngel) in Venezuela is the highest waterfall in the world. The falls are 3230 feet in height, with an uninterrupted drop of 2647 feet. Angel Falls is located on a tributary of the Rio Caroni." }, { "id": 114, @@ -180,7 +195,7 @@ "id": 124, "question": "When did the Second World War end?", "answer": "1945", - "info": "World War 2 ended with the unconditional surrender of the Axis powers. On 8 May 1945, the Allies accepted Germany's surrender, about a week after Adolf Hitler had committed suicide. VE Day – Victory in Europe celebrates the end of the Second World War on 8 May 1945." + "info": "World War 2 ended with the unconditional surrender of the Axis powers. On 8 May 1945, the Allies accepted Germany's surrender, about a week after Adolf Hitler had committed suicide. VE Day \u2013 Victory in Europe celebrates the end of the Second World War on 8 May 1945." }, { "id": 125, @@ -190,72 +205,66 @@ }, { "id": 126, - "question": "What's the name of the largest river in the world?", - "answer": "Nile", - "info": "The Nile, which is about 6,650 km (4,130 mi) long, is an \"international\" river as its drainage basin covers eleven countries, namely, Tanzania, Uganda, Rwanda, Burundi, the Democratic Republic of the Congo, Kenya, Ethiopia, Eritrea, South Sudan, Republic of the Sudan and Egypt." - }, - { - "id": 127, "question": "Which is the smallest planet in the Solar System?", "answer": "Mercury", "info": "Mercury is the smallest planet in our solar system. It's just a little bigger than Earth's moon. It is the closest planet to the sun, but it's actually not the hottest. Venus is hotter." }, { - "id": 128, + "id": 127, "question": "What is the smallest country?", "answer": "Vatican City", "info": "With an area of 0.17 square miles (0.44 km2) and a population right around 1,000, Vatican City is the smallest country in the world, both in terms of size and population." }, { - "id": 129, + "id": 128, "question": "What's the name of the largest bird?", "answer": "Ostrich", "info": "The largest living bird, a member of the Struthioniformes, is the ostrich (Struthio camelus), from the plains of Africa and Arabia. A large male ostrich can reach a height of 2.8 metres (9.2 feet) and weigh over 156 kilograms (344 pounds)." }, { - "id": 130, + "id": 129, "question": "What does the acronym GPRS stand for?", "answer": "General Packet Radio Service", "info": "General Packet Radio Service (GPRS) is a packet-based mobile data service on the global system for mobile communications (GSM) of 3G and 2G cellular communication systems. It is a non-voice, high-speed and useful packet-switching technology intended for GSM networks." }, { - "id": 131, + "id": 130, "question": "In what country is the Ebro river located?", "answer": "Spain", "info": "The Ebro river is located in Spain. It is 930 kilometers long and it's the second longest river that ends on the Mediterranean Sea." }, { - "id": 132, + "id": 131, "question": "What year was the IBM PC model 5150 introduced into the market?", "answer": "1981", "info": "The IBM PC was introduced into the market in 1981. It used the Intel 8088, with a clock speed of 4.77 MHz, along with the MDA and CGA as a video card." }, { - "id": 133, + "id": 132, "question": "What's the world's largest urban area?", "answer": "Tokyo", "info": "Tokyo is the most populated city in the world, with a population of 37 million people. It is located in Japan." }, { - "id": 134, + "id": 133, "question": "How many planets are there in the Solar system?", "answer": "8", "info": "In the Solar system, there are 8 planets: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus and Neptune. Pluto isn't considered a planet in the Solar System anymore." }, { - "id": 135, + "id": 134, "question": "What is the capital of Iraq?", "answer": "Baghdad", "info": "Baghdad is the capital of Iraq. It has a population of 7 million people." }, { - "id": 136, + "id": 135, "question": "The United Nations headquarters is located at which city?", "answer": "New York", "info": "The United Nations is headquartered in New York City in a complex designed by a board of architects led by Wallace Harrison and built by the architectural firm Harrison & Abramovitz. The complex has served as the official headquarters of the United Nations since its completion in 1951." }, { - "id": 137, + "id": 136, "question": "At what year did Christopher Columbus discover America?", "answer": "1492", "info": "The explorer Christopher Columbus made four trips across the Atlantic Ocean from Spain: in 1492, 1493, 1498 and 1502. He was determined to find a direct water route west from Europe to Asia, but he never did. Instead, he stumbled upon the Americas" diff --git a/bot/resources/halloween/spooky_rating.json b/bot/resources/halloween/spooky_rating.json index 533e7107..8e3e66bb 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/sir-lancebot/master/bot/resources/halloween/spookyrating/candle.jpeg" + "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/main/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/sir-lancebot/master/bot/resources/halloween/spookyrating/baby.jpeg" + "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/main/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/sir-lancebot/master/bot/resources/halloween/spookyrating/tiger.jpeg" + "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/main/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/sir-lancebot/master/bot/resources/halloween/spookyrating/costume.jpeg" + "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/main/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/sir-lancebot/master/bot/resources/halloween/spookyrating/ghost.jpeg" + "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/main/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/sir-lancebot/master/bot/resources/halloween/spookyrating/necromancer.jepg" + "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/main/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/sir-lancebot/master/bot/resources/halloween/spookyrating/clown.jpeg" + "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/main/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/sir-lancebot/master/bot/resources/halloween/spookyrating/jackolantern.jpg" + "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/main/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/sir-lancebot/master/bot/resources/halloween/spookyrating/devil.jpeg" + "image": "https://raw.githubusercontent.com/python-discord/sir-lancebot/main/bot/resources/halloween/spookyrating/devil.jpeg" } } diff --git a/bot/utils/decorators.py b/bot/utils/decorators.py index 9cdaad3f..c12a15ff 100644 --- a/bot/utils/decorators.py +++ b/bot/utils/decorators.py @@ -13,6 +13,7 @@ from discord.ext.commands import CheckFailure, Command, Context from bot.constants import ERROR_REPLIES, Month from bot.utils import human_months, resolve_current_month +from bot.utils.checks import in_whitelist_check ONE_DAY = 24 * 60 * 60 @@ -186,82 +187,104 @@ def without_role(*role_ids: int) -> t.Callable: return commands.check(predicate) -def in_channel_check(*channels: int, bypass_roles: t.Container[int] = None) -> t.Callable[[Context], bool]: +def whitelist_check(**default_kwargs: t.Container[int]) -> t.Callable[[Context], bool]: """ - Checks that the message is in a whitelisted channel or optionally has a bypass role. + Checks if a message is sent in a whitelisted context. - If `in_channel_override` is present, check if it contains channels - and use them in place of the global whitelist. + All arguments from `in_whitelist_check` are supported, with the exception of "fail_silently". + If `whitelist_override` is present, it is added to the global whitelist. """ def predicate(ctx: Context) -> bool: + # Skip DM invocations if not ctx.guild: log.debug(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM.") return True - if ctx.channel.id in channels: - log.debug( - f"{ctx.author} tried to call the '{ctx.command.name}' command " - f"and the command was used in a whitelisted channel." - ) - return True - if bypass_roles and any(r.id in bypass_roles for r in ctx.author.roles): - log.debug( - f"{ctx.author} called the '{ctx.command.name}' command and " - f"had a role to bypass the in_channel check." - ) - return True + kwargs = default_kwargs.copy() - if hasattr(ctx.command.callback, "in_channel_override"): - override = ctx.command.callback.in_channel_override - if override is None: + # Update kwargs based on override + if hasattr(ctx.command.callback, "override"): + # Remove default kwargs if reset is True + if ctx.command.callback.override_reset: + kwargs = {} log.debug( - f"{ctx.author} called the '{ctx.command.name}' command " - f"and the command was whitelisted to bypass the in_channel check." + f"{ctx.author} called the '{ctx.command.name}' command and " + f"overrode default checks." ) - return True - else: - if ctx.channel.id in override: - log.debug( - f"{ctx.author} tried to call the '{ctx.command.name}' command " - f"and the command was used in an overridden whitelisted channel." - ) - return True - log.debug( - f"{ctx.author} tried to call the '{ctx.command.name}' command. " - f"The overridden in_channel check failed." - ) - channels_str = ', '.join(f"<#{c_id}>" for c_id in override) - raise InChannelCheckFailure( - f"Sorry, but you may only use this command within {channels_str}." - ) + # Merge overwrites and defaults + for arg in ctx.command.callback.override: + default_value = kwargs.get(arg) + new_value = ctx.command.callback.override[arg] + + # Skip values that don't need merging, or can't be merged + if default_value is None or isinstance(arg, int): + kwargs[arg] = new_value + + # Merge containers + elif isinstance(default_value, t.Container): + if isinstance(new_value, t.Container): + kwargs[arg] = (*default_value, *new_value) + else: + kwargs[arg] = new_value + + log.debug( + f"Updated default check arguments for '{ctx.command.name}' " + f"invoked by {ctx.author}." + ) + + log.trace(f"Calling whitelist check for {ctx.author} for command {ctx.command.name}.") + result = in_whitelist_check(ctx, fail_silently=True, **kwargs) + + # Return if check passed + if result: + log.debug( + f"{ctx.author} tried to call the '{ctx.command.name}' command " + f"and the command was used in an overridden context." + ) + return result log.debug( f"{ctx.author} tried to call the '{ctx.command.name}' command. " - f"The in_channel check failed." + f"The whitelist check failed." ) - channels_str = ', '.join(f"<#{c_id}>" for c_id in channels) - raise InChannelCheckFailure( - f"Sorry, but you may only use this command within {channels_str}." - ) + # Raise error if the check did not pass + channels = set(kwargs.get("channels") or {}) + categories = kwargs.get("categories") - return predicate + # Add all whitelisted category channels + if categories: + for category_id in categories: + category = ctx.guild.get_channel(category_id) + if category is None: + continue + [channels.add(channel.id) for channel in category.text_channels] -in_channel = commands.check(in_channel_check) + if channels: + channels_str = ', '.join(f"<#{c_id}>" for c_id in channels) + message = f"Sorry, but you may only use this command within {channels_str}." + else: + message = "Sorry, but you may not use this command." + + raise InChannelCheckFailure(message) + + return predicate -def override_in_channel(channels: t.Tuple[int] = None) -> t.Callable: +def whitelist_override(bypass_defaults: bool = False, **kwargs: t.Container[int]) -> t.Callable: """ - Set command callback attribute for detection in `in_channel_check`. + Override global whitelist context, with the kwargs specified. - Override global whitelist if channels are specified. + All arguments from `in_whitelist_check` are supported, with the exception of `fail_silently`. + Set `bypass_defaults` to True if you want to completely bypass global checks. This decorator has to go before (below) below the `command` decorator. """ def inner(func: t.Callable) -> t.Callable: - func.in_channel_override = channels + func.override = kwargs + func.override_reset = bypass_defaults return func return inner diff --git a/bot/utils/extensions.py b/bot/utils/extensions.py index 50350ea8..459588a1 100644 --- a/bot/utils/extensions.py +++ b/bot/utils/extensions.py @@ -3,6 +3,8 @@ import inspect import pkgutil from typing import Iterator, NoReturn +from discord.ext.commands import Context + from bot import exts @@ -31,4 +33,12 @@ def walk_extensions() -> Iterator[str]: yield module.name +async def invoke_help_command(ctx: Context) -> None: + """Invoke the help command or default help command if help extensions is not loaded.""" + if 'bot.exts.evergreen.help' in ctx.bot.extensions: + help_command = ctx.bot.get_command('help') + await ctx.invoke(help_command, ctx.command.qualified_name) + return + await ctx.send_help(ctx.command) + EXTENSIONS = frozenset(walk_extensions()) diff --git a/bot/utils/time.py b/bot/utils/time.py new file mode 100644 index 00000000..fbf2fd21 --- /dev/null +++ b/bot/utils/time.py @@ -0,0 +1,84 @@ +import datetime + +from dateutil.relativedelta import relativedelta + + +# All these functions are from https://github.com/python-discord/bot/blob/main/bot/utils/time.py +def _stringify_time_unit(value: int, unit: str) -> str: + """ + Returns a string to represent a value and time unit, ensuring that it uses the right plural form of the unit. + + >>> _stringify_time_unit(1, "seconds") + "1 second" + >>> _stringify_time_unit(24, "hours") + "24 hours" + >>> _stringify_time_unit(0, "minutes") + "less than a minute" + """ + if unit == "seconds" and value == 0: + return "0 seconds" + elif value == 1: + return f"{value} {unit[:-1]}" + elif value == 0: + return f"less than a {unit[:-1]}" + else: + return f"{value} {unit}" + + +def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units: int = 6) -> str: + """ + Returns a human-readable version of the relativedelta. + + precision specifies the smallest unit of time to include (e.g. "seconds", "minutes"). + max_units specifies the maximum number of units of time to include (e.g. 1 may include days but not hours). + """ + if max_units <= 0: + raise ValueError("max_units must be positive") + + units = ( + ("years", delta.years), + ("months", delta.months), + ("days", delta.days), + ("hours", delta.hours), + ("minutes", delta.minutes), + ("seconds", delta.seconds), + ) + + # Add the time units that are >0, but stop at accuracy or max_units. + time_strings = [] + unit_count = 0 + for unit, value in units: + if value: + time_strings.append(_stringify_time_unit(value, unit)) + unit_count += 1 + + if unit == precision or unit_count >= max_units: + break + + # Add the 'and' between the last two units, if necessary + if len(time_strings) > 1: + time_strings[-1] = f"{time_strings[-2]} and {time_strings[-1]}" + del time_strings[-2] + + # If nothing has been found, just make the value 0 precision, e.g. `0 days`. + if not time_strings: + humanized = _stringify_time_unit(0, precision) + else: + humanized = ", ".join(time_strings) + + return humanized + + +def time_since(past_datetime: datetime.datetime, precision: str = "seconds", max_units: int = 6) -> str: + """ + Takes a datetime and returns a human-readable string that describes how long ago that datetime was. + + precision specifies the smallest unit of time to include (e.g. "seconds", "minutes"). + max_units specifies the maximum number of units of time to include (e.g. 1 may include days but not hours). + """ + now = datetime.datetime.utcnow() + delta = abs(relativedelta(now, past_datetime)) + + humanized = humanize_delta(delta, precision, max_units) + + return f"{humanized} ago" diff --git a/docker-compose.yml b/docker-compose.yml index bb6ad6ac..a18534a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,13 +12,9 @@ services: - redis environment: - - BOT_TOKEN - - BOT_DEBUG - - BOT_GUILD - - BOT_ADMIN_ROLE_ID - - CHANNEL_DEVLOG - - CHANNEL_COMMUNITY_BOT_COMMANDS - REDIS_HOST=redis + env_file: + - .env volumes: - .:/bot |