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 00000000..acea011b Binary files /dev/null and b/assets/TMDb.png differ diff --git a/assets/alert.png b/assets/alert.png new file mode 100644 index 00000000..c9ac0367 Binary files /dev/null and b/assets/alert.png differ diff --git a/assets/bank.png b/assets/bank.png new file mode 100644 index 00000000..14d258a0 Binary files /dev/null and b/assets/bank.png differ diff --git a/assets/bank_dark.png b/assets/bank_dark.png new file mode 100644 index 00000000..e812b081 Binary files /dev/null and b/assets/bank_dark.png differ diff --git a/assets/error.png b/assets/error.png new file mode 100644 index 00000000..fb18364b Binary files /dev/null and b/assets/error.png differ diff --git a/assets/flag.png b/assets/flag.png new file mode 100644 index 00000000..a15afb00 Binary files /dev/null and b/assets/flag.png differ diff --git a/assets/flag_dark.png b/assets/flag_dark.png new file mode 100644 index 00000000..7df9e3c3 Binary files /dev/null and b/assets/flag_dark.png differ diff --git a/assets/megaphone.png b/assets/megaphone.png new file mode 100644 index 00000000..b724460d Binary files /dev/null and b/assets/megaphone.png differ diff --git a/assets/megaphone_dark.png b/assets/megaphone_dark.png new file mode 100644 index 00000000..c91f476f Binary files /dev/null and b/assets/megaphone_dark.png differ diff --git a/assets/money.png b/assets/money.png new file mode 100644 index 00000000..ec5bb9b5 Binary files /dev/null and b/assets/money.png differ diff --git a/assets/money_dark.png b/assets/money_dark.png new file mode 100644 index 00000000..9a6a9f8d Binary files /dev/null and b/assets/money_dark.png differ diff --git a/assets/ping/ping_green.png b/assets/ping/ping_green.png new file mode 100644 index 00000000..ec531f8b Binary files /dev/null and b/assets/ping/ping_green.png differ diff --git a/assets/ping/ping_orange.png b/assets/ping/ping_orange.png new file mode 100644 index 00000000..d64f22e4 Binary files /dev/null and b/assets/ping/ping_orange.png differ diff --git a/assets/ping/ping_red.png b/assets/ping/ping_red.png new file mode 100644 index 00000000..3324a999 Binary files /dev/null and b/assets/ping/ping_red.png differ diff --git a/assets/ping_green.png b/assets/ping_green.png new file mode 100644 index 00000000..ec531f8b Binary files /dev/null and b/assets/ping_green.png differ diff --git a/assets/ping_orange.png b/assets/ping_orange.png new file mode 100644 index 00000000..d64f22e4 Binary files /dev/null and b/assets/ping_orange.png differ diff --git a/assets/ping_red.png b/assets/ping_red.png new file mode 100644 index 00000000..3324a999 Binary files /dev/null and b/assets/ping_red.png differ diff --git a/assets/reddit.png b/assets/reddit.png new file mode 100644 index 00000000..9c97ac9e Binary files /dev/null and b/assets/reddit.png differ diff --git a/assets/role.png b/assets/role.png new file mode 100644 index 00000000..8974e6a9 Binary files /dev/null and b/assets/role.png differ diff --git a/assets/role_dark.png b/assets/role_dark.png new file mode 100644 index 00000000..2603e1fb Binary files /dev/null and b/assets/role_dark.png differ diff --git a/assets/rules.png b/assets/rules.png new file mode 100644 index 00000000..70215012 Binary files /dev/null and b/assets/rules.png differ diff --git a/assets/rules_dark.png b/assets/rules_dark.png new file mode 100644 index 00000000..776322da Binary files /dev/null and b/assets/rules_dark.png differ diff --git a/assets/search.png b/assets/search.png new file mode 100644 index 00000000..e2c63866 Binary files /dev/null and b/assets/search.png differ diff --git a/assets/search_dark.png b/assets/search_dark.png new file mode 100644 index 00000000..d89e6e79 Binary files /dev/null and b/assets/search_dark.png differ diff --git a/assets/tag.png b/assets/tag.png new file mode 100644 index 00000000..72999850 Binary files /dev/null and b/assets/tag.png differ diff --git a/assets/tag_dark.png b/assets/tag_dark.png new file mode 100644 index 00000000..79ac2d16 Binary files /dev/null and b/assets/tag_dark.png differ 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 00000000..924980ad Binary files /dev/null and b/assets/translation.png differ diff --git a/assets/translation_dark.png b/assets/translation_dark.png new file mode 100644 index 00000000..c1e227f6 Binary files /dev/null and b/assets/translation_dark.png differ diff --git a/assets/trash.png b/assets/trash.png new file mode 100644 index 00000000..2dea5912 Binary files /dev/null and b/assets/trash.png differ diff --git a/assets/warning.png b/assets/warning.png new file mode 100644 index 00000000..58480004 Binary files /dev/null and b/assets/warning.png differ diff --git a/assets/webhook.png b/assets/webhook.png new file mode 100644 index 00000000..e11efbd8 Binary files /dev/null and b/assets/webhook.png differ diff --git a/assets/webhook_dark.png b/assets/webhook_dark.png new file mode 100644 index 00000000..a189ffec Binary files /dev/null and b/assets/webhook_dark.png differ diff --git a/cogs/config/__init__.py b/cogs/config/__init__.py new file mode 100644 index 00000000..8c701006 --- /dev/null +++ b/cogs/config/__init__.py @@ -0,0 +1,13 @@ +from .autojoin import Autojoin +from .color import Color +from .crosspost import Crosspost +from .language import Language +from .selfroles import Selfroles + + +async def setup(bot): + await bot.add_cog(Autojoin(bot)) + await bot.add_cog(Color(bot)) + await bot.add_cog(Crosspost(bot)) + await bot.add_cog(Language(bot)) + await bot.add_cog(Selfroles(bot)) diff --git a/cogs/config/autojoin.py b/cogs/config/autojoin.py new file mode 100644 index 00000000..574cd6c9 --- /dev/null +++ b/cogs/config/autojoin.py @@ -0,0 +1,196 @@ +import i18n +import discord +from TakoBot import TakoBot +from discord import app_commands +from discord.ext import commands +from utils import get_color, get_language, thumbnail, delete_thumbnail + + +async def autojoin_logic( + bot: TakoBot, interaction: discord.Interaction, role: discord.Role, column: str +): + language = get_language(bot, interaction.guild.id) + if ( + role.is_default() + or role.is_bot_managed() + or role.managed + or not role.is_assignable() + or role >= 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)