├── .deepsource.toml ├── .flake8 ├── .github └── workflows │ └── codeql.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode ├── settings.json └── why.code-snippets ├── COC.md ├── Dockerfile ├── LICENCE ├── Makefile ├── PRIVACY.md ├── README.md ├── assets ├── fonts │ ├── Monocraft.otf │ └── Some_Time_Later.otf ├── images │ ├── heads.png │ ├── logo.png │ ├── selfhosting │ │ ├── advanced.jpg │ │ ├── bug_alert.jpg │ │ ├── devmode.jpg │ │ ├── dm1.jpg │ │ ├── dm2.jpg │ │ ├── dm3.jpg │ │ ├── join_alert.jpg │ │ ├── leave_alert.jpg │ │ ├── online_alert.jpg │ │ ├── rightclickid.jpg │ │ ├── suggestion_alert.jpg │ │ └── user_settings.jpg │ ├── spongebob │ │ ├── bamboo.png │ │ ├── blueseaweed.png │ │ ├── crosshatch.png │ │ ├── flowers.png │ │ ├── greenseaweed.jpg │ │ ├── heads.png │ │ ├── purplewood.jpg │ │ ├── sand.png │ │ ├── seaweed.png │ │ ├── steel.png │ │ ├── tiles.png │ │ ├── title.png │ │ └── wood.png │ └── tails.png ├── json_files │ └── fun_text.json └── videos │ └── crab.mp4 ├── docs └── SELFHOSTING.md ├── src ├── README.md ├── cogs │ ├── audio │ │ └── music.py │ ├── events │ │ ├── alert.py │ │ ├── on_error.py │ │ ├── on_event.py │ │ └── on_guild.py │ ├── fun │ │ ├── convert.py │ │ ├── fun.py │ │ ├── poll.py │ │ └── rand.py │ ├── games │ │ ├── counting.py │ │ ├── rps.py │ │ └── tictactoe.py │ ├── image │ │ ├── animals.py │ │ ├── colors.py │ │ └── other.py │ ├── leveling │ │ └── leveling.py │ ├── moderation │ │ ├── banning.py │ │ ├── channel.py │ │ └── tickets.py │ ├── owner │ │ ├── blacklist.py │ │ ├── cog_tools.py │ │ ├── dmreply.py │ │ ├── errors.py │ │ ├── ipc.py │ │ └── server.py │ ├── programming │ │ ├── runcode.py │ │ └── whybotdev.py │ ├── roles │ │ └── roles.py │ └── utilities │ │ ├── info.py │ │ ├── tags.py │ │ └── utilities.py ├── config.example.yaml ├── core │ ├── __init__.py │ ├── db │ │ ├── __init__.py │ │ ├── create_tables.py │ │ └── setup_guild.py │ ├── helpers │ │ ├── __init__.py │ │ ├── checks.py │ │ ├── client_functions.py │ │ ├── exception.py │ │ ├── http.py │ │ ├── log.py │ │ ├── music.py │ │ ├── views.py │ │ └── why_leveling.py │ ├── models │ │ ├── __init__.py │ │ ├── client.py │ │ ├── counting.py │ │ ├── level.py │ │ ├── rps.py │ │ ├── tag.py │ │ ├── ticket.py │ │ └── ttt.py │ └── utils │ │ ├── __init__.py │ │ ├── asyncpg_context.py │ │ ├── calc.py │ │ ├── count_lines.py │ │ ├── formatters.py │ │ └── other.py ├── main.py ├── requirements.txt ├── scripts │ ├── backup.sh │ └── venv.sh └── setup.py └── tests ├── async_redis.py ├── c_plus_py.py ├── main.c └── rankimage_test.py /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "python" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | runtime_version = "3.x.x" -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, W503, E266, F722, F821, E999 3 | max-line-length=120 4 | max-complexity=39 -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | paths: 6 | - "src/**.py" 7 | branches: ["master", "rewrite-the-rewrite"] 8 | pull_request: 9 | branches: ["master", "rewrite-the-rewrite" ] 10 | schedule: 11 | - cron: '38 17 * * 5' 12 | 13 | jobs: 14 | analyze: 15 | name: Analyze 16 | runs-on: ubuntu-latest 17 | permissions: 18 | actions: read 19 | contents: read 20 | security-events: write 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | language: [ 'python' ] 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v3 30 | 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v2 33 | with: 34 | languages: ${{ matrix.language }} 35 | 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v2 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v2 41 | with: 42 | category: "/language:${{matrix.language}}" 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | *.DS_Store 3 | *.env 4 | *.pyc 5 | *__pycache__/ 6 | *venv/ 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | share/python-wheels/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | MANIFEST 24 | .cache 25 | .env 26 | config.yaml 27 | *.log 28 | *.so 29 | *.mypy_cache 30 | *logfiles/ 31 | *testing_cog.py 32 | src/scripts/backups 33 | src/scripts/b.sh -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 22.3.0 4 | hooks: 5 | - id: black 6 | args: [--safe] 7 | - repo: https://github.com/pycqa/flake8 8 | rev: 6.0.0 9 | hooks: 10 | - id: flake8 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.typeCheckingMode": "off" 3 | } -------------------------------------------------------------------------------- /.vscode/why.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "newcog": { 3 | "prefix": "newcog", 4 | "body": [ 5 | "import discord", 6 | "from discord.ext import commands\n", 7 | "from core.models import WhyBot", 8 | "from core.helpers.checks import run_bot_checks\n", 9 | "class $1(commands.Cog):", 10 | "\tdef __init__(self, client: WhyBot):", 11 | "\t\tself.client = client", 12 | "\t\tself.cog_check = run_bot_checks\n\n\n", 13 | "def setup(client):", 14 | "\tclient.add_cog($1(client))" 15 | ], 16 | "description": "Creates a Cog" 17 | }, 18 | "slash": { 19 | "prefix": "slash", 20 | "body": [ 21 | "@commands.slash_command()", 22 | "async def $1(self, ctx: discord.ApplicationContext):", 23 | "\tpass" 24 | ], 25 | "description": "Create a new slash command" 26 | } 27 | } -------------------------------------------------------------------------------- /COC.md: -------------------------------------------------------------------------------- 1 | 2 | # Collaborator's Code of Conduct 3 | 4 | ## Our commitment 5 | 6 | In the interest of promoting an open and welcoming environment, we 7 | contributors and maintenance workers we are committed to making participation in our 8 | project and our community an experience free from harassment for 9 | all, regardless of age, build, disability, ethnicity, 10 | sexual characteristics, identity and gender expression, level of 11 | experience, education, socio-economic status, nationality, appearance, race, 12 | religion or identity and sexual orientation. 13 | 14 | ## Our Standards 15 | 16 | Examples of behaviors that contribute to creating an environment 17 | positive: 18 | 19 | * Use of a welcoming and inclusive language 20 | * Respect different points of view and experiences 21 | * Accept constructive criticism gracefully 22 | * Focus on what's best for the community 23 | * Show empathy towards other community members 24 | 25 | Examples of unacceptable behavior of participants: 26 | 27 | * The use of language or sexualized images and sexual attention or 28 | unwanted advances 29 | * Troll behavior, offensive comments and personal or political attacks 30 | * Harassment in public or private 31 | * Publication of other people's private information, such as a postal address or 32 | electronic, without explicit authorization 33 | * Other behaviors that could reasonably be considered 34 | inappropriate in a professional context 35 | 36 | ## Our responsibilities 37 | 38 | Project maintainers are responsible for clarifying the standards of 39 | acceptable behavior and are required to take corrective action 40 | appropriate and fair in response to any case of unacceptable behavior. 41 | 42 | Project maintainers have the right and responsibility to remove, 43 | modify or reject comments, commits, code, changes to wikis, issuee and others 44 | contributions not aligned with this Code of Conduct, or to exclude, 45 | temporarily or permanently, any contributor for 46 | behaviors deemed inappropriate, threatening, offensive or harmful. 47 | 48 | ## Purpose 49 | 50 | This Code of Conduct applies both within the spaces of the 51 | project that in public spaces when an individual represents the project 52 | or its community. Examples of representation of a project or community 53 | include the use of an official project email address, publication 54 | through an official account through social media or the function of 55 | designated representative at an online or offline event. The representation of a 56 | project can be further defined and clarified by the project maintainers. 57 | 58 | ## Application 59 | 60 | Cases of abusive, harassing or otherwise unacceptable behavior can 61 | be presented by contacting the project leader at 62 | `whybot@fusionsid.xyz` or by DMing `FusionSid#3645`. All complaints will be reviewed and investigated and give 63 | place of an answer deemed necessary and appropriate to the circumstances. The 64 | project team is obliged to maintain confidentiality regarding 65 | the person who reported the case. 66 | Further details on specific implementation policies can be 67 | published separately. 68 | 69 | Project maintainers who do not respect or apply the Code of 70 | Behavior in good faith may suffer temporary repercussions or 71 | permanent as determined by other members at the helm of the project. 72 | 73 | ## Attribution 74 | 75 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 76 | version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 77 | 78 | [ homepage ]: https://www.contributor-covenant.org 79 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim 2 | 3 | # install packages 4 | RUN apt-get update && apt-get install -y --no-install-recommends \ 5 | git unzip wget fontconfig imagemagick fonts-symbola \ 6 | libmagick++-dev gcc ffmpeg python3-dev && rm -rf /var/lib/apt/lists/* 7 | 8 | # Set work dir as root 9 | WORKDIR / 10 | 11 | # copy all the files from project to container 12 | COPY ./ ./ 13 | 14 | # set work dir as src kinda like cd-ing into it and staying there 15 | WORKDIR /src 16 | 17 | # Configure imagemagick settings 18 | RUN sed -i '/ /etc/ImageMagick-6/policy.xml 20 | RUN sed -i_bak 's/rights="none" pattern="PDF"/rights="read | write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml 21 | 22 | # install verdana font for crab command 23 | RUN wget --progress=dot:giga "https://www.dafontfree.io/download/verdana/?wpdmdl=71901&refresh=6362eb8bd1a9e1667427211&ind=1612703173429&filename=verdana-font-family.zip" -O font.zip 24 | RUN unzip font.zip -d font 25 | RUN cp -r font/* /usr/local/share/fonts 26 | 27 | # reload font cache 28 | RUN fc-cache -f -v 29 | 30 | # Install required packages from requirements.txt 31 | RUN pip install -r requirements.txt --no-cache-dir 32 | 33 | # run the bot :) 34 | CMD ["python3", "main.py"] -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Siddhesh Zantye (FusionSid) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | format: 2 | @python3 -m black ./ 3 | 4 | clean: 5 | @find . | grep -E '(__pycache__|\.pyc|\.pyo$|\.DS_Store|\.mypy_cache)' | xargs rm -rf 6 | @clear 7 | 8 | run: 9 | @python3 src/main.py -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | The use of this application ("Bot") in a server requires the collection of some specific user data ("Data"). The Data collected includes, but is not limited to Discord User username values, Discord User ID values, Per command usage statistics, and Discord Guild ID/Name values. 4 | Use of the Bot is considered an agreement to the terms of this Policy. 5 | 6 | ## Access to Data 7 | 8 | Access to Data is only permitted to Bot's developers, and only in the scope required for the development, testing, and implementation of features for the Bot. Data is not sold, provided to, or shared with any third party, except when/where required by law or a Terms of Service agreement. You can view the data upon request to [`@FusionSid`](#contact). 9 | 10 | ## Storage of Data 11 | 12 | Data is stored in a PostgreSQL database, User data is also cached in Redis databases. The database is secured to prevent external access, however, no guarantee is provided and the Bot owners assume no liability for the unintentional or malicious breach of Data. In the event of unauthorized Data access, users will be notified through the Discord client application. 13 | 14 | ## User Rights 15 | 16 | At any time, you have the right to request to view the Data pertaining to your Discord account. You may submit a request through the [Discord Server](https://discord.gg/ryEmgnpKND). You have the right to request the removal of relevant Data. 17 | 18 | ## Underage Users 19 | 20 | The use of the Bot is not permitted for minors under the age of 13, or under the age of legal consent in their country. This is in compliance with the [Discord Terms of Service](https://discord.com/terms). No information will be knowingly stored from an underage user. If it is found out that a user is underage we will take all necessary actions to delete the stored data. 21 | 22 | ## Questions 23 | 24 | If you have any questions or are concerned about what data might be being stored from your account contact [`@FusionSid`](#contact). For more information check the [Discord Terms Of Service](https://discord.com/terms). 25 | 26 | ## Contact 27 | 28 | **Email:** `whybot@fusionsid.xyz` 29 | **Discord:** `FusionSid#3645` 30 | 31 | Alternatively, you can also contact the owner by DMing the discord application 32 | 33 | ## Attribution 34 | 35 | This privacy policy was adapted from the freeCodeCamp 100 Days of Code Bot and can be found here: https://github.com/freeCodeCamp/100DaysOfCode-discord-bot/blob/main/PRIVACY.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![Contributors][contributors-shield]][contributors-url] 4 | [![Forks][forks-shield]][forks-url] 5 | [![Issues][issues-shield]][issues-url] 6 | [![Stargazers][stars-shield]][stars-url] 7 | [![MIT License][license-shield]][license-url] 8 | [![CodeFactor](https://img.shields.io/codefactor/grade/github/FusionSid/Why-Bot?style=for-the-badge)](https://www.codefactor.io/repository/github/fusionsid/why-bot) 9 | 10 |
11 |
12 | 13 | Logo 14 | 15 | 18 | 19 | ---- 20 | 21 |

Why Bot

22 | 23 |

24 |

An open source, multi-purpose discord bot made to enhance your discord experience

25 | [Add to your server] 26 |
27 |
28 | Discord Server 29 | · 30 | Report Bug 31 |

32 |
33 | 34 | --- 35 | 36 |
37 |
38 | Table of Contents 39 |
    40 |
  1. About The Project
  2. 41 |
  3. Built With
  4. 42 |
  5. Contributing
  6. 43 |
  7. License
  8. 44 |
  9. Contact
  10. 45 |
  11. Acknowledgments
  12. 46 |
  13. Commands
  14. 47 |
48 |
49 |
50 | 51 | 52 | ## About The Project 53 | 54 | 55 | 56 |

(back to top)

57 | 58 | 59 | 60 | ## Built With 61 | 62 | 63 | 64 |

(back to top)

65 | 66 | 67 | ## Contributing 68 | 69 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 70 | 71 | If you have a suggestion that would make this better, please fork the repo and create a pull request. 72 | 73 | Don't forget to give the project a star :) 74 | 75 | 1. Fork the Project 76 | 2. Create your Feature Branch (`git checkout -b newfeature`) 77 | 3. Commit your Changes (`git commit -m 'feat: Add newfeature'`) Please try to follow [conventional commit names](https://www.conventionalcommits.org/en/v1.0.0/) 78 | 4. Push to the Branch (`git push origin newfeature`) 79 | 5. Open a Pull Request 80 | 81 |

(back to top)

82 | 83 | ## License 84 | 85 | Distributed under the MIT License. See [`LICENSE`](/LICENCE) for more information. 86 | 87 |

(back to top)

88 | 89 | 90 | ## Contact 91 | 92 | Siddhesh Zantye - whybot@fusionsid.xyz 93 | 94 | Discord Account: [`FusionSid#3645`](https://discordapp.com/users/624076054969188363) 95 | 96 | Join the Why [Discord Server](https://discord.gg/ryEmgnpKND) For help. 97 | 98 | You can also directly DM the bot to contact the Why Bot devs 99 |

(back to top)

100 | 101 | 102 | ## Acknowledgments 103 | 104 | Even though I take take credit for most of the code, I would like to thank everyone who helped with this bot or any other open source projects which I used code from. 105 | 106 |

(back to top)

107 | 108 | ## Commands: 109 | 110 | 111 | 112 | [contributors-shield]: https://img.shields.io/github/contributors/FusionSid/Why-Bot.svg?style=for-the-badge 113 | [contributors-url]: https://github.com/FusionSid/Why-Bot/graphs/contributors 114 | [forks-shield]: https://img.shields.io/github/forks/FusionSid/Why-Bot.svg?style=for-the-badge 115 | [forks-url]: https://github.com/FusionSid/Why-Bot/network/members 116 | [stars-shield]: https://img.shields.io/github/stars/FusionSid/Why-Bot.svg?style=for-the-badge 117 | [stars-url]: https://github.com/FusionSid/Why-Bot/stargazers 118 | [issues-shield]: https://img.shields.io/github/issues/FusionSid/Why-Bot.svg?style=for-the-badge 119 | [issues-url]: https://github.com/FusionSid/Why-Bot/issues 120 | [license-shield]: https://img.shields.io/github/license/FusionSid/Why-Bot.svg?style=for-the-badge 121 | [license-url]: https://github.com/FusionSid/Why-Bot/blob/master/LICENSE.txt -------------------------------------------------------------------------------- /assets/fonts/Monocraft.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/fonts/Monocraft.otf -------------------------------------------------------------------------------- /assets/fonts/Some_Time_Later.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/fonts/Some_Time_Later.otf -------------------------------------------------------------------------------- /assets/images/heads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/heads.png -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/logo.png -------------------------------------------------------------------------------- /assets/images/selfhosting/advanced.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/selfhosting/advanced.jpg -------------------------------------------------------------------------------- /assets/images/selfhosting/bug_alert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/selfhosting/bug_alert.jpg -------------------------------------------------------------------------------- /assets/images/selfhosting/devmode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/selfhosting/devmode.jpg -------------------------------------------------------------------------------- /assets/images/selfhosting/dm1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/selfhosting/dm1.jpg -------------------------------------------------------------------------------- /assets/images/selfhosting/dm2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/selfhosting/dm2.jpg -------------------------------------------------------------------------------- /assets/images/selfhosting/dm3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/selfhosting/dm3.jpg -------------------------------------------------------------------------------- /assets/images/selfhosting/join_alert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/selfhosting/join_alert.jpg -------------------------------------------------------------------------------- /assets/images/selfhosting/leave_alert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/selfhosting/leave_alert.jpg -------------------------------------------------------------------------------- /assets/images/selfhosting/online_alert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/selfhosting/online_alert.jpg -------------------------------------------------------------------------------- /assets/images/selfhosting/rightclickid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/selfhosting/rightclickid.jpg -------------------------------------------------------------------------------- /assets/images/selfhosting/suggestion_alert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/selfhosting/suggestion_alert.jpg -------------------------------------------------------------------------------- /assets/images/selfhosting/user_settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/selfhosting/user_settings.jpg -------------------------------------------------------------------------------- /assets/images/spongebob/bamboo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/spongebob/bamboo.png -------------------------------------------------------------------------------- /assets/images/spongebob/blueseaweed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/spongebob/blueseaweed.png -------------------------------------------------------------------------------- /assets/images/spongebob/crosshatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/spongebob/crosshatch.png -------------------------------------------------------------------------------- /assets/images/spongebob/flowers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/spongebob/flowers.png -------------------------------------------------------------------------------- /assets/images/spongebob/greenseaweed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/spongebob/greenseaweed.jpg -------------------------------------------------------------------------------- /assets/images/spongebob/heads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/spongebob/heads.png -------------------------------------------------------------------------------- /assets/images/spongebob/purplewood.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/spongebob/purplewood.jpg -------------------------------------------------------------------------------- /assets/images/spongebob/sand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/spongebob/sand.png -------------------------------------------------------------------------------- /assets/images/spongebob/seaweed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/spongebob/seaweed.png -------------------------------------------------------------------------------- /assets/images/spongebob/steel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/spongebob/steel.png -------------------------------------------------------------------------------- /assets/images/spongebob/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/spongebob/tiles.png -------------------------------------------------------------------------------- /assets/images/spongebob/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/spongebob/title.png -------------------------------------------------------------------------------- /assets/images/spongebob/wood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/spongebob/wood.png -------------------------------------------------------------------------------- /assets/images/tails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/images/tails.png -------------------------------------------------------------------------------- /assets/videos/crab.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionSid/Why-Bot/1a05a3dfbd5aa861df90958b9d2f143b6fdccd4b/assets/videos/crab.mp4 -------------------------------------------------------------------------------- /docs/SELFHOSTING.md: -------------------------------------------------------------------------------- 1 | # todo -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Folder Structure: 2 | 3 | ```ansi 4 | . 5 | ├── README.md 6 | ├── cogs/ 7 | ├── config.yaml 8 | ├── core 9 | │ ├── db/ 10 | │ ├── helpers/ 11 | │ ├── models/ 12 | │ └── utils/ 13 | ├── logfiles/ 14 | ├── main.py 15 | ├── requirements.txt 16 | ├── scripts/ 17 | └── setup.py 18 | ``` 19 | --- 20 | 21 | ### cogs/ 22 | This folder contains the cogs for the bot. It is full of subfolder for further orginization of the cogs. 23 | 24 | --- 25 | 26 | ### config.yaml 27 | This is created after running the setup script and it contains the config for the bot. 28 | 29 | --- 30 | 31 | ### core/ 32 | 33 | This folder contains the rest of the bots code. All code thats not in one of the cog folder will be in here. Its full of useful classes, database functions/setup functions, helpers and utils 34 | 35 | #### db/ 36 | This folder contains database related functions. Mostly for setting up tables or rows in the db. 37 | 38 | #### helpers/ 39 | This folder is full of helper code for the bot. Things like checks, exeptions, logging, http and much more 40 | 41 | #### models/ 42 | This folder is for the classes that help coding easier. 43 | 44 | #### utils/ 45 | This folder has utilities to speedup development. Things like calculators and formatters are found here. 46 | 47 | --- 48 | 49 | ### logfiles/ 50 | 51 | This folder wont be there until you run the bot. After running there will be 2 log files in this folder. `main.log` and `discord.log`. The discord log file is created by pycord while running and is reset everytime the bot is run. `main.log` however is created by the program and it contains error logs and debug messages which are caused at runtime. 52 | 53 | --- 54 | 55 | ### main.py 56 | 57 | This file is a script and is used to start the bot. It handles loading all the cogs and connecting to the discord API. 58 | 59 | --- 60 | 61 | ### scripts/ 62 | 63 | This folder contains scripts. 64 | 65 | --- 66 | 67 | ### setup.py 68 | 69 | This file is used to setup the bot and the config files. Without running this the bot will NOT work as it is not set up correctly -------------------------------------------------------------------------------- /src/cogs/audio/music.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | import pycord.wavelink as wavelink 4 | from discord import ApplicationContext 5 | 6 | from core import BaseCog, WhyBot 7 | 8 | 9 | class Music(BaseCog): 10 | def __init__(self, client: WhyBot): 11 | self.pool = wavelink.NodePool() 12 | 13 | client.loop.create_task(self.connect_nodes()) 14 | super().__init__(client) 15 | 16 | async def connect_nodes(self): 17 | """Connect to our Lavalink nodes.""" 18 | await self.client.wait_until_ready() 19 | 20 | nodes = [ 21 | { 22 | "host": "168.138.102.186", 23 | "port": 2333, 24 | "password": "shitcodengl", 25 | "https": False, 26 | "region": discord.VoiceRegion.sydney, 27 | "identifier": "MAIN", 28 | }, 29 | {"host": "lava.link", "port": 80, "password": "dismusic", "https": False}, 30 | ] 31 | 32 | for node in nodes: 33 | await self.pool.create_node(bot=self.client, **node) 34 | 35 | @commands.Cog.listener() 36 | async def on_wavelink_node_ready(self, node: wavelink.Node): 37 | self.client.console.print( 38 | f"\n[bold green]Music Node ({node.identifier}) is ready!" 39 | ) 40 | 41 | @commands.Cog.listener() 42 | async def on_voice_state_update(self, member, before, after): 43 | if not member.bot and after.channel is None: 44 | pass 45 | # members = [i for i in before.channel.members if not i.bot] 46 | 47 | @commands.slash_command() 48 | async def play(self, ctx: ApplicationContext, search: str): 49 | """Play a song with the given search query. 50 | If not connected, connect to our voice channel. 51 | """ 52 | if not ctx.voice_client: 53 | vc: wavelink.Player = await ctx.author.voice.channel.connect( 54 | cls=wavelink.Player 55 | ) 56 | else: 57 | vc: wavelink.Player = ctx.voice_client 58 | print(vc.node.identifier) 59 | search = await wavelink.YouTubeTrack.search(query=search, return_first=True) 60 | print(search.author) 61 | await vc.play(search) 62 | 63 | @commands.slash_command() 64 | async def join( 65 | self, ctx: discord.ApplicationContext, channel: discord.VoiceChannel = None 66 | ): 67 | pass 68 | 69 | 70 | def setup(client): 71 | client.add_cog(Music(client)) 72 | -------------------------------------------------------------------------------- /src/cogs/events/alert.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import discord 4 | from discord.ext import commands 5 | 6 | from core import BaseCog 7 | from core.helpers import InputModalView, GUILD_IDS 8 | from core.utils import discord_timestamp 9 | 10 | 11 | class Alerts(BaseCog): 12 | async def __setup_settings(self, member_id: int): 13 | return await self.client.db.execute( 14 | "INSERT INTO alerts_users (user_id) VALUES ($1)", member_id 15 | ) 16 | 17 | @commands.Cog.listener() 18 | async def on_application_command_completion(self, ctx: discord.ApplicationContext): 19 | data = await self.client.db.fetch( 20 | "SELECT * FROM alerts_users WHERE user_id=$1", ctx.author.id 21 | ) 22 | if not data: 23 | await self.__setup_settings(ctx.author.id) 24 | data = [[ctx.author.id, False, False]] 25 | data = data[0] 26 | 27 | # data[1] if if they have already seen the alert 28 | # data[2] is if they have alert notifications toggled on 29 | if data[1] or data[2]: 30 | return 31 | 32 | em = discord.Embed( 33 | title="New Alert", 34 | description="You have a new alert from the devs\nUse to check it out\n\ 35 | Use to not show these types of messages", 36 | color=discord.Color.random(), 37 | ) 38 | 39 | try: # try to send the message 40 | await ctx.followup.send(embed=em, ephemeral=True) 41 | except discord.HTTPException: # frik it failed, probably perms or smth like that 42 | return 43 | 44 | @commands.slash_command(description="Shows the latest update from the why bot devs") 45 | async def alert(self, ctx: discord.ApplicationContext): 46 | data = await self.client.db.fetch("SELECT * FROM alerts ORDER BY id") 47 | data = data[::-1][0] 48 | 49 | description = data[2] 50 | date = discord_timestamp(data[3], format_type="ts") 51 | description += f"\n\nThis alert was made {date}" 52 | 53 | em = discord.Embed( 54 | title=data[1], description=description, color=discord.Color.random() 55 | ) 56 | 57 | em.set_footer(text=f"This alert was viewed {data[4]} times") 58 | 59 | await ctx.respond(embed=em) 60 | 61 | await self.client.db.execute( 62 | "UPDATE alerts_users SET alert_viewed=true WHERE user_id=$1", ctx.author.id 63 | ) 64 | await self.client.db.execute( 65 | "UPDATE alerts SET viewed=$1 WHERE id=$2", (data[4] + 1), data[0] 66 | ) 67 | 68 | @commands.slash_command( 69 | description="Disables the new alert message that shows when theres a new update" 70 | ) 71 | async def togglealerts(self, ctx: discord.ApplicationContext): 72 | data = await self.client.db.fetch( 73 | "SELECT * FROM alerts_users WHERE user_id=$1", ctx.author.id 74 | ) 75 | if not data: 76 | await self.__setup_settings(ctx.author.id) 77 | data = [[ctx.author.id, False, False]] 78 | data = data[0] 79 | 80 | on_or_off = not data[2] 81 | 82 | await self.client.db.execute( 83 | "UPDATE alerts_users SET ignore_alerts=$1 WHERE user_id=$2", 84 | on_or_off, 85 | ctx.guild.id, 86 | ) 87 | 88 | await ctx.respond( 89 | embed=discord.Embed( 90 | title="Alerts Toggled!", 91 | description=( 92 | f"Alert notifications is now {'on ✅' if on_or_off else 'off ❌'}\nIf you" 93 | f" wish to toggle it back {'off' if on_or_off else 'on'} run this" 94 | " command again" 95 | ), 96 | color=discord.Color.green() if on_or_off else discord.Color.red(), 97 | ) 98 | ) 99 | 100 | @commands.slash_command(description="Creates a new alert", guild_ids=GUILD_IDS) 101 | @commands.is_owner() 102 | async def newalert(self, ctx, name): 103 | input = InputModalView( 104 | title="Alert Value", label="Enter the value of the alert:" 105 | ) 106 | await ctx.send_modal(input) 107 | await input.wait() 108 | 109 | if input.value is None: 110 | return await ctx.respond( 111 | "Not creating alert as input was either None or invalid", ephemeral=True 112 | ) 113 | 114 | # create tag 115 | await self.client.db.execute( 116 | """INSERT INTO alerts ( 117 | alert_title, alert_message, time_created 118 | ) VALUES ($1, $2, $3)""", 119 | name, 120 | input.value, 121 | int(time.time()), 122 | ) 123 | 124 | await self.client.db.execute("UPDATE alerts_users SET alert_viewed=false") 125 | 126 | await ctx.respond( 127 | "Alert Created! It will look like this:", 128 | embed=discord.Embed( 129 | title=name, description=input.value, color=discord.Color.random() 130 | ), 131 | ) 132 | 133 | 134 | def setup(client): 135 | client.add_cog(Alerts(client)) 136 | -------------------------------------------------------------------------------- /src/cogs/events/on_event.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import discord 4 | from discord.ext import commands 5 | 6 | from core.models import NewTicketView, WhyBot 7 | from core.helpers import ( 8 | InvalidDatabaseUrl, 9 | log_normal, 10 | update_activity, 11 | create_connection_pool, 12 | create_redis_connection, 13 | ) 14 | 15 | 16 | class OnEvent(commands.Cog): 17 | def __init__(self, client: WhyBot): 18 | self.client = client 19 | 20 | @commands.Cog.listener() 21 | async def on_message(self, message: discord.Message): 22 | """ 23 | This is the event that is called when a message is sent in a channel 24 | It will check if the bot has been mentioned in the message and if so 25 | it will reply with a message containing the guild prefix 26 | """ 27 | 28 | if message.author.bot: 29 | return 30 | 31 | if ( 32 | self.client.user.mentioned_in(message) 33 | and message.mention_everyone is False 34 | and message.reference is None 35 | ): 36 | em = discord.Embed( 37 | title=f"Hi, my name is {self.client.user.display_name}. Use for help", 38 | color=message.author.color, 39 | ) 40 | return await message.channel.send(embed=em) 41 | 42 | @commands.Cog.listener() 43 | async def on_ready(self): 44 | """ 45 | Runs when the bot is ready 46 | Prints a message to console and updates the bot's activity 47 | """ 48 | # update the bots activity 49 | await update_activity(self.client) 50 | 51 | # (try to) connect to the postgresql database 52 | try: 53 | self.client.db = await create_connection_pool() 54 | except ValueError: 55 | raise InvalidDatabaseUrl 56 | 57 | # connect to redis db and reset the cache 58 | self.client.redis = await create_redis_connection() 59 | await self.client.redis.flushall() # reset cache 60 | 61 | # Send online alert 62 | online_alert_channel = self.client.config.get("online_alert_channel") 63 | if online_alert_channel in (0, None): 64 | return 65 | 66 | try: 67 | channel = await self.client.fetch_channel(online_alert_channel) 68 | except discord.errors.NotFound: 69 | return 70 | 71 | await self.__setup_ticket_buttons() 72 | 73 | await channel.send( 74 | embed=discord.Embed( 75 | title="Bot is online", 76 | color=discord.Color.green(), 77 | timestamp=datetime.datetime.now(), 78 | ) 79 | ) 80 | 81 | if self.client.config.get("LOGGING"): 82 | await log_normal("Bot is Online") 83 | 84 | # print to console that its ready 85 | self.client.console.print("\n[bold green]Bot is ready") 86 | 87 | async def __setup_ticket_buttons(self): 88 | guilds = await self.client.db.fetch( 89 | "SELECT guild_id FROM ticket_guild WHERE create_button=true" 90 | ) 91 | for guild_id in map(lambda x: x[0], guilds): 92 | self.client.add_view(NewTicketView(guild_id, self.client)) 93 | 94 | 95 | def setup(client: WhyBot): 96 | client.add_cog(OnEvent(client)) 97 | -------------------------------------------------------------------------------- /src/cogs/events/on_guild.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import discord 4 | from discord.ext import commands 5 | 6 | from core.models import WhyBot 7 | from core.helpers import log_normal, update_activity 8 | from core.db import create_db_tables 9 | 10 | 11 | class OnGuild(commands.Cog): 12 | def __init__(self, client: WhyBot): 13 | self.client = client 14 | 15 | @commands.Cog.listener() 16 | async def on_guild_remove(self, guild: discord.Guild): 17 | """ 18 | Called when the bot is removed from a guild 19 | It will update the bots activity 20 | It will also send a message to the the leave_alert_channel which is set in the config 21 | """ 22 | await update_activity(self.client) 23 | leave_alert_channel = self.client.config["leave_alert_channel"] 24 | if leave_alert_channel in (0, None): 25 | return 26 | 27 | try: 28 | channel = self.client.get_channel(leave_alert_channel) 29 | except discord.errors.NotFound: 30 | return 31 | 32 | em = discord.Embed( 33 | title="Leave", description=f"Left: {guild.name}", color=discord.Color.red() 34 | ) 35 | em.timestamp = datetime.datetime.utcnow() 36 | await channel.send(embed=em) 37 | 38 | if self.client.config["LOGGING"]: 39 | await log_normal(f"Left Guild: '{guild.name}'") 40 | 41 | @commands.Cog.listener() 42 | async def on_guild_join(self, guild: discord.Guild): 43 | """ 44 | Called when the bot joins a guild 45 | It will update the bots activity 46 | It will also send a message to the the join_alert_channel which is set in the config 47 | """ 48 | await update_activity(self.client) 49 | 50 | join_alert_channel = self.client.config["join_alert_channel"] 51 | if join_alert_channel in (0, None): 52 | return 53 | 54 | try: 55 | channel = self.client.get_channel(join_alert_channel) 56 | except discord.errors.NotFound: 57 | return 58 | 59 | em = discord.Embed( 60 | title="Join", 61 | description=f"Joined: {guild.name}", 62 | color=discord.Color.green(), 63 | ) 64 | em.timestamp = datetime.datetime.utcnow() 65 | await channel.send(embed=em) 66 | 67 | if self.client.config["LOGGING"]: 68 | await log_normal(f"Joined Guild: '{guild.name}'") 69 | 70 | await create_db_tables(self.client.db, guild.id) 71 | 72 | 73 | def setup(client: WhyBot): 74 | client.add_cog(OnGuild(client)) 75 | -------------------------------------------------------------------------------- /src/cogs/fun/convert.py: -------------------------------------------------------------------------------- 1 | import io 2 | import random 3 | 4 | import discord 5 | import pyfiglet 6 | import discord_colorize 7 | from discord.commands import SlashCommandGroup 8 | 9 | from core import BaseCog 10 | from core.helpers import get_request_bytes 11 | 12 | colors = discord_colorize.Colors() 13 | fonts = pyfiglet.FigletFont.getFonts() 14 | for idx, font in enumerate(fonts): 15 | fonts[idx] = font.replace("_", "") 16 | fonts = dict(zip(fonts, pyfiglet.FigletFont.getFonts())) 17 | 18 | 19 | class TextConvert(BaseCog): 20 | 21 | textconvert = SlashCommandGroup("text", "Convert text to something else") 22 | 23 | @textconvert.command(description="Convert text to stickycaps") 24 | async def stickycaps( 25 | self, 26 | ctx: discord.ApplicationContext, 27 | text: discord.Option(str, "The text to convert"), 28 | ): 29 | functions = [str.upper, str.lower] 30 | result = "".join(random.choice(functions)(char) for char in text) 31 | if len(result) <= 1999: 32 | return await ctx.respond(result) 33 | await ctx.respond("Too long to send :(") 34 | 35 | @textconvert.command(description="Expand some text") 36 | async def expand( 37 | self, 38 | ctx: discord.ApplicationContext, 39 | space: int, 40 | text: discord.Option(str, "The text to convert"), 41 | ): 42 | spacing = " " * space 43 | result = spacing.join(text) 44 | if len(result) <= 1999: 45 | return await ctx.respond(result) 46 | await ctx.respond("Too long to send :(") 47 | 48 | @textconvert.command(description="Reverse some text") 49 | async def reverse( 50 | self, 51 | ctx: discord.ApplicationContext, 52 | text: discord.Option(str, "The text to convert"), 53 | ): 54 | result = text[::-1] 55 | if len(result) <= 1999: 56 | return await ctx.respond(result) 57 | await ctx.respond("Too long to send :(") 58 | 59 | @textconvert.command(description="Convert text to hex") 60 | async def texttohex( 61 | self, 62 | ctx: discord.ApplicationContext, 63 | text: discord.Option(str, "The text to convert"), 64 | ): 65 | try: 66 | hex_output = " ".join(f"{ord(char):02x}" for char in text) 67 | except Exception as e: 68 | return await ctx.respond( 69 | f"Error: `{e}`. This probably means the text is malformed" 70 | ) 71 | if len(hex_output) <= 1999: 72 | return await ctx.respond(f"```fix\n{hex_output}```") 73 | await ctx.respond("Too long to send :(") 74 | 75 | @textconvert.command(description="Convert hex to text") 76 | async def hextotext( 77 | self, 78 | ctx: discord.ApplicationContext, 79 | text: discord.Option(str, "The text to convert"), 80 | ): 81 | try: 82 | text_output = bytearray.fromhex(text).decode() 83 | except Exception as e: 84 | return await ctx.respond( 85 | f"**Error: `{e}`. This probably means the text is malformed**" 86 | ) 87 | if len(text_output) <= 1999: 88 | return await ctx.respond(f"```fix\n{text_output}```") 89 | await ctx.respond("Too long to send :(") 90 | 91 | @textconvert.command(description="Convert text to binary") 92 | async def texttobinary( 93 | self, 94 | ctx: discord.ApplicationContext, 95 | text: discord.Option(str, "The text to convert"), 96 | ): 97 | try: 98 | binary_output = " ".join(format(ord(char), "b") for char in text) 99 | except Exception as e: 100 | return await ctx.respond( 101 | f"**Error: `{e}`. This probably means the text is malformed." 102 | ) 103 | if len(binary_output) <= 1999: 104 | return await ctx.respond(f"```fix\n{binary_output}```") 105 | await ctx.respond("Too long to send :(") 106 | 107 | @textconvert.command(description="Convert binary to text") 108 | async def binarytotext( 109 | self, 110 | ctx: discord.ApplicationContext, 111 | text: discord.Option(str, "The text to convert"), 112 | ): 113 | try: 114 | text_output = "".join([chr(int(char, 2)) for char in text.split()]) 115 | except Exception as e: 116 | await ctx.respond( 117 | f"**Error: `{e}`. This probably means the text is malformed" 118 | ) 119 | if len(text_output) <= 1999: 120 | return await ctx.respond(f"```fix\n{text_output}```") 121 | await ctx.respond("Too long to send :(") 122 | 123 | @textconvert.command(description="Emojify some text") 124 | async def emojify( 125 | self, 126 | ctx: discord.ApplicationContext, 127 | text: discord.Option(str, "The text to convert"), 128 | ): 129 | emojis = [] 130 | 131 | extra = { 132 | "?": ":question:", 133 | "!": ":exclamation:", 134 | } 135 | numbers = { 136 | 0: ":zero:", 137 | 1: ":one:", 138 | 2: ":two:", 139 | 3: ":three:", 140 | 4: ":four:", 141 | 5: ":five:", 142 | 6: ":six:", 143 | 7: ":seven:", 144 | 8: ":eight:", 145 | 9: ":nine:", 146 | } 147 | 148 | for char in text.lower(): 149 | if char.isdecimal(): 150 | emojis.append(numbers.get(char, "")) 151 | elif char.isalpha(): 152 | emojis.append(f":regional_indicator_{char}:") 153 | elif char in extra: 154 | emojis.append(extra.get(char, "")) 155 | else: 156 | emojis.append(char) 157 | 158 | await ctx.respond(" ".join(emojis)) 159 | 160 | @textconvert.command(description="Create ascii art from the given text") 161 | async def ascii( 162 | self, 163 | ctx: discord.ApplicationContext, 164 | message: str, 165 | color: discord.Option( 166 | str, 167 | default="nocolor", 168 | choices=["red", "yellow", "blue", "green", "gray", "pink", "cyan", "white"], 169 | ), 170 | font: discord.Option(str, "The font to do the ascii art with") = "big", 171 | ): 172 | if fonts.get(font) is None: 173 | return await ctx.respond( 174 | embed=discord.Embed( 175 | title="Invalid Font!", 176 | description=f'Available Fonts:\n{", ".join(fonts.keys())}', 177 | color=discord.Color.red(), 178 | ), 179 | ephemeral=True, 180 | ) 181 | 182 | if color == "nocolor": 183 | message = f"```\n{pyfiglet.figlet_format(message, font=fonts[font])}\n```" 184 | else: 185 | message = f"```ansi\n{colors.colorize(pyfiglet.figlet_format(message, font=fonts[font]), fg=color)}\n```" 186 | 187 | if len(message) > 4000: 188 | return await ctx.respond("Text to long to send :(", ephemeral=True) 189 | await ctx.respond( 190 | embed=discord.Embed(title="Ascii Art Output:", description=message) 191 | ) 192 | 193 | @textconvert.command(description="Convert text to a font and show it as an image") 194 | async def fontconvert( 195 | self, 196 | ctx: discord.ApplicationContext, 197 | message: str, 198 | font: str = None, 199 | color: str = "black", 200 | ): 201 | if font is None: 202 | return await ctx.respond( 203 | embed=discord.Embed( 204 | title="You must specify a font", 205 | description=( 206 | "[Click this to get a list of fonts you can" 207 | " use](https://api.fusionsid.xyz/api/font/list)" 208 | ), 209 | color=discord.Colour.red(), 210 | ), 211 | ephemeral=True, 212 | ) 213 | URL = "https://api.fusionsid.xyz/api/font/convert" 214 | 215 | text_image = await get_request_bytes( 216 | URL, 217 | data={"font": font, "text": message, "text_color": color}, 218 | bytes_io=True, 219 | ) 220 | 221 | if not isinstance(text_image, io.BytesIO): 222 | return await ctx.respond( 223 | embed=discord.Embed( 224 | title="An error occured while trying to get the image", 225 | description=( 226 | "This is most likely because you used an invalid font\n " 227 | " [Click this to get a list of fonts you can" 228 | " use](https://api.fusionsid.xyz/api/font/list)" 229 | ), 230 | color=discord.Colour.red(), 231 | ), 232 | ephemeral=True, 233 | ) 234 | 235 | await ctx.respond(file=discord.File(text_image, "text.png")) 236 | 237 | 238 | def setup(client): 239 | client.add_cog(TextConvert(client)) 240 | -------------------------------------------------------------------------------- /src/cogs/fun/poll.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time as pytime 3 | 4 | import discord 5 | from discord.utils import get 6 | from discord.ext import commands 7 | 8 | from core import BaseCog 9 | from core.utils.formatters import discord_timestamp 10 | 11 | 12 | class Poll(BaseCog): 13 | @commands.slash_command(description="Makes a Yah or Nah poll") 14 | @commands.guild_only() 15 | async def yesorno(self, ctx: discord.ApplicationContext, message: str): 16 | msg = await ctx.respond( 17 | embed=discord.Embed( 18 | title="Yah or Nah?", description=message, color=ctx.author.color 19 | ) 20 | ) 21 | msg = await msg.original_message() 22 | await msg.add_reaction("👍") 23 | await msg.add_reaction("👎") 24 | 25 | @commands.slash_command(description="Create a poll") 26 | @commands.guild_only() 27 | async def poll( 28 | self, 29 | ctx: discord.ApplicationContext, 30 | title: str, 31 | choice1: str, 32 | choice2: str, 33 | choice3: str = None, 34 | choice4: str = None, 35 | choice5: str = None, 36 | choice6: str = None, 37 | choice7: str = None, 38 | choice8: str = None, 39 | choice9: str = None, 40 | choice10: str = None, 41 | end_poll_in: str = None, 42 | ): 43 | await ctx.defer() 44 | options = [ 45 | i 46 | for i in [ 47 | choice1, 48 | choice2, 49 | choice3, 50 | choice4, 51 | choice5, 52 | choice6, 53 | choice7, 54 | choice8, 55 | choice9, 56 | choice10, 57 | ] 58 | if i is not None 59 | ] 60 | numbers = { 61 | 1: "1️⃣", 62 | 2: "2️⃣", 63 | 3: "3️⃣", 64 | 4: "4️⃣", 65 | 5: "5️⃣", 66 | 6: "6️⃣", 67 | 7: "7️⃣", 68 | 8: "8️⃣", 69 | 9: "9️⃣", 70 | 10: "🔟", 71 | } 72 | reactions_todo = [] 73 | desc = "" 74 | for index, option in enumerate(options): 75 | emoji = numbers[index + 1] 76 | desc += f"\n{emoji} {option}" 77 | reactions_todo.append(emoji) 78 | 79 | em = discord.Embed( 80 | title=f'{ctx.author.display_name} asks: "{title}"', 81 | description=desc, 82 | color=discord.Color.random(), 83 | ) 84 | 85 | if end_poll_in is not None: 86 | if end_poll_in.isnumeric(): 87 | time = int(end_poll_in) 88 | else: 89 | seconds_per_unit = {"m": 60, "h": 3600, "d": 86400, "w": 604800} 90 | try: 91 | time = int(end_poll_in[:-1]) * seconds_per_unit[end_poll_in[-1]] 92 | except ValueError: 93 | return await ctx.respond( 94 | "Poll failed to be created" 95 | f" {self.client.get_why_emojies.get('redcross', '❌')}\nThe format code" 96 | " was not found", 97 | ephemeral=True, 98 | ) 99 | 100 | if time > 604800: 101 | return await ctx.respond( 102 | "Poll failed to be created" 103 | f" {self.client.get_why_emojies.get('redcross', '❌')}\nTime can not" 104 | " be longer than a week", 105 | ephemeral=True, 106 | ) 107 | 108 | timern = pytime.time() 109 | 110 | em.add_field( 111 | name="Voting ends in:", 112 | value=discord_timestamp(int(timern + time), "ts"), 113 | ) 114 | 115 | message = await ctx.send(embed=em) 116 | await ctx.respond( 117 | f"Poll created successfuly {self.client.get_why_emojies.get('checkmark', '✅')}", 118 | ephemeral=True, 119 | ) 120 | for reaction in reactions_todo: 121 | await message.add_reaction(reaction) 122 | 123 | if end_poll_in is None: 124 | return 125 | 126 | await asyncio.sleep(time) 127 | 128 | message_later = await ctx.channel.fetch_message(message.id) 129 | results = {} 130 | for i in reactions_todo: 131 | reaction = get(message_later.reactions, emoji=i) 132 | count = reaction.count - 1 133 | results[i] = f"{count} vote{'s' if count > 1 else ''}" 134 | 135 | results = "\n".join([f"{key} got {value}" for key, value in results.items()]) 136 | embed = discord.Embed( 137 | title=f'Poll results for: "{title}"', 138 | description=f"**Votes:**\n {results}", 139 | color=ctx.author.color, 140 | ) 141 | embed.set_footer(text="Voting is closed") 142 | try: 143 | await message.clear_reactions() 144 | except discord.HTTPException: 145 | pass 146 | return await message.edit(embed=embed) 147 | 148 | 149 | def setup(client): 150 | client.add_cog(Poll(client)) 151 | -------------------------------------------------------------------------------- /src/cogs/fun/rand.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import json 4 | import string 5 | import random 6 | 7 | import discord 8 | import aiofiles 9 | from PIL import Image 10 | import discord_colorize 11 | from discord.commands import SlashCommandGroup 12 | 13 | import __main__ 14 | from core import BaseCog 15 | from core.helpers import ImageAPIFail 16 | from core.helpers.http import get_request_bytes 17 | 18 | 19 | class Random(BaseCog): 20 | @staticmethod 21 | async def open_json_fun(): 22 | path = os.path.join( 23 | os.path.dirname(__main__.__file__).replace("src", ""), 24 | "assets/json_files/fun_text.json", 25 | ) 26 | async with aiofiles.open(path, mode="r") as f: 27 | contents = await f.read() 28 | data = json.loads(contents) 29 | 30 | return data 31 | 32 | random = SlashCommandGroup("random", "Random commands") 33 | 34 | @random.command(description="Show a random compliment") 35 | async def compliment(self, ctx: discord.ApplicationContext): 36 | data = await self.open_json_fun() 37 | await ctx.respond(random.choice(data["compliment"])) 38 | 39 | @random.command(description="Show a random dare") 40 | async def dare(self, ctx: discord.ApplicationContext): 41 | data = await self.open_json_fun() 42 | await ctx.respond(random.choice(data["dares"])) 43 | 44 | @random.command(description="Show a random fact") 45 | async def fact(self, ctx: discord.ApplicationContext): 46 | data = await self.open_json_fun() 47 | await ctx.respond(random.choice(data["facts"])) 48 | 49 | @random.command(description="Show a random roast") 50 | async def roast(self, ctx: discord.ApplicationContext): 51 | data = await self.open_json_fun() 52 | await ctx.respond(random.choice(data["roasts"])) 53 | 54 | @random.command(description="Show a random truth") 55 | async def truth(self, ctx: discord.ApplicationContext): 56 | data = await self.open_json_fun() 57 | await ctx.respond(random.choice(data["truth"])) 58 | 59 | @random.command(description="Show a random truth and dare") 60 | async def truth_or_dare(self, ctx: discord.ApplicationContext): 61 | data = await self.open_json_fun() 62 | await ctx.respond( 63 | embed=discord.Embed( 64 | title="Truth Or Dare", 65 | description=( 66 | f'**Truth:** {random.choice(data["truth"])}\n**Dare:**' 67 | f' {random.choice(data["dares"])}\n\n**Computer Choice:**' 68 | f' {random.choice(["truth", "dare"])}' 69 | ), 70 | color=ctx.author.color, 71 | ) 72 | ) 73 | 74 | @random.command(description="Pick a random number") 75 | async def number(self, ctx: discord.ApplicationContext, stop: int, start: int = 0): 76 | return await ctx.respond(random.randint(start, stop)) 77 | 78 | @random.command(description="Pick a random choice out of the options you give") 79 | async def choice( 80 | self, 81 | ctx: discord.ApplicationContext, 82 | choice1: str, 83 | choice2: str, 84 | choice3: str = None, 85 | choice4: str = None, 86 | choice5: str = None, 87 | choice6: str = None, 88 | choice7: str = None, 89 | choice8: str = None, 90 | choice9: str = None, 91 | choice10: str = None, 92 | ): 93 | await ctx.defer() 94 | choice = random.choice( 95 | [ 96 | i 97 | for i in ( 98 | choice1, 99 | choice2, 100 | choice3, 101 | choice4, 102 | choice5, 103 | choice6, 104 | choice7, 105 | choice8, 106 | choice9, 107 | choice10, 108 | ) 109 | if i is not None 110 | ] 111 | ) 112 | return await ctx.respond( 113 | embed=discord.Embed( 114 | title="Random Choice", 115 | description=f"**Computer Choice:** {choice}", 116 | color=ctx.author.color, 117 | ) 118 | ) 119 | 120 | @random.command(description="Choose a random card") 121 | async def card(self, ctx: discord.ApplicationContext): 122 | url = "https://api.fusionsid.xyz/api/image/random-card" 123 | img = await get_request_bytes( 124 | url, 125 | bytes_io=True, 126 | ) 127 | 128 | if not isinstance(img, io.BytesIO): 129 | raise ImageAPIFail 130 | 131 | await ctx.respond(file=discord.File(img, "text.png")) 132 | 133 | @random.command(description="Flip a coin") 134 | async def flipcoin(self, ctx: discord.ApplicationContext): 135 | h_or_t = random.choice(["heads", "tails"]) 136 | path = os.path.join( 137 | os.path.dirname(__main__.__file__).replace("src", ""), 138 | f"assets/images/{h_or_t}.png", 139 | ) 140 | await ctx.respond(f"Its {h_or_t}!", file=discord.File(path)) 141 | 142 | @random.command(description="Pick a random color") 143 | async def color(self, ctx: discord.ApplicationContext): 144 | color = tuple(random.randint(0, 255) for _ in range(3)) 145 | img = Image.new("RGB", (500, 500), color) 146 | send = io.BytesIO() 147 | img.save(send, "PNG") 148 | send.seek(0) 149 | await ctx.respond(file=discord.File(send, "color.png")) 150 | 151 | @random.command(description="Pick a random letter") 152 | async def letter(self, ctx: discord.ApplicationContext): 153 | return await ctx.respond( 154 | f"Your random letter is '{random.choice(string.ascii_lowercase)}'" 155 | ) 156 | 157 | @random.command(description="Roll a dice") 158 | async def diceroll(self, ctx: discord.ApplicationContext): 159 | color = random.choice( 160 | ["red", "yellow", "blue", "green", "gray", "pink", "cyan", "white"] 161 | ) 162 | 163 | colors = discord_colorize.Colors() 164 | dice = random.choice(self.dice) 165 | message = f"```ansi\n{colors.colorize(dice, fg=color)}\n```" 166 | await ctx.respond( 167 | embed=discord.Embed( 168 | title=f"Rolled a {self.dice.index(dice)+1}", 169 | description=message, 170 | color=discord.Color.random(), 171 | ) 172 | ) 173 | 174 | # credit to micfun123 for the dice ascii art 175 | dice = [ 176 | """\ 177 | ----- 178 | | | 179 | | o | 180 | | | 181 | ----- 182 | """, 183 | """\ 184 | ----- 185 | |o | 186 | | | 187 | | o| 188 | ----- 189 | """, 190 | """\ 191 | ----- 192 | |o | 193 | | o | 194 | | o| 195 | ----- 196 | """, 197 | """\ 198 | ----- 199 | |o o| 200 | | | 201 | |o o| 202 | ----- 203 | """, 204 | """\ 205 | ----- 206 | |o o| 207 | | o | 208 | |o o| 209 | ----- 210 | """, 211 | """\ 212 | ----- 213 | |o o| 214 | |o o| 215 | |o o| 216 | ----- 217 | """, 218 | ] 219 | 220 | 221 | def setup(client): 222 | client.add_cog(Random(client)) 223 | -------------------------------------------------------------------------------- /src/cogs/games/rps.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | 4 | from core import BaseCog 5 | from core.helpers.views import ConfirmView 6 | from core.models.rps import RockPaperScissorsView 7 | 8 | 9 | class RockPaperScissors(BaseCog): 10 | @commands.slash_command( 11 | name="rps", description="Challenge someone to a game of rock paper scissors" 12 | ) 13 | async def rock_paper_scissors_command( 14 | self, ctx: discord.ApplicationContext, opponent: discord.Member 15 | ): 16 | 17 | if opponent == ctx.author: 18 | return await ctx.respond("You can't play against yourself", ephemeral=True) 19 | 20 | await ctx.respond("Waiting for opponent to accept", ephemeral=True) 21 | 22 | view = ConfirmView(target=opponent) 23 | em = discord.Embed( 24 | title="Confirm Or Deny", 25 | description=( 26 | f"{ctx.author.mention} would like to play a game of rock paper scissors" 27 | " with you\nDo you want to play?" 28 | ), 29 | color=discord.Color.random(), 30 | ) 31 | 32 | await ctx.send(content=opponent.mention, embed=em, view=view) 33 | await view.wait() 34 | 35 | if not view.accepted: 36 | return await ctx.send( 37 | embed=discord.Embed( 38 | title="Rock Paper Scissors", 39 | description=( 40 | f"{opponent.mention} denied your request to rock paper scissors" 41 | " with them" 42 | ), 43 | color=discord.Color.random(), 44 | ), 45 | ephemeral=True, 46 | ) 47 | 48 | game = RockPaperScissorsView(ctx.author, opponent) 49 | await ctx.respond( 50 | f"{opponent.mention} accepted to play with you. Game is now starting...", 51 | ephemeral=True, 52 | ) 53 | await ctx.send( 54 | embed=discord.Embed( 55 | title="Rock Paper Scissors", 56 | description=( 57 | f"{ctx.author.mention} & {opponent.mention} choose your move:" 58 | ), 59 | color=discord.Color.random(), 60 | ), 61 | view=game, 62 | ) 63 | 64 | 65 | def setup(client): 66 | client.add_cog(RockPaperScissors(client)) 67 | -------------------------------------------------------------------------------- /src/cogs/games/tictactoe.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.commands import SlashCommandGroup 3 | 4 | from core import BaseCog 5 | from core.helpers import ConfirmView 6 | from core.models import TicTacToeAIView, TicTacToe2PlayerView 7 | 8 | 9 | class TicTacToeCog(BaseCog): 10 | 11 | tictactoe_cmd = SlashCommandGroup("tictactoe", "Tic tac toe commands") 12 | 13 | @tictactoe_cmd.command( 14 | name="tictactoe", description="Play against someone on your server" 15 | ) 16 | async def tictactoe_multiplayer( 17 | self, ctx: discord.ApplicationContext, opponent: discord.Member 18 | ): 19 | if opponent == ctx.author: 20 | return await ctx.respond("You can't play against yourself", ephemeral=True) 21 | await ctx.respond("Waiting for opponent to accept", ephemeral=True) 22 | 23 | view = ConfirmView(target=opponent) 24 | em = discord.Embed( 25 | title="Confirm Or Deny", 26 | description=( 27 | f"{ctx.author.mention} would like to play a game of tic tac toe with" 28 | " you\nDo you want to play?" 29 | ), 30 | color=discord.Color.random(), 31 | ) 32 | await ctx.send(content=opponent.mention, embed=em, view=view) 33 | await view.wait() 34 | 35 | if not view.accepted: 36 | return await ctx.respond( 37 | embed=discord.Embed( 38 | title="Tic Tac Toe", 39 | description=( 40 | f"{opponent.mention} denied your request to play tic tac toe" 41 | " with them" 42 | ), 43 | color=discord.Color.random(), 44 | ), 45 | ephemeral=True, 46 | ) 47 | 48 | game = TicTacToe2PlayerView(ctx.author, opponent) 49 | await ctx.send( 50 | embed=discord.Embed( 51 | title="Tic Tac Toe", 52 | description=( 53 | f"X = {opponent.display_name}\nO =" 54 | f" {ctx.author.display_name}\n{opponent.display_name} starts!" 55 | ), 56 | color=discord.Color.random(), 57 | ), 58 | view=game, 59 | ) 60 | 61 | @tictactoe_cmd.command(name="ai", description="Play against the bot") 62 | async def tictactoe_ai(self, ctx: discord.ApplicationContext): 63 | 64 | game = TicTacToeAIView(ctx.author) 65 | await ctx.respond( 66 | embed=discord.Embed( 67 | title="Tic Tac Toe", 68 | description=f"X = {ctx.author.display_name}\nO = Bot", 69 | color=discord.Color.random(), 70 | ), 71 | view=game, 72 | ) 73 | 74 | 75 | def setup(client): 76 | client.add_cog(TicTacToeCog(client)) 77 | -------------------------------------------------------------------------------- /src/cogs/image/animals.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | import discord 4 | from discord.commands import SlashCommandGroup 5 | 6 | from core import BaseCog 7 | from core.helpers import get_request, ImageAPIFail 8 | 9 | 10 | def animal_embed(response: dict = None): 11 | if response is None: 12 | raise ImageAPIFail 13 | 14 | em = discord.Embed( 15 | title=response["title"], 16 | description=response["desc"], 17 | color=discord.Color.random(), 18 | ) 19 | 20 | em.set_image(url=response["image"]) 21 | 22 | return em 23 | 24 | 25 | def animal_embed_randomapi(response: dict | None, title: str): 26 | if response is None: 27 | raise ImageAPIFail 28 | 29 | em = discord.Embed( 30 | title=title, description=response["fact"], color=discord.Color.random() 31 | ) 32 | 33 | em.set_image(url=response["image"]) 34 | 35 | return em 36 | 37 | 38 | class AnimalURLS(Enum): 39 | # some random api 40 | dog = "https://some-random-api.ml/animal/dog" 41 | cat = "https://some-random-api.ml/animal/cat" 42 | fox = "https://some-random-api.ml/animal/fox" 43 | panda = "https://some-random-api.ml/animal/panda" 44 | bird = "https://some-random-api.ml/animal/bird" 45 | kangaroo = "https://some-random-api.ml/animal/kangaroo" 46 | koala = "https://some-random-api.ml/animal/koala" 47 | raccoon = "https://some-random-api.ml/animal/raccoon" 48 | red_panda = "https://some-random-api.ml/animal/red_panda" 49 | 50 | # other 51 | capybara = "https://api.capy.lol/v1/capybara?json=true" 52 | shibe = "https://shibe.online/api/shibes?count=1&urls=true&httpsUrls=true" 53 | duck = "https://random-d.uk/api/v2/quack" 54 | whale = "https://some-random-api.ml/img/whale" 55 | 56 | 57 | class Animals(BaseCog): 58 | 59 | animal = SlashCommandGroup("animal", "Get images and facts about animals") 60 | 61 | @animal.command(description="Show a picture of a dog") 62 | async def dog(self, ctx: discord.ApplicationContext): 63 | url = AnimalURLS.dog.value 64 | response = await get_request(url) 65 | 66 | await ctx.respond(embed=animal_embed_randomapi(response, "Dog!")) 67 | 68 | @animal.command(description="Show a picture of a cat") 69 | async def cat(self, ctx: discord.ApplicationContext): 70 | url = AnimalURLS.cat.value 71 | response = await get_request(url) 72 | 73 | await ctx.respond(embed=animal_embed_randomapi(response, "Cat!")) 74 | 75 | @animal.command(description="Show a picture of a fox") 76 | async def fox(self, ctx: discord.ApplicationContext): 77 | url = AnimalURLS.fox.value 78 | response = await get_request(url) 79 | 80 | await ctx.respond(embed=animal_embed_randomapi(response, "Fox!")) 81 | 82 | @animal.command(description="Show a picture of a panda") 83 | async def panda(self, ctx: discord.ApplicationContext): 84 | url = AnimalURLS.panda.value 85 | response = await get_request(url) 86 | 87 | await ctx.respond(embed=animal_embed_randomapi(response, "Panda!")) 88 | 89 | @animal.command(description="Show a picture of a bird") 90 | async def bird(self, ctx: discord.ApplicationContext): 91 | url = AnimalURLS.bird.value 92 | response = await get_request(url) 93 | 94 | await ctx.respond(embed=animal_embed_randomapi(response, "Bird!")) 95 | 96 | @animal.command(description="Show a picture of a kangaroo") 97 | async def kangaroo(self, ctx: discord.ApplicationContext): 98 | url = AnimalURLS.kangaroo.value 99 | response = await get_request(url) 100 | 101 | await ctx.respond(embed=animal_embed_randomapi(response, "Kangaroo!")) 102 | 103 | @animal.command(description="Show a picture of a koala") 104 | async def koala(self, ctx: discord.ApplicationContext): 105 | url = AnimalURLS.koala.value 106 | response = await get_request(url) 107 | 108 | await ctx.respond(embed=animal_embed_randomapi(response, "Koala!")) 109 | 110 | @animal.command(description="Show a picture of a racoon") 111 | async def raccon(self, ctx: discord.ApplicationContext): 112 | url = AnimalURLS.raccoon.value 113 | response = await get_request(url) 114 | 115 | await ctx.respond(embed=animal_embed_randomapi(response, "Racoon!")) 116 | 117 | @animal.command(description="Show a picture of a red panda") 118 | async def redpanda(self, ctx: discord.ApplicationContext): 119 | url = AnimalURLS.red_panda.value 120 | response = await get_request(url) 121 | 122 | await ctx.respond(embed=animal_embed_randomapi(response, "Red Panda!")) 123 | 124 | @animal.command(description="Show a picture of a capybara") 125 | async def capybara(self, ctx: discord.ApplicationContext): 126 | url = AnimalURLS.capybara.value 127 | image = await get_request(url) 128 | 129 | if ( 130 | image is None 131 | or image.get("data") is None 132 | or image["data"].get("url") is None 133 | ): 134 | await ctx.respond(embed=animal_embed(None)) 135 | 136 | response = { 137 | "desc": "Ok I Pull Up!", 138 | "image": image["data"]["url"], 139 | "title": "Capybara!", 140 | } 141 | await ctx.respond(embed=animal_embed(response)) 142 | 143 | @animal.command(description="Show a picture of a duck") 144 | async def duck(self, ctx: discord.ApplicationContext): 145 | url = AnimalURLS.duck.value 146 | image = await get_request(url) 147 | 148 | if image is None or image.get("url") is None: 149 | await ctx.respond(embed=animal_embed(None)) 150 | 151 | response = { 152 | "desc": "Quack!", 153 | "image": image["url"], 154 | "title": "Duck!", 155 | } 156 | await ctx.respond(embed=animal_embed(response)) 157 | 158 | @animal.command(description="Show a picture of a shiba inu") 159 | async def shibe(self, ctx: discord.ApplicationContext): 160 | url = AnimalURLS.shibe.value 161 | image = await get_request(url) 162 | 163 | if image is None or not image: 164 | await ctx.respond(embed=animal_embed(None)) 165 | 166 | response = { 167 | "desc": "Certified good boi!", 168 | "image": image[0], 169 | "title": "Shiba Inu!", 170 | } 171 | await ctx.respond(embed=animal_embed(response)) 172 | 173 | @animal.command(description="Show a picture of a whale") 174 | async def whale(self, ctx: discord.ApplicationContext): 175 | url = AnimalURLS.whale.value 176 | image = await get_request(url) 177 | 178 | if image is None or not image: 179 | await ctx.respond(embed=animal_embed(None)) 180 | 181 | response = { 182 | "desc": "Big boi!", 183 | "image": image["link"], 184 | "title": "Whale!", 185 | } 186 | await ctx.respond(embed=animal_embed(response)) 187 | 188 | 189 | def setup(client): 190 | client.add_cog(Animals(client)) 191 | -------------------------------------------------------------------------------- /src/cogs/image/colors.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | import discord 4 | import aiohttp 5 | from PIL import Image 6 | from discord.ext import commands 7 | 8 | from core import BaseCog 9 | 10 | 11 | class Colors(BaseCog): 12 | @commands.slash_command(description="Get the colors in an image") 13 | async def get_colors( 14 | self, ctx: discord.ApplicationContext, file: discord.Attachment 15 | ): 16 | await ctx.defer() 17 | file = await file.read() 18 | async with aiohttp.ClientSession() as session: 19 | async with session.post( 20 | "https://api.fusionsid.xyz/api/image/get_colors/?show_hex=true", 21 | data={"image": file}, 22 | ) as resp: 23 | response = await resp.json() 24 | if resp.ok is False: 25 | return await ctx.respond( 26 | embed=discord.Embed( 27 | title="An error occured while trying to get the image", 28 | description=( 29 | "This could be because you didnt upload an image\n" 30 | "If not the API basically had a skill issue.\n" 31 | "If this persists and you are able to, report this as a bug with :)" 32 | ), 33 | color=discord.Colour.red(), 34 | ), 35 | ephemeral=True, 36 | ) 37 | 38 | palette_joined = "\n".join(response["palette"]) 39 | em = discord.Embed( 40 | title="Image Colors", 41 | description=f"**Dominant Color:** {response['dominant_color']}\n**Palette:**\n{palette_joined}", 42 | color=discord.Color.random(), 43 | ) 44 | 45 | palette_img = Image.new("RGB", (300, 200)) 46 | x, y = 0, 0 47 | for i in response["palette"]: 48 | try: 49 | palette_img.paste(Image.new("RGB", (100, 100), i), (x, y)) 50 | except ValueError: 51 | pass 52 | x += 100 53 | if x == 400: 54 | x = 0 55 | y += 100 56 | 57 | palette_file = io.BytesIO() 58 | palette_img.save(palette_file, "PNG") 59 | palette_file.seek(0) 60 | 61 | try: 62 | dcolor_img = Image.new("RGB", (150, 150), response["dominant_color"]) 63 | except ValueError: 64 | return await ctx.respond( 65 | embed=em, 66 | files=[ 67 | discord.File(palette_file, "palette.png"), 68 | ], 69 | ) 70 | 71 | dcolor_file = io.BytesIO() 72 | dcolor_img.save(dcolor_file, "PNG") 73 | dcolor_file.seek(0) 74 | 75 | await ctx.respond( 76 | embed=em, 77 | files=[ 78 | discord.File(dcolor_file, "dominant_color.png"), 79 | discord.File(palette_file, "palette.png"), 80 | ], 81 | ) 82 | 83 | 84 | def setup(client): 85 | client.add_cog(Colors(client)) 86 | -------------------------------------------------------------------------------- /src/cogs/image/other.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | import discord 4 | from discord.ext import commands 5 | 6 | from core import BaseCog 7 | from core.helpers import ImageAPIFail 8 | from core.helpers import get_request_bytes 9 | 10 | 11 | class ImageURLS(Enum): 12 | ## Some Random API Endpoints 13 | # overlays 14 | comrade = "https://some-random-api.ml/canvas/overlay/comrade" 15 | gay = "https://some-random-api.ml/canvas/overlay/gay" 16 | wasted = "https://some-random-api.ml/canvas/overlay/wasted" 17 | jail = "https://some-random-api.ml/canvas/overlay/jail" 18 | triggered = "https://some-random-api.ml/canvas/overlay/triggered" 19 | glass = "https://some-random-api.ml/canvas/overlay/glass" 20 | passed = "https://some-random-api.ml/canvas/overlay/passed" 21 | 22 | # filters 23 | blue = "https://some-random-api.ml/canvas/filter/blue" 24 | red = "https://some-random-api.ml/canvas/filter/red" 25 | blurple = "https://some-random-api.ml/canvas/filter/blurple" 26 | green = "https://some-random-api.ml/canvas/filter/green" 27 | invertgreyscale = "https://some-random-api.ml/canvas/filter/invertgreyscale" 28 | sepia = "https://some-random-api.ml/canvas/filter/sepia" 29 | color = "https://some-random-api.ml/canvas/filter/color" 30 | greyscale = "https://some-random-api.ml/canvas/filter/greyscale" 31 | brightness = "https://some-random-api.ml/canvas/filter/brightness" 32 | 33 | # misc 34 | youtube = "https://some-random-api.ml/canvas/misc/youtube-comment" 35 | blur = "https://some-random-api.ml/canvas/misc/blur" 36 | spin = "https://some-random-api.ml/canvas/misc/spin" 37 | circle = "https://some-random-api.ml/canvas/misc/circle" 38 | pixelate = "https://some-random-api.ml/canvas/misc/pixelate" 39 | lolice = "https://some-random-api.ml/canvas/misc/lolice" 40 | oogway = "https://some-random-api.ml/canvas/misc/oogway" 41 | heart = "https://some-random-api.ml/canvas/misc/heart" 42 | tweet = "https://some-random-api.ml/canvas/misc/tweet" 43 | horny = "https://some-random-api.ml/canvas/misc/horny" 44 | lied = "https://some-random-api.ml/canvas/misc/lied" 45 | nobitches = "https://some-random-api.ml/canvas/misc/nobitches" 46 | simpcard = "https://some-random-api.ml/canvas/misc/simpcard" 47 | stupid = "https://some-random-api.ml/canvas/misc/its-so-stupid" 48 | 49 | ## Other 50 | unsplash = "https://source.unsplash.com/random" 51 | 52 | 53 | class OtherImage(BaseCog): 54 | @commands.slash_command(description="Get a random image from unsplash") 55 | async def unsplash(self, ctx: discord.ApplicationContext, search_terms: str = None): 56 | url = ImageURLS.unsplash.value 57 | if search_terms is not None: 58 | url += f"?{search_terms}" 59 | 60 | response = await get_request_bytes(url, bytes_io=True) 61 | if response is None: 62 | raise ImageAPIFail 63 | 64 | await ctx.respond(file=discord.File(response, "image.png")) 65 | 66 | 67 | def setup(client): 68 | client.add_cog(OtherImage(client)) 69 | -------------------------------------------------------------------------------- /src/cogs/moderation/banning.py: -------------------------------------------------------------------------------- 1 | import re 2 | import discord 3 | from discord.ext import commands 4 | from discord.commands import default_permissions 5 | 6 | from core import BaseCog 7 | 8 | 9 | class Banning(BaseCog): 10 | @commands.slash_command(description="Ban a member from the server") 11 | @default_permissions(ban_members=True) 12 | @commands.bot_has_permissions(ban_members=True) 13 | @commands.has_permissions(ban_members=True) 14 | @commands.cooldown(1, 5, commands.BucketType.user) 15 | async def ban( 16 | self, ctx: discord.ApplicationContext, member: discord.Member, reason=None 17 | ): 18 | if ( 19 | ctx.author.top_role.position > member.top_role.position 20 | and (ctx.guild.get_member(self.client.user.id)).top_role.position 21 | > member.top_role.position 22 | ): 23 | if reason is not None: 24 | reason = f"{reason} - Requested by {ctx.author.name} ({ctx.author.id})" 25 | await member.ban( 26 | reason="".join( 27 | reason 28 | if reason is not None 29 | else f"Requested by {ctx.author} ({ctx.author.id})" 30 | ) 31 | ) 32 | await ctx.respond(f"Banned {member} successfully.") 33 | else: 34 | await ctx.respond( 35 | "Sorry, you cannot perform that action due to role hierarchy\nMake sure" 36 | " both you and the bot have higher perms then the target member" 37 | ) 38 | 39 | @commands.slash_command(description="Mass ban users from the server") 40 | @default_permissions(ban_members=True) 41 | @commands.bot_has_permissions(ban_members=True) 42 | @commands.has_permissions(ban_members=True) 43 | @commands.cooldown(1, 5, commands.BucketType.user) 44 | async def massban( 45 | self, 46 | ctx: discord.ApplicationContext, 47 | members: discord.Option(str, "User ids or ping users seperated by a space"), 48 | reason=None, 49 | ): 50 | try: 51 | members = [int(i) for i in re.sub("\\<|\\>|@", "", members).split(" ")] 52 | except ValueError: 53 | return await ctx.respond("Invalid input was provided", ephemeral=True) 54 | banned_members = [] 55 | for member in members: 56 | try: 57 | member = await self.client.fetch_user(member) 58 | except discord.NotFound: 59 | continue 60 | if ( 61 | ctx.author.top_role.position > member.top_role.position 62 | and (ctx.guild.get_member(self.client.user.id)).top_role.position 63 | > member.top_role.position 64 | ): 65 | if reason is not None: 66 | reason = ( 67 | f"{reason} - Requested by {ctx.author.name} ({ctx.author.id})" 68 | ) 69 | await member.ban( 70 | reason="".join( 71 | reason 72 | if reason is not None 73 | else f"Requested by {ctx.author} ({ctx.author.id})" 74 | ) 75 | ) 76 | banned_members.append(member) 77 | 78 | names = "- \n".join([member.name for member in banned_members]) 79 | em = discord.Embed( 80 | title="Banned Members", 81 | description=f"**Banned {len(banned_members)} members:**\n{names}", 82 | color=ctx.author.color, 83 | ) 84 | em.add_field( 85 | name=f"Could not ban {len(members)-len(banned_members)} members", 86 | value="- \n".join( 87 | [member.name for member in members if member not in banned_members] 88 | ), 89 | ) 90 | await ctx.respond(embed=em) 91 | 92 | @commands.slash_command(description="Kick a specific member from the server") 93 | @default_permissions(kick_members=True) 94 | @commands.has_permissions(kick_members=True) 95 | @commands.bot_has_permissions(kick_members=True) 96 | @commands.cooldown(1, 5, commands.BucketType.user) 97 | async def kick( 98 | self, ctx: discord.ApplicationContext, member: discord.Member, reason=None 99 | ): 100 | if ( 101 | ctx.author.top_role.position > member.top_role.position 102 | and (ctx.guild.get_member(self.client.user.id)).top_role.position 103 | > member.top_role.position 104 | ): 105 | if reason is not None: 106 | reason = f"{reason} - Requested by {ctx.author.name} ({ctx.author.id})" 107 | await member.kick( 108 | reason="".join( 109 | reason 110 | if reason is not None 111 | else f"Requested by {ctx.author} ({ctx.author.id})" 112 | ) 113 | ) 114 | await ctx.respond(f"Kicked {member} successfully.") 115 | else: 116 | await ctx.respond( 117 | "Sorry, you cannot perform that action due to role hierarchy\nMake sure" 118 | " both you and the bot have higher perms then the target member" 119 | ) 120 | 121 | @commands.slash_command(description="Mass kick members") 122 | @default_permissions(kick_members=True) 123 | @commands.bot_has_permissions(kick_members=True) 124 | @commands.has_permissions(kick_members=True) 125 | @commands.cooldown(1, 5, commands.BucketType.user) 126 | async def masskick( 127 | self, 128 | ctx: discord.ApplicationContext, 129 | members: discord.Option(str, "User ids or ping users seperated by a space"), 130 | reason=None, 131 | ): 132 | try: 133 | members = [int(i) for i in re.sub("\\<|\\>|@", "", members).split(" ")] 134 | except ValueError: 135 | return await ctx.respond("Invalid input was provided", ephemeral=True) 136 | kicked_members = [] 137 | for member in members: 138 | try: 139 | member = await self.client.fetch_user(member) 140 | except discord.NotFound: 141 | continue 142 | if ( 143 | ctx.author.top_role.position > member.top_role.position 144 | and (ctx.guild.get_member(self.client.user.id)).top_role.position 145 | > member.top_role.position 146 | ): 147 | if reason is not None: 148 | reason = ( 149 | f"{reason} - Requested by {ctx.author.name} ({ctx.author.id})" 150 | ) 151 | await member.kick( 152 | reason="".join( 153 | reason 154 | if reason is not None 155 | else f"Requested by {ctx.author} ({ctx.author.id})" 156 | ) 157 | ) 158 | kicked_members.append(member) 159 | 160 | names = "- \n".join([member.name for member in kicked_members]) 161 | em = discord.Embed( 162 | title="kickned Members", 163 | description=f"**Kicked {len(kicked_members)} members:**\n{names}", 164 | color=ctx.author.color, 165 | ) 166 | em.add_field( 167 | name=f"Could not kick {len(members)-len(kicked_members)} members", 168 | value="- \n".join( 169 | [member.name for member in members if member not in kicked_members] 170 | ), 171 | ) 172 | await ctx.respond(embed=em) 173 | 174 | 175 | def setup(client): 176 | client.add_cog(Banning(client)) 177 | -------------------------------------------------------------------------------- /src/cogs/owner/blacklist.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from discord.commands import SlashCommandGroup 4 | 5 | from core import WhyBot 6 | from core.helpers import UserAlreadyBlacklisted, UserAlreadyWhitelisted, GUILD_IDS 7 | 8 | 9 | class Blacklist(commands.Cog): 10 | def __init__(self, client): 11 | self.client: WhyBot = client 12 | 13 | blacklisted = SlashCommandGroup( 14 | "blacklist", "Commands for why bot blacklist management. OWNER ONLY" 15 | ) 16 | 17 | @blacklisted.command( 18 | name="blacklist", 19 | description="ban a user from using whybot", 20 | guild_ids=GUILD_IDS, 21 | ) 22 | @commands.is_owner() 23 | async def blacklist( 24 | self, 25 | ctx: discord.ApplicationContext, 26 | user_id: discord.Option(str, description="User id of user to ban"), 27 | ): 28 | """ 29 | This command is used to ban a user from using Why Bot 30 | 31 | Help Info: 32 | ---------- 33 | Category: Owner 34 | 35 | Usage: blacklist 36 | """ 37 | if not user_id.isnumeric(): 38 | return await ctx.respond( 39 | embed=discord.Embed( 40 | title="Invalid discord user id", 41 | description="Please provide an integer", 42 | color=ctx.author.color, 43 | ), 44 | ephemeral=True, 45 | ) 46 | 47 | user_id = int(user_id) 48 | 49 | try: 50 | await self.client.fetch_user(user_id) 51 | except ( 52 | discord.NotFound, 53 | discord.HTTPException, 54 | discord.ApplicationCommandInvokeError, 55 | ): 56 | return await ctx.respond( 57 | embed=discord.Embed( 58 | title="Something went wrong fetching the user", 59 | description=( 60 | "Most likely an invalid discord user id.\nPlease provide a real" 61 | " user" 62 | ), 63 | color=ctx.author.color, 64 | ), 65 | ephemeral=True, 66 | ) 67 | 68 | try: 69 | await self.client.blacklist_user(user_id) 70 | except UserAlreadyBlacklisted: 71 | return await ctx.respond( 72 | embed=discord.Embed( 73 | title="User Already Blacklisted", 74 | description=( 75 | "The user you tried to blacklist was already blacklisted." 76 | ), 77 | color=ctx.author.color, 78 | ), 79 | ephemeral=True, 80 | ) 81 | 82 | await ctx.respond( 83 | embed=discord.Embed( 84 | description="User blacklisted successfuly!", color=ctx.author.color 85 | ), 86 | ephemeral=True, 87 | ) 88 | 89 | @blacklisted.command( 90 | name="whitelist", 91 | description="unban a user from using whybot", 92 | guild_ids=GUILD_IDS, 93 | ) 94 | @commands.is_owner() 95 | async def whitelist( 96 | self, 97 | ctx: discord.ApplicationContext, 98 | user_id: discord.Option(str, description="User id of user to unban"), 99 | ): 100 | """ 101 | This command is used to unban a user from using Why Bot 102 | 103 | Help Info: 104 | ---------- 105 | Category: Owner 106 | 107 | Usage: whitelist 108 | """ 109 | if not user_id.isnumeric(): 110 | return await ctx.respond( 111 | embed=discord.Embed( 112 | title="Invalid discord user id", 113 | description="Please provide an integer", 114 | color=ctx.author.color, 115 | ), 116 | ephemeral=True, 117 | ) 118 | 119 | user_id = int(user_id) 120 | 121 | try: 122 | await self.client.fetch_user(user_id) 123 | except ( 124 | discord.NotFound, 125 | discord.HTTPException, 126 | discord.ApplicationCommandInvokeError, 127 | ): 128 | return await ctx.respond( 129 | embed=discord.Embed( 130 | title="Something went wrong fetching the user", 131 | description=( 132 | "Most likely an invalid discord user id.\nPlease provide a real" 133 | " user" 134 | ), 135 | color=ctx.author.color, 136 | ), 137 | ephemeral=True, 138 | ) 139 | 140 | try: 141 | await self.client.whitelist_user(user_id) 142 | except UserAlreadyWhitelisted: 143 | return await ctx.respond( 144 | embed=discord.Embed( 145 | title="User Already Whitelisted", 146 | description=( 147 | "The user you tried to whitelist was already whitelisted." 148 | ), 149 | color=ctx.author.color, 150 | ), 151 | ephemeral=True, 152 | ) 153 | 154 | await ctx.respond( 155 | embed=discord.Embed( 156 | description="User whitelisted successfuly!", color=ctx.author.color 157 | ), 158 | ephemeral=True, 159 | ) 160 | 161 | @blacklisted.command( 162 | guild_ids=GUILD_IDS, description="Check if a user is blacklisted" 163 | ) 164 | @commands.is_owner() 165 | async def isblacklisted( 166 | self, 167 | ctx: discord.ApplicationContext, 168 | user_id: discord.Option(str, description="User id of user to check") = None, 169 | ): 170 | users = await self.client.get_blacklisted_users(reasons=True) 171 | if user_id is not None: 172 | if not user_id.isnumeric(): 173 | return await ctx.respond( 174 | embed=discord.Embed( 175 | title="Invalid discord user id", 176 | description="Please provide an integer", 177 | color=ctx.author.color, 178 | ), 179 | ephemeral=True, 180 | ) 181 | 182 | user_id = int(user_id) 183 | for userid, reason in users: 184 | if userid == user_id: 185 | try: 186 | user = await self.client.fetch_user(user_id) 187 | except ( 188 | discord.NotFound, 189 | discord.HTTPException, 190 | discord.ApplicationCommandInvokeError, 191 | ): 192 | return await ctx.respond( 193 | embed=discord.Embed( 194 | title="Something went wrong fetching the user", 195 | description=( 196 | "Most likely an invalid discord user id.\nPlease" 197 | " provide a real user" 198 | ), 199 | color=ctx.author.color, 200 | ), 201 | ephemeral=True, 202 | ) 203 | 204 | return await ctx.respond( 205 | embed=discord.Embed( 206 | title=( 207 | f"User: {user.name}#{user.discriminator} ({user.id}) is" 208 | " blacklisted" 209 | ), 210 | description=f"Reason Provided: {reason}", 211 | color=ctx.author.color, 212 | ) 213 | ) 214 | embed = discord.Embed( 215 | title=f"User with id {user_id} is NOT blacklisted", 216 | color=ctx.author.color, 217 | ) 218 | return await ctx.respond(embed=embed) 219 | 220 | await ctx.respond( 221 | embed=discord.Embed( 222 | title="Blacklisted User IDs", 223 | description="\n".join(str(i[0]) for i in users), 224 | ) 225 | ) 226 | 227 | 228 | def setup(client): 229 | client.add_cog(Blacklist(client)) 230 | -------------------------------------------------------------------------------- /src/cogs/owner/cog_tools.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from discord.commands import SlashCommandGroup 4 | 5 | from core import WhyBot 6 | from core.helpers import GUILD_IDS 7 | 8 | 9 | class CogTools(commands.Cog): 10 | def __init__(self, client: WhyBot): 11 | self.client = client 12 | 13 | cogtools = SlashCommandGroup( 14 | "cogtools", "Commands for why bot cogs management. OWNER ONLY" 15 | ) 16 | 17 | @cogtools.command(guild_ids=GUILD_IDS, description="Reload a cog") 18 | @commands.is_owner() 19 | async def reload(self, ctx: discord.ApplicationContext, extension): 20 | """This command is used to reload a cog""" 21 | 22 | if extension not in self.client.cogs_list.keys(): 23 | return await ctx.respond( 24 | embed=discord.Embed( 25 | title="Cog doesn't exist or was not loaded", 26 | description="Use listcogs command to check cogs", 27 | color=ctx.author.color, 28 | ), 29 | ephemeral=True, 30 | ) 31 | 32 | self.client.reload_extension(self.client.cogs_list[extension]) 33 | embed = discord.Embed( 34 | title="Reload", 35 | description=f"{extension} successfully reloaded", 36 | color=ctx.author.color, 37 | ) 38 | await ctx.respond(embed=embed, ephemeral=True) 39 | 40 | @cogtools.command(guild_ids=GUILD_IDS, description="Load a cog") 41 | @commands.is_owner() 42 | async def load(self, ctx: discord.ApplicationContext, extension, name): 43 | """This command is used to load a cog""" 44 | 45 | try: 46 | self.client.load_extension(extension) 47 | except discord.ApplicationCommandInvokeError: 48 | await ctx.respond( 49 | embed=discord.Embed( 50 | title="Cog doesn't exist", 51 | description="Please provied path properly", 52 | color=ctx.author.color, 53 | ), 54 | ephemeral=True, 55 | ) 56 | self.client.cogs_list[name] = extension 57 | 58 | embed = discord.Embed( 59 | title="Load", 60 | description=f"{extension} successfully loaded", 61 | color=ctx.author.color, 62 | ) 63 | await ctx.respond(embed=embed, ephemeral=True) 64 | 65 | @cogtools.command(guild_ids=GUILD_IDS, description="Unload a cog") 66 | @commands.is_owner() 67 | async def unload(self, ctx: discord.ApplicationContext, extension): 68 | """This command is used to unload a cog""" 69 | 70 | if extension not in self.client.cogs_list.keys(): 71 | await ctx.respond( 72 | embed=discord.Embed( 73 | title="Cog doesn't exist or was not loaded", 74 | description="Use listcogs command to check cogs", 75 | color=ctx.author.color, 76 | ), 77 | ephemeral=True, 78 | ) 79 | 80 | self.client.unload_extension(self.client.cogs_list[extension]) 81 | self.client.cogs_list.pop(extension) 82 | 83 | embed = discord.Embed( 84 | title="Unload", 85 | description=f"{extension} successfully unloaded", 86 | color=ctx.author.color, 87 | ) 88 | await ctx.respond(embed=embed, ephemeral=True) 89 | 90 | @cogtools.command(guild_ids=GUILD_IDS, description="List all the cogs") 91 | @commands.is_owner() 92 | async def listcogs(self, ctx: discord.ApplicationContext): 93 | """This command lists the cogs that the bot has""" 94 | return await ctx.respond( 95 | embed=discord.Embed( 96 | title="Why Bot Cogs List", 97 | description="\n".join(self.client.cogs_list.keys()), 98 | color=ctx.author.color, 99 | ), 100 | ephemeral=True, 101 | ) 102 | 103 | @cogtools.command(guild_ids=GUILD_IDS, description="Reload all the cogs") 104 | @commands.is_owner() 105 | async def reloadall(self, ctx: discord.ApplicationContext): 106 | """This command is used to reload all the cogs""" 107 | 108 | cogs = self.client.cogs_list.values() 109 | 110 | for cog in cogs: 111 | self.client.reload_extension(cog) 112 | 113 | await ctx.respond( 114 | embed=discord.Embed(title="All Cogs Reloaded", color=ctx.author.color), 115 | ephemeral=True, 116 | ) 117 | 118 | 119 | def setup(client: WhyBot): 120 | client.add_cog(CogTools(client)) 121 | -------------------------------------------------------------------------------- /src/cogs/owner/dmreply.py: -------------------------------------------------------------------------------- 1 | import io 2 | import time 3 | import datetime 4 | 5 | import discord 6 | from discord.ext import commands 7 | 8 | from core import WhyBot 9 | from core.helpers import blacklist_check, GUILD_IDS 10 | from core.utils import format_seconds 11 | 12 | 13 | class DMReply(commands.Cog): 14 | """ 15 | This is the dmreply cog 16 | It is used to provide support to the users of the bot 17 | When a dm is sent to the bot it will be copied and sent to the dm reply channel 18 | The bot owner will have the option to reply to the message 19 | You can send images, videos messages etc 20 | """ 21 | 22 | def __init__(self, client: WhyBot): 23 | self.client = client 24 | 25 | dm_channel = self.client.config["dm_reply_channel"] 26 | if dm_channel in (0, None): 27 | self.dm_reply_channel = None 28 | self.dm_reply_channel = dm_channel 29 | 30 | self.cooldown = commands.CooldownMapping.from_cooldown( 31 | 10, 60, commands.BucketType.user 32 | ) 33 | 34 | async def is_member_cooldown(self, message: discord.Message): 35 | bucket = self.cooldown.get_bucket(message) 36 | cooldown = bucket.update_rate_limit() 37 | 38 | if cooldown is None: 39 | return False 40 | 41 | retry_after = await format_seconds(int(cooldown)) 42 | em = discord.Embed( 43 | title="Wow buddy, Slow it down\nYou are on cooldown from sending dms", 44 | description=( 45 | f"Try again {f'in **{retry_after}' if retry_after != '' else 'now'}**" 46 | ), 47 | color=discord.Color.red(), 48 | ) 49 | await message.reply(embed=em) 50 | 51 | return True 52 | 53 | @commands.Cog.listener() 54 | async def on_message(self, message: discord.Message): 55 | """The on message event that handles the dm's""" 56 | 57 | if message.author.bot: 58 | return 59 | 60 | # this should only run once 61 | if self.dm_reply_channel is not None and isinstance(self.dm_reply_channel, int): 62 | try: 63 | self.dm_reply_channel = await self.client.fetch_channel( 64 | self.dm_reply_channel 65 | ) 66 | except discord.errors.NotFound: 67 | self.dm_reply_channel = None 68 | 69 | if not await blacklist_check(message.author.id): 70 | return 71 | 72 | # check if in dm / thread 73 | if not isinstance(message.channel, discord.DMChannel): 74 | if ( 75 | not isinstance(message.channel, discord.Thread) 76 | or message.author.id != self.client.owner_id 77 | ): 78 | return 79 | 80 | data = await self.client.db.fetch( 81 | "SELECT * FROM dmreply WHERE thread_id=$1", message.channel.id 82 | ) 83 | if not data: 84 | return 85 | 86 | person = await self.client.fetch_user(data[0][0]) 87 | 88 | if message.content is not None and message.content != "": 89 | await person.send(message.content) 90 | await message.add_reaction("✅") 91 | 92 | if message.attachments is not None: 93 | for attachment in message.attachments: 94 | return await person.send(attachment.url) 95 | return 96 | 97 | # If user is on cooldown 98 | if await self.is_member_cooldown(message): 99 | return 100 | 101 | # if error getting dm reply channel or not set 102 | if self.dm_reply_channel is None: 103 | return await message.channel.send( 104 | "The owner has disabled the dm reply feature from the bot" 105 | ) 106 | 107 | channel = self.dm_reply_channel 108 | author = message.author 109 | thread_id = await self.client.db.fetch( 110 | "SELECT * FROM dmreply WHERE user_id=$1", author.id 111 | ) 112 | 113 | if not thread_id: 114 | thread_id = None 115 | 116 | thread = None 117 | 118 | if thread_id is not None: 119 | try: 120 | thread = channel.get_thread(thread_id[0][1]) 121 | except discord.NotFound: 122 | thread = None 123 | if thread is None: 124 | await self.client.db.execute( 125 | "DELETE FROM dmreply WHERE user_id=$1", author.id 126 | ) 127 | 128 | if thread is None: 129 | emb = discord.Embed( 130 | title=author.name, 131 | color=discord.Color.random(), 132 | timestamp=datetime.datetime.now(), 133 | ) 134 | emb.set_thumbnail(url=author.avatar.url) 135 | 136 | emb.add_field(name="User ID:", value=author.id, inline=False) 137 | emb.add_field( 138 | name="Created Account:", 139 | value=f"", 140 | inline=False, 141 | ) 142 | 143 | shared_guilds = [ 144 | guild.name for guild in self.client.guilds if author in guild.members 145 | ] 146 | emb.add_field( 147 | name=f"Shared Guilds: ({len(shared_guilds)})", 148 | value=", ".join(shared_guilds), 149 | ) 150 | 151 | message_to_create_thread = await channel.send(embed=emb) 152 | thread = await message_to_create_thread.create_thread(name=author.name) 153 | await self.client.db.execute( 154 | "INSERT INTO dmreply (user_id, thread_id) VALUES ($1, $2)", 155 | author.id, 156 | thread.id, 157 | ) 158 | if message.content != "" and message.content is not None: 159 | await thread.send(message.content) 160 | 161 | if message.attachments is not None: 162 | for attachment in message.attachments: 163 | await thread.send(attachment.url) 164 | 165 | @commands.slash_command( 166 | guild_ids=GUILD_IDS, description="Ban someone from DMing the bot" 167 | ) 168 | @commands.is_owner() 169 | async def dm_ban(self, ctx: discord.ApplicationContext, _id: int): 170 | """TODO""" 171 | 172 | raise NotImplementedError 173 | 174 | @commands.slash_command( 175 | guild_ids=GUILD_IDS, description="Unban someone from DMing the bot" 176 | ) 177 | @commands.is_owner() 178 | async def dm_unban(self, ctx: discord.ApplicationContext, _id: int): 179 | """TODO""" 180 | 181 | raise NotImplementedError 182 | 183 | @commands.slash_command(guild_ids=GUILD_IDS, description="Close a thread of dms") 184 | @commands.is_owner() 185 | async def close_thread( 186 | self, ctx: discord.ApplicationContext, author_id: str, archive: bool = False 187 | ): 188 | try: 189 | author_id = int(author_id) 190 | except ValueError: 191 | return await ctx.respond("Not found") 192 | 193 | thread_id = await self.client.db.fetch( 194 | "SELECT * FROM dmreply WHERE user_id=$1", author_id 195 | ) 196 | 197 | if not thread_id: 198 | thread_id = None 199 | 200 | if thread_id is not None: 201 | try: 202 | thread: discord.Thread = self.dm_reply_channel.get_thread( 203 | thread_id[0][1] 204 | ) 205 | except discord.NotFound: 206 | return await ctx.respond("Not found") 207 | 208 | messages = "\n".join( 209 | [ 210 | f"{message.author.name}: {message.content}\n---" 211 | async for message in thread.history(oldest_first=True) 212 | if message.content is not None or message.content != "" 213 | ] 214 | ) 215 | file = io.BytesIO(messages.encode()) 216 | await ctx.respond(file=discord.File(file, "messages.txt"), ephemeral=True) 217 | 218 | if archive: 219 | return await thread.archive() 220 | 221 | await thread.delete() 222 | 223 | 224 | def setup(client: WhyBot): 225 | client.add_cog(DMReply(client)) 226 | -------------------------------------------------------------------------------- /src/cogs/owner/errors.py: -------------------------------------------------------------------------------- 1 | import os 2 | import datetime 3 | 4 | import discord 5 | import aiofiles 6 | from discord.commands import SlashCommandGroup 7 | from discord.ext import commands 8 | 9 | import __main__ 10 | from core import WhyBot 11 | from core.helpers import ErrorView, get_last_errors, GUILD_IDS 12 | 13 | LOGFILE_PATH = os.path.join(os.path.dirname(__main__.__file__), "logfiles/main.log") 14 | 15 | 16 | class ErrorLog(commands.Cog): 17 | def __init__(self, client: WhyBot): 18 | self.client = client 19 | 20 | error = SlashCommandGroup( 21 | "errors", "Commands for why bot error management. OWNER ONLY" 22 | ) 23 | 24 | @error.command(guild_ids=GUILD_IDS, description="Send the whole logs file") 25 | @commands.is_owner() 26 | async def logs_file(self, ctx: discord.ApplicationContext): 27 | file = discord.File(LOGFILE_PATH, "main.log") 28 | await ctx.respond(file=file, ephemeral=True) 29 | 30 | @error.command(guild_ids=GUILD_IDS, description="Clear the logs file") 31 | @commands.is_owner() 32 | async def clear_logs_file(self, ctx: discord.ApplicationContext): 33 | async with aiofiles.open(LOGFILE_PATH, "r+") as f: 34 | await f.truncate(0) 35 | await ctx.respond("Logfile Cleared", ephemeral=True) 36 | 37 | @error.command( 38 | guild_ids=GUILD_IDS, description="Get the last error from the log file" 39 | ) 40 | @commands.is_owner() 41 | async def get_last_error(self, ctx: discord.ApplicationContext, limit: int = 1): 42 | """This command is used to get the most recent errors/error that the bot logged to the log file""" 43 | 44 | await ctx.defer() 45 | if limit >= 24: 46 | return await ctx.respond( 47 | "To big of a number", 48 | ephemeral=True, 49 | ) 50 | 51 | errors = await get_last_errors(count=limit) 52 | 53 | if errors is None: 54 | return await ctx.respond( 55 | "No recent error (you probably cleaned the file recently)", 56 | ephemeral=True, 57 | ) 58 | 59 | em = discord.Embed( 60 | title=f"Last {str(limit)+' Errors' if limit > 1 else 'Error'}", 61 | color=ctx.author.color, 62 | timestamp=datetime.datetime.utcnow(), 63 | ) 64 | if limit == 1: 65 | title = list(errors.keys())[0] 66 | err = list(errors.values())[0] 67 | em.description = f"**{title[:220]}**```py\n{err[:1900]}```" 68 | 69 | view = ErrorView(self.client.owner_id, f"{title}{err}") 70 | return await ctx.respond(embed=em, ephemeral=True, view=view) 71 | 72 | for key, value in errors.items(): 73 | em.add_field( 74 | name=f"{key[:220]} **(read logfile for full)**" 75 | if len(key) >= 220 76 | else key, 77 | value=f"```py\n{value[:980]}```\n**(read logfile for full)**" 78 | if len(value) >= 980 79 | else f"```py\n{value}```", 80 | inline=False, 81 | ) 82 | 83 | await ctx.respond(embed=em, ephemeral=True) 84 | 85 | 86 | def setup(client: WhyBot): 87 | client.add_cog(ErrorLog(client)) 88 | -------------------------------------------------------------------------------- /src/cogs/owner/ipc.py: -------------------------------------------------------------------------------- 1 | from pycord.ext import ipc 2 | from discord.ext import commands 3 | 4 | from core.models import WhyBot 5 | 6 | 7 | class IPCRoutes(commands.Cog): 8 | def __init__(self, client: WhyBot): 9 | self.client = client 10 | 11 | @ipc.server.route() 12 | async def get_member_count(self, data: dict): 13 | guild = await self.client.fetch_guild(data.guild_id) 14 | return guild.member_count 15 | 16 | 17 | def setup(client): 18 | client.add_cog(IPCRoutes(client)) 19 | -------------------------------------------------------------------------------- /src/cogs/owner/server.py: -------------------------------------------------------------------------------- 1 | import time 2 | import datetime 3 | 4 | import discord 5 | from discord.ext import commands 6 | 7 | from core import WhyBot 8 | from core.utils import chunkify 9 | from core.helpers import GUILD_IDS 10 | 11 | 12 | class Server(commands.Cog): 13 | def __init__(self, client: WhyBot): 14 | self.client = client 15 | 16 | @commands.slash_command( 17 | guild_ids=GUILD_IDS, description="List the servers that the bot is in" 18 | ) 19 | @commands.is_owner() 20 | async def server_list(self, ctx: discord.ApplicationContext): 21 | """ 22 | This command is used to list the servers the bots in 23 | It makes an embed with a list of the servers 24 | """ 25 | em = discord.Embed( 26 | title=f"Connected on {str(len(self.client.guilds))} servers:", 27 | color=ctx.author.color, 28 | timestamp=datetime.datetime.utcnow(), 29 | ) 30 | 31 | if len(self.client.guilds) < 10: 32 | for guild in self.client.guilds: 33 | em.add_field( 34 | name=guild.name, 35 | value=( 36 | f"Owner: {guild.owner.name}\nMembers: {guild.member_count}\nID:" 37 | f" {guild.id}" 38 | ), 39 | inline=True, 40 | ) 41 | return await ctx.respond(embed=em) 42 | 43 | chunked_list = await chunkify(self.client.guilds) 44 | 45 | em = discord.Embed( 46 | title=f"Connected on {str(len(self.client.guilds))} servers:", 47 | color=ctx.author.color, 48 | timestamp=datetime.datetime.utcnow(), 49 | ) 50 | 51 | await ctx.send(embed=em) 52 | for chunk in chunked_list: 53 | em = discord.Embed( 54 | title="** **", 55 | color=ctx.author.color, 56 | timestamp=datetime.datetime.utcnow(), 57 | ) 58 | for guild in chunk: 59 | em.add_field( 60 | name=guild.name, 61 | value=( 62 | f"Owner: {guild.owner.name}\nMembers: {guild.member_count}\nID:" 63 | f" {guild.id}" 64 | ), 65 | inline=True, 66 | ) 67 | await ctx.respond(embed=em) 68 | 69 | @commands.slash_command(guild_ids=GUILD_IDS, description="Fetch info for a server") 70 | @commands.is_owner() 71 | async def fetch_server_info(self, ctx: discord.ApplicationContext, server_id: int): 72 | """This command is used to get info on a server that the bot is in""" 73 | 74 | guild = self.client.get_guild(server_id) 75 | 76 | em = discord.Embed( 77 | title="Server Info:", 78 | description=f"For: {guild.name}", 79 | color=ctx.author.color, 80 | ) 81 | em.add_field(name="Member Count:", value=guild.member_count, inline=False) 82 | em.add_field( 83 | name="Created: ", 84 | value=f"", 85 | inline=False, 86 | ) 87 | em.add_field(name="ID:", value=guild.id, inline=False) 88 | 89 | em.set_thumbnail(url=guild.icon.url) 90 | em.set_author( 91 | name=f"Guild Owner: {guild.owner.name}", icon_url=guild.owner.avatar.url 92 | ) 93 | 94 | await ctx.respond(embed=em) 95 | 96 | @commands.slash_command( 97 | description="fetch user info", 98 | guild_ids=GUILD_IDS, 99 | ) 100 | @commands.is_owner() 101 | async def fetch_user_info(self, ctx: discord.ApplicationContext, user: str): 102 | """ 103 | This command is used to fetch info a specific user. 104 | It is useful if you are messaging someone in dmreply and want to know who you are messaging 105 | """ 106 | 107 | try: 108 | user = await self.client.fetch_user(int(user)) 109 | except discord.NotFound: 110 | return 111 | 112 | emb = discord.Embed( 113 | title=user.name, 114 | color=discord.Color.random(), 115 | timestamp=datetime.datetime.now(), 116 | ) 117 | emb.set_thumbnail(url=user.avatar.url) 118 | 119 | emb.add_field(name="User ID:", value=user.id, inline=False) 120 | emb.add_field( 121 | name="Created Account:", 122 | value=f"", 123 | inline=False, 124 | ) 125 | 126 | shared_guilds = [ 127 | guild.name for guild in self.client.guilds if user in guild.members 128 | ] 129 | emb.add_field( 130 | name=f"Shared Guilds: ({len(shared_guilds)})", 131 | value=", ".join(shared_guilds), 132 | ) 133 | 134 | emb.timestamp = datetime.datetime.now() 135 | 136 | data = await self.client.db.fetch( 137 | "SELECT * FROM command_stats WHERE user_id=$1", user.id 138 | ) 139 | if data: 140 | usage = sum(i[3] for i in data) 141 | emb.add_field( 142 | name="Command Usage", 143 | value=f"This user has used the bot {usage} times", 144 | inline=False, 145 | ) 146 | else: 147 | emb.add_field( 148 | name="Why Bot Usage", 149 | value="This user has not used any why bot commands", 150 | inline=False, 151 | ) 152 | 153 | await ctx.respond(embed=emb) 154 | 155 | 156 | def setup(client): 157 | client.add_cog(Server(client)) 158 | -------------------------------------------------------------------------------- /src/cogs/programming/runcode.py: -------------------------------------------------------------------------------- 1 | import io 2 | from contextlib import redirect_stdout 3 | 4 | import aiohttp 5 | import discord 6 | from aioconsole import aexec 7 | from discord.ext import commands 8 | 9 | from core import BaseCog 10 | from core.helpers import GUILD_IDS, post_request, InputModalView 11 | 12 | 13 | class RunCode(BaseCog): 14 | @commands.slash_command() 15 | @commands.cooldown(1, 15, commands.BucketType.user) 16 | async def zprol(self, ctx: discord.ApplicationContext): 17 | modal = InputModalView(label="Please enter the code:", title="Code Input") 18 | await ctx.send_modal(modal) 19 | await modal.wait() 20 | 21 | if modal.value is None: 22 | return await ctx.respond("Invalid Input", ephemeral=True) 23 | 24 | async with aiohttp.ClientSession() as session: 25 | async with session.post( 26 | "https://zprol.epicpix.ga/api/v1/run", 27 | json={"code": modal.value}, 28 | ) as resp: 29 | try: 30 | response = await resp.json() 31 | except aiohttp.ContentTypeError: 32 | response = None 33 | 34 | if response is None or response.get("compilation") is None: 35 | em = discord.Embed( 36 | title="zProl", 37 | description="Something went wrong!\n(API probably had a skill issue)", 38 | color=discord.Color.blue(), 39 | ) 40 | return await ctx.respond(embed=em) 41 | 42 | em = discord.Embed( 43 | title="zProl Code Output", 44 | color=discord.Color.blue(), 45 | ) 46 | 47 | if response["compilation"]["stderr"] != "": 48 | em.description = ( 49 | f"**Compilation result:**```{response['compilation']['stderr']}```" 50 | ) 51 | em.color = discord.Color.red() 52 | elif response["compilation"]["stdout"] != "": 53 | em.description = ( 54 | f"**Compilation result:**```{response['compilation']['stdout']}```" 55 | ) 56 | 57 | # If the program produced output 58 | if response.get("run") is not None and response.get("run") != "": 59 | em.add_field(name="Program Output:", value=f"```\n{response['run']}\n```") 60 | 61 | await ctx.respond(embed=em) 62 | 63 | @commands.slash_command(description="Run code in the rickroll programming language") 64 | @commands.cooldown(1, 15, commands.BucketType.user) 65 | async def ricklang(self, ctx: discord.ApplicationContext): 66 | modal = InputModalView(label="Please enter the code:", title="Code Input") 67 | await ctx.send_modal(modal) 68 | await modal.wait() 69 | 70 | if modal.value is None: 71 | return await ctx.respond("Invalid Input", ephemeral=True) 72 | 73 | response = await post_request( 74 | "https://api.fusionsid.xyz/api/runcode", 75 | body={"code": modal.value, "language": "rickroll_lang"}, 76 | ) 77 | if response is None: 78 | em = discord.Embed( 79 | title="Rickroll-Lang", 80 | description="Something went wrong!\n(API probably had a skill issue)", 81 | color=discord.Color.blue(), 82 | ) 83 | return await ctx.respond(embed=em) 84 | 85 | em = discord.Embed( 86 | title="Output", 87 | color=discord.Color.blue(), 88 | description=f"""```\n{response['stdout']}\n```""", 89 | ) 90 | 91 | await ctx.respond(embed=em) 92 | 93 | @commands.slash_command( 94 | guild_ids=GUILD_IDS, description="Run code in the rickroll programming language" 95 | ) 96 | @commands.is_owner() 97 | @commands.cooldown(1, 15, commands.BucketType.user) 98 | async def exec_code(self, ctx: discord.ApplicationContext): 99 | # this command dangerous af so second check just to make sure: 100 | if ctx.author.id != self.client.owner_id: 101 | raise commands.NotOwner 102 | 103 | modal = InputModalView(label="Please enter the code:", title="Code Input") 104 | await ctx.send_modal(modal) 105 | await modal.wait() 106 | 107 | if modal.value is None: 108 | return await ctx.respond("Invalid Input", ephemeral=True) 109 | 110 | locals = { 111 | "ctx": ctx, 112 | "client": self.client, 113 | } 114 | 115 | # Run the code 116 | stdout = io.StringIO() 117 | stderr = None 118 | with redirect_stdout(stdout): 119 | try: 120 | await aexec(modal.value, locals) 121 | except Exception as err: 122 | stderr = err 123 | output = stdout.getvalue() 124 | 125 | em = discord.Embed( 126 | title="Code Output:", 127 | description=f"```bash\n{output if output else 'No Stdout'}\n```", 128 | color=discord.Color.random(), 129 | ) 130 | if stderr is not None: 131 | em.add_field( 132 | name="Stderr:", 133 | value="```bash\n{}: {}\n```".format(type(stderr).__name__, stderr), 134 | ) 135 | 136 | await ctx.respond(embed=em) 137 | 138 | 139 | def setup(client): 140 | client.add_cog(RunCode(client)) 141 | -------------------------------------------------------------------------------- /src/cogs/roles/roles.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import discord 4 | from discord.ext import commands 5 | from discord.commands import default_permissions 6 | 7 | from core import WhyBot, BaseCog 8 | 9 | 10 | class Roles(BaseCog): 11 | def __init__(self, client: WhyBot): 12 | self.edit_role_mentions = discord.AllowedMentions( 13 | users=False, everyone=False, roles=False, replied_user=False 14 | ) 15 | super().__init__(client) 16 | 17 | @commands.slash_command( 18 | name="addrole", description="gives role / roles to a member" 19 | ) 20 | @default_permissions(manage_roles=True) 21 | @commands.has_permissions(manage_roles=True) 22 | @commands.bot_has_permissions(manage_roles=True) 23 | async def addrole( 24 | self, 25 | ctx: discord.ApplicationContext, 26 | member: discord.Member, 27 | roles: str, 28 | ): 29 | 30 | try: 31 | roles = list(map(int, re.findall("\\d+", roles))) 32 | except ValueError: 33 | return await ctx.respond("Invalid input was provided", ephemeral=True) 34 | 35 | fetched_roles = [] 36 | for role in roles: 37 | fetched_role = ctx.guild.get_role(role) 38 | if fetched_role is None: 39 | continue 40 | 41 | conditions = [ 42 | ctx.author.top_role.position <= member.top_role.position, 43 | (ctx.guild.get_member(self.client.user.id)).top_role.position 44 | <= member.top_role.position, 45 | ctx.author.top_role.position <= fetched_role.position, 46 | (ctx.guild.get_member(self.client.user.id)).top_role.position 47 | <= fetched_role.position, 48 | ] 49 | if any(conditions): 50 | continue 51 | 52 | fetched_roles.append(fetched_role) 53 | 54 | if len(fetched_roles) == 0: 55 | return await ctx.respond( 56 | "Unable to give any roles because of permissions", ephemeral=True 57 | ) 58 | 59 | await member.add_roles(*fetched_roles) 60 | 61 | em = discord.Embed( 62 | title="Role Given" if len(fetched_roles) == 1 else "Roles Given", 63 | description=f"Member: {member.mention} has been given role: {fetched_roles[0].mention}" 64 | if len(fetched_roles) == 1 65 | else f"Member: {member.mention} has been given roles: {''.join([role.mention for role in fetched_roles])}", 66 | color=discord.Color.random(), 67 | ) 68 | em.set_footer( 69 | text=f"Role given by {ctx.author.name}" 70 | if len(roles) == 1 71 | else f"Roles given by {ctx.author.name}" 72 | ) 73 | 74 | await ctx.respond(embed=em, allowed_mentions=self.edit_role_mentions) 75 | 76 | @commands.slash_command( 77 | name="removerole", description="removes role / roles from a member" 78 | ) 79 | @default_permissions(manage_roles=True) 80 | @commands.has_permissions(manage_roles=True) 81 | @commands.bot_has_permissions(manage_roles=True) 82 | async def removerole( 83 | self, 84 | ctx: discord.ApplicationContext, 85 | member: discord.Member, 86 | roles: str, 87 | ): 88 | try: 89 | roles = list(map(int, re.findall("\\d+", roles))) 90 | except ValueError: 91 | return await ctx.respond("Invalid input was provided", ephemeral=True) 92 | 93 | fetched_roles = [] 94 | for role in roles: 95 | fetched_role = ctx.guild.get_role(role) 96 | if fetched_role is None: 97 | continue 98 | 99 | conditions = [ 100 | ctx.author.top_role.position <= member.top_role.position, 101 | (ctx.guild.get_member(self.client.user.id)).top_role.position 102 | <= member.top_role.position, 103 | ctx.author.top_role.position <= fetched_role.position, 104 | (ctx.guild.get_member(self.client.user.id)).top_role.position 105 | <= fetched_role.position, 106 | ] 107 | if any(conditions): 108 | continue 109 | 110 | fetched_roles.append(fetched_role) 111 | 112 | if len(fetched_roles) == 0: 113 | return await ctx.respond( 114 | "Unable to remove any roles because of permissions", ephemeral=True 115 | ) 116 | 117 | await member.remove_roles(*fetched_roles) 118 | 119 | em = discord.Embed( 120 | title="Role Removed" if len(fetched_roles) == 1 else "Roles Removed", 121 | description=f"Member: {member.mention} has had these role removed: {fetched_roles[0].mention}" 122 | if len(fetched_roles) == 1 123 | else f"Member: {member.mention} has had these roles removed: \ 124 | {''.join([role.mention for role in fetched_roles])}", 125 | color=discord.Color.random(), 126 | ) 127 | em.set_footer( 128 | text=f"Role removed by {ctx.author.name}" 129 | if len(roles) == 1 130 | else f"Roles removed by {ctx.author.name}" 131 | ) 132 | 133 | await ctx.respond(embed=em, allowed_mentions=self.edit_role_mentions) 134 | 135 | 136 | def setup(client): 137 | client.add_cog(Roles(client)) 138 | -------------------------------------------------------------------------------- /src/cogs/utilities/tags.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Optional 3 | 4 | import discord 5 | from discord.ext import commands 6 | from discord.commands import SlashCommandGroup 7 | 8 | from core import BaseCog 9 | from core.models import Tag 10 | from core.helpers import InputModalView 11 | from core.utils import discord_timestamp 12 | 13 | 14 | class Tags(BaseCog): 15 | tags = SlashCommandGroup("tags", "Command related to the tags plugin") 16 | 17 | async def __get_tag_by_name(self, tag_name: str, guild_id: int) -> Optional[Tag]: 18 | tag = await self.client.db.fetch( 19 | "SELECT * FROM tags WHERE guild_id=$1 AND tag_name=$2", 20 | guild_id, 21 | tag_name, 22 | ) 23 | if not tag: 24 | return None 25 | 26 | return Tag(*tag[0][1:]) 27 | 28 | @tags.command(description="Create a new tag") 29 | @commands.has_permissions(administrator=True) 30 | async def create( 31 | self, 32 | ctx: discord.ApplicationContext, 33 | name: str, 34 | ): 35 | tag_name = name.lower() 36 | 37 | # check 38 | if await self.__get_tag_by_name(tag_name, ctx.guild.id) is not None: 39 | return await ctx.respond( 40 | embed=discord.Embed( 41 | title="Name Conflict!", 42 | description="Tag with this name already exists!\n\ 43 | If you want you can edit the tag with the command", 44 | color=discord.Color.red(), 45 | ), 46 | ephemeral=True, 47 | ) 48 | 49 | # get tags value 50 | input = InputModalView( 51 | title="Tag Value", label="Please enter the value of the tag:" 52 | ) 53 | await ctx.send_modal(input) 54 | await input.wait() 55 | 56 | if input.value is None: 57 | return await ctx.respond( 58 | "Not creating tag as input was either None or invalid", ephemeral=True 59 | ) 60 | 61 | # create tag 62 | await self.client.db.execute( 63 | """INSERT INTO tags ( 64 | guild_id, tag_name, tag_value, tag_author, time_created 65 | ) VALUES ($1, $2, $3, $4, $5)""", 66 | ctx.guild.id, 67 | tag_name, 68 | input.value, 69 | ctx.author.name, 70 | int(time.time()), 71 | ) 72 | 73 | await ctx.respond( 74 | "Tag Created Successfully! It can be viewed with the command\nIt will look like this:", 75 | embed=discord.Embed( 76 | title=tag_name, description=input.value, color=discord.Color.random() 77 | ), 78 | ) 79 | 80 | @tags.command(description="Delete an existing tag") 81 | @commands.has_permissions(administrator=True) 82 | async def delete(self, ctx: discord.ApplicationContext, name: str): 83 | name = name.lower() 84 | if await self.__get_tag_by_name(name, ctx.guild.id) is None: 85 | return await ctx.respond( 86 | embed=discord.Embed( 87 | title="Tag doesn't exist!", 88 | description="Tag with this name does not exist!\nYou can check tags on\ 89 | this server with the command or create one with ", 90 | color=discord.Color.red(), 91 | ), 92 | ephemeral=True, 93 | ) 94 | 95 | await self.client.db.execute( 96 | "DELETE FROM tags WHERE guild_id=$1 AND tag_name=$2", ctx.guild.id, name 97 | ) 98 | await ctx.respond( 99 | embed=discord.Embed( 100 | title="Tag Deleted", 101 | description=f"Tag: `{name}` was successfuly deleted!", 102 | color=discord.Color.green(), 103 | ) 104 | ) 105 | 106 | @tags.command(description="List the tags on this server") 107 | async def list(self, ctx: discord.ApplicationContext): 108 | tags = await self.client.db.fetch( 109 | "SELECT * FROM tags WHERE guild_id=$1", ctx.guild.id 110 | ) 111 | if not tags: 112 | return await ctx.respond( 113 | embed=discord.Embed( 114 | title="Tags", 115 | description="This guild has no tags\nCreate one with ", 116 | color=discord.Color.random(), 117 | ) 118 | ) 119 | 120 | return await ctx.respond( 121 | embed=discord.Embed( 122 | title="Tags", 123 | description=", ".join( 124 | map(lambda tag: f"`{tag[2]}`", tags) 125 | ), # tag[2] = tag_name 126 | color=discord.Color.random(), 127 | ) 128 | ) 129 | 130 | @tags.command(description="Edit the value of an existing tag") 131 | @commands.has_permissions(administrator=True) 132 | async def edit(self, ctx: discord.ApplicationContext, name: str): 133 | tag_name = name.lower() 134 | 135 | if await self.__get_tag_by_name(tag_name, ctx.guild.id) is None: 136 | return await ctx.respond( 137 | embed=discord.Embed( 138 | title="Tag doesnt exist!", 139 | description="Tag with this name does not exist!\n\ 140 | If you want you can create the tag with the command", 141 | color=discord.Color.red(), 142 | ), 143 | ephemeral=True, 144 | ) 145 | 146 | # get tags value 147 | input = InputModalView( 148 | title="Tag New Value", label="Please enter the new value of the tag:" 149 | ) 150 | await ctx.send_modal(input) 151 | await input.wait() 152 | 153 | if input.value is None: 154 | return await ctx.respond( 155 | "Not editing tag as input was either None or invalid", ephemeral=True 156 | ) 157 | 158 | # create tag 159 | await self.client.db.execute( 160 | "UPDATE tags SET tag_value=$1 WHERE guild_id=$2 AND tag_name=$3", 161 | input.value, 162 | ctx.guild.id, 163 | tag_name, 164 | ) 165 | 166 | await ctx.respond( 167 | "Tag Modified! It will now look like this:", 168 | embed=discord.Embed( 169 | title=tag_name, description=input.value, color=discord.Color.random() 170 | ), 171 | ) 172 | 173 | @commands.slash_command(description="Show the value of a tag") 174 | async def tag(self, ctx: discord.ApplicationContext, name: str): 175 | name = name.lower() 176 | tag = await self.__get_tag_by_name(name, ctx.guild.id) 177 | if tag is None: 178 | return await ctx.respond( 179 | embed=discord.Embed( 180 | title="Tag doesn't exist!", 181 | description="Tag with this name does not exist!\nYou can check tags on this\ 182 | server with the command or create one with ", 183 | color=discord.Color.red(), 184 | ), 185 | ephemeral=True, 186 | ) 187 | 188 | timestamp = discord_timestamp(tag.time_created, "ts") 189 | em = discord.Embed( 190 | title=tag.tag_name, 191 | description=f"{tag.tag_value}\n\nTag created: {timestamp}", 192 | color=discord.Color.random(), 193 | ) 194 | em.set_footer(text=f"Tag created by {tag.tag_author}") 195 | await ctx.respond(embed=em) 196 | 197 | 198 | def setup(client): 199 | client.add_cog(Tags(client)) 200 | -------------------------------------------------------------------------------- /src/cogs/utilities/utilities.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | import qrcode 4 | import discord 5 | from discord.ext import commands 6 | from discord.commands import SlashCommandGroup 7 | 8 | from core import BaseCog 9 | from core.helpers import CalculatorView 10 | from core.utils import slow_safe_calculate 11 | 12 | 13 | class Utilities(BaseCog): 14 | utilities = SlashCommandGroup("utilities", "Utility Commands") 15 | 16 | @utilities.command( 17 | name="calculator", description="Open an interactive button calculator" 18 | ) 19 | async def calculator(self, ctx: discord.ApplicationContext): 20 | """This command is used to show an interactive button calculator""" 21 | 22 | await ctx.defer() 23 | 24 | view = CalculatorView(ctx) 25 | await ctx.respond("``` ```", view=view) 26 | 27 | @utilities.command(description="Calculate an expression and give the result") 28 | async def calculate(self, ctx: discord.ApplicationContext, expression: str): 29 | em = discord.Embed( 30 | title="Calculation Result", 31 | description=f"**Expression:**\n{expression}", 32 | color=discord.Color.random(), 33 | ) 34 | 35 | result = await slow_safe_calculate(expression) 36 | em.add_field(name="Result", value=result) 37 | 38 | await ctx.respond(embed=em) 39 | 40 | @utilities.command(name="invite", description="Create an invite for the server") 41 | @commands.has_permissions(create_instant_invite=True) 42 | @commands.bot_has_permissions(create_instant_invite=True) 43 | async def invite( 44 | self, 45 | ctx: discord.ApplicationContext, 46 | expire_in: str = None, 47 | max_uses: str = None, 48 | ): 49 | """This command is used to make an invite for the server""" 50 | 51 | expire_in = 0 if expire_in is not None else expire_in 52 | max_uses = 0 if max_uses is not None else max_uses 53 | 54 | link = await ctx.channel.create_invite(max_age=expire_in, max_uses=max_uses) 55 | await ctx.respond(link) 56 | 57 | @utilities.command(description="Creates a qrcode for a given URL") 58 | async def qrcode( 59 | self, 60 | ctx: discord.ApplicationContext, 61 | url: str, 62 | color: str = "black", 63 | background_color: str = "white", 64 | ): 65 | qr = qrcode.QRCode( 66 | version=1, 67 | error_correction=qrcode.constants.ERROR_CORRECT_H, 68 | box_size=10, 69 | border=4, 70 | ) 71 | qr.add_data(str(url)) 72 | qr.make(fit=True) 73 | try: 74 | img = qr.make_image(fill_color=color, back_color=background_color).convert( 75 | "RGB" 76 | ) 77 | except ValueError: 78 | img = qr.make_image(fill_color="black", back_color="white").convert("RGB") 79 | image = BytesIO() 80 | img.save(image, "PNG") 81 | image.seek(0) 82 | await ctx.respond(file=discord.File(image, "qrcode.png")) 83 | 84 | 85 | def setup(client): 86 | client.add_cog(Utilities(client)) 87 | -------------------------------------------------------------------------------- /src/config.example.yaml: -------------------------------------------------------------------------------- 1 | # PLEASE DO NOT MODIFY THIS FILE 2 | # This file is not only an example but a template that will be used by setup.py 3 | 4 | # General 5 | DEFAULT_PREFIX: "?" 6 | BOT_OWNER_ID: 0 7 | BOT_TOKEN: "Token for the bot" 8 | LOGGING: True 9 | 10 | # Guild Details 11 | MAIN_GUILD: 0 # the bots main guild for emojis and owner commands 12 | DEBUG_GUILDS: [] # for debug purposes, slash commands work instantly but only in selected guilds 13 | DEBUG_GUILD_MODE: False 14 | 15 | # Channels 16 | join_alert_channel: 0 17 | leave_alert_channel: 0 18 | online_alert_channel: 0 19 | bug_report_channel: 0 20 | suggestion_channel: 0 21 | dm_reply_channel: 0 22 | 23 | # Database & Cache Auth Details 24 | DATABASE_URL: "postgresql:///?user=&password=" 25 | REDIS_URI: "redis://127.0.0.1" 26 | REDIS_PASSWORD: "Password for redis auth" 27 | 28 | # IPC info 29 | IPC_KEY: "whybot" 30 | IPC_HOST: "0.0.0.0" 31 | 32 | # API Keys 33 | GITHUB_ACCESS_TOKEN: "This api key is for bug reports" 34 | HYPIXEL_API_KEY: "This api key for hypixel related commands" 35 | PRAW_CLIENT_SECRET: "This key is for reddit commands" 36 | PRAW_CLIENT_ID: "This key is for reddit commands" 37 | GOOGLE_IMAGE_API: "This key is for google image search commands" 38 | NASA_API_KEY: "This key is for nasa commands" 39 | WEATHER_API_KEY: "This key is for weather commands" 40 | -------------------------------------------------------------------------------- /src/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .models import WhyBot, BaseCog 2 | 3 | __all__ = ["WhyBot", "BaseCog"] 4 | -------------------------------------------------------------------------------- /src/core/db/__init__.py: -------------------------------------------------------------------------------- 1 | from .create_tables import create_tables, TABLES_TO_CREATE 2 | from .setup_guild import ( 3 | setup_counting, 4 | setup_leveling_guild, 5 | setup_tickets, 6 | create_db_tables, 7 | ) 8 | 9 | __all__ = [ 10 | "TABLES_TO_CREATE", 11 | "create_tables", 12 | "setup_counting", 13 | "setup_leveling_guild", 14 | "setup_tickets", 15 | "create_db_tables", 16 | ] 17 | -------------------------------------------------------------------------------- /src/core/db/create_tables.py: -------------------------------------------------------------------------------- 1 | from typing import Final 2 | import asyncio 3 | 4 | from core.helpers import create_connection_pool 5 | 6 | 7 | blacklist_query: Final = """ 8 | DROP TABLE IF EXISTS blacklist; CREATE TABLE blacklist 9 | ( 10 | user_id integer NOT NULL, 11 | reason text, 12 | PRIMARY KEY (user_id) 13 | ); 14 | """ 15 | 16 | command_stats_query: Final = """ 17 | DROP TABLE IF EXISTS command_stats; CREATE TABLE command_stats 18 | ( 19 | id SERIAL PRIMARY KEY, 20 | user_id bigint NOT NULL, 21 | command_name text NOT NULL, 22 | usage_count integer NOT NULL DEFAULT 0 23 | ); 24 | """ 25 | 26 | counting_query: Final = """ 27 | DROP TABLE IF EXISTS counting; CREATE TABLE counting 28 | ( 29 | guild_id bigint NOT NULL PRIMARY KEY, 30 | last_counter bigint, 31 | current_number integer, 32 | counting_channel bigint, 33 | high_score integer, 34 | plugin_enabled boolean, 35 | auto_calculate boolean, 36 | banned_users bigint[] 37 | ); 38 | """ 39 | 40 | leveling_member_query: Final = """ 41 | DROP TABLE IF EXISTS leveling_member; CREATE TABLE leveling_member 42 | ( 43 | guild_id bigint NOT NULL, 44 | member_id bigint NOT NULL, 45 | member_name text, 46 | member_xp integer, 47 | member_level integer, 48 | member_total_xp bigint 49 | ); 50 | """ 51 | 52 | leveling_guild_query: Final = """ 53 | DROP TABLE IF EXISTS leveling_guild; CREATE TABLE leveling_guild 54 | ( 55 | guild_id bigint NOT NULL PRIMARY KEY, 56 | plugin_enabled boolean, 57 | text_font text, 58 | text_color text, 59 | background_image text, 60 | background_color text, 61 | progress_bar_color text, 62 | no_xp_roles json, 63 | no_xp_channels json, 64 | level_up_text text, 65 | level_up_enabled boolean, 66 | per_minute text 67 | ); 68 | """ 69 | 70 | leveling_rewards_query: Final = """ 71 | DROP TABLE IF EXISTS leveling_rewards; CREATE TABLE leveling_rewards 72 | ( 73 | guild_id bigint NOT NULL PRIMARY KEY, 74 | level integer, 75 | role bigint 76 | ); 77 | """ 78 | 79 | counters_query: Final = """ 80 | DROP TABLE IF EXISTS counters; CREATE TABLE counters 81 | ( 82 | key text NOT NULL PRIMARY KEY, 83 | value integer NOT NULL DEFAULT 0 84 | ); 85 | """ 86 | 87 | dmreply_query: Final = """ 88 | DROP TABLE IF EXISTS dmreply; CREATE TABLE dmreply 89 | ( 90 | user_id bigint NOT NULL PRIMARY KEY, 91 | thread_id bigint NOT NULL 92 | ); 93 | """ 94 | 95 | tags_query: Final = """ 96 | DROP TABLE IF EXISTS tags; CREATE TABLE tags 97 | ( 98 | id SERIAL NOT NULL PRIMARY KEY, 99 | guild_id bigint NOT NULL, 100 | tag_name TEXT NOT NULL, 101 | tag_value TEXT NOT NULL, 102 | tag_author TEXT NOT NULL, 103 | time_created INTEGER NOT NULL 104 | ); 105 | """ 106 | 107 | alerts_query: Final = """ 108 | DROP TABLE IF EXISTS alerts; CREATE TABLE alerts 109 | ( 110 | id serial NOT NULL PRIMARY KEY, 111 | alert_title text NOT NULL, 112 | alert_message text NOT NULL, 113 | time_created INTEGER NOT NULL, 114 | viewed integer NOT NULL DEFAULT 1 115 | ); 116 | """ 117 | 118 | alerts_user_query: Final = """ 119 | DROP TABLE IF EXISTS alerts_users; CREATE TABLE alerts_users 120 | ( 121 | user_id bigint NOT NULL PRIMARY KEY, 122 | alert_viewed boolean NOT NULL DEFAULT false, 123 | ignore_alerts boolean NOT NULL default false 124 | ); 125 | """ 126 | 127 | ticket_guild_query: Final = """ 128 | DROP TABLE IF EXISTS ticket_guild; CREATE TABLE ticket_guild 129 | ( 130 | guild_id bigint NOT NULL PRIMARY KEY, 131 | roles_allowed bigint[], 132 | ping_roles bigint[], 133 | create_button boolean DEFAULT false, 134 | category bigint, 135 | banned_users bigint[] 136 | ); 137 | """ 138 | 139 | tickets_query: Final = """ 140 | DROP TABLE IF EXISTS tickets; CREATE TABLE tickets 141 | ( 142 | id serial NOT NULL PRIMARY KEY, 143 | guild_id bigint NOT NULL, 144 | channel_id bigint NOT NULL, 145 | ticket_creator bigint NOT NULL, 146 | time_created INTEGER NOT NULL 147 | ); 148 | """ 149 | 150 | # If you wish not to create one of these tables in the setup process 151 | # Simply just remove/comment that item from this list: 152 | TABLES_TO_CREATE: Final = [ 153 | blacklist_query, 154 | command_stats_query, 155 | counting_query, 156 | leveling_member_query, 157 | leveling_guild_query, 158 | leveling_rewards_query, 159 | counters_query, 160 | dmreply_query, 161 | tags_query, 162 | alerts_query, 163 | alerts_user_query, 164 | ticket_guild_query, 165 | tickets_query, 166 | ] 167 | 168 | 169 | async def create_tables(): 170 | """ 171 | Runs all the table creation queries at once. 172 | To choose which tables to create add / remove the queries from the tables_to_create list 173 | """ 174 | pool = await create_connection_pool() 175 | tasks = [pool.execute(table) for table in TABLES_TO_CREATE] 176 | await asyncio.gather(*tasks) 177 | -------------------------------------------------------------------------------- /src/core/db/setup_guild.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import asyncpg 4 | 5 | 6 | async def setup_counting(db: asyncpg.Pool, guild_id: int): 7 | """ 8 | Setup counting for the guild. basically adds to the counting table 9 | 10 | Parameters: 11 | db (asyncpg.Pool): The database connection pool 12 | guild_id (int): the id of the guild to create a row for 13 | """ 14 | try: 15 | await db.execute( 16 | "INSERT INTO counting (guild_id, high_score, banned_users) VALUES ($1, 0, $2)", 17 | guild_id, 18 | [], 19 | ) 20 | except asyncpg.UniqueViolationError: 21 | return 22 | 23 | 24 | async def setup_leveling_guild(db: asyncpg.Pool, guild_id: int): 25 | """ 26 | Setup leveling for the guild. Creates a row with the default values in the leveling table 27 | 28 | Parameters: 29 | db (asyncpg.Pool): The database connection pool 30 | guild_id (int): the id of the guild to create a row for 31 | """ 32 | default_data = [ 33 | guild_id, 34 | False, 35 | "default", 36 | "black", 37 | None, 38 | "black", 39 | "green", 40 | [], 41 | [], 42 | "GG {member.mention} you just leveled up to {level}", 43 | True, 44 | "20", 45 | ] 46 | query = """ 47 | INSERT INTO leveling_guild ( 48 | guild_id, plugin_enabled, 49 | text_font, text_color, 50 | background_image, background_color, progress_bar_color, 51 | no_xp_roles, no_xp_channels, 52 | level_up_text, level_up_enabled, per_minute 53 | ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) 54 | """ 55 | try: 56 | await db.execute(query, *default_data) 57 | except asyncpg.UniqueViolationError: 58 | return 59 | 60 | 61 | async def setup_tickets(db: asyncpg.Pool, guild_id: int): 62 | """ 63 | Setup ticketing for the guild. 64 | 65 | Parameters: 66 | db (asyncpg.Pool): The database connection pool 67 | guild_id (int): the id of the guild to create a row for 68 | """ 69 | default_data = [guild_id, [], [], False, None, []] 70 | try: 71 | await db.execute( 72 | """INSERT INTO ticket_guild ( 73 | guild_id, roles_allowed, ping_roles, create_button, category, banned_users 74 | ) VALUES ($1, $2, $3, $4, $5, $6)""", 75 | *default_data, 76 | ) 77 | except asyncpg.UniqueViolationError: 78 | return 79 | 80 | 81 | async def create_db_tables(db: asyncpg.Pool, guild_id: int): 82 | """ 83 | Function for running all the setup functions at once 84 | This will be used when a new guild is joined and will help with not having to run these functions later 85 | 86 | Parameters: 87 | db (asyncpg.Pool): The database connection pool 88 | guild_id (int): the id of the guild to setup 89 | """ 90 | 91 | things_to_setup = [setup_counting, setup_leveling_guild, setup_tickets] 92 | tasks = [setup_function(db, guild_id) for setup_function in things_to_setup] 93 | await asyncio.gather(*tasks) 94 | -------------------------------------------------------------------------------- /src/core/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from .checks import blacklist_check, plugin_enabled, update_stats, run_bot_checks 2 | from .client_functions import ( 3 | update_activity, 4 | get_why_config, 5 | create_connection_pool, 6 | create_redis_connection, 7 | GUILD_IDS, 8 | ) 9 | from .exception import ( 10 | BaseException, 11 | RichBaseException, 12 | ConfigNotFound, 13 | InvalidDatabaseUrl, 14 | UserAlreadyBlacklisted, 15 | UserAlreadyWhitelisted, 16 | ImageAPIFail, 17 | ) 18 | from .http import get_request, get_request_bytes, post_request, post_request_bytes 19 | from .log import ( 20 | LOGFILE_PATH, 21 | log_errors, 22 | log_normal, 23 | convert_to_dict, 24 | get_last_errors, 25 | on_error, 26 | ) 27 | from .music import Player 28 | from .views import ( 29 | RickRollView, 30 | BotInfoView, 31 | LinkView, 32 | CalculatorView, 33 | ConfirmView, 34 | InputModalView, 35 | ErrorView, 36 | ) 37 | from .why_leveling import ( 38 | xp_needed, 39 | get_level_data, 40 | get_member_data, 41 | update_member_data, 42 | get_all_member_data, 43 | ) 44 | 45 | __all__ = [ 46 | "blacklist_check", 47 | "plugin_enabled", 48 | "update_stats", 49 | "run_bot_checks", 50 | "update_activity", 51 | "get_why_config", 52 | "create_connection_pool", 53 | "create_redis_connection", 54 | "GUILD_IDS", 55 | "BaseException", 56 | "RichBaseException", 57 | "ConfigNotFound", 58 | "InvalidDatabaseUrl", 59 | "UserAlreadyBlacklisted", 60 | "UserAlreadyWhitelisted", 61 | "ImageAPIFail", 62 | "get_request", 63 | "get_request_bytes", 64 | "post_request", 65 | "post_request_bytes", 66 | "LOGFILE_PATH", 67 | "log_errors", 68 | "log_normal", 69 | "convert_to_dict", 70 | "get_last_errors", 71 | "on_error", 72 | "Player", 73 | "RickRollView", 74 | "BotInfoView", 75 | "LinkView", 76 | "CalculatorView", 77 | "ConfirmView", 78 | "InputModalView", 79 | "ErrorView", 80 | "xp_needed", 81 | "get_level_data", 82 | "get_member_data", 83 | "update_member_data", 84 | "get_all_member_data", 85 | ] 86 | -------------------------------------------------------------------------------- /src/core/helpers/checks.py: -------------------------------------------------------------------------------- 1 | """ (module) checks 2 | This module contains checks that will be run before most commands in the @commands.check() decorator 3 | """ 4 | 5 | import asyncio 6 | import datetime 7 | 8 | import aioredis 9 | from discord.ext import commands 10 | from discord import ApplicationContext # for the autocomplete 11 | 12 | from .client_functions import get_why_config 13 | from core.utils import asyncpg_connect 14 | 15 | 16 | async def blacklist_check(user_id: int) -> bool: 17 | """returns true if the user is not blacklisted""" 18 | 19 | config = get_why_config() 20 | 21 | redis_url = config["REDIS_URI"] 22 | redis_password = config["REDIS_PASSWORD"] 23 | database_url = config["DATABASE_URL"] 24 | 25 | # Caching 26 | redis = aioredis.from_url( 27 | redis_url, decode_responses=True, password=redis_password, port=6379 28 | ) 29 | 30 | if await redis.exists("blacklisted"): # if the key exists then do this: 31 | cached_blacklisted_users = await redis.lrange("blacklisted", 0, -1) 32 | return not str(user_id) in cached_blacklisted_users 33 | 34 | async with asyncpg_connect(database_url) as conn: 35 | data = await conn.fetch("SELECT * FROM blacklist;") 36 | users = [int(user[0]) for user in data] 37 | if users: 38 | await redis.lpush("blacklisted", *users) 39 | await redis.expire("blacklisted", datetime.timedelta(hours=12)) 40 | return not str(user_id) in users 41 | 42 | 43 | async def plugin_enabled(cog: commands.Cog) -> bool: 44 | # TODO 𐐘 45 | # cog_name = cog.__cog_name__ 46 | return True 47 | 48 | 49 | async def update_stats(ctx: ApplicationContext): 50 | config = get_why_config() 51 | database_url = config["DATABASE_URL"] 52 | 53 | async with asyncpg_connect(database_url) as conn: 54 | data = await conn.fetch( 55 | "SELECT * FROM command_stats WHERE user_id=$1 AND command_name=$2", 56 | ctx.author.id, 57 | ctx.command.name, 58 | ) 59 | if data: 60 | await conn.execute( 61 | "UPDATE command_stats SET usage_count=$1 WHERE user_id=$2 AND" 62 | " command_name=$3", 63 | data[0][3] + 1, 64 | ctx.author.id, 65 | ctx.command.name, 66 | ) 67 | else: 68 | await conn.execute( 69 | "INSERT INTO command_stats (user_id, command_name, usage_count) VALUES" 70 | " ($1, $2, $3)", 71 | ctx.author.id, 72 | ctx.command.name, 73 | 1, 74 | ) 75 | 76 | 77 | async def run_bot_checks(ctx: ApplicationContext): 78 | 79 | blacklisted_check = await blacklist_check(ctx.author.id) 80 | plugin_enabled_check = await plugin_enabled(ctx.cog) 81 | all_checks_successful = all([blacklisted_check, plugin_enabled_check]) 82 | 83 | asyncio.get_event_loop().create_task(update_stats(ctx)) 84 | 85 | return all_checks_successful 86 | -------------------------------------------------------------------------------- /src/core/helpers/client_functions.py: -------------------------------------------------------------------------------- 1 | """ (module) client_functions 2 | Useful functions for the WhyBot client 3 | """ 4 | 5 | import os 6 | import json 7 | 8 | import yaml 9 | import discord 10 | import asyncpg 11 | import aioredis 12 | from discord.ext import commands 13 | 14 | import __main__ 15 | from .exception import ConfigNotFound 16 | 17 | 18 | async def update_activity(client: commands.Bot): 19 | """ 20 | Updates the bot's activity with the amount of servers 21 | 22 | Parameters: 23 | client (WhyBot): The bot to update presence for 24 | """ 25 | 26 | await client.change_presence( 27 | activity=discord.Game(f"On {len(client.guilds)} servers! | /help") 28 | ) 29 | 30 | 31 | def get_why_config() -> dict: 32 | """ 33 | Gets the why bot config 34 | 35 | Returns: 36 | dict: The parsed result of config.yaml file 37 | 38 | Raises: 39 | ConfigNotFound: If the config was not found 40 | """ 41 | 42 | path = os.path.join(os.path.dirname(__main__.__file__), "config.yaml") 43 | 44 | if not os.path.exists(path): 45 | raise ConfigNotFound 46 | 47 | with open(path) as f: 48 | data = yaml.load(f, Loader=yaml.SafeLoader) 49 | 50 | return data 51 | 52 | 53 | async def create_connection_pool() -> asyncpg.Pool: 54 | """ 55 | Creates a connection pool to the bots postgresql db 56 | 57 | Returns: 58 | asyncpg.Pool: an asyncpg connection pool. 59 | """ 60 | 61 | async def init(conn): 62 | # Set up auto json encoder/decoder: 63 | await conn.set_type_codec( 64 | "json", encoder=json.dumps, decoder=json.loads, schema="pg_catalog" 65 | ) 66 | 67 | config = get_why_config() 68 | pool = await asyncpg.create_pool(dsn=config["DATABASE_URL"], init=init) 69 | 70 | return pool 71 | 72 | 73 | async def create_redis_connection() -> aioredis.Redis: 74 | """ 75 | Creates a connection the the redis database 76 | 77 | Returns: 78 | aioredis.Redis: the connection to the db 79 | """ 80 | config = get_why_config() 81 | 82 | redis = aioredis.from_url( 83 | config["REDIS_URI"], 84 | decode_responses=True, 85 | password=config["REDIS_PASSWORD"], 86 | port=6379, 87 | ) 88 | 89 | return redis 90 | 91 | 92 | GUILD_IDS = [ 93 | get_why_config()["MAIN_GUILD"] 94 | ] # owner commands only work in the guilds in this array 95 | -------------------------------------------------------------------------------- /src/core/helpers/exception.py: -------------------------------------------------------------------------------- 1 | """ (module) exception 2 | This module contains exceptions to make development easier 3 | """ 4 | 5 | import sys 6 | 7 | from rich.text import Text 8 | from rich.panel import Panel 9 | from rich.console import Console 10 | 11 | 12 | class BaseException(Exception): 13 | """Base class for other exceptions to inherit form""" 14 | 15 | pass 16 | 17 | 18 | class RichBaseException(BaseException): 19 | """ 20 | Base rich class for other exceptions to inherit form 21 | This one prints the error to console with rich 22 | """ 23 | 24 | def __init__(self, title: str, message: str) -> None: 25 | error_message = Panel( 26 | Text.from_markup(f"[yellow]{message}"), 27 | title=title, 28 | border_style="red", 29 | ) 30 | Console().print(error_message, justify="left") 31 | super().__init__() 32 | 33 | 34 | class ConfigNotFound(RichBaseException): 35 | def __init__(self) -> None: 36 | super().__init__( 37 | "CONFIG FILE NOT FOUND!!!", 38 | "Config file (config.yaml) was not found.\nPlease run setup.py to create" 39 | " config files", 40 | ) 41 | sys.exit(1) 42 | 43 | 44 | class InvalidDatabaseUrl(RichBaseException): 45 | def __init__(self) -> None: 46 | super().__init__( 47 | "INVALID DATABASE URL!!!", 48 | "Invalid postgresql connection string was provided.\nPlease provide the" 49 | " correct string in config", 50 | ) 51 | sys.exit(1) 52 | 53 | 54 | class UserAlreadyBlacklisted(BaseException): 55 | pass 56 | 57 | 58 | class UserAlreadyWhitelisted(BaseException): 59 | pass 60 | 61 | 62 | class ImageAPIFail(BaseException): 63 | pass 64 | -------------------------------------------------------------------------------- /src/core/helpers/http.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from typing import Optional, Any 3 | from urllib.parse import urlencode 4 | 5 | import aiohttp 6 | 7 | 8 | async def get_request( 9 | url: str, 10 | data: Optional[dict] = None, 11 | headers: Optional[dict] = None, 12 | timeout: Optional[int] = None, 13 | ) -> Optional[aiohttp.ClientResponse | str | dict]: 14 | """Makes a get request and returns the json or text result""" 15 | 16 | if data is not None: 17 | url = f"{url}?{urlencode(data)}" 18 | 19 | kwargs = { 20 | "headers": headers, 21 | } 22 | 23 | if timeout is not None: 24 | kwargs["timeout"] = aiohttp.ClientTimeout(total=timeout) 25 | 26 | async with aiohttp.ClientSession(**kwargs) as session: 27 | async with session.get(url) as resp: 28 | if resp.ok is False: 29 | return None 30 | 31 | try: 32 | return await resp.json() 33 | except aiohttp.ContentTypeError: 34 | try: 35 | return await resp.text() 36 | except aiohttp.ContentTypeError: 37 | return None 38 | 39 | 40 | async def post_request( 41 | url: str, 42 | data: Optional[dict] = None, 43 | body: Optional[Any] = None, 44 | json: Optional[bool] = True, 45 | headers: Optional[dict] = None, 46 | timeout: Optional[int] = None, 47 | ) -> Optional[aiohttp.ClientResponse | str | dict]: 48 | """Makes a post request and returns the json or text result""" 49 | 50 | if data is not None: 51 | url = f"{url}?{urlencode(data)}" 52 | 53 | kwargs = { 54 | "headers": headers, 55 | } 56 | 57 | if timeout is not None: 58 | kwargs["timeout"] = aiohttp.ClientTimeout(total=timeout) 59 | async with aiohttp.ClientSession(**kwargs) as session: 60 | # decide if to do json=body or data=body in session.post() 61 | json_or_data = "json" if json is True else "data" 62 | async with session.post(url, **{json_or_data: body}) as resp: 63 | if resp.ok is False: 64 | return None 65 | 66 | try: 67 | return await resp.json() 68 | except aiohttp.ContentTypeError: 69 | try: 70 | return await resp.text() 71 | except aiohttp.ContentTypeError: 72 | return None 73 | 74 | 75 | async def get_request_bytes( 76 | url: str, 77 | data: Optional[dict] = None, 78 | headers: Optional[dict] = None, 79 | timeout: Optional[int] = None, 80 | bytes_io: Optional[bool] = False, 81 | ) -> Optional[aiohttp.ClientResponse | bytes | BytesIO]: 82 | """Makes a get request and returns the byte result. This is useful for something like a file/image""" 83 | 84 | if data is not None: 85 | url = f"{url}?{urlencode(data)}" 86 | 87 | kwargs = { 88 | "headers": headers, 89 | } 90 | 91 | if timeout is not None: 92 | kwargs["timeout"] = aiohttp.ClientTimeout(total=timeout) 93 | 94 | async with aiohttp.ClientSession(**kwargs) as session: 95 | async with session.get(url) as resp: 96 | if resp.ok is False: 97 | return None 98 | 99 | if bytes_io: 100 | response_bytes = BytesIO(await resp.read()) 101 | response_bytes.seek(0) 102 | return response_bytes 103 | 104 | return await resp.read() 105 | 106 | 107 | async def post_request_bytes( 108 | url: str, 109 | data: Optional[dict] = None, 110 | body: Optional[Any] = None, 111 | json: Optional[bool] = True, 112 | headers: Optional[dict] = None, 113 | timeout: Optional[int] = None, 114 | bytes_io: Optional[bool] = False, 115 | ) -> Optional[aiohttp.ClientResponse | bytes | BytesIO]: 116 | """Makes a post request and returns the byte result. This is useful for something like a file/image""" 117 | 118 | if data is not None: 119 | url = f"{url}?{urlencode(data)}" 120 | 121 | kwargs = { 122 | "headers": headers, 123 | } 124 | 125 | if timeout is not None: 126 | kwargs["timeout"] = aiohttp.ClientTimeout(total=timeout) 127 | 128 | async with aiohttp.ClientSession(**kwargs) as session: 129 | # decide if to do json=body or data=body in session.post() 130 | json_or_data = "json" if json is True else "data" 131 | async with session.post(url, **{json_or_data: body}) as resp: 132 | if resp.ok is False: 133 | return None 134 | 135 | if bytes_io: 136 | response_bytes = BytesIO(await resp.read()) 137 | response_bytes.seek(0) 138 | return response_bytes 139 | 140 | return await resp.read() 141 | -------------------------------------------------------------------------------- /src/core/helpers/log.py: -------------------------------------------------------------------------------- 1 | """ (module) log: 2 | This is for logging errors and exceptions 3 | """ 4 | 5 | import os 6 | import sys 7 | import logging 8 | import traceback 9 | from datetime import datetime 10 | from typing import Final, Optional 11 | 12 | import aiofiles 13 | from rich.panel import Panel 14 | from rich.console import Console 15 | 16 | import __main__ 17 | from .client_functions import get_why_config 18 | 19 | rich_console = Console() 20 | 21 | path: Final = os.path.join(os.path.dirname(__main__.__file__), "logfiles") 22 | 23 | # check if log files dir exists 24 | if not os.path.exists(path): 25 | # and if not make it 26 | os.makedirs(path) 27 | 28 | 29 | # setup discord logger 30 | logger = logging.getLogger("discord") 31 | logger.setLevel(logging.INFO) 32 | discord_logfile_path = os.path.join(path, "discord.log") 33 | handler = logging.FileHandler(filename=discord_logfile_path, encoding="utf-8", mode="w") 34 | handler.setFormatter( 35 | logging.Formatter( 36 | "[%(levelname)s] (%(asctime)s) - %(message)s", "%d-%b-%y %H:%M:%S" 37 | ) 38 | ) 39 | logger.addHandler(handler) 40 | 41 | 42 | LOGFILE_PATH: Final = os.path.join(path, "main.log") 43 | 44 | 45 | # Custom exeption handler 46 | def log_errors(etype, value, tb) -> None: 47 | """Logs errors to the file instead of terminal""" 48 | 49 | error = ( 50 | f"{etype.__name__}:\n\tTraceback (most recent call" 51 | f" last):\n\t{' '.join(traceback.format_tb(tb))}\n\t{value}" 52 | ) 53 | 54 | # Pythons core module "logging" doesnt wanna work me very sad so me make this workaround: 55 | config = get_why_config() 56 | if config["LOGGING"]: 57 | with open(LOGFILE_PATH, "a") as f: 58 | f.write( 59 | f"[ERROR] ({datetime.now().strftime('%d-%b-%Y %H:%M:%S')}) - {error}\n" 60 | ) 61 | 62 | rich_console.print( 63 | Panel( 64 | "Traceback (most recent call" 65 | f" last):\n\t{''.join(traceback.format_tb(tb))}\n{value}", 66 | title=etype.__name__, 67 | border_style="red", 68 | ) 69 | ) 70 | 71 | 72 | async def log_normal(message: str) -> None: 73 | """ 74 | Logs an error 75 | 76 | Parameters 77 | message (str): The message you want to log 78 | """ 79 | async with aiofiles.open(LOGFILE_PATH, "a") as f: 80 | await f.write( 81 | f"[INFO] ({datetime.now().strftime('%d-%b-%Y %H:%M:%S')}) - {message}\n" 82 | ) 83 | 84 | 85 | async def convert_to_dict() -> dict: 86 | """ 87 | Converts the log.txt file to a dict 88 | 89 | Returns 90 | dict: The log file 91 | """ 92 | async with aiofiles.open(LOGFILE_PATH) as logs_data: 93 | logs = {} 94 | 95 | async for line in logs_data: 96 | if line.startswith("[INFO]"): 97 | continue 98 | if line.startswith("[ERROR]"): 99 | logs[line] = "" 100 | continue 101 | try: 102 | logs[(list(logs.keys())[-1])] += line 103 | except (IndexError, TypeError): 104 | break 105 | 106 | return logs 107 | 108 | 109 | async def get_last_errors(count: int = 1) -> Optional[dict]: 110 | """ 111 | Gets the last x amount of errors from the logs file 112 | 113 | Parameters 114 | count (Optional[int]): The amount of errors you want. (default = 1) 115 | 116 | Returns: 117 | dict: The last errors in a dictionary 118 | """ 119 | logs: dict = await convert_to_dict() 120 | 121 | if len(logs) == 0: 122 | return None 123 | 124 | last_errors = {} 125 | 126 | last_keys = [list(logs.keys())[-(i + 1)] for i in range(count)] 127 | 128 | for key in last_keys: 129 | last_errors[key] = logs[key] 130 | return last_errors 131 | 132 | 133 | # client.event -> on_error 134 | async def on_error(event_method, *args, **kwargs): 135 | """ 136 | This function is run when the client/bot encounters an error. 137 | I will overwrite the default client.on_error method with this one 138 | Basically it stops the bot from ignoring/printing error to terminal 139 | instead logs the error to main.log and prints with rich 140 | """ 141 | # get error 142 | ex_type, ex_value, tb = sys.exc_info() 143 | log_errors(ex_type, ex_value, tb) 144 | -------------------------------------------------------------------------------- /src/core/helpers/music.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | import pycord.wavelink as wavelink 4 | 5 | 6 | class Player(wavelink.Player): 7 | def __init__(self, *args, **kwargs): 8 | super().__init__(*args, **kwargs) 9 | 10 | 11 | async def get_player(self, ctx: discord.ApplicationContext): 12 | if isinstance(ctx, (discord.ApplicationContext, commands.Context)): 13 | return 14 | -------------------------------------------------------------------------------- /src/core/helpers/why_leveling.py: -------------------------------------------------------------------------------- 1 | from typing import Final, Optional 2 | 3 | import discord 4 | import asyncpg 5 | 6 | from core.models.level import LevelingDataGuild, LevelingDataMember 7 | 8 | 9 | def xp_needed(level: int) -> int: 10 | """ 11 | Calculate the xp needed for any level 12 | 13 | Parameters: 14 | level (int): The level to calculate xp needed 15 | 16 | Returns: 17 | int: The amount of xp to reach the level provided 18 | """ 19 | 20 | x, y = 0.125, 2 21 | return int((level / x) ** y) 22 | 23 | 24 | async def get_level_data( 25 | db: asyncpg.Pool, guild_id: int 26 | ) -> Optional[LevelingDataGuild]: 27 | """ 28 | This function gets the leveling data for a guild 29 | 30 | Parameters: 31 | db (asyncpg.Pool): the connection pool to the database. This is found in client.db. 32 | guild_id (int): The guild id of the guild to get leveling data for 33 | 34 | Returns: 35 | Optional[LevelingDataGuild]: If data for the guild is not found it returns None else 36 | it returns a LevelingDataGuild object with the data in it 37 | """ 38 | 39 | data = await db.fetch("SELECT * FROM leveling_guild WHERE guild_id=$1", guild_id) 40 | if not data: 41 | return None 42 | 43 | return LevelingDataGuild(*data[0]) 44 | 45 | 46 | async def get_member_data( 47 | db: asyncpg.Pool, member: discord.Member, guild_id: int 48 | ) -> LevelingDataMember: 49 | """ 50 | This function gets the leveling data for a single member 51 | 52 | Parameters: 53 | db (asyncpg.Pool): the connection pool to the database. This is found in client.db. 54 | member (discord.Member): The member to get the data for 55 | guild_id (int): The guild where to get the data for as members can be in multiple 56 | guilds with different leveling data 57 | 58 | Returns: 59 | LevelingDataMember: it returns a LevelingDataMember object with the mmeber's data in it 60 | """ 61 | 62 | data = await db.fetch( 63 | "SELECT * FROM leveling_member WHERE member_id=$1 AND guild_id=$2", 64 | member.id, 65 | guild_id, 66 | ) 67 | 68 | if not data: 69 | DEFAULT_MEMBER_DATA: Final[int] = [ 70 | guild_id, 71 | member.id, 72 | f"{member.name}#{member.discriminator}", 73 | 0, 74 | 0, 75 | 0, 76 | ] 77 | await db.execute( 78 | "INSERT INTO leveling_member (guild_id, member_id, member_name, member_xp," 79 | " member_level, member_total_xp) VALUES ($1, $2, $3, $4, $5, $6)", 80 | *DEFAULT_MEMBER_DATA, 81 | ) 82 | return LevelingDataMember(*DEFAULT_MEMBER_DATA) 83 | 84 | return LevelingDataMember(*data[0]) 85 | 86 | 87 | async def update_member_data( 88 | db: asyncpg.Pool, message: discord.Message, member_data: LevelingDataMember 89 | ) -> None: 90 | """ 91 | Updates the data for a member 92 | 93 | Parameters: 94 | db (asyncpg.Pool): the connection pool to the database. This is found in client.db. 95 | message (discord.Message): The message that was sent. This is used to get things 96 | like guild id and extra info 97 | member_data (LevelingDataMember): The object with the new data 98 | """ 99 | 100 | member = message.author 101 | await db.execute( 102 | """UPDATE leveling_member 103 | SET member_name=$1, member_xp=$2, member_level=$3, member_total_xp=$4 104 | WHERE member_id=$5 AND guild_id=$6 105 | """, 106 | f"{member.name}#{member.discriminator}", 107 | member_data.member_xp, 108 | member_data.member_level, 109 | member_data.member_total_xp, 110 | message.author.id, 111 | message.guild.id, 112 | ) 113 | 114 | 115 | async def get_all_member_data(db: asyncpg.Pool, guild_id: int): 116 | """ 117 | Gets member data for all the members in a guild. This is used for leaderboards 118 | 119 | Parameters: 120 | db (asyncpg.Pool) 121 | """ 122 | 123 | data = await db.fetch( 124 | "SELECT * FROM leveling_member WHERE guild_id=$1 ORDER BY member_total_xp", 125 | guild_id, 126 | ) 127 | 128 | return data[::-1] 129 | -------------------------------------------------------------------------------- /src/core/models/__init__.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | 3 | from .client import WhyBot 4 | from .counting import CountingData 5 | from .level import ( 6 | LevelingDataGuild, 7 | LevelingDataMember, 8 | ) 9 | from .rps import RockPaperScissorsView 10 | from .tag import Tag 11 | from .ticket import TicketGuild, Ticket, TicketView, NewTicketView, ClosedTicketView 12 | from .ttt import TicTacToeAIView, TicTacToe2PlayerView 13 | from core.helpers.checks import run_bot_checks 14 | 15 | __all__ = [ 16 | "WhyBot", 17 | "CountingData", 18 | "LevelingDataGuild", 19 | "LevelingDataMember", 20 | "RockPaperScissorsView", 21 | "Tag", 22 | "TicketGuild", 23 | "Ticket", 24 | "TicketView", 25 | "NewTicketView", 26 | "ClosedTicketView", 27 | "TicTacToeAIView", 28 | "TicTacToe2PlayerView", 29 | ] 30 | 31 | 32 | class BaseCog(commands.Cog): 33 | """base class for cogs""" 34 | 35 | def __init__(self, client: WhyBot) -> None: 36 | self.client = client 37 | self.cog_check = run_bot_checks 38 | -------------------------------------------------------------------------------- /src/core/models/client.py: -------------------------------------------------------------------------------- 1 | """ (module) whybot 2 | 3 | This contains the WhyBot commands.Bot client class and its class methods 4 | Its also where most init tasks are done 5 | """ 6 | 7 | __author__ = "FusionSid" 8 | __licence__ = "MIT License" 9 | 10 | import datetime 11 | from typing import Optional 12 | 13 | import discord 14 | import aioredis 15 | import asyncpg 16 | from pycord.ext import ipc 17 | from rich.console import Console 18 | from discord.ext import commands 19 | 20 | from core.utils import format_seconds 21 | from core.helpers import UserAlreadyBlacklisted, UserAlreadyWhitelisted 22 | 23 | 24 | class WhyBot(commands.Bot): 25 | """ 26 | The Why Bot Class (subclass of: `discord.ext.commands.Bot`) 27 | 28 | Parameters: 29 | config (dict): The parsed result of the config.yaml file 30 | this is usualy obtained from the get_why_config function 31 | 32 | Attributes: 33 | db Optional[asyncpg.Pool]: The connection to the postgres db 34 | redis Optional[aioredis.Redis]: The connection to the redis db 35 | config (dict): The bots config 36 | version (str): the bots version 37 | console (rich.console.Console): a Console object useful for rich printing 38 | last_login_time (datetime.datetime.now()): The last time the bot started, used for uptime 39 | 40 | + all the ones inherited from `discord.ext.commands.Bot` 41 | """ 42 | 43 | def __init__(self, config: dict, version: str): 44 | 45 | self.cogs_list = {} 46 | self.db: asyncpg.Pool 47 | self.redis: aioredis.Redis 48 | 49 | self.config = config 50 | self.version = version 51 | self.console = Console() 52 | self.last_login_time = datetime.datetime.now() 53 | 54 | intents = discord.Intents.all() 55 | allowed_mentions = discord.AllowedMentions(everyone=False) 56 | 57 | super().__init__( 58 | intents=intents, 59 | help_command=None, 60 | case_insensitive=True, 61 | command_prefix=config.get("DEFAULT_PREFIX"), 62 | owner_id=config["BOT_OWNER_ID"], 63 | debug_guilds=config.get("DEBUG_GUILDS") 64 | if config.get("DEBUG_GUILD_MODE") 65 | else None, 66 | allowed_mentions=allowed_mentions, 67 | ) 68 | self.ipc = ipc.Server( 69 | self, secret_key=config["IPC_KEY"], host=config["IPC_HOST"] 70 | ) 71 | 72 | @property 73 | async def uptime(self) -> str: 74 | """ 75 | This property returns the uptime for the bot. 76 | 77 | Returns: 78 | str : Formated string with the uptime 79 | """ 80 | time_right_now = datetime.datetime.now() 81 | seconds = int((time_right_now - self.last_login_time).total_seconds()) 82 | 83 | time = await format_seconds(seconds) 84 | return time 85 | 86 | @property 87 | def get_why_emojies(self) -> dict: 88 | """ 89 | This property returns the emojis for the bot 90 | these emojis are the ones in the Why Bot guild 91 | 92 | Returns: 93 | dict : A dictionary of emojis 94 | """ 95 | emojis_dict = {} 96 | 97 | for emoji in self.get_guild(self.config.get("MAIN_GUILD")).emojis: 98 | emojis_dict[emoji.name] = str(emoji) 99 | 100 | return emojis_dict 101 | 102 | async def get_blacklisted_users( 103 | self, reasons: Optional[bool] = False 104 | ) -> list[int] | list[asyncpg.protocol.Record]: 105 | """ 106 | this function returns the blacklisted users for the bot from the db 107 | this is used when the cache is empty or needs to be updated 108 | 109 | Parameters: 110 | reasons (Optional[bool]): If this is True it will return a list 111 | blacklisted users with their userids and reason for being banned 112 | This default to false 113 | 114 | Returns: 115 | list[int] | list[asyncpg.Record]: it will return a list with user ids of people blacklisted 116 | but if reasons is True it will be a list of asyncpg.Records which will look like: list[list[int, str]] 117 | """ 118 | users = await self.db.fetch("SELECT * FROM blacklist;") 119 | 120 | if reasons: 121 | return users 122 | 123 | return [int(user[0]) for user in users] 124 | 125 | async def reset_redis_blacklisted_cache(self): 126 | """This function resets the blacklisted cache for the bot""" 127 | 128 | await self.redis.delete("blacklisted") 129 | users = [int(user) for user in await self.get_blacklisted_users()] 130 | if users: 131 | await self.redis.lpush("blacklisted", *users) 132 | await self.redis.expire("blacklisted", datetime.timedelta(days=5)) 133 | 134 | await self.redis.lpush("blacklisted", 0) 135 | await self.redis.expire("blacklisted", datetime.timedelta(days=5)) 136 | 137 | async def blacklist_user(self, user_id: int, reason: Optional[str] = None): 138 | """ 139 | This function is used to black list a user from using the bot 140 | 141 | Parameters: 142 | user_id (int): the user id to ban 143 | reason (Optional[str]): the optional reason why the user was blacklisted 144 | 145 | Raises: 146 | UserAlreadyBlacklisted: If the user is already blacklisted 147 | """ 148 | is_user_blacklisted = user_id in await self.get_blacklisted_users() 149 | if is_user_blacklisted: # check if they are already blacklisted 150 | raise UserAlreadyBlacklisted 151 | 152 | if reason is not None: 153 | await self.db.execute( 154 | "INSERT INTO public.blacklist (user_id) VALUES ($1)", user_id 155 | ) 156 | else: 157 | await self.db.execute( 158 | "INSERT INTO public.blacklist (user_id, reason) VALUES ($1, $2)", 159 | user_id, 160 | reason, 161 | ) 162 | 163 | # Reset cache 164 | await self.reset_redis_blacklisted_cache() 165 | 166 | async def whitelist_user(self, user_id: int): 167 | """ 168 | if a user was blacklisted from the bot it will whitelist them 169 | 170 | Parameters: 171 | user_id (int): The user_id to unban 172 | 173 | Raises: 174 | UserAlreadyWhitelisted: If the user is already whitelisted 175 | """ 176 | is_user_blacklisted = user_id in await self.get_blacklisted_users() 177 | if not is_user_blacklisted: # check if they are already whitelisted 178 | raise UserAlreadyWhitelisted 179 | 180 | await self.db.execute("DELETE FROM public.blacklist WHERE user_id=$1", user_id) 181 | 182 | # Reset cache 183 | await self.reset_redis_blacklisted_cache() 184 | 185 | @staticmethod 186 | async def on_ipc_error(endpoint, error): 187 | print(endpoint, "raised", error) 188 | -------------------------------------------------------------------------------- /src/core/models/counting.py: -------------------------------------------------------------------------------- 1 | """ (module) counting 2 | Model for making counting management easier 3 | """ 4 | 5 | from typing import Optional 6 | from dataclasses import dataclass 7 | 8 | 9 | @dataclass 10 | class CountingData: 11 | """dataclass to help with counting data information from the counting table""" 12 | 13 | guild_id: int 14 | last_counter: Optional[int] 15 | current_number: Optional[int] 16 | counting_channel: Optional[int] 17 | 18 | high_score: Optional[int] 19 | plugin_enabled: Optional[bool] 20 | auto_calculate: Optional[bool] 21 | banned_counters: Optional[list[int]] 22 | 23 | @property 24 | def next_number(self) -> int: 25 | """ 26 | This property returns the next number which is just one 27 | more then the previous number (self.current_number + 1) 28 | 29 | Returns: 30 | int: the next number 31 | """ 32 | if self.current_number is None: 33 | return 1 34 | 35 | return self.current_number + 1 36 | -------------------------------------------------------------------------------- /src/core/models/level.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import random 4 | from dataclasses import dataclass 5 | 6 | 7 | @dataclass 8 | class LevelingDataGuild: 9 | """ 10 | Dataclass to help with using the leveling data guild table 11 | """ 12 | 13 | guild_id: int 14 | plugin_enabled: Optional[bool] 15 | 16 | # card customization 17 | text_font: Optional[str] 18 | text_color: Optional[str] 19 | background_image: Optional[str] 20 | background_color: Optional[str] 21 | progress_bar_color: Optional[str] 22 | 23 | # give xp execptions 24 | no_xp_roles: Optional[list] 25 | no_xp_channels: Optional[list] 26 | 27 | # level up announcment 28 | level_up_text: Optional[str] 29 | level_up_enabled: Optional[bool] 30 | per_minute: Optional[str] 31 | 32 | def get_per_minute_xp(self) -> int: 33 | if self.per_minute is None: 34 | return random.randrange(15, 30) 35 | 36 | if self.per_minute.isnumeric(): 37 | return int(self.per_minute) 38 | 39 | try: 40 | left, right = self.per_minute.split("-") 41 | return random.randrange(int(left), int(right)) 42 | except ValueError: 43 | return random.randrange(15, 30) 44 | 45 | 46 | @dataclass 47 | class LevelingDataMember: 48 | """Dataclass to help with the leveling data member table""" 49 | 50 | guild_id: int 51 | member_id: int 52 | member_name: str 53 | member_xp: int 54 | member_level: int 55 | member_total_xp: int 56 | -------------------------------------------------------------------------------- /src/core/models/rps.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | import discord 4 | 5 | 6 | class RockPaperScissorsView(discord.ui.View): 7 | """Rock paper scissors game view""" 8 | 9 | def __init__(self, player1: discord.Member, player2: discord.Member): 10 | self.results = {} 11 | self.p1 = player1 12 | self.p2 = player2 13 | super().__init__(timeout=100) 14 | 15 | @discord.ui.button( 16 | style=discord.ButtonStyle.green, label="Rock", emoji="🗿", custom_id="rock" 17 | ) 18 | async def rock(self, button: discord.ui.Button, interaction: discord.Interaction): 19 | await interaction.response.send_message("You chose rock", ephemeral=True) 20 | await self.handle_input(interaction.user, button.custom_id) 21 | 22 | @discord.ui.button( 23 | style=discord.ButtonStyle.green, emoji="📄", label="Paper", custom_id="paper" 24 | ) 25 | async def paper(self, button: discord.ui.Button, interaction: discord.Interaction): 26 | await interaction.response.send_message("You chose paper", ephemeral=True) 27 | await self.handle_input(interaction.user, button.custom_id) 28 | 29 | @discord.ui.button( 30 | style=discord.ButtonStyle.green, 31 | label="Scissors", 32 | emoji="✂️", 33 | custom_id="scissors", 34 | ) 35 | async def scissor( 36 | self, button: discord.ui.Button, interaction: discord.Interaction 37 | ): 38 | await interaction.response.send_message("You chose scissors", ephemeral=True) 39 | await self.handle_input(interaction.user, button.custom_id) 40 | 41 | async def handle_input( 42 | self, user: discord.Member, input: Literal["rock", "paper", "scissors"] 43 | ): 44 | self.results[user.id] = input 45 | 46 | if self.results.get(self.p1.id) is None or self.results.get(self.p2.id) is None: 47 | return 48 | 49 | for button in self.children: 50 | button.disabled = True 51 | 52 | await self.message.edit(view=self) 53 | await super().on_timeout() 54 | 55 | self.stop() 56 | 57 | if self.results[self.p1.id] == self.results[self.p2.id]: 58 | await self.message.channel.send( 59 | embed=discord.Embed( 60 | title="Rock Paper Scissors", 61 | description="It is a Draw!", 62 | color=discord.Color.random(), 63 | ) 64 | ) 65 | 66 | p1_win = discord.Embed( 67 | title="Rock Paper Scissors", 68 | description=f"Yay {self.p1.mention} wins!", 69 | color=discord.Color.random(), 70 | ) 71 | p2_win = discord.Embed( 72 | title="Rock Paper Scissors", 73 | description=f"Yay {self.p2.mention} wins!", 74 | color=discord.Color.random(), 75 | ) 76 | 77 | if self.results[self.p1.id] == "rock": 78 | if self.results[self.p2.id] == "scissors": 79 | await self.message.channel.send(embed=p1_win) 80 | elif self.results[self.p2.id] == "paper": 81 | await self.message.channel.send(embed=p2_win) 82 | 83 | elif self.results[self.p1.id] == "paper": 84 | if self.results[self.p2.id] == "rock": 85 | await self.message.channel.send(embed=p1_win) 86 | elif self.results[self.p2.id] == "scissors": 87 | await self.message.channel.send(embed=p2_win) 88 | 89 | elif self.results[self.p1.id] == "scissors": 90 | if self.results[self.p2.id] == "paper": 91 | await self.message.channel.send(embed=p1_win) 92 | elif self.results[self.p2.id] == "rock": 93 | await self.message.channel.send(embed=p2_win) 94 | 95 | async def interaction_check(self, interaction: discord.Interaction) -> bool: 96 | if interaction.user not in [self.p1, self.p2]: 97 | await interaction.response.send_message( 98 | "This button is not for you!", 99 | ephemeral=True, 100 | ) 101 | return False 102 | return True 103 | 104 | async def on_timeout(self) -> None: 105 | for button in self.children: 106 | button.disabled = True 107 | 108 | await self.message.edit(view=self) 109 | await super().on_timeout() 110 | 111 | self.stop() 112 | await self.message.reply("Timed Out") 113 | -------------------------------------------------------------------------------- /src/core/models/tag.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class Tag: 6 | """Dataclass for tags""" 7 | 8 | guild_id: int 9 | tag_name: str 10 | tag_value: str 11 | tag_author: str 12 | time_created: int 13 | -------------------------------------------------------------------------------- /src/core/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .asyncpg_context import asyncpg_connect 2 | from .calc import slow_safe_calculate, calculate 3 | from .count_lines import get_files, get_lines 4 | from .formatters import format_seconds, number_suffix, discord_timestamp 5 | from .other import chunkify, functime 6 | 7 | __all__ = [ 8 | "asyncpg_connect", 9 | "slow_safe_calculate", 10 | "calculate", 11 | "get_files", 12 | "get_lines", 13 | "format_seconds", 14 | "number_suffix", 15 | "discord_timestamp", 16 | "chunkify", 17 | "functime", 18 | ] 19 | -------------------------------------------------------------------------------- /src/core/utils/asyncpg_context.py: -------------------------------------------------------------------------------- 1 | """ (module) asyncpg_context 2 | Context manager for asyncpg 3 | """ 4 | 5 | from contextlib import asynccontextmanager 6 | 7 | import asyncpg 8 | 9 | 10 | @asynccontextmanager 11 | async def asyncpg_connect(database_url: str) -> asyncpg.connection.Connection: 12 | """ 13 | Custom context manager to use asyncpg 14 | Very useful to ensure that once I open a connection it will ALWAYS be closed even upon error 15 | 16 | Parameters: 17 | database_url: str: The connection string 18 | 19 | Yeilds: 20 | asyncpg.connection.Connection: The connection to the database 21 | """ 22 | # Connect to the database 23 | connection = await asyncpg.connect(database_url) 24 | 25 | # Return connection for with statement 26 | yield connection 27 | 28 | # close connection once context manager is closed 29 | await connection.close() 30 | -------------------------------------------------------------------------------- /src/core/utils/calc.py: -------------------------------------------------------------------------------- 1 | """ (module) calc 2 | Used to calculate an expression using the mathjs api 3 | """ 4 | 5 | import urllib 6 | from typing import Optional 7 | 8 | import numexpr 9 | import aiohttp 10 | 11 | 12 | async def slow_safe_calculate( 13 | expr: str, only_int: Optional[bool] = False 14 | ) -> Optional[int | str]: 15 | """ 16 | Calculates a math expression using the mathjs API 17 | 18 | Parameters: 19 | expr (str): The expression to evaluate 20 | only_int (Optional[bool]): If this is True it will return the result only if its int 21 | if its not an int it will return None. (By default this option is False) 22 | 23 | Returns: 24 | int | str | None: It will return int if the result is already numeric or the result is int 25 | it will return str if the result is something like a float 26 | it will reutrn None if only_int is True and the result is not int 27 | """ 28 | if expr.isnumeric(): 29 | return int(expr) 30 | 31 | expression = urllib.parse.quote(expr.replace("**", "^")) 32 | async with aiohttp.ClientSession() as session: 33 | async with session.get(f"http://api.mathjs.org/v4/?expr={expression}") as r: 34 | result = await r.text() 35 | 36 | if only_int and result.isnumeric() is False: 37 | return None 38 | 39 | if only_int: 40 | return int(result) 41 | 42 | return result 43 | 44 | 45 | async def calculate(expr: str) -> Optional[float]: 46 | """ 47 | Evaluates a math expression with numexpr 48 | 49 | Parameters: 50 | expr (str): the expression to evaluate 51 | 52 | Returns: 53 | Optional[float[]: It will return the result of the expression and if it 54 | fails then it will return None 55 | """ 56 | try: 57 | result = numexpr.evaluate(expr) 58 | except ( 59 | OverflowError, 60 | AttributeError, 61 | SyntaxError, 62 | ZeroDivisionError, 63 | KeyError, 64 | ValueError, 65 | ): 66 | return None 67 | 68 | return result 69 | -------------------------------------------------------------------------------- /src/core/utils/count_lines.py: -------------------------------------------------------------------------------- 1 | """ (module) line_count 2 | Used to get the amount of lines in the current project 3 | """ 4 | 5 | import os 6 | from datetime import timedelta 7 | 8 | import aiofiles 9 | from aioredis import Redis 10 | 11 | import __main__ 12 | 13 | 14 | async def get_files() -> list[str]: 15 | """ 16 | Gets a list of all the files in a python project 17 | 18 | Returns: 19 | list[str]: List of file paths for the python files in the project 20 | """ 21 | file_list = [] 22 | path = os.path.dirname(os.path.abspath(__main__.__file__)) 23 | for root, _dirs, files in os.walk(path): 24 | if "git" in root: 25 | continue 26 | 27 | for file in files: 28 | file_name = os.path.join(root, file) 29 | 30 | if file_name.endswith(".py"): 31 | file_list.append(file_name) 32 | 33 | return file_list 34 | 35 | 36 | async def get_lines(redis: Redis) -> int: 37 | """ 38 | Gets the amount of lines in a python project 39 | 40 | Parameters: 41 | redis (Redis): an instance of redis.Redis which is connection to the redis database 42 | this will be used to cache the line count to speed up performance 43 | 44 | Returns: 45 | int: The amount of lines of code in the project 46 | """ 47 | if await redis.exists("python_line_count"): 48 | # 6ms compute time, 0.3ms get from cache time 49 | return int(await redis.get("python_line_count")) 50 | 51 | file_list = await get_files() 52 | 53 | lines = 0 54 | 55 | for file in file_list: 56 | async with aiofiles.open(file) as f: 57 | lines += len(list(await f.readlines())) 58 | 59 | await redis.set("python_line_count", lines) 60 | await redis.expire("python_line_count", timedelta(hours=6)) 61 | return lines 62 | -------------------------------------------------------------------------------- /src/core/utils/formatters.py: -------------------------------------------------------------------------------- 1 | """ (module) formatters 2 | This module contains formatting functions 3 | """ 4 | 5 | from enum import Enum 6 | from typing import Literal, Optional, Final 7 | 8 | 9 | # Enum for amount of seconds per time period 10 | class SecondIntervals(Enum): 11 | second = 1 12 | minute = 60 13 | hour = 3600 14 | day = 86400 15 | week = 604800 16 | month = 2627424 17 | year = 31536000 18 | century = 3153600000 19 | millennium = 31536000000 20 | 21 | 22 | async def format_seconds(seconds: int, short: Optional[bool] = False) -> str: 23 | """ 24 | Takes in seconds and formats it into a more human readable format 25 | 26 | Parameters: 27 | seconds (int): The time in seconds, this is the number that will be formated 28 | short (Optional[bool]): This decides if to format the text into a shorter form. 29 | Eg seconds -> sec, minutes -> min 30 | This is off by default and also it is only supported for ms, sec, min & hrs 31 | 32 | Returns: 33 | str: The formmated output 34 | if it fails to format is will return an empty string ("") 35 | """ 36 | 37 | if not isinstance(seconds, int) or seconds == 0: 38 | return "" 39 | 40 | # milliseconds 41 | elif 0 < seconds < SecondIntervals.second.value: 42 | ms = int(round((seconds * 1000), 0)) 43 | if short: 44 | return f"{ms} ms" 45 | return f"{ms} millisecond{'s' if ms != 1 else ''}" 46 | 47 | # convert to int (if not already) so we dont get shit like "7.0 minutes" 48 | seconds = int(seconds) 49 | 50 | # seconds 51 | if SecondIntervals.second.value <= seconds < SecondIntervals.minute.value: 52 | if short: 53 | return f"{seconds} sec" 54 | return f"{seconds} second{'s' if seconds != 1 else ''}" 55 | 56 | # minutes 57 | elif SecondIntervals.minute.value <= seconds < SecondIntervals.hour.value: 58 | minutes, seconds = divmod(seconds, SecondIntervals.minute.value) 59 | if short: 60 | seconds = f"{seconds} sec" if seconds != 0 else "" 61 | return f"{minutes} min {seconds}" 62 | 63 | seconds = ( 64 | f"{seconds} second{'s' if seconds != 1 else ''}" if seconds != 0 else "" 65 | ) 66 | return f"{minutes} minute{'s' if minutes != 1 else ''} {seconds}" 67 | 68 | # hours 69 | elif SecondIntervals.hour.value <= seconds < SecondIntervals.day.value: 70 | hours, minutes = divmod(seconds, SecondIntervals.hour.value) 71 | if short: 72 | return f"{hours} hrs " + await format_seconds(minutes, short) 73 | return f"{hours} hour{'s' if hours != 1 else ''} " + await format_seconds( 74 | minutes 75 | ) 76 | 77 | # days 78 | elif SecondIntervals.day.value <= seconds < SecondIntervals.week.value: 79 | days, hours = divmod(seconds, SecondIntervals.day.value) 80 | return f"{days} day{'s' if days != 1 else ''} " + await format_seconds(hours) 81 | 82 | # weeks 83 | elif SecondIntervals.week.value <= seconds < SecondIntervals.month.value: 84 | weeks, days = divmod(seconds, SecondIntervals.week.value) 85 | return f"{weeks} week{'s' if weeks != 1 else ''} " + await format_seconds(days) 86 | 87 | # months 88 | elif SecondIntervals.month.value <= seconds < SecondIntervals.year.value: 89 | months, weeks = divmod(seconds, SecondIntervals.month.value) 90 | return f"{months} month{'s' if months != 1 else ''} " + await format_seconds( 91 | weeks 92 | ) 93 | 94 | # years 95 | elif SecondIntervals.year.value <= seconds < SecondIntervals.century.value: 96 | years, months = divmod(seconds, SecondIntervals.year.value) 97 | return f"{years} year{'s' if years != 1 else ''} " + await format_seconds( 98 | months 99 | ) 100 | 101 | # centuries 102 | elif SecondIntervals.century.value <= seconds < SecondIntervals.millennium.value: 103 | century, years = divmod(seconds, SecondIntervals.century.value) 104 | return ( 105 | f"{century} {'centuries' if century != 1 else 'century'} " 106 | + await format_seconds(years) 107 | ) 108 | 109 | # millennia 110 | elif SecondIntervals.millennium.value <= seconds: 111 | millennium, century = divmod(seconds, SecondIntervals.millennium.value) 112 | return ( 113 | f"{millennium} {'millennia' if millennium != 1 else 'millennium'} " 114 | + await format_seconds(century) 115 | ) 116 | 117 | # if none 118 | return "" 119 | 120 | 121 | def number_suffix(number: int) -> str: 122 | """ 123 | This function adds the suffix / ordinal after a number provided 124 | 125 | Parameters: 126 | number (int): The number that will be formatted 127 | 128 | Returns: 129 | str: The formatted result 130 | """ 131 | SUFFIXES = {1: "st", 2: "nd", 3: "rd"} 132 | if 10 <= number % 100 < 20: 133 | suffix = "th" 134 | else: 135 | suffix = SUFFIXES.get(number % 10, "th") 136 | return str(number) + suffix 137 | 138 | 139 | def discord_timestamp( 140 | time: int, 141 | format_type: Literal["mdy", "md_yt", "t", "md_y", "w_md_yt", "ts", "h_m_s"], 142 | ) -> Optional[str]: 143 | """ 144 | This function takes in a timestamp and formats it into a discord timestamp 145 | Discord timestamps look something like this: 146 | 147 | Parameters: 148 | time (int): The unix timestamp 149 | format_type: (Literal[str]): The type of timestamp you want 150 | mdy = Month/Day/Year 151 | md_yt = Month Day, Year Time 152 | t = Time 153 | md_y = Month Day, Year 154 | w_md_yt = Weekday, Month Day, Year Time 155 | ts = Time since 156 | h_m_s = Hour:Minute:Second 157 | 158 | Returns: 159 | Union[str, None]: str with the discord timestamp. If invalid code is provided it will return None 160 | """ 161 | formated_times: Final = { 162 | "mdy": f"", 163 | "md_yt": f"", 164 | "t": f"", 165 | "md_y": f"", 166 | "w_md_yt": f"", 167 | "ts": f"", 168 | "h_m_s": f"", 169 | } 170 | 171 | return formated_times.get(format_type) 172 | -------------------------------------------------------------------------------- /src/core/utils/other.py: -------------------------------------------------------------------------------- 1 | import time 2 | import inspect 3 | import functools 4 | from typing import Any, Optional, Callable 5 | 6 | 7 | async def chunkify( 8 | big_list: list[Any], chunk_size: Optional[int] = 10 9 | ) -> list[list[Any]]: 10 | """ 11 | This function splits up a list into chunks of specified size 12 | 13 | Parameters: 14 | big_list (list[Any]): The list that will be split into chunks 15 | chunk_size (Optional[int]): The size of each chunk. The default size is 10 16 | 17 | Returns: 18 | list[list[Any]]: A list of lists. Each sub list is a chunk. 19 | """ 20 | return [big_list[i : i + chunk_size] for i in range(0, len(big_list), chunk_size)] 21 | 22 | 23 | def functime(func: Callable, ns=False): 24 | @functools.wraps(func) 25 | async def wrapper(*args: tuple, **kwargs: dict): 26 | if ns: 27 | start = time.perf_counter_ns() 28 | else: 29 | start = time.perf_counter() 30 | 31 | # run function 32 | if inspect.iscoroutinefunction(func): 33 | await func(*args, **kwargs) 34 | else: 35 | func(*args, **kwargs) 36 | 37 | if ns: 38 | stop = time.perf_counter_ns() 39 | else: 40 | stop = time.perf_counter() 41 | 42 | print("Function Execution Time:", stop - start) 43 | 44 | return wrapper 45 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is the main file for the bot 3 | It contains functions to startup the bot 4 | 5 | If you are reading my code i'm sorry :moyai: 6 | """ 7 | 8 | __version__ = "2.0.0" 9 | __author__ = "FusionSid" 10 | __licence__ = "MIT License" 11 | 12 | 13 | import os 14 | import sys 15 | import time 16 | 17 | from rich.progress import Progress 18 | from rich.traceback import install 19 | 20 | from core import WhyBot 21 | from core.helpers import get_why_config, log_errors, on_error 22 | 23 | 24 | def start_bot(client: WhyBot) -> None: 25 | """ 26 | Starts up the amazing Why Bot 27 | 28 | Parameters: 29 | client (WhyBot): The instance of commands.Bot / subclasses. 30 | This is the bot that will be started 31 | """ 32 | cogs = {} 33 | 34 | # get path of cogs directory 35 | path = os.path.join(os.path.dirname(__file__), "cogs") 36 | 37 | for category in os.listdir(path): 38 | if not os.path.isdir( 39 | os.path.join(path, category) 40 | ): # if its not a folder continue 41 | continue 42 | 43 | # loop through the files in cogs/ 44 | for filename in os.listdir(os.path.join(path, category)): 45 | if os.path.isfile( 46 | os.path.join(path, category, filename) 47 | ) and filename.endswith( 48 | ".py" 49 | ): # check if the item is a python file 50 | cog_name = filename[:-3] # remove .py from name 51 | cogs[cog_name] = f"cogs.{category}.{cog_name}" 52 | 53 | # load the testing_cog (if it exists) 54 | if os.path.exists(os.path.join(path, "testing_cog.py")): 55 | cogs["testing"] = "cogs.testing_cog" 56 | 57 | print("\n") 58 | client.cogs_list = cogs # for cog functions later like reload, load, unload 59 | 60 | # start all cogs with a spicy progress bar 61 | with Progress() as progress: 62 | loading_cogs = progress.add_task("[bold green]Loading Cogs", total=len(cogs)) 63 | while not progress.finished: 64 | for cog in cogs.values(): 65 | client.load_extension(cog) 66 | time.sleep(0.1) 67 | progress.update( 68 | loading_cogs, 69 | advance=1, 70 | description=f"[bold green]Loaded[/] [blue]{cog}[/]", 71 | ) 72 | progress.update(loading_cogs, description="[bold green]Loaded all cogs") 73 | 74 | time.sleep(1) 75 | 76 | client.event(on_error) # set event handler 77 | client.ipc.start() 78 | client.run(client.config.get("BOT_TOKEN")) 79 | 80 | 81 | if __name__ == "__main__": 82 | install(show_locals=True) 83 | sys.__excepthook__ = log_errors 84 | 85 | config = get_why_config() 86 | client = WhyBot(config, __version__) 87 | 88 | # Start 89 | start_bot(client) 90 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles 2 | Pillow 3 | qrcode 4 | simpcalci 5 | discord_colorize 6 | moviepy 7 | numexpr 8 | py-cord 9 | pycord_ext_ipc 10 | pyfiglet 11 | black 12 | rich 13 | pre-commit 14 | aiohttp 15 | aioredis 16 | asyncpg 17 | psutil 18 | PyYAML 19 | validators 20 | pycord.wavelink 21 | python-dateutil 22 | chat_exporter\ 23 | aioconsole -------------------------------------------------------------------------------- /src/scripts/backup.sh: -------------------------------------------------------------------------------- 1 | path=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 2 | export PGPASSWORD="Replace With PSQL Password" 3 | name=`date +%d_%m_%Y` 4 | mkdir -p $path/backups 5 | pg_dump -Fc -h -U -f $path/backups/$name.dump -------------------------------------------------------------------------------- /src/scripts/venv.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -rf ./venv 4 | python3 -m venv venv 5 | source venv/bin/activate 6 | pip install -r src/requirements.txt -------------------------------------------------------------------------------- /src/setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import sys 4 | import asyncio 5 | import subprocess 6 | from typing import Callable 7 | 8 | import yaml 9 | from rich.console import Console 10 | from rich.prompt import Prompt, Confirm 11 | 12 | from core.db import create_tables 13 | 14 | 15 | console = Console() 16 | PATH = os.path.dirname(__file__) 17 | 18 | 19 | def clear(): 20 | subprocess.call("clear" if os.name == "posix" else "cls") 21 | 22 | 23 | def install_requirements() -> None: 24 | """Install the required libraries from requirements.txt""" 25 | 26 | console.print("[bold blue]Requirements:") 27 | section_text = [ 28 | "In this section the script will ask you to install requirements", 29 | "It is recomended that you use a virtual env when installing them but this is not required", 30 | "Saying 'y' to the next prompt will install all the packages in src/requirements.txt", 31 | ] 32 | for text in section_text: 33 | console.print(f"[blue]{text}") 34 | 35 | if Confirm.ask("[bold blue]\nWould you like to install requirements?"): 36 | requirements_path = os.path.join(PATH, "requirements.txt") 37 | try: 38 | subprocess.check_call( 39 | [sys.executable, "-m", "pip", "install", "-r", requirements_path] 40 | ) 41 | except (subprocess.CalledProcessError, FileNotFoundError): 42 | console.print("[red bold]Failed to install requirements D:[/]") 43 | 44 | clear() 45 | 46 | 47 | def create_db_tables() -> None: 48 | """Create the database tables""" 49 | 50 | console.print("[bold blue]Database Tables:") 51 | var_path = os.path.join(PATH, "core/db/create_tables.py") 52 | section_text = [ 53 | "In this section the script will ask you to create the db tables", 54 | "Make sure you have already put the PostgreSQL database url in your config.yaml file", 55 | "By default the script will create all the tables. If the table exists it will DELETE it and remake it", 56 | "If you don't want that to happen or just want to make a specific table please edit TABLES_TO_CREATE", 57 | f'TABLES_TO_CREATE is located at: "{var_path}", line 152', 58 | ] 59 | for text in section_text: 60 | console.print(f"[blue]{text}") 61 | 62 | if Confirm.ask( 63 | "[bold blue]\nWould you like to create the tables in TABLES_TO_CREATE?" 64 | ): 65 | asyncio.run(create_tables()) 66 | 67 | clear() 68 | 69 | 70 | def create_config_file() -> None: 71 | """Create the config.yaml file""" 72 | 73 | console.print("[bold blue]Config File:") 74 | selfhost_path = os.path.join(PATH, "..", "docs/SELFHOSTING.md") 75 | section_text = [ 76 | "In this section the script will ask you to create the config.yaml file", 77 | "There will be a series of prompts asking for the data needed.", 78 | f"If you are not sure how to get the values or keys for a prompt read the selfhosting guide: {selfhost_path}", 79 | "Also not that answering 'y' to the next prompt will overwrite the current config.yaml if there is one", 80 | "So if you don't want that happening and just want to edit one value modify it directly in the file", 81 | ] 82 | for text in section_text: 83 | console.print(f"[blue]{text}") 84 | 85 | create_file = Confirm.ask( 86 | "[bold blue]\nWould you like to create the config.yaml file?" 87 | ) 88 | if not create_file: 89 | return clear() 90 | 91 | config_path = os.path.join(PATH, "config.example.yaml") 92 | 93 | with open(config_path, "r") as f: 94 | data: dict = yaml.load(f, Loader=yaml.SafeLoader) 95 | 96 | new_config_file_data = {key: None for key in data} 97 | 98 | for key, val in data.items(): 99 | value = None 100 | 101 | if type(val) == str: 102 | value = Prompt.ask( 103 | f"[bold yellow]Enter the value for '{key}' (type=string)", 104 | default=None, 105 | ) 106 | elif type(val) == int: 107 | value = Prompt.ask( 108 | f"[bold yellow]Enter the value for '{key}' (type=integer)", 109 | default=None, 110 | ) 111 | value = int(value) if value is not None and value.isnumeric() else 0 112 | elif type(val) == bool: 113 | value = Prompt.ask( 114 | f"[bold yellow]Enter the value for '{key}' (type=list)", 115 | choices=["true", "false"], 116 | default="false", 117 | ) 118 | value = True if value.lower() == "true" else False 119 | elif type(val) == list: 120 | console.print( 121 | "[bold yellow]Enter each item sperated by a comma or space eg: 123, 42 123" 122 | ) 123 | value = Prompt.ask( 124 | f"[bold yellow]Enter the value for '{key}' (type=list)", 125 | default=None, 126 | ) 127 | value = ( 128 | [int(x) for x in re.findall("\\d+", value) if x.isnumeric()] 129 | if value is not None 130 | else [] 131 | ) 132 | 133 | if value is not None: 134 | new_config_file_data[key] = value 135 | 136 | clear() 137 | 138 | with open(os.path.join(PATH, "config.aml"), "w") as f: 139 | yaml.dump(new_config_file_data, f) 140 | 141 | clear() 142 | 143 | 144 | def is_first_time() -> bool: 145 | """Ask user if its their first time""" 146 | 147 | console.print("[red bold]Why Bot Setup\n") 148 | 149 | section_text = [ 150 | "Welcome to the why bot setup script", 151 | "Please make sure you have the prerequisites that are found in the README", 152 | "Also if this is your first time running the script please answer 'y' to most of the prompts", 153 | ] 154 | for text in section_text: 155 | console.print(f"[red]{text}") 156 | 157 | is_first_time = Confirm.ask( 158 | "[bold red]\nIs this your first time running this script?" 159 | ) 160 | clear() 161 | return is_first_time 162 | 163 | 164 | def first_time() -> None: 165 | install_requirements() 166 | create_config_file() 167 | create_db_tables() 168 | 169 | 170 | def main() -> None: 171 | if is_first_time(): 172 | return first_time() 173 | 174 | options: dict[str, list[str | Callable[[], None]]] = { 175 | "req": ["Goes to the install requirements section", install_requirements], 176 | "cfg": ["Goes to the create new config file section", create_config_file], 177 | "dbt": ["Goes to the create db tables section", create_db_tables], 178 | "exit": ["Exits the script"], 179 | } 180 | while True: 181 | console.print("[b u green]Options:") 182 | for key, value in options.items(): 183 | console.print(f"[b green]{key}:[/] [green]{value[0]}") 184 | option = options.get(Prompt.ask("[b green]What would you like to do?"), None) 185 | if option is None: 186 | clear() 187 | continue 188 | if len(option) == 1: 189 | break 190 | 191 | option[1]() 192 | 193 | 194 | if __name__ == "__main__": 195 | clear() 196 | main() 197 | -------------------------------------------------------------------------------- /tests/async_redis.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aioredis 3 | from datetime import timedelta 4 | 5 | 6 | async def main(): 7 | redis = aioredis.from_url("", decode_responses=True, password="", port=6379) 8 | 9 | name = await redis.get("name") 10 | if name is None: 11 | name = input("What is your name? ") 12 | await redis.set("name", name) 13 | await redis.expire("name", timedelta(minutes=1)) 14 | return print("Hello", name) 15 | print("Hello", name) 16 | 17 | 18 | if __name__ == "__main__": 19 | asyncio.run(main()) 20 | -------------------------------------------------------------------------------- /tests/c_plus_py.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | 3 | lib = ctypes.CDLL("e.so") 4 | lib.run() 5 | 6 | print(5 + 5) 7 | 8 | # `gcc -fPIC -shared` 9 | -------------------------------------------------------------------------------- /tests/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int fib(n) { 4 | if (n <= 1) { 5 | return n; 6 | }; 7 | return fib(n - 1) + fib(n - 2); 8 | } 9 | 10 | void run() { 11 | for (int i = 0; i < 42; i++) 12 | { 13 | int res = fib(i); 14 | printf("%i\n", res); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /tests/rankimage_test.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | 3 | img = Image.new("RGBA", (900, 300), "#24272a") 4 | profile = Image.open("test.png").resize((250, 250)) 5 | 6 | 7 | def round_corners(image: Image.Image): 8 | background = Image.new("RGBA", size=image.size, color=(255, 255, 255, 0)) 9 | holder = Image.new("RGBA", size=image.size, color=(255, 255, 255, 0)) 10 | mask = Image.new("RGBA", size=image.size, color=(255, 255, 255, 0)) 11 | mask_draw = ImageDraw.Draw(mask) 12 | mask_draw.rounded_rectangle( 13 | (2, 2) + (image.size[0] - 2, image.size[1] - 2), 14 | radius=10, 15 | fill="black", 16 | ) 17 | holder.paste(image, (0, 0)) 18 | return Image.composite(holder, background, mask) 19 | 20 | 21 | img = round_corners(img) 22 | 23 | blank = Image.new("RGBA", size=img.size, color=(255, 255, 255, 0)) 24 | blank.paste(profile, (25, 25)) 25 | img = Image.alpha_composite(img, blank) 26 | 27 | draw = ImageDraw.Draw(img) 28 | font = ImageFont.truetype(f"f.ttf", 40) 29 | draw.text((300, 50), "FusionSid#3645", fill="white", font=font, align="center") 30 | font = ImageFont.truetype(f"f.ttf", 35) 31 | draw.text((700, 50), "#1", fill="white", font=font, align="center") 32 | 33 | 34 | position = (300, 225) 35 | ratio = 600 / 100 36 | to_width = (ratio * 55 + position[0]) / 1.05 37 | 38 | height = 40 + position[1] 39 | 40 | draw.rounded_rectangle( 41 | position + (to_width, height), 42 | fill="#ffffff", 43 | outline=None, 44 | width=1, 45 | ) 46 | 47 | draw.rounded_rectangle( 48 | (position[0] - 3, position[1] - 3) 49 | + ((ratio * 100 + position[0] + 3) / 1.05, height + 3), 50 | outline="#ffffff", 51 | width=3, 52 | ) 53 | 54 | img.save("e.png") 55 | # draw.rounded_rectangle( 56 | # position + (to_width, height), 57 | # radius=10, 58 | # fill="#00fa81", 59 | # outline=outline, 60 | # width=stroke_width, 61 | # ) 62 | --------------------------------------------------------------------------------