From 5b6cf5562f42daa636966d25a1d87c4a9d4fc47a Mon Sep 17 00:00:00 2001 From: Pukimaa Date: Thu, 1 Dec 2022 20:30:48 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Initial=20Commit=20(Public=20Bet?= =?UTF-8?q?a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/black.yml | 31 +++ .gitignore | 7 + .gitmoji-changelogrc | 7 + .idea/.gitignore | 8 + .idea/fileTemplates/Translation.yml | 1 + .idea/fileTemplates/cog.py | 12 + .vscode/cog.code-snippets | 36 +++ .vscode/cog_group.code-snippets | 37 +++ .vsls.json | 6 + CHANGELOG.md | 19 ++ LICENSE | 8 + README.md | 72 ++++++ TakoBot.py | 269 +++++++++++++++++++++ assets/TMDb.png | Bin 0 -> 27656 bytes assets/alert.png | Bin 0 -> 4022 bytes assets/bank.png | Bin 0 -> 3386 bytes assets/bank_dark.png | Bin 0 -> 3417 bytes assets/error.png | Bin 0 -> 3696 bytes assets/flag.png | Bin 0 -> 2537 bytes assets/flag_dark.png | Bin 0 -> 2596 bytes assets/megaphone.png | Bin 0 -> 3387 bytes assets/megaphone_dark.png | Bin 0 -> 3425 bytes assets/money.png | Bin 0 -> 4357 bytes assets/money_dark.png | Bin 0 -> 4570 bytes assets/ping/ping_green.png | Bin 0 -> 3132 bytes assets/ping/ping_orange.png | Bin 0 -> 3125 bytes assets/ping/ping_red.png | Bin 0 -> 3102 bytes assets/ping_green.png | Bin 0 -> 3132 bytes assets/ping_orange.png | Bin 0 -> 3125 bytes assets/ping_red.png | Bin 0 -> 3102 bytes assets/reddit.png | Bin 0 -> 11250 bytes assets/role.png | Bin 0 -> 7879 bytes assets/role_dark.png | Bin 0 -> 8223 bytes assets/rules.png | Bin 0 -> 3975 bytes assets/rules_dark.png | Bin 0 -> 4034 bytes assets/search.png | Bin 0 -> 9015 bytes assets/search_dark.png | Bin 0 -> 9787 bytes assets/tag.png | Bin 0 -> 5051 bytes assets/tag_dark.png | Bin 0 -> 5362 bytes assets/thumbnails/.exists | 0 assets/translation.png | Bin 0 -> 13576 bytes assets/translation_dark.png | Bin 0 -> 13997 bytes assets/trash.png | Bin 0 -> 3052 bytes assets/warning.png | Bin 0 -> 3614 bytes assets/webhook.png | Bin 0 -> 12384 bytes assets/webhook_dark.png | Bin 0 -> 12963 bytes cogs/config/__init__.py | 13 ++ cogs/config/autojoin.py | 196 ++++++++++++++++ cogs/config/color.py | 43 ++++ cogs/config/crosspost.py | 69 ++++++ cogs/config/language.py | 43 ++++ cogs/config/selfroles.py | 147 ++++++++++++ cogs/economy/__init__.py | 11 + cogs/economy/balance.py | 35 +++ cogs/economy/bank.py | 60 +++++ cogs/economy/gamble.py | 77 ++++++ cogs/economy/give.py | 84 +++++++ cogs/errors/__init__.py | 5 + cogs/errors/_cog.py | 95 ++++++++ cogs/info/__init__.py | 9 + cogs/info/_cog.py | 11 + cogs/info/announcements.py | 171 ++++++++++++++ cogs/info/info.py | 223 ++++++++++++++++++ cogs/info/ping.py | 36 +++ cogs/info/raw_message.py | 47 ++++ cogs/info/stats.py | 77 ++++++ cogs/misc/__init__.py | 27 +++ cogs/misc/affirmations.py | 19 ++ cogs/misc/autotranslate.py | 56 +++++ cogs/misc/embed.py | 74 ++++++ cogs/misc/emoji.py | 132 +++++++++++ cogs/misc/flags.py | 250 ++++++++++++++++++++ cogs/misc/image.py | 101 ++++++++ cogs/misc/media.py | 212 +++++++++++++++++ cogs/misc/reaction_translate.py | 69 ++++++ cogs/misc/reddit.py | 29 +++ cogs/misc/show_tag.py | 52 +++++ cogs/misc/tag.py | 317 +++++++++++++++++++++++++ cogs/misc/translate.py | 51 ++++ cogs/misc/youtube.py | 39 ++++ cogs/moderation/__init__.py | 9 + cogs/moderation/anti_phishing.py | 22 ++ cogs/moderation/ban_game.py | 94 ++++++++ cogs/moderation/clear.py | 96 ++++++++ cogs/owner/__init__.py | 5 + cogs/owner/_cog.py | 11 + cogs/owner/extension.py | 74 ++++++ cogs/owner/manage_announcements.py | 97 ++++++++ cogs/owner/sync.py | 30 +++ config.py | 26 +++ crowdin.yml | 17 ++ extensions/.exists | 4 + helper.py | 99 ++++++++ i18n/config/de.yml | 16 ++ i18n/config/en.yml | 16 ++ i18n/config/es.yml | 6 + i18n/config/fr.yml | 8 + i18n/config/he.yml | 8 + i18n/config/hr.yml | 6 + i18n/config/id.yml | 7 + i18n/config/pl.yml | 2 + i18n/config/pt.yml | 6 + i18n/config/sv.yml | 6 + i18n/economy/de.yml | 20 ++ i18n/economy/en.yml | 20 ++ i18n/economy/es.yml | 1 + i18n/economy/fr.yml | 13 ++ i18n/economy/he.yml | 1 + i18n/economy/hr.yml | 1 + i18n/economy/id.yml | 13 ++ i18n/economy/pl.yml | 1 + i18n/economy/pt.yml | 1 + i18n/economy/sv.yml | 1 + i18n/errors/de.yml | 15 ++ i18n/errors/en.yml | 15 ++ i18n/errors/es.yml | 1 + i18n/errors/fr.yml | 13 ++ i18n/errors/he.yml | 1 + i18n/errors/hr.yml | 1 + i18n/errors/id.yml | 1 + i18n/errors/pl.yml | 1 + i18n/errors/pt.yml | 1 + i18n/errors/sv.yml | 1 + i18n/info/de.yml | 40 ++++ i18n/info/en.yml | 43 ++++ i18n/info/es.yml | 38 +++ i18n/info/fr.yml | 35 +++ i18n/info/he.yml | 12 + i18n/info/hr.yml | 30 +++ i18n/info/id.yml | 38 +++ i18n/info/pl.yml | 23 ++ i18n/info/pt.yml | 19 ++ i18n/info/sv.yml | 30 +++ i18n/misc/de.yml | 27 +++ i18n/misc/en.yml | 30 +++ i18n/misc/es.yml | 26 +++ i18n/misc/fr.yml | 1 + i18n/misc/he.yml | 1 + i18n/misc/hr.yml | 19 ++ i18n/misc/id.yml | 26 +++ i18n/misc/pl.yml | 1 + i18n/misc/pt.yml | 9 + i18n/misc/sv.yml | 9 + i18n/moderation/de.yml | 9 + i18n/moderation/en.yml | 9 + i18n/moderation/es.yml | 2 + i18n/moderation/fr.yml | 1 + i18n/moderation/he.yml | 1 + i18n/moderation/hr.yml | 2 + i18n/moderation/id.yml | 3 + i18n/moderation/pl.yml | 1 + i18n/moderation/pt.yml | 2 + i18n/moderation/sv.yml | 2 + i18n/owner/de.yml | 3 + i18n/owner/en.yml | 3 + i18n/owner/es.yml | 1 + i18n/owner/fr.yml | 1 + i18n/owner/he.yml | 1 + i18n/owner/hr.yml | 1 + i18n/owner/id.yml | 1 + i18n/owner/pl.yml | 1 + i18n/owner/pt.yml | 1 + i18n/owner/sv.yml | 1 + main.py | 49 ++++ requirements.txt | 15 ++ test.py | 5 + utils.py | 351 ++++++++++++++++++++++++++++ 167 files changed, 5103 insertions(+) create mode 100644 .github/workflows/black.yml create mode 100644 .gitignore create mode 100644 .gitmoji-changelogrc create mode 100644 .idea/.gitignore create mode 100644 .idea/fileTemplates/Translation.yml create mode 100644 .idea/fileTemplates/cog.py create mode 100644 .vscode/cog.code-snippets create mode 100644 .vscode/cog_group.code-snippets create mode 100644 .vsls.json create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 TakoBot.py create mode 100644 assets/TMDb.png create mode 100644 assets/alert.png create mode 100644 assets/bank.png create mode 100644 assets/bank_dark.png create mode 100644 assets/error.png create mode 100644 assets/flag.png create mode 100644 assets/flag_dark.png create mode 100644 assets/megaphone.png create mode 100644 assets/megaphone_dark.png create mode 100644 assets/money.png create mode 100644 assets/money_dark.png create mode 100644 assets/ping/ping_green.png create mode 100644 assets/ping/ping_orange.png create mode 100644 assets/ping/ping_red.png create mode 100644 assets/ping_green.png create mode 100644 assets/ping_orange.png create mode 100644 assets/ping_red.png create mode 100644 assets/reddit.png create mode 100644 assets/role.png create mode 100644 assets/role_dark.png create mode 100644 assets/rules.png create mode 100644 assets/rules_dark.png create mode 100644 assets/search.png create mode 100644 assets/search_dark.png create mode 100644 assets/tag.png create mode 100644 assets/tag_dark.png create mode 100644 assets/thumbnails/.exists create mode 100644 assets/translation.png create mode 100644 assets/translation_dark.png create mode 100644 assets/trash.png create mode 100644 assets/warning.png create mode 100644 assets/webhook.png create mode 100644 assets/webhook_dark.png create mode 100644 cogs/config/__init__.py create mode 100644 cogs/config/autojoin.py create mode 100644 cogs/config/color.py create mode 100644 cogs/config/crosspost.py create mode 100644 cogs/config/language.py create mode 100644 cogs/config/selfroles.py create mode 100644 cogs/economy/__init__.py create mode 100644 cogs/economy/balance.py create mode 100644 cogs/economy/bank.py create mode 100644 cogs/economy/gamble.py create mode 100644 cogs/economy/give.py create mode 100644 cogs/errors/__init__.py create mode 100644 cogs/errors/_cog.py create mode 100644 cogs/info/__init__.py create mode 100644 cogs/info/_cog.py create mode 100644 cogs/info/announcements.py create mode 100644 cogs/info/info.py create mode 100644 cogs/info/ping.py create mode 100644 cogs/info/raw_message.py create mode 100644 cogs/info/stats.py create mode 100644 cogs/misc/__init__.py create mode 100644 cogs/misc/affirmations.py create mode 100644 cogs/misc/autotranslate.py create mode 100644 cogs/misc/embed.py create mode 100644 cogs/misc/emoji.py create mode 100644 cogs/misc/flags.py create mode 100644 cogs/misc/image.py create mode 100644 cogs/misc/media.py create mode 100644 cogs/misc/reaction_translate.py create mode 100644 cogs/misc/reddit.py create mode 100644 cogs/misc/show_tag.py create mode 100644 cogs/misc/tag.py create mode 100644 cogs/misc/translate.py create mode 100644 cogs/misc/youtube.py create mode 100644 cogs/moderation/__init__.py create mode 100644 cogs/moderation/anti_phishing.py create mode 100644 cogs/moderation/ban_game.py create mode 100644 cogs/moderation/clear.py create mode 100644 cogs/owner/__init__.py create mode 100644 cogs/owner/_cog.py create mode 100644 cogs/owner/extension.py create mode 100644 cogs/owner/manage_announcements.py create mode 100644 cogs/owner/sync.py create mode 100644 config.py create mode 100644 crowdin.yml create mode 100644 extensions/.exists create mode 100644 helper.py create mode 100644 i18n/config/de.yml create mode 100644 i18n/config/en.yml create mode 100644 i18n/config/es.yml create mode 100644 i18n/config/fr.yml create mode 100644 i18n/config/he.yml create mode 100644 i18n/config/hr.yml create mode 100644 i18n/config/id.yml create mode 100644 i18n/config/pl.yml create mode 100644 i18n/config/pt.yml create mode 100644 i18n/config/sv.yml create mode 100644 i18n/economy/de.yml create mode 100644 i18n/economy/en.yml create mode 100644 i18n/economy/es.yml create mode 100644 i18n/economy/fr.yml create mode 100644 i18n/economy/he.yml create mode 100644 i18n/economy/hr.yml create mode 100644 i18n/economy/id.yml create mode 100644 i18n/economy/pl.yml create mode 100644 i18n/economy/pt.yml create mode 100644 i18n/economy/sv.yml create mode 100644 i18n/errors/de.yml create mode 100644 i18n/errors/en.yml create mode 100644 i18n/errors/es.yml create mode 100644 i18n/errors/fr.yml create mode 100644 i18n/errors/he.yml create mode 100644 i18n/errors/hr.yml create mode 100644 i18n/errors/id.yml create mode 100644 i18n/errors/pl.yml create mode 100644 i18n/errors/pt.yml create mode 100644 i18n/errors/sv.yml create mode 100644 i18n/info/de.yml create mode 100644 i18n/info/en.yml create mode 100644 i18n/info/es.yml create mode 100644 i18n/info/fr.yml create mode 100644 i18n/info/he.yml create mode 100644 i18n/info/hr.yml create mode 100644 i18n/info/id.yml create mode 100644 i18n/info/pl.yml create mode 100644 i18n/info/pt.yml create mode 100644 i18n/info/sv.yml create mode 100644 i18n/misc/de.yml create mode 100644 i18n/misc/en.yml create mode 100644 i18n/misc/es.yml create mode 100644 i18n/misc/fr.yml create mode 100644 i18n/misc/he.yml create mode 100644 i18n/misc/hr.yml create mode 100644 i18n/misc/id.yml create mode 100644 i18n/misc/pl.yml create mode 100644 i18n/misc/pt.yml create mode 100644 i18n/misc/sv.yml create mode 100644 i18n/moderation/de.yml create mode 100644 i18n/moderation/en.yml create mode 100644 i18n/moderation/es.yml create mode 100644 i18n/moderation/fr.yml create mode 100644 i18n/moderation/he.yml create mode 100644 i18n/moderation/hr.yml create mode 100644 i18n/moderation/id.yml create mode 100644 i18n/moderation/pl.yml create mode 100644 i18n/moderation/pt.yml create mode 100644 i18n/moderation/sv.yml create mode 100644 i18n/owner/de.yml create mode 100644 i18n/owner/en.yml create mode 100644 i18n/owner/es.yml create mode 100644 i18n/owner/fr.yml create mode 100644 i18n/owner/he.yml create mode 100644 i18n/owner/hr.yml create mode 100644 i18n/owner/id.yml create mode 100644 i18n/owner/pl.yml create mode 100644 i18n/owner/pt.yml create mode 100644 i18n/owner/sv.yml create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 test.py create mode 100644 utils.py diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 00000000..130c4ac6 --- /dev/null +++ b/.github/workflows/black.yml @@ -0,0 +1,31 @@ +name: Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Run linter + run: | + python -m pip install black + black . + #- name: Check Diff + # id: check_diff + # run: | + # git diff . || echo "::set-output name=diff::true" + - name: Commit and Push + #if: steps.check_diff.outputs.diff == 'true' + continue-on-error: true + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git add . + git commit -m "🚨 Linting" + git push diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..da769271 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +**/__pycache__ +.gitmodules +bot_secrets.py +assets/thumbnails/* +!assets/thumbnails/.exists +discord.log +venv diff --git a/.gitmoji-changelogrc b/.gitmoji-changelogrc new file mode 100644 index 00000000..b5287011 --- /dev/null +++ b/.gitmoji-changelogrc @@ -0,0 +1,7 @@ +{ + "project": { + "name": "Tako", + "description": "A multipurpose Discord bot for everything", + "version": "0.1.0-beta" + } +} \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/fileTemplates/Translation.yml b/.idea/fileTemplates/Translation.yml new file mode 100644 index 00000000..63f1c3e9 --- /dev/null +++ b/.idea/fileTemplates/Translation.yml @@ -0,0 +1 @@ +en: diff --git a/.idea/fileTemplates/cog.py b/.idea/fileTemplates/cog.py new file mode 100644 index 00000000..eafdfb21 --- /dev/null +++ b/.idea/fileTemplates/cog.py @@ -0,0 +1,12 @@ +import discord +from discord import app_commands +from discord.ext import commands + + +class Cog(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @app_commands.command() + async def example(self, interaction: discord.Interaction): + return diff --git a/.vscode/cog.code-snippets b/.vscode/cog.code-snippets new file mode 100644 index 00000000..07f8e55d --- /dev/null +++ b/.vscode/cog.code-snippets @@ -0,0 +1,36 @@ +{ + // Place your kayano-rewrite workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + // "Print to console": { + // "scope": "javascript,typescript", + // "prefix": "log", + // "body": [ + // "console.log('$1');", + // "$2" + // ], + // "description": "Log output to console" + // } + "Cog": { + "scope": "python", + "prefix": "cog", + "body": [ + "import discord", + "from discord import app_commands", + "from discord.ext import commands", + "", + "class Cog(commands.Cog):", + " def __init__(self, bot):", + " self.bot = bot", + "", + " @app_commands.command()", + " async def example(self, interaction: discord.Interaction):", + "" + ], + "description": "A simple yet elegant Cog for discord.py" + } +} \ No newline at end of file diff --git a/.vscode/cog_group.code-snippets b/.vscode/cog_group.code-snippets new file mode 100644 index 00000000..566d17a8 --- /dev/null +++ b/.vscode/cog_group.code-snippets @@ -0,0 +1,37 @@ +{ + // Place your kayano-rewrite workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + // "Print to console": { + // "scope": "javascript,typescript", + // "prefix": "log", + // "body": [ + // "console.log('$1');", + // "$2" + // ], + // "description": "Log output to console" + // } + "Cog": { + "scope": "python", + "prefix": "cog group", + "body": [ + "import discord", + "from discord import app_commands", + "from discord.ext import commands", + "", + "class Cog(commands.GroupCog, group_name=\"cog\"):", + " def __init__(self, bot):", + " self.bot = bot", + "", + " @app_commands.command()", + " async def example(self, interaction: discord.Interaction):", + " return", + "" + ], + "description": "A simple yet elegant Cog Group for discord.py" + } +} \ No newline at end of file diff --git a/.vsls.json b/.vsls.json new file mode 100644 index 00000000..f980af55 --- /dev/null +++ b/.vsls.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json.schemastore.org/vsls", + "excludeFiles": [ + "bot_secrets.py" + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..239af258 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + + +## 0.1.0-alpha (2022-07-15) + +### Added + +- 🎉 Initial Commit [[0fa12d5](https://github.com/tako-discord/tako/commit/0fa12d52e0af64a3a83c80b57b1e4a0d570e776e)] + +### Changed + +- 🚨 Linting [[71f8789](https://github.com/tako-discord/tako/commit/71f8789152f1dbed9c9b35f3900463424086fb44)] +- 🎨 Better note regarding the public bot [[2e42cec](https://github.com/tako-discord/tako/commit/2e42cecb26680fe71ce5665cfb43282eb0193b60)] + +### Miscellaneous + +- 🌐 New Crowdin Setup [[ea0aee9](https://github.com/tako-discord/tako/commit/ea0aee9dcd77fa8c8e607f25dfc50dc122d0fe7a)] + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..ac63b1ff --- /dev/null +++ b/LICENSE @@ -0,0 +1,8 @@ +MIT License +©️ Copyright 2021-current Jaron Ain + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +End license text. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..19848a8b --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# Tako +[![Crowdin](https://badges.crowdin.net/tako/localized.svg)](https://translate.tako.rocks) + +A Discord bot done right. No bullshit like pay- or votewalls. + +This is the rewrite for Kayano (now Tako). Before the rewrite it was written in JavaScript/Node.js with the Discord.js Library. But now it's written in Python with the discord.py Library. We have made some very great improvements and we hope you'll like it. + +> **Warning** | +> This project is still in *beta* and might be unstable and buggy + +> **Note** | We strongly recommend using [the public bot](https://top.gg/bot/878366398269771847) instead of selfhosting as it's well configured and does not have any disadvantages. Selfhosting is very complicated if you want everything to work perfectly. + +## 🏃‍♂️ Get Started +> Do you need help or found a bug? +> [Open an issue](https://github.com/tako-discord/tako/issues/new)! +### 📀 What you need +- [Python](https://www.python.org/) (3.10 or higher) +- [PIP](https://pip.pypa.io/) +- [PostgreSQL](https://www.postgresql.org/) (Tested: v14-15) + +Be sure to install everything before heading to the next step. +### 📥 Installation +Please note that instead of `python` your command may be `python3` or similar. +1. Clone this repository + - `git clone https://github.com/kayano-bot/tako` +2. Install dependencies + - `cd tako` + - `pip install -r requirements.txt` +3. Create the database + - [postgresql.org/docs/15/tutorial-createdb.html](https://www.postgresql.org/docs/15/tutorial-createdb.html) +4. Add Secrets + - Create a `bot_secrets.py` inside the `tako` folder + - Use this as a template: + ```python + TOKEN = "" # https://discord.com/developers/applications + DB_NAME = "tako" # Or however you named your database + DB_HOST = "localhost" # gonna be different if using a non-local database + DB_PORT = 5432 # Optional port of the DB (Default: 5432) + DB_USER = "postgres" + DB_PASSWORD = "" + YOUTUBE_API_KEY = "" # https://developers.google.com/youtube/v3/getting-started + TMDB_API_KEY = "" # https://www.themoviedb.org/documentation/api + ``` + - More info: soon™️ +5. Initialize Database + - `python helper.py` (Be sure that you are still in the `tako` folder you cloned earlier) + - Choose "Init Database" +6. Start the bot + - `python helper.py` and choose "Start the bot" *OR* `python main.py` +7. Sync commands + - Inside discord run `tk!sync` (or if available: `/sync`) in a channel the bot has access to. This is to make all slash commands visible. +8. Enjoy! 😀 + +## 🤝 Contributing +1. Fork this repository +2. Create a new branch +3. Make your changes in that branch +4. Commit with [gitmoji](https://gitmoji.dev/) as a commit guide +5. Make a Pull Request +6. Be proud of yourself 👍 + +## 💖 Credits +Huge thanks to the discord.py Community, helping out if I had any question. +Another big thanks goes out to the users using this bot and all the contributors to this project. + +Also huge thanks to the other (core) developer(s): +- [*LyQuid*](https://github.com/LyQuid12) + +and all the testers: +- [*@cmod31*](https://github.com/cmod31) +- [*@abUwUser*](https://github.com/abUwUser) +- *Olex3#3259* diff --git a/TakoBot.py b/TakoBot.py new file mode 100644 index 00000000..2896c10d --- /dev/null +++ b/TakoBot.py @@ -0,0 +1,269 @@ +import json +import os +import i18n +import config +import random +import discord +import aiohttp +import requests +import bot_secrets +from discord.ext import commands, tasks +from utils import new_meme, thumbnail, get_color, get_language + +trimmer = "----------" + + +class TakoBot(commands.Bot): + async def on_ready(self): + print(trimmer) + print(f"Logged in as {self.user} ({self.user.id})\n{trimmer}") + + async def setup_hook(self): + for category in os.listdir("cogs"): + await self.load_extension(f"cogs.{category}") + i18n.set("filename_format", "{locale}.{format}") + i18n.set("fallback", "en") + i18n.load_path.append(f"i18n") + self.update_phishing_list.start() + if hasattr(bot_secrets, "UPTIME_KUMA"): + self.uptime_kuma.start() + self.presence_update.start() + self.postgre_guilds = await self.db_pool.fetch("SELECT * FROM guilds") + self.badges_update.start() + self.add_view(MemeButtons(self)) + self.add_view(AffirmationButtons()) + self.loop.create_task(self.selfrole_setup()) + + @tasks.loop(seconds=55) + async def uptime_kuma(self): + async with aiohttp.ClientSession() as cs: + await cs.get(bot_secrets.UPTIME_KUMA + str(round(self.latency * 1000))) + return + + @uptime_kuma.before_loop + async def before_uptime_kuma(self): + await self.wait_until_ready() + + @tasks.loop(hours=1) + async def update_phishing_list(self): + self.sussy_domains = [] + if hasattr(config, "ANTI_PHISHING_LIST"): + for list in config.ANTI_PHISHING_LIST: + self.sussy_domains.extend(requests.get(list).json()["domains"]) + + @tasks.loop(seconds=7.5) + async def presence_update(self): + presences = [ + {"name": "with the new rewrite", "type": discord.ActivityType.playing}, + { + "name": f"over {len(self.guilds)} server{'s' if len(self.guilds) > 1 else ''}", + "type": discord.ActivityType.watching, + }, + { + "name": f"{len(self.users)} user{'s' if len(self.users) > 1 else ''}", + "type": discord.ActivityType.listening, + }, + { + "name": f"{len(self.tree.get_commands())} {'commands' if len(self.tree.get_commands()) > 1 else 'command'}", + "type": discord.ActivityType.listening, + }, + { + "name": "/ commands", + "type": discord.ActivityType.listening, + }, + { + "name": f"with version {self.version}", + "type": discord.ActivityType.playing, + }, + { + "name": "translate at translate.tako.rocks", + "type": discord.ActivityType.playing, + }, + ] + random_presence = random.choice(presences) + await self.change_presence( + activity=discord.Activity( + type=random_presence["type"], name=random_presence["name"] + ) + ) + + @presence_update.before_loop + async def before_presence_update(self): + await self.wait_until_ready() + + @tasks.loop(hours=1) + async def badges_update(self): + for guild in self.guilds: + for role in guild.roles: + if role.id == config.DONATOR_ROLE: + users = [] + for member in role.members: + users.append(member.id) + await self.db_pool.execute( + "UPDATE badges SET users = $1 WHERE name = 'Donator';", users + ) + continue + if role.id == config.TRANSLATOR_ROLE: + users = [] + for member in role.members: + users.append(member.id) + await self.db_pool.execute( + "UPDATE badges SET users = $1 WHERE name = 'Translator';", users + ) + continue + if role.id == config.ALPHA_TESTER_ROLE: + users = [] + for member in role.members: + users.append(member.id) + await self.db_pool.execute( + "UPDATE badges SET users = $1 WHERE name = 'Alpha Tester';", + users, + ) + continue + if role.id == config.DEV_ROLE: + users = [] + for member in role.members: + users.append(member.id) + await self.db_pool.execute( + "UPDATE badges SET users = $1 WHERE name = 'Core Developer';", + users, + ) + continue + + @badges_update.before_loop + async def before_badges_update(self): + await self.wait_until_ready() + + async def selfrole_setup(self): + await self.wait_until_ready() + selfrole_menus = await self.db_pool.fetch("SELECT * FROM selfroles") + for item in selfrole_menus: + view = discord.ui.View(timeout=None) + menu = SelfMenu( + self, + item["select_array"], + item["min_values"], + item["max_values"], + str(item["id"]), + ) + view.add_item(menu) + self.add_view(view) + + +class AffirmationButtons(discord.ui.View): + def __init__(self): + super().__init__(timeout=None) + + @discord.ui.button( + label="Another one", + style=discord.ButtonStyle.blurple, + emoji="❤️", + custom_id="next_affirmation", + ) + async def next_affirmation( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + async with aiohttp.ClientSession() as session: + async with session.get("https://affirmations.dev/") as r: + data = await r.json() + await interaction.response.edit_message( + content=data["affirmation"], view=self + ) + + +class MemeButtons(discord.ui.View): + def __init__(self, bot): + super().__init__(timeout=None) + self.bot = bot + + @discord.ui.button( + label="Another one", style=discord.ButtonStyle.blurple, custom_id="next_meme" + ) + async def next_meme( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + embed, file = await new_meme( + interaction.guild.id, interaction.user.id, self.bot, self.bot.db_pool + ) + + await interaction.response.edit_message( + embed=embed, attachments=[file], view=self + ) + + @discord.ui.button( + label="Share it", + style=discord.ButtonStyle.grey, + custom_id="share_meme", + ) + async def share_meme( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + data = await self.bot.db_pool.fetchval( + "SELECT last_meme FROM users WHERE user_id = $1;", interaction.user.id + ) + if not data: + return await interaction.response.send_message( + "We couldn't share this meme!", ephemeral=True + ) + + data = json.loads(data) + thumbnail_path = await thumbnail(interaction.guild.id, "reddit", self) + file = discord.File(thumbnail_path, filename="thumbnail.png") + + embed = discord.Embed( + title=f"{data['title']}", + description=data["postLink"], + color=await get_color(self, interaction.guild.id), + ) + embed.set_author( + name=data["author"], + url=f"https://reddit.com/u/{data['author']}", + icon_url="https://www.redditstatic.com/avatars/defaults/v2/avatar_default_1.png", + ) + embed.set_thumbnail(url="attachment://thumbnail.png") + embed.set_image(url=data["url"]) + embed.set_footer(text=f"r/{data['subreddit']} • {data['ups']} 👍") + + await interaction.response.send_message( + i18n.t("misc.meme_share", user=interaction.user.display_avatar), + embed=embed, + file=file, + ) + + +class SelfMenu(discord.ui.Select): + def __init__( + self, bot, select_array: list, min_values: int, max_values: int, uuid: str + ): + options = [] + for role_id in select_array: + for guild in bot.guilds: + for role in guild.roles: + if role.id == role_id: + options.append( + discord.SelectOption(label=role.name, value=str(role_id)) + ) + super().__init__( + custom_id=uuid, + placeholder="No roles selected", + options=options, + min_values=min_values, + max_values=max_values, + ) + self.bot = bot + + async def callback(self, interaction: discord.Interaction): + await interaction.response.defer() + for option in self.options: + role = discord.utils.get(interaction.guild.roles, id=int(option.value)) + if str(role.id) in self.values: + await interaction.user.add_roles(role) + else: + await interaction.user.remove_roles(role) + await interaction.followup.send( + content=i18n.t( + "config.selfroles_updated", + locale=get_language(self.bot, interaction.guild.id), + ), + ephemeral=True, + ) diff --git a/assets/TMDb.png b/assets/TMDb.png new file mode 100644 index 0000000000000000000000000000000000000000..acea011b80db78c5d7d5d5e5b222d4c38af06906 GIT binary patch literal 27656 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π<{qr4jQ3`bSwVtS-}7364A0{i zUrZ7~$gM^3=_=*)2C`GG(T3hsSb+eMQK3v5@AA4YN7x0B52I&KalhGOrn2u6R5k$> zjf#W+O%tI!!|YZ(^JZ`&)MA%NXwQwLZ)a{O^a(2m%;{rW8Eom@1lOlWY!vuYx830I?v9P=@v<|b+;K)<6XFh-a<(&?C>+X#b zotJlGF&JBC#>&0V{fxFMfnQzl{`*Nv+Ie7dQRJaSm5m?vr^cjIU^A0i<320OdcBJj z_YsSYmHQJTif(JS`7o_JX_<$E!D& zGPkzr>;>Y1y-8Q-mqY0Agmz1Ti;m47Ryv=d$y zR;l>Jy$g;ZU8MNL&6@hCnOPv+S{t|y}m4{;ZG84{S@ZD(C)1L+L4E=G`DcC zd@feG99sOYPsZw#NB1^t zMV~K6BhB-O;dcsxJxF`=SkHg1eA6y zrv(4$2805QhKgkWNn}5*mbg(6GuV?8#W--c*6V`MnuwQ32YQ^O2v~eYMO`e!Scf@5 z5I3*yX*?zFt7q#EP=VYzFOEf>_Ky2VbI{gkKeM@XbK&v6x9D#+W&^OQQ_th)G|H&5 zg0y**bqC;hfyLQt4%yUzw$IF&?MImH-%{@yB!cghDvp<+hFRR*=^9@28UMU`Qg`1y z+8=IYnNfuT@J8rbMe>#iIGhK{S817>m6;`sS*D{%J!%%1fA|DRuu)2g~C;}wM2 zHWFg#9t{YI#c^Kl*5cHZ6KCSMb50i74>e@odHobHR_MquJq99f^JrJ`3kOyx`sT%5 zz;5QXXi7?<-n&Fk>OZqB`1ZPIK`T_P6Ph=DZ)&*)Dtsm+dYtQ)4H}~dtm**`umcn3BK1}WeP#D(x!YvZ zSdI82>STOe&M)|U$rfmd@VdiiQ`o{%XNK!VQTZms;)s8AShI&+iZizhG1qZzFO_+FkH~-?RoXI6 zkqcp^U#p_~@^N9HSNdA~pE!5+uu`xG;h;o0rZ$bs)JEr)|5>dsQpVv{P5nK{;2m#` zr7>y5xVA?|nJ*}S{_zb`9~|Ib@|I5fFf6K&^w&Z=e6s#3k7if?9tlZ|zCMo>1~Q+?Ff%`|LO}WDUAY%Fys9>^-*S)AO_OT_jshQ2&(RxICftT z-aMb&W4lY?fBCm`(W~euA{i5AlrpcWuW{{m=>YP23=ry+bqk+ND@QWr*?r+3+u=yQ zx)gw}Q~z+N!~SLtMMJRul@YP=uG2UaEr(QqQY-GGT{x=B6ZAovt-n##P{%XlEy7si zh(LLphQ&8U?{Z^H^M+wMv8VPEvZ^lDRw~As7m4{6x%VznMj^}px^yu>6Q9jK?*SMh zc<@YT4t%~%U!u~v=1~FSKqT~)_IcQMe<+y?fRh`Y&+OfCWZ%xzl#2y@vtl}eYHd#- zFCtoCH7cH#xbjimX00H-S-``3&;-3q_ZHJ=_%zUi@tsxL6;sIaY9^4CuQ^5zqk=(& zB*N^pZLaG4*B;>{`jl9lz#(-mvhTucO{{}wyKhMm(vaSp;^P^h8z+G<6Yi_D&%U*Q zT=c4$7XkuT3y%DCA?EhgNv0+N`jc*^-a>#KP&mBRzh#u)JsIw44>@oW8HHOkv`V8? z8K;%J2#Oi?BTZ<7vq!&O-~@VqvTC+# z1Qy5;W91(61%JU*iriy@t^gw=+JnV_79q|iqavD`G{mSoW~Gxxn_X} zOwLysGm?#Gf!!bP`a9p%>!*tise$_W?%*Xb!98w|NP1A`+i3xb*%V8O|HU$is2t}5 zSt03I3a>7Z-UnQt7IWRsaKeYX*yLNP(f*wP4p5h-Jn+1@z4Hl=>R2tEbMF zvTBG#_GBGea;~Ah7_3Jerm$l=^9OVemWnF+vLqXe=D4Uz5u+1KnTUhA&1`uSYyk`7 zwd`vufp&tne`QbkWtwd2y!3{9GC8m)J;{o5J*M^Ie#%c3wv7#%?%Wp4MG`6tNH%_# z$jS5A;(^a_^svYDB`85Psed?)g&?Id6bmeuarNz@m07v2#PA%Y_i1)cmeRBjb zw9gpwn~YC1H7dd?_(ngI5{>31nXUyMAz7_>}*#A;U49(KazoYG2vUbAFuN^c4 z3)w(}_|}Hg@iU8k!#Q8fuOuh-mF{P1R}`k2g2^8gj(-)I+Eeb zXg8-Xb=qX_wd}GgZ#9>;S9`JE+LteDjtjlZA93juth#4;87wDlvwkkd;ZwSDJ!WA- zW|j5wPm3;9FYjNzo6p;KY5Sj~K&B<@Y@mw9Wc4xT$(Za9tlw`zrDIoW4!EPg;3#RtSnmYCJ4 zo=}+xuh0JK>&{x0%gdI>@Tv!_tg+stkrCllOA%ftx!5SAU7P<5MC5k{;6&EKgpPMA zw#d5+KJyf+H0Q;4yF2!{YyVZh9M|FpZ+Q#A3S7u4cxaYkb0Dyq8;ameJk}5@M|_H*cvV zRk3lY!1i5R2&VfjJsPeiv|`@TtM$ia4p=UDty>mN5DuROdCE9l@t>Ds{_|YUf8GrG j&%;Uo*I$n!S0IugpPokE$p9Yi0^QU#)G5}se)<0Z`rIYa literal 0 HcmV?d00001 diff --git a/assets/alert.png b/assets/alert.png new file mode 100644 index 0000000000000000000000000000000000000000..c9ac03675e3a334c0a7b071e7de4b315cbf9bfc8 GIT binary patch literal 4022 zcmeHKX;f3!7Ctu;2xAl+P??-SMIkk61d0KQA|l{G0mVpA87x9%76K+r#;S;YbwCRm zr6@xgg92elFlwKSp@4t^6Gb2dAu1$m<^_C2%6FLm|Q+vi2^laYgUl@1Rp2@^o;#2VqPA$ zyLepOIGq%#?p#w^Ex8=hA#NDNdJ|H{pIITng{pi2qBP)>7y__x4**UU5CEzFSV8;} zqQ_0~Yg=(({rAGAZW=i_QGFb0x(le9k@%#e9ei0aH^oXTT^$WQKCTNpL>mCqS^zAr z0zh?wUG!D()gE8N;cGVhZ8G4NAx@t7-#D}%1g-pop+e>KQ4LeU}{8y zb8lK5MxL}gtV~a#D5Gv=w6EV0cP7woZstx^HR&Fh_5f@XrlavY{QSIcT|WyaQ!v|H zhBoeyv^7PK{yR16djzow1#;FbBk`@pWkb0GbT#1yncOnYY0SbCccOr>wtYaQP&2nx zjwqvwN)VxU9rvHJ>$j}f2!<>HJ%99+KpnVEC=?{Lgn((RDv8yA^%s%I zNv!Z6Pz~`j^ioI&*GZv%qWP5gRGiEuwOh5K%_yDV`7j<@`MS{2t*XeIc`V`D}p~<3PXY-@e zNl$9EfpI=Flu%w9+|Z$%K5w;v*row49~fdxmGc;PGWzh!!9-xZ1_CDzhN=vdo?a;t z$~241*)Wvte9{^9c z{y8Q8B_&Gv`f*DH^UIWoY8Vn_PQVFcpYfX-4pF5VpxTF|tnX1Xx%$hr#F;$ko+AV{ z--?RwEa0W-y)p#ce~qZ8ifB-3JgmF_rrNe%S`#A*a^eB*6+m5Tv;3re>vmAR2GBEg zag*wV>q|hG8*tL~QYZK+ckRB3aE^g_=13ix54wXSChU=Bvc^_e``#n1NYw=Mx`RQO z%OO~UDZF}k>@Y+f)c{+okDnQDxK8#3^bHWum%ylZ%)`zk9}tQj>uG z@2REQAWYCdWxkj?=?VaEFKMg;aoQ6&EpXdx-Kj5L@_DCK3p{2e@l)#+3GyT9mdOX{ z_X?g3Wkv?Pns7{e03Rzhw%M6o#!#c%VK8CFK=4@J`^gPXSomJ@U8G8Ggona)#nyH3 zt&>n(h!GjE^b|LTgbiypj7iu`CefF`Hs2)1ao!jI9HQo4gzf6($SCJ}J~T)o(e>fQ zo*3{LVvY%?2vdN11NOwpm2~or*-Kl(Jb;r&Xtv4v*w$NXIv3MTVdY-MXvBh8w?_Jl zPxvn$FE@4Y7iwB>!I(hcyTWB1wTr`~qBktKA4P_B;*9miYdUWwcD!oUq!`}@)K$F& zp(TJ3EJbssCz0P3)1XpBTSvGXI&!cT=(zzlO0>~wh!(Y2XUm?+QF6}0F`JGz!~5i1si8#w`e)EvNo}t2c{(|@ST}=f1|MDOcg#Vd!h8egei#<xzwjz zUnL9uJ--8)%-~bn1Zb=!AU+p;n*AM*H;dw@=$p}$^p$_r95>wO>mm zzs4B|Ma#Z17p{jZgI^8OMk;Q9c9EKDK1u^ryMr~FJ6FsTL=*z^WL?d}T>IMc+?_qx z51ibE07DZMv)yoV?2uWZw{6d*WRXC8#=OlO0x@OJ*_pQDYpmA|hiv#M`{mhYH{A`oN))z7q^66)ikBtQ>x|+%T=Z_-w zc>7EX)qEnsPsi8%+d&yivw2c|F)e9bl<5buLqcTR%;YdfY6UD@^LiCsro^AywElL_ e^nYwS)tYwi=dQj^mA1prY~a4j%eBmfnD!s|2I>j` literal 0 HcmV?d00001 diff --git a/assets/bank.png b/assets/bank.png new file mode 100644 index 0000000000000000000000000000000000000000..14d258a012146133faa9eea39b4ed5b190305ecd GIT binary patch literal 3386 zcmeHJX;hQf7X3a(%Zw-p3PceRrA3*P5FjXnS_VO8V*nXL!W@|th@u6hAt(ZcD5Ol$ zFp2~aAz+!KGKfH-G8tn+5M&-gfET(}-;cNYy83?f-@9v_b?-hu?mcU-efB+fFWOp3 ziYbW!03dn(oVf!4fN49iON5`nH5t6&Z{LKTbBh1~ak-rUft*MC_(?Fr!Ric9-hE(- z4}|>9Y|H@Q89|)qB@6%(J?G8M9HYURQFO2}E2eqgzx&P}iwmFbViXNHnZbS1n|pkP z@4KYULT1ITmXW%}@=HlwQV;gJB5x{_;5V!9Wb84sehdh89uR zKi(84fAGOVl3`BM*y{!j0shnf2!_@w!BnO&z%h;=+ZBVHoF@A`kt%tq7pzui^}23Nv8zj8jTO?!Juwd_>3%@-P|fsI zpPj9##sY88peN5hSrizrv6sxsBhtBnLJQcX`52&Cku&vWv+L7H%fJ3T-Bv>WL5uha`^he4e5*W7?+Lr14K(uh)P4=~hz z89O%y`GN5p2q>I{(TtPpBYnOB6fQ{zOHu;Y7ywTENj?|#JQ*1x0wB!y?)Oy~Z^87 z4>=3E!XArvcDFRHlb`YU&)$cQRKh7Uqm5AL#N6D`twpEHW)8=q`0K;@^qvtI)<0-|WvOzC3ft%EnGEm|oz%C!u&d-YGZK#?m z5QH7deP0w`CrIJyE+hvCX;sIbOEcrcAPv4{yaE{jKWeEGi8CqX{=(t2j$vmUP)&QRb4; zro%@sD|xKlalwy%?@UFlBE8Lqs&yd6x?EOoj9;&zEkpP(xcU<&i!wk($s`c|lBx?) z6D**)HO8vrf>F%Jv6~}-z1ji-qy2Q8miD*$stl4Uol1L$h~SrE>Qievc! zqx~behQ&e2lqutjma0QGZf7*81`u_Xue3$K%3ta1mZiPeOC_~NjS-9Z=#9^K&yyKw zG-Y>v$?}Hp_SR@Ek@&{4(v`0N?T|dX+!hu(Par19eWeBce9itL+O1r(H)vpG7OAWp z7LDDLz~gB@+X&ER=T4(b7WJij)|;=g9pSl}2iKdlYrI*LNZ8moA$>|KAg`*00_(qY zYyOL%>w(7-yMU~opDzK0yLJJZ0s`=RI3Vp1057*T~ ztVnsnzmLiKB{PK4_>3lh)~E`XskvITOfBh)Dx(=?%`}7rBWg)f)s`<*Gd3P{NJ8fz zlkcMUg=_|lo)g;P!g1D$Ni(;~HBXSgPt$e02Xih4 z|DFl|iLv~(V`B_b3er*5!s2`d}A3v!iCCH`7sihV8RnNDroW0~!-9EN8$y3!0!5~tK z!u@FaBm!)YUjF3%yi~6>1b@+Odx!`nmdcAS){!P8@&ZBb3ziC`hnAi zaqqb(D z&{r0gse8pg%P329qSTKt*DzmSHLpG`4UtG{aaF>9?l}tDVYw*d%W>d$gbdW*_{0B@ zk0v-Q1=KhbB+0A%SLLYw~;- zPcpYQYSknpepmvC&VbH_lvdDAB}f%!zvc$<$CCH<+cX|j^(H1LD9Ev;s0_flJRv8p z9!byGSd{Jb(HRbQ57!1J)N|}RLN37QEDvR02Rm{T|E{~k*g2=;G_g*f@EK%(=D0A> z&^*;=HUGiu-g0%lT&e*$LFCjW#4g#x6w(|rW8H~#feFLJz`4Guz7F0hZQUllYh<)O zcM}Rg=mjdv6%`&H>w^rH0XNgi#^p8(k8 zVsQBjXTv}4MeJ}-x9W)lT5*`e0=>F+jFZ`p{=ucOQ*1yn za-z}r2g-(yf>@{S8G*{9oa@Z>CJpj^g!M5<4@|Nv$;r_ay;b-YAZ_L_lc9c1_~DUJ zm$Hb4H&J4psKuXJmm;52!b>P0JM3ME`2wMLbM*0_4aRTiq4&$?wPIG>TGz%bil6?I f^Z)O6<5)aXmX7m69q+^Goy2(yTl4ZWp11x59y_31 literal 0 HcmV?d00001 diff --git a/assets/bank_dark.png b/assets/bank_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..e812b0818a193327627a8cdf3fbc82835ce55eba GIT binary patch literal 3417 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 Rwl#83pE+%1LN@le{V)8wgD(I8 literal 0 HcmV?d00001 diff --git a/assets/error.png b/assets/error.png new file mode 100644 index 0000000000000000000000000000000000000000..fb18364b45b9a51320a813fe525f7dccba4dca3b GIT binary patch literal 3696 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 G75)#cSz+`5 literal 0 HcmV?d00001 diff --git a/assets/flag_dark.png b/assets/flag_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..7df9e3c3b5a86d3d4dbe64599a2d56c5877cad07 GIT binary patch literal 2596 zcmdT`dr(tX8o#+Fav=x_voVtw+$L^Qs*^nSLb7rGMO^}=Azb_gd*`j%j<5y|^f ze)pkgWPRa7n6uPk= zyi@M3lvw5^9;3G|fKnM-{9)NLyQ=lI+ec$@x^D6dcZd2{7Z($BXg>yHe_my`$Eg(> z{(b-bd%MNu_Q`xfQ`1>~nEmb8?0^97c&}jK#PU~Zk3S#Ik31tTNvZ2dTI!9!SyxP> z8pSt=zQM(<6|&mdYI)jKg{Aq94^z3-*AGP>M38c+MCajUkK9q< z%YvT-uT@@mk@WVzkF)sEmy&UVZu9a{M~10V$GvlxODbI2WwMKe)|K>UL$D*~*<_b-NifRZlQ&4>l_0~@bq^zN!3bpER| zVwKBnYdnv|js@yEbw|ZacHSR8%=ASN;elWul2&xWm+x5`Jf%Q6k2ST)+}J|R^vE*) zOie5dvkijj(?>ut*#3^WVD=>y$J|FbZ@l9@DG>FOHb=Qk#uI}$4~kiOvq|wD6sX`P z-La`LzSf zj7J5rVp=lwgh_^f&@%tI7RCQR+wX&R7T?GyHI=dM_{`s4JW zz0A_Ol8^3>oWM+y%&!!1gw5JW`JP?-Q-LZ|t9C0U^mJvtApRtoVF$YXPNv9Xhx0xL z3CPM*)EnmX~uf-24{7>aXcRwu$u>2M(o;* zl9!M=0~^HDK9J@byWpdlci1*Y5>a`PyebFGVWDc9S+fx=Di26$ z*gUu1BNECks6@^U<9;}eYqxl*cBeO9@4(FIVtOijQT&g!7DWlF95IRr(0^`ScRX4>e52o4 z*^W#{sbYG1d+s12keBV^LdEh)d%UYl&WpoFA>cQE7t-O-CyP61l6>=hd2zpNz4KhN zbhSwFIW|39HQC&8N+{!Z!GDG`=6|-G*w<{2PH3SHbtXO=+50=MNB?C;d$<94B31nH RoQ;T0h1W5vJI|Go_6w$;hcy5I literal 0 HcmV?d00001 diff --git a/assets/megaphone.png b/assets/megaphone.png new file mode 100644 index 0000000000000000000000000000000000000000..b724460da2a55f5ed59fabee18ae630e38ec59e1 GIT binary patch literal 3387 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 literal 0 HcmV?d00001 diff --git a/assets/money.png b/assets/money.png new file mode 100644 index 0000000000000000000000000000000000000000..ec5bb9b52c221ffcdddec4f34b75e08ef41b528a GIT binary patch literal 4357 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 literal 0 HcmV?d00001 diff --git a/assets/ping/ping_green.png b/assets/ping/ping_green.png new file mode 100644 index 0000000000000000000000000000000000000000..ec531f8b1cc142226613182a08eeac63f1350d0c GIT binary patch literal 3132 zcmd5;c~sJA7yf}8NlrPYmV2Jo$!%zr( z5|=P{G<8JO+`wE&NhwiN95TgJbnyGmch2|6_xCh^-1DCM+Ht-Y4ml8$wA@ zLlFP~C1)qNI{<(lY#KyfiVz#kx}?Vzyc04M0H7+H1_E;Oc1l4|s5`hYbQRydOM67w#dac%c z{lU(lAOD3h?)Fm_X&1FH5s3=lqe{@n%9CdC}Tjkq(1R(KV9Bg?77UwT)a%u9vR+7F^xYxqMh_ zBaiB%`Csb7$Xg(kGm%;n%qoAht=J&Fwr<8Y7AfDu%gE)~eh(N)Ud-s6ysRsC-WVnNKlKgfe@KX^!*L4Pd`>3ii)POy&S-D5>`d z*dD_(~cq=pMbUGjX@bUEikY z|4#a}y47+pC0gaX&#!*~$NttK{;m_eOZ#i*^mv`A)hziHKV5|JPFx7>t}X7-N}eKwMTlPAw{2-?adt7&H)zejUMUoR4M9pLIxtu=Oyu}@G_J?t&>_F=m2zsob$SnD)gDg7 z4u0HS8?`n$;k_37#keW_ijm%g2%!(|atH+}r>0UxQDS~F81I*(=oHs~!sb|s4o!Ey z-Ut|cl1=2OE{VExn?KE*XM_ZTHW3nYfP=_YpsC`Oav)BArytKY8v44%)mPn?MP=nc z+F8_s<}mp>1vymK2&6qZK#^@Z9Ql?wJVSuZ%u!dul-V^=*-u--P!U$T4_1DMo6@vR zPExj!MiJeWG8dn(R+YKjEN9EbH5~0#8s*)nPSssFV7!^pUgJe>?M6J4LwT;*_NhPA zhM1iEz8m@fGy3TQ2#Fl5&cG^kxr;G7(@{ZzaV0?42?0iMX4Anogi-w5X^B`2LET4y zh@4xxZ+|hk&daJtR>eo!N(zQ>-tZnyEYsh-XXrHcapcBGiH}~Z_Yitz^=+jIL;4ac z+A1zD5?M8g?cryjd~U1J^8#-YPTT`)Ya|+GRAXn11EBHsSgucE*9s-K@Uxjtkh-a+ zxGsJ!r`oRO+mt8TO1l1y42!9k6iV3CE_kWw%oQS877WFF#i_Fe4S&m#2fC zWDbS1+FE)}LTBLBqFylG+i>>Si+$h;GTthej~(c55Y)oTPEg!V)?KSAFB*)EsiNm+ zH;$BuT@N)9<_E4;LThT@Ud8)(>v2ZQRGiflj4MtNkNmS~q^bdr}DWmb%Eki}4RFhNs?_aCkKONhk}Qdv}%r>)ds zT8uj|7BjLW`SY?%TV&F{oMdLsyi51itZ`Q1n=$k*q0Ua8ydfMmX05GoVR>TiBTplJ zp%00aUGzhRSGx1!JPOKR;>xtOmpWNJh-7mH-}ype3QodkO$g>=3AyE`?m!lQey7P_ u{llOrFhyW$!k81i)%o7?_J2N8`vzbRu&xz;_J>HtKH%)=2B+DhZvF*t42=x{ literal 0 HcmV?d00001 diff --git a/assets/ping/ping_orange.png b/assets/ping/ping_orange.png new file mode 100644 index 0000000000000000000000000000000000000000..d64f22e43aa9927d8cce9bfcc6bbf74849fa7bd7 GIT binary patch literal 3125 zcmc&$X;71C68=8q$iOHdD=26{R9Hp~D63I)A+R7)LKqH-904S9BglQo@kK;&QBVQ7 zi4F?JkN^Re(_|FOD5n8p6i6a+2;qpC7{g)0MrZzPP3_;|N7q~3Pj_{7z0ce4bNy$u z!)8@oRR91sJD#$00|4;WH-jn5BM|*apZxgY;wi5<08rEVW*|^jv0WYn|MNVd%2Mc6O?4d*&;87M^m2L|$WL6mOzM!F! zKYPPttP}$OW#p_)J*T4+i7s&><{0^hG)We$27fG|$3=Zf8#pFenr^v7ps%}wkh>)Y zFfcFx>=YHD90<_R)Bp?>6i{|3AO{8mG(ph7@UH|vtD;mEHp7c6E3>GQPcb|4je`!Q zc+S|=Cs9nz2xj|r)`Bi92DVdE@As*JU~tXJK#}ZTBv-G`**t?H^UHcVJL@mS7Oy^u zoqQ4AF8mT6RtW(Xu1Pyv>0^jV<^$6^%!wtw|2o&CygVy*obRR8 zO`QFVBl){CYPjd4uS0-4W^l#F_Z*=`_cn8uF{BEDH5BxX+%L^%e&MQx znKUaXIGy37w+Uimebm3?!(*cG@|tNHV~!O))rvwv#L<<(hKrUyWBFyv!8AGMo*y_P z-nMVpgAti|6oEB1_;sW?_HV=X2X4rRbZ^RhEj<&DxmgvzPCP8{Rbz#>4{j5{)XoOb z)^3-dNYL5^LA4D)iO&Y!??!NHgL|eb={6#heG?P6^l-ZwK_{vTUrl+HsyyMgQ@he= zb-oM5EiJjzJ^XBi0~#8Z3Q}?yBvvC4r-nnrI-PgK}m(msai04{f-~CMm0g=hu{Y&4*+UeEPPn(?>bWwbY^DRZ}&OEF0^xKKk!h$Jv-FtJF!w7rV2oAqlfkq}t(k$cb;S|{+N8~VmZY~YQU#oh#&_CC^ z4<9v~hPt|zs85vMZ`@QEDJ~x_^`AFS?>HV%W@+8s6}9z-*bkZb>*x54t0|Kej^-=@-L+!? zVZwg)ww)#-i!ezEV%yzK)HGUr<*r}~Pf$dOOOHFf*_Suq&zUaDkV8XK$hSQbf6LpO zFWV9~9Vl5>?tGlBb!9+hs)z5kyD7DO4aa_lAd)EmxR_q7TcRjO2Ty+7*YSS)TH}Yu>81 zeskKLN=x&-QkO+?QhnKZ^rIqc8cy6XnqA?$ zi*#2TCN3FL_%!A14IHu9!?y4*=!+%^L&IW^c_!_K-(9}oK**XeJ{@5SxYYRZ?rKBM zSmvt3EU^+p!nrdZ*(b!VEO#T-=u7$f*oavC>(S>&_lreiL*ffjj-Td?qrHf-%d4RD zv&;O$An~K_y`u1hNN<^+2Pde5R`1EEX{n~oD7YE-&@&RTc78uIrpyRWI8!~JKk`-k z_KK*MM&u=gzelEu1JjH&5h`3glJa*G!*vT6;ZY;QbwQTo&5R+OVU?WOFHX1Fm78xJ zE?WNbyL=2A2|Zz=mz3E1Cd(m6lf~F$E&DV&&aD{N&+BFLB8!BfZrZNcogS0~T&LGEu=4ff@sdHNB4g#w0L)fEZ^ z>b^YXetT5jqB^0FOm=Xvj&L$62nm9ZuoKzEL#%)iiT(k;nvq1gn%ZPh(K+v9d|S5i zk=;sLe<@B#_c@3G!S@Tx!Rmj#l>PJD`9oVz(fNb#l=i#t#miMb;AoGwqu2&q{x2P8 BihKY7 literal 0 HcmV?d00001 diff --git a/assets/ping/ping_red.png b/assets/ping/ping_red.png new file mode 100644 index 0000000000000000000000000000000000000000..3324a999bc49012704536145b9723d0eff4d6d2c GIT binary patch literal 3102 zcmd5;eK^x=AOG!V#j(oU5yDKTt2c&pkYNX{_g#*E7^N>4|z$$T0?9@#;|$mtm}IIJb%~o$M?GL@Avz;?)$p0`*VNp&t0sm({|-u z$^ZatcRqnR0|1bmZ=tkRUP)@RWXgx1uAjgW0RX1)Eg-j7Kjs2XRCgL^b&cI+h4_YLHqy!yy0VkeJZl7q(TBrp-B zyR#eIIkNv;uv%#k)(cbI9C$o}df8o7;kc|L*tIC8@F2Oq5M4}aemkSXja!fgRVo9c z{TmQUcVzXY*1OaHGAqW>L(V!HwAH=G|Xt z%rNmIqrK>fmDh|-$*k_T@6wDWyL(dlwLp-`oEBG$^x*lFl;UwH-$fERxPPdKlESa1 zH)888hkAx&#r-Gwxh`_CWU;TM`YednMdy zFQT1N#w%Fico5F=y^r>*Lm*TdyjL@u>%l0?I%foh7SM+^`0Bt#emj=rnh z|9~npzGSACN7GRC4-ugwkfUAd^4ZqT(}qNI!RB)dIe2QRqi<~BRoU8tro0uu{L5!U z6p$-JvUqsjv5b+zY+UF1N|5 zTwdPSoSAO!CJ=5!i0KWAX_IdAZBccHZ<&a<-FB75R`XXsaax+v8E%8ao5NaMd@7Xw z=Z@R#k@4Sty|RhX&n_PyN;audcyjhNKVL_Se!_k_L&ogfBNc{V1lkUsN=g)L_T-S_ z)Jwd6ackl6Vmr`r-#{^aalL(=D$t(QgKJKMkbo({Trp|$)*sI=Ue47 zv1!ySL_VGg;Q4z z44EH2h_cYmQfA&uGCTV$%$$#}LE6My1KXWyAK4aCRlF_9o$*oPl>uwVhnOHf zEf9BuOh+O_E!)q7F)`2Gcm|A;sbFcv;mEvvO|sx-iwedXLK(#3|?#vy9|kX z@*wFBgEdgh>QZimxj;*3Wy0KZVbrX18($3Df3;A{(IPH8yyAAUd8Z)-)V-j1o`1h% zRs`2M>`zA-t#!q@7Y^Lq7iX+8_BPmv2A2+e{XqTHnP<`CAUg}>SQ1L z?@B*{%8jF+6(VAJXi`Xg3=W`{-b$MW?}>4${twrSrtK`N6(H0T|Bt-BfkvdS%xjvT zT&Yk=nweSOR&z}~19{if>=>np;*#8ymJTZwip+>ekr|##60X~PSx8Y#Wlo9D%%E(T zzAzYc*s9?}yJU8KFf>42RGwKnmrdE?7#8_tO@rAbzF_#S1AMY(lcT9|O5m(;F(|pa z?s|f!mM_;Gmj0QoSQ5tfe%}+x>Y88;cQm$KOw+j<5H3j@Bs)&cbk1etHLUg**?A*J z+28*8WYkZw|Ba24EO!MeG&j(5!V?IU@SNa)v=NMbz2%33ZOtXAR@}Pmp<(5QMV_~A zdO1ps6by;JiWH&C_kw4*w{IF+c{0jNIi$#2{^VF2B5T^l|9*!6d$p3kE?)N!BfoBCeB z_^FmwBog>T_AfVmF9tdhpWrWzuc;Vg)X`y5K0ivCu+n`g!`l}zkVt$n&uVsOl$8oO zqYTV%316$o0@JdK;+eI1a^9ei{@w?Eo?2L>epd85C*7;Gd7l9xr-ygvDr-y0kkr(` zRxP(EdbP7)iMMDj$^LWCA~8WHUxxCwzCg+^bm{2(BncI#hhQe05`Je`s7d_{+L}jR zEUd1!kXj1AdM30b_D|1p;!M$BMdz*zbgw?WCO1Z7{QKtF0^EXI;Y$vOU<}{pG)dql z__^*Ij)4!paiDm4w}whAzoJkFI4MVm8+by_4SU+4)hG6=)7ILVy|&To{!P*~Uc(`R zhKpT=yPIB^OQ8<4Gco9kWP%X&`OAArDAp8=sGxa8?Yr-?VW*whvs!InS}|T2+xVaB zni&kEUZO^p)hvaQueC3zrMWfI5x2pUq4D_e1Uxrz=^z3a)&I(YeP65k_hR`!?>y1g Y7d3ogru}ZK+|C2e$6PTqNB`u%0D6>i!2kdN literal 0 HcmV?d00001 diff --git a/assets/ping_green.png b/assets/ping_green.png new file mode 100644 index 0000000000000000000000000000000000000000..ec531f8b1cc142226613182a08eeac63f1350d0c GIT binary patch literal 3132 zcmd5;c~sJA7yf}8NlrPYmV2Jo$!%zr( z5|=P{G<8JO+`wE&NhwiN95TgJbnyGmch2|6_xCh^-1DCM+Ht-Y4ml8$wA@ zLlFP~C1)qNI{<(lY#KyfiVz#kx}?Vzyc04M0H7+H1_E;Oc1l4|s5`hYbQRydOM67w#dac%c z{lU(lAOD3h?)Fm_X&1FH5s3=lqe{@n%9CdC}Tjkq(1R(KV9Bg?77UwT)a%u9vR+7F^xYxqMh_ zBaiB%`Csb7$Xg(kGm%;n%qoAht=J&Fwr<8Y7AfDu%gE)~eh(N)Ud-s6ysRsC-WVnNKlKgfe@KX^!*L4Pd`>3ii)POy&S-D5>`d z*dD_(~cq=pMbUGjX@bUEikY z|4#a}y47+pC0gaX&#!*~$NttK{;m_eOZ#i*^mv`A)hziHKV5|JPFx7>t}X7-N}eKwMTlPAw{2-?adt7&H)zejUMUoR4M9pLIxtu=Oyu}@G_J?t&>_F=m2zsob$SnD)gDg7 z4u0HS8?`n$;k_37#keW_ijm%g2%!(|atH+}r>0UxQDS~F81I*(=oHs~!sb|s4o!Ey z-Ut|cl1=2OE{VExn?KE*XM_ZTHW3nYfP=_YpsC`Oav)BArytKY8v44%)mPn?MP=nc z+F8_s<}mp>1vymK2&6qZK#^@Z9Ql?wJVSuZ%u!dul-V^=*-u--P!U$T4_1DMo6@vR zPExj!MiJeWG8dn(R+YKjEN9EbH5~0#8s*)nPSssFV7!^pUgJe>?M6J4LwT;*_NhPA zhM1iEz8m@fGy3TQ2#Fl5&cG^kxr;G7(@{ZzaV0?42?0iMX4Anogi-w5X^B`2LET4y zh@4xxZ+|hk&daJtR>eo!N(zQ>-tZnyEYsh-XXrHcapcBGiH}~Z_Yitz^=+jIL;4ac z+A1zD5?M8g?cryjd~U1J^8#-YPTT`)Ya|+GRAXn11EBHsSgucE*9s-K@Uxjtkh-a+ zxGsJ!r`oRO+mt8TO1l1y42!9k6iV3CE_kWw%oQS877WFF#i_Fe4S&m#2fC zWDbS1+FE)}LTBLBqFylG+i>>Si+$h;GTthej~(c55Y)oTPEg!V)?KSAFB*)EsiNm+ zH;$BuT@N)9<_E4;LThT@Ud8)(>v2ZQRGiflj4MtNkNmS~q^bdr}DWmb%Eki}4RFhNs?_aCkKONhk}Qdv}%r>)ds zT8uj|7BjLW`SY?%TV&F{oMdLsyi51itZ`Q1n=$k*q0Ua8ydfMmX05GoVR>TiBTplJ zp%00aUGzhRSGx1!JPOKR;>xtOmpWNJh-7mH-}ype3QodkO$g>=3AyE`?m!lQey7P_ u{llOrFhyW$!k81i)%o7?_J2N8`vzbRu&xz;_J>HtKH%)=2B+DhZvF*t42=x{ literal 0 HcmV?d00001 diff --git a/assets/ping_orange.png b/assets/ping_orange.png new file mode 100644 index 0000000000000000000000000000000000000000..d64f22e43aa9927d8cce9bfcc6bbf74849fa7bd7 GIT binary patch literal 3125 zcmc&$X;71C68=8q$iOHdD=26{R9Hp~D63I)A+R7)LKqH-904S9BglQo@kK;&QBVQ7 zi4F?JkN^Re(_|FOD5n8p6i6a+2;qpC7{g)0MrZzPP3_;|N7q~3Pj_{7z0ce4bNy$u z!)8@oRR91sJD#$00|4;WH-jn5BM|*apZxgY;wi5<08rEVW*|^jv0WYn|MNVd%2Mc6O?4d*&;87M^m2L|$WL6mOzM!F! zKYPPttP}$OW#p_)J*T4+i7s&><{0^hG)We$27fG|$3=Zf8#pFenr^v7ps%}wkh>)Y zFfcFx>=YHD90<_R)Bp?>6i{|3AO{8mG(ph7@UH|vtD;mEHp7c6E3>GQPcb|4je`!Q zc+S|=Cs9nz2xj|r)`Bi92DVdE@As*JU~tXJK#}ZTBv-G`**t?H^UHcVJL@mS7Oy^u zoqQ4AF8mT6RtW(Xu1Pyv>0^jV<^$6^%!wtw|2o&CygVy*obRR8 zO`QFVBl){CYPjd4uS0-4W^l#F_Z*=`_cn8uF{BEDH5BxX+%L^%e&MQx znKUaXIGy37w+Uimebm3?!(*cG@|tNHV~!O))rvwv#L<<(hKrUyWBFyv!8AGMo*y_P z-nMVpgAti|6oEB1_;sW?_HV=X2X4rRbZ^RhEj<&DxmgvzPCP8{Rbz#>4{j5{)XoOb z)^3-dNYL5^LA4D)iO&Y!??!NHgL|eb={6#heG?P6^l-ZwK_{vTUrl+HsyyMgQ@he= zb-oM5EiJjzJ^XBi0~#8Z3Q}?yBvvC4r-nnrI-PgK}m(msai04{f-~CMm0g=hu{Y&4*+UeEPPn(?>bWwbY^DRZ}&OEF0^xKKk!h$Jv-FtJF!w7rV2oAqlfkq}t(k$cb;S|{+N8~VmZY~YQU#oh#&_CC^ z4<9v~hPt|zs85vMZ`@QEDJ~x_^`AFS?>HV%W@+8s6}9z-*bkZb>*x54t0|Kej^-=@-L+!? zVZwg)ww)#-i!ezEV%yzK)HGUr<*r}~Pf$dOOOHFf*_Suq&zUaDkV8XK$hSQbf6LpO zFWV9~9Vl5>?tGlBb!9+hs)z5kyD7DO4aa_lAd)EmxR_q7TcRjO2Ty+7*YSS)TH}Yu>81 zeskKLN=x&-QkO+?QhnKZ^rIqc8cy6XnqA?$ zi*#2TCN3FL_%!A14IHu9!?y4*=!+%^L&IW^c_!_K-(9}oK**XeJ{@5SxYYRZ?rKBM zSmvt3EU^+p!nrdZ*(b!VEO#T-=u7$f*oavC>(S>&_lreiL*ffjj-Td?qrHf-%d4RD zv&;O$An~K_y`u1hNN<^+2Pde5R`1EEX{n~oD7YE-&@&RTc78uIrpyRWI8!~JKk`-k z_KK*MM&u=gzelEu1JjH&5h`3glJa*G!*vT6;ZY;QbwQTo&5R+OVU?WOFHX1Fm78xJ zE?WNbyL=2A2|Zz=mz3E1Cd(m6lf~F$E&DV&&aD{N&+BFLB8!BfZrZNcogS0~T&LGEu=4ff@sdHNB4g#w0L)fEZ^ z>b^YXetT5jqB^0FOm=Xvj&L$62nm9ZuoKzEL#%)iiT(k;nvq1gn%ZPh(K+v9d|S5i zk=;sLe<@B#_c@3G!S@Tx!Rmj#l>PJD`9oVz(fNb#l=i#t#miMb;AoGwqu2&q{x2P8 BihKY7 literal 0 HcmV?d00001 diff --git a/assets/ping_red.png b/assets/ping_red.png new file mode 100644 index 0000000000000000000000000000000000000000..3324a999bc49012704536145b9723d0eff4d6d2c GIT binary patch literal 3102 zcmd5;eK^x=AOG!V#j(oU5yDKTt2c&pkYNX{_g#*E7^N>4|z$$T0?9@#;|$mtm}IIJb%~o$M?GL@Avz;?)$p0`*VNp&t0sm({|-u z$^ZatcRqnR0|1bmZ=tkRUP)@RWXgx1uAjgW0RX1)Eg-j7Kjs2XRCgL^b&cI+h4_YLHqy!yy0VkeJZl7q(TBrp-B zyR#eIIkNv;uv%#k)(cbI9C$o}df8o7;kc|L*tIC8@F2Oq5M4}aemkSXja!fgRVo9c z{TmQUcVzXY*1OaHGAqW>L(V!HwAH=G|Xt z%rNmIqrK>fmDh|-$*k_T@6wDWyL(dlwLp-`oEBG$^x*lFl;UwH-$fERxPPdKlESa1 zH)888hkAx&#r-Gwxh`_CWU;TM`YednMdy zFQT1N#w%Fico5F=y^r>*Lm*TdyjL@u>%l0?I%foh7SM+^`0Bt#emj=rnh z|9~npzGSACN7GRC4-ugwkfUAd^4ZqT(}qNI!RB)dIe2QRqi<~BRoU8tro0uu{L5!U z6p$-JvUqsjv5b+zY+UF1N|5 zTwdPSoSAO!CJ=5!i0KWAX_IdAZBccHZ<&a<-FB75R`XXsaax+v8E%8ao5NaMd@7Xw z=Z@R#k@4Sty|RhX&n_PyN;audcyjhNKVL_Se!_k_L&ogfBNc{V1lkUsN=g)L_T-S_ z)Jwd6ackl6Vmr`r-#{^aalL(=D$t(QgKJKMkbo({Trp|$)*sI=Ue47 zv1!ySL_VGg;Q4z z44EH2h_cYmQfA&uGCTV$%$$#}LE6My1KXWyAK4aCRlF_9o$*oPl>uwVhnOHf zEf9BuOh+O_E!)q7F)`2Gcm|A;sbFcv;mEvvO|sx-iwedXLK(#3|?#vy9|kX z@*wFBgEdgh>QZimxj;*3Wy0KZVbrX18($3Df3;A{(IPH8yyAAUd8Z)-)V-j1o`1h% zRs`2M>`zA-t#!q@7Y^Lq7iX+8_BPmv2A2+e{XqTHnP<`CAUg}>SQ1L z?@B*{%8jF+6(VAJXi`Xg3=W`{-b$MW?}>4${twrSrtK`N6(H0T|Bt-BfkvdS%xjvT zT&Yk=nweSOR&z}~19{if>=>np;*#8ymJTZwip+>ekr|##60X~PSx8Y#Wlo9D%%E(T zzAzYc*s9?}yJU8KFf>42RGwKnmrdE?7#8_tO@rAbzF_#S1AMY(lcT9|O5m(;F(|pa z?s|f!mM_;Gmj0QoSQ5tfe%}+x>Y88;cQm$KOw+j<5H3j@Bs)&cbk1etHLUg**?A*J z+28*8WYkZw|Ba24EO!MeG&j(5!V?IU@SNa)v=NMbz2%33ZOtXAR@}Pmp<(5QMV_~A zdO1ps6by;JiWH&C_kw4*w{IF+c{0jNIi$#2{^VF2B5T^l|9*!6d$p3kE?)N!BfoBCeB z_^FmwBog>T_AfVmF9tdhpWrWzuc;Vg)X`y5K0ivCu+n`g!`l}zkVt$n&uVsOl$8oO zqYTV%316$o0@JdK;+eI1a^9ei{@w?Eo?2L>epd85C*7;Gd7l9xr-ygvDr-y0kkr(` zRxP(EdbP7)iMMDj$^LWCA~8WHUxxCwzCg+^bm{2(BncI#hhQe05`Je`s7d_{+L}jR zEUd1!kXj1AdM30b_D|1p;!M$BMdz*zbgw?WCO1Z7{QKtF0^EXI;Y$vOU<}{pG)dql z__^*Ij)4!paiDm4w}whAzoJkFI4MVm8+by_4SU+4)hG6=)7ILVy|&To{!P*~Uc(`R zhKpT=yPIB^OQ8<4Gco9kWP%X&`OAArDAp8=sGxa8?Yr-?VW*whvs!InS}|T2+xVaB zni&kEUZO^p)hvaQueC3zrMWfI5x2pUq4D_e1Uxrz=^z3a)&I(YeP65k_hR`!?>y1g Y7d3ogru}ZK+|C2e$6PTqNB`u%0D6>i!2kdN literal 0 HcmV?d00001 diff --git a/assets/reddit.png b/assets/reddit.png new file mode 100644 index 0000000000000000000000000000000000000000..9c97ac9e131e7477e7269c8e0715d9fa8098f8d0 GIT binary patch literal 11250 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 literal 0 HcmV?d00001 diff --git a/assets/role.png b/assets/role.png new file mode 100644 index 0000000000000000000000000000000000000000..8974e6a9e06a73c55438a502c68ddd674728996e GIT binary patch literal 7879 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 JBlf%~;{dOIW3YOlGjvfT=>7he1l7MSlSi0VO5 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 W>awxmROAkk&eqzIcE{5H(mw#b;$Oc2 literal 0 HcmV?d00001 diff --git a/assets/search.png b/assets/search.png new file mode 100644 index 0000000000000000000000000000000000000000..e2c6386669c515f0e42d6b283f4ff1213ce54b79 GIT binary patch literal 9015 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 literal 0 HcmV?d00001 diff --git a/assets/search_dark.png b/assets/search_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d89e6e79e49f98b67822b2363eae13c713b1c686 GIT binary patch literal 9787 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 T1u_0zFW{l7wn~+fRmlGV5M%)i literal 0 HcmV?d00001 diff --git a/assets/tag.png b/assets/tag.png new file mode 100644 index 0000000000000000000000000000000000000000..72999850c03ba6d24f80cf494c097ccdb53abcc0 GIT binary patch literal 5051 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 ^_y&*s-#`r_5! zunOluDSWa*4^00;geQsp4W~eGJp&Gc6wDE%PZMQg(wP+UeUVTTrbWkb>W$?L;r@md zHJS|fM`2N)b=P&oj{O~?a#k_9F}CCq(kCTlz*|t|do`wB%))mb&hUoM!$UdkcKr>@ zFCm_i|M#?FPgotmq)nOh+t)m7P}XiPr+#G=1c^Ou0O0Ug7}^+XjH7g68U!i4V$xmG zoN%EcZfiuI_FVh^7}xbbDZOF3w5|paFkTZ$;pwUUdQL@L4kEGTJQm?%O5`S_D?(zS z3;--&&4BwYw}aryswWsG?T91!25lIo895?Ct)sRm%RiQbB)!G3h;}7unLHau4U_YF z<|`E5!u0mLrqCZ6*3m1+@XT1jFV{(P){yk)X|4TBIJXdHk^GqddSAzu!~pSk>(2+A z-hrTOFfCPltGrlceE&y<9aK9^9IR&NZ1H`(jZZW|5FtG>?KG=)v0Aa;L)L3{;y@DP vd9eoH&fv$FvF;#?KULxX?O&_^Cp|NN#;ajWt4ZbLqb0t*jT2gC<$3mBO>tZ| literal 0 HcmV?d00001 diff --git a/assets/tag_dark.png b/assets/tag_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..79ac2d1619d1136e082ba074f1c5b4318bf172b4 GIT binary patch literal 5362 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 DNW3HV literal 0 HcmV?d00001 diff --git a/assets/thumbnails/.exists b/assets/thumbnails/.exists new file mode 100644 index 00000000..e69de29b diff --git a/assets/translation.png b/assets/translation.png new file mode 100644 index 0000000000000000000000000000000000000000..924980ada880d0445a481756c42aea17c6b33fa0 GIT binary patch literal 13576 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 literal 0 HcmV?d00001 diff --git a/assets/translation_dark.png b/assets/translation_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..c1e227f6d1f1b733f8e5fd3cc06988b74b517068 GIT binary patch literal 13997 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 literal 0 HcmV?d00001 diff --git a/assets/trash.png b/assets/trash.png new file mode 100644 index 0000000000000000000000000000000000000000..2dea5912f5eafe075c38abbecca090e8216c7266 GIT binary patch literal 3052 zcmeHJ{ZEru6h3b$2!dt9MHE}gGBy(V8bS*73o$4Vv`#XrOkfqzsuXIi8;U>jx zfJQZO3E@v$2NJ4DP_xXt7`Q5U!PZ7~zrm=`zgPfVpD=8;_{dUu- z^F;=2dx&<&Tj%!N?&I$9FHe6nroH4JSzdIyX!%9%Mj!9^`$-vDfu;BT(80QcF=6u6 zw{3${ylO)nSItv!)dKT_?c}>tZ2(E>L|9)2Ao4>1VzA_o;<0?(09?HQKJy3giw9U= zJ|FNAc-YH@K--(rnq!t0>uU?runACKZL8w|tmz^mpO)n7)clZ{v5u?Ani^ABXhn0g zi#Y%!i=XBx$f$f7S+*+f6IVkHLE>J~toH#~2yTGdB#)v3BkjXkd&@|=5=Qgu2XnaA(0^Ud+^45)G1(f@Z|m$>fT z)0!W}*?Ju*?mbZjwnQWAv3W-6pBkwBFL^Q zrPth5{Tai@k2hu z_f(W6yTUn2PG~iQA(5JH*}I$O4jZyYdmbSwv9dPV4d@34CrS`i7df5BLs#G-&@#}F z;4<>CFDn@*`gQZ@N$({;BMh33_lMmmAY8$2tIoKnI_ufzm&sNj=%4EZKZViwgRZI$ z^@EqGbmkJ~ZcP|U)=#t}+&88x^(==x04G{ob(3hop-*nNOqvn?E*Iw3aSYjBB%I(| znb+{Zrxmi&K(TS?mRvI1?~S1MHaCl-IbT5W{?_S8 zdn92lP9KtcVbb?tl)t7|Q7UXWhIyoQqAsCb>R2dpqWp40$YS?fnZc^Q24%#TX*xk7 z9z|Lw-eoTl_I9uyKf7b@Hkm5SilwM^OR2+_-uBqX4SM&tR!&&hI@w~#jtHI&u{Y)J z+1LBpzCB$~KpY?;vS>|}BjQ}pED?{OM=HylB8dwvy2)AW5t4~Z*d~0@HBd> z)n*><8dVb*JWZgWUa3DGO*qZ=UXWX`6;%(lPmgyx%65AC5(fIE^|ND!YKfKV5qfy? z2|wB+%76Pk9ZBezirfH#tW~dSb-!80UNJi|G#x?3ZG<@zR1s^Rr|Rzp;2+B0aLU!D_)6|tXQA%IobbhLWYfSLak{qpkp|-`wK({In zRk25VN;re6N2dE#6~R{3QDB^t`n7P3vwejVFN}GflG*2l(|b{EFeOrwYhYjKBP!PN wSS*sQ%DFb>edQ-2A3-$C%dh`jhF)|y4bx@v%iCHOVB#DSHqv4nH!zO=0{~0e0ssI2 literal 0 HcmV?d00001 diff --git a/assets/warning.png b/assets/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..58480004e1c2c6c88ffb1a9e4728af0d253d03e5 GIT binary patch literal 3614 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 interaction.user.top_role + ): + return await interaction.response.send_message( + i18n.t("config.invalid_role", locale=language), ephemeral=True + ) + data = await bot.db_pool.fetchrow( + "SELECT * FROM guilds WHERE guild_id = $1", interaction.guild.id + ) + if not data: + if column == "join_roles_user": + await bot.db_pool.execute( + "INSERT INTO guilds (guild_id, join_roles_user) VALUES ($1, $2)", + interaction.guild.id, + [role.id], + ) + else: + await bot.db_pool.execute( + "INSERT INTO guilds (guild_id, join_roles_bot) VALUES ($1, $2)", + interaction.guild.id, + [role.id], + ) + return await interaction.response.send_message( + i18n.t("config.autojoinroles_added", role=role.name, locale=language), + ephemeral=True, + ) + array = data[column] + if array is None: + array = [] + if role.id not in array: + array.append(role.id) + if column == "join_roles_user": + await bot.db_pool.execute( + "UPDATE guilds SET join_roles_user = $1 WHERE guild_id = $2", + array, + interaction.guild.id, + ) + else: + await bot.db_pool.execute( + "UPDATE guilds SET join_roles_bot = $1 WHERE guild_id = $2", + array, + interaction.guild.id, + ) + return await interaction.response.send_message( + i18n.t("config.autojoinroles_added", role=role.name, locale=language), + ephemeral=True, + ) + array.remove(role.id) + if column == "join_roles_user": + await bot.db_pool.execute( + "UPDATE guilds SET join_roles_user = $1 WHERE guild_id = $2", + array, + interaction.guild.id, + ) + else: + await bot.db_pool.execute( + "UPDATE guilds SET join_roles_bot = $1 WHERE guild_id = $2", + array, + interaction.guild.id, + ) + return await interaction.response.send_message( + i18n.t("config.autojoinroles_removed", role=role.name, locale=language), + ephemeral=True, + ) + + +def no_roles_field(embed: discord.Embed, type: str, language: str = "en"): + embed.add_field( + name=i18n.t("config.autojoinroles_empty_title", type=type, locale=language), + value=i18n.t("config.autojoinroles_empty", type=type, locale=language), + inline=False, + ) + + +class Autojoin(commands.GroupCog, group_name="autojoinroles"): + def __init__(self, bot: TakoBot): + self.bot = bot + + @app_commands.command( + description="Toggle a role that will be automatically added to new users" + ) + @app_commands.checks.has_permissions(manage_roles=True) + @app_commands.checks.bot_has_permissions(manage_roles=True) + @app_commands.describe( + role="The role that should be toggled in the autojoinrole list" + ) + async def user(self, interaction: discord.Interaction, role: discord.Role): + await autojoin_logic(self.bot, interaction, role, "join_roles_user") + + @app_commands.command( + description="Toggle a role that will be automatically added to new bots" + ) + @app_commands.checks.has_permissions(manage_roles=True) + @app_commands.checks.bot_has_permissions(manage_roles=True) + @app_commands.describe( + role="The role that should be toggled in the autojoinrole list" + ) + async def bot(self, interaction: discord.Interaction, role: discord.Role): + await autojoin_logic(self.bot, interaction, role, "join_roles_bot") + + @app_commands.command( + description="List all roles that will be automatically added to new members" + ) + async def list(self, interaction: discord.Interaction): + await interaction.response.defer() + language = get_language(self.bot, interaction.guild.id) + data = await self.bot.db_pool.fetchrow( + "SELECT * FROM guilds WHERE guild_id = $1", interaction.guild.id + ) + thumbnail_path = await thumbnail(interaction.guild.id, "role", self.bot) + file = discord.File(thumbnail_path, filename="thumbnail.png") + embed = discord.Embed( + title=i18n.t( + "config.autojoinroles_title", + guild=interaction.guild.name, + locale=language, + ), + description=i18n.t("config.autojoinroles_desc", locale=language), + color=await get_color(self.bot, interaction.guild.id), + ) + embed.set_thumbnail(url="attachment://thumbnail.png") + if not data: + no_roles_field(embed, "users", language) + no_roles_field(embed, "bots", language) + else: + user_array = data["join_roles_user"] + bot_array = data["join_roles_bot"] + # Adding User field + if not user_array: + no_roles_field(embed, "users", language) + else: + embed.add_field( + name=i18n.t("config.autojoinroles_users", locale=language), + value="\n".join([f"<@&{role}> ({role})" for role in user_array]), + inline=False, + ) + # Adding Bot field + if not bot_array: + no_roles_field(embed, "bots", language) + else: + embed.add_field( + name=i18n.t("config.autojoinroles_bots", locale=language), + value="\n".join([f"<@&{role}> ({role})" for role in bot_array]), + inline=False, + ) + await interaction.followup.send(embed=embed, file=file) + delete_thumbnail(interaction.guild.id, "role") + + @commands.Cog.listener() + async def on_member_join(self, member: discord.Member): + data = await self.bot.db_pool.fetchrow( + "SELECT * FROM guilds WHERE guild_id = $1", member.guild.id + ) + if not data: + return + if member.bot: + for role in data["join_roles_bot"]: + role = member.guild.get_role(int(role)) + if role: + await member.add_roles(role) + return + if "MEMBER_VERIFICATION_GATE_ENABLED" in member.guild.features: + return + for role in data["join_roles_user"]: + role = member.guild.get_role(int(role)) + if role: + await member.add_roles(role) + + @commands.Cog.listener() + async def on_member_update(self, before: discord.Member, after: discord.Member): + if before.pending and not after.pending: + data = await self.bot.db_pool.fetchval( + "SELECT join_roles_user FROM guilds WHERE guild_id = $1", after.guild.id + ) + if not data: + return + for role in data: + role = after.guild.get_role(int(role)) + if role: + await after.add_roles(role) diff --git a/cogs/config/color.py b/cogs/config/color.py new file mode 100644 index 00000000..55379613 --- /dev/null +++ b/cogs/config/color.py @@ -0,0 +1,43 @@ +import re +import discord +from discord import app_commands +from discord.ext import commands + + +class Color(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(description="Set the color of your embeds") + @app_commands.describe( + color="A valid 6 character HEX code. (Example: #FFFFFF, 0xFFFFFF, FFFFFF (White), None (Default))" + ) + @app_commands.checks.has_permissions(manage_guild=True) + async def set_color(self, interaction: discord.Interaction, color: str): + if color.lower() == "none": + await self.bot.db_pool.execute( + "INSERT INTO guilds(guild_id, color) VALUES($1, $2) ON CONFLICT(guild_id) DO UPDATE SET guild_id = $1, color = $2", + interaction.guild.id, + None, + ) + return await interaction.response.send_message( + f"Your personal embed color is now back to default.", ephemeral=True + ) + if color.startswith("#"): + color = color.replace("#", "0x") + if not color.startswith("0x"): + color = f"0x{color}" + match = re.search(r"^0x([A-Fa-f0-9]{6})$", color) + if not match: + return await interaction.response.send_message( + "That's not a valid (*6* character) hex color.", ephemeral=True + ) + + await self.bot.db_pool.execute( + "INSERT INTO guilds(guild_id, color) VALUES($1, $2) ON CONFLICT(guild_id) DO UPDATE SET guild_id = $1, color = $2", + interaction.guild.id, + color, + ) + await interaction.response.send_message( + f"Your personal embed color is now `{color}`.", ephemeral=True + ) diff --git a/cogs/config/crosspost.py b/cogs/config/crosspost.py new file mode 100644 index 00000000..f161df18 --- /dev/null +++ b/cogs/config/crosspost.py @@ -0,0 +1,69 @@ +import i18n +import discord +from utils import get_language +from discord import app_commands +from discord.ext import commands + + +class Crosspost(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + @app_commands.command( + description="Set a channel where the messages automatically will be published" + ) + @app_commands.checks.has_permissions(manage_channels=True) + @app_commands.checks.bot_has_permissions(manage_channels=True) + @app_commands.describe( + channel="The news channel auto-crossposting should be enabled in", + state="Whetever auto-crossposting should be activated (True) or deactivated (False) (Default: True)", + ) + async def crosspost( + self, + interaction: discord.Interaction, + channel: discord.TextChannel = None, + state: bool = True, + ): + language = get_language(self.bot, interaction.guild.id) + if not channel: + channel = interaction.channel + if channel.type is discord.ChannelType.news: + await self.bot.db_pool.execute( + "INSERT INTO channels (channel_id, crosspost) VALUES ($1, $2) ON CONFLICT(channel_id) DO UPDATE SET crosspost = $2", + channel.id, + state, + ) + await interaction.response.send_message( + i18n.t( + f"config.crossposting_{'activated' if state else 'deactivated'}", + channel=channel.mention, + locale=language, + ), + ephemeral=True, + ) + else: + await interaction.response.send_message( + i18n.t( + "config.crossposting_not_news", + channel=channel.mention, + locale=language, + ), + ephemeral=True, + ) + + @commands.Cog.listener() + async def on_message(self, message: discord.Message): + if message.channel.type is discord.ChannelType.news: + crosspost = await self.bot.db_pool.fetchval( + "SELECT crosspost FROM channels WHERE channel_id = $1", + message.channel.id, + ) + if not crosspost: + return + if crosspost: + try: + return await message.publish() + except discord.HTTPException as e: + if e.code == 40033: + return + return print(e) diff --git a/cogs/config/language.py b/cogs/config/language.py new file mode 100644 index 00000000..e9ff40aa --- /dev/null +++ b/cogs/config/language.py @@ -0,0 +1,43 @@ +import i18n +import discord +from discord import app_commands +from discord.ext import commands +from discord.app_commands import Choice + + +class Language(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @app_commands.command( + description="Set the language of the bot (in the current server)" + ) + @app_commands.checks.has_permissions(manage_guild=True) + @app_commands.describe(language="The language to set the bot to") + @app_commands.choices( + language=[ + Choice(name="English", value="en"), + Choice(name="Deutsch", value="de"), + Choice(name="Español", value="es"), + Choice(name="Français", value="fr"), + Choice(name="עִברִית", value="he"), + Choice(name="Hrvatski", value="hr"), + Choice(name="Indonesia", value="id"), + Choice(name="Polski", value="pl"), + Choice(name="Português (brasileiro)", value="pt"), + Choice(name="Svenska", value="sv"), + ] + ) + async def set_language(self, interaction: discord.Interaction, language: str): + async with self.bot.db_pool.acquire() as conn: + await conn.execute( + "INSERT INTO guilds(guild_id, language) VALUES($1, $2) ON CONFLICT(guild_id) DO UPDATE SET guild_id = $1, language = $2", + interaction.guild.id, + language, + ) + data = await conn.fetch("SELECT * FROM guilds") + self.bot.postgre_guilds = data + await interaction.response.send_message( + i18n.t("config.language_success", language=language, locale=language), + ephemeral=True, + ) diff --git a/cogs/config/selfroles.py b/cogs/config/selfroles.py new file mode 100644 index 00000000..6625c46a --- /dev/null +++ b/cogs/config/selfroles.py @@ -0,0 +1,147 @@ +import i18n +import uuid +import discord +from TakoBot import TakoBot, SelfMenu +from discord import app_commands +from discord.ext import commands +from utils import get_language, thumbnail, get_color + + +class Selfroles(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(description="Create a role selection menu") + @app_commands.checks.has_permissions(manage_roles=True) + @app_commands.checks.bot_has_permissions(manage_roles=True) + @app_commands.guild_only() + async def selfroles( + self, + interaction: discord.Interaction, + title: str, + description: str, + role_1: discord.Role, + embed_state: bool = True, + min_values: int = None, + max_values: int = None, + role_2: discord.Role = None, + role_3: discord.Role = None, + role_4: discord.Role = None, + role_5: discord.Role = None, + role_6: discord.Role = None, + role_7: discord.Role = None, + role_8: discord.Role = None, + role_9: discord.Role = None, + role_10: discord.Role = None, + role_11: discord.Role = None, + role_12: discord.Role = None, + role_13: discord.Role = None, + role_14: discord.Role = None, + role_15: discord.Role = None, + role_16: discord.Role = None, + role_17: discord.Role = None, + role_18: discord.Role = None, + role_19: discord.Role = None, + role_20: discord.Role = None, + ): + role_array = [role_1] + + if role_2: + role_array.append(role_2) + if role_3: + role_array.append(role_3) + if role_4: + role_array.append(role_4) + if role_5: + role_array.append(role_5) + if role_6: + role_array.append(role_6) + if role_7: + role_array.append(role_7) + if role_8: + role_array.append(role_8) + if role_9: + role_array.append(role_9) + if role_10: + role_array.append(role_10) + if role_11: + role_array.append(role_11) + if role_12: + role_array.append(role_12) + if role_13: + role_array.append(role_13) + if role_14: + role_array.append(role_14) + if role_15: + role_array.append(role_15) + if role_16: + role_array.append(role_16) + if role_17: + role_array.append(role_17) + if role_18: + role_array.append(role_18) + if role_19: + role_array.append(role_19) + if role_20: + role_array.append(role_20) + + select_array = [] + language = get_language(self.bot, interaction.guild.id) + + for role in role_array: + if ( + role.is_default() + or role.is_bot_managed() + or role.managed + or not role.is_assignable() + or role >= interaction.user.top_role + ): + continue + select_array.append(role.id) + if not select_array: + return await interaction.response.send_message( + i18n.t("config.invalid_role", locale=language), ephemeral=True + ) + + if max_values is None: + max_values = len(select_array) + if max_values > len(select_array): + max_values = len(select_array) + + id = uuid.uuid4() + await self.bot.db_pool.execute( + "INSERT INTO selfroles (id, guild_id, select_array, min_values, max_values) VALUES ($1, $2, $3, $4, $5);", + id, + interaction.guild.id, + select_array, + min_values, + max_values, + ) + view = discord.ui.View(timeout=None) + menu = SelfMenu(self.bot, select_array, min_values, max_values, str(id)) + view.add_item(menu) + + file = discord.File( + await thumbnail(interaction.guild.id, "role", self.bot), + filename="thumbnail.png", + ) + + if embed_state: + embed = discord.Embed( + title=title, + description=description, + color=await get_color(self.bot, interaction.guild.id), + ) + embed.set_thumbnail(url="attachment://thumbnail.png") + await interaction.channel.send(embed=embed, view=view, file=file) + else: + await interaction.channel.send( + content=f"**{title}**\n{description}", view=view + ) + await interaction.response.send_message( + content=i18n.t( + "config.selfrole_created", + locale=language, + ), + ephemeral=True, + ) diff --git a/cogs/economy/__init__.py b/cogs/economy/__init__.py new file mode 100644 index 00000000..95a66881 --- /dev/null +++ b/cogs/economy/__init__.py @@ -0,0 +1,11 @@ +from .balance import Balance +from .bank import Bank +from .gamble import Gamble +from .give import Give + + +async def setup(bot): + await bot.add_cog(Balance(bot)) + await bot.add_cog(Bank(bot)) + await bot.add_cog(Gamble(bot)) + await bot.add_cog(Give(bot)) diff --git a/cogs/economy/balance.py b/cogs/economy/balance.py new file mode 100644 index 00000000..e144bd6e --- /dev/null +++ b/cogs/economy/balance.py @@ -0,0 +1,35 @@ +import discord +from discord import app_commands +from discord.ext import commands +from utils import fetch_cash, balance_embed, get_language + + +class Balance(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(description="Check a users balance") + @app_commands.describe(user="The user to check the balance of (Default: you)") + async def balance( + self, interaction: discord.Interaction, user: discord.User = None + ): + if user is None: + user = interaction.user + if user.bot: + import i18n + import config + + return await interaction.response.send_message( + i18n.t( + "economy.not_bot_balance", + locale=get_language(self.bot, interaction.guild_id), + currency=config.CURRENCY.replace(" ", "", 1) + if hasattr(config, "CURRENCY") + else " :coin:", + ), + ephemeral=True, + ) + + cash = await fetch_cash(self.bot.db_pool, user) + embed, file = await balance_embed(self.bot, user, interaction.guild.id, cash) + await interaction.response.send_message(embed=embed, file=file) diff --git a/cogs/economy/bank.py b/cogs/economy/bank.py new file mode 100644 index 00000000..a3f46731 --- /dev/null +++ b/cogs/economy/bank.py @@ -0,0 +1,60 @@ +import i18n +import discord +from discord import app_commands +from discord.ext import commands +from utils import fetch_cash, get_language, balance_embed + + +class Bank(commands.GroupCog, group_name="bank"): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(description="Deposit TK on your bank account") + @app_commands.describe(amount="The amount of TK to deposit") + async def deposit(self, interaction: discord.Interaction, amount: int): + cash = await fetch_cash(self.bot.db_pool, interaction.user) + language = get_language(self.bot, interaction.guild.id) + + if amount > cash[0]: + return await interaction.response.send_message( + i18n.t("economy.not_enough", locale=language), ephemeral=True + ) + + async with self.bot.db_pool.acquire() as con: + async with con.transaction(): + await con.execute( + "UPDATE users SET wallet=$2, bank=$3 WHERE user_id=$1", + interaction.user.id, + cash[0] - amount, + cash[1] + amount, + ) + cash = await fetch_cash(con, interaction.user) + embed, file = await balance_embed( + self.bot, interaction.user, interaction.guild.id, cash + ) + await interaction.response.send_message(embed=embed, file=file) + + @app_commands.command(description="Withdraw TK from your bank account") + @app_commands.describe(amount="The amount of TK to withdraw") + async def withdraw(self, interaction: discord.Interaction, amount: int): + cash = await fetch_cash(self.bot.db_pool, interaction.user) + language = get_language(self.bot, interaction.guild.id) + + if amount > cash[1]: + return await interaction.response.send_message( + i18n.t("economy.not_enough", locale=language), ephemeral=True + ) + + async with self.bot.db_pool.acquire() as con: + async with con.transaction(): + await con.execute( + "UPDATE users SET wallet=$2, bank=$3 WHERE user_id=$1", + interaction.user.id, + cash[0] + amount, + cash[1] - amount, + ) + cash = await fetch_cash(con, interaction.user) + embed, file = await balance_embed( + self.bot, interaction.user, interaction.guild.id, cash + ) + await interaction.response.send_message(embed=embed, file=file) diff --git a/cogs/economy/gamble.py b/cogs/economy/gamble.py new file mode 100644 index 00000000..1be9c495 --- /dev/null +++ b/cogs/economy/gamble.py @@ -0,0 +1,77 @@ +import i18n +import random +import config +import discord +from TakoBot import TakoBot +from discord import app_commands +from discord.ext import commands +from utils import get_color, create_user, get_language + + +class Gamble(commands.GroupCog, group_name="gamble"): + def __init__(self, bot: TakoBot): + self.bot = bot + + @app_commands.command(description="Play head or tail") + @app_commands.describe( + bet="The amount of TK to bet", guess="The guess you want to make" + ) + @app_commands.choices( + guess=[ + app_commands.Choice(name="Head", value=1), + app_commands.Choice(name="Tail", value=2), + ] + ) + async def flip(self, interaction: discord.Interaction, bet: int, guess: int): + language = get_language(self.bot, interaction.guild.id) + wallet = await self.bot.db_pool.fetchval( + "SELECT wallet FROM users WHERE user_id = $1;", interaction.user.id + ) + if not wallet: + await create_user(self.bot.db_pool, interaction.user) + if bet > wallet: + return await interaction.response.send_message( + i18n.t("economy.not_enough", locale=language), ephemeral=True + ) + result = random.randint(1, 2) + embed = discord.Embed( + title=i18n.t("economy.ht", locale=language), + color=await get_color(self.bot, interaction.guild.id), + description=i18n.t( + "economy.ht_won", locale=language, amount=str(bet) + config.CURRENCY + ) + if result == guess + else i18n.t( + "economy.ht_lost", locale=language, amount=str(bet) + config.CURRENCY + ), + ) + embed.add_field( + name=i18n.t("economy.bet", locale=language), + value=str(bet) + config.CURRENCY, + ) + embed.add_field( + name=i18n.t("economy.guess", locale=language), + value=i18n.t("economy.head", locale=language) + if guess == 1 + else i18n.t("economy.tail", locale=language), + ) + embed.add_field( + name=i18n.t("economy.new_balance", locale=language), + value=str(wallet + bet) + config.CURRENCY + if result == guess + else str(wallet - bet) + config.CURRENCY, + inline=False, + ) + if result == guess: + await self.bot.db_pool.execute( + "UPDATE users SET wallet = wallet + $1 WHERE user_id = $2;", + bet, + interaction.user.id, + ) + else: + await self.bot.db_pool.execute( + "UPDATE users SET wallet = wallet - $1 WHERE user_id = $2;", + bet, + interaction.user.id, + ) + await interaction.response.send_message(embed=embed) diff --git a/cogs/economy/give.py b/cogs/economy/give.py new file mode 100644 index 00000000..67a98677 --- /dev/null +++ b/cogs/economy/give.py @@ -0,0 +1,84 @@ +import i18n +import config +import discord +from discord import app_commands +from discord.ext import commands +from utils import fetch_cash, get_color, get_language, is_owner_func + + +class Give(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(description="Give other users money from your wallet") + @app_commands.describe( + user="The user to give the money to", amount="The amount of money to give away" + ) + async def give( + self, + interaction: discord.Interaction, + user: discord.User | discord.Member, + amount: int, + ): + language = get_language(self.bot, interaction.guild.id) + if user.bot: + return await interaction.response.send_message( + i18n.t("economy.not_bot", locale=language), ephemeral=True + ) + if user == interaction.user and not await self.bot.is_owner(interaction.user): + return await interaction.response.send_message( + i18n.t("economy.not_self", locale=language), ephemeral=True + ) + if amount <= 0: + return await interaction.response.send_message( + i18n.t( + "economy.more_than", amount=f"0{config.CURRENCY}", locale=language + ), + ephemeral=True, + ) + cash = await fetch_cash(self.bot.db_pool, interaction.user) + is_owner = await is_owner_func(self.bot, interaction.user) + if cash[0] < amount and not is_owner: + return await interaction.response.send_message( + i18n.t("economy.not_enough", locale=language), ephemeral=True + ) + + target_cash = await fetch_cash(self.bot.db_pool, user) + async with self.bot.db_pool.acquire() as conn: + async with conn.transaction(): + if not is_owner: + await conn.execute( + "UPDATE users SET wallet = $1 WHERE user_id = $2;", + cash[0] - amount, + interaction.user.id, + ) + await conn.execute( + "UPDATE users SET wallet = $1 WHERE user_id = $2", + target_cash[0] + amount, + user.id, + ) + + embed = discord.Embed( + description=f"", color=await get_color(self.bot, interaction.guild.id) + ) + if not is_owner: + embed.add_field( + name=str(interaction.user), + value=f"{cash[0]} → {cash[0] - amount}{config.CURRENCY}", + inline=False, + ) + embed.add_field( + name=str(user), + value=f"{target_cash[0]} → {target_cash[0] + amount}{config.CURRENCY}", + ) + + await interaction.response.send_message( + i18n.t( + "economy.give", + user=interaction.user.mention, + target=user.mention, + amount=f"**{amount}{config.CURRENCY}**", + locale=language, + ), + embed=embed, + ) diff --git a/cogs/errors/__init__.py b/cogs/errors/__init__.py new file mode 100644 index 00000000..4b4553bd --- /dev/null +++ b/cogs/errors/__init__.py @@ -0,0 +1,5 @@ +from ._cog import CommandErrorHandler + + +async def setup(bot): + await bot.add_cog(CommandErrorHandler(bot)) diff --git a/cogs/errors/_cog.py b/cogs/errors/_cog.py new file mode 100644 index 00000000..4189c0e2 --- /dev/null +++ b/cogs/errors/_cog.py @@ -0,0 +1,95 @@ +import sys +import i18n +import discord +import logging +from TakoBot import TakoBot +from discord import app_commands +from discord.ext import commands +from utils import get_language, error_embed + + +class CommandErrorHandler(commands.Cog): + def __init__(self, bot: TakoBot): + @bot.tree.error + async def on_app_command_error( + interaction: discord.Interaction, + error: app_commands.AppCommandError, + ): + if isinstance(error, app_commands.CommandNotFound): + return + + language = get_language(bot, interaction.guild.id) + footer = i18n.t("errors.error_occured", locale=language) + + if isinstance(error, app_commands.BotMissingPermissions): + missing = [ + perm.replace("_", " ").replace("guild", "server").title() + for perm in error.missing_permissions + ] + if len(missing) > 2: + fmt = "{}, & {}".format("**, **".join(missing[:-1]), missing[-1]) + else: + fmt = " & ".join(missing) + embed, file = error_embed( + i18n.t("errors.bot_missing_perms_title", locale=language), + i18n.t("errors.bot_missing_perms", perms=fmt, locale=language), + footer=footer, + ) + return await interaction.response.send_message( + embed=embed, file=file, ephemeral=True + ) + + if isinstance(error, app_commands.MissingPermissions): + missing = [ + perm.replace("_", " ").replace("guild", "server").title() + for perm in error.missing_permissions + ] + if len(missing) > 2: + fmt = "{}, & {}".format("**, **".join(missing[:-1]), missing[-1]) + else: + fmt = " & ".join(missing) + embed, file = error_embed( + i18n.t("errors.user_missing_perms_title", locale=language), + i18n.t("errors.user_missing_perms", perms=fmt, locale=language), + footer=footer, + ) + return await interaction.response.send_message( + embed=embed, file=file, ephemeral=True + ) + + if isinstance(error, app_commands.CommandOnCooldown): + embed, file = error_embed( + i18n.t("errors.cooldown_title", locale=language), + i18n.t("errors.cooldown", time=error.retry_after, locale=language), + footer=footer, + ) + return await interaction.response.send_message( + embed=embed, file=file, ephemeral=True + ) + + if isinstance(error, app_commands.NoPrivateMessage): + try: + embed, file = error_embed( + i18n.t("errors.no_pm_title", locale=language), + i18n.t("errors.no_pm", locale=language), + footer=footer, + ) + return await interaction.response.send_message( + embed=embed, file=file, ephemeral=True + ) + except discord.Forbidden: + pass + return + + if isinstance(error, app_commands.CheckFailure): + embed, file = error_embed( + i18n.t("errors.check_failure_title", locale=language), + i18n.t("errors.check_failure", locale=language), + footer=footer, + ) + return await interaction.response.send_message( + embed=embed, file=file, ephemeral=True + ) + + logger = logging.getLogger("discord") + logger.error(error) diff --git a/cogs/info/__init__.py b/cogs/info/__init__.py new file mode 100644 index 00000000..b6b14311 --- /dev/null +++ b/cogs/info/__init__.py @@ -0,0 +1,9 @@ +from ._cog import Info +from .info import InfoGroup +from .raw_message import RawMessage + + +async def setup(bot): + await bot.add_cog(Info(bot)) + await bot.add_cog(InfoGroup(bot)) + await bot.add_cog(RawMessage(bot)) diff --git a/cogs/info/_cog.py b/cogs/info/_cog.py new file mode 100644 index 00000000..263fd361 --- /dev/null +++ b/cogs/info/_cog.py @@ -0,0 +1,11 @@ +from .ping import Ping +from .stats import Stats +from .announcements import Announcements + +subclasses = Ping, Stats, Announcements + + +class Info(*subclasses): + """ + Get some infos like the ping or some stats + """ diff --git a/cogs/info/announcements.py b/cogs/info/announcements.py new file mode 100644 index 00000000..c49c4777 --- /dev/null +++ b/cogs/info/announcements.py @@ -0,0 +1,171 @@ +import i18n +import discord +from TakoBot import TakoBot +from datetime import datetime +from discord import app_commands +from discord.ext import commands +from utils import get_color, get_language, thumbnail + + +async def announcement_embed( + bot: TakoBot, + guild_id: int, + title: str, + description: str, + type: str, + timestamp: datetime, + id: str, +): + thumbnail_path = await thumbnail( + guild_id, "megaphone" if type == "general" else "tag", bot + ) + file = discord.File(thumbnail_path, filename="thumbnail.png") + + embed = discord.Embed( + title=title, + description=description, + color=await get_color(bot, guild_id), + timestamp=timestamp, + ) + embed.set_footer(text=id) + embed.set_author(name=type.capitalize()) + embed.set_thumbnail(url="attachment://thumbnail.png") + return embed, file + + +class Announcements(commands.Cog): + def __init__(self, bot): + self.bot: TakoBot = bot + + class AnnouncementPaginator(discord.ui.View): + def __init__(self, bot: TakoBot, index: int): + super().__init__(timeout=None) + self.bot = bot + self.index = index + + @discord.ui.button(emoji="◀", style=discord.ButtonStyle.blurple, row=1) + async def prev( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + if self.index == 0: + return await interaction.response.send_message( + i18n.t( + "info.no_more_announcements", + locale=get_language(self.bot, interaction.guild.id), + ), + ephemeral=True, + ) + self.index -= 1 + announcements = await self.bot.db_pool.fetch("SELECT * FROM announcements") + announcement = announcements[self.index] + embed, file = await announcement_embed( + self.bot, + interaction.guild.id, + announcement["title"] + " (Latest)" + if self.index == len(announcements) - 1 + else announcement["title"], + announcement["description"], + announcement["type"], + announcement["timestamp"], + announcement["id"], + ) + await interaction.response.edit_message( + embed=embed, attachments=[file], view=self + ) + + @discord.ui.button(emoji="▶", style=discord.ButtonStyle.blurple, row=1) + async def next( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + announcements = await self.bot.db_pool.fetch("SELECT * FROM announcements") + if self.index == len(announcements) - 1: + return await interaction.response.send_message( + i18n.t( + "info.no_more_announcements", + locale=get_language(self.bot, interaction.guild.id), + ), + ephemeral=True, + ) + self.index += 1 + announcement = announcements[self.index] + embed, file = await announcement_embed( + self.bot, + interaction.guild.id, + announcement["title"] + " (Latest)" + if self.index == len(announcements) - 1 + else announcement["title"], + announcement["description"], + announcement["type"], + announcement["timestamp"], + announcement["id"], + ) + await interaction.response.edit_message( + embed=embed, attachments=[file], view=self + ) + + @discord.ui.button(label="Latest", style=discord.ButtonStyle.green) + async def latest( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + announcements = await self.bot.db_pool.fetch("SELECT * FROM announcements") + self.index = len(announcements) - 1 + announcement = announcements[self.index] + embed, file = await announcement_embed( + self.bot, + interaction.guild.id, + announcement["title"] + " (Latest)", + announcement["description"], + announcement["type"], + announcement["timestamp"], + announcement["id"], + ) + await interaction.response.edit_message( + embed=embed, attachments=[file], view=self + ) + + @discord.ui.button(label="Remove Controls", style=discord.ButtonStyle.red) + async def remove_controls( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + announcements = await self.bot.db_pool.fetch("SELECT * FROM announcements") + announcement = announcements[self.index] + embed, file = await announcement_embed( + self.bot, + interaction.guild.id, + announcement["title"], + announcement["description"], + announcement["type"], + announcement["timestamp"], + announcement["id"], + ) + await interaction.response.edit_message( + embed=embed, attachments=[file], view=None + ) + + @app_commands.command( + description="View announcements from the developers of the bot" + ) + async def announcements(self, interaction: discord.Interaction): + announcements = await self.bot.db_pool.fetch("SELECT * FROM announcements;") + if not announcements: + return await interaction.response.send_message( + i18n.t( + "info.no_announcements", + locale=get_language(self.bot, interaction.guild.id), + ), + ephemeral=True, + ) + index = len(announcements) - 1 + announcement = announcements[index] + embed, file = await announcement_embed( + self.bot, + interaction.guild.id, + announcement["title"] + " (Latest)", + announcement["description"], + announcement["type"], + announcement["timestamp"], + announcement["id"], + ) + await interaction.response.send_message( + embed=embed, file=file, view=self.AnnouncementPaginator(self.bot, index) + ) diff --git a/cogs/info/info.py b/cogs/info/info.py new file mode 100644 index 00000000..d729b3aa --- /dev/null +++ b/cogs/info/info.py @@ -0,0 +1,223 @@ +import i18n +import discord +from typing import List +from TakoBot import TakoBot +from discord import app_commands +from discord.ext import commands +from utils import get_color, get_language + + +def handle_flags(flags: discord.UserFlags, language: str): + flags_array = [] + flag_dict = { + "staff": i18n.t("info.staff", locale=language), + "partner": i18n.t("info.partner", locale=language), + "bug_hunter": i18n.t("info.bug_hunter", locale=language), + "hypesquad": i18n.t("info.hypesquad", locale=language), + "hypesquad_bravery": i18n.t("info.hypesquad_bravery", locale=language), + "hypesquad_brilliance": i18n.t("info.hypesquad_brilliance", locale=language), + "hypesquad_balance": i18n.t("info.hypesquad_balance", locale=language), + "early_supporter": i18n.t("info.early_supporter", locale=language), + "team_user": i18n.t("info.team_user", locale=language), + "system": i18n.t("info.system", locale=language), + "bug_hunter_level_2": i18n.t("info.bug_hunter_level_2", locale=language), + "verified_bot": i18n.t("info.verified_bot", locale=language), + "verified_bot_developer": i18n.t( + "info.verified_bot_developer", locale=language + ), + "early_verified_bot_developer": i18n.t( + "info.early_verified_bot_developer", locale=language + ), + "discord_certified_moderator": i18n.t( + "info.discord_certified_moderator", locale=language + ), + "bot_http_interactions": i18n.t("info.bot_http_interactions", locale=language), + "spammer": i18n.t("info.spammer", locale=language), + } + for flag in flags: + flag_for_dict = str(flag).replace("UserFlags.", "") + if flags.index(flag) == len(flags) - 2: + flags_array.append(flag_dict[flag_for_dict] + " & ") + continue + if flags.index(flag) == len(flags) - 1: + flags_array.append(flag_dict[flag_for_dict]) + continue + flags_array.append(flag_dict[flag_for_dict] + ", ") + return "".join(flags_array) + + +def handle_roles(roles: List[discord.Role]): + final_roles_list = [] + roles_list = list(reversed(roles[1:])) + for role in roles_list: + if roles_list.index(role) == len(roles_list) - 2: + final_roles_list.append(role.mention + " & ") + continue + if roles_list.index(role) == len(roles_list) - 1: + final_roles_list.append(role.mention) + continue + final_roles_list.append(role.mention + ", ") + return "".join(final_roles_list) + + +def handle_badge_users(bot: TakoBot, users: List[int]): + final_users_list = [] + for user in users: + user = bot.get_user(user) + if users.index(user.id) == len(users) - 2: + final_users_list.append(user.mention + " & ") + continue + if users.index(user.id) == len(users) - 1: + final_users_list.append(user.mention) + continue + final_users_list.append(user.mention + ", ") + return ( + f"{''.join(final_users_list[:50])}{'...' if len(final_users_list) > 50 else ''}" + ) + + +class InfoGroup(commands.GroupCog, group_name="info"): + def __init__(self, bot: TakoBot): + self.bot = bot + + @app_commands.command(description="Get information about a user or yourself") + @app_commands.describe(user="The user to get information about") + async def user( + self, + interaction: discord.Interaction, + user: discord.User | discord.Member = None, + ): + if user == None: + user = interaction.user + language = get_language(self.bot, interaction.guild.id) + user_flags = user.public_flags.all() + general = [ + "", + i18n.t( + "info.username_discrim", + name=str(user), + locale=language, + ), + f"**ID**: {user.id}", + f"**Flags**: {handle_flags(user_flags, language) if len(user_flags) else i18n.t('info.no_flags', locale=language)}", + i18n.t( + "info.created_on", + locale=language, + date=user.created_at.strftime( + i18n.t("info.date_format", locale=language) + ), + ), + i18n.t( + "info.avatar", + avatar=f"[PNG]({user.avatar.replace(size=512, format='png').url}), [JPG]({user.avatar.replace(size=512, format='jpg').url}), [{'GIF' if user.avatar.is_animated() else 'WEBP'}]({user.avatar.replace(size=512, format='gif', static_format='webp').url})", + locale=language, + ), + ] + if hasattr(user, "guild"): + server = [ + "", + i18n.t( + "info.joined_on", + date=user.joined_at.strftime( + i18n.t("info.date_format", locale=language) + ), + locale=language, + ), + i18n.t( + "info.top_role", + role=user.top_role.mention + if len(user.roles) > 1 + else i18n.t("info.no_roles", locale=language), + locale=language, + ), + i18n.t( + "info.hoist_role", + role=discord.utils.get(reversed(user.roles), hoist=True).mention + if discord.utils.get(reversed(user.roles), hoist=True) + else i18n.t("info.no_roles", locale=language), + locale=language, + ), + i18n.t( + "info.roles", + roles=handle_roles(user.roles) + if len(user.roles) > 1 + else i18n.t("info.no_roles", locale=language), + locale=language, + ), + ] + embed = discord.Embed( + title=i18n.t("info.title", name=user.display_name, locale=language), + description=i18n.t("info.infos_about", name=user.mention, locale=language), + color=user.color if hasattr(user, "guild") else user.accent_color, + ) + embed.add_field( + name=i18n.t("info.general", locale=language), + value="\n**❯** ".join(general), + inline=False, + ) + embed.add_field( + name=i18n.t("info.server", locale=language), + value="\n**❯** ".join(server) + if hasattr(user, "guild") + else i18n.t("info.not_in_server", locale=language), + inline=False, + ) + embed.set_thumbnail(url=user.display_avatar) + badges_list = [] + badges = await self.bot.db_pool.fetch("SELECT emoji, users FROM badges") + for badge in badges: + if badge["users"] is None: + continue + for id in badge["users"]: + if user.id == id: + badges_list.append(badge["emoji"]) + if len(badges_list): + embed.add_field( + name="Badges", + value=i18n.t("info.more_badge_info", locale=language) + + " ".join(badges_list), + ) + await interaction.response.send_message(embed=embed) + + @app_commands.command(description="Get some infos about a badge") + async def badge(self, interaction: discord.Interaction, badge: str): + badge = await self.bot.db_pool.fetchrow( + "SELECT * FROM badges WHERE name = $1", badge + ) + if badge is None: + return await interaction.response.send_message( + i18n.t( + "info.badge_not_found", + locale=get_language(self.bot, interaction.guild.id), + ), + ephemeral=True, + ) + embed = discord.Embed( + title=badge["emoji"] + " " + badge["name"], + description=badge["description"], + color=(await get_color(self.bot, interaction.guild.id)), + ) + if badge["users"]: + embed.add_field( + name=i18n.t( + "info.users_with_badge", + amount=len(badge["users"]), + locale=get_language(self.bot, interaction.guild.id), + ), + value=handle_badge_users(self.bot, badge["users"]), + ) + await interaction.response.send_message(embed=embed) + + @badge.autocomplete("badge") + async def autocomplete_callback( + self, interaction: discord.Interaction, current: str + ): + current = current.lower() + badges = await self.bot.db_pool.fetch("SELECT * FROM badges") + return [ + app_commands.Choice( + name=f"{badge['name']} ({badge['emoji']})", value=badge["name"] + ) + for badge in badges + if current in badge["name"].lower() or current in badge["emoji"].lower() + ] diff --git a/cogs/info/ping.py b/cogs/info/ping.py new file mode 100644 index 00000000..ee1faf4e --- /dev/null +++ b/cogs/info/ping.py @@ -0,0 +1,36 @@ +import discord +from discord import app_commands +from discord.ext import commands + + +def get_ping_color(ping: int): + if ping < 200: + return discord.Color.green() + if ping < 500: + return discord.Color.orange() + return discord.Color.red() + + +def get_ping_color_name(ping: int): + if ping < 200: + return "green" + if ping < 500: + return "orange" + return "red" + + +class Ping(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + @app_commands.command(description="Get my latency") + async def ping(self, interaction: discord.Interaction): + ping = round(self.bot.latency * 1000) + color = get_ping_color(ping) + thumbnail = discord.File( + f"assets/ping/ping_{get_ping_color_name(ping)}.png", "thumbnail.png" + ) + embed = discord.Embed(title="🏓 Pong!", color=color) + embed.add_field(name="Latency", value=f"{ping} ms.") + embed.set_thumbnail(url="attachment://thumbnail.png") + await interaction.response.send_message(embed=embed, file=thumbnail) diff --git a/cogs/info/raw_message.py b/cogs/info/raw_message.py new file mode 100644 index 00000000..97c2dcfa --- /dev/null +++ b/cogs/info/raw_message.py @@ -0,0 +1,47 @@ +import i18n +import discord +from utils import get_language +from discord import app_commands +from discord.ext import commands + + +class RawMessage(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.ctx_menu = app_commands.ContextMenu( + name="Get Raw Message", + callback=self.raw_message, + ) + self.bot.tree.add_command(self.ctx_menu) + + async def cog_unload(self): + self.bot.tree.remove_command(self.ctx_menu.name, type=self.ctx_menu.type) + + async def raw_message( + self, interaction: discord.Interaction, message: discord.Message + ): + language = get_language(self.bot, interaction.guild.id) + if not message.content and not message.embeds: + return await interaction.response.send_message( + i18n.t("info.no_content", locale=language), ephemeral=True + ) + if message.embeds: + embed = i18n.t("info.embed", locale=language) + embeds = [""] + embed_count = 0 + for embed in message.embeds: + embed_count += 1 + embeds.append( + f"{i18n.t('info.embed', count=embed_count, locale=language)}\n```\n{embed.description}```" + ) + return await interaction.response.send_message( + f"{i18n.t('info.message')} ```" + + "\n" + + message.content + + "```" + + "\n".join(embeds), + ephemeral=True, + ) + await interaction.response.send_message( + "```" + "\n" + message.content + "```", ephemeral=True + ) diff --git a/cogs/info/stats.py b/cogs/info/stats.py new file mode 100644 index 00000000..82cd122c --- /dev/null +++ b/cogs/info/stats.py @@ -0,0 +1,77 @@ +import sys +import time +import psutil +import config +import discord +import datetime +from discord import app_commands +from discord.ext import commands +from cpuinfo import get_cpu_info +from utils import format_bytes, get_color + + +class Stats(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + @app_commands.command(description="Get some stats about me") + async def stats(self, interaction): + await interaction.response.defer() + # latest_version = requests.get("https://raw.githubusercontent.com/kayano-bot/kayano-rewrite/master/cz.json").json()["commitizen"]["version"] + operating_systems = { + "aix": "AIX", + "darwin": "MacOS", + "linux": "Linux", + "win32": "Windows", + } + general = [ + f"**<:server:950769912958320680> Server count**: {len(self.bot.guilds)}", + f"**<:users:950777719417876502> User count**: {len(self.bot.users)}", + f"**<:channel:951127622820171846> Channel count**: {len(list(self.bot.get_all_channels()))}", + f"**<:slash_command:951124330459328553> Commands**: {len(self.bot.tree.get_commands())}", + f"**<:server:950769912958320680> Current Shard**: {self.bot.shard_id if self.bot.shard_id else 'No shard'}", + f"**🏷️ Version**: {self.bot.version}", + f"**<:discordpy:968192318836465714> Discord.py Version**: {discord.__version__}", + f"**<:python:968192022232055808> Python Version**: {sys.version.split(' ', 1)[0]}", + f"**🏓 Ping**: {round(self.bot.latency * 1000)} ms.", + ] + cpu_info = get_cpu_info() + system = [ + f"**🖥️ Platform**: {operating_systems[sys.platform]}", + f"**🕐 Uptime**: {str(datetime.timedelta(seconds=time.time() - psutil.boot_time())).split('.')[0]}", + f"**⚡ CPU**:", + f"\u3000*Model*: {cpu_info['brand_raw']}", + f"\u3000*Cores*: {cpu_info['count']}", + f"\u3000*Speed*: {cpu_info['hz_advertised_friendly'][0]} GHz", + f"\u3000*Usage (Systemwide)*: {psutil.cpu_percent()}%", + f"**🗄️ Memory**:", + f"\u3000*Total*: {format_bytes(psutil.virtual_memory().total)}", + f"\u3000*Available*: {format_bytes(psutil.virtual_memory().available)}", + ] + social_media = [] + if hasattr(config, "TWITTER_LINK"): + social_media.append( + f"**{config.EMOJI_TWITTER if hasattr(config, 'EMOJI_TWITTER') else ''} Twitter**: [@DiscordTako]({config.TWITTER_LINK})" + ) + if hasattr(config, "YOUTUBE_LINK"): + social_media.append( + f"**{config.EMOJI_YT if hasattr(config, 'EMOJI_YT') else ''} Youtube**: [Tako]({config.YOUTUBE_LINK})" + ) + + embed = discord.Embed( + title="📊 Stats", + description="Here are some stats about me", + color=await get_color(self.bot, interaction.guild.id), + ) + embed.set_author( + name=self.bot.user.name + "#" + self.bot.user.discriminator, + icon_url=self.bot.user.avatar.url, + ) + embed.add_field(name="General", value="\n".join(general)) + embed.add_field(name="System", value="\n".join(system)) + if hasattr(config, "YOUTUBE_LINK") or hasattr(config, "TWITTER_LINK"): + embed.add_field( + name="Social Media", value="\n".join(social_media), inline=False + ) + + await interaction.followup.send(embed=embed) diff --git a/cogs/misc/__init__.py b/cogs/misc/__init__.py new file mode 100644 index 00000000..8631a0c0 --- /dev/null +++ b/cogs/misc/__init__.py @@ -0,0 +1,27 @@ +from .tag import Tag +from .embed import Embed +from .emoji import Emoji +from .image import Image +from .media import Media +from .reddit import Reddit +from .youtube import Youtube +from .show_tag import ShowTag +from .translate import Translate +from .affirmations import Affirmations +from .autotranslate import AutoTranslate +from .reaction_translate import ReactionTranslate + + +async def setup(bot): + await bot.add_cog(Tag(bot)) + await bot.add_cog(Embed(bot)) + await bot.add_cog(Emoji(bot)) + await bot.add_cog(Image(bot)) + await bot.add_cog(Media(bot)) + await bot.add_cog(Reddit(bot)) + await bot.add_cog(Youtube(bot)) + await bot.add_cog(ShowTag(bot)) + await bot.add_cog(Translate(bot)) + await bot.add_cog(Affirmations(bot)) + await bot.add_cog(ReactionTranslate(bot)) + await bot.add_cog(AutoTranslate(bot)) diff --git a/cogs/misc/affirmations.py b/cogs/misc/affirmations.py new file mode 100644 index 00000000..bd3d8803 --- /dev/null +++ b/cogs/misc/affirmations.py @@ -0,0 +1,19 @@ +import discord +import aiohttp +from discord import app_commands +from discord.ext import commands +from TakoBot import AffirmationButtons + + +class Affirmations(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(description="Feel bad? Get some affirmations!") + async def affirmation(self, interaction: discord.Interaction): + async with aiohttp.ClientSession() as session: + async with session.get("https://affirmations.dev/") as r: + data = await r.json() + await interaction.response.send_message( + data["affirmation"], view=AffirmationButtons(), ephemeral=True + ) diff --git a/cogs/misc/autotranslate.py b/cogs/misc/autotranslate.py new file mode 100644 index 00000000..a8eb6768 --- /dev/null +++ b/cogs/misc/autotranslate.py @@ -0,0 +1,56 @@ +import i18n +import aiohttp +import discord +from discord import app_commands +from discord.ext import commands +from utils import get_language, translate + + +class AutoTranslate(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(description="Disable or enable auto translate") + @app_commands.describe(value="Wheter to enable or disable auto translate") + @app_commands.checks.has_permissions(manage_guild=True) + async def auto_translate(self, interaction: discord.Interaction, value: bool): + await self.bot.db_pool.execute( + "INSERT INTO guilds (guild_id, auto_translate) VALUES ($1, $2) ON CONFLICT(guild_id) DO UPDATE SET auto_translate = $2", + interaction.guild.id, + value, + ) + return await interaction.response.send_message( + i18n.t( + f"misc.auto_translate_{'activated' if value else 'deactivated'}", + locale=get_language(self.bot, interaction.guild.id), + ) + ) + + @commands.Cog.listener() + async def on_message(self, message: discord.Message): + state = await self.bot.db_pool.fetchval( + "SELECT auto_translate FROM guilds WHERE guild_id = $1", message.guild.id + ) + if not message.content or not state or message.author.id == self.bot.user.id: + return + + headers = { + "accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded", + } + async with aiohttp.ClientSession() as session: + async with session.post( + f"https://translate.argosopentech.com/detect", + data=f"q={message.content.replace('&', '%26')}", + headers=headers, + ) as r: + data = await r.json() + data = data[0] + guild_language = get_language(self.bot, message.guild.id) + if data["language"] != guild_language or data["confidence"] < 5: + try: + await message.reply( + f"> {await translate(message.content, guild_language)}\n\n` {data['language']} ➜ {guild_language} `" + ) + except discord.Forbidden: + return diff --git a/cogs/misc/embed.py b/cogs/misc/embed.py new file mode 100644 index 00000000..0b36ac31 --- /dev/null +++ b/cogs/misc/embed.py @@ -0,0 +1,74 @@ +import re +import discord +from utils import get_color +from datetime import datetime +from discord import app_commands +from discord.ext import commands + + +class EmbedModal(discord.ui.Modal, title="Embed Creator"): + def __init__(self, embed: discord.Embed): + super().__init__() + self.embed = embed + + embed_title = discord.ui.TextInput(label="Title", required=False) + description = discord.ui.TextInput( + label="Description", + max_length=1024, + placeholder="Enter a description", + style=discord.TextStyle.long, + ) + thumbnail = discord.ui.TextInput(label="Thumbnail", required=False) + image = discord.ui.TextInput(label="Image", required=False) + footer = discord.ui.TextInput(label="Footer", required=False) + + async def on_submit(self, interaction: discord.Interaction): + embed = self.embed + embed.title = self.embed_title.value + embed.description = self.description.value + embed.set_thumbnail(url=self.thumbnail.value) + embed.set_image(url=self.image.value) + embed.set_footer(text=self.footer.value) + await interaction.channel.send(embed=self.embed) + await interaction.response.send_message( + content="Succesfully send your embed!", ephemeral=True + ) + + +class Embed(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(description="Create an embed") + async def embed( + self, + interaction: discord.Interaction, + color: str = None, + timestamp: bool = False, + ): + if color == None: + color = await get_color(self.bot, interaction.guild.id, False) + if color.startswith("#"): + color = color.replace("#", "0x") + if not color.startswith("0x"): + color = f"0x{color}" + match = re.search(r"^0x([A-Fa-f0-9]{6})$", color) + if not match: + return await interaction.response.send_message( + f"Your color (`{color}`) is not a valid (*6* character) hex color.", + ephemeral=True, + ) + if timestamp: + timestamp = datetime.now() + else: + timestamp = None + embed = discord.Embed( + color=color if isinstance(color, int) else int(color, 16), + timestamp=timestamp, + ) + embed.set_author( + name=interaction.user.name + "#" + interaction.user.discriminator, + icon_url=interaction.user.display_avatar, + url=f"https://discord.com/users/{interaction.user.id}", + ) + await interaction.response.send_modal(EmbedModal(embed)) diff --git a/cogs/misc/emoji.py b/cogs/misc/emoji.py new file mode 100644 index 00000000..e331d412 --- /dev/null +++ b/cogs/misc/emoji.py @@ -0,0 +1,132 @@ +import io +import re +import i18n +import aiohttp +import discord +from TakoBot import TakoBot +from utils import get_language +from discord.ext import commands +from discord import HTTPException, NotFound, app_commands + + +class Emoji(commands.GroupCog, group_name="emoji"): + def __init__(self, bot: TakoBot): + self.bot = bot + + @app_commands.command( + description="Add an an emoji with an ID from emoji.gg or url pointing to an image" + ) + @app_commands.checks.has_permissions(manage_emojis=True) + async def add(self, interaction: discord.Interaction, emoji: str, name: str = None): + if name is None: + name = emoji + if ( + emoji.startswith("http://") is False + and emoji.startswith("https://") is False + ): + emoji = f"https://cdn3.emoji.gg/emojis/{emoji}.png" + match = re.match( + "^https?:\/\/cdn3.emoji.gg\/|^https?:\/\/i[.]imgur[.]com\/|^https?:\/\/raw[.]githubusercontent[.]com\/|http[s]?:\/\/cdn[.]betterttv[.]net\/emote|^https?:\/\/cdn[.]discordapp[.]com\/emojis\/", + emoji, + ) + if not match: + return await interaction.response.send_message( + i18n.t( + "misc.not_emoji", + locale=get_language(self.bot, interaction.guild.id), + ), + ephemeral=True, + ) + emoji_dict = {"title": name, "image": emoji} + title = emoji_dict["title"].replace(" ", "").replace("-", "_") + title = ( + f"{title[:27 if len(title) < 2 else 32]}{'_tako' if len(title) < 2 else ''}" + ) + async with aiohttp.ClientSession() as cs: + async with cs.get(emoji_dict["image"]) as r: + res = await r.read() + try: + added_emoji = await interaction.guild.create_custom_emoji( + name=title, image=res + ) + except ValueError: + return await interaction.response.send_message( + i18n.t( + "misc.not_emoji", + locale=get_language(self.bot, interaction.guild.id), + ), + ephemeral=True, + ) + except HTTPException: + return await interaction.response.send_message( + i18n.t( + "misc.too_big", + locale=get_language(self.bot, interaction.guild.id), + ), + ephemeral=True, + ) + embed = discord.Embed( + title="Emoji added", + description=i18n.t( + "misc.added_emoji", + emoji=added_emoji.name, + locale=get_language(self.bot, interaction.guild.id), + ).replace("\n", "\n"), + color=discord.Color.green(), + ) + embed.set_thumbnail(url=emoji_dict["image"]) + await interaction.response.send_message(embed=embed, ephemeral=True) + + @app_commands.command() + @app_commands.checks.has_permissions(manage_emojis=True) + async def remove(self, interaction: discord.Interaction, emoji: str): + try: + emoji = int(emoji) + except: + return await interaction.response.send_message( + i18n.t( + "misc.not_id", locale=get_language(self.bot, interaction.guild.id) + ), + ephemeral=True, + ) + try: + fetched_emoji = await interaction.guild.fetch_emoji(emoji) + except NotFound: + return await interaction.response.send_message( + i18n.t( + "misc.not_id", locale=get_language(self.bot, interaction.guild.id) + ), + ephemeral=True, + ) + await fetched_emoji.delete( + reason=i18n.t( + "misc.deleted_emoji_log", + user=str(interaction.user), + locale=get_language(self.bot, interaction.guild.id), + ) + ) + embed = discord.Embed( + title="Emoji removed", + description=i18n.t( + "misc.deleted_emoji", + emoji=fetched_emoji.name, + locale=get_language(self.bot, interaction.guild.id), + ), + color=discord.Color.red(), + ) + image_bytes = await fetched_emoji.read() + image = discord.File(io.BytesIO(image_bytes), "thumbnail.png") + embed.set_thumbnail(url="attachment://thumbnail.png") + await interaction.response.send_message(embed=embed, file=image, ephemeral=True) + + @remove.autocomplete(name="emoji") + async def autocomplete_callback( + self, interaction: discord.Interaction, current: str + ): + current = current.lower() + emojis = await interaction.guild.fetch_emojis() + return [ + app_commands.Choice(name=f"{emoji.name} ({emoji.id})", value=str(emoji.id)) + for emoji in emojis + if current in emoji.name.lower() or current in str(emoji.id) + ] diff --git a/cogs/misc/flags.py b/cogs/misc/flags.py new file mode 100644 index 00000000..28224c4e --- /dev/null +++ b/cogs/misc/flags.py @@ -0,0 +1,250 @@ +language_dict = { + "🇦🇫": "fa", + "🇦🇱": "sq", + "🇦🇲": "hy", + "🇦🇴": "pt", + "🇦🇷": "es", + "🇦🇺": "en", + "🇦🇼": "nl", + "🇦🇽": "sv", + "🇦🇿": "az", + "🇧🇦": "bs", + "🇧🇧": "en", + "🇧🇩": "bn", + "🇧🇪": "fr", + "🇧🇫": "fr", + "🇧🇬": "bg", + "🇧🇭": "ar", + "🇧🇮": "fr", + "🇧🇯": "fr", + "🇧🇱": "fr", + "🇧🇲": "en", + "🇧🇳": "ms", + "🇧🇴": "es", + "🇧🇷": "pt", + "🇧🇸": "en", + "🇧🇹": "dz", + "🇧🇼": "en", + "🇧🇾": "be", + "🇧🇿": "en", + "🇨🇦": "en", + "🇨🇨": "ms", + "🇨🇩": "fr", + "🇨🇫": "fr", + "🇨🇬": "fr", + "🇨🇭": "de", + "🇨🇮": "fr", + "🇨🇰": "en", + "🇨🇱": "es", + "🇨🇲": "fr", + "🇨🇳": "zh-CN", + "🇨🇴": "es", + "🇨🇵": "fr", + "🇨🇷": "es", + "🇨🇺": "es", + "🇨🇻": "pt", + "🇨🇼": "nl", + "🇨🇽": "en", + "🇨🇾": "el", + "🇨🇿": "cs", + "🇩🇪": "de", + "🇩🇬": "en", + "🇩🇯": "fr", + "🇩🇰": "da", + "🇩🇲": "en", + "🇩🇴": "es", + "🇩🇿": "ar", + "🇪🇦": "es", + "🇪🇨": "es", + "🇪🇪": "et", + "🇪🇬": "ar", + "🇪🇭": "ar", + "🇪🇷": "ti", + "🇪🇸": "es", + "🇪🇹": "am", + "🇪🇺": "en", + "🇫🇮": "fi", + "🇫🇯": "en", + "🇫🇰": "en", + "🇫🇲": "en", + "🇫🇴": "fo", + "🇫🇷": "fr", + "🇬🇦": "fr", + "🇬🇧": "en", + "🇬🇩": "en", + "🇬🇪": "ka", + "🇬🇫": "fr", + "🇬🇬": "en", + "🇬🇭": "en", + "🇬🇮": "en", + "🇬🇱": "kl", + "🇬🇲": "en", + "🇬🇳": "fr", + "🇬🇵": "fr", + "🇬🇶": "es", + "🇬🇷": "el", + "🇬🇸": "en", + "🇬🇹": "es", + "🇬🇺": "en", + "🇬🇼": "pt", + "🇬🇾": "en", + "🇭🇰": "zh-TW", + "🇭🇲": "en", + "🇭🇳": "es", + "🇭🇷": "hr", + "🇭🇹": "fr", + "🇭🇺": "hu", + "🇮🇨": "es", + "🇮🇩": "id", + "🇮🇪": "ga", + "🇮🇱": "he", + "🇮🇲": "en", + "🇮🇳": "hi", + "🇮🇴": "en", + "🇮🇶": "ar", + "🇮🇷": "fa", + "🇮🇸": "is", + "🇮🇹": "it", + "🇯🇪": "en", + "🇯🇲": "en", + "🇯🇴": "ar", + "🇯🇵": "ja", + "🇰🇪": "sw", + "🇰🇬": "ky", + "🇰🇭": "km", + "🇰🇮": "en", + "🇰🇲": "ar", + "🇰🇳": "en", + "🇰🇵": "ko", + "🇰🇷": "ko", + "🇰🇼": "ar", + "🇰🇾": "en", + "🇰🇿": "kk", + "🇱🇦": "lo", + "🇱🇧": "ar", + "🇱🇨": "en", + "🇱🇮": "de", + "🇱🇰": "si", + "🇱🇷": "en", + "🇱🇸": "en", + "🇱🇹": "lt", + "🇱🇺": "fr", + "🇱🇻": "lv", + "🇱🇾": "ar", + "🇲🇦": "ar", + "🇲🇨": "fr", + "🇲🇩": "ro", + "🇲🇪": "sr", + "🇲🇫": "fr", + "🇲🇬": "mg", + "🇲🇭": "mh", + "🇲🇰": "mk", + "🇲🇱": "fr", + "🇲🇲": "my", + "🇲🇳": "mn", + "🇲🇴": "zh-TW", + "🇲🇵": "en", + "🇲🇶": "fr", + "🇲🇷": "ar", + "🇲🇸": "en", + "🇲🇹": "mt", + "🇲🇺": "en", + "🇲🇻": "dv", + "🇲🇼": "ny", + "🇲🇽": "es", + "🇲🇾": "ms", + "🇲🇿": "pt", + "🇳🇦": "en", + "🇳🇨": "fr", + "🇳🇪": "fr", + "🇳🇫": "en", + "🇳🇬": "en", + "🇳🇮": "es", + "🇳🇱": "nl", + "🇳🇴": "nb", + "🇳🇵": "ne", + "🇳🇷": "na", + "🇳🇿": "mi", + "🇴🇲": "ar", + "🇵🇦": "es", + "🇵🇪": "es", + "🇵🇫": "fr", + "🇵🇬": "en", + "🇵🇭": "tl", + "🇵🇰": "ur", + "🇵🇱": "pl", + "🇵🇲": "fr", + "🇵🇳": "en", + "🇵🇷": "es", + "🇵🇸": "ar", + "🇵🇹": "pt", + "🇵🇾": "es", + "🇶🇦": "ar", + "🇷🇪": "fr", + "🇷🇴": "ro", + "🇷🇸": "sr", + "🇷🇺": "ru", + "🇷🇼": "rw", + "🇸🇦": "ar", + "🇸🇧": "en", + "🇸🇨": "fr", + "🇸🇩": "ar", + "🇸🇪": "sv", + "🇸🇬": "en", + "🇸🇭": "en", + "🇸🇮": "sl", + "🇸🇯": "no", + "🇸🇰": "sk", + "🇸🇱": "en", + "🇸🇲": "it", + "🇸🇳": "fr", + "🇸🇴": "so", + "🇸🇷": "nl", + "🇸🇸": "en", + "🇸🇹": "pt", + "🇸🇻": "es", + "🇸🇽": "nl", + "🇸🇾": "ar", + "🇸🇿": "en", + "🇹🇦": "en", + "🇹🇨": "en", + "🇹🇩": "fr", + "🇹🇫": "fr", + "🇹🇬": "fr", + "🇹🇭": "th", + "🇹🇯": "tg", + "🇹🇰": "tk", + "🇹🇲": "tk", + "🇹🇳": "ar", + "🇹🇴": "to", + "🇹🇷": "tr", + "🇹🇹": "en", + "🇹🇻": "en", + "🇹🇼": "zh-TW", + "🇹🇿": "sw", + "🇺🇦": "uk", + "🇺🇬": "en", + "🇺🇲": "en", + "🇺🇸": "en", + "🇺🇾": "es", + "🇺🇿": "uz", + "🇻🇦": "la", + "🇻🇨": "en", + "🇻🇪": "es", + "🇻🇬": "en", + "🇻🇮": "en", + "🇻🇳": "vi", + "🇻🇺": "bi", + "🇼🇫": "fr", + "🇼🇸": "sm", + "🇽🇰": "sq", + "🇾🇪": "ar", + "🇾🇹": "fr", + "🇿🇦": "af", + "🇿🇲": "en", + "🇿🇼": "sn", + "🏴󠁧󠁢󠁥󠁮󠁧󠁿": "en", + "🏴󠁧󠁢󠁳󠁣󠁴󠁿": "en", + "🏴󠁧󠁢󠁷󠁬󠁳󠁿": "en", + "🏳️": "fr", +} diff --git a/cogs/misc/image.py b/cogs/misc/image.py new file mode 100644 index 00000000..66e1d2da --- /dev/null +++ b/cogs/misc/image.py @@ -0,0 +1,101 @@ +import i18n +import config +import aiohttp +import discord +from io import BytesIO +from random import randint +from discord import app_commands +from discord.ext import commands +from discord.app_commands import Choice +from utils import get_color, get_language, thumbnail + + +class Image(commands.GroupCog, group_name="image"): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(description="Put a flag behind an avatar") + @app_commands.describe( + type="The flag to use", user="The avatar to use (Default: Your Avatar)" + ) + @app_commands.choices( + type=[ + Choice(name="Rainbow (Default)", value="lgbtq"), + Choice(name="Agender", value="agender"), + Choice(name="Ally", value="ally"), + Choice(name="Aromantic", value="aromantic"), + Choice(name="Asexual", value="asexual"), + Choice(name="Bisexual", value="bi"), + Choice(name="Butch Lesbian", value="butch-lesbian"), + Choice(name="Demiromantic", value="demiromantic"), + Choice(name="Demisexual", value="demisexual"), + Choice(name="Genderfluid", value="genderfluid"), + Choice(name="Genderqueer", value="genderqueer"), + Choice(name="Intersex", value="intersex"), + Choice(name="Labrys Lesbian", value="labrys-lesbian"), + Choice(name="Lesbian", value="lesbian"), + Choice(name="Non-Binary", value="non-binary"), + Choice(name="Pansexual", value="pan"), + Choice(name="Polyamorous", value="polyamorous"), + Choice(name="Polysexual", value="polysexual"), + Choice(name="Progress", value="progress"), + Choice(name="Transgender", value="trans"), + ] + ) + async def lgbtq( + self, + interaction: discord.Interaction, + type: str = "lgbtq", + user: discord.User = None, + ): + if user is None: + user = interaction.user + + language = get_language(self.bot, interaction.guild.id) + thumb = await thumbnail(interaction.guild.id, "flag", self.bot) + thumb = discord.File(thumb, "thumbnail.png") + embed = discord.Embed( + color=await get_color(self.bot, interaction.guild.id), + title="LGBTQ+ Avatar", + description=i18n.t("misc.lgbtq_tip", locale=language) + + f"\n\n{i18n.t('misc.no_gif_support', locale=language)}" + if user.display_avatar.is_animated + else "", + ) + embed.set_thumbnail(url="attachment://thumbnail.png") + embed.set_image( + url=f"{config.IMGEN}/pride?type={type}&avatar={user.display_avatar.replace(size=512, format='png').url}" + ) + return await interaction.response.send_message(embed=embed, file=thumb) + + @app_commands.command(description="Put jail bars over an avatar (Black & White)") + @app_commands.describe(user="The avatar to use (Default: Your Avatar)") + async def jail(self, interaction: discord.Interaction, user: discord.User = None): + if user is None: + user = interaction.user + + language = get_language(self.bot, interaction.guild.id) + rand = randint(1, 100) + new_rand = 1 + if rand <= 70: + new_rand = 2 + if rand <= 50: + new_rand = 3 + if rand <= 20: + new_rand = 4 + if rand == 1: + new_rand = 5 + embed = discord.Embed( + color=await get_color(self.bot, interaction.guild.id), + title=f"{str(user)} is now in jail!", + description=i18n.t( + f"misc.jail_desc_{new_rand}", + user=user.display_name, + officer=interaction.user.display_name, + locale=language, + ), + ) + embed.set_image( + url=f"{config.IMGEN}/jail?avatar={user.display_avatar.replace(size=512, format='png').url}" + ) + return await interaction.response.send_message(embed=embed) diff --git a/cogs/misc/media.py b/cogs/misc/media.py new file mode 100644 index 00000000..e6f58ba0 --- /dev/null +++ b/cogs/misc/media.py @@ -0,0 +1,212 @@ +import os +import discord +import tmdbsimple as tmdb +from TakoBot import TakoBot +from discord import app_commands +from discord.ext import commands +from utils import get_color, thumbnail + + +async def button_logic( + results, + interaction: discord.Interaction, + index: int, + embed: discord.Embed, + user: discord.User, + bot: TakoBot, +): + if interaction.user.id != user.id: + return await interaction.response.send_message( + "You cannot interact with this message because it was not invoked by you.", + ephemeral=True, + ) + result_id = results[index]["id"] + view = ReturnButton(bot, results, embed, user, interaction.guild) + tmdb_logo = discord.File(f"{os.getcwd()}/assets/TMDb.png") + title = ( + results[index]["title"] if "title" in results[index] else results[index]["name"] + ) + types = {"movie": "Movie", "tv": "TV", "person": "Person"} + if results[index]["media_type"] == "movie": + more_info = tmdb.Movies(result_id).info() + if results[index]["media_type"] == "tv": + more_info = tmdb.TV(result_id).info() + if results[index]["media_type"] == "person": + more_info = tmdb.People(result_id).info() + if results[index]["media_type"] == "movie" or results[index]["media_type"] == "tv": + tagline = ( + f"**{more_info['tagline']}**" if more_info["tagline"] else "**No Tagline**" + ) + detailed_embed = discord.Embed( + title=f"{title} ({types[results[index]['media_type']]})", + description=tagline, + color=await get_color(bot, interaction.guild.id), + ) + else: + biography = more_info["biography"] + detailed_embed = discord.Embed( + title=f"{title} ({types[results[index]['media_type']]})", + description=biography + if len(biography) <= 1024 + else f"{biography[:1021]}...", + color=await get_color(bot, interaction.guild.id), + ) + url = f"https://image.tmdb.org/t/p/original{more_info['backdrop_path'] if 'backdrop_path' in more_info else more_info['profile_path']}" + detailed_embed.set_image(url=url) + detailed_embed.set_footer(text="Data by TMDb", icon_url="attachment://TMDb.png") + await interaction.response.edit_message( + embed=detailed_embed, attachments=[tmdb_logo], view=view + ) + + +def remove_items(results, view: discord.ui.View): + for item in view.children: + if len(results) < 2 and item.emoji.name == "2️⃣": + view.remove_item(item) + continue + if len(results) < 3 and item.emoji.name == "3️⃣": + view.remove_item(item) + continue + + +class ReturnButton(discord.ui.View): + def __init__(self, bot, results, embed: discord.Embed, user, guild: discord.Guild): + super().__init__() + self.bot = bot + self.results = results + self.embed = embed + self.user = user + self.guild = guild + + @discord.ui.button( + label="Back to search results", emoji="⬅️", style=discord.ButtonStyle.primary + ) + async def close(self, interaction: discord.Interaction, button: discord.Button): + if interaction.user.id != self.user.id: + return await interaction.response.send_message( + "You cannot interact with this message because it was not invoked by you.", + ephemeral=True, + ) + view = MediaButtons(self.results, self.embed, interaction.user, self.bot) + remove_items(self.results, view) + file = discord.File( + await thumbnail(self.guild.id, "search", self.bot), filename="thumbnail.png" + ) + tmdb_logo = discord.File(f"{os.getcwd()}/assets/TMDb.png") + await interaction.response.edit_message( + embed=self.embed, attachments=[file, tmdb_logo], view=view + ) + + +class MediaButtons(discord.ui.View): + def __init__(self, results, embed, user, bot): + super().__init__() + self.results = results + self.embed = embed + self.user = user + self.bot = bot + + @discord.ui.button(emoji="1️⃣", style=discord.ButtonStyle.grey) + async def one(self, interaction: discord.Interaction, button: discord.ui.Button): + await button_logic( + results=self.results, + interaction=interaction, + index=0, + embed=self.embed, + user=self.user, + bot=self.bot, + ) + + @discord.ui.button(emoji="2️⃣", style=discord.ButtonStyle.grey) + async def two(self, interaction: discord.Interaction, button: discord.ui.Button): + await button_logic( + results=self.results, + interaction=interaction, + index=1, + embed=self.embed, + user=self.user, + bot=self.bot, + ) + + @discord.ui.button(emoji="3️⃣", style=discord.ButtonStyle.grey) + async def three(self, interaction: discord.Interaction, button: discord.ui.Button): + await button_logic( + results=self.results, + interaction=interaction, + index=2, + embed=self.embed, + user=self.user, + bot=self.bot, + ) + + +class Media(commands.GroupCog): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(description="Search for a movie, TV show or person") + @app_commands.describe(query="The movie, TV show or person to search for") + async def search(self, interaction: discord.Interaction, query: str): + count = 0 + search = tmdb.Search() + search.multi(query=query) + search.results = search.results[:3] + types = {"movie": "Movie", "tv": "TV", "person": "Person"} + thumbnail_path = await thumbnail(interaction.guild.id, "search", self.bot) + file = discord.File(thumbnail_path, filename="thumbnail.png") + tmdb_logo = discord.File(f"{os.getcwd()}/assets/TMDb.png") + embed = discord.Embed( + title="Media search", + description=f"Top {len(search.results[:3])} Search results for *{query}*" + if len(search.results) + else "Nothing matched your search", + color=await get_color(self.bot, interaction.guild.id), + ) + for s in search.results: + if s["media_type"] == "movie": + more_info = tmdb.Movies(s["id"]).info() + if s["media_type"] == "tv": + more_info = tmdb.TV(s["id"]).info() + if s["media_type"] == "person": + more_info = tmdb.People(s["id"]).info() + count = count + 1 + if s["media_type"] == "person": + tagline = f"*Known for department*: {more_info['known_for_department']}" + else: + tagline = ( + f"**{more_info['tagline']}**" + if more_info["tagline"] + else "**No Tagline**" + ) + date = f"*Release date*: {more_info['release_date'] if 'release_date' in more_info else 'No release date'}" + if s["media_type"] == "person": + date = f"*Birthday*: {more_info['birthday']}" + score = ( + f"*Score*: {int(float(s['vote_average']) * 10)}%" + if "vote_average" in s + else "" + ) + embed_values = [ + tagline, + date, + score, + ] + title = s["title"] if "title" in s else s["name"] + embed.add_field( + name=f"[{count}] {title} ({types[search.results[count-1]['media_type']]})", + value="\n".join(embed_values), + inline=False, + ) + embed.set_thumbnail(url="attachment://thumbnail.png") + embed.set_footer(text="Data by TMDb", icon_url="attachment://TMDb.png") + + view = ( + MediaButtons(search.results, embed, interaction.user, self.bot) + if len(search.results) + else None + ) + if view: + remove_items(search.results, view) + await interaction.response.send_message( + embed=embed, files=[file, tmdb_logo], view=view + ) diff --git a/cogs/misc/reaction_translate.py b/cogs/misc/reaction_translate.py new file mode 100644 index 00000000..c3a7c451 --- /dev/null +++ b/cogs/misc/reaction_translate.py @@ -0,0 +1,69 @@ +import i18n +import discord +from .flags import language_dict +from discord import app_commands +from discord.ext import commands +from utils import get_language, get_color, translate, thumbnail, delete_thumbnail + + +class ReactionTranslate(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(description="Disable or enable reaction translate") + @app_commands.describe(value="Wheter to enable or disable reaction translate") + @app_commands.checks.has_permissions(manage_guild=True) + async def reaction_translate(self, interaction: discord.Interaction, value: bool): + await self.bot.db_pool.execute( + "INSERT INTO guilds (guild_id, reaction_translate) VALUES ($1, $2) ON CONFLICT(guild_id) DO UPDATE SET reaction_translate = $2", + interaction.guild.id, + value, + ) + return await interaction.response.send_message( + i18n.t( + f"misc.reaction_translate_{'activated' if value else 'deactivated'}", + locale=get_language(self.bot, interaction.guild.id), + ) + ) + + @commands.Cog.listener() + async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): + state = await self.bot.db_pool.fetchval( + "SELECT reaction_translate FROM guilds WHERE guild_id = $1", + payload.guild_id, + ) + try: + if not state or not language_dict[payload.emoji.name] or payload.member.bot: + return + except KeyError: + return + + message: discord.Message = await self.bot.get_channel( + payload.channel_id + ).fetch_message(payload.message_id) + language = language_dict[payload.emoji.name] + translation = await translate(message.content, language) + + if not message.content: + return + + thumbnail_path = await thumbnail(payload.guild_id, "translation", self.bot) + file = discord.File(thumbnail_path, filename="thumbnail.png") + + embed = discord.Embed( + description=translation, color=await get_color(self.bot, payload.guild_id) + ) + embed.set_author( + name=message.author.display_name, icon_url=message.author.avatar.url + ) + embed.set_footer( + text=i18n.t( + "misc.reaction_translate_footer", + locale=get_language(self.bot, payload.guild_id), + user=payload.member.display_name, + ) + ) + embed.set_thumbnail(url="attachment://thumbnail.png") + + await message.reply(embed=embed, mention_author=False, file=file) + await delete_thumbnail(payload.guild_id, "translation") diff --git a/cogs/misc/reddit.py b/cogs/misc/reddit.py new file mode 100644 index 00000000..cb5a3a63 --- /dev/null +++ b/cogs/misc/reddit.py @@ -0,0 +1,29 @@ +import discord +import aiohttp +from discord import app_commands +from discord.ext import commands +from TakoBot import TakoBot, MemeButtons +from utils import delete_thumbnail, new_meme + + +class Reddit(commands.Cog): + def __init__(self, bot: TakoBot): + self.bot = bot + + @app_commands.command( + description="Get a random meme from the subreddits: memes, me_irl or dankmemes" + ) + async def meme(self, interaction: discord.Interaction): + async with aiohttp.ClientSession() as session: + async with session.get("https://meme-api.herokuapp.com/gimme/") as r: + embed, file = await new_meme( + interaction.guild.id, + interaction.user.id, + self.bot, + self.bot.db_pool, + ) + + await interaction.response.send_message( + embed=embed, file=file, view=MemeButtons(self.bot), ephemeral=True + ) + delete_thumbnail(interaction.guild.id, "reddit") diff --git a/cogs/misc/show_tag.py b/cogs/misc/show_tag.py new file mode 100644 index 00000000..bb10029c --- /dev/null +++ b/cogs/misc/show_tag.py @@ -0,0 +1,52 @@ +import discord +from utils import get_color +from discord import app_commands +from discord.ext import commands + + +class ShowTag(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(description="Show a tag", name="tag-show") + async def show(self, interaction: discord.Interaction, tag: str): + if len(tag) < 32 or len(tag) > 36: + return await interaction.response.send_message( + "This tag does not exist!", ephemeral=True + ) + tag = await self.bot.db_pool.fetchrow( + "SELECT * FROM tags WHERE id = $1 AND guild_id = $2;", + tag, + interaction.guild.id, + ) + if tag == None: + return await interaction.response.send_message( + "This tag does not exist!", ephemeral=True + ) + if tag["embed"]: + embed = discord.Embed( + title=tag["name"], + description=tag["content"], + color=await get_color(self.bot, interaction.guild.id), + ) + embed.set_thumbnail(url=tag["thumbnail"]) + embed.set_image(url=tag["image"]) + embed.set_footer(text=tag["footer"]) + return await interaction.response.send_message(embed=embed) + await interaction.response.send_message(f"**{tag['name']}**\n{tag['content']}") + + @show.autocomplete(name="tag") + async def autocomplete_callback( + self, interaction: discord.Interaction, current: str + ): + tags = await self.bot.db_pool.fetch( + "SELECT * FROM tags WHERE guild_id = $1;", interaction.guild.id + ) + return [ + app_commands.Choice( + name=f"{tag['name']} ({str(tag['id'])})", value=str(tag["id"]) + ) + for tag in tags + if current.lower() in tag["name"].lower() + or current.lower() in str(tag["id"]).lower() + ] diff --git a/cogs/misc/tag.py b/cogs/misc/tag.py new file mode 100644 index 00000000..51e4918d --- /dev/null +++ b/cogs/misc/tag.py @@ -0,0 +1,317 @@ +import uuid +import discord +from TakoBot import TakoBot +from datetime import datetime +from discord import app_commands +from discord.ext import commands +from utils import get_color, number_of_pages_needed + + +class TagCreation(discord.ui.Modal, title="Create a Tag"): + def __init__(self, embed: bool, bot: TakoBot): + super().__init__() + self.embed = embed + self.bot = bot + + self.add_item( + discord.ui.TextInput( + label="Name", + placeholder="Name of the tag", + max_length=35, + required=True, + ) + ) + self.add_item( + discord.ui.TextInput( + label="Content", + placeholder="Content of the tag", + max_length=4000 if embed else 1950, + style=discord.TextStyle.long, + required=True, + ) + ) + if embed: + self.add_item(discord.ui.TextInput(label="Thumbnail", required=False)) + self.add_item(discord.ui.TextInput(label="Image", required=False)) + self.add_item( + discord.ui.TextInput(label="Footer", max_length=1965, required=False) + ) + + async def on_submit(self, interaction: discord.Interaction): + id = uuid.uuid4() + if not self.embed: + await self.bot.db_pool.execute( + "INSERT INTO tags (id, name, content, embed, guild_id) VALUES ($1, $2, $3, $4, $5)", + id, + self.children[0].value, + self.children[1].value, + False, + interaction.guild.id, + ) + else: + await self.bot.db_pool.execute( + "INSERT INTO tags (id, name, content, thumbnail, image, footer, embed, guild_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + id, + self.children[0].value, + self.children[1].value, + self.children[2].value, + self.children[3].value, + self.children[4].value, + True, + interaction.guild.id, + ) + await interaction.response.send_message( + f"Succesfully created the tag! (ID: *{str(id)}*)", ephemeral=True + ) + + +class TagEdit(discord.ui.Modal, title="Edit a Tag"): + def __init__(self, tag: str, embed: bool, bot: TakoBot): + super().__init__() + self.embed = embed + self.tag = tag + self.bot = bot + + self.add_item( + discord.ui.TextInput( + label="Name", + placeholder="Name of the tag", + max_length=35, + default=tag["name"], + required=True, + ) + ) + self.add_item( + discord.ui.TextInput( + label="Content", + placeholder="Content of the tag", + max_length=4000 if embed else 1950, + default=tag["content"], + style=discord.TextStyle.long, + required=True, + ) + ) + if embed: + self.add_item( + discord.ui.TextInput( + label="Thumbnail", default=tag["thumbnail"], required=False + ) + ) + self.add_item( + discord.ui.TextInput( + label="Image", default=tag["image"], required=False + ) + ) + self.add_item( + discord.ui.TextInput( + label="Footer", + max_length=1965, + default=tag["footer"], + required=False, + ) + ) + + async def on_submit(self, interaction: discord.Interaction): + if not self.embed: + await self.bot.db_pool.execute( + "UPDATE tags SET name = $1, content = $2 WHERE id = $3;", + self.children[0].value, + self.children[1].value, + self.tag["id"], + ) + else: + await self.bot.db_pool.execute( + "UPDATE tags SET name = $1, content = $2, thumbnail = $3, image = $4, footer = $5 WHERE id = $6;", + self.children[0].value, + self.children[1].value, + self.children[2].value, + self.children[3].value, + self.children[4].value, + self.tag["id"], + ) + await interaction.response.send_message( + f"Succesfully edited the tag! (ID: *{self.tag['id']}*)", ephemeral=True + ) + + +class PaginatorButtons(discord.ui.View): + def __init__(self, array, total_pages, current_page, bot): + super().__init__() + self.array = array + self.total_pages = total_pages + self.current_page = current_page + self.bot = bot + + @discord.ui.button(label="First Page", emoji="⏪") + async def first_page( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + self.current_page = 1 + embed = discord.Embed( + title="Tags", + description="\n".join( + self.array[55 * self.current_page - 55 : 55 * self.current_page] + ), + color=await get_color(self.bot, interaction.guild.id), + timestamp=datetime.now(), + ) + embed.set_footer(text=f"Page {self.current_page}/{self.total_pages}") + await interaction.response.edit_message(embed=embed, view=self) + + @discord.ui.button(label="Previous Page", emoji="◀️") + async def previous_page( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + if self.current_page == 1: + return await interaction.response.send_message( + "You are already on the first page!", ephemeral=True + ) + self.current_page = self.current_page - 1 + embed = discord.Embed( + title="Tags", + description="\n".join( + self.array[55 * self.current_page - 55 : 55 * self.current_page] + ), + color=await get_color(self.bot, interaction.guild.id), + timestamp=datetime.now(), + ) + embed.set_footer(text=f"Page {self.current_page}/{self.total_pages}") + await interaction.response.edit_message(embed=embed, view=self) + + @discord.ui.button(label="Next Page", emoji="▶️") + async def next_page( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + if self.current_page == self.total_pages: + return await interaction.response.send_message( + "You are already on the last page!", ephemeral=True + ) + self.current_page = self.current_page + 1 + embed = discord.Embed( + title="Tags", + description="\n".join( + self.array[55 * self.current_page - 55 : 55 * self.current_page] + ), + color=await get_color(self.bot, interaction.guild.id), + timestamp=datetime.now(), + ) + embed.set_footer(text=f"Page {self.current_page}/{self.total_pages}") + await interaction.response.edit_message(embed=embed, view=self) + + @discord.ui.button(label="Last Page", emoji="⏩") + async def last_page( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + if self.current_page == self.total_pages: + return await interaction.response.send_message( + "You are already on the last page!", ephemeral=True + ) + self.current_page = self.total_pages + embed = discord.Embed( + title="Tags", + description="\n".join( + self.array[55 * self.current_page - 55 : 55 * self.current_page] + ), + color=await get_color(self.bot, interaction.guild.id), + timestamp=datetime.now(), + ) + embed.set_footer(text=f"Page {self.current_page}/{self.total_pages}") + await interaction.response.edit_message(embed=embed, view=self) + + +class Tag(commands.GroupCog, group_name="tag"): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @app_commands.command(description="Create a tag") + async def create(self, interaction: discord.Interaction, embed: bool = True): + await interaction.response.send_modal(TagCreation(embed, self.bot)) + + @app_commands.command(description="Edit a tag") + async def edit(self, interaction: discord.Interaction, tag: str): + if len(tag) < 32 or len(tag) > 36: + return await interaction.response.send_message( + "This tag does not exist!", ephemeral=True + ) + tag = await self.bot.db_pool.fetchrow( + "SELECT * FROM tags WHERE id = $1 AND guild_id = $2;", + tag, + interaction.guild.id, + ) + if tag == None: + return await interaction.response.send_message( + "This tag does not exist!", ephemeral=True + ) + await interaction.response.send_modal( + TagEdit(tag=tag, embed=tag["embed"], bot=self.bot) + ) + + @app_commands.command(description="Delete a tag") + async def delete(self, interaction: discord.Interaction, tag: str): + if len(tag) < 32 or len(tag) > 36: + return await interaction.response.send_message( + "This tag does not exist!", ephemeral=True + ) + try: + data = await self.bot.db_pool.fetchrow( + "SELECT * FROM tags WHERE id = $1 AND guild_id = $2;", + tag, + interaction.guild.id, + ) + except: + pass + if data == None: + return await interaction.response.send_message( + "This tag does not exist!", ephemeral=True + ) + else: + await self.bot.db_pool.execute( + "DELETE FROM tags WHERE id = $1 AND guild_id = $2;", + tag, + interaction.guild.id, + ) + await interaction.response.send_message( + "Succesfully deleted the tag!", ephemeral=True + ) + + @app_commands.command(description="List all tags in the current guild") + async def list(self, interaction: discord.Interaction): + tags = await self.bot.db_pool.fetch( + "SELECT * FROM tags WHERE guild_id = $1;", interaction.guild.id + ) + tags_array = [] + for tag in tags: + tags_array.append(f"{tag['name']} ({tag['id']})") + if not tags_array: + tags_array = ["There are no tags on this server"] + total_pages = number_of_pages_needed(55, len(tags_array)) + index = 55 + embed = discord.Embed( + title="Tags", + description="\n".join(tags_array[:index]), + timestamp=datetime.now(), + color=await get_color(self.bot, interaction.guild.id), + ) + embed.set_footer(text=f"Page 1/{total_pages}") + if total_pages > 1: + view = PaginatorButtons(tags_array, total_pages, 1, self.bot) + await interaction.response.send_message(embed=embed, view=view) + else: + await interaction.response.send_message(embed=embed) + + @edit.autocomplete(name="tag") + @delete.autocomplete(name="tag") + async def autocomplete_callback( + self, interaction: discord.Interaction, current: str + ): + tags = await self.bot.db_pool.fetch( + "SELECT * FROM tags WHERE guild_id = $1;", interaction.guild.id + ) + return [ + app_commands.Choice( + name=f"{tag['name']} ({str(tag['id'])})", value=str(tag["id"]) + ) + for tag in tags + if current.lower() in tag["name"].lower() + or current.lower() in str(tag["id"]).lower() + ] diff --git a/cogs/misc/translate.py b/cogs/misc/translate.py new file mode 100644 index 00000000..762a555e --- /dev/null +++ b/cogs/misc/translate.py @@ -0,0 +1,51 @@ +import i18n +import discord +import requests +from discord import app_commands +from discord.ext import commands +from utils import get_language, get_color, thumbnail, translate + + +class Translate(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(description="Translate something") + @app_commands.describe( + text="The text to translate", + language="The language to translate to (default: server language or en) (examples: de, ch, en, es, ar) (special: morse)", + source="The source language (default: auto)", + ) + async def translate( + self, + interaction: discord.Interaction, + text: str, + language: str = None, + source: str = "auto", + ): + await interaction.response.defer() + if not language: + language = get_language(self.bot, interaction.guild.id) + if language.lower() == "morse": + request = requests.get( + f"https://api.funtranslations.com/translate/morse.json?text={text}" + ).json() + translation = request["contents"]["translated"] + source = "any" + else: + translation = await translate(text, language, source) + thumbnail_path = await thumbnail(interaction.user.id, "translation", self.bot) + file = discord.File(thumbnail_path, filename="thumbnail.png") + description = [ + translation, + "", + i18n.t("misc.source", locale=language, source=source), + i18n.t("misc.target", locale=language, target=language), + ] + embed = discord.Embed( + title=i18n.t("misc.translation", locale=language), + description="\n".join(description), + color=await get_color(self.bot, interaction.guild.id), + ) + embed.set_thumbnail(url="attachment://thumbnail.png") + await interaction.followup.send(embed=embed, file=file) diff --git a/cogs/misc/youtube.py b/cogs/misc/youtube.py new file mode 100644 index 00000000..9a917335 --- /dev/null +++ b/cogs/misc/youtube.py @@ -0,0 +1,39 @@ +import discord +from millify import millify +from main import youtube_api +from discord import app_commands +from discord.ext import commands +from utils import get_color + + +class Youtube(commands.GroupCog, group_name="youtube"): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @app_commands.command(description="Search a video on YouTube") + @app_commands.describe(query="The query for the YouTube Search") + async def search(self, interaction: discord.Interaction, query: str): + await interaction.response.defer() + results = youtube_api.search_by_keywords(q=query, count=3).items + embed = discord.Embed( + title="YouTube Search", + color=await get_color(self.bot, interaction.guild.id), + description=f"Top {len(results)} Search results for *{query}*", + ) + + if not results: + embed.description = f"No results found for *{query}*" + for result in results: + video = youtube_api.get_video_by_id(video_id=result.id.videoId).items[0] + precision = 2 + field_value = [ + f"[__Video Link__](https://youtube.com/watch?v={result.id.videoId})", + f"*Channel*: {video.snippet.channelTitle}", + f"👀 {millify(video.statistics.viewCount, precision=precision)}", + f"👍 {millify(video.statistics.likeCount, precision=precision)}", + f"💬 {millify(video.statistics.commentCount, precision=precision)}", + ] + embed.add_field( + name=video.snippet.title, value="\n".join(field_value), inline=False + ) + await interaction.followup.send(embed=embed) diff --git a/cogs/moderation/__init__.py b/cogs/moderation/__init__.py new file mode 100644 index 00000000..a0ae0f6f --- /dev/null +++ b/cogs/moderation/__init__.py @@ -0,0 +1,9 @@ +from .anti_phishing import AntiPhishing +from .ban_game import BanGame +from .clear import Clear + + +async def setup(bot): + await bot.add_cog(AntiPhishing(bot)) + await bot.add_cog(BanGame(bot)) + await bot.add_cog(Clear(bot)) diff --git a/cogs/moderation/anti_phishing.py b/cogs/moderation/anti_phishing.py new file mode 100644 index 00000000..8a5a26b1 --- /dev/null +++ b/cogs/moderation/anti_phishing.py @@ -0,0 +1,22 @@ +import i18n +import discord +from discord.ext import commands +from utils import get_language + + +class AntiPhishing(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + @commands.Cog.listener() + async def on_message(self, message: discord.Message): + for domain in self.bot.sussy_domains: + if message.content.lower().__contains__(domain): + await message.delete() + await message.channel.send( + i18n.t( + "moderation.malicious_link", + user=message.author.mention, + locale=get_language(self.bot, message.guild.id), + ) + ) diff --git a/cogs/moderation/ban_game.py b/cogs/moderation/ban_game.py new file mode 100644 index 00000000..792ba572 --- /dev/null +++ b/cogs/moderation/ban_game.py @@ -0,0 +1,94 @@ +import i18n +import discord +from utils import get_language +from discord.ext import commands + + +class BanGame(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.hybrid_command( + description="Automatically ban an user if they're playing a specific game" + ) + @commands.has_guild_permissions(ban_members=True, manage_guild=True) + @commands.bot_has_guild_permissions(ban_members=True) + async def ban_game(self, ctx: commands.Context, game: str): + game = game.lower() + data = await self.bot.db_pool.fetchrow( + "SELECT * FROM guilds WHERE guild_id = $1;", ctx.author.guild.id + ) + if data == None: + data = await self.bot.db_pool.execute( + "INSERT INTO guilds (guild_id, banned_games) VALUES ($1, $2);", + ctx.author.guild.id, + [game], + ) + else: + array = data["banned_games"] + if array is None: + array = [] + if game not in array: + array.append(game.lower()) + data = await self.bot.db_pool.execute( + "UPDATE guilds SET banned_games = $1 WHERE guild_id = $2", + array, + ctx.author.guild.id, + ) + await ctx.reply( + f"✅ Added *{game}* to the banned games list", + ephemeral=True, + delete_after=5, + ) + await ctx.message.delete(delay=5) + return + else: + array.remove(game.lower()) + data = await self.bot.db_pool.execute( + "UPDATE guilds SET banned_games = $1 WHERE guild_id = $2", + array, + ctx.author.guild.id, + ) + await ctx.reply( + f"🗑️ Removed *{game}* from the banned games list", + ephemeral=True, + delete_after=5, + ) + await ctx.message.delete(delay=5) + return + + @commands.Cog.listener() + async def on_presence_update(self, before: discord.Member, after: discord.Member): + if before.bot: + return + if before.activities != after.activities: + data = await self.bot.db_pool.fetchrow( + "SELECT * FROM guilds WHERE guild_id = $1;", after.guild.id + ) + if not data or not hasattr(data, "banned_games"): + return + for activity in after.activities: + activity_name = activity + if hasattr(activity, "name"): + activity_name = activity.name + if activity_name.lower() in data["banned_games"]: + language = get_language(self.bot, after.guild.id) + try: + await after.ban( + reason=i18n.t( + "moderation.ban_game_log", + game=activity_name, + locale=language, + ), + delete_message_days=0, + ) + await after.send( + i18n.t( + "moderation.ban_game_dm", + guild=after.guild.id, + game=activity_name, + locale=language, + ) + ) + except: + pass diff --git a/cogs/moderation/clear.py b/cogs/moderation/clear.py new file mode 100644 index 00000000..8108ef6f --- /dev/null +++ b/cogs/moderation/clear.py @@ -0,0 +1,96 @@ +import i18n +import discord +from TakoBot import TakoBot +from datetime import datetime +from discord import app_commands +from discord.ext import commands +from utils import get_color, get_language + + +class Clear(commands.Cog): + def __init__(self, bot: TakoBot): + self.bot = bot + + @app_commands.command(description="Delete multiple messages at once") + @app_commands.describe( + amount="The amount of messages to be deleted", + target="The user to delete the messages from", + channel="The channel to delete the messages from", + ) + @app_commands.checks.has_permissions(manage_messages=True) + @app_commands.checks.bot_has_permissions(manage_messages=True) + async def clear( + self, + interaction: discord.Interaction, + amount: int = 1, + target: discord.User | discord.Member = None, + channel: discord.TextChannel = None, + ): + await interaction.response.defer(ephemeral=True) + language = get_language(self.bot, interaction.guild.id) + too_many_messages = False + if amount > 100: + amount = 100 + too_many_messages_file = discord.File( + "assets/warning.png", filename="warning.png" + ) + too_many_messages_embed = discord.Embed( + title=f"**{i18n.t('moderation.too_many_messages_title', locale=language)}**", + description=i18n.t("moderation.too_many_messages", locale=language), + color=discord.Color.yellow(), + timestamp=datetime.now(), + ) + too_many_messages_embed.set_thumbnail(url="attachment://warning.png") + too_many_messages_embed.set_footer( + text=i18n.t("errors.warning", locale=language) + ) + too_many_messages = True + if not channel: + channel = interaction.channel + if not target: + await interaction.channel.purge( + limit=amount, + reason=i18n.t( + "moderation.clear_reason", user=interaction.user, locale=language + ), + ) + else: + messages = [] + message_count = 0 + async for msg in channel.history(limit=None): + if message_count == amount: + break + if msg.author.id == target.id: + messages += [msg] + message_count += 1 + await interaction.channel.delete_messages( + messages=messages, + reason=i18n.t( + "moderation.clear_reason", user=interaction.user, locale=language + ), + ) + embed = discord.Embed( + description=i18n.t( + f"moderation.cleared{'_target' if target else ''}", + amount=amount, + channel=channel.mention, + target=str(target), + locale=language, + ), + color=discord.Color.red(), + timestamp=datetime.utcnow(), + ) + embed.set_author( + name=str(interaction.user), + url=f"https://discord.com/user/{interaction.user.id}", + icon_url=interaction.user.display_avatar.url, + ) + file = discord.File("assets/trash.png", filename="trash.png") + embed.set_thumbnail(url="attachment://trash.png") + embeds = [embed] + files = [file] + if too_many_messages: + embeds.append(too_many_messages_embed) + files.append(too_many_messages_file) + await interaction.followup.send(embeds=embeds, files=files) + return diff --git a/cogs/owner/__init__.py b/cogs/owner/__init__.py new file mode 100644 index 00000000..67aee505 --- /dev/null +++ b/cogs/owner/__init__.py @@ -0,0 +1,5 @@ +from ._cog import Owner + + +async def setup(bot): + await bot.add_cog(Owner(bot)) diff --git a/cogs/owner/_cog.py b/cogs/owner/_cog.py new file mode 100644 index 00000000..5575468f --- /dev/null +++ b/cogs/owner/_cog.py @@ -0,0 +1,11 @@ +from .sync import Sync +from .extension import Extension +from .manage_announcements import ManageAnnouncements + +subclasses = Extension, ManageAnnouncements, Sync + + +class Owner(*subclasses): + """ + These are commands only executable by the bot owner + """ diff --git a/cogs/owner/extension.py b/cogs/owner/extension.py new file mode 100644 index 00000000..9ff5cd15 --- /dev/null +++ b/cogs/owner/extension.py @@ -0,0 +1,74 @@ +import i18n +import discord +from discord import app_commands +from discord.ext import commands +from utils import add_extension, error_embed, get_language + + +class Extension(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + @commands.is_owner() + @commands.hybrid_group() + async def extension(self, ctx: commands.Context): + return ctx.send( + "Please use a subcommand.\nRun `k!help extension` for more information." + ) + + @commands.is_owner() + @extension.command(description="Add an extension from Git") + @app_commands.describe(url="The url of the repository to add") + async def add(self, ctx: commands.Context, url: str): + installing = discord.Embed( + title="📥 Installing", + description=f"Installing [the extension]({url})...\nThis may take a while depending on the extension's size", + color=discord.Color.light_gray(), + ) + msg = await ctx.send(embed=installing, ephemeral=True) + adder = add_extension(url) + if adder == 0: + embed = discord.Embed( + title="✅ Installed", + description=f"[The extension]({url}) has been installed!", + color=discord.Color.green(), + ) + if adder == 1: + embed = discord.Embed( + title="❌ Invalid url", + description=f"[The extension]({url}) could not be installed!", + color=discord.Color.red(), + ) + if adder != 0 or adder != 1: + embed = discord.Embed( + title="❌ Unknown error", + description=f"[The extension]({url}) could not be installed!", + color=discord.Color.red(), + ) + await msg.edit(embed=embed) + + @commands.is_owner() + @extension.command(description="Reload a specific extension") + @app_commands.describe(extension="The extension to reload") + async def reload(self, ctx, extension: str): + await self.bot.reload_extension(f"cogs.{extension.lower()}") + embed = discord.Embed( + title="Reload", + description=f"Category `{extension.lower()}` successfully reloaded", + color=discord.Color.green(), + ) + await ctx.reply(embed=embed, ephemeral=True) + + @add.error + @reload.error + async def on_command_error( + self, ctx: commands.Context, error: commands.CommandError + ): + if isinstance(error, commands.NotOwner): + language = get_language(self.bot, ctx.guild.id if ctx.guild else None) + embed, file = error_embed( + i18n.t("errors.not_owner_title", locale=language), + i18n.t("errors.not_owner", locale=language), + footer=i18n.t("errors.error_occured", locale=language), + ) + return await ctx.reply(embed=embed, file=file, ephemeral=True) diff --git a/cogs/owner/manage_announcements.py b/cogs/owner/manage_announcements.py new file mode 100644 index 00000000..75ed6d87 --- /dev/null +++ b/cogs/owner/manage_announcements.py @@ -0,0 +1,97 @@ +import i18n +import discord +from TakoBot import TakoBot +from discord import app_commands +from discord.ext import commands +from utils import get_language, owner_only + + +class AnnouncementModal(discord.ui.Modal, title="Announcement Creator"): + def __init__(self, bot: TakoBot, type: str): + super().__init__() + self.bot = bot + self.type = type + + annnouncement_title = discord.ui.TextInput( + label="Title", max_length=256, required=True + ) + description = discord.ui.TextInput( + label="Description", + max_length=4000, + placeholder="Enter a description", + style=discord.TextStyle.long, + required=True, + ) + + async def on_submit(self, interaction: discord.Interaction): + await self.bot.db_pool.execute( + "INSERT INTO announcements (title, description, type) VALUES ($1, $2, $3);", + self.annnouncement_title.value, + self.description.value, + self.type, + ) + await interaction.response.send_message( + i18n.t( + "owner.success", locale=get_language(self.bot, interaction.guild.id) + ), + ephemeral=True, + ) + + +class ManageAnnouncements(commands.Cog): + def __init__(self, bot: TakoBot): + self.bot = bot + + @app_commands.command( + description="Create a new global announcement (Bot Owner only)" + ) + @app_commands.choices( + type=[ + app_commands.Choice(name="General", value="general"), + app_commands.Choice(name="Changelog", value="changelog"), + ] + ) + @owner_only() + async def set_announcement( + self, + interaction: discord.Interaction, + type: str = "general", + ): + await interaction.response.send_modal(AnnouncementModal(self.bot, type)) + + @app_commands.command(description="Delete an announcement (Bot Owner only)") + @app_commands.describe(id="The id of the announcement to delete") + @owner_only() + async def del_announcement(self, interaction: discord.Interaction, id: str): + announcement = await self.bot.db_pool.fetchrow( + "SELECT * FROM announcements WHERE id = $1;", id + ) + if not announcement: + return await interaction.response.send_message( + i18n.t( + "owner.announcement_not_found", + locale=get_language(self.bot, interaction.guild.id), + ) + ) + await self.bot.db_pool.execute("DELETE FROM announcements WHERE id = $1", id) + await interaction.response.send_message( + i18n.t( + "owner.success", locale=get_language(self.bot, interaction.guild.id) + ), + ephemeral=True, + ) + + @del_announcement.autocomplete("id") + async def autocomplete_callback( + self, interaction: discord.Interaction, current: str + ): + announcements = await self.bot.db_pool.fetch("SELECT * FROM announcements;") + return [ + app_commands.Choice( + name=f"{announcement['title']} ({str(announcement['id'])})", + value=str(announcement["id"]), + ) + for announcement in announcements + if current.lower() in announcement["title"].lower() + or current.lower() in str(announcement["id"]).lower() + ] diff --git a/cogs/owner/sync.py b/cogs/owner/sync.py new file mode 100644 index 00000000..89388665 --- /dev/null +++ b/cogs/owner/sync.py @@ -0,0 +1,30 @@ +import i18n +import discord +import config +from discord.ext import commands +from utils import error_embed, get_language + + +class Sync(commands.Cog): + @commands.hybrid_command(description="Sync all slash commands") + @commands.is_owner() + async def sync(self, ctx): + if hasattr(config, "TEST_GUILD"): + self.bot.tree.copy_global_to(guild=discord.Object(id=config.TEST_GUILD)) + await self.bot.tree.sync(guild=discord.Object(id=config.TEST_GUILD)) + else: + await self.bot.tree.sync() + await ctx.reply("Successfully synced!", ephemeral=True) + + @sync.error + async def on_command_error( + self, ctx: commands.Context, error: commands.CommandError + ): + if isinstance(error, commands.NotOwner): + language = get_language(self.bot, ctx.guild.id if ctx.guild else None) + embed, file = error_embed( + i18n.t("errors.not_owner_title", locale=language), + i18n.t("errors.not_owner", locale=language), + footer=i18n.t("errors.error_occured", locale=language), + ) + return await ctx.reply(embed=embed, file=file, ephemeral=True) diff --git a/config.py b/config.py new file mode 100644 index 00000000..8c592530 --- /dev/null +++ b/config.py @@ -0,0 +1,26 @@ +TEST_GUILD = 884046271176912917 +DEFAULT_BANK = 0 +DEFAULT_WALLET = 1000 +ANTI_PHISHING_LIST = [] +DEFAULT_COLOR = 0x4ADE80 +DELETE_THUMBNAILS = True +DEFAULT_COLOR_STR = "0x4ADE80" +DONATOR_ROLE = 969286409200468028 +TRANSLATOR_ROLE = 980904580286140426 +CURRENCY = " <:TK:1025679113777848320>" +ALPHA_TESTER_ROLE = 969306314981376071 +DEV_ROLE = 969285824107642990 +SIMPLY_TRANSLATE = "https://translate.slipfox.xyz" # no / at the end is important +IMGEN = "https://imgen.tako.rocks" # no / at the end is important +# Regex used for valid sources on the emoji command (`^https?:\/example[].]org\/` recommended, seperate with `|`) +# ! If removed people can enter every url even non-image ones (viruses etc.). +ALLOWED_SOURCES = "^https?:\/\/cdn3[.]emoji[.]gg\/|^https:\/\/cdn[.]discordapp[.]com\/|^https?:\/\/i[.]imgur[.]com\/|^https?:\/\/raw[.]githubusercontent[.]com\/" +# * Social Media +EMOJI_YT = "<:youtube:991732817585242112>" # optional +EMOJI_TWITTER = "<:twitter:991732692905377794>" # optional +TWITTER_LINK = "https://twitter.com/DiscordTako" # optional +YOUTUBE_LINK = "https://youtube.com/channel/UCRFUsdQIfinsdiKhLUs7ZFQ" # optional +# * Emojis (<[a if animated]:[emoji_name]:[emoji_id]>) +EMOJI_ALPHA_TESTER = "🧪" +EMOJI_DONATOR = "" +EMOJI_TRANSLATOR = "🌐" diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000..587899ec --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,17 @@ +"project_id": "515826" +"base_path": "." +"base_url": "https://api.crowdin.com" +"commit_message": "🌐 Updated %language%" +"append_commit_message": false +"pull_request_labels": ["translation"] + +"preserve_hierarchy": true + +files: [ + { + "source": "/i18n/*/en.yml", + "translation": "/%original_path%/%two_letters_code%.yml", + "skip_untranslated_strings": true, + "export_only_approved": true + } +] \ No newline at end of file diff --git a/extensions/.exists b/extensions/.exists new file mode 100644 index 00000000..8c73bea3 --- /dev/null +++ b/extensions/.exists @@ -0,0 +1,4 @@ +Here could be your extension! +Hier könnte ihre Extension sein! + +Add more languages pls \ No newline at end of file diff --git a/helper.py b/helper.py new file mode 100644 index 00000000..4a510fdf --- /dev/null +++ b/helper.py @@ -0,0 +1,99 @@ +import os +import asyncio +import bot_secrets +from pick import pick +from utils import clear_console, add_extension + + +async def main(): + clear_console() + options = [ + "Start the bot", + "Start RPC", + "Update", + "Init Database", + "Add Extension from Git", + "Exit", + ] + selected = pick( + options, "What do you want to do? (Use arrow keys to navigate)", "x" + ) + if selected[1] == len(options) - 1: + return + if selected[1] == 0: + print("Starting the bot...") + from main import main as bot_main + + await bot_main() + + if selected[1] == 1: + import DiscordRPC + + buttons = DiscordRPC.button( + button_one_label="Join Server", + button_one_url="https://discord.gg/dfmXNTmzyp", + button_two_label="View on top.gg", + button_two_url="https://top.gg/bot/878366398269771847", + ) + rpc = DiscordRPC.RPC.Set_ID(app_id="878366398269771847") + rpc.set_activity( + state="Tako", + details="A bot done right", + large_image="tako", + buttons=buttons, + ) + rpc.run() + if selected[1] == 2: + print("Pulling latest changes...", end="\r") + os.system("git pull > /dev/null") + print("Installing dependencies...", end="\r") + os.system("pip install -r requirements.txt > /dev/null 2>&1") + clear_console() + print("✔️ Done with updating") + if selected[1] == 3: + import asyncpg + + conn = await asyncpg.connect( + database=bot_secrets.DB_NAME, + host=bot_secrets.DB_HOST, + port=bot_secrets.DB_PORT if hasattr(bot_secrets, "DB_PORT") else 5432, + user=bot_secrets.DB_USER, + password=bot_secrets.DB_PASSWORD, + ) + version = await conn.fetch( + "SELECT version();", + ) + print(f"Initializing database with {next(version[0].values())}") + await conn.execute( + """ + CREATE TABLE IF NOT EXISTS badges (name TEXT PRIMARY KEY, emoji TEXT NOT NULL, description TEXT, users BIGINT ARRAY); + INSERT INTO badges (name, emoji, description) VALUES ('Alpha Tester', '🧪', 'Users who tested the bot in its early stage receive this badge') ON CONFLICT DO NOTHING; + INSERT INTO badges (name, emoji, description) VALUES ('Donator', '', 'Users who donated to the Tako Team receive this badge') ON CONFLICT DO NOTHING; + INSERT INTO badges (name, emoji, description) VALUES ('Translator', '🌐', 'Users who translated this bot receive this badge') ON CONFLICT DO NOTHING; + INSERT INTO badges (name, emoji, description) VALUES ('Core Developer', '🧑‍💻', 'Users who are the core developers from the the bot') ON CONFLICT DO NOTHING; + CREATE TABLE IF NOT EXISTS channels (channel_id BIGINT PRIMARY KEY, crosspost BOOLEAN NOT NULL); + CREATE TABLE IF NOT EXISTS guilds (guild_id BIGINT PRIMARY KEY, banned_games TEXT ARRAY, join_roles_user BIGINT ARRAY, join_roles_bot BIGINT ARRAY, language TEXT, reaction_translate BOOLEAN, auto_translate BOOLEAN DEFAULT FALSE, color TEXT); + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + CREATE TABLE IF NOT EXISTS tags (id uuid DEFAULT uuid_generate_v4() PRIMARY KEY, name TEXT, content TEXT, thumbnail TEXT, image TEXT, footer TEXT, embed BOOLEAN DEFAULT TRUE, guild_id BIGINT); + CREATE TABLE IF NOT EXISTS users (user_id BIGINT PRIMARY KEY, wallet BIGINT DEFAULT 1000, bank BIGINT DEFAULT 0, last_meme TEXT); + CREATE TABLE IF NOT EXISTS announcements (id uuid DEFAULT uuid_generate_v4() PRIMARY KEY, title TEXT, description TEXT, type TEXT, timestamp TIMESTAMP DEFAULT NOW()); + CREATE TABLE IF NOT EXISTS selfroles (id uuid DEFAULT uuid_generate_v4() PRIMARY KEY, guild_id BIGINT, select_array BIGINT ARRAY, min_values INT, max_values INT); + """ + ) + await conn.close() + clear_console() + print("✔️ Done with initializing database") + if selected[1] == 4: + url = input("Enter the url of the extension: ") + print("📥 Installing extension...", end="\r") + adder = add_extension(url) + if adder == 0: + return print("✔️ Done with installing the extension") + if adder == 1: + return print("❌ Invalid url") + if adder == 2: + print("❌ Failed to install the extension") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/i18n/config/de.yml b/i18n/config/de.yml new file mode 100644 index 00000000..fdad0200 --- /dev/null +++ b/i18n/config/de.yml @@ -0,0 +1,16 @@ +de: + language_success: "Meine Sprache ist nun auf `%{language}` gesetzt." + selfrole_created: "Dein Rollenauswahlmenü wurde erfolgreich erstellt!" + selfroles_updated: "Deine Rollen wurden erfolgreich aktualisiert!" + crossposting_not_news: "❌ %{channel} ist kein News-Kanal!" + crossposting_activated: "✅ Auto-Crossposting für %{channel} ist jetzt aktiviert" + crossposting_deactivated: "❌ Auto-Crossposting für %{channel} ist jetzt deaktiviert" + invalid_role: "Der Bot oder du hast nicht die Berechtigung, eine der ausgewählten Rollen zuzuweisen." + autojoinroles_title: "Autojoinroles für %{guild}" + autojoinroles_desc: "Dies ist eine Liste von Rollen, die automatisch neuen Mitgliedern hinzugefügt werden" + autojoinroles_users: "👤 Rollen für Menschen" + autojoinroles_bots: "🤖 Rollen für Bots" + autojoinroles_added: "✅ *%{role}* zur Autojoinrole-Liste hinzugefügt" + autojoinroles_removed: "🗑️ *%{role}* von der Autojoinrole-Liste entfernt" + autojoinroles_empty_title: "❌ Keine Rollen für %{type}" + autojoinroles_empty: "Es gibt keine Rollen für neue %{type} in der Liste der Autojoinroles" diff --git a/i18n/config/en.yml b/i18n/config/en.yml new file mode 100644 index 00000000..563c11d0 --- /dev/null +++ b/i18n/config/en.yml @@ -0,0 +1,16 @@ +en: + language_success: "My language is now set to `%{language}`." + selfrole_created: "Successfully created your role selection menu!" + selfroles_updated: "Successfully updated your roles!" + crossposting_not_news: "❌ %{channel} is not a news channel!" + crossposting_activated: "✅ Auto-crossposting for %{channel} is now activated" + crossposting_deactivated: "❌ Auto-crossposting for %{channel} is now deactivated" + invalid_role: "The bot or you does not have the permission to assign at least one role you selected." + autojoinroles_title: "Autojoinroles for %{guild}" + autojoinroles_desc: "This is a list of roles that will be automatically added to new members" + autojoinroles_users: "👤 User roles" + autojoinroles_bots: "🤖 Bot roles" + autojoinroles_added: "✅ Added *%{role}* to the autojoinrole list" + autojoinroles_removed: "🗑️ Removed *%{role}* from the autojoinrole list" + autojoinroles_empty_title: "❌ No roles for %{type}" + autojoinroles_empty: "There are no roles for new %{type} in the autojoinrole list" diff --git a/i18n/config/es.yml b/i18n/config/es.yml new file mode 100644 index 00000000..70375462 --- /dev/null +++ b/i18n/config/es.yml @@ -0,0 +1,6 @@ +es: + language_success: "Mi idioma se ha cambiado a `%{language}`." + selfrole_created: "Se ha creado tu menú de selección de roles correctamente!" + crossposting_not_news: "❌ %{channel} no es un canal de noticias!" + crossposting_activated: "✅ La publicación transversal automática para %{channel} está activada" + crossposting_deactivated: "❌ La publicación transversal automática para %{channel} ha sido desactivada" diff --git a/i18n/config/fr.yml b/i18n/config/fr.yml new file mode 100644 index 00000000..324296a1 --- /dev/null +++ b/i18n/config/fr.yml @@ -0,0 +1,8 @@ +fr: + language_success: "Mon langage est défini a `%{language}`." + selfrole_created: "La création du menu de rôle a était un succès!" + selfroles_updated: "Vous avez bien mis à jour vos rôles!" + crossposting_not_news: "❌ %{channel} n'est pas un salon d'information!" + crossposting_activated: "✅ Publication croisée pour %{channel} est activée" + crossposting_deactivated: "❌ Publication croisée pour %{channel} est maintenant désactiver" + invalid_role: "Le bot ou vous n'avez pas l'autorisation d'attribuer au moins un rôle que vous avez sélectionné." diff --git a/i18n/config/he.yml b/i18n/config/he.yml new file mode 100644 index 00000000..201c6e48 --- /dev/null +++ b/i18n/config/he.yml @@ -0,0 +1,8 @@ +he: + language_success: "השפה שלי מכוון ל `%{language}`." + selfrole_created: "כל התפקידים שלך סיימו בהצלחה!" + selfroles_updated: "התפקידים שלך מעודכנים בשלום!" + crossposting_not_news: "❌ %{channel} הוא לא מקום לחדשות!" + crossposting_activated: "✅ פירסום אוטומטי בשביל %{channel} עכשיו הופעל" + crossposting_deactivated: "❌ פירסום אוטומטי בשביל %{channel} עכשיו נכבה" + invalid_role: "הרובוט או אתה לא יכולים לבחור 1 או יותר תפקידים בגלל שאין לך או לרובוט רשות." diff --git a/i18n/config/hr.yml b/i18n/config/hr.yml new file mode 100644 index 00000000..9072f229 --- /dev/null +++ b/i18n/config/hr.yml @@ -0,0 +1,6 @@ +hr: + language_success: "Moj jezik je sada postavljen na `%{language}`." + selfrole_created: "Uspješno je kreiran tvoj meni za odabir uloga!" + crossposting_not_news: "%{channel} nije novinski kanal!" + crossposting_activated: "✅ Auto-crossposting za %{channel} sada je aktiviran" + crossposting_deactivated: "❌ Auto-crossposting za %{channel} sada je deaktiviran" diff --git a/i18n/config/id.yml b/i18n/config/id.yml new file mode 100644 index 00000000..5f2e117d --- /dev/null +++ b/i18n/config/id.yml @@ -0,0 +1,7 @@ +id: + language_success: "Bahasa diatur ke `%{language}`." + selfrole_created: "Berhasil membuat menu pemilihan role Anda!" + selfroles_updated: "Berhasil meng-update role Anda!" + crossposting_not_news: "❌️ %{channel} bukan channel News!" + crossposting_activated: "✅️ Posting silang otomatis untuk %{channel} sekarang aktif" + crossposting_deactivated: "❌️ Posting silang otomatis untuk %{channel} dimatikan" diff --git a/i18n/config/pl.yml b/i18n/config/pl.yml new file mode 100644 index 00000000..cc8293a2 --- /dev/null +++ b/i18n/config/pl.yml @@ -0,0 +1,2 @@ +pl: + language_success: "Mój język został ustawiony na `%{language}`." diff --git a/i18n/config/pt.yml b/i18n/config/pt.yml new file mode 100644 index 00000000..aa4ecb07 --- /dev/null +++ b/i18n/config/pt.yml @@ -0,0 +1,6 @@ +pt: + language_success: "Meu idioma agora está definido como `%{language}`." + selfrole_created: "Criado com sucesso os menu de reação!" + crossposting_not_news: "❌ %{channel} não é um canal de anúncios!" + crossposting_activated: "✅ O Auto-crossposting para o canal %{channel} foi ativado" + crossposting_deactivated: "❌ O Auto-crossposting para o canal %{channel} foi desativado" diff --git a/i18n/config/sv.yml b/i18n/config/sv.yml new file mode 100644 index 00000000..9e93ba4d --- /dev/null +++ b/i18n/config/sv.yml @@ -0,0 +1,6 @@ +sv: + language_success: "Mitt språk är nu satt till `%{language}`." + selfrole_created: "Skapelse av din rollurvalsmeny lyckad!" + crossposting_not_news: "❌ %{channel} är inte en nyhetskanal!" + crossposting_activated: "✅ Auto-crossposting för %{channel} är nu aktiverat" + crossposting_deactivated: "❌ Auto-crossposting för %{channel} är nu avaktiverat" diff --git a/i18n/economy/de.yml b/i18n/economy/de.yml new file mode 100644 index 00000000..c182f189 --- /dev/null +++ b/i18n/economy/de.yml @@ -0,0 +1,20 @@ +de: + balance: "Saldo" + wallet: "Portemonnaie" + bank: "Bank" + not_enough: "Du hast nicht genug Geld, um diese Aktion durchzuführen!" + more_than: "Du musst mindestens %{amount} ausgeben, um diese Aktion durchzuführen!" + not_self: "Du kannst dich nicht selbst wählen!" + not_bot: "Du kannst keinen Bot auswählen!" + not_bot_balance: "Bots können %{currency}s nicht besitzen!" + give: "%{user} fühlte sich großzügig und gab %{target} %{amount}" + transfer: "Geldtransfer" + #Head or Tail + ht: "Kopf oder Zahl" + ht_won: "Du hast gewonnen! (%{amount})" + ht_lost: "Du hast verloren! (-%{amount})" + bet: "Einsatz" + guess: "Vermutung" + head: "Kopf" + tail: "Zahl" + new_balance: "Neuer Saldo" diff --git a/i18n/economy/en.yml b/i18n/economy/en.yml new file mode 100644 index 00000000..83b0bfb9 --- /dev/null +++ b/i18n/economy/en.yml @@ -0,0 +1,20 @@ +en: + balance: "Balance" + wallet: "Wallet" + bank: "Bank" + not_enough: "You don't have enough money to perform that action!" + more_than: "You need to spend at least %{amount} to perform that action!" + not_self: "You cannot pick yourself!" + not_bot: "You cannot pick a bot!" + not_bot_balance: "Bots cannot own %{currency}s!" + give: "%{user} felt generous and gave %{target} %{amount}" + transfer: "Money Transfer" + # Head or Tail + ht: "Head or Tail" + ht_won: "You won! (%{amount})" + ht_lost: "You lost! (-%{amount})" + bet: "Bet" + guess: "Guess" + head: "Head" + tail: "Tail" + new_balance: "New Balance" diff --git a/i18n/economy/es.yml b/i18n/economy/es.yml new file mode 100644 index 00000000..a07a74d1 --- /dev/null +++ b/i18n/economy/es.yml @@ -0,0 +1 @@ +es: diff --git a/i18n/economy/fr.yml b/i18n/economy/fr.yml new file mode 100644 index 00000000..57de0ff2 --- /dev/null +++ b/i18n/economy/fr.yml @@ -0,0 +1,13 @@ +fr: + balance: "Balance" + wallet: "Portefeuille" + bank: "Banque" + #Head or Tail + ht: "Pile ou face" + ht_won: "Tu as gagné! (%{amount})" + ht_lost: "Tu as perdu! (%{amount})" + bet: "Parier" + guess: "Deviner" + head: "Face" + tail: "Pile" + new_balance: "Nouvelle balance" diff --git a/i18n/economy/he.yml b/i18n/economy/he.yml new file mode 100644 index 00000000..af6fa60a --- /dev/null +++ b/i18n/economy/he.yml @@ -0,0 +1 @@ +he: diff --git a/i18n/economy/hr.yml b/i18n/economy/hr.yml new file mode 100644 index 00000000..f67f33c7 --- /dev/null +++ b/i18n/economy/hr.yml @@ -0,0 +1 @@ +hr: diff --git a/i18n/economy/id.yml b/i18n/economy/id.yml new file mode 100644 index 00000000..b1aff9bf --- /dev/null +++ b/i18n/economy/id.yml @@ -0,0 +1,13 @@ +id: + balance: "Saldo" + wallet: "Dompet" + bank: "Bank" + #Head or Tail + ht: "Kepala atau Ekor" + ht_won: "Kamu menang! (%{amount})" + ht_lost: "Kamu kalah! (-%{amount})" + bet: "Taruhan" + guess: "Tebak" + head: "Kepala" + tail: "Ekor" + new_balance: "Saldo baru" diff --git a/i18n/economy/pl.yml b/i18n/economy/pl.yml new file mode 100644 index 00000000..a8e4dde7 --- /dev/null +++ b/i18n/economy/pl.yml @@ -0,0 +1 @@ +pl: diff --git a/i18n/economy/pt.yml b/i18n/economy/pt.yml new file mode 100644 index 00000000..9cbe1f03 --- /dev/null +++ b/i18n/economy/pt.yml @@ -0,0 +1 @@ +pt: diff --git a/i18n/economy/sv.yml b/i18n/economy/sv.yml new file mode 100644 index 00000000..7e73a972 --- /dev/null +++ b/i18n/economy/sv.yml @@ -0,0 +1 @@ +sv: diff --git a/i18n/errors/de.yml b/i18n/errors/de.yml new file mode 100644 index 00000000..990998de --- /dev/null +++ b/i18n/errors/de.yml @@ -0,0 +1,15 @@ +de: + warning: "Warnung" + error_occured: "Ein Fehler ist aufgetreten" + bot_missing_perms_title: "Fehlende Bot Berechtigung(en)" + bot_missing_perms: "Ich brauche *%{perms}* Berechtigung(en) um diesen Befehl auszuführen." + user_missing_perms_title: "Fehlende Nutzer Berechtigung(en)" + user_missing_perms: "Du brauchst *%{perms}* Berechtigung(en) um diesen Befehl auszuführen." + cooldown_title: "Cooldown" + cooldown: "Dieser Befehl befindet sich im Cooldown. Bitte versuche es erneut in %{time}s." + no_pm_title: "Keine Unterstützung für private Nachrichten" + no_pm: "Dieser Befehl kann nicht in Direktnachrichten genutzt werden." + check_failure_title: "Eine Überprüfung ist fehlgeschlagen" + check_failure: "Du hast nicht die Berechtigung diesen Befehl zu benutzen. (Eine Überprüfung ist fehlgeschlagen)" + not_owner_title: "Geheime Sachen" + not_owner: "Du musst der Besitzer des Bots sein, um diesen Befehl ausführen zu können." diff --git a/i18n/errors/en.yml b/i18n/errors/en.yml new file mode 100644 index 00000000..c98e4b2d --- /dev/null +++ b/i18n/errors/en.yml @@ -0,0 +1,15 @@ +en: + warning: "Warning" + error_occured: "An error occured" + bot_missing_perms_title: "Missing Bot Permission(s)" + bot_missing_perms: "I need the *%{perms}* permission(s) in order to run this command." + user_missing_perms_title: "Missing User Permission(s)" + user_missing_perms: "You need the *%{perms}* permission(s) in order to run this command." + cooldown_title: "Cooldown" + cooldown: "This command is on cooldown, please retry in %{time}s." + no_pm_title: "No Private Message Support" + no_pm: "This command cannot be used in private messages." + check_failure_title: "A Check has failed" + check_failure: "You do not have permission to use this command. (A check has failed)" + not_owner_title: "Secret Stuff" + not_owner: "You need to be the Owner of the bot in order to run this command." diff --git a/i18n/errors/es.yml b/i18n/errors/es.yml new file mode 100644 index 00000000..a07a74d1 --- /dev/null +++ b/i18n/errors/es.yml @@ -0,0 +1 @@ +es: diff --git a/i18n/errors/fr.yml b/i18n/errors/fr.yml new file mode 100644 index 00000000..04572e70 --- /dev/null +++ b/i18n/errors/fr.yml @@ -0,0 +1,13 @@ +fr: + warning: "Avertissement" + error_occured: "Une erreur est survenue" + bot_missing_perms: "J'ai besoin des permission(s) *%{perms}* pour exécuter cette commande." + user_missing_perms: "Tu as besoin des permission(s) *%{perms}* pour exécuter cette commande." + cooldown_title: "Cooldown" + cooldown: "Cette commande est en cours de rechargement, veuillez réessayer dans %{time}s." + no_pm_title: "Pas de support pour les messages privés" + no_pm: "Cette commande ne peut pas être utilisée dans les DMs." + check_failure_title: "Une vérification a échoué" + check_failure: "Vous n'avez pas la permission d'utiliser cette commande. (Une vérification a échoué)" + not_owner_title: "Truc secret" + not_owner: "Vous devez être le propriétaire du bot pour exécuter cette commande." diff --git a/i18n/errors/he.yml b/i18n/errors/he.yml new file mode 100644 index 00000000..af6fa60a --- /dev/null +++ b/i18n/errors/he.yml @@ -0,0 +1 @@ +he: diff --git a/i18n/errors/hr.yml b/i18n/errors/hr.yml new file mode 100644 index 00000000..f67f33c7 --- /dev/null +++ b/i18n/errors/hr.yml @@ -0,0 +1 @@ +hr: diff --git a/i18n/errors/id.yml b/i18n/errors/id.yml new file mode 100644 index 00000000..8446cbad --- /dev/null +++ b/i18n/errors/id.yml @@ -0,0 +1 @@ +id: diff --git a/i18n/errors/pl.yml b/i18n/errors/pl.yml new file mode 100644 index 00000000..a8e4dde7 --- /dev/null +++ b/i18n/errors/pl.yml @@ -0,0 +1 @@ +pl: diff --git a/i18n/errors/pt.yml b/i18n/errors/pt.yml new file mode 100644 index 00000000..9cbe1f03 --- /dev/null +++ b/i18n/errors/pt.yml @@ -0,0 +1 @@ +pt: diff --git a/i18n/errors/sv.yml b/i18n/errors/sv.yml new file mode 100644 index 00000000..7e73a972 --- /dev/null +++ b/i18n/errors/sv.yml @@ -0,0 +1 @@ +sv: diff --git a/i18n/info/de.yml b/i18n/info/de.yml new file mode 100644 index 00000000..bc0f8505 --- /dev/null +++ b/i18n/info/de.yml @@ -0,0 +1,40 @@ +de: + no_announcements: "Es gibt aktuell keine Ankündigungen!" + no_more_announcements: "Es gibt im Moment keine weiteren Ankündigungen, die angezeigt werden könnten!" + title: "Infos für %{name}" + infos_about: "Hier sind ein paar Informationen über %{name}" + general: "**Allgemein**" + username_discrim: "**Benutzername & Diskriminator**: %{name}" + no_flags: "Keine Flags" + created_on: "**Erstellt am**: %{date}" + date_format: "%d.%m.%Y" + avatar: "**Profilbild**: %{avatar}" + server: "**Server**" + joined_on: "**Beigetreten am**: %{date}" + top_role: "**Oberste Rolle**: %{role}" + hoist_role: "**Hoist Rolle**: %{role}" + roles: "**Rollen**: %{roles}" + no_roles: "*Keine Rollen*" + not_in_server: "*Dieser Betnutzer ist nicht auf diesem Server*" + #Flags + staff: "Discord-Mitarbeiter" + partner: "Eigentümer eines Partner-Servers" + bug_hunter: "Discord-Bugbuster" + hypesquad: "HypeSquad-Events" + hypesquad_bravery: "HypeSquad Bravery" + hypesquad_brilliance: "HypeSquad Brilliance" + hypesquad_balance: "HypeSquad Balance" + early_supporter: "Supporter der ersten Stunde" + team_user: "Team-Benutzer" + system: "System" + bug_hunter_level_2: "Discord-Bugbuster Level 2" + verified_bot: "Verifizierter Bot" + verified_bot_developer: "Verifizierter Bot-Entwickler" + early_verified_bot_developer: "Verifizierter Bot-Entwickler der ersten Stunde" + discord_certified_moderator: "Von Discord zertifizierter Moderator" + bot_http_interactions: "Webhook" + spammer: "Spammer" + #Badges + badge_not_found: "Abzeichen nicht gefunden 😢" + users_with_badge: "Benutzer mit dem Abzeichen (%{amount})" + more_badge_info: "*Um mehr Informationen über ein Badge zu erhalten, führe `/info badge` aus*\n\n" diff --git a/i18n/info/en.yml b/i18n/info/en.yml new file mode 100644 index 00000000..79ecaf70 --- /dev/null +++ b/i18n/info/en.yml @@ -0,0 +1,43 @@ +en: + no_announcements: "There are currently no announcements!" + no_more_announcements: "There are no more announcements to show!" + title: "Infos for %{name}" + infos_about: "Here are some informations about %{name}" + general: "**General**" + username_discrim: "**Username & discriminator**: %{name}" + no_flags: "No Flags" + created_on: "**Created on**: %{date}" + date_format: "%Y-%m-%d" + avatar: "**Avatar**: %{avatar}" + server: "**Server**" + joined_on: "**Joined on**: %{date}" + top_role: "**Top Role**: %{role}" + hoist_role: "**Hoist Role**: %{role}" + roles: "**Roles**: %{roles}" + no_roles: "*No roles*" + not_in_server: "*The user is not in this server*" + #Flags + staff: "Discord Staff" + partner: "Partnered Server Owner" + bug_hunter: "Discord Bug Hunter" + hypesquad: "HypeSquad Events" + hypesquad_bravery: "HypeSquad Bravery" + hypesquad_brilliance: "HypeSquad Brilliance" + hypesquad_balance: "HypeSquad Balance" + early_supporter: "Early Supporter" + team_user: "Team User" + system: "System" + bug_hunter_level_2: "Discord Bug Hunter Level 2" + verified_bot: "Verified Bot" + verified_bot_developer: "Verified Bot Developer" + early_verified_bot_developer: "Early Verified Bot Developer" + discord_certified_moderator: "Discord Certified Moderator" + bot_http_interactions: "Webhook" + spammer: "Spammer" + #Badges + badge_not_found: "Badge not found 😢" + users_with_badge: "Users with the badge (%{amount})" + more_badge_info: "*To get more information about a badge run `/info badge`*\n\n" + no_content: "This message is empty." + embed: "Embed %{count}:" + message: "Message:" diff --git a/i18n/info/es.yml b/i18n/info/es.yml new file mode 100644 index 00000000..af7fc2d7 --- /dev/null +++ b/i18n/info/es.yml @@ -0,0 +1,38 @@ +es: + title: "Información para %{name}" + infos_about: "Aquí hay algunas informaciones sobre %{name}" + general: "**General**" + username_discrim: "**Usuario y discriminador**: %{name}" + no_flags: "Sin flags" + created_on: "**Creado el**: %{date}" + date_format: "%d-%m-%Y" + avatar: "**Avatar**: %{avatar}" + server: "**Servidor**" + joined_on: "**Se unió el**: %{date}" + top_role: "**Top Rol**: %{role}" + hoist_role: "**Rol de Carga**: %{role}" + roles: "**Roles**: %{roles}" + no_roles: "*Sin roles*" + not_in_server: "*El usuario no esta en este servidor*" + #Flags + staff: "Personal de Discord" + partner: "Propietario de Servidor Asociado" + bug_hunter: "Cazador de bugs de Discord" + hypesquad: "Eventos de HypeSquad" + hypesquad_bravery: "Bravery del HypeSquad" + hypesquad_brilliance: "Brilliance del HypeSquad" + hypesquad_balance: "Balance del HypeSquad" + early_supporter: "Ayudante Anticipado" + team_user: "Equipo Usuario" + system: "Sistema" + bug_hunter_level_2: "Cazador de bugs de Discord Nivel 2" + verified_bot: "Bot verificado" + verified_bot_developer: "Desarrollador de bots verificado" + early_verified_bot_developer: "Desarrollador de bots verificado temprano" + discord_certified_moderator: "Moderador Certificado de Discord" + bot_http_interactions: "Webhook" + spammer: "Spammer" + #Badges + badge_not_found: "Insignia no encontrada 😢" + users_with_badge: "Usuarios con la insignia (%{amount})" + more_badge_info: "*Para obtener más información sobre una insignia ejecute `/info badge`*\n\n" diff --git a/i18n/info/fr.yml b/i18n/info/fr.yml new file mode 100644 index 00000000..be1b33cc --- /dev/null +++ b/i18n/info/fr.yml @@ -0,0 +1,35 @@ +fr: + title: "Informations pour %{name}" + infos_about: "Voici quelques informations sur %{name}" + general: "**Général**" + username_discrim: "**Nom d'utilisateur et discriminateur** : %{name}" + created_on: "**Créé le** : %{date}" + date_format: "%d-%m-%Y" + avatar: "**Photo de profil** : %{avatar}" + server: "**Serveur**" + joined_on: "**Rejoint le**: %{date}" + top_role: "**Rôle principal**: %{role}" + hoist_role: "**Rôle hôte**: %{role}" + roles: "**Rôles** : %{roles}" + not_in_server: "Cet utilisateur n'est pas dans le serveur" + #Flags + staff: "Équipe Discord" + partner: "Propriétaire d'un serveur partenaire" + bug_hunter: "Chasseur de bugs Discord" + hypesquad: "Événements de la HypeSquad" + hypesquad_bravery: "HypeSquad de Bravoure" + hypesquad_brilliance: "Éclat de la HypeSquad" + hypesquad_balance: "Équilibre d'HypeSquad" + early_supporter: "Soutien précoce" + team_user: "Utilisateurs de l'équipe" + system: "Système" + bug_hunter_level_2: "Discord Bug Hunter Niveau 2" + verified_bot: "Bot vérifié" + verified_bot_developer: "Développeur de Bot vérifié" + discord_certified_moderator: "Modérateur certifié de Discord" + bot_http_interactions: "Webhook" + spammer: "Spammeur" + #Badges + badge_not_found: "Badge introuvable 😢" + users_with_badge: "Utilisateurs avec le badge (%{amount})" + more_badge_info: "*Pour obtenir plus d'informations sur un badge, exécutez `/info badge`*\n\n" diff --git a/i18n/info/he.yml b/i18n/info/he.yml new file mode 100644 index 00000000..74990a66 --- /dev/null +++ b/i18n/info/he.yml @@ -0,0 +1,12 @@ +he: + title: "מידע בשביל %{name}" + infos_about: "הנה קצת מידע על %{name}" + general: "**מידע גנרלי**" + username_discrim: "**שם משתמש ו מאבחן**: %{name}" + no_flags: "אין תגי דיסקורד" + created_on: "**נוצר ב-**: %{date}" + date_format: "Y/%m/%d%" + avatar: "**תמונת פרופיל**: %{avatar}" + server: "**שרת**" + joined_on: "**הצתרף ב-**: %{date}" + top_role: "**תג העליון**: %{role}" diff --git a/i18n/info/hr.yml b/i18n/info/hr.yml new file mode 100644 index 00000000..36c90eac --- /dev/null +++ b/i18n/info/hr.yml @@ -0,0 +1,30 @@ +hr: + title: "Informacije o %{name}" + infos_about: "Ovdje su neke informacije o %{name}" + general: "**Općenito**" + username_discrim: "**Korisničko ime i diskriminator**: %{name}" + no_flags: "Bez oznaka" + created_on: "**Stvoreno**: %{date}" + date_format: "%d.%m.%Y" + server: "**Poslužitelj**" + joined_on: "**Pridružio se**: %{date}" + not_in_server: "*Korisnik nije u ovom serveru*" + #Flags + staff: "Discord osoblje" + partner: "Vlasnik partnerskog servera" + hypesquad: "HypeSquad događanja" + hypesquad_bravery: "HypeSquad kuća Hrabrosti" + hypesquad_brilliance: "HypeSquad kuća Brilijantnosti" + hypesquad_balance: "HypeSquad kuća Ravnoteže" + early_supporter: "Rana podrška" + team_user: "Korisnik tima" + system: "Sustav" + bug_hunter_level_2: "Discord Bug Hunter 2. razine" + verified_bot: "Verificirani bot" + verified_bot_developer: "Verificirani bot developer" + early_verified_bot_developer: "Rani verificirani bot developer" + discord_certified_moderator: "Moderator s Discordovim odobrenjem" + spammer: "Spamer" + #Badges + badge_not_found: "Značka nije pronađena 😢" + users_with_badge: "Korisnici sa značkom (%{amount})" diff --git a/i18n/info/id.yml b/i18n/info/id.yml new file mode 100644 index 00000000..941e4720 --- /dev/null +++ b/i18n/info/id.yml @@ -0,0 +1,38 @@ +id: + title: "Informasi untuk %{name}" + infos_about: "Ini beberapa informasi tentang %{name}" + general: "**Umum**" + username_discrim: "**Nama Pengguna & Pembeda**: %{name}" + no_flags: "Tidak ada Flags" + created_on: "**Dibuat pada**: %{date}" + date_format: "%d-%m-%Y" + avatar: "**Avatar**: %{avatar}" + server: "**Server**" + joined_on: "**Join pada**: %{date}" + top_role: "**Role Teratas**: %{role}" + hoist_role: "**Role Angkat**: %{role}" + roles: "**Roles**: %{roles}" + no_roles: "*Tidak ada roles*" + not_in_server: "*User-nya tidak ada di server ini*" + #Flags + staff: "Staf Discord" + partner: "Pemilik Server yang Berpartner" + bug_hunter: "Discord Bug Hunter" + hypesquad: "Event HypeSquad" + hypesquad_bravery: "HypeSquad Bravery" + hypesquad_brilliance: "HypeSquad Brilliance" + hypesquad_balance: "HypeSquad Balance" + early_supporter: "Early Supporter" + team_user: "Team User" + system: "Sistem" + bug_hunter_level_2: "Discord Bug Hunter Level 2" + verified_bot: "Verified Bot" + verified_bot_developer: "Verified Bot Developer" + early_verified_bot_developer: "Early Verified Bot Developer" + discord_certified_moderator: "Moderator Discord bersertifikat" + bot_http_interactions: "Webhook" + spammer: "Penyepam" + #Badges + badge_not_found: "Badge tidak ditemukan 😢" + users_with_badge: "Users dengan badge (%{amount})" + more_badge_info: "*Untuk mendapatkan informasi lebih lanjut tentang badge, run `/info badge`*\n\n" diff --git a/i18n/info/pl.yml b/i18n/info/pl.yml new file mode 100644 index 00000000..32d3577c --- /dev/null +++ b/i18n/info/pl.yml @@ -0,0 +1,23 @@ +pl: + title: "Informacje o %{name}" + infos_about: "Kilka informacji o %{name}" + general: "**Ogólne**" + username_discrim: "**Nazwa użytkownika i tag**:%{name}" + created_on: "**Utworzono dnia**: %{date}" + date_format: "%d.%m.%Y" + server: "**Serwer**" + #Flags + staff: "Personel Discorda" + partner: "Właściciel serwera partnerskiego" + bug_hunter: "Łowca Bugów Discord" + hypesquad: "Wydarzenia HypeSquadu" + hypesquad_bravery: "Dom Bravery HypeSquadu" + hypesquad_brilliance: "Dom Brilliance HypeSquadu" + hypesquad_balance: "Dom Balance HypeSquadu" + early_supporter: "Wczesny Wspierający" + team_user: "Użytkownik Drużyny" + bug_hunter_level_2: "Łowca Bugów Discorda poziom 2" + verified_bot: "Zweryfikowany Bot" + verified_bot_developer: "Zweryfikowany deweloper bota" + early_verified_bot_developer: "Wcześnie zweryfikowany deweloper bota" + discord_certified_moderator: "Certyfikowany moderator Discorda" diff --git a/i18n/info/pt.yml b/i18n/info/pt.yml new file mode 100644 index 00000000..04fdfc55 --- /dev/null +++ b/i18n/info/pt.yml @@ -0,0 +1,19 @@ +pt: + title: "Informações para %{name}" + infos_about: "Aqui estão algumas informações sobre %{name}" + general: "**Geral**" + username_discrim: "**Username e Tag**: %{name}" + created_on: "**Conta criada**: %{date}" + date_format: "%d/%m/%Y" + server: "**Servidor**" + #Flags + staff: "Staff do Discord" + partner: "Proprietário de algum servidor parceiro" + hypesquad: "Eventos do HypeSquad" + early_supporter: "Primeiro apoiador Nitro" + system: "Sistema" + bug_hunter_level_2: "Discord Bug Hunter nível 2" + verified_bot: "Bot verificado" + verified_bot_developer: "Desenvolvedor de bots verificado" + early_verified_bot_developer: "Desenvolvedor de bots verificado antecipadamente" + discord_certified_moderator: "Moderador do Discord certificado" diff --git a/i18n/info/sv.yml b/i18n/info/sv.yml new file mode 100644 index 00000000..ae1ab65e --- /dev/null +++ b/i18n/info/sv.yml @@ -0,0 +1,30 @@ +sv: + title: "Informationer för %{name}" + infos_about: "Här är lite information om %{name}" + general: "**Allmänt**" + username_discrim: "**Användarnamn & diskriminator**: %{name}" + no_flags: "Inga flaggor" + created_on: "**Skapades på**: %{date}" + date_format: "%Y/%m/%d" + joined_on: "**Gick med**: %{date}" + top_role: "**Högsta roll**: %{role}" + hoist_role: "**Hoist roll**: %{role}" + roles: "**Roller**: %{roles}" + not_in_server: "*Användaren är inte i denna server*" + #Flags + staff: "Discord-personal" + partner: "Partnerad Serverägare" + bug_hunter: "Discords felsökare" + hypesquad: "HypeSquad-händelser" + hypesquad_balance: "HypeSquad Brilliance" + early_supporter: "Tidig Supporter" + team_user: "Laganvändare" + bug_hunter_level_2: "Discords feljägare Nivå 2" + verified_bot: "Verifierad bot" + verified_bot_developer: "Verifierad Bot-utvecklare" + early_verified_bot_developer: "Tidig verifierad bot-utvecklare" + discord_certified_moderator: "Discord-certifierad moderator" + spammer: "Spammare" + #Badges + badge_not_found: "Märket hittades inte 😢" + users_with_badge: "Användare med märket (%{amount})" diff --git a/i18n/misc/de.yml b/i18n/misc/de.yml new file mode 100644 index 00000000..c4b375f5 --- /dev/null +++ b/i18n/misc/de.yml @@ -0,0 +1,27 @@ +de: + translation: "Übersetzung" + source: "**Originalsprache**: %{source}" + target: "**Zielsprache**: %{target}" + added_emoji: "`%{emoji}` wurde erfolgreich hinzugefügt" + deleted_emoji: "`%{emoji}` wurde erfolgreich entfernt" + deleted_emoji_log: "Entfernt von %{user}" + not_emoji: | + Du musst eine gültige Emoji-ID von [emoji.gg]() eingeben (z.B. `5345-blobcatblush`) *ODER* eine gültige URL, die auf ein Bild verweist. + + Bitte beachte, dass wir nur folgende Quellen unterstützen: + [cdn.betterttv.net]() + [cdn.discordapp.com]() + [emoji.gg]() + [i.imgur.com]() + [raw.githubusercontent.com]() + not_id: "Das ist keine gültige Emoji-ID" + too_big: "Die Größe des Emojis ist zu groß. Wir haben versucht, es zu komprimieren, aber es hat nicht funktioniert. Bitte versuche es erneut mit einer kleineren Größe." + emoji_not_found: "Emoji nicht gefunden 😢" + lgbtq_tip: "Tipp: Du kannst das Bild unten perfekt als Profilbild nutzen." + no_gif_support: "*Wir unterstützen derzeit keine animierten Avatare :c*" + jail_desc_1: "%{user} wird lebenslänglich im Gefängnis sein." + jail_desc_2: "%{user} war gemein zu %{officer}. %{officer} ist jedoch ein Polizist." + jail_desc_3: "%{user} wird das hoffentlich nicht nochmal tun." + jail_desc_4: "Vielleicht wurde %{user} doch nicht verhaftet..." + jail_desc_5: "%{officer} hat %{user} zum 1000sten Mal verhaftet 🎉 [Klicke auf diesen Link](https://tako.rocks), um einen Preis zu erhalten!" + meme_share: "%{user} hält das für lustig. Auch wenn es nicht so ist, bitte sag es ihnen nicht. Er/Sie sind sehr sensibel." diff --git a/i18n/misc/en.yml b/i18n/misc/en.yml new file mode 100644 index 00000000..f2eeb17a --- /dev/null +++ b/i18n/misc/en.yml @@ -0,0 +1,30 @@ +en: + translation: "Translation" + source: "**Source**: %{source}" + target: "**Target**: %{target}" + added_emoji: "Successfully added `%{emoji}`" + deleted_emoji: "Successfully removed `%{emoji}`" + deleted_emoji_log: "Removed by %{user}" + not_emoji: | + You need to pass in a valid emoji ID from [emoji.gg]() (for example: `5345-blobcatblush`) *OR* an valid url pointing to an image. + + Please note that we only support the following sources: + [cdn.betterttv.net]() + [cdn.discordapp.com]() + [emoji.gg]() + [i.imgur.com]() + [raw.githubusercontent.com]() + not_id: "This is not a valid emoji ID" + too_big: "The size of the emoji is too big. We tried to compress it but it didn't work. Please try again with a lower size." + emoji_not_found: "Emoji not found 😢" + lgbtq_tip: "Tip: You can use the image below perfectly as a profile picture." + no_gif_support: "*We currently do not support animated avatars :c*" + jail_desc_1: "%{user} will be in prison for lifetime." + jail_desc_2: "%{user} was mean to %{officer}. %{officer} however is a police officer." + jail_desc_3: "%{user} hopefully won't do that again." + jail_desc_4: "Maybe %{user} didn't got arrested..." + jail_desc_5: "%{officer} arrested %{user} for the 1000th time 🎉 [Click this link](https://tako.rocks) to get a prize!" + meme_share: "%{user} thinks that's funny. Even if it's not please don't tell them. They are very sensitive." + reaction_translate_activated: "✅ Reaction Translate is now activated" + reaction_translate_deactivated: "❌ Reaction Translate is now deactivated" + reaction_translate_footer: "Requested by %{user}" diff --git a/i18n/misc/es.yml b/i18n/misc/es.yml new file mode 100644 index 00000000..6c399abc --- /dev/null +++ b/i18n/misc/es.yml @@ -0,0 +1,26 @@ +es: + translation: "Traducción" + source: "**Origen**: %{source}" + target: "**Objetivo**: %{target}" + added_emoji: "Añadido con éxito `%{emoji}`" + deleted_emoji: "Eliminado con éxito `%{emoji}`" + deleted_emoji_log: "Eliminado por %{user}" + not_emoji: | + Necesitas poner una ID válida de emoji.gg [emoji.gg]() (for example: `5345-blobcatblush`) *O* una URL válida a una imagen. + + Por favor ten en cuenta que sólo usamos estas fuentes: + [cdn.betterttv.net]() + [cdn.discordapp.com]() + [emoji.gg]() + [i.imgur.com]() + [raw.githubusercontent.com]() + not_id: "Eso no es una ID de un emoji válido" + too_big: "El tamaño del emoji es demasiado grande. Intentamos comprimirlo pero no funcionó. Por favor, inténtalo de nuevo con un tamaño inferior." + emoji_not_found: "Emoji no encontrado 😢" + lgbtq_tip: "Sugerencia: Puedes usar la imagen de abajo a la perfección como una foto de perfil." + no_gif_support: "*Actualmente no soportamos avatares animados :c*" + jail_desc_1: "%{user} estará en prisión por vida." + jail_desc_2: "%{user} significaba %{officer}. %{officer} sin embargo es un agente de policía." + jail_desc_3: "%{user} esperemos que no vuelva a hacer eso." + jail_desc_4: "Tal vez %{user} no se ha detectado..." + jail_desc_5: "%{officer} arrestó a %{user} por 1000a vez 🎉 [Haz clic en este enlace](https://tako.rocks) para obtener un premio!" diff --git a/i18n/misc/fr.yml b/i18n/misc/fr.yml new file mode 100644 index 00000000..1831d439 --- /dev/null +++ b/i18n/misc/fr.yml @@ -0,0 +1 @@ +fr: diff --git a/i18n/misc/he.yml b/i18n/misc/he.yml new file mode 100644 index 00000000..af6fa60a --- /dev/null +++ b/i18n/misc/he.yml @@ -0,0 +1 @@ +he: diff --git a/i18n/misc/hr.yml b/i18n/misc/hr.yml new file mode 100644 index 00000000..f20d91e5 --- /dev/null +++ b/i18n/misc/hr.yml @@ -0,0 +1,19 @@ +hr: + translation: "Prijevod" + source: "**Izvor**: %{source}" + target: "**Cilj**: %{target}" + added_emoji: "Uspješno dodan `%{emoji}`" + deleted_emoji: "Uspješno uklonjen `%{emoji}`" + deleted_emoji_log: "Uklonjeno od %{user}" + not_emoji: | + Morate proslijediti važeći ID emojija iz [emoji.gg]() (na primjer: `5345-blobcatblush`) *ILI* važeći url koji upućuje na sliku. + + Napominjemo da podržavamo samo sljedeće izvore: + [cdn.betterttv.net]() + [cdn.discordapp.com]() + [emoji.gg]() + [i.imgur.com]() + [raw.githubusercontent.com]() + not_id: "Ovo nije ispravan emoji ID" + too_big: "Veličina emojija je prevelika. Pokušali smo ga komprimirati, ali nije uspijelo. Pokušajte ponovno s manjom veličinom." + emoji_not_found: "Emotikon nije pronađen 😢" diff --git a/i18n/misc/id.yml b/i18n/misc/id.yml new file mode 100644 index 00000000..bd17059e --- /dev/null +++ b/i18n/misc/id.yml @@ -0,0 +1,26 @@ +id: + translation: "Terjemahan" + source: "**Sumber**: %{source}" + target: "**Target**: %{target}" + added_emoji: "Berhasil menambahkan `%{emoji}`" + deleted_emoji: "Berhasil menghapus `%{emoji}`" + deleted_emoji_log: "Dihilangkan oleh %{user}" + not_emoji: | + Kamu harus meggunakan ID emoji yang valid dari [emoji.gg]() (Contoh: `5345-blobcatblush`) *ATAU* url gambar yang valid. + + Harap catat, kami hanya men-support dari: + [cdn.betterttv.net]() + [cdn.discordapp.com]() + [emoji.gg]() + [i.imgur.com]() + [raw.githubusercontent.com]() + not_id: "Ini bukan ID emoji yang valid" + too_big: "Size emoji terlalu besar. Kami mencoba meng-kompres nya, tapi tidak bisa. Harap coba lagi dengan size yang lebih kecil." + emoji_not_found: "Emoji tidak ditemukan 😢" + lgbtq_tip: "Tip: Kamu bisa menggunakan foto ini sebagai foto profil secara sempurna." + no_gif_support: "*Kami saat ini tidak mensupport avatar yang beranimasi :c*" + jail_desc_1: "%{user} akan di penjara selamanya." + jail_desc_2: "%{user} bermaksud untuk %{officer}. %{officer} bagaimanapun adalah seorang perwira polisi." + jail_desc_3: "Semoga %{user} tidak melakukannya lagi." + jail_desc_4: "Mungkin %{user} tidak akan ditahan..." + jail_desc_5: "%{officer} menghukum %{user} yang ke 1000 kalinya 🎉 [Click link ini](https://tako.rocks) untuk mendapatkan hadiah!" diff --git a/i18n/misc/pl.yml b/i18n/misc/pl.yml new file mode 100644 index 00000000..a8e4dde7 --- /dev/null +++ b/i18n/misc/pl.yml @@ -0,0 +1 @@ +pl: diff --git a/i18n/misc/pt.yml b/i18n/misc/pt.yml new file mode 100644 index 00000000..7972fb25 --- /dev/null +++ b/i18n/misc/pt.yml @@ -0,0 +1,9 @@ +pt: + translation: "Tradução" + source: "**Fonte**: %{source}" + target: "**Alvo**: %{target}" + added_emoji: "Emoji `%{emoji}` adicionado" + deleted_emoji: "Emoji `%{emoji}` removido" + deleted_emoji_log: "Removido por %{user}" + not_id: "Esse não é um ID de um emoji válido" + too_big: "O tamanho do emoji é muito grande. Nós tentamos comprimi-lo, mas não funcionou. Por favor, tente novamente com um tamanho menor." diff --git a/i18n/misc/sv.yml b/i18n/misc/sv.yml new file mode 100644 index 00000000..ff574ab7 --- /dev/null +++ b/i18n/misc/sv.yml @@ -0,0 +1,9 @@ +sv: + translation: "Översättning" + source: "**Källa**: %{source}" + target: "**Mål**: %{target}" + added_emoji: "Lyckades lägga till `%{emoji}`" + deleted_emoji: "Lyckades ta bort `%{emoji}`" + deleted_emoji_log: "Borttaget av %{user}" + not_id: "Detta emoji-ID är ogiltigt" + too_big: "Storleken på emojin är för stor. Vi försökte komprimera den men den fungerade inte. Försök igen med en mindre storlek." diff --git a/i18n/moderation/de.yml b/i18n/moderation/de.yml new file mode 100644 index 00000000..c3aa9834 --- /dev/null +++ b/i18n/moderation/de.yml @@ -0,0 +1,9 @@ +de: + malicious_link: "%{user} hat versucht, einen bösartigen Link zu senden. Ich habe den Link gelöscht!" + ban_game_log: "Gebannt fürs %{game} spielen." + ban_game_dm: "Du wurdest von *%{guild}* gebannt, weil du %{game} gespielt hast." + clear_reason: "Gelöscht von %{user} mithilfe des clear Befehls." + cleared: "%{amount} Nachricht(en) in %{channel} gelöscht." + cleared_target: "%{amount} Nachricht(en) von %{target} in %{channel} gelöscht." + too_many_messages_title: "Bist du verrückt?!" + too_many_messages: "Du kannst nur 100 Nachrichten gleichzeitig löschen. Wir haben die Anzahl automatisch für dich angepasst." diff --git a/i18n/moderation/en.yml b/i18n/moderation/en.yml new file mode 100644 index 00000000..66f0ca4d --- /dev/null +++ b/i18n/moderation/en.yml @@ -0,0 +1,9 @@ +en: + malicious_link: "%{user} tried to send a malicious link. I deleted the link!" + ban_game_log: "Banned for playing %{game}." + ban_game_dm: "You have been banned from *%{guild}* because you played %{game}." + clear_reason: "Deleted by %{user} using the clear command." + cleared: "Cleared %{amount} message(s) in %{channel}." + cleared_target: "Cleared %{amount} message(s) from %{target} in %{channel}." + too_many_messages_title: "Are you crazy?!" + too_many_messages: "You can only delete 100 messages at a time. We automatically adjusted the amount for you." diff --git a/i18n/moderation/es.yml b/i18n/moderation/es.yml new file mode 100644 index 00000000..4ca9d5fc --- /dev/null +++ b/i18n/moderation/es.yml @@ -0,0 +1,2 @@ +es: + malicious_link: "%{user} intentó enviar un enlace malicioso. ¡He eliminado el enlace!" diff --git a/i18n/moderation/fr.yml b/i18n/moderation/fr.yml new file mode 100644 index 00000000..1831d439 --- /dev/null +++ b/i18n/moderation/fr.yml @@ -0,0 +1 @@ +fr: diff --git a/i18n/moderation/he.yml b/i18n/moderation/he.yml new file mode 100644 index 00000000..af6fa60a --- /dev/null +++ b/i18n/moderation/he.yml @@ -0,0 +1 @@ +he: diff --git a/i18n/moderation/hr.yml b/i18n/moderation/hr.yml new file mode 100644 index 00000000..9150c719 --- /dev/null +++ b/i18n/moderation/hr.yml @@ -0,0 +1,2 @@ +hr: + malicious_link: "%{user} je pokušao poslati zlonamjeran link. Izbrisao sam ga!" diff --git a/i18n/moderation/id.yml b/i18n/moderation/id.yml new file mode 100644 index 00000000..c2747810 --- /dev/null +++ b/i18n/moderation/id.yml @@ -0,0 +1,3 @@ +id: + malicious_link: "%{user} mencoba mengirim link jahat. Aku sudah menghapus link nya!" + ban_game_dm: "Kamu telah di ban dari *%{guild}* karena kamu bermain %{game}." diff --git a/i18n/moderation/pl.yml b/i18n/moderation/pl.yml new file mode 100644 index 00000000..a8e4dde7 --- /dev/null +++ b/i18n/moderation/pl.yml @@ -0,0 +1 @@ +pl: diff --git a/i18n/moderation/pt.yml b/i18n/moderation/pt.yml new file mode 100644 index 00000000..8eab03f2 --- /dev/null +++ b/i18n/moderation/pt.yml @@ -0,0 +1,2 @@ +pt: + malicious_link: "%{user} tentou enviar um link malicioso. Eu excluímos o link!" diff --git a/i18n/moderation/sv.yml b/i18n/moderation/sv.yml new file mode 100644 index 00000000..bf97dc91 --- /dev/null +++ b/i18n/moderation/sv.yml @@ -0,0 +1,2 @@ +sv: + malicious_link: "%{user} försökte skicka en skadlig länk. Jag raderade länken!" diff --git a/i18n/owner/de.yml b/i18n/owner/de.yml new file mode 100644 index 00000000..f59092bd --- /dev/null +++ b/i18n/owner/de.yml @@ -0,0 +1,3 @@ +de: + success: "Erfolgreich!" + announcement_not_found: "Konnte die Ankündigung, die du suchst, nicht finden :c" diff --git a/i18n/owner/en.yml b/i18n/owner/en.yml new file mode 100644 index 00000000..76acbdcf --- /dev/null +++ b/i18n/owner/en.yml @@ -0,0 +1,3 @@ +en: + success: "Success!" + announcement_not_found: "Could not find the announcement you were looking for :c" diff --git a/i18n/owner/es.yml b/i18n/owner/es.yml new file mode 100644 index 00000000..a07a74d1 --- /dev/null +++ b/i18n/owner/es.yml @@ -0,0 +1 @@ +es: diff --git a/i18n/owner/fr.yml b/i18n/owner/fr.yml new file mode 100644 index 00000000..1831d439 --- /dev/null +++ b/i18n/owner/fr.yml @@ -0,0 +1 @@ +fr: diff --git a/i18n/owner/he.yml b/i18n/owner/he.yml new file mode 100644 index 00000000..af6fa60a --- /dev/null +++ b/i18n/owner/he.yml @@ -0,0 +1 @@ +he: diff --git a/i18n/owner/hr.yml b/i18n/owner/hr.yml new file mode 100644 index 00000000..f67f33c7 --- /dev/null +++ b/i18n/owner/hr.yml @@ -0,0 +1 @@ +hr: diff --git a/i18n/owner/id.yml b/i18n/owner/id.yml new file mode 100644 index 00000000..8446cbad --- /dev/null +++ b/i18n/owner/id.yml @@ -0,0 +1 @@ +id: diff --git a/i18n/owner/pl.yml b/i18n/owner/pl.yml new file mode 100644 index 00000000..a8e4dde7 --- /dev/null +++ b/i18n/owner/pl.yml @@ -0,0 +1 @@ +pl: diff --git a/i18n/owner/pt.yml b/i18n/owner/pt.yml new file mode 100644 index 00000000..9cbe1f03 --- /dev/null +++ b/i18n/owner/pt.yml @@ -0,0 +1 @@ +pt: diff --git a/i18n/owner/sv.yml b/i18n/owner/sv.yml new file mode 100644 index 00000000..7e73a972 --- /dev/null +++ b/i18n/owner/sv.yml @@ -0,0 +1 @@ +sv: diff --git a/main.py b/main.py new file mode 100644 index 00000000..68ae20ff --- /dev/null +++ b/main.py @@ -0,0 +1,49 @@ +import json +import discord +import asyncio +import logging +import asyncpg +import pyyoutube +import bot_secrets +import tmdbsimple as tmdb +from TakoBot import TakoBot +from utils import clear_console + +youtube_api = pyyoutube.Api(api_key=bot_secrets.YOUTUBE_API_KEY) +tmdb.API_KEY = bot_secrets.TMDB_API_KEY + + +async def main(): + clear_console() + logger = logging.getLogger("discord") + logger.setLevel(logging.INFO) + handler = logging.FileHandler(filename="discord.log", encoding="utf-8", mode="w") + formatter = logging.Formatter( + "{asctime} | {levelname: <8} | {module}:{funcName}:{lineno} - {message}", + style="{", + ) + handler.setFormatter(formatter) + logger.addHandler(handler) + intents = discord.Intents.default() + intents.message_content = True + intents.guilds = True + intents.members = True + intents.presences = True + intents.reactions = True + + bot: TakoBot = TakoBot(command_prefix="tk!", intents=intents) + bot.db_pool: asyncpg.Pool = await asyncpg.create_pool( + database=bot_secrets.DB_NAME, + host=bot_secrets.DB_HOST, + port=bot_secrets.DB_PORT if hasattr(bot_secrets, "DB_PORT") else 5432, + user=bot_secrets.DB_USER, + password=bot_secrets.DB_PASSWORD, + ) + with open(".gitmoji-changelogrc", "r") as f: + bot.version = json.load(f)["project"]["version"] + + await bot.start(bot_secrets.TOKEN) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..2f0d7278 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +git+https://github.com/kayano-bot/python-i18n.git +pick +black +psutil +pillow +pyyaml +millify +asyncpg +requests +discord.py==2.1.0 +py-cpuinfo +tmdbsimple +discord-rpc +python-youtube +BeautifulSoup4 diff --git a/test.py b/test.py new file mode 100644 index 00000000..9811c1c3 --- /dev/null +++ b/test.py @@ -0,0 +1,5 @@ +import requests + +requests.get( + "https://up.pukima.site/api/push/lJ3fB5lojy?status=up&msg=OK&ping=" + str(100) +) diff --git a/utils.py b/utils.py new file mode 100644 index 00000000..231abe25 --- /dev/null +++ b/utils.py @@ -0,0 +1,351 @@ +import os +import i18n +import config +import discord +import asyncpg +import aiohttp +from bs4 import BeautifulSoup +from datetime import datetime +from discord import app_commands +from PIL import Image, ImageColor + + +def clear_console(): + """Clears the console (Supported: Windows & Unix)""" + command = "clear" + if os.name in ("nt", "dos"): + command = "cls" + return os.system(command) + + +def format_bytes(size: int): + """:class:`str`: Returns given bytes into human readable text. + Supported Range: Bytes -> TB + + Parameters + ----------- + size: :class:`int` + Size in bytes to convert + """ + power = 2**10 + n = 0 + power_labels = ["Bytes", "KB", "MB", "GB", "TB"] + while size > power and n != 4: + size /= power + n += 1 + return str(round(size)) + power_labels[n] + + +async def get_color(bot, guild_id: int, integer: bool = True): + """:class:`str` or :class:`int`: Returns the set color of a guild (Default: config.DEFAULT_COLOR) + + Parameters + ----------- + guild_id: :class:`int` + The id of the guild you want the color from + integer: :class:`bool` + Whetever you want to get the color as an integer or as a string + """ + color = None + try: + color = await bot.db_pool.fetchval( + "SELECT color FROM guilds WHERE guild_id = $1", guild_id + ) + except: + pass + if integer: + return int(color, 16) if color is not None else config.DEFAULT_COLOR + return color if color is not None else config.DEFAULT_COLOR_STR + + +def color_check(color: str): + """:class:`bool`: Checks if black or white has more contrast to the input color. + + Parameters + ----------- + color: :class:`str` + The color to check (#HEXCOLOR) + + Returns + -------- + :class:`bool` + True = White has more contrast + + False = Black has more contrast + """ + value = True + rgb = ImageColor.getcolor(color, "RGB") + value = True if (rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114) < 143 else False + return value + + +async def thumbnail(id: int, icon_name: str, bot): + """:class:`str`: Creates a thumbnail and returns the filepath of the image. + Parameters + ----------- + id: :class:`int` + The id of the guild to create the thumbnail for. + icon_name: :class:`str` + The name of the icon used for the thumbnail. Needs to be in the assets directory and have a `name.png` and `name_dark.png` variant. + """ + color = await get_color(bot, id, False) + img = Image.new("RGB", (512, 512), color=color.replace("0x", "#")) + icon = Image.open( + f"assets/{icon_name}{'' if color_check(color.replace('0x', '#')) or icon_name is 'reddit' else '_dark'}.png" + ) + img.paste(icon, (56, 56), mask=icon) + img.save(f"assets/thumbnails/{icon_name}_{id}.png") + return f"assets/thumbnails/{icon_name}_{id}.png" + + +def delete_thumbnail(id: int, icon: str): + """Deletes a thumbnail (mostly created with the thumbnail() function). + + Parameters + ----------- + id: :class:`int` + The id of the guild the thumbnail should be deleted for. + icon: :class:`str` + The name of the icon that was used for the thumbnail creation. Needs to be in the assets directory and have a `name.png` and `name_dark.png` variant. + """ + if config.DELETE_THUMBNAILS == False: + return + if os.path.exists(f"assets/thumbnails/{icon}_{id}.png"): + os.remove(f"assets/thumbnails/{icon}_{id}.png") + + +def get_language(bot, guild_id: int | None = None): + """:class:`str`: Get the language of a guild. + + Parameters + ----------- + bot: :class:`TakoBot` + guild_id: :class:`int` + The id of the guild to get the language from. + """ + language = "en" + for guild in bot.postgre_guilds: + if guild["guild_id"] == guild_id: + language = guild["language"] + return language + + +def number_of_pages_needed(elements_per_page: int, total_elements: int): + """:class:`int`: Returns the number of pages needed if elements per page are limited. + + Parameters + ----------- + elements_per_page: :class:`int` + The maximum amount of elements per page. + total_element: :class:`int` + The total elements. + """ + q, r = divmod(total_elements, elements_per_page) + if r > 0: + return q + 1 + return q + + +async def create_user( + pool: asyncpg.Pool | asyncpg.Connection, + user: discord.User, + wallet: int = config.DEFAULT_WALLET, + bank: int = config.DEFAULT_BANK, +): + """ + Creates an user in the database. + + Parameters + ----------- + pool: :class:`asyncpg.Pool` + The PostgreSQL pool to use. + user: :class:`discord.User` + The user to create a row for. + wallet: :class:`int` + The amount of money the user has in it's wallet. (Default: config.DEFAULT_WALLET) + bank: :class:`int` + The amount of money the user has in it's bank. (Default: config.DEFAULT_BANK) + """ + await pool.execute( + "INSERT INTO users (user_id, wallet, bank) VALUES ($1, $2, $3)", + user.id, + wallet, + bank, + ) + + +def add_extension(url: str): + """Add an extension from git to the bot. + + Parameters + ----------- + url: :class:`str` + The url pointing to the git repository the extension is located at. + """ + valid_url = os.system(f"git ls-remote {url} > /dev/null 2>&1") + if valid_url != 0: + return 1 + cloning = os.system( + f"cd extensions > /dev/null && git submodule add {url} > /dev/null 2>&1" + ) + if cloning != 0: + return 2 + return 0 + + +async def fetch_cash(pool: asyncpg.Pool | asyncpg.Connection, user: discord.User): + """list[`int`, `int`]: Returns the amount of money a user has in a list where the first value is the money + in the wallet and the second value is the money in the bank of the user. It will also create a user if it doesn't exist yet. + + Parameters + ----------- + pool: :class:`asyncpg.Pool` | :class:`asyncpg.Connection` + The PostgreSQL pool to use. Generally `bot.db_pool`. + user: :class:`discord.User` + The user to get the current balance from.""" + data = await pool.fetchrow( + "SELECT wallet, bank FROM users WHERE user_id = $1;", user.id + ) + if not data: + await create_user(pool, user) + return list(data) if data else [config.DEFAULT_WALLET, config.DEFAULT_BANK] + + +async def balance_embed( + bot, user: discord.User | discord.Member, guild_id: int, cash: list[int] +): + """tuple[:class:`discord.Embed`, :class:`discord.File`]: Returns a tuple of an embed and it's file with the balance of a user. + + Parameters + ----------- + bot: :class:`TakoBot` + user: :class:`discord.User` | :class:`discord.Member` + The user to get the balance from. + guild_id: :class:`int` + The id of the guild where the language and color is getting fetched from. + cash: :class:`list[:class:`int`]` + The amount of money the user has in it's wallet and bank. (Use the fetch_cash() function to get this)""" + thumbnail_path = await thumbnail(guild_id, "money", bot) + file = discord.File(thumbnail_path, filename="thumbnail.png") + + embed = discord.Embed( + title=i18n.t("economy.balance", locale=get_language(bot, guild_id)), + color=await get_color(bot, guild_id), + ) + embed.add_field( + name=i18n.t("economy.wallet", locale=get_language(bot, guild_id)), + value=f"{cash[0]:,}{config.CURRENCY}", + ) + embed.add_field( + name=i18n.t("economy.bank", locale=get_language(bot, guild_id)), + value=f"{cash[1]:,}{config.CURRENCY}", + ) + embed.set_author(name=str(user), icon_url=user.display_avatar.url) + embed.set_thumbnail(url="attachment://thumbnail.png") + + return embed, file + + +def error_embed(title: str, description: str, footer: str = "An error occured"): + """tuple[:class:`discord.Embed`, :class:`discord.File`]: Returns an error embed and a file for it's Thumbnail. + The first value is the embed and the second value is the file. + + Parameters + ----------- + title: :class:`str` + The title of the embed. + description: :class:`str` + The description of the embed. + footer: :class:`str` = "An error occured" + The footer of the embed. (Mostly just the translation of "An error occured")""" + file = discord.File("assets/alert.png", filename="thumbnail.png") + embed = discord.Embed( + title=f"**{title}**", + description=description, + color=discord.Color.red(), + timestamp=datetime.now(), + ) + embed.set_thumbnail(url="attachment://thumbnail.png") + embed.set_footer(text=footer) + return embed, file + + +async def translate(text: str, target: str, source: str = "auto"): + async with aiohttp.ClientSession() as session: + async with session.get( + f"{config.SIMPLY_TRANSLATE}/?engine=google&text={text.replace('&', '%26')}&sl={source}&tl={target}" + ) as response: + if response.status != 200: + return + response = await response.text() + translation = ( + BeautifulSoup(response, features="html.parser") + .body.find("textarea", attrs={"placeholder": "Translation"}) + .text + ) + return translation + + +async def new_meme(guild_id: int, user_id: int, bot, db_pool: asyncpg.Pool): + async with aiohttp.ClientSession() as session: + async with session.get("https://meme-api.herokuapp.com/gimme/") as r: + thumbnail_path = await thumbnail(guild_id, "reddit", bot) + file = discord.File(thumbnail_path, filename="thumbnail.png") + + data = await r.json() + embed = discord.Embed( + title=f"{data['title']}", + description=data["postLink"], + color=await get_color(bot, guild_id), + ) + embed.set_author( + name=data["author"], + url=f"https://reddit.com/u/{data['author']}", + icon_url="https://www.redditstatic.com/avatars/defaults/v2/avatar_default_1.png", + ) + embed.set_thumbnail(url="attachment://thumbnail.png") + embed.set_image(url=data["url"]) + embed.set_footer(text=f"r/{data['subreddit']} • {data['ups']} 👍") + + async with db_pool.acquire() as con: + user_data = await con.fetchrow( + "SELECT * FROM users WHERE user_id = $1;", user_id + ) + data = ( + str(data) + .replace("'", '"') + .replace("True", "true") + .replace("False", "false") + ) + if user_data: + await con.execute( + "UPDATE users SET last_meme = $1 WHERE user_id = $2;", + data, + user_id, + ) + else: + await con.execute( + "INSERT INTO users (user_id, last_meme) VALUES ($1, $2);", + user_id, + data, + ) + + return embed, file + + +async def is_owner_func(bot, user: discord.User | discord.Member): + app_info = await bot.application_info() + if hasattr(app_info, "team") and hasattr(app_info.team, "members"): + owners = [] + for member in app_info.team.members: + if member.membership_state == discord.TeamMembershipState.accepted: + owners.append(member.id) + return True if user.id in owners else False + return True if user.id == bot.owner_id else False + + +def owner_only(): + async def check(interaction: discord.Interaction): + return await is_owner_func(interaction.user) + + return app_commands.check(check)