From 787a367d13d89bfa8adb4ec56ed90d061278129c Mon Sep 17 00:00:00 2001 From: Valerie Date: Wed, 2 Apr 2025 17:02:06 -0400 Subject: [PATCH] Add Cogs --- advancedinvite/__init__.py | 13 + advancedinvite/advanced_invite.py | 451 ++ advancedinvite/info.json | 17 + advancedinvite/utils.py | 135 + autotraceback/README.rst | 64 + autotraceback/__init__.py | 62 + autotraceback/autotraceback.py | 142 + autotraceback/dashboard_integration.py | 56 + autotraceback/info.json | 15 + autotraceback/locales/de-DE.po | 28 + autotraceback/locales/el-GR.po | 28 + autotraceback/locales/es-ES.po | 28 + autotraceback/locales/fi-FI.po | 28 + autotraceback/locales/fr-FR.po | 28 + autotraceback/locales/it-IT.po | 28 + autotraceback/locales/ja-JP.po | 28 + autotraceback/locales/messages.pot | 23 + autotraceback/locales/nl-NL.po | 28 + autotraceback/locales/pl-PL.po | 28 + autotraceback/locales/pt-BR.po | 28 + autotraceback/locales/pt-PT.po | 28 + autotraceback/locales/ro-RO.po | 28 + autotraceback/locales/ru-RU.po | 28 + autotraceback/locales/tr-TR.po | 28 + autotraceback/locales/uk-UA.po | 28 + autotraceback/utils_version.json | 1 + avatar/__init__.py | 20 + avatar/info.json | 14 + battleship/__init__.py | 6 + battleship/ai.py | 154 + battleship/battleship.py | 215 + battleship/data/board.png | Bin 0 -> 140315 bytes battleship/data/hit.png | Bin 0 -> 637 bytes battleship/data/len2.png | Bin 0 -> 391 bytes battleship/data/len2destroyed.png | Bin 0 -> 2885 bytes battleship/data/len3.png | Bin 0 -> 570 bytes battleship/data/len3destroyed.png | Bin 0 -> 2952 bytes battleship/data/len4.png | Bin 0 -> 666 bytes battleship/data/len4destroyed.png | Bin 0 -> 4636 bytes battleship/data/len5.png | Bin 0 -> 819 bytes battleship/data/len5destroyed.png | Bin 0 -> 6730 bytes battleship/data/miss.png | Bin 0 -> 508 bytes battleship/game.py | 366 + battleship/info.json | 12 + battleship/views.py | 106 + cogcount/__init__.py | 37 + cogcount/cogcount.py | 83 + cogcount/info.json | 13 + cogpaths/README.rst | 71 + cogpaths/__init__.py | 10 + cogpaths/cogpaths.py | 41 + cogpaths/info.json | 16 + consolelogs/LICENSE | 674 ++ consolelogs/README.rst | 82 + consolelogs/__init__.py | 47 + consolelogs/consolelogs.py | 798 ++ consolelogs/dashboard_integration.py | 54 + consolelogs/info.json | 17 + consolelogs/locales/de-DE.po | 94 + consolelogs/locales/el-GR.po | 94 + consolelogs/locales/es-ES.po | 94 + consolelogs/locales/fi-FI.po | 94 + consolelogs/locales/fr-FR.po | 94 + consolelogs/locales/it-IT.po | 94 + consolelogs/locales/ja-JP.po | 94 + consolelogs/locales/messages.pot | 85 + consolelogs/locales/nl-NL.po | 94 + consolelogs/locales/pl-PL.po | 94 + consolelogs/locales/pt-BR.po | 94 + consolelogs/locales/pt-PT.po | 94 + consolelogs/locales/ro-RO.po | 94 + consolelogs/locales/ru-RU.po | 94 + consolelogs/locales/tr-TR.po | 94 + consolelogs/locales/uk-UA.po | 94 + consolelogs/utils_version.json | 1 + csvparse/__init__.py | 9 + csvparse/csvparse.py | 35 + csvparse/info.json | 9 + cyclestatus/__init__.py | 13 + cyclestatus/cycle_status.py | 380 + cyclestatus/info.json | 20 + cyclestatus/menus.py | 172 + dankutils/__init__.py | 37 + dankutils/dankutils.py | 121 + dankutils/info.json | 14 + dankutils/utils.py | 141 + dankutils/views.py | 61 + dashboard/LICENSE | 662 ++ dashboard/README.rst | 121 + dashboard/__init__.py | 46 + dashboard/dashboard.py | 428 ++ dashboard/info.json | 15 + dashboard/locales/de-DE.po | 71 + dashboard/locales/el-GR.po | 71 + dashboard/locales/es-ES.po | 71 + dashboard/locales/fi-FI.po | 71 + dashboard/locales/fr-FR.po | 71 + dashboard/locales/it-IT.po | 71 + dashboard/locales/ja-JP.po | 71 + dashboard/locales/messages.pot | 70 + dashboard/locales/nl-NL.po | 71 + dashboard/locales/pl-PL.po | 71 + dashboard/locales/pt-BR.po | 71 + dashboard/locales/pt-PT.po | 71 + dashboard/locales/ro-RO.po | 71 + dashboard/locales/ru-RU.po | 71 + dashboard/locales/tr-TR.po | 71 + dashboard/locales/uk-UA.po | 71 + dashboard/rpc/__init__.py | 789 ++ dashboard/rpc/default_cogs.py | 192 + dashboard/rpc/form.py | 368 + dashboard/rpc/locales/de-DE.po | 43 + dashboard/rpc/locales/el-GR.po | 43 + dashboard/rpc/locales/es-ES.po | 43 + dashboard/rpc/locales/fi-FI.po | 43 + dashboard/rpc/locales/fr-FR.po | 43 + dashboard/rpc/locales/it-IT.po | 43 + dashboard/rpc/locales/ja-JP.po | 43 + dashboard/rpc/locales/messages.pot | 45 + dashboard/rpc/locales/nl-NL.po | 43 + dashboard/rpc/locales/pl-PL.po | 43 + dashboard/rpc/locales/pt-BR.po | 43 + dashboard/rpc/locales/pt-PT.po | 43 + dashboard/rpc/locales/ro-RO.po | 43 + dashboard/rpc/locales/ru-RU.po | 43 + dashboard/rpc/locales/tr-TR.po | 43 + dashboard/rpc/locales/uk-UA.po | 43 + dashboard/rpc/pagination.py | 89 + dashboard/rpc/third_parties.py | 370 + dashboard/rpc/utils.py | 21 + dashboard/rpc/webhooks.py | 23 + dashboard/utils_version.json | 1 + dev/LICENSE | 674 ++ dev/README.rst | 130 + dev/__init__.py | 108 + dev/dashboard_integration.py | 12 + dev/dev.py | 1123 +++ dev/env.py | 996 +++ dev/info.json | 18 + dev/locales/de-DE.po | 90 + dev/locales/el-GR.po | 90 + dev/locales/es-ES.po | 90 + dev/locales/fi-FI.po | 90 + dev/locales/fr-FR.po | 90 + dev/locales/it-IT.po | 90 + dev/locales/ja-JP.po | 90 + dev/locales/messages.pot | 94 + dev/locales/nl-NL.po | 90 + dev/locales/pl-PL.po | 90 + dev/locales/pt-BR.po | 90 + dev/locales/pt-PT.po | 90 + dev/locales/ro-RO.po | 90 + dev/locales/ru-RU.po | 90 + dev/locales/tr-TR.po | 90 + dev/locales/uk-UA.po | 90 + dev/utils_version.json | 1 + dev/view.py | 128 + devutils/README.rst | 91 + devutils/__init__.py | 46 + devutils/devutils.py | 420 + devutils/info.json | 15 + devutils/locales/de-DE.po | 150 + devutils/locales/el-GR.po | 150 + devutils/locales/es-ES.po | 150 + devutils/locales/fi-FI.po | 150 + devutils/locales/fr-FR.po | 150 + devutils/locales/it-IT.po | 150 + devutils/locales/ja-JP.po | 150 + devutils/locales/messages.pot | 139 + devutils/locales/nl-NL.po | 150 + devutils/locales/pl-PL.po | 150 + devutils/locales/pt-BR.po | 150 + devutils/locales/pt-PT.po | 150 + devutils/locales/ro-RO.po | 150 + devutils/locales/ru-RU.po | 150 + devutils/locales/tr-TR.po | 150 + devutils/locales/uk-UA.po | 150 + devutils/utils_version.json | 1 + dictionary/__init__.py | 45 +- dictionary/dictionary.py | 300 +- dictionary/info.json | 23 +- discordsearch/README.rst | 64 + discordsearch/__init__.py | 46 + discordsearch/discordsearch.py | 349 + discordsearch/info.json | 15 + discordsearch/locales/de-DE.po | 58 + discordsearch/locales/el-GR.po | 58 + discordsearch/locales/es-ES.po | 58 + discordsearch/locales/fi-FI.po | 58 + discordsearch/locales/fr-FR.po | 58 + discordsearch/locales/it-IT.po | 58 + discordsearch/locales/ja-JP.po | 58 + discordsearch/locales/messages.pot | 51 + discordsearch/locales/nl-NL.po | 58 + discordsearch/locales/pl-PL.po | 58 + discordsearch/locales/pt-BR.po | 58 + discordsearch/locales/pt-PT.po | 58 + discordsearch/locales/ro-RO.po | 58 + discordsearch/locales/ru-RU.po | 58 + discordsearch/locales/tr-TR.po | 58 + discordsearch/locales/uk-UA.po | 58 + discordsearch/utils_version.json | 1 + draw/LICENSE | 674 ++ draw/README.rst | 64 + draw/__init__.py | 46 + draw/board.py | 417 + draw/color.py | 137 + draw/constants.py | 133 + draw/draw.py | 174 + draw/info.json | 16 + draw/locales/de-DE.po | 58 + draw/locales/el-GR.po | 58 + draw/locales/es-ES.po | 58 + draw/locales/fi-FI.po | 57 + draw/locales/fr-FR.po | 58 + draw/locales/it-IT.po | 58 + draw/locales/ja-JP.po | 58 + draw/locales/messages.pot | 58 + draw/locales/nl-NL.po | 58 + draw/locales/pl-PL.po | 58 + draw/locales/pt-BR.po | 58 + draw/locales/pt-PT.po | 58 + draw/locales/ro-RO.po | 58 + draw/locales/ru-RU.po | 58 + draw/locales/tr-TR.po | 58 + draw/locales/uk-UA.po | 58 + draw/start_view.py | 174 + draw/tools.py | 303 + draw/utils_version.json | 1 + draw/view.py | 1044 +++ embedutils/README.rst | 115 + embedutils/__init__.py | 46 + embedutils/converters.py | 402 + embedutils/dashboard_integration.py | 181 + embedutils/editor.html | 9766 ++++++++++++++++++++++++ embedutils/embedutils.py | 1015 +++ embedutils/info.json | 20 + embedutils/locales/de-DE.po | 339 + embedutils/locales/el-GR.po | 339 + embedutils/locales/es-ES.po | 339 + embedutils/locales/fi-FI.po | 339 + embedutils/locales/fr-FR.po | 339 + embedutils/locales/it-IT.po | 339 + embedutils/locales/ja-JP.po | 339 + embedutils/locales/messages.pot | 335 + embedutils/locales/nl-NL.po | 339 + embedutils/locales/pl-PL.po | 339 + embedutils/locales/pt-BR.po | 339 + embedutils/locales/pt-PT.po | 339 + embedutils/locales/ro-RO.po | 339 + embedutils/locales/ru-RU.po | 339 + embedutils/locales/tr-TR.po | 339 + embedutils/locales/uk-UA.po | 339 + embedutils/utils_version.json | 1 + 254 files changed, 41944 insertions(+), 185 deletions(-) create mode 100644 advancedinvite/__init__.py create mode 100644 advancedinvite/advanced_invite.py create mode 100644 advancedinvite/info.json create mode 100644 advancedinvite/utils.py create mode 100644 autotraceback/README.rst create mode 100644 autotraceback/__init__.py create mode 100644 autotraceback/autotraceback.py create mode 100644 autotraceback/dashboard_integration.py create mode 100644 autotraceback/info.json create mode 100644 autotraceback/locales/de-DE.po create mode 100644 autotraceback/locales/el-GR.po create mode 100644 autotraceback/locales/es-ES.po create mode 100644 autotraceback/locales/fi-FI.po create mode 100644 autotraceback/locales/fr-FR.po create mode 100644 autotraceback/locales/it-IT.po create mode 100644 autotraceback/locales/ja-JP.po create mode 100644 autotraceback/locales/messages.pot create mode 100644 autotraceback/locales/nl-NL.po create mode 100644 autotraceback/locales/pl-PL.po create mode 100644 autotraceback/locales/pt-BR.po create mode 100644 autotraceback/locales/pt-PT.po create mode 100644 autotraceback/locales/ro-RO.po create mode 100644 autotraceback/locales/ru-RU.po create mode 100644 autotraceback/locales/tr-TR.po create mode 100644 autotraceback/locales/uk-UA.po create mode 100644 autotraceback/utils_version.json create mode 100644 avatar/__init__.py create mode 100644 avatar/info.json create mode 100644 battleship/__init__.py create mode 100644 battleship/ai.py create mode 100644 battleship/battleship.py create mode 100644 battleship/data/board.png create mode 100644 battleship/data/hit.png create mode 100644 battleship/data/len2.png create mode 100644 battleship/data/len2destroyed.png create mode 100644 battleship/data/len3.png create mode 100644 battleship/data/len3destroyed.png create mode 100644 battleship/data/len4.png create mode 100644 battleship/data/len4destroyed.png create mode 100644 battleship/data/len5.png create mode 100644 battleship/data/len5destroyed.png create mode 100644 battleship/data/miss.png create mode 100644 battleship/game.py create mode 100644 battleship/info.json create mode 100644 battleship/views.py create mode 100644 cogcount/__init__.py create mode 100644 cogcount/cogcount.py create mode 100644 cogcount/info.json create mode 100644 cogpaths/README.rst create mode 100644 cogpaths/__init__.py create mode 100644 cogpaths/cogpaths.py create mode 100644 cogpaths/info.json create mode 100644 consolelogs/LICENSE create mode 100644 consolelogs/README.rst create mode 100644 consolelogs/__init__.py create mode 100644 consolelogs/consolelogs.py create mode 100644 consolelogs/dashboard_integration.py create mode 100644 consolelogs/info.json create mode 100644 consolelogs/locales/de-DE.po create mode 100644 consolelogs/locales/el-GR.po create mode 100644 consolelogs/locales/es-ES.po create mode 100644 consolelogs/locales/fi-FI.po create mode 100644 consolelogs/locales/fr-FR.po create mode 100644 consolelogs/locales/it-IT.po create mode 100644 consolelogs/locales/ja-JP.po create mode 100644 consolelogs/locales/messages.pot create mode 100644 consolelogs/locales/nl-NL.po create mode 100644 consolelogs/locales/pl-PL.po create mode 100644 consolelogs/locales/pt-BR.po create mode 100644 consolelogs/locales/pt-PT.po create mode 100644 consolelogs/locales/ro-RO.po create mode 100644 consolelogs/locales/ru-RU.po create mode 100644 consolelogs/locales/tr-TR.po create mode 100644 consolelogs/locales/uk-UA.po create mode 100644 consolelogs/utils_version.json create mode 100644 csvparse/__init__.py create mode 100644 csvparse/csvparse.py create mode 100644 csvparse/info.json create mode 100644 cyclestatus/__init__.py create mode 100644 cyclestatus/cycle_status.py create mode 100644 cyclestatus/info.json create mode 100644 cyclestatus/menus.py create mode 100644 dankutils/__init__.py create mode 100644 dankutils/dankutils.py create mode 100644 dankutils/info.json create mode 100644 dankutils/utils.py create mode 100644 dankutils/views.py create mode 100644 dashboard/LICENSE create mode 100644 dashboard/README.rst create mode 100644 dashboard/__init__.py create mode 100644 dashboard/dashboard.py create mode 100644 dashboard/info.json create mode 100644 dashboard/locales/de-DE.po create mode 100644 dashboard/locales/el-GR.po create mode 100644 dashboard/locales/es-ES.po create mode 100644 dashboard/locales/fi-FI.po create mode 100644 dashboard/locales/fr-FR.po create mode 100644 dashboard/locales/it-IT.po create mode 100644 dashboard/locales/ja-JP.po create mode 100644 dashboard/locales/messages.pot create mode 100644 dashboard/locales/nl-NL.po create mode 100644 dashboard/locales/pl-PL.po create mode 100644 dashboard/locales/pt-BR.po create mode 100644 dashboard/locales/pt-PT.po create mode 100644 dashboard/locales/ro-RO.po create mode 100644 dashboard/locales/ru-RU.po create mode 100644 dashboard/locales/tr-TR.po create mode 100644 dashboard/locales/uk-UA.po create mode 100644 dashboard/rpc/__init__.py create mode 100644 dashboard/rpc/default_cogs.py create mode 100644 dashboard/rpc/form.py create mode 100644 dashboard/rpc/locales/de-DE.po create mode 100644 dashboard/rpc/locales/el-GR.po create mode 100644 dashboard/rpc/locales/es-ES.po create mode 100644 dashboard/rpc/locales/fi-FI.po create mode 100644 dashboard/rpc/locales/fr-FR.po create mode 100644 dashboard/rpc/locales/it-IT.po create mode 100644 dashboard/rpc/locales/ja-JP.po create mode 100644 dashboard/rpc/locales/messages.pot create mode 100644 dashboard/rpc/locales/nl-NL.po create mode 100644 dashboard/rpc/locales/pl-PL.po create mode 100644 dashboard/rpc/locales/pt-BR.po create mode 100644 dashboard/rpc/locales/pt-PT.po create mode 100644 dashboard/rpc/locales/ro-RO.po create mode 100644 dashboard/rpc/locales/ru-RU.po create mode 100644 dashboard/rpc/locales/tr-TR.po create mode 100644 dashboard/rpc/locales/uk-UA.po create mode 100644 dashboard/rpc/pagination.py create mode 100644 dashboard/rpc/third_parties.py create mode 100644 dashboard/rpc/utils.py create mode 100644 dashboard/rpc/webhooks.py create mode 100644 dashboard/utils_version.json create mode 100644 dev/LICENSE create mode 100644 dev/README.rst create mode 100644 dev/__init__.py create mode 100644 dev/dashboard_integration.py create mode 100644 dev/dev.py create mode 100644 dev/env.py create mode 100644 dev/info.json create mode 100644 dev/locales/de-DE.po create mode 100644 dev/locales/el-GR.po create mode 100644 dev/locales/es-ES.po create mode 100644 dev/locales/fi-FI.po create mode 100644 dev/locales/fr-FR.po create mode 100644 dev/locales/it-IT.po create mode 100644 dev/locales/ja-JP.po create mode 100644 dev/locales/messages.pot create mode 100644 dev/locales/nl-NL.po create mode 100644 dev/locales/pl-PL.po create mode 100644 dev/locales/pt-BR.po create mode 100644 dev/locales/pt-PT.po create mode 100644 dev/locales/ro-RO.po create mode 100644 dev/locales/ru-RU.po create mode 100644 dev/locales/tr-TR.po create mode 100644 dev/locales/uk-UA.po create mode 100644 dev/utils_version.json create mode 100644 dev/view.py create mode 100644 devutils/README.rst create mode 100644 devutils/__init__.py create mode 100644 devutils/devutils.py create mode 100644 devutils/info.json create mode 100644 devutils/locales/de-DE.po create mode 100644 devutils/locales/el-GR.po create mode 100644 devutils/locales/es-ES.po create mode 100644 devutils/locales/fi-FI.po create mode 100644 devutils/locales/fr-FR.po create mode 100644 devutils/locales/it-IT.po create mode 100644 devutils/locales/ja-JP.po create mode 100644 devutils/locales/messages.pot create mode 100644 devutils/locales/nl-NL.po create mode 100644 devutils/locales/pl-PL.po create mode 100644 devutils/locales/pt-BR.po create mode 100644 devutils/locales/pt-PT.po create mode 100644 devutils/locales/ro-RO.po create mode 100644 devutils/locales/ru-RU.po create mode 100644 devutils/locales/tr-TR.po create mode 100644 devutils/locales/uk-UA.po create mode 100644 devutils/utils_version.json create mode 100644 discordsearch/README.rst create mode 100644 discordsearch/__init__.py create mode 100644 discordsearch/discordsearch.py create mode 100644 discordsearch/info.json create mode 100644 discordsearch/locales/de-DE.po create mode 100644 discordsearch/locales/el-GR.po create mode 100644 discordsearch/locales/es-ES.po create mode 100644 discordsearch/locales/fi-FI.po create mode 100644 discordsearch/locales/fr-FR.po create mode 100644 discordsearch/locales/it-IT.po create mode 100644 discordsearch/locales/ja-JP.po create mode 100644 discordsearch/locales/messages.pot create mode 100644 discordsearch/locales/nl-NL.po create mode 100644 discordsearch/locales/pl-PL.po create mode 100644 discordsearch/locales/pt-BR.po create mode 100644 discordsearch/locales/pt-PT.po create mode 100644 discordsearch/locales/ro-RO.po create mode 100644 discordsearch/locales/ru-RU.po create mode 100644 discordsearch/locales/tr-TR.po create mode 100644 discordsearch/locales/uk-UA.po create mode 100644 discordsearch/utils_version.json create mode 100644 draw/LICENSE create mode 100644 draw/README.rst create mode 100644 draw/__init__.py create mode 100644 draw/board.py create mode 100644 draw/color.py create mode 100644 draw/constants.py create mode 100644 draw/draw.py create mode 100644 draw/info.json create mode 100644 draw/locales/de-DE.po create mode 100644 draw/locales/el-GR.po create mode 100644 draw/locales/es-ES.po create mode 100644 draw/locales/fi-FI.po create mode 100644 draw/locales/fr-FR.po create mode 100644 draw/locales/it-IT.po create mode 100644 draw/locales/ja-JP.po create mode 100644 draw/locales/messages.pot create mode 100644 draw/locales/nl-NL.po create mode 100644 draw/locales/pl-PL.po create mode 100644 draw/locales/pt-BR.po create mode 100644 draw/locales/pt-PT.po create mode 100644 draw/locales/ro-RO.po create mode 100644 draw/locales/ru-RU.po create mode 100644 draw/locales/tr-TR.po create mode 100644 draw/locales/uk-UA.po create mode 100644 draw/start_view.py create mode 100644 draw/tools.py create mode 100644 draw/utils_version.json create mode 100644 draw/view.py create mode 100644 embedutils/README.rst create mode 100644 embedutils/__init__.py create mode 100644 embedutils/converters.py create mode 100644 embedutils/dashboard_integration.py create mode 100644 embedutils/editor.html create mode 100644 embedutils/embedutils.py create mode 100644 embedutils/info.json create mode 100644 embedutils/locales/de-DE.po create mode 100644 embedutils/locales/el-GR.po create mode 100644 embedutils/locales/es-ES.po create mode 100644 embedutils/locales/fi-FI.po create mode 100644 embedutils/locales/fr-FR.po create mode 100644 embedutils/locales/it-IT.po create mode 100644 embedutils/locales/ja-JP.po create mode 100644 embedutils/locales/messages.pot create mode 100644 embedutils/locales/nl-NL.po create mode 100644 embedutils/locales/pl-PL.po create mode 100644 embedutils/locales/pt-BR.po create mode 100644 embedutils/locales/pt-PT.po create mode 100644 embedutils/locales/ro-RO.po create mode 100644 embedutils/locales/ru-RU.po create mode 100644 embedutils/locales/tr-TR.po create mode 100644 embedutils/locales/uk-UA.po create mode 100644 embedutils/utils_version.json diff --git a/advancedinvite/__init__.py b/advancedinvite/__init__.py new file mode 100644 index 0000000..34e1bc8 --- /dev/null +++ b/advancedinvite/__init__.py @@ -0,0 +1,13 @@ +import json +import pathlib + +from redbot.core.bot import Red + +from .advanced_invite import AdvancedInvite + +with open(pathlib.Path(__file__).parent / "info.json") as fp: + __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] + + +async def setup(bot: Red): + await bot.add_cog(AdvancedInvite(bot)) diff --git a/advancedinvite/advanced_invite.py b/advancedinvite/advanced_invite.py new file mode 100644 index 0000000..84ec6bf --- /dev/null +++ b/advancedinvite/advanced_invite.py @@ -0,0 +1,451 @@ +# Copyright (c) 2021 - Jojo#7791 +# Licensed under MIT + +import asyncio +import datetime +import logging +from typing import Any, Dict, Final, List, Optional, Union, Tuple, TypeVar, TYPE_CHECKING + +import aiohttp +import discord +from redbot.core import Config, commands +from redbot.core.bot import Red +from redbot.core.utils.chat_formatting import humanize_list, humanize_number + +from .utils import * + +log = logging.getLogger("red.JojoCogs.advanced_invite") + + +if TYPE_CHECKING: + NoneStrict = NoneConverter +else: + class NoneStrict(NoneConverter): + strict = True + + +async def can_invite(ctx: commands.Context) -> bool: + return await ctx.bot.is_owner(ctx.author) or await ctx.bot.is_invite_url_public() + + +_config_structure: Final[Dict[str, Any]] = { + "custom_url": None, + "image_url": None, + "custom_message": "Thanks for choosing {bot_name}!", + "send_in_channel": False, + "embeds": True, + "title": "Invite {bot_name}", + "support_server": None, + "footer": None, + "extra_link": False, + "support_server_emoji": {}, + "invite_emoji": {}, +} + + +class AdvancedInvite(commands.Cog): + """An advanced invite for [botname] + + To configure the invite command, check out `[p]invite set`. + """ + + __authors__: Final[List[str]] = ["Jojo#7791"] + __version__: Final[str] = "4.0.0" + + def __init__(self, bot: Red): + self.bot = bot + self._invite_command: Optional[commands.Command] = self.bot.remove_command("invite") + self.config = Config.get_conf(self, 544974305445019651, True) + self.config.register_global(**_config_structure) + self._supported_images: Tuple[str, ...] = ("jpg", "jpeg", "png", "gif") + + async def cog_unload(self) -> None: + self.bot.remove_command("invite"), self.bot.add_command( # type:ignore + self._invite_command + ) if self._invite_command else None + + @staticmethod + def _humanize_list(data: List[str]) -> str: + return humanize_list([f"`{i}`" for i in data]) + + def format_help_for_context(self, ctx: commands.Context) -> str: + plural = "" if len(self.__authors__) == 1 else "s" + return ( + f"{super().format_help_for_context(ctx)}\n" + f"**Author{plural}:** {self._humanize_list(self.__authors__)}\n" + f"**Version:** `{self.__version__}`" + ) + + async def red_delete_data_for_user(self, *args, **kwargs) -> None: + """Nothing to delete""" + return + + @commands.command(hidden=True) + async def inviteversion(self, ctx: commands.Context): + """Get the version of Advanced Invite""" + msg = ( + "**Advanced Invite**\n" + f"Version: `{self.__version__}`\n\n" + '"This cog was created for a friend of mine, and as such is close to my heart.\n' + 'Thanks for being awesome and using my stuff!" - Jojo (the author of this cog)\n\n' + "Created with ❤" + ) + await ctx.maybe_send_embed(msg) + + @commands.group(name="invite", usage="", invoke_without_command=True) + @commands.check(can_invite) + async def invite(self, ctx: commands.Context, send_in_channel: Optional[bool] = False): + """Invite [botname] to your server!""" + + if TYPE_CHECKING: + channel: discord.TextChannel + else: + channel = ( + await self._get_channel(ctx) + if not send_in_channel and await self.bot.is_owner(ctx.author) + else ctx.channel + ) + settings = await self.config.all() + title, message = self._get_items(settings, ["title", "custom_message"], ctx) + url = await self.bot.get_invite_url() + time = datetime.datetime.now(tz=datetime.timezone.utc) + support_msg = ( + f"Join the support server <{support}>\n" + if (support := settings.get("support_server")) else "" + ) + invite_emoji, support_emoji = ( + Emoji.from_data(settings.get(x)) for x in ("invite_emoji", "support_emoji") + ) + footer = settings.get("footer") + if footer: + footer = footer.replace("{bot_name}", ctx.me.name).replace( + "{guild_count}", humanize_number(len(ctx.bot.guilds)) + ).replace( + "{user_count}", + humanize_number(len(self.bot.users)) + ) + kwargs: Dict[str, Any] = { + "content": ( + f"**{title}**\n\n{message}\n<{url}>\n{support_msg}\n\n{footer}" + ), + } + if await self._embed_requested(ctx, channel): + message = f"{message}\n{url}" if settings.get("extra_link") else f"[{message}]({url})" + embed = discord.Embed( + title=title, + description=message, + colour=await ctx.embed_colour(), + timestamp=time, + ) + if support: + embed.add_field(name="Join the support server", value=support) + if curl := settings.get("custom_url"): + embed.set_thumbnail(url=curl) + if iurl := settings.get("image_url"): + embed.set_image(url=iurl) + if footer: + embed.set_footer(text=footer) + kwargs = {"embed": embed} + view = discord.ui.View() + view.add_item(self._make_button(url, f"Invite {ctx.me.name}", invite_emoji)) + if support: + view.add_item(self._make_button(support, "Join the support server", support_emoji)) + kwargs["view"] = view + try: + await channel.send(**kwargs) + except discord.Forbidden: # Couldn't dm + if channel == ctx.author.dm_channel: + return await ctx.send("I could not dm you!") + await ctx.send( + "I'm sorry, something went wrong when trying to send the invite." + "Please let my owner know if this problem continues." + ) + except discord.HTTPException: + await ctx.send( + "I'm sorry, something went wrong when trying to send the invite." + "Please let my owner know if this problem continues." + ) + + @staticmethod + def _make_button(url: str, label: str, emoji: Optional[Emoji]) -> discord.ui.Button: + emoji_data = emoji.as_emoji() if emoji else None + return discord.ui.Button(style=discord.ButtonStyle.url, label=label, url=url, emoji=emoji_data) + + @staticmethod + def _get_items(settings: dict, keys: List[str], ctx: commands.Context) -> list: + return [ + settings.get(key, _config_structure[key]).replace( + "{bot_name}", ctx.me.name, + ) for key in keys + ] + + @invite.group(name="settings", aliases=("set",)) + @commands.is_owner() + async def invite_settings(self, ctx: commands.Context): + """Manage the settings for the invite command. + + You can set the title, message, support server invite. + + If you have embeds enabled you can also set the thumbnail url, and the footer. + """ + pass + + @invite_settings.command(name="support") + async def invite_support(self, ctx: commands.Context, invite: InviteNoneConverter): + """Set the support server invite. + + Type `None` to reset it + + **Arguments** + - `invite` The invite url for your support server. Type `None` to remove it. + """ + invite = getattr(invite, "url", invite) + set_reset = f"set as <{invite}>." if invite else "reset." + await ctx.send(f"The support server has been {set_reset}") + await self.config.support_server.set(invite) + + @invite_settings.command(name="embed") + async def invite_embed(self, ctx: commands.Context, toggle: bool): + """Set whether the invite command should use embeds. + + **Arguments** + - `toggle` Whether the invite command should be embedded or not. + """ + toggled = "enabled" if toggle else "disabled" + await ctx.send(f"Embeds are now {toggled} for the invite command.") + await self.config.embeds.set(toggle) + + @invite_settings.command(name="message") + async def invite_message(self, ctx: commands.Context, *, message: NoneStrict): + """Set the message for the invite command. + + Type `None` to reset it. + You can use `{bot_name}` in your message to display [botname] in the message. + + **Arguments** + - `message` The message for the invite command. Type `None` to reset it. + """ + reset = False + if message is None: + reset = True + message = _config_structure["custom_message"] + elif len(message) > 1500: + return await ctx.send("The message's length cannot be more than 1500 characters.") + set_reset = "reset" if reset else "set" + + await ctx.send(f"The message has been {set_reset}.") + await self.config.custom_message.set(message) + + @invite_settings.command(name="title") + async def invite_title(self, ctx: commands.Context, *, title: NoneStrict): + """Set the title for the invite command. + + Type `None` to reset it. + You can use `{bot_name}` to display [botname] in the title. + + **Arguments** + - `title` The title for the invite command. Type `None` to reset it. + """ + reset = False + if title is None: + reset = True + title = _config_structure["title"] + set_reset = "reset" if reset else "set" + + await ctx.send(f"The title has been {set_reset}") + await self.config.title.set(title) + + @invite_settings.command(name="footer") + async def invite_footer(self, ctx: commands.Context, *, footer: NoneConverter): + """Set the footer for the invite command + + **Variables** + - `{bot_name}` Displays [botname] in the footer + - `{guild_count}` Displays in how many guilds is the bot in + - `{user_count}` Displays how many users in total + + **Arguments** + - `footer` The footer for the invite command. + """ + + if not footer: + await self.config.footer.set(None) + return await ctx.send("The footer has been reset.") + if len(footer) > 100: + return await ctx.send("The footer's length cannot be over 100 characters long.") + await self.config.footer.set(footer) + await ctx.send("The footer has been set.") + + @invite_settings.command(name="public") + async def invite_send_in_channel(self, ctx: commands.Context, toggle: bool): + """Set whether the invite command should send in the channel it was invoked in + + **Arguments** + - `toggle` Whether or not the invite command should be sent in the channel it was used in. + """ + await self.config.send_in_channel.set(toggle) + now_no_longer = "now" if toggle else "no longer" + await ctx.send( + f"The invite command will {now_no_longer} send the message in the channel it was invoked in" + ) + + @invite_settings.command(name="supportserveremoji", aliases=["ssemoji"]) + async def support_server_emoji(self, ctx: commands.Context, emoji: EmojiConverter): + """Set the emoji for the support server invite button. + + Type "None" to remove it. + + **Arguments** + - `emoji` The emoji for the support server button. Type "none" to remove it. + """ + if not emoji: + await self.config.support_server_emoji.clear() + return await ctx.send("I have reset the support server emoji.") + await self.config.support_server_emoji.set(emoji.to_dict()) + await ctx.send(f"Set the support server emoji to {emoji.as_emoji()}") + + @invite_settings.command(name="inviteemoji", aliases=["iemoji"]) + async def invite_emoji(self, ctx: commands.Context, emoji: EmojiConverter): + """Set the emoji for the invite button. + + Type "None" to remove it. + + **Arguments** + - `emoji` The emoji for the invite button. Type "none" to remove it. + """ + if not emoji: + await self.config.invite_emoji.clear() + return await ctx.send("I have reset the invite emoji.") + await self.config.invite_emoji.set(emoji.to_dict()) + await ctx.send(f"Set the invite emoji to {emoji.as_emoji()}") + + @invite_settings.command(name="thumbnailurl") + async def invite_custom_url(self, ctx: commands.Context, url: str = None): + """Set the thumbnail url for the invite command's embed + + This setting only applies for embeds. + + **Arguments** + - `url` The thumbnail url for embed command. This can also be a file (upload the image when you run the command) + Type `none` to reset the url. + """ + if len(ctx.message.attachments) == 0 and url is None: + return await ctx.send_help() + + if len(ctx.message.attachments) > 0: + if not (attach := ctx.message.attachments[0]).filename.endswith( + self._supported_images + ): + return await ctx.send("That image is invalid.") + url = attach.url + elif url is not None: + if url.lower == "none": + await self.config.custom_url.clear() + return await ctx.send("I have reset the thumbnail url.") + if url.startswith("<") and url.endswith(">"): + url = url[1:-1] + if not url.endswith(self._supported_images): + return await ctx.send( + f"That url does not point to a proper image type, ({', '.join(self._supported_images)})" + ) + async with ctx.typing(): + async with aiohttp.ClientSession() as sess: + try: + async with sess.get(url) as re: + await re.read() + except aiohttp.InvalidURL: + return await ctx.send("That is not a valid url.") + except aiohttp.ClientError: + return await ctx.send( + "Something went wrong while trying to get the image." + ) + await self.config.custom_url.set(url) + await ctx.send("Done. I have set the thumbnail url.") + + @invite_settings.command(name="imageurl", usage="") + async def invite_image_url(self, ctx: commands.Context, url: str = None): + """Set the image url for the invite command. + + This setting only applies for embeds. + + **Arguments** + - `url` The url for the embed's image. Type `none` to clear it. + You can also upload an image instead of providing this argument + """ + if len(ctx.message.attachments) > 0: + # Attachments take priority + if not (attach := ctx.message.attachments[0]).filename.endswith( + self._supported_images + ): + return await ctx.send("That image is invalid.") + url = attach.url + elif url is not None: + if url == "none": + await self.config.image_url.clear() + return await ctx.send("Reset the image url.") + async with ctx.typing(): + try: + async with aiohttp.request("GET", url) as re: + await re.read() + except aiohttp.InvalidURL: + return await ctx.send("That url is invalid.") + except aiohttp.ClientError: + return await ctx.send("Something went wrong when trying to validate that url.") + else: + # as much as I hate else blocks this is hard to bypass + return await ctx.send_help() + await self.config.image_url.set(url) + await ctx.send("Done. I have set the image url.") + + @invite_settings.command(name="extralink") + async def invite_extra_links(self, ctx: commands.Context, toggle: bool): + """Toggle whether the invite command's embed should have extra links showing the invite url + + **Arguments** + - `toggle` Whether the invite command's embed should have extra links. + """ + await self.config.extra_link.set(toggle) + now_no_longer = "now" if toggle else "no longer" + await ctx.send(f"Extra links are {now_no_longer} enabled.") + + @invite_settings.command(name="showsettings") + async def invite_show_settings(self, ctx: commands.Context): + """Show the settings for the invite command""" + _data: dict = {} + settings = await self.config.all() + for key, value in settings.items(): + if key == "mobile_check": + continue + key = key.replace("_", " ").replace("custom ", "") + key = " ".join(x.capitalize() for x in key.split()) + if key.lower() == "url": + key = "Embed Thumbnail Url" + _data[key] = value + msg = "**Invite settings**\n\n" + "\n".join( + f"**{key}:** {value}" for key, value in _data.items() + ) + kwargs: dict = {"content": msg} + if await ctx.embed_requested(): + embed = discord.Embed( + title="Invite settings", + colour=await ctx.embed_colour(), + timestamp=datetime.datetime.now(tz=datetime.timezone.utc), + ) + [embed.add_field(name=key, value=value, inline=False) for key, value in _data.items()] + kwargs = {"embed": embed} + await ctx.send(**kwargs) + + async def _get_channel(self, ctx: commands.Context) -> Union[discord.TextChannel, discord.DMChannel]: + if await self.config.send_in_channel(): + return ctx.channel + + if ret := ctx.author.dm_channel: + return ret + return await ctx.author.create_dm() + + async def _embed_requested(self, ctx: commands.Context, channel: discord.TextChannel) -> bool: + if not await self.config.embeds(): + return False + if isinstance(channel, discord.DMChannel): + return True + return channel.permissions_for(ctx.me).embed_links diff --git a/advancedinvite/info.json b/advancedinvite/info.json new file mode 100644 index 0000000..87887e0 --- /dev/null +++ b/advancedinvite/info.json @@ -0,0 +1,17 @@ +{ + "name": "advancedinvite", + "short": "Embed the invite command", + "description": "Embeds the invite command if possible.", + "end_user_data_statement": "This cog does not store end user data.", + "install_msg": "Thanks for installing Jojo's AdvancedInvite cog!\nThis cog was requested by DSC#6238 :heart:\n\nTake a look at `[p]help invite` for setting commands.", + "author": [ + "Jojo#7791" + ], + "required_cogs": {}, + "requirements": ["emoji"], + "tags": ["Utility"], + "min_bot_version": "3.5.0.dev0", + "hidden": false, + "disabled": false, + "type": "COG" +} diff --git a/advancedinvite/utils.py b/advancedinvite/utils.py new file mode 100644 index 0000000..feb582c --- /dev/null +++ b/advancedinvite/utils.py @@ -0,0 +1,135 @@ +# Copyright (c) 2021 - Jojo#7791 +# Licensed under MIT + +import logging +from datetime import datetime +from enum import Enum +from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional + +import discord +try: + from emoji.unicode_codes import UNICODE_EMOJI_ENGLISH +except ImportError: + from emoji import EMOJI_DATA as UNICODE_EMOJI_ENGLISH +from redbot.core import commands + +log = logging.getLogger("red.jojocogs.advancedinvite.utils") + +__all__ = [ + "create_doc", + "TimestampFormats", + "timestamp_format", + "NoneConverter", + "InviteNoneConverter", + "Emoji", + "EmojiConverter", +] +NoneType = type(None) + + +def create_doc(default: Optional[str] = None, *, override: bool = False): + """Create a docstring if you don't wanna""" + + def inner(func): + doc = default or "No Documentation" + if not func.__doc__ or override: + func.__doc__ = doc + return func + + return inner + + +@create_doc() +class TimestampFormats(Enum): + DEFAULT = "f" + LONG_DATE_TIME = "F" + SHORT_DATE = "d" + LONG_DATE = "D" + SHORT_TIME = "t" + LONG_TIME = "T" + RELATIVE_TIME = "R" + + +@create_doc() +def timestamp_format(dt: Optional[datetime] = None, *, dt_format: Optional[TimestampFormats] = None) -> str: + if not dt: + dt = datetime.now() + if not dt_format or dt_format == TimestampFormats.DEFAULT: + return f"" + else: + return f"" + + +if TYPE_CHECKING: + NoneConverter = Optional[str] +else: + class NoneConverter(commands.Converter): + """A simple converter for NoneType args for commands""" + + def __init__(self, *, strict: bool = False): + self.strict = strict + + async def convert(self, ctx: commands.Context, arg: str) -> Union[NoneType, str]: + args = ["none"] + if not self.strict: + args.extend(["no", "nothing"]) + if arg.lower() in args: + return None + return arg + + +if TYPE_CHECKING: + InviteNoneConverter = Union[NoneType, discord.Invite] +else: + + class InviteNoneConverter(NoneConverter): + def __init__(self): + self.strict = False + + async def convert( + self, ctx: commands.Context, arg: str + ) -> Union[NoneType, discord.Invite]: + ret = await super().convert(ctx, arg) + if ret is None: + return ret + return await commands.InviteConverter().convert(ctx, ret) + + +class Emoji: + def __init__(self, data: Dict[str, Any]): + self.name = data["name"] + self.id = data.get("id", None) + self.animated = data.get("animated", None) + self.custom = self.id is not None + + @classmethod + def from_data(cls, data: Union[str, Dict[str, Any]]): + log.debug(data) + if not data: + return None + if isinstance(data, str): + return cls({"name": data}) + return cls(data) + + def to_dict(self) -> Dict[str, Any]: + return {"name": self.name, "id": self.id} + + def as_emoji(self) -> str: + if not self.custom: + return self.name + an = "a" if self.animated else "" + return f"<{an}:{self.name}:{self.id}>" + + +if TYPE_CHECKING: + EmojiConverter = Union[Emoji, NoneType] +else: + + class EmojiConverter(commands.PartialEmojiConverter): + async def convert(self, ctx: commands.Context, arg: str) -> Union[Emoji, NoneType]: + if arg.lower() == "none": + return None + arg = arg.strip() + data = arg if arg in UNICODE_EMOJI_ENGLISH.keys() else await super().convert(ctx, arg) + data = getattr(data, "to_dict", lambda: data)() + return Emoji.from_data(data) diff --git a/autotraceback/README.rst b/autotraceback/README.rst new file mode 100644 index 0000000..8784c66 --- /dev/null +++ b/autotraceback/README.rst @@ -0,0 +1,64 @@ +.. _autotraceback: +============= +AutoTraceback +============= + +This is the cog guide for the ``AutoTraceback`` cog. This guide contains the collection of commands which you can use in the cog. +Through this guide, ``[p]`` will always represent your prefix. Replace ``[p]`` with your own prefix when you use these commands in Discord. + +.. note:: + + Ensure that you are up to date by running ``[p]cog update autotraceback``. + If there is something missing, or something that needs improving in this documentation, feel free to create an issue `here `_. + This documentation is generated everytime this cog receives an update. + +--------------- +About this cog: +--------------- + +A cog to display the error traceback of a command automatically after the error! + +--------- +Commands: +--------- + +Here are all the commands included in this cog (1): + +* ``[p]traceback [public=True] [index=0]`` + Sends to the owner the last command exception that has occurred. + +------------ +Installation +------------ + +If you haven't added my repo before, lets add it first. We'll call it "AAA3A-cogs" here. + +.. code-block:: ini + + [p]repo add AAA3A-cogs https://github.com/AAA3A-AAA3A/AAA3A-cogs + +Now, we can install AutoTraceback. + +.. code-block:: ini + + [p]cog install AAA3A-cogs autotraceback + +Once it's installed, it is not loaded by default. Load it by running the following command: + +.. code-block:: ini + + [p]load autotraceback + +---------------- +Further Support: +---------------- + +Check out my docs `here `_. +Mention me in the #support_other-cogs in the `cog support server `_ if you need any help. +Additionally, feel free to open an issue or pull request to this repo. + +-------- +Credits: +-------- + +Thanks to Kreusada for the Python code to automatically generate this documentation! \ No newline at end of file diff --git a/autotraceback/__init__.py b/autotraceback/__init__.py new file mode 100644 index 0000000..ea05917 --- /dev/null +++ b/autotraceback/__init__.py @@ -0,0 +1,62 @@ +from redbot.core import errors # isort:skip +import importlib +import sys + +try: + import AAA3A_utils +except ModuleNotFoundError: + raise errors.CogLoadError( + "The needed utils to run the cog were not found. Please execute the command `[p]pipinstall git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." + ) +modules = sorted( + [module for module in sys.modules if module.split(".")[0] == "AAA3A_utils"], reverse=True +) +for module in modules: + try: + importlib.reload(sys.modules[module]) + except ModuleNotFoundError: + pass +del AAA3A_utils +# import AAA3A_utils +# import json +# import os +# __version__ = AAA3A_utils.__version__ +# with open(os.path.join(os.path.dirname(__file__), "utils_version.json"), mode="r") as f: +# data = json.load(f) +# needed_utils_version = data["needed_utils_version"] +# if __version__ > needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a higher version than the one supported by this version of the cog. Please update the cogs of the `AAA3A-cogs` repo." +# ) +# elif __version__ < needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a lower version than the one supported by this version of the cog. Please execute the command `[p]pipinstall --upgrade git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." +# ) + +from redbot.core.bot import Red # isort:skip +import asyncio + +from redbot.core.utils import get_end_user_data_statement + +from .autotraceback import AutoTraceback + +__red_end_user_data_statement__ = get_end_user_data_statement(file=__file__) + +old_traceback = None + + +async def setup_after_ready(bot: Red) -> None: + global old_traceback + await bot.wait_until_red_ready() + cog = AutoTraceback(bot) + if old_traceback := bot.get_command("traceback"): + bot.remove_command(old_traceback.name) + await bot.add_cog(cog) + + +async def setup(bot: Red) -> None: + asyncio.create_task(setup_after_ready(bot)) + + +def teardown(bot: Red) -> None: + bot.add_command(old_traceback) diff --git a/autotraceback/autotraceback.py b/autotraceback/autotraceback.py new file mode 100644 index 0000000..8cb12bd --- /dev/null +++ b/autotraceback/autotraceback.py @@ -0,0 +1,142 @@ +from AAA3A_utils import Cog, CogsUtils, Menu # isort:skip +from redbot.core import commands # isort:skip +from redbot.core.bot import Red # isort:skip +from redbot.core.i18n import Translator, cog_i18n # isort:skip +import discord # isort:skip +import typing # isort:skip + +import traceback + +from redbot.core.utils.chat_formatting import box, pagify + +from .dashboard_integration import DashboardIntegration + +# Credits: +# General repo credits. + +_: Translator = Translator("AutoTraceback", __file__) + +IGNORED_ERRORS = ( + commands.UserInputError, + commands.DisabledCommand, + commands.CommandNotFound, + commands.CheckFailure, + commands.NoPrivateMessage, + commands.CommandOnCooldown, + commands.MaxConcurrencyReached, + commands.BadArgument, + commands.BadBoolArgument, +) + + +@cog_i18n(_) +class AutoTraceback(DashboardIntegration, Cog): + """A cog to display the error traceback of a command automatically after the error!""" + + def __init__(self, bot: Red) -> None: + super().__init__(bot=bot) + + self.tracebacks: typing.List[str] = [] + + @commands.is_owner() + @commands.hybrid_command() + async def traceback( + self, ctx: commands.Context, public: typing.Optional[bool] = True, index: int = 0 + ) -> None: + """Sends to the owner the last command exception that has occurred. + + If public (yes is specified), it will be sent to the chat instead. + + Warning: Sending the traceback publicly can accidentally reveal sensitive information about your computer or configuration. + + **Examples:** + - `[p]traceback` - Sends the traceback to your DMs. + - `[p]traceback True` - Sends the last traceback in the current context. + + **Arguments:** + - `[public]` - Whether to send the traceback to the current context. Default is `True`. + - `[index]` - The error index. `0` is the last one. + """ + if not self.tracebacks and not ctx.bot._last_exception: + raise commands.UserFeedbackCheckFailure(_("No exception has occurred yet.")) + if index == 0: # Last bot exception can be set directly by cogs. + _last_exception = ctx.bot._last_exception + else: + try: + _last_exception = self.tracebacks[-(index + 1)] + except IndexError: + _last_exception = ctx.bot._last_exception + _last_exception = _last_exception.split("\n") + _last_exception[0] = _last_exception[0] + ( + "" if _last_exception[0].endswith(":") else ":\n" + ) + _last_exception = "\n".join(_last_exception) + _last_exception = CogsUtils.replace_var_paths(_last_exception) + if public: + try: + await Menu(pages=_last_exception, timeout=180, lang="py").start(ctx) + except discord.HTTPException: + pass + else: + return + for page in pagify(_last_exception, shorten_by=15): + try: + await ctx.author.send(box(page, lang="py")) + except discord.HTTPException: + raise commands.UserFeedbackCheckFailure( + "I couldn't send the traceback message to you in DM. " + "Either you blocked me or you disabled DMs in this server." + ) + + @commands.Cog.listener() + async def on_command_error( + self, ctx: commands.Context, error: commands.CommandError, unhandled_by_cog: bool = False + ) -> None: + if await self.bot.cog_disabled_in_guild(cog=self, guild=ctx.guild): + return + if isinstance(error, IGNORED_ERRORS): + return + traceback_error = "".join( + traceback.format_exception(type(error), error, error.__traceback__) + ) + _traceback_error = traceback_error.split("\n") + _traceback_error[0] = _traceback_error[0] + ( + "" if _traceback_error[0].endswith(":") else ":\n" + ) + traceback_error = "\n".join(_traceback_error) + traceback_error = CogsUtils.replace_var_paths(traceback_error) + self.tracebacks.append(traceback_error) + if ctx.author.id not in ctx.bot.owner_ids: + return + pages = [box(page, lang="py") for page in pagify(traceback_error, shorten_by=10)] + try: + await Menu(pages=pages, timeout=180, delete_after_timeout=False).start(ctx) + except discord.HTTPException: + pass + + @commands.Cog.listener() + async def on_assistant_cog_add( + self, assistant_cog: typing.Optional[commands.Cog] = None + ) -> None: # Vert's Assistant integration/third party. + if assistant_cog is None: + return self.get_last_command_error_traceback_for_assistant + schema = { + "name": "get_last_command_error_traceback_for_assistant", + "description": "Get the traceback of the last command error occured on the bot.", + "parameters": {"type": "object", "properties": {}, "required": []}, + } + await assistant_cog.register_function(cog_name=self.qualified_name, schema=schema) + + async def get_last_command_error_traceback_for_assistant( + self, user: typing.Union[discord.Member, discord.User], *args, **kwargs + ): + if user.id not in self.bot.owner_ids: + return "Only bot owners can view errors tracebacks." + if not self.bot._last_exception: + return "No last command error recorded." + last_traceback = self.bot._last_exception + last_traceback = CogsUtils.replace_var_paths(last_traceback) + data = { + "Last command error traceback": f"\n{last_traceback}", + } + return [f"{key}: {value}\n" for key, value in data.items() if value is not None] diff --git a/autotraceback/dashboard_integration.py b/autotraceback/dashboard_integration.py new file mode 100644 index 0000000..0fd82b0 --- /dev/null +++ b/autotraceback/dashboard_integration.py @@ -0,0 +1,56 @@ +from redbot.core import commands # isort:skip +from redbot.core.bot import Red # isort:skip +from redbot.core.i18n import Translator # isort:skip +import typing # isort:skip + +_: Translator = Translator("AutoTraceback", __file__) + + +def dashboard_page(*args, **kwargs): + def decorator(func: typing.Callable): + func.__dashboard_decorator_params__ = (args, kwargs) + return func + + return decorator + + +class DashboardIntegration: + bot: Red + tracebacks = [] + + @commands.Cog.listener() + async def on_dashboard_cog_add(self, dashboard_cog: commands.Cog) -> None: + if hasattr(self, "settings") and hasattr(self.settings, "commands_added"): + await self.settings.commands_added.wait() + dashboard_cog.rpc.third_parties_handler.add_third_party(self) + + @dashboard_page( + name=None, + description="Display the traceback of the last occured exceptions.", + is_owner=True, + ) + async def rpc_callback(self, **kwargs) -> typing.Dict[str, typing.Any]: + tracebacks = self.tracebacks.copy() + if not tracebacks: + return {"status": 0, "error_title": _("No exception has occurred yet.")} + source = """ + {% for traceback in tracebacks %} + {{ traceback|highlight("python") }} + {% if not loop.last %} +
+ {% endif %} + {% endfor %} + """ + return { + "status": 0, + "web_content": { + "source": source, + "tracebacks": kwargs["Pagination"].from_list( + tracebacks, + per_page=kwargs["extra_kwargs"].get("per_page"), + page=kwargs["extra_kwargs"].get("page"), + default_per_page=1, + default_page=len(tracebacks), + ), + }, + } diff --git a/autotraceback/info.json b/autotraceback/info.json new file mode 100644 index 0000000..bb987f1 --- /dev/null +++ b/autotraceback/info.json @@ -0,0 +1,15 @@ +{ + "author": ["AAA3A"], + "name": "AutoTraceback", + "install_msg": "Thank you for installing this cog!\nDo `[p]help CogName` to get the list of commands and their description. If you enjoy my work, please consider donating on [Buy Me a Coffee]() or [Ko-Fi]()!", + "short": "A cog to display the error traceback of a command automatically after the error!", + "description": "A cog to display the error traceback of a command automatically after the error! If you are developing cogs for Red for example, you don't need to use the traceback command every time you run a new command.", + "tags": [ + "error", + "traceback", + "dev" + ], + "requirements": ["git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git"], + "min_bot_version": "3.5.0", + "end_user_data_statement": "This cog does not persistently store data or metadata about users." +} \ No newline at end of file diff --git a/autotraceback/locales/de-DE.po b/autotraceback/locales/de-DE.po new file mode 100644 index 0000000..8570273 --- /dev/null +++ b/autotraceback/locales/de-DE.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:17\n" +"Last-Translator: \n" +"Language-Team: German\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: de\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/autotraceback/locales/messages.pot\n" +"X-Crowdin-File-ID: 74\n" +"Language: de_DE\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "A cog to display the error traceback of a command automatically after the error!" +msgstr "Ein Zahnrad, um den Fehler-Traceback eines Befehls automatisch nach dem Fehler anzuzeigen!" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "Es ist noch keine Ausnahme aufgetreten." + diff --git a/autotraceback/locales/el-GR.po b/autotraceback/locales/el-GR.po new file mode 100644 index 0000000..171e27f --- /dev/null +++ b/autotraceback/locales/el-GR.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:17\n" +"Last-Translator: \n" +"Language-Team: Greek\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: el\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/autotraceback/locales/messages.pot\n" +"X-Crowdin-File-ID: 74\n" +"Language: el_GR\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "A cog to display the error traceback of a command automatically after the error!" +msgstr "Ένα γρανάζι για την αυτόματη εμφάνιση της αναδρομής σφάλματος μιας εντολής μετά το σφάλμα!" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "Καμία εξαίρεση δεν έχει συμβεί ακόμη." + diff --git a/autotraceback/locales/es-ES.po b/autotraceback/locales/es-ES.po new file mode 100644 index 0000000..a8de158 --- /dev/null +++ b/autotraceback/locales/es-ES.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:17\n" +"Last-Translator: \n" +"Language-Team: Spanish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: es-ES\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/autotraceback/locales/messages.pot\n" +"X-Crowdin-File-ID: 74\n" +"Language: es_ES\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "A cog to display the error traceback of a command automatically after the error!" +msgstr "Un engranaje para mostrar automáticamente la traza de error de un comando después del error!" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "Todavía no se ha producido ninguna excepción." + diff --git a/autotraceback/locales/fi-FI.po b/autotraceback/locales/fi-FI.po new file mode 100644 index 0000000..95c58b0 --- /dev/null +++ b/autotraceback/locales/fi-FI.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:17\n" +"Last-Translator: \n" +"Language-Team: Finnish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fi\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/autotraceback/locales/messages.pot\n" +"X-Crowdin-File-ID: 74\n" +"Language: fi_FI\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "A cog to display the error traceback of a command automatically after the error!" +msgstr "Hammasratas, joka näyttää komennon virheen jäljityksen automaattisesti virheen jälkeen!" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "Poikkeusta ei ole vielä tapahtunut." + diff --git a/autotraceback/locales/fr-FR.po b/autotraceback/locales/fr-FR.po new file mode 100644 index 0000000..c139110 --- /dev/null +++ b/autotraceback/locales/fr-FR.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:17\n" +"Last-Translator: \n" +"Language-Team: French\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/autotraceback/locales/messages.pot\n" +"X-Crowdin-File-ID: 74\n" +"Language: fr_FR\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "A cog to display the error traceback of a command automatically after the error!" +msgstr "Un cog pour afficher le traceback de l'erreur d'une commande automatiquement après l'erreur !" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "Aucune exception ne s'est encore produite." + diff --git a/autotraceback/locales/it-IT.po b/autotraceback/locales/it-IT.po new file mode 100644 index 0000000..e99806c --- /dev/null +++ b/autotraceback/locales/it-IT.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:17\n" +"Last-Translator: \n" +"Language-Team: Italian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: it\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/autotraceback/locales/messages.pot\n" +"X-Crowdin-File-ID: 74\n" +"Language: it_IT\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "A cog to display the error traceback of a command automatically after the error!" +msgstr "Una rotella per visualizzare automaticamente il traceback dell'errore di un comando dopo l'errore!" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "Non si è ancora verificata alcuna eccezione." + diff --git a/autotraceback/locales/ja-JP.po b/autotraceback/locales/ja-JP.po new file mode 100644 index 0000000..b25fc49 --- /dev/null +++ b/autotraceback/locales/ja-JP.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:17\n" +"Last-Translator: \n" +"Language-Team: Japanese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ja\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/autotraceback/locales/messages.pot\n" +"X-Crowdin-File-ID: 74\n" +"Language: ja_JP\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "A cog to display the error traceback of a command automatically after the error!" +msgstr "コマンドのエラートレースバックをエラーの後に自動的に表示するためのコグです!" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "まだ例外は発生していません。" + diff --git a/autotraceback/locales/messages.pot b/autotraceback/locales/messages.pot new file mode 100644 index 0000000..9fd3bf0 --- /dev/null +++ b/autotraceback/locales/messages.pot @@ -0,0 +1,23 @@ +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2024-12-29 10:43+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "" +"A cog to display the error traceback of a command automatically after the " +"error!" +msgstr "" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "" diff --git a/autotraceback/locales/nl-NL.po b/autotraceback/locales/nl-NL.po new file mode 100644 index 0000000..9d512b8 --- /dev/null +++ b/autotraceback/locales/nl-NL.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:17\n" +"Last-Translator: \n" +"Language-Team: Dutch\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: nl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/autotraceback/locales/messages.pot\n" +"X-Crowdin-File-ID: 74\n" +"Language: nl_NL\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "A cog to display the error traceback of a command automatically after the error!" +msgstr "Een tandwiel om de fouttrackback van een commando automatisch weer te geven na de fout!" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "Er heeft zich nog geen uitzondering voorgedaan." + diff --git a/autotraceback/locales/pl-PL.po b/autotraceback/locales/pl-PL.po new file mode 100644 index 0000000..95238c8 --- /dev/null +++ b/autotraceback/locales/pl-PL.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:17\n" +"Last-Translator: \n" +"Language-Team: Polish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/autotraceback/locales/messages.pot\n" +"X-Crowdin-File-ID: 74\n" +"Language: pl_PL\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "A cog to display the error traceback of a command automatically after the error!" +msgstr "Trybik do wyświetlania śladu błędu polecenia automatycznie po wystąpieniu błędu!" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "Nie wystąpił jeszcze żaden wyjątek." + diff --git a/autotraceback/locales/pt-BR.po b/autotraceback/locales/pt-BR.po new file mode 100644 index 0000000..281fbe0 --- /dev/null +++ b/autotraceback/locales/pt-BR.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:17\n" +"Last-Translator: \n" +"Language-Team: Portuguese, Brazilian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-BR\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/autotraceback/locales/messages.pot\n" +"X-Crowdin-File-ID: 74\n" +"Language: pt_BR\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "A cog to display the error traceback of a command automatically after the error!" +msgstr "Uma engrenagem para mostrar o traço de erro de um comando automaticamente após o erro!" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "Ainda não ocorreu qualquer excepção." + diff --git a/autotraceback/locales/pt-PT.po b/autotraceback/locales/pt-PT.po new file mode 100644 index 0000000..7fb35c6 --- /dev/null +++ b/autotraceback/locales/pt-PT.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:17\n" +"Last-Translator: \n" +"Language-Team: Portuguese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-PT\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/autotraceback/locales/messages.pot\n" +"X-Crowdin-File-ID: 74\n" +"Language: pt_PT\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "A cog to display the error traceback of a command automatically after the error!" +msgstr "Uma engrenagem para mostrar o traço de erro de um comando automaticamente após o erro!" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "Ainda não ocorreu qualquer excepção." + diff --git a/autotraceback/locales/ro-RO.po b/autotraceback/locales/ro-RO.po new file mode 100644 index 0000000..f9337ec --- /dev/null +++ b/autotraceback/locales/ro-RO.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:17\n" +"Last-Translator: \n" +"Language-Team: Romanian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ro\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/autotraceback/locales/messages.pot\n" +"X-Crowdin-File-ID: 74\n" +"Language: ro_RO\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "A cog to display the error traceback of a command automatically after the error!" +msgstr "O rotiță pentru a afișa automat urmărirea erorii unei comenzi după eroare!" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "Încă nu s-a produs nicio excepție." + diff --git a/autotraceback/locales/ru-RU.po b/autotraceback/locales/ru-RU.po new file mode 100644 index 0000000..9817f8f --- /dev/null +++ b/autotraceback/locales/ru-RU.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:17\n" +"Last-Translator: \n" +"Language-Team: Russian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ru\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/autotraceback/locales/messages.pot\n" +"X-Crowdin-File-ID: 74\n" +"Language: ru_RU\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "A cog to display the error traceback of a command automatically after the error!" +msgstr "Шестеренка для автоматического отображения трассировки ошибки команды после ошибки!" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "Исключение пока не произошло." + diff --git a/autotraceback/locales/tr-TR.po b/autotraceback/locales/tr-TR.po new file mode 100644 index 0000000..f17a06b --- /dev/null +++ b/autotraceback/locales/tr-TR.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 13:27\n" +"Last-Translator: \n" +"Language-Team: Turkish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: tr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/autotraceback/locales/messages.pot\n" +"X-Crowdin-File-ID: 74\n" +"Language: tr_TR\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "A cog to display the error traceback of a command automatically after the error!" +msgstr "Bir komutun hata geri dönüşünü hatadan sonra otomatik olarak görüntülemek için bir cog!" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "Henüz hiçbir istisna gerçekleşmedi." + diff --git a/autotraceback/locales/uk-UA.po b/autotraceback/locales/uk-UA.po new file mode 100644 index 0000000..65a52fe --- /dev/null +++ b/autotraceback/locales/uk-UA.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:17\n" +"Last-Translator: \n" +"Language-Team: Ukrainian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: uk\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/autotraceback/locales/messages.pot\n" +"X-Crowdin-File-ID: 74\n" +"Language: uk_UA\n" + +#: autotraceback\autotraceback.py:34 +#, docstring +msgid "A cog to display the error traceback of a command automatically after the error!" +msgstr "Гвинтик для автоматичного відображення трасування помилки команди після виникнення помилки!" + +#: autotraceback\autotraceback.py:61 autotraceback\dashboard_integration.py:35 +msgid "No exception has occurred yet." +msgstr "Винятків поки що не було." + diff --git a/autotraceback/utils_version.json b/autotraceback/utils_version.json new file mode 100644 index 0000000..bfab002 --- /dev/null +++ b/autotraceback/utils_version.json @@ -0,0 +1 @@ +{"needed_utils_version": 7.0} \ No newline at end of file diff --git a/avatar/__init__.py b/avatar/__init__.py new file mode 100644 index 0000000..65ae483 --- /dev/null +++ b/avatar/__init__.py @@ -0,0 +1,20 @@ +from typing import Union + +import discord +from redbot.core import app_commands +from redbot.core.bot import Red + + +@app_commands.context_menu(name="Avatar", extras={"red_force_enable": True}) +@app_commands.user_install() +async def avatar(interaction: discord.Interaction[Red], user: Union[discord.Member, discord.User]): + await interaction.response.send_message( + embed=discord.Embed(title=f"{user.display_name} - {user.id}", color=user.color).set_image( + url=user.display_avatar.url + ), + ephemeral=True, + ) + + +async def setup(bot: Red): + bot.tree.add_command(avatar) diff --git a/avatar/info.json b/avatar/info.json new file mode 100644 index 0000000..c909c90 --- /dev/null +++ b/avatar/info.json @@ -0,0 +1,14 @@ +{ + "author": [ + "Zephyrkul (Zephyrkul#1089)" + ], + "install_msg": "This cog requires your bot to be installed on your user account.", + "short": "Adds a simple Avatar context menu to your user-installed bot.", + "description": "Adds a simple Avatar context menu to your user-installed bot.", + "min_bot_version": "3.5.10", + "tags": [ + "utility" + ], + "hidden": false, + "end_user_data_statement": "This cog does not persistently store any data or metadata about users." +} diff --git a/battleship/__init__.py b/battleship/__init__.py new file mode 100644 index 0000000..b56ef2f --- /dev/null +++ b/battleship/__init__.py @@ -0,0 +1,6 @@ +from .battleship import Battleship + +__red_end_user_data_statement__ = 'This cog does not store user data.' + +async def setup(bot): + await bot.add_cog(Battleship(bot)) diff --git a/battleship/ai.py b/battleship/ai.py new file mode 100644 index 0000000..3ca128d --- /dev/null +++ b/battleship/ai.py @@ -0,0 +1,154 @@ +import random + + +class BattleshipAI(): + """ + AI opponent for Battleship. + + Params: + Optional[name] = str, The name for this AI. + """ + def __init__(self, name=None): + if name is None: + name = '[AI]' + self.display_name = name + self.mention = self.display_name + self.id = None + + async def send(self, *args, **kwargs): + """Absorbs attempts to DM what would normally be a human player.""" + pass + + def place(self, board, length): + """Decides where to place ships.""" + options = self._get_possible_ships(board, length) + if not options: + raise RuntimeError('There does not appear to be any valid location to place a ship.') + return random.choice(options) + + def shoot(self, board, ship_status): + """Picks an optimal place to shoot.""" + options = [] + min_len = [2, 3, 3, 4, 5][ship_status[::-1].index(None)] + max_len = [5, 4, 3, 3, 2][ship_status.index(None)] + #Replace all of the dead ship positions with misses to avoid attempting to finish the ship + for ship_num, cords in enumerate(ship_status): + if not cords: + continue + ship_len = [5, 4, 3, 3, 2][ship_num] + idx = cords[0] + (cords[1] * 10) + d = cords[2] + if d == 'r': + for n in range(ship_len): + if board[idx + n] != 2: + raise RuntimeError('Inconsistency in board and ship_status.') + board[idx + n] = 1 + else: + for n in range(ship_len): + if board[idx + (n * 10)] != 2: + raise RuntimeError('Inconsistency in board and ship_status.') + board[idx + (n * 10)] = 1 + #Get all of the possible ship positions with the remaining spaces + possible_ships = self._get_possible_ships(board, min_len) + #If there are any hits left, attempt to find the rest of the ship + if 2 in board: + #Try to move in a straight line with other hits + best = 0 + for length in range(min_len, max_len + 1): + if length == 2: #2 length ships will not produce a line + continue + ships = self._get_possible_ships(board, length) + for cords in ships: + idx = self._cord_to_index(cords) + if cords[2] == 'r': + index = lambda i: idx + i + else: + index = lambda i: idx + (i * 10) + hits_in_ship = 0 + for n in range(length): + if board[index(n)] == 2: + hits_in_ship += 1 + if hits_in_ship > 1 and hits_in_ship != length: + if best == hits_in_ship: + options.append((idx, cords[2], length)) + elif best < hits_in_ship: + best = hits_in_ship + options = [(idx, cords[2], length)] + if options: + break + if options: + maybe_ship = random.choice(options) + options = [] + if maybe_ship[1] == 'r': + index = lambda i: maybe_ship[0] + i + else: + index = lambda i: maybe_ship[0] + (i * 10) + for n in range(maybe_ship[2]): + if board[index(n)] == 0: + options.append(self._index_to_cord(index(n))) + #If no lines exist (or existing lines do not allow for extension), attempt a random spot next to a hit. + else: + hit_indexes = [] + for idx, n in enumerate(board): + if n == 2: + hit_indexes.append(idx) + for idx in hit_indexes: + if idx + 1 <= 99 and board[idx + 1] == 0: + options.append(self._index_to_cord(idx + 1)) + if idx - 1 >= 0 and board[idx - 1] == 0: + options.append(self._index_to_cord(idx - 1)) + if idx + 10 <= 99 and board[idx + 10] == 0: + options.append(self._index_to_cord(idx + 10)) + if idx - 10 >= 0 and board[idx - 10] == 0: + options.append(self._index_to_cord(idx - 10)) + #Otherwise, attack the best possible spot + else: + best = len(possible_ships) + for idx in range(100): + if board[idx] != 0: + continue + test_board = board[:] #copy the board + test_board[idx] = 1 + num_remaining = len(self._get_possible_ships(test_board, min_len)) + if best == num_remaining: + options.append(self._index_to_cord(idx)) + elif best > num_remaining: + best = num_remaining + options = [self._index_to_cord(idx)] + if not options: + raise RuntimeError('There does not appear to be any valid location to shoot.') + return random.choice(options) + + @staticmethod + def _index_to_cord(idx): + """Converts a board index to its string representation.""" + lets = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'] + return lets[idx % 10] + str(idx // 10) + + @staticmethod + def _cord_to_index(cord): + """Converts a string cord to its board index.""" + letnum = {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5, 'g': 6, 'h': 7, 'i': 8, 'j': 9} + x = letnum[cord[0].lower()] + y = int(cord[1]) + return (y * 10) + x + + def _get_possible_ships(self, board, length): + """Find all of the possible ship positions remaining for ships of a specific length.""" + locations = [] + for idx in range(100): + canR = True + canD = True + if 10 - length < idx % 10: + canR = False + for n in range(length): + if idx + n > 99 or board[idx + n] in (1, 3): + canR = False + if idx + (n * 10) > 99 or board[idx + (n * 10)] in (1, 3): + canD = False + cord = self._index_to_cord(idx) + if canR: + locations.append(cord + 'r') + if canD: + locations.append(cord + 'd') + return locations diff --git a/battleship/battleship.py b/battleship/battleship.py new file mode 100644 index 0000000..e7b302e --- /dev/null +++ b/battleship/battleship.py @@ -0,0 +1,215 @@ +import discord +from redbot.core import commands +from redbot.core import Config +from redbot.core import checks +import asyncio +from .game import BattleshipGame +from .ai import BattleshipAI +from .views import ConfirmView, GetPlayersView + + +class Battleship(commands.Cog): + """Play battleship with one other person.""" + def __init__(self, bot): + self.bot = bot + self.games = [] + self.config = Config.get_conf(self, identifier=7345167901) + self.config.register_guild( + extraHit = True, + doMention = False, + doImage = True, + useThreads = False, + ) + + @commands.guild_only() + @commands.command() + async def battleship(self, ctx, opponent: discord.Member=None): + """Start a game of battleship.""" + if [game for game in self.games if game.channel == ctx.channel]: + return await ctx.send('A game is already running in this channel.') + + if opponent is None: + view = GetPlayersView(ctx, 2) + initial_message = await ctx.send(view.generate_message(), view=view) + else: + view = ConfirmView(opponent) + initial_message = await ctx.send(f'{opponent.mention} You have been challenged to a game of Battleship by {ctx.author.display_name}!', view=view) + + channel = ctx.channel + if ( + await self.config.guild(ctx.guild).useThreads() + and ctx.channel.permissions_for(ctx.guild.me).create_public_threads + and ctx.channel.type is discord.ChannelType.text + ): + try: + channel = await initial_message.create_thread( + name='Battleship', + reason='Automated thread for Battleship.', + ) + except discord.HTTPException: + pass + + await view.wait() + + if opponent is None: + players = view.players + else: + if not view.result: + await channel.send(f'{opponent.display_name} does not want to play, shutting down.') + return + players = [ctx.author, opponent] + + if len(players) < 2: + return await channel.send('Nobody else wants to play, shutting down.') + players = players[:2] + + if [game for game in self.games if game.channel == channel]: + return await channel.send('Another game started in this channel while setting up.') + + await channel.send( + 'A game of battleship will be played between ' + f'{" and ".join(p.display_name for p in players)}.' + ) + game = BattleshipGame(ctx, channel, *players) + self.games.append(game) + + @commands.guild_only() + @checks.guildowner() + @commands.command() + async def battleshipstop(self, ctx): + """Stop the game of battleship in this channel.""" + wasGame = False + for game in [g for g in self.games if g.channel == ctx.channel]: + game._task.cancel() + wasGame = True + if wasGame: #prevent multiple messages if more than one game exists for some reason + await ctx.send('The game was stopped successfully.') + else: + await ctx.send('There is no ongoing game in this channel.') + + @commands.command() + async def battleshipboard(self, ctx, channel: discord.TextChannel=None): + """ + View your current board from an ongoing game in your DMs. + + Specify the channel ID of the channel the game is in. + """ + if channel is None: + channel = ctx.channel + game = [game for game in self.games if game.channel.id == channel.id] + if not game: + return await ctx.send( + 'There is no game in that channel or that channel does not exist.' + ) + game = [g for g in game if ctx.author.id in [m.id for m in g.player]] + if not game: + return await ctx.send('You are not in that game.') + game = game[0] + p = [m.id for m in game.player].index(ctx.author.id) + await game.send_board(p, 1, ctx.author, '') + + @commands.guild_only() + @checks.guildowner() + @commands.group(invoke_without_command=True) + async def battleshipset(self, ctx): + """Config options for battleship.""" + await ctx.send_help() + cfg = await self.config.guild(ctx.guild).all() + msg = ( + 'Extra shot on hit: {extraHit}\n' + 'Mention on turn: {doMention}\n' + 'Display the board using an image: {doImage}\n' + 'Game contained to a thread: {useThreads}\n' + ).format_map(cfg) + await ctx.send(f'```py\n{msg}```') + + @battleshipset.command() + async def extra(self, ctx, value: bool=None): + """ + Set if an extra shot should be given after a hit. + + Defaults to True. + This value is server specific. + """ + if value is None: + v = await self.config.guild(ctx.guild).extraHit() + if v: + await ctx.send('You are currently able to shoot again after a hit.') + else: + await ctx.send('You are currently not able to shoot again after a hit.') + else: + await self.config.guild(ctx.guild).extraHit.set(value) + if value: + await ctx.send('You will now be able to shoot again after a hit.') + else: + await ctx.send('You will no longer be able to shoot again after a hit.') + + @battleshipset.command() + async def mention(self, ctx, value: bool=None): + """ + Set if players should be mentioned when their turn begins. + + Defaults to False. + This value is server specific. + """ + if value is None: + v = await self.config.guild(ctx.guild).doMention() + if v: + await ctx.send('Players are being mentioned when their turn begins.') + else: + await ctx.send('Players are not being mentioned when their turn begins.') + else: + await self.config.guild(ctx.guild).doMention.set(value) + if value: + await ctx.send('Players will be mentioned when their turn begins.') + else: + await ctx.send('Players will not be mentioned when their turn begins.') + + @battleshipset.command() + async def imgboard(self, ctx, value: bool=None): + """ + Set if the board should be displayed using an image. + + Defaults to True. + This value is server specific. + """ + if value is None: + v = await self.config.guild(ctx.guild).doImage() + if v: + await ctx.send('The board is currently displayed using an image.') + else: + await ctx.send('The board is currently displayed using text.') + else: + await self.config.guild(ctx.guild).doImage.set(value) + if value: + await ctx.send('The board will now be displayed using an image.') + else: + await ctx.send('The board will now be displayed using text.') + + @battleshipset.command() + async def thread(self, ctx, value: bool=None): + """ + Set if a thread should be created per-game to contain game messages. + + Defaults to False. + This value is server specific. + """ + if value is None: + v = await self.config.guild(ctx.guild).useThreads() + if v: + await ctx.send('The game is currently run in a per-game thread.') + else: + await ctx.send('The game is not currently run in a thread.') + else: + await self.config.guild(ctx.guild).useThreads.set(value) + if value: + await ctx.send('The game will now be run in a per-game thread.') + else: + await ctx.send('The game will not be run in a thread.') + + def cog_unload(self): + return [game._task.cancel() for game in self.games] + + async def red_delete_data_for_user(self, **kwargs): + """Nothing to delete.""" + return diff --git a/battleship/data/board.png b/battleship/data/board.png new file mode 100644 index 0000000000000000000000000000000000000000..661b19a83a4edb571cec82025d28773f4e024308 GIT binary patch literal 140315 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

R|V{6b*3{}MP z=Pr~MQ5F6vZ_!2fz!3Iwkt5aALG{k^uL8EAcs-17Ff1X+>^(`1%fdWNX1Jfrg+kAc z1p~3CK}8Ljeb-nf5Hd~kH&s2qk4$*APcUju0-5tGAOnpx_rBEtv$b_-)9HFWLfmCm zd@unfHC`?mYV*)pGID|{UIivJG4qV)Gj0F zUQT}th3?FSpZu1K8(=;M9uhJ zeaT-P7yg!MSM}LPw`+jg-Ii=3bT+R~*gA(DF3`RKPE@=RmlECu+EDS!j&S?IU=k6c z`1cIi(-(D4T=4U>!87Fp#NkVs2V3^#>B@!_9qJB4=qqBS`6kM!DNl0V7yZ?cvKjD5 zY~Y!%dX6Roi`6HNY$H3z7{WpNUUCNpk^D;b?}oAFFSh(1mP;us0*UdLe;-^-tsIDH zS^OVQ=N--F`~QEjH>IVuRco)-juEOYYPQszBh;o=Rqa^>tzEnJjM3I!QM4qn zMZ`{s5q$Ih{LcCPo4;~S&Yk;py{_l;@xT$qIX2w9j;0*fy+~+tTW@J>K!nONRKl1v zZn1durZ3PbhmPxcPv;i=pfUgs2JA%%%{T?760Jp*OU5fqHSmcYiCcrlYS5XWVFY&6 zDhiOw?d9y4kNbqc0Bh6m8(CqqcQx}TeT|~7nYVim&IuyoWHmQWstBW5$}W6tyL}nE zU|zPKR|mHY-(PhmT<+9MdIXXnhDNQ^Vk|XdS%D^WXbq~d48Vp?$PvqqwR=;=d~E0s zmYEKw|L$x)L?>b*xqd$OE_8C%ph$bZslVkw8)|23(8KmFf`g~G{p$HH5ilwiAN;;G zkE8UAVk&yOM^SVBRZY$5wF8C8t^qJ*Fh@ftl?j_HIIHDw^GveVYwIaAUE^ueOr@7~ zw?lw`Iy|HF41Vg{&LOl_hk*3qH#W*w52qdVptpj3-#C(wVbGRk#^YF`Uf?fIB^6Sc zhFCapIc^&lmKy}}I33Y$pMDyeI_$HhF=E|qBvw;bk`e^uP3tMk{|;LzI56r0n4COR zUdClfE1_LlnN)l5G0DfMi$%qI-*{iybb43-z|!)5{m-syrwdXe3YJM2uCAQT=Q^_P zxvZ8yjI?IMc5i;i`H%5N^-d3~O4NUdl$k15Sc~~CDT}sqr;O)Pb4v}aefUHA*9Ftn zI0#X9SeqO!sE%H-D)vqf4dnJ?5V7@+no0qr!U%)hIq~{`Ov$Jrw|DziYTjgQbM;uJ z!2h$x#E;kq4nHP`CT?4XMpbweGm$&IWXP^|sPc4XsmZV>QOXl~)oj_fwB%T+s0;su zzkNvL$R}`XgzUlnr5E1*VO`L6SVWydYhjP6@_TN|_NHcU{B2u4uz&%of&iI^npa

GwwM zWeKzj5)}4_^G@|k3I{LvK44J%6n2ch!bqzPmH94SRDA6}3+s zo-kJS@^XUsYF^9im^tt2b?WtQ5dAr9;CYZLS#BC$F9?!#&vP}AAYhZshZdgv+)CGR zHVNDP((ZV#lvA?dPGkwr&K0luHqr7$;xMD%dcB$|OL7vn zm@5s!1}LQJZ@hnLi#PftZ!fCk@%Uk8%VD8L)G}w@3EF?SqDWQ`v)iUNcJL=xVYncF z*hJ%}RLF7^CKwXFju_!fl$hpd<{x`nFf96?(CbgD{45Lc+r7B~@^^wXWTXO*7e`F^ zv(|92Do5b*W7N^WWhC$Y$lMus?KcwJT?*54F{58U@YS6<-A9idsQ9!%HtkQrS2)9I zji;EKnH2Ar0GccFDR*glB1-5d+OJ7VNdLX&L3szXPGA{?oz{Y?#-e+onAYHfFAWY& zPuISZ5HMHjzylkKx=wRuSG@xMk=W>)YLKK(ibxW|D=z?wb3-UUV3<3HG{>k?x_F0PGELLv{t`C|5L*9==ffoCk(7c5 zs~eU9+~N|Z(`<-eEH)iI;LXC8KvCqOQ-*yu{)sB@I^mk%;q9uFh(?4=`&hh3>y!i! zp&`K&*1nL^?9+5>KNyhBH6z_RnmorjrL_%c5pno%U0D%9ZHzYWmD*O$=_f+MR`?}Z z0o83!wBL~MW_(vtmn2fn072;Y|F8>}3DriTILUkac$OsCM#p8ZolLr~2U92<4_Hph z2@d_cMv!vq>W0=}lA9^@7`A=+T+NrY?){xAN!@ybh=?zt^&m!x6Oz@&t5isRi>|MOZ_$ zc!?Tst8_Apz7l8*h0c3iEj@D{>zBQEf9C(n6|TPqEsB%so@J z4X*)UzuB{GPi!s-CNc;GP;Z>4{*PuaeU?q3;3*b*t_bDM&jmAPlYY9(3x@2lCC}Qs zG`>~9FEsaYK~-53XU(t{-Za&UhO>D3@H-9|hVAjWz&xYV3R32?*_1r_Q+Xff3!1-X zPkJb|+O2|Oa)9|>e4=nKDVzScrc_cd@vp>0DVfM&q4|XF$B|4f`uj0w2vNsS|v~M`kYNA3tPUGo0FL6jf2fnF!8D#V(}d?mBQ> z;TMD|oq%7BvLMfBs1tz2*q@NVh{G}apMkdsw+_iaj!WAVrjN_XMgzEj(tj7FLTcJuMHhM9IpNUF|@I4sMhV%EWV(Idj)lhHo;XyMedZUUfoOkTh1> z&QO;j|HEUx#mCfen7-2`1QvfKkT~0A*;kDxPRoPD(mlt^@1RS9-31neH>SxO*WOT^qrvL1*o!O0xqp+A@y{XKk$JpmBI_Cvp!gd1pB>t6Cg zXAR>+D6Pb2C&KzuR77#Ry(RO_8P1{GEX4x(&Q>TWg&6i$9;eT03*@db*pO4{C^Yn6 ze!*e&4X{3>zl(?`V|7;2J+nIS5R7#PGSdeo9M zP1|eYaBF|03@_pOwYY~lnyz7_D*&}xi#sArdwDF9?B?q+Fr4l{MGLX%qN1~;rS#hn&jv{)GjEgb* z`#0pHspITaDaAQ#z#%{?w?s-6fzwUTlX!|c{?Ma(3R~?%H|0xhhNlB8_dJ2xej81R zId+q{`67JDbF_<8b2uQ!Dwy~&QM#tl*VjB1rzu_V@b5S>9KXS-tB3BRvO)Vc_^K=a zVF4-q8*{OJZRn$q2u>h#;F-oMzeab`=g-@b?`5$fv85Lsu;bO}{AIdShUlb-P= zoSdSRCnhea{_^%sGzH;B_wqPVr_A-p7CN^|R!~S`kiHF84m1DWUv;IX+Fg}rie>-q z@&68+fycLBy#F6suLg9Do7Sr8p1dbEX~sQXio;WLmqO#Jqs(#KERXzb~0s|=u>DPhwfKe1dwpUFo?Q!sZU)R5|qFTHkn zY49K?lIb)u*|&4uiHc}{5((<|6l?5N8u^$lau(%Dl;{8`cX8>OQ1P?QgFvPcSa3s*^}LV#?Q( zcb~?P)xHI=&ODv;Ro#ILm4?q8kb~Kd*q?@_J8eDVR=7XE?6Y$m^EZ-02&uF?(X*Y8 z1EX>ul=^C%xz!}u%ZbykzWJKA#v%C&ZD+kw>W<-rFZ+>}H?E1Sq8h6#S36XUnlJwO zA(Y&xH4s|tvR!QyzUDufDy@>thwaIQ%R~3gnk|GusM);wN%21f12cNZVSAT7Sd;9 z)g<{prt5yJ8eLy>ji98ZEX0$Cu&&YEBx0PAyV?%p8Ma(~9Ess~TLV7reqWSN1-8YY z=@^RnaZO>47jW)ZYLA%I$*+0+F`<=^3ZKH3t6lz^Hopzd3&-fLiQSy%I8Izt@6V|y z!#|0H$)_jrlk^#d7~ryB-!r}u`-D7eXW#|g+C{bGz03QZag~sTlu~Ar$_6WWS5|Ei zMe4@@(o|KJIq=c|+Qj(;2O){2+1&DdNsGO?Dw<8o)2^6wS6UGkINg+xwC17^;kL|udial0Zt64Iv@uk~F*7;)Kng`6G(uE{^05KZ3Z<*A{fo)>@KFEA`-J=1?n*nxy-$Me# z=*G9#i5P25qxWaL{(!eJi{Y-%LzY!`xn7+DIS12)x`vHDI4vlM2OV*UvIiWJ&18PC zf3UY2aqm=M9n{7Bdc|kYacMBY-ZgH--BAS%th@-~KM8PhMjn0F=)LcdQzlgQ>kBHJ zRaUQ#HV^T^j6BEf-&nwMUcCPUd{k<{WUQT+>KXz}LXP~n;a$1WQT?$4t6K0&3R?BV z_2#FJ=InG-LvX|owbbDUilfNa+&&#Hy$R*2LU-K3wVtqD(;l_GRa6*ZQ^LMwbv`b% zmvd?qzxQKF^I$}*HI2P^9E!QEke(Y)g6XD z+({QCt%6GGGb;*^JTQx44!l7;`quN$Dq11WaM3nIu;QaK}pQdYhjN|W>ZE1*7A zv-J0JwjG))vg|wYI|GJ6=WSj@f@qq``bGGjCOkR#tI7{1aG$s_K`#gKQ*~q^vcfm= zE~eXzKk?zM^`=fSA;1ru;L}SbP?}$0TLruGYlS>>dDxaHCMR)cw1xS|AUuUqi?jtT zRZ_k#oSi%^V-oSi+(|$(&+XuY@ns@IZ-fQO9Q{(6>GMS9`irbX0nn=P-c~M-p%i>U zF3Sr@L$PPC$L00Nz%rsK4Oa5=Q-cUq=eJJUbujBN?>)c$WIFKgX}$yjWRUBhZe91T z?%a;!K`4wk2_cnHD5MKtrdUbVPp)S4Z`_Lu(5w{*9HEvE`^SG`?IzajJScv0bdXT5 zSr!SRmzoUeyf}J5@|HC``Eu@A96O)Z+h?{RlS-G!7vGj-s5* zxxySJ8tM}9?nSNKA7hqF19flJP%woleF+Lw56NYrq$HH%xaJi5I-U+iQm5gql+I<{ zL8+j1(J+NNt|jd~Tjstb-7xOe9iw(oAN$VN$I_mMy1wGG4=c-tUleJKbU?M+TU7s@ zhZL}fmh5G})5&c|G6z%0El~{#{DlNk{F0M3e&fX{Jh?`YJk8j+B{FjGUtMa}A@fzC zFW+_2qHm88an6#)y`dl}#pC|MKgazoD8e=64GvsZ^lqytIDg&sPXi=quHm?a#f#vB zAS}xhhSdE$PoV^nuJINm$~9G;-=E;OFr4S+a%R6fZ|*+v)Vg|1q-ll;+)_)bNsZd^ zH{KtAOTmlFNYs24lybv2wD6@H>m(rE&Ail&1Fg@$y+6yM&nD1Cm+m?yK78m{{V0$s zjbD3agFhm*uFo>k{J8q9DhRDkhxK4kGyisK!4cDEv5b$1&s{})x*2~%-pWiWG+Dg% z35wq0U*G3e6}UMw)b3J(#vce@;Gu(7X=$(dC*G~XnX}}Me{6MlSld`EWxHNtaamG= zQhS#-lJ&d3Kh4z_6N275S+-S}6966ik81B$hAufm>5XB*;@rHw3_ZLdYW=ZFMD=K? z#~~$yFD)4CIKcUYj!*oearlh8(8G=ZbK|0{0_6zMwQl9%A^T3a$!tUCma#3a_FWLq zWarcrA2ta*l}Mh{(-u=o)r!Ztw2=&x09dXx#Ba@@Gs zv{rpr^2QKXGx_bF7L(hFA1bax6T%AbeYX4ZAW}O}>+yhotrYFjjK(B;_QW@Gh_ceg zwii~B#0Gbf2=2pnVxG@BnB@Nynt^;9KtFkHl8 z{mS3N>rTlk>9%r3^@G{+A*fmlo~%N`hnMB?*LWbWTxl*BXe4?4AK7$^7`FQ}AAsEj z+e-oDuLBpkWeHBaEqBk`J1z1nW7kr~nbe=zlE1=b>xIrsIRNv$b8iQ1LJvKHpsW9k zc;=8t2>%_ifbQ{~^sMj-Tf6^hPZ(Li5hPHl!{Ey3R$~RPfwh=te(jEGAB0fbm zI?+=|k=gg*suk>smgdRBJUq~z6~$CCF22||oP|APUL!dfO*#KPC=2y#XOZvJpDI7G?GXxWvo#zqhnxq;EWRLD{DC^rWV~9 z-qPLE|8W!+?^|3g^*+0ua9RypeL6Ao^~aRy#YQN7i|OX9G`31Ix4IUfIa$SAIgJnf z%V~h(_vuZ6#byQF?rhyPG!aW6wnBDnY*>AeVdSR_*)6=8YUQ`2RjEfEFtphcI=94i z27J+I9$9-5Xtpn5P)=-YIC-JSz_e_<<${S489BeAk=K}Ti>lSA6-sCc8k$JqUpC5L zut^PaKQ9yNvnQ=&?D}g0zO#_cec-v5G!*#iIgr5n#NnOv#DkCf8|qqqO7Tf0B#oze zSy!g{RBiOv{=?HVrOWjXr)u$Pz(+m4@`%|KXN}6TvT%!2v-ZT4Xj0v?VpQOpP_dGd zx2A{ZyzMe#YZM~?>_sBk*ZXo86v9}M%A`rL9lW0t`{lZIJ4PATZ+w&G1_`#qeoXQD z{l=gt&YvaB$RQspLq=<=KC_>G*HWDO=|Gi!9o~_7?+aKCFC3AY8+6+!5XaCL*Pa@- zZv4Gr>se^A-kJ`OFTK>Uk!2V1X-m3mTz^_UJ>PBJEbO7}^MN*hVqJPB=sOoaRGEwo17u#~>310R8|)0C?EhM~Z8?}XOX2TNp@ zg-0o(XzrvDnm56jU$MizJCZKnm}(3z*pCXyQN20tRN}-WV*H?Y8MEFI=Mr0w7}&a@rXbVJw# zxSNb-uv2=6W*83m5Z_?!vP+Y2mNvUprfr33o0!oKt8{BPJ9^6O)w$&lP(FI@bX}|myeBN&gnn6v4QR_KEe~zD*ISx>>aY? znQW9TlrMhQ-58v`p#a*BFRey-QzR5MM$_G1db0rk%^jwFz@pSr)(x8A5R3=T*$v_8 zY0|61gO2XKVFQ09VpIi%dCwIiSi|`pI&rrI_yf8x_yaMWxR6hEE?9?788gjTEW;_n;5+SD}aP>ij*FJaJ9p9ty-;!TkHt zVJkBX zg)uuj{7hAiJQ*{Q%yk$L_b0jlNY&?Nwtp`kc5+mN#+}WdE?qkkkks=$TI|B+&nok# z^(l+z7RkcFzP6#!t9&MvMfh$Hnyiekh>X*8pzB-`2_IL%N0FD zyv<+srT_y;YP`0LWnanL5t}k^OquU*&a##?PwmSO?Gyg*{kwNUy(bz};KPN#*h`WD z?7Q5KISMbImPj5y!otw}(l+F!8P*)!e}pMeS3}pJM0}Tz zzuz-gmG3qI<$bv=HoCfQ9@vdYQ>WNcOU=8gZd63@tE6ZPJT{jBdU7KR)R3rGB7aACmcO=mn`&o!rQt{|f zJ~#Wn_Kp#l=E3Djm&o+uZYW;#Dds;-n9KzhWUKBJz{w71t zUuU^7B4ddEZwUdV?j zy8boV^KgwAJ6TCQEz7PuyqgF12-kgUP@LGE@Q$nexmPXLwHUbRhbOzwQbuAug}G?i z^gpwkb$5O~K0afgV1jhdvkqJx&{3|DscGOUV|0S}fI+(JkR(LMC4+7tvBNk{7KwZK z*L-eam`HO|yGEzO$Pc^It*JZlk0)QW9TQB}5&fJy;& zKuF|5#~zMQM7W!R(s#Z5yjEs~DuAfqJygEO7TgV}Jq_hw4@tx|yf`L&B@|Tte**TN z6vaYCoJ>&x+FxgJ{D@qBQJ_z4=(o_`qrLOgf*XTGxy=i!6m8F=Q$EMbfWdRoJq;PgRLmz5$-=c2RP3c zO{Y@!fVXMy`_%$AjFO0X+K#uozkYDcbJW)U#4BuUGXS;cA>hT+l%qXwZnon$Z!_uW zYU|d1&C$3w@vhR`{@BC$NUr$CTkY`k)H0H_=a=x#Qaeo6v#XKyVBz@IV8CbK#x`;im9-(V_dcz8(GcMTX zC#b&TvO3x(8?Eo7y4R1&k_lDML#Zq~ZMwdnpZp={5;*a7b67p9x%}|v@e8wC^`1XS znSX?f@;lU_Hi*F1zZ~;>%o*s}q}H__xor{@J2}mZ6)f?&GvzH;?BSFwrT9K!0`Dyt8cSFt?-aN z8&$pWV>6f{PfODKGTg3M*ZjQLe~`Ntey* zA~B&t=R|`I3FUCgYss=A)n#$suY2VA@k5IzMXEAyC9;y_$}I9ZSBm5u#64MJ*?~yL-X0 zEnW>*k1m}}U0Nu|GE`xhrlZg0#s_){>M6~Ee$1KtCwHfNMeI*G=i1crUE?|q3h=Yu zjY)J9``gtNXr!)rEwV^|365Qn)h4!R(6;1@R19S*I@f`$Hompw7`SLRM9#-Q~ z;0yq+sBA}>#DSLW~byz;1T@3VGe+X%fzI7ZIB z%Ak%?uMRJ!SPS}q(HFRLo_}m%ObvUTj$HUWcq=BF?loDd0GBsamNyeDrn>0Ow&c`B zAiUKR-6aumMEel|ygKnbtIQ4MC;`W7!&zZZ3MIPWE2U}S>DBiN_B}5)x2`c)`E`u` zM8o1u!PAn|n+880Du9$STbF+_`K<{v#j9e}1$D=Nf35do|Lx5^Db+Wq?`aTG412IY zd)zP?_{wfuqTyhhzWxhhr}5ppB-*cM}z-U_!R^6H0{ zCTV=}f8MrvVbpOt?{1{VM1PCMwn7>7$0JyLOQfWH`mFOU2MfcbuczRtCkt=VruC(T z2Yiw}_qejmKFV$2cSjn-tS?At6*u2v=pT-oFBv6wbh*7*;?Bp(0&G-`PO_Io6VQs! zZ(=SEL1*lHbOzdc#upo~=Go$;H+3&9<>staJfrRabdv9&_78q1U1TeEE^JEkYU@a> z_Z7Cp$T;19+Iz!D3-W|17e@Hm`%H&TM?_T>A=bUFbA;ZZ4BJ~ zNM^9iUw3=7t#}YPT5ZR(_@tk|m(hyV0QP8f^-e^7I}?K9MhvromDuXCHcqYSwnSSRkk%3+#GuwuZ3xJe zd4~*23O_xfE$lkByy-z?OybO2^?M-I)wR6(rWHwibFR3qP)>bGw$*tU-rTy>83Yfp*v2e1j$GbYag2?-pS*`eM8pbAxzTi{!YQ=SvC3BbRm5#WeX~f4i zS6)5smoM#}o;LhOxkk8ol%r;lO_8FQs2o z-S>qqwK2D{&&f<`RH(mKX(arN6s~-h&2AH2i`a@*&Qd~eiRyV4hRCm_P%UQ}(g?*S zM(rMohTUnImYFgip0=_hkBM^&fLxyqcy=;;V?PxbJTMOE>lg3hd&98QhIYHfL>Bp~ zhw%Z!?YcZg`i~vuU3oRl*;jW#-TE zAXFu4jU+(lSg`zJmE&2zjQW1yd)D;8^UZ8U@l4+o_Je&qIfA@&f1w`z;;t(m_9{jq z5&1da0cja<91hTD;e^b}A{M9G1t)qaJ9f;JPV%n$77uxYiU*h|7x1O}t0^?gzDr!? z%;o*AYrx#)wCCWf@c?pD;KHUb(|S<&evJ}KhB0vic1cF!?)SW~OKdGx(^D!O!D$r# zmg!nyJ!)RRwQS$|@Mhrpum0NeCXW^)woyITS_sf}P(RR?9mu<09c*69u}=Ql`5%z0 zB7Z19sE(J7mRv6|5}q$i3S5?i|Dmj%lBh#`0lqZs%+3}Q;$**kBa@t_Q~}{=pQSddZs1i*>x4*Jbg_M&#ofJo!u-AWpXwSXf}JlRy)q1PO27%`R-Z+( z43GE7;U@0R%$(LB)0zx77FK?pOW!L$6=#<-cHUje4V6D5p0OfeaPiLXqvCfFzZ6&- z{>EGM|7nFhu?8^?K+jd3z}aZ=Hs4}@#gFdSt6JjFz4-s)nEV|4fZL8-;xgGD6%iug z4JPm<5syF&xLS?dZ`{~~4fvg~TY&S8s4Vi@O_R^-h>e1s31Ot{fitdJnG-b>uFEC$ z(1Vz}fKedGu^P(iosZhBkg_9NzI@emjf~<4qTN5d6`YgCFQA5&2Gfkft=zyQw{kXW zvrU6Xay$KW_HM8Pg}%-+tMMOHmd^KSMyi{sKW38jFHx6jlzCzs7-r)dE?&YvvfTVH9P+Qb+u@W=)_RbR z)%@Mg1Y_pU010%r*P8Lr8GHR@$U$B|UgmS(V0uWV=DGgW~lVYxFN zz_W`Nf}R3yrcPf{Z%oR`6Dix zA?conca2f1S0BYFSfw|GM_>E|8}Y3}Pl{b*;h>{r9blit)y+4>=YP}%kt0N-fbD#mbSAz`;cY3ud&l8GNYyAe$+Lclp`kE%oGd$^@xBE8;>y9`7UsXE3ArE^K1IC4 znpf8-BUi~LyEBK^dDERmmwcJeK^p;o+qUY|s($G(G`wxuyQs)f+Nw(AE5!6v@j|uH zj#gYgkwT~YivbL@Mg!ZP)_P!Xl7dy-5O+;8c|FGtQz@G9<3^xGC`Z{R#?Rru+9{wO z*W0SD&wh%I2G#rcJz2uZt6LCq%C_lfQxk*0A|Z1klBcesfPISB%8x^bJWt1i{wnjn z<2uwavJEfANf=L?h_F-=I6JX3GTc~R*@eL6H?(k`y0eAT#i=q4T>(>>#5G?EiGVc! zqy7nXZ+`LG7<4z6b6#DL+LDJY+~%qcJ;r+@vsF5D65`txx{_4f5~TTu>W%s03^wM= zxi)FDenTE=9Se#Ws{YkXVg}+@(+BNF^ATQpgXjcoC zIwN3-_t}5x(+_uDJr1Mp-a}C8GpHx-oy72?U^!%2O)sbuV^$t}WwEJFzLEH- z0LQy}i9Anlu3OziWrGx4PW3hRI?SV}GVtxE!~P!z+k(pYnx7({6)l(5e_ZJQbx*0! zP(8PDmP2h@Z4EBr{;c12(CBQa0%z6Q0qdC7LRK0>G zCW*e8=8J13GPZM>bXeEE*f__%oJqrk|Inx8DJv0 z;i@C>uoEw#2+5KCMqkj{xaTwgOZ`pDyRXzA7kciU^}5ht)5-fhKE1hL!MPZU6&VVc zup`|?AvU_(ubLvAQ@SB7)AJT}uN<4n9==s_0VNwfTY%v;tgU!mid-5H`Z zpPWvK_O^{Rs{J&z%=0{bUs4cPW4&zp)GAP5<9EB3i0DLaNX*Xbd|5-!p6#2GV1`22 zegh2rl_rOyp!iV_yi1l*iFmy4LlQ&2_sM-x@eTvo8NpM&BR5L;_M0u+28`abvE7Y) zb$IJe95}O62w*^({8ZEdN%`T~d9tGg@BjW=v&(0KjR;+9{w-C2c^>=l!eU?shZ0#Z zV|JKrgZP<0XSk^)KS(RVNDJLfIz$ODe3AfP<-`&T+iFGRDaK!bd(;Wsg@Z4?u*bko z05}8HE)iMK`|vh!woolUd2Phd4xrf@wz`#ij1D2JId8o@l;HREywoAc$wFz6M;(qB z-3jU75Da0@1(Da{fAZn31TNvCyn8htI&h~8X-`no4p-iDtTFtfp~)u8oe6ri!v0Rw z=sS&oUZ;g&FkYjWv9aTyN@F5kA>b_IJi|hAo z?Dc(D2H#~0Z_;tI7DLIm*6)=zFq*Gr2+jqzdzo5jM=u1b^9$xyIK^Zy+>3WNE zv8DBldZ9foay82~49uA72r@_LNt9>^{Pk1n4Sc$W#84tRuYJ+^uYz+Z1H6=qT)0pT zKYm2V&HBuRt6wp-BO*EGC)cxt4Wf}4>3$E-_U*XX2(>+JM|d&^EseSd zo@$qUg~Y?7&<_YDd%yKLaVwGUJhInE5a>AO^mZXj8CHWV+8x7!cP$D8F{H;l6`)B?@LP4`LKVVomGFyAjNvq;i6J;F}AjtoUZ=oAI4dH;=e ze?s_|H~z#X_PM66nx>+fCWWFg0=+^1o4r(SR9EAJ&`$wI@ehjKb?J|uS*|GjiUC8q z$9iO1FHuQ?|Bnvgq(oeL$UIB{d8`R!57^!SCoA+DcGdP1IFl4{9pCg~mb z#knhqlx7gN)~MCYcQ16*{l{v9<}P$wp+~kn=o0-o*JYnH{^fRIj8qp1aVlqc5LtfS zVy)lpGpuoh%9=JF?0jYGmyZ%ifzX7z{Tgd)Gm=;pAhpEBCtmpO-c;kX5jvh@^P>Nz z$!Lc;;=B>36nb`*|FC;lj_zgeeaoz7skgo~qtGWeCnL+fk`9^Gb}pVP^*#%MAm>to z@sUCHbMvuhf}A`uD*Rsahc`CvJHV|M;htD+?$B}Mc~=X(V-MmUZoOl5HN}?+0$()= zh5wXr#LLYQ|6>-88Il(;6X2x~@>0N$qP9kC+&!g@k~Rj;26{YVPlzH{avxjY;x!CDoZ9xOzC>dT~~|Ay~O7tg5p*56NghzEn~$|Okzg7 zRc(eD!|sqoWtN-6Y0oh)xI8`vzm_V^>W*0m|8iCHei2HqY^4%$-_2C$nZ0i%jk5{5 z5R(tN3U9byW)W%=nHUVpGVR`6z==r!^cOm!Sd0)QQ`%7r;G#R28;^4yW@qijH5xnQ zhyGLv@r}3V!l+M72l-{g1#2+hP2In+07j6ULE3j2^w9nuj}Y2YtW)BPhQ3pL6w~=`T- zE83ec^5Z3{&94?o7eqQ*oHpOHm-uL)p)I4!Hv4&%#Qpp8E}h!I`s2HMfr9xyRim5ZAJ(W9%<}$z7yhy4X41c zc#T~EETvS4&GlU*2!1Fa#2~*coETi(As=1M*q2J`%8%9Fzww&C-Phm(Vn&b*!i|qZ zE^V9mRC@mtZmX~sE9p{+%Ua2zJOEF4yp0`RV^mybQfSIIct-PR#-{s-lM}pe#=JTS zL;O8_ykq>sEJO5&Ri+g@mt7E!mh5zTTAn-vAm7#J;^=XbvbFg=x2@LA)(AOWeFazQ z98_k=Zt~XI(c`aUFO#hr!&48rRH)CmyfuBOI_Z(A0QSIBXC5t{Y3=NEc9R-5jj^7v zK(I9l-{54MisH9m<;SwJywhr~el*woRq1z`S&Vb|%(GrBeAno0)xZjbR`mlhZX`Cef>^XIN)(!)a?6TfsGriafu3ZjZ^Z za7F_2Pv9EdQw>2Ly>81Sz~aq-XEjo*P#9M^7XPGRAv2{$q=E>SV(~2lwTw>?H+1Y% z?a)N{Uc2%*W(T#+Q58{=l#?9N7Jk>ym*|j&f8lnCVfx%l|IvH{=pdaf9D<=7-C$;5 zd8W62r%Y9^76igF@tan@P$xbKck8!vWykqemS+AaT zI}7U~+Vhf#wY1ybrp@+ypRCZl5&G^tUvZ&LjP19T%uHTSl|7}7_gmw(4!^y$v#8^K z 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%0AD7CgII(8)_ri!!qbRO*(S zKxGMLS47%ksGjo9twsRj=X(Ex>a-c9Vu^99E6C;RAzN{aR;${k#^6Qd04R1MlCS6k z3FOk5oEBKyAQAz^VHNPLOV@$aF-_$ijXr~=UmL-MQ-zu4wenu2qu*F)mH+77p)Fu? z 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

FBrDie)nPvdb7)0VbiNMSDwc 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 Xc9dzx4(rXr00000NkvXXu0mjfMFcv$ literal 0 HcmV?d00001 diff --git a/battleship/data/len2.png b/battleship/data/len2.png new file mode 100644 index 0000000000000000000000000000000000000000..168c76e03e72e751292a0baee5db3d367461f960 GIT binary patch literal 391 zcmV;20eJq2P)5LUFngwerCJ4i4HjjkcZJFIZD#L|B&X@QZ8?X`5wh$BO zK02_wTR?5Vtt095YJwh)C+jH` zR6$js;b>e5hT@Lm|BCH&SX*clR(sK*P#sDY7#F;{NujE-Z9$VH)nY41ymJhN@2hoS zgv8Jd!McLN_~H|=X`1F)mQ}FMp#ZF#4KAU$vu{Wj&?0pUh)<_((`w379c@=odV7hY zV+m{H9IdNIPDe$ZqoS_5V(z1&Rz~91x}u(5L_LqHE>b3iedQYl6F4VaBwYxYh6QaS l+ylhjUvJQ^(|ZA3x?I002ovPDHLkV1my4sW$)s literal 0 HcmV?d00001 diff --git a/battleship/data/len2destroyed.png b/battleship/data/len2destroyed.png new file mode 100644 index 0000000000000000000000000000000000000000..7456d36472cbdcdd30ff7dfa00dc380b5f7ce0e3 GIT binary patch literal 2885 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 literal 0 HcmV?d00001 diff --git a/battleship/data/len3.png b/battleship/data/len3.png new file mode 100644 index 0000000000000000000000000000000000000000..481a1ede60b633050a98a78868459471288c4f1a GIT binary patch literal 570 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 literal 0 HcmV?d00001 diff --git a/battleship/game.py b/battleship/game.py new file mode 100644 index 0000000..ba2f381 --- /dev/null +++ b/battleship/game.py @@ -0,0 +1,366 @@ +import discord +from PIL import Image +from redbot.core.data_manager import bundled_data_path +from io import BytesIO +import asyncio +import logging +from .ai import BattleshipAI + + +class BattleshipGame(): + """ + A game of Battleship. + + Params: + ctx = redbot.core.commands.context.Context, The context that created the game. + channel = discord.abc.GuildChannel, the channel where the game messages will be sent to. + p1 = discord.member.Member, The member object of player 1. + p2 = discord.member.Member, The member object of player 2. + """ + def __init__(self, ctx, channel, p1, p2): + self.ctx = ctx + self.channel = channel + self.bot = ctx.bot + self.cog = ctx.cog + self.player = [p1, p2] + self.name = [p1.display_name, p2.display_name] + self.p = 1 + self.board = [[0] * 100, [0] * 100] + self.letnum = { + 'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, + 'f': 5, 'g': 6, 'h': 7, 'i': 8, 'j': 9 + } + self.pmsg = [] + self.key = [[], []] + self.ship_pos = [[], []] + self.log = logging.getLogger('red.flamecogs.battleship') + self._task = asyncio.create_task(self.run()) + self._task.add_done_callback(self.error_callback) #Thanks Sinbad <3 + + async def send_error(self): + """Sends a message to the channel after an error.""" + await self.channel.send( + 'A fatal error has occurred, shutting down.\n' + 'Please have the bot owner copy the error from console ' + 'and post it in the support channel of .' + ) + + async def send_forbidden(self): + """Sends a message to the channel warning that a player could not be DMed.""" + await self.channel.send( + 'I cannot send direct messages to one of the players. Please ensure ' + 'that the privacy setting "Allow direct messages from server members" ' + 'is enabled and that the bot is not blocked.' + ) + + def error_callback(self, fut): + """Checks for errors in stopped games.""" + try: + fut.result() + except asyncio.CancelledError: + pass + except discord.errors.Forbidden: + asyncio.create_task(self.send_forbidden()) + self.log.warning('Canceled a game due to a discord.errors.Forbidden error.') + except Exception as exc: + asyncio.create_task(self.send_error()) + msg = 'Error in Battleship.\n' + self.log.exception(msg) + self.bot.dispatch('flamecogs_game_error', self, exc) + try: + self.cog.games.remove(self) + except ValueError: + pass + + def _gen_text(self, player, show_unhit): + """ + Creates a visualization of the board. + Returns a str of the board. + + Params: + player = int, Which player's board to print. + show_unhit = int, Should unhit ships be shown. + """ + outputchars = [{0:'· ', 1:'O ', 2:'X ', 3:'· '}, {0:'· ', 1:'O ', 2:'X ', 3:'# '}] + output = ' ' + ' '.join(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']) #header row + for y in range(10): #vertical positions + output += f'\n{y} ' + for x in range(10): #horizontal positions + output += outputchars[show_unhit][self.board[player][(y*10)+x]] + return f'```\n{output}```' + + def _gen_img(self, player, show_unhit): + """ + Creates a visualization of the board. + Returns a bytes image of the board. + + Params: + player = int, Which player's board to print. + show_unhit = int, Should unhit ships be shown. + """ + path = bundled_data_path(self.cog) + img = Image.open(path / 'board.png') + hit = Image.open(path / 'hit.png') + miss = Image.open(path / 'miss.png') + ships = [ + [ + Image.open(path / 'len5.png'), + Image.open(path / 'len4.png'), + Image.open(path / 'len3.png'), + Image.open(path / 'len3.png'), + Image.open(path / 'len2.png') + ], [ + Image.open(path / 'len5destroyed.png'), + Image.open(path / 'len4destroyed.png'), + Image.open(path / 'len3destroyed.png'), + Image.open(path / 'len3destroyed.png'), + Image.open(path / 'len2destroyed.png') + ] + ] + + #place ships + for index, pos in enumerate(self.ship_pos[player]): + x, y, d = pos + if show_unhit and not all(self.key[player][index].values()): #show a non-damaged ship + if d == 'd': #down + ships[0][index] = ships[0][index].rotate(90, expand=True) + img.paste(ships[0][index], box=((x*30)+32, (y*30)+32), mask=ships[0][index]) + elif all(self.key[player][index].values()): #show a damaged ship + if d == 'd': #down + ships[1][index] = ships[1][index].rotate(90, expand=True) + img.paste(ships[1][index], box=((x*30)+32, (y*30)+32), mask=ships[1][index]) + + #place hit/miss markers + for y in range(10): + for x in range(10): + if self.board[player][((y)*10)+x] == 1: #miss + img.paste(miss, box=((x*30)+32, (y*30)+32), mask=miss) + elif self.board[player][((y)*10)+x] == 2: #hit + img.paste(hit, box=((x*30)+32, (y*30)+32), mask=hit) + + temp = BytesIO() + temp.name = 'board.png' + img.save(temp) + temp.seek(0) + return temp + + async def update_dm(self, player): + """ + Update the DM board for a specific player. + Only updates the board if the board is not an image. + + Params: + player = int, Which player's board to print. + """ + if not await self.cog.config.guild(self.channel.guild).doImage(): + if self.pmsg[player]: + content = self._gen_text(player, 1) + await self.pmsg[player].edit(content=content) + + async def send_board(self, player, show_unhit, dest, msg): + """ + Send either an image of the board or a text representation of the board. + + player = int, Which player's board to print. + show_unhit = int, Should unhit ships be shown. + dest = Union[discord.User, discord.abc.GuildChannel], Where to send to. + msg = str, Text to include with the board. + """ + if isinstance(dest, BattleshipAI): + return + if await self.cog.config.guild(self.channel.guild).doImage(): + if isinstance(dest, (discord.User, discord.Member)): + filesize_limit = 8388608 + attach_files = True + else: + filesize_limit = dest.guild.filesize_limit + attach_files = dest.permissions_for(dest.guild.me).attach_files + if attach_files: + img = self._gen_img(player, show_unhit) + file_size = img.tell() + img.seek(0) + if file_size <= filesize_limit: + file = discord.File(img, 'board.png') + await dest.send(file=file) + if msg: + await dest.send(msg) + return + content = self._gen_text(player, show_unhit) + m = await dest.send(f'{content}{msg}') + return m + + async def _place(self, player, length, value): + """ + Add a ship to the board. + Returns True when the ship is successfully placed. + Returns False and sends a message when the ship cannot be placed. + + Params: + player = int, Which player's board to place to. + length = int, Length of the ship to place. + value = str, The XYD to place ship at. + """ + hold = {} + try: + x = self.letnum[value[0]] + except (KeyError, IndexError): + await self.player[player].send('Invalid input, x cord must be a letter from A-J.') + return False + try: + y = int(value[1]) + except (ValueError, IndexError): + await self.player[player].send('Invalid input, y cord must be a number from 0-9.') + return False + try: + d = value[2] + except IndexError: + await self.player[player].send('Invalid input, d cord must be a direction of d or r.') + return False + try: + if d == 'r': #right + if 10 - length < x: #ship would wrap over right edge + await self.player[player].send('Invalid input, too far to the right.') + return False + for z in range(length): + if self.board[player][(y*10)+x+z] != 0: #a spot taken by another ship + await self.player[player].send( + 'Invalid input, another ship is in that range.' + ) + return False + for z in range(length): + self.board[player][(y*10)+x+z] = 3 + hold[(y*10)+x+z] = 0 + elif d == 'd': #down + for z in range(length): + if self.board[player][((y+z)*10)+x] != 0: #a spot taken by another ship + await self.player[player].send( + 'Invalid input, another ship is in that range.' + ) + return False + for z in range(length): + self.board[player][((y+z)*10)+x] = 3 + hold[((y+z)*10)+x] = 0 + else: + await self.player[player].send( + 'Invalid input, d cord must be a direction of d or r.' + ) + return False + except IndexError: + await self.player[player].send('Invalid input, too far down.') + return False + self.key[player].append(hold) + self.ship_pos[player].append((x, y, d)) + return True + + async def run(self): + """ + Runs the actual game. + Should only be called by __init__. + """ + for x in range(2): #each player + await self.channel.send(f'Messaging {self.name[x]} for setup now.') + privateMessage = await self.player[x].send( + f'{self.name[x]}, it is your turn to set up your ships.\n' + 'Place ships by entering the top left coordinate using the letter of the column ' + 'followed by the number of the row and the direction of (r)ight or (d)own ' + 'in ColumnRowDirection format (such as c2r).' + ) + for ship_len in [5, 4, 3, 3, 2]: #each ship length + await self.send_board(x, 1, self.player[x], f'Place your {ship_len} length ship.') + while True: + if isinstance(self.player[x], BattleshipAI): + await asyncio.sleep(1) + cords = self.player[x].place(self.board[x], ship_len) + else: + try: + cords = await self.bot.wait_for( + 'message', + timeout=120, + check=lambda m: ( + m.channel == privateMessage.channel + and not m.author.bot + ) + ) + cords = cords.content + except asyncio.TimeoutError: + await self.channel.send(f'{self.name[x]} took too long, shutting down.') + return + if await self._place(x, ship_len, cords.lower()): #only break if _place succeeded + break + m = await self.send_board(x, 1, self.player[x], '') + self.pmsg.append(m) #save this message for editing later + pswap = {1:0, 0:1} #helper to swap player + while True: + self.p = pswap[self.p] #swap players + if await self.cog.config.guild(self.channel.guild).doMention(): #should player be mentioned + mention = self.player[self.p].mention + else: + mention = self.name[self.p] + await self.channel.send(f'{mention}\'s turn!') + await self.send_board( + pswap[self.p], 0, self.channel, f'{self.name[self.p]}, take your shot.' + ) + while True: + if isinstance(self.player[self.p], BattleshipAI): + safe_board = [i if i != 3 else 0 for i in self.board[pswap[self.p]]] + ship_status = [] + for idx, ship_dict in enumerate(self.key[pswap[self.p]]): + if all(ship_dict.values()): + ship_status.append(self.ship_pos[pswap[self.p]][idx]) + else: + ship_status.append(None) + cords = self.player[self.p].shoot(safe_board, ship_status) + cords = cords.lower() + else: + try: + cords = await self.bot.wait_for( + 'message', + timeout=120, + check=lambda m: ( + m.author == self.player[self.p] + and m.channel == self.channel + and len(m.content) == 2 + ) + ) + cords = cords.content.lower() + except asyncio.TimeoutError: + await self.channel.send('You took too long, shutting down.') + return + try: #makes sure input is valid + x = self.letnum[cords[0]] + y = int(cords[1]) + except (ValueError, KeyError, IndexError): + continue + if self.board[pswap[self.p]][(y*10)+x] == 0: + self.board[pswap[self.p]][(y*10)+x] = 1 + await self.update_dm(pswap[self.p]) + await self.send_board(pswap[self.p], 0, self.channel, '**Miss!**') + break + elif self.board[pswap[self.p]][(y*10)+x] in [1, 2]: + await self.channel.send('You already shot there!') + elif self.board[pswap[self.p]][(y*10)+x] == 3: + self.board[pswap[self.p]][(y*10)+x] = 2 + #DEAD SHIP + ship_dead = None + for a in range(5): + if (y*10)+x in self.key[pswap[self.p]][a]: + self.key[pswap[self.p]][a][(y*10)+x] = 1 + if all(self.key[pswap[self.p]][a].values()): #if ship destroyed + ship_dead = [5, 4, 3, 3, 2][a] + await self.update_dm(pswap[self.p]) + if ship_dead: + msg = ( + f'**Hit!**\n**{self.name[pswap[self.p]]}\'s ' + f'{ship_dead} length ship was destroyed!**' + ) + await self.send_board(pswap[self.p], 0, self.channel, msg) + else: + await self.send_board(pswap[self.p], 0, self.channel, '**Hit!**') + #DEAD PLAYER + if 3 not in self.board[pswap[self.p]]: + await self.channel.send(f'**{self.name[self.p]} wins!**') + return + if await self.cog.config.guild(self.channel.guild).extraHit(): + await self.channel.send('Take another shot.') + else: + break diff --git a/battleship/info.json b/battleship/info.json new file mode 100644 index 0000000..d85bcfd --- /dev/null +++ b/battleship/info.json @@ -0,0 +1,12 @@ +{ + "author" : ["Flame442"], + "install_msg" : "Thanks for installing battleship. Use `[p]battleship` to play.\nThis cog comes with bundled data.", + "name" : "Battleship", + "short" : "Play battleship with a friend.", + "requirements" : ["pillow"], + "description" : "Play battleship against a friend or an AI opponent. Generates an image to represent the board state. Run [p]battleship to play!", + "tags" : ["fun", "games", "battleship"], + "min_bot_version": "3.5.0.dev1", + "min_python_version": [3, 6, 0], + "end_user_data_statement": "This cog does not store user data." +} diff --git a/battleship/views.py b/battleship/views.py new file mode 100644 index 0000000..00aebaa --- /dev/null +++ b/battleship/views.py @@ -0,0 +1,106 @@ +import discord +from .ai import BattleshipAI + + +class ConfirmView(discord.ui.View): + def __init__(self, member: discord.Member): + super().__init__(timeout=60) + self.member = member + self.result = False + + async def interaction_check(self, interaction): + if interaction.user.id != self.member.id: + await interaction.response.send_message(content='You are not allowed to interact with this button.', ephemeral=True) + return False + return True + + @discord.ui.button(label='Accept', style=discord.ButtonStyle.green) + async def yes(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.edit_message(view=None) + self.result = True + self.stop() + + @discord.ui.button(label='Deny', style=discord.ButtonStyle.red) + async def no(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.edit_message(view=None) + self.stop() + + +class GetPlayersView(discord.ui.View): + """View to gather the players that will play in a game.""" + def __init__(self, ctx, max_players): + super().__init__(timeout=60) + self.ctx = ctx + self.max_players = max_players + self.players = [ctx.author] + + def generate_message(self): + """Generates a message to show the players currently added to the game.""" + msg = "" + for idx, player in enumerate(self.players, start=1): + msg += f"Player {idx} - {player.display_name}\n" + msg += ( + f"\nClick the `Join Game` button to join. Up to {self.max_players} players can join. " + "To start with less than that many, use the `Start Game` button to begin." + ) + return msg + + async def interaction_check(self, interaction): + if len(self.players) >= self.max_players: + await interaction.response.send_message(content='The game is full.', ephemeral=True) + return False + if interaction.user.id != self.ctx.author.id and interaction.user in self.players: + await interaction.response.send_message( + content='You have already joined the game. Please wait for others to join or for the game to be started.', + ephemeral=True, + ) + return False + return True + + @discord.ui.button(label="Join Game", style=discord.ButtonStyle.blurple) + async def join(self, interaction: discord.Interaction, button: discord.ui.Button): + """Allows a user not currently added to join.""" + if interaction.user.id == self.ctx.author.id: + await interaction.response.send_message( + content='You have already joined the game. You can add AI players or start the game early with the other two buttons.', + ephemeral=True, + ) + return + self.players.append(interaction.user) + #self.start.disabled = False + if len(self.players) >= self.max_players: + view = None + self.stop() + else: + view = self + await interaction.response.edit_message(content=self.generate_message(), view=view) + + @discord.ui.button(label="Play vs AI", style=discord.ButtonStyle.blurple) + async def ai(self, interaction: discord.Interaction, button: discord.ui.Button): + """Fills the next player slot with an AI player.""" + if interaction.user.id != self.ctx.author.id: + await interaction.response.send_message( + content='Only the host can use this button.', + ephemeral=True, + ) + return + self.players.append(BattleshipAI(self.ctx.guild.me.display_name)) + #self.start.disabled = False + if len(self.players) >= self.max_players: + view = None + self.stop() + else: + view = self + await interaction.response.edit_message(content=self.generate_message(), view=view) + + #@discord.ui.button(label="Start Game", style=discord.ButtonStyle.green, disabled=True) + #async def start(self, interaction: discord.Interaction, button: discord.ui.Button): + # """Starts the game with less than max_players players.""" + # if interaction.user.id != self.ctx.author.id: + # await interaction.response.send_message( + # content='Only the host can use this button.', + # ephemeral=True, + # ) + # return + # await interaction.response.edit_message(view=None) + # self.stop() diff --git a/cogcount/__init__.py b/cogcount/__init__.py new file mode 100644 index 0000000..bf0d5a9 --- /dev/null +++ b/cogcount/__init__.py @@ -0,0 +1,37 @@ +""" +MIT License + +Copyright (c) 2021-present Kuro-Rui + +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. +""" + +import json +from pathlib import Path + +from redbot.core.bot import Red + +from .cogcount import CogCount + +with open(Path(__file__).parent / "info.json") as fp: + __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] + + +async def setup(bot: Red): + await bot.add_cog(CogCount(bot)) diff --git a/cogcount/cogcount.py b/cogcount/cogcount.py new file mode 100644 index 0000000..429f0b0 --- /dev/null +++ b/cogcount/cogcount.py @@ -0,0 +1,83 @@ +""" +MIT License + +Copyright (c) 2021-present Kuro-Rui + +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. +""" + +import discord +import kuroutils +from redbot.core import commands +from redbot.core.bot import Red +from redbot.core.commands.converter import CogConverter + + +class CogCount(kuroutils.Cog): + """Count [botname]'s cogs and commands.""" + + __author__ = ["Kuro"] + __version__ = "0.0.1" + + def __init__(self, bot: Red): + super().__init__(bot) + + @commands.is_owner() + @commands.group() + async def count(self, ctx: commands.Context): + """See how many cogs/commands [botname] has.""" + pass + + @commands.is_owner() + @count.command() + async def cogs(self, ctx: commands.Context): + """See how many cogs [botname] has.""" + + total = len(set(await self.bot._cog_mgr.available_modules())) + loaded = len(set(self.bot.extensions.keys())) + unloaded = total - loaded + + description = ( + f"`Loaded :` **{loaded}** Cogs.\n" + f"`Unloaded :` **{unloaded}** Cogs.\n" + f"`Total :` **{total}** Cogs." + ) + if not await ctx.embed_requested(): + await ctx.send(f"**Cogs**\n\n{description}") + return + embed = discord.Embed( + title="Cogs Count", description=description, color=await ctx.embed_color() + ) + await ctx.send(embed=embed) + + @commands.is_owner() + @count.command() + async def commands(self, ctx: commands.Context, cog: CogConverter = None): + """ + See how many commands [botname] has. + + You can also provide a cog name to see how many commands is in that cog. + The commands count includes subcommands. + """ + if cog: + commands = len(set(cog.walk_commands())) + await ctx.send(f"I have `{commands}` commands on that cog.") + return + commands = len(set(self.bot.walk_commands())) + await ctx.send(f"I have `{commands}` commands.") diff --git a/cogcount/info.json b/cogcount/info.json new file mode 100644 index 0000000..37a9a29 --- /dev/null +++ b/cogcount/info.json @@ -0,0 +1,13 @@ +{ + "author": ["Kuro"], + "description": "Have you ever wondered, how many commands/cogs does your bot has? Well this cog can help with that!", + "disabled": false, + "end_user_data_statement": "This cog does not store any end user data.", + "hidden": false, + "install_msg": "Thanks for installing `CogCount`! Get started with `[p]count`.\nThis cog has docs! Check it out at .", + "name": "CogCount", + "requirements": ["git+https://github.com/Kuro-Rui/Kuro-Utils"], + "short": "A cog that shows how many commands/cogs the bot has.", + "tags": ["cogs", "commands", "count"], + "type": "COG" +} \ No newline at end of file diff --git a/cogpaths/README.rst b/cogpaths/README.rst new file mode 100644 index 0000000..bc06fd3 --- /dev/null +++ b/cogpaths/README.rst @@ -0,0 +1,71 @@ +.. _cogpaths: + +======== +CogPaths +======== + +This is the cog guide for the 'CogPaths' cog. This guide +contains the collection of commands which you can use in the cog. + +Through this guide, ``[p]`` will always represent your prefix. Replace +``[p]`` with your own prefix when you use these commands in Discord. + +.. note:: + + This guide was last updated for version 1.1.0. Ensure + that you are up to date by running ``[p]cog update cogpaths``. + + If there is something missing, or something that needs improving + in this documentation, feel free to create an issue `here `_. + + This documentation is auto-generated everytime this cog receives an update. + +-------------- +About this cog +-------------- + +Get information about a cog's paths. + +-------- +Commands +-------- + +Here are all the commands included in this cog (1): + ++----------------+--------------------------+ +| Command | Help | ++================+==========================+ +| ``[p]cogpath`` | Get the paths for a cog. | ++----------------+--------------------------+ + +------------ +Installation +------------ + +If you haven't added my repo before, lets add it first. We'll call it +"kreusada-cogs" here. + +.. code-block:: + + [p]repo add kreusada-cogs https://github.com/Kreusada/Kreusada-Cogs + +Now, we can install CogPaths. + +.. code-block:: + + [p]cog install kreusada-cogs cogpaths + +Once it's installed, it is not loaded by default. Load it by running the following +command: + +.. code-block:: + + [p]load cogpaths + +--------------- +Further Support +--------------- + +For more support, head over to the `cog support server `_, +I have my own channel over there at #support_kreusada-cogs. Feel free to join my +`personal server `_ whilst you're here. diff --git a/cogpaths/__init__.py b/cogpaths/__init__.py new file mode 100644 index 0000000..924f439 --- /dev/null +++ b/cogpaths/__init__.py @@ -0,0 +1,10 @@ +from redbot.core.bot import Red +from redbot.core.utils import get_end_user_data_statement + +from .cogpaths import CogPaths + +__red_end_user_data_statement__ = get_end_user_data_statement(__file__) + + +async def setup(bot: Red): + await bot.add_cog(CogPaths(bot)) diff --git a/cogpaths/cogpaths.py b/cogpaths/cogpaths.py new file mode 100644 index 0000000..ef403de --- /dev/null +++ b/cogpaths/cogpaths.py @@ -0,0 +1,41 @@ +import inspect +import os +import pathlib + +from redbot.core import commands, data_manager +from redbot.core.bot import Red +from redbot.core.commands import CogConverter +from redbot.core.config import Config +from redbot.core.utils.chat_formatting import box + + +class CogPaths(commands.Cog): + """Get information about a cog's paths.""" + + __author__ = "Kreusada" + __version__ = "1.1.0" + + def __init__(self, bot: Red): + self.bot = bot + + def format_help_for_context(self, ctx: commands.Context) -> str: + context = super().format_help_for_context(ctx) + return f"{context}\n\nAuthor: {self.__author__}\nVersion: {self.__version__}" + + async def red_delete_data_for_user(self, **kwargs): + return + + @commands.is_owner() + @commands.command(aliases=["cogpaths"]) + async def cogpath(self, ctx: commands.Context, cog: CogConverter): + """Get the paths for a cog.""" + cog_path = pathlib.Path(inspect.getfile(cog.__class__)).parent.resolve() + cog_data_path = pathlib.Path(data_manager.cog_data_path() / cog.qualified_name).resolve() + if not os.path.exists(cog_data_path): + cog_data_path = None + if not isinstance(getattr(cog, "config", None), Config): + reason = "This cog does not store any data, or does not use Red's Config API." + else: + reason = "This cog had its data directory removed." + message = "Cog path: {}\nData path: {}".format(cog_path, cog_data_path or reason) + await ctx.send(box(message, lang="yaml")) diff --git a/cogpaths/info.json b/cogpaths/info.json new file mode 100644 index 0000000..65ab839 --- /dev/null +++ b/cogpaths/info.json @@ -0,0 +1,16 @@ +{ + "author": [ + "Kreusada" + ], + "description": "Get various paths for a cog.", + "install_msg": "Thanks for installing, have fun. Please refer to my docs if you need any help: https://kreusadacogs.readthedocs.io/en/latest/cog_cogpaths.html", + "short": "Get various paths for a cog.", + "name": "CogPaths", + "tags": [ + "Paths", + "Cogs" + ], + "requirements": [], + "type": "COG", + "end_user_data_statement": "This cog does not persistently store data or metadata about users." +} diff --git a/consolelogs/LICENSE b/consolelogs/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/consolelogs/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/consolelogs/README.rst b/consolelogs/README.rst new file mode 100644 index 0000000..3c90021 --- /dev/null +++ b/consolelogs/README.rst @@ -0,0 +1,82 @@ +.. _consolelogs: +=========== +ConsoleLogs +=========== + +This is the cog guide for the ``ConsoleLogs`` cog. This guide contains the collection of commands which you can use in the cog. +Through this guide, ``[p]`` will always represent your prefix. Replace ``[p]`` with your own prefix when you use these commands in Discord. + +.. note:: + + Ensure that you are up to date by running ``[p]cog update consolelogs``. + If there is something missing, or something that needs improving in this documentation, feel free to create an issue `here `_. + This documentation is generated everytime this cog receives an update. + +--------------- +About this cog: +--------------- + +A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels! + +--------- +Commands: +--------- + +Here are all the commands included in this cog (7): + +* ``[p]consolelogs [lines_break=2] ["critical"|"error"|"warning"|"info"|"debug"|"trace"|"node"|"criticals"|"errors"|"warnings"|"infos"|"debugs"|"traces"|"nodes"] [ids] [logger_name]`` + View a console log, for a provided level/logger name. + +* ``[p]consolelogs addchannel [global_errors=True] [prefixed_commands_errors=True] [slash_commands_errors=True] [dpy_ignored_exceptions=False] [full_console=False] [guild_invite=True] [ignored_cogs]`` + Enable errors logging in a channel. + +* ``[p]consolelogs getdebugloopsstatus`` + Get an embed to check loops status. + +* ``[p]consolelogs removechannel `` + Disable errors logging in a channel. + +* ``[p]consolelogs scroll [lines_break=2] ["critical"|"error"|"warning"|"info"|"debug"|"trace"|"node"|"criticals"|"errors"|"warnings"|"infos"|"debugs"|"traces"|"nodes"] [ids] [logger_name]`` + Scroll the console logs, for all levels/loggers or provided level/logger name. + +* ``[p]consolelogs stats`` + Display the stats for the bot logs since the bot start. + +* ``[p]consolelogs view [index=-1] ["critical"|"error"|"warning"|"info"|"debug"|"trace"|"node"|"criticals"|"errors"|"warnings"|"infos"|"debugs"|"traces"|"nodes"] [ids] [logger_name]`` + View the console logs one by one, for all levels/loggers or provided level/logger name. + +------------ +Installation +------------ + +If you haven't added my repo before, lets add it first. We'll call it "AAA3A-cogs" here. + +.. code-block:: ini + + [p]repo add AAA3A-cogs https://github.com/AAA3A-AAA3A/AAA3A-cogs + +Now, we can install ConsoleLogs. + +.. code-block:: ini + + [p]cog install AAA3A-cogs consolelogs + +Once it's installed, it is not loaded by default. Load it by running the following command: + +.. code-block:: ini + + [p]load consolelogs + +---------------- +Further Support: +---------------- + +Check out my docs `here `_. +Mention me in the #support_other-cogs in the `cog support server `_ if you need any help. +Additionally, feel free to open an issue or pull request to this repo. + +-------- +Credits: +-------- + +Thanks to Kreusada for the Python code to automatically generate this documentation! \ No newline at end of file diff --git a/consolelogs/__init__.py b/consolelogs/__init__.py new file mode 100644 index 0000000..cf719a5 --- /dev/null +++ b/consolelogs/__init__.py @@ -0,0 +1,47 @@ +from redbot.core import errors # isort:skip +import importlib +import sys + +try: + import AAA3A_utils +except ModuleNotFoundError: + raise errors.CogLoadError( + "The needed utils to run the cog were not found. Please execute the command `[p]pipinstall git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." + ) +modules = sorted( + [module for module in sys.modules if module.split(".")[0] == "AAA3A_utils"], reverse=True +) +for module in modules: + try: + importlib.reload(sys.modules[module]) + except ModuleNotFoundError: + pass +del AAA3A_utils +# import AAA3A_utils +# import json +# import os +# __version__ = AAA3A_utils.__version__ +# with open(os.path.join(os.path.dirname(__file__), "utils_version.json"), mode="r") as f: +# data = json.load(f) +# needed_utils_version = data["needed_utils_version"] +# if __version__ > needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a higher version than the one supported by this version of the cog. Please update the cogs of the `AAA3A-cogs` repo." +# ) +# elif __version__ < needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a lower version than the one supported by this version of the cog. Please execute the command `[p]pipinstall --upgrade git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." +# ) + +from redbot.core.bot import Red # isort:skip + +from redbot.core.utils import get_end_user_data_statement + +from .consolelogs import ConsoleLogs + +__red_end_user_data_statement__ = get_end_user_data_statement(file=__file__) + + +async def setup(bot: Red) -> None: + cog = ConsoleLogs(bot) + await bot.add_cog(cog) diff --git a/consolelogs/consolelogs.py b/consolelogs/consolelogs.py new file mode 100644 index 0000000..f9191fe --- /dev/null +++ b/consolelogs/consolelogs.py @@ -0,0 +1,798 @@ +from AAA3A_utils import Cog, CogsUtils, Menu, Loop # isort:skip +from redbot.core import commands, Config # isort:skip +from redbot.core.bot import Red # isort:skip +from redbot.core.i18n import Translator, cog_i18n # isort:skip +import discord # isort:skip +import typing # isort:skip + +from redbot import __version__ as red_version +from redbot.core import data_manager +from redbot.core.utils.chat_formatting import box, humanize_list, pagify + +try: + from redbot.core._events import INTRO +except ModuleNotFoundError: # Lemon's fork. + INTRO = "" + +import asyncio +import datetime +import logging +import re +import traceback +from collections import Counter +from dataclasses import dataclass +from io import BytesIO, TextIOWrapper + +from colorama import Fore +from rich import box as rich_box +from rich import print as rich_print +from rich.columns import Columns +from rich.panel import Panel +from rich.table import Table + +from .dashboard_integration import DashboardIntegration + +# Credits: +# General repo credits. +# Thanks to Tobotimus for the part to get logs files lines (https://github.com/Tobotimus/Tobo-Cogs/blob/V3/errorlogs/errorlogs.py)! +# Thanks to Trusty for the part to get the "message" content for slash commands (https://github.com/TrustyJAID/Trusty-cogs/blob/master/extendedmodlog/eventmixin.py#L222-L249! + +_: Translator = Translator("ConsoleLogs", __file__) + +LATEST_LOG_RE = re.compile(r"latest(?:-part(?P\d+))?\.log") +CONSOLE_LOG_RE = re.compile( + r"^\[(?P.*?)\] \[(?P.*?)\] (?P.*?): (?P.*)" +) + +IGNORED_ERRORS = ( + commands.UserInputError, + commands.DisabledCommand, + commands.CommandNotFound, + commands.CheckFailure, + commands.NoPrivateMessage, + commands.CommandOnCooldown, + commands.MaxConcurrencyReached, + commands.BadArgument, + commands.BadBoolArgument, +) + + +class IdConverter(commands.Converter): + async def convert(self, ctx: commands.Context, argument: str) -> int: + try: + return int(argument.lstrip("#")) + except ValueError: + raise commands.BadArgument() + + +@dataclass(frozen=False) +class ConsoleLog: + id: int + time: datetime.datetime + time_timestamp: int + time_str: str + level: typing.Literal["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE", "NODE"] + logger_name: str + message: str + exc_info: typing.Optional[str] = None + display_without_informations: bool = False + + @property + def logger(self) -> logging.Logger: + return logging.getLogger(self.logger_name) + + def __str__(self, with_ansi: bool = False, with_extra_break_line: bool = True) -> str: + if self.display_without_informations: + return self.message + BREAK_LINE = "\n" + if not with_ansi: + return f"#{self.id} [{self.time_str}] {self.level} [{self.logger_name}] {self.message}{BREAK_LINE if self.exc_info is not None else ''}{BREAK_LINE if with_extra_break_line and self.exc_info is not None else ''}{self.exc_info if self.exc_info is not None else ''}" + levels_colors = { + "CRITICAL": Fore.RED, + "ERROR": Fore.RED, + "WARNING": Fore.YELLOW, + "INFO": Fore.BLUE, + "DEBUG": Fore.GREEN, + "TRACE": Fore.CYAN, + "NODE": Fore.MAGENTA, + } + level_color = levels_colors.get(self.level, Fore.MAGENTA) + return f"{Fore.CYAN}#{self.id} {Fore.BLACK}[{self.time_str}] {level_color}{self.level} {Fore.WHITE}[{Fore.MAGENTA}{self.logger_name}{Fore.WHITE}] {Fore.WHITE}{self.message.split(BREAK_LINE)[0]}{Fore.RESET}{BREAK_LINE if self.exc_info is not None else ''}{BREAK_LINE if with_extra_break_line and self.exc_info is not None else ''}{self.exc_info if self.exc_info is not None else ''}" + + +@cog_i18n(_) +class ConsoleLogs(DashboardIntegration, Cog): + """A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!""" + + __authors__: typing.List[str] = ["AAA3A", "Tobotimus"] + + def __init__(self, bot: Red) -> None: + super().__init__(bot=bot) + + self.config: Config = Config.get_conf( + self, + identifier=205192943327321000143939875896557571750, + force_registration=True, + ) + self.config.register_channel( + enabled=False, + global_errors=True, + prefixed_commands_errors=True, + slash_commands_errors=True, + dpy_ignored_exceptions=False, + full_console=False, + guild_invite=True, + ignored_cogs=[], + ) + + self.RED_INTRO: str = None + self._last_console_log_sent_timestamp: int = None + + async def cog_load(self) -> None: + await super().cog_load() + asyncio.create_task(self.load()) + + async def load(self) -> None: + await self.bot.wait_until_red_ready() + self.RED_INTRO: str = INTRO + guilds = len(self.bot.guilds) + users = len(set(list(self.bot.get_all_members()))) + prefixes = getattr(self.bot._cli_flags, "prefix", None) or ( + await self.bot._config.prefix() + ) + lang = await self.bot._config.locale() + dpy_version = discord.__version__ + table_general_info = Table(show_edge=False, show_header=False, box=rich_box.MINIMAL) + table_general_info.add_row("Prefixes", ", ".join(prefixes)) + table_general_info.add_row("Language", lang) + table_general_info.add_row("Red version", red_version) + table_general_info.add_row("Discord.py version", dpy_version) + table_general_info.add_row("Storage type", data_manager.storage_type()) + table_counts = Table(show_edge=False, show_header=False, box=rich_box.MINIMAL) + table_counts.add_row("Shards", str(self.bot.shard_count)) + table_counts.add_row("Servers", str(guilds)) + if self.bot.intents.members: + table_counts.add_row("Unique Users", str(users)) + io_file = BytesIO() + with TextIOWrapper(io_file, encoding="utf-8") as text_wrapper: + rich_print( + Columns( + [ + Panel(table_general_info, title=self.bot.user.display_name), + Panel(table_counts), + ], + equal=True, + align="center", + ), + file=text_wrapper, + ) + io_file.seek(0) + self.RED_INTRO += io_file.read().decode("utf-8") + self.RED_INTRO += ( + f"\nLoaded {len(self.bot.cogs)} cogs with {len(self.bot.commands)} commands" + ) + + self._last_console_log_sent_timestamp: int = int( + datetime.datetime.now(tz=datetime.timezone.utc).timestamp() + ) + self.loops.append( + Loop( + cog=self, + name="Check Console Logs", + function=self.check_console_logs, + minutes=1, + ) + ) + + @property + def console_logs(self) -> typing.List[ConsoleLog]: + # Thanks to Tobotimus for this part! + console_logs_files = sorted( + [ + path + for path in (data_manager.core_data_path() / "logs").iterdir() + if LATEST_LOG_RE.match(path.name) is not None + ], + key=lambda x: x.name, + ) + if not console_logs_files: + return [] + console_logs_lines = [] + for console_logs_file in console_logs_files: + with console_logs_file.open(mode="rt") as f: + console_logs_lines.extend([line.strip() for line in f.readlines()]) + + # Parse logs. + console_logs = [] + for console_log_line in console_logs_lines: + if (match := re.match(CONSOLE_LOG_RE, console_log_line)) is None: + if not console_logs: + continue + if console_logs[-1].exc_info is None: + console_logs[-1].exc_info = "" + console_logs[-1].exc_info += f"\n{CogsUtils.replace_var_paths(console_log_line)}" + console_logs[-1].exc_info = console_logs[-1].exc_info.strip() + continue + kwargs = match.groupdict() + time = datetime.datetime.strptime(kwargs["time_str"], "%Y-%m-%d %H:%M:%S") + kwargs["time"] = time + kwargs["time_timestamp"] = int(time.timestamp()) + kwargs["message"] = kwargs["message"].strip() + if not kwargs["message"]: + continue + kwargs["message"] += ( + "." + if not kwargs["message"].endswith((".", "!", "?")) + and kwargs["message"][0] == kwargs["message"][0].upper() + else "" + ) + kwargs["exc_info"] = None # Maybe next lines... + console_logs.append(ConsoleLog(id=0, **kwargs)) + + # Add Red INTRO. + if red_ready_console_log := discord.utils.get( + console_logs, logger_name="red", message="Connected to Discord. Getting ready..." + ): + console_logs.insert( + console_logs.index(red_ready_console_log) + 1, + ConsoleLog( + id=0, + time=red_ready_console_log.time, + time_timestamp=red_ready_console_log.time_timestamp, + time_str=red_ready_console_log.time_str, + level="INFO", + logger_name="red", + message=self.RED_INTRO, + exc_info=None, + display_without_informations=True, + ), + ) + + # Update ID. + for id, console_log in enumerate(console_logs, start=1): + console_log.id = id + + return console_logs + + async def send_console_logs( + self, + ctx: commands.Context, + level: typing.Optional[ + typing.Literal["critical", "error", "warning", "info", "debug", "trace", "node"] + ] = None, + ids: typing.Optional[typing.List[int]] = None, + logger_name: typing.Optional[str] = None, + view: typing.Optional[int] = -1, + lines_break: int = 2, + ) -> None: + console_logs = self.console_logs + console_logs_to_display = [ + console_log + for console_log in console_logs + if (level is None or console_log.level == level) + and ( + logger_name is None + or ".".join(console_log.logger_name.split(".")[: len(logger_name.split("."))]) + == logger_name + ) + and (ids is None or console_log.id in ids) + ] + if not console_logs_to_display: + raise commands.UserFeedbackCheckFailure(_("No logs to display.")) + console_logs_to_display_str = [ + console_log.__str__( + with_ansi=not ( + ctx.author.is_on_mobile() if isinstance(ctx.author, discord.Member) else False + ), + with_extra_break_line=view is not None, + ) + for console_log in console_logs_to_display + ] + levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE", "NODE"] + total_stats = [ + f"{len(console_logs)} logs", + f"{len({console_log.logger_name for console_log in console_logs})} loggers", + *[ + f"{stat[1]} {stat[0]}" + for stat in sorted( + Counter([console_log.level for console_log in console_logs]).items(), + key=lambda x: levels.index(x[0]) if x[0] in levels else 10, + ) + ], + ] + loggers = {console_log.logger_name for console_log in console_logs_to_display} + current_stats = [ + f"{len(console_logs_to_display)} log{'' if len(console_logs_to_display) == 1 else 's'}", + f"{len(loggers)} logger{'' if len(loggers) == 1 else 's'}", + *[ + f"{stat[1]} {stat[0]}" + for stat in sorted( + Counter( + [console_log.level for console_log in console_logs_to_display] + ).items(), + key=lambda x: levels.index(x[0]) if x[0] in levels else 10, + ) + ], + ] + prefix = box( + f"Total stats: {humanize_list(total_stats)}." + + ( + f"\nCurrent stats: {humanize_list(current_stats)}." + if total_stats != current_stats + else "" + ), + lang="py", + ) + if view is not None: + try: + view = console_logs_to_display_str.index( + console_logs_to_display_str[view] + ) # Handle negative index. + except IndexError: + view = len(console_logs_to_display_str) + pages = [] + for i, console_log_to_display_str in enumerate(console_logs_to_display_str): + if i == view: + page_index = len(pages) + pages.extend(list(pagify(console_log_to_display_str, shorten_by=12 + len(prefix)))) + else: + pages = list( + pagify( + ("\n" * lines_break).join(console_logs_to_display_str), + shorten_by=12 + len(prefix), + ) + ) + page_index = [ + i + for i, page in enumerate(pages) + if any( + line.startswith(("#", f"{Fore.CYAN}#", "[", f"{Fore.BLACK}[")) + for line in page.split("\n") + ) + ][-1] + menu = Menu( + pages=pages, + prefix=prefix, + lang=( + "py" + if (ctx.author.is_on_mobile() if isinstance(ctx.author, discord.Member) else False) + else "ansi" + ), + ) + menu._current_page = page_index + await menu.start(ctx) + + @commands.is_owner() + @commands.hybrid_group(aliases=["clogs"], invoke_without_command=True) + async def consolelogs( + self, + ctx: commands.Context, + lines_break: typing.Optional[commands.Range[int, 1, 5]] = 2, + level: typing.Optional[ + typing.Literal[ + "critical", + "error", + "warning", + "info", + "debug", + "trace", + "node", + "criticals", + "errors", + "warnings", + "infos", + "debugs", + "traces", + "nodes", + ] + ] = None, + ids: commands.Greedy[IdConverter] = None, + logger_name: typing.Optional[str] = None, + ) -> None: + """View a console log, for a provided level/logger name.""" + if ids is not None and len(ids) == 1: + return await self.view( + ctx, + level=level.rstrip("s").upper() if level is not None else None, + ids=ids, + logger_name=logger_name, + ) + await self.scroll( + ctx, + lines_break=lines_break, + level=level.rstrip("s").upper() if level is not None else None, + ids=ids, + logger_name=logger_name, + ) + + @consolelogs.command() + async def scroll( + self, + ctx: commands.Context, + lines_break: typing.Optional[commands.Range[int, 1, 5]] = 2, + level: typing.Optional[ + typing.Literal[ + "critical", + "error", + "warning", + "info", + "debug", + "trace", + "node", + "criticals", + "errors", + "warnings", + "infos", + "debugs", + "traces", + "nodes", + ] + ] = None, + ids: commands.Greedy[IdConverter] = None, + logger_name: typing.Optional[str] = None, + ) -> None: + """Scroll the console logs, for all levels/loggers or provided level/logger name.""" + await self.send_console_logs( + ctx, + level=level.rstrip("s").upper() if level is not None else None, + ids=ids, + logger_name=logger_name, + view=None, + lines_break=lines_break, + ) + + @consolelogs.command() + async def view( + self, + ctx: commands.Context, + index: typing.Optional[int] = -1, + level: typing.Optional[ + typing.Literal[ + "critical", + "error", + "warning", + "info", + "debug", + "trace", + "node", + "criticals", + "errors", + "warnings", + "infos", + "debugs", + "traces", + "nodes", + ] + ] = None, + ids: commands.Greedy[IdConverter] = None, + logger_name: typing.Optional[str] = None, + ) -> None: + """View the console logs one by one, for all levels/loggers or provided level/logger name.""" + await self.send_console_logs( + ctx, + level=level.rstrip("s").upper() if level is not None else None, + ids=ids, + logger_name=logger_name, + view=index, + ) + + @consolelogs.command(aliases=["listloggers"]) + async def stats(self, ctx: commands.Context) -> None: + """Display the stats for the bot logs since the bot start.""" + console_logs = self.console_logs + console_logs_for_each_logger = {"Global Stats": console_logs} + for console_log in console_logs: + if console_log.logger_name not in console_logs_for_each_logger: + console_logs_for_each_logger[console_log.logger_name] = [] + console_logs_for_each_logger[console_log.logger_name].append(console_log) + stats = "" + for logger_name, logs in console_logs_for_each_logger.items(): + stats += f"\n\n---------- {logger_name} ----------" + stats += f"\n• {len(logs)} logs" + levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE", "NODE"] + for stat in sorted( + Counter([console_log.level for console_log in logs]).items(), + key=lambda x: levels.index(x[0]) if x[0] in levels else 10, + ): + stats += f"\n• {stat[1]} {stat[0]}" + await Menu(pages=list(pagify(stats, page_length=500)), lang="py").start(ctx) + + @consolelogs.command(aliases=["+"]) + async def addchannel( + self, + ctx: commands.Context, + channel: typing.Union[discord.TextChannel, discord.VoiceChannel, discord.Thread], + global_errors: typing.Optional[bool] = True, + prefixed_commands_errors: typing.Optional[bool] = True, + slash_commands_errors: typing.Optional[bool] = True, + dpy_ignored_exceptions: typing.Optional[bool] = False, + full_console: typing.Optional[bool] = False, + guild_invite: typing.Optional[bool] = True, + *, + ignored_cogs: commands.Greedy[commands.CogConverter] = None, + ) -> None: + """Enable errors logging in a channel. + + **Parameters:** + - `channel`: The channel where the commands errors will be sent. + - `global_errors`: Log errors for the entire bot, not just the channel server. + - `prefixed_commands_errors`: Log prefixed commands errors. + - `slash_commands_errors`: Log slash commands errors. + - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors). + - `full_console`: Log all the console logs. + - `guild_invite`: Add a button "Guild Invite" in commands errors logs, only for community servers. + - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog. + """ + channel_permissions = channel.permissions_for(ctx.me) + if not all( + [ + channel_permissions.view_channel, + channel_permissions.send_messages, + channel_permissions.embed_links, + ] + ): + raise commands.UserFeedbackCheckFailure( + _("I don't have the permissions to send embeds in this channel.") + ) + await self.config.channel(channel).set( + { + "enabled": True, + "global_errors": global_errors, + "prefixed_commands_errors": prefixed_commands_errors, + "slash_commands_errors": slash_commands_errors, + "dpy_ignored_exceptions": dpy_ignored_exceptions, + "full_console": full_console, + "guild_invite": guild_invite, + "ignored_cogs": ( + [cog.qualified_name for cog in ignored_cogs] + if ignored_cogs is not None + else [] + ), + } + ) + await ctx.send(_("Errors logging enabled in {channel.mention}.").format(channel=channel)) + + @consolelogs.command(aliases=["-"]) + async def removechannel( + self, + ctx: commands.Context, + channel: typing.Union[discord.TextChannel, discord.VoiceChannel, discord.Thread], + ) -> None: + """Disable errors logging in a channel.""" + if not await self.config.channel(channel).enabled(): + raise commands.UserFeedbackCheckFailure( + _("Errors logging isn't enabled in this channel.") + ) + await self.config.channel(channel).clear() + await ctx.send(_("Errors logging disabled in {channel.mention}.").format(channel=channel)) + + @consolelogs.command(hidden=True) + async def getdebugloopsstatus(self, ctx: commands.Context) -> None: + """Get an embed to check loops status.""" + embeds = [loop.get_debug_embed() for loop in self.loops] + await Menu(pages=embeds).start(ctx) + + @commands.Cog.listener() + async def on_command_error( + self, ctx: commands.Context, error: commands.CommandError, unhandled_by_cog: bool = False + ) -> None: + if await self.bot.cog_disabled_in_guild(cog=self, guild=ctx.guild): + return + if isinstance(error, IGNORED_ERRORS): + return + destinations = { + channel: settings + for channel_id, settings in (await self.config.all_channels()).items() + if settings["enabled"] + and (channel := ctx.bot.get_channel(channel_id)) is not None + and channel.permissions_for(channel.guild.me).send_messages + } + if not destinations: + return + + # Thanks to Trusty for this part. + if ctx.interaction: + data = ctx.interaction.data + com_id = data.get("id") + root_command = data.get("name") + sub_commands = "" + arguments = "" + for option in data.get("options", []): + if option["type"] in (1, 2): + sub_commands += " " + option["name"] + else: + option_name = option["name"] + option_value = option.get("value") + arguments += f"{option_name}: {option_value}" + for sub_option in option.get("options", []): + if sub_option["type"] in (1, 2): + sub_commands += " " + sub_option["name"] + else: + sub_option_name = sub_option.get("name") + sub_option_value = sub_option.get("value") + arguments += f"{sub_option_name}: {sub_option_value}" + for arg in sub_option.get("options", []): + arg_option_name = arg.get("name") + arg_option_value = arg.get("value") + arguments += f"{arg_option_name}: {arg_option_value} " + command_name = f"{root_command}{sub_commands}" + com_str = f" {arguments}" + else: + com_str = ctx.message.content + + embed = discord.Embed( + title=f"⚠ Exception in command `{ctx.command.qualified_name}`! ¯\\_(ツ)_/¯", + color=discord.Color.red(), + timestamp=ctx.message.created_at, + description=f">>> {com_str}", + ) + embed.add_field( + name="Invoker:", value=f"{ctx.author.mention}\n{ctx.author} ({ctx.author.id})" + ) + embed.add_field(name="Message:", value=f"[Jump to message.]({ctx.message.jump_url})") + embed.add_field( + name="Channel:", + value=( + f"{ctx.channel.mention}\n{ctx.channel} ({ctx.channel.id})" + if ctx.guild is not None + else str(ctx.channel) + ), + ) + if ctx.guild is not None: + embed.add_field(name="Guild:", value=f"{ctx.guild.name} ({ctx.guild.id})") + guild_invite = None + if ctx.guild is not None and "COMMUNITY" in ctx.guild.features: + try: + if "VANITY_URL" not in ctx.guild.features: + raise KeyError("VANITY_URL") + guild_invite = await ctx.guild.vanity_invite() + except (KeyError, discord.HTTPException): + try: + invites = await ctx.guild.invites() + except discord.HTTPException: + invites = [] + for inv in invites: + if not (inv.max_uses or inv.max_age or inv.temporary): + guild_invite = inv + break + else: + channels_and_perms = zip( + ctx.guild.text_channels, + map(lambda x: x.permissions_for(ctx.guild.me), ctx.guild.text_channels), + ) + channel = next( + ( + channel + for channel, perms in channels_and_perms + if perms.create_instant_invite + ), + None, + ) + if channel is not None: + try: + guild_invite = await channel.create_invite(max_age=86400) + except discord.HTTPException: + pass + traceback_error = "".join( + traceback.format_exception(type(error), error, error.__traceback__) + ) + _traceback_error = traceback_error.split("\n") + _traceback_error[0] = _traceback_error[0] + ( + "" if _traceback_error[0].endswith(":") else ":\n" + ) + traceback_error = "\n".join(_traceback_error) + traceback_error = CogsUtils.replace_var_paths(traceback_error) + pages = [box(page, lang="py") for page in pagify(traceback_error, shorten_by=10)] + for channel, settings in destinations.items(): + if not settings["global_errors"] and ctx.guild != channel.guild: + continue + if not settings["prefixed_commands_errors"] and ctx.interaction is None: + continue + if not settings["slash_commands_errors"] and ctx.interaction is not None: + continue + if ctx.cog is not None and ctx.cog.qualified_name in settings["ignored_cogs"]: + continue + view = discord.ui.View() + view.add_item( + discord.ui.Button( + style=discord.ButtonStyle.url, + label="Jump to Message", + url=ctx.message.jump_url, + ) + ) + if settings["guild_invite"] and guild_invite is not None: + view.add_item( + discord.ui.Button( + style=discord.ButtonStyle.url, label="Guild Invite", url=guild_invite.url + ) + ) + await channel.send(embed=embed, view=view) + for page in pages: + await channel.send(page) + + async def check_console_logs(self) -> None: + destinations = { + channel: settings + for channel_id, settings in (await self.config.all_channels()).items() + if settings["enabled"] + and (settings["dpy_ignored_exceptions"] or settings["full_console"]) + and (channel := self.bot.get_channel(channel_id)) is not None + and channel.permissions_for(channel.guild.me).send_messages + } + if not destinations: + return + console_logs = self.console_logs + console_logs_to_send: typing.List[ + typing.Tuple[typing.Optional[discord.Embed], typing.List[str]] + ] = [] + pages_to_send: typing.List[str] = [] + for console_log in console_logs: + if self._last_console_log_sent_timestamp >= console_log.time_timestamp: + continue + self._last_console_log_sent_timestamp = console_log.time_timestamp + pages_to_send.append(console_log.__str__(with_ansi=False, with_extra_break_line=False)) + if ( + console_log.level in ("CRITICAL", "ERROR") + and console_log.logger_name.split(".")[0] == "discord" + and console_log.message.split("\n")[0].startswith("Ignoring exception ") + ): + if pages_to_send: + console_logs_to_send.append( + ( + None, + [ + box(page, lang="py") + for page in list( + pagify( + "\n\n".join(pages_to_send), + shorten_by=10, + ) + ) + ], + ) + ) + pages_to_send = [] + + embed: discord.Embed = discord.Embed(color=discord.Color.dark_embed()) + embed.title = console_log.message.split("\n")[0] + embed.timestamp = console_log.time + embed.add_field(name="Logger name:", value=f"`{console_log.logger_name}`") + embed.add_field(name="Error level:", value=f"`{console_log.level}`") + pages = [ + box(page, lang="py") + for page in list( + pagify( + console_log.__str__(with_ansi=False, with_extra_break_line=True), + shorten_by=10, + ) + ) + ] + console_logs_to_send.append((embed, pages)) + + if pages_to_send: + console_logs_to_send.append( + ( + None, + [ + box(page, lang="py") + for page in list( + pagify( + "\n\n".join(pages_to_send), + shorten_by=10, + ) + ) + ], + ) + ) + pages_to_send = [] + + for channel, settings in destinations.items(): + for embed, pages in console_logs_to_send: + if embed is not None and not settings["dpy_ignored_exceptions"]: + continue + elif embed is None and not settings["full_console"]: + continue + if embed is not None: + await channel.send(embed=embed) + for page in pages: + await channel.send(page) diff --git a/consolelogs/dashboard_integration.py b/consolelogs/dashboard_integration.py new file mode 100644 index 0000000..13e07f4 --- /dev/null +++ b/consolelogs/dashboard_integration.py @@ -0,0 +1,54 @@ +from redbot.core import commands # isort:skip +from redbot.core.bot import Red # isort:skip +from redbot.core.i18n import Translator # isort:skip +import typing # isort:skip + +_: Translator = Translator("ConsoleLogs", __file__) + + +def dashboard_page(*args, **kwargs): + def decorator(func: typing.Callable): + func.__dashboard_decorator_params__ = (args, kwargs) + return func + + return decorator + + +class DashboardIntegration: + bot: Red + tracebacks = [] + + @commands.Cog.listener() + async def on_dashboard_cog_add(self, dashboard_cog: commands.Cog) -> None: + if hasattr(self, "settings") and hasattr(self.settings, "commands_added"): + await self.settings.commands_added.wait() + dashboard_cog.rpc.third_parties_handler.add_third_party(self) + + @dashboard_page(name=None, description="Display the console logs.", is_owner=True) + async def rpc_callback(self, **kwargs) -> typing.Dict[str, typing.Any]: + console_logs = self.console_logs + source = """ + {% for console_log in console_logs %} + {{ console_log|highlight("python") }} + {% if not loop.last %} +
+ {% endif %} + {% endfor %} + """ + console_logs = kwargs["Pagination"].from_list( + console_logs, + per_page=kwargs["extra_kwargs"].get("per_page"), + page=kwargs["extra_kwargs"].get("page"), + default_per_page=1, + default_page=len(console_logs), + ) + _console_logs = [str(console_log) for console_log in console_logs] + console_logs.clear() + console_logs.extend(_console_logs) + return { + "status": 0, + "web_content": { + "source": source, + "console_logs": console_logs, + }, + } diff --git a/consolelogs/info.json b/consolelogs/info.json new file mode 100644 index 0000000..2dbb33b --- /dev/null +++ b/consolelogs/info.json @@ -0,0 +1,17 @@ +{ + "author": ["AAA3A", "Tobotimus"], + "name": "ConsoleLogs", + "install_msg": "Thank you for installing this cog!\nDo `[p]help CogName` to get the list of commands and their description. If you enjoy my work, please consider donating on [Buy Me a Coffee]() or [Ko-Fi]()!", + "short": "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!", + "description": "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!", + "tags": [ + "error", + "console", + "logs", + "dev", + "errorlogs" + ], + "requirements": ["git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git", "colorama"], + "min_bot_version": "3.5.0", + "end_user_data_statement": "This cog does not persistently store data or metadata about users." +} \ No newline at end of file diff --git a/consolelogs/locales/de-DE.po b/consolelogs/locales/de-DE.po new file mode 100644 index 0000000..f231231 --- /dev/null +++ b/consolelogs/locales/de-DE.po @@ -0,0 +1,94 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:24\n" +"Last-Translator: \n" +"Language-Team: German\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: de\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/consolelogs/locales/messages.pot\n" +"X-Crowdin-File-ID: 261\n" +"Language: de_DE\n" + +#: consolelogs\consolelogs.py:102 +#, docstring +msgid "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!" +msgstr "Ein Zahnrad zur Anzeige der Konsolenprotokolle, mit Schaltflächen und Filteroptionen, und zum Senden von Fehlerbefehlen in konfigurierten Kanälen!" + +#: consolelogs\consolelogs.py:264 +msgid "No logs to display." +msgstr "Es werden keine Protokolle angezeigt." + +#: consolelogs\consolelogs.py:415 +#, docstring +msgid "Scroll the console logs, for all levels/loggers or provided level/logger name." +msgstr "Blättern Sie in den Konsolenprotokollen für alle Ebenen/Logger oder für die angegebene Ebene/Loggernamen." + +#: consolelogs\consolelogs.py:451 +#, docstring +msgid "View the console logs one by one, for all levels/loggers or provided level/logger name." +msgstr "Zeigen Sie die Konsolenprotokolle einzeln an, für alle Ebenen/Protokollierer oder den angegebenen Namen der Ebene/Protokollierer." + +#: consolelogs\consolelogs.py:462 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "Zeigt die Statistiken für die Bot-Protokolle seit dem Start des Bots an." + +#: consolelogs\consolelogs.py:495 +#, docstring +msgid "Enable errors logging in a channel.\n\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "Aktivieren der Fehlerprotokollierung in einem Kanal.\n\n" +" **Parameter:**\n" +" - `Kanal`: Der Kanal, an den die Befehlsfehler gesendet werden sollen.\n" +" - global_errors\": Loggt Fehler für den gesamten Bot, nicht nur für den Channel-Server.\n" +" - prefixed_commands_errors`: Loggt Fehler bei vorangestellten Befehlen.\n" +" - Schrägstrich_befehle_Fehler\": Protokolliert Schrägstrich-Befehlsfehler.\n" +" - dpy_ignored_exceptions`: Loggt dpy ignorierte Ausnahmen (Ereignis-Listener und Views Fehler).\n" +" - `full_console`: Protokolliert alle Konsolenprotokolle.\n" +" - Gilde_einladen`: Fügt einen Button \"Guild Invite\" in den Kommandofehler-Logs hinzu, nur für Community-Server.\n" +" - ignorierte_kogs`: Ignoriere einige Cogs für `prefixed_commands_errors` und `slash_commands_errors`. Du musst den cog qualified_name wie `ConsoleLogs` für diesen cog verwenden.\n" +" " + +#: consolelogs\consolelogs.py:516 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "Ich habe nicht die Berechtigung, Einbettungen in diesem Kanal zu senden." + +#: consolelogs\consolelogs.py:532 +msgid "Errors logging enabled in {channel.mention}." +msgstr "Die Fehlerprotokollierung ist in {channel.mention}aktiviert." + +#: consolelogs\consolelogs.py:540 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "Deaktivieren Sie die Fehlerprotokollierung in einem Kanal." + +#: consolelogs\consolelogs.py:543 +msgid "Errors logging isn't enabled in this channel." +msgstr "Die Fehlerprotokollierung ist in diesem Kanal nicht aktiviert." + +#: consolelogs\consolelogs.py:546 +msgid "Errors logging disabled in {channel.mention}." +msgstr "Die Fehlerprotokollierung ist in {channel.mention}deaktiviert." + +#: consolelogs\consolelogs.py:550 +#, docstring +msgid "Get an embed to check loops status." +msgstr "Holen Sie sich eine Einbettung, um den Schleifenstatus zu überprüfen." + diff --git a/consolelogs/locales/el-GR.po b/consolelogs/locales/el-GR.po new file mode 100644 index 0000000..fe6d94c --- /dev/null +++ b/consolelogs/locales/el-GR.po @@ -0,0 +1,94 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:24\n" +"Last-Translator: \n" +"Language-Team: Greek\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: el\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/consolelogs/locales/messages.pot\n" +"X-Crowdin-File-ID: 261\n" +"Language: el_GR\n" + +#: consolelogs\consolelogs.py:102 +#, docstring +msgid "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!" +msgstr "Ένα γρανάζι για την εμφάνιση των αρχείων καταγραφής της κονσόλας, με κουμπιά και επιλογές φίλτρου, και για την αποστολή εντολών σφαλμάτων σε διαμορφωμένα κανάλια!" + +#: consolelogs\consolelogs.py:264 +msgid "No logs to display." +msgstr "Δεν υπάρχουν αρχεία καταγραφής για εμφάνιση." + +#: consolelogs\consolelogs.py:415 +#, docstring +msgid "Scroll the console logs, for all levels/loggers or provided level/logger name." +msgstr "Κύλιση των αρχείων καταγραφής της κονσόλας, για όλα τα επίπεδα/καταγραφείς ή για το όνομα επιπέδου/καταγραφέα που παρέχεται." + +#: consolelogs\consolelogs.py:451 +#, docstring +msgid "View the console logs one by one, for all levels/loggers or provided level/logger name." +msgstr "Προβολή των αρχείων καταγραφής της κονσόλας ένα προς ένα, για όλα τα επίπεδα/καταχωρητές ή για το όνομα του επιπέδου/καταχωρητή που παρέχεται." + +#: consolelogs\consolelogs.py:462 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "Εμφάνιση των στατιστικών για τα αρχεία καταγραφής του bot από την έναρξη του bot." + +#: consolelogs\consolelogs.py:495 +#, docstring +msgid "Enable errors logging in a channel.\n\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "Ενεργοποίηση της καταγραφής σφαλμάτων σε ένα κανάλι.\n\n" +" **Παράμετροι:**\n" +" - `κανάλι`: Το κανάλι στο οποίο θα αποστέλλονται τα σφάλματα των εντολών.\n" +" - `global_errors`: Καταγραφή σφαλμάτων για ολόκληρο το bot, όχι μόνο για τον διακομιστή του καναλιού.\n" +" - `prefixed_commands_errors`: Καταγραφή σφαλμάτων εντολών με πρόθεμα.\n" +" - `slash_commands_errors`: Καταγραφή σφαλμάτων εντολών slash.\n" +" - `dpy_ignored_exceptions`: Καταγραφή εξαιρέσεων που αγνοούνται από το dpy (σφάλματα ακροατών συμβάντων και Views).\n" +" - `full_console`: Καταγραφή όλων των αρχείων καταγραφής της κονσόλας.\n" +" - `guild_invite`: Προσθήκη ενός κουμπιού \"Guild Invite\" στα αρχεία καταγραφής σφαλμάτων εντολών, μόνο για διακομιστές κοινότητας.\n" +" - `ignored_cogs`: Αγνοήστε ορισμένα cogs για τα `prefixed_commands_errors` και `slash_commands_errors`. Θα πρέπει να χρησιμοποιήσετε το cog qualified_name όπως το `ConsoleLogs` για αυτό το cog.\n" +" " + +#: consolelogs\consolelogs.py:516 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "Δεν έχω τα δικαιώματα για να στέλνω ενσωματώσεις σε αυτό το κανάλι." + +#: consolelogs\consolelogs.py:532 +msgid "Errors logging enabled in {channel.mention}." +msgstr "Η καταγραφή σφαλμάτων είναι ενεργοποιημένη στο {channel.mention}." + +#: consolelogs\consolelogs.py:540 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "Απενεργοποίηση της καταγραφής σφαλμάτων σε ένα κανάλι." + +#: consolelogs\consolelogs.py:543 +msgid "Errors logging isn't enabled in this channel." +msgstr "Η καταγραφή σφαλμάτων δεν είναι ενεργοποιημένη σε αυτό το κανάλι." + +#: consolelogs\consolelogs.py:546 +msgid "Errors logging disabled in {channel.mention}." +msgstr "Η καταγραφή σφαλμάτων είναι απενεργοποιημένη στο {channel.mention}." + +#: consolelogs\consolelogs.py:550 +#, docstring +msgid "Get an embed to check loops status." +msgstr "Πάρτε μια ενσωμάτωση για να ελέγξετε την κατάσταση των βρόχων." + diff --git a/consolelogs/locales/es-ES.po b/consolelogs/locales/es-ES.po new file mode 100644 index 0000000..dab58af --- /dev/null +++ b/consolelogs/locales/es-ES.po @@ -0,0 +1,94 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:24\n" +"Last-Translator: \n" +"Language-Team: Spanish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: es-ES\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/consolelogs/locales/messages.pot\n" +"X-Crowdin-File-ID: 261\n" +"Language: es_ES\n" + +#: consolelogs\consolelogs.py:102 +#, docstring +msgid "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!" +msgstr "Un engranaje para mostrar los registros de la consola, con botones y opciones de filtrado, y para enviar comandos de errores en los canales configurados." + +#: consolelogs\consolelogs.py:264 +msgid "No logs to display." +msgstr "No hay registros para mostrar." + +#: consolelogs\consolelogs.py:415 +#, docstring +msgid "Scroll the console logs, for all levels/loggers or provided level/logger name." +msgstr "Desplácese por los registros de la consola, para todos los niveles/registradores o nombre de nivel/registrador proporcionado." + +#: consolelogs\consolelogs.py:451 +#, docstring +msgid "View the console logs one by one, for all levels/loggers or provided level/logger name." +msgstr "Ver los registros de la consola uno a uno, para todos los niveles/registradores o nombre de nivel/registrador proporcionado." + +#: consolelogs\consolelogs.py:462 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "Muestra las estadísticas de los registros del bot desde su inicio." + +#: consolelogs\consolelogs.py:495 +#, docstring +msgid "Enable errors logging in a channel.\n\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "Habilitar el registro de errores en un canal.\n\n" +" **Parámetros:**\n" +" - `canal`: El canal donde se enviarán los errores de los comandos.\n" +" - `global_errors`: Registra los errores de todo el bot, no sólo del servidor del canal.\n" +" - `prefixed_commands_errors`: Registra errores de comandos prefijados.\n" +" - `slash_commands_errors`: Registra errores de comandos de barra.\n" +" - `dpy_ignored_exceptions`: Registra las excepciones ignoradas por dpy (escuchas de eventos y errores de Views).\n" +" - `full_console`: Registra todos los logs de la consola.\n" +" - `guild_invite`: Añade un botón \"Guild Invite\" en los logs de errores de comandos, sólo para servidores community.\n" +" - `ignored_cogs`: Ignorar algunos cogs para `prefixed_commands_errors` y `slash_commands_errors`. Tienes que usar el nombre_calificado del cog como `ConsoleLogs` para este cog.\n" +" " + +#: consolelogs\consolelogs.py:516 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "No tengo permisos para enviar archivos incrustados en este canal." + +#: consolelogs\consolelogs.py:532 +msgid "Errors logging enabled in {channel.mention}." +msgstr "Registro de errores activado en {channel.mention}." + +#: consolelogs\consolelogs.py:540 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "Desactivar el registro de errores en un canal." + +#: consolelogs\consolelogs.py:543 +msgid "Errors logging isn't enabled in this channel." +msgstr "El registro de errores no está habilitado en este canal." + +#: consolelogs\consolelogs.py:546 +msgid "Errors logging disabled in {channel.mention}." +msgstr "Registro de errores desactivado en {channel.mention}." + +#: consolelogs\consolelogs.py:550 +#, docstring +msgid "Get an embed to check loops status." +msgstr "Obtenga una incrustación para comprobar el estado de los bucles." + diff --git a/consolelogs/locales/fi-FI.po b/consolelogs/locales/fi-FI.po new file mode 100644 index 0000000..8dd01b7 --- /dev/null +++ b/consolelogs/locales/fi-FI.po @@ -0,0 +1,94 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:24\n" +"Last-Translator: \n" +"Language-Team: Finnish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fi\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/consolelogs/locales/messages.pot\n" +"X-Crowdin-File-ID: 261\n" +"Language: fi_FI\n" + +#: consolelogs\consolelogs.py:102 +#, docstring +msgid "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!" +msgstr "Logi näyttää konsolin lokit, painikkeilla ja suodatusvaihtoehtoja, ja lähettää komentoja virheitä määritettyihin kanaviin!" + +#: consolelogs\consolelogs.py:264 +msgid "No logs to display." +msgstr "Ei näytettäviä lokitietoja." + +#: consolelogs\consolelogs.py:415 +#, docstring +msgid "Scroll the console logs, for all levels/loggers or provided level/logger name." +msgstr "Selaa konsolin lokitietoja kaikkien tasojen/loggaajien tai annetun tason/loggaajan nimen osalta." + +#: consolelogs\consolelogs.py:451 +#, docstring +msgid "View the console logs one by one, for all levels/loggers or provided level/logger name." +msgstr "Näytä konsolin lokit yksitellen, kaikkien tasojen/loggaajien tai tietyn tason/loggaajan nimen osalta." + +#: consolelogs\consolelogs.py:462 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "Näyttää botin lokitilastot botin käynnistymisestä lähtien." + +#: consolelogs\consolelogs.py:495 +#, docstring +msgid "Enable errors logging in a channel.\n\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "Ota virheiden kirjaaminen käyttöön kanavassa.\n\n" +" **Parametrit:**\n" +" - `kanava`: Kanava, johon komentojen virheet lähetetään.\n" +" - `global_errors`: Loggaa virheet koko botille, ei vain kanavapalvelimelle.\n" +" - `prefixed_commands_errors`: Kirjaa etukäteiskomentojen virheet.\n" +" - `slash_commands_errors`: Kirjaa slash-komentojen virheet.\n" +" - `dpy_ignored_exceptions`: Loki dpy:n huomiotta jätetyt poikkeukset (tapahtumien kuuntelijoiden ja näkymien virheet).\n" +" - `full_console`: Kirjaa kaikki konsolin lokit.\n" +" - `guild_invite`: Lisää painike \"Guild Invite\" komentojen virhelokiin, vain yhteisöpalvelimille.\n" +" - `ignored_cogs`: Joidenkin lokien huomiotta jättäminen `prefixed_commands_errors`- ja `slash_commands_errors`-virheissä. Sinun on käytettävä tätä lokia varten lokin pätevää nimeä, kuten `ConsoleLogs`.\n" +" " + +#: consolelogs\consolelogs.py:516 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "Minulla ei ole oikeuksia lähettää upotuksia tällä kanavalla." + +#: consolelogs\consolelogs.py:532 +msgid "Errors logging enabled in {channel.mention}." +msgstr "Virheiden kirjaaminen on käytössä osoitteessa {channel.mention}." + +#: consolelogs\consolelogs.py:540 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "Poista virheiden kirjaaminen käytöstä kanavassa." + +#: consolelogs\consolelogs.py:543 +msgid "Errors logging isn't enabled in this channel." +msgstr "Virheiden kirjaaminen ei ole käytössä tällä kanavalla." + +#: consolelogs\consolelogs.py:546 +msgid "Errors logging disabled in {channel.mention}." +msgstr "Virheiden kirjaaminen ei ole käytössä osoitteessa {channel.mention}." + +#: consolelogs\consolelogs.py:550 +#, docstring +msgid "Get an embed to check loops status." +msgstr "Hanki upotus silmukoiden tilan tarkistamiseksi." + diff --git a/consolelogs/locales/fr-FR.po b/consolelogs/locales/fr-FR.po new file mode 100644 index 0000000..d1f6ae5 --- /dev/null +++ b/consolelogs/locales/fr-FR.po @@ -0,0 +1,94 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:24\n" +"Last-Translator: \n" +"Language-Team: French\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/consolelogs/locales/messages.pot\n" +"X-Crowdin-File-ID: 261\n" +"Language: fr_FR\n" + +#: consolelogs\consolelogs.py:102 +#, docstring +msgid "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!" +msgstr "Un mécanisme pour afficher les journaux de la console, avec des boutons et des options de filtrage, et pour envoyer des commandes d'erreurs dans les canaux configurés !" + +#: consolelogs\consolelogs.py:264 +msgid "No logs to display." +msgstr "Aucun journal à afficher." + +#: consolelogs\consolelogs.py:415 +#, docstring +msgid "Scroll the console logs, for all levels/loggers or provided level/logger name." +msgstr "Fait défiler les journaux de la console, pour tous les niveaux/enregistreurs ou pour un niveau/enregistreur donné." + +#: consolelogs\consolelogs.py:451 +#, docstring +msgid "View the console logs one by one, for all levels/loggers or provided level/logger name." +msgstr "Visualiser les journaux de la console un par un, pour tous les niveaux/déclencheurs ou pour un niveau/déclencheur donné." + +#: consolelogs\consolelogs.py:462 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "Affiche les statistiques des logs du bot depuis son démarrage." + +#: consolelogs\consolelogs.py:495 +#, docstring +msgid "Enable errors logging in a channel.\n\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "Active la journalisation des erreurs dans un canal.\n\n" +" **Paramètres:**\n" +" - `canal` : Le canal où les erreurs de commandes seront envoyées.\n" +" - `global_errors` : Enregistre les erreurs pour l'ensemble du bot, et pas seulement pour le serveur du canal.\n" +" - `prefixed_commands_errors` : Enregistre les erreurs de commandes préfixées.\n" +" - `slash_commands_errors` : Journalise les erreurs des commandes slash.\n" +" - `dpy_ignored_exceptions` : Enregistre les exceptions ignorées par dpy (erreurs des auditeurs d'événements et des vues).\n" +" - `full_console` : Enregistre tous les logs de la console.\n" +" - `guild_invite` : Ajoute un bouton \"Guild Invite\" dans les logs d'erreurs des commandes, seulement pour les serveurs communautaires.\n" +" - `ignored_cogs` : Ignore certains cogs pour `prefixed_commands_errors` et `slash_commands_errors`. Vous devez utiliser le nom qualifié du cog comme `ConsoleLogs` pour ce cog.\n" +" " + +#: consolelogs\consolelogs.py:516 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "Je n'ai pas les autorisations nécessaires pour envoyer des liens dans ce canal." + +#: consolelogs\consolelogs.py:532 +msgid "Errors logging enabled in {channel.mention}." +msgstr "La journalisation des erreurs est activée sur {channel.mention}." + +#: consolelogs\consolelogs.py:540 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "Désactive l'enregistrement des erreurs dans un canal." + +#: consolelogs\consolelogs.py:543 +msgid "Errors logging isn't enabled in this channel." +msgstr "La journalisation des erreurs n'est pas activée dans ce canal." + +#: consolelogs\consolelogs.py:546 +msgid "Errors logging disabled in {channel.mention}." +msgstr "La journalisation des erreurs est désactivée sur le site {channel.mention}." + +#: consolelogs\consolelogs.py:550 +#, docstring +msgid "Get an embed to check loops status." +msgstr "Obtenir un embed pour vérifier l'état des boucles." + diff --git a/consolelogs/locales/it-IT.po b/consolelogs/locales/it-IT.po new file mode 100644 index 0000000..a20c326 --- /dev/null +++ b/consolelogs/locales/it-IT.po @@ -0,0 +1,94 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:24\n" +"Last-Translator: \n" +"Language-Team: Italian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: it\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/consolelogs/locales/messages.pot\n" +"X-Crowdin-File-ID: 261\n" +"Language: it_IT\n" + +#: consolelogs\consolelogs.py:102 +#, docstring +msgid "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!" +msgstr "Un ingranaggio per visualizzare i log della console, con pulsanti e opzioni di filtro, e per inviare comandi di errore nei canali configurati!" + +#: consolelogs\consolelogs.py:264 +msgid "No logs to display." +msgstr "Nessun registro da visualizzare." + +#: consolelogs\consolelogs.py:415 +#, docstring +msgid "Scroll the console logs, for all levels/loggers or provided level/logger name." +msgstr "Scorre i registri della console, per tutti i livelli/logger o per il nome del livello/logger fornito." + +#: consolelogs\consolelogs.py:451 +#, docstring +msgid "View the console logs one by one, for all levels/loggers or provided level/logger name." +msgstr "Visualizza i registri della console uno per uno, per tutti i livelli/logger o per il nome del livello/logger fornito." + +#: consolelogs\consolelogs.py:462 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "Visualizza le statistiche dei log del bot dall'avvio del bot." + +#: consolelogs\consolelogs.py:495 +#, docstring +msgid "Enable errors logging in a channel.\n\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "Abilita la registrazione degli errori in un canale.\n\n" +" **Parametri:**\n" +" - `canale`: Il canale dove verranno inviati gli errori dei comandi.\n" +" - `global_errors`: Registra gli errori per l'intero bot, non solo per il server del canale.\n" +" - `prefixed_commands_errors`: Registra gli errori dei comandi prefissati.\n" +" - Errori_comandi_slash`: Registra gli errori dei comandi slash.\n" +" - `dpy_ignored_exceptions`: Registra le eccezioni ignorate da dpy (errori degli ascoltatori di eventi e delle viste).\n" +" - `full_console`: Registra tutti i log della console.\n" +" - `guild_invite`: Aggiunge un pulsante \"Invita la gilda\" nei log degli errori dei comandi, solo per i server della comunità.\n" +" - `ignored_cogs`: Ignora alcuni cog per `prefixed_commands_errors` e `slash_commands_errors`. È necessario utilizzare il nome qualificato del cog, come `ConsoleLogs`, per questo cog.\n" +" " + +#: consolelogs\consolelogs.py:516 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "Non ho i permessi per inviare gli embed in questo canale." + +#: consolelogs\consolelogs.py:532 +msgid "Errors logging enabled in {channel.mention}." +msgstr "Registrazione degli errori abilitata in {channel.mention}." + +#: consolelogs\consolelogs.py:540 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "Disabilita la registrazione degli errori in un canale." + +#: consolelogs\consolelogs.py:543 +msgid "Errors logging isn't enabled in this channel." +msgstr "La registrazione degli errori non è abilitata in questo canale." + +#: consolelogs\consolelogs.py:546 +msgid "Errors logging disabled in {channel.mention}." +msgstr "Registrazione degli errori disabilitata in {channel.mention}." + +#: consolelogs\consolelogs.py:550 +#, docstring +msgid "Get an embed to check loops status." +msgstr "Ottenere un embed per controllare lo stato dei loop." + diff --git a/consolelogs/locales/ja-JP.po b/consolelogs/locales/ja-JP.po new file mode 100644 index 0000000..40669a2 --- /dev/null +++ b/consolelogs/locales/ja-JP.po @@ -0,0 +1,94 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:24\n" +"Last-Translator: \n" +"Language-Team: Japanese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ja\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/consolelogs/locales/messages.pot\n" +"X-Crowdin-File-ID: 261\n" +"Language: ja_JP\n" + +#: consolelogs\consolelogs.py:102 +#, docstring +msgid "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!" +msgstr "コンソールのログを表示し、ボタンとフィルターオプションがあり、設定されたチャンネルでコマンドエラーを送信するための歯車!" + +#: consolelogs\consolelogs.py:264 +msgid "No logs to display." +msgstr "表示するログがない。" + +#: consolelogs\consolelogs.py:415 +#, docstring +msgid "Scroll the console logs, for all levels/loggers or provided level/logger name." +msgstr "すべてのレベル/ロガー、または指定されたレベル/ロガー名のコンソール・ログをスクロールする。" + +#: consolelogs\consolelogs.py:451 +#, docstring +msgid "View the console logs one by one, for all levels/loggers or provided level/logger name." +msgstr "すべてのレベル/ロガー、または指定されたレベル/ロガー名のコンソール・ログを1つずつ表示します。" + +#: consolelogs\consolelogs.py:462 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "ボット開始からのボットログの統計情報を表示します。" + +#: consolelogs\consolelogs.py:495 +#, docstring +msgid "Enable errors logging in a channel.\n\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "チャンネルのエラーログを有効にする。\n\n" +" **Parameters:**\n" +" - `channel`:コマンドのエラーを送信するチャンネル。\n" +" - global_errors`:チャンネルサーバーだけでなく、ボット全体のエラーを記録する。\n" +" - prefixed_commands_errors`:接頭辞付きコマンドのエラーをログに記録する。\n" +" - `slash_commands_errors`: スラッシュコマンドのエラーをログに記録する:スラッシュコマンドのエラーをログに記録する。\n" +" - dpy_ignored_exceptions`: dpy が無視した例外を記録する:dpy が無視した例外 (イベントリスナーと Views のエラー) を記録する。\n" +" - full_console`: 全てのコンソールログを記録する:全てのコンソールログを記録する。\n" +" - guild_invite`: ギルド招待ボタンを追加する:コミュニティサーバーでのみ、コマンドのエラーログに \"Guild Invite\" ボタンを追加する。\n" +" - ignore_cogs`:prefixed_commands_errors`と`slash_commands_errors`のコグを無視するようにした。このコグには `ConsoleLogs` のような修飾名を使う必要があります。\n" +" " + +#: consolelogs\consolelogs.py:516 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "このチャンネルでエンベッドを送信する権限がありません。" + +#: consolelogs\consolelogs.py:532 +msgid "Errors logging enabled in {channel.mention}." +msgstr "{channel.mention}でエラーログを有効にする。" + +#: consolelogs\consolelogs.py:540 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "チャンネルのエラーログを無効にする。" + +#: consolelogs\consolelogs.py:543 +msgid "Errors logging isn't enabled in this channel." +msgstr "このチャンネルではエラーログは有効になっていない。" + +#: consolelogs\consolelogs.py:546 +msgid "Errors logging disabled in {channel.mention}." +msgstr "{channel.mention}でエラーロギングを無効にする。" + +#: consolelogs\consolelogs.py:550 +#, docstring +msgid "Get an embed to check loops status." +msgstr "ループの状態を確認するためのエンベデッドを取得します。" + diff --git a/consolelogs/locales/messages.pot b/consolelogs/locales/messages.pot new file mode 100644 index 0000000..c9e0179 --- /dev/null +++ b/consolelogs/locales/messages.pot @@ -0,0 +1,85 @@ +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2024-12-29 10:43+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" + +#: consolelogs\consolelogs.py:105 +#, docstring +msgid "" +"A cog to display the console logs, with buttons and filter options, and to " +"send commands errors in configured channels!" +msgstr "" + +#: consolelogs\consolelogs.py:281 +msgid "No logs to display." +msgstr "" + +#: consolelogs\consolelogs.py:434 +#, docstring +msgid "" +"Scroll the console logs, for all levels/loggers or provided level/logger " +"name." +msgstr "" + +#: consolelogs\consolelogs.py:470 +#, docstring +msgid "" +"View the console logs one by one, for all levels/loggers or provided " +"level/logger name." +msgstr "" + +#: consolelogs\consolelogs.py:481 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "" + +#: consolelogs\consolelogs.py:514 +#, docstring +msgid "" +"Enable errors logging in a channel.\n" +"\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "" + +#: consolelogs\consolelogs.py:535 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "" + +#: consolelogs\consolelogs.py:553 +msgid "Errors logging enabled in {channel.mention}." +msgstr "" + +#: consolelogs\consolelogs.py:561 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "" + +#: consolelogs\consolelogs.py:564 +msgid "Errors logging isn't enabled in this channel." +msgstr "" + +#: consolelogs\consolelogs.py:567 +msgid "Errors logging disabled in {channel.mention}." +msgstr "" + +#: consolelogs\consolelogs.py:571 +#, docstring +msgid "Get an embed to check loops status." +msgstr "" diff --git a/consolelogs/locales/nl-NL.po b/consolelogs/locales/nl-NL.po new file mode 100644 index 0000000..4333ee0 --- /dev/null +++ b/consolelogs/locales/nl-NL.po @@ -0,0 +1,94 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:24\n" +"Last-Translator: \n" +"Language-Team: Dutch\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: nl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/consolelogs/locales/messages.pot\n" +"X-Crowdin-File-ID: 261\n" +"Language: nl_NL\n" + +#: consolelogs\consolelogs.py:102 +#, docstring +msgid "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!" +msgstr "Een cog om console logs weer te geven, met knoppen en filter opties, en om commando's te versturen in ingestelde kanalen!" + +#: consolelogs\consolelogs.py:264 +msgid "No logs to display." +msgstr "Geen logs om weer te geven." + +#: consolelogs\consolelogs.py:415 +#, docstring +msgid "Scroll the console logs, for all levels/loggers or provided level/logger name." +msgstr "Scroll door de console logs, voor alle levels/loggers of geef een naam van een level/logger." + +#: consolelogs\consolelogs.py:451 +#, docstring +msgid "View the console logs one by one, for all levels/loggers or provided level/logger name." +msgstr "Bekijk de console logs één voor één, voor alle levels/loggers of geef een naam van een level/logger." + +#: consolelogs\consolelogs.py:462 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "De statistieken van de botlogs sinds de botstart weergeven." + +#: consolelogs\consolelogs.py:495 +#, docstring +msgid "Enable errors logging in a channel.\n\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "Het loggen van fouten in een kanaal inschakelen.\n\n" +" **Parameters:**\n" +" - `kanaal`: Het kanaal waar de fouten van de commando's naartoe worden gestuurd.\n" +" - `global_errors`: Log fouten voor de hele bot, niet alleen de kanaalserver.\n" +" - `prefixed_commands_errors`: Log prefixed commando fouten.\n" +" - `slash_commands_errors`: Log slash commando fouten.\n" +" - `dpy_geaccepteerde_fouten`: Log dpy genegeerde uitzonderingen (events listeners en Views fouten).\n" +" - `full_console`: Log alle console logs.\n" +" - gilde uitnodigen`: Voeg een knop \"Guild Invite\" toe in commando fouten logs, alleen voor community servers.\n" +" - genegeerde_koggen`: Negeer sommige tandwielen voor `prefixed_commands_errors` en `slash_commands_errors`. Je moet de cog qualified_name gebruiken zoals `ConsoleLogs` voor deze cog.\n" +" " + +#: consolelogs\consolelogs.py:516 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "Ik heb geen permissies om embeds te sturen in dit channel." + +#: consolelogs\consolelogs.py:532 +msgid "Errors logging enabled in {channel.mention}." +msgstr "Foutregistratie ingeschakeld in {channel.mention}." + +#: consolelogs\consolelogs.py:540 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "Schakel foutregistratie uit in een kanaal." + +#: consolelogs\consolelogs.py:543 +msgid "Errors logging isn't enabled in this channel." +msgstr "Foutregistratie is niet ingeschakeld in dit kanaal." + +#: consolelogs\consolelogs.py:546 +msgid "Errors logging disabled in {channel.mention}." +msgstr "Foutregistratie uitgeschakeld in {channel.mention}." + +#: consolelogs\consolelogs.py:550 +#, docstring +msgid "Get an embed to check loops status." +msgstr "Krijg een embed om de lusstatus te controleren." + diff --git a/consolelogs/locales/pl-PL.po b/consolelogs/locales/pl-PL.po new file mode 100644 index 0000000..27a7c90 --- /dev/null +++ b/consolelogs/locales/pl-PL.po @@ -0,0 +1,94 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:24\n" +"Last-Translator: \n" +"Language-Team: Polish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/consolelogs/locales/messages.pot\n" +"X-Crowdin-File-ID: 261\n" +"Language: pl_PL\n" + +#: consolelogs\consolelogs.py:102 +#, docstring +msgid "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!" +msgstr "Tryb do wyświetlania logów konsoli, z przyciskami i opcjami filtrowania oraz do wysyłania błędów poleceń w skonfigurowanych kanałach!" + +#: consolelogs\consolelogs.py:264 +msgid "No logs to display." +msgstr "Brak dzienników do wyświetlenia." + +#: consolelogs\consolelogs.py:415 +#, docstring +msgid "Scroll the console logs, for all levels/loggers or provided level/logger name." +msgstr "Przewija logi konsoli dla wszystkich poziomów/loggerów lub dla podanego poziomu/nazwy loggera." + +#: consolelogs\consolelogs.py:451 +#, docstring +msgid "View the console logs one by one, for all levels/loggers or provided level/logger name." +msgstr "Wyświetlanie dzienników konsoli jeden po drugim, dla wszystkich poziomów/logerów lub podanej nazwy poziomu/logera." + +#: consolelogs\consolelogs.py:462 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "Wyświetla statystyki logów bota od momentu jego uruchomienia." + +#: consolelogs\consolelogs.py:495 +#, docstring +msgid "Enable errors logging in a channel.\n\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "Włącz rejestrowanie błędów w kanale.\n\n" +" **Parametry:**\n" +" - `channel`: Kanał, na który będą wysyłane błędy poleceń.\n" +" - `global_errors`: Loguje błędy dla całego bota, nie tylko dla serwera kanału.\n" +" - `prefixed_commands_errors`: Loguje błędy poleceń z prefiksem.\n" +" - `slash_commands_errors`: Loguje błędy poleceń z ukośnikiem.\n" +" - `dpy_ignored_exceptions`: Loguje zignorowane wyjątki dpy (event listeners i Views errors).\n" +" - `full_console`: Loguje wszystkie logi konsoli.\n" +" - `guild_invite`: Dodaje przycisk \"Guild Invite\" w logach błędów komend, tylko dla serwerów społeczności.\n" +" - `ignored_cogs`: Ignorowanie niektórych trybów dla `prefixed_commands_errors` i `slash_commands_errors`. Musisz użyć kwalifikowanej nazwy trybiku, takiej jak `ConsoleLogs` dla tego trybiku.\n" +" " + +#: consolelogs\consolelogs.py:516 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "Nie mam uprawnień do wysyłania embedów na tym kanale." + +#: consolelogs\consolelogs.py:532 +msgid "Errors logging enabled in {channel.mention}." +msgstr "Rejestrowanie błędów włączone w {channel.mention}." + +#: consolelogs\consolelogs.py:540 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "Wyłączenie rejestrowania błędów w kanale." + +#: consolelogs\consolelogs.py:543 +msgid "Errors logging isn't enabled in this channel." +msgstr "Rejestrowanie błędów nie jest włączone w tym kanale." + +#: consolelogs\consolelogs.py:546 +msgid "Errors logging disabled in {channel.mention}." +msgstr "Rejestrowanie błędów wyłączone w {channel.mention}." + +#: consolelogs\consolelogs.py:550 +#, docstring +msgid "Get an embed to check loops status." +msgstr "Pobierz embed, aby sprawdzić status pętli." + diff --git a/consolelogs/locales/pt-BR.po b/consolelogs/locales/pt-BR.po new file mode 100644 index 0000000..5fa92ad --- /dev/null +++ b/consolelogs/locales/pt-BR.po @@ -0,0 +1,94 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:24\n" +"Last-Translator: \n" +"Language-Team: Portuguese, Brazilian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-BR\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/consolelogs/locales/messages.pot\n" +"X-Crowdin-File-ID: 261\n" +"Language: pt_BR\n" + +#: consolelogs\consolelogs.py:102 +#, docstring +msgid "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!" +msgstr "Uma engrenagem para visualizar os registos da consola, com botões e opções de filtragem, e para enviar comandos de erros nos canais configurados!" + +#: consolelogs\consolelogs.py:264 +msgid "No logs to display." +msgstr "Não há registos a apresentar." + +#: consolelogs\consolelogs.py:415 +#, docstring +msgid "Scroll the console logs, for all levels/loggers or provided level/logger name." +msgstr "Percorre os registos da consola, para todos os níveis/loggers ou para o nome do nível/logger fornecido." + +#: consolelogs\consolelogs.py:451 +#, docstring +msgid "View the console logs one by one, for all levels/loggers or provided level/logger name." +msgstr "Ver os registos da consola um a um, para todos os níveis/registadores ou para o nome do nível/registador fornecido." + +#: consolelogs\consolelogs.py:462 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "Exibe as estatísticas dos registros do bot desde o início do bot." + +#: consolelogs\consolelogs.py:495 +#, docstring +msgid "Enable errors logging in a channel.\n\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "Ativar o registro de erros em um canal.\n\n" +" **Parametros:**\n" +" - `channel`: O canal para o qual os erros dos comandos serão enviados.\n" +" - `global_errors`: Registrar erros para todo o bot, não apenas para o servidor do canal.\n" +" - `prefixed_commands_errors`: Registra erros de comandos prefixados.\n" +" - `slash_commands_errors`: Registra erros de comandos de barra.\n" +" - `dpy_ignored_exceptions`: Registra exceções ignoradas pelo dpy (ouvintes de eventos e erros de visualizações).\n" +" - `full_console`: Registra todos os logs do console.\n" +" - `guild_invite`: Adiciona um botão \"Guild Invite\" nos registros de erros dos comandos, apenas para servidores comunitários.\n" +" - `ignored_cogs`: Ignora alguns cogs para `prefixed_commands_errors` e `slash_commands_errors`. Você deve usar o nome qualificado do cog como `ConsoleLogs` para esse cog.\n" +" " + +#: consolelogs\consolelogs.py:516 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "Não tenho permissões para enviar incorporações neste canal." + +#: consolelogs\consolelogs.py:532 +msgid "Errors logging enabled in {channel.mention}." +msgstr "Registo de erros ativado em {channel.mention}." + +#: consolelogs\consolelogs.py:540 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "Desativar o registo de erros num canal." + +#: consolelogs\consolelogs.py:543 +msgid "Errors logging isn't enabled in this channel." +msgstr "O registo de erros não está ativado neste canal." + +#: consolelogs\consolelogs.py:546 +msgid "Errors logging disabled in {channel.mention}." +msgstr "Registo de erros desativado em {channel.mention}." + +#: consolelogs\consolelogs.py:550 +#, docstring +msgid "Get an embed to check loops status." +msgstr "Obter uma incorporação para verificar o estado dos loops." + diff --git a/consolelogs/locales/pt-PT.po b/consolelogs/locales/pt-PT.po new file mode 100644 index 0000000..850c3c4 --- /dev/null +++ b/consolelogs/locales/pt-PT.po @@ -0,0 +1,94 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:24\n" +"Last-Translator: \n" +"Language-Team: Portuguese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-PT\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/consolelogs/locales/messages.pot\n" +"X-Crowdin-File-ID: 261\n" +"Language: pt_PT\n" + +#: consolelogs\consolelogs.py:102 +#, docstring +msgid "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!" +msgstr "Uma engrenagem para visualizar os registos da consola, com botões e opções de filtragem, e para enviar comandos de erros nos canais configurados!" + +#: consolelogs\consolelogs.py:264 +msgid "No logs to display." +msgstr "Não há registos a apresentar." + +#: consolelogs\consolelogs.py:415 +#, docstring +msgid "Scroll the console logs, for all levels/loggers or provided level/logger name." +msgstr "Percorre os registos da consola, para todos os níveis/loggers ou para o nome do nível/logger fornecido." + +#: consolelogs\consolelogs.py:451 +#, docstring +msgid "View the console logs one by one, for all levels/loggers or provided level/logger name." +msgstr "Ver os registos da consola um a um, para todos os níveis/registadores ou para o nome do nível/registador fornecido." + +#: consolelogs\consolelogs.py:462 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "Mostra as estatísticas dos logs do bot desde o início do bot." + +#: consolelogs\consolelogs.py:495 +#, docstring +msgid "Enable errors logging in a channel.\n\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "Ativar o registo de erros num canal.\n\n" +" **Parâmetros:**\n" +" - `canal`: O canal para onde serão enviados os erros dos comandos.\n" +" - `global_errors`: Logar erros para todo o bot, não apenas para o servidor do canal.\n" +" - `erros_comandos_prefixados`: Registra erros de comandos prefixados.\n" +" - `slash_commands_errors`: Registra erros de comandos com barra.\n" +" - `dpy_ignored_exceptions`: Registra exceções ignoradas pelo dpy (eventos de escuta e erros de Views).\n" +" - `full_console`: Registra todos os logs do console.\n" +" - `guild_invite`: Adiciona um botão \"Guild Invite\" nos logs de erros dos comandos, apenas para servidores comunitários.\n" +" - `ignored_cogs`: Ignora alguns cogs para `erros_de_comandos_prefixados` e `erros_de_comandos_com barra`. É necessário utilizar o nome qualificado do cog como `ConsoleLogs` para este cog.\n" +" " + +#: consolelogs\consolelogs.py:516 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "Não tenho permissões para enviar incorporações neste canal." + +#: consolelogs\consolelogs.py:532 +msgid "Errors logging enabled in {channel.mention}." +msgstr "Registo de erros ativado em {channel.mention}." + +#: consolelogs\consolelogs.py:540 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "Desativar o registo de erros num canal." + +#: consolelogs\consolelogs.py:543 +msgid "Errors logging isn't enabled in this channel." +msgstr "O registo de erros não está ativado neste canal." + +#: consolelogs\consolelogs.py:546 +msgid "Errors logging disabled in {channel.mention}." +msgstr "Registo de erros desativado em {channel.mention}." + +#: consolelogs\consolelogs.py:550 +#, docstring +msgid "Get an embed to check loops status." +msgstr "Obter uma incorporação para verificar o estado dos loops." + diff --git a/consolelogs/locales/ro-RO.po b/consolelogs/locales/ro-RO.po new file mode 100644 index 0000000..38be740 --- /dev/null +++ b/consolelogs/locales/ro-RO.po @@ -0,0 +1,94 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:24\n" +"Last-Translator: \n" +"Language-Team: Romanian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ro\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/consolelogs/locales/messages.pot\n" +"X-Crowdin-File-ID: 261\n" +"Language: ro_RO\n" + +#: consolelogs\consolelogs.py:102 +#, docstring +msgid "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!" +msgstr "O rotiță pentru a afișa jurnalele consolei, cu butoane și opțiuni de filtrare, și pentru a trimite erori de comandă în canalele configurate!" + +#: consolelogs\consolelogs.py:264 +msgid "No logs to display." +msgstr "Nu există jurnale de afișat." + +#: consolelogs\consolelogs.py:415 +#, docstring +msgid "Scroll the console logs, for all levels/loggers or provided level/logger name." +msgstr "Derulează jurnalele consolei, pentru toate nivelurile/loggerii sau pentru numele nivelului/loggerului furnizat." + +#: consolelogs\consolelogs.py:451 +#, docstring +msgid "View the console logs one by one, for all levels/loggers or provided level/logger name." +msgstr "Vizualizați jurnalele de consolă unul câte unul, pentru toate nivelurile/loggerii sau pentru numele nivelului/loggerului furnizat." + +#: consolelogs\consolelogs.py:462 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "Afișează statisticile pentru jurnalele robotului de la pornirea robotului." + +#: consolelogs\consolelogs.py:495 +#, docstring +msgid "Enable errors logging in a channel.\n\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "Activați înregistrarea erorilor într-un canal.\n\n" +" **Parametri:**\n" +" - `channel`: Canalul unde vor fi trimise erorile de comandă.\n" +" - `global_errors`: Jurnalizează erorile pentru întregul bot, nu doar pentru serverul canalului.\n" +" - `prefixed_commands_errors`: Înregistrarea erorilor de comenzi prefixate.\n" +" - `slash_commands_errors`: Jurnalizează erorile comenzilor slash.\n" +" - `dpy_ignored_exceptions`: Înregistrați excepțiile ignorate de dpy (erori de ascultare a evenimentelor și erori de vizualizare).\n" +" - `full_console`: Înregistrează toate jurnalele de consolă.\n" +" - `guild_invite`: Adăugați un buton \"Guild Invite\" în jurnalele de erori de comandă, numai pentru serverele comunitare.\n" +" - `ignored_cogs`: Ignoră unele cogs pentru `prefixed_commands_errors` și `slash_commands_errors`. Trebuie să folosiți numele calificat al cog-ului, cum ar fi `ConsoleLogs` pentru acest cog.\n" +" " + +#: consolelogs\consolelogs.py:516 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "Nu am permisiunile necesare pentru a trimite imagini în acest canal." + +#: consolelogs\consolelogs.py:532 +msgid "Errors logging enabled in {channel.mention}." +msgstr "Înregistrarea erorilor este activată în {channel.mention}." + +#: consolelogs\consolelogs.py:540 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "Dezactivați înregistrarea erorilor într-un canal." + +#: consolelogs\consolelogs.py:543 +msgid "Errors logging isn't enabled in this channel." +msgstr "Înregistrarea erorilor nu este activată în acest canal." + +#: consolelogs\consolelogs.py:546 +msgid "Errors logging disabled in {channel.mention}." +msgstr "Înregistrarea erorilor este dezactivată în {channel.mention}." + +#: consolelogs\consolelogs.py:550 +#, docstring +msgid "Get an embed to check loops status." +msgstr "Obțineți o inserție pentru a verifica starea buclelor." + diff --git a/consolelogs/locales/ru-RU.po b/consolelogs/locales/ru-RU.po new file mode 100644 index 0000000..695c16a --- /dev/null +++ b/consolelogs/locales/ru-RU.po @@ -0,0 +1,94 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:24\n" +"Last-Translator: \n" +"Language-Team: Russian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ru\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/consolelogs/locales/messages.pot\n" +"X-Crowdin-File-ID: 261\n" +"Language: ru_RU\n" + +#: consolelogs\consolelogs.py:102 +#, docstring +msgid "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!" +msgstr "Ког для отображения журналов консоли, с кнопками и опциями фильтрации, а также для отправки команд ошибок в настроенных каналах!" + +#: consolelogs\consolelogs.py:264 +msgid "No logs to display." +msgstr "Нет журналов для отображения." + +#: consolelogs\consolelogs.py:415 +#, docstring +msgid "Scroll the console logs, for all levels/loggers or provided level/logger name." +msgstr "Прокрутка журналов консоли для всех уровней/логгеров или указанного уровня/имени блоггера." + +#: consolelogs\consolelogs.py:451 +#, docstring +msgid "View the console logs one by one, for all levels/loggers or provided level/logger name." +msgstr "Просмотр журналов консоли по одному, для всех уровней/логгеров или указанного уровня/имени блоггера." + +#: consolelogs\consolelogs.py:462 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "Отображение статистики журналов бота с момента его запуска." + +#: consolelogs\consolelogs.py:495 +#, docstring +msgid "Enable errors logging in a channel.\n\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "Включение регистрации ошибок в канале.\n\n" +" **Параметры:**\n" +" - `канал`: Канал, в который будут отправляться ошибки команд.\n" +" - `global_errors`: Вести журнал ошибок для всего бота, а не только для сервера канала.\n" +" - `prefixed_commands_errors`: Заносить в журнал ошибки префиксных команд.\n" +" - `слэш_команды_ошибки`: Вести журнал ошибок слэш-команд.\n" +" - `dpy_ignored_exceptions`: Журнал игнорируемых dpy исключений (слушателей событий и ошибок Views).\n" +" - `full_console`: Запись всех логов консоли.\n" +" - `guild_invite`: Добавляет кнопку \"Пригласить в гильдию\" в журналы ошибок команд, только для серверов сообщества.\n" +" - `ignored_cogs`: Игнорировать некоторые коги для `prefixed_commands_errors` и `lash_commands_errors`. Вы должны использовать квалифицированное имя_кога, например `ConsoleLogs` для этого кога.\n" +" " + +#: consolelogs\consolelogs.py:516 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "У меня нет прав на отправку вложений в этом канале." + +#: consolelogs\consolelogs.py:532 +msgid "Errors logging enabled in {channel.mention}." +msgstr "Регистрация ошибок включена в {channel.mention}." + +#: consolelogs\consolelogs.py:540 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "Отключение регистрации ошибок в канале." + +#: consolelogs\consolelogs.py:543 +msgid "Errors logging isn't enabled in this channel." +msgstr "Регистрация ошибок в этом канале не включена." + +#: consolelogs\consolelogs.py:546 +msgid "Errors logging disabled in {channel.mention}." +msgstr "Регистрация ошибок отключена в {channel.mention}." + +#: consolelogs\consolelogs.py:550 +#, docstring +msgid "Get an embed to check loops status." +msgstr "Получите вставку для проверки состояния петель." + diff --git a/consolelogs/locales/tr-TR.po b/consolelogs/locales/tr-TR.po new file mode 100644 index 0000000..2950e8f --- /dev/null +++ b/consolelogs/locales/tr-TR.po @@ -0,0 +1,94 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 13:27\n" +"Last-Translator: \n" +"Language-Team: Turkish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: tr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/consolelogs/locales/messages.pot\n" +"X-Crowdin-File-ID: 261\n" +"Language: tr_TR\n" + +#: consolelogs\consolelogs.py:102 +#, docstring +msgid "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!" +msgstr "Düğmeler ve filtre seçenekleri ile konsol günlüklerini görüntülemek ve yapılandırılmış kanallara komut hatalarını göndermek için bir cog!" + +#: consolelogs\consolelogs.py:264 +msgid "No logs to display." +msgstr "Görüntülenecek günlük yok." + +#: consolelogs\consolelogs.py:415 +#, docstring +msgid "Scroll the console logs, for all levels/loggers or provided level/logger name." +msgstr "Tüm seviyeler/günlükçüler veya belirtilen seviye/günlükçü adı için konsol günlüklerini kaydırın." + +#: consolelogs\consolelogs.py:451 +#, docstring +msgid "View the console logs one by one, for all levels/loggers or provided level/logger name." +msgstr "Tüm seviyeler/günlükçüler veya belirtilen seviye/günlükçü adı için konsol günlüklerini tek tek görüntüleyin." + +#: consolelogs\consolelogs.py:462 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "Bot başlatıldığından beri bot günlüklerinin istatistiklerini görüntüleyin." + +#: consolelogs\consolelogs.py:495 +#, docstring +msgid "Enable errors logging in a channel.\n\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "Bir kanalda hata günlüğünü etkinleştirin.\n\n" +" **Parametreler:**\n" +" - `channel`: Komut hatalarının gönderileceği kanal.\n" +" - `global_errors`: Hataları sadece kanal sunucusu için değil, tüm bot için günlüğe kaydedin.\n" +" - `prefixed_commands_errors`: Ön ekli komut hatalarını günlüğe kaydedin.\n" +" - `slash_commands_errors`: Eğik çizgi komutları hatalarını günlüğe kaydedin.\n" +" - `dpy_ignored_exceptions`: Dpy yok sayılan istisnaları günlüğe kaydedin (olay dinleyicileri ve Görünüm hataları).\n" +" - `full_console`: Tüm konsol günlüklerini günlüğe kaydedin.\n" +" - `guild_invite`: Sadece topluluk sunucuları için komut hataları günlüklerine bir \"Sunucu Daveti\" düğmesi ekleyin.\n" +" - `ignored_cogs`: Bazı `prefixed_commands_errors` ve `slash_commands_errors` çarklarını göz ardı edin. Bu cog için `ConsoleLogs` gibi cog qualified_name kullanmanız gerekir.\n" +" " + +#: consolelogs\consolelogs.py:516 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "Bu kanalda gömme mesaj göndermek için izinlerim yok." + +#: consolelogs\consolelogs.py:532 +msgid "Errors logging enabled in {channel.mention}." +msgstr "{channel.mention} kanalında hata günlükleme etkinleştirildi." + +#: consolelogs\consolelogs.py:540 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "Bir kanalda hata günlüklemeyi devre dışı bırakın." + +#: consolelogs\consolelogs.py:543 +msgid "Errors logging isn't enabled in this channel." +msgstr "Bu kanalda hata günlükleme etkinleştirilmemiş." + +#: consolelogs\consolelogs.py:546 +msgid "Errors logging disabled in {channel.mention}." +msgstr "{channel.mention} kanalında hata günlükleme devre dışı bırakıldı." + +#: consolelogs\consolelogs.py:550 +#, docstring +msgid "Get an embed to check loops status." +msgstr "Döngü durumunu kontrol etmek için bir gömme alın." + diff --git a/consolelogs/locales/uk-UA.po b/consolelogs/locales/uk-UA.po new file mode 100644 index 0000000..b9f2fef --- /dev/null +++ b/consolelogs/locales/uk-UA.po @@ -0,0 +1,94 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:24\n" +"Last-Translator: \n" +"Language-Team: Ukrainian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: uk\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/consolelogs/locales/messages.pot\n" +"X-Crowdin-File-ID: 261\n" +"Language: uk_UA\n" + +#: consolelogs\consolelogs.py:102 +#, docstring +msgid "A cog to display the console logs, with buttons and filter options, and to send commands errors in configured channels!" +msgstr "Гвинтик для відображення логів консолі, з кнопками та опціями фільтрації, а також для надсилання команд про помилки в налаштованих каналах!" + +#: consolelogs\consolelogs.py:264 +msgid "No logs to display." +msgstr "Немає журналів для відображення." + +#: consolelogs\consolelogs.py:415 +#, docstring +msgid "Scroll the console logs, for all levels/loggers or provided level/logger name." +msgstr "Прокрутіть журнали консолі для всіх рівнів/логгерів або для вказаного рівня/логгера." + +#: consolelogs\consolelogs.py:451 +#, docstring +msgid "View the console logs one by one, for all levels/loggers or provided level/logger name." +msgstr "Переглядайте журнали консолі по черзі, для всіх рівнів/логгерів або вказаного рівня/логгера." + +#: consolelogs\consolelogs.py:462 +#, docstring +msgid "Display the stats for the bot logs since the bot start." +msgstr "Відображати статистику логів бота з моменту запуску бота." + +#: consolelogs\consolelogs.py:495 +#, docstring +msgid "Enable errors logging in a channel.\n\n" +" **Parameters:**\n" +" - `channel`: The channel where the commands errors will be sent.\n" +" - `global_errors`: Log errors for the entire bot, not just the channel server.\n" +" - `prefixed_commands_errors`: Log prefixed commands errors.\n" +" - `slash_commands_errors`: Log slash commands errors.\n" +" - `dpy_ignored_exceptions`: Log dpy ignored exceptions (events listeners and Views errors).\n" +" - `full_console`: Log all the console logs.\n" +" - `guild_invite`: Add a button \"Guild Invite\" in commands errors logs, only for community servers.\n" +" - `ignored_cogs`: Ignore some cogs for `prefixed_commands_errors` and `slash_commands_errors`. You have to use the cog qualified_name like `ConsoleLogs` for this cog.\n" +" " +msgstr "Увімкнути логування помилок у каналі.\n\n" +" **Параметри:**\n" +" - `channel`: Канал, куди будуть надсилатися помилки команд.\n" +" - `global_errors`: Писати помилки для всього бота, а не тільки для сервера каналу.\n" +" - `prefixed_commands_errors`: Записувати помилки префіксованих команд.\n" +" - `lash_commands_errors`: Журнал помилок косої риски.\n" +" - `dpy_ignored_exceptions`: Журнал винятків, проігнорованих dpy (помилки слухачів подій та Views).\n" +" - `full_console`: Записувати всі журнали консолі.\n" +" - `guild_invite`: Додати кнопку \"Запросити до гільдії\" до логів помилок команд, лише для серверів спільноти.\n" +" - `ignored_cogs`: Ігнорувати деякі гвинтики для `prefixed_commands_errors` та `slash_commands_errors`. Ви повинні використовувати кваліфіковане ім'я гвинтика на кшталт `ConsoleLogs` для цього гвинтика.\n" +" " + +#: consolelogs\consolelogs.py:516 +msgid "I don't have the permissions to send embeds in this channel." +msgstr "Я не маю дозволу надсилати вставки в цьому каналі." + +#: consolelogs\consolelogs.py:532 +msgid "Errors logging enabled in {channel.mention}." +msgstr "Логування помилок увімкнено в {channel.mention}." + +#: consolelogs\consolelogs.py:540 +#, docstring +msgid "Disable errors logging in a channel." +msgstr "Вимкнути реєстрацію помилок у каналі." + +#: consolelogs\consolelogs.py:543 +msgid "Errors logging isn't enabled in this channel." +msgstr "Логування помилок у цьому каналі не ввімкнено." + +#: consolelogs\consolelogs.py:546 +msgid "Errors logging disabled in {channel.mention}." +msgstr "Логування помилок вимкнено в {channel.mention}." + +#: consolelogs\consolelogs.py:550 +#, docstring +msgid "Get an embed to check loops status." +msgstr "Отримайте вбудовування для перевірки стану циклів." + diff --git a/consolelogs/utils_version.json b/consolelogs/utils_version.json new file mode 100644 index 0000000..bfab002 --- /dev/null +++ b/consolelogs/utils_version.json @@ -0,0 +1 @@ +{"needed_utils_version": 7.0} \ No newline at end of file diff --git a/csvparse/__init__.py b/csvparse/__init__.py new file mode 100644 index 0000000..2f0fa17 --- /dev/null +++ b/csvparse/__init__.py @@ -0,0 +1,9 @@ + + +from redbot.core.bot import Red + +from .csvparse import CSVParse + + +async def setup(bot: Red): + await bot.add_cog(CSVParse(bot)) diff --git a/csvparse/csvparse.py b/csvparse/csvparse.py new file mode 100644 index 0000000..dc81455 --- /dev/null +++ b/csvparse/csvparse.py @@ -0,0 +1,35 @@ +import csv +from redbot.core import commands +import discord + + + +class CSVParse(commands.Cog): + def __init__(self, bot): + self.bot = bot + + + @commands.command(name="parsecsv", help="Parses a CSV file and returns it in a more readable format") + async def parse_csv(self, ctx, *, file: str): + if file.endswith(".csv"): + try: + with open(file, 'r') as f: + csv_data = list(csv.reader(f)) + formatted_data = "```\n" + for row in csv_data: + formatted_data += ",".join(row) + "\n" + formatted_data += "```" + if len(formatted_data) > 2000: + with open("output.csv", "w", newline="") as f: + writer = csv.writer(f) + writer.writerows(csv_data) + file = discord.File("output.csv", filename="output.csv") + await ctx.send("The output is too large to be sent as a message. Here is the file instead:", file=file) + else: + await ctx.send(formatted_data) + except FileNotFoundError: + await ctx.send("The file was not found. Please make sure the file is in the same directory as the bot.") + else: + await ctx.send("Please upload a CSV file.") + + diff --git a/csvparse/info.json b/csvparse/info.json new file mode 100644 index 0000000..04e50ed --- /dev/null +++ b/csvparse/info.json @@ -0,0 +1,9 @@ +{ + "author": ["bencos18 (492089091320446976)"], + "install_msg": "thank for adding my repo\n feel free to message me on discord or create a github issue if you need support or help with anything.", + "short": "some random cogs I made for my own use and decided to make public.", + "description": "csv parsing stuff.", + "tags": ["csv"], + "end_user_data_statement": "This cog does not store any End User Data.", + "type": "COG" +} diff --git a/cyclestatus/__init__.py b/cyclestatus/__init__.py new file mode 100644 index 0000000..090295d --- /dev/null +++ b/cyclestatus/__init__.py @@ -0,0 +1,13 @@ +import json +import pathlib + +from redbot.core.bot import Red + +from .cycle_status import CycleStatus + +with open(pathlib.Path(__file__).parent / "info.json") as fp: + __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] + + +async def setup(bot: Red): + await bot.add_cog(CycleStatus(bot)) diff --git a/cyclestatus/cycle_status.py b/cyclestatus/cycle_status.py new file mode 100644 index 0000000..ba8683f --- /dev/null +++ b/cyclestatus/cycle_status.py @@ -0,0 +1,380 @@ +# Copyright (c) 2021 - Jojo#7791 +# Licensed under MIT + +from __future__ import annotations + +import asyncio +import enum +import logging +import random +import re +try: + from datetime import datetime, UTC as DatetimeUTC + def get_datetime(): + return datetime.now(DatetimeUTC) +except ImportError: + from datetime import datetime + def get_datetime(): + return datetime.utcnow() + +from typing import Any, Final, List, Dict, Optional, TYPE_CHECKING + +import discord +from discord.ext import tasks +from redbot.core import Config, commands +from redbot.core.bot import Red +from redbot.core.utils.chat_formatting import humanize_list, humanize_number, pagify +from redbot.core.utils.predicates import MessagePredicate + +from .menus import Menu, Page, PositiveInt + +log = logging.getLogger("red.JojoCogs.cyclestatus") +_config_structure = { + "global": { + "statuses": [], + "use_help": True, + "next_iter": 0, + "toggled": True, # Toggle if the status should be cycled or not + "random": False, + "status_type": 0, # int, the value corresponds with a `discord.ActivityType` value + "status_mode": "online", # str, the value that corresponds with a `discord.Status` value + }, +} + +_bot_guild_var: Final[str] = r"{bot_guild_count}" +_bot_member_var: Final[str] = r"{bot_member_count}" +_bot_prefix_var: Final[str] = r"{bot_prefix}" + + +def humanize_enum_vals(e: enum.Enum) -> str: + return humanize_list( + list(map(lambda c: f"`{c.name.replace('_', ' ')}`", e)) # type:ignore + ) + + +class ActivityType(enum.Enum): + """Copy of `discord.ActivityType` minus `unknown`""" + + playing = 0 + listening = 2 + watching = 3 + custom = 4 + competing = 5 + + def __int__(self): + return self.value + + +if TYPE_CHECKING: + ActivityConverter = ActivityType +else: + class ActivityConverter(commands.Converter): + async def convert(self, ctx: commands.Context, arg: str) -> ActivityType: + arg = arg.lower() + ret = getattr(ActivityType, arg, None) + if not ret: + raise commands.BadArgument( + f"The argument must be one of the following: {humanize_enum_vals(ActivityType)}" + ) + return ret + + +class Status(enum.Enum): + online = "online" + idle = "idle" + do_not_disturb = dnd = "dnd" + + def __str__(self) -> str: + return self.value + + +if TYPE_CHECKING: + StatusConverter = Status +else: + class StatusConverter(commands.Converter): + async def convert(self, ctx: commands.Context, arg: str) -> Status: + arg = arg.lower().replace(" ", "_") + try: + return Status(arg) + except ValueError: + raise commands.BadArgument( + f"The argument must be one of the following: {humanize_enum_vals(Status)}" + ) + + +class CycleStatus(commands.Cog): + """Automatically change the status of your bot every minute""" + + __authors__: Final[List[str]] = ["Jojo#7791"] + # These people have suggested something for this cog! + __suggesters__: Final[List[str]] = ["ItzXenonUnity | Lou#2369", "StormyGalaxy#1297"] + __version__: Final[str] = "1.0.16" + + def __init__(self, bot: Red): + self.bot = bot + self.config = Config.get_conf(self, 115849, True) + self.config.register_global(**_config_structure["global"]) + self.toggled: Optional[bool] = None + self.random: Optional[bool] = None + self.last_random: Optional[int] = None + self.main_task.start() + + async def cog_load(self) -> None: + self.toggled = await self.config.toggled() + self.random = await self.config.random() + + async def cog_unload(self) -> None: + self.main_task.cancel() + + def format_help_for_context(self, ctx: commands.Context) -> str: + pre = super().format_help_for_context(ctx) + plural = "s" if len(self.__authors__) > 1 else "" + return ( + f"{pre}\n" + f"Author{plural}: `{humanize_list(self.__authors__)}`\n" + f"Version: `{self.__version__}`\n" + f"People who have put in suggestions: `{humanize_list(self.__suggesters__)}`" + ) + + @commands.command(name="cyclestatusversion", aliases=["csversion"], hidden=True) + async def cycle_status_version(self, ctx: commands.Context): + """Get the version of Cycle Status that [botname] is running""" + await ctx.send( + f"Cycle Status, Version `{self.__version__}`. Made with :heart: by Jojo#7791" + ) + + @commands.group(name="cyclestatus", aliases=["cstatus"]) + @commands.is_owner() + async def status(self, ctx: commands.Context): + """Commands working with the status""" + pass + + @status.command(name="type") + async def status_type(self, ctx: commands.Context, status: ActivityConverter): + """Change the type of [botname]'s status + + **Arguments** + - `status` The status type. Valid types are + `playing, listening, watching, custom, and competing` + """ + await self.config.status_type.set(status.value) + await ctx.send(f"Done, set the status type to `{status.name}`.") + + @status.command(name="mode") + async def status_mode(self, ctx: commands.Context, mode: StatusConverter): + """Change [botname]'s status mode + + **Arguments** + - `mode` The mode type. Valid types are: + `online, idle, dnd, and do not disturb` + """ + await self.config.status_mode.set(mode.value) + await ctx.send(f"Done, set the status mode to `{mode.value}`.") + + @status.command() + @commands.check(lambda ctx: ctx.cog.random is False) # type:ignore + async def forcenext(self, ctx: commands.Context): + """Force the next status to display on the bot""" + nl = await self.config.next_iter() + statuses = await self.config.statuses() + if not statuses: + return await ctx.send("There are no statuses") + if len(statuses) == 1: + await ctx.tick() + return await self._status_add(statuses[0], await self.config.use_help()) + try: + status = statuses[nl] + except IndexError: + status = statuses[0] + nl = 0 + await self.config.next_iter.set(nl + 1 if nl < len(statuses) else 0) + await self._status_add(status, await self.config.use_help()) + await ctx.tick() + + @status.command(name="usehelp") + async def status_set(self, ctx: commands.Context, toggle: Optional[bool] = None): + """Change whether the status should have ` | [p]help` + + **Arguments** + - `toggle` Whether help should be used or not. + """ + if toggle is None: + msg = f"Added help is {'enabled' if await self.config.use_help() else 'disabled'}" + return await ctx.send(msg) + await self.config.use_help.set(toggle) + await ctx.tick() + + @status.command(name="add") + async def status_add(self, ctx: commands.Context, *, status: str): + """Add a status to the list + + Put `{bot_guild_count}` or `{bot_member_count}` in your message to have the user count and guild count of your bot! + You can also put `{bot_prefix}` in your message to have the bot's prefix be displayed (eg. `{bot_prefix}ping`) + + **Arguments** + - `status` The status to add to the cycle. + """ + if len(status) > 100: + return await ctx.send("Statuses cannot be longer than 100 characters.") + async with self.config.statuses() as s: + s.append(status) + await ctx.tick() + + @status.command(name="remove", aliases=["del", "rm", "delete"]) + async def status_remove(self, ctx: commands.Context, num: Optional[PositiveInt] = None): + """Remove a status from the list + + **Arguments** + - `num` The index of the status you want to remove. + """ + if num is None: + return await ctx.invoke(self.status_list) + num -= 1 + async with self.config.statuses() as sts: + if num >= len(sts): + return await ctx.send("You don't have that many statuses, silly") + del sts[num] + await ctx.tick() + + @status.command(name="list") + async def status_list(self, ctx: commands.Context): + """List the available statuses""" + if not (status := await self.config.statuses()): + return await ctx.send("There are no statuses") + await self._show_statuses(ctx=ctx, statuses=status) + + @status.command(name="clear") + async def status_clear(self, ctx: commands.Context): + """Clear all of the statuses""" + msg = await ctx.send("Would you like to clear all of your statuses? (y/n)") + pred = MessagePredicate.yes_or_no() + try: + await self.bot.wait_for("message", check=pred) + except asyncio.TimeoutError: + pass + await msg.delete() + if not pred.result: + return await ctx.send("Okay! I won't remove your statuses") + + await self.config.statuses.set([]) + await self.bot.change_presence() + await ctx.tick() + + @status.command(name="random") + async def status_random(self, ctx: commands.Context, value: bool): + """Have the bot cycle to a random status + + **Arguments** + - `value` Whether to have random statuses be enabled or not + """ + + if value == self.random: + enabled = "enabled" if value else "disabled" + return await ctx.send(f"Random statuses are already {enabled}") + self.random = value + await self.config.random.set(value) + now_no_longer = "now" if value else "no longer" + await ctx.send(f"Statuses will {now_no_longer} be random") + + @status.command(name="toggle") + async def status_toggle(self, ctx: commands.Context, value: Optional[bool]): + """Toggle whether the status should be cycled. + + This is handy for if you want to keep your statuses but don't want them displayed at the moment + + **Arguments** + - `value` Whether to toggle cycling statues + """ + if value is None: + await ctx.send(f"Cycling Statuses is {'enabled' if self.toggled else 'disabled'}") + return + if value == self.toggled: + enabled = "enabled" if value else "disabled" + return await ctx.send(f"Cycling statuses is already {enabled}") + self.toggled = value + await self.config.toggled.set(value) + now_not = "now" if value else "not" + await ctx.send(f"I will {now_not} cycle statuses") + + @status.command(name="settings") + async def status_settings(self, ctx: commands.Context): + """Show your current settings for the cycle status cog""" + settings = { + "Randomized statuses?": "Enabled" if self.random else "Disabled", + "Toggled?": "Yes" if self.toggled else "No", + "Statuses?": f"See `{ctx.clean_prefix}cyclestatus list`", + "Status Type?": ActivityType(await self.config.status_type()).name, + } + title = "Your Cycle Status settings" + kwargs: Dict[str, Any] = { + "content": f"**{title}**\n\n" + "\n".join(f"**{k}** {v}" for k, v in settings.items()) + } + if await ctx.embed_requested(): + embed = discord.Embed( + title=title, colour=await ctx.embed_colour(), timestamp=get_datetime() + ) + [embed.add_field(name=k, value=v, inline=False) for k, v in settings.items()] + kwargs = {"embed": embed} + await ctx.send(**kwargs) + + @tasks.loop(minutes=1) + async def main_task(self): + if not (statuses := await self.config.statuses()) or not self.toggled: + return + if self.random: + if self.last_random is not None and len(statuses) > 1: + statuses.pop(self.last_random) # Remove that last picked one + msg = random.choice(statuses) + self.last_random = statuses.index(msg) + else: + try: + # So, sometimes this gets larger than the list of the statuses + # so, if this raises an `IndexError` we need to reset the next iter + msg = statuses[(nl := await self.config.next_iter())] + except IndexError: + nl = 0 # Hard reset + msg = statuses[0] + await self._status_add(msg, await self.config.use_help()) + if not self.random: + nl = 0 if len(statuses) - 1 == nl else nl + 1 + await self.config.next_iter.set(nl) + + @main_task.before_loop + async def main_tas_before_loop(self) -> None: + await self.bot.wait_until_red_ready() + + async def _num_lists(self, data: List[str]) -> List[str]: + """|coro| + + Return a list of numbered items + """ + return [f"{num}. {d}" for num, d in enumerate(data, 1)] + + async def _show_statuses(self, ctx: commands.Context, statuses: List[str]) -> None: + source = Page( + list(pagify("\n".join(await self._num_lists(statuses)), page_length=400)), + title="Statuses", + ) + await Menu(source=source, bot=self.bot, ctx=ctx).start() + + async def red_delete_data_for_user(self, *, requester: str, user_id: int) -> None: + """Nothing to delete""" + return + + async def _status_add(self, status: str, use_help: bool) -> None: + status = status.replace(_bot_guild_var, humanize_number(len(self.bot.guilds))).replace( + _bot_member_var, humanize_number(len(self.bot.users)) + ) + + prefix = (await self.bot.get_valid_prefixes())[0] + prefix = re.sub(rf"<@!?{self.bot.user.id}>", f"@{self.bot.user.name}", prefix) # type:ignore + + status = status.replace(_bot_prefix_var, prefix) + + if use_help: + status += f" | {prefix}help" + + # For some reason using `discord.Activity(type=discord.ActivityType.custom)` will result in the bot not changing its status + # So I'm gonna use this until I figure out something better lmao + game = discord.Activity(type=status_type, name=status) if (status_type := await self.config.status_type()) != 4 else discord.CustomActivity(name=status) + await self.bot.change_presence(activity=game, status=await self.config.status_mode()) diff --git a/cyclestatus/info.json b/cyclestatus/info.json new file mode 100644 index 0000000..d2b02e1 --- /dev/null +++ b/cyclestatus/info.json @@ -0,0 +1,20 @@ +{ + "name": "cyclestatus", + "short": "Cycle your bot's status", + "description": "Cycle the status on your bot so you don't have to update it constantly", + "end_user_data_statement": "This cog does not store user data", + "install_msg": "Thank you for installing Jojo's cycle status cog!\n\nIf you are running on an older version of python (eg. 3.8.1) you might not be able to have the guild and member counts (I don't know why yet)", + "author": [ + "Jojo#7791" + ], + "required_cogs": {}, + "requirements": [], + "tags": [ + "utility", + "status" + ], + "min_bot_version": "3.5.0.dev0", + "hidden": false, + "disabled": false, + "type": "COG" +} diff --git a/cyclestatus/menus.py b/cyclestatus/menus.py new file mode 100644 index 0000000..74a46bf --- /dev/null +++ b/cyclestatus/menus.py @@ -0,0 +1,172 @@ +# Copyright (c) 2021 - Jojo#7791 +# Licensed under MIT + +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Union, Optional + +from contextlib import suppress +import discord +import datetime +from redbot.core import commands +from redbot.core.bot import Red +from redbot.core.utils.chat_formatting import box +from redbot.vendored.discord.ext import menus # type:ignore + +__all__ = ["Page", "Menu", "PositiveInt"] + +button_emojis = { + (False, True): "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}", + (False, False): "\N{BLACK LEFT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}", + (True, False): "\N{BLACK RIGHT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}", + (True, True): "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}", +} + + +class BaseButton(discord.ui.Button): + def __init__( + self, + forward: bool, + skip: bool, + disabled: bool = False, + ) -> None: + super().__init__(style=discord.ButtonStyle.grey, emoji=button_emojis[(forward, skip)], disabled=disabled) + self.forward = forward # If the menu should go to the next page or previous + self.skip = skip # If the menu should step once or go to the first/last page + if TYPE_CHECKING: + self.view: Menu + + async def callback(self, inter: discord.Interaction) -> None: + page_num = 1 if self.forward else -1 + if self.skip: + page_num = -1 if self.forward else 0 # -1 triggers the `else` clause which sends it to the last page + await self.view.show_checked_page(page_number=page_num) + + +class StopButton(discord.ui.Button): + if TYPE_CHECKING: + view: Menu + + def __init__(self): + super().__init__( + style=discord.ButtonStyle.red, + emoji="\N{HEAVY MULTIPLICATION X}\N{VARIATION SELECTOR-16}", + disabled=False, + ) + + async def callback(self, inter: discord.Interaction) -> None: + self.view.stop() + with suppress(discord.Forbidden): + if msg := self.view.msg: + await msg.delete() + + +class Page: + def __init__(self, entries: List[str], title: str): + self.entries = entries + self.title = title + self.max_pages = len(entries) + + async def format_page(self, page: str, view: Menu) -> Union[str, discord.Embed]: + ctx = view.ctx + footer = f"Page {view.current_page + 1}/{self.max_pages}" + if ctx and await ctx.embed_requested(): # not gonna embed unless + return discord.Embed( + title=self.title, + colour=await ctx.embed_colour(), + description=page, + timestamp=datetime.datetime.now(tz=datetime.timezone.utc), + ).set_footer(text=footer) + return f"**{self.title}**\n\n{page}\n\n{footer}" + + +class Menu(discord.ui.View): + if TYPE_CHECKING: + ctx: commands.Context + + def __init__(self, source: Page, bot: Red, ctx: commands.Context): + super().__init__() + self.bot = bot + self.ctx = ctx + self.source = source + + self.msg: discord.Message = None # type:ignore + self.current_page: int = 0 + self._add_buttons() + + def add_item(self, item: discord.ui.Item): + # Editted to just not add the item if it's disabled + if getattr(item, "disabled", False): + return self + return super().add_item(item) + + def _add_buttons(self) -> None: + # Stupid me getting myself excited for something + # that I can't even do lmfao + single_disabled = self.source.max_pages <= 1 + multi_disabled = self.source.max_pages <= 5 + [self.add_item(i) for i in [ + BaseButton(False, True), + BaseButton(False, False), + StopButton(), + BaseButton(True, False), + BaseButton(True, True) + ] + ] + + async def on_timeout(self) -> None: + with suppress(discord.Forbidden): + await self.msg.delete() + + async def _get_kwargs_from_page(self, page: str) -> dict: + data = await self.source.format_page(page, self) + if isinstance(data, discord.Embed): + return {"embed": data} + return {"content": data} + + async def start(self) -> None: + page = self.source.entries[0] + kwargs = await self._get_kwargs_from_page(page) + self.msg = await self.ctx.send(view=self, **kwargs) + + async def interaction_check(self, inter: discord.Interaction) -> bool: + if inter.user.id != self.ctx.author.id: + await inter.response.send_message( + "You are not authorized to use this interaction.", + ephemeral=True, + ) + return False + return True + + async def show_page(self, page_number: int) -> None: + page = self.source.entries[page_number] + self.current_page = page_number + kwargs = await self._get_kwargs_from_page(page) + await self.msg.edit(view=self, **kwargs) + + async def show_checked_page(self, page_number: int) -> None: + max_pages = self.source.max_pages + try: + if max_pages > page_number >= 0: + await self.show_page(page_number) + elif max_pages <= page_number: + await self.show_page(0) + else: + await self.show_page(max_pages - 1) + except IndexError: + pass + + +if TYPE_CHECKING: + PositiveInt = int +else: + + class PositiveInt(commands.Converter): + async def convert(self, ctx: commands.Context, arg: str) -> int: + try: + ret = int(arg) + except ValueError: + raise commands.BadArgument("That was not an integer") + if ret <= 0: + raise commands.BadArgument(f"'{arg}' is not a positive integer") + return ret diff --git a/dankutils/__init__.py b/dankutils/__init__.py new file mode 100644 index 0000000..1df7a50 --- /dev/null +++ b/dankutils/__init__.py @@ -0,0 +1,37 @@ +""" +MIT License + +Copyright (c) 2021-present Kuro-Rui + +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. +""" + +import json +from pathlib import Path + +from redbot.core.bot import Red + +from .dankutils import DankUtils + +with open(Path(__file__).parent / "info.json") as fp: + __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] + + +async def setup(bot: Red): + await bot.add_cog(DankUtils(bot)) diff --git a/dankutils/dankutils.py b/dankutils/dankutils.py new file mode 100644 index 0000000..a67f122 --- /dev/null +++ b/dankutils/dankutils.py @@ -0,0 +1,121 @@ +""" +MIT License + +Copyright (c) 2021-present Kuro-Rui + +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. +""" + +import asyncio +from random import randint +from typing import Union + +import discord +import kuroutils +from redbot.core import commands +from redbot.core.bot import Red +from redbot.core.utils.chat_formatting import humanize_number + +from .utils import * +from .views import DoxxView + + +class DankUtils(kuroutils.Cog): + """Dank Memer related commands and utilities!""" + + __author__ = ["Kuro"] + __version__ = "0.0.2" + + def __init__(self, bot: Red): + super().__init__(bot) + + @commands.command(aliases=["danktax"]) + async def taxcalc(self, ctx: commands.Context, amount: Union[int, float]): + """Calculate Dank Memer tax!""" + amount = round(amount) + q = humanize_number(amount) + tq1 = humanize_number(total(amount)) + tq2 = humanize_number(total(amount, False)) + desc = ( + f"*If you send `⏣ {q}`, you will pay `⏣ {tq1}`.\n" + f"To spend `⏣ {q}` with tax included, send `⏣ {tq2}`.*" + ) + tx = humanize_number(tax(amount)) + if not await ctx.embed_requested(): + await ctx.send(f"{desc}\n\nTax: ⏣ {tx} (Rate: 1%)") + return + embed = discord.Embed(title="Tax Calc", description=desc, color=await ctx.embed_color()) + embed.set_footer(text=f"Tax: ⏣ {tx} (Rate: 1%)") + await ctx.send(embed=embed) + + @commands.guild_only() + @commands.cooldown(1, 25, commands.BucketType.channel) + @commands.command(aliases=["heck"]) + async def hack(self, ctx: commands.Context, member: discord.Member): + """Hack someone!""" + if member == ctx.author: + await ctx.send("Umm, please don't DOXX yourself \N{SKULL}") + return + + # Mass editing lol + message = await ctx.send(f"{loading(0)} Hacking {member.name} now...") + await asyncio.sleep(2) + try: + await message.edit(content=f"{loading(1)} Finding Discord Login...") + await asyncio.sleep(2) + await message.edit(content=f"{loading(2)} Bypassing 2FA...") + await asyncio.sleep(3) + email, password = get_email_and_password(member) + await message.edit( + content=( + f"{loading(3)} Found login information:\n" + f"**Email**: `{email}`\n" + f"**Password**: `{password}`" + ) + ) + await asyncio.sleep(4) + await message.edit(content=f"{loading(0)} Fetching user DMs...") + await asyncio.sleep(1) + last_dm = get_last_dm() + await message.edit(content=f"{loading(1)} **Last DM**: `{last_dm}`") + await asyncio.sleep(3) + await message.edit(content=f"{loading(2)} Injecting trojan virus into {member}...") + await asyncio.sleep(2) + await message.edit(content=f"{loading(3)} Virus injected. Finding IP Address...") + await asyncio.sleep(3) + # A valid IP address must be in the form of x.x.x.x, where x is a number from 0-255. + ip_address = f"{randint(0, 255)}.{randint(0, 255)}.{randint(0, 255)}.{randint(0, 255)}" + await message.edit(content=f"{loading(0)} **IP Address**: `{ip_address}`") + await asyncio.sleep(2) + await message.edit(content=f"{loading(1)} Selling user data to the government...") + await asyncio.sleep(2) + await message.edit( + content=f"{loading(2)} Reporting account to Discord for breaking ToS..." + ) + await asyncio.sleep(1) + await message.edit(content=f"{commands.context.TICK} Finished hacking {member.name}.") + info = format_doxx_info(email, password, ip_address, last_dm) + info_embed = discord.Embed( + title="Hack Information", description=info, color=await ctx.embed_color() + ) + view = DoxxView(info_embed=info_embed) + await view.start(ctx, "The *totally* real and dangerous hack is complete.") + except discord.NotFound: + await ctx.send("Process terminated. The hack failed.") + return diff --git a/dankutils/info.json b/dankutils/info.json new file mode 100644 index 0000000..a1b3ae8 --- /dev/null +++ b/dankutils/info.json @@ -0,0 +1,14 @@ +{ + "author": ["Kuro"], + "description": "Dank Memer related commands and utilities.", + "disabled": false, + "end_user_data_statement": "This cog does not store any end user data.", + "hidden": false, + "install_msg": "Thanks for installing `DankUtils`! Get started with `[p]help DankUtils`.\nThis cog has docs! Check it out at .", + "min_bot_version": "3.5.0", + "name": "DankUtils", + "requirements": ["git+https://github.com/Kuro-Rui/Kuro-Utils"], + "short": "Dank Memer commands.", + "tags": ["dankmemer", "dank memer"], + "type": "COG" +} \ No newline at end of file diff --git a/dankutils/utils.py b/dankutils/utils.py new file mode 100644 index 0000000..7286b74 --- /dev/null +++ b/dankutils/utils.py @@ -0,0 +1,141 @@ +""" +MIT License + +Copyright (c) 2021-present Kuro-Rui + +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. +""" + +import random +from string import ascii_letters, digits, punctuation +from typing import Optional, Union + +import discord +import emoji + +# Tax utils + + +def percent(number: Union[int, float]): + """Change number to percent""" + return number / 100 + + +def tax(dmc: int): + """Tax = 1%""" + return round(dmc * percent(1)) + + +def total(amount: int, tax_included: Optional[bool] = True): + if tax_included: + return amount + tax(amount) + else: + # Math Moment: + # tax_included_amount = tax_unincluded_amount * 101% + # tax_unincluded_amount = tax_included_amount / 101% + return round(amount / percent(101)) + + +# Hack utils + + +def loading(step: int): + steps = ["▖", "▘", "▝", "▗"] + screen = f"[{steps[step]}]" + return screen + + +def remove_punctuations(text: str): + letters = list(text) + for letter in letters: + if letter in punctuation: + letters.remove(letter) + text = "".join(letters) + return text + + +def get_email_and_password(user: discord.Member): + name = emoji.replace_emoji(remove_punctuations(user.name), "") + name = name.replace(" ", "") + if name == "": + name = random.choice( + [ + "bitchass", + "femaledog", + "freeporn", + "ilovesluts", + "ineedbitches", + "smexyuser69", + "takingashit", + "waiting4u", + ] + ) + domain = random.choice( + [ + "@aol.com", + "@disposablemail.com", + "@edu.com", + "@gmail.com", + "@gmx.net", + "@hotmail.com", + "@icloud.com", + "@msn.com", + "@outlook.com", + "@protonmail.com", + "@yahoo.com", + "@yandex.com", + ] + ) + email = name + domain + letters = "".join(random.choice(ascii_letters) for _ in range(6)) + numbers = "".join(random.choice(digits) for _ in range(5)) + puncts = "".join(random.choice(punctuation) for _ in range(4)) + password = list(letters + numbers + puncts) + random.shuffle(password) + password = "".join(password).replace("`", "'") + return email, password + + +def get_last_dm(): + return random.choice( + [ + "I hope blueballs aren't real.", + "I hope noone sees my nudes folder.", + "I think it's smaller than most.", + "UwU", + "can I see your feet pics?", + "dont frgt to like and subscrube!!", + "honestly I'm pretty sure blue waffle is real and I have it.", + "imagine having a peen as small as mine in 2022", + "man I love my mommy.", + "pwetty pwease?", + "yeah I'm just built different.", + "yeah she goes to another school.", + ] + ) + + +def format_doxx_info(email: str, password: str, ip: str, last_dm: str): + info = [ + f"`Email :` {email}", + f"`Password :` {password}", + f"`IP Address :` {ip}", + f'`Last DM :` "{last_dm}"', + ] + return "\n".join(info) diff --git a/dankutils/views.py b/dankutils/views.py new file mode 100644 index 0000000..c8e30b1 --- /dev/null +++ b/dankutils/views.py @@ -0,0 +1,61 @@ +""" +MIT License + +Copyright (c) 2021-present Kuro-Rui + +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. +""" + +import discord +from redbot.core.commands import Context + + +class DoxxView(discord.ui.View): + def __init__(self, *, info_embed: discord.Embed): + super().__init__(timeout=60.0) + self.info_embed = info_embed + + @discord.ui.button(label="Send Details", style=discord.ButtonStyle.blurple) + async def send_button(self, interaction: discord.Interaction, button: discord.ui.Button): + button.style = discord.ButtonStyle.gray + button.disabled = True + await self.message.edit(view=self) + await interaction.response.send_message(embed=self.info_embed, ephemeral=True) + + async def start(self, ctx: Context, content: str = None, **kwargs): + self.author = ctx.author + self.ctx = ctx + kwargs["reference"] = ctx.message.to_reference(fail_if_not_exists=False) + kwargs["mention_author"] = False + kwargs["view"] = self + self.message = await ctx.send(content, **kwargs) + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user != self.author: + await interaction.response.send_message( + "You are not authorized to interact with this menu.", ephemeral=True + ) + return False + return True + + async def on_timeout(self): + for child in self.children: + child.style = discord.ButtonStyle.gray + child.disabled = True + await self.message.edit(view=self) diff --git a/dashboard/LICENSE b/dashboard/LICENSE new file mode 100644 index 0000000..224955a --- /dev/null +++ b/dashboard/LICENSE @@ -0,0 +1,662 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + Red Discord Bot - Dashboard: An easy-to-use interactive web dashboard to control your Redbot. + Copyright (C) 2020 Neuro Assassin + Copyright (C) 2021 Neuro Assassin, Cog Creators + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/dashboard/README.rst b/dashboard/README.rst new file mode 100644 index 0000000..9e0ba7f --- /dev/null +++ b/dashboard/README.rst @@ -0,0 +1,121 @@ +.. _dashboard: +========= +Dashboard +========= + +This is the cog guide for the ``Dashboard`` cog. This guide contains the collection of commands which you can use in the cog. +Through this guide, ``[p]`` will always represent your prefix. Replace ``[p]`` with your own prefix when you use these commands in Discord. + +.. note:: + + Ensure that you are up to date by running ``[p]cog update dashboard``. + If there is something missing, or something that needs improving in this documentation, feel free to create an issue `here `_. + This documentation is generated everytime this cog receives an update. + +--------------- +About this cog: +--------------- + +Interact with your bot through a web Dashboard! + +**Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest +⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all. + +--------- +Commands: +--------- + +Here are all the commands included in this cog (19): + +* ``[p]dashboard`` + Get the link to the Dashboard. + +* ``[p]setdashboard`` + Configure Dashboard. + +* ``[p]setdashboard allinone `` + Run the Dashboard in the bot process, without having to open another window. You have to install Red-Web-Dashboard in your bot venv with Pip and reload the cog. + +* ``[p]setdashboard allowunsecurehttprequests `` + Allow unsecure http requests. This is not recommended for production, but required if you can't set up a SSL certificate. + +* ``[p]setdashboard defaultbackgroundtheme `` + Set the default Background theme of the dashboard. + +* ``[p]setdashboard defaultcolor `` + Set the default Color of the dashboard. + +* ``[p]setdashboard defaultsidenavtheme `` + Set the default Sidenav theme of the dashboard. + +* ``[p]setdashboard disabledthirdparties `` + The third parties to disable. + +* ``[p]setdashboard flaskflags `` + The flags used to setting the webserver if `all_in_one` is enabled. They are the cli flags of `reddash` without `--rpc-port`. + +* ``[p]setdashboard metadescription `` + The website long description to use. + +* ``[p]setdashboard metaicon `` + The website icon to use. + +* ``[p]setdashboard metatitle `` + The website title to use. + +* ``[p]setdashboard metawebsitedescription `` + The website short description to use. + +* ``[p]setdashboard modalconfig [confirmation=False]`` + Set all settings for the cog with a Discord Modal. + +* ``[p]setdashboard redirecturi `` + The redirect uri to use for the Discord OAuth. + +* ``[p]setdashboard resetsetting `` + Reset a setting. + +* ``[p]setdashboard secret [secret]`` + Set the client secret needed for Discord OAuth. + +* ``[p]setdashboard showsettings [with_dev=False]`` + Show all settings for the cog with defaults and values. + +* ``[p]setdashboard supportserver `` + Set the support server url of your bot. + +------------ +Installation +------------ + +If you haven't added my repo before, lets add it first. We'll call it "AAA3A-cogs" here. + +.. code-block:: ini + + [p]repo add AAA3A-cogs https://github.com/AAA3A-AAA3A/AAA3A-cogs + +Now, we can install Dashboard. + +.. code-block:: ini + + [p]cog install AAA3A-cogs dashboard + +Once it's installed, it is not loaded by default. Load it by running the following command: + +.. code-block:: ini + + [p]load dashboard + +---------------- +Further Support: +---------------- + +Check out my docs `here `_. +Mention me in the #support_other-cogs in the `cog support server `_ if you need any help. +Additionally, feel free to open an issue or pull request to this repo. + +-------- +Credits: +-------- + +Thanks to Kreusada for the Python code to automatically generate this documentation! \ No newline at end of file diff --git a/dashboard/__init__.py b/dashboard/__init__.py new file mode 100644 index 0000000..5e088db --- /dev/null +++ b/dashboard/__init__.py @@ -0,0 +1,46 @@ +from redbot.core import errors # isort:skip +import importlib +import sys + +try: + import AAA3A_utils +except ModuleNotFoundError: + raise errors.CogLoadError( + "The needed utils to run the cog were not found. Please execute the command `[p]pipinstall git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." + ) +modules = sorted( + [module for module in sys.modules if module.split(".")[0] == "AAA3A_utils"], reverse=True +) +for module in modules: + try: + importlib.reload(sys.modules[module]) + except ModuleNotFoundError: + pass +del AAA3A_utils +# import AAA3A_utils +# import json +# import os +# __version__ = AAA3A_utils.__version__ +# with open(os.path.join(os.path.dirname(__file__), "utils_version.json"), mode="r") as f: +# data = json.load(f) +# needed_utils_version = data["needed_utils_version"] +# if __version__ > needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a higher version than the one supported by this version of the cog. Please update the cogs of the `AAA3A-cogs` repo." +# ) +# elif __version__ < needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a lower version than the one supported by this version of the cog. Please execute the command `[p]pipinstall --upgrade git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." +# ) + +from redbot.core.bot import Red # isort:skip +from redbot.core.utils import get_end_user_data_statement + +from .dashboard import Dashboard + +__red_end_user_data_statement__ = get_end_user_data_statement(file=__file__) + + +async def setup(bot: Red) -> None: + cog = Dashboard(bot) + await bot.add_cog(cog) diff --git a/dashboard/dashboard.py b/dashboard/dashboard.py new file mode 100644 index 0000000..bebd3ba --- /dev/null +++ b/dashboard/dashboard.py @@ -0,0 +1,428 @@ +from AAA3A_utils import Cog, Settings # isort:skip +from redbot.core import commands, Config # isort:skip +from redbot.core.bot import Red # isort:skip +from redbot.core.i18n import Translator, cog_i18n # isort:skip +import discord # isort:skip +import typing # isort:skip + +import argparse +import asyncio + +# import importlib +# import sys +from fernet import Fernet + +from .rpc import DashboardRPC + +# Credits: +# General repo credits. +# Thank you very much to Neuro Assassin for the original code (https://github.com/NeuroAssassin/Toxic-Cogs/tree/master/dashboard)! + +_: Translator = Translator("Dashboard", __file__) + + +class StrConverter(commands.Converter): + async def convert(self, ctx: commands.Context, argument: str) -> str: + return argument + + +class RedirectURIConverter(commands.Converter): + async def convert(self, ctx: commands.Context, argument: str) -> str: + if not argument.startswith("http"): + raise commands.BadArgument(_("This is not a valid URL.")) + if not argument.endswith("/callback"): + raise commands.BadArgument( + _("This is not a valid Dashboard redirect URI: it must end with `/callback`.") + ) + return argument + + +class ThirdPartyConverter(commands.Converter): + async def convert(self, ctx: commands.Context, argument: str) -> str: + cog = ctx.bot.get_cog("Dashboard") + if argument not in cog.rpc.third_parties_handler.third_parties: + raise commands.BadArgument(_("This third party is not available.")) + return argument + + +@cog_i18n(_) +class Dashboard(Cog): + """Interact with your bot through a web Dashboard! + + **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest + ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all. + """ + + __authors__: typing.List[str] = ["AAA3A", "Neuro Assassin"] + + def __init__(self, bot: Red) -> None: + super().__init__(bot=bot) + + self.config: Config = Config.get_conf( + self, + identifier=205192943327321000143939875896557571750, + force_registration=True, + ) + self.CONFIG_SCHEMA: int = 2 + self.config.register_global( + CONFIG_SCHEMA=None, + all_in_one=False, + flask_flags=[], + webserver={ + "core": { + "secret_key": None, + "jwt_secret_key": None, + "secret": None, + "redirect_uri": None, + "allow_unsecure_http_requests": False, + "blacklisted_ips": [], + }, + "ui": { + "meta": { + "title": None, + "icon": None, + "website_description": None, + "description": None, + "support_server": None, + "default_color": "success", + "default_background_theme": "white", + "default_sidenav_theme": "white", + }, + "sidenav": [ + { + "pos": 1, + "name": "builtin-home", + "icon": "ni ni-atom text-success", + "route": "base_blueprint.index", + "session": None, + "owner": False, + "locked": True, + "hidden": False, + }, + { + "pos": 2, + "name": "builtin-commands", + "icon": "ni ni-bullet-list-67 text-danger", + "route": "base_blueprint.commands", + "session": None, + "owner": False, + "locked": False, + "hidden": False, + }, + { + "pos": 3, + "name": "builtin-dashboard", + "icon": "ni ni-settings text-primary", + "route": "base_blueprint.dashboard", + "session": True, + "owner": False, + "locked": False, + "hidden": False, + }, + { + "pos": 4, + "name": "builtin-third_parties", + "icon": "ni ni-diamond text-success", + "route": "third_parties_blueprint.third_parties", + "session": True, + "owner": False, + "locked": False, + "hidden": False, + }, + { + "pos": 5, + "name": "builtin-admin", + "icon": "ni ni-badge text-danger", + "route": "base_blueprint.admin", + "session": True, + "owner": True, + "locked": True, + "hidden": False, + }, + { + "pos": 6, + "name": "builtin-credits", + "icon": "ni ni-book-bookmark text-info", + "route": "base_blueprint.credits", + "session": None, + "owner": False, + "locked": True, + "hidden": False, + }, + { + "pos": 7, + "name": "builtin-login", + "icon": "ni ni-key-25 text-success", + "route": "login_blueprint.login", + "session": False, + "owner": False, + "locked": True, + "hidden": False, + }, + { + "pos": 8, + "name": "builtin-logout", + "icon": "ni ni-user-run text-warning", + "route": "login_blueprint.logout", + "session": True, + "owner": False, + "locked": True, + "hidden": False, + }, + ], + }, + "disabled_third_parties": [], + "custom_pages": [], + }, + ) + + _settings: typing.Dict[str, typing.Dict[str, typing.Any]] = { + "all_in_one": { + "converter": bool, + "description": "Run the webserver in the bot process, without having to open another window. You have to install Red-Web-Dashboard in your bot venv with Pip and reload the cog.", + "hidden": True, + "no_slash": True, + }, + "flask_flags": { + "converter": commands.Greedy[StrConverter], + "description": "The flags used to setting the webserver if `all_in_one` is enabled. They are the cli flags of `reddash` without `--rpc-port`.", + "hidden": True, + "no_slash": True, + }, + "redirect_uri": { + "converter": RedirectURIConverter, + "description": "The redirect uri to use for the Discord OAuth.", + "path": ["webserver", "core", "redirect_uri"], + "aliases": ["redirect"], + }, + "allow_unsecure_http_requests": { + "converter": bool, + "description": "Allow unsecure http requests. This is not recommended for production, but required if you can't set up a SSL certificate.", + "path": ["webserver", "core", "allow_unsecure_http_requests"], + "aliases": ["allowunsecure"], + }, + "meta_title": { + "converter": str, + "description": "The website title to use.", + "path": ["webserver", "ui", "meta", "title"], + }, + "meta_icon": { + "converter": str, + "description": "The website icon to use.", + "path": ["webserver", "ui", "meta", "icon"], + }, + "meta_website_description": { + "converter": str, + "description": "The website short description to use.", + "path": ["webserver", "ui", "meta", "website_description"], + }, + "meta_description": { + "converter": str, + "description": "The website long description to use.", + "path": ["webserver", "ui", "meta", "description"], + }, + "support_server": { + "converter": str, + "description": "Set the support server url of your bot.", + "path": ["webserver", "ui", "meta", "support_server"], + "aliases": ["support"], + }, + "default_color": { + "converter": typing.Literal[ + "success", "danger", "primary", "info", "warning", "dark" + ], + "description": "Set the default Color of the dashboard.", + "path": ["webserver", "ui", "meta", "default_color"], + }, + "default_background_theme": { + "converter": typing.Literal["white", "dark"], + "description": "Set the default Background theme of the dashboard.", + "path": ["webserver", "ui", "meta", "default_background_theme"], + }, + "default_sidenav_theme": { + "converter": typing.Literal["white", "dark"], + "description": "Set the default Sidenav theme of the dashboard.", + "path": ["webserver", "ui", "meta", "default_sidenav_theme"], + }, + "disabled_third_parties": { + "converter": commands.Greedy[ThirdPartyConverter], + "description": "The third parties to disable.", + "path": ["webserver", "disabled_third_parties"], + }, + } + self.settings: Settings = Settings( + bot=self.bot, + cog=self, + config=self.config, + group=self.config.GLOBAL, + settings=_settings, + global_path=[], + use_profiles_system=False, + can_edit=True, + commands_group=self.setdashboard, + ) + + self.app: typing.Optional[typing.Any] = None + self.rpc: DashboardRPC = DashboardRPC(bot=self.bot, cog=self) + + async def cog_load(self) -> None: + await super().cog_load() + await self.edit_config_schema() + await self.settings.add_commands() + self.logger.info("Loading cog...") + asyncio.create_task(self.create_app(flask_flags=await self.config.flask_flags())) + + async def edit_config_schema(self) -> None: + CONFIG_SCHEMA = await self.config.CONFIG_SCHEMA() + if CONFIG_SCHEMA is None: + CONFIG_SCHEMA = 1 + await self.config.CONFIG_SCHEMA(CONFIG_SCHEMA) + if CONFIG_SCHEMA == self.CONFIG_SCHEMA: + return + if CONFIG_SCHEMA == 1: + global_group = self.config._get_base_group(self.config.GLOBAL) + async with global_group() as global_data: + if "default_sidebar_theme" in global_data: + global_data["default_sidenav_theme"] = global_data.pop("default_sidebar_theme") + CONFIG_SCHEMA = 2 + await self.config.CONFIG_SCHEMA.set(CONFIG_SCHEMA) + if CONFIG_SCHEMA < self.CONFIG_SCHEMA: + CONFIG_SCHEMA = self.CONFIG_SCHEMA + await self.config.CONFIG_SCHEMA.set(CONFIG_SCHEMA) + self.logger.info( + f"The Config schema has been successfully modified to {self.CONFIG_SCHEMA} for the {self.qualified_name} cog." + ) + + async def cog_unload(self) -> None: + self.logger.info("Unloading cog...") + if self.app is not None and self.app.server_thread is not None: + await asyncio.to_thread(self.app.server_thread.shutdown) + await asyncio.to_thread(self.app.tasks_manager.stop_tasks) + self.rpc.unload() + await super().cog_unload() + + async def create_app(self, flask_flags: str) -> None: + await self.bot.wait_until_red_ready() + if await self.config.webserver.core.secret_key() is None: + await self.config.webserver.core.secret_key.set(Fernet.generate_key().decode()) + if await self.config.webserver.core.jwt_secret_key() is None: + await self.config.webserver.core.jwt_secret_key.set(Fernet.generate_key().decode()) + if await self.config.all_in_one(): + try: + # for module_name in ("flask", "reddash"): + # modules = sorted( + # [module for module in sys.modules if module.split(".")[0] == module_name], reverse=True + # ) + # for module in modules: + # try: + # importlib.reload(sys.modules[module]) + # except ModuleNotFoundError: + # pass + from reddash import FlaskApp + + parser: argparse.ArgumentParser = argparse.ArgumentParser(exit_on_error=False) + parser.add_argument("--host", dest="host", type=str, default="0.0.0.0") + parser.add_argument("--port", dest="port", type=int, default=42356) + # parser.add_argument("--rpc-port", dest="rpcport", type=int, default=6133) + parser.add_argument( + "--interval", dest="interval", type=int, default=5, help=argparse.SUPPRESS + ) + parser.add_argument( + "--development", dest="dev", action="store_true", help=argparse.SUPPRESS + ) + # parser.add_argument("--instance", dest="instance", type=str, default=None) + args = vars(parser.parse_args(args=flask_flags)) + self.app: FlaskApp = FlaskApp(cog=self, **args) + await self.app.create_app() + await self.app.run_app() + except Exception as e: + self.logger.critical("Error when creating the Flask webserver app.", exc_info=e) + + @commands.bot_has_permissions(embed_links=True) + @commands.hybrid_command() + async def dashboard(self, ctx: commands.Context) -> None: + """Get the link to the Dashboard.""" + if (dashboard_url := getattr(ctx.bot, "dashboard_url", None)) is None: + raise commands.UserFeedbackCheckFailure( + _( + "Red-Web-Dashboard is not installed. Check ." + ) + ) + if not dashboard_url[1] and ctx.author.id not in ctx.bot.owner_ids: + raise commands.UserFeedbackCheckFailure(_("You can't access the Dashboard.")) + embed: discord.Embed = discord.Embed( + title=_("Red-Web-Dashboard"), + color=await ctx.embed_color(), + ) + url = dashboard_url[0] + if ctx.guild is not None and ( + ctx.author.id in ctx.bot.owner_ids or await self.bot.is_mod(ctx.author) + ): + url += f"/dashboard/{ctx.guild.id}" + embed.set_footer(text=ctx.guild.name, icon_url=ctx.guild.icon) + embed.url = url + await ctx.send(embed=embed) + + @commands.is_owner() + @commands.hybrid_group() + async def setdashboard(self, ctx: commands.Context) -> None: + """Configure Dashboard.""" + pass + + @setdashboard.command() + async def secret(self, ctx: commands.Context, *, secret: str = None): + """Set the client secret needed for Discord OAuth.""" + if secret is not None: + await self.config.webserver.core.secret.set(secret) + return + + class SecretModal(discord.ui.Modal): + def __init__(_self) -> None: + super().__init__(title="Discord OAuth Secret") + _self.secret: discord.ui.TextInput = discord.ui.TextInput( + label=_("Discord Secret"), + style=discord.TextStyle.short, + custom_id="discord_secret", + ) + _self.add_item(_self.secret) + + async def on_submit(_self, interaction: discord.Interaction) -> None: + await self.config.webserver.core.secret.set(_self.secret.value) + await interaction.response.send_message(_("Discord OAuth secret set.")) + + class SecretView(discord.ui.View): + def __init__(_self) -> None: + super().__init__() + _self._message: discord.Message = None + + async def on_timeout(_self) -> None: + for child in _self.children: + child: discord.ui.Item + if hasattr(child, "disabled") and not ( + isinstance(child, discord.ui.Button) + and child.style == discord.ButtonStyle.url + ): + child.disabled = True + try: + await _self._message.edit(view=_self) + except discord.HTTPException: + pass + + async def interaction_check(_self, interaction: discord.Interaction) -> bool: + if interaction.user.id not in [ctx.author.id] + list(ctx.bot.owner_ids): + await interaction.response.send_message( + _("You are not allowed to use this interaction."), ephemeral=True + ) + return False + return True + + @discord.ui.button(label=_("Set Discord OAuth Secret")) + async def set_secret_button( + _self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + await interaction.response.send_modal(SecretModal()) + + view = SecretView() + view._message = await ctx.send( + _("Click on the button below to set a secret for Discord OAuth."), view=view + ) diff --git a/dashboard/info.json b/dashboard/info.json new file mode 100644 index 0000000..0dcd62e --- /dev/null +++ b/dashboard/info.json @@ -0,0 +1,15 @@ +{ + "author": ["AAA3A", "Neuro Assassin"], + "name": "Dashboard", + "install_msg": "Thank you for installing this cog!\nDo `[p]help CogName` to get the list of commands and their description. If you enjoy my work, please consider donating on [Buy Me a Coffee]() or [Ko-Fi]()!\nPlease follow this **installation documentation**: https://red-web-dashboard.readthedocs.io/en/latest\n⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.", + "short": "Interact with your bot through a web Dashboard!", + "description": "Interact with your bot through a web Dashboard! Thank you very much to Neuro for the initial work!", + "tags": [ + "dashboard", + "web", + "panel" + ], + "requirements": ["git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git", "fernet", "markdown2", "wtforms", "werkzeug", "markupsafe", "itsdangerous"], + "min_bot_version": "3.5.0", + "end_user_data_statement": "This cog does not persistently store data or metadata about users." +} \ No newline at end of file diff --git a/dashboard/locales/de-DE.po b/dashboard/locales/de-DE.po new file mode 100644 index 0000000..f39e79e --- /dev/null +++ b/dashboard/locales/de-DE.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:12\n" +"Last-Translator: \n" +"Language-Team: German\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: de\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/locales/messages.pot\n" +"X-Crowdin-File-ID: 308\n" +"Language: de_DE\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "Dies ist keine gültige URL." + +#: dashboard\dashboard.py:35 +msgid "This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "Dies ist kein gültiger Dashboard-Redirect-URI: Er muss mit `/callback` enden." + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "Diese dritte Partei ist nicht verfügbar." + +#: dashboard\dashboard.py:50 +#, docstring +msgid "Interact with your bot through a web Dashboard!\n\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "Interagieren Sie mit Ihrem Bot über ein Web-Dashboard!\n\n" +" **Installationsanleitung:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ Dieses Paket ist eine Abspaltung der Arbeit von Neuro Assassin und wird von der Org in keiner Weise befürwortet.\n" +" " + +#: dashboard\dashboard.py:347 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard ist nicht installiert. Prüfen Sie ." + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "Sie können nicht auf das Dashboard zugreifen." + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "Red-Web-Dashboard" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "Legen Sie das Client-Geheimnis fest, das für Discord OAuth benötigt wird." + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "Diskord Geheimnis" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "Discord OAuth-Geheimsatz." + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "Klicken Sie auf die Schaltfläche unten, um ein Geheimnis für Discord OAuth festzulegen." + diff --git a/dashboard/locales/el-GR.po b/dashboard/locales/el-GR.po new file mode 100644 index 0000000..4bf086c --- /dev/null +++ b/dashboard/locales/el-GR.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:12\n" +"Last-Translator: \n" +"Language-Team: Greek\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: el\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/locales/messages.pot\n" +"X-Crowdin-File-ID: 308\n" +"Language: el_GR\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "Αυτό δεν είναι έγκυρο URL." + +#: dashboard\dashboard.py:35 +msgid "This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "Αυτό δεν είναι έγκυρο URI ανακατεύθυνσης Dashboard: πρέπει να τελειώνει με `/callback`." + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "Αυτό το τρίτο μέρος δεν είναι διαθέσιμο." + +#: dashboard\dashboard.py:50 +#, docstring +msgid "Interact with your bot through a web Dashboard!\n\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "Αλληλεπίδραση με το bot σας μέσω ενός πίνακα ελέγχου στο διαδίκτυο!\n\n" +" **Οδηγός εγκατάστασης:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ Αυτό το πακέτο είναι μια διακλάδωση της εργασίας του Neuro Assassin και δεν υποστηρίζεται καθόλου από το Org.\n" +" " + +#: dashboard\dashboard.py:347 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Το Red-Web-Dashboard δεν είναι εγκατεστημένο. Ελέγξτε το ." + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "Δεν μπορείτε να έχετε πρόσβαση στον πίνακα ελέγχου." + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "Red-Web-Dashboard" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "Ορίστε το μυστικό του πελάτη που απαιτείται για το Discord OAuth." + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "Μυστικό Discord" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "Σύνολο μυστικών Discord OAuth." + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "Κάντε κλικ στο παρακάτω κουμπί για να ορίσετε ένα μυστικό για το Discord OAuth." + diff --git a/dashboard/locales/es-ES.po b/dashboard/locales/es-ES.po new file mode 100644 index 0000000..7273231 --- /dev/null +++ b/dashboard/locales/es-ES.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:12\n" +"Last-Translator: \n" +"Language-Team: Spanish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: es-ES\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/locales/messages.pot\n" +"X-Crowdin-File-ID: 308\n" +"Language: es_ES\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "Esta URL no es válida." + +#: dashboard\dashboard.py:35 +msgid "This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "Este no es un URI de redirección de Dashboard válido: debe terminar con `/callback`." + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "Este tercero no está disponible." + +#: dashboard\dashboard.py:50 +#, docstring +msgid "Interact with your bot through a web Dashboard!\n\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "¡Interactuar con su bot a través de una web Dashboard!\n\n" +" **Guía de instalación:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ Este paquete es un fork del trabajo de Neuro Assassin, y no está respaldado por la Org en absoluto.\n" +" " + +#: dashboard\dashboard.py:347 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard no está instalado. Compruebe ." + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "No puedes acceder al Panel de control." + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "Tablero rojo" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "Establece el secreto de cliente necesario para Discord OAuth." + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "Secreto de la discordia" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "Conjunto de secretos OAuth de Discord." + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "Haz clic en el botón de abajo para establecer un secreto para Discord OAuth." + diff --git a/dashboard/locales/fi-FI.po b/dashboard/locales/fi-FI.po new file mode 100644 index 0000000..6747dfc --- /dev/null +++ b/dashboard/locales/fi-FI.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:12\n" +"Last-Translator: \n" +"Language-Team: Finnish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fi\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/locales/messages.pot\n" +"X-Crowdin-File-ID: 308\n" +"Language: fi_FI\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "Tämä ei ole kelvollinen URL-osoite." + +#: dashboard\dashboard.py:35 +msgid "This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "Tämä ei ole kelvollinen Dashboardin uudelleenohjaus-URI: sen on päätyttävä `/callback`." + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "Tämä kolmas osapuoli ei ole käytettävissä." + +#: dashboard\dashboard.py:50 +#, docstring +msgid "Interact with your bot through a web Dashboard!\n\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "Vuorovaikuta botin kanssa web Dashboardin kautta!\n\n" +" **Asennusopas:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ Tämä paketti on haara Neuro Assassinin työstä, eikä Org ole lainkaan sen hyväksymä.\n" +" " + +#: dashboard\dashboard.py:347 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboardia ei ole asennettu. Tarkista ." + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "Et pääse kojelautaan." + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "Red-Web-Dashboard" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "Aseta asiakassalaisuus, jota tarvitaan Discord OAuthia varten." + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "Epäsopu salaisuus" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "Discordin OAuth-salaisuus." + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "Napsauta alla olevaa painiketta asettaaksesi salaisuuden Discord OAuthia varten." + diff --git a/dashboard/locales/fr-FR.po b/dashboard/locales/fr-FR.po new file mode 100644 index 0000000..b064e7b --- /dev/null +++ b/dashboard/locales/fr-FR.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:12\n" +"Last-Translator: \n" +"Language-Team: French\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/locales/messages.pot\n" +"X-Crowdin-File-ID: 308\n" +"Language: fr_FR\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "Il ne s'agit pas d'une URL valide." + +#: dashboard\dashboard.py:35 +msgid "This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "Ce n'est pas un URI de redirection valide pour le tableau de bord : il doit se terminer par `/callback`." + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "Ce tiers n'est pas disponible." + +#: dashboard\dashboard.py:50 +#, docstring +msgid "Interact with your bot through a web Dashboard!\n\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "Interagissez avec votre bot à travers un tableau de bord web !\n\n" +" **Guide d'installation:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ Ce paquetage est un fork du travail de Neuro Assassin, et n'est pas du tout approuvé par l'Org.\n" +" " + +#: dashboard\dashboard.py:347 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard n'est pas installé. Vérifiez ." + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "Vous ne pouvez pas accéder au tableau de bord." + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "Tableau de bord rouge" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "Définir le secret client nécessaire pour Discord OAuth." + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "Secret de discorde" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "Ensemble de secrets OAuth de Discord." + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "Cliquez sur le bouton ci-dessous pour définir un secret pour Discord OAuth." + diff --git a/dashboard/locales/it-IT.po b/dashboard/locales/it-IT.po new file mode 100644 index 0000000..dbb35f3 --- /dev/null +++ b/dashboard/locales/it-IT.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:12\n" +"Last-Translator: \n" +"Language-Team: Italian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: it\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/locales/messages.pot\n" +"X-Crowdin-File-ID: 308\n" +"Language: it_IT\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "Questo non è un URL valido." + +#: dashboard\dashboard.py:35 +msgid "This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "Questo non è un URI di reindirizzamento valido per Dashboard: deve terminare con `/callback`." + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "Questa terza parte non è disponibile." + +#: dashboard\dashboard.py:50 +#, docstring +msgid "Interact with your bot through a web Dashboard!\n\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "Interagisci con il tuo bot attraverso una Dashboard web!\n\n" +" **Guida all'installazione:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ Questo pacchetto è un fork del lavoro di Neuro Assassin e non è assolutamente approvato dall'Org.\n" +" " + +#: dashboard\dashboard.py:347 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard non è installato. Controllare ." + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "Non è possibile accedere al Dashboard." + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "Cruscotto rosso" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "Impostare il segreto del client necessario per Discord OAuth." + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "Segreto di discordia" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "Set di segreti OAuth di Discord." + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "Fare clic sul pulsante sottostante per impostare un segreto per Discord OAuth." + diff --git a/dashboard/locales/ja-JP.po b/dashboard/locales/ja-JP.po new file mode 100644 index 0000000..c29a856 --- /dev/null +++ b/dashboard/locales/ja-JP.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:12\n" +"Last-Translator: \n" +"Language-Team: Japanese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ja\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/locales/messages.pot\n" +"X-Crowdin-File-ID: 308\n" +"Language: ja_JP\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "これは有効なURLではない。" + +#: dashboard\dashboard.py:35 +msgid "This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "これは有効な Dashboard リダイレクト URI ではありません: `/callback` で終わっていなければなりません。" + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "この第三者は利用できない。" + +#: dashboard\dashboard.py:50 +#, docstring +msgid "Interact with your bot through a web Dashboard!\n\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "ウェブダッシュボードを通してボットと対話する!\n\n" +" **インストールガイド: ** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ 本パッケージは Neuro Assassin のフォークであり、Org によって承認されたものではありません。\n" +" " + +#: dashboard\dashboard.py:347 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard がインストールされていません。を確認してください。" + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "ダッシュボードにアクセスできません。" + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "レッドダッシュボード" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "Discord OAuthに必要なクライアントシークレットを設定します。" + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "不和の秘密" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "Discord OAuth シークレットセット。" + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "Discord OAuthのシークレットを設定するには、下のボタンをクリックしてください。" + diff --git a/dashboard/locales/messages.pot b/dashboard/locales/messages.pot new file mode 100644 index 0000000..b8249be --- /dev/null +++ b/dashboard/locales/messages.pot @@ -0,0 +1,70 @@ +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2024-12-29 10:43+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "" + +#: dashboard\dashboard.py:35 +msgid "" +"This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "" + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "" + +#: dashboard\dashboard.py:50 +#, docstring +msgid "" +"Interact with your bot through a web Dashboard!\n" +"\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "" + +#: dashboard\dashboard.py:347 +msgid "" +"Red-Web-Dashboard is not installed. Check ." +msgstr "" + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "" + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "" + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "" + +#: dashboard\dashboard.py:414 +msgid "You are not allowed to use this interaction." +msgstr "" + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "" diff --git a/dashboard/locales/nl-NL.po b/dashboard/locales/nl-NL.po new file mode 100644 index 0000000..4b5185f --- /dev/null +++ b/dashboard/locales/nl-NL.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:12\n" +"Last-Translator: \n" +"Language-Team: Dutch\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: nl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/locales/messages.pot\n" +"X-Crowdin-File-ID: 308\n" +"Language: nl_NL\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "Dit is geen geldige URL." + +#: dashboard\dashboard.py:35 +msgid "This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "Dit is geen geldige Dashboard redirect URI: het moet eindigen op `/callback`." + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "Deze derde partij is niet beschikbaar." + +#: dashboard\dashboard.py:50 +#, docstring +msgid "Interact with your bot through a web Dashboard!\n\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "Communiceer met je bot via een webdashboard!\n\n" +" **Installatiegids:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ Dit pakket is een fork van het werk van Neuro Assassin en wordt helemaal niet onderschreven door de Org.\n" +" " + +#: dashboard\dashboard.py:347 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard is niet geïnstalleerd. Controleer ." + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "Je hebt geen toegang tot het dashboard." + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "Rood Dashboard" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "Stel het clientgeheim in dat nodig is voor Discord OAuth." + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "Discord geheim" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "Discord OAuth geheime set." + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "Klik op de knop hieronder om een geheim in te stellen voor Discord OAuth." + diff --git a/dashboard/locales/pl-PL.po b/dashboard/locales/pl-PL.po new file mode 100644 index 0000000..f4b80c8 --- /dev/null +++ b/dashboard/locales/pl-PL.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:12\n" +"Last-Translator: \n" +"Language-Team: Polish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/locales/messages.pot\n" +"X-Crowdin-File-ID: 308\n" +"Language: pl_PL\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "To nie jest prawidłowy adres URL." + +#: dashboard\dashboard.py:35 +msgid "This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "To nie jest prawidłowy identyfikator URI przekierowania Dashboard: musi kończyć się na `/callback`." + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "Ta strona trzecia nie jest dostępna." + +#: dashboard\dashboard.py:50 +#, docstring +msgid "Interact with your bot through a web Dashboard!\n\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "Interakcja z botem za pośrednictwem pulpitu nawigacyjnego!\n\n" +" **Podręcznik instalacji:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ Ten pakiet jest rozwidleniem pracy Neuro Assassina i nie jest w ogóle wspierany przez organizację.\n" +" " + +#: dashboard\dashboard.py:347 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard nie jest zainstalowany. Sprawdź ." + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "Nie można uzyskać dostępu do pulpitu nawigacyjnego." + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "Red-Web-Dashboard" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "Ustaw sekret klienta wymagany dla Discord OAuth." + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "Sekret Discorda" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "Zestaw sekretów Discord OAuth." + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "Kliknij poniższy przycisk, aby ustawić sekret Discord OAuth." + diff --git a/dashboard/locales/pt-BR.po b/dashboard/locales/pt-BR.po new file mode 100644 index 0000000..550580c --- /dev/null +++ b/dashboard/locales/pt-BR.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:12\n" +"Last-Translator: \n" +"Language-Team: Portuguese, Brazilian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-BR\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/locales/messages.pot\n" +"X-Crowdin-File-ID: 308\n" +"Language: pt_BR\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "Esse não é um URL válido." + +#: dashboard\dashboard.py:35 +msgid "This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "Esse não é um URI de redirecionamento válido do Dashboard: ele deve terminar com `/callback`." + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "Esse terceiro não está disponível." + +#: dashboard\dashboard.py:50 +#, docstring +msgid "Interact with your bot through a web Dashboard!\n\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "Interaja com seu bot por meio de um painel da Web!\n\n" +" **Guia de instalação:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ Este pacote é uma derivação do trabalho do Neuro Assassin e não é endossado pela organização de forma alguma.\n" +" " + +#: dashboard\dashboard.py:347 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "O Red-Web-Dashboard não está instalado. Verifique ." + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "Não é possível acessar o Dashboard." + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "Painel de controle vermelho" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "Defina o segredo do cliente necessário para o Discord OAuth." + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "Segredo do Discord" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "Conjunto de segredos do Discord OAuth." + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "Clique no botão abaixo para definir um segredo para o Discord OAuth." + diff --git a/dashboard/locales/pt-PT.po b/dashboard/locales/pt-PT.po new file mode 100644 index 0000000..5f24699 --- /dev/null +++ b/dashboard/locales/pt-PT.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:12\n" +"Last-Translator: \n" +"Language-Team: Portuguese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-PT\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/locales/messages.pot\n" +"X-Crowdin-File-ID: 308\n" +"Language: pt_PT\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "Este não é um URL válido." + +#: dashboard\dashboard.py:35 +msgid "This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "Este não é um URI de redireccionamento do Dashboard válido: deve terminar com `/callback`." + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "Este terceiro não está disponível." + +#: dashboard\dashboard.py:50 +#, docstring +msgid "Interact with your bot through a web Dashboard!\n\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "Interaja com seu bot através de um Dashboard na web!\n\n" +" **Guia de instalação:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ Este pacote é um fork do trabalho do Neuro Assassin, e não é endossado pela Org de forma alguma.\n" +" " + +#: dashboard\dashboard.py:347 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "O Red-Web-Dashboard não está instalado. Verifique ." + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "Não é possível aceder ao painel de controlo." + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "Painel de controlo vermelho" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "Definir o segredo do cliente necessário para o Discord OAuth." + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "Segredo do Discord" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "Conjunto de segredos do Discord OAuth." + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "Clique no botão abaixo para definir um segredo para o Discord OAuth." + diff --git a/dashboard/locales/ro-RO.po b/dashboard/locales/ro-RO.po new file mode 100644 index 0000000..e1c2a0f --- /dev/null +++ b/dashboard/locales/ro-RO.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:12\n" +"Last-Translator: \n" +"Language-Team: Romanian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ro\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/locales/messages.pot\n" +"X-Crowdin-File-ID: 308\n" +"Language: ro_RO\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "Aceasta nu este o adresă URL validă." + +#: dashboard\dashboard.py:35 +msgid "This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "Acesta nu este un URI de redirecționare valid pentru Dashboard: trebuie să se termine cu `/callback`." + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "Această parte terță nu este disponibilă." + +#: dashboard\dashboard.py:50 +#, docstring +msgid "Interact with your bot through a web Dashboard!\n\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "Interacționați cu robotul dvs. prin intermediul unui tablou de bord web!\n\n" +" **Ghid de instalare:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ Acest pachet este un fork al lucrării lui Neuro Assassin și nu este aprobat deloc de Org.\n" +" " + +#: dashboard\dashboard.py:347 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard nu este instalat. Verificați ." + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "Nu puteți accesa tabloul de bord." + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "Red-Web-Dashboard" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "Setați secretul clientului necesar pentru Discord OAuth." + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "Discord Secret" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "Setul de secrete Discord OAuth." + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "Faceți clic pe butonul de mai jos pentru a seta un secret pentru Discord OAuth." + diff --git a/dashboard/locales/ru-RU.po b/dashboard/locales/ru-RU.po new file mode 100644 index 0000000..12fc6e7 --- /dev/null +++ b/dashboard/locales/ru-RU.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:12\n" +"Last-Translator: \n" +"Language-Team: Russian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ru\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/locales/messages.pot\n" +"X-Crowdin-File-ID: 308\n" +"Language: ru_RU\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "Это неправильный URL-адрес." + +#: dashboard\dashboard.py:35 +msgid "This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "Это не правильный URI перенаправления Dashboard: он должен заканчиваться `/callback`." + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "Эта третья сторона недоступна." + +#: dashboard\dashboard.py:50 +#, docstring +msgid "Interact with your bot through a web Dashboard!\n\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "Взаимодействуйте с вашим ботом через веб-панель!\n\n" +" **Руководство по установке:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ Этот пакет является форком работы Neuro Assassin, и не поддерживается орг.\n" +" " + +#: dashboard\dashboard.py:347 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard не установлен. Проверьте ." + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "Вы не можете получить доступ к панели управления." + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "Red-Web-Dashboard" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "Установите секрет клиента, необходимый для Discord OAuth." + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "Секрет раздора" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "Набор секретов Discord OAuth." + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "Нажмите на кнопку ниже, чтобы задать секрет для Discord OAuth." + diff --git a/dashboard/locales/tr-TR.po b/dashboard/locales/tr-TR.po new file mode 100644 index 0000000..f33c4fc --- /dev/null +++ b/dashboard/locales/tr-TR.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 13:27\n" +"Last-Translator: \n" +"Language-Team: Turkish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: tr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/locales/messages.pot\n" +"X-Crowdin-File-ID: 308\n" +"Language: tr_TR\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "Geçerli bir URL değil." + +#: dashboard\dashboard.py:35 +msgid "This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "Bu geçerli bir Dashboard yönlendirme URI'sı değil: `/callback` ile bitmelidir." + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "Bu üçüncü taraf mevcut değil." + +#: dashboard\dashboard.py:50 +#, docstring +msgid "Interact with your bot through a web Dashboard!\n\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "Botunuzla bir web Dashboard üzerinden etkileşimde bulunun!\n\n" +" **Kurulum rehberi:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ Bu paket, Neuro Assassin'in çalışmasının bir çatalıdır ve Org tarafından hiçbir şekilde onaylanmamıştır.\n" +" " + +#: dashboard\dashboard.py:347 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard yüklü değil. adresini kontrol edin." + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "Dashboard'a erişemezsiniz." + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "Red-Web-Dashboard" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "Discord OAuth için gereken istemci sırrını ayarlayın." + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "Discord Sırrı" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "Discord OAuth sırrı ayarlandı." + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "Discord OAuth için bir sır ayarlamak üzere aşağıdaki düğmeye tıklayın." + diff --git a/dashboard/locales/uk-UA.po b/dashboard/locales/uk-UA.po new file mode 100644 index 0000000..626aed1 --- /dev/null +++ b/dashboard/locales/uk-UA.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:12\n" +"Last-Translator: \n" +"Language-Team: Ukrainian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: uk\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/locales/messages.pot\n" +"X-Crowdin-File-ID: 308\n" +"Language: uk_UA\n" + +#: dashboard\dashboard.py:32 +msgid "This is not a valid URL." +msgstr "Ця URL-адреса не є дійсною." + +#: dashboard\dashboard.py:35 +msgid "This is not a valid Dashboard redirect URI: it must end with `/callback`." +msgstr "Це неправильний URI перенаправлення Dashboard: він повинен закінчуватися на `/callback`." + +#: dashboard\dashboard.py:44 +msgid "This third party is not available." +msgstr "Ця третя сторона недоступна." + +#: dashboard\dashboard.py:50 +#, docstring +msgid "Interact with your bot through a web Dashboard!\n\n" +" **Installation guide:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ This package is a fork of Neuro Assassin's work, and isn't endorsed by the Org at all.\n" +" " +msgstr "Взаємодійте зі своїм ботом через веб-панель управління!\n\n" +" **Інструкція з встановлення:** https://red-web-dashboard.readthedocs.io/en/latest\n" +" ⚠️ Цей пакунок є форком роботи Neuro Assassin і не схвалюється організацією.\n" +" " + +#: dashboard\dashboard.py:347 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard не встановлено. Перевірте ." + +#: dashboard\dashboard.py:352 +msgid "You can't access the Dashboard." +msgstr "Ви не маєте доступу до інформаційної панелі." + +#: dashboard\dashboard.py:354 +msgid "Red-Web-Dashboard" +msgstr "Червона панель приладів" + +#: dashboard\dashboard.py:374 +#, docstring +msgid "Set the client secret needed for Discord OAuth." +msgstr "Встановіть секрет клієнта, необхідний для Discord OAuth." + +#: dashboard\dashboard.py:383 +msgid "Discord Secret" +msgstr "Таємниця розбрату" + +#: dashboard\dashboard.py:391 +msgid "Discord OAuth secret set." +msgstr "Секретний набір Discord OAuth." + +#: dashboard\dashboard.py:427 +msgid "Click on the button below to set a secret for Discord OAuth." +msgstr "Натисніть кнопку нижче, щоб задати секрет для Discord OAuth." + diff --git a/dashboard/rpc/__init__.py b/dashboard/rpc/__init__.py new file mode 100644 index 0000000..093fba1 --- /dev/null +++ b/dashboard/rpc/__init__.py @@ -0,0 +1,789 @@ +from redbot.core import commands, core_commands, i18n # isort:skip +from redbot.core.bot import Red # isort:skip +from redbot.core.i18n import Translator # isort:skip +import discord # isort:skip +import typing # isort:skip + +import asyncio +import base64 +import pathlib +import random +import re +import time + +from redbot.core.utils import AsyncIter +from redbot.core.utils.chat_formatting import humanize_list + +from .default_cogs import DashboardRPC_DefaultCogs +from .pagination import Pagination +from .third_parties import DashboardRPC_ThirdParties +from .utils import rpc_check +from .webhooks import DashboardRPC_Webhooks + +# Credits: +# Thank you to NeuroAssassin for the original code. + +_: Translator = Translator("Dashboard", __file__) + + +class DashboardRPC: + """RPC server handlers for the dashboard to get special things from the bot.""" + + def __init__(self, bot: Red, cog: commands.Cog) -> None: + self.bot: Red = bot + self.cog: commands.Cog = cog + + # To make sure that both RPC server and client are on the same "version". + self.version: int = random.randint(1, 10000) + + # Initialize RPC handlers. + self.bot.register_rpc_handler(self.check_version) + self.bot.register_rpc_handler(self.get_data) + self.bot.register_rpc_handler(self.get_variables) + self.bot.register_rpc_handler(self.get_bot_variables) + self.bot.register_rpc_handler(self.get_commands) + self.bot.register_rpc_handler(self.get_user_guilds) + self.bot.register_rpc_handler(self.get_guild) + self.bot.register_rpc_handler(self.leave_guild) + self.bot.register_rpc_handler(self.set_guild_settings) + self.bot.register_rpc_handler(self.set_bot_profile) + self.bot.register_rpc_handler(self.get_dashboard_settings) + self.bot.register_rpc_handler(self.set_dashboard_settings) + self.bot.register_rpc_handler(self.get_bot_settings) + self.bot.register_rpc_handler(self.set_bot_settings) + self.bot.register_rpc_handler(self.set_custom_pages) + + # Initialize handlers. + self.handlers: typing.Dict[str, typing.Any] = {} + self.handlers["default_cogs"]: DashboardRPC_DefaultCogs = DashboardRPC_DefaultCogs( + self.cog + ) + self.handlers["webhooks"]: DashboardRPC_Webhooks = DashboardRPC_Webhooks(self.cog) + self.third_parties_handler: DashboardRPC_ThirdParties = DashboardRPC_ThirdParties(self.cog) + self.handlers["third_parties"]: DashboardRPC_ThirdParties = self.third_parties_handler + + # Caches: you can thank Trusty for the cogs infos. + self.invite_url: str = None + self.owner: str = None + self.cogs_infos_cache: typing.Dict[str, typing.Dict[str, str]] = {} + self.guilds_cache: typing.Dict[ + int, + typing.Dict[ + typing.Literal["guilds", "time"], typing.Union[typing.List[typing.Dict], int] + ], + ] = {} + + def unload(self) -> None: + if hasattr(self.bot, "dashboard_url"): + delattr(self.bot, "dashboard_url") + self.bot.unregister_rpc_handler(self.check_version) + self.bot.unregister_rpc_handler(self.get_data) + self.bot.unregister_rpc_handler(self.get_variables) + self.bot.unregister_rpc_handler(self.get_bot_variables) + self.bot.unregister_rpc_handler(self.get_commands) + self.bot.unregister_rpc_handler(self.get_user_guilds) + self.bot.unregister_rpc_handler(self.get_guild) + self.bot.unregister_rpc_handler(self.leave_guild) + self.bot.unregister_rpc_handler(self.set_guild_settings) + self.bot.unregister_rpc_handler(self.set_bot_profile) + self.bot.unregister_rpc_handler(self.get_dashboard_settings) + self.bot.unregister_rpc_handler(self.set_dashboard_settings) + self.bot.unregister_rpc_handler(self.get_bot_settings) + self.bot.unregister_rpc_handler(self.set_bot_settings) + self.bot.unregister_rpc_handler(self.set_custom_pages) + for handler in self.handlers.values(): + handler.unload() + + @rpc_check() + async def check_version(self) -> typing.Dict[str, int]: + return {"version": self.bot.get_cog("Dashboard").rpc.version} + + @rpc_check() + async def get_data(self) -> typing.Dict[str, typing.Any]: + data = await self.cog.config.webserver() + if data["ui"]["meta"]["title"] is None: + data["ui"]["meta"]["title"] = _("{name} Dashboard").format(name=self.bot.user.name) + else: + data["ui"]["meta"]["title"] = data["ui"]["meta"]["title"].replace( + "{name}", self.bot.user.name + ) + if data["ui"]["meta"]["icon"] is None: + data["ui"]["meta"]["icon"] = self.bot.user.display_avatar.url + if data["ui"]["meta"]["description"] is None: + data["ui"]["meta"]["description"] = _( + "Hello, welcome to the **Red-DiscordBot web Dashboard** for {name}! " + "{name} is based off the popular bot **Red-DiscordBot**, an open " + "source, multifunctional bot. It has *tons of features* including moderation, " + "audio, economy, fun and more! Here, you can control and interact with " + "{name}'s settings. **So what are you waiting for? Invite it now!**" + ).format(name=self.bot.user.name) + else: + data["ui"]["meta"]["description"] = data["ui"]["meta"]["description"].replace( + "{name}", self.bot.user.name + ) + if data["ui"]["meta"]["website_description"] is None: + data["ui"]["meta"]["website_description"] = _( + "Interactive Dashboard to control and interact with {name}." + ).format(name=self.bot.user.name) + # if data["ui"]["meta"]["support_server"] is None: + # data["ui"]["meta"]["support_server"] = "https://discord.gg/red" + return data + + @rpc_check() + async def get_variables( + self, + only_bot_variables: bool = False, + host_port: typing.Optional[typing.Tuple[str, int]] = None, + ) -> typing.Dict[str, typing.Any]: + variables = await self.get_bot_variables() + variables.update(third_parties=await self.third_parties_handler.get_third_parties()) + variables.update(commands={} if only_bot_variables else await self.get_commands()) + if host_port is not None: + redirect_uri = await self.cog.config.webserver.core.redirect_uri() + host, port = host_port + dashboard_url = ( + redirect_uri[:-9] + if redirect_uri is not None + else ( + f"http://127.0.0.1:{port}" + if host in ("0.0.0.0", "127.0.0.1") + else f"http://{host}" + ) + ) + is_private = redirect_uri is None and host in ("0.0.0.0", "127.0.0.1") + setattr(self.bot, "dashboard_url", (dashboard_url, not is_private)) + return variables + + @rpc_check() + async def get_bot_variables(self) -> typing.Dict[str, typing.Any]: + bot_info = await self.bot._config.custom_info() + prefixes = [ + p for p in await self.bot.get_valid_prefixes() if not re.match(r"<@!?([0-9]+)>", p) + ] + + guilds_count = len(self.bot.guilds) + users_count = len(self.bot.users) + text_channels_count = 0 + voice_channels_count = 0 + categories_count = 0 + for guild in self.bot.guilds: + text_channels_count += len(guild.text_channels) + voice_channels_count += len(guild.voice_channels) + categories_count += len(guild.categories) + + if self.invite_url is None: + self.invite_url: str = await self.bot.get_invite_url() + + if self.owner is None: + app_info = await self.bot.application_info() + self.owner: str = ( + str(app_info.team.name) if app_info.team else app_info.owner.display_name + ) + + return { + "bot": { + "name": self.bot.user.name, + "id": self.bot.user.id, + "application_id": self.bot.application_id, + "info": bot_info, + "profile_description": (await self.bot.application_info()).description, + "prefixes": prefixes, + "owner_ids": list(self.bot.owner_ids), + "owner": self.owner, + "avatar": str(self.bot.user.display_avatar.url).split("?")[0], + "default_avatar": str(self.bot.user.default_avatar.url).split("?")[0], + "is_verified": self.bot.user.public_flags.verified_bot, + "invite_url": self.invite_url, + "invite_public": await self.bot._config.invite_public(), + "blacklisted_users": list(await self.bot.get_blacklist()), + }, + "stats": { + "guilds": guilds_count, + "text": text_channels_count, + "voice": voice_channels_count, + "categories": categories_count, + "users": users_count, + "uptime": int(self.bot.uptime.timestamp()), + }, + "constants": { + "MIN_PREFIX_LENGTH": getattr( + core_commands, "MINIMUM_PREFIX_LENGTH", 1 + ), # Added by #6013 in Red 3.5.6. + "MAX_PREFIX_LENGTH": core_commands.MAX_PREFIX_LENGTH, + "MAX_DISCORD_PERMISSIONS_VALUE": discord.Permissions.all().value, + }, + } + + async def build_cmd_list( + self, + commands_list: typing.List[commands.Command], + details: bool = True, + is_owner: bool = False, + ) -> typing.List[typing.Dict[str, typing.Union[str, typing.List]]]: + final = [] + async for command in AsyncIter(sorted(commands_list, key=lambda c: c.name)): + if details: + if command.hidden: + continue + is_owner = ( + is_owner + or command.requires.privilege_level == commands.PrivilegeLevel.BOT_OWNER + ) + try: + details = { + "name": command.qualified_name, + "signature": command.signature, + "short_description": command.short_doc.strip() or "", + "description": command.help.strip() or "", + "aliases": list(command.aliases), + # "is_owner": is_owner, + "privilege_level": ( + command.requires.privilege_level.name + if command.requires.privilege_level is not None + else None + ), + "user_permissions": ( + "\n".join( + [ + permission.replace("_", " ").capitalize() + for permission, value in dict( + command.requires.user_perms + ).items() + if value + ] + ) + if command.requires.user_perms is not None + else None + ), + "user_permissions": ( + "\n".join( + [ + permission.replace("_", " ").capitalize() + for permission, value in dict( + command.requires.user_perms + ).items() + if value + ] + ) + if command.requires.user_perms is not None + else None + ), + "subs": [], + } + except ValueError: + continue + if isinstance(command, commands.Group): + details["subs"] = await self.build_cmd_list( + command.commands, is_owner=is_owner + ) + final.append(details) + else: + if ( + command.hidden + or command.requires.privilege_level == commands.PrivilegeLevel.BOT_OWNER + ): + continue + final.append(command.qualified_name) + if isinstance(command, commands.Group): + final += await self.build_cmd_list(command.commands, details=False) + return final + + @rpc_check() + async def get_commands( + self, + ) -> typing.Dict[ + str, + typing.Dict[ + str, typing.Union[str, typing.List[typing.Dict[str, typing.Union[str, typing.List]]]] + ], + ]: + returning = {} + downloader_cog = self.bot.get_cog("Downloader") + installed_cogs = ( + await downloader_cog.installed_cogs() if downloader_cog is not None else [] + ) + for cog in self.bot.cogs.copy().values(): + name = cog.qualified_name + stripped = [c for c in cog.__cog_commands__ if c.parent is None] + cmds = await self.build_cmd_list(stripped) + if not cmds: + continue + + author = "Unknown" + repo = "Unknown" + # Taken from Trusty's downloader fuckery (https://gist.github.com/TrustyJAID/784c8c32dd45b1cc8155ed42c0c56591). + if name in self.cogs_infos_cache: + author = self.cogs_infos_cache[name]["author"] + repo = self.cogs_infos_cache[name]["repo"] + elif downloader_cog is not None: + module = cog.__module__.split(".")[0] # downloader_cog.cog_name_from_instance(cog) + cog_info = next( + ( + installed_cog + for installed_cog in installed_cogs + if installed_cog.name == module + ), + None, + ) + if cog_info is not None: + author = humanize_list(cog_info.author) if cog_info.author else "Unknown" + try: + repo = cog_info.repo.clean_url or "Unknown" + except AttributeError: + repo = "Unknown (Removed from Downloader)" + elif cog.__module__.startswith("redbot."): + author = "Cog Creators" + repo = "https://github.com/Cog-Creators/Red-DiscordBot" + elif ( + pathlib.Path(__import__(cog.__module__).__path__[0]).parent.name + == "AAA3A-cogs" + ): # Handle my repo's clones... :P + author = "AAA3A" + repo = "https://github.com/AAA3A-AAA3A/AAA3A-cogs" + author = getattr(cog, "__authors__", []) or getattr(cog, "__author__", []) or author + if isinstance(author, (typing.List, typing.Tuple)): + author = humanize_list(author) + self.cogs_infos_cache[name] = {"author": author, "repo": repo} + returning[name] = { + "name": name, + "description": (cog.__doc__ or "").strip(), + "author": author or "", + "repo": repo, + "commands": cmds, + } + return {name: returning[name] for name in sorted(returning.keys())} + + async def notify_owners_of_blacklist(self, ip: str): + async with self.cog.config.webserver.core.blacklisted_ips() as blacklisted_ips: + blacklisted_ips.append(ip) + await self.bot.send_to_owners( + f"[Dashboard] Detected suspicious activity from IP `{ip}`. They have been blacklisted." + ) + + @rpc_check() + async def get_user_guilds( + self, + user_id: int, + per_page: typing.Optional[typing.Union[int, str]] = None, + page: typing.Optional[typing.Union[int, str]] = None, + query: typing.Optional[str] = None, + filter: typing.Optional[typing.Literal["owner", "admin", "mod"]] = None, + ) -> typing.Dict[str, typing.Any]: + user = self.bot.get_user(user_id) + if user is None: + # Bot doesn't even find user using bot.get_user, might as well spare all the data processing and return. + return {"guilds": [], "total": 0, "per_page": 10, "pages": 0, "page": 1} + is_owner = user.id in self.bot.owner_ids + guilds = [] + if filter is None and user_id in self.guilds_cache: + cached = self.guilds_cache[user_id] + if (cached["time"] + 60) > time.time(): + guilds = cached["guilds"] + else: + del self.guilds_cache[user_id] + + if not guilds: + # This could take a while. + async for guild in AsyncIter( + sorted( + self.bot.guilds, + key=lambda guild: (guild.owner.id != user_id, guild.name.lower()), + ), + steps=1300, + ): + guild_infos = { + "id": guild.id, + "name": guild.name, + "owner": guild.owner.display_name, + "owner_id": guild.owner.id, + "icon_url": ( + guild.icon.url.split("?")[0] + if guild.icon is not None + else "https://cdn.discordapp.com/embed/avatars/1.png" + ), + "icon_animated": guild.icon.is_animated() if guild.icon is not None else False, + "user_role": None, + } + if filter is None and is_owner: + guilds.append(guild_infos) + continue + member = guild.get_member(user_id) + if member is None: + continue + if (filter is None or filter == "owner") and member == guild.owner: + guild_infos["user_role"] = "OWNER" + guilds.append(guild_infos) + elif (filter is None or filter == "admin") and ( + await self.bot.is_admin(member) or member.guild_permissions.manage_guild + ): + guild_infos["user_role"] = "ADMIN" + guilds.append(guild_infos) + elif (filter is None or filter == "mod") and await self.bot.is_mod(member): + guild_infos["user_role"] = "MOD" + guilds.append(guild_infos) + if filter is None: + self.guilds_cache[user_id] = {"guilds": guilds, "time": time.time()} + + if query is not None: + query = query.strip().lower() + guilds = [ + guild + for guild in guilds + if query in guild["name"].lower() or query == str(guild["id"]) + ] + return Pagination.from_list(guilds, per_page=per_page, page=page).to_dict() + + @rpc_check() + async def get_guild(self, user_id: int, guild_id: int, for_third_parties: bool = False): + guild = self.bot.get_guild(guild_id) + if guild is None: + return {"status": 1} + member = guild.get_member(user_id) + is_owner = user_id in self.bot.owner_ids + if not is_owner and ( + member is None + or ( + not await self.bot.is_mod(member) + and not member.guild_permissions.manage_guild + and not for_third_parties + ) + ): + return {"status": 1} + + # joined_at = member.joined_at if member is not None else None + if is_owner: + humanized = "Everything (Bot Owner)" + elif member == guild.owner: + humanized = "Everything (Guild Owner)" + else: + humanized = "Admin" if await self.bot.is_admin(member) else "Mod" + + status_stats = {"online": 0, "dnd": 0, "idle": 0, "offline": 0} + for m in guild.members: + status_stats[m.raw_status if m.raw_status in status_stats else "offline"] += 1 + + if guild.verification_level is discord.VerificationLevel.none: + verification_level = "None" + elif guild.verification_level is discord.VerificationLevel.low: + verification_level = "1 - Low" + elif guild.verification_level is discord.VerificationLevel.medium: + verification_level = "2 - Medium" + elif guild.verification_level is discord.VerificationLevel.high: + verification_level = "3 - High" + elif guild.verification_level is discord.VerificationLevel.highest: + verification_level = "4 - Extreme" + else: + verification_level = "Unknown" + + all_roles = list(reversed([{"id": role.id, "name": role.name} for role in guild.roles])) + config_group = self.bot._config.guild(guild) + admin_roles = [ + {"id": role.id, "name": role.name} + for role_id in await config_group.admin_role() + if (role := guild.get_role(role_id)) is not None + ] + mod_roles = [ + {"id": role.id, "name": role.name} + for role_id in await config_group.mod_role() + if (role := guild.get_role(role_id)) is not None + ] + + return { + "status": 0, + "id": guild.id, + "name": guild.name, + "owner": guild.owner.display_name, + "owner_id": guild.owner.id, + "icon_url": ( + guild.icon.url + if guild.icon is not None + else "https://cdn.discordapp.com/embed/avatars/1.png" + ), + "icon_animated": guild.icon.is_animated() if guild.icon is not None else False, + "verification_level": verification_level, + "created_at": guild.created_at.timestamp(), + "joined_at": guild.me.joined_at.timestamp(), + # Guild stats. + "members_number": len(guild.members), + "online_number": status_stats["online"], + "dnd_number": status_stats["dnd"], + "idle_number": status_stats["idle"], + "offline_number": status_stats["offline"], + "bots_number": len([user for user in guild.members if user.bot]), + "humans_number": len([user for user in guild.members if not user.bot]), + "channels_number": len(guild.channels), + "text_channels_number": len(guild.text_channels), + "voice_channels_number": len(guild.voice_channels), + "roles_number": len(guild.roles), + "roles": all_roles, + # Bot wide settings. + "prefixes": sorted(await self.bot.get_valid_prefixes(guild)), + "settings": { + "edit_permission": user_id in self.bot.owner_ids + or await self.bot.is_admin(member) + or member.guild_permissions.manage_guild, + # Base. + "bot_nickname": guild.me.nick, + "prefixes": await config_group.prefix(), + "admin_roles": admin_roles, + "mod_roles": mod_roles, + "whitelist": await config_group.whitelist(), + "blacklist": await config_group.blacklist(), + # Commands. + "ignored": await self.bot._ignored_cache.get_ignored_guild(guild), + "disabled_commands": await config_group.disabled_commands(), + # Look. + "embeds": await config_group.embeds(), + "use_bot_color": await config_group.use_bot_color(), + "fuzzy": await config_group.fuzzy(), + "delete_delay": await config_group.delete_delay(), + # Locale. + "locale": await i18n.get_locale_from_guild(self.bot, guild), + "regional_format": await i18n.get_regional_format_from_guild(self.bot, guild), + }, + "perms": humanize_list(humanized), + } + + @rpc_check() + async def leave_guild(self, user_id: int, guild_id: int): + guild = self.bot.get_guild(guild_id) + if guild is None: + return {"status": 1} + if user_id not in self.bot.owner_ids: + return {"status": 1} + await guild.leave() + return {"status": 0} + + @rpc_check() + async def set_guild_settings( + self, user_id: int, guild_id: int, settings: typing.Dict[str, typing.Any] + ): + guild = self.bot.get_guild(guild_id) + if guild is None: + return {"status": 1} + member = guild.get_member(user_id) + if user_id not in self.bot.owner_ids and ( + member is None + or not (await self.bot.is_admin(member) or member.guild_permissions.manage_guild) + ): + return {"status": 1} + change_nickname_error = False + if settings["bot_nickname"] != guild.me.nick: + try: + await guild.me.edit(nick=settings["bot_nickname"]) + except discord.HTTPException as e: + change_nickname_error = str(e) + await self.bot.set_prefixes(settings["prefixes"], guild=guild) + config_group = self.bot._config.guild(guild) + await config_group.admin_role.set([int(role_id) for role_id in settings["admin_roles"]]) + await config_group.mod_role.set([int(role_id) for role_id in settings["mod_roles"]]) + await config_group.ignore.set(settings["ignored"]) + already_disabled_commands = await config_group.disabled_commands() + for command_name in settings["disabled_commands"].copy(): + if command_name in already_disabled_commands: + continue + if ( + (command := self.bot.get_command(command_name)) is None + or isinstance(command, commands.commands._RuleDropper) + or ( + command.requires.privilege_level is not None + and command.requires.privilege_level + > await commands.PrivilegeLevel.from_ctx( + type("Context", (), {"bot": self.bot, "author": member, "guild": guild}) + ) + ) + ): + settings["disabled_commands"].remove(command_name) + else: + command.disable_in(guild) + for command_name in already_disabled_commands: + if command_name not in settings["disabled_commands"]: + if (command := self.bot.get_command(command_name)) is not None and ( + command.requires.privilege_level is None + or not command.requires.privilege_level + > await commands.PrivilegeLevel.from_ctx( + type("Context", (), {"bot": self.bot, "author": member, "guild": guild}) + ) + ): + command.enable_in(guild) + await config_group.disabled_commands.set(settings["disabled_commands"]) + await config_group.embeds.set(settings["embeds"]) + await config_group.use_bot_color.set(settings["use_bot_color"]) + await config_group.fuzzy.set(settings["fuzzy"]) + await config_group.delete_delay.set(settings["delete_delay"]) + if settings["locale"] is None: + settings["locale"] = await self.bot._config.locale() + i18n.set_contextual_locale(settings["locale"]) + await self.bot._i18n_cache.set_locale(guild, settings["locale"]) + i18n.set_contextual_regional_format(settings["regional_format"]) + await self.bot._i18n_cache.set_regional_format(guild, settings["regional_format"]) + return {"status": 0, "change_nickname_error": change_nickname_error} + + @rpc_check() + async def set_bot_profile(self, user_id: int, settings: typing.Dict[str, typing.Any]): + if user_id not in self.bot.owner_ids: + return {"status": 1} + try: + if settings["avatar"] == "default": + await self.bot.user.edit(avatar=None) + elif settings["avatar"] != "keep": + avatar = base64.b64decode(settings["avatar"]) + await self.bot.user.edit(avatar=avatar) + if settings["name"] != self.bot.user.name: + try: + await asyncio.wait_for( + self.bot.get_cog("Core")._name(name=settings["name"]), timeout=30 + ) + except TimeoutError: + return { + "status": 1, + "error": "Changing the name timed out. Remember that you can only change it twice per hour.", + } + if settings["profile_description"] is not None: + from discord.http import Route + + await self.bot.http.request( + Route("PATCH", "/applications/@me"), + json={"description": settings["profile_description"]}, + ) + except discord.HTTPException as e: + return {"status": 1, "error": str(e)} + + @rpc_check() + async def get_dashboard_settings(self, user_id: int): + if user_id not in self.bot.owner_ids: + return {"status": 1} + config_group = self.cog.config.webserver.ui.meta + return { + "status": 0, + "title": await config_group.title(), + "icon": await config_group.icon(), + "website_description": await config_group.website_description(), + "description": await config_group.description(), + "support_server": await config_group.support_server(), + "default_color": await config_group.default_color(), + "default_background_theme": await config_group.default_background_theme(), + "default_sidenav_theme": await config_group.default_sidenav_theme(), + "disabled_third_parties": await self.cog.config.webserver.disabled_third_parties(), + } + + @rpc_check() + async def set_dashboard_settings(self, user_id: int, settings: typing.Dict[str, typing.Any]): + if user_id not in self.bot.owner_ids: + return {"status": 1} + config_group = self.cog.config.webserver.ui.meta + await config_group.title.set(settings["title"]) + await config_group.icon.set(settings["icon"]) + await config_group.website_description.set(settings["website_description"]) + await config_group.description.set(settings["description"]) + await config_group.support_server.set(settings["support_server"]) + await config_group.default_color.set(settings["default_color"]) + await config_group.default_background_theme.set(settings["default_background_theme"]) + await config_group.default_sidenav_theme.set(settings["default_sidenav_theme"]) + await self.cog.config.webserver.disabled_third_parties.set( + settings["disabled_third_parties"] + ) + return {"status": 0} + + @rpc_check() + async def get_bot_settings(self, user_id: int): + if user_id not in self.bot.owner_ids: + return {"status": 1} + config_group = self.bot._config + color = discord.Color(await config_group.color()) + return { + "status": 0, + # Base. + "prefixes": await config_group.prefix(), + "invoke_error_msg": await config_group.invoke_error_msg(), + "whitelist": await config_group.whitelist(), + "blacklist": await config_group.blacklist(), + # Commands. + "disabled_commands": await config_group.disabled_commands(), + "disabled_command_msg": await config_group.disabled_command_msg(), + # Descriptions. + "description": await config_group.description(), + "custom_info": await config_group.custom_info(), + # Look. + "embeds": await config_group.embeds(), + "color": f"#{color.value:06X}", + "fuzzy": await config_group.fuzzy(), + "use_buttons": await config_group.use_buttons(), + # Invite. + "invite_public": await config_group.invite_public(), + "invite_commands_scope": await config_group.invite_commands_scope(), + "invite_perms": await config_group.invite_perm(), + # Locale. + "locale": await config_group.locale(), + "regional_format": await config_group.regional_format(), + } + + @rpc_check() + async def set_bot_settings(self, user_id: int, settings: typing.Dict[str, typing.Any]): + if user_id not in self.bot.owner_ids: + return {"status": 1} + config_group = self.bot._config + await config_group.prefix.set(settings["prefixes"]) + await config_group.invoke_error_msg.set(settings["invoke_error_msg"]) + already_disabled_commands = await config_group.disabled_commands() + for command_name in settings["disabled_commands"].copy(): + if command_name in already_disabled_commands: + continue + if (command := self.bot.get_command(command_name)) is None or isinstance( + command, commands.commands._RuleDropper + ): + settings["disabled_commands"].remove(command_name) + else: + command.enabled = False + for command_name in already_disabled_commands: + if command_name not in settings["disabled_commands"]: + if (command := self.bot.get_command(command_name)) is not None: + command.enabled = True + await config_group.disabled_commands.set(settings["disabled_commands"]) + if settings["disabled_command_msg"] is not None: + await config_group.disabled_command_msg.set(settings["disabled_command_msg"]) + else: + await config_group.disabled_command_msg.clear() + if settings["description"] is not None: + await config_group.description.set(settings["description"]) + self.bot.description = settings["description"] + else: + await config_group.description.clear() + self.bot.description = "Red V3" + if settings["custom_info"] is not None: + await config_group.custom_info.set(settings["custom_info"]) + else: + await config_group.custom_info.clear() + await config_group.embeds.set(settings["embeds"]) + if settings["color"] is not None: + hex_color = settings["color"].lstrip("#") + r = int(hex_color[:2], 16) + g = int(hex_color[2:4], 16) + b = int(hex_color[4:6], 16) + color = discord.Color.from_rgb(r, g, b) + await config_group.color.set(color.value) + self.bot._color = color + else: + await config_group.color.clear() + self.bot._color = discord.Color.red() + await config_group.fuzzy.set(settings["fuzzy"]) + await config_group.use_buttons.set(settings["use_buttons"]) + await config_group.invite_public.set(settings["invite_public"]) + await config_group.invite_commands_scope.set(settings["invite_commands_scope"]) + await config_group.invite_perm.set(settings["invite_perms"]) + if settings["locale"] is None: + settings["locale"] = await self.bot._config.locale() + i18n.set_contextual_locale(settings["locale"]) + await self.bot._i18n_cache.set_locale(None, settings["locale"]) + i18n.set_contextual_regional_format(settings["regional_format"]) + await self.bot._i18n_cache.set_regional_format(None, settings["regional_format"]) + return {"status": 0} + + @rpc_check() + async def set_custom_pages( + self, user_id: int, custom_pages: typing.List[typing.Dict[str, str]] + ): + if user_id not in self.bot.owner_ids: + return {"status": 1} + await self.cog.config.webserver.custom_pages.set(custom_pages) + return {"status": 0} diff --git a/dashboard/rpc/default_cogs.py b/dashboard/rpc/default_cogs.py new file mode 100644 index 0000000..a201ce0 --- /dev/null +++ b/dashboard/rpc/default_cogs.py @@ -0,0 +1,192 @@ +from AAA3A_utils import CogsUtils # isort:skip +from redbot.core import commands # isort:skip +from redbot.core.bot import Red # isort:skip +from redbot.core.i18n import Translator # isort:skip +import discord # isort:skip +import typing # isort:skip + +from redbot.cogs.customcom.customcom import ArgParseError + +from .utils import rpc_check + +_: Translator = Translator("Dashboard", __file__) + + +class DashboardRPC_DefaultCogs: + def __init__(self, cog: commands.Cog) -> None: + self.bot: Red = cog.bot + self.cog: commands.Cog = cog + + self.bot.register_rpc_handler(self.get_aliases) + self.bot.register_rpc_handler(self.set_aliases) + self.bot.register_rpc_handler(self.get_custom_commands) + self.bot.register_rpc_handler(self.set_custom_commands) + + def unload(self) -> None: + self.bot.unregister_rpc_handler(self.get_aliases) + self.bot.unregister_rpc_handler(self.set_aliases) + self.bot.unregister_rpc_handler(self.get_custom_commands) + self.bot.unregister_rpc_handler(self.set_custom_commands) + + @rpc_check() + async def get_aliases(self, user_id: int, guild_id: typing.Optional[int]): + if guild_id is not None: + guild = self.bot.get_guild(guild_id) + if guild is None: + return {"status": 1} + member = guild.get_member(user_id) + if user_id not in self.bot.owner_ids and ( + member is None + or not (await self.bot.is_mod(member) or member.guild_permissions.manage_guild) + ): + return {"status": 1} + else: + guild = None + if user_id not in self.bot.owner_ids: + return {"status": 1} + Alias = self.bot.get_cog("Alias") + if Alias is None: + return {"status": 2} + if guild is not None: + aliases = await Alias._aliases.get_guild_aliases(guild) + else: + aliases = await Alias._aliases.get_global_aliases() + return { + "status": 0, + "aliases": { + alias.name: alias.command + for alias in sorted(aliases, key=lambda alias: alias.name) + }, + } + + @rpc_check() + async def set_aliases( + self, user_id: int, guild_id: typing.Optional[int], aliases: typing.Dict[str, str] + ): + if guild_id is not None: + guild = self.bot.get_guild(guild_id) + if guild is None: + return {"status": 1} + member = guild.get_member(user_id) + if user_id not in self.bot.owner_ids and ( + member is None + or not (await self.bot.is_mod(member) or member.guild_permissions.manage_guild) + ): + return {"status": 1} + else: + guild, member = None, None + if user_id not in self.bot.owner_ids: + return {"status": 1} + Alias = self.bot.get_cog("Alias") + if Alias is None: + return {"status": 2} + + if guild is not None: + existing_aliases = await Alias._aliases.get_guild_aliases(guild) + else: + existing_aliases = await Alias._aliases.get_global_aliases() + ctx = await CogsUtils.invoke_command( + bot=self.bot, + author=member or self.bot.get_user(user_id), + channel=discord.Object(id=0) if guild is None else guild.text_channels[0], + command="alias", + invoke=False, + ) + existing_aliases = {alias.name: alias for alias in existing_aliases} + errors = [] + for alias, command in aliases.items(): + if alias not in existing_aliases: + if Alias.is_command(alias): + errors.append( + _( + "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." + ).format(name=alias) + ) + continue + if self.bot.get_command(command.split(maxsplit=1)[0]) is None: + errors.append( + _( + "You attempted to create a new alias with the name {name}, but the command {command} does not exist." + ).format(name=alias, command=command) + ) + continue + await Alias._aliases.add_alias(ctx, alias, command, global_=guild is None) + elif command != existing_aliases[alias]: + await Alias._aliases.edit_alias(ctx, alias, command, global_=guild is None) + for alias in existing_aliases: + if alias not in aliases: + await Alias._aliases.delete_alias(ctx, alias, global_=guild is None) + if errors: + return {"status": 1, "errors": errors} + return {"status": 0} + + @rpc_check() + async def get_custom_commands(self, user_id: int, guild_id: int): + guild = self.bot.get_guild(guild_id) + if guild is None: + return {"status": 1} + member = guild.get_member(user_id) + if user_id not in self.bot.owner_ids and ( + member is None + or not (await self.bot.is_mod(member) or member.guild_permissions.administrator) + ): + return {"status": 1} + CustomCommands = self.bot.get_cog("CustomCommands") + if CustomCommands is None: + return {"status": 2} + custom_commands = ( + await CustomCommands.commandobj.get_commands(CustomCommands.config.guild(guild)) + ).values() + return { + "status": 0, + "custom_commands": { + custom_command["command"]: custom_command["response"] + for custom_command in sorted( + custom_commands, key=lambda custom_command: custom_command["command"] + ) + }, + } + + @rpc_check() + async def set_custom_commands( + self, user_id: int, guild_id: int, custom_commands: typing.Dict[str, str] + ): + guild = self.bot.get_guild(guild_id) + if guild is None: + return {"status": 1} + member = guild.get_member(user_id) + if user_id not in self.bot.owner_ids and ( + member is None + or not (await self.bot.is_mod(member) or member.guild_permissions.administrator) + ): + return {"status": 1} + CustomCommands = self.bot.get_cog("CustomCommands") + if CustomCommands is None: + return {"status": 2} + ctx = await CogsUtils.invoke_command( + bot=self.bot, + author=member, + channel=guild.text_channels[0], + command="customcom", + invoke=False, + ) + existing_custom_commands = await CustomCommands.commandobj.get_commands( + CustomCommands.config.guild(guild) + ) + errors = [] + for command, responses in custom_commands.items(): + if command not in existing_custom_commands: + try: + await CustomCommands.commandobj.create(ctx, command, response=responses) + except ArgParseError as e: + errors.append(_("`{command}`: ").format(command=command) + e.args[0]) + elif responses != existing_custom_commands[command]["response"]: + await CustomCommands.commandobj.edit( + ctx, command, response=responses, ask_for=False + ) + for command in existing_custom_commands: + if command not in custom_commands: + await CustomCommands.commandobj.delete(ctx, command) + if errors: + return {"status": 1, "errors": errors} + return {"status": 0} diff --git a/dashboard/rpc/form.py b/dashboard/rpc/form.py new file mode 100644 index 0000000..e48b3c5 --- /dev/null +++ b/dashboard/rpc/form.py @@ -0,0 +1,368 @@ +from AAA3A_utils import CogsUtils # isort:skip +from redbot.core import commands # isort:skip +import discord # isort:skip +import typing # isort:skip + +import hmac +import inspect + +from itsdangerous import BadData, SignatureExpired, URLSafeTimedSerializer +from markupsafe import Markup +from werkzeug.datastructures import ImmutableMultiDict +from werkzeug.utils import cached_property +from wtforms import ( + BooleanField, + Field, + Form, + FormField, + HiddenField, + SelectFieldBase, + SelectMultipleField, + SubmitField, +) # NOQA +from wtforms.csrf.core import CSRF +from wtforms.fields.core import UnboundField +from wtforms.meta import DefaultMeta +from wtforms.validators import ValidationError +from wtforms.widgets import HiddenInput + +INITIAL_INIT_FIELD = Field.__init__ + + +async def get_form_class( + _self, + third_party_cog: commands.Cog, + method: typing.Literal["HEAD", "GET", "OPTIONS", "POST", "PATCH", "DELETE"], + csrf_token: typing.Tuple[str, str], + wtf_csrf_secret_key: bytes, + data: typing.Dict[typing.Literal["form", "json"], typing.Dict[str, typing.Any]], + **kwargs, +): + extra_notifications = [] + _Auto = object() + + def _is_submitted() -> bool: + return method in {"POST", "PUT", "PATCH", "DELETE"} + + class _FlaskFormCSRF(CSRF): + def setup_form(self, form) -> typing.List[typing.Tuple[str, UnboundField]]: + self.meta = form.meta + return super().setup_form(form) + + def generate_csrf_token(self, csrf_token_field) -> str: + return csrf_token[1] + + def validate_csrf_token(self, form, field) -> None: + # At this point, the CSRF token should be already validated by the webserver because of the field name in `request.form`. + data = field.data + secret_key = self.meta.csrf_secret + time_limit = self.meta.csrf_time_limit + if not data: + raise ValidationError("The CSRF token is missing.") + s = URLSafeTimedSerializer(secret_key, salt="wtf-csrf-token") + try: + token = s.loads(data, max_age=time_limit) + except SignatureExpired as e: + raise ValidationError("The CSRF token has expired.") from e + except BadData as e: + raise ValidationError("The CSRF token is invalid.") from e + if not hmac.compare_digest(csrf_token[0], token): + raise ValidationError("The CSRF tokens do not match.") + + class FlaskForm(Form): + class Meta(DefaultMeta): + csrf_class = _FlaskFormCSRF + + @cached_property + def csrf(self) -> bool: + return True + + @cached_property + def csrf_secret(self) -> bytes: + return wtf_csrf_secret_key + + @cached_property + def csrf_time_limit(self) -> int: + return 3600 + + def wrap_formdata(self, form, formdata) -> typing.Optional[ImmutableMultiDict]: + if formdata is _Auto: + if _is_submitted(): + if data["form"]: + return ImmutableMultiDict(data["form"]) + elif data["json"]: + return ImmutableMultiDict(data["json"]) + return None + return formdata + + def __init__(self, formdata=_Auto, **kwargs) -> None: + super().__init__(formdata=formdata, **kwargs) + + def is_submitted(self) -> bool: + return _is_submitted() + + def validate_on_submit(self, extra_validators=None) -> bool: + if self.is_submitted() and self.validate(extra_validators=extra_validators): + return True + if any(field.data for field in self if isinstance(field, SubmitField)) and self.errors: + for field_name, error_messages in self.errors.items(): + if isinstance(error_messages[0], typing.Dict): + for sub_field_name, sub_error_messages in error_messages[0].items(): + extra_notifications.append( + { + "message": f"{field_name}-{sub_field_name}: {' '.join(sub_error_messages)}", + "category": "warning", + } + ) + continue + extra_notifications.append( + { + "message": f"{field_name}: {' '.join(error_messages)}", + "category": "warning", + } + ) + return False + + async def validate_dpy_converters(self) -> bool: + result = True + for field in self: + for validator in field.validators: + if not isinstance(validator, DpyObjectConverter): + continue + if isinstance(field, SelectMultipleField): + try: + field.data = [await validator.convert(value) for value in field.data] + except commands.BadArgument as e: + extra_notifications.append( + {"message": f"{field.name}: {e}", "category": "warning"} + ) + result = False + continue + if field.data is None or not field.data.strip(): + field.data = "" + continue + try: + field.data = await validator.convert(field.data) + except commands.BadArgument as e: + extra_notifications.append( + {"message": f"{field.name}: {e}", "category": "warning"} + ) + result = False + return result + + def hidden_tag(self, *fields) -> Markup: + def hidden_fields(fields): + for f in fields: + if isinstance(f, str): + f = getattr(self, f, None) + if f is None or not isinstance(f.widget, HiddenInput): + continue + yield f + + return Markup("\n".join(str(f) for f in hidden_fields(fields or self))) + + def __str__(self) -> Markup: + html_form = [ + '

', + f" {self.hidden_tag()}", + ] + for field in self: + if isinstance(field, (HiddenField, SubmitField)): + continue + html_form.append('
') + if not isinstance(field, BooleanField): + html_form.append('
') + html_form.append( + f' ' + ) + html_form.append( + f' {field(class_="form-control form-control-default")}' + ) + else: + html_form.append('
') + html_form.append(f' {field(class_="form-check-input ms-0", type="checkbox")}') + html_form.append(f' ') + html_form.append("
") + # else: + # html_form.append('
') + # html_form.append(f' {field(class_="form-check-input")}') + # html_form.append(f' ') + # html_form.append("
") + html_form.append("
") + html_form.append('
') + for field in self: + if isinstance(field, SubmitField): + if field.render_kw is None: + field.render_kw = {} + field.render_kw.setdefault( + "class", "btn mb-0 bg-gradient-success btn-md w-100 my-4" + ) + html_form.append(f" {field()}") + html_form.extend(["
", ""]) + return Markup("\n".join(html_form)) + + def init_field(field, *args, **kwargs): + INITIAL_INIT_FIELD(field, *args, **kwargs) + if isinstance(field, FormField): + return + if hasattr(field, "_value"): + field._real_value = field._value + field._value = lambda: (field._real_value() if hasattr(field, "_real_value") else "") or ( + (field.default if isinstance(field.default, typing.List) else str(field.default)) + if field.default is not None + else "" + ) + if isinstance(field, SelectFieldBase): + old_choices_generator = field._choices_generator + + def _choices_generator(choices): + for value, label, selected, render_kw in old_choices_generator(choices): + yield ( + value, + label, + selected + or ( + field.coerce(value) in field._value() + if isinstance(field._value(), typing.List) + else field.coerce(value) == field._value() + ), + render_kw, + ) + + field._choices_generator = _choices_generator + + Field.__init__ = init_field + + class DpyObjectConverter: + def __init__( + self, + converter: typing.Callable[[str], typing.Any], + param: typing.Optional[discord.ext.commands.parameters.Parameter] = None, + ) -> None: + self.converter: typing.Callable[[str], typing.Any] = converter + self.param: discord.ext.commands.parameters.Parameter = ( + param + or discord.ext.commands.parameters.Parameter( + name="converter", + kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, + annotation=self.converter, + ) + ) + + def __call__(self, form: Form, field: Field) -> None: + pass + + async def convert(self, argument: str) -> typing.Any: + context = await CogsUtils.invoke_command( + bot=_self.bot, + author=kwargs["user"], + channel=kwargs.get( + "channel", + ( + kwargs["guild"].text_channels[0] + if kwargs.get("guild") is not None + else kwargs["user"].create_dm() + ), + ), + command="ping", + invoke=False, + cog=third_party_cog, + ) + return await discord.ext.commands.converter.run_converters( + context, + converter=self.param.converter, + argument=argument, + param=self.param, + ) + + def get_sorted_channels( + guild: discord.Guild, + types: typing.Optional[typing.Tuple[discord.abc.GuildChannel]] = ( + discord.TextChannel, + discord.VoiceChannel, + ), + filter_func: typing.Optional[ + typing.Callable[[discord.abc.GuildChannel], bool] + ] = discord.utils.MISSING, + ) -> typing.List[typing.Tuple[int, str]]: + if filter_func is discord.utils.MISSING: + + def filter_func(channel: discord.abc.GuildChannel) -> bool: + bot_permissions = channel.permissions_for(guild.me) + member = guild.get_member(kwargs["user"].id) + member_permissions = ( + channel.permissions_for(member) if member is not None else None + ) + return ( + bot_permissions.view_channel + and bot_permissions.send_messages + and bot_permissions.embed_links + and ( + kwargs["user"].id in _self.bot.owner_ids + or ( + member_permissions is not None + and member_permissions.view_channel + and member_permissions.send_messages + and member_permissions.embed_links + ) + ) + ) + + channels = [] + voice_channels = [] + categorized_channels = {} + for channel in sorted(guild.channels, key=lambda channel: channel.position): + if not isinstance(channel, types) or ( + filter_func is not None and not filter_func(channel) + ): + continue + if channel.category is not None: + categorized_channels.setdefault(channel.category, []).append(channel) + elif isinstance(channel, discord.VoiceChannel): + voice_channels.append(channel) + else: + channels.append(channel) + channels += voice_channels + for category in sorted( + categorized_channels.items(), key=lambda category: category[0].position + ): + channels.extend( + sorted( + category[1], + key=lambda channel: ( + 1 if isinstance(channel, discord.VoiceChannel) else 0, + channel.position, + ), + ) + ) + return [ + ( + str(channel.id), + f"{'#!' if isinstance(channel, discord.VoiceChannel) else '#'}{channel.name}", + ) + for channel in channels + ] + + def get_sorted_roles( + guild: discord.Guild, + filter_func: typing.Optional[ + typing.Callable[[discord.Role], bool] + ] = discord.utils.MISSING, + ) -> typing.List[typing.Tuple[int, str]]: + if filter_func is discord.utils.MISSING: + filter_func = ( + lambda role: not role.is_bot_managed() + ) # and role.position < guild.me.top_role.position + return [ + (str(role.id), role.name) + for role in sorted(guild.roles, reverse=True) + if not role.is_default() and (filter_func is None or filter_func(role)) + ] + + return ( + FlaskForm, + DpyObjectConverter, + extra_notifications, + get_sorted_channels, + get_sorted_roles, + ) diff --git a/dashboard/rpc/locales/de-DE.po b/dashboard/rpc/locales/de-DE.po new file mode 100644 index 0000000..e7a3f8c --- /dev/null +++ b/dashboard/rpc/locales/de-DE.po @@ -0,0 +1,43 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: German\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: de\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/rpc/locales/messages.pot\n" +"X-Crowdin-File-ID: 310\n" +"Language: de_DE\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "{name} Dashboard" + +#: dashboard\rpc\__init__.py:113 +msgid "Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is based off the popular bot **Red-DiscordBot**, an open source, multifunctional bot. It has *tons of features* including moderation, audio, economy, fun and more! Here, you can control and interact with {name}'s settings. **So what are you waiting for? Invite it now!**" +msgstr "Hallo, willkommen zum **Red-DiscordBot Dashboard** für {name}! {name} basiert auf dem beliebten Bot **Red-DiscordBot**, einem quelloffenen, multifunktionalen Bot. Er hat *tausende von Funktionen* wie Moderation, Audio, Wirtschaft, Spaß und mehr! Hier kannst du die Einstellungen von {name}kontrollieren und mit ihnen interagieren. **Worauf wartest du noch? Lade ihn jetzt ein!**" + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "Interaktives Dashboard zur Steuerung und Interaktion mit {name}." + +#: dashboard\rpc\default_cogs.py:101 +msgid "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." +msgstr "Du hast versucht, einen neuen Alias mit dem Namen {name} zu erstellen, aber dieser Name ist bereits ein Befehl auf diesem Bot." + +#: dashboard\rpc\default_cogs.py:108 +msgid "You attempted to create a new alias with the name {name}, but the command {command} does not exist." +msgstr "Sie haben versucht, einen neuen Alias mit dem Namen {name} zu erstellen, aber der Befehl {command} existiert nicht." + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "{command}\": " + diff --git a/dashboard/rpc/locales/el-GR.po b/dashboard/rpc/locales/el-GR.po new file mode 100644 index 0000000..a263cc8 --- /dev/null +++ b/dashboard/rpc/locales/el-GR.po @@ -0,0 +1,43 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Greek\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: el\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/rpc/locales/messages.pot\n" +"X-Crowdin-File-ID: 310\n" +"Language: el_GR\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "{name} Ταμπλό" + +#: dashboard\rpc\__init__.py:113 +msgid "Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is based off the popular bot **Red-DiscordBot**, an open source, multifunctional bot. It has *tons of features* including moderation, audio, economy, fun and more! Here, you can control and interact with {name}'s settings. **So what are you waiting for? Invite it now!**" +msgstr "Γεια σας, καλώς ήρθατε στο **Red-DiscordBot Dashboard** για το {name}! Το {name} βασίζεται στο δημοφιλές bot **Red-DiscordBot**, ένα πολυλειτουργικό bot ανοιχτού κώδικα. Διαθέτει *τόνους χαρακτηριστικών*, συμπεριλαμβανομένων της μετριοπάθειας, του ήχου, της οικονομίας, της διασκέδασης και πολλά άλλα! Εδώ, μπορείτε να ελέγξετε και να αλληλεπιδράσετε με τις ρυθμίσεις του {name}. **Οπότε τι περιμένετε; Προσκαλέστε το τώρα!**" + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "Διαδραστικό ταμπλό για τον έλεγχο και την αλληλεπίδραση με το {name}." + +#: dashboard\rpc\default_cogs.py:101 +msgid "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." +msgstr "Προσπαθήσατε να δημιουργήσετε ένα νέο ψευδώνυμο με το όνομα {name}, αλλά αυτό το όνομα είναι ήδη μια εντολή σε αυτό το ρομπότ." + +#: dashboard\rpc\default_cogs.py:108 +msgid "You attempted to create a new alias with the name {name}, but the command {command} does not exist." +msgstr "Προσπαθήσατε να δημιουργήσετε ένα νέο ψευδώνυμο με το όνομα {name}, αλλά η εντολή {command} δεν υπάρχει." + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "`{command}`: " + diff --git a/dashboard/rpc/locales/es-ES.po b/dashboard/rpc/locales/es-ES.po new file mode 100644 index 0000000..72a0687 --- /dev/null +++ b/dashboard/rpc/locales/es-ES.po @@ -0,0 +1,43 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Spanish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: es-ES\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/rpc/locales/messages.pot\n" +"X-Crowdin-File-ID: 310\n" +"Language: es_ES\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "{name} Cuadro de mandos" + +#: dashboard\rpc\__init__.py:113 +msgid "Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is based off the popular bot **Red-DiscordBot**, an open source, multifunctional bot. It has *tons of features* including moderation, audio, economy, fun and more! Here, you can control and interact with {name}'s settings. **So what are you waiting for? Invite it now!**" +msgstr "Hola, bienvenido al **Red-DiscordBot Dashboard** para {name}! {name} está basado en el popular bot **Red-DiscordBot**, un bot multifuncional de código abierto. Tiene *toneladas de características* incluyendo moderación, audio, economía, diversión y ¡mucho más! Aquí puedes controlar e interactuar con la configuración de {name}. **¿A qué esperas? Invítalo ahora!**" + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "Cuadro de mandos interactivo para controlar e interactuar con {name}." + +#: dashboard\rpc\default_cogs.py:101 +msgid "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." +msgstr "Has intentado crear un nuevo alias con el nombre {name}, pero ese nombre ya es un comando en este bot." + +#: dashboard\rpc\default_cogs.py:108 +msgid "You attempted to create a new alias with the name {name}, but the command {command} does not exist." +msgstr "Has intentado crear un nuevo alias con el nombre {name}, pero el comando {command} no existe." + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "`{command}`: " + diff --git a/dashboard/rpc/locales/fi-FI.po b/dashboard/rpc/locales/fi-FI.po new file mode 100644 index 0000000..5dbdf04 --- /dev/null +++ b/dashboard/rpc/locales/fi-FI.po @@ -0,0 +1,43 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Finnish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fi\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/rpc/locales/messages.pot\n" +"X-Crowdin-File-ID: 310\n" +"Language: fi_FI\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "{name} Kojelauta" + +#: dashboard\rpc\__init__.py:113 +msgid "Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is based off the popular bot **Red-DiscordBot**, an open source, multifunctional bot. It has *tons of features* including moderation, audio, economy, fun and more! Here, you can control and interact with {name}'s settings. **So what are you waiting for? Invite it now!**" +msgstr "Hei, tervetuloa **Red-DiscordBot Dashboardiin** osoitteessa {name}! {name} perustuu suosittuun **Red-DiscordBotiin**, joka on avoimen lähdekoodin monikäyttöinen botti. Siinä on *tonneittain ominaisuuksia*, kuten moderointi, ääni, talous, hauskanpito ja paljon muuta! Täällä voit hallita ja vuorovaikutuksessa {name}'n asetuksia. **Mitä siis odotat? Kutsu se nyt!**" + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "Vuorovaikutteinen kojelauta, jonka avulla voit hallita ja olla vuorovaikutuksessa {name}." + +#: dashboard\rpc\default_cogs.py:101 +msgid "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." +msgstr "Yritit luoda uuden aliaksen nimellä {name}, mutta kyseinen nimi on jo komento tässä botissa." + +#: dashboard\rpc\default_cogs.py:108 +msgid "You attempted to create a new alias with the name {name}, but the command {command} does not exist." +msgstr "Yritit luoda uuden aliaksen nimellä {name}, mutta komentoa {command} ei ole olemassa." + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "`{command}`: " + diff --git a/dashboard/rpc/locales/fr-FR.po b/dashboard/rpc/locales/fr-FR.po new file mode 100644 index 0000000..d1e4c15 --- /dev/null +++ b/dashboard/rpc/locales/fr-FR.po @@ -0,0 +1,43 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: French\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/rpc/locales/messages.pot\n" +"X-Crowdin-File-ID: 310\n" +"Language: fr_FR\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "{name} Tableau de bord" + +#: dashboard\rpc\__init__.py:113 +msgid "Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is based off the popular bot **Red-DiscordBot**, an open source, multifunctional bot. It has *tons of features* including moderation, audio, economy, fun and more! Here, you can control and interact with {name}'s settings. **So what are you waiting for? Invite it now!**" +msgstr "Bonjour, bienvenue sur le **Red-DiscordBot Dashboard** pour {name}! {name} est basé sur le populaire bot **Red-DiscordBot**, un bot open source et multifonctionnel. Il possède *des tonnes de fonctionnalités*, dont la modération, l'audio, l'économie, l'amusement et bien plus encore ! Ici, vous pouvez contrôler et interagir avec les paramètres de {name}. **Alors, qu'attendez-vous ? Invitez-le maintenant!**" + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "Tableau de bord interactif pour contrôler et interagir avec {name}." + +#: dashboard\rpc\default_cogs.py:101 +msgid "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." +msgstr "Vous avez tenté de créer un nouvel alias avec le nom {name}, mais ce nom est déjà une commande sur ce bot." + +#: dashboard\rpc\default_cogs.py:108 +msgid "You attempted to create a new alias with the name {name}, but the command {command} does not exist." +msgstr "Vous avez tenté de créer un nouvel alias avec le nom {name}, mais la commande {command} n'existe pas." + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "`{command}` : " + diff --git a/dashboard/rpc/locales/it-IT.po b/dashboard/rpc/locales/it-IT.po new file mode 100644 index 0000000..fa489ca --- /dev/null +++ b/dashboard/rpc/locales/it-IT.po @@ -0,0 +1,43 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Italian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: it\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/rpc/locales/messages.pot\n" +"X-Crowdin-File-ID: 310\n" +"Language: it_IT\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "{name} Cruscotto" + +#: dashboard\rpc\__init__.py:113 +msgid "Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is based off the popular bot **Red-DiscordBot**, an open source, multifunctional bot. It has *tons of features* including moderation, audio, economy, fun and more! Here, you can control and interact with {name}'s settings. **So what are you waiting for? Invite it now!**" +msgstr "Salve, benvenuti nella **Red-DiscordBot Dashboard** per {name}! {name} è basato sul popolare bot **Red-DiscordBot**, un bot open source e multifunzionale. Ha *tantissime funzioni* tra cui moderazione, audio, economia, divertimento e altro ancora! Qui è possibile controllare e interagire con le impostazioni di {name}. **Quindi, cosa stai aspettando? Invitatelo ora!" + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "Dashboard interattivo per controllare e interagire con {name}." + +#: dashboard\rpc\default_cogs.py:101 +msgid "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." +msgstr "Si è tentato di creare un nuovo alias con il nome {name}, ma questo nome è già un comando su questo bot." + +#: dashboard\rpc\default_cogs.py:108 +msgid "You attempted to create a new alias with the name {name}, but the command {command} does not exist." +msgstr "Si è tentato di creare un nuovo alias con il nome {name}, ma il comando {command} non esiste." + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "{command}: " + diff --git a/dashboard/rpc/locales/ja-JP.po b/dashboard/rpc/locales/ja-JP.po new file mode 100644 index 0000000..de750b7 --- /dev/null +++ b/dashboard/rpc/locales/ja-JP.po @@ -0,0 +1,43 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Japanese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ja\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/rpc/locales/messages.pot\n" +"X-Crowdin-File-ID: 310\n" +"Language: ja_JP\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "{name} ダッシュボード" + +#: dashboard\rpc\__init__.py:113 +msgid "Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is based off the popular bot **Red-DiscordBot**, an open source, multifunctional bot. It has *tons of features* including moderation, audio, economy, fun and more! Here, you can control and interact with {name}'s settings. **So what are you waiting for? Invite it now!**" +msgstr "こんにちは、**Red-DiscordBot Dashboard** for {name}へようこそ! {name} は、オープンソースの多機能ボットとして人気の**Red-DiscordBot**をベースにしています。Red-DiscordBot**は、モデレーション、オーディオ、エコノミー、遊びなど、*たくさんの機能*を備えています!ここでは、 {name}'の設定を制御し、対話することができます。**だから、何を待っていますか?今それを招待!***" + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "インタラクティブなダッシュボードで、 {name}を制御し、対話する。" + +#: dashboard\rpc\default_cogs.py:101 +msgid "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." +msgstr "{name}という名前で新しいエイリアスを作成しようとしましたが、その名前はすでにこのボットのコマンドになっています。" + +#: dashboard\rpc\default_cogs.py:108 +msgid "You attempted to create a new alias with the name {name}, but the command {command} does not exist." +msgstr "{name}という名前で新しいエイリアスを作成しようとしましたが、 {command} というコマンドは存在しません。" + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "{command}`: " + diff --git a/dashboard/rpc/locales/messages.pot b/dashboard/rpc/locales/messages.pot new file mode 100644 index 0000000..c97f8d7 --- /dev/null +++ b/dashboard/rpc/locales/messages.pot @@ -0,0 +1,45 @@ +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2024-12-29 10:44+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "" + +#: dashboard\rpc\__init__.py:113 +msgid "" +"Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is " +"based off the popular bot **Red-DiscordBot**, an open source, " +"multifunctional bot. It has *tons of features* including moderation, audio, " +"economy, fun and more! Here, you can control and interact with {name}'s " +"settings. **So what are you waiting for? Invite it now!**" +msgstr "" + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "" + +#: dashboard\rpc\default_cogs.py:101 +msgid "" +"You attempted to create a new alias with the name {name}, but that name is " +"already a command on this bot." +msgstr "" + +#: dashboard\rpc\default_cogs.py:108 +msgid "" +"You attempted to create a new alias with the name {name}, but the command " +"{command} does not exist." +msgstr "" + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "" diff --git a/dashboard/rpc/locales/nl-NL.po b/dashboard/rpc/locales/nl-NL.po new file mode 100644 index 0000000..d8f23d3 --- /dev/null +++ b/dashboard/rpc/locales/nl-NL.po @@ -0,0 +1,43 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Dutch\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: nl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/rpc/locales/messages.pot\n" +"X-Crowdin-File-ID: 310\n" +"Language: nl_NL\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "{name} Dashboard" + +#: dashboard\rpc\__init__.py:113 +msgid "Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is based off the popular bot **Red-DiscordBot**, an open source, multifunctional bot. It has *tons of features* including moderation, audio, economy, fun and more! Here, you can control and interact with {name}'s settings. **So what are you waiting for? Invite it now!**" +msgstr "Hallo, welkom bij het **Red-DiscordBot Dashboard** voor {name}! {name} is gebaseerd op de populaire bot **Red-DiscordBot**, een open source, multifunctionele bot. Het heeft *veel functies* waaronder moderatie, audio, economie, plezier en meer! Hier kun je de instellingen van {name} beheren en ermee werken. **Dus waar wacht je nog op? Nodig hem nu uit!**" + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "Interactief dashboard voor controle en interactie met {name}." + +#: dashboard\rpc\default_cogs.py:101 +msgid "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." +msgstr "Je probeerde een nieuwe alias aan te maken met de naam {name}, maar die naam is al een commando op deze bot." + +#: dashboard\rpc\default_cogs.py:108 +msgid "You attempted to create a new alias with the name {name}, but the command {command} does not exist." +msgstr "U hebt geprobeerd een nieuwe alias aan te maken met de naam {name}, maar de opdracht {command} bestaat niet." + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "{command}: " + diff --git a/dashboard/rpc/locales/pl-PL.po b/dashboard/rpc/locales/pl-PL.po new file mode 100644 index 0000000..0ceab32 --- /dev/null +++ b/dashboard/rpc/locales/pl-PL.po @@ -0,0 +1,43 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Polish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/rpc/locales/messages.pot\n" +"X-Crowdin-File-ID: 310\n" +"Language: pl_PL\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "{name} Pulpit nawigacyjny" + +#: dashboard\rpc\__init__.py:113 +msgid "Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is based off the popular bot **Red-DiscordBot**, an open source, multifunctional bot. It has *tons of features* including moderation, audio, economy, fun and more! Here, you can control and interact with {name}'s settings. **So what are you waiting for? Invite it now!**" +msgstr "Witamy w **Red-DiscordBot Dashboard** dla {name}! {name} jest oparty na popularnym bocie **Red-DiscordBot**, otwartym, wielofunkcyjnym bocie. Posiada *mnóstwo funkcji*, w tym moderację, audio, ekonomię, zabawę i wiele więcej! Tutaj możesz kontrolować i wchodzić w interakcje z ustawieniami {name}. **Na co więc czekasz? Zaproś go teraz!**." + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "Interaktywny pulpit nawigacyjny do sterowania i interakcji z {name}." + +#: dashboard\rpc\default_cogs.py:101 +msgid "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." +msgstr "Próbowano utworzyć nowy alias o nazwie {name}, ale ta nazwa jest już komendą tego bota." + +#: dashboard\rpc\default_cogs.py:108 +msgid "You attempted to create a new alias with the name {name}, but the command {command} does not exist." +msgstr "Podjęto próbę utworzenia nowego aliasu o nazwie {name}, ale polecenie {command} nie istnieje." + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "`{command}`: " + diff --git a/dashboard/rpc/locales/pt-BR.po b/dashboard/rpc/locales/pt-BR.po new file mode 100644 index 0000000..97a9d0b --- /dev/null +++ b/dashboard/rpc/locales/pt-BR.po @@ -0,0 +1,43 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Portuguese, Brazilian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-BR\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/rpc/locales/messages.pot\n" +"X-Crowdin-File-ID: 310\n" +"Language: pt_BR\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "{name} Painel de controle" + +#: dashboard\rpc\__init__.py:113 +msgid "Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is based off the popular bot **Red-DiscordBot**, an open source, multifunctional bot. It has *tons of features* including moderation, audio, economy, fun and more! Here, you can control and interact with {name}'s settings. **So what are you waiting for? Invite it now!**" +msgstr "Olá, bem-vindo ao **Red-DiscordBot Dashboard** para {name}! O {name} é baseado no popular bot **Red-DiscordBot**, um bot multifuncional de código aberto. Ele tem *tons de recursos*, incluindo moderação, áudio, economia, diversão e muito mais! Aqui, você pode controlar e interagir com as configurações do {name}. **Então, o que está esperando? Convide-o agora!**" + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "Painel interativo para controlar e interagir com {name}." + +#: dashboard\rpc\default_cogs.py:101 +msgid "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." +msgstr "Você tentou criar um novo alias com o nome {name}, mas esse nome já é um comando neste bot." + +#: dashboard\rpc\default_cogs.py:108 +msgid "You attempted to create a new alias with the name {name}, but the command {command} does not exist." +msgstr "Você tentou criar um novo alias com o nome {name}, mas o comando {command} não existe." + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "`{command}`: " + diff --git a/dashboard/rpc/locales/pt-PT.po b/dashboard/rpc/locales/pt-PT.po new file mode 100644 index 0000000..ffd4daa --- /dev/null +++ b/dashboard/rpc/locales/pt-PT.po @@ -0,0 +1,43 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Portuguese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-PT\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/rpc/locales/messages.pot\n" +"X-Crowdin-File-ID: 310\n" +"Language: pt_PT\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "{name} Painel de controlo" + +#: dashboard\rpc\__init__.py:113 +msgid "Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is based off the popular bot **Red-DiscordBot**, an open source, multifunctional bot. It has *tons of features* including moderation, audio, economy, fun and more! Here, you can control and interact with {name}'s settings. **So what are you waiting for? Invite it now!**" +msgstr "Olá, bem-vindo ao **Red-DiscordBot Dashboard** para {name}! {name} é baseado no popular bot **Red-DiscordBot**, um bot multifuncional de código aberto. Tem *toneladas de funcionalidades* incluindo moderação, áudio, economia, diversão e muito mais! Aqui, podes controlar e interagir com as definições do {name}. **Então, de que estás à espera? Convide-o agora!**" + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "Painel de controlo interativo para controlar e interagir com {name}." + +#: dashboard\rpc\default_cogs.py:101 +msgid "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." +msgstr "Você tentou criar um novo alias com o nome {name}, mas esse nome já é um comando neste bot." + +#: dashboard\rpc\default_cogs.py:108 +msgid "You attempted to create a new alias with the name {name}, but the command {command} does not exist." +msgstr "Tentou criar um novo pseudónimo com o nome {name}, mas o comando {command} não existe." + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "`{command}`: " + diff --git a/dashboard/rpc/locales/ro-RO.po b/dashboard/rpc/locales/ro-RO.po new file mode 100644 index 0000000..831068f --- /dev/null +++ b/dashboard/rpc/locales/ro-RO.po @@ -0,0 +1,43 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Romanian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ro\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/rpc/locales/messages.pot\n" +"X-Crowdin-File-ID: 310\n" +"Language: ro_RO\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "{name} Tabloul de bord" + +#: dashboard\rpc\__init__.py:113 +msgid "Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is based off the popular bot **Red-DiscordBot**, an open source, multifunctional bot. It has *tons of features* including moderation, audio, economy, fun and more! Here, you can control and interact with {name}'s settings. **So what are you waiting for? Invite it now!**" +msgstr "Bună ziua, bine ați venit la **Red-DiscordBot Dashboard** pentru {name}! {name} se bazează pe popularul bot **Red-DiscordBot**, un bot multifuncțional cu sursă deschisă. Are *o mulțime de caracteristici*, inclusiv moderare, audio, economie, distracție și multe altele! Aici, puteți controla și interacționa cu setările lui {name}'s. **Acum ce mai aștepți? Invită-l acum!**." + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "Tablou de bord interactiv pentru a controla și interacționa cu {name}." + +#: dashboard\rpc\default_cogs.py:101 +msgid "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." +msgstr "Ați încercat să creați un alias nou cu numele {name}, dar acest nume este deja o comandă pe acest robot." + +#: dashboard\rpc\default_cogs.py:108 +msgid "You attempted to create a new alias with the name {name}, but the command {command} does not exist." +msgstr "Ați încercat să creați un nou alias cu numele {name}, dar comanda {command} nu există." + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "`{command}`: " + diff --git a/dashboard/rpc/locales/ru-RU.po b/dashboard/rpc/locales/ru-RU.po new file mode 100644 index 0000000..5059dca --- /dev/null +++ b/dashboard/rpc/locales/ru-RU.po @@ -0,0 +1,43 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Russian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ru\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/rpc/locales/messages.pot\n" +"X-Crowdin-File-ID: 310\n" +"Language: ru_RU\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "{name} Приборная панель" + +#: dashboard\rpc\__init__.py:113 +msgid "Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is based off the popular bot **Red-DiscordBot**, an open source, multifunctional bot. It has *tons of features* including moderation, audio, economy, fun and more! Here, you can control and interact with {name}'s settings. **So what are you waiting for? Invite it now!**" +msgstr "Здравствуйте, добро пожаловать на **Red-DiscordBot Dashboard** для {name}! {name} основан на популярном боте **Red-DiscordBot**, многофункциональном боте с открытым исходным кодом. Он имеет *тонны возможностей*, включая модерацию, аудио, экономику, развлечения и многое другое! Здесь вы можете управлять и взаимодействовать с настройками {name}. **Так чего же вы ждете? Пригласите его прямо сейчас!" + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "Интерактивная приборная панель для управления и взаимодействия с {name}." + +#: dashboard\rpc\default_cogs.py:101 +msgid "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." +msgstr "Вы попытались создать новый псевдоним с именем {name}, но это имя уже является командой на этом боте." + +#: dashboard\rpc\default_cogs.py:108 +msgid "You attempted to create a new alias with the name {name}, but the command {command} does not exist." +msgstr "Вы попытались создать новый псевдоним с именем {name}, но команда {command} не существует." + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "`{command}`: " + diff --git a/dashboard/rpc/locales/tr-TR.po b/dashboard/rpc/locales/tr-TR.po new file mode 100644 index 0000000..dedf014 --- /dev/null +++ b/dashboard/rpc/locales/tr-TR.po @@ -0,0 +1,43 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 13:27\n" +"Last-Translator: \n" +"Language-Team: Turkish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: tr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/rpc/locales/messages.pot\n" +"X-Crowdin-File-ID: 310\n" +"Language: tr_TR\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "{name} Kontrol Paneli" + +#: dashboard\rpc\__init__.py:113 +msgid "Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is based off the popular bot **Red-DiscordBot**, an open source, multifunctional bot. It has *tons of features* including moderation, audio, economy, fun and more! Here, you can control and interact with {name}'s settings. **So what are you waiting for? Invite it now!**" +msgstr "Merhaba, {name} için **Red-DiscordBot Kontrol Paneli**'ne hoş geldiniz! {name}, popüler bot **Red-DiscordBot**'a dayanmaktadır; bu açık kaynaklı, çok işlevli bir bottur. Moderasyon, ses, ekonomi, eğlence ve daha birçok özelliğe sahiptir! Burada, {name}'ın ayarlarını kontrol edebilir ve onunla etkileşimde bulunabilirsiniz. **Peki, ne bekliyorsunuz? Onu şimdi davet edin!**" + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "{name} ile kontrol ve etkileşim için interaktif kontrol paneli." + +#: dashboard\rpc\default_cogs.py:101 +msgid "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." +msgstr "Yeni bir takma ad oluşturmayı denediniz, ancak {name} adı bu botta zaten bir komut olarak mevcut." + +#: dashboard\rpc\default_cogs.py:108 +msgid "You attempted to create a new alias with the name {name}, but the command {command} does not exist." +msgstr "Yeni bir takma ad oluşturmayı denediniz, ancak {command} komutu mevcut değil." + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "`{command}`: " + diff --git a/dashboard/rpc/locales/uk-UA.po b/dashboard/rpc/locales/uk-UA.po new file mode 100644 index 0000000..7ad209d --- /dev/null +++ b/dashboard/rpc/locales/uk-UA.po @@ -0,0 +1,43 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Ukrainian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: uk\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dashboard/rpc/locales/messages.pot\n" +"X-Crowdin-File-ID: 310\n" +"Language: uk_UA\n" + +#: dashboard\rpc\__init__.py:105 +msgid "{name} Dashboard" +msgstr "{name} Приладова панель" + +#: dashboard\rpc\__init__.py:113 +msgid "Hello, welcome to the **Red-DiscordBot Dashboard** for {name}! {name} is based off the popular bot **Red-DiscordBot**, an open source, multifunctional bot. It has *tons of features* including moderation, audio, economy, fun and more! Here, you can control and interact with {name}'s settings. **So what are you waiting for? Invite it now!**" +msgstr "Привіт, ласкаво просимо до **Red-DiscordBot Dashboard** для {name}! {name} заснований на популярному боті **Red-DiscordBot**, багатофункціональному боті з відкритим вихідним кодом. Він має *тонни функцій*, включаючи модерацію, аудіо, економію, розваги та багато іншого! Тут ви можете керувати та взаємодіяти з налаштуваннями {name}. **Так чого ж ти чекаєш? Запросіть його зараз!**" + +#: dashboard\rpc\__init__.py:125 +msgid "Interactive Dashboard to control and interact with {name}." +msgstr "Інтерактивна панель управління та взаємодії з {name}." + +#: dashboard\rpc\default_cogs.py:101 +msgid "You attempted to create a new alias with the name {name}, but that name is already a command on this bot." +msgstr "Ви спробували створити новий псевдонім з ім'ям {name}, але це ім'я вже є командою для цього бота." + +#: dashboard\rpc\default_cogs.py:108 +msgid "You attempted to create a new alias with the name {name}, but the command {command} does not exist." +msgstr "Ви спробували створити новий псевдонім з іменем {name}, але команда {command} не існує." + +#: dashboard\rpc\default_cogs.py:182 +msgid "`{command}`: " +msgstr "{command}: " + diff --git a/dashboard/rpc/pagination.py b/dashboard/rpc/pagination.py new file mode 100644 index 0000000..71f10dd --- /dev/null +++ b/dashboard/rpc/pagination.py @@ -0,0 +1,89 @@ +import typing # isort:skip + + +class Pagination(typing.List): + """Pagination system for lists.""" + + DEFAULT_PER_PAGE: int = 20 + DEFAULT_PAGE: int = 1 + + def __init__(self, *args, **kwargs) -> None: + self.total: int = kwargs.pop("total", None) + self.per_page: int = kwargs.pop("per_page", None) + self.pages: int = kwargs.pop("pages", None) + self.page: int = kwargs.pop("page", None) + self.default_per_page: int = kwargs.pop("default_per_page", self.DEFAULT_PER_PAGE) + self.default_page: int = kwargs.pop("default_page", 1) + super().__init__(*args, **kwargs) + + def to_dict(self) -> typing.Dict[str, typing.Any]: + return { + "items": list(self), + "total": self.total, + "per_page": self.per_page, + "pages": self.pages, + "page": self.page, + "default_per_page": self.default_per_page, + "default_page": self.default_page, + } + + def __repr__(self) -> str: + return f"" + + def has_prev(self) -> bool: + return self.page > 1 + + def has_next(self) -> bool: + return self.page < self.pages + + @property + def elements_numbers(self) -> typing.List[int]: + return list(range(1, self.total + 1)) + + @property + def pages_numbers(self) -> typing.List[int]: + return list(range(1, self.pages + 1)) + + @classmethod + def from_list( + cls, + items: typing.List[typing.Any], + per_page: typing.Optional[typing.Union[int, str]] = None, + page: typing.Optional[typing.Union[int, str]] = None, + default_per_page: int = DEFAULT_PER_PAGE, + default_page: int = DEFAULT_PAGE, + ) -> typing.Any: + per_page = ( + default_per_page + if per_page is None + else ( + int(per_page) + if isinstance(per_page, str) + and per_page.isdigit() + and 1 <= int(per_page) <= max(default_per_page * 5, 100) + else default_per_page + ) + ) + page = ( + default_page + if page is None + else ( + int(page) + if isinstance(page, str) and page.isdigit() and int(page) >= 1 + else default_page + ) + ) + total = len(items) + pages = (total // per_page) + (total % per_page > 0) + page = min(page, pages) + start = (page - 1) * per_page + end = start + per_page + return cls( + items[start:end], + total=total, + per_page=per_page, + pages=pages, + page=page, + default_per_page=default_per_page, + default_page=default_page, + ) diff --git a/dashboard/rpc/third_parties.py b/dashboard/rpc/third_parties.py new file mode 100644 index 0000000..1573edb --- /dev/null +++ b/dashboard/rpc/third_parties.py @@ -0,0 +1,370 @@ +from redbot.core import commands # isort:skip +from redbot.core.bot import Red # isort:skip +import discord # isort:skip +import typing # isort:skip + +import base64 +import inspect +import types + +from redbot.core.i18n import ( + get_locale_from_guild, + set_contextual_locale, + set_contextual_regional_format, +) # NOQA +from werkzeug.datastructures import ImmutableMultiDict + +from .form import INITIAL_INIT_FIELD, Field, get_form_class +from .pagination import Pagination +from .utils import rpc_check + + +def dashboard_page( + name: typing.Optional[str] = None, + description: typing.Optional[str] = None, + methods: typing.Tuple[str] = ("GET",), + context_ids: typing.List[str] = None, + required_kwargs: typing.List[str] = None, + optional_kwargs: typing.List[str] = None, + is_owner: bool = False, + hidden: typing.Optional[bool] = None, +): + if context_ids is None: + context_ids = [] + if required_kwargs is None: + required_kwargs = [] + if optional_kwargs is None: + optional_kwargs = [] + + def decorator(func: typing.Callable): + if name is not None: + if not isinstance(name, str): + raise TypeError("Name of a page must be a string.") + discord.app_commands.commands.validate_name(name) + if not inspect.iscoroutinefunction(func): + raise TypeError("Func must be a coroutine.") + + params = { + "name": name, + "description": description or func.__doc__, + "methods": methods, + "context_ids": context_ids, + "required_kwargs": required_kwargs, + "optional_kwargs": optional_kwargs, + "is_owner": is_owner, + "hidden": hidden, + } + for key, value in inspect.signature(func).parameters.items(): + if value.name == "self" or value.kind in ( + inspect._ParameterKind.POSITIONAL_ONLY, + inspect._ParameterKind.VAR_KEYWORD, + ): + continue + if value.default is not inspect._empty: + params["optional_kwargs"].append(key) + continue + if ( + key in ("user_id", "guild_id", "member_id", "role_id", "channel_id") + and key not in params["context_ids"] + ): + params["context_ids"].append(key) + elif f"{key}_id" in ("user_id", "guild_id", "member_id", "role_id", "channel_id"): + if f"{key}_id" not in params["context_ids"]: + params["context_ids"].append(f"{key}_id") + elif key not in ( + "method", + "request_url", + "csrf_token", + "wtf_csrf_secret_key", + "extra_kwargs", + "data", + "lang_code", + "Form", + "DpyObjectConverter", + "Pagination", + ): + params["required_kwargs"].append(key) + + # A guild must be chose for these kwargs. + if "guild_id" not in params["context_ids"]: + for key in ("member_id", "role_id", "channel_id"): + if key in params["context_ids"]: + params["context_ids"].append("guild_id") + break + + # No method `GET`, no guild available and no owner check without user connection. + if "user_id" not in params["context_ids"] and ( + "guild_id" in params["context_ids"] or is_owner + ): + params["context_ids"].append("user_id") + if params["hidden"] is None: + params["hidden"] = ( + "GET" not in methods + or params["required_kwargs"] + or any(x for x in params["context_ids"] if x not in ("user_id", "guild_id")) + ) + + func.__dashboard_params__ = params.copy() + return func + + return decorator + + +class DashboardRPC_ThirdParties: + def __init__(self, cog: commands.Cog) -> None: + self.bot: Red = cog.bot + self.cog: commands.Cog = cog + + self.third_parties: typing.Dict[ + str, typing.Dict[str, typing.Tuple[typing.Callable, typing.Dict[str, bool]]] + ] = {} + self.third_parties_cogs: typing.Dict[str, commands.Cog] = {} + + self.bot.register_rpc_handler(self.oauth_receive) + self.bot.register_rpc_handler(self.get_third_parties) + self.bot.register_rpc_handler(self.data_receive) + self.bot.add_listener(self.on_cog_add) + self.bot.add_listener(self.on_cog_remove) + self.bot.dispatch("dashboard_cog_add", self.cog) + + def unload(self) -> None: + self.bot.dispatch("dashboard_cog_remove", self.cog) + self.bot.unregister_rpc_handler(self.oauth_receive) + self.bot.unregister_rpc_handler(self.get_third_parties) + self.bot.unregister_rpc_handler(self.data_receive) + self.bot.remove_listener(self.on_cog_add) + self.bot.remove_listener(self.on_cog_remove) + + @commands.Cog.listener() + async def on_cog_add(self, cog: commands.Cog) -> None: + ev = "on_dashboard_cog_add" + funcs = [listener[1] for listener in cog.get_listeners() if listener[0] == ev] + for func in funcs: + self.bot._schedule_event(func, ev, self.cog) # Like in `bot.dispatch`. + + @commands.Cog.listener() + async def on_cog_remove(self, cog: commands.Cog) -> None: + if cog not in self.third_parties_cogs.values(): + return + self.remove_third_party(cog) + + def add_third_party(self, cog: commands.Cog, overwrite: bool = False) -> None: + name = cog.qualified_name + if name in self.third_parties and not overwrite: + raise RuntimeError(f"`{name}` is already an existing third party.") + _pages = {} + for attr in dir(cog): + try: + if hasattr((func := getattr(cog, attr)), "__dashboard_decorator_params__"): + setattr( + cog, + attr, + types.MethodType( + dashboard_page( + *func.__dashboard_decorator_params__[0], + **func.__dashboard_decorator_params__[1], + )(func.__func__), + func.__self__, + ), + ) + if hasattr((func := getattr(cog, attr)), "__dashboard_params__"): + page = func.__dashboard_params__["name"] + if page in _pages: + raise RuntimeError( + f"The page {page} is already an existing page for this third party." + ) + _pages[page] = (func, func.__dashboard_params__) + except TypeError: + continue + if not _pages: + raise RuntimeError("No page found.") + self.third_parties[name] = _pages + self.third_parties_cogs[name] = cog + + def remove_third_party( + self, cog: commands.Cog + ) -> typing.Dict[str, typing.Tuple[typing.Callable, typing.Dict[str, bool]]]: + name = cog.qualified_name + try: + del self.third_parties_cogs[name] + except KeyError: + pass + return self.third_parties.pop(name, None) + + @rpc_check() + async def oauth_receive( + self, user_id: int, payload: typing.Dict[str, str] + ) -> typing.Dict[str, int]: + self.bot.dispatch("oauth_receive", user_id, payload) + return {"status": 0} + + @rpc_check() + async def get_third_parties( + self, + ) -> typing.Dict[str, typing.Dict[str, typing.Tuple[typing.Callable, typing.Dict[str, bool]]]]: + return { + key: {k: v[1] for k, v in value.items()} for key, value in self.third_parties.items() + } + + @rpc_check() + async def data_receive( + self, + method: typing.Literal["HEAD", "GET", "OPTIONS", "POST", "PATCH", "DELETE"], + name: str, + page: str, + request_url: str, + csrf_token: typing.Tuple[str, str], + wtf_csrf_secret_key: str, + context_ids: typing.Optional[typing.Dict[str, int]] = None, + required_kwargs: typing.Dict[str, typing.Any] = None, + optional_kwargs: typing.Dict[str, typing.Any] = None, + extra_kwargs: typing.Dict[str, typing.Any] = None, + data: typing.Dict[typing.Literal["form", "json"], typing.Dict[str, typing.Any]] = None, + lang_code: typing.Optional[str] = None, + ) -> typing.Dict[str, typing.Any]: + if context_ids is None: + context_ids = {} + if required_kwargs is None: + required_kwargs = {} + if optional_kwargs is None: + optional_kwargs = {} + if extra_kwargs is None: + extra_kwargs = {} + if data is None: + data = {"form": {}, "json": {}} + kwargs = {} + if not name or name not in self.third_parties or name not in self.third_parties_cogs: + return { + "status": 1, + "message": "Third party not found.", + "error_code": 404, + "error_message": "Looks like that third party doesn't exist... Strange...", + } + if self.bot.get_cog(self.third_parties_cogs[name].qualified_name) is None: + return { + "status": 1, + "message": "Third party not loaded.", + "error_code": 404, + "error_message": "Looks like that third party doesn't exist... Strange...", + } + page = page.lower() if page is not None else page + if page not in self.third_parties[name]: + return { + "status": 1, + "message": "Page not found.", + "error_code": 404, + "error_message": "Looks like that page doesn't exist... Strange...", + } + if "user_id" in self.third_parties[name][page][1]["context_ids"]: + if (user := self.bot.get_user(context_ids["user_id"])) is None: + return { + "status": 1, + "message": "Forbidden access.", + "error_code": 404, + "error_message": "Looks like that I do not share any server with you...", + } + kwargs["user_id"] = context_ids["user_id"] + kwargs["user"] = user + if ( + self.third_parties[name][page][1]["is_owner"] + and context_ids["user_id"] not in self.bot.owner_ids + ): + return { + "status": 1, + "message": "Forbidden access.", + "error_code": 403, + "error_message": "You must be the owner of the bot to access this page.", + } + if ( + "guild_id" in self.third_parties[name][page][1]["context_ids"] + and "user_id" in self.third_parties[name][page][1]["context_ids"] + ): + if (guild := self.bot.get_guild(context_ids["guild_id"])) is None: + return { + "status": 1, + "message": "Forbidden access.", + "error_code": 404, + "error_message": "Looks like that I'm not in this server...", + } + if ( + context_ids["user_id"] not in self.bot.owner_ids + and guild.get_member(context_ids["user_id"]) is None + ): + return { + "status": 1, + "message": "Forbidden access.", + "error_code": 403, + "error_message": "Looks like that you're not in this server...", + } + kwargs["guild_id"] = context_ids["guild_id"] + kwargs["guild"] = guild + if "member_id" in self.third_parties[name][page][1]["context_ids"]: + if (member := guild.get_member(context_ids["member_id"])) is None: + return { + "status": 1, + "message": "Forbidden access.", + "error_code": 403, + "error_message": "Looks like that this member is not found in this guild...", + } + kwargs["member_id"] = context_ids["member_id"] + kwargs["member"] = member + if "role_id" in self.third_parties[name][page][1]["context_ids"]: + if (role := guild.get_role(context_ids["role_id"])) is None: + return { + "status": 1, + "message": "Forbidden access.", + "error_code": 404, + "error_message": "Looks like that this role is not found in this guild...", + } + kwargs["role_id"] = context_ids["role_id"] + kwargs["role"] = role + if "channel_id" in self.third_parties[name][page][1]["context_ids"]: + if (channel := guild.get_channel(context_ids["channel_id"])) is None: + return { + "status": 1, + "message": "Forbidden access.", + "error_code": 404, + "error_message": "Looks like that this channel is not found in this guild...", + } + kwargs["channel_id"] = context_ids["channel_id"] + kwargs["channel"] = channel + for key, value in required_kwargs.items(): + kwargs[key] = value + for key, value in optional_kwargs.items(): + if key not in kwargs: + kwargs[key] = value + kwargs["method"] = method + kwargs["request_url"] = request_url + kwargs["csrf_token"] = tuple(csrf_token) + kwargs["wtf_csrf_secret_key"] = base64.urlsafe_b64decode(wtf_csrf_secret_key) + kwargs["extra_kwargs"] = extra_kwargs + kwargs["data"] = { + "form": ImmutableMultiDict(data["form"]), + "json": ImmutableMultiDict(data["json"]), + } + kwargs["lang_code"] = lang_code or await get_locale_from_guild( + self.bot, guild=kwargs.get("guild") + ) + set_contextual_locale(kwargs["lang_code"].replace("_", "-")) + set_contextual_regional_format(kwargs["lang_code"].replace("_", "-")) + + ( + kwargs["Form"], + kwargs["DpyObjectConverter"], + extra_notifications, + kwargs["get_sorted_channels"], + kwargs["get_sorted_roles"], + ) = await get_form_class(self, third_party_cog=self.third_parties_cogs[name], **kwargs) + kwargs["Pagination"] = Pagination + + result = await self.third_parties[name][page][0](**kwargs) + if "web_content" in result and isinstance(result["web_content"], typing.Dict): + for key, value in result["web_content"].items(): + if isinstance(value, kwargs["Form"]): + result["web_content"][key] = str(value) + elif isinstance(value, Pagination): + result["web_content"][key] = value.to_dict() + setattr(Field, "__init__", INITIAL_INIT_FIELD) + result.setdefault("notifications", []) + result["notifications"].extend(extra_notifications) + return result diff --git a/dashboard/rpc/utils.py b/dashboard/rpc/utils.py new file mode 100644 index 0000000..82c4c6a --- /dev/null +++ b/dashboard/rpc/utils.py @@ -0,0 +1,21 @@ +import typing # isort:skip + +import functools +from inspect import signature + + +def rpc_check(): + def conditional(func): + @functools.wraps(func) + async def rpccheckwrapped(self, *args, **kwargs) -> typing.Dict[str, typing.Any]: + if self.bot.get_cog("Dashboard") is not None and self.bot.is_ready(): + return await func(self, *args, **kwargs) + else: + return {"disconnected": True} + + rpccheckwrapped.__signature__ = signature( + func + ) # Because aiohttp json rpc doesn't accept `*args, **kwargs`. + return rpccheckwrapped + + return conditional diff --git a/dashboard/rpc/webhooks.py b/dashboard/rpc/webhooks.py new file mode 100644 index 0000000..c11a90f --- /dev/null +++ b/dashboard/rpc/webhooks.py @@ -0,0 +1,23 @@ +from redbot.core import commands # isort:skip +from redbot.core.bot import Red # isort:skip +import typing # isort:skip + +from .utils import rpc_check + + +class DashboardRPC_Webhooks: + def __init__(self, cog: commands.Cog) -> None: + self.bot: Red = cog.bot + self.cog: commands.Cog = cog + + self.bot.register_rpc_handler(self.webhook_receive) + + def unload(self) -> None: + self.bot.unregister_rpc_handler(self.webhook_receive) + + @rpc_check() + async def webhook_receive( + self, payload: typing.Dict[str, typing.Any] + ) -> typing.Dict[str, int]: + self.bot.dispatch("webhook_receive", payload) + return {"status": 0} diff --git a/dashboard/utils_version.json b/dashboard/utils_version.json new file mode 100644 index 0000000..bfab002 --- /dev/null +++ b/dashboard/utils_version.json @@ -0,0 +1 @@ +{"needed_utils_version": 7.0} \ No newline at end of file diff --git a/dev/LICENSE b/dev/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/dev/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/dev/README.rst b/dev/README.rst new file mode 100644 index 0000000..8e83d7f --- /dev/null +++ b/dev/README.rst @@ -0,0 +1,130 @@ +.. _dev: +=== +Dev +=== + +This is the cog guide for the ``Dev`` cog. This guide contains the collection of commands which you can use in the cog. +Through this guide, ``[p]`` will always represent your prefix. Replace ``[p]`` with your own prefix when you use these commands in Discord. + +.. note:: + + Ensure that you are up to date by running ``[p]cog update dev``. + If there is something missing, or something that needs improving in this documentation, feel free to create an issue `here `_. + This documentation is generated everytime this cog receives an update. + +--------------- +About this cog: +--------------- + +Various development focused utilities! + +--------- +Commands: +--------- + +Here are all the commands included in this cog (23): + +* ``[p]bypasscooldowns [toggle] [time]`` + Give bot owners the ability to bypass cooldowns. + +* ``[p]debug [code]`` + Evaluate a statement of python code. + +* ``[p]eshell [silent=False] [command]`` + Execute Shell commands. + +* ``[p]eval [body]`` + Execute asynchronous code. + +* ``[p]mock `` + Mock another user invoking a command. + +* ``[p]mockmsg [content]`` + Dispatch a message event as if it were sent by a different user. + +* ``[p]repl`` + Open an interactive REPL. + +* ``[p]replpause [toggle]`` + Pauses/resumes the REPL running in the current channel. + +* ``[p]setdev`` + Commands to configure Dev. + +* ``[p]setdev ansiformatting `` + Use the `ansi` formatting for results. + +* ``[p]setdev autoimports `` + Enable or disable auto imports. + +* ``[p]setdev downloaderalreadyagreed `` + If enabled, Downloader will no longer prompt you to type `I agree` when adding a repo, even after a bot restart. + +* ``[p]setdev getenvironment [show_values=True]`` + Display all Dev environment values. + +* ``[p]setdev modalconfig [confirmation=False]`` + Set all settings for the cog with a Discord Modal. + +* ``[p]setdev outputmode `` + Set the output mode. `repr` is to display the repr of the result. `repr_or_str` is to display in the same way, but a string as a string. `str` is to display the string of the result. + +* ``[p]setdev resetlocals`` + Reset its own locals in evals. + +* ``[p]setdev resetsetting `` + Reset a setting. + +* ``[p]setdev richtracebacks `` + Use `rich` to display tracebacks. + +* ``[p]setdev senddpyobjects `` + If the result is an embed/file/attachment object or an iterable of these, send. + +* ``[p]setdev sendinteractive `` + Send results with `commands.Context.send_interactive`, not a Menu. + +* ``[p]setdev showsettings [with_dev=False]`` + Show all settings for the cog with defaults and values. + +* ``[p]setdev useextendedenvironment `` + Use my own Dev env with useful values. + +* ``[p]setdev uselastlocals `` + Use the last locals for each evals. Locals are only registered for `[p]eval`, but can be used in other commands. + +------------ +Installation +------------ + +If you haven't added my repo before, lets add it first. We'll call it "AAA3A-cogs" here. + +.. code-block:: ini + + [p]repo add AAA3A-cogs https://github.com/AAA3A-AAA3A/AAA3A-cogs + +Now, we can install Dev. + +.. code-block:: ini + + [p]cog install AAA3A-cogs dev + +Once it's installed, it is not loaded by default. Load it by running the following command: + +.. code-block:: ini + + [p]load dev + +---------------- +Further Support: +---------------- + +Check out my docs `here `_. +Mention me in the #support_other-cogs in the `cog support server `_ if you need any help. +Additionally, feel free to open an issue or pull request to this repo. + +-------- +Credits: +-------- + +Thanks to Kreusada for the Python code to automatically generate this documentation! \ No newline at end of file diff --git a/dev/__init__.py b/dev/__init__.py new file mode 100644 index 0000000..3d3e548 --- /dev/null +++ b/dev/__init__.py @@ -0,0 +1,108 @@ +from redbot.core import errors # isort:skip +import importlib +import sys + +try: + import AAA3A_utils +except ModuleNotFoundError: + raise errors.CogLoadError( + "The needed utils to run the cog were not found. Please execute the command `[p]pipinstall git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." + ) +modules = sorted( + [module for module in sys.modules if module.split(".")[0] == "AAA3A_utils"], reverse=True +) +for module in modules: + try: + importlib.reload(sys.modules[module]) + except ModuleNotFoundError: + pass +del AAA3A_utils +# import AAA3A_utils +# import json +# import os +# __version__ = AAA3A_utils.__version__ +# with open(os.path.join(os.path.dirname(__file__), "utils_version.json"), mode="r") as f: +# data = json.load(f) +# needed_utils_version = data["needed_utils_version"] +# if __version__ > needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a higher version than the one supported by this version of the cog. Please update the cogs of the `AAA3A-cogs` repo." +# ) +# elif __version__ < needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a lower version than the one supported by this version of the cog. Please execute the command `[p]pipinstall --upgrade git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." +# ) + +from redbot.core.bot import Red # isort:skip +import asyncio +import builtins +import typing + +import rich +from redbot.core.utils import get_end_user_data_statement +from redbot.core.utils.chat_formatting import humanize_list + +from .dev import Dev +from .env import ctxconsole + +__red_end_user_data_statement__ = get_end_user_data_statement(file=__file__) + +original_sys_displayhook = None +original_rich_get_console = None + + +async def setup_after_ready(bot: Red) -> None: + await bot.wait_until_red_ready() + + def _displayhook(obj: typing.Any) -> None: + if obj is not None: + _console = ctxconsole.get() + builtins._ = None + rich.pretty.pprint(obj, console=_console) + builtins._ = obj + + def _get_console() -> rich.console.Console: + return ctxconsole.get() + + global original_sys_displayhook + original_sys_displayhook = sys.displayhook + sys.displayhook = _displayhook + global original_rich_get_console + original_rich_get_console = rich.get_console + rich.get_console = _get_console + + +async def setup(bot: Red) -> None: + if not bot._cli_flags.dev: + raise errors.CogLoadError("This cog requires the `--dev` CLI flag.") + cog = Dev(bot) + if (core_dev := bot.get_cog("Dev")) is not None: + if (env_extensions := getattr(core_dev, "env_extensions", None)) is not None: + cog.env_extensions = env_extensions + if (source_cache := getattr(core_dev, "source_cache", None)) is not None: + cog.source_cache = source_cache + if (dev_space := getattr(core_dev, "dev_space", None)) is not None: + cog.dev_space = dev_space + if (_last_result := getattr(core_dev, "_last_result", None)) is not None: + cog._last_result = _last_result + # if (sessions := getattr(core_dev, "sessions", None)) is not None: + # cog.sessions = sessions + if sessions := getattr(core_dev, "sessions", None): + s = "s" if len(sessions) > 1 else "" + is_private = bot._connection._private_channels.__contains__ + raise errors.CogLoadError( + f"End your REPL session{s} first: " + + humanize_list( + ["Private channel" if is_private(id) else f"<#{id}>" for id in sessions] + ) + ) + await bot.remove_cog("Dev") + await bot.add_cog(cog) + asyncio.create_task(setup_after_ready(bot)) + + +async def teardown(bot: Red) -> None: + if original_sys_displayhook is not None: + sys.displayhook = original_sys_displayhook + if original_rich_get_console is not None: + rich.get_console = original_rich_get_console diff --git a/dev/dashboard_integration.py b/dev/dashboard_integration.py new file mode 100644 index 0000000..443e378 --- /dev/null +++ b/dev/dashboard_integration.py @@ -0,0 +1,12 @@ +from redbot.core import commands # isort:skip +from redbot.core.bot import Red # isort:skip + + +class DashboardIntegration: + bot: Red + + @commands.Cog.listener() + async def on_dashboard_cog_add(self, dashboard_cog: commands.Cog) -> None: + if hasattr(self, "settings") and hasattr(self.settings, "commands_added"): + await self.settings.commands_added.wait() + dashboard_cog.rpc.third_parties_handler.add_third_party(self) diff --git a/dev/dev.py b/dev/dev.py new file mode 100644 index 0000000..79e9f5d --- /dev/null +++ b/dev/dev.py @@ -0,0 +1,1123 @@ +from AAA3A_utils import Cog, Menu, Settings, CogsUtils # isort:skip +from redbot.core import commands, app_commands, Config # isort:skip +from redbot.core.bot import Red # isort:skip +from redbot.core.i18n import Translator, cog_i18n # isort:skip +import discord # isort:skip +import typing # isort:skip + +import ast +import asyncio +import collections +import contextlib +import io +import random +import re +import subprocess +import sys +import textwrap + +import aiohttp +import rich +from pygments.styles import get_style_by_name +from redbot.core import dev_commands +from redbot.core.utils.chat_formatting import box +from redbot.core.utils.predicates import MessagePredicate + +from .dashboard_integration import DashboardIntegration +from .env import DevEnv, DevSpace, Exit, ctxconsole +from .view import ExecuteView, cleanup_code + +# Credits: +# General repo credits. +# Thanks to Cogs-Creators for the original Dev cog! +# Thanks to Zeph for many ideas and a big part of the code (code removed from public)! + +_: Translator = Translator("Dev", __file__) + +TimeConverter: commands.converter.TimedeltaConverter = commands.converter.TimedeltaConverter( + minimum=None, + maximum=None, + allowed_units=None, + default_unit="minutes", +) + + +class SolarizedCustom(get_style_by_name("solarized-dark")): + background_color = None + line_number_background_color = None + + +@contextlib.contextmanager +def redirect(**kwargs): + if "file" not in kwargs: + kwargs["file"] = file = io.StringIO() + else: + file = None + console = rich.console.Console(**kwargs) + token = ctxconsole.set(console) + try: + yield console + finally: + ctxconsole.reset(token) + if file: + file.close() + + +class DevOutput(dev_commands.DevOutput): + def __init__(self, *args, **kwargs) -> None: + self._locals: typing.Dict[str, typing.Any] = kwargs.pop("_locals", {}) + self.prints: str = "" + self.rich_tracebacks: bool = kwargs.pop("rich_tracebacks", False) + self.exc: typing.Optional[Exception] = None + super().__init__(*args, **kwargs) + + def __str__(self, output_mode: typing.Literal["repr", "repr_or_str", "str"] = "repr") -> str: + _console_custom_kwargs: typing.Dict[str, typing.Any] = self.env.get( + "_console_custom", + { + "width": 80, + "no_color": True, + "color_system": None, + "tab_size": 2, + "soft_wrap": False, + }, + ) + with redirect(**_console_custom_kwargs) as console: + with console.capture() as captured: + if formatted_imports := self.env.get_formatted_imports(): + console.print( + rich.syntax.Syntax(formatted_imports, "pycon", theme=SolarizedCustom) + ) + if self.prints: + console.print(self.prints) + if printed := self._stream.getvalue(): + console.print(printed.strip()) + if self.formatted_exc: + console.print(self.formatted_exc.strip()) + elif ( + self.result is not None + or self.always_include_result + # and not self.prints + # and not formatted_imports + # and not printed + ): + if output_mode == "str": + result = str(self.result) + elif ( + isinstance(self.result, collections.abc.Iterable) + and not (output_mode == "repr" and isinstance(self.result, str)) + or hasattr(self.result, "__dataclass_fields__") + ): + result = self.result + else: + result = repr(self.result) + try: + console.print(result) + except Exception as exc: + console.print(self.format_exception(exc).strip()) + output = captured.get().strip() + return CogsUtils.replace_var_paths(dev_commands.sanitize_output(self.ctx, output)).replace( + "```", "\u02CB\u02CB\u02CB" + ) + + async def send( + self, + *, + tick: bool = True, + output_mode: typing.Literal["repr", "repr_or_str", "str"] = "repr", + ansi_formatting: bool = False, + send_interactive: bool = False, + send_dpy_objects: bool = True, + wait: bool = True, + ) -> None: + if send_dpy_objects and self.result is not None: + kwargs = {} + channel_permissions = self.ctx.channel.permissions_for(self.ctx.me) + if isinstance(self.result, discord.Embed) and channel_permissions.embed_links: + kwargs["embed"] = self.result + elif isinstance(self.result, discord.File) and channel_permissions.attach_files: + kwargs["file"] = self.result + elif isinstance(self.result, discord.abc.Iterable): + kwargs = {"embeds": [], "files": []} + for element in self.result: + if isinstance(element, discord.Embed) and channel_permissions.embed_links: + if ( + len(kwargs["embeds"]) < 10 + and (sum(len(embed) for embed in kwargs["embeds"]) + len(element)) + <= 6000 + ): + kwargs["embeds"].append(element) + elif isinstance(element, discord.File) and channel_permissions.attach_files: + if (sum(len(file) for file in kwargs["files"]) + len(element)) <= 6000: + kwargs["files"].append(element) + for key in ("embeds", "files"): + if not kwargs[key]: + del kwargs[key] + if kwargs: + try: + await Menu(pages=[kwargs]).start(self.ctx, wait=False) + except discord.HTTPException: + pass + if tick and self.exc is not None: + await self.ctx.react_quietly( + reaction=( + "❗" + if isinstance(self.exc, SyntaxError) + else ( + "⏰" + if isinstance( + self.exc, + ( + TimeoutError, + asyncio.TimeoutError, + aiohttp.ClientTimeout, + aiohttp.ServerTimeoutError, + subprocess.TimeoutExpired, + ), + ) + else "❌" + ) + ) + ) + box_lang = ( + "ini" if self.ctx.command.name == "eshell" else ("ansi" if ansi_formatting else "py") + ) + if send_interactive: + task = self.ctx.send_interactive( + [ + box(page, lang=box_lang) + for page in dev_commands.get_pages( + ( + f"{self.env['prefix_dev_output']}\n\n" + if "prefix_dev_output" in self.env + else None + ) + + self.__str__(output_mode=output_mode) + ) + ], + ) + if wait: + await task + else: + await asyncio.create_task(task) + elif pages := self.__str__(output_mode=output_mode): + await Menu( + pages=pages, + prefix=self.env.get("prefix_dev_output"), + lang=box_lang, + ).start( + self.ctx, + wait=wait, + ) + if tick and self.exc is None: + await self.ctx.react_quietly( + # sourcery skip: swap-if-expression + reaction=( + commands.context.TICK + if not hasattr(commands.context, "MORE_TICKS") + else random.choice(list(commands.context.MORE_TICKS)) + ) + ) + + @classmethod + async def from_debug( + cls, + ctx: commands.Context, + *, + source: str, + source_cache: dev_commands.SourceCache, + env: typing.Dict[str, typing.Any], + **kwargs, + ) -> "DevOutput": + output = cls( + ctx, + source=source, + source_cache=source_cache, + filename=f"", + env=env, + **kwargs, + ) + await output.run_debug() + return output + + @classmethod + async def from_eval( + cls, + ctx: commands.Context, + *, + source: str, + source_cache: dev_commands.SourceCache, + env: typing.Dict[str, typing.Any], + **kwargs, + ) -> "DevOutput": + output = cls( + ctx, + source=source, + source_cache=source_cache, + filename=f"", + env=env, + **kwargs, + ) + await output.run_eval() + return output + + @classmethod + async def from_repl( + cls, + ctx: commands.Context, + *, + source: str, + source_cache: dev_commands.SourceCache, + env: typing.Dict[str, typing.Any], + **kwargs, + ) -> "DevOutput": + output = cls( + ctx, + source=source, + source_cache=source_cache, + filename=f"", + env=env, + **kwargs, + ) + await output.run_repl() + return output + + async def run_debug(self) -> None: + async def add_triangle_reaction_after_1_seconds(): + await asyncio.sleep(2) + try: + await self.ctx.message.add_reaction("▶") + except discord.HTTPException: + pass + + task = asyncio.create_task(add_triangle_reaction_after_1_seconds()) + + self.env.update({"dev_output": self}) + self.env.update(**self._locals) + _console_custom_kwargs: typing.Dict[str, typing.Any] = self.env.get( + "_console_custom", + { + "width": 80, + "no_color": True, + "color_system": None, + "tab_size": 2, + "soft_wrap": False, + }, + ).copy() + _console_custom_kwargs["color_system"] = None + with redirect(**_console_custom_kwargs) as console: + with console.capture() as captured: + try: + await super().run_debug() + except Exit: # Not a real exception... + pass + except (SystemExit, KeyboardInterrupt): + raise + except BaseException as exc: + self.set_exception(exc) + self.always_include_result: bool = False + self.prints: str = captured.get().strip() + + task.cancel() + + async def run_eval(self) -> None: + async def add_triangle_reaction_after_1_seconds(): + await asyncio.sleep(2) + try: + await self.ctx.message.add_reaction("▶") + except discord.HTTPException: + pass + + task = asyncio.create_task(add_triangle_reaction_after_1_seconds()) + + self.env.update({"dev_output": self}) + try: + parse = ast.parse("async def func():\n%s" % textwrap.indent(self.raw_source, " ")) + try: + return_found = [d for d in parse.body[0].body if isinstance(d, ast.Return)][0] + except IndexError: + line = len(self.raw_source.split("\n")) + else: + line = return_found.lineno - 2 + _raw_source = self.raw_source.split("\n") + _raw_source.insert(line, textwrap.indent("dev_output._locals.update(**locals())", "")) + # `yield` like in Jishaku. + for line, line_text in enumerate(_raw_source.copy()): + _line_text = textwrap.dedent(line_text) + if _line_text.startswith("yield "): + _raw_source[line] = textwrap.indent( + f"print(repr(({_line_text[6:]})))", + (len(line_text) - len(_line_text)) * " ", + ) + self.raw_source = "\n".join(_raw_source) + except SyntaxError: + pass + self.env.update(**self._locals) + _console_custom_kwargs: typing.Dict[str, typing.Any] = self.env.get( + "_console_custom", + { + "width": 80, + "no_color": True, + "color_system": None, + "tab_size": 2, + "soft_wrap": False, + }, + ).copy() + _console_custom_kwargs["color_system"] = None + with redirect(**_console_custom_kwargs) as console: + with console.capture() as captured: + try: + await super().run_eval() + except Exit: # Not a real exception... + pass + except (SystemExit, KeyboardInterrupt): + raise + except BaseException as exc: + self.set_exception(exc) + self.prints: str = captured.get().strip() + + task.cancel() + + async def run_repl(self) -> None: + async def add_triangle_reaction_after_1_seconds(): + await asyncio.sleep(2) + try: + await self.ctx.message.add_reaction("▶") + except discord.HTTPException: + pass + + task = asyncio.create_task(add_triangle_reaction_after_1_seconds()) + + self.env.update({"dev_output": self}) + self.env.update(**self._locals) + _console_custom_kwargs: typing.Dict[str, typing.Any] = self.env.get( + "_console_custom", + { + "width": 80, + "no_color": True, + "color_system": None, + "tab_size": 2, + "soft_wrap": False, + }, + ).copy() + _console_custom_kwargs["color_system"] = None + with redirect(**_console_custom_kwargs) as console: + with console.capture() as captured: + try: + await super().run_repl() + except (Exit, SystemExit, KeyboardInterrupt): # `Exit` isn't a real exception... + raise + except BaseException as exc: + self.set_exception(exc) + self.prints: str = captured.get().strip() + + task.cancel() + + def set_exception(self, exc: Exception, *, skip_frames: int = 1) -> None: + self.exc: Exception = exc + self.formatted_exc: str = self.format_exception(exc, skip_frames=skip_frames) + + def format_exception(self, exc: Exception, *, skip_frames: int = 1) -> str: + if not self.rich_tracebacks: + return super().format_exception(exc=exc, skip_frames=skip_frames) + _console_custom_kwargs: typing.Dict[str, typing.Any] = self.env.get( + "_console_custom", + { + "width": 80, + "no_color": True, + "color_system": None, + "tab_size": 2, + "soft_wrap": False, + }, + ).copy() + _console_custom_kwargs["color_system"] = None + with redirect(**_console_custom_kwargs) as console: + with console.capture() as captured: + tb = exc.__traceback__ + for _ in range(skip_frames): + if tb is None: + break + tb = tb.tb_next + # sometimes SyntaxError.text is None, sometimes it isn't + if issubclass(type(exc), SyntaxError) and exc.lineno is not None: + try: + source_lines, line_offset = self.source_cache[exc.filename] + except KeyError: + pass + else: + if exc.text is None: + try: + # line numbers are 1-based, the list indexes are 0-based + exc.text = source_lines[exc.lineno - 1] + except IndexError: + # the frame might be pointing at a different source code, ignore... + pass + else: + exc.lineno -= line_offset + if sys.version_info >= (3, 10) and exc.end_lineno is not None: + exc.end_lineno -= line_offset + else: + exc.lineno -= line_offset + if sys.version_info >= (3, 10) and exc.end_lineno is not None: + exc.end_lineno -= line_offset + rich_tb = rich.traceback.Traceback.from_exception( + type(exc), exc, tb, extra_lines=0, theme=SolarizedCustom + ) + console.print(rich_tb) + return captured.get().strip() + + +@cog_i18n(_) +class Dev(DashboardIntegration, Cog, dev_commands.Dev): + """Various development focused utilities!""" + + __authors__: typing.List[str] = ["Cog-Creators", "Zephyrkul (Zephyrkul#1089)", "AAA3A"] + + def __init__(self, bot: Red) -> None: + super().__init__(bot=bot) + + self.env_extensions: typing.Dict[str, typing.Any] = {} + self.source_cache: dev_commands.SourceCache = dev_commands.SourceCache() + self._session: aiohttp.ClientSession = None + self.dev_space: DevSpace = DevSpace() + + self._last_result: typing.Optional[typing.Any] = None + self._last_locals: typing.Dict[ + typing.Union[discord.Member, discord.User], typing.Dict[str, typing.Any] + ] = {} + self.dev_outputs: typing.Dict[discord.Message, DevOutput] = {} + self.sessions: typing.Dict[int, bool] = {} + self._repl_tasks: typing.List[asyncio.Task] = [] + self._bypass_cooldowns_task: typing.Optional[asyncio.Task] = None + + self.config: Config = Config.get_conf( + self, + identifier=205192943327321000143939875896557571750, + force_registration=True, + ) + self.dev_global: typing.Dict[ + str, typing.Union[typing.Literal["repr", "repr_or_str", "str"], bool] + ] = { + "auto_imports": True, + "output_mode": "repr", + "rich_tracebacks": False, + "ansi_formatting": False, + "send_interactive": False, + "send_dpy_objects": True, + "use_last_locals": True, + "downloader_already_agreed": False, + "use_extended_environment": True, + } + self.config.register_global( + auto_imports=True, + output_mode="repr", + rich_tracebacks=False, + ansi_formatting=False, + send_interactive=False, + send_dpy_objects=True, + use_last_locals=True, + downloader_already_agreed=False, + use_extended_environment=True, + ) + + _settings: typing.Dict[ + str, typing.Dict[str, typing.Union[typing.List[str], bool, str]] + ] = { + "auto_imports": { + "converter": bool, + "description": "Enable or disable auto imports.", + }, + "output_mode": { + "converter": typing.Literal["repr", "repr_or_str", "str"], + "description": "Set the output mode. `repr` is to display the repr of the result. `repr_or_str` is to display in the same way, but a string as a string. `str` is to display the string of the result.", + }, + "rich_tracebacks": { + "converter": bool, + "description": "Use `rich` to display tracebacks.", + }, + "ansi_formatting": { + "converter": bool, + "description": "Use the `ansi` formatting for results.", + }, + "send_interactive": { + "converter": bool, + "description": "Send results with `commands.Context.send_interactive`, not a Menu.", + }, + "send_dpy_objects": { + "converter": bool, + "description": "If the result is an embed/file/attachment object or an iterable of these, send.", + }, + "use_last_locals": { + "converter": bool, + "description": "Use the last locals for each evals. Locals are only registered for `[p]eval`, but can be used in other commands.", + }, + "downloader_already_agreed": { + "converter": bool, + "description": "If enabled, Downloader will no longer prompt you to type `I agree` when adding a repo, even after a bot restart.", + }, + "use_extended_environment": { + "converter": bool, + "description": "Use my own Dev env with useful values.", + }, + } + self.settings: Settings = Settings( + bot=self.bot, + cog=self, + config=self.config, + group=self.config.GLOBAL, + settings=_settings, + global_path=[], + use_profiles_system=False, + can_edit=True, + commands_group=self.configuration, + ) + + async def cog_load(self) -> None: + await super().cog_load() + await self.settings.add_commands() + if ( + await self.config.downloader_already_agreed() + and (downloader_cog := self.bot.get_cog("Downloader")) is not None + ): + downloader_cog.already_agreed = True + self._session: aiohttp.ClientSession = aiohttp.ClientSession() + + async def cog_unload(self) -> None: + if self._session is not None: + await self._session.close() + core_dev: dev_commands.Dev = dev_commands.Dev() + core_dev.env_extensions: typing.Dict[str, typing.Any] = self.env_extensions + core_dev.source_cache: dev_commands.SourceCache = self.source_cache + core_dev.dev_space: DevSpace = self.dev_space + core_dev._last_result: typing.Optional[typing.Any] = self._last_result + # core_dev.sessions: typing.Dict[int, bool] = self.sessions + for task in self._repl_tasks: + task.cancel() + await self.bot.add_cog(core_dev) + await super().cog_unload() + + def get_environment( + self, ctx: commands.Context, use_extended_environment: bool = True + ) -> DevEnv: + return DevEnv.get_environment(ctx, use_extended_environment=use_extended_environment) + + async def my_exec( + self, + ctx: commands.Context, + type: typing.Literal["debug", "eval", "repl"], + source: str, + env: typing.Optional[typing.Dict[str, typing.Any]] = None, + send_result: bool = False, + wait: bool = True, + ) -> bool: + tasks: typing.List[asyncio.Task] = [ + asyncio.create_task( + ctx.bot.wait_for("message", check=MessagePredicate.cancelled(ctx)) + ), + asyncio.create_task( + self._my_exec( + ctx, type=type, source=source, env=env, send_result=send_result, wait=wait + ) + ), + ] + done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) + for task in pending: + task.cancel() + result = done.pop().result() + return result if result is not None else None + + async def _my_exec( + self, + ctx: commands.Context, + type: typing.Literal["debug", "eval", "repl"], + source: str, + env: typing.Optional[typing.Dict[str, typing.Any]] = None, + send_result: bool = False, + wait: bool = True, + ) -> DevOutput: + source = cleanup_code(source) + if env is None: + env = self.get_environment( + ctx, use_extended_environment=await self.config.use_extended_environment() + ) + env["auto_imports"] = await self.config.auto_imports() + if ( + isinstance(ctx.author, (discord.Member, discord.User)) + and ctx.author in self._last_locals + and await self.config.use_last_locals() + ): + _locals = self._last_locals[ctx.author] + else: + _locals = {} + types = { + "debug": DevOutput.from_debug, + "eval": DevOutput.from_eval, + "repl": DevOutput.from_repl, + } + mobile = ctx.author.is_on_mobile() if isinstance(ctx.author, discord.Member) else False + if await self.config.ansi_formatting(): + _console_custom_kwargs: typing.Dict[str, typing.Any] = { + "width": 37 if mobile else 80, + "no_color": mobile, + "color_system": None if mobile else "standard", + "tab_size": 2, + "soft_wrap": False, + } + else: + _console_custom_kwargs: typing.Dict[str, typing.Any] = { + "width": 80, + "no_color": True, + "color_system": None, + "tab_size": 2, + "soft_wrap": False, + } + if _console_custom := env.get("_console_custom"): + _console_custom_kwargs.update(_console_custom) + env["_console_custom"] = _console_custom_kwargs + output: DevOutput = await types[type]( + ctx, + source=source, + source_cache=self.source_cache, + env=env, + rich_tracebacks=await self.config.rich_tracebacks(), + _locals=_locals, + ) + self._last_result = output.result + self.dev_outputs[ctx.message] = output + if ( + type == "eval" + and isinstance(ctx.author, (discord.Member, discord.User)) + and output._locals + ): + if ctx.author not in self._last_locals: + self._last_locals[ctx.author] = {} + self._last_locals[ctx.author].update(**output._locals) + if send_result: + send_interactive = await self.config.send_interactive() + send_coroutine = output.send( + tick=type != "repl", + output_mode=await self.config.output_mode(), + ansi_formatting=await self.config.ansi_formatting(), + send_interactive=send_interactive, + send_dpy_objects=await self.config.send_dpy_objects(), + wait=wait, + ) + if wait and not send_coroutine: + await send_coroutine + else: + asyncio.create_task(send_coroutine) + return output + + @commands.is_owner() + @commands.hybrid_command() + @app_commands.allowed_installs(guilds=True, users=True) + # @discord.utils.copy_doc(dev_commands.Dev.debug.callback) + async def debug(self, ctx: commands.Context, *, code: str = None) -> None: + """Evaluate a statement of python code. + + The bot will always respond with the return value of the code. + If the return value of the code is a coroutine, it will be awaited, + and the result of that will be the bot's response. + + Note: Only one statement may be evaluated. Using certain restricted + keywords, e.g. yield, will result in a syntax error. For multiple + lines or asynchronous code, see [p]repl or [p]eval. + + The code can be within a codeblock, inline code or neither, as long as they are not mixed and they are formatted correctly. + You can upload a file with the code to be executed, or reply to a message containing the command, from any bot. + + Environment Variables: + `ctx` - the command invocation context + `bot` - the bot object + `channel` - the current channel object + `author` - the command author's member object + `guild` - the current guild object + `message` - the command's message object + `_` - the result of the last dev command + `aiohttp` - the aiohttp library + `asyncio` - the asyncio library + `discord` - the discord.py library + `commands` - the redbot.core.commands module + `cf` - the redbot.core.utils.chat_formatting module + (See `[p]setdev getenvironment` for more.) + """ + if code is None: + if ctx.message.attachments: + try: + code = (await ctx.message.attachments[0].read()).decode(encoding="utf-8") + except UnicodeDecodeError: + raise commands.UserFeedbackCheckFailure( + _("Unreadable attachment with `utf-8`.") + ) + elif ctx.message.reference is not None and isinstance( + (reference := ctx.message.reference.resolved), discord.Message + ): + if ( + match := re.compile( + r"(debug|(jsk|jishaku) (py|python|eval|ev))(\n)?( )?(?P(.|\n)*)" + ).search(reference.content) + ) is not None and match.groupdict()["code"].strip(): + code = match.groupdict()["code"] + elif ( + re.compile(r"```py\n(.|\n)*\n```").match(reference.content) + and reference.content.count("```") == 2 + ): + code = reference.content + else: + raise commands.UserFeedbackCheckFailure(_("This message isn't reachable.")) + else: + return asyncio.create_task(ExecuteView(cog=self).start(ctx)) + source = cleanup_code(code) + await self.my_exec( + getattr(ctx, "original_context", ctx), + type="debug", + source=source, + send_result=True, + ) + + @commands.is_owner() + @commands.hybrid_command(name="eval") + @app_commands.allowed_installs(guilds=True, users=True) + # @discord.utils.copy_doc(dev_commands.Dev._eval.callback) + async def _eval(self, ctx: commands.Context, *, body: str = None) -> None: + """Execute asynchronous code. + + This command wraps code into the body of an async function and then + calls and awaits it. The bot will respond with anything printed to + stdout, as well as the return value of the function. + + The code can be within a codeblock, inline code or neither, as long as they are not mixed and they are formatted correctly. + You can upload a file with the code to be executed, or reply to a message containing the command, from any bot. + + Environment Variables: + `ctx` - the command invocation context + `bot` - the bot object + `channel` - the current channel object + `author` - the command author's member object + `guild` - the current guild object + `message` - the command's message object + `_` - the result of the last dev command + `aiohttp` - the aiohttp library + `asyncio` - the asyncio library + `discord` - the discord.py library + `commands` - the redbot.core.commands module + `cf` - the redbot.core.utils.chat_formatting module + (See `[p]setdev getenvironment` for more.) + """ + if body is None: + if ctx.message.attachments: + try: + body = (await ctx.message.attachments[0].read()).decode(encoding="utf-8") + except UnicodeDecodeError: + raise commands.UserFeedbackCheckFailure( + _("Unreadable attachment with `utf-8`.") + ) + elif ctx.message.reference is not None and isinstance( + (reference := ctx.message.reference.resolved), discord.Message + ): + if ( + match := re.compile( + r"(eval|ev|e|(jsk|jishaku) (py|python|eval|ev)|(runcode|executecode) (py|python))(\n)?( )?(?P(.|\n)*)" + ).search(reference.content) + ) is not None and match.groupdict()["body"].strip(): + body = match.groupdict()["body"] + elif ( + re.compile(r"```py\n(.|\n)*\n```").match(reference.content) + and reference.content.count("```") == 2 + ): + body = reference.content + else: + raise commands.UserInputError() + else: + return asyncio.create_task(ExecuteView(cog=self).start(ctx)) + source = cleanup_code(body) + await self.my_exec( + getattr(ctx, "original_context", ctx), + type="eval", + source=source, + send_result=True, + ) + + @commands.is_owner() + @commands.hybrid_command() + # @discord.utils.copy_doc(dev_commands.Dev.repl.callback) + async def repl(self, ctx: commands.Context) -> None: + """Open an interactive REPL. + + The REPL will only recognise code as messages which start with a + backtick. This includes codeblocks, and as such multiple lines can be + evaluated. + + Use `exit()` or `quit` to exit the REPL session, prefixed with + a backtick so they may be interpreted. + + You can upload a file with the code to be executed, or reply to a message containing the same command, for any bot. + + Environment Variables: + `ctx` - the command invocation context + `bot` - the bot object + `channel` - the current channel object + `author` - the command author's member object + `guild` - the current guild object + `message` - the command's message object + `_` - the result of the last dev command + `aiohttp` - the aiohttp library + `asyncio` - the asyncio library + `discord` - the discord.py library + `commands` - the redbot.core.commands module + `cf` - the redbot.core.utils.chat_formatting module + (See `[p]setdev getenvironment` for more.) + """ + if ctx.channel.id in self.sessions: + if self.sessions[ctx.channel.id]: + await ctx.send( + _("Already running a REPL session in this channel. Exit it with `quit`.") + ) + else: + await ctx.send( + _( + "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." + ).format(prefix=ctx.prefix) + ) + return + + env = self.get_environment( + ctx, use_extended_environment=await self.config.use_extended_environment() + ) + env["_"] = None + self.sessions[ctx.channel.id] = True + await ctx.send( + _( + "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." + ).format(prefix=ctx.prefix) + ) + + while True: + task = asyncio.create_task( + ctx.bot.wait_for("message", check=MessagePredicate.regex(r"^`", ctx)) + ) + self._repl_tasks.append(task) + try: + response = await task + except asyncio.CancelledError: + return + finally: + self._repl_tasks.remove(task) + if not self.sessions[ctx.channel.id]: + continue + env["message"] = response + source = cleanup_code(response.content) + try: + # if source in ("quit", "exit", "exit()"): + # raise Exit() + output = await self._my_exec( + getattr(ctx, "original_context", ctx), + type="repl", + source=source, + env=env, + wait=False, + send_result=True, + ) + except Exit: + break + try: + if output.formatted_exc: + await response.add_reaction("❌") + elif not str(output): + await response.add_reaction( + commands.context.TICK + if not hasattr(commands.context, "MORE_TICKS") + else random.choice(list(commands.context.MORE_TICKS)) + ) + except discord.HTTPException: + pass + + await ctx.send(_("Exiting.")) + del self.sessions[ctx.channel.id] + + @commands.is_owner() + @commands.hybrid_command(name="replpause", aliases=["replresume"]) + async def pause(self, ctx: commands.Context, toggle: bool = None) -> None: + """Pauses/resumes the REPL running in the current channel.""" + if ctx.channel.id not in self.sessions: + await ctx.send(_("There is no currently running REPL session in this channel.")) + return + if toggle is None: + toggle = not self.sessions[ctx.channel.id] + self.sessions[ctx.channel.id] = toggle + if toggle: + await ctx.send(_("The REPL session in this channel has been resumed.")) + else: + await ctx.send(_("The REPL session in this channel is now paused.")) + + @commands.is_owner() + @commands.hybrid_command() + async def bypasscooldowns( + self, + ctx: commands.Context, + toggle: typing.Optional[bool] = None, + *, + time: TimeConverter = None, + ) -> None: + """Give bot owners the ability to bypass cooldowns. + + Does not persist through restarts. + """ + if toggle is None: + toggle = not ctx.bot._bypass_cooldowns + if self._bypass_cooldowns_task is not None: + self._bypass_cooldowns_task.cancel() + ctx.bot._bypass_cooldowns = toggle + if toggle: + await ctx.send( + _( + "Bot owners will now bypass all commands with cooldowns{optional_duration}." + ).format( + optional_duration=( + "" if time is None else f" for {CogsUtils.get_interval_string(time)}" + ) + ) + ) + else: + await ctx.send( + _( + "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." + ).format( + optional_duration=( + "" if time is None else f" for {CogsUtils.get_interval_string(time)}" + ) + ) + ) + if time is not None: + task = asyncio.create_task(asyncio.sleep(time.total_seconds())) + self._bypass_cooldowns_task: asyncio.Task = task + try: + await task + except asyncio.CancelledError: + return + finally: + self._bypass_cooldowns_task = None + ctx.bot._bypass_cooldowns = not toggle + + @commands.is_owner() + @commands.hybrid_command(name="eshell") + @app_commands.allowed_installs(guilds=True, users=True) + async def _eshell( + self, ctx: commands.Context, silent: typing.Optional[bool] = False, *, command: str = None + ) -> None: + """Execute Shell commands. + + This command wraps the shell command into a Python code to invoke them. + + The code can be within a codeblock, inline code or neither, as long as they are not mixed and they are formatted correctly. + You can upload a file with the code to be executed, or reply to a message containing the command, from any bot. + """ + if command is None: + if ctx.message.attachments: + try: + command = (await ctx.message.attachments[0].read()).decode(encoding="utf-8") + except UnicodeDecodeError: + raise commands.UserFeedbackCheckFailure( + _("Unreadable attachment with `utf-8`.") + ) + elif ctx.message.reference is not None and isinstance( + (reference := ctx.message.reference.resolved), discord.Message + ): + if ( + match := re.compile( + r"(eshell|shell|qshell)(\n)?( )?(?P(.|\n)*)" + ).search(reference.content) + ) is not None and match.groupdict()["command"].strip(): + command = match.groupdict()["command"] + elif ( + re.compile(r"```py\n(.|\n)*\n```").match(reference.content) + and reference.content.count("```") == 2 + ): + command = reference.content + else: + raise commands.UserInputError() + else: + raise commands.UserInputError() + command = cleanup_code(command) + + # Thanks Jack for a part of this code! + source = ( + cleanup_code( + """ + import asyncio + import asyncio.subprocess as asp + import os + import sys + import typing + + command = ''' + COMMAND + '''.strip() + + def get_env() -> typing.Dict[str, str]: + env = os.environ.copy() + if hasattr(sys, "real_prefix") or sys.base_prefix != sys.prefix: + if sys.platform == "win32": + binfolder = f"{sys.prefix}{os.path.sep}Scripts" + env["PATH"] = f"{binfolder}{os.pathsep}{env['PATH']}" + else: + binfolder = f"{sys.prefix}{os.path.sep}bin" + env["PATH"] = f"{binfolder}{os.pathsep}{env['PATH']}" + return env + + process = await asp.create_subprocess_shell( + command, + stdout=asp.PIPE, + stderr=asp.STDOUT, + env=get_env(), + executable=None, + ) + try: + await process.wait() + except asyncio.CancelledError: + prefix = f"Command was terminated early and this is a partial output:\\n\\n" + # raise + else: + prefix = "" + finally: + lines = [line async for line in process.stdout] + print(prefix + b"".join(lines).decode("utf-8", "replace").strip().replace("\\r", "")) + """ + ) + .strip() + .replace("COMMAND", command) + ) + if silent: + source = "\n".join(source.split("\n")[:-3]) + + await self.my_exec( + getattr(ctx, "original_context", ctx), + type="eval", + source=source, + send_result=True, + ) + + @commands.is_owner() + @commands.hybrid_group(name="setdev") + async def configuration(self, ctx: commands.Context) -> None: + """ + Commands to configure Dev. + """ + pass + + @configuration.command(aliases=["getenv", "getformattedenvironment", "getformattedenv"]) + async def getenvironment(self, ctx: commands.Context, show_values: bool = True) -> None: + """Display all Dev environment values.""" + env = self.get_environment( + ctx, use_extended_environment=await self.config.use_extended_environment() + ) + formatted_env = env.get_formatted_env(show_values=show_values) + await Menu(pages=formatted_env, lang="py").start(ctx) + + @configuration.command(aliases=["rlocals"]) + async def resetlocals(self, ctx: commands.Context) -> None: + """Reset its own locals in evals.""" + try: + del self._last_locals[ctx.author] + except ValueError: + pass diff --git a/dev/env.py b/dev/env.py new file mode 100644 index 0000000..aba0fa3 --- /dev/null +++ b/dev/env.py @@ -0,0 +1,996 @@ +import asyncio +import builtins +import collections +import datetime +import functools +import importlib +import inspect +import logging +import os +import re +import sys +import textwrap +import time +import traceback +import types +from contextvars import ContextVar +from functools import partial +from io import BytesIO, StringIO + +import aiohttp +import discord.ext +import redbot +import rich +from AAA3A_utils.cog import Cog +from AAA3A_utils.cogsutils import CogsUtils +from AAA3A_utils.context import Context, is_dev +from AAA3A_utils.loop import Loop +from AAA3A_utils.menus import Menu, Reactions +from AAA3A_utils.sentry import SentryHelper +from AAA3A_utils.settings import Settings +from AAA3A_utils.shared_cog import SharedCog +from AAA3A_utils.views import ( + Buttons, + ChannelSelect, + ConfirmationAskView, + Dropdown, + MentionableSelect, + Modal, + RoleSelect, + Select, + UserSelect, +) # NOQA +from redbot.core import Config, dev_commands +from redbot.core import utils as redutils +from redbot.core.utils import chat_formatting as cf +from redbot.core.utils.chat_formatting import box +from rich.console import Console +from rich.table import Table + +from redbot.core import commands # isort:skip +from redbot.core.bot import Red # isort:skip +import discord # isort:skip +import typing # isort:skip + + +ctxconsole = ContextVar[rich.console.Console]("ctxconsole") + + +class Exit(BaseException): + pass + + +def no_colour_rich_markup( + *objects: typing.Any, lang: str = "", no_box: typing.Optional[bool] = False +) -> str: + """ + Slimmed down version of rich_markup which ensure no colours (/ANSI) can exist + https://github.com/Cog-Creators/Red-DiscordBot/pull/5538/files (Kowlin) + """ + temp_console = Console( # Prevent messing with STDOUT's console + color_system=None, + file=StringIO(), + force_terminal=True, + width=80, + ) + temp_console.print(*objects) + if no_box: + return temp_console.file.getvalue() + return box(temp_console.file.getvalue(), lang=lang) # type: ignore + + +class DevSpace: + def __init__(self, **kwargs) -> None: + self.__dict__.update(**kwargs) + + def __repr__(self) -> str: + items = [f"{k}={v!r}" for k, v in self.__dict__.items()] + return ( + f"<{self.__class__.__name__} {' '.join(items)}>" + if items + else f"<{self.__class__.__name__} [Nothing]>" + ) + + def __eq__(self, other: object) -> bool: + if isinstance(self, self.__class__) and isinstance(other, self.__class__): + return self.__dict__ == other.__dict__ + return NotImplemented + + def __len__(self) -> int: + return len(self.__dict__) + + def __contains__(self, key: str) -> typing.Any: + return key in self.__dict__ + + def __iter__(self) -> typing.Iterator[typing.Tuple[str, typing.Any]]: + yield from self.__dict__.items() + + def __reversed__(self) -> typing.Dict: + return self.__dict__.__reversed__() + + def __getattr__(self, attr: str) -> typing.Any: + raise AttributeError(attr) + + def __setattr__(self, attr: str, value: typing.Any) -> None: + self.__dict__[attr] = value + + def __delattr__(self, attr: str) -> None: + del self.__dict__[attr] + + def __getitem__(self, key: str) -> typing.Any: + return self.__dict__[key] + + def __setitem__(self, key: str, value: typing.Any) -> None: + self.__dict__[key] = value + + def __delitem__(self, key: str) -> None: + del self.__dict__[key] + + def clear(self) -> None: + self.__dict__.clear() + + def update(self, **kwargs) -> None: + self.__dict__.update(**kwargs) + + def copy(self) -> typing.Any: # typing_extensions.Self + return self.__class__(**self.__dict__) + + def items(self): + return self.__dict__.items() + + def keys(self): + return self.__dict__.keys() + + def values(self): + return self.__dict__.values() + + def get(self, key: str, _default: typing.Optional[typing.Any] = None) -> typing.Any: + return self.__dict__.get(key, _default) + + def pop(self, key: str, _default: typing.Optional[typing.Any] = None) -> typing.Any: + return self.__dict__.pop(key, _default) + + def popitem(self) -> typing.Any: + return self.__dict__.popitem() + + def _update_with_defaults( + self, defaults: typing.Iterable[typing.Tuple[str, typing.Any]] + ) -> None: + for key, value in defaults: + self.__dict__.setdefault(key, value) + + +class DevEnv(typing.Dict[str, typing.Any]): + is_dev = is_dev + + def __init__(self, *args, **kwargs) -> None: + # self.__dict__ = {} + super().__init__(*args, **kwargs) + self.imported: typing.List[typing.Union[str, typing.Tuple[str, str]]] = [] + + @classmethod + def get_environment( + cls, ctx: commands.Context, use_extended_environment: bool = True + ) -> typing.Dict[str, typing.Any]: + env = cls( # In Dev cog by Zeph. + **{ + "me": ctx.me, + # Redirect builtin console functions to rich. + "print": rich.print, + "help": functools.partial(rich.inspect, help=True), + # Eval and exec automatically put this in, but types.FunctionType does not. + "__builtins__": builtins, + # Fill in various other environment keys that some code might expect. + "__builtin__": builtins, + "__doc__": ctx.command.help, + "__package__": None, + "__loader__": None, + "__spec__": None, + } + ) + env["interaction"] = ctx.interaction + if getattr(ctx.channel, "category", None) is not None: + env["category"] = ctx.channel.category + Dev = ctx.bot.get_cog("Dev") + if use_extended_environment: + env.update(cls.get_env(ctx.bot, ctx)) # My nice env! + base_env = dev_commands.Dev.get_environment(Dev, ctx) # In Dev in Core. + # del base_env["_"] + env.update(base_env) + env.update({"dev_env": env, "devenv": env}) + return env + + def get_formatted_env( + self, + ctx: typing.Optional[commands.Context] = None, + show_values: typing.Optional[bool] = True, + ) -> str: + if show_values: + raw_table = Table( + "Key", + "Value", + title="------------------------------ DevEnv ------------------------------", + ) + else: + raw_table = Table( + "Key", title="------------------------------ DevEnv ------------------------------" + ) + for name, value in self.items(): + if name in self.imported: + continue + if show_values: + raw_table.add_row(str(name), repr(value)) + else: + raw_table.add_row(str(name)) + raw_table_str = no_colour_rich_markup(raw_table, no_box=True) + raw_table_str = self.sanitize_output(self.get("ctx", ctx), raw_table_str) + if ctx is not None: + asyncio.create_task(Menu(pages=raw_table_str, lang="py").start(ctx)) + return raw_table_str + + def get_formatted_imports(self) -> str: + if not (imported := self.imported): + return "" + imported.sort(key=lambda x: x if isinstance(x, str) else f"z{x[0]}") + message = "".join( + ( + f">>> import {_import}\n" + if isinstance(_import, str) + else f">>> from {_import[0]} import {_import[1]}\n" + ) + for _import in imported + ) + imported.clear() + return message + + def __missing__(self, key: str) -> typing.Any: + try: + if (value := self["devspace"].get(key)) is not None: + return value + except (KeyError, AttributeError): + pass + if not self.get("auto_imports", True): + raise KeyError(key) + if key in {"exit", "quit"}: + raise Exit() + try: + # this is called implicitly after KeyError, but + # some modules would overwrite builtins (e.g. bin) + return getattr(builtins, key) + except AttributeError: + pass + try: + module = importlib.import_module(key) + except ImportError: + pass + else: + self.imported.append(key) + self[key] = module + return module + try: + if (cog := self["bot"].get_cog(key)) is not None: + self[key] = cog + return cog + except (KeyError, AttributeError): + pass + if key.lower().startswith("id"): + _id = key[2:] if key[2] != "_" else key[3:] + try: + _id = int(_id) + except ValueError: + pass + else: + try: + if (member := self["guild"].get_member(_id)) is not None: + self[key] = member + return member + except (KeyError, AttributeError): + pass + try: + if (user := self["bot"].get_user(_id)) is not None: + self[key] = user + return user + except (KeyError, AttributeError): + pass + try: + if (guild := self["bot"].get_guild(_id)) is not None: + self[key] = guild + return guild + except (KeyError, AttributeError): + pass + try: + if (channel := self["guild"].get_channel(_id)) is not None: + self[key] = channel + return channel + except (KeyError, AttributeError): + pass + try: + if (role := self["guild"].get_role(_id)) is not None: + self[key] = role + return role + except (KeyError, AttributeError): + pass + try: + if (message := self["channel"].get_partial_message(_id)) is not None: + self[key] = message + return message + except (KeyError, AttributeError): + pass + if (attr := getattr(discord, key, None)) is not None: + self.imported.append(("discord", key)) + self[key] = attr + return attr + if (attr := getattr(typing, key, None)) is not None: + self.imported.append(("typing", key)) + self[key] = attr + return attr + if (attr := getattr(cf, key, None)) is not None: + self.imported.append(("redbot.core.utils.chat_formatting", key)) + self[key] = attr + return attr + try: + if is_dev(bot=self["bot"]) and (attr := getattr(CogsUtils, key, None)) is not None: + self.imported.append(("AAA3A_utils.CogsUtils", key)) + self[key] = attr + return attr + except (KeyError, AttributeError): + pass + raise KeyError(key) + + @staticmethod + def sanitize_output(ctx: commands.Context, input_: str) -> str: + """Hides the bot's token from a string.""" + token = ctx.bot.http.token + input_ = CogsUtils.replace_var_paths(input_) + return re.sub(re.escape(token), "[EXPUNGED]", input_, re.I) + + @classmethod + def get_env( + cls, bot: Red, ctx: typing.Optional[commands.Context] = None + ) -> typing.Dict[str, typing.Any]: + logger = CogsUtils.get_logger(name="Test") + + def where(name_or_module: typing.Union[str, types.MethodType]) -> str: + name = ( + name_or_module + if isinstance(name_or_module, str) + else (getattr(name_or_module, "__module__", None) or name_or_module.__name__) + ).replace("-", "_") + spec = importlib.util.find_spec(name) + if spec is None: + raise RuntimeError("Module `{name}` not found.".format(name=name)) + return spec.origin + + async def _rtfs(ctx: commands.Context, object): + code = inspect.getsource(object) + await Menu(pages=code, lang="py").start(ctx) + + def reference(ctx: commands.Context) -> typing.Optional[discord.Message]: + if ctx.message.reference is not None and isinstance( + ctx.message.reference.resolved, discord.Message + ): + return ctx.message.reference.resolved + + # def _console_custom(ctx: commands.Context): + # return {"width": 80, "color_system": None} + + def search_attribute( + a, b: typing.Optional[str] = "", startswith: typing.Optional[str] = "" + ) -> typing.List[str]: + return [ + x + for x in dir(a) + if b.lower() in x.lower() and x.lower().startswith(startswith.lower()) + ] + + async def run_converter( + converter: typing.Any, argument: str, label: typing.Optional[str] = "test" + ) -> typing.Any: + param = discord.ext.commands.parameters.Parameter( + name=label, kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=converter + ) + return await discord.ext.commands.converter.run_converters( + ctx, converter=param.converter, argument=argument, param=param + ) + + async def run_converters( + command_or_function: typing.Union[str, commands.Command, typing.Callable], + arguments: str, + ) -> typing.Dict[str, typing.Any]: + if isinstance(command_or_function, str): + if (command := ctx.bot.get_command(command_or_function)) is None: + raise RuntimeError() + elif isinstance(command_or_function, commands.Command): + command = command_or_function + else: + command = commands.Command(command_or_function) + view = discord.ext.commands.view.StringView(arguments) + fake_context = type( + "FakeContext", + (), + { + "bot": ctx.bot, + "command": command, + "message": ctx.message, + "prefix": None, + "view": view, + }, + ) + await command._parse_arguments(fake_context) + kwargs = {} + for i, arg in enumerate(fake_context.args[1:]): + kwargs[list(command.params.keys())[i]] = arg + kwargs.update(fake_context.kwargs) + return kwargs + + def get_internal(ctx: commands.Context): + def _get_internal( + name: typing.Literal["events", "listeners", "loggers", "parsers", "converters"], + b: typing.Optional[str] = "", + startswith: typing.Optional[str] = "", + ): + if name == "events": + if b == "": + result = ctx.bot.extra_events.copy() + else: + return ctx.bot.extra_events[b] + elif name == "listeners": + if b == "": + result = ctx.bot._listeners.copy() + else: + return ctx.bot._listeners[b] + elif name == "loggers": + result = logging.Logger.manager.loggerDict.copy() + elif name == "parsers": + result = ctx.bot._get_websocket(0)._discord_parsers.copy() + elif name == "converters": + result = discord.ext.commands.converter.CONVERTER_MAPPING.copy() + else: + raise ValueError(name) + result = { + name: value + for name, value in result.items() + if b.lower() in name.lower() and name.lower().startswith(startswith.lower()) + } + return result + + return _get_internal + + def set_loggers_level( + level: typing.Optional[str] = logging.DEBUG, + loggers: typing.Optional[typing.List] = None, + exclusions: typing.Optional[typing.List] = None, + b: typing.Optional[str] = "", + startswith: typing.Optional[str] = "", + ) -> int: + __loggers = logging.Logger.manager.loggerDict + if loggers is not None: + _loggers = [ + logger + for name, logger in __loggers.items() + if name in loggers and isinstance(logger, logging.Logger) + ] + else: + _loggers = [ + logger for logger in __loggers.values() if isinstance(logger, logging.Logger) + ] + _loggers = [ + logger + for logger in _loggers + if b.lower() in logger.name and logger.name.lower().startswith(startswith.lower()) + ] + if exclusions is not None: + _loggers = [logger for logger in _loggers if logger.name not in exclusions] + for logger in _loggers: + logger.setLevel(level) + return len(_loggers) + + def params(_object: typing.Any) -> None: + return {param.name: param for param in inspect.signature(_object).parameters.values()} + + def find_all(predicate, iterable: collections.abc.Iterable) -> typing.List[typing.Any]: + if hasattr(iterable, "__aiter__"): + + async def _a_find_all(): + return [element async for element in iterable if predicate(element)] + + return _a_find_all() + else: + return [element for element in iterable if predicate(element)] + + def get_all(iterable: collections.abc.Iterable, **attrs) -> typing.List[typing.Any]: + attrget = discord.utils.attrgetter + converted = [ + (attrget(attr.replace("__", ".")), value) for attr, value in attrs.items() + ] + if hasattr(iterable, "__aiter__"): + + async def _a_get_all(): + return [ + element + async for element in iterable + if all(pred(element) == value for pred, value in converted) + ] + + return _a_get_all() + else: + return [ + element + for element in iterable + if all(pred(element) == value for pred, value in converted) + ] + + dev_space: DevSpace = getattr(ctx.bot.get_cog("Dev"), "dev_space", AttributeError()) + + def get_url(ctx: commands.Context): + async def _get_url(url: str, **kwargs): + async with ctx.bot.get_cog("Dev")._session.get(url=url, **kwargs) as r: + return r + + return _get_url + + if is_dev(bot=bot): + env = { + # CogsUtils + "CogsUtils": lambda ctx: CogsUtils, + "Loop": lambda ctx: Loop, + "Reactions": lambda ctx: Reactions, + "Menu": lambda ctx: Menu, + "SharedCog": lambda ctx: SharedCog, + "Cog": lambda ctx: Cog, + "Context": lambda ctx: Context, + "Settings": lambda ctx: Settings, + "SentryHelper": lambda ctx: SentryHelper, + "logger": lambda ctx: logger, + "_rtfs": lambda ctx: partial(_rtfs, ctx), + "DevEnv": lambda ctx: cls, + "DevSpace": lambda ctx: DevSpace, + "Cogs": lambda ctx: CogsCommands.Cogs( + bot=ctx.bot, Cog=CogsCommands.Cog, Command=CogsCommands.Command + ), + "Commands": lambda ctx: CogsCommands.Commands( + bot=ctx.bot, Cog=CogsCommands.Cog, Command=CogsCommands.Command + ), + } + # Dpy2 things + env.update( + { + "ConfirmationAskView": lambda ctx: ConfirmationAskView, + "Buttons": lambda ctx: Buttons, + "Dropdown": lambda ctx: Dropdown, + "Select": lambda ctx: Select, + "ChannelSelect": lambda ctx: ChannelSelect, + "MentionableSelect": lambda ctx: MentionableSelect, + "RoleSelect": lambda ctx: RoleSelect, + "UserSelect": lambda ctx: UserSelect, + "Modal": lambda ctx: Modal, + } + ) + else: + env = {} + env.update( + { + # Dpy & Red + "discord": lambda ctx: discord, + "redbot": lambda ctx: redbot, + "Red": lambda ctx: Red, + "redutils": lambda ctx: redutils, + "cf": lambda ctx: cf, + "Config": lambda ctx: Config, + "run_converter": lambda ctx: run_converter, + "run_converters": lambda ctx: run_converters, + "Route": lambda ctx: discord.http.Route, + "websocket": lambda ctx: ctx.bot._get_websocket(ctx.guild.id), + "get_internal": get_internal, + "set_loggers_level": lambda ctx: set_loggers_level, + "find": lambda ctx: discord.utils.find, + "get": lambda ctx: discord.utils.get, + "MISSING": lambda ctx: discord.utils.MISSING, + "escape_markdown": lambda ctx: discord.utils.escape_markdown, + "as_chunks": lambda ctx: discord.utils.as_chunks, + "format_dt": lambda ctx: discord.utils.format_dt, + # Ty Lemon. + "params": lambda ctx: params, + "find_all": lambda ctx: find_all, + "get_all": lambda ctx: get_all, + # Dev Space + "dev_space": lambda ctx: dev_space, + "devspace": lambda ctx: dev_space, + # Typing + "typing": lambda ctx: typing, + # Inspect + "inspect": lambda ctx: inspect, + "source": lambda ctx: lambda _object: rich.print( + textwrap.dedent(inspect.getsource(_object)).strip() + ), + "gs": lambda ctx: lambda _object: rich.print( + textwrap.dedent(inspect.getsource(_object)).strip() + ), + # "gs": lambda ctx: inspect.getsource, + # logging + "logging": lambda ctx: logging, + # Date & Time + "datetime": lambda ctx: datetime, + "time": lambda ctx: time, + "utc_now": lambda ctx: datetime.datetime.now(tz=datetime.timezone.utc), + "local_now": lambda ctx: datetime.datetime.now(), + "get_utc_now": lambda ctx: functools.partial( + datetime.datetime.now, tz=datetime.timezone.utc + ), + "get_local_now": lambda ctx: datetime.datetime.now, + # Os & Sys + "os": lambda ctx: os, + "sys": lambda ctx: sys, + # Aiohttp + "aiohttp": lambda ctx: aiohttp, + "session": lambda ctx: ctx.bot.get_cog("Dev")._session, + "get_url": get_url, + "BytesIO": lambda ctx: BytesIO, + # TextWrap + "textwrap": lambda ctx: textwrap, + # Search attributes + "search_attrs": lambda ctx: search_attribute, + # search python library + "where": lambda ctx: where, + # `reference` + "reference": reference, + # No color (Dev cog from fluffy-cogs in mobile). + # "_console_custom": _console_custom, + # Dpy get + "get_cog": lambda ctx: ctx.bot.get_cog, + "get_command": lambda ctx: ctx.bot.get_command, + "get_guild": lambda ctx: ctx.bot.get_guild, + "get_channel": lambda ctx: ctx.guild.get_channel, + "fetch_message": lambda ctx: ctx.channel.fetch_message, + # Fake + "token": lambda ctx: "[EXPUNGED]", + } + ) + if ctx is not None: + _env = {} + for name, value in env.items(): + try: + _env[name] = value(ctx) + except Exception as exc: + traceback.clear_frames(exc.__traceback__) + _env[name] = exc + else: + _env = env + return _env + + +class CogsCommands: + class Cog(commands.Cog): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + @classmethod + def _setup(cls, bot: Red, Cog, Command, cog) -> typing.Any: # typing_extensions.Self + c = cls() + c.__dict__ = cog.__dict__ + c.__cog_name__ = cog.__cog_name__ + c.bot = bot + c.Cog = Cog + c.Command = Command + c.original_object = cog + return c + + def __repr__(self) -> str: + if getattr(self, "original_object", None) is not None: + return repr(self.original_object) + return super().__repr__() + + def __len__(self) -> int: + cog = self + source = { + command.name: command + for command in self.bot.all_commands.values() + if getattr(command.cog, "qualified_name", None) + == getattr(cog, "qualified_name", None) + } + return len(source) + + def __contains__(self, key: str) -> bool: + cog = self + source = { + command.name: command + for command in self.bot.all_commands.values() + if getattr(command.cog, "qualified_name", None) + == getattr(cog, "qualified_name", None) + } + return key in source + + def __iter__(self) -> typing.Iterator[typing.Tuple[str, typing.Any]]: + cog = self + source = { + command.name: command + for command in self.bot.all_commands.values() + if getattr(command.cog, "qualified_name", None) + == getattr(cog, "qualified_name", None) + } + _items = source + items = { + key: self.Command._setup( + bot=self.bot, Cog=self.Cog, Command=self.Command, command=value + ) + for key, value in _items.items() + } + yield from items.items() + + def __getitem__(self, key: str) -> typing.Any: + cog = self + source = { + command.name: command + for command in self.bot.all_commands.values() + if getattr(command.cog, "qualified_name", None) + == getattr(cog, "qualified_name", None) + } + _item = source[key] + return self.Command._setup( + bot=self.bot, Cog=self.Cog, Command=self.Command, command=_item + ) + + def items(self): + cog = self + source = { + command.name: command + for command in self.bot.all_commands.values() + if getattr(command.cog, "qualified_name", None) + == getattr(cog, "qualified_name", None) + } + _items = source + return { + key: self.Command._setup( + bot=self.bot, Cog=self.Cog, Command=self.Command, command=value + ) + for key, value in _items.items() + } + + def keys(self): + cog = self + source = { + command.name: command + for command in self.bot.all_commands.values() + if getattr(command.cog, "qualified_name", None) + == getattr(cog, "qualified_name", None) + } + return source.keys() + + def values(self): + cog = self + source = { + command.name: command + for command in self.bot.all_commands.values() + if getattr(command.cog, "qualified_name", None) + == getattr(cog, "qualified_name", None) + } + _items = source + items = { + key: self.Command._setup( + bot=self.bot, Cog=self.Cog, Command=self.Command, command=value + ) + for key, value in _items.items() + } + return items.values() + + class Command(commands.Command): + def __init__(func, *args, **kwargs): + super().__init__(func=func, *args, **kwargs) + + @classmethod + def _setup(cls, bot: Red, Cog, Command, command) -> typing.Any: # typing_extensions.Self + c = cls(command.callback) + c.__dict__ = command.__dict__ + c.bot = bot + c.Cog = Cog + c.Command = Command + c.original_object = command + return c + + def __repr__(self) -> str: + if getattr(self, "original_object", None) is not None: + return repr(self.original_object) + return super().__repr__() + + def __eq__(self, other) -> bool: + return ( + isinstance(other, commands.Command) + and other.qualified_name == self.qualified_name + and other.callback == self.callback + ) + + def __len__(self) -> int: + command = self + source = { + c.name: c + for c in self.bot.walk_commands() + if getattr(c.parent, "qualified_name", None) == command.qualified_name + } + return len(source) + + def __contains__(self, key: str) -> bool: + command = self + source = { + c.name: c + for c in self.bot.walk_commands() + if getattr(c.parent, "qualified_name", None) == command.qualified_name + } + return key in source + + def __iter__(self) -> typing.Iterator[typing.Tuple[str, typing.Any]]: + command = self + source = { + c.name: c + for c in self.bot.walk_commands() + if getattr(c.parent, "qualified_name", None) == command.qualified_name + } + _items = source + items = { + key: self.Command._setup( + bot=self.bot, Cog=self.Cog, Command=self.Command, command=value + ) + for key, value in _items.items() + } + yield from items.items() + + def __getitem__(self, key: str) -> typing.Any: + command = self + source = { + c.name: c + for c in self.bot.walk_commands() + if getattr(c.parent, "qualified_name", None) == command.qualified_name + } + _item = source[key] + return self.Command._setup( + bot=self.bot, Cog=self.Cog, Command=self.Command, command=_item + ) + + def items(self): + command = self + source = { + c.name: c + for c in self.bot.walk_commands() + if getattr(c.parent, "qualified_name", None) == command.qualified_name + } + _items = source + return { + key: self.Command._setup( + bot=self.bot, Cog=self.Cog, Command=self.Command, command=value + ) + for key, value in _items.items() + } + + def keys(self): + command = self + source = { + c.name: c + for c in self.bot.walk_commands() + if getattr(c.parent, "qualified_name", None) == command.qualified_name + } + return source.keys() + + def values(self): + command = self + source = { + c.name: c + for c in self.bot.walk_commands() + if getattr(c.parent, "qualified_name", None) == command.qualified_name + } + _items = source + items = { + key: self.Command._setup( + bot=self.bot, Cog=self.Cog, Command=self.Command, command=value + ) + for key, value in _items.items() + } + return items.values() + + class Cogs: + def __init__(self, bot: Red, Cog, Command) -> None: + self.bot: Red = bot + self.Cog = Cog + self.Command = Command + + def __eq__(self, other: object) -> bool: + return isinstance(self, self.__class__) and isinstance(other, self.__class__) + + def __len__(self) -> int: + source = {cog.qualified_name: cog for cog in self.bot.cogs.values()} + return len(source) + + def __contains__(self, key: str) -> bool: + source = {cog.qualified_name: cog for cog in self.bot.cogs.values()} + return key in source + + def __iter__(self) -> typing.Iterator[typing.Tuple[str, typing.Any]]: + source = {cog.qualified_name: cog for cog in self.bot.cogs.values()} + _items = source.items() + yield from { + key: self.Cog._setup(bot=self.bot, Cog=self.Cog, Command=self.Command, cog=value) + for key, value in _items + } + + def __getitem__(self, key: str) -> typing.Any: + source = {cog.qualified_name: cog for cog in self.bot.cogs.values()} + _item = source[key] + return self.Cog._setup(bot=self.bot, Cog=self.Cog, Command=self.Command, cog=_item) + + def items(self): + source = {cog.qualified_name: cog for cog in self.bot.cogs.values()} + _items = source.items() + return { + key: self.Cog._setup(bot=self.bot, Cog=self.Cog, Command=self.Command, cog=value) + for key, value in _items + } + + def keys(self): + source = {cog.qualified_name: cog for cog in self.bot.cogs.values()} + return source.keys() + + def values(self): + source = {cog.qualified_name: cog for cog in self.bot.cogs.values()} + _items = source.items() + items = { + key: self.Cog._setup(bot=self.bot, Cog=self.Cog, Command=self.Command, cog=value) + for key, value in _items + } + return items.values() + + class Commands: + def __init__(self, bot: Red, Cog, Command) -> None: + self.bot: Red = bot + self.Cog = Cog + self.Command = Command + + def __eq__(self, other: object) -> bool: + return isinstance(self, self.__class__) and isinstance(other, self.__class__) + + def __len__(self) -> int: + source = {command.name: command for command in self.bot.all_commands.values()} + return len(source) + + def __contains__(self, key: str) -> bool: + source = {command.name: command for command in self.bot.all_commands.values()} + return key in source + + def __iter__(self) -> typing.Iterator[typing.Tuple[str, typing.Any]]: + source = {command.name: command for command in self.bot.all_commands.values()} + _items = source.items() + yield from { + key: self.Command._setup( + bot=self.bot, Cog=self.Cog, Command=self.Command, command=value + ) + for key, value in _items.items() + } + + def __getitem__(self, key: str) -> typing.Any: + source = {command.name: command for command in self.bot.all_commands.values()} + _item = source[key] + return self.Command._setup( + bot=self.bot, Cog=self.Cog, Command=self.Command, command=_item + ) + + def items(self): + source = {command.name: command for command in self.bot.all_commands.values()} + _items = source.items() + return { + key: self.Command._setup( + bot=self.bot, Cog=self.Cog, Command=self.Command, command=value + ) + for key, value in _items.items() + } + + def keys(self): + source = {command.name: command for command in self.bot.all_commands.values()} + return source.keys() + + def values(self): + source = {command.name: command for command in self.bot.all_commands.values()} + _items = source.items() + items = { + key: self.Command._setup( + bot=self.bot, Cog=self.Cog, Command=self.Command, command=value + ) + for key, value in _items.items() + } + return items.values() diff --git a/dev/info.json b/dev/info.json new file mode 100644 index 0000000..33b6ac2 --- /dev/null +++ b/dev/info.json @@ -0,0 +1,18 @@ +{ + "author": ["Cog-Creators", "Zephyrkul (Zephyrkul#1089)", "AAA3A"], + "name": "Dev", + "install_msg": "Thank you for installing this cog!\nDo `[p]help CogName` to get the list of commands and their description. If you enjoy my work, please consider donating on [Buy Me a Coffee]() or [Ko-Fi]()!\n⚠ Note that this cog requires the `--dev` flag, or it will refuse to load. If you don't know what this means, uninstall the cog.", + "short": "Various development focused utilities!", + "description": "Various development focused utilities.", + "tags": [ + "dev", + "python", + "code", + "eval", + "repl" + ], + "requirements": ["git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git"], + "min_bot_version": "3.5.0", + "end_user_data_statement": "This cog does not persistently store data or metadata about users.", + "hidden": false +} \ No newline at end of file diff --git a/dev/locales/de-DE.po b/dev/locales/de-DE.po new file mode 100644 index 0000000..7e59162 --- /dev/null +++ b/dev/locales/de-DE.po @@ -0,0 +1,90 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: German\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: de\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dev/locales/messages.pot\n" +"X-Crowdin-File-ID: 263\n" +"Language: de_DE\n" + +#: dev\dev.py:468 +#, docstring +msgid "Various development focused utilities!" +msgstr "Verschiedene entwicklungsorientierte Dienstprogramme!" + +#: dev\dev.py:745 dev\dev.py:807 dev\dev.py:1014 +msgid "Unreadable attachment with `utf-8`." +msgstr "Unleserlicher Anhang mit `utf-8`." + +#: dev\dev.py:762 +msgid "This message isn't reachable." +msgstr "Diese Nachricht ist nicht zu erreichen." + +#: dev\dev.py:868 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "Es läuft bereits eine REPL-Sitzung in diesem Kanal. Beenden Sie sie mit `quit`." + +#: dev\dev.py:872 +msgid "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." +msgstr "Es läuft bereits eine REPL-Sitzung in diesem Kanal. Setzen Sie die REPL mit \"{prefix}replresume\" fort." + +#: dev\dev.py:884 +msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." +msgstr "Geben Sie den Code ein, der ausgeführt oder ausgewertet werden soll. `exit()` oder `quit` zum Beenden.{prefix}replpause` zum Anhalten." + +#: dev\dev.py:929 +msgid "Exiting." +msgstr "Ausstieg." + +#: dev\dev.py:937 +msgid "There is no currently running REPL session in this channel." +msgstr "In diesem Kanal läuft derzeit keine REPL-Sitzung." + +#: dev\dev.py:943 +msgid "The REPL session in this channel has been resumed." +msgstr "Die REPL-Sitzung in diesem Kanal wurde wiederaufgenommen." + +#: dev\dev.py:945 +msgid "The REPL session in this channel is now paused." +msgstr "Die REPL-Sitzung in diesem Kanal wird jetzt angehalten." + +#: dev\dev.py:967 +msgid "Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "Bot-Besitzer werden nun alle Befehle mit Abklingzeit umgehen{optional_duration}." + +#: dev\dev.py:977 +msgid "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." +msgstr "Bot-Besitzer werden nicht mehr alle Befehle mit Abklingzeit umgehen{optional_duration}." + +#: dev\dev.py:1103 +#, docstring +msgid "Display all Dev environment values." +msgstr "Alle Dev-Umgebungswerte anzeigen." + +#: dev\dev.py:1112 +#, docstring +msgid "Reset its own locals in evals." +msgstr "Setzt seine eigenen Lokale in Evals zurück." + +#: dev\view.py:38 +msgid "Execute" +msgstr "Ausführen" + +#: dev\view.py:40 +msgid "Code to Execute" +msgstr "Auszuführender Code" + +#: dev\view.py:42 +msgid "Write your code here..." +msgstr "Schreiben Sie Ihren Code hier..." + diff --git a/dev/locales/el-GR.po b/dev/locales/el-GR.po new file mode 100644 index 0000000..11965c0 --- /dev/null +++ b/dev/locales/el-GR.po @@ -0,0 +1,90 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Greek\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: el\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dev/locales/messages.pot\n" +"X-Crowdin-File-ID: 263\n" +"Language: el_GR\n" + +#: dev\dev.py:468 +#, docstring +msgid "Various development focused utilities!" +msgstr "Διάφορα βοηθητικά προγράμματα με επίκεντρο την ανάπτυξη!" + +#: dev\dev.py:745 dev\dev.py:807 dev\dev.py:1014 +msgid "Unreadable attachment with `utf-8`." +msgstr "Μη αναγνώσιμο συνημμένο με `utf-8`." + +#: dev\dev.py:762 +msgid "This message isn't reachable." +msgstr "Αυτό το μήνυμα δεν είναι προσβάσιμο." + +#: dev\dev.py:868 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "Τρέχει ήδη μια συνεδρία REPL σε αυτό το κανάλι. Βγείτε από αυτήν με `quit`." + +#: dev\dev.py:872 +msgid "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." +msgstr "Τρέχει ήδη μια συνεδρία REPL σε αυτό το κανάλι. Συνεχίστε την REPL με `{prefix}replresume`." + +#: dev\dev.py:884 +msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." +msgstr "Εισάγετε τον κώδικα για εκτέλεση ή αξιολόγηση. `exit()` ή `quit` για έξοδο. \"{prefix}replpause\" για παύση." + +#: dev\dev.py:929 +msgid "Exiting." +msgstr "Έξοδος." + +#: dev\dev.py:937 +msgid "There is no currently running REPL session in this channel." +msgstr "Δεν υπάρχει τρέχουσα σύνοδος REPL σε αυτό το κανάλι." + +#: dev\dev.py:943 +msgid "The REPL session in this channel has been resumed." +msgstr "Η σύνοδος REPL σε αυτό το κανάλι συνεχίστηκε." + +#: dev\dev.py:945 +msgid "The REPL session in this channel is now paused." +msgstr "Η σύνοδος REPL σε αυτό το κανάλι έχει πλέον διακοπεί." + +#: dev\dev.py:967 +msgid "Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "Οι ιδιοκτήτες bot θα παρακάμπτουν πλέον όλες τις εντολές με cooldowns{optional_duration}." + +#: dev\dev.py:977 +msgid "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." +msgstr "Οι ιδιοκτήτες bot δεν θα παρακάμπτουν πλέον όλες τις εντολές με cooldowns{optional_duration}." + +#: dev\dev.py:1103 +#, docstring +msgid "Display all Dev environment values." +msgstr "Εμφάνιση όλων των τιμών περιβάλλοντος Dev." + +#: dev\dev.py:1112 +#, docstring +msgid "Reset its own locals in evals." +msgstr "Επαναφέρει τις δικές του τοπικές τιμές στις αξιολογήσεις." + +#: dev\view.py:38 +msgid "Execute" +msgstr "Εκτέλεση" + +#: dev\view.py:40 +msgid "Code to Execute" +msgstr "Κώδικας προς εκτέλεση" + +#: dev\view.py:42 +msgid "Write your code here..." +msgstr "Γράψτε τον κώδικά σας εδώ..." + diff --git a/dev/locales/es-ES.po b/dev/locales/es-ES.po new file mode 100644 index 0000000..4e4cde9 --- /dev/null +++ b/dev/locales/es-ES.po @@ -0,0 +1,90 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Spanish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: es-ES\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dev/locales/messages.pot\n" +"X-Crowdin-File-ID: 263\n" +"Language: es_ES\n" + +#: dev\dev.py:468 +#, docstring +msgid "Various development focused utilities!" +msgstr "Varias utilidades centradas en el desarrollo" + +#: dev\dev.py:745 dev\dev.py:807 dev\dev.py:1014 +msgid "Unreadable attachment with `utf-8`." +msgstr "Archivo adjunto ilegible con `utf-8`." + +#: dev\dev.py:762 +msgid "This message isn't reachable." +msgstr "Este mensaje no está localizable." + +#: dev\dev.py:868 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "Ya se está ejecutando una sesión REPL en este canal. Salga de ella con `quit`." + +#: dev\dev.py:872 +msgid "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." +msgstr "Ya se está ejecutando una sesión REPL en este canal. Reanude la REPL con `{prefix}replresume`." + +#: dev\dev.py:884 +msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." +msgstr "Introduzca el código a ejecutar o evaluar. Exit()` o `quit` para salir.{prefix}replpause` para pausar." + +#: dev\dev.py:929 +msgid "Exiting." +msgstr "Saliendo." + +#: dev\dev.py:937 +msgid "There is no currently running REPL session in this channel." +msgstr "No hay ninguna sesión REPL en curso en este canal." + +#: dev\dev.py:943 +msgid "The REPL session in this channel has been resumed." +msgstr "Se ha reanudado la sesión REPL en este canal." + +#: dev\dev.py:945 +msgid "The REPL session in this channel is now paused." +msgstr "La sesión REPL en este canal está ahora en pausa." + +#: dev\dev.py:967 +msgid "Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "Los propietarios de bots ahora pasarán por alto todos los comandos con tiempos de reutilización{optional_duration}." + +#: dev\dev.py:977 +msgid "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." +msgstr "Los propietarios de bots ya no anularán todos los comandos con tiempos de reutilización{optional_duration}." + +#: dev\dev.py:1103 +#, docstring +msgid "Display all Dev environment values." +msgstr "Muestra todos los valores del entorno Dev." + +#: dev\dev.py:1112 +#, docstring +msgid "Reset its own locals in evals." +msgstr "Restablecer sus propios locales en evals." + +#: dev\view.py:38 +msgid "Execute" +msgstr "Ejecute" + +#: dev\view.py:40 +msgid "Code to Execute" +msgstr "Código a ejecutar" + +#: dev\view.py:42 +msgid "Write your code here..." +msgstr "Escriba aquí su código..." + diff --git a/dev/locales/fi-FI.po b/dev/locales/fi-FI.po new file mode 100644 index 0000000..cfa05c2 --- /dev/null +++ b/dev/locales/fi-FI.po @@ -0,0 +1,90 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Finnish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fi\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dev/locales/messages.pot\n" +"X-Crowdin-File-ID: 263\n" +"Language: fi_FI\n" + +#: dev\dev.py:468 +#, docstring +msgid "Various development focused utilities!" +msgstr "Erilaisia kehitykseen keskittyviä apuohjelmia!" + +#: dev\dev.py:745 dev\dev.py:807 dev\dev.py:1014 +msgid "Unreadable attachment with `utf-8`." +msgstr "Lukukelvoton liitetiedosto `utf-8`:lla." + +#: dev\dev.py:762 +msgid "This message isn't reachable." +msgstr "Tämä viesti ei ole tavoitettavissa." + +#: dev\dev.py:868 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "REPL-istunto on jo käynnissä tällä kanavalla. Lopeta se komennolla `quit`." + +#: dev\dev.py:872 +msgid "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." +msgstr "REPL-istunto on jo käynnissä tällä kanavalla. Jatka REPL-istuntoa komennolla `{prefix}replresume`." + +#: dev\dev.py:884 +msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." +msgstr "Syötä koodi, joka suoritetaan tai arvioidaan. `exit()` tai `quit` poistumiseen. `{prefix}replpause` keskeyttääksesi." + +#: dev\dev.py:929 +msgid "Exiting." +msgstr "Poistuminen." + +#: dev\dev.py:937 +msgid "There is no currently running REPL session in this channel." +msgstr "Tällä hetkellä tällä kanavalla ei ole käynnissä REPL-istuntoa." + +#: dev\dev.py:943 +msgid "The REPL session in this channel has been resumed." +msgstr "Tämän kanavan REPL-istunto on jatkettu." + +#: dev\dev.py:945 +msgid "The REPL session in this channel is now paused." +msgstr "Tämän kanavan REPL-istunto on nyt keskeytetty." + +#: dev\dev.py:967 +msgid "Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "Bottien omistajat ohittavat nyt kaikki komennot, joilla on jäähdytysaika{optional_duration}." + +#: dev\dev.py:977 +msgid "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." +msgstr "Bottien omistajat eivät enää ohita kaikkia komentoja, joilla on jäähdytysaika{optional_duration}." + +#: dev\dev.py:1103 +#, docstring +msgid "Display all Dev environment values." +msgstr "Näytä kaikki Dev-ympäristön arvot." + +#: dev\dev.py:1112 +#, docstring +msgid "Reset its own locals in evals." +msgstr "Nollaa omat paikalliset arviot evalsissa." + +#: dev\view.py:38 +msgid "Execute" +msgstr "Suorita" + +#: dev\view.py:40 +msgid "Code to Execute" +msgstr "Suoritettava koodi" + +#: dev\view.py:42 +msgid "Write your code here..." +msgstr "Kirjoita koodisi tähän..." + diff --git a/dev/locales/fr-FR.po b/dev/locales/fr-FR.po new file mode 100644 index 0000000..86f036e --- /dev/null +++ b/dev/locales/fr-FR.po @@ -0,0 +1,90 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: French\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dev/locales/messages.pot\n" +"X-Crowdin-File-ID: 263\n" +"Language: fr_FR\n" + +#: dev\dev.py:468 +#, docstring +msgid "Various development focused utilities!" +msgstr "Divers utilitaires axés sur le développement !" + +#: dev\dev.py:745 dev\dev.py:807 dev\dev.py:1014 +msgid "Unreadable attachment with `utf-8`." +msgstr "Pièce jointe illisible avec `utf-8`." + +#: dev\dev.py:762 +msgid "This message isn't reachable." +msgstr "Ce message n'est pas accessible." + +#: dev\dev.py:868 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "Une session REPL est déjà en cours dans ce canal. Quittez-la avec `quit`." + +#: dev\dev.py:872 +msgid "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." +msgstr "Une session REPL est déjà en cours dans ce canal. Reprenez la REPL avec `{prefix}replresume`." + +#: dev\dev.py:884 +msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." +msgstr "Entrez le code à exécuter ou à évaluer. `exit()` ou `quit` pour quitter. `{prefix}replpause` pour faire une pause." + +#: dev\dev.py:929 +msgid "Exiting." +msgstr "Sortie." + +#: dev\dev.py:937 +msgid "There is no currently running REPL session in this channel." +msgstr "Il n'y a pas de session REPL en cours dans ce canal." + +#: dev\dev.py:943 +msgid "The REPL session in this channel has been resumed." +msgstr "La session REPL de ce canal a été reprise." + +#: dev\dev.py:945 +msgid "The REPL session in this channel is now paused." +msgstr "La session REPL de ce canal est maintenant en pause." + +#: dev\dev.py:967 +msgid "Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "Les propriétaires de bots contourneront désormais toutes les commandes avec des temps de recharge{optional_duration}." + +#: dev\dev.py:977 +msgid "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." +msgstr "Les propriétaires de bots ne pourront plus contourner toutes les commandes ayant un temps de recharge{optional_duration}." + +#: dev\dev.py:1103 +#, docstring +msgid "Display all Dev environment values." +msgstr "Afficher toutes les valeurs de l'environnement Dev." + +#: dev\dev.py:1112 +#, docstring +msgid "Reset its own locals in evals." +msgstr "Réinitialiser ses propres locaux dans les évaluations." + +#: dev\view.py:38 +msgid "Execute" +msgstr "Exécuter" + +#: dev\view.py:40 +msgid "Code to Execute" +msgstr "Code à exécuter" + +#: dev\view.py:42 +msgid "Write your code here..." +msgstr "Ecrivez votre code ici..." + diff --git a/dev/locales/it-IT.po b/dev/locales/it-IT.po new file mode 100644 index 0000000..4657371 --- /dev/null +++ b/dev/locales/it-IT.po @@ -0,0 +1,90 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Italian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: it\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dev/locales/messages.pot\n" +"X-Crowdin-File-ID: 263\n" +"Language: it_IT\n" + +#: dev\dev.py:468 +#, docstring +msgid "Various development focused utilities!" +msgstr "Diverse utilità per lo sviluppo!" + +#: dev\dev.py:745 dev\dev.py:807 dev\dev.py:1014 +msgid "Unreadable attachment with `utf-8`." +msgstr "Allegato illeggibile con `utf-8`." + +#: dev\dev.py:762 +msgid "This message isn't reachable." +msgstr "Questo messaggio non è raggiungibile." + +#: dev\dev.py:868 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "È già in corso una sessione REPL in questo canale. Uscire con `quit`." + +#: dev\dev.py:872 +msgid "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." +msgstr "È già in corso una sessione REPL in questo canale. Riprendere la REPL con `{prefix}replresume`." + +#: dev\dev.py:884 +msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." +msgstr "Inserire il codice da eseguire o valutare. `exit()` o `quit` per uscire.{prefix}replpause` per mettere in pausa." + +#: dev\dev.py:929 +msgid "Exiting." +msgstr "Uscita." + +#: dev\dev.py:937 +msgid "There is no currently running REPL session in this channel." +msgstr "Non c'è nessuna sessione REPL in corso in questo canale." + +#: dev\dev.py:943 +msgid "The REPL session in this channel has been resumed." +msgstr "La sessione REPL in questo canale è stata ripresa." + +#: dev\dev.py:945 +msgid "The REPL session in this channel is now paused." +msgstr "La sessione REPL di questo canale è ora in pausa." + +#: dev\dev.py:967 +msgid "Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "I proprietari di bot ora bypasseranno tutti i comandi con cooldown{optional_duration}." + +#: dev\dev.py:977 +msgid "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." +msgstr "I proprietari di bot non bypasseranno più tutti i comandi con cooldown{optional_duration}." + +#: dev\dev.py:1103 +#, docstring +msgid "Display all Dev environment values." +msgstr "Visualizza tutti i valori dell'ambiente Dev." + +#: dev\dev.py:1112 +#, docstring +msgid "Reset its own locals in evals." +msgstr "Azzera i propri locali nelle valutazioni." + +#: dev\view.py:38 +msgid "Execute" +msgstr "Eseguire" + +#: dev\view.py:40 +msgid "Code to Execute" +msgstr "Codice da eseguire" + +#: dev\view.py:42 +msgid "Write your code here..." +msgstr "Scrivi il tuo codice qui..." + diff --git a/dev/locales/ja-JP.po b/dev/locales/ja-JP.po new file mode 100644 index 0000000..de0c27c --- /dev/null +++ b/dev/locales/ja-JP.po @@ -0,0 +1,90 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Japanese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ja\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dev/locales/messages.pot\n" +"X-Crowdin-File-ID: 263\n" +"Language: ja_JP\n" + +#: dev\dev.py:468 +#, docstring +msgid "Various development focused utilities!" +msgstr "開発に特化した各種ユーティリティ!" + +#: dev\dev.py:745 dev\dev.py:807 dev\dev.py:1014 +msgid "Unreadable attachment with `utf-8`." +msgstr "utf-8`の添付ファイルが読めない。" + +#: dev\dev.py:762 +msgid "This message isn't reachable." +msgstr "このメッセージは届きません。" + +#: dev\dev.py:868 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "このチャンネルですでに REPL セッションを実行している。quit`で終了してください。" + +#: dev\dev.py:872 +msgid "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." +msgstr "このチャンネルですでにREPLセッションを実行している。{prefix}replresume`でREPLを再開する。" + +#: dev\dev.py:884 +msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." +msgstr "実行または評価するコードを入力する。exit()`または `quit` で終了する。{prefix}replpause` で一時停止する。" + +#: dev\dev.py:929 +msgid "Exiting." +msgstr "退場する。" + +#: dev\dev.py:937 +msgid "There is no currently running REPL session in this channel." +msgstr "このチャネルでは、現在実行中のREPLセッションはない。" + +#: dev\dev.py:943 +msgid "The REPL session in this channel has been resumed." +msgstr "このチャネルのREPLセッションが再開された。" + +#: dev\dev.py:945 +msgid "The REPL session in this channel is now paused." +msgstr "このチャネルのREPLセッションは一時停止している。" + +#: dev\dev.py:967 +msgid "Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "ボット所有者は、クールダウンのあるすべてのコマンドをバイパスするようになりました{optional_duration}。" + +#: dev\dev.py:977 +msgid "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." +msgstr "ボット所有者は、クールダウンを持つすべてのコマンドをバイパスしなくなった{optional_duration}。" + +#: dev\dev.py:1103 +#, docstring +msgid "Display all Dev environment values." +msgstr "すべてのDev環境の値を表示する。" + +#: dev\dev.py:1112 +#, docstring +msgid "Reset its own locals in evals." +msgstr "エバリュエーションでローカルをリセットする。" + +#: dev\view.py:38 +msgid "Execute" +msgstr "実行する" + +#: dev\view.py:40 +msgid "Code to Execute" +msgstr "実行するコード" + +#: dev\view.py:42 +msgid "Write your code here..." +msgstr "ここにコードを書く" + diff --git a/dev/locales/messages.pot b/dev/locales/messages.pot new file mode 100644 index 0000000..dca98ad --- /dev/null +++ b/dev/locales/messages.pot @@ -0,0 +1,94 @@ +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2024-12-29 10:43+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" + +#: dev\dev.py:472 +#, docstring +msgid "Various development focused utilities!" +msgstr "" + +#: dev\dev.py:750 dev\dev.py:813 dev\dev.py:1021 +msgid "Unreadable attachment with `utf-8`." +msgstr "" + +#: dev\dev.py:767 +msgid "This message isn't reachable." +msgstr "" + +#: dev\dev.py:874 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "" + +#: dev\dev.py:878 +msgid "" +"Already running a REPL session in this channel. Resume the REPL with " +"`{prefix}replresume`." +msgstr "" + +#: dev\dev.py:890 +msgid "" +"Enter code to execute or evaluate. `exit()` or `quit` to exit. " +"`{prefix}replpause` to pause." +msgstr "" + +#: dev\dev.py:935 +msgid "Exiting." +msgstr "" + +#: dev\dev.py:943 +msgid "There is no currently running REPL session in this channel." +msgstr "" + +#: dev\dev.py:949 +msgid "The REPL session in this channel has been resumed." +msgstr "" + +#: dev\dev.py:951 +msgid "The REPL session in this channel is now paused." +msgstr "" + +#: dev\dev.py:973 +msgid "" +"Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "" + +#: dev\dev.py:983 +msgid "" +"Bot owners will no longer bypass all commands with " +"cooldowns{optional_duration}." +msgstr "" + +#: dev\dev.py:1110 +#, docstring +msgid "Display all Dev environment values." +msgstr "" + +#: dev\dev.py:1119 +#, docstring +msgid "Reset its own locals in evals." +msgstr "" + +#: dev\view.py:42 +msgid "Execute" +msgstr "" + +#: dev\view.py:44 +msgid "Code to Execute" +msgstr "" + +#: dev\view.py:46 +msgid "Write your code here..." +msgstr "" + +#: dev\view.py:84 +msgid "You are not allowed to use this interaction." +msgstr "" diff --git a/dev/locales/nl-NL.po b/dev/locales/nl-NL.po new file mode 100644 index 0000000..b52bcb0 --- /dev/null +++ b/dev/locales/nl-NL.po @@ -0,0 +1,90 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Dutch\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: nl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dev/locales/messages.pot\n" +"X-Crowdin-File-ID: 263\n" +"Language: nl_NL\n" + +#: dev\dev.py:468 +#, docstring +msgid "Various development focused utilities!" +msgstr "Verschillende development gerelateerde functies!" + +#: dev\dev.py:745 dev\dev.py:807 dev\dev.py:1014 +msgid "Unreadable attachment with `utf-8`." +msgstr "Onleesbare bijlage met `utf-8`." + +#: dev\dev.py:762 +msgid "This message isn't reachable." +msgstr "Dit bericht is niet bereikbaar." + +#: dev\dev.py:868 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "Al een REPL-sessie aan het uitvoeren in dit kanaal. Stop het met `quit`." + +#: dev\dev.py:872 +msgid "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." +msgstr "Al een REPL-sessie aan het uitvoeren in dit kanaal. Hervat de REPL met `{prefix}replresume`." + +#: dev\dev.py:884 +msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." +msgstr "Voer code in om uit te voeren of the evalueren.`exit()` of `quit` om te stoppen. `{prefix}replpause` om te pauzeren." + +#: dev\dev.py:929 +msgid "Exiting." +msgstr "Stoppen." + +#: dev\dev.py:937 +msgid "There is no currently running REPL session in this channel." +msgstr "Er is momenteel geen REPL-sessie gaande in dit kanaal." + +#: dev\dev.py:943 +msgid "The REPL session in this channel has been resumed." +msgstr "De REPL-sessie in dit kanaal is hervat." + +#: dev\dev.py:945 +msgid "The REPL session in this channel is now paused." +msgstr "De REPL-sessie in dit kanaal is nu gepauzeerd." + +#: dev\dev.py:967 +msgid "Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "Bot eigenaren zullen nu cooldowns{optional_duration} in commando's negeren." + +#: dev\dev.py:977 +msgid "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." +msgstr "Bot eigenaren zullen niet meer cooldowns{optional_duration} in commando's negeren." + +#: dev\dev.py:1103 +#, docstring +msgid "Display all Dev environment values." +msgstr "Geef alle Dev environment values weer." + +#: dev\dev.py:1112 +#, docstring +msgid "Reset its own locals in evals." +msgstr "Reset zijn eigen locals in evals." + +#: dev\view.py:38 +msgid "Execute" +msgstr "Uitvoeren" + +#: dev\view.py:40 +msgid "Code to Execute" +msgstr "Uit te voeren code" + +#: dev\view.py:42 +msgid "Write your code here..." +msgstr "Schrijf je code hier..." + diff --git a/dev/locales/pl-PL.po b/dev/locales/pl-PL.po new file mode 100644 index 0000000..f7967d5 --- /dev/null +++ b/dev/locales/pl-PL.po @@ -0,0 +1,90 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Polish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dev/locales/messages.pot\n" +"X-Crowdin-File-ID: 263\n" +"Language: pl_PL\n" + +#: dev\dev.py:468 +#, docstring +msgid "Various development focused utilities!" +msgstr "Różne narzędzia ukierunkowane na rozwój!" + +#: dev\dev.py:745 dev\dev.py:807 dev\dev.py:1014 +msgid "Unreadable attachment with `utf-8`." +msgstr "Nieczytelny załącznik z `utf-8`." + +#: dev\dev.py:762 +msgid "This message isn't reachable." +msgstr "Ta wiadomość jest niedostępna." + +#: dev\dev.py:868 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "W tym kanale jest już uruchomiona sesja REPL. Zakończ ją za pomocą `quit`." + +#: dev\dev.py:872 +msgid "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." +msgstr "W tym kanale jest już uruchomiona sesja REPL. Wznów REPL za pomocą `{prefix}replresume`." + +#: dev\dev.py:884 +msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." +msgstr "Wprowadź kod do wykonania lub oceny. `exit()` lub `quit` aby wyjść. `{prefix}replpause` aby wstrzymać." + +#: dev\dev.py:929 +msgid "Exiting." +msgstr "Wyjście." + +#: dev\dev.py:937 +msgid "There is no currently running REPL session in this channel." +msgstr "W tym kanale nie ma aktualnie uruchomionej sesji REPL." + +#: dev\dev.py:943 +msgid "The REPL session in this channel has been resumed." +msgstr "Sesja REPL w tym kanale została wznowiona." + +#: dev\dev.py:945 +msgid "The REPL session in this channel is now paused." +msgstr "Sesja REPL w tym kanale jest teraz wstrzymana." + +#: dev\dev.py:967 +msgid "Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "Właściciele botów będą teraz omijać wszystkie polecenia z czasem odnowienia{optional_duration}." + +#: dev\dev.py:977 +msgid "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." +msgstr "Właściciele botów nie będą już omijać wszystkich poleceń z czasem odnowienia{optional_duration}." + +#: dev\dev.py:1103 +#, docstring +msgid "Display all Dev environment values." +msgstr "Wyświetla wszystkie wartości środowiska Dev." + +#: dev\dev.py:1112 +#, docstring +msgid "Reset its own locals in evals." +msgstr "Resetuje własne lokalizacje w ocenach." + +#: dev\view.py:38 +msgid "Execute" +msgstr "Wykonanie" + +#: dev\view.py:40 +msgid "Code to Execute" +msgstr "Kod do wykonania" + +#: dev\view.py:42 +msgid "Write your code here..." +msgstr "Wpisz tutaj swój kod..." + diff --git a/dev/locales/pt-BR.po b/dev/locales/pt-BR.po new file mode 100644 index 0000000..183b27e --- /dev/null +++ b/dev/locales/pt-BR.po @@ -0,0 +1,90 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Portuguese, Brazilian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-BR\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dev/locales/messages.pot\n" +"X-Crowdin-File-ID: 263\n" +"Language: pt_BR\n" + +#: dev\dev.py:468 +#, docstring +msgid "Various development focused utilities!" +msgstr "Vários utilitários centrados no desenvolvimento!" + +#: dev\dev.py:745 dev\dev.py:807 dev\dev.py:1014 +msgid "Unreadable attachment with `utf-8`." +msgstr "Anexo ilegível com `utf-8`." + +#: dev\dev.py:762 +msgid "This message isn't reachable." +msgstr "Esta mensagem não pode ser acessada." + +#: dev\dev.py:868 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "Já está a correr uma sessão REPL neste canal. Saia dela com `quit`." + +#: dev\dev.py:872 +msgid "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." +msgstr "Já está a correr uma sessão REPL neste canal. Retome o REPL com `{prefix}replresume`." + +#: dev\dev.py:884 +msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." +msgstr "Introduza o código a executar ou avaliar. `exit()` ou `quit` para sair. `{prefix}replpause` para pausar." + +#: dev\dev.py:929 +msgid "Exiting." +msgstr "Sair." + +#: dev\dev.py:937 +msgid "There is no currently running REPL session in this channel." +msgstr "Não há nenhuma sessão REPL em execução neste canal." + +#: dev\dev.py:943 +msgid "The REPL session in this channel has been resumed." +msgstr "A sessão REPL neste canal foi retomada." + +#: dev\dev.py:945 +msgid "The REPL session in this channel is now paused." +msgstr "A sessão REPL neste canal está agora em pausa." + +#: dev\dev.py:967 +msgid "Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "Os proprietários de bots passam a ignorar todos os comandos com tempo de espera{optional_duration}." + +#: dev\dev.py:977 +msgid "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." +msgstr "Os proprietários de bots já não ignoram todos os comandos com tempo de espera{optional_duration}." + +#: dev\dev.py:1103 +#, docstring +msgid "Display all Dev environment values." +msgstr "Apresenta todos os valores do ambiente Dev." + +#: dev\dev.py:1112 +#, docstring +msgid "Reset its own locals in evals." +msgstr "Redefinir os seus próprios locais nas avaliações." + +#: dev\view.py:38 +msgid "Execute" +msgstr "Executar" + +#: dev\view.py:40 +msgid "Code to Execute" +msgstr "Código a ser executado" + +#: dev\view.py:42 +msgid "Write your code here..." +msgstr "Escreva seu código aqui..." + diff --git a/dev/locales/pt-PT.po b/dev/locales/pt-PT.po new file mode 100644 index 0000000..3b66269 --- /dev/null +++ b/dev/locales/pt-PT.po @@ -0,0 +1,90 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Portuguese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-PT\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dev/locales/messages.pot\n" +"X-Crowdin-File-ID: 263\n" +"Language: pt_PT\n" + +#: dev\dev.py:468 +#, docstring +msgid "Various development focused utilities!" +msgstr "Vários utilitários centrados no desenvolvimento!" + +#: dev\dev.py:745 dev\dev.py:807 dev\dev.py:1014 +msgid "Unreadable attachment with `utf-8`." +msgstr "Anexo ilegível com `utf-8`." + +#: dev\dev.py:762 +msgid "This message isn't reachable." +msgstr "Esta mensagem não é contactável." + +#: dev\dev.py:868 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "Já está a correr uma sessão REPL neste canal. Saia dela com `quit`." + +#: dev\dev.py:872 +msgid "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." +msgstr "Já está a correr uma sessão REPL neste canal. Retome o REPL com `{prefix}replresume`." + +#: dev\dev.py:884 +msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." +msgstr "Introduza o código a executar ou avaliar. `exit()` ou `quit` para sair. `{prefix}replpause` para pausar." + +#: dev\dev.py:929 +msgid "Exiting." +msgstr "Sair." + +#: dev\dev.py:937 +msgid "There is no currently running REPL session in this channel." +msgstr "Não há nenhuma sessão REPL em execução neste canal." + +#: dev\dev.py:943 +msgid "The REPL session in this channel has been resumed." +msgstr "A sessão REPL neste canal foi retomada." + +#: dev\dev.py:945 +msgid "The REPL session in this channel is now paused." +msgstr "A sessão REPL neste canal está agora em pausa." + +#: dev\dev.py:967 +msgid "Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "Os proprietários de bots passam a ignorar todos os comandos com tempo de espera{optional_duration}." + +#: dev\dev.py:977 +msgid "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." +msgstr "Os proprietários de bots já não ignoram todos os comandos com tempo de espera{optional_duration}." + +#: dev\dev.py:1103 +#, docstring +msgid "Display all Dev environment values." +msgstr "Apresenta todos os valores do ambiente Dev." + +#: dev\dev.py:1112 +#, docstring +msgid "Reset its own locals in evals." +msgstr "Redefinir os seus próprios locais nas avaliações." + +#: dev\view.py:38 +msgid "Execute" +msgstr "Executar" + +#: dev\view.py:40 +msgid "Code to Execute" +msgstr "Código a executar" + +#: dev\view.py:42 +msgid "Write your code here..." +msgstr "Escreva o seu código aqui..." + diff --git a/dev/locales/ro-RO.po b/dev/locales/ro-RO.po new file mode 100644 index 0000000..a9d1bd9 --- /dev/null +++ b/dev/locales/ro-RO.po @@ -0,0 +1,90 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Romanian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ro\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dev/locales/messages.pot\n" +"X-Crowdin-File-ID: 263\n" +"Language: ro_RO\n" + +#: dev\dev.py:468 +#, docstring +msgid "Various development focused utilities!" +msgstr "Diverse utilități axate pe dezvoltare!" + +#: dev\dev.py:745 dev\dev.py:807 dev\dev.py:1014 +msgid "Unreadable attachment with `utf-8`." +msgstr "Atașament ilizibil cu `utf-8`." + +#: dev\dev.py:762 +msgid "This message isn't reachable." +msgstr "Acest mesaj nu este accesibil." + +#: dev\dev.py:868 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "Rulează deja o sesiune REPL în acest canal. Ieșiți din ea cu `quit`." + +#: dev\dev.py:872 +msgid "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." +msgstr "Rulează deja o sesiune REPL în acest canal. Reluați REPL cu `{prefix}replresume`." + +#: dev\dev.py:884 +msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." +msgstr "Introduceți codul care urmează să fie executat sau evaluat. `exit()` sau `quit` pentru a ieși. `{prefix}replpause` pentru a face o pauză." + +#: dev\dev.py:929 +msgid "Exiting." +msgstr "Ieșire." + +#: dev\dev.py:937 +msgid "There is no currently running REPL session in this channel." +msgstr "Nu există nicio sesiune REPL în curs de desfășurare în acest canal." + +#: dev\dev.py:943 +msgid "The REPL session in this channel has been resumed." +msgstr "Sesiunea REPL din acest canal a fost reluată." + +#: dev\dev.py:945 +msgid "The REPL session in this channel is now paused." +msgstr "Sesiunea REPL din acest canal este acum în pauză." + +#: dev\dev.py:967 +msgid "Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "Proprietarii de roboți vor ocoli acum toate comenzile cu cooldowns{optional_duration}." + +#: dev\dev.py:977 +msgid "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." +msgstr "Proprietarii de roboți nu vor mai ocoli toate comenzile cu cooldowns{optional_duration}." + +#: dev\dev.py:1103 +#, docstring +msgid "Display all Dev environment values." +msgstr "Afișează toate valorile mediului Dev." + +#: dev\dev.py:1112 +#, docstring +msgid "Reset its own locals in evals." +msgstr "Resetează propriile locale în evaluări." + +#: dev\view.py:38 +msgid "Execute" +msgstr "Executare" + +#: dev\view.py:40 +msgid "Code to Execute" +msgstr "Cod de execuție" + +#: dev\view.py:42 +msgid "Write your code here..." +msgstr "Scrie-ți codul aici..." + diff --git a/dev/locales/ru-RU.po b/dev/locales/ru-RU.po new file mode 100644 index 0000000..c9c34f4 --- /dev/null +++ b/dev/locales/ru-RU.po @@ -0,0 +1,90 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Russian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ru\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dev/locales/messages.pot\n" +"X-Crowdin-File-ID: 263\n" +"Language: ru_RU\n" + +#: dev\dev.py:468 +#, docstring +msgid "Various development focused utilities!" +msgstr "Различные утилиты, ориентированные на разработку!" + +#: dev\dev.py:745 dev\dev.py:807 dev\dev.py:1014 +msgid "Unreadable attachment with `utf-8`." +msgstr "Нечитаемое вложение с `utf-8`." + +#: dev\dev.py:762 +msgid "This message isn't reachable." +msgstr "Это сообщение недоступно." + +#: dev\dev.py:868 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "В этом канале уже запущен сеанс REPL. Выйдите из него с помощью `quit`." + +#: dev\dev.py:872 +msgid "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." +msgstr "В этом канале уже запущен сеанс REPL. Возобновите работу REPL с помощью команды `{prefix}replresume`." + +#: dev\dev.py:884 +msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." +msgstr "Введите код для выполнения или оценки. `exit()` или `quit` для выхода. `{prefix}replpause` для приостановки." + +#: dev\dev.py:929 +msgid "Exiting." +msgstr "Выход." + +#: dev\dev.py:937 +msgid "There is no currently running REPL session in this channel." +msgstr "В этом канале нет ни одной текущей сессии REPL." + +#: dev\dev.py:943 +msgid "The REPL session in this channel has been resumed." +msgstr "Сеанс REPL в этом канале был возобновлен." + +#: dev\dev.py:945 +msgid "The REPL session in this channel is now paused." +msgstr "Сессия REPL в этом канале теперь приостановлена." + +#: dev\dev.py:967 +msgid "Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "Владельцы ботов теперь будут обходить все команды с кулдаунами{optional_duration}." + +#: dev\dev.py:977 +msgid "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." +msgstr "Владельцы ботов больше не будут обходить все команды с кулдаунами{optional_duration}." + +#: dev\dev.py:1103 +#, docstring +msgid "Display all Dev environment values." +msgstr "Отображение всех значений окружения Dev." + +#: dev\dev.py:1112 +#, docstring +msgid "Reset its own locals in evals." +msgstr "Сбросьте свои собственные локали при проверке." + +#: dev\view.py:38 +msgid "Execute" +msgstr "Выполнить" + +#: dev\view.py:40 +msgid "Code to Execute" +msgstr "Код для выполнения" + +#: dev\view.py:42 +msgid "Write your code here..." +msgstr "Напишите свой код здесь..." + diff --git a/dev/locales/tr-TR.po b/dev/locales/tr-TR.po new file mode 100644 index 0000000..9be76e0 --- /dev/null +++ b/dev/locales/tr-TR.po @@ -0,0 +1,90 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 13:27\n" +"Last-Translator: \n" +"Language-Team: Turkish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: tr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dev/locales/messages.pot\n" +"X-Crowdin-File-ID: 263\n" +"Language: tr_TR\n" + +#: dev\dev.py:468 +#, docstring +msgid "Various development focused utilities!" +msgstr "Çeşitli geliştirme odaklı araçlar!" + +#: dev\dev.py:745 dev\dev.py:807 dev\dev.py:1014 +msgid "Unreadable attachment with `utf-8`." +msgstr "`utf-8` ile okunamayan ek." + +#: dev\dev.py:762 +msgid "This message isn't reachable." +msgstr "Bu mesaja ulaşılamıyor." + +#: dev\dev.py:868 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "Bu kanalda zaten bir REPL oturumu çalışıyor. `quit` ile çıkın." + +#: dev\dev.py:872 +msgid "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." +msgstr "Bu kanalda zaten bir REPL oturumu çalışıyor. REPL'yi `{prefix}replresume` ile devam ettirin." + +#: dev\dev.py:884 +msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." +msgstr "Yürütmek veya değerlendirmek için kod girin. Çıkmak için `exit()` veya `quit`. Duraklatmak için `{prefix}replpause`." + +#: dev\dev.py:929 +msgid "Exiting." +msgstr "Çıkılıyor." + +#: dev\dev.py:937 +msgid "There is no currently running REPL session in this channel." +msgstr "Bu kanalda şu anda çalışan bir REPL oturumu yok." + +#: dev\dev.py:943 +msgid "The REPL session in this channel has been resumed." +msgstr "Bu kanaldaki REPL oturumu devam ettirildi." + +#: dev\dev.py:945 +msgid "The REPL session in this channel is now paused." +msgstr "Bu kanaldaki REPL oturumu şimdi duraklatıldı." + +#: dev\dev.py:967 +msgid "Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "Bot sahipleri artık bekleme süresi olan tüm komutları atlayacak{optional_duration}." + +#: dev\dev.py:977 +msgid "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." +msgstr "Bot sahipleri artık bekleme süresi olan tüm komutları atlamayacak{optional_duration}." + +#: dev\dev.py:1103 +#, docstring +msgid "Display all Dev environment values." +msgstr "Tüm Geliştirici ortamı değerlerini göster." + +#: dev\dev.py:1112 +#, docstring +msgid "Reset its own locals in evals." +msgstr "Değerlendirmelerde kendi yerel değişkenlerini sıfırla." + +#: dev\view.py:38 +msgid "Execute" +msgstr "Yürüt" + +#: dev\view.py:40 +msgid "Code to Execute" +msgstr "Yürütülecek Kod" + +#: dev\view.py:42 +msgid "Write your code here..." +msgstr "Kodunuzu buraya yazın..." + diff --git a/dev/locales/uk-UA.po b/dev/locales/uk-UA.po new file mode 100644 index 0000000..9ee5ace --- /dev/null +++ b/dev/locales/uk-UA.po @@ -0,0 +1,90 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Ukrainian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: uk\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/dev/locales/messages.pot\n" +"X-Crowdin-File-ID: 263\n" +"Language: uk_UA\n" + +#: dev\dev.py:468 +#, docstring +msgid "Various development focused utilities!" +msgstr "Різноманітні утиліти, орієнтовані на розвиток!" + +#: dev\dev.py:745 dev\dev.py:807 dev\dev.py:1014 +msgid "Unreadable attachment with `utf-8`." +msgstr "Нечитабельне вкладення з `utf-8`." + +#: dev\dev.py:762 +msgid "This message isn't reachable." +msgstr "Це повідомлення недоступне." + +#: dev\dev.py:868 +msgid "Already running a REPL session in this channel. Exit it with `quit`." +msgstr "У цьому каналі вже запущено сеанс REPL. Вийдіть з нього за допомогою `quit`." + +#: dev\dev.py:872 +msgid "Already running a REPL session in this channel. Resume the REPL with `{prefix}replresume`." +msgstr "На цьому каналі вже запущено сеанс REPL. Відновіть REPL за допомогою `{prefix}replresume`." + +#: dev\dev.py:884 +msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit. `{prefix}replpause` to pause." +msgstr "Введіть код для виконання або обчислення. `exit()` або `quit` для виходу. `{prefix}replpause` щоб поставити на паузу." + +#: dev\dev.py:929 +msgid "Exiting." +msgstr "Виходьте." + +#: dev\dev.py:937 +msgid "There is no currently running REPL session in this channel." +msgstr "На цьому каналі немає поточної сесії REPL." + +#: dev\dev.py:943 +msgid "The REPL session in this channel has been resumed." +msgstr "Сесію REPL на цьому каналі відновлено." + +#: dev\dev.py:945 +msgid "The REPL session in this channel is now paused." +msgstr "Сеанс REPL у цьому каналі призупинено." + +#: dev\dev.py:967 +msgid "Bot owners will now bypass all commands with cooldowns{optional_duration}." +msgstr "Власники ботів тепер будуть обходити всі команди з перезавантаженням{optional_duration}." + +#: dev\dev.py:977 +msgid "Bot owners will no longer bypass all commands with cooldowns{optional_duration}." +msgstr "Власники ботів більше не будуть обходити всі команди з перезавантаженням{optional_duration}." + +#: dev\dev.py:1103 +#, docstring +msgid "Display all Dev environment values." +msgstr "Відобразити всі значення середовища Dev." + +#: dev\dev.py:1112 +#, docstring +msgid "Reset its own locals in evals." +msgstr "Скинути власні локалі в оцінках." + +#: dev\view.py:38 +msgid "Execute" +msgstr "Виконати" + +#: dev\view.py:40 +msgid "Code to Execute" +msgstr "Код для виконання" + +#: dev\view.py:42 +msgid "Write your code here..." +msgstr "Напишіть свій код тут..." + diff --git a/dev/utils_version.json b/dev/utils_version.json new file mode 100644 index 0000000..bfab002 --- /dev/null +++ b/dev/utils_version.json @@ -0,0 +1 @@ +{"needed_utils_version": 7.0} \ No newline at end of file diff --git a/dev/view.py b/dev/view.py new file mode 100644 index 0000000..e325acd --- /dev/null +++ b/dev/view.py @@ -0,0 +1,128 @@ +from AAA3A_utils import CogsUtils # isort:skip +from redbot.core import commands # isort:skip +from redbot.core.i18n import Translator # isort:skip +import discord # isort:skip +import typing # isort:skip + +import asyncio +import io +import textwrap + +from redbot.core.dev_commands import START_CODE_BLOCK_RE + +_: Translator = Translator("Dev", __file__) + + +def cleanup_code(code: str) -> str: + if code.startswith("```") and code.endswith("```"): + code = START_CODE_BLOCK_RE.sub("", code)[:-3].rstrip("\n") + else: + code = code.strip("\n`") + code = textwrap.dedent(code) + with io.StringIO(code) as codeio: + for line in codeio: + line = line.strip() + if line and not line.startswith("#"): + break + else: + return "pass" + return code + + +class ExecuteModal(discord.ui.Modal): + def __init__( + self, + cog: commands.Cog, + ctx: commands.Context, + choice: typing.Literal["debug", "eval"] = "eval", + ) -> None: + self.cog: commands.Cog = cog + self.ctx: commands.Context = ctx + self.choice: typing.Literal["debug", "eval"] = choice + super().__init__(title=_("Execute") + " " + self.choice.title(), timeout=60 * 10) + self.code: discord.ui.TextInput = discord.ui.TextInput( + label=_("Code to Execute"), + style=discord.TextStyle.long, + placeholder=_("Write your code here..."), + required=True, + ) + self.add_item(self.code) + + async def on_submit(self, interaction: discord.Interaction) -> None: + await interaction.response.defer() + context = getattr(self.ctx, "original_context", self.ctx) + context.interaction = interaction + code = self.code.value + source = cleanup_code(code) + await self.cog.my_exec( + context, + type="debug", + source=source, + send_result=True, + ) + + +class ExecuteView(discord.ui.View): + def __init__(self, cog: commands.Cog) -> None: + super().__init__(timeout=180) + self.cog: commands.Cog = cog + self.ctx: commands.Context = None + + self._message: discord.Message = None + self._ready: asyncio.Event = asyncio.Event() + + async def start(self, ctx: commands.Context) -> discord.Message: + self.ctx: commands.Context = ctx + self._message: discord.Message = await self.ctx.send(view=self) + self.cog.views[self._message] = self + await self._ready.wait() + return self._message + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user.id not in [self.ctx.author.id] + list(self.ctx.bot.owner_ids): + await interaction.response.send_message( + _("You are not allowed to use this interaction."), ephemeral=True + ) + return False + return True + + async def on_timeout(self) -> None: + for child in self.children: + child: discord.ui.Item + if hasattr(child, "disabled") and not ( + isinstance(child, discord.ui.Button) and child.style == discord.ButtonStyle.url + ): + child.disabled = True + try: + await self._message.edit(view=self) + except discord.HTTPException: + pass + self._ready.set() + + @discord.ui.button(label=_("Execute Debug"), custom_id="execute_debug") + async def execute_debug( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + await interaction.response.send_modal( + ExecuteModal(cog=self.cog, ctx=self.ctx, choice="debug") + ) + + @discord.ui.button(label=_("Execute Eval"), custom_id="execute_eval") + async def execute_eval( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + await interaction.response.send_modal( + ExecuteModal(cog=self.cog, ctx=self.ctx, choice="eval") + ) + + @discord.ui.button(style=discord.ButtonStyle.danger, emoji="✖️", custom_id="close_page") + async def close_page( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + try: + await interaction.response.defer() + except discord.errors.NotFound: + pass + self.stop() + await CogsUtils.delete_message(self._message) + self._ready.set() diff --git a/devutils/README.rst b/devutils/README.rst new file mode 100644 index 0000000..fc7b37a --- /dev/null +++ b/devutils/README.rst @@ -0,0 +1,91 @@ +.. _devutils: +======== +DevUtils +======== + +This is the cog guide for the ``DevUtils`` cog. This guide contains the collection of commands which you can use in the cog. +Through this guide, ``[p]`` will always represent your prefix. Replace ``[p]`` with your own prefix when you use these commands in Discord. + +.. note:: + + Ensure that you are up to date by running ``[p]cog update devutils``. + If there is something missing, or something that needs improving in this documentation, feel free to create an issue `here `_. + This documentation is generated everytime this cog receives an update. + +--------------- +About this cog: +--------------- + +Various development utilities! + +--------- +Commands: +--------- + +Here are all the commands included in this cog (10): + +* ``[p]devutils`` + Various development utilities. + +* ``[p]devutils bypass `` + Bypass a command's checks and cooldowns. + +* ``[p]devutils do [sequential=True] `` + Repeats a command a specified number of times. + +* ``[p]devutils execute [sequential=True] `` + Execute multiple commands at once. Split them using |. + +* ``[p]devutils loglevel [logger_name=red]`` + Change the logging level for a logger. If no name is provided, the root logger (`red`) is used. + +* ``[p]devutils rawrequest `` + Display the JSON of a Discord object with a raw request. + +* ``[p]devutils reinvoke [message]`` + Reinvoke a command message. + +* ``[p]devutils reloadmodule [modules]...`` + Force reload a module (to use code changes without restarting your bot). + +* ``[p]devutils stoptyping`` + Stop all bot typing tasks. + +* ``[p]devutils timing `` + Run a command timing execution and catching exceptions. + +------------ +Installation +------------ + +If you haven't added my repo before, lets add it first. We'll call it "AAA3A-cogs" here. + +.. code-block:: ini + + [p]repo add AAA3A-cogs https://github.com/AAA3A-AAA3A/AAA3A-cogs + +Now, we can install DevUtils. + +.. code-block:: ini + + [p]cog install AAA3A-cogs devutils + +Once it's installed, it is not loaded by default. Load it by running the following command: + +.. code-block:: ini + + [p]load devutils + +---------------- +Further Support: +---------------- + +Check out my docs `here `_. +Mention me in the #support_other-cogs in the `cog support server `_ if you need any help. +Additionally, feel free to open an issue or pull request to this repo. + +-------- +Credits: +-------- + +Thanks to Kreusada for the Python code to automatically generate this documentation! \ No newline at end of file diff --git a/devutils/__init__.py b/devutils/__init__.py new file mode 100644 index 0000000..7b79346 --- /dev/null +++ b/devutils/__init__.py @@ -0,0 +1,46 @@ +from redbot.core import errors # isort:skip +import importlib +import sys + +try: + import AAA3A_utils +except ModuleNotFoundError: + raise errors.CogLoadError( + "The needed utils to run the cog were not found. Please execute the command `[p]pipinstall git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." + ) +modules = sorted( + [module for module in sys.modules if module.split(".")[0] == "AAA3A_utils"], reverse=True +) +for module in modules: + try: + importlib.reload(sys.modules[module]) + except ModuleNotFoundError: + pass +del AAA3A_utils +# import AAA3A_utils +# import json +# import os +# __version__ = AAA3A_utils.__version__ +# with open(os.path.join(os.path.dirname(__file__), "utils_version.json"), mode="r") as f: +# data = json.load(f) +# needed_utils_version = data["needed_utils_version"] +# if __version__ > needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a higher version than the one supported by this version of the cog. Please update the cogs of the `AAA3A-cogs` repo." +# ) +# elif __version__ < needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a lower version than the one supported by this version of the cog. Please execute the command `[p]pipinstall --upgrade git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." +# ) + +from redbot.core.bot import Red # isort:skip +from redbot.core.utils import get_end_user_data_statement + +from .devutils import DevUtils + +__red_end_user_data_statement__ = get_end_user_data_statement(file=__file__) + + +async def setup(bot: Red) -> None: + cog = DevUtils(bot) + await bot.add_cog(cog) diff --git a/devutils/devutils.py b/devutils/devutils.py new file mode 100644 index 0000000..6c96475 --- /dev/null +++ b/devutils/devutils.py @@ -0,0 +1,420 @@ +from AAA3A_utils import Cog, CogsUtils, Menu # isort:skip +from redbot.core import commands # isort:skip +from redbot.core.bot import Red # isort:skip +from redbot.core.i18n import Translator, cog_i18n # isort:skip +import discord # isort:skip +import typing # isort:skip + +import asyncio +import importlib +import json +import logging +import re +import sys +import time + +from discord.http import Route +from red_commons.logging import TRACE, VERBOSE, getLogger +from redbot.core.utils.chat_formatting import humanize_list + +# Credits: +# General repo credits. +# Thanks to Phen for the original code (https://github.com/phenom4n4n/phen-cogs/tree/master/phenutils)! + +_: Translator = Translator("Devutils", __file__) + +SLEEP_FLAG = re.compile(r"(?:--|—)sleep (\d+)$") + + +class LogLevelConverter(commands.Converter): + async def convert(self, ctx: commands.Context, argument: str) -> int: + levels = { + "CRITICAL": logging.CRITICAL, + "ERROR": logging.ERROR, + "WARNING": logging.WARNING, + "INFO": logging.INFO, + "DEBUG": logging.DEBUG, + "VERBOSE": VERBOSE, + "TRACE": TRACE, + } + if argument.upper() in levels: + return levels[argument.upper()] + try: + argument = int(argument) + except ValueError: + pass + else: + try: + return list(levels.values())[argument] + except IndexError: + pass + raise commands.BadArgument(_("No valid log level provided.")) + + +class StrConverter(commands.Converter): + async def convert(self, ctx: commands.Context, argument: str) -> str: + return argument + + +class RawRequestConverter(commands.Converter): + async def convert(self, ctx: commands.Context, argument: str): + _types = [ + discord.Guild, + discord.abc.GuildChannel, + discord.Thread, + discord.Member, + discord.User, + discord.Role, + discord.Emoji, + discord.Message, + discord.Invite, + ] + # _types = list(discord.ext.commands.converter.CONVERTER_MAPPING.keys())[1:] + for _type in _types: + try: + return await discord.ext.commands.converter.CONVERTER_MAPPING[_type]().convert( + ctx, argument + ) + except commands.BadArgument: + pass + raise commands.BadArgument(_("No valid discord object provided.")) + + +@cog_i18n(_) +class DevUtils(Cog): + """Various development utilities!""" + + __authors__: typing.List[str] = ["PhenoM4n4n", "AAA3A"] + + @commands.Cog.listener() + async def on_message_without_command(self, message: discord.Message): + if await self.bot.cog_disabled_in_guild( + cog=self, guild=message.guild + ) or not await self.bot.allowed_by_whitelist_blacklist(who=message.author): + return + if message.webhook_id is not None or message.author.bot: + return + context = await self.bot.get_context(message) + if context.prefix is None: + return + command = context.message.content[len(str(context.prefix)) :] + if len(command.split(" ")) == 0: + return + command_name = command.split(" ")[0] + if command_name not in ( + "do", + "execute", + "bypass", + "timing", + "reinvoke", + "loglevel", + "stoptyping", + "reloadmodule", + "rawrequest", + ): + return + await CogsUtils.invoke_command( + bot=self.bot, + author=context.author, + channel=context.channel, + command=f"devutils {command}", + prefix=context.prefix, + message=context.message, + ) + + @commands.is_owner() + @commands.hybrid_group() + async def devutils(self, ctx: commands.Context) -> None: + """Various development utilities.""" + pass + + @devutils.command() + async def do( + self, ctx, times: int, sequential: typing.Optional[bool] = True, *, command: str + ) -> None: + """ + Repeats a command a specified number of times. + + `--sleep ` is an optional flag specifying how much time to wait between command invocations. + """ + if (match := SLEEP_FLAG.search(command)) is not None: + sleep = int(match.group(1)) + command = command[: -len(match.group(0))] + else: + sleep = 1 + + new_ctx = await CogsUtils.invoke_command( + bot=ctx.bot, + author=ctx.author, + channel=ctx.channel, + command=command, + prefix=ctx.prefix, + message=ctx.message, + invoke=False, + ) + if not new_ctx.valid: + raise commands.UserFeedbackCheckFailure(_("You have not specified a correct command.")) + if not await discord.utils.async_all([check(new_ctx) for check in new_ctx.command.checks]): + raise commands.UserFeedbackCheckFailure(_("You can't execute yourself this command.")) + if sequential: + for __ in range(times): + await ctx.bot.invoke(new_ctx) + await asyncio.sleep(sleep) + else: + todo = [ctx.bot.invoke(new_ctx) for _ in range(times)] + await asyncio.gather(*todo) + + @devutils.command() + async def execute( + self, + ctx: commands.Context, + sequential: typing.Optional[bool] = True, + *, + commands_list: str, + ) -> None: + """Execute multiple commands at once. Split them using |.""" + commands_list = [command.strip() for command in commands_list.split("|")] + if sequential: + for command in commands_list: + new_ctx = await CogsUtils.invoke_command( + bot=ctx.bot, + author=ctx.author, + channel=ctx.channel, + command=command, + prefix=ctx.prefix, + message=ctx.message, + invoke=True, + ) + if not new_ctx.valid: + raise commands.UserFeedbackCheckFailure( + _("`{command}` isn't a valid command.").format(command=command) + ) + if not await discord.utils.async_all( + [check(new_ctx) for check in new_ctx.command.checks] + ): + raise commands.UserFeedbackCheckFailure( + _("You can't execute yourself `{command}`.").format(command=command) + ) + else: + todo = [] + for command in commands_list: + new_ctx = await CogsUtils.invoke_command( + bot=ctx.bot, + author=ctx.author, + channel=ctx.channel, + command=command, + prefix=ctx.prefix, + message=ctx.message, + invoke=False, + ) + if not new_ctx.valid: + raise commands.UserFeedbackCheckFailure( + _("`{command}` isn't a valid command.").format(command=command) + ) + if not await discord.utils.async_all( + [check(new_ctx) for check in new_ctx.command.checks] + ): + raise commands.UserFeedbackCheckFailure( + _("You can't execute yourself `{command}`.").format(command=command) + ) + todo.append(ctx.bot.invoke(new_ctx)) + await asyncio.gather(*todo) + + @devutils.command() + async def bypass(self, ctx: commands.Context, *, command: str) -> None: + """Bypass a command's checks and cooldowns.""" + new_ctx = await CogsUtils.invoke_command( + bot=ctx.bot, + author=ctx.author, + channel=ctx.channel, + command=command, + prefix=ctx.prefix, + message=ctx.message, + invoke=False, + ) + if not new_ctx.valid: + raise commands.UserFeedbackCheckFailure(_("You have not specified a correct command.")) + await new_ctx.reinvoke() + + @devutils.command() + async def timing(self, ctx: commands.Context, *, command: str) -> None: + """Run a command timing execution and catching exceptions.""" + new_ctx = await CogsUtils.invoke_command( + bot=ctx.bot, + author=ctx.author, + channel=ctx.channel, + command=command, + prefix=ctx.prefix, + message=ctx.message, + invoke=False, + ) + if not new_ctx.valid: + raise commands.UserFeedbackCheckFailure(_("You have not specified a correct command.")) + if not await discord.utils.async_all([check(new_ctx) for check in new_ctx.command.checks]): + raise commands.UserFeedbackCheckFailure(_("You can't execute yourself this command.")) + + start = time.perf_counter() + await ctx.bot.invoke(new_ctx) + end = time.perf_counter() + return await ctx.send( + _("Command `{command}` finished in `{timing}`s.").format( + command=new_ctx.command.qualified_name, timing=f"{end - start:.3f}" + ) + ) + + @devutils.command() + async def reinvoke(self, ctx: commands.Context, message: discord.Message = None) -> None: + """Reinvoke a command message. + + You may reply to a message to reinvoke it or pass a message ID/link. + The command will be invoked with the author and the channel of the specified message. + """ + if message is None: + if not ( + ctx.message.reference is not None + and isinstance((message := ctx.message.reference.resolved), discord.Message) + ): + raise commands.UserInputError() + new_ctx = await CogsUtils.invoke_command( + bot=ctx.bot, + author=message.author, + channel=message.channel, + command=( + f"{ctx.prefix}devutils reinvoke{message.content[len(ctx.prefix)+8:]}" + if message.content.startswith(f"{ctx.prefix}reinvoke") + else message.content + ), + prefix="", + message=message, + ) + if not new_ctx.valid: + raise commands.UserFeedbackCheckFailure(_("The command isn't valid.")) + if not await discord.utils.async_all([check(new_ctx) for check in new_ctx.command.checks]): + raise commands.UserFeedbackCheckFailure(_("This command can't be executed.")) + + @devutils.command() + async def loglevel( + self, ctx: commands.Context, level: LogLevelConverter, logger_name: str = "red" + ) -> None: + """Change the logging level for a logger. If no name is provided, the root logger (`red`) is used. + + Levels are the following: + - `0`: `CRITICAL` + - `1`: `ERROR` + - `2`: `WARNING` + - `3`: `INFO` + - `4`: `DEBUG` + - `5`: `VERBOSE` + - `6`: `TRACE` + """ + logger = getLogger(logger_name) + logger.setLevel(level) + await ctx.send( + _("Logger `{logger_name}` level set to `{level}`.").format( + level=logging.getLevelName(logger.level), logger_name=logger_name + ) + ) + + @devutils.command() + async def stoptyping(self, ctx: commands.Context) -> None: + """Stop all bot typing tasks.""" + tasks = [] + was_typing = False + for task in asyncio.all_tasks(): + if task.get_stack(limit=1)[0].f_code.co_name == "do_typing": + tasks.append(task) + was_typing = True + if not was_typing: + raise commands.UserFeedbackCheckFailure("Hmm, it doesn't look like I'm typing...") + for task in tasks: + task.cancel() + + @devutils.command() + async def reloadmodule( + self, ctx: commands.Context, modules: commands.Greedy[StrConverter] + ) -> None: + """Force reload a module (to use code changes without restarting your bot). + + ⚠️ Please only use this if you know what you're doing. + """ + _modules = [] + for module in modules: + _modules.extend( + [ + m + for m in sys.modules + if m.split(".")[: len(module.split("."))] == module.split(".") + ] + ) + modules = sorted(_modules, reverse=True) + if not modules: + raise commands.UserFeedbackCheckFailure( + _("I couldn't find any module with this name.") + ) + for module in modules: + importlib.reload(sys.modules[module]) + text = _("Module(s) {modules} reloaded.").format( + modules=humanize_list([f"`{module}`" for module in modules]) + ) + if len(text) <= 2000: + await ctx.send(text) + else: + await ctx.send(_("Modules [...] reloaded.")) + + @devutils.command(aliases=["rawcontent"]) + async def rawrequest(self, ctx: commands.Context, *, thing: RawRequestConverter) -> None: + """Display the JSON of a Discord object with a raw request.""" + if isinstance(thing, discord.Guild): + raw_content = await ctx.bot.http.request( + route=Route(method="GET", path="/guilds/{guild_id}", guild_id=thing.id) + ) + elif isinstance(thing, (discord.abc.GuildChannel, discord.Thread)): + raw_content = await ctx.bot.http.request( + route=Route(method="GET", path="/channels/{channel_id}", channel_id=thing.id) + ) + elif isinstance(thing, discord.Member): + raw_content = await ctx.bot.http.request( + route=Route( + method="GET", + path="/guilds/{guild_id}/members/{user_id}", + guild_id=thing.guild.id, + user_id=thing.id, + ) + ) + elif isinstance(thing, discord.User): + raw_content = await ctx.bot.http.request( + route=Route(method="GET", path="/users/{user_id}", user_id=thing.id) + ) + elif isinstance(thing, discord.Role): + raw_content = [ + role + for role in await ctx.bot.http.request( + route=Route( + method="GET", path="/guilds/{guild_id}/roles", guild_id=thing.guild.id + ) + ) + if int(role["id"]) == thing.id + ][0] + elif isinstance(thing, discord.Emoji): + raw_content = await ctx.bot.http.request( + route=Route( + method="GET", + path="/guilds/{guild_id}/emojis/{emoji_id}", + guild_id=thing.guild.id, + emoji_id=thing.id, + ) + ) + elif isinstance(thing, discord.Message): + raw_content = await ctx.bot.http.request( + route=Route( + method="GET", + path="/channels/{channel_id}/messages/{message_id}", + channel_id=thing.channel.id, + message_id=thing.id, + ) + ) + elif isinstance(thing, discord.Invite): + raw_content = await ctx.bot.http.request( + route=Route(method="GET", path="/invites/{invite_code}", invite_code=thing.code) + ) + await Menu(json.dumps(raw_content, indent=4), lang="json").start(ctx) diff --git a/devutils/info.json b/devutils/info.json new file mode 100644 index 0000000..dc3c3a9 --- /dev/null +++ b/devutils/info.json @@ -0,0 +1,15 @@ +{ + "author": ["PhenoM4n4n", "AAA3A"], + "name": "DevUtils", + "install_msg": "Thank you for installing this cog!\nDo `[p]help CogName` to get the list of commands and their description. If you enjoy my work, please consider donating on [Buy Me a Coffee]() or [Ko-Fi]()!", + "short": "Various development utilities!", + "description": "Various development utilities!", + "tags": [ + "dev", + "utility", + "dpy" + ], + "requirements": ["git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git"], + "min_bot_version": "3.5.0", + "end_user_data_statement": "This cog does not persistently store data or metadata about users." +} \ No newline at end of file diff --git a/devutils/locales/de-DE.po b/devutils/locales/de-DE.po new file mode 100644 index 0000000..6e7790c --- /dev/null +++ b/devutils/locales/de-DE.po @@ -0,0 +1,150 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: German\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: de\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/devutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 312\n" +"Language: de_DE\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "Keine gültige Protokollebene angegeben." + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "Kein gültiges Discord-Objekt angegeben." + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "Verschiedene Entwicklungshilfsmittel!" + +#: devutils\devutils.py:134 +#, docstring +msgid "\n" +" Repeats a command a specified number of times.\n\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "\n" +" Wiederholt einen Befehl eine bestimmte Anzahl von Malen.\n\n" +" `--sleep ` ist ein optionales Flag, das angibt, wie viel Zeit zwischen den Befehlsaufrufen gewartet werden soll.\n" +" " + +#: devutils\devutils.py:155 devutils\devutils.py:235 devutils\devutils.py:251 +msgid "You have not specified a correct command." +msgstr "Sie haben keinen korrekten Befehl angegeben." + +#: devutils\devutils.py:157 devutils\devutils.py:253 +msgid "You can't execute yourself this command." +msgstr "Sie können diesen Befehl nicht selbst ausführen." + +#: devutils\devutils.py:174 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "Führen Sie mehrere Befehle auf einmal aus. Trennen Sie sie mit |." + +#: devutils\devutils.py:189 devutils\devutils.py:211 +msgid "`{command}` isn't a valid command." +msgstr "{command}\" ist kein gültiger Befehl." + +#: devutils\devutils.py:195 devutils\devutils.py:217 +msgid "You can't execute yourself `{command}`." +msgstr "Sie können sich nicht selbst hinrichten `{command}`." + +#: devutils\devutils.py:224 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "Umgehe die Kontrollen und Abklingzeiten eines Befehls." + +#: devutils\devutils.py:240 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "Führen Sie einen Befehl aus, um die Ausführung zu timen und Ausnahmen abzufangen." + +#: devutils\devutils.py:259 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "Der Befehl `{command}` wurde in `{timing}`s beendet." + +#: devutils\devutils.py:266 +#, docstring +msgid "Reinvoke a command message.\n\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "Widerrufen Sie eine Befehlsnachricht.\n\n" +" Sie können auf eine Nachricht antworten, um sie zu widerrufen, oder eine Nachrichten-ID/einen Link übergeben.\n" +" Der Befehl wird mit dem Autor und dem Kanal der angegebenen Nachricht aufgerufen.\n" +" " + +#: devutils\devutils.py:289 +msgid "The command isn't valid." +msgstr "Der Befehl ist ungültig." + +#: devutils\devutils.py:291 +msgid "This command can't be executed." +msgstr "Dieser Befehl kann nicht ausgeführt werden." + +#: devutils\devutils.py:297 +#, docstring +msgid "Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "Ändert die Protokollierungsstufe für einen Logger. Wenn kein Name angegeben wird, wird der Root-Logger (`red`) verwendet.\n\n" +" Die Stufen sind die folgenden:\n" +" - `0`: `KRITISCH`\n" +" - `1`: `FEHLER`\n" +" - `2`: `WARNUNG`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " + +#: devutils\devutils.py:311 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "Logger `{logger_name}` Level auf `{level}` gesetzt." + +#: devutils\devutils.py:320 +#, docstring +msgid "Force reload a module (to use code changes without restarting your bot).\n\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "Erzwingen Sie das Neuladen eines Moduls (um Codeänderungen zu verwenden, ohne den Bot neu zu starten).\n\n" +" ⚠️ Bitte verwenden Sie dies nur, wenn Sie wissen, was Sie tun.\n" +" " + +#: devutils\devutils.py:336 +msgid "I couldn't find any module with this name." +msgstr "Ich konnte kein Modul mit diesem Namen finden." + +#: devutils\devutils.py:340 +msgid "Module(s) {modules} reloaded." +msgstr "Modul(e) {modules} neu geladen." + +#: devutils\devutils.py:346 +msgid "Modules [...] reloaded." +msgstr "Module [...] neu geladen." + +#: devutils\devutils.py:350 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "Anzeige des JSON eines Discord-Objekts mit einer Rohanfrage." + diff --git a/devutils/locales/el-GR.po b/devutils/locales/el-GR.po new file mode 100644 index 0000000..0c2e6da --- /dev/null +++ b/devutils/locales/el-GR.po @@ -0,0 +1,150 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Greek\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: el\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/devutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 312\n" +"Language: el_GR\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "Δεν παρέχεται έγκυρο επίπεδο καταγραφής." + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "Δεν παρέχεται έγκυρο αντικείμενο discord." + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "Διάφορα βοηθητικά προγράμματα ανάπτυξης!" + +#: devutils\devutils.py:134 +#, docstring +msgid "\n" +" Repeats a command a specified number of times.\n\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "\n" +" Επαναλαμβάνει μια εντολή έναν καθορισμένο αριθμό φορών.\n\n" +" `--sleep ` είναι μια προαιρετική σημαία που καθορίζει πόσο χρόνο θα περιμένει μεταξύ των κλήσεων εντολών.\n" +" " + +#: devutils\devutils.py:155 devutils\devutils.py:235 devutils\devutils.py:251 +msgid "You have not specified a correct command." +msgstr "Δεν έχετε καθορίσει μια σωστή εντολή." + +#: devutils\devutils.py:157 devutils\devutils.py:253 +msgid "You can't execute yourself this command." +msgstr "Δεν μπορείτε να εκτελέσετε μόνοι σας αυτή την εντολή." + +#: devutils\devutils.py:174 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "Εκτέλεση πολλαπλών εντολών ταυτόχρονα. Διαχωρίστε τις με τη χρήση |." + +#: devutils\devutils.py:189 devutils\devutils.py:211 +msgid "`{command}` isn't a valid command." +msgstr "`{command}` δεν είναι έγκυρη εντολή." + +#: devutils\devutils.py:195 devutils\devutils.py:217 +msgid "You can't execute yourself `{command}`." +msgstr "Δεν μπορείτε να εκτελέσετε τον εαυτό σας `{command}`." + +#: devutils\devutils.py:224 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "Παράκαμψη των ελέγχων και των χρονικών ορίων μιας εντολής." + +#: devutils\devutils.py:240 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "Εκτέλεση μιας εντολής με χρονομέτρηση της εκτέλεσης και σύλληψη εξαιρέσεων." + +#: devutils\devutils.py:259 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "Η εντολή \"{command}\" ολοκληρώθηκε σε \"{timing}\" δευτερόλεπτα." + +#: devutils\devutils.py:266 +#, docstring +msgid "Reinvoke a command message.\n\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "Επαναφορά ενός μηνύματος εντολής.\n\n" +" Μπορείτε να απαντήσετε σε ένα μήνυμα για να το ανακαλέσετε ή να δώσετε ένα αναγνωριστικό/σύνδεσμο μηνύματος.\n" +" Η εντολή θα κληθεί με τον συντάκτη και το κανάλι του καθορισμένου μηνύματος.\n" +" " + +#: devutils\devutils.py:289 +msgid "The command isn't valid." +msgstr "Η εντολή δεν είναι έγκυρη." + +#: devutils\devutils.py:291 +msgid "This command can't be executed." +msgstr "Αυτή η εντολή δεν μπορεί να εκτελεστεί." + +#: devutils\devutils.py:297 +#, docstring +msgid "Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "Αλλαγή του επιπέδου καταγραφής για έναν καταγραφέα. Αν δεν δοθεί όνομα, χρησιμοποιείται ο ριζικός καταγραφέας (`red`).\n\n" +" Τα επίπεδα είναι τα εξής: Η καταγραφή των δεδομένων μπορεί να γίνει με τη χρήση των ακόλουθων επιπέδων:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " + +#: devutils\devutils.py:311 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "Το επίπεδο του καταγραφέα \"{logger_name}\" έχει οριστεί σε \"{level}\"." + +#: devutils\devutils.py:320 +#, docstring +msgid "Force reload a module (to use code changes without restarting your bot).\n\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "Αναγκαστική επαναφόρτωση μιας ενότητας (για να χρησιμοποιήσετε τις αλλαγές στον κώδικα χωρίς επανεκκίνηση του bot σας).\n\n" +" ⚠️ Παρακαλούμε χρησιμοποιήστε το μόνο αν ξέρετε τι κάνετε.\n" +" " + +#: devutils\devutils.py:336 +msgid "I couldn't find any module with this name." +msgstr "Δεν μπόρεσα να βρω καμία ενότητα με αυτό το όνομα." + +#: devutils\devutils.py:340 +msgid "Module(s) {modules} reloaded." +msgstr "Μονάδα(ες) {modules} επαναφορτώθηκε." + +#: devutils\devutils.py:346 +msgid "Modules [...] reloaded." +msgstr "Οι ενότητες [...] επαναφορτώθηκαν." + +#: devutils\devutils.py:350 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "Εμφάνιση του JSON ενός αντικειμένου Discord με ένα ακατέργαστο αίτημα." + diff --git a/devutils/locales/es-ES.po b/devutils/locales/es-ES.po new file mode 100644 index 0000000..ab3e312 --- /dev/null +++ b/devutils/locales/es-ES.po @@ -0,0 +1,150 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Spanish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: es-ES\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/devutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 312\n" +"Language: es_ES\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "No se ha proporcionado un nivel de registro válido." + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "No se ha proporcionado un objeto de discordia válido." + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "Varias utilidades de desarrollo" + +#: devutils\devutils.py:134 +#, docstring +msgid "\n" +" Repeats a command a specified number of times.\n\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "\n" +" Repite un comando un número especificado de veces.\n\n" +" `--sleep ` es un indicador opcional que especifica cuánto tiempo se debe esperar entre invocaciones de comandos.\n" +" " + +#: devutils\devutils.py:155 devutils\devutils.py:235 devutils\devutils.py:251 +msgid "You have not specified a correct command." +msgstr "No ha especificado un comando correcto." + +#: devutils\devutils.py:157 devutils\devutils.py:253 +msgid "You can't execute yourself this command." +msgstr "No puedes ejecutar tú mismo este comando." + +#: devutils\devutils.py:174 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "Ejecuta varios comandos a la vez. Divídelos utilizando |." + +#: devutils\devutils.py:189 devutils\devutils.py:211 +msgid "`{command}` isn't a valid command." +msgstr "`{command}` no es un comando válido." + +#: devutils\devutils.py:195 devutils\devutils.py:217 +msgid "You can't execute yourself `{command}`." +msgstr "No puedes ejecutarte a ti mismo `{command}`." + +#: devutils\devutils.py:224 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "Evita los controles y enfriamientos de un comando." + +#: devutils\devutils.py:240 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "Ejecutar un comando cronometrando la ejecución y capturando excepciones." + +#: devutils\devutils.py:259 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "El comando `{command}` finalizó en `{timing}`s." + +#: devutils\devutils.py:266 +#, docstring +msgid "Reinvoke a command message.\n\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "Reinvocar un mensaje de comando.\n\n" +" Puede responder a un mensaje para reinvocarlo o pasar un ID/enlace de mensaje.\n" +" El comando se invocará con el autor y el canal del mensaje especificado.\n" +" " + +#: devutils\devutils.py:289 +msgid "The command isn't valid." +msgstr "El comando no es válido." + +#: devutils\devutils.py:291 +msgid "This command can't be executed." +msgstr "Este comando no se puede ejecutar." + +#: devutils\devutils.py:297 +#, docstring +msgid "Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "Cambia el nivel de registro de un registrador. Si no se proporciona ningún nombre, se utilizará el registrador raíz (`red`).\n\n" +" Los niveles son los siguientes\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " + +#: devutils\devutils.py:311 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "Logger `{logger_name}` nivel establecido en `{level}`." + +#: devutils\devutils.py:320 +#, docstring +msgid "Force reload a module (to use code changes without restarting your bot).\n\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "Fuerza la recarga de un módulo (para usar los cambios de código sin reiniciar tu bot).\n\n" +" ⚠️ Por favor, usa esto sólo si sabes lo que estás haciendo.\n" +" " + +#: devutils\devutils.py:336 +msgid "I couldn't find any module with this name." +msgstr "No he podido encontrar ningún módulo con este nombre." + +#: devutils\devutils.py:340 +msgid "Module(s) {modules} reloaded." +msgstr "Módulo(s) {modules} recargado." + +#: devutils\devutils.py:346 +msgid "Modules [...] reloaded." +msgstr "Módulos [...] recargados." + +#: devutils\devutils.py:350 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "Muestra el JSON de un objeto Discord con una petición sin procesar." + diff --git a/devutils/locales/fi-FI.po b/devutils/locales/fi-FI.po new file mode 100644 index 0000000..1664f7a --- /dev/null +++ b/devutils/locales/fi-FI.po @@ -0,0 +1,150 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Finnish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fi\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/devutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 312\n" +"Language: fi_FI\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "Kelvollista lokitasoa ei ole annettu." + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "Kelvollista discord-objektia ei ole annettu." + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "Erilaisia kehitysapuohjelmia!" + +#: devutils\devutils.py:134 +#, docstring +msgid "\n" +" Repeats a command a specified number of times.\n\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "\n" +" Toistaa komennon tietyn määrän kertoja.\n\n" +" `--sleep ` on valinnainen lippu, joka määrittää, kuinka kauan komentojen kutsujen välillä odotetaan.\n" +" " + +#: devutils\devutils.py:155 devutils\devutils.py:235 devutils\devutils.py:251 +msgid "You have not specified a correct command." +msgstr "Et ole antanut oikeaa komentoa." + +#: devutils\devutils.py:157 devutils\devutils.py:253 +msgid "You can't execute yourself this command." +msgstr "Et voi suorittaa tätä komentoa itse." + +#: devutils\devutils.py:174 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "Suorita useita komentoja kerralla. Jaa ne käyttämällä |." + +#: devutils\devutils.py:189 devutils\devutils.py:211 +msgid "`{command}` isn't a valid command." +msgstr "`{command}` ei ole kelvollinen komento." + +#: devutils\devutils.py:195 devutils\devutils.py:217 +msgid "You can't execute yourself `{command}`." +msgstr "Et voi teloittaa itseäsi `{command}`." + +#: devutils\devutils.py:224 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "Ohita komennon tarkistukset ja jäähdytysajat." + +#: devutils\devutils.py:240 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "Suorita komento ajoittaen suoritus ja poimien poikkeuksia." + +#: devutils\devutils.py:259 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "Komento `{command}` valmistui `{timing}`s:ssa." + +#: devutils\devutils.py:266 +#, docstring +msgid "Reinvoke a command message.\n\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "Komentoviestin peruuttaminen uudelleen.\n\n" +" Voit vastata viestiin peruuttaaksesi sen tai antaa viestin ID:n/linkin.\n" +" Komento käynnistetään määritetyn viestin kirjoittajalla ja kanavalla.\n" +" " + +#: devutils\devutils.py:289 +msgid "The command isn't valid." +msgstr "Komento ei ole voimassa." + +#: devutils\devutils.py:291 +msgid "This command can't be executed." +msgstr "Tätä komentoa ei voida suorittaa." + +#: devutils\devutils.py:297 +#, docstring +msgid "Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "Vaihda lokin kirjaustasoa. Jos nimeä ei anneta, käytetään juuriloggeria (`red`).\n\n" +" Tasot ovat seuraavat:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " + +#: devutils\devutils.py:311 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "Lokin \"{logger_name}\" taso on asetettu arvoon \"{level}\"." + +#: devutils\devutils.py:320 +#, docstring +msgid "Force reload a module (to use code changes without restarting your bot).\n\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "Pakota lataamaan moduuli uudelleen (jotta voit käyttää koodimuutoksia käynnistämättä bottiasi uudelleen).\n\n" +" ⚠️ Käytä tätä vain, jos tiedät, mitä olet tekemässä.\n" +" " + +#: devutils\devutils.py:336 +msgid "I couldn't find any module with this name." +msgstr "En löytänyt yhtään moduulia tällä nimellä." + +#: devutils\devutils.py:340 +msgid "Module(s) {modules} reloaded." +msgstr "Moduuli(t) {modules} ladattu uudelleen." + +#: devutils\devutils.py:346 +msgid "Modules [...] reloaded." +msgstr "Moduulit [...] ladattu uudelleen." + +#: devutils\devutils.py:350 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "Näyttää Discord-objektin JSON-tiedoston raa'alla pyynnöllä." + diff --git a/devutils/locales/fr-FR.po b/devutils/locales/fr-FR.po new file mode 100644 index 0000000..9fc2843 --- /dev/null +++ b/devutils/locales/fr-FR.po @@ -0,0 +1,150 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: French\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/devutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 312\n" +"Language: fr_FR\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "Aucun niveau d'enregistrement valide n'a été fourni." + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "Aucun objet de discorde valide n'a été fourni." + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "Divers utilitaires de développement !" + +#: devutils\devutils.py:134 +#, docstring +msgid "\n" +" Repeats a command a specified number of times.\n\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "\n" +" Répète une commande le nombre de fois spécifié.\n\n" +" `--sleep ` est un drapeau optionnel spécifiant le temps d'attente entre les invocations de commandes.\n" +" " + +#: devutils\devutils.py:155 devutils\devutils.py:235 devutils\devutils.py:251 +msgid "You have not specified a correct command." +msgstr "Vous n'avez pas spécifié une commande correcte." + +#: devutils\devutils.py:157 devutils\devutils.py:253 +msgid "You can't execute yourself this command." +msgstr "Vous ne pouvez pas exécuter vous-même cette commande." + +#: devutils\devutils.py:174 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "Exécuter plusieurs commandes à la fois. Divisez-les à l'aide de |." + +#: devutils\devutils.py:189 devutils\devutils.py:211 +msgid "`{command}` isn't a valid command." +msgstr "`{command}` n'est pas une commande valide." + +#: devutils\devutils.py:195 devutils\devutils.py:217 +msgid "You can't execute yourself `{command}`." +msgstr "Vous ne pouvez pas vous exécuter vous-même `{command}`." + +#: devutils\devutils.py:224 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "Contourner les contrôles et les durées de vie d'une commande." + +#: devutils\devutils.py:240 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "Exécuter une commande en chronométrant l'exécution et en capturant les exceptions." + +#: devutils\devutils.py:259 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "La commande `{command}` s'est terminée en `{timing}`s." + +#: devutils\devutils.py:266 +#, docstring +msgid "Reinvoke a command message.\n\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "Réinvoquer un message de commande.\n\n" +" Vous pouvez répondre à un message pour le réinvoquer ou transmettre un identifiant/lien de message.\n" +" La commande sera invoquée avec l'auteur et le canal du message spécifié.\n" +" " + +#: devutils\devutils.py:289 +msgid "The command isn't valid." +msgstr "La commande n'est pas valide." + +#: devutils\devutils.py:291 +msgid "This command can't be executed." +msgstr "Cette commande ne peut pas être exécutée." + +#: devutils\devutils.py:297 +#, docstring +msgid "Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "Modifier le niveau de journalisation d'un enregistreur. Si aucun nom n'est fourni, le logger racine (`red`) est utilisé.\n\n" +" Les niveaux sont les suivants :\n" +" - `0` : `CRITICAL`\n" +" - `1` : `ERROR`\n" +" - `2` : `WARNING`\n" +" - `3` : `INFO`\n" +" - `4` : `DEBUG`\n" +" - `5` : `VERBOSE`\n" +" - `6` : `TRACE`\n" +" " + +#: devutils\devutils.py:311 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "Le niveau du logger `{logger_name}` est fixé à `{level}`." + +#: devutils\devutils.py:320 +#, docstring +msgid "Force reload a module (to use code changes without restarting your bot).\n\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "Forcer le rechargement d'un module (pour utiliser les changements de code sans redémarrer le bot).\n\n" +" ⚠️ N'utilisez cette option que si vous savez ce que vous faites.\n" +" " + +#: devutils\devutils.py:336 +msgid "I couldn't find any module with this name." +msgstr "Je n'ai trouvé aucun module portant ce nom." + +#: devutils\devutils.py:340 +msgid "Module(s) {modules} reloaded." +msgstr "Module(s) {modules} rechargé(s)." + +#: devutils\devutils.py:346 +msgid "Modules [...] reloaded." +msgstr "Modules [...] rechargés." + +#: devutils\devutils.py:350 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "Affiche le JSON d'un objet Discord avec une requête brute." + diff --git a/devutils/locales/it-IT.po b/devutils/locales/it-IT.po new file mode 100644 index 0000000..5c96030 --- /dev/null +++ b/devutils/locales/it-IT.po @@ -0,0 +1,150 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Italian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: it\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/devutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 312\n" +"Language: it_IT\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "Non è stato fornito un livello di log valido." + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "Non è stato fornito un oggetto discordia valido." + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "Diverse utilità di sviluppo!" + +#: devutils\devutils.py:134 +#, docstring +msgid "\n" +" Repeats a command a specified number of times.\n\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "\n" +" Ripete un comando per un numero specifico di volte.\n\n" +" `--sleep ` è un flag opzionale che specifica il tempo di attesa tra un comando e l'altro.\n" +" " + +#: devutils\devutils.py:155 devutils\devutils.py:235 devutils\devutils.py:251 +msgid "You have not specified a correct command." +msgstr "Non è stato specificato un comando corretto." + +#: devutils\devutils.py:157 devutils\devutils.py:253 +msgid "You can't execute yourself this command." +msgstr "Non è possibile eseguire da soli questo comando." + +#: devutils\devutils.py:174 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "Eseguire più comandi contemporaneamente. Divideteli usando |." + +#: devutils\devutils.py:189 devutils\devutils.py:211 +msgid "`{command}` isn't a valid command." +msgstr "`{command}` non è un comando valido." + +#: devutils\devutils.py:195 devutils\devutils.py:217 +msgid "You can't execute yourself `{command}`." +msgstr "Non è possibile eseguire se stessi `{command}`." + +#: devutils\devutils.py:224 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "Bypassare i controlli e i cooldown di un comando." + +#: devutils\devutils.py:240 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "Eseguire un comando temporizzando l'esecuzione e catturando le eccezioni." + +#: devutils\devutils.py:259 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "Il comando `{command}` è terminato in `{timing}`s." + +#: devutils\devutils.py:266 +#, docstring +msgid "Reinvoke a command message.\n\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "Revocare un messaggio di comando.\n\n" +" È possibile rispondere a un messaggio per revocarlo o passare un ID/link del messaggio.\n" +" Il comando verrà invocato con l'autore e il canale del messaggio specificato.\n" +" " + +#: devutils\devutils.py:289 +msgid "The command isn't valid." +msgstr "Il comando non è valido." + +#: devutils\devutils.py:291 +msgid "This command can't be executed." +msgstr "Questo comando non può essere eseguito." + +#: devutils\devutils.py:297 +#, docstring +msgid "Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "Cambia il livello di registrazione di un logger. Se non viene fornito alcun nome, viene utilizzato il logger principale (`red`).\n\n" +" I livelli sono i seguenti:\n" +" - `0`: `CRITICO`\n" +" - `1`: `ERRORE`\n" +" - `2`: `AVVISO`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACCIA`\n" +" " + +#: devutils\devutils.py:311 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "Il livello del logger `{logger_name}` è impostato su `{level}`." + +#: devutils\devutils.py:320 +#, docstring +msgid "Force reload a module (to use code changes without restarting your bot).\n\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "Forza il ricaricamento di un modulo (per utilizzare le modifiche al codice senza riavviare il bot).\n\n" +" ⚠️ Usare questa funzione solo se si sa cosa si sta facendo.\n" +" " + +#: devutils\devutils.py:336 +msgid "I couldn't find any module with this name." +msgstr "Non ho trovato nessun modulo con questo nome." + +#: devutils\devutils.py:340 +msgid "Module(s) {modules} reloaded." +msgstr "Modulo(i) {modules} ricaricato." + +#: devutils\devutils.py:346 +msgid "Modules [...] reloaded." +msgstr "Moduli [...] ricaricati." + +#: devutils\devutils.py:350 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "Visualizza il JSON di un oggetto Discord con una richiesta grezza." + diff --git a/devutils/locales/ja-JP.po b/devutils/locales/ja-JP.po new file mode 100644 index 0000000..06dce96 --- /dev/null +++ b/devutils/locales/ja-JP.po @@ -0,0 +1,150 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Japanese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ja\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/devutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 312\n" +"Language: ja_JP\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "有効なログレベルが提供されていません。" + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "有効な discord オブジェクトが提供されていません。" + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "様々な開発ユーティリティ!" + +#: devutils\devutils.py:134 +#, docstring +msgid "\n" +" Repeats a command a specified number of times.\n\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "\n" +" コマンドを指定された回数繰り返す。\n\n" +" `--sleep` はオプションのフラグで、コマンドを実行するまでの待ち時間を指定する。\n" +" " + +#: devutils\devutils.py:155 devutils\devutils.py:235 devutils\devutils.py:251 +msgid "You have not specified a correct command." +msgstr "正しいコマンドを指定していません。" + +#: devutils\devutils.py:157 devutils\devutils.py:253 +msgid "You can't execute yourself this command." +msgstr "このコマンドを自分で実行することはできない。" + +#: devutils\devutils.py:174 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "複数のコマンドを一度に実行する。で分割する。" + +#: devutils\devutils.py:189 devutils\devutils.py:211 +msgid "`{command}` isn't a valid command." +msgstr "{command}` は有効なコマンドではない。" + +#: devutils\devutils.py:195 devutils\devutils.py:217 +msgid "You can't execute yourself `{command}`." +msgstr "{command}`を自分で実行することはできない。" + +#: devutils\devutils.py:224 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "コマンドのチェックとクールダウンを回避する。" + +#: devutils\devutils.py:240 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "コマンドの実行タイミングを計り、例外をキャッチする。" + +#: devutils\devutils.py:259 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "コマンド `{command}` は `{timing}`s で終了した。" + +#: devutils\devutils.py:266 +#, docstring +msgid "Reinvoke a command message.\n\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "コマンド・メッセージを取り消す。\n\n" +" メッセージに返信して取り消すか、メッセージID/リンクを渡す。\n" +" コマンドは指定されたメッセージの作者とチャンネルで起動されます。\n" +" " + +#: devutils\devutils.py:289 +msgid "The command isn't valid." +msgstr "コマンドが有効でない。" + +#: devutils\devutils.py:291 +msgid "This command can't be executed." +msgstr "このコマンドは実行できない。" + +#: devutils\devutils.py:297 +#, docstring +msgid "Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "ロガーのロギングレベルを変更する。名前を指定しない場合、ルートロガー(`red`)が使用される。\n\n" +" レベルは以下のとおりである:\n" +" - 0`: `critical`\n" +" - `1`: `error`\n" +" - `2`: `warning`\n" +" - `3`: `info`\n" +" - `4`: `debug`\n" +" - `5`: `verbose`\n" +" - `6`: `trace`\n" +" " + +#: devutils\devutils.py:311 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "ロガー `{logger_name}` レベルを `{level}` に設定した。" + +#: devutils\devutils.py:320 +#, docstring +msgid "Force reload a module (to use code changes without restarting your bot).\n\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "モジュールを強制的にリロードします(ボットを再起動せずにコードの変更を使用するため)。\n\n" +" ⚠️ 何をしているかわかっている場合のみ使用してください。\n" +" " + +#: devutils\devutils.py:336 +msgid "I couldn't find any module with this name." +msgstr "この名前のモジュールは見つからなかった。" + +#: devutils\devutils.py:340 +msgid "Module(s) {modules} reloaded." +msgstr "モジュール(複数可) {modules} リロードされました。" + +#: devutils\devutils.py:346 +msgid "Modules [...] reloaded." +msgstr "モジュール [...] がリロードされた。" + +#: devutils\devutils.py:350 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "生のリクエストでDiscordオブジェクトのJSONを表示する。" + diff --git a/devutils/locales/messages.pot b/devutils/locales/messages.pot new file mode 100644 index 0000000..1dcb2d0 --- /dev/null +++ b/devutils/locales/messages.pot @@ -0,0 +1,139 @@ +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2024-12-29 10:43+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "" + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "" + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "" + +#: devutils\devutils.py:135 +#, docstring +msgid "" +"\n" +" Repeats a command a specified number of times.\n" +"\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "" + +#: devutils\devutils.py:156 devutils\devutils.py:236 devutils\devutils.py:252 +msgid "You have not specified a correct command." +msgstr "" + +#: devutils\devutils.py:158 devutils\devutils.py:254 +msgid "You can't execute yourself this command." +msgstr "" + +#: devutils\devutils.py:175 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "" + +#: devutils\devutils.py:190 devutils\devutils.py:212 +msgid "`{command}` isn't a valid command." +msgstr "" + +#: devutils\devutils.py:196 devutils\devutils.py:218 +msgid "You can't execute yourself `{command}`." +msgstr "" + +#: devutils\devutils.py:225 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "" + +#: devutils\devutils.py:241 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "" + +#: devutils\devutils.py:260 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "" + +#: devutils\devutils.py:267 +#, docstring +msgid "" +"Reinvoke a command message.\n" +"\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "" + +#: devutils\devutils.py:291 +msgid "The command isn't valid." +msgstr "" + +#: devutils\devutils.py:293 +msgid "This command can't be executed." +msgstr "" + +#: devutils\devutils.py:299 +#, docstring +msgid "" +"Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n" +"\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "" + +#: devutils\devutils.py:313 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "" + +#: devutils\devutils.py:320 +#, docstring +msgid "Stop all bot typing tasks." +msgstr "" + +#: devutils\devutils.py:336 +#, docstring +msgid "" +"Force reload a module (to use code changes without restarting your bot).\n" +"\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "" + +#: devutils\devutils.py:352 +msgid "I couldn't find any module with this name." +msgstr "" + +#: devutils\devutils.py:356 +msgid "Module(s) {modules} reloaded." +msgstr "" + +#: devutils\devutils.py:362 +msgid "Modules [...] reloaded." +msgstr "" + +#: devutils\devutils.py:366 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "" diff --git a/devutils/locales/nl-NL.po b/devutils/locales/nl-NL.po new file mode 100644 index 0000000..bdb2eba --- /dev/null +++ b/devutils/locales/nl-NL.po @@ -0,0 +1,150 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Dutch\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: nl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/devutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 312\n" +"Language: nl_NL\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "Er is geen geldig logniveau opgegeven." + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "Geen geldig discord object verstrekt." + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "Diverse hulpprogramma's voor ontwikkeling!" + +#: devutils\devutils.py:134 +#, docstring +msgid "\n" +" Repeats a command a specified number of times.\n\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "\n" +" Herhaalt een commando een opgegeven aantal keer.\n\n" +" `-sleep ` is een optionele vlag die aangeeft hoeveel tijd er moet worden gewacht tussen het uitvoeren van een commando.\n" +" " + +#: devutils\devutils.py:155 devutils\devutils.py:235 devutils\devutils.py:251 +msgid "You have not specified a correct command." +msgstr "U hebt geen correct commando opgegeven." + +#: devutils\devutils.py:157 devutils\devutils.py:253 +msgid "You can't execute yourself this command." +msgstr "Je kunt dit commando niet zelf uitvoeren." + +#: devutils\devutils.py:174 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "Meerdere commando's tegelijk uitvoeren. Splits ze met |." + +#: devutils\devutils.py:189 devutils\devutils.py:211 +msgid "`{command}` isn't a valid command." +msgstr "`{command}` is geen geldig commando." + +#: devutils\devutils.py:195 devutils\devutils.py:217 +msgid "You can't execute yourself `{command}`." +msgstr "Je kunt jezelf niet uitvoeren `{command}`." + +#: devutils\devutils.py:224 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "De controles en cooldowns van een commando omzeilen." + +#: devutils\devutils.py:240 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "Een commando uitvoeren met timing van uitvoering en het opvangen van uitzonderingen." + +#: devutils\devutils.py:259 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "Commando `{command}` voltooid in `{timing}`s." + +#: devutils\devutils.py:266 +#, docstring +msgid "Reinvoke a command message.\n\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "Een opdrachtbericht herroepen.\n\n" +" Je kunt een bericht beantwoorden om het te herroepen of een bericht-ID/link doorgeven.\n" +" Het commando wordt dan aangeroepen met de auteur en het kanaal van het opgegeven bericht.\n" +" " + +#: devutils\devutils.py:289 +msgid "The command isn't valid." +msgstr "Het commando is niet geldig." + +#: devutils\devutils.py:291 +msgid "This command can't be executed." +msgstr "Dit commando kan niet worden uitgevoerd." + +#: devutils\devutils.py:297 +#, docstring +msgid "Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "Wijzig het logboekniveau voor een logger. Als er geen naam is opgegeven, wordt de rootlogger (`red`) gebruikt.\n\n" +" De niveaus zijn de volgende:\n" +" - `0`: `KRITISCH`\n" +" - `1`: `FOUT`\n" +" - `2`: `WAARSCHUWING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " + +#: devutils\devutils.py:311 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "Logger `{logger_name}` niveau ingesteld op `{level}`." + +#: devutils\devutils.py:320 +#, docstring +msgid "Force reload a module (to use code changes without restarting your bot).\n\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "Forceer het herladen van een module (om codewijzigingen te gebruiken zonder uw bot te herstarten).\n\n" +" ⚠️ Gebruik dit alleen als u weet wat u doet.\n" +" " + +#: devutils\devutils.py:336 +msgid "I couldn't find any module with this name." +msgstr "Ik kon geen module met deze naam vinden." + +#: devutils\devutils.py:340 +msgid "Module(s) {modules} reloaded." +msgstr "Module(s) {modules} opnieuw geladen." + +#: devutils\devutils.py:346 +msgid "Modules [...] reloaded." +msgstr "Modules [...] opnieuw geladen." + +#: devutils\devutils.py:350 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "Toon de JSON van een Discord object met een onbewerkt verzoek." + diff --git a/devutils/locales/pl-PL.po b/devutils/locales/pl-PL.po new file mode 100644 index 0000000..33957fd --- /dev/null +++ b/devutils/locales/pl-PL.po @@ -0,0 +1,150 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Polish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/devutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 312\n" +"Language: pl_PL\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "Nie podano prawidłowego poziomu dziennika." + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "Nie podano prawidłowego obiektu discord." + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "Różne narzędzia programistyczne!" + +#: devutils\devutils.py:134 +#, docstring +msgid "\n" +" Repeats a command a specified number of times.\n\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "\n" +" Powtarza polecenie określoną liczbę razy.\n\n" +" `--sleep ` jest opcjonalną flagą określającą czas oczekiwania pomiędzy wywołaniami komend.\n" +" " + +#: devutils\devutils.py:155 devutils\devutils.py:235 devutils\devutils.py:251 +msgid "You have not specified a correct command." +msgstr "Nie określiłeś poprawnego polecenia." + +#: devutils\devutils.py:157 devutils\devutils.py:253 +msgid "You can't execute yourself this command." +msgstr "Nie można samodzielnie wykonać tego polecenia." + +#: devutils\devutils.py:174 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "Wykonywanie wielu poleceń jednocześnie. Podziel je za pomocą |." + +#: devutils\devutils.py:189 devutils\devutils.py:211 +msgid "`{command}` isn't a valid command." +msgstr "`{command}` nie jest prawidłowym poleceniem." + +#: devutils\devutils.py:195 devutils\devutils.py:217 +msgid "You can't execute yourself `{command}`." +msgstr "Nie można samemu wykonać egzekucji `{command}`." + +#: devutils\devutils.py:224 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "Ominięcie kontroli i czasu odnowienia polecenia." + +#: devutils\devutils.py:240 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "Uruchamia polecenie, synchronizując jego wykonanie i wychwytując wyjątki." + +#: devutils\devutils.py:259 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "Polecenie `{command}` zakończone w `{timing}`s." + +#: devutils\devutils.py:266 +#, docstring +msgid "Reinvoke a command message.\n\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "Odwołanie wiadomości z poleceniem.\n\n" +" Możesz odpowiedzieć na wiadomość, aby ją odwołać lub przekazać identyfikator/łącze wiadomości.\n" +" Polecenie zostanie wywołane z autorem i kanałem określonej wiadomości.\n" +" " + +#: devutils\devutils.py:289 +msgid "The command isn't valid." +msgstr "Polecenie jest nieprawidłowe." + +#: devutils\devutils.py:291 +msgid "This command can't be executed." +msgstr "To polecenie nie może zostać wykonane." + +#: devutils\devutils.py:297 +#, docstring +msgid "Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "Zmienia poziom logowania dla loggera. Jeśli nie podano nazwy, używany jest główny logger (`red`).\n\n" +" Poziomy są następujące:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " + +#: devutils\devutils.py:311 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "Poziom loggera `{logger_name}` ustawiony na `{level}`." + +#: devutils\devutils.py:320 +#, docstring +msgid "Force reload a module (to use code changes without restarting your bot).\n\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "Wymuś przeładowanie modułu (aby użyć zmian w kodzie bez ponownego uruchamiania bota).\n\n" +" ⚠️ Używaj tej opcji tylko wtedy, gdy wiesz, co robisz.\n" +" " + +#: devutils\devutils.py:336 +msgid "I couldn't find any module with this name." +msgstr "Nie znalazłem żadnego modułu o tej nazwie." + +#: devutils\devutils.py:340 +msgid "Module(s) {modules} reloaded." +msgstr "Moduł(y) {modules} przeładowany." + +#: devutils\devutils.py:346 +msgid "Modules [...] reloaded." +msgstr "Moduły [...] zostały ponownie załadowane." + +#: devutils\devutils.py:350 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "Wyświetla JSON obiektu Discord z nieprzetworzonym żądaniem." + diff --git a/devutils/locales/pt-BR.po b/devutils/locales/pt-BR.po new file mode 100644 index 0000000..4f44329 --- /dev/null +++ b/devutils/locales/pt-BR.po @@ -0,0 +1,150 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Portuguese, Brazilian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-BR\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/devutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 312\n" +"Language: pt_BR\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "Nenhum nível de registro válido fornecido." + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "Nenhum objeto de discórdia válido foi fornecido." + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "Vários utilitários de desenvolvimento!" + +#: devutils\devutils.py:134 +#, docstring +msgid "\n" +" Repeats a command a specified number of times.\n\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "\n" +" Repete um comando um número especificado de vezes.\n\n" +" `--sleep ` é um sinalizador opcional que especifica o tempo de espera entre as chamadas de comando.\n" +" " + +#: devutils\devutils.py:155 devutils\devutils.py:235 devutils\devutils.py:251 +msgid "You have not specified a correct command." +msgstr "Não especificou um comando correcto." + +#: devutils\devutils.py:157 devutils\devutils.py:253 +msgid "You can't execute yourself this command." +msgstr "Não é possível executar este comando sozinho." + +#: devutils\devutils.py:174 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "Execute vários comandos de uma só vez. Divida-os usando |." + +#: devutils\devutils.py:189 devutils\devutils.py:211 +msgid "`{command}` isn't a valid command." +msgstr "`{command}` não é um comando válido." + +#: devutils\devutils.py:195 devutils\devutils.py:217 +msgid "You can't execute yourself `{command}`." +msgstr "Você não pode executar a si mesmo `{command}`." + +#: devutils\devutils.py:224 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "Ignorar as verificações e os tempos de espera de um comando." + +#: devutils\devutils.py:240 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "Executar um comando, cronometrando a execução e capturando exceções." + +#: devutils\devutils.py:259 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "O comando `{command}` foi concluído em `{timing}`s." + +#: devutils\devutils.py:266 +#, docstring +msgid "Reinvoke a command message.\n\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "Revogar uma mensagem de comando.\n\n" +" Você pode responder a uma mensagem para revogá-la ou passar uma ID/link de mensagem.\n" +" O comando será chamado com o autor e o canal da mensagem especificada.\n" +" " + +#: devutils\devutils.py:289 +msgid "The command isn't valid." +msgstr "O comando não é válido." + +#: devutils\devutils.py:291 +msgid "This command can't be executed." +msgstr "Esse comando não pode ser executado." + +#: devutils\devutils.py:297 +#, docstring +msgid "Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "Altera o nível de registro de um registrador. Se nenhum nome for fornecido, o registrador raiz (`red`) será usado.\n\n" +" Os níveis são os seguintes:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " + +#: devutils\devutils.py:311 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "Nível do registrador `{logger_name}` definido como `{level}`." + +#: devutils\devutils.py:320 +#, docstring +msgid "Force reload a module (to use code changes without restarting your bot).\n\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "Forçar o recarregamento de um módulo (para usar as alterações de código sem reiniciar o bot).\n\n" +" ⚠️ Use isso somente se souber o que está fazendo.\n" +" " + +#: devutils\devutils.py:336 +msgid "I couldn't find any module with this name." +msgstr "Não consegui encontrar nenhum módulo com esse nome." + +#: devutils\devutils.py:340 +msgid "Module(s) {modules} reloaded." +msgstr "Módulo(s) {modules} recarregado(s)." + +#: devutils\devutils.py:346 +msgid "Modules [...] reloaded." +msgstr "Módulos [...] recarregados." + +#: devutils\devutils.py:350 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "Exibe o JSON de um objeto Discord com uma solicitação bruta." + diff --git a/devutils/locales/pt-PT.po b/devutils/locales/pt-PT.po new file mode 100644 index 0000000..be06abd --- /dev/null +++ b/devutils/locales/pt-PT.po @@ -0,0 +1,150 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Portuguese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-PT\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/devutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 312\n" +"Language: pt_PT\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "Não foi fornecido um nível de registo válido." + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "Não foi fornecido nenhum objeto de discórdia válido." + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "Vários utilitários de desenvolvimento!" + +#: devutils\devutils.py:134 +#, docstring +msgid "\n" +" Repeats a command a specified number of times.\n\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "\n" +" Repete um comando um número especificado de vezes.\n\n" +" `--sleep ` é um sinalizador opcional que especifica quanto tempo esperar entre as invocações de comando.\n" +" " + +#: devutils\devutils.py:155 devutils\devutils.py:235 devutils\devutils.py:251 +msgid "You have not specified a correct command." +msgstr "Não especificou um comando correcto." + +#: devutils\devutils.py:157 devutils\devutils.py:253 +msgid "You can't execute yourself this command." +msgstr "Não é possível executar este comando sozinho." + +#: devutils\devutils.py:174 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "Executar vários comandos de uma só vez. Divida-os utilizando |." + +#: devutils\devutils.py:189 devutils\devutils.py:211 +msgid "`{command}` isn't a valid command." +msgstr "`{command}` não é um comando válido." + +#: devutils\devutils.py:195 devutils\devutils.py:217 +msgid "You can't execute yourself `{command}`." +msgstr "Não se pode executar a si próprio `{command}`." + +#: devutils\devutils.py:224 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "Ignora as verificações e os tempos de espera de um comando." + +#: devutils\devutils.py:240 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "Executar um comando, cronometrando a execução e capturando excepções." + +#: devutils\devutils.py:259 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "O comando `{command}` terminou em `{timing}`s." + +#: devutils\devutils.py:266 +#, docstring +msgid "Reinvoke a command message.\n\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "Revogar uma mensagem de comando.\n\n" +" Pode responder a uma mensagem para a revogar ou passar uma ID/link de mensagem.\n" +" O comando será invocado com o autor e o canal da mensagem especificada.\n" +" " + +#: devutils\devutils.py:289 +msgid "The command isn't valid." +msgstr "O comando não é válido." + +#: devutils\devutils.py:291 +msgid "This command can't be executed." +msgstr "Este comando não pode ser executado." + +#: devutils\devutils.py:297 +#, docstring +msgid "Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "Altera o nível de registo de um logger. Se nenhum nome for fornecido, o logger raiz (`red`) é utilizado.\n\n" +" Os níveis são os seguintes:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " + +#: devutils\devutils.py:311 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "Nível do registador `{logger_name}` definido para `{level}`." + +#: devutils\devutils.py:320 +#, docstring +msgid "Force reload a module (to use code changes without restarting your bot).\n\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "Forçar o recarregamento de um módulo (para usar as mudanças de código sem reiniciar seu bot).\n\n" +" ⚠️ Por favor, use isto apenas se souber o que está a fazer.\n" +" " + +#: devutils\devutils.py:336 +msgid "I couldn't find any module with this name." +msgstr "Não consegui encontrar nenhum módulo com este nome." + +#: devutils\devutils.py:340 +msgid "Module(s) {modules} reloaded." +msgstr "Módulo(s) {modules} recarregado(s)." + +#: devutils\devutils.py:346 +msgid "Modules [...] reloaded." +msgstr "Módulos [...] recarregados." + +#: devutils\devutils.py:350 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "Exibe o JSON de um objeto Discord com uma solicitação bruta." + diff --git a/devutils/locales/ro-RO.po b/devutils/locales/ro-RO.po new file mode 100644 index 0000000..348e849 --- /dev/null +++ b/devutils/locales/ro-RO.po @@ -0,0 +1,150 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Romanian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ro\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/devutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 312\n" +"Language: ro_RO\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "Nu a fost furnizat niciun nivel de jurnal valid." + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "Nu a fost furnizat niciun obiect discordant valid." + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "Diverse utilități de dezvoltare!" + +#: devutils\devutils.py:134 +#, docstring +msgid "\n" +" Repeats a command a specified number of times.\n\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "\n" +" Repetă o comandă de un număr specificat de ori.\n\n" +" `--sleep ` este un indicator opțional care specifică cât timp trebuie să se aștepte între apelurile comenzilor.\n" +" " + +#: devutils\devutils.py:155 devutils\devutils.py:235 devutils\devutils.py:251 +msgid "You have not specified a correct command." +msgstr "Nu ați specificat o comandă corectă." + +#: devutils\devutils.py:157 devutils\devutils.py:253 +msgid "You can't execute yourself this command." +msgstr "Nu puteți executa singur această comandă." + +#: devutils\devutils.py:174 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "Executați mai multe comenzi simultan. Împărțiți-le folosind |." + +#: devutils\devutils.py:189 devutils\devutils.py:211 +msgid "`{command}` isn't a valid command." +msgstr "`{command}` nu este o comandă validă." + +#: devutils\devutils.py:195 devutils\devutils.py:217 +msgid "You can't execute yourself `{command}`." +msgstr "Nu te poți executa singur `{command}`." + +#: devutils\devutils.py:224 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "Ocolește verificările și reducerile unei comenzi." + +#: devutils\devutils.py:240 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "Rularea unei comenzi cronometrează execuția și prinde excepțiile." + +#: devutils\devutils.py:259 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "Comanda `{command}` s-a terminat în `{timing}`s." + +#: devutils\devutils.py:266 +#, docstring +msgid "Reinvoke a command message.\n\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "Reapelarea unui mesaj de comandă.\n\n" +" Puteți răspunde la un mesaj pentru a-l reanula sau puteți transmite un ID/link de mesaj.\n" +" Comanda va fi invocată cu autorul și canalul mesajului specificat.\n" +" " + +#: devutils\devutils.py:289 +msgid "The command isn't valid." +msgstr "Comanda nu există." + +#: devutils\devutils.py:291 +msgid "This command can't be executed." +msgstr "Această comandă nu poate fi executată." + +#: devutils\devutils.py:297 +#, docstring +msgid "Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "Schimbă nivelul de logare pentru un logger. Dacă nu este furnizat niciun nume, este utilizat loggerul rădăcină (`red`).\n\n" +" Nivelurile sunt următoarele:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " + +#: devutils\devutils.py:311 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "Nivelul loggerului `{logger_name}` setat la `{level}`." + +#: devutils\devutils.py:320 +#, docstring +msgid "Force reload a module (to use code changes without restarting your bot).\n\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "Reîncărcarea forțată a unui modul (pentru a utiliza modificările de cod fără a reporni robotul).\n\n" +" ⚠️ Vă rugăm să folosiți acest lucru numai dacă știți ce faceți.\n" +" " + +#: devutils\devutils.py:336 +msgid "I couldn't find any module with this name." +msgstr "Nu am putut găsi niciun modul cu acest nume." + +#: devutils\devutils.py:340 +msgid "Module(s) {modules} reloaded." +msgstr "Modulul (modulele) {modules} reîncărcat." + +#: devutils\devutils.py:346 +msgid "Modules [...] reloaded." +msgstr "Module [...] reîncărcate." + +#: devutils\devutils.py:350 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "Afișează JSON-ul unui obiect Discord cu o cerere brută." + diff --git a/devutils/locales/ru-RU.po b/devutils/locales/ru-RU.po new file mode 100644 index 0000000..2b6681f --- /dev/null +++ b/devutils/locales/ru-RU.po @@ -0,0 +1,150 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Russian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ru\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/devutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 312\n" +"Language: ru_RU\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "Не указан допустимый уровень журнала." + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "Не указан действительный объект раздора." + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "Различные утилиты для разработки!" + +#: devutils\devutils.py:134 +#, docstring +msgid "\n" +" Repeats a command a specified number of times.\n\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "\n" +" Повторяет команду указанное количество раз.\n\n" +" `--sleep ` - необязательный флаг, указывающий, сколько времени нужно ждать между вызовами команд.\n" +" " + +#: devutils\devutils.py:155 devutils\devutils.py:235 devutils\devutils.py:251 +msgid "You have not specified a correct command." +msgstr "Вы не указали правильную команду." + +#: devutils\devutils.py:157 devutils\devutils.py:253 +msgid "You can't execute yourself this command." +msgstr "Вы не можете выполнить эту команду самостоятельно." + +#: devutils\devutils.py:174 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "Выполняйте несколько команд одновременно. Разделите их с помощью |." + +#: devutils\devutils.py:189 devutils\devutils.py:211 +msgid "`{command}` isn't a valid command." +msgstr "`{command}` не является допустимой командой." + +#: devutils\devutils.py:195 devutils\devutils.py:217 +msgid "You can't execute yourself `{command}`." +msgstr "Вы не можете казнить себя `{command}`." + +#: devutils\devutils.py:224 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "Обходите проверки и кулдауны команд." + +#: devutils\devutils.py:240 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "Запустите команду, засекая время ее выполнения и отлавливая исключения." + +#: devutils\devutils.py:259 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "Команда `{command}` завершилась через `{timing}``." + +#: devutils\devutils.py:266 +#, docstring +msgid "Reinvoke a command message.\n\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "Отмена командного сообщения.\n\n" +" Вы можете ответить на сообщение, чтобы отозвать его, или передать идентификатор сообщения/ссылку.\n" +" Команда будет вызвана с указанием автора и канала указанного сообщения.\n" +" " + +#: devutils\devutils.py:289 +msgid "The command isn't valid." +msgstr "Команда недействительна." + +#: devutils\devutils.py:291 +msgid "This command can't be executed." +msgstr "Эта команда не может быть выполнена." + +#: devutils\devutils.py:297 +#, docstring +msgid "Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "Изменение уровня регистрации для регистратора. Если имя не указано, используется корневой логгер (`red`).\n\n" +" Уровни следующие:\n" +" - `0`: `КРИТИЧЕСКИЙ`\n" +" - `1`: `ОШИБКА`\n" +" - `2`: `ПРЕДУПРЕЖДЕНИЕ`\n" +" - `3`: `ИНФОРМАЦИЯ`\n" +" - `4`: `ОТЛАДКА`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `ТРАССИРОВКА`\n" +" " + +#: devutils\devutils.py:311 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "Уровень регистратора `{logger_name}` установлен на `{level}`." + +#: devutils\devutils.py:320 +#, docstring +msgid "Force reload a module (to use code changes without restarting your bot).\n\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "Принудительная перезагрузка модуля (чтобы использовать изменения кода без перезапуска бота).\n\n" +" ⚠️ Пожалуйста, используйте это только в том случае, если вы знаете, что делаете.\n" +" " + +#: devutils\devutils.py:336 +msgid "I couldn't find any module with this name." +msgstr "Я не смог найти ни одного модуля с таким названием." + +#: devutils\devutils.py:340 +msgid "Module(s) {modules} reloaded." +msgstr "Модуль(ы) {modules} перезагружен(ы)." + +#: devutils\devutils.py:346 +msgid "Modules [...] reloaded." +msgstr "Модули [...] перезагружены." + +#: devutils\devutils.py:350 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "Отображение JSON объекта Discord при необработанном запросе." + diff --git a/devutils/locales/tr-TR.po b/devutils/locales/tr-TR.po new file mode 100644 index 0000000..90123de --- /dev/null +++ b/devutils/locales/tr-TR.po @@ -0,0 +1,150 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 13:27\n" +"Last-Translator: \n" +"Language-Team: Turkish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: tr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/devutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 312\n" +"Language: tr_TR\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "Geçerli bir günlük seviyesi sağlanmadı." + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "Geçerli bir discord nesnesi sağlanmadı." + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "Çeşitli geliştirme araçları!" + +#: devutils\devutils.py:134 +#, docstring +msgid "\n" +" Repeats a command a specified number of times.\n\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "\n" +" Bir komutu belirtilen sayıda tekrar eder.\n\n" +" `--sleep ` komut çağrıları arasında ne kadar bekleyeceğini belirten isteğe bağlı bir bayraktır.\n" +" " + +#: devutils\devutils.py:155 devutils\devutils.py:235 devutils\devutils.py:251 +msgid "You have not specified a correct command." +msgstr "Doğru bir komut belirtmediniz." + +#: devutils\devutils.py:157 devutils\devutils.py:253 +msgid "You can't execute yourself this command." +msgstr "Bu komutu kendiniz yürütemezsiniz." + +#: devutils\devutils.py:174 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "Birden fazla komutu aynı anda çalıştırın. Onları | kullanarak ayırın." + +#: devutils\devutils.py:189 devutils\devutils.py:211 +msgid "`{command}` isn't a valid command." +msgstr "`{command}` geçerli bir komut değil." + +#: devutils\devutils.py:195 devutils\devutils.py:217 +msgid "You can't execute yourself `{command}`." +msgstr "`{command}` komutunu kendiniz çalıştıramazsınız." + +#: devutils\devutils.py:224 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "Bir komutun kontrollerini ve bekleme sürelerini atlayın." + +#: devutils\devutils.py:240 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "Bir komutu zamanlama yürütmesi ve istisnaları yakalama ile çalıştırın." + +#: devutils\devutils.py:259 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "`{command}` komutu `{timing}` saniyede tamamlandı." + +#: devutils\devutils.py:266 +#, docstring +msgid "Reinvoke a command message.\n\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "Bir komut mesajını yeniden çağırın.\n\n" +" Bir mesaja yanıt vererek yeniden çağırabilir veya bir mesaj kimliği/bağlantısı geçebilirsiniz.\n" +" Komut, belirtilen mesajın yazarı ve kanalı ile çağrılacaktır.\n" +" " + +#: devutils\devutils.py:289 +msgid "The command isn't valid." +msgstr "Komut geçerli değil." + +#: devutils\devutils.py:291 +msgid "This command can't be executed." +msgstr "Bu komut çalıştırılamaz." + +#: devutils\devutils.py:297 +#, docstring +msgid "Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "Bir kaydedici için günlük seviyesini değiştirin. Hiçbir ad belirtilmezse, kök kaydedici (`red`) kullanılır.\n\n" +" Seviyeler şunlardır:\n" +" - `0`: `KRİTİK`\n" +" - `1`: `HATA`\n" +" - `2`: `UYARI`\n" +" - `3`: `BİLGİ`\n" +" - `4`: `HATA AYIKLAMA`\n" +" - `5`: `AYRINTILI`\n" +" - `6`: `İZLEME`\n" +" " + +#: devutils\devutils.py:311 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "`{logger_name}` kaydedicisi seviyesi `{level}` olarak ayarlandı." + +#: devutils\devutils.py:320 +#, docstring +msgid "Force reload a module (to use code changes without restarting your bot).\n\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "Bir modülü zorla yeniden yükleyin (botunuzu yeniden başlatmadan kod değişikliklerini kullanmak için).\n\n" +" ⚠️ Lütfen bunu yalnızca ne yaptığınızı biliyorsanız kullanın.\n" +" " + +#: devutils\devutils.py:336 +msgid "I couldn't find any module with this name." +msgstr "Bu isimde bir modül bulamadım." + +#: devutils\devutils.py:340 +msgid "Module(s) {modules} reloaded." +msgstr "Modül(ler) {modules} yeniden yüklendi." + +#: devutils\devutils.py:346 +msgid "Modules [...] reloaded." +msgstr "Modüller [...] yeniden yüklendi." + +#: devutils\devutils.py:350 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "Ham bir istek ile bir Discord nesnesinin JSON'unu görüntüleyin." + diff --git a/devutils/locales/uk-UA.po b/devutils/locales/uk-UA.po new file mode 100644 index 0000000..09bc19e --- /dev/null +++ b/devutils/locales/uk-UA.po @@ -0,0 +1,150 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Ukrainian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: uk\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/devutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 312\n" +"Language: uk_UA\n" + +#: devutils\devutils.py:51 +msgid "No valid log level provided." +msgstr "Не надано дійсного рівня журналу." + +#: devutils\devutils.py:80 +msgid "No valid discord object provided." +msgstr "Не надано жодного дійсного об'єкта конфлікту." + +#: devutils\devutils.py:85 +#, docstring +msgid "Various development utilities!" +msgstr "Різноманітні утиліти для розробки!" + +#: devutils\devutils.py:134 +#, docstring +msgid "\n" +" Repeats a command a specified number of times.\n\n" +" `--sleep ` is an optional flag specifying how much time to wait between command invocations.\n" +" " +msgstr "\n" +" Повторює команду вказану кількість разів.\n\n" +" `--sleep ` - необов'язковий прапорець, який вказує, скільки часу чекати між викликами команди.\n" +" " + +#: devutils\devutils.py:155 devutils\devutils.py:235 devutils\devutils.py:251 +msgid "You have not specified a correct command." +msgstr "Ви вказали неправильну команду." + +#: devutils\devutils.py:157 devutils\devutils.py:253 +msgid "You can't execute yourself this command." +msgstr "Ви не можете виконати собі цю команду." + +#: devutils\devutils.py:174 +#, docstring +msgid "Execute multiple commands at once. Split them using |." +msgstr "Виконання декількох команд одночасно. Розділіть їх за допомогою |." + +#: devutils\devutils.py:189 devutils\devutils.py:211 +msgid "`{command}` isn't a valid command." +msgstr "`{command}` не є правильною командою." + +#: devutils\devutils.py:195 devutils\devutils.py:217 +msgid "You can't execute yourself `{command}`." +msgstr "Ти не можеш стратити себе `{command}`." + +#: devutils\devutils.py:224 +#, docstring +msgid "Bypass a command's checks and cooldowns." +msgstr "Оминати перевірки та перезавантаження команди." + +#: devutils\devutils.py:240 +#, docstring +msgid "Run a command timing execution and catching exceptions." +msgstr "Запустіть хронометраж виконання команди та перехоплення винятків." + +#: devutils\devutils.py:259 +msgid "Command `{command}` finished in `{timing}`s." +msgstr "Команда `{command}` завершилася в `{timing}`." + +#: devutils\devutils.py:266 +#, docstring +msgid "Reinvoke a command message.\n\n" +" You may reply to a message to reinvoke it or pass a message ID/link.\n" +" The command will be invoked with the author and the channel of the specified message.\n" +" " +msgstr "Повторний виклик командного повідомлення.\n\n" +" Ви можете відповісти на повідомлення, щоб викликати його повторно, або передати ідентифікатор повідомлення/посилання.\n" +" Команда буде викликана з автором і каналом вказаного повідомлення.\n" +" " + +#: devutils\devutils.py:289 +msgid "The command isn't valid." +msgstr "Команда недійсна." + +#: devutils\devutils.py:291 +msgid "This command can't be executed." +msgstr "Ця команда не може бути виконана." + +#: devutils\devutils.py:297 +#, docstring +msgid "Change the logging level for a logger. If no name is provided, the root logger (`red`) is used.\n\n" +" Levels are the following:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " +msgstr "Змінити рівень ведення журналу для журналу. Якщо ім'я не вказано, буде використано кореневий логгер (`red`).\n\n" +" Існують наступні рівні:\n" +" - `0`: `CRITICAL`\n" +" - `1`: `ERROR`\n" +" - `2`: `WARNING`\n" +" - `3`: `INFO`\n" +" - `4`: `DEBUG`\n" +" - `5`: `VERBOSE`\n" +" - `6`: `TRACE`\n" +" " + +#: devutils\devutils.py:311 +msgid "Logger `{logger_name}` level set to `{level}`." +msgstr "Рівень журналу `{logger_name}` встановлено на `{level}`." + +#: devutils\devutils.py:320 +#, docstring +msgid "Force reload a module (to use code changes without restarting your bot).\n\n" +" ⚠️ Please only use this if you know what you're doing.\n" +" " +msgstr "Примусове перезавантаження модуля (щоб використовувати зміни коду без перезапуску бота).\n\n" +" ⚠️ Будь ласка, використовуйте це тільки якщо ви знаєте, що робите.\n" +" " + +#: devutils\devutils.py:336 +msgid "I couldn't find any module with this name." +msgstr "Я не знайшов жодного модуля з такою назвою." + +#: devutils\devutils.py:340 +msgid "Module(s) {modules} reloaded." +msgstr "Модуль(и) {modules} перезавантажено." + +#: devutils\devutils.py:346 +msgid "Modules [...] reloaded." +msgstr "Модулі [...] перезавантажено." + +#: devutils\devutils.py:350 +#, docstring +msgid "Display the JSON of a Discord object with a raw request." +msgstr "Відобразити JSON об'єкта Discord за допомогою необробленого запиту." + diff --git a/devutils/utils_version.json b/devutils/utils_version.json new file mode 100644 index 0000000..bfab002 --- /dev/null +++ b/devutils/utils_version.json @@ -0,0 +1 @@ +{"needed_utils_version": 7.0} \ No newline at end of file diff --git a/dictionary/__init__.py b/dictionary/__init__.py index 17f6b2a..513d25b 100644 --- a/dictionary/__init__.py +++ b/dictionary/__init__.py @@ -1,7 +1,46 @@ +from redbot.core import errors # isort:skip +import importlib +import sys + +try: + import AAA3A_utils +except ModuleNotFoundError: + raise errors.CogLoadError( + "The needed utils to run the cog were not found. Please execute the command `[p]pipinstall git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." + ) +modules = sorted( + [module for module in sys.modules if module.split(".")[0] == "AAA3A_utils"], reverse=True +) +for module in modules: + try: + importlib.reload(sys.modules[module]) + except ModuleNotFoundError: + pass +del AAA3A_utils +# import AAA3A_utils +# import json +# import os +# __version__ = AAA3A_utils.__version__ +# with open(os.path.join(os.path.dirname(__file__), "utils_version.json"), mode="r") as f: +# data = json.load(f) +# needed_utils_version = data["needed_utils_version"] +# if __version__ > needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a higher version than the one supported by this version of the cog. Please update the cogs of the `AAA3A-cogs` repo." +# ) +# elif __version__ < needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a lower version than the one supported by this version of the cog. Please execute the command `[p]pipinstall --upgrade git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." +# ) + +from redbot.core.bot import Red # isort:skip +from redbot.core.utils import get_end_user_data_statement + from .dictionary import Dictionary -__red_end_user_data_statement__ = "This cog does not persistently store data or metadata about users." +__red_end_user_data_statement__ = get_end_user_data_statement(file=__file__) -async def setup(bot): - await bot.add_cog(Dictionary(bot)) +async def setup(bot: Red) -> None: + cog = Dictionary(bot) + await bot.add_cog(cog) diff --git a/dictionary/dictionary.py b/dictionary/dictionary.py index 95ccb0c..de117e4 100644 --- a/dictionary/dictionary.py +++ b/dictionary/dictionary.py @@ -1,185 +1,139 @@ +from AAA3A_utils import Cog # isort:skip +from redbot.core import commands, app_commands # isort:skip +from redbot.core.bot import Red # isort:skip +from redbot.core.i18n import Translator, cog_i18n # isort:skip +import typing # isort:skip + +from urllib.parse import quote_plus + import aiohttp -import discord -import contextlib -from bs4 import BeautifulSoup -import json -import logging -import re -from redbot.core import commands -from redbot.core.utils.chat_formatting import pagify +from redbot.core.utils.chat_formatting import humanize_list + +from .types import Word +from .view import DictionaryView + +# Credits: +# General repo credits. + +_: Translator = Translator("Dictionary", __file__) -log = logging.getLogger("red.aikaterna.dictionary") +@cog_i18n(_) +class Dictionary(Cog): + """A cog to search an english term/word in the dictionary! Synonyms, antonyms, phonetics (with audio)...""" + def __init__(self, bot: Red) -> None: + super().__init__(bot=bot) -class Dictionary(commands.Cog): - """ - Word, yo - Parts of this cog are adapted from the PyDictionary library. - """ + self._session: aiohttp.ClientSession = None + self.cache: typing.Dict[str, Word] = {} - async def red_delete_data_for_user(self, **kwargs): - """Nothing to delete""" - return + async def cog_load(self) -> None: + await super().cog_load() + self._session: aiohttp.ClientSession = aiohttp.ClientSession() - def __init__(self, bot): - self.bot = bot - self.session = aiohttp.ClientSession() + async def cog_unload(self) -> None: + await self._session.close() + await super().cog_unload() - def cog_unload(self): - self.bot.loop.create_task(self.session.close()) - - @commands.command() - async def define(self, ctx, *, word: str): - """Displays definitions of a given word.""" - search_msg = await ctx.send("Searching...") - search_term = word.split(" ", 1)[0] - result = await self._definition(ctx, search_term) - str_buffer = "" - if not result: - with contextlib.suppress(discord.NotFound): - await search_msg.delete() - await ctx.send("This word is not in the dictionary.") - return - for key in result: - str_buffer += f"\n**{key}**: \n" - counter = 1 - j = False - for val in result[key]: - if val.startswith("("): - str_buffer += f"{str(counter)}. *{val})* " - counter += 1 - j = True - else: - if j: - str_buffer += f"{val}\n" - j = False - else: - str_buffer += f"{str(counter)}. {val}\n" - counter += 1 - with contextlib.suppress(discord.NotFound): - await search_msg.delete() - for page in pagify(str_buffer, delims=["\n"]): - await ctx.send(page) - - async def _definition(self, ctx, word): - data = await self._get_soup_object(f"http://wordnetweb.princeton.edu/perl/webwn?s={word}") - if not data: - return await ctx.send("Error fetching data.") - types = data.findAll("h3") - length = len(types) - lists = data.findAll("ul") - out = {} - if not lists: - return - for a in types: - reg = str(lists[types.index(a)]) - meanings = [] - for x in re.findall(r">\s\((.*?)\)\s<", reg): - if "often followed by" in x: - pass - elif len(x) > 5 or " " in str(x): - meanings.append(x) - name = a.text - out[name] = meanings - return out - - @commands.command() - async def antonym(self, ctx, *, word: str): - """Displays antonyms for a given word.""" - search_term = word.split(" ", 1)[0] - result = await self._antonym_or_synonym(ctx, "antonyms", search_term) - if not result: - await ctx.send("This word is not in the dictionary or nothing was found.") - return - - result_text = "*, *".join(result) - msg = f"Antonyms for **{search_term}**: *{result_text}*" - for page in pagify(msg, delims=["\n"]): - await ctx.send(page) - - @commands.command() - async def synonym(self, ctx, *, word: str): - """Displays synonyms for a given word.""" - search_term = word.split(" ", 1)[0] - result = await self._antonym_or_synonym(ctx, "synonyms", search_term) - if not result: - await ctx.send("This word is not in the dictionary or nothing was found.") - return - - result_text = "*, *".join(result) - msg = f"Synonyms for **{search_term}**: *{result_text}*" - for page in pagify(msg, delims=["\n"]): - await ctx.send(page) - - async def _antonym_or_synonym(self, ctx, lookup_type, word): - if lookup_type not in ["antonyms", "synonyms"]: - return None - data = await self._get_soup_object(f"http://www.thesaurus.com/browse/{word}") - if not data: - await ctx.send("Error getting information from the website.") - return - - script = data.find("script", id="preloaded-state") - if script: - script_text = script.string - script_text = script_text.strip() - script_text = script_text.replace("window.__PRELOADED_STATE__ = ", "") - else: - await ctx.send("Error fetching script from the website.") - return - - try: - data = json.loads(script_text) - except json.decoder.JSONDecodeError: - await ctx.send("Error decoding script from the website.") - return - except Exception as e: - log.exception(e, exc_info=e) - await ctx.send("Something broke. Check your console for more information.") - return - - try: - data_prefix = data["thesaurus"]["thesaurusData"]["data"]["slugs"][0]["entries"][0]["partOfSpeechGroups"][0]["shortDefinitions"][0] - except KeyError: - return None - - if lookup_type == "antonyms": - try: - antonym_subsection = data_prefix["antonyms"] - except KeyError: + async def get_word(self, query: str) -> Word: + if query in self.cache: + return self.cache[query] + url = f"https://api.dictionaryapi.dev/api/v2/entries/en/{quote_plus(query)}" + async with self._session.get(url) as r: + json_content = await r.json() + if "title" in json_content: + if json_content["title"] == "No Definitions Found": return None - antonyms = [] - for item in antonym_subsection: - try: - antonyms.append(item["targetWord"]) - except KeyError: - pass - if antonyms: - return antonyms else: - return None + raise commands.UserFeedbackCheckFailure(json_content["title"]) + json_content = json_content[0] + word = Word( + url=url, + source_url=json_content["sourceUrls"][0] if json_content.get("sourceUrls") else None, + word=json_content["word"], + phonetics=[ + { + "text": phonetic.get("text"), + "audio_url": phonetic["audio"], + "audio_file": None, + "source_url": phonetic.get("sourceUrl"), + } + for phonetic in json_content["phonetics"] + ], + meanings={ + meaning["partOfSpeech"]: [ + { + "definition": definition["definition"], + "synonyms": definition["synonyms"], + "antonyms": definition["antonyms"], + "example": definition.get("example"), + } + for definition in meaning["definitions"] + ] + for meaning in json_content["meanings"] + }, + ) + self.cache[query] = word + return word - if lookup_type == "synonyms": - try: - synonyms_subsection = data_prefix["synonyms"] - except KeyError: - return None - synonyms = [] - for item in synonyms_subsection: - try: - synonyms.append(item["targetWord"]) - except KeyError: - pass - if synonyms: - return synonyms - else: - return None + @commands.bot_has_permissions(embed_links=True) + @commands.hybrid_command(aliases=["define"]) + @app_commands.allowed_installs(guilds=True, users=True) + async def dictionary(self, ctx: commands.Context, query: str) -> None: + """Search a word in the english dictionnary.""" + word = await self.get_word(query) + if word is None: + raise commands.UserFeedbackCheckFailure(_("Word not found in English dictionary.")) + await DictionaryView(cog=self, word=word).start(ctx) - async def _get_soup_object(self, url): - try: - async with self.session.request("GET", url) as response: - return BeautifulSoup(await response.text(), "html.parser") - except Exception: - log.error("Error fetching dictionary.py related webpage", exc_info=True) - return None + @commands.Cog.listener() + async def on_assistant_cog_add( + self, assistant_cog: typing.Optional[commands.Cog] = None + ) -> None: # Vert's Assistant integration/third party. + if assistant_cog is None: + return self.get_word_in_dictionary_for_assistant + schema = { + "name": "get_word_in_dictionary_for_assistant", + "description": "Get the meanings, the definition, the synonyms and the antonyms of an English word.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The word to search in the English dictionary.", + }, + }, + "required": ["query"], + }, + } + await assistant_cog.register_function(cog_name=self.qualified_name, schema=schema) + + async def get_word_in_dictionary_for_assistant(self, query: str, *args, **kwargs): + word = await self.get_word(query) + if word is None: + return "Word not found in English dictionary." + meanings = "" + for meaning in word.meanings: + meanings += "\n\n" + "\n".join( + [ + (f"{n}. " if len(word.meanings[meaning]) > 1 else "") + + f"{definition['definition']}" + + ( + f"\n- Synonyms: {humanize_list(definition['synonyms'])}" + if definition["synonyms"] + else "" + ) + + ( + f"\n- Antonyms: {humanize_list(definition['antonyms'])}" + if definition["antonyms"] + else "" + ) + for n, definition in enumerate(word.meanings[meaning], start=1) + ] + ) + data = { + "Word": word.word, + "Meanings": meanings, + } + return [f"{key}: {value}\n" for key, value in data.items() if value is not None] diff --git a/dictionary/info.json b/dictionary/info.json index d59fc54..0663202 100644 --- a/dictionary/info.json +++ b/dictionary/info.json @@ -1,11 +1,16 @@ { - "author": ["UltimatePancake", "aikaterna"], - "description": "Gets definitions, antonyms, or synonyms for given words", - "end_user_data_statement": "This cog does not persistently store data or metadata about users.", - "install_msg": "After loading the cog with `[p]load dictionary`, use [p]help Dictionary to view commands.", + "author": ["AAA3A"], + "name": "Dictionary", + "install_msg": "Thank you for installing this cog!\nDo `[p]help CogName` to get the list of commands and their description. If you enjoy my work, please consider donating on [Buy Me a Coffee]() or [Ko-Fi]()!", + "short": "A cog to search an english term/word in the dictionary! Synonyms, antonyms, phonetics (with audio)...", + "description": "A cog to search an english term/word in the dictionary! Synonyms, antonyms, phonetics (with audio)...", + "tags": [ + "dictionary", + "english", + "word", + "term" + ], + "requirements": ["git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git"], "min_bot_version": "3.5.0", - "short": "Gets definitions, antonyms, or synonyms for given words", - "tags": ["dictionary", "synonym", "antonym"], - "requirements": ["beautifulsoup4"], - "type": "COG" -} + "end_user_data_statement": "This cog does not persistently store data or metadata about users." +} \ No newline at end of file diff --git a/discordsearch/README.rst b/discordsearch/README.rst new file mode 100644 index 0000000..9a44a8f --- /dev/null +++ b/discordsearch/README.rst @@ -0,0 +1,64 @@ +.. _discordsearch: +============= +DiscordSearch +============= + +This is the cog guide for the ``DiscordSearch`` cog. This guide contains the collection of commands which you can use in the cog. +Through this guide, ``[p]`` will always represent your prefix. Replace ``[p]`` with your own prefix when you use these commands in Discord. + +.. note:: + + Ensure that you are up to date by running ``[p]cog update discordsearch``. + If there is something missing, or something that needs improving in this documentation, feel free to create an issue `here `_. + This documentation is generated everytime this cog receives an update. + +--------------- +About this cog: +--------------- + +A cog to edit roles! + +--------- +Commands: +--------- + +Here are all the commands included in this cog (1): + +* ``[p]discordsearch [channel] [args]...`` + Search for a message on Discord in a channel. + +------------ +Installation +------------ + +If you haven't added my repo before, lets add it first. We'll call it "AAA3A-cogs" here. + +.. code-block:: ini + + [p]repo add AAA3A-cogs https://github.com/AAA3A-AAA3A/AAA3A-cogs + +Now, we can install DiscordSearch. + +.. code-block:: ini + + [p]cog install AAA3A-cogs discordsearch + +Once it's installed, it is not loaded by default. Load it by running the following command: + +.. code-block:: ini + + [p]load discordsearch + +---------------- +Further Support: +---------------- + +Check out my docs `here `_. +Mention me in the #support_other-cogs in the `cog support server `_ if you need any help. +Additionally, feel free to open an issue or pull request to this repo. + +-------- +Credits: +-------- + +Thanks to Kreusada for the Python code to automatically generate this documentation! \ No newline at end of file diff --git a/discordsearch/__init__.py b/discordsearch/__init__.py new file mode 100644 index 0000000..1966d04 --- /dev/null +++ b/discordsearch/__init__.py @@ -0,0 +1,46 @@ +from redbot.core import errors # isort:skip +import importlib +import sys + +try: + import AAA3A_utils +except ModuleNotFoundError: + raise errors.CogLoadError( + "The needed utils to run the cog were not found. Please execute the command `[p]pipinstall git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." + ) +modules = sorted( + [module for module in sys.modules if module.split(".")[0] == "AAA3A_utils"], reverse=True +) +for module in modules: + try: + importlib.reload(sys.modules[module]) + except ModuleNotFoundError: + pass +del AAA3A_utils +# import AAA3A_utils +# import json +# import os +# __version__ = AAA3A_utils.__version__ +# with open(os.path.join(os.path.dirname(__file__), "utils_version.json"), mode="r") as f: +# data = json.load(f) +# needed_utils_version = data["needed_utils_version"] +# if __version__ > needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a higher version than the one supported by this version of the cog. Please update the cogs of the `AAA3A-cogs` repo." +# ) +# elif __version__ < needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a lower version than the one supported by this version of the cog. Please execute the command `[p]pipinstall --upgrade git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." +# ) + +from redbot.core.bot import Red # isort:skip +from redbot.core.utils import get_end_user_data_statement + +from .discordsearch import DiscordSearch + +__red_end_user_data_statement__ = get_end_user_data_statement(file=__file__) + + +async def setup(bot: Red) -> None: + cog = DiscordSearch(bot) + await bot.add_cog(cog) diff --git a/discordsearch/discordsearch.py b/discordsearch/discordsearch.py new file mode 100644 index 0000000..f697d7b --- /dev/null +++ b/discordsearch/discordsearch.py @@ -0,0 +1,349 @@ +from AAA3A_utils import Cog, Menu # isort:skip +from redbot.core import commands # isort:skip +from redbot.core.bot import Red # isort:skip +from redbot.core.i18n import Translator, cog_i18n # isort:skip +import discord # isort:skip +import typing # isort:skip + +import argparse +import asyncio +import datetime +import functools +import multiprocessing +import re +from time import monotonic + +import dateparser +from redbot.core.utils.chat_formatting import bold, box, underline +from redbot.core.utils.common_filters import URL_RE + +# Credits: +# General repo credits. +# Thanks to Trusty for the secure way to manage user Regexes (https://github.com/TrustyJAID/Trusty-cogs/blob/master/retrigger/triggerhandler.py#L542-L606)! + +_: Translator = Translator("DiscordSearch", __file__) + + +class StrConverter(commands.Converter): + async def convert(self, ctx: commands.Context, argument: str) -> str: + return argument + + +@cog_i18n(_) +class DiscordSearch(Cog): + """A cog to edit roles!""" + + def __init__(self, bot: Red) -> None: + super().__init__(bot=bot) + + self.re_pool: multiprocessing.Pool = multiprocessing.Pool() + + @commands.guild_only() + @commands.admin_or_permissions(administrator=True) + @commands.cooldown(rate=3, per=30, type=commands.BucketType.user) + @commands.bot_has_permissions(embed_links=True) + @commands.hybrid_command(name="discordsearch", aliases=["dsearch"]) + async def discordsearch( + self, + ctx: commands.Context, + channel: typing.Optional[discord.TextChannel], + args: commands.Greedy[StrConverter], + ) -> None: + """Search for a message on Discord in a channel. + + Warning: The bot uses the api for each search. + Arguments: + `--author @user1 --author user2#1234 --author 0123456789` + `--mention @user1 --mention user2#1234 --mention 0123456789` + `--before now` + `--after "25/12/2000 00h00"` + `--pinned true` + `--content "AAA3A-cogs"` + `--regex "\\[p\\]"` + `--contain link --contain embed --contain file` + `--limit 100` (It's the limit of the number of messages taken into account in the search, not the number of results.) + """ + if not args: + raise commands.UserInputError() + try: + args = await SearchArgs().convert(ctx, args) + except commands.BadArgument as e: + await ctx.send(e) + return + authors = args.authors + mentions = args.mentions + before = args.before + after = args.after + pinned = args.pinned + content = args.content + regex = args.regex + contains = args.contains + limit = args.limit + if channel is None: + channel = ctx.channel + if all( + setting is None + for setting in ( + authors, + mentions, + before, + after, + pinned, + content, + regex, + contains, + limit, + ) + ): + raise commands.UserFeedbackCheckFailure(_("You must provide at least one parameter.")) + args_str = [ + underline("--- Settings of search ---"), + bold("Authors:") + + " " + + ( + ", ".join([author.mention for author in authors]) + if authors is not None + else "None" + ), + bold("Mentions:") + + " " + + ( + ", ".join([mention.mention for mention in mentions]) + if mentions is not None + else "None" + ), + bold("Before:") + " " + f"{before}", + bold("After:") + " " + f"{after}", + bold("Pinned:") + " " + f"{pinned}", + bold("Content:") + " " + (f"`{content}`" if content is not None else "None"), + bold("Regex:") + " " + f"{regex}", + bold("Contains:") + + " " + + (", ".join(list(contains)) if contains is not None else "None"), + bold("Limit:") + " " + f"{limit}", + ] + start = monotonic() + messages: typing.List[discord.Message] = [] + async for message in channel.history( + limit=limit, oldest_first=False, before=before, after=after + ): + if message.id == ctx.message.id: + continue + if authors is not None and message.author not in authors: + continue + if mentions is not None and not any( + True for mention in message.mentions if mention in mentions + ): + continue + if pinned is not None and message.pinned != pinned: + continue + if ( + content is not None + and content.lower() not in message.content.lower() + and all( + content.lower() not in str(embed.to_dict()).lower() for embed in message.embeds + ) + ): + continue + if regex is not None and message.content is not None: + # Thanks Trusty for this. + try: + trigger_timeout = 1 + process = self.re_pool.apply_async(regex.findall, (message.content,)) + task = functools.partial(process.get, timeout=trigger_timeout) + loop = asyncio.get_running_loop() + new_task = loop.run_in_executor(None, task) + search = await asyncio.wait_for(new_task, timeout=trigger_timeout + 5) + except (multiprocessing.TimeoutError, asyncio.TimeoutError): + raise commands.UserFeedbackCheckFailure( + _("Your regex process took too long. Removing from memory.") + ) + except ValueError: + continue + except Exception as e: + raise commands.UserFeedbackCheckFailure( + _("There is an error in your regex.\n{e}").format(e=box(str(e), lang="py")) + ) + else: + if not search: + continue + if contains is not None: + if "link" in contains: + regex = URL_RE.findall(message.content.lower()) + if regex == []: + continue + if "embed" in contains and len(message.embeds) == 0: + continue + if "file" in contains and len(message.attachments) == 0: + continue + messages.append(message) + embeds = [] + not_found = len(messages) == 0 + args_str = "\n".join(args_str) + if not not_found: + for i, message in enumerate(messages, start=1): + embed: discord.Embed = discord.Embed() + embed.title = f"Search in #{channel.name} ({channel.id})" + embed.description = args_str + embed.url = message.jump_url + embed.set_author(name=f"{message.author.display_name} ({message.author.id})") + embed.add_field( + name=f"Message ({message.id}) content:", + value=( + ( + message.content + if len(message.content) < 1025 + else (message.content[:1020] + "\n...") + ) + if message.content + else "None" + ), + inline=False, + ) + embed.add_field( + name="Embed(s):", + value=( + _("Look at the original message.") if len(message.embeds) > 0 else "None" + ), + inline=False, + ) + embed.timestamp = message.created_at + embed.set_thumbnail( + url="https://us.123rf.com/450wm/sommersby/sommersby1610/sommersby161000062/66918773-recherche-ic%C3%B4ne-plate-recherche-ic%C3%B4ne-conception-recherche-ic%C3%B4ne-web-vecteur-loupe.jpg" + ) + embed.set_footer( + text=f"Page {i}/{len(messages)}", + icon_url="https://us.123rf.com/450wm/sommersby/sommersby1610/sommersby161000062/66918773-recherche-ic%C3%B4ne-plate-recherche-ic%C3%B4ne-conception-recherche-ic%C3%B4ne-web-vecteur-loupe.jpg", + ) + embeds.append(embed) + else: + embed: discord.Embed = discord.Embed() + embed.title = _("Search in #{channel.name} ({channel.id})").format(channel=channel) + embed.add_field(name="Result:", value=_("Sorry, I could not find any results.")) + embed.timestamp = datetime.datetime.now() + embed.set_thumbnail( + url="https://us.123rf.com/450wm/sommersby/sommersby1610/sommersby161000062/66918773-recherche-ic%C3%B4ne-plate-recherche-ic%C3%B4ne-conception-recherche-ic%C3%B4ne-web-vecteur-loupe.jpg" + ) + embed.set_footer( + text="Page 1/1", + icon_url="https://us.123rf.com/450wm/sommersby/sommersby1610/sommersby161000062/66918773-recherche-ic%C3%B4ne-plate-recherche-ic%C3%B4ne-conception-recherche-ic%C3%B4ne-web-vecteur-loupe.jpg", + ) + embeds.append(embed) + end = monotonic() + total = round(end - start, 1) + for embed in embeds: + embed.title = _("Search in #{channel.name} ({channel.id}) in {total}s").format( + channel=channel, total=total + ) + await Menu(pages=embeds).start(ctx) + + +class DateConverter(commands.Converter): + """Date converter which uses dateparser.parse().""" + + async def convert(self, ctx: commands.Context, argument: str) -> datetime.datetime: + parsed = dateparser.parse(argument) + if parsed is None: + raise commands.BadArgument(_("Unrecognized date/time.")) + return parsed + + +# class SearchArgs(commands.FlagConverter, case_insensitive=False, prefix="--", delimiter=" "): +# authors: commands.Greedy[discord.Member] +# mentions: commands.Greedy[discord.Member] +# before: DateConverter +# after: DateConverter +# pinned: bool +# content: str +# regex: str +# contains: commands.Greedy[str] +# limit: int + + +class NoExitParser(argparse.ArgumentParser): + def error(self, message) -> None: + raise commands.BadArgument(message) + + +class SearchArgs: + def parse_arguments(self, arguments: str) -> argparse.Namespace: + parser = NoExitParser(description="Selection args for DiscordSearch.", add_help=False) + parser.add_argument("--author", dest="authors", nargs="+") + parser.add_argument("--mention", dest="mentions", nargs="+") + parser.add_argument("--before", dest="before") + parser.add_argument("--after", dest="after") + parser.add_argument("--pinned", dest="pinned") + parser.add_argument("--content", dest="content", nargs="*") + parser.add_argument("--regex", dest="regex", nargs="*") + parser.add_argument("--contain", dest="contains", nargs="+") + parser.add_argument("--limit", dest="limit") + + return parser.parse_args(arguments) + + async def convert(self, ctx: commands.Context, arguments) -> typing.Any: + self.ctx = ctx + args = self.parse_arguments(arguments) + if args.authors is not None: + self.authors = [] + for author in args.authors: + author = await commands.MemberConverter().convert(ctx, author) + if author is None: + raise commands.BadArgument("`--author` must be a member.") + self.authors.append(author) + else: + self.authors = None + if args.mentions is not None: + self.mentions = [] + for mention in args.mentions: + mention = await commands.MemberConverter().convert(ctx, mention) + if mention is None: + raise commands.BadArgument("`--mention` must be a member.") + self.mentions.append(mention) + else: + self.mentions = None + self.before = ( + await DateConverter().convert(ctx, args.before) + if args.before is not None + else args.before + ) + self.after = ( + await DateConverter().convert(ctx, args.after) + if args.after is not None + else args.after + ) + if args.pinned is not None: + args.pinned = str(args.pinned) + if args.pinned.lower() in ("true", "y", "yes"): + self.pinned = True + elif args.pinned.lower() in ("false", "n", "no"): + self.pinned = False + else: + raise commands.BadArgument("`--pinned` must be a bool.") + else: + self.pinned = args.pinned + self.content = "".join(args.content) if args.content is not None else args.content + if args.regex is not None: + try: + self.regex = re.compile("".join(args.regex)) + except Exception as e: + raise commands.BadArgument(f"`{args.regex}` is not a valid regex pattern.\n{e}") + else: + self.regex = None + if args.contains is not None: + self.contains = [] + for contain in args.contains: + if contain.lower() not in ("link", "embed", "file"): + raise commands.BadArgument("`--contain` must be `link`, `embed` or `file`.") + self.contains.append(contain.lower()) + else: + self.contains = None + if args.limit is not None: + try: + self.limit = int(args.limit) + except ValueError: + raise commands.BadArgument("`--limit` must be a int.") + else: + self.limit = int(args.limit) + else: + self.limit = None + return self diff --git a/discordsearch/info.json b/discordsearch/info.json new file mode 100644 index 0000000..18454f4 --- /dev/null +++ b/discordsearch/info.json @@ -0,0 +1,15 @@ +{ + "author": ["AAA3A"], + "name": "DiscordSearch", + "install_msg": "Thank you for installing this cog!\nDo `[p]help CogName` to get the list of commands and their description. If you enjoy my work, please consider donating on [Buy Me a Coffee]() or [Ko-Fi]()!", + "short": "A cog to search a message on Discord!", + "description": "Have you ever wanted to search a message on Discord? Then this cog is for you!", + "tags": [ + "search", + "message", + "channel" + ], + "requirements": ["git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git", "dateparser"], + "min_bot_version": "3.5.0", + "end_user_data_statement": "This cog does not persistently store data or metadata about users." +} \ No newline at end of file diff --git a/discordsearch/locales/de-DE.po b/discordsearch/locales/de-DE.po new file mode 100644 index 0000000..b3f9de6 --- /dev/null +++ b/discordsearch/locales/de-DE.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:20\n" +"Last-Translator: \n" +"Language-Team: German\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: de\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/discordsearch/locales/messages.pot\n" +"X-Crowdin-File-ID: 138\n" +"Language: de_DE\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "Ein Zahnrad zum Bearbeiten von Rollen!" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "Sie müssen mindestens einen Parameter angeben." + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "Ihr Regex-Prozess hat zu lange gedauert. Aus dem Speicher entfernen." + +#: discordsearch\discordsearch.py:165 +msgid "There is an error in your regex.\n" +"{e}" +msgstr "Es gibt einen Fehler in Ihrer Regex.\n" +"{e}" + +#: discordsearch\discordsearch.py:203 +msgid "Look at the original message." +msgstr "Sehen Sie sich die ursprüngliche Nachricht an." + +#: discordsearch\discordsearch.py:219 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "Suche in #{channel.name} ({channel.id})" + +#: discordsearch\discordsearch.py:220 +msgid "Sorry, I could not find any results." +msgstr "Leider konnte ich keine Ergebnisse finden." + +#: discordsearch\discordsearch.py:233 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "Suche in #{channel.name} ({channel.id}) in {total}s" + +#: discordsearch\discordsearch.py:245 +msgid "Unrecognized date/time." +msgstr "Unerkanntes Datum/Uhrzeit." + diff --git a/discordsearch/locales/el-GR.po b/discordsearch/locales/el-GR.po new file mode 100644 index 0000000..059a9f3 --- /dev/null +++ b/discordsearch/locales/el-GR.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:20\n" +"Last-Translator: \n" +"Language-Team: Greek\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: el\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/discordsearch/locales/messages.pot\n" +"X-Crowdin-File-ID: 138\n" +"Language: el_GR\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "Ένα γρανάζι για την επεξεργασία ρόλων!" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "Πρέπει να δώσετε τουλάχιστον μία παράμετρο." + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "Η διαδικασία regex σας πήρε πολύ χρόνο. Αφαίρεση από τη μνήμη." + +#: discordsearch\discordsearch.py:165 +msgid "There is an error in your regex.\n" +"{e}" +msgstr "Υπάρχει ένα σφάλμα στην regex σας.\n" +"{e}" + +#: discordsearch\discordsearch.py:203 +msgid "Look at the original message." +msgstr "Κοιτάξτε το αρχικό μήνυμα." + +#: discordsearch\discordsearch.py:219 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "Αναζήτηση στο #{channel.name} ({channel.id})" + +#: discordsearch\discordsearch.py:220 +msgid "Sorry, I could not find any results." +msgstr "Λυπάμαι, δεν μπόρεσα να βρω αποτελέσματα." + +#: discordsearch\discordsearch.py:233 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "Αναζήτηση στο #{channel.name} ({channel.id}) στο {total}s" + +#: discordsearch\discordsearch.py:245 +msgid "Unrecognized date/time." +msgstr "Μη αναγνωρισμένη ημερομηνία/ώρα." + diff --git a/discordsearch/locales/es-ES.po b/discordsearch/locales/es-ES.po new file mode 100644 index 0000000..2af8b1e --- /dev/null +++ b/discordsearch/locales/es-ES.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:20\n" +"Last-Translator: \n" +"Language-Team: Spanish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: es-ES\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/discordsearch/locales/messages.pot\n" +"X-Crowdin-File-ID: 138\n" +"Language: es_ES\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "¡Un engranaje para editar roles!" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "Debe proporcionar al menos un parámetro." + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "Tu proceso regex tardó demasiado. Eliminar de la memoria." + +#: discordsearch\discordsearch.py:165 +msgid "There is an error in your regex.\n" +"{e}" +msgstr "Hay un error en tu regex.\n" +"{e}" + +#: discordsearch\discordsearch.py:203 +msgid "Look at the original message." +msgstr "Mira el mensaje original." + +#: discordsearch\discordsearch.py:219 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "Buscar en #{channel.name} ({channel.id})" + +#: discordsearch\discordsearch.py:220 +msgid "Sorry, I could not find any results." +msgstr "Lo siento, no he podido encontrar ningún resultado." + +#: discordsearch\discordsearch.py:233 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "Búsqueda en #{channel.name} ({channel.id}) en {total}s" + +#: discordsearch\discordsearch.py:245 +msgid "Unrecognized date/time." +msgstr "Fecha/hora no reconocida." + diff --git a/discordsearch/locales/fi-FI.po b/discordsearch/locales/fi-FI.po new file mode 100644 index 0000000..6e3ba8c --- /dev/null +++ b/discordsearch/locales/fi-FI.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:21\n" +"Last-Translator: \n" +"Language-Team: Finnish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fi\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/discordsearch/locales/messages.pot\n" +"X-Crowdin-File-ID: 138\n" +"Language: fi_FI\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "Roolinmuokkauksen hammasratas!" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "Sinun on annettava vähintään yksi parametri." + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "Regex-prosessi kesti liian kauan. Poistetaan muistista." + +#: discordsearch\discordsearch.py:165 +msgid "There is an error in your regex.\n" +"{e}" +msgstr "Regexissäsi on virhe.\n" +"{e}" + +#: discordsearch\discordsearch.py:203 +msgid "Look at the original message." +msgstr "Katso alkuperäistä viestiä." + +#: discordsearch\discordsearch.py:219 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "Haku osoitteessa #{channel.name} ({channel.id})" + +#: discordsearch\discordsearch.py:220 +msgid "Sorry, I could not find any results." +msgstr "Anteeksi, en löytänyt tuloksia." + +#: discordsearch\discordsearch.py:233 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "Haku osoitteessa #{channel.name} ({channel.id}) osoitteessa {total}s" + +#: discordsearch\discordsearch.py:245 +msgid "Unrecognized date/time." +msgstr "Tunnistamaton päivämäärä/aika." + diff --git a/discordsearch/locales/fr-FR.po b/discordsearch/locales/fr-FR.po new file mode 100644 index 0000000..1c6aeba --- /dev/null +++ b/discordsearch/locales/fr-FR.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:20\n" +"Last-Translator: \n" +"Language-Team: French\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/discordsearch/locales/messages.pot\n" +"X-Crowdin-File-ID: 138\n" +"Language: fr_FR\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "Un cog pour modifier les rôles !" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "Vous devez fournir au moins un paramètre." + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "Votre processus de regex a pris trop de temps. Suppression de la mémoire." + +#: discordsearch\discordsearch.py:165 +msgid "There is an error in your regex.\n" +"{e}" +msgstr "Il y a une erreur dans votre regex.\n" +"{e}" + +#: discordsearch\discordsearch.py:203 +msgid "Look at the original message." +msgstr "Regardez le message original." + +#: discordsearch\discordsearch.py:219 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "Recherche dans #{channel.name} ({channel.id})" + +#: discordsearch\discordsearch.py:220 +msgid "Sorry, I could not find any results." +msgstr "Désolé, je n'ai trouvé aucun résultat." + +#: discordsearch\discordsearch.py:233 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "Recherche dans #{channel.name} ({channel.id}) dans {total}s" + +#: discordsearch\discordsearch.py:245 +msgid "Unrecognized date/time." +msgstr "Date/heure non reconnue." + diff --git a/discordsearch/locales/it-IT.po b/discordsearch/locales/it-IT.po new file mode 100644 index 0000000..8fa8438 --- /dev/null +++ b/discordsearch/locales/it-IT.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:21\n" +"Last-Translator: \n" +"Language-Team: Italian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: it\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/discordsearch/locales/messages.pot\n" +"X-Crowdin-File-ID: 138\n" +"Language: it_IT\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "Un ingranaggio per modificare i ruoli!" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "È necessario fornire almeno un parametro." + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "Il processo di regex ha richiesto troppo tempo. Rimuovere dalla memoria." + +#: discordsearch\discordsearch.py:165 +msgid "There is an error in your regex.\n" +"{e}" +msgstr "C'è un errore nella regex.\n" +"{e}" + +#: discordsearch\discordsearch.py:203 +msgid "Look at the original message." +msgstr "Guardate il messaggio originale." + +#: discordsearch\discordsearch.py:219 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "Ricerca in #{channel.name} ({channel.id})" + +#: discordsearch\discordsearch.py:220 +msgid "Sorry, I could not find any results." +msgstr "Spiacente, non sono riuscito a trovare alcun risultato." + +#: discordsearch\discordsearch.py:233 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "Ricerca in #{channel.name} ({channel.id}) in {total}s" + +#: discordsearch\discordsearch.py:245 +msgid "Unrecognized date/time." +msgstr "Data e ora non riconosciute." + diff --git a/discordsearch/locales/ja-JP.po b/discordsearch/locales/ja-JP.po new file mode 100644 index 0000000..bf082fa --- /dev/null +++ b/discordsearch/locales/ja-JP.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:21\n" +"Last-Translator: \n" +"Language-Team: Japanese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ja\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/discordsearch/locales/messages.pot\n" +"X-Crowdin-File-ID: 138\n" +"Language: ja_JP\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "役割を編集するためのコグです!" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "少なくとも1つのパラメータを指定する必要があります。" + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "正規表現処理に時間がかかりすぎました。メモリから削除します。" + +#: discordsearch\discordsearch.py:165 +msgid "There is an error in your regex.\n" +"{e}" +msgstr "正規表現に誤りがあります。\n" +"{e}" + +#: discordsearch\discordsearch.py:203 +msgid "Look at the original message." +msgstr "元のメッセージを見てください。" + +#: discordsearch\discordsearch.py:219 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "#{channel.name} で検索 ({channel.id})" + +#: discordsearch\discordsearch.py:220 +msgid "Sorry, I could not find any results." +msgstr "申し訳ありませんが、結果は見つかりませんでした。" + +#: discordsearch\discordsearch.py:233 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "#{channel.name} で検索 ({channel.id}) in {total}s" + +#: discordsearch\discordsearch.py:245 +msgid "Unrecognized date/time." +msgstr "認識できない日付/時刻。" + diff --git a/discordsearch/locales/messages.pot b/discordsearch/locales/messages.pot new file mode 100644 index 0000000..8e8cdec --- /dev/null +++ b/discordsearch/locales/messages.pot @@ -0,0 +1,51 @@ +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2024-12-29 10:43+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "" + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "" + +#: discordsearch\discordsearch.py:165 +msgid "" +"There is an error in your regex.\n" +"{e}" +msgstr "" + +#: discordsearch\discordsearch.py:206 +msgid "Look at the original message." +msgstr "" + +#: discordsearch\discordsearch.py:221 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "" + +#: discordsearch\discordsearch.py:222 +msgid "Sorry, I could not find any results." +msgstr "" + +#: discordsearch\discordsearch.py:235 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "" + +#: discordsearch\discordsearch.py:247 +msgid "Unrecognized date/time." +msgstr "" diff --git a/discordsearch/locales/nl-NL.po b/discordsearch/locales/nl-NL.po new file mode 100644 index 0000000..f59560a --- /dev/null +++ b/discordsearch/locales/nl-NL.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:21\n" +"Last-Translator: \n" +"Language-Team: Dutch\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: nl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/discordsearch/locales/messages.pot\n" +"X-Crowdin-File-ID: 138\n" +"Language: nl_NL\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "Een radertje om rollen te bewerken!" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "Je moet minstens één parameter opgeven." + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "Jouw regex proces duurde te lang. Verwijderen van geheugen." + +#: discordsearch\discordsearch.py:165 +msgid "There is an error in your regex.\n" +"{e}" +msgstr "Er is een foutmelding in jouw regex.\n" +"{e}" + +#: discordsearch\discordsearch.py:203 +msgid "Look at the original message." +msgstr "Kijk naar het oorspronkelijke bericht." + +#: discordsearch\discordsearch.py:219 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "Zoeken op #{channel.name} ({channel.id})" + +#: discordsearch\discordsearch.py:220 +msgid "Sorry, I could not find any results." +msgstr "Sorry, ik kon geen resultaten vinden." + +#: discordsearch\discordsearch.py:233 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "Zoeken in #{channel.name} ({channel.id}) in {total}s" + +#: discordsearch\discordsearch.py:245 +msgid "Unrecognized date/time." +msgstr "Niet-herkende datum/tijd." + diff --git a/discordsearch/locales/pl-PL.po b/discordsearch/locales/pl-PL.po new file mode 100644 index 0000000..35595b3 --- /dev/null +++ b/discordsearch/locales/pl-PL.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:21\n" +"Last-Translator: \n" +"Language-Team: Polish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/discordsearch/locales/messages.pot\n" +"X-Crowdin-File-ID: 138\n" +"Language: pl_PL\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "Trybik do edycji ról!" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "Musisz podać co najmniej jeden parametr." + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "Proces wyrażenia regularnego trwał zbyt długo. Usuwanie z pamięci." + +#: discordsearch\discordsearch.py:165 +msgid "There is an error in your regex.\n" +"{e}" +msgstr "W wyrażeniu regularnym wystąpił błąd.\n" +"{e}" + +#: discordsearch\discordsearch.py:203 +msgid "Look at the original message." +msgstr "Spójrz na oryginalną wiadomość." + +#: discordsearch\discordsearch.py:219 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "Szukaj w #{channel.name} ({channel.id})" + +#: discordsearch\discordsearch.py:220 +msgid "Sorry, I could not find any results." +msgstr "Niestety, nie udało mi się znaleźć żadnych wyników." + +#: discordsearch\discordsearch.py:233 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "Szukaj w #{channel.name} ({channel.id}) w {total}s" + +#: discordsearch\discordsearch.py:245 +msgid "Unrecognized date/time." +msgstr "Nierozpoznana data/czas." + diff --git a/discordsearch/locales/pt-BR.po b/discordsearch/locales/pt-BR.po new file mode 100644 index 0000000..232e6ce --- /dev/null +++ b/discordsearch/locales/pt-BR.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:21\n" +"Last-Translator: \n" +"Language-Team: Portuguese, Brazilian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-BR\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/discordsearch/locales/messages.pot\n" +"X-Crowdin-File-ID: 138\n" +"Language: pt_BR\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "Uma engrenagem para editar papéis!" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "Deve fornecer pelo menos um parâmetro." + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "O seu processo regex demorou demasiado tempo. Removendo da memória." + +#: discordsearch\discordsearch.py:165 +msgid "There is an error in your regex.\n" +"{e}" +msgstr "Há um erro no seu regex.\n" +"{e}" + +#: discordsearch\discordsearch.py:203 +msgid "Look at the original message." +msgstr "Veja-se a mensagem original." + +#: discordsearch\discordsearch.py:219 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "Pesquisar em #{channel.name} ({channel.id})" + +#: discordsearch\discordsearch.py:220 +msgid "Sorry, I could not find any results." +msgstr "Lamento, não consegui encontrar quaisquer resultados." + +#: discordsearch\discordsearch.py:233 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "Pesquisa em #{channel.name} ({channel.id}) em {total}s" + +#: discordsearch\discordsearch.py:245 +msgid "Unrecognized date/time." +msgstr "Data/hora não reconhecida." + diff --git a/discordsearch/locales/pt-PT.po b/discordsearch/locales/pt-PT.po new file mode 100644 index 0000000..25a95ad --- /dev/null +++ b/discordsearch/locales/pt-PT.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:21\n" +"Last-Translator: \n" +"Language-Team: Portuguese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-PT\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/discordsearch/locales/messages.pot\n" +"X-Crowdin-File-ID: 138\n" +"Language: pt_PT\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "Uma engrenagem para editar papéis!" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "Deve fornecer pelo menos um parâmetro." + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "O seu processo regex demorou demasiado tempo. Removendo da memória." + +#: discordsearch\discordsearch.py:165 +msgid "There is an error in your regex.\n" +"{e}" +msgstr "Há um erro no seu regex.\n" +"{e}" + +#: discordsearch\discordsearch.py:203 +msgid "Look at the original message." +msgstr "Veja-se a mensagem original." + +#: discordsearch\discordsearch.py:219 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "Pesquisar em #{channel.name} ({channel.id})" + +#: discordsearch\discordsearch.py:220 +msgid "Sorry, I could not find any results." +msgstr "Lamento, não consegui encontrar quaisquer resultados." + +#: discordsearch\discordsearch.py:233 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "Pesquisa em #{channel.name} ({channel.id}) em {total}s" + +#: discordsearch\discordsearch.py:245 +msgid "Unrecognized date/time." +msgstr "Data/hora não reconhecida." + diff --git a/discordsearch/locales/ro-RO.po b/discordsearch/locales/ro-RO.po new file mode 100644 index 0000000..b465aa4 --- /dev/null +++ b/discordsearch/locales/ro-RO.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:20\n" +"Last-Translator: \n" +"Language-Team: Romanian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ro\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/discordsearch/locales/messages.pot\n" +"X-Crowdin-File-ID: 138\n" +"Language: ro_RO\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "O rotiță pentru editarea rolurilor!" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "Trebuie să furnizați cel puțin un parametru." + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "Procesul regex a durat prea mult. Eliminarea din memorie." + +#: discordsearch\discordsearch.py:165 +msgid "There is an error in your regex.\n" +"{e}" +msgstr "Există o eroare în regex-ul dumneavoastră.\n" +"{e}" + +#: discordsearch\discordsearch.py:203 +msgid "Look at the original message." +msgstr "Uitați-vă la mesajul original." + +#: discordsearch\discordsearch.py:219 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "Căutați în #{channel.name} ({channel.id})" + +#: discordsearch\discordsearch.py:220 +msgid "Sorry, I could not find any results." +msgstr "Îmi pare rău, dar nu am găsit niciun rezultat." + +#: discordsearch\discordsearch.py:233 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "Căutați în #{channel.name} ({channel.id}) în {total}s" + +#: discordsearch\discordsearch.py:245 +msgid "Unrecognized date/time." +msgstr "Data/ora nerecunoscută." + diff --git a/discordsearch/locales/ru-RU.po b/discordsearch/locales/ru-RU.po new file mode 100644 index 0000000..0aaddcd --- /dev/null +++ b/discordsearch/locales/ru-RU.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:21\n" +"Last-Translator: \n" +"Language-Team: Russian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ru\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/discordsearch/locales/messages.pot\n" +"X-Crowdin-File-ID: 138\n" +"Language: ru_RU\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "Зубчик для редактирования ролей!" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "Вы должны указать как минимум один параметр." + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "Ваш процесс regex занял слишком много времени. Удаление из памяти." + +#: discordsearch\discordsearch.py:165 +msgid "There is an error in your regex.\n" +"{e}" +msgstr "В вашем регексе допущена ошибка.\n" +"{e}" + +#: discordsearch\discordsearch.py:203 +msgid "Look at the original message." +msgstr "Посмотрите на исходное сообщение." + +#: discordsearch\discordsearch.py:219 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "Поиск в #{channel.name} ({channel.id})" + +#: discordsearch\discordsearch.py:220 +msgid "Sorry, I could not find any results." +msgstr "К сожалению, я не смог найти никаких результатов." + +#: discordsearch\discordsearch.py:233 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "Поиск в #{channel.name} ({channel.id}) в {total}s" + +#: discordsearch\discordsearch.py:245 +msgid "Unrecognized date/time." +msgstr "Нераспознанная дата/время." + diff --git a/discordsearch/locales/tr-TR.po b/discordsearch/locales/tr-TR.po new file mode 100644 index 0000000..31b8c51 --- /dev/null +++ b/discordsearch/locales/tr-TR.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 13:27\n" +"Last-Translator: \n" +"Language-Team: Turkish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: tr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/discordsearch/locales/messages.pot\n" +"X-Crowdin-File-ID: 138\n" +"Language: tr_TR\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "Rolleri düzenlemek için bir cog!" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "En az bir parametre sağlamalısınız." + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "Regex işleminiz çok uzun sürdü. Hafızadan siliniyor." + +#: discordsearch\discordsearch.py:165 +msgid "There is an error in your regex.\n" +"{e}" +msgstr "Regex'inizde bir hata var.\n" +"{e}" + +#: discordsearch\discordsearch.py:203 +msgid "Look at the original message." +msgstr "Orijinal mesaja bakın." + +#: discordsearch\discordsearch.py:219 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "#{channel.name} ({channel.id}) içinde ara" + +#: discordsearch\discordsearch.py:220 +msgid "Sorry, I could not find any results." +msgstr "Üzgünüm, herhangi bir sonuç bulamadım." + +#: discordsearch\discordsearch.py:233 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "#{channel.name} ({channel.id}) içinde {total} ara" + +#: discordsearch\discordsearch.py:245 +msgid "Unrecognized date/time." +msgstr "Tanınmayan tarih/saat." + diff --git a/discordsearch/locales/uk-UA.po b/discordsearch/locales/uk-UA.po new file mode 100644 index 0000000..61f03b6 --- /dev/null +++ b/discordsearch/locales/uk-UA.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:21\n" +"Last-Translator: \n" +"Language-Team: Ukrainian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: uk\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/discordsearch/locales/messages.pot\n" +"X-Crowdin-File-ID: 138\n" +"Language: uk_UA\n" + +#: discordsearch\discordsearch.py:34 +#, docstring +msgid "A cog to edit roles!" +msgstr "Гвинтик для редагування ролей!" + +#: discordsearch\discordsearch.py:98 +msgid "You must provide at least one parameter." +msgstr "Ви повинні вказати хоча б один параметр." + +#: discordsearch\discordsearch.py:159 +msgid "Your regex process took too long. Removing from memory." +msgstr "Процес regex виконувався надто довго. Видалення з пам'яті." + +#: discordsearch\discordsearch.py:165 +msgid "There is an error in your regex.\n" +"{e}" +msgstr "У вашому рексі є помилка.\n" +"{e}" + +#: discordsearch\discordsearch.py:203 +msgid "Look at the original message." +msgstr "Подивіться на оригінальне повідомлення." + +#: discordsearch\discordsearch.py:219 +msgid "Search in #{channel.name} ({channel.id})" +msgstr "Шукайте в #{channel.name} ({channel.id})" + +#: discordsearch\discordsearch.py:220 +msgid "Sorry, I could not find any results." +msgstr "Вибачте, я не знайшов жодних результатів." + +#: discordsearch\discordsearch.py:233 +msgid "Search in #{channel.name} ({channel.id}) in {total}s" +msgstr "Шукайте в #{channel.name} ({channel.id}) в {total}s" + +#: discordsearch\discordsearch.py:245 +msgid "Unrecognized date/time." +msgstr "Нерозпізнана дата/час." + diff --git a/discordsearch/utils_version.json b/discordsearch/utils_version.json new file mode 100644 index 0000000..bfab002 --- /dev/null +++ b/discordsearch/utils_version.json @@ -0,0 +1 @@ +{"needed_utils_version": 7.0} \ No newline at end of file diff --git a/draw/LICENSE b/draw/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/draw/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/draw/README.rst b/draw/README.rst new file mode 100644 index 0000000..3f41c36 --- /dev/null +++ b/draw/README.rst @@ -0,0 +1,64 @@ +.. _draw: +==== +Draw +==== + +This is the cog guide for the ``Draw`` cog. This guide contains the collection of commands which you can use in the cog. +Through this guide, ``[p]`` will always represent your prefix. Replace ``[p]`` with your own prefix when you use these commands in Discord. + +.. note:: + + Ensure that you are up to date by running ``[p]cog update draw``. + If there is something missing, or something that needs improving in this documentation, feel free to create an issue `here `_. + This documentation is generated everytime this cog receives an update. + +--------------- +About this cog: +--------------- + +A cog to make pixel arts on Discord! + +--------- +Commands: +--------- + +Here are all the commands included in this cog (1): + +* ``[p]draw [from_message] [height=9] [width=9] ["🟥"|"🟧"|"🟨"|"🟩"|"🟦"|"🟪"|"🟫"|"⬛"|"⬜"|"transparent"=transparent]`` + Make a pixel art on Discord. + +------------ +Installation +------------ + +If you haven't added my repo before, lets add it first. We'll call it "AAA3A-cogs" here. + +.. code-block:: ini + + [p]repo add AAA3A-cogs https://github.com/AAA3A-AAA3A/AAA3A-cogs + +Now, we can install Draw. + +.. code-block:: ini + + [p]cog install AAA3A-cogs draw + +Once it's installed, it is not loaded by default. Load it by running the following command: + +.. code-block:: ini + + [p]load draw + +---------------- +Further Support: +---------------- + +Check out my docs `here `_. +Mention me in the #support_other-cogs in the `cog support server `_ if you need any help. +Additionally, feel free to open an issue or pull request to this repo. + +-------- +Credits: +-------- + +Thanks to Kreusada for the Python code to automatically generate this documentation! \ No newline at end of file diff --git a/draw/__init__.py b/draw/__init__.py new file mode 100644 index 0000000..3236ddb --- /dev/null +++ b/draw/__init__.py @@ -0,0 +1,46 @@ +from redbot.core import errors # isort:skip +import importlib +import sys + +try: + import AAA3A_utils +except ModuleNotFoundError: + raise errors.CogLoadError( + "The needed utils to run the cog were not found. Please execute the command `[p]pipinstall git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." + ) +modules = sorted( + [module for module in sys.modules if module.split(".")[0] == "AAA3A_utils"], reverse=True +) +for module in modules: + try: + importlib.reload(sys.modules[module]) + except ModuleNotFoundError: + pass +del AAA3A_utils +# import AAA3A_utils +# import json +# import os +# __version__ = AAA3A_utils.__version__ +# with open(os.path.join(os.path.dirname(__file__), "utils_version.json"), mode="r") as f: +# data = json.load(f) +# needed_utils_version = data["needed_utils_version"] +# if __version__ > needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a higher version than the one supported by this version of the cog. Please update the cogs of the `AAA3A-cogs` repo." +# ) +# elif __version__ < needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a lower version than the one supported by this version of the cog. Please execute the command `[p]pipinstall --upgrade git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." +# ) + +from redbot.core.bot import Red # isort:skip +from redbot.core.utils import get_end_user_data_statement + +from .draw import Draw + +__red_end_user_data_statement__ = get_end_user_data_statement(file=__file__) + + +async def setup(bot: Red) -> None: + cog = Draw(bot) + await bot.add_cog(cog) diff --git a/draw/board.py b/draw/board.py new file mode 100644 index 0000000..bed0d71 --- /dev/null +++ b/draw/board.py @@ -0,0 +1,417 @@ +from redbot.core import commands # isort:skip +import discord # isort:skip +import typing # isort:skip +import typing_extensions # isort:skip + +import io +from dataclasses import dataclass + +import numpy as np +from PIL import Image, ImageDraw + +from .color import Color +from .constants import ( + COLUMN_ICONS, + COLUMN_ICONS_DICT, + IMAGE_EXTENSION, + LB, + MAIN_COLORS, + MAIN_COLORS_DICT, + PADDING, + ROW_ICONS, + ROW_ICONS_DICT, + u200b, +) # NOQA + + +@dataclass +class Coords: + x: int + y: int + + def __post_init__(self) -> None: + self.ix: int = self.x * -1 + self.iy: int = self.y * -1 + + +class Board: + def __init__( + self, + cog: commands.Cog, + *, + height: typing.Optional[int] = 9, + width: typing.Optional[int] = 9, + background: typing.Optional[ + typing.Literal["🟥", "🟧", "🟨", "🟩", "🟦", "🟪", "🟫", "⬛", "⬜", "transparent"] + ] = MAIN_COLORS[ + -1 + ], # Literal[*MAIN_COLORS] + ) -> None: + self.cog: commands.Cog = cog + + self.height: int = height + self.width: int = width + self.background: str = background + + self.cursor_display: bool = True + + self.initial_board: np.ndarray = np.full( + (self.height, self.width), self.background, dtype="object" + ) + self.board_history: typing.List[np.ndarray] = [self.initial_board.copy()] + self.board_index: int = 0 + self.set_attributes() + + # This is for the select tool. + self.initial_coords: typing.Tuple[int, int] + self.initial_row: int = 0 + self.initial_col: int = 0 + self.final_coords: typing.Tuple[int, int] + self.final_row: int = 0 + self.final_col: int = 0 + + self.clear_cursors() + + def set_attributes(self) -> None: + self.row_labels: typing.Tuple[str] = ROW_ICONS[: self.height] + self.col_labels: typing.Tuple[str] = COLUMN_ICONS[: self.width] + self.centre: typing.Tuple[int, int] = ( + len(self.row_labels) // 2, + len(self.col_labels) // 2, + ) + self.centre_row, self.centre_col = self.centre + + self.cursor: str = self.background + self.cursor_row, self.cursor_col = self.centre + self.cursor_row_max = len(self.row_labels) - 1 + self.cursor_col_max = len(self.col_labels) - 1 + self.cursor_coords: typing.List[typing.Tuple[int, int]] = [ + (self.cursor_row, self.cursor_col) + ] + + def __str__(self) -> str: + """Method that gives a formatted version of the board with row/col labels.""" + cursor_rows = tuple(row for row, __ in self.cursor_coords) + cursor_cols = tuple(col for __, col in self.cursor_coords) + row_labels = [ + (str(row) if idx not in cursor_rows else str(ROW_ICONS_DICT[row])) + for idx, row in enumerate(self.row_labels) + ] + col_labels = [ + (str(col) if idx not in cursor_cols else str(COLUMN_ICONS_DICT[col])) + for idx, col in enumerate(self.col_labels) + ] + return ( + f"{self.cursor}{PADDING}{u200b.join(col_labels)}\n" + f"\n{LB.join([f'{row_labels[idx]}{PADDING}{u200b.join(row)}' for idx, row in enumerate(self.board)])}" + ) + + async def to_image(self) -> Image: + height, width = len(self.board), len(self.board[0]) + cursor_rows = tuple(row for row, __ in self.cursor_coords) + cursor_cols = tuple(col for __, col in self.cursor_coords) + row_labels = [ + (row if idx not in cursor_rows else ROW_ICONS_DICT[row]) + for idx, row in enumerate(self.row_labels) + ] + col_labels = [ + (col if idx not in cursor_cols else COLUMN_ICONS_DICT[col]) + for idx, col in enumerate(self.col_labels) + ] + + size = 25 + sp = 1 if self.cursor_display else 0 + _width = size * (width + 1) + sp * width + round(size / 4) + _height = size * (height + 1) + sp * height + round(size / 4) + img: Image.Image = Image.new("RGBA", (_width, _height), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + y = 0 + for _y in range(0 if self.cursor_display else 2, height + 2): + if _y == 1: + y += round(size / 4) + continue + x = 0 + for _x in range(0 if self.cursor_display else 2, width + 2): + if _x == 1: + x += round(size / 4) + continue + # draw.rounded_rectangle((x, y, x + size, y + size), radius=3, fill=(255, 255, 255), outline=(255, 0, 0) if (_y - 2, _x - 2) in self.cursor_coords else (0, 0, 0)) + if _x == 0 and _y == 0: + cursor = self.cursor + if cursor == "transparent": + x += size + sp + continue + image: Image.Image = await self.cog.get_pixel( + MAIN_COLORS_DICT.get(cursor, cursor) + ) + elif _x == 0 and _y > 1: + emoji = row_labels[_y - 2] + image: Image.Image = await self.cog.get_pixel(emoji) + elif _y == 0 and _x > 1: + emoji = col_labels[_x - 2] + image: Image.Image = await self.cog.get_pixel(emoji) + else: + pixel = self.board[_y - 2, _x - 2] + if pixel == "transparent": + if self.cursor_display: + if (_y - 2, _x - 2) in self.cursor_coords: + draw.rounded_rectangle( + (x, y, x + size, y + size), + radius=3, + fill=None, + outline=( + (18, 18, 20, 255) + if getattr( + MAIN_COLORS_DICT.get(self.cursor, self.cursor), + "RGBA", + (0, 0, 0, 0), + ) + == (0, 0, 0, 255) + else ( + MAIN_COLORS_DICT.get(self.cursor, self.cursor).RGBA + if isinstance( + MAIN_COLORS_DICT.get(self.cursor, self.cursor), + Color, + ) + and self.cursor != "transparent" + else (255, 0, 0, 255) + ) + ), + width=2, + ) + else: + draw.rounded_rectangle( + (x, y, x + size, y + size), radius=3, outline=(0, 0, 0, 255) + ) + x += size + sp + continue + image: Image.Image = await self.cog.get_pixel( + MAIN_COLORS_DICT.get(pixel, pixel) + ) + image = image.resize((size, size)) + mask = Image.new("L", image.size, 0) + d = ImageDraw.Draw(mask) + # if self.cursor_display and (_y - 2, _x - 2) in self.cursor_coords: + # d.ellipse((0, 0, image.width, image.height), fill=255) + # else: + d.rounded_rectangle( + (0, 0, image.width, image.height), + radius=3 if self.cursor_display else 0, + fill=255, + ) + img.paste(image, (x, y, x + size, y + size), mask=mask) + if self.cursor_display and (_y - 2, _x - 2) in self.cursor_coords: + draw.rounded_rectangle( + (x, y, x + size, y + size), + radius=3, + fill=None, + outline=( + (18, 18, 20, 255) + if getattr( + MAIN_COLORS_DICT.get(self.cursor, self.cursor), + "RGBA", + (0, 0, 0, 0), + ) + == (0, 0, 0, 255) + else ( + MAIN_COLORS_DICT.get(self.cursor, self.cursor).RGBA + if isinstance( + MAIN_COLORS_DICT.get(self.cursor, self.cursor), Color + ) + and self.cursor != "transparent" + else (255, 0, 0, 255) + ) + ), + width=2, + ) + x += size + sp + y += size + sp + return img + + async def to_file(self) -> discord.File: + img: Image.Image = await self.to_image() + buffer = io.BytesIO() + img.save(buffer, format=IMAGE_EXTENSION, optimize=True) + buffer.seek(0) + return discord.File(buffer, filename=f"image.{IMAGE_EXTENSION.lower()}") + + @property + def board(self) -> np.ndarray: + return self.board_history[self.board_index] + + @board.setter + def board(self, board: np.ndarray): + self.board_history.append(board) + self.board_index += 1 + + @property + def backup_board(self) -> np.ndarray: + return self.board_history[self.board_index - 1] + + def modify( + self, + *, + height: typing.Optional[int] = None, + width: typing.Optional[int] = None, + background: typing.Optional[ + typing.Literal["🟥", "🟧", "🟨", "🟩", "🟦", "🟪", "🟫", "⬛", "⬜", "transparent"] + ] = None, # typing.Literal[*MAIN_COLORS] + ) -> None: + height = height or self.height + width = width or self.width + background = background or self.background + if all( + (self.height == height, self.width == width, self.background == background) + ): # the attributes haven't been changed + return + if np.array_equal( + self.initial_board, self.board + ): # Board has only background, so replace all pixels. + self.__init__(cog=self.cog, height=height, width=width, background=background) + return + overlay = self.board + base = np.full((height, width), background, dtype="object") + # Coordinates of the centre of the overlay board + overlay_centre = Coords(overlay.shape[1] // 2, overlay.shape[0] // 2) + # Coordinates of the centre of the base board + base_centre = Coords(base.shape[1] // 2, base.shape[0] // 2) + # Difference between the centres + centre_diff = Coords(base_centre.x - overlay_centre.x, base_centre.y - overlay_centre.y) + # Coordinates where the overlay board should crop from + # x = overlay's centre's width MINUS base's centre's width, if greater than 0, else 0 + # y = overlay's centre's height MINUS base's centre's height, if greater than 0, else 0 + # Meaning that if base is larger than overlay, it will include from the start of overlay + overlay_from = Coords(max(centre_diff.ix, 0), max(centre_diff.iy, 0)) + # Coordinates where the overlay board should crop to + # x = base's total width MINUS its centre's x-coord PLUS overlay's centre's x-coord + # y = base's total height MINUS its centre's y-coord PLUS overlay's centre's y-coord + # This formula gives an optimal value to crop the overlay board *to*, for both + # smaller and larger overlay boards + overlay_to = Coords( + (base.shape[1] - base_centre.x) + overlay_centre.x, + (base.shape[0] - base_centre.y) + overlay_centre.y, + ) + # Coordinates where the base board should paste from + # x = base's centre's width MINUS overlay's centre's width, if bigger than 0, else 0 + # y = base's centre's height MINUS overlay's centre's height, if bigger than 0, else 0 + # Meaning that if overlay is larger than base, it will start pasting from the start of base + base_overlay_from = Coords(max(centre_diff.x, 0), max(centre_diff.y, 0)) + # Coordinates where the base board should paste to + # x = whichever is less b/w base board's width and overlay board's width PLUS x-coord of beginning (for respective offset) + # y = whichever is less b/w base board's height and overlay board's height PLUS y-coord of beginning (for respective offset) + base_overlay_to = Coords( + min(overlay.shape[1], base.shape[1]) + base_overlay_from.x, + min(overlay.shape[0], base.shape[0]) + base_overlay_from.y, + ) + # Crops overlay board if necessary (i.e. if base < overlay) + overlay = overlay[overlay_from.y : overlay_to.y, overlay_from.x : overlay_to.x] + # Pastes cropped overlay board on top of the selected portion of base board + base[ + base_overlay_from.y : base_overlay_to.y, + base_overlay_from.x : base_overlay_to.x, + ] = overlay + # return Board.from_board(base, background=background) + self.__init__(cog=self.cog, height=len(base), width=len(base[0]), background=background) + self.board_history = [base] + + @property + def cursor_pixel(self) -> typing.Any: + return self.board[self.cursor_row, self.cursor_col] + + @cursor_pixel.setter + def cursor_pixel(self, value: str) -> None: + if not isinstance(value, str): + raise TypeError("Value must be a string.") + self.board[self.cursor_row, self.cursor_col] = value + + def get_pixel( + self, + row: typing.Optional[int] = None, + col: typing.Optional[int] = None, + ) -> typing.Any: + row = row if row is not None else self.cursor_row + col = col if col is not None else self.cursor_col + return self.board[row, col] + + @classmethod + def from_board( + cls, + cog: commands.Cog, + board: np.ndarray, + *, + background: typing.Optional[str] = MAIN_COLORS[-1], + ) -> typing_extensions.Self: + height = len(board) + width = len(board[0]) + board_obj = cls(cog=cog, height=height, width=width, background=background) + board_obj.board_history = [board] + return board_obj + + @classmethod + def from_str( + cls, string: str, *, background: typing.Optional[str] = None + ) -> typing_extensions.Self: + lines = string.split("\n")[2:] + board = [line.split(PADDING)[-1].split("\u200b") for line in lines] + board = cls.from_board(np.array(board, dtype="object"), background=background) + board.clear_cursors() + return board + + def clear(self) -> None: + self.draw(self.background, coords=np.array(np.where(self.board != self.background)).T) + self.clear_cursors() + + def draw( + self, + color: typing.Optional[typing.Union[str, discord.Emoji, int, Color]] = None, + *, + coords: typing.Optional[typing.List[typing.Tuple[int, int]]] = None, + ) -> bool: + color = color or self.cursor + color_pixel = getattr(color, "id", color) + coords = coords if coords is not None else self.cursor_coords + + cursor_matches = [] + for row, col in coords: + if self.board[row, col] == color_pixel: + cursor_matches.append(True) + else: + cursor_matches.append(False) + if all(cursor_matches): + return False + + self.board_history = self.board_history[: self.board_index + 1] + self.board = self.board.copy() + for row, col in coords: + self.board[row, col] = color_pixel + return True + + def clear_cursors(self, *, empty: typing.Optional[bool] = False) -> None: + self.cursor_coords = [(self.cursor_row, self.cursor_col)] if empty is False else [] + + def move_cursor( + self, + row_move: typing.Optional[int] = 0, + col_move: typing.Optional[int] = 0, + select: typing.Optional[bool] = False, + ) -> None: + self.clear_cursors() + self.cursor_row = (self.cursor_row + row_move) % (self.cursor_row_max + 1) + self.cursor_col = (self.cursor_col + col_move) % (self.cursor_col_max + 1) + if select is True: + self.initial_col, self.initial_row = self.initial_coords + self.final_coords = (self.cursor_row, self.cursor_col) + self.final_row, self.final_col = self.final_coords + self.cursor_coords = [ + (row, col) + for col in range( + min(self.initial_col, self.final_col), + max(self.initial_col, self.final_col) + 1, + ) + for row in range( + min(self.initial_row, self.final_row), + max(self.initial_row, self.final_row) + 1, + ) + ] + else: + self.cursor_coords = [(self.cursor_row, self.cursor_col)] diff --git a/draw/color.py b/draw/color.py new file mode 100644 index 0000000..23635be --- /dev/null +++ b/draw/color.py @@ -0,0 +1,137 @@ +from redbot.core import commands # isort:skip +import discord # isort:skip +import typing # isort:skip +import typing_extensions # isort:skip + +import asyncio +import io +import re +from functools import cached_property + +import aiohttp +from PIL import Image + +IMAGE_EXTENSION = "PNG" + +CHANNEL = "[a-fA-F0-9]{2}" +HEX_REGEX = re.compile( + rf"\b(?P{CHANNEL})(?P{CHANNEL})(?P{CHANNEL})(?P{CHANNEL})?\b" +) + + +class Color: + # RGB_A accepts RGB values and an optional Alpha value. + def __init__(self, RGB_A: typing.Tuple[int, int, int, typing.Optional[int]]) -> None: + self.RGBA: typing.Tuple[int, int, int, int] = RGB_A if len(RGB_A) == 4 else (*RGB_A, 255) + self.RGB: typing.Tuple[int, int, int] = self.RGBA[:3] + self.R, self.G, self.B, self.A = self.RGBA + + self.loop = asyncio.get_running_loop() + + @cached_property + def hex(self) -> str: + return "%02x%02x%02x%02x" % self.RGBA + + def __str__(self) -> str: + return f"#{self.hex}" + + async def get_name(self) -> str: + async with aiohttp.ClientSession() as session: + async with session.get( + "https://www.thecolorapi.com/id", params={"hex": self.hex[:-2]} + ) as r: + color_response = await r.json() + return color_response.get("name", {}).get("value", "?") + + async def to_bytes(self) -> bytes: + return await self.loop.run_in_executor(None, self._to_bytes) + + def _to_bytes(self) -> bytes: + image = self._to_image() + buffer = io.BytesIO() + image.save(buffer, IMAGE_EXTENSION) + buffer.seek(0) + return buffer.getvalue() + + async def to_file(self) -> discord.File: + return await self.loop.run_in_executor(None, self._to_file) + + def _to_file(self) -> discord.File: + image_bytes = io.BytesIO(self._to_bytes()) + return discord.File(image_bytes, filename=f"{self.hex}.{IMAGE_EXTENSION.lower()}") + + async def to_image(self) -> Image.Image: + return await self.loop.run_in_executor(None, self._to_image) + + def _to_image(self) -> Image.Image: + # # If you pass in an emoji, it uses that as base + # # Else it uses the base_emoji property which uses 🟪 + # base_emoji = self.base_emoji + # data = np.array(base_emoji) + # r, g, b, a = data.T + # data[..., :-1][a != 0] = self.RGB + # # Set the alpha relatively, to respect individual alpha values + # alpha_percent = self.A / 255 + # data[..., -1] = alpha_percent * data[..., -1] + # return Image.fromarray(data) + return Image.new("RGBA", (100, 100), self.RGBA) + + async def to_emoji(self, guild: discord.Guild) -> discord.Emoji: + return await guild.create_custom_emoji(name=self.hex, image=await self.to_bytes()) + + @classmethod + async def from_emoji( + cls, cog: commands.Cog, emoji: typing.Union[str, discord.Emoji, discord.PartialEmoji] + ) -> typing_extensions.Self: + image = await cog.get_pixel(emoji) + colors = [ + color + for color in sorted( + image.getcolors(image.size[0] * image.size[1]), + key=lambda c: c[0], + reverse=True, + ) + if color[-1][-1] != 0 + ] + return cls(colors[0][1]) + + @classmethod + async def from_attachment( + cls, attachment: discord.Attachment, *, n_colors: typing.Optional[int] = 5 + ) -> typing.List[typing_extensions.Self]: + image = Image.open(io.BytesIO(await attachment.read())) + colors: typing.Tuple[int, typing.Tuple[int, int, int]] = [ + color + for color in sorted( + image.getcolors(image.size[0] * image.size[1]), + key=lambda c: c[0], + reverse=True, + ) + if color[-1][-1] != 0 + ] + return [cls(colors[min(len(colors), i)][-1]) for i in range(n_colors)] + + @classmethod + def from_hex(cls, hex: str) -> typing_extensions.Self: + if (match := HEX_REGEX.match(hex)) is None: + raise ValueError("Invalid hex code provided.") + RGBA = ( + int(match.group("red"), 16), + int(match.group("green"), 16), + int(match.group("blue"), 16), + int(match.group("alpha") or "ff", 16), + ) + return cls(RGBA) + + @classmethod + def mix_colors( + cls, + colors: typing.List[ + typing.Union[typing_extensions.Self, typing.Tuple[int, int, int, int]] + ], + ) -> typing_extensions.Self: + colors = [color.RGBA if isinstance(color, Color) else color for color in colors] + total_weight = len(colors) + return cls( + tuple(round(sum(color) / total_weight) for color in zip(*colors)), + ) diff --git a/draw/constants.py b/draw/constants.py new file mode 100644 index 0000000..88ea05e --- /dev/null +++ b/draw/constants.py @@ -0,0 +1,133 @@ +import discord # isort:skip +import typing # isort:skip + +from .color import Color + + +def base_colors_options() -> typing.List[discord.SelectOption]: + return [ + discord.SelectOption(label="Red", emoji="🟥", value="🟥"), + discord.SelectOption(label="Orange", emoji="🟧", value="🟧"), + discord.SelectOption(label="Yellow", emoji="🟨", value="🟨"), + discord.SelectOption(label="Green", emoji="🟩", value="🟩"), + discord.SelectOption(label="Blue", emoji="🟦", value="🟦"), + discord.SelectOption(label="Purple", emoji="🟪", value="🟪"), + discord.SelectOption(label="Brown", emoji="🟫", value="🟫"), + discord.SelectOption(label="Black", emoji="⬛", value="⬛"), + discord.SelectOption(label="White", emoji="⬜", value="⬜"), + discord.SelectOption(label="Transparent", emoji=None, value="transparent"), + ] + + +MAIN_COLORS_DICT: typing.Dict[str, Color] = { + "🟥": Color((255, 0, 0, 255)), + "🟧": Color((255, 130, 0, 255)), + "🟨": Color((255, 255, 0, 255)), + "🟩": Color((0, 255, 0, 255)), + "🟦": Color((0, 190, 255, 255)), + "🟪": Color((210, 110, 255, 255)), + "🟫": Color((200, 100, 80, 255)), + "⬛": Color((0, 0, 0, 255)), + "⬜": Color((255, 255, 255, 255)), +} +MAIN_COLORS: typing.List[str] = list(MAIN_COLORS_DICT.keys()) + ["transparent"] + +MIN_HEIGHT_OR_WIDTH: int = 5 +MAX_HEIGHT_OR_WIDTH: int = 17 + + +def base_height_or_width_select_options( + prefix: typing.Optional[str] = "", +) -> typing.List[discord.SelectOption]: + return [ + discord.SelectOption(label=f"{f'{prefix} = ' if prefix else prefix}{n}", value=n) + for n in range(MIN_HEIGHT_OR_WIDTH, MAX_HEIGHT_OR_WIDTH + 1) + ] + + +ROW_ICONS_DICT: typing.Dict[str, int] = { + "🇦": 799628816846815233, + "🇧": 799628882713509891, + "🇨": 799620822716383242, + "🇩": 799621070319255572, + "🇪": 799621103030894632, + "🇫": 799621133174571008, + "🇬": 799621170450137098, + "🇭": 799621201621811221, + "🇮": 799621235226050561, + "🇯": 799621266842583091, + "🇰": 799621296408887357, + "🇱": 799621320408301638, + "🇲": 799621344740114473, + "🇳": 799621367297343488, + "🇴": 799628923260370945, + "🇵": 799621387219369985, + "🇶": 799621417049260042, +} +ROW_ICONS = list(ROW_ICONS_DICT.keys()) +COLUMN_ICONS_DICT: typing.Dict[typing.Union[str, int], int] = { + "0️⃣": 1000010892500537437, + "1️⃣": 1000010893981143040, + "2️⃣": 1000010895331692555, + "3️⃣": 1000010896946499614, + "4️⃣": 1000010898213195937, + "5️⃣": 1000010899714740224, + "6️⃣": 1000010901744791653, + "7️⃣": 1000010902726262857, + "8️⃣": 1000010904240402462, + "9️⃣": 1000010905276403773, + "🔟": 1000011148537626624, + 1032564324281098240: 1000011153226874930, + 1032564339946823681: 1000011154262851634, + 1032564356380098630: 1000011155391131708, + 1032564734609862696: 1000011156787834970, + 1032564783850983464: 1000011158348120125, + 1032564935412174868: 1000011159623192616, +} +COLUMN_ICONS = list(COLUMN_ICONS_DICT.keys()) + +LETTER_TO_NUMBER: typing.Dict[str, int] = { + "A": 0, + "B": 1, + "C": 2, + "D": 3, + "E": 4, + "F": 5, + "G": 6, + "H": 7, + "I": 8, + "J": 9, + "K": 10, + "L": 11, + "M": 12, + "N": 13, + "O": 14, + "P": 15, + "Q": 16, + "R": 17, + "S": 18, + "T": 19, + "U": 20, + "V": 21, + "W": 22, + "X": 23, + "Y": 24, + "Z": 25, +} +ALPHABETS: typing.Tuple[str] = tuple(LETTER_TO_NUMBER.keys()) +NUMBERS: typing.Tuple[int] = tuple(LETTER_TO_NUMBER.values()) + +u200b: str = "\u200b" +PADDING: str = f" {u200b}" * 6 +LB: str = "\n" + +DEFAULT_CACHE: typing.List[typing.Union[str, int]] = ( + list(MAIN_COLORS_DICT.keys()) + + list(MAIN_COLORS_DICT.values()) + + ROW_ICONS + + COLUMN_ICONS + + list(ROW_ICONS_DICT.values()) + + list(COLUMN_ICONS_DICT.values()) +) + +IMAGE_EXTENSION = "PNG" diff --git a/draw/draw.py b/draw/draw.py new file mode 100644 index 0000000..543f035 --- /dev/null +++ b/draw/draw.py @@ -0,0 +1,174 @@ +from AAA3A_utils import Cog # isort:skip +from redbot.core import commands, app_commands # isort:skip +from redbot.core.bot import Red # isort:skip +from redbot.core.i18n import Translator, cog_i18n # isort:skip +import discord # isort:skip +import typing # isort:skip + + +import asyncio +import io +from urllib.parse import quote_plus + +import aiohttp +from PIL import Image, ImageFilter, UnidentifiedImageError + +from .board import Board +from .color import Color +from .constants import ( + DEFAULT_CACHE, + IMAGE_EXTENSION, + MAIN_COLORS, + MAX_HEIGHT_OR_WIDTH, + MIN_HEIGHT_OR_WIDTH, + base_colors_options, +) # NOQA +from .start_view import StartDrawView +from .view import DrawView + +# Credits: +# General repo credits. +# Thanks to WitherredAway for the full Draw code (https://github.com/WitherredAway/Yeet/blob/master/cogs/Draw) and his indispensable help! +# Changes: Use Pillow images instead of custom emojis for the board itself, adaptation to Red bot, download images from Discord or Internet, add "Display Cursor" and "Raw Paint" buttons, allow to do a pixels selection with "ABCD" button... +# Thanks to Karlo in Red main server for his ideas and testing the cog! + +_: Translator = Translator("Draw", __file__) + + +@cog_i18n(_) +class Draw(Cog): + """A cog to make pixel arts on Discord!""" + + __authors__: typing.List[str] = ["WitherredAway", "AAA3A"] + + def __init__(self, bot: Red) -> None: + super().__init__(bot=bot) + + self._session: aiohttp.ClientSession = None + self.cache: typing.Dict[ + typing.Union[str, int, typing.Tuple[int, int, int, int]] + ] = {} # Unicode emojis, colors RGB and Discord custom emojis ids. + + async def cog_load(self) -> None: + await super().cog_load() + self._session: aiohttp.ClientSession = aiohttp.ClientSession() + asyncio.create_task(self.generate_cache()) + + async def generate_cache(self) -> None: + for pixel in DEFAULT_CACHE: + await self.get_pixel(pixel) + + async def cog_unload(self) -> None: + if self._session is not None: + await self._session.close() + await super().cog_unload() + + @property + def drawings(self) -> typing.Dict[discord.Message, DrawView]: + return self.views + + async def get_pixel( + self, + pixel: typing.Union[ + str, discord.Emoji, int, Color, typing.Tuple[int, int, int, typing.Optional[int]] + ], + to_file: typing.Optional[bool] = False, + ) -> typing.Union[Image.Image, discord.File]: + if isinstance(pixel, typing.Tuple) and len(pixel) in {3, 4}: + pixel = Color(pixel) + try: + pixel = int(pixel) + except (ValueError, TypeError): + pass + if isinstance(pixel, discord.PartialEmoji): + pixel = pixel.id or pixel.name + if isinstance(pixel, (discord.Emoji, int)): # Discord custom emoji + key = getattr(pixel, "id", pixel) + url = f"https://cdn.discordapp.com/emojis/{key}.png" + elif isinstance(pixel, str): + if pixel.startswith("http"): # URL + key = pixel + url = pixel + else: # Unicode + try: + key = hex(ord(pixel))[2:] + url = f"https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/72x72/{key}.png" + except TypeError: + key = pixel + url = f"https://emojicdn.elk.sh/{quote_plus(key)}?style=twitter" + elif isinstance(pixel, Color): + key = pixel.RGBA + if key not in self.cache: + url = None + image = await pixel.to_image() + else: + raise TypeError(pixel) + if key in self.cache: + image = self.cache[key] + else: + if url is not None: + async with self._session.get(url) as r: + image_bytes = await r.read() + try: + image = Image.open(io.BytesIO(image_bytes)) + except (AttributeError, UnidentifiedImageError) as e: + self.logger.error( + f"Error when retrieving the pixel {key} ({url}) image for the cache.", + exc_info=e, + ) + return Image.new("RGBA", (100, 100), (0, 0, 0, 0)) + try: + image = image.filter(ImageFilter.SHARPEN) # Maybe useless. + except ValueError: + pass + self.cache[key] = image + if to_file: + buffer = io.BytesIO() + image.save(buffer, format=IMAGE_EXTENSION, optimize=True) + buffer.seek(0) + return discord.File(buffer, filename=f"pixel.{IMAGE_EXTENSION.lower()}") + return image + + @commands.bot_has_permissions(embed_links=True, attach_files=True) + @commands.hybrid_command(aliases=["paint", "pixelart"]) + @app_commands.choices( + height=[ + app_commands.Choice(name=str(n), value=str(n)) + for n in range(MIN_HEIGHT_OR_WIDTH, MAX_HEIGHT_OR_WIDTH + 1) + ], + width=[ + app_commands.Choice(name=str(n), value=str(n)) + for n in range(MIN_HEIGHT_OR_WIDTH, MAX_HEIGHT_OR_WIDTH + 1) + ], + background=[ + app_commands.Choice(name=f"{option.emoji} {option.label}", value=option.value) + for option in base_colors_options() + ], + ) + async def draw( + self, + ctx: commands.Context, + from_message: typing.Optional[commands.MessageConverter] = None, + height: typing.Optional[commands.Range[int, MIN_HEIGHT_OR_WIDTH, MAX_HEIGHT_OR_WIDTH]] = 9, + width: typing.Optional[commands.Range[int, MIN_HEIGHT_OR_WIDTH, MAX_HEIGHT_OR_WIDTH]] = 9, + background: typing.Literal[ + "🟥", "🟧", "🟨", "🟩", "🟦", "🟪", "🟫", "⬛", "⬜", "transparent" + ] = MAIN_COLORS[ + -1 + ], # typing.Literal[*MAIN_COLORS] + ) -> None: + """Make a pixel art on Discord.""" + if from_message is None: + board = (height, width, background) + else: + if from_message not in self.drawings: + raise commands.UserFeedbackCheckFailure(_("This message isn't in the cache.")) + board = Board( + cog=self, + height=self.drawings[from_message].board.height, + width=self.drawings[from_message].width, + background=background, + ) + board.board_history = self.drawings[from_message].board.board_history.copy() + board.board_index = self.drawings[from_message].board.board_index + await StartDrawView(cog=self, board=board).start(ctx) diff --git a/draw/info.json b/draw/info.json new file mode 100644 index 0000000..ce97b04 --- /dev/null +++ b/draw/info.json @@ -0,0 +1,16 @@ +{ + "author": ["WitherredAway", "AAA3A"], + "name": "Draw", + "install_msg": "Thank you for installing this cog!\nDo `[p]help CogName` to get the list of commands and their description. If you enjoy my work, please consider donating on [Buy Me a Coffee]() or [Ko-Fi]()!", + "short": "A cog to make pixel arts on Discord!", + "description": "A cog to make pixel arts on Discord!", + "tags": [ + "draw", + "pixel", + "art", + "paint" + ], + "requirements": ["git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git", "numpy", "pillow", "emoji"], + "min_bot_version": "3.5.0", + "end_user_data_statement": "This cog does not persistently store data or metadata about users." +} \ No newline at end of file diff --git a/draw/locales/de-DE.po b/draw/locales/de-DE.po new file mode 100644 index 0000000..f22c052 --- /dev/null +++ b/draw/locales/de-DE.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:23\n" +"Last-Translator: \n" +"Language-Team: German\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: de\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/draw/locales/messages.pot\n" +"X-Crowdin-File-ID: 231\n" +"Language: de_DE\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "Ein Rädchen zum Erstellen von Pixelkunst auf Discord!" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "Diese Nachricht befindet sich nicht im Cache." + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." +msgstr "Erstellen Sie eine neue Zeichentafel mit `Höhe = {height}`, `Breite = {width}` und `Hintergrund = {background}`." + +#: draw\view.py:332 +msgid "Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "Bitte geben Sie alle Farben ein, die Sie hinzufügen möchten. Sie können entweder eine oder alle der folgenden sein:\n" +"- Die Hex-Codes (z.B. `ff64c4` oder `ff64c4ff`, um Alpha einzuschließen) **getrennt durch Leerzeichen**,\n" +"- Die RGB(A)-Werte getrennt durch Leerzeichen oder Komma oder beides (z.B.. `(255 100 196)` oder `(255, 100, 196, 125)`) jeder Farbe **umgeben von Klammern**\n" +"- Jedes Emoji, dessen Hauptfarbe Sie extrahieren möchten (z.B. 🐸 ergibt 77b255)\n" +"- Jede Bilddatei (die ersten 5 reichlich vorhandenen Farben werden extrahiert)." + +#: draw\view.py:402 +msgid "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." +msgstr "Bitte sende eine Nachricht mit den Emojis, die du zu deiner Palette hinzufügen möchtest. Z.B. `😎 Ich mag Schildkröten 🐢`." + +#: draw\view.py:810 +msgid "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." +msgstr "Bitte geben Sie die Zelle ein, in die Sie den Cursor bewegen möchten, z. B. \"A1\", \"A1\", \"A10\", \"A\", \"10\" usw." + +#: draw\view.py:939 +msgid "Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "Bitte geben Sie die Zellen mit den Namen der Hauptfarben ein. Eine Farbe für jede Zeile, getrennt von der Zelle durch ein \":\". Beispiel: `A1:rot\n" +"B7-C9:grün`." + diff --git a/draw/locales/el-GR.po b/draw/locales/el-GR.po new file mode 100644 index 0000000..f321489 --- /dev/null +++ b/draw/locales/el-GR.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:23\n" +"Last-Translator: \n" +"Language-Team: Greek\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: el\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/draw/locales/messages.pot\n" +"X-Crowdin-File-ID: 231\n" +"Language: el_GR\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "Ένα γρανάζι για να κάνετε pixel arts στο Discord!" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "Αυτό το μήνυμα δεν βρίσκεται στην προσωρινή μνήμη." + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." +msgstr "Δημιουργήστε ένα νέο πίνακα σχεδίασης με `height = {height}`, `width = {width}` και `background = {background}`." + +#: draw\view.py:332 +msgid "Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "Πληκτρολογήστε όλα τα χρώματα που θέλετε να προσθέσετε. Μπορούν να είναι ένα ή όλα τα εξής:\n" +"- Οι δεκαεξαδικοί κωδικοί (π.χ. \"ff64c4\" ή \"ff64c4ff\" για να συμπεριλάβετε το άλφα) **διαχωρισμένοι με διάστημα**,\n" +"- Οι τιμές RGB(A) χωρισμένες με διάστημα ή κόμμα ή και τα δύο (π.χ. `(255 100 196)` ή `(255, 100, 196, 125)`) του κάθε χρώματος **περιτριγυρισμένες από αγκύλες**\n" +"- Οποιοδήποτε emoji του οποίου το κύριο χρώμα θέλετε να εξαγάγετε (π.χ. 🐸 θα δώσει 77b255)\n" +"- Οποιοδήποτε αρχείο εικόνας (τα 5 πρώτα άφθονα χρώματα θα εξαχθούν)." + +#: draw\view.py:402 +msgid "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." +msgstr "Παρακαλούμε στείλτε ένα μήνυμα με τα emojis που θέλετε να προσθέσετε στην παλέτα σας. Π.χ. `😎 Μου αρέσουν οι χελώνες 🐢`." + +#: draw\view.py:810 +msgid "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." +msgstr "Πληκτρολογήστε το κελί στο οποίο θέλετε να μετακινήσετε τον κέρσορα. π.χ. `Α1`, `α1`, `Α10`, `Α`, `10`, κ.λπ." + +#: draw\view.py:939 +msgid "Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "Πληκτρολογήστε τα κελιά με τα ονόματα των κύριων χρωμάτων. Ένα χρώμα για κάθε γραμμή, χωρισμένο από το κελί με \":\". Παράδειγμα: `A1:κόκκινο\n" +"B7-C9:πράσινο`." + diff --git a/draw/locales/es-ES.po b/draw/locales/es-ES.po new file mode 100644 index 0000000..80a3e75 --- /dev/null +++ b/draw/locales/es-ES.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:23\n" +"Last-Translator: \n" +"Language-Team: Spanish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: es-ES\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/draw/locales/messages.pot\n" +"X-Crowdin-File-ID: 231\n" +"Language: es_ES\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "¡Un engranaje para hacer pixel arts en Discord!" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "Este mensaje no está en la caché." + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." +msgstr "Cree un nuevo Tablero de dibujo con `altura = {height}`, `anchura = {width}` y `fondo = {background}`." + +#: draw\view.py:332 +msgid "Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "Por favor, escriba todos los colores que desea añadir. Pueden ser uno o todos:\n" +"- Los códigos hexadecimales (p.ej. `ff64c4` o `ff64c4ff` para incluir alfa) **separados por espacio**,\n" +"- Los valores RGB(A) separados por espacio o coma o ambos (p.ej. `(255 100 196)` o `(255, 100, 196, 125)`) de cada color **entre paréntesis**\n" +"- Cualquier emoji cuyo color principal quieras extraer (por ejemplo, 🐸 dará 77b255)\n" +"- Cualquier archivo de imagen (se extraerán los 5 primeros colores abundantes)." + +#: draw\view.py:402 +msgid "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." +msgstr "Envía un mensaje con los emojis que quieres añadir a tu paleta. Por ejemplo: `😎 Me gustan las tortugas 🐢`." + +#: draw\view.py:810 +msgid "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." +msgstr "Escriba la celda a la que desea mover el cursor. Por ejemplo, `A1`, `a1`, `A10`, `A`, `10`, etc." + +#: draw\view.py:939 +msgid "Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "Escriba las celdas con los nombres de los colores principales. Un color para cada línea, separado de la celda por un \":\". Ejemplo: `A1:rojo\n" +"B7-C9:verde`." + diff --git a/draw/locales/fi-FI.po b/draw/locales/fi-FI.po new file mode 100644 index 0000000..cf3bc3d --- /dev/null +++ b/draw/locales/fi-FI.po @@ -0,0 +1,57 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:23\n" +"Last-Translator: \n" +"Language-Team: Finnish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fi\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/draw/locales/messages.pot\n" +"X-Crowdin-File-ID: 231\n" +"Language: fi_FI\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "Hammasratas pikselitaiteen tekemiseen Discordissa!" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "Tätä viestiä ei ole välimuistissa." + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." +msgstr "Luo uusi piirustustaulu, jonka `height = {height}`, `width = {width}` ja `background = {background}`." + +#: draw\view.py:332 +msgid "Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "Kirjoita kaikki värit, jotka haluat lisätä. Ne voivat olla jompikumpi tai kaikki seuraavista:\n\n" +"- RGB(A)-arvot erotettuna välilyönnillä tai pilkulla tai molemmilla (esim. `(255 100 196)` tai `(255, 100, 196, 125)`) kunkin värin **sulkeilla ympäröityinä**\n" +"- Mikä tahansa emoji, jonka päävärin haluat poimia (esim. 🐸 antaa 77b255)\n" +"- Mikä tahansa kuvatiedosto (5 ensimmäistä runsasta väriä poimitaan)." + +#: draw\view.py:402 +msgid "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." +msgstr "Lähetä viesti, joka sisältää emojit, jotka haluat lisätä palettiin. Esim. `😎 Pidän kilpikonnista 🐢`." + +#: draw\view.py:810 +msgid "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." +msgstr "Kirjoita solu, johon haluat siirtää kursorin. esim. `A1`, `a1`, `A10`, `A`, `10` jne." + +#: draw\view.py:939 +msgid "Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "Kirjoita solut, joissa on päävärien nimet. Yksi väri jokaiselle riville, joka erotetaan solusta \":\" -merkillä. Esimerkki: `A1:punainen\n" +"B7-C9:vihreä`." + diff --git a/draw/locales/fr-FR.po b/draw/locales/fr-FR.po new file mode 100644 index 0000000..f31fbb4 --- /dev/null +++ b/draw/locales/fr-FR.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:23\n" +"Last-Translator: \n" +"Language-Team: French\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/draw/locales/messages.pot\n" +"X-Crowdin-File-ID: 231\n" +"Language: fr_FR\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "Une roue dentée pour faire du pixel art sur Discord !" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "Ce message n'est pas dans le cache." + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." +msgstr "Créez un nouveau tableau avec `hauteur = {height}`, `largeur = {width}` et `arrière-plan = {background}`." + +#: draw\view.py:332 +msgid "Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "Veuillez saisir toutes les couleurs que vous souhaitez ajouter. Il peut s'agir de l'une ou l'autre ou de toutes les couleurs :\n" +"- Les codes hexagonaux (par exemple `ff64c4` ou `ff64c4ff` pour inclure l'alpha) **séparés par un espace**,\n" +"- Les valeurs RVB(A) séparées par un espace ou une virgule ou les deux (par exemple `(255 100 196)` ou `(255, 100, 196, 125)`) de chaque couleur **entourées de crochets**\n" +"- Tout emoji dont vous voulez extraire la couleur principale (par exemple 🐸 donnera 77b255)\n" +"- Tout fichier image (les 5 premières couleurs abondantes seront extraites)." + +#: draw\view.py:402 +msgid "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." +msgstr "Veuillez envoyer un message contenant les emojis que vous souhaitez ajouter à votre palette. Par exemple : `😎 J'aime les tortues 🐢`." + +#: draw\view.py:810 +msgid "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." +msgstr "Veuillez saisir la cellule dans laquelle vous souhaitez déplacer le curseur, par exemple `A1`, `a1`, `A10`, `A`, `10`, etc." + +#: draw\view.py:939 +msgid "Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "Veuillez taper les cellules avec les noms des couleurs principales. Une couleur par ligne, séparée de la cellule par un \" :\". Exemple : `A1:rouge\n" +"B7-C9:vert`." + diff --git a/draw/locales/it-IT.po b/draw/locales/it-IT.po new file mode 100644 index 0000000..4a3c095 --- /dev/null +++ b/draw/locales/it-IT.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:23\n" +"Last-Translator: \n" +"Language-Team: Italian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: it\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/draw/locales/messages.pot\n" +"X-Crowdin-File-ID: 231\n" +"Language: it_IT\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "Un ingranaggio per fare pixel art su Discord!" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "Questo messaggio non è presente nella cache." + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." +msgstr "Creare un nuovo Draw Board con `altezza = {height}`, `larghezza = {width}` e `sfondo = {background}`." + +#: draw\view.py:332 +msgid "Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "Digitare tutti i colori che si desidera aggiungere. Possono essere uno o tutti i seguenti:\n" +"- I codici esadecimali (ad esempio `ff64c4` o `ff64c4ff` per includere l'alfa) **separati da spazio**,\n" +"- I valori RGB(A) separati da spazio o virgola o da entrambi (ad esempio `(255 100 196)` o `(255, 100, 196, 125)`) di ciascun colore **circondato da parentesi**\n" +"- Qualsiasi emoji di cui si desidera estrarre il colore principale (ad esempio 🐸 darà 77b255)\n" +"- Qualsiasi file immagine (i primi 5 colori abbondanti saranno estratti)." + +#: draw\view.py:402 +msgid "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." +msgstr "Inviate un messaggio contenente le emoji che desiderate aggiungere alla vostra tavolozza. Ad esempio, `😎 Mi piacciono le tartarughe 🐢`." + +#: draw\view.py:810 +msgid "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." +msgstr "Digitare la cella in cui si desidera spostare il cursore, ad esempio `A1`, `a1`, `A10`, `A`, `10`, ecc." + +#: draw\view.py:939 +msgid "Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "Digitare le celle con i nomi dei colori principali. Un colore per ogni riga, separato dalla cella da un \":\". Esempio: `A1:rosso\n" +"B7-C9:verde`." + diff --git a/draw/locales/ja-JP.po b/draw/locales/ja-JP.po new file mode 100644 index 0000000..8bcece5 --- /dev/null +++ b/draw/locales/ja-JP.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:23\n" +"Last-Translator: \n" +"Language-Team: Japanese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ja\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/draw/locales/messages.pot\n" +"X-Crowdin-File-ID: 231\n" +"Language: ja_JP\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "Discordでピクセルアートを作るための歯車です!" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "このメッセージはキャッシュにない。" + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." +msgstr "高さ = {height}`、幅 = {width}`、背景 = {background}`の新しいドローボードを作成します。" + +#: draw\view.py:332 +msgid "Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "追加したい色をすべて入力してください。以下のいずれか、またはすべてとなります:\n" +"- 16進コード(例:アルファを含む場合は `ff64c4` または `ff64c4ff` ) **スペースで区切られたもの**,\n" +"- RGB(A)値をスペースまたはコンマ、またはその両方で区切ったもの(例:`(255 100 196)` または `(255, 100, 196, 125)` )。各色の `(255 100 196)` または `(255, 100, 196, 125)`) **括弧で囲む**\n" +"- メインカラーを抽出したい絵文字(例:🐸は77b255)\n" +"- 任意の画像ファイル(最初の5色が抽出される)." + +#: draw\view.py:402 +msgid "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." +msgstr "パレットに追加したい絵文字を含むメッセージを送信してください。例:`👠亀が好きです🐢」。" + +#: draw\view.py:810 +msgid "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." +msgstr "カーソルを移動させたいセルを入力してください。例:`A1`, `a1`, `A10`, `A`, `10`, etc." + +#: draw\view.py:939 +msgid "Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "主要な色の名前を持つセルを入力してください。1行に1色、セルとは「:」で区切ってください。例:`A1:red\n" +"B7-C9:green`." + diff --git a/draw/locales/messages.pot b/draw/locales/messages.pot new file mode 100644 index 0000000..a50cbd3 --- /dev/null +++ b/draw/locales/messages.pot @@ -0,0 +1,58 @@ +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2024-12-29 10:43+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "" + +#: draw\start_view.py:60 draw\view.py:523 +msgid "You are not allowed to use this interaction." +msgstr "" + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "" +"Create a new Draw Board with `height = {height}`, `width = {width}` and " +"`background = {background}`." +msgstr "" + +#: draw\view.py:332 +msgid "" +"Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "" + +#: draw\view.py:402 +msgid "" +"Please send a message containing the emojis you want to add to your palette." +" E.g. `😎 I like turtles 🐢`." +msgstr "" + +#: draw\view.py:812 +msgid "" +"Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`," +" `A`, `10`, etc." +msgstr "" + +#: draw\view.py:941 +msgid "" +"Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "" diff --git a/draw/locales/nl-NL.po b/draw/locales/nl-NL.po new file mode 100644 index 0000000..f433500 --- /dev/null +++ b/draw/locales/nl-NL.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:23\n" +"Last-Translator: \n" +"Language-Team: Dutch\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: nl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/draw/locales/messages.pot\n" +"X-Crowdin-File-ID: 231\n" +"Language: nl_NL\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "Een radertje om pixelkunst te maken op Discord!" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "Dit bericht staat niet in de cache." + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." +msgstr "Maak een nieuw Tekenbord met `hoogte = {height}`, `breedte = {width}` en `achtergrond = {background}`." + +#: draw\view.py:332 +msgid "Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "Voer alle kleuren in die je wilt toevoegen. Dit kunnen alle of een van de volgende kleuren zijn:\n" +"- De hexcodes (bijv. `ff64c4` of `ff64c4ff` om alfa in te sluiten) **gescheiden door spatie**,\n" +"- De RGB(A)-waarden gescheiden door spatie of komma of beide (bijv. `(255 100 196)` of `(255, 100, 196, 125)`) van elke kleur **omgeven door haakjes**\n" +"- Elke emoji waarvan je de hoofdkleur wilt krijgen (bijv. 🐸 geeft 77b255)\n" +"- Elk willekeurig afbeeldingsbestand (de eerste 5 overvloedige kleuren worden gebruikt)." + +#: draw\view.py:402 +msgid "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." +msgstr "Stuur een bericht met de emoji's die je aan je palet wilt toevoegen. Bijvoorbeeld `😎 Ik hou van schildpadden 🐢`." + +#: draw\view.py:810 +msgid "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." +msgstr "Typ de cel waar je de cursor naartoe wilt verplaatsen. Bijvoorbeeld `A1`, `a1`, `A10`, `A`, `10`, etc." + +#: draw\view.py:939 +msgid "Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "Typ de cellen met de namen van de hoofdkleuren. Eén kleur voor elke regel, gescheiden van de cel door een \":\". Voorbeeld: `A1:rood\n" +"B7-C9:groen`." + diff --git a/draw/locales/pl-PL.po b/draw/locales/pl-PL.po new file mode 100644 index 0000000..724c49b --- /dev/null +++ b/draw/locales/pl-PL.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:23\n" +"Last-Translator: \n" +"Language-Team: Polish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/draw/locales/messages.pot\n" +"X-Crowdin-File-ID: 231\n" +"Language: pl_PL\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "Trybik do tworzenia pixel artów na Discordzie!" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "Tej wiadomości nie ma w pamięci podręcznej." + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." +msgstr "Utwórz nową tablicę Draw Board z `height = {height}`, `width = {width}` i `background = {background}`." + +#: draw\view.py:332 +msgid "Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "Wpisz wszystkie kolory, które chcesz dodać. Mogą to być kody heksadecymalne (np:\n" +"- Kody heksadecymalne (np. `ff64c4` lub `ff64c4ff`, aby uwzględnić alfa) **rozdzielone spacją**,\n" +"- Wartości RGB(A) rozdzielone spacją lub przecinkiem lub obydwoma (np. `(255 100 196)` lub `(255, 100, 196, 125)`) każdego koloru **otoczone nawiasami**\n" +"- Dowolne emoji, którego główny kolor chcesz wyodrębnić (np. 🐸 da 77b255)\n" +"- Dowolny plik obrazu (zostanie wyodrębnionych pierwszych 5 kolorów)." + +#: draw\view.py:402 +msgid "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." +msgstr "Wyślij wiadomość zawierającą emotikony, które chcesz dodać do swojej palety. Na przykład `😎 Lubię żółwie 🐢`." + +#: draw\view.py:810 +msgid "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." +msgstr "Wpisz komórkę, do której chcesz przenieść kursor, np. `A1`, `a1`, `A10`, `A`, `10` itd." + +#: draw\view.py:939 +msgid "Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "Wpisz komórki z nazwami głównych kolorów. Jeden kolor dla każdej linii, oddzielony od komórki znakiem \":\". Przykład: `A1:czerwony\n" +"B7-C9:zielony`." + diff --git a/draw/locales/pt-BR.po b/draw/locales/pt-BR.po new file mode 100644 index 0000000..95a375f --- /dev/null +++ b/draw/locales/pt-BR.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:23\n" +"Last-Translator: \n" +"Language-Team: Portuguese, Brazilian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-BR\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/draw/locales/messages.pot\n" +"X-Crowdin-File-ID: 231\n" +"Language: pt_BR\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "Uma engrenagem para fazer pixel arts no Discord!" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "Esta mensagem não está na cache." + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." +msgstr "Criar um novo Draw Board com `altura = {height}`, `largura = {width}` e `fundo = {background}`." + +#: draw\view.py:332 +msgid "Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "Introduza todas as cores que pretende adicionar. Elas podem ser uma ou todas:\n" +"- Os códigos hexadecimais (por exemplo, `ff64c4` ou `ff64c4ff` para incluir alfa) **separados por espaço**,\n" +"- Os valores RGB(A) separados por espaço ou vírgula ou ambos (por exemplo `(255 100 196)` ou `(255, 100, 196, 125)`) de cada cor **surrounded by brackets**\n" +"- Qualquer emoji cuja cor principal queira extrair (e.g. 🐸 dará 77b255)\n" +"- Qualquer ficheiro de imagem (as primeiras 5 cores abundantes serão extraídas)." + +#: draw\view.py:402 +msgid "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." +msgstr "Por favor, envie uma mensagem contendo os emojis que deseja adicionar à sua paleta. Por exemplo, `😎 Gosto de tartarugas 🐢`." + +#: draw\view.py:810 +msgid "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." +msgstr "Por favor, digite a célula para a qual pretende mover o cursor. Por exemplo, `A1`, `a1`, `A10`, `A`, `10`, etc." + +#: draw\view.py:939 +msgid "Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "Escreva as células com os nomes das cores principais. Uma cor para cada linha, separada da célula por um \":\". Exemplo: `A1:vermelho\n" +"B7-C9:verde`." + diff --git a/draw/locales/pt-PT.po b/draw/locales/pt-PT.po new file mode 100644 index 0000000..343dc84 --- /dev/null +++ b/draw/locales/pt-PT.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:23\n" +"Last-Translator: \n" +"Language-Team: Portuguese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-PT\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/draw/locales/messages.pot\n" +"X-Crowdin-File-ID: 231\n" +"Language: pt_PT\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "Uma engrenagem para fazer pixel arts no Discord!" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "Esta mensagem não está na cache." + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." +msgstr "Criar um novo Draw Board com `altura = {height}`, `largura = {width}` e `fundo = {background}`." + +#: draw\view.py:332 +msgid "Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "Introduza todas as cores que pretende adicionar. Elas podem ser uma ou todas:\n" +"- Os códigos hexadecimais (por exemplo, `ff64c4` ou `ff64c4ff` para incluir alfa) **separados por espaço**,\n" +"- Os valores RGB(A) separados por espaço ou vírgula ou ambos (por exemplo `(255 100 196)` ou `(255, 100, 196, 125)`) de cada cor **surrounded by brackets**\n" +"- Qualquer emoji cuja cor principal queira extrair (e.g. 🐸 dará 77b255)\n" +"- Qualquer ficheiro de imagem (as primeiras 5 cores abundantes serão extraídas)." + +#: draw\view.py:402 +msgid "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." +msgstr "Por favor, envie uma mensagem contendo os emojis que deseja adicionar à sua paleta. Por exemplo, `😎 Gosto de tartarugas 🐢`." + +#: draw\view.py:810 +msgid "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." +msgstr "Por favor, digite a célula para a qual pretende mover o cursor. Por exemplo, `A1`, `a1`, `A10`, `A`, `10`, etc." + +#: draw\view.py:939 +msgid "Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "Escreva as células com os nomes das cores principais. Uma cor para cada linha, separada da célula por um \":\". Exemplo: `A1:vermelho\n" +"B7-C9:verde`." + diff --git a/draw/locales/ro-RO.po b/draw/locales/ro-RO.po new file mode 100644 index 0000000..90a4d38 --- /dev/null +++ b/draw/locales/ro-RO.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:23\n" +"Last-Translator: \n" +"Language-Team: Romanian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ro\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/draw/locales/messages.pot\n" +"X-Crowdin-File-ID: 231\n" +"Language: ro_RO\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "O rotiță pentru a face artă pixel pe Discord!" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "Acest mesaj nu se află în memoria cache." + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." +msgstr "Creați o nouă tablă de desen cu `height = {height}`, `width = {width}` și `background = {background}`." + +#: draw\view.py:332 +msgid "Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "Vă rugăm să introduceți toate culorile pe care doriți să le adăugați. Acestea pot fi oricare sau toate:\n" +"- Codurile hexazecimal (de exemplu `ff64c4` sau `ff64c4ff` pentru a include alfa) **separate prin spațiu**,\n" +"- Valorile RGB(A) separate prin spațiu sau virgulă sau ambele (de ex. `(255 100 196)` sau `(255, 100, 196, 196, 125)`) ale fiecărei culori **înconjurate de paranteze**\n" +"- Orice emoji a cărui culoare principală doriți să o extrageți (de exemplu, 🐸 va da 77b255)\n" +"- Orice fișier de imagine (primele 5 culori abundente vor fi extrase)." + +#: draw\view.py:402 +msgid "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." +msgstr "Vă rugăm să trimiteți un mesaj care să conțină emoticoanele pe care doriți să le adăugați la paleta dumneavoastră. De exemplu, `😎 Îmi plac broaștele țestoase 🐢`." + +#: draw\view.py:810 +msgid "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." +msgstr "Vă rugăm să introduceți celula în care doriți să mutați cursorul. de exemplu, `A1`, `a1`, `A10`, `A`, `10`, etc." + +#: draw\view.py:939 +msgid "Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "Vă rugăm să introduceți celulele cu numele culorilor principale. O culoare pentru fiecare linie, separată de celulă prin \":\". Exemplu: `A1:roșu\n" +"B7-C9:verde`." + diff --git a/draw/locales/ru-RU.po b/draw/locales/ru-RU.po new file mode 100644 index 0000000..8927421 --- /dev/null +++ b/draw/locales/ru-RU.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:23\n" +"Last-Translator: \n" +"Language-Team: Russian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ru\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/draw/locales/messages.pot\n" +"X-Crowdin-File-ID: 231\n" +"Language: ru_RU\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "Ког для создания пиксель-артов на Discord!" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "Этого сообщения нет в кэше." + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." +msgstr "Создайте новую Доску рисования с `высотой = {height}`, `шириной = {width}` и `фоном = {background}`." + +#: draw\view.py:332 +msgid "Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "Пожалуйста, введите все цвета, которые вы хотите добавить. Они могут быть любыми или всеми:\n" +"- шестнадцатеричные коды (например, `ff64c4` или `ff64c4ff` для включения альфы) **разделенные пробелом**,\n" +"- значения RGB(A), разделенные пробелом или запятой, или оба (например. `(255 100 196)` или `(255, 100, 196, 125)`) каждого цвета **окруженные скобками**\n" +"- Любой эмодзи, чей основной цвет вы хотите извлечь (например, 🐸 даст 77b255)\n" +"- Любой файл изображения (первые 5 обильных цветов будут извлечены)." + +#: draw\view.py:402 +msgid "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." +msgstr "Пожалуйста, отправьте сообщение с эмодзи, которые вы хотите добавить в свою палитру. Например, `😎 Я люблю черепах 🐢`." + +#: draw\view.py:810 +msgid "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." +msgstr "Введите ячейку, в которую вы хотите переместить курсор. Например, `A1`, `a1`, `A10`, `A`, `10` и т.д." + +#: draw\view.py:939 +msgid "Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "Пожалуйста, введите ячейки с названиями основных цветов. Один цвет для каждой строки, отделенный от ячейки знаком \":\". Пример: `A1:красный\n" +"B7-C9:зеленый`." + diff --git a/draw/locales/tr-TR.po b/draw/locales/tr-TR.po new file mode 100644 index 0000000..e02982b --- /dev/null +++ b/draw/locales/tr-TR.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 13:27\n" +"Last-Translator: \n" +"Language-Team: Turkish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: tr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/draw/locales/messages.pot\n" +"X-Crowdin-File-ID: 231\n" +"Language: tr_TR\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "Discord'da piksel sanatı yapmak için bir cog!" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "Bu mesaj önbellekte değil." + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." +msgstr "`yükseklik = {height}`, `genişlik = {width}` ve `arka plan = {background}` ile yeni bir Çizim Tahtası oluşturun." + +#: draw\view.py:332 +msgid "Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "Eklemek istediğiniz tüm renkleri yazın. Bunlar şu şekilde olabilir:\n" +"• Hex kodları (örneğin, `ff64c4` veya alfa dahil `ff64c4ff`) **boşlukla ayrılmış**,\n" +"• Her rengin parantez içine alınmış, boşluk veya virgül veya her ikisiyle ayrılmış RGB(A) değerleri (örneğin `(255 100 196)` veya `(255, 100, 196, 125)`)\n" +"• Ana rengini çıkarmak istediğiniz herhangi bir emoji (örneğin 🐸 size 77b255 verir)\n" +"• Herhangi bir resim dosyası (ilk 5 bol renk çıkarılacaktır)." + +#: draw\view.py:402 +msgid "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." +msgstr "Paletinize eklemek istediğiniz emojileri içeren bir mesaj gönderin. Örneğin `😎 Kaplumbağaları seviyorum 🐢`." + +#: draw\view.py:810 +msgid "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." +msgstr "İmleci taşımak istediğiniz hücreyi yazın. Örneğin `A1`, `a1`, `A10`, `A`, `10`, vb." + +#: draw\view.py:939 +msgid "Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "Ana renklerin isimleriyle hücreleri yazın. Her satır için bir renk, hücreden \":\" ile ayrılmış. Örnek: `A1:red\n" +"B7-C9:green`." + diff --git a/draw/locales/uk-UA.po b/draw/locales/uk-UA.po new file mode 100644 index 0000000..3c2917a --- /dev/null +++ b/draw/locales/uk-UA.po @@ -0,0 +1,58 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-20 20:23\n" +"Last-Translator: \n" +"Language-Team: Ukrainian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: uk\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/draw/locales/messages.pot\n" +"X-Crowdin-File-ID: 231\n" +"Language: uk_UA\n" + +#: draw\draw.py:40 +#, docstring +msgid "A cog to make pixel arts on Discord!" +msgstr "Гвинтик для створення піксельних артів на Discord!" + +#: draw\draw.py:165 +msgid "This message isn't in the cache." +msgstr "Цього повідомлення немає в кеші." + +#: draw\start_view.py:94 draw\start_view.py:103 +msgid "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." +msgstr "Створіть нову дошку для малювання з параметрами `height = {height}`, `width = {width}` та `background = {background}`." + +#: draw\view.py:332 +msgid "Please type all the colors you want to add. They can be either or all of:\n" +"• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**,\n" +"• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**\n" +"• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)\n" +"• Any image file (first 5 abundant colors will be extracted)." +msgstr "Будь ласка, введіть всі кольори, які ви хочете додати. Вони можуть бути будь-якими або всіма:\n" +"- Шістнадцяткові коди (наприклад, `ff64c4` або `ff64c4ff`, щоб включити альфа) **відокремлені пробілом**,\n" +"- Значення RGB(A), відокремлені пробілом або комою, або і тим, і іншим (наприклад, `(255 100 100)` або `(255 100 100)`) кожного кольору **в дужках `(255 100 196)` або `(255, 100, 196, 125)`) кожного кольору **в дужках**\n" +"- Будь-який емодзі, основний колір якого ви хочете витягти (наприклад, 🐸 дасть 77b255)\n" +"- Будь-який файл зображення (будуть витягнуті перші 5 насичених кольорів)." + +#: draw\view.py:402 +msgid "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." +msgstr "Будь ласка, надішліть повідомлення, що містить емодзі, які ви хочете додати до своєї палітри. Наприклад, `😎 Я люблю черепах 🐢`." + +#: draw\view.py:810 +msgid "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." +msgstr "Будь ласка, введіть комірку, до якої ви хочете перемістити курсор, наприклад, `A1`, `a1`, `A10`, `A`, `10` і т.д." + +#: draw\view.py:939 +msgid "Please type the cells with the main colors names. One color for each line, separated from the cell by a \":\". Example: `A1:red\n" +"B7-C9:green`." +msgstr "Будь ласка, введіть клітинки з назвами основних кольорів. Один колір для кожного рядка, відокремлений від комірки символом \":\". Приклад: `A1:червоний\n" +"B7-C9:зелений`." + diff --git a/draw/start_view.py b/draw/start_view.py new file mode 100644 index 0000000..41877f6 --- /dev/null +++ b/draw/start_view.py @@ -0,0 +1,174 @@ +from AAA3A_utils import CogsUtils # isort:skip +from redbot.core import commands # isort:skip +from redbot.core.i18n import Translator # isort:skip +import discord # isort:skip +import typing # isort:skip + +import asyncio + +from .board import Board +from .constants import ( + IMAGE_EXTENSION, + MAIN_COLORS, + base_colors_options, + base_height_or_width_select_options, +) # NOQA +from .view import DrawView + +_: Translator = Translator("Draw", __file__) + + +class StartDrawView(discord.ui.View): + def __init__( + self, + cog: commands.Cog, + board: typing.Union[typing.Tuple[int, int, str], Board] = (0, 0, MAIN_COLORS[-1]), + tool_options: typing.Optional[typing.List[discord.SelectOption]] = None, + color_options: typing.Optional[typing.List[discord.SelectOption]] = None, + ) -> None: + super().__init__(timeout=60) + self.cog: commands.Cog = cog + self.ctx: commands.Context = None + + if isinstance(board, typing.Tuple): + board = Board(cog=self.cog, height=board[0], width=board[1], background=board[2]) + self._board: Board = board + self.height: int = self._board.height + self.width: int = self._board.width + self.background: str = self._board.background + self.draw_view: typing.Optional[DrawView] = None + + self.tool_options: typing.Optional[typing.List[discord.SelectOption]] = tool_options + self.color_options: typing.Optional[typing.List[discord.SelectOption]] = color_options + + self._message: discord.Message = None + self._embed: discord.Embed = None + + self._ready: asyncio.Event = asyncio.Event() + + async def start(self, ctx: commands.Context) -> discord.Message: + self.ctx: commands.Context = ctx + await self._update() + await self._ready.wait() + if self.draw_view is not None: + await self.draw_view._ready.wait() + return self._message + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user.id not in [self.ctx.author.id] + list(self.ctx.bot.owner_ids): + await interaction.response.send_message( + _("You are not allowed to use this interaction."), ephemeral=True + ) + return False + return True + + async def on_timeout(self) -> None: + for child in self.children: + child: discord.ui.Item + if hasattr(child, "disabled") and not ( + isinstance(child, discord.ui.Button) and child.style == discord.ButtonStyle.url + ): + child.disabled = True + try: + await self._message.edit(view=self) + except discord.HTTPException: + pass + self._ready.set() + + @property + def board(self) -> Board: + self._board.modify(height=self.height, width=self.width, background=self.background) + return self._board + + async def _update(self) -> None: + self._embed: discord.Embed = await self.get_embed(self.ctx) + file = await self.board.to_file() + self.select_background.options = base_colors_options() + discord.utils.get(self.select_background.options, value=self.background).default = True + self.select_height.options = base_height_or_width_select_options("height") + discord.utils.get(self.select_height.options, value=int(self.height)).default = True + self.select_width.options = base_height_or_width_select_options("width") + discord.utils.get(self.select_width.options, value=int(self.width)).default = True + if self._message is None: + self._message: discord.Message = await self.ctx.send( + _( + "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." + ).format(height=self.height, width=self.width, background=self.background), + embed=self._embed, + file=file, + view=self, + ) + else: + self._message: discord.Message = await self._message.edit( + content=_( + "Create a new Draw Board with `height = {height}`, `width = {width}` and `background = {background}`." + ).format(height=self.height, width=self.width, background=self.background), + embed=self._embed, + attachments=[file], + view=self, + ) + + async def get_embed(self, ctx: commands.Context) -> discord.Embed: + embed: discord.Embed = discord.Embed(title="Draw Board", color=await ctx.embed_color()) + # embed.description = str(self.board) + embed.set_image(url=f"attachment://image.{IMAGE_EXTENSION.lower()}") + return embed + + @discord.ui.button(style=discord.ButtonStyle.danger, emoji="✖️", custom_id="close_page") + async def close_page( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + try: + await interaction.response.defer() + except discord.errors.NotFound: + pass + self.stop() + await CogsUtils.delete_message(self._message) + self._ready.set() + + @discord.ui.button(label="Create Draw", style=discord.ButtonStyle.success) + async def create_draw(self, interaction: discord.Interaction, button: discord.Button) -> None: + await interaction.response.defer() + self.stop() + self.draw_view: DrawView = DrawView( + cog=self.cog, + board=self.board, + tool_options=self.tool_options, + color_options=self.color_options, + ) + await self.draw_view.start(self.ctx, message=self._message) + self._ready.set() + + @discord.ui.select(options=base_colors_options(), placeholder="Select Board Background.") + async def select_background( + self, interaction: discord.Interaction, select: discord.ui.Select + ) -> None: + await interaction.response.defer() + if self.background == select.values[0]: + return + self.background: str = select.values[0] + await self._update() + + @discord.ui.select( + options=base_height_or_width_select_options("height"), placeholder="Select Board Height." + ) + async def select_height( + self, interaction: discord.Interaction, select: discord.ui.Select + ) -> None: + await interaction.response.defer() + if self.height == int(select.values[0]): + return + self.height: str = int(select.values[0]) + await self._update() + + @discord.ui.select( + options=base_height_or_width_select_options("width"), placeholder="Select Board Width." + ) + async def select_width( + self, interaction: discord.Interaction, select: discord.ui.Select + ) -> None: + await interaction.response.defer() + if self.width == int(select.values[0]): + return + self.width: str = int(select.values[0]) + await self._update() diff --git a/draw/tools.py b/draw/tools.py new file mode 100644 index 0000000..0f3d798 --- /dev/null +++ b/draw/tools.py @@ -0,0 +1,303 @@ +from redbot.core.bot import Red # isort:skip +import discord # isort:skip +import typing # isort:skip + +import numpy as np + +from .board import Board +from .color import Color +from .constants import MAIN_COLORS_DICT + + +class Tool(discord.ui.Button): + """A template class for each of the tools.""" + + def __init__(self, view: discord.ui.View, *, primary: typing.Optional[bool] = True) -> None: + super().__init__( + emoji=self.emoji, + style=( + discord.ButtonStyle.success if primary is True else discord.ButtonStyle.secondary + ), + ) + self._view: discord.ui.View = view + self.board: Board = self._view.board + self.bot: Red = self._view.cog.bot + + @property + def name(self) -> str: + return None + + @property + def emoji(self) -> str: + return None + + @property + def description(self) -> str: + return None + + @property + def auto_use(self) -> bool: + return False + + async def use(self, *, interaction: discord.Interaction) -> bool: + """The method that is called when the tool is used.""" + pass + + async def callback(self, interaction: discord.Interaction) -> None: + await interaction.response.defer() + if await self.use(interaction=interaction): + await self.view._update() + + +class BrushTool(Tool): + @property + def name(self) -> str: + return "Brush" + + @property + def emoji(self) -> str: + return "<:brush:1056853866563506176>" # "🖌️" + + @property + def description(self) -> str: + return "Draw where the cursor is." + + async def use(self, *, interaction: discord.Interaction) -> bool: + """The method that is called when the tool is used.""" + return self.board.draw(self.board.cursor) + + +class EraseTool(Tool): + @property + def name(self) -> str: + return "Eraser" + + @property + def emoji(self) -> str: + return "<:eraser:1056853917973094420>" # "✏️" + + @property + def description(self) -> str: + return "Erase where the cursor is." + + async def use(self, *, interaction: discord.Interaction) -> bool: + """The method that is called when the tool is used.""" + return self.board.draw(self.board.background) + + +class EyedropperTool(Tool): + @property + def name(self) -> str: + return "Eyedropper" + + @property + def emoji(self) -> str: + return "<:eyedropper:1056854084004630568>" # "💉" + + @property + def description(self) -> str: + return "Pick and add color to Palette." + + @property + def auto_use(self) -> bool: + return True + + async def use(self, *, interaction: discord.Interaction) -> bool: + """The method that is called when the tool is used.""" + cursor_pixel = self.board.cursor_pixel + pixel = cursor_pixel + + # Check if the option already exists. + option = self.view.color_menu.value_to_option(pixel) + if option is None: + # Try to find the emoji so that we can use its real name as label. + try: + pixel = int(pixel) + except (ValueError, TypeError): + pass + if isinstance(pixel, int) and (fetched_emoji := self.bot.get_emoji(pixel)) is not None: + label = fetched_emoji.name + emoji = fetched_emoji + value = str(fetched_emoji.id) + elif isinstance(pixel, Color): + label = (await pixel.get_name()) + f" ({pixel.hex})" + emoji = None + value = f"#{pixel.hex}" + else: + label = pixel + emoji = discord.PartialEmoji.from_str(pixel) + value = pixel + option = discord.SelectOption( + label=f"Eyedropped option: {label}", + emoji=emoji, + value=value, + ) + self.view.color_menu.append_option(option) + + if self.board.cursor == option.value: + return False + self.board.cursor = option.value + self.view.color_menu.set_default(option) + return True + + +class FillTool(Tool): + @property + def name(self) -> str: + return "Fill" + + @property + def emoji(self) -> str: + return "<:fill:1056853974394867792>" # "🎨" + + @property + def description(self) -> str: + return "Fill closed area." + + @property + def auto_use(self) -> bool: + return True + + async def use( + self, + *, + interaction: discord.Interaction, + initial_coords: typing.Optional[typing.Tuple[int, int]] = None, + ) -> bool: + """The method that is called when the tool is used.""" + color = self.board.cursor + if self.board.cursor_pixel == color: + return + + # Use Breadth-First Search algorithm to fill an area. + initial_coords = initial_coords or ( + self.board.cursor_row, + self.board.cursor_col, + ) + initial_pixel = self.board.get_pixel(*initial_coords) + + coords = [] + queue = [initial_coords] + i = 0 + + while i < len(queue): + row, col = queue[i] + i += 1 + # Skip to next cell in the queue if + # the row is less than 0 or greater than the max row possible, + # the col is less than 0 or greater than the max col possible or + # the current pixel (or its cursor version) is not the same as the pixel to replace (or its cursor version) + if ( + any((row < 0, row > self.board.cursor_row_max)) + or any((col < 0, col > self.board.cursor_col_max)) + or self.board.get_pixel(row, col) != initial_pixel + or (row, col) in coords + ): + continue + + coords.append((row, col)) + + queue.extend(((row + 1, col), (row - 1, col), (row, col + 1), (row, col - 1))) + return self.board.draw(coords=coords) # Draw all the cells. + + +class ReplaceTool(Tool): + @property + def name(self) -> str: + return "Replace" + + @property + def emoji(self) -> str: + return "<:replace:1056854037066154034>" # "🎨" + + @property + def description(self) -> str: + return "Replace all pixels." + + @property + def auto_use(self) -> bool: + return True + + async def use(self, *, interaction: discord.Interaction) -> bool: + """The method that is called when the tool is used.""" + color = self.board.cursor + to_replace = self.board.cursor_pixel + return self.board.draw(color, coords=np.array(np.where(self.board.board == to_replace)).T) + + +CHANGE_AMOUNT = 17 # Change amount for Lighten & Darken tools to allow exactly 15 changes from 0 or 255, respectively. + + +class DarkenTool(Tool): + @property + def name(self) -> str: + return "Darken" + + @property + def emoji(self) -> str: + return "🔅" + + @property + def description(self) -> str: + return "Darken pixel(s) by 17 RGB values." + + @staticmethod + def edit(value: int) -> int: + return max( + value - CHANGE_AMOUNT, 0 + ) # The max func makes sure it doesn't go below 0 when decreasing, for example, black. + + async def use(self, *, interaction: discord.Interaction) -> bool: + """The method that is called when the tool is used.""" + coords = self.board.cursor_coords + for coord in coords: + pixel = self.board.board[coord] + color = MAIN_COLORS_DICT.get(pixel, pixel) + if isinstance(color, Color): + RGB_A = ( + self.edit(color.R), + self.edit(color.G), + self.edit(color.B), + color.A, + ) + modified_color = Color(RGB_A) + self.board.draw(modified_color, coords=[coord]) + return True + + +class LightenTool(DarkenTool): + @property + def name(self) -> str: + return "Lighten" + + @property + def emoji(self) -> str: + return "🔆" + + @property + def description(self) -> str: + return "Lighten pixel(s) by 17 RGB values." + + @staticmethod + def edit(value: int) -> int: + return min( + value + CHANGE_AMOUNT, 255 + ) # The min func makes sure it doesn't go above 255 when increasing, for example, white. + + +class InverseTool(DarkenTool): + @property + def name(self) -> str: + return "Invert Colors" + + @property + def emoji(self) -> str: + return "🔦" + + @property + def description(self) -> str: + return "Invert colors in pixel(s)." + + @staticmethod + def edit(value: int) -> int: + return 255 - value diff --git a/draw/utils_version.json b/draw/utils_version.json new file mode 100644 index 0000000..bfab002 --- /dev/null +++ b/draw/utils_version.json @@ -0,0 +1 @@ +{"needed_utils_version": 7.0} \ No newline at end of file diff --git a/draw/view.py b/draw/view.py new file mode 100644 index 0000000..ad52332 --- /dev/null +++ b/draw/view.py @@ -0,0 +1,1044 @@ +from AAA3A_utils import CogsUtils # isort:skip +from redbot.core import commands # isort:skip +from redbot.core.bot import Red # isort:skip +from redbot.core.i18n import Translator # isort:skip +import discord # isort:skip +import typing # isort:skip + +import asyncio +import re + +import emoji as _emoji +from redbot.core.utils.chat_formatting import humanize_list + +from .board import Board +from .color import Color +from .constants import ( + ALPHABETS, + IMAGE_EXTENSION, + LETTER_TO_NUMBER, + MAIN_COLORS_DICT, + NUMBERS, + base_colors_options, +) # NOQA +from .tools import ( + BrushTool, + DarkenTool, + EraseTool, + EyedropperTool, + FillTool, + InverseTool, + LightenTool, + ReplaceTool, + Tool, +) # NOQA + +_: Translator = Translator("Draw", __file__) + +ADD_COLORS_EMOJI = "🏳️‍🌈" +ADD_EMOJIS_EMOJI = discord.PartialEmoji(name="emojismiley", id=1056857231125123152) # "😃" +MIX_COLORS_EMOJI = "🔀" +SET_CURSOR_EMOJI = discord.PartialEmoji(name="ABCD", id=1032565203608547328) +AUTO_DRAW_EMOJI = discord.PartialEmoji(name="auto_draw", id=1032565224903016449) # "🔄" +SELECT_EMOJI = discord.PartialEmoji(name="select_tool", id=1037847279169704028) # "📓" +CURSOR_DISPLAY_EMOJI = "📍" +RAW_PAINT_EMOJI = "📤" + +UP_LEFT_EMOJI = discord.PartialEmoji(name="up_left", id=1032565175930343484) # "↖️" +UP_EMOJI = discord.PartialEmoji(name="up", id=1032564978676400148) # "⬆️" +UP_RIGHT_EMOJI = discord.PartialEmoji(name="up_right", id=1032564997869543464) # "↗️" +LEFT_EMOJI = discord.PartialEmoji(name="left", id=1032565106934022185) # "⬅️" +RIGHT_EMOJI = discord.PartialEmoji(name="right", id=1032565019352764438) # "➡️" +DOWN_LEFT_EMOJI = discord.PartialEmoji(name="down_left", id=1032565090223935518) # "↙️" +DOWN_EMOJI = discord.PartialEmoji(name="down", id=1032565072981131324) # "⬇️" +DOWN_RIGHT_EMOJI = discord.PartialEmoji(name="down_right", id=1032565043604230214) # "↘️" + + +class Notification: + def __init__( + self, + content: typing.Optional[str] = "", + *, + emoji: typing.Optional[ + typing.Union[discord.PartialEmoji, discord.Emoji] + ] = discord.PartialEmoji.from_str("🔔"), + view: discord.ui.View, + ): + self.emoji: typing.Union[discord.PartialEmoji, discord.Emoji] = emoji + self.content: str = content + self.view: discord.ui.View = view + + async def edit( + self, + content: typing.Optional[str] = None, + *, + emoji: typing.Optional[typing.Union[discord.PartialEmoji, discord.Emoji]] = None, + ) -> None: + if emoji is not None: + self.emoji = emoji + else: + emoji = self.emoji + self.content = content + await self.view._update() + + def get_truncated_content(self, length: typing.Optional[int] = None) -> str: + if length is None: + trunc = self.content.split("\n")[0] + else: + trunc = self.content[:length] + return trunc + (" ..." if len(self.content) > len(trunc) else "") + + +class ToolsMenu(discord.ui.Select): + def __init__( + self, + view: discord.ui.View, + *, + options: typing.Optional[typing.List[discord.SelectOption]] = None, + ) -> None: + self.tool_list: typing.List[Tool] = [ + BrushTool(view), + EraseTool(view), + EyedropperTool(view), + FillTool(view), + ReplaceTool(view), + DarkenTool(view), + LightenTool(view), + InverseTool(view), + ] + default_options: typing.List[discord.SelectOption] = [ + discord.SelectOption( + label=tool.name, + emoji=tool.emoji, + value=tool.name.lower(), + description=f"{tool.description}{' (Used automatically)' if tool.auto_use is True else ''}", + ) + for tool in self.tool_list + ] + options = options or default_options + self.END_INDEX = len(default_options) # The ending index of default options. + super().__init__( + placeholder="🖌️ Tools", + max_values=1, + options=options, + ) + self._view: discord.ui.View = view + + @property + def tools(self) -> typing.Dict[str, Tool]: + return {tool.name.lower(): tool for tool in self.tool_list} + + @property + def value_to_option_dict(self) -> typing.Dict[str, discord.SelectOption]: + return {option.value: option for option in self.options} + + def value_to_option( + self, value: typing.Union[str, int] + ) -> typing.Union[None, discord.SelectOption]: + return self.value_to_option_dict.get(value) + + def set_default(self, def_option: discord.SelectOption): + for option in self.options: + option.default = False + def_option.default = True + + async def callback(self, interaction: discord.Interaction): + await interaction.response.defer() + value = self.values[0] + tool = self.tools[value] + # If the tool selected is one of these, use it directly instead of equipping. + if tool.auto_use: + if await tool.use( + interaction=interaction + ): # This is to decide whether or not to edit the message, depending on if the tool was used successfully. + await self.view._update() + # Else, equip the tool (to the primary tool button slot). + else: + self.view.primary_tool = tool + self.view.load_items() + self.set_default(self.value_to_option(value)) + await self.view._update() + + +class ColorsMenu(discord.ui.Select): + def __init__( + self, + view: discord.ui.View, + *, + options: typing.Optional[typing.List[discord.SelectOption]] = None, + background: str, + ) -> None: + default_options: typing.List[discord.SelectOption] = [ + *base_colors_options(), + discord.SelectOption( + label="Add Color(s)", + emoji=ADD_COLORS_EMOJI, + value="color", + ), + discord.SelectOption( + label="Add Emoji(s)", + emoji=ADD_EMOJIS_EMOJI, + value="emoji", + ), + discord.SelectOption(label="Mix Colors", emoji=MIX_COLORS_EMOJI, value="mix"), + ] + options = options or default_options + self.END_INDEX = len(default_options) # The ending index of default options + for option in options: + if str(option.emoji) == background and not option.label.endswith(" (bg)"): + option.label += " (bg)" + super().__init__( + placeholder="🎨 Palette", + options=options, + ) + self._view: discord.ui.View = view + self.bot: Red = self._view.cog.bot + self.board: Board = self._view.board + + @property + def value_to_option_dict(self) -> typing.Dict[str, discord.SelectOption]: + return {option.value: option for option in self.options} + + def value_to_option( + self, value: typing.Union[str, int] + ) -> typing.Union[None, discord.SelectOption]: + return self.value_to_option_dict.get(value) + + def emoji_to_option( + self, emoji: typing.Union[discord.Emoji, discord.PartialEmoji, Color] + ) -> typing.Union[None, discord.SelectOption]: + if isinstance(emoji, discord.Emoji): + identifier = emoji.id + elif isinstance(emoji, discord.PartialEmoji): + identifier = emoji.name if emoji.is_unicode_emoji() else emoji.id + else: + identifier = f"#{emoji.hex}" + return self.value_to_option_dict.get(str(identifier)) + + def append_option( + self, option: discord.SelectOption + ) -> typing.Tuple[bool, typing.Union[discord.SelectOption, None]]: + if (found_option := self.value_to_option(option.value)) is not None: + return False, found_option + replaced_option = None + if len(self.options) == 25: + replaced_option = self.options.pop(self.END_INDEX) + replaced_option.emoji.name = replaced_option.label + super().append_option(option) + return replaced_option is not None, replaced_option + + def set_default(self, def_option: discord.SelectOption) -> None: + for option in self.options: + option.default = False + def_option.default = True + + async def append_sent_emojis( + self, sent_emojis: typing.List[typing.Union[discord.Emoji, discord.PartialEmoji, Color]] + ) -> typing.Dict[typing.Union[discord.Emoji, discord.PartialEmoji, Color], str]: + added_emojis = { + sent_emoji: "Already exists." if self.emoji_to_option(sent_emoji) else "Added." + for sent_emoji in sent_emojis + } + replaced_emojis = {} + for added_emoji, status in added_emojis.items(): + if status != "Added.": + continue + if isinstance(added_emoji, discord.Emoji) or ( + isinstance(added_emoji, discord.PartialEmoji) and added_emoji.is_custom_emoji() + ): + name = f"{added_emoji.name} ({added_emoji.id})" + emoji = added_emoji + value = str(added_emoji.id) + elif ( + isinstance(added_emoji, discord.PartialEmoji) and not added_emoji.is_custom_emoji() + ): + name = added_emoji.name + emoji = added_emoji + value = added_emoji.name + elif isinstance(added_emoji, Color): + name = (await added_emoji.get_name()) + f" ({added_emoji.hex})" + emoji = None + value = f"#{added_emoji.hex}" + else: + continue + option = discord.SelectOption( + label=name, + emoji=emoji, + value=value, + ) + replaced, returned_option = self.append_option(option) + if replaced: + replaced_emoji = returned_option.value + replaced_emojis[added_emoji] = replaced_emoji + for added_emoji, replaced_emoji in replaced_emojis.items(): + added_emojis[added_emoji] = f"Added (replaced {replaced_emoji})." + return added_emojis + + async def added_emojis_respond( + self, + added_emojis: typing.Dict[typing.Union[discord.Emoji, discord.PartialEmoji, Color], str], + *, + notification: Notification, + interaction: discord.Interaction, + ) -> None: + if not added_emojis: + return await notification.edit("Aborted.") + response = [f"{added_emoji} - {status}" for added_emoji, status in added_emojis.items()] + if any("Added." in status for status in added_emojis.values()): + value = self.options[-1].value + if value.startswith("#"): + value = Color.from_hex(value[1:]) + self.board.cursor = value + self.set_default(self.options[-1]) + response = "\n".join(response) + await notification.edit(f"{response}..." if len(response) > 2500 else response) + await self.view._update() + + def extract_emojis( + self, content: str + ) -> typing.List[typing.Union[discord.PartialEmoji, Color]]: + # Get any unicode emojis from the content and list them as SentEmoji objects. + unicode_emojis = [ + discord.PartialEmoji.from_str(emoji) for emoji in _emoji.distinct_emoji_list(content) + ] + # Get any flag/regional indicator emojis from the content and list them as SentEmoji objects. + FLAG_EMOJI_REGEX = re.compile("[\U0001F1E6-\U0001F1FF]") + flag_emojis = [ + discord.PartialEmoji.from_str(emoji.group(0)) + for emoji in FLAG_EMOJI_REGEX.finditer(content) + ] + # Get any custom emojis from the content and list them as SentEmoji objects. + CUSTOM_EMOJI_REGEX = re.compile("") + custom_emojis = [ + discord.PartialEmoji.from_str(emoji.group(0)) + for emoji in CUSTOM_EMOJI_REGEX.finditer(content) + ] + return unicode_emojis + flag_emojis + custom_emojis + + async def callback(self, interaction: discord.Interaction) -> None: + await interaction.response.defer() + + # Set max values to 1 everytime the menu is used. + initial_max_values = self.max_values + self.max_values = 1 + + # If the "Add Color(s)" option was selected. Always takes first priority. + if "color" in self.values: + + def check(m): + return m.author == interaction.user and m.channel == interaction.channel + + notification = await self.view.create_notification( + _( + "Please type all the colors you want to add. They can be either or all of:" + "\n• The hex codes (e.g. `ff64c4` or `ff64c4ff` to include alpha) **separated by space**," + "\n• The RGB(A) values separated by space or comma or both (e.g. `(255 100 196)` or `(255, 100, 196, 125)`) of each color **surrounded by brackets**" + "\n• Any emoji whose main color you want to extract (e.g. 🐸 will give 77b255)" + "\n• Any image file (first 5 abundant colors will be extracted)." + ), + interaction=interaction, + ) + try: + msg = await self.bot.wait_for("message", timeout=30, check=check) + except asyncio.TimeoutError: + await notification.edit("Timed out, aborted.") + return + await CogsUtils.delete_message(msg) + + CHANNEL = "[a-fA-F0-9]{2}" + HEX_REGEX = re.compile( + rf"\b(?P{CHANNEL})(?P{CHANNEL})(?P{CHANNEL})(?P{CHANNEL})?\b" + ) + ZERO_TO_255 = "0*25[0-5]|0*2[0-4][0-9]|0*1[0-9]{2}|0*[1-9][0-9]|0*[0-9]" + RGB_A_REGEX = re.compile( + rf"\((?P{ZERO_TO_255}) *,? +(?P{ZERO_TO_255}) *,? +(?P{ZERO_TO_255})(?: *,? +(?P{ZERO_TO_255}))?\)" + ) + + content = msg.content.lower().strip() + + # Get any hex codes from the content + hex_matches = list(HEX_REGEX.finditer(content)) + # Get any RGB/A values from the content + rgb_a_matches = list(RGB_A_REGEX.finditer(content)) + total_matches = hex_matches + rgb_a_matches + # Organize all the matches into SentEmoji objects. + sent_emojis = [] + for match in total_matches: + base = 16 if match in hex_matches else 10 + red = int(match.group("red"), base) + green = int(match.group("green"), base) + blue = int(match.group("blue"), base) + alpha = int( + match.group("alpha") or ("ff" if match in hex_matches else "255"), + base, + ) + color = Color((red, green, blue, alpha)) + sent_emojis.append(color) + + # Extract from emoji. + emoji_matches = self.extract_emojis(content) + for match in emoji_matches: + color = await Color.from_emoji(cog=self._view.cog, emoji=match) + sent_emojis.append(color) + + # Extract from first attachment. + if msg.attachments: + attachment_colors = await Color.from_attachment(msg.attachments[0]) + for color in attachment_colors: + sent_emojis.append(color) + + added_emojis = await self.append_sent_emojis(sent_emojis) + await self.added_emojis_respond( + added_emojis, notification=notification, interaction=interaction + ) + + # First it checks if the "Add Emoji(s)" option was selected. Takes second priority. + elif "emoji" in self.values: + + def check(m): + return m.author == interaction.user and m.channel == interaction.channel + + notification = await self.view.create_notification( + _( + "Please send a message containing the emojis you want to add to your palette. E.g. `😎 I like turtles 🐢`." + ), + interaction=interaction, + ) + try: + msg = await self.bot.wait_for("message", timeout=30, check=check) + except asyncio.TimeoutError: + await notification.edit("Timed out, aborted.") + return + await CogsUtils.delete_message(msg) + + content = msg.content.strip() + sent_emojis = self.extract_emojis(content) + added_emojis = await self.append_sent_emojis(sent_emojis) + await self.added_emojis_respond( + added_emojis, notification=notification, interaction=interaction + ) + + # If user has chosen to "Mix Colors". + elif "mix" in self.values: + if initial_max_values > 1: + self.max_values = 1 + await self.view.create_notification( + f"Mixing disabled.", + emoji="🔀", + interaction=interaction, + ) + else: + self.max_values = len(self.options) + await self.view.create_notification( + f"Mixing enabled. You can now select multiple colors/emojis to mix their primary colors.", + emoji="🔀", + interaction=interaction, + ) + + # If multiple options were selected. + elif len(self.values) > 1: + selected_options = [self.value_to_option(value) for value in self.values] + selected_colors = [ + str(option.value) + for option in selected_options + if option.value.startswith("#") or option.value in MAIN_COLORS_DICT + ] + notification = await self.view.create_notification( + f"Mixing colors {humanize_list(selected_colors)}...", + emoji="🔀", + interaction=interaction, + ) + colors = [ + MAIN_COLORS_DICT.get(str(color)) or (Color.from_hex(color[1:])) + for color in selected_colors + ] + mixed_color = Color.mix_colors(colors) + label = (await mixed_color.get_name()) + f" (#{mixed_color.hex})" + option = discord.SelectOption( + label=label, + value=f"#{mixed_color.hex}", + ) + replaced, returned_option = self.append_option(option) + self.board.cursor = mixed_color + self.set_default(option) + await notification.edit( + f"Mixed colors:\n{' + '.join(selected_colors)} = {label}" + + (f" (replaced {returned_option.emoji})." if replaced else "") + ) + await self.view._update() + + # If only one option was selected. + elif self.board.cursor != (value := self.values[0]): + if value.startswith("#"): + value = Color.from_hex(value[1:]) + self.board.cursor = value + self.set_default(self.value_to_option(value)) + await self.view._update() + + +class DrawView(discord.ui.View): + def __init__( + self, + cog: commands.Cog, + board: Board, + tool_options: typing.Optional[typing.List[discord.SelectOption]] = None, + color_options: typing.Optional[typing.List[discord.SelectOption]] = None, + ) -> None: + super().__init__(timeout=600) + self.cog: commands.Cog = cog + self.ctx: commands.Context = None + + self.board: Board = board + + self.tool_menu: ToolsMenu = ToolsMenu(self, options=tool_options) + self.color_menu: ColorsMenu = ColorsMenu( + self, options=color_options, background=board.background + ) + self.primary_tool: Tool = self.tool_menu.tools["brush"] + + self.auto: bool = False + self.select: bool = False + self.disabled: bool = False + self.secondary_page: bool = False + + self.lock: asyncio.Lock = asyncio.Lock() + self.notifications: typing.List[Notification] = [Notification(view=self)] + + self._message: discord.Message = None + + self._ready: asyncio.Event = asyncio.Event() + + async def start( + self, ctx: commands.Context, message: typing.Optional[discord.Message] = None + ) -> discord.Message: + self.ctx: commands.Context = ctx + self._message = message + await self._update() + await self._ready.wait() + return self._message + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user.id not in [self.ctx.author.id] + list(self.ctx.bot.owner_ids): + await interaction.response.send_message( + _("You are not allowed to use this interaction."), ephemeral=True + ) + return False + return True + + async def on_timeout(self) -> None: + self.board.clear_cursors(empty=True) + for child in self.children: + child: discord.ui.Item + if hasattr(child, "disabled") and not ( + isinstance(child, discord.ui.Button) and child.style == discord.ButtonStyle.url + ): + child.disabled = True + try: + await self._update(empty=True) + except discord.HTTPException: + pass + self._ready.set() + + async def _update(self, empty: bool = False) -> None: + self._embed: discord.Embed = await self.get_embed(self.ctx) + file = await self.board.to_file() + if not empty: + self.load_items() + if self._message is None: + self._message: discord.Message = await self.ctx.send( + embed=self._embed, + file=file, + view=self, + ) + self.cog.views[self._message] = self + else: + self._message: discord.Message = await self._message.edit( + content=None, + embed=self._embed, + attachments=[file], + view=self, + ) + + async def get_embed(self, ctx: commands.Context) -> discord.Embed: + embed: discord.Embed = discord.Embed(title="Draw Board", color=await ctx.embed_color()) + # embed.description = str(self.board) + embed.set_image(url=f"attachment://image.{IMAGE_EXTENSION.lower()}") + # This section adds the notification field only if any one + # of the notifications is not empty. In such a case, it only + # shows the notification(s) that is not empty + if any((len(n.content) != 0 for n in self.notifications)): + embed.add_field( + name="Notifications", + value="\n\n".join( + [ + ( + ( + f"{str(n.emoji)} " + + (n.content if idx == 0 else n.get_truncated_content()).replace( + "\n", "\n> " + ) + ) # Put each notification into seperate quotes + if len(n.content) != 0 + else "" + ) # Show only non-empty notifications + for idx, n in enumerate(self.notifications) + ] + ), + ) + # The embed footer. + embed.set_footer( + text=( + f"The board looks wack? Try decreasing its size! Do {self.ctx.clean_prefix}help draw for more info." + if any((len(self.board.row_labels) >= 10, len(self.board.col_labels) >= 10)) + else f"You can customize this board! Do {self.ctx.clean_prefix}help draw for more info." + ) + ) + return embed + + async def create_notification( + self, + content: typing.Optional[str] = None, + *, + emoji: typing.Optional[ + typing.Union[discord.PartialEmoji, discord.Emoji] + ] = discord.PartialEmoji.from_str("🔔"), + interaction: typing.Optional[discord.Interaction] = None, + ) -> Notification: + self.notifications = self.notifications[:2] + notification = Notification(content, emoji=emoji, view=self) + self.notifications.insert(0, notification) + if interaction is not None: + await self._update() + return notification + + @property + def placeholder_button(self) -> discord.ui.Button: + button = discord.ui.Button( + label="\u200b", + style=discord.ButtonStyle.gray, + custom_id=str(len(self.children)), + ) + button.callback = lambda interaction: interaction.response.defer() + return button + + def load_items(self) -> None: + self.clear_items() + self.add_item(self.tool_menu) + self.add_item(self.color_menu) + # This is necessary for "paginating" the view and different buttons. + if self.secondary_page is False: + self.add_item(self.undo) + self.add_item(self.up_left) + self.add_item(self.up) + self.add_item(self.up_right) + self.add_item(self.secondary_page_button) + + self.add_item(self.redo) + self.add_item(self.left) + self.add_item(self.set_cursor) + self.add_item(self.right) + # self.add_item(self.placeholder_button) + self.add_item(self.set_auto_draw) + + self.add_item(self.primary_tool) + self.add_item(self.down_left) + self.add_item(self.down) + self.add_item(self.down_right) + # self.add_item(self.placeholder_button) + self.add_item(self.select_area) + elif self.secondary_page is True: + self.add_item(self.stop_button) + self.add_item(self.up_left) + self.add_item(self.up) + self.add_item(self.up_right) + self.add_item(self.secondary_page_button) + + self.add_item(self.clear) + self.add_item(self.left) + self.add_item(self.set_cursor) + self.add_item(self.right) + # self.add_item(self.placeholder_button) + self.add_item(self.raw_paint) + + self.add_item(self.primary_tool) + self.add_item(self.down_left) + self.add_item(self.down) + self.add_item(self.down_right) + # self.add_item(self.placeholder_button) + self.add_item(self.set_cursor_display) + self.update_buttons() + + def update_buttons(self) -> None: + self.secondary_page_button.style = ( + discord.ButtonStyle.success if self.secondary_page else discord.ButtonStyle.secondary + ) + self.undo.disabled = self.board.board_index == 0 or self.disabled + self.undo.label = f"{self.board.board_index} ↶" + self.redo.disabled = ( + self.board.board_index == len(self.board.board_history) - 1 + ) or self.disabled + self.redo.label = f"↷ {(len(self.board.board_history) - 1) - self.board.board_index}" + + async def move_cursor( + self, + interaction: discord.Interaction, + row_move: typing.Optional[int] = 0, + col_move: typing.Optional[int] = 0, + ) -> None: + self.board.move_cursor(row_move, col_move, self.select) + if self.auto: + await self.primary_tool.use(interaction=interaction) + await self._update() + + # Buttons + + # 1st row + @discord.ui.button(label="↶", style=discord.ButtonStyle.secondary) + async def undo(self, interaction: discord.Interaction, button: discord.Button) -> None: + await interaction.response.defer() + if self.board.board_index > 0: + self.board.board_index -= 1 + await self._update() + + @discord.ui.button(style=discord.ButtonStyle.danger, emoji="✖️", custom_id="close_page") + async def stop_button( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + try: + await interaction.response.defer() + except discord.errors.NotFound: + pass + self.stop() + await CogsUtils.delete_message(self._message) + self._ready.set() + + @discord.ui.button(emoji=UP_LEFT_EMOJI, style=discord.ButtonStyle.primary) + async def up_left(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: + await interaction.response.defer() + row_move = -1 + col_move = -1 + await self.move_cursor(interaction, row_move=row_move, col_move=col_move) + + @discord.ui.button(emoji=UP_EMOJI, style=discord.ButtonStyle.primary) + async def up(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: + await interaction.response.defer() + row_move = -1 + col_move = 0 + await self.move_cursor(interaction, row_move=row_move, col_move=col_move) + + @discord.ui.button(emoji=UP_RIGHT_EMOJI, style=discord.ButtonStyle.primary) + async def up_right(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: + await interaction.response.defer() + row_move = -1 + col_move = 1 + await self.move_cursor(interaction, row_move=row_move, col_move=col_move) + + @discord.ui.button(label="2nd", style=discord.ButtonStyle.secondary) + async def secondary_page_button( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + await interaction.response.defer() + self.secondary_page = not self.secondary_page + + self.load_items() + await self._update() + + # 2nd Row + @discord.ui.button(label="↷", style=discord.ButtonStyle.secondary) + async def redo(self, interaction: discord.Interaction, button: discord.Button) -> None: + await interaction.response.defer() + if self.board.board_index < len(self.board.board_history) - 1: + self.board.board_index += 1 + await self._update() + + @discord.ui.button(label="Clear", style=discord.ButtonStyle.danger) + async def clear(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: + await interaction.response.defer() + self.secondary_page = False + self.auto = False + self.select = False + self.board.clear() + self.load_items() + await self._update() + + @discord.ui.button(emoji=LEFT_EMOJI, style=discord.ButtonStyle.primary) + async def left(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: + await interaction.response.defer() + row_move = 0 + col_move = -1 + await self.move_cursor(interaction, row_move=row_move, col_move=col_move) + + @discord.ui.button(emoji=RIGHT_EMOJI, style=discord.ButtonStyle.primary) + async def right(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: + await interaction.response.defer() + row_move = 0 + col_move = 1 + await self.move_cursor(interaction, row_move=row_move, col_move=col_move) + + # 3rd / Last Row + @discord.ui.button(emoji=DOWN_LEFT_EMOJI, style=discord.ButtonStyle.primary) + async def down_left(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: + await interaction.response.defer() + row_move = 1 + col_move = -1 + await self.move_cursor(interaction, row_move=row_move, col_move=col_move) + + @discord.ui.button(emoji=DOWN_EMOJI, style=discord.ButtonStyle.primary) + async def down(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: + await interaction.response.defer() + row_move = 1 + col_move = 0 + await self.move_cursor(interaction, row_move=row_move, col_move=col_move) + + @discord.ui.button(emoji=DOWN_RIGHT_EMOJI, style=discord.ButtonStyle.primary) + async def down_right( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + await interaction.response.defer() + row_move = 1 + col_move = 1 + await self.move_cursor(interaction, row_move=row_move, col_move=col_move) + + @discord.ui.button(emoji=SET_CURSOR_EMOJI, style=discord.ButtonStyle.secondary) + async def set_cursor( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + await interaction.response.defer() + + def check(m): + return m.author == interaction.user and m.channel == interaction.channel + + notification = await self.create_notification( + _( + "Please type the cell you want to move the cursor to. e.g. `A1`, `a1`, `A10`, `A`, `10`, etc." + ), + emoji="🔠", + interaction=interaction, + ) + try: + msg = await self.ctx.bot.wait_for("message", timeout=30, check=check) + except asyncio.TimeoutError: + await notification.edit("Timed out, aborted.") + return + await CogsUtils.delete_message(msg) + + cell: str = msg.content.upper() + ABC = ALPHABETS[: self.board.cursor_row_max + 1] + NUM = NUMBERS[: self.board.cursor_col_max + 1] + end_row_key = None + end_col_key = None + + CELL_REGEX = ( + f"^(?P[A-{ABC[-1]}])(?P[0-9]|(?:1[0-{NUM[-1] % 10}]))" + f"(-(?P[A-{ABC[-1]}])(?P[0-9]|(?:1[0-{NUM[-1] % 10}]))?)?$" + ) + ROW_OR_COL_REGEX = ( + f"(?:^(?P[A-{ABC[-1]}])$)|(?:^(?P[0-9]|(?:1[0-{NUM[-1] % 10}]))$)" + ) + match = re.match(CELL_REGEX, cell) + if match is not None: + start_row_key = match["start_row"] + start_col_key = int(match["start_col"]) + if match["end_row"] is not None: + end_row_key = match["end_row"] + end_col_key = ( + int(match["end_col"]) if match["end_col"] is not None else start_col_key + ) + else: + match = re.match(ROW_OR_COL_REGEX, cell) + if match is not None: + start_row_key = ( + match["row"] if match["row"] is not None else ABC[self.board.cursor_row] + ) + start_col_key = ( + int(match["col"]) if match["col"] is not None else self.board.cursor_col + ) + else: + return await notification.edit("Aborted.") + + if ( + start_row_key not in ABC + or start_col_key not in NUM + or (end_row_key is not None and end_row_key not in ABC) + or (end_col_key is not None and end_col_key not in NUM) + ): + return await notification.edit("Aborted.") + + if end_row_key is None and end_col_key is None: + row_move = LETTER_TO_NUMBER[start_row_key] - self.board.cursor_row + col_move = start_col_key - self.board.cursor_col + await notification.edit( + f"Moved cursor to **{cell}** ({LETTER_TO_NUMBER[start_row_key]}, {start_col_key}).", + ) + await self.move_cursor(interaction, row_move=row_move, col_move=col_move) + else: + row_move = LETTER_TO_NUMBER[start_row_key] - self.board.cursor_row + col_move = start_col_key - self.board.cursor_col + self.board.move_cursor(row_move, col_move, self.select) + self.board.initial_coords = ( + self.board.cursor_row, + self.board.cursor_col, + ) + ( + self.board.initial_row, + self.board.initial_col, + ) = self.board.initial_coords + self.select = not self.select + row_move = LETTER_TO_NUMBER[end_row_key] - self.board.cursor_row + col_move = end_col_key - self.board.cursor_col + await notification.edit( + f"Moved cursor to select **{cell}** ({LETTER_TO_NUMBER[start_row_key]}, {start_col_key} | {LETTER_TO_NUMBER[end_row_key]}, {end_col_key}).", + ) + await self.move_cursor(interaction, row_move=row_move, col_move=col_move) + + @discord.ui.button( + label="Auto Draw", emoji=AUTO_DRAW_EMOJI, style=discord.ButtonStyle.secondary + ) + async def set_auto_draw( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + await interaction.response.defer() + self.auto = not self.auto + state = "enabled" if self.auto else "disabled" + await self.create_notification( + f"Auto Draw {state}.", + emoji="🔄", + interaction=interaction, + ) + + @discord.ui.button( + label="Select an Area", emoji=SELECT_EMOJI, style=discord.ButtonStyle.secondary + ) + async def select_area( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + await interaction.response.defer() + if self.select is False: + self.board.initial_coords = ( + self.board.cursor_row, + self.board.cursor_col, + ) + ( + self.board.initial_row, + self.board.initial_col, + ) = self.board.initial_coords + self.select = not self.select + elif self.select is True: + self.board.clear_cursors() + self.select = not self.select + await self._update() + + @discord.ui.button( + label="Raw Paint", emoji=RAW_PAINT_EMOJI, style=discord.ButtonStyle.secondary + ) + async def raw_paint(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: + await interaction.response.defer() + + def check(m): + return m.author == interaction.user and m.channel == interaction.channel + + notification = await self.create_notification( + _( + 'Please type the cells with the main colors names. One color for each line, separated from the cell by a ":". Example: `A1:red\nB7-C9:green`.' + ), + emoji=RAW_PAINT_EMOJI, + interaction=interaction, + ) + try: + msg = await self.ctx.bot.wait_for("message", timeout=30, check=check) + except asyncio.TimeoutError: + await notification.edit("Timed out, aborted.") + return + await CogsUtils.delete_message(msg) + + for i, line in enumerate(msg.content.split("\n"), start=1): + if len(line.split(":")) != 2: + return await notification.edit(f"Aborted. No `:` in line {i}.") + cell: str = line.split(":")[0].strip().upper() + color: str = line.split(":")[1].strip().lower() + + ABC = ALPHABETS[: self.board.cursor_row_max + 1] + NUM = NUMBERS[: self.board.cursor_col_max + 1] + end_row_key = None + end_col_key = None + + CELL_REGEX = ( + f"^(?P[A-{ABC[-1]}])(?P[0-9]|(?:1[0-{NUM[-1] % 10}]))" + f"(-(?P[A-{ABC[-1]}])(?P[0-9]|(?:1[0-{NUM[-1] % 10}]))?)?$" + ) + ROW_OR_COL_REGEX = ( + f"(?:^(?P[A-{ABC[-1]}])$)|(?:^(?P[0-9]|(?:1[0-{NUM[-1] % 10}]))$)" + ) + match = re.match(CELL_REGEX, cell) + if match is not None: + start_row_key = match["start_row"] + start_col_key = int(match["start_col"]) + if match["end_row"] is not None: + end_row_key = match["end_row"] + end_col_key = ( + int(match["end_col"]) if match["end_col"] is not None else start_col_key + ) + else: + match = re.match(ROW_OR_COL_REGEX, cell) + if match is not None: + start_row_key = ( + match["row"] if match["row"] is not None else ABC[self.board.cursor_row] + ) + start_col_key = ( + int(match["col"]) if match["col"] is not None else self.board.cursor_col + ) + else: + return await notification.edit(f"Aborted. No cell match in line {i}.") + + if ( + start_row_key not in ABC + or start_col_key not in NUM + or (end_row_key is not None and end_row_key not in ABC) + or (end_col_key is not None and end_col_key not in NUM) + ): + return await notification.edit( + f"Aborted. Wrong letter/num for cell(s) in line {i}." + ) + + colors = {option.label.lower(): option.value for option in base_colors_options()} + if color not in colors: + return await notification.edit(f"Aborted. Invalid color in line {i}.") + color = colors[color] + + if end_row_key is None and end_col_key is None: + self.board.draw( + color=color, coords=[(LETTER_TO_NUMBER[start_row_key], start_col_key)] + ) + else: + self.board.draw( + color=color, + coords=[ + (row, col) + for col in range( + min(start_col_key, end_col_key), + max(start_col_key, end_col_key) + 1, + ) + for row in range( + min(LETTER_TO_NUMBER[start_row_key], LETTER_TO_NUMBER[end_row_key]), + max(LETTER_TO_NUMBER[start_row_key], LETTER_TO_NUMBER[end_row_key]) + + 1, + ) + ], + ) + + await notification.edit("Draw paint successful.") + + @discord.ui.button( + label="Cursor Display", emoji=CURSOR_DISPLAY_EMOJI, style=discord.ButtonStyle.success + ) + async def set_cursor_display( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + await interaction.response.defer() + self.board.cursor_display = not self.board.cursor_display + state = "enabled" if self.board.cursor_display else "disabled" + await self.create_notification( + f"Cursor Display {state}.", + emoji="📍", + interaction=interaction, + ) diff --git a/embedutils/README.rst b/embedutils/README.rst new file mode 100644 index 0000000..1ef2c6a --- /dev/null +++ b/embedutils/README.rst @@ -0,0 +1,115 @@ +.. _embedutils: +========== +EmbedUtils +========== + +This is the cog guide for the ``EmbedUtils`` cog. This guide contains the collection of commands which you can use in the cog. +Through this guide, ``[p]`` will always represent your prefix. Replace ``[p]`` with your own prefix when you use these commands in Discord. + +.. note:: + + Ensure that you are up to date by running ``[p]cog update embedutils``. + If there is something missing, or something that needs improving in this documentation, feel free to create an issue `here `_. + This documentation is generated everytime this cog receives an update. + +--------------- +About this cog: +--------------- + +Create, send, and store rich embeds, from Red-Web-Dashboard too! + +--------- +Commands: +--------- + +Here are all the commands included in this cog (18): + +* ``[p]embed [channel_or_message] [color] <description>`` + Post a simple embed with a color, a title and a description. + +* ``[p]embed dashboard ["json"|"fromjson"|"fromdata"|"yaml"|"fromyaml"|"fromfile"|"jsonfile"|"fromjsonfile"|"fromdatafile"|"yamlfile"|"fromyamlfile"|"gist"|"pastebin"|"hastebin"|"message"|"frommessage"|"msg"|"frommsg"] [data]`` + Get the link to the Dashboard. + +* ``[p]embed download [message] [index] [include_content]`` + Download a JSON file for a message's embed(s). + +* ``[p]embed downloadstored [global_level=False] <name>`` + Download a JSON file for a stored embed. + +* ``[p]embed edit <message> <json|yaml|jsonfile|yamlfile|pastebin|message> [data]`` + Edit a message sent by [botname]. + +* ``[p]embed fromfile [channel_or_message]`` + Post an embed from a valid JSON file (upload it). + +* ``[p]embed info [global_level=False] <name>`` + Get info about a stored embed. + +* ``[p]embed json [channel_or_message] [data]`` + Post embeds from valid JSON. + +* ``[p]embed list [global_level=False]`` + Get info about a stored embed. + +* ``[p]embed message [channel_or_message] [message] [index] [include_content]`` + Post embed(s) from an existing message. + +* ``[p]embed migratefromphen`` + Migrate stored embeds from EmbedUtils by Phen. + +* ``[p]embed pastebin [channel_or_message] <data>`` + Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON. + +* ``[p]embed poststored [channel_or_message=<CurrentChannel>] [global_level=False] <names>`` + Post stored embeds. + +* ``[p]embed postwebhook [channel_or_message=<CurrentChannel>] <username> <avatar_url> [global_level=False] <names>`` + Post stored embeds with a webhook. + +* ``[p]embed store [global_level=False] [locked=False] <name> <json|yaml|jsonfile|yamlfile|pastebin|message> [data]`` + Store an embed. + +* ``[p]embed unstore [global_level=False] <name>`` + Remove a stored embed. + +* ``[p]embed yaml [channel_or_message] [data]`` + Post embeds from valid YAML. + +* ``[p]embed yamlfile [channel_or_message]`` + Post an embed from a valid YAML file (upload it). + +------------ +Installation +------------ + +If you haven't added my repo before, lets add it first. We'll call it "AAA3A-cogs" here. + +.. code-block:: ini + + [p]repo add AAA3A-cogs https://github.com/AAA3A-AAA3A/AAA3A-cogs + +Now, we can install EmbedUtils. + +.. code-block:: ini + + [p]cog install AAA3A-cogs embedutils + +Once it's installed, it is not loaded by default. Load it by running the following command: + +.. code-block:: ini + + [p]load embedutils + +---------------- +Further Support: +---------------- + +Check out my docs `here <https://aaa3a-cogs.readthedocs.io/en/latest/>`_. +Mention me in the #support_other-cogs in the `cog support server <https://discord.gg/GET4DVk>`_ if you need any help. +Additionally, feel free to open an issue or pull request to this repo. + +-------- +Credits: +-------- + +Thanks to Kreusada for the Python code to automatically generate this documentation! \ No newline at end of file diff --git a/embedutils/__init__.py b/embedutils/__init__.py new file mode 100644 index 0000000..5c824cc --- /dev/null +++ b/embedutils/__init__.py @@ -0,0 +1,46 @@ +from redbot.core import errors # isort:skip +import importlib +import sys + +try: + import AAA3A_utils +except ModuleNotFoundError: + raise errors.CogLoadError( + "The needed utils to run the cog were not found. Please execute the command `[p]pipinstall git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." + ) +modules = sorted( + [module for module in sys.modules if module.split(".")[0] == "AAA3A_utils"], reverse=True +) +for module in modules: + try: + importlib.reload(sys.modules[module]) + except ModuleNotFoundError: + pass +del AAA3A_utils +# import AAA3A_utils +# import json +# import os +# __version__ = AAA3A_utils.__version__ +# with open(os.path.join(os.path.dirname(__file__), "utils_version.json"), mode="r") as f: +# data = json.load(f) +# needed_utils_version = data["needed_utils_version"] +# if __version__ > needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a higher version than the one supported by this version of the cog. Please update the cogs of the `AAA3A-cogs` repo." +# ) +# elif __version__ < needed_utils_version: +# raise errors.CogLoadError( +# "The needed utils to run the cog has a lower version than the one supported by this version of the cog. Please execute the command `[p]pipinstall --upgrade git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git`. A restart of the bot isn't necessary." +# ) + +from redbot.core.bot import Red # isort:skip +from redbot.core.utils import get_end_user_data_statement + +from .embedutils import EmbedUtils + +__red_end_user_data_statement__ = get_end_user_data_statement(file=__file__) + + +async def setup(bot: Red) -> None: + cog = EmbedUtils(bot) + await bot.add_cog(cog) diff --git a/embedutils/converters.py b/embedutils/converters.py new file mode 100644 index 0000000..4e8dc13 --- /dev/null +++ b/embedutils/converters.py @@ -0,0 +1,402 @@ +from AAA3A_utils import Menu # isort:skip +from redbot.core import commands # isort:skip +from redbot.core.i18n import Translator # isort:skip +import discord # isort:skip +import typing # isort:skip + +import io +import json +import re +import textwrap + +import aiohttp +import yaml +from redbot.core import dev_commands +from redbot.core.utils.chat_formatting import box + +_: Translator = Translator("EmbedUtils", __file__) + + +def cleanup_code(code: str) -> str: + code = dev_commands.cleanup_code(textwrap.dedent(code)).strip() + if code.startswith("json\n"): + code = code[5:] + with io.StringIO(code) as codeio: + for line in codeio: + line = line.strip() + if line and not line.startswith("#"): + break + else: + return "pass" + return code + + +class StringToEmbed(commands.Converter): + def __init__( + self, *, conversion_type: str = "json", validate: bool = False, allow_content: bool = True + ) -> None: + self.CONVERSION_TYPES: typing.Dict[str, typing.Any] = { + "json": self.load_from_json, + "yaml": self.load_from_yaml, + } + + self.validate: bool = validate + self.conversion_type: typing.Literal["json", "yaml"] = conversion_type.lower() + self.allow_content: bool = allow_content + try: + self.converter = self.CONVERSION_TYPES[self.conversion_type] + except KeyError as exc: + raise ValueError( + f"`{conversion_type}` is not a valid conversion type for Embed conversion." + ) from exc + + async def convert( + self, ctx: commands.Context, argument: str + ) -> typing.Dict[typing.Literal["content", "embed"], typing.Union[discord.Embed, str]]: + argument = cleanup_code(argument) + data = await self.converter(ctx, argument=argument) + + content = self.get_content(data) if isinstance(data, typing.Dict) else None + if isinstance(data, typing.List): + data = data[0] + elif "embed" in data: + data = data["embed"] + elif "embeds" in data: + data = data.get("embeds")[0] + if not data: + raise commands.BadArgument( + _( + "This doesn't seem to be properly formatted embed {conversion_type}. " + "Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." + ).format(conversion_type=self.conversion_type.upper(), ctx=ctx) + ) + self.check_data_type(ctx, data=data) + + kwargs = await self.create_embed(ctx, data=data, content=content) + if self.validate: + await self.validate_embed(ctx, kwargs) + return kwargs + + def check_data_type(self, ctx: commands.Context, data, *, data_type=dict) -> None: + if not isinstance(data, data_type): + raise commands.BadArgument( + _( + "This doesn't seem to be properly formatted embed {conversion_type}. " + "Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." + ).format(conversion_type=self.conversion_type.upper(), ctx=ctx) + ) + + async def load_from_json( + self, ctx: commands.Context, argument: str, **kwargs + ) -> typing.Dict[str, typing.Any]: + try: + data = json.loads(argument) + except json.decoder.JSONDecodeError as error: + await self.embed_convert_error(ctx, _("JSON Parse Error"), error) + raise commands.BadArgument() + self.check_data_type(ctx, data, **kwargs) + return data + + async def load_from_yaml( + self, ctx: commands.Context, argument: str, **kwargs + ) -> typing.Dict[str, typing.Any]: + try: + data = yaml.safe_load(argument) + except Exception as error: + await self.embed_convert_error(ctx, _("YAML Parse Error"), error) + raise commands.BadArgument() + self.check_data_type(ctx, data, **kwargs) + return data + + def get_content( + self, data: typing.Dict[str, typing.Any], *, content: str = None + ) -> typing.Optional[str]: + content = data.pop("content", content) + if content is not None and not self.allow_content: + raise commands.BadArgument(_("The `content` field is not supported for this command.")) + return content + + async def create_embed( + self, ctx: commands.Context, data: typing.Dict[str, typing.Any], *, content: str = None + ) -> typing.Dict[typing.Literal["content", "embed"], typing.Union[discord.Embed, str]]: + content = self.get_content(data, content=content) + + if data.get("color") is None: + data.pop("color", None) + if (timestamp := data.get("timestamp")) is not None: + data["timestamp"] = ( + timestamp.strip("Z") if isinstance(timestamp, str) else str(timestamp) + ) + else: + data.pop("timestamp", None) + try: + embed = discord.Embed.from_dict(data) + length = len(embed) + except Exception as error: + await self.embed_convert_error(ctx, _("Embed Parse Error"), error) + raise commands.BadArgument() + + if length > 6000: + raise commands.BadArgument( + _("Embed size exceeds Discord limit of 6000 characters ({length}).").format( + length=length + ) + ) + return {"content": content, "embed": embed} + + async def validate_embed( + self, ctx: commands.Context, kwargs: typing.Dict[str, typing.Union[discord.Embed, str]] + ) -> None: + try: + await ctx.channel.send(**kwargs) # ignore tips/monkeypatch cogs + except discord.errors.HTTPException as error: + await self.embed_convert_error(ctx, _("Embed Send Error"), error) + raise commands.BadArgument() + + @staticmethod + async def embed_convert_error( + ctx: commands.Context, error_type: str, error: Exception + ) -> None: + if getattr(ctx, "__is_mocked__", False): + raise commands.BadArgument(f"{error_type}: `{type(error).__name__}`") + embed: discord.Embed = discord.Embed( + title=f"{error_type}: `{type(error).__name__}`", + description=box(str(error), lang="py"), + color=await ctx.embed_color(), + ) + embed.set_footer( + text=_( + "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." + ).format(ctx=ctx) + ) + await Menu(pages=[embed]).start(ctx) + + +class ListStringToEmbed(StringToEmbed): + def __init__(self, *, conversion_type: str = "json", limit: int = 10) -> None: + super().__init__(conversion_type=conversion_type, allow_content=False) + self.limit: int = min(limit, 10) + + async def convert( + self, ctx: commands.Context, argument: str + ) -> typing.Dict[ + typing.Literal["content", "embeds"], typing.Union[typing.List[discord.Embed], str] + ]: + argument = cleanup_code(argument) + data = await self.converter(ctx, argument=argument, data_type=(dict, list)) + + content = data.get("content") if isinstance(data, typing.Dict) else None + if isinstance(data, typing.List): + pass + elif "embed" in data: + data = [data["embed"]] + elif "embeds" in data: + data = data["embeds"] + if isinstance(data, typing.Dict): + data = list(data.values()) + elif "content" in data: + data = [] + else: + data = [data] + self.check_data_type(ctx, data=data, data_type=list) + + embeds = [] + for i, embed_data in enumerate(data, 1): + kwargs = await self.create_embed(ctx, data=embed_data) + embed = kwargs["embed"] + embeds.append(embed) + if i > self.limit: + raise commands.BadArgument( + _("Embed limit reached ({limit}).").format(limit=self.limit) + ) + if content or embeds: + return {"content": content, "embeds": embeds} + else: + raise commands.BadArgument(_("Failed to convert input into embeds.")) + + +class MessageableConverter(commands.Converter): + async def convert( + self, ctx: commands.Context, argument: str + ) -> typing.Union[discord.TextChannel, discord.VoiceChannel, discord.Thread]: + for converter in ( + commands.TextChannelConverter, + commands.VoiceChannelConverter, + commands.ThreadConverter, + ): + try: + channel = await converter().convert(ctx, argument=argument) + except commands.BadArgument: + pass + else: + break + else: + raise commands.BadArgument(_("It's not a valid channel or thread.")) + bot_permissions = channel.permissions_for(ctx.me) + if not (bot_permissions.send_messages and bot_permissions.embed_links): + raise commands.BadArgument( + _("I do not have permissions to send embeds in {channel.mention}.").format( + channel=channel + ) + ) + permissions = channel.permissions_for(ctx.author) + if not ( + permissions.send_messages and permissions.embed_links and permissions.manage_messages + ): + raise commands.BadArgument( + _("You do not have permissions to send embeds in {channel.mention}.").format( + channel=channel + ) + ) + return channel + + +class MyMessageConverter(commands.MessageConverter): + async def convert(self, ctx: commands.Context, argument: str) -> discord.Message: + message = await super().convert(ctx, argument=argument) + if message.author != ctx.me: + raise commands.UserFeedbackCheckFailure( + _( + "I have to be the author of the message. You can use the command without providing a message to send one." + ) + ) + ctx.message.channel = message.channel + fake_context = await ctx.bot.get_context(ctx.message) + if not await discord.utils.async_all( + [check(fake_context) for check in ctx.bot.get_cog("EmbedUtils").embed_edit.checks] + ): + raise commands.BadArgument( + _( + "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." + ) + ) + return message + + +class MessageableOrMessageConverter(commands.Converter): + async def convert( + self, ctx: commands.Context, argument: str + ) -> typing.Union[discord.TextChannel, discord.VoiceChannel, discord.Thread, discord.Message]: + try: + return await MessageableConverter().convert(ctx, argument=argument) + except commands.BadArgument as e: + try: + return await MyMessageConverter().convert(ctx, argument=argument) + except commands.BadArgument: + raise e + + +GITHUB_RE = re.compile( + r"https://(?:www\.)?github\.com/(?P<repo>[a-zA-Z0-9-]+/[\w.-]+)/blob/(?P<path>[^#>]+)" +) +GITHUB_GIST_RE = re.compile( + r"https://(?:www\.)?gist\.github\.com/([a-zA-Z0-9-]+)/(?P<gist_id>[a-zA-Z0-9]+)/*" + r"(?P<revision>[a-zA-Z0-9]*)/*(#file-(?P<file_path>[^#>]+))?" +) +PASTEBIN_RE = re.compile(r"https://(?:www\.)?pastebin\.com/(?P<paste_id>[a-zA-Z0-9]+)/*") +HASTEBIN_RE = re.compile(r"https://(?:www\.)?hastebin\.com/(?P<paste_id>[a-zA-Z0-9]+)/*") + +GITHUB_HEADERS = {"Accept": "application/vnd.github.v3.raw"} + + +class PastebinMixin: + async def convert( + self, ctx: commands.Context, argument: str + ) -> typing.Dict[ + typing.Literal["content", "embed", "embeds"], + typing.Union[discord.Embed, typing.List[discord.Embed], str], + ]: + async def _fetch_response(url: str, response_format: str, **kwargs) -> typing.Any: + if "github.com" in url: + api_tokens = await ctx.bot.get_shared_api_tokens(service_name="github") + if (token := api_tokens.get("token")) is not None: + if "headers" not in kwargs: + kwargs["headers"] = {} + kwargs["headers"]["Authorization"] = f"Token {token}" + try: + async with ctx.bot.get_cog("EmbedUtils")._session.get( + url, raise_for_status=True, **kwargs + ) as response: + if response_format == "text": + return await response.text() + return await response.json() if response_format == "json" else None + except (aiohttp.ClientResponseError, aiohttp.ClientError) as error: + raise commands.BadArgument( + f"Failed to fetch the content from the URL: {url}\n{box(error.message, lang='py')}" + ) + + def _find_ref(path: str, refs: typing.List[dict]) -> typing.Tuple[str, str]: + ref, file_path = path.split("/", 1) + for possible_ref in refs: + if path.startswith(possible_ref["name"] + "/"): + ref = possible_ref["name"] + file_path = path[len(ref) + 1 :] + break + return ref, file_path + + if _match := list(GITHUB_RE.finditer(argument)): + _match = _match[0].groupdict() + branches = await _fetch_response( + f"https://api.github.com/repos/{_match['repo']}/branches", + response_format="json", + headers=GITHUB_HEADERS, + ) + tags = await _fetch_response( + f"https://api.github.com/repos/{_match['repo']}/tags", response_format="json" + ) + refs = branches + tags + ref, file_path = _find_ref(_match["path"], refs) + argument = await _fetch_response( + f"https://api.github.com/repos/{_match['repo']}/contents/{file_path}?ref={ref}", + response_format="text", + headers=GITHUB_HEADERS, + ) + elif _match := list(GITHUB_GIST_RE.finditer(argument)): + _match = _match[0].groupdict() + revision = _match["revision"] + gist_json = await _fetch_response( + f"https://api.github.com/gists/{_match['gist_id']}{f'/{revision}' if revision != '' else ''}", + response_format="json", + headers=GITHUB_HEADERS, + ) + if len(gist_json["files"]) == 1 and _match["file_path"] is None: + file_path = list(gist_json["files"])[0].lower().replace(".", "-") + for gist_file in gist_json["files"]: + if file_path == gist_file.lower().replace(".", "-"): + argument = await _fetch_response( + gist_json["files"][gist_file]["raw_url"], + "text", + headers=GITHUB_HEADERS, + ) + elif _match := list(PASTEBIN_RE.finditer(argument)): + _match = _match[0].groupdict() + argument = await _fetch_response( + f"https://pastebin.com/raw/{_match['paste_id']}", response_format="text" + ) + elif (_match := list(HASTEBIN_RE.finditer(argument))) and ( + token := (await ctx.bot.get_shared_api_tokens(service_name="hastebin")).get("token") + ) is not None: + _match = _match[0].groupdict() + argument = await _fetch_response( + f"https://hastebin.com/raw/{_match['paste_id']}", + response_format="text", + headers={"Authentification": f"Bearer {token}"}, + ) + else: + raise commands.BadArgument( + f"`{argument}` is not a valid code GitHub/Gist/Pastebin/Hastebin link." + ) + return await super().convert(ctx, argument=argument) + + +class PastebinConverter(PastebinMixin, StringToEmbed): + pass + + +class PastebinListConverter(PastebinMixin, ListStringToEmbed): + pass + + +class StrConverter(commands.Converter): + async def convert(self, ctx: commands.Context, argument: str) -> str: + return argument diff --git a/embedutils/dashboard_integration.py b/embedutils/dashboard_integration.py new file mode 100644 index 0000000..a53a3df --- /dev/null +++ b/embedutils/dashboard_integration.py @@ -0,0 +1,181 @@ +from AAA3A_utils import CogsUtils # isort:skip +from redbot.core import commands # isort:skip +from redbot.core.bot import Red # isort:skip +from redbot.core.i18n import Translator # isort:skip +import discord # isort:skip +import typing # isort:skip + +import os + +from redbot.core.utils.chat_formatting import humanize_list + +from .converters import ListStringToEmbed + +_: Translator = Translator("EmbedUtils", __file__) + + +def dashboard_page(*args, **kwargs): + def decorator(func: typing.Callable): + func.__dashboard_decorator_params__ = (args, kwargs) + return func + + return decorator + + +class DashboardIntegration: + bot: Red + + @commands.Cog.listener() + async def on_dashboard_cog_add(self, dashboard_cog: commands.Cog) -> None: + dashboard_cog.rpc.third_parties_handler.add_third_party(self) + + # @dashboard_page(name=None, description="Create Embeds!") + # async def global_callback(self, **kwargs) -> None: + # return {"status": 0, "web_content": {"source": '<iframe class="..." src="{{ url_for("third_parties_blueprint.third_party", name=name, page="editor") }}" style="width: 100%; height: 1000px; border: none;"></iframe>', "fullscreen": True}} + + @dashboard_page(name=None, description="Create rich Embeds!") + async def dashboard_editor(self, **kwargs) -> None: + file_path = os.path.join(os.path.dirname(__file__), "editor.html") + with open(file_path, "rt", encoding="utf-8") as f: + source = f.read() + return {"status": 0, "web_content": {"source": source, "standalone": True}} + + @dashboard_page( + name="guild", + description="Create rich Embeds and send them to a guild!", + methods=("GET", "POST"), + ) + async def dashboard_guild(self, user: discord.User, guild: discord.Guild, **kwargs) -> None: + is_owner = user.id in self.bot.owner_ids + member = guild.get_member(user.id) + if not is_owner and not await self.bot.is_mod(member): + return { + "status": 0, + "error_code": 403, + "message": _("You don't have permissions to access this page."), + } + channels = kwargs["get_sorted_channels"](guild) + if not channels: + return { + "status": 0, + "error_code": 403, + "message": _( + "I or you don't have permissions to send messages or embeds in any channel in this guild." + ), + } + + file_path = os.path.join(os.path.dirname(__file__), "editor.html") + with open(file_path, "rt", encoding="utf-8") as f: + source = f.read() + + import wtforms + + class SendForm(kwargs["Form"]): + def __init__(self) -> None: + super().__init__(prefix="send_form_") + + username: wtforms.HiddenField = wtforms.HiddenField( + _("Username:"), + validators=[wtforms.validators.Optional(), wtforms.validators.Length(max=80)], + ) + avatar: wtforms.HiddenField = wtforms.HiddenField( + _("Avatar URL:"), + validators=[wtforms.validators.Optional(), wtforms.validators.URL()], + ) + data: wtforms.HiddenField = wtforms.HiddenField( + _("Data"), + validators=[ + wtforms.validators.DataRequired(), + kwargs["DpyObjectConverter"](ListStringToEmbed), + ], + ) + channels: wtforms.SelectMultipleField = wtforms.SelectMultipleField( + _("Channels:"), + choices=[], + validators=[ + wtforms.validators.DataRequired(), + kwargs["DpyObjectConverter"]( + typing.Union[discord.TextChannel, discord.VoiceChannel] + ), + ], + ) + submit = wtforms.SubmitField(_("Send Message(s)")) + + send_form: SendForm = SendForm() + send_form.channels.choices = channels + send_form_string = f""" + <form action="" method="POST" role="form" enctype="multipart/form-data"> + {send_form.hidden_tag()} + {send_form.channels() } + {send_form.submit(onclick='this.parentElement.querySelector("#send_form_username").value = document.querySelector(".editSenderUsername").value; this.parentElement.querySelector("#send_form_avatar").value = document.querySelector(".editSenderAvatar").value; this.parentElement.querySelector("#send_form_data").value = (JSON.stringify(typeof jsonCode === "object" ? jsonCode : json));', style="cursor: pointer; margin-left: 105px;") } + </form> + """ + + if send_form.validate_on_submit() and await send_form.validate_dpy_converters(): + notifications = [] + for channel in send_form.channels.data: + if send_form.username.data or send_form.avatar.data: + if not channel.permissions_for(guild.me).manage_webhooks: + notifications.append( + { + "message": f"{channel.name} ({channel.id}): I don't have permissions to manage webhooks in this channel.", + "category": "danger", + } + ) + continue + if not is_owner and not channel.permissions_for(member).manage_webhooks: + notifications.append( + { + "message": f"{channel.name} ({channel.id}): You don't have permissions to manage webhooks in this channel.", + "category": "danger", + } + ) + continue + try: + hook: discord.Webhook = await CogsUtils.get_hook( + bot=self.bot, channel=channel + ) + await hook.send( + **send_form.data.data, + username=send_form.username.data or guild.me.display_name, + avatar_url=send_form.avatar.data or guild.me.display_avatar, + wait=True, + ) + except discord.HTTPException as error: + notifications.append( + { + "message": f"{channel.name} ({channel.id}): {str(error)}", + "category": "danger", + } + ) + else: + try: + await channel.send(**send_form.data.data) + except Exception as e: + notifications.append( + { + "message": f"{channel.name} ({channel.id}): {str(e)}", + "category": "danger", + } + ) + s = "s" if len(send_form.channels.data) > 1 else "" + self.logger.trace( + f"{len(send_form.channels.data)} message{s} sent in {humanize_list([f'`#{channel.name}` ({channel.id})' for channel in send_form.channels.data])} in `{guild.name}` ({guild.id}), from the Dashboard by `{user.display_name}` ({user.id})." + ) + if not notifications: + notifications.append( + { + "message": _("Message{s} sent successfully!").format(s=s), + "category": "success", + } + ) + return { + "status": 0, + "notifications": notifications, + "redirect_url": kwargs["request_url"], + } + + return { + "status": 0, + "web_content": {"source": source, "standalone": True, "send_form": send_form_string}, + } diff --git a/embedutils/editor.html b/embedutils/editor.html new file mode 100644 index 0000000..9dcea2f --- /dev/null +++ b/embedutils/editor.html @@ -0,0 +1,9766 @@ +<!-- +================================================================================= +* Embeds Builder by Glitchii +================================================================================= + +* Licensed under MIT (https://github.com/Glitchii/embedbuilder/blob/main/LICENSE) +* Coded by Glitchii (https://github.com/Glitchii) + +================================================================================= + +* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + --> + +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> + <link rel="icon" type="image/png" href="{{ variables["meta"]["icon"] }}" /> + + <title> + {{ variables["meta"]["title"] }} - {{ _("Third Party") }} {{ name }}{% if guild %} - {{ guild.name }}{% endif %}{% if page %} / {{ page|replace("_", " ")|replace("-", " ")|title }}{% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% include "includes/scripts.html" %} + + + diff --git a/embedutils/embedutils.py b/embedutils/embedutils.py new file mode 100644 index 0000000..37eb637 --- /dev/null +++ b/embedutils/embedutils.py @@ -0,0 +1,1015 @@ +from AAA3A_utils import Cog, CogsUtils, Menu # isort:skip +from redbot.core import commands, app_commands, Config # isort:skip +from redbot.core.bot import Red # isort:skip +from redbot.core.i18n import Translator, cog_i18n # isort:skip +import discord # isort:skip +import typing # isort:skip + +import base64 +import json +from urllib.parse import quote + +import aiohttp +from redbot.core.utils.chat_formatting import pagify, text_to_file + +from .converters import ( + ListStringToEmbed, + MessageableConverter, + MessageableOrMessageConverter, + MyMessageConverter, + PastebinConverter, + PastebinListConverter, + StrConverter, + StringToEmbed, +) # NOQA +from .dashboard_integration import DashboardIntegration + +# Credits: +# General repo credits. +# Thanks to Phen for the original code (https://github.com/phenom4n4n/phen-cogs/tree/master/embedutils)! +# Thanks to Max for hosting an embeds creator (https://embedutils.com/)! + +_: Translator = Translator("EmbedUtils", __file__) + +JSON_CONVERTER = StringToEmbed(allow_content=False) +JSON_CONTENT_CONVERTER = StringToEmbed() +JSON_LIST_CONVERTER = ListStringToEmbed() +YAML_CONVERTER = StringToEmbed(conversion_type="yaml", allow_content=False) +YAML_CONTENT_CONVERTER = StringToEmbed(conversion_type="yaml") +YAML_LIST_CONVERTER = ListStringToEmbed(conversion_type="yaml") +PASTEBIN_CONVERTER = PastebinConverter(conversion_type="json", allow_content=False) +PASTEBIN_CONTENT_CONVERTER = PastebinConverter(conversion_type="json") +PASTEBIN_LIST_CONVERTER = PastebinListConverter(conversion_type="json") + + +@cog_i18n(_) +class EmbedUtils(DashboardIntegration, Cog): + """Create, send, and store rich embeds, from Red-Web-Dashboard too!""" + + __authors__: typing.List[str] = ["PhenoM4n4n", "AAA3A"] + + def __init__(self, bot: Red) -> None: + super().__init__(bot=bot) + + self.config: Config = Config.get_conf( + self, + identifier=205192943327321000143939875896557571750, + force_registration=True, + ) + self.config.register_global(stored_embeds={}) + self.config.register_guild(stored_embeds={}) + + self._session: aiohttp.ClientSession = None + + async def cog_load(self) -> None: + await super().cog_load() + self._session: aiohttp.ClientSession = aiohttp.ClientSession() + + async def cog_unload(self) -> None: + if self._session is not None: + await self._session.close() + await super().cog_unload() + + @commands.guild_only() + @commands.mod_or_permissions(manage_messages=True) + @commands.bot_has_permissions(embed_links=True) + @commands.hybrid_group(invoke_without_command=True, aliases=["embedutils"]) + @app_commands.allowed_installs(guilds=True, users=True) + async def embed( + self, + ctx: commands.Context, + channel_or_message: typing.Optional[MessageableOrMessageConverter], + color: typing.Optional[discord.Color], + title: str, + *, + description: str, + ) -> None: + """Post a simple embed with a color, a title and a description. + + Put the title in quotes if it contains spaces. + If you provide a message, it will be edited. + """ + color = color or await ctx.embed_color() + embed: discord.Embed = discord.Embed(color=color, title=title, description=description) + try: + if not isinstance(channel_or_message, discord.Message): + channel = ( + channel_or_message + if channel_or_message is not None + else ( + ctx.channel + if ctx.interaction is None or ctx.interaction.is_guild_integration() + else ctx + ) + ) + await channel.send(embed=embed) + else: + await channel_or_message.edit(embed=embed) + except discord.HTTPException as error: + return await StringToEmbed.embed_convert_error(ctx, _("Embed Sending Error"), error) + + @embed.command(name="json", aliases=["fromjson", "fromdata"]) + async def embed_json( + self, + ctx: commands.Context, + channel_or_message: typing.Optional[MessageableOrMessageConverter] = None, + *, + data: JSON_LIST_CONVERTER = None, + ): + """Post embeds from valid JSON. + + This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object). + Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4). + You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload. + + If you provide a message, it will be edited. + You can use an attachment and the command `[p]embed fromfile` will be invoked automatically. + """ + if data is None: + return await self.embed_fromfile(ctx, channel_or_message=channel_or_message) + try: + if not isinstance(channel_or_message, discord.Message): + channel = ( + channel_or_message + if channel_or_message is not None + else ( + ctx.channel + if ctx.interaction is None or ctx.interaction.is_guild_integration() + else ctx + ) + ) + await channel.send( + **data, + allowed_mentions=( + discord.AllowedMentions(everyone=True, users=True, roles=True) + if ctx.permissions.mention_everyone + else discord.utils.MISSING + ), + ) + else: + await channel_or_message.edit(**data) + except discord.HTTPException as error: + return await StringToEmbed.embed_convert_error(ctx, _("Embed Sending Error"), error) + + @embed.command(name="yaml", aliases=["fromyaml"]) + async def embed_yaml( + self, + ctx: commands.Context, + channel_or_message: typing.Optional[MessageableOrMessageConverter] = None, + *, + data: YAML_LIST_CONVERTER = None, + ): + """Post embeds from valid YAML. + + This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object). + Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4). + + If you provide a message, it will be edited. + You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically. + """ + if data is None: + return await self.embed_yamlfile(ctx, channel_or_message=channel_or_message) + try: + if not isinstance(channel_or_message, discord.Message): + channel = ( + channel_or_message + if channel_or_message is not None + else ( + ctx.channel + if ctx.interaction is None or ctx.interaction.is_guild_integration() + else ctx + ) + ) + await channel.send( + **data, + allowed_mentions=( + discord.AllowedMentions(everyone=True, users=True, roles=True) + if ctx.permissions.mention_everyone + else discord.utils.MISSING + ), + ) + else: + await channel_or_message.edit(**data) + except discord.HTTPException as error: + return await StringToEmbed.embed_convert_error(ctx, _("Embed Sending Error"), error) + + @embed.command(name="fromfile", aliases=["jsonfile", "fromjsonfile", "fromdatafile"]) + async def embed_fromfile( + self, ctx: commands.Context, channel_or_message: MessageableOrMessageConverter = None + ): + """Post an embed from a valid JSON file (upload it). + + This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object). + Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4). + You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload. + + If you provide a message, it will be edited. + """ + if not ctx.message.attachments or ctx.message.attachments[0].filename.split(".")[ + -1 + ] not in ("json", "txt"): + raise commands.UserInputError() + try: + argument = (await ctx.message.attachments[0].read()).decode(encoding="utf-8") + except UnicodeDecodeError: + raise commands.UserFeedbackCheckFailure(_("Unreadable attachment with `utf-8`.")) + data = await JSON_LIST_CONVERTER.convert(ctx, argument=argument) + try: + if not isinstance(channel_or_message, discord.Message): + channel = ( + channel_or_message + if channel_or_message is not None + else ( + ctx.channel + if ctx.interaction is None or ctx.interaction.is_guild_integration() + else ctx + ) + ) + await channel.send( + **data, + allowed_mentions=( + discord.AllowedMentions(everyone=True, users=True, roles=True) + if ctx.permissions.mention_everyone + else discord.utils.MISSING + ), + ) + else: + await channel_or_message.edit(**data) + except discord.HTTPException as error: + return await StringToEmbed.embed_convert_error(ctx, _("Embed Sending Error"), error) + + @embed.command(name="yamlfile", aliases=["fromyamlfile"]) + async def embed_yamlfile( + self, ctx: commands.Context, channel_or_message: MessageableOrMessageConverter = None + ): + """Post an embed from a valid YAML file (upload it). + + If you provide a message, it will be edited. + """ + if not ctx.message.attachments or ctx.message.attachments[0].filename.split(".")[ + -1 + ] not in ("yaml", "txt"): + raise commands.UserInputError() + try: + argument = (await ctx.message.attachments[0].read()).decode(encoding="utf-8") + except UnicodeDecodeError: + raise commands.UserFeedbackCheckFailure(_("Unreadable attachment with `utf-8`.")) + data = await YAML_LIST_CONVERTER.convert(ctx, argument=argument) + try: + if not isinstance(channel_or_message, discord.Message): + channel = ( + channel_or_message + if channel_or_message is not None + else ( + ctx.channel + if ctx.interaction is None or ctx.interaction.is_guild_integration() + else ctx + ) + ) + await channel.send( + **data, + allowed_mentions=( + discord.AllowedMentions(everyone=True, users=True, roles=True) + if ctx.permissions.mention_everyone + else discord.utils.MISSING + ), + ) + else: + await channel_or_message.edit(**data) + except discord.HTTPException as error: + return await StringToEmbed.embed_convert_error(ctx, _("Embed Sending Error"), error) + + @embed.command( + name="pastebin", aliases=["frompastebin", "gist", "fromgist", "hastebin", "fromhastebin"] + ) + async def embed_pastebin( + self, + ctx: commands.Context, + channel_or_message: typing.Optional[MessageableOrMessageConverter] = None, + *, + data: PASTEBIN_LIST_CONVERTER, + ): + """Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON. + + This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object). + Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4). + + If you provide a message, it will be edited. + """ + try: + if not isinstance(channel_or_message, discord.Message): + channel = ( + channel_or_message + if channel_or_message is not None + else ( + ctx.channel + if ctx.interaction is None or ctx.interaction.is_guild_integration() + else ctx + ) + ) + await channel.send( + **data, + allowed_mentions=( + discord.AllowedMentions(everyone=True, users=True, roles=True) + if ctx.permissions.mention_everyone + else discord.utils.MISSING + ), + ) + else: + await channel_or_message.edit(**data) + except discord.HTTPException as error: + return await StringToEmbed.embed_convert_error(ctx, _("Embed Sending Error"), error) + + @embed.command(name="message", aliases=["frommessage", "msg", "frommsg"]) + async def embed_message( + self, + ctx: commands.Context, + channel_or_message: typing.Optional[MessageableOrMessageConverter], + message: discord.Message = None, + index: int = None, + include_content: typing.Optional[bool] = None, + ): + """Post embed(s) from an existing message. + + The message must have at least one embed. + You can specify an index (starting by 0) if you want to send only one of the embeds. + The content of the message already sent is included if no index is specified. + + If you provide a message, it will be edited. + """ + if message is None: + if ctx.message.reference is not None and isinstance( + (reference := ctx.message.reference.resolved), discord.Message + ): + message = reference + else: + raise commands.UserInputError() + if include_content is None and message.content: + include_content = index is None + data = {} + # if not message.embeds: + # raise commands.UserInputError() + if include_content: + data["content"] = message.content + if index is None: + data["embeds"] = message.embeds.copy() + else: + try: + data["embeds"] = [message.embeds[index]] + except IndexError: + raise commands.UserInputError + try: + if not isinstance(channel_or_message, discord.Message): + channel = ( + channel_or_message + if channel_or_message is not None + else ( + ctx.channel + if ctx.interaction is None or ctx.interaction.is_guild_integration() + else ctx + ) + ) + await channel.send( + **data, + allowed_mentions=( + discord.AllowedMentions(everyone=True, users=True, roles=True) + if ctx.permissions.mention_everyone + else discord.utils.MISSING + ), + ) + else: + await channel_or_message.edit(**data) + except discord.HTTPException as error: + return await StringToEmbed.embed_convert_error(ctx, _("Embed Sending Error"), error) + + @commands.bot_has_permissions(attach_files=True) + @embed.command(name="download") + async def embed_download( + self, + ctx: commands.Context, + message: discord.Message = None, + index: int = None, + include_content: typing.Optional[bool] = None, + ): + """Download a JSON file for a message's embed(s). + + The message must have at least one embed. + You can specify an index (starting by 0) if you want to include only one of the embeds. + The content of the message already sent is included if no index is specified. + """ + if message is None: + if ctx.message.reference is not None and isinstance( + (reference := ctx.message.reference.resolved), discord.Message + ): + message = reference + else: + raise commands.UserInputError() + if include_content is None: + include_content = index is None + data = {} + # if not message.embeds: + # raise commands.UserInputError() + if include_content and message.content: + data["content"] = message.content + if index is None: + data["embeds"] = [embed.to_dict() for embed in message.embeds.copy()] + else: + try: + data["embeds"] = [message.embeds[index].to_dict()] + except IndexError: + raise commands.UserInputError + await ctx.send(file=text_to_file(text=json.dumps(data, indent=4), filename="embed.json")) + + @commands.mod_or_permissions(manage_messages=True) + @embed.command( + name="edit", usage=" [data]" + ) + async def embed_edit( + self, + ctx: commands.Context, + message: MyMessageConverter, + conversion_type: typing.Literal[ + "json", + "fromjson", + "fromdata", + "yaml", + "fromyaml", + "fromfile", + "jsonfile", + "fromjsonfile", + "fromdatafile", + "yamlfile", + "fromyamlfile", + "gist", + "pastebin", + "hastebin", + "message", + "frommessage", + "msg", + "frommsg", + ], + *, + data: str = None, + ): + """Edit a message sent by [botname]. + + It would be better to use the `message` parameter of all the other commands. + """ + if conversion_type in ("json", "fromjson", "fromdata"): + if data is None: + raise commands.UserInputError() + data = await JSON_LIST_CONVERTER.convert(ctx, argument=data) + elif conversion_type in ("yaml", "fromyaml"): + if data is None: + raise commands.UserInputError() + data = await YAML_LIST_CONVERTER.convert(ctx, argument=data) + elif conversion_type in ("fromfile", "jsonfile", "fromjsonfile", "fromdatafile"): + if not ctx.message.attachments or ctx.message.attachments[0].filename.split(".")[ + -1 + ] not in ("json", "txt"): + raise commands.UserInputError() + try: + argument = (await ctx.message.attachments[0].read()).decode(encoding="utf-8") + except UnicodeDecodeError: + raise commands.UserFeedbackCheckFailure(_("Unreadable attachment with `utf-8`.")) + data = await JSON_LIST_CONVERTER.convert(ctx, argument=argument) + elif conversion_type in ("yamlfile", "fromyamlfile"): + if not ctx.message.attachments or ctx.message.attachments[0].filename.split(".")[ + -1 + ] not in ("yaml", "txt"): + raise commands.UserInputError() + try: + argument = (await ctx.message.attachments[0].read()).decode(encoding="utf-8") + except UnicodeDecodeError: + raise commands.UserFeedbackCheckFailure(_("Unreadable attachment with `utf-8`.")) + data = await YAML_LIST_CONVERTER.convert(ctx, argument=argument) + elif conversion_type in ("gist", "pastebin", "hastebin"): + if data is None: + raise commands.UserInputError() + data = await PASTEBIN_LIST_CONVERTER.convert(ctx, argument=data) + elif conversion_type in ("message", "frommessage", "msg", "frommsg"): + if data is not None: + message = await commands.MessageConverter().convert(ctx, argument=data) + elif ctx.message.reference is not None and isinstance( + (reference := ctx.message.reference.resolved), discord.Message + ): + message = reference + else: + raise commands.UserInputError() + data = {} + if message.content: + data["content"] = message.content + if message.embeds: + data["embeds"] = message.embeds.copy() + if not data: + raise commands.UserInputError() + try: + await message.edit(**data) + except discord.HTTPException as error: + return await StringToEmbed.embed_convert_error(ctx, _("Embed Sending Error"), error) + + @commands.mod_or_permissions(manage_guild=True) + @embed.command( + name="store", + aliases=["storeembed"], + usage="[global_level=False] [locked=False] [data]", + ) + async def embed_store( + self, + ctx: commands.Context, + global_level: typing.Optional[bool], + locked: typing.Optional[bool], + name: str, + conversion_type: typing.Literal[ + "json", + "fromjson", + "fromdata", + "yaml", + "fromyaml", + "fromfile", + "jsonfile", + "fromjsonfile", + "fromdatafile", + "yamlfile", + "fromyamlfile", + "gist", + "pastebin", + "hastebin", + "message", + "frommessage", + "msg", + "frommsg", + ], + *, + data: str = None, + ): + """Store an embed. + + Put the name in quotes if it is multiple words. + The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level). + """ + if global_level is None: + global_level = False + elif global_level and ctx.author.id not in ctx.bot.owner_ids: + raise commands.UserFeedbackCheckFailure(_("You can't manage global stored embeds.")) + if locked is None: + locked = False + + if conversion_type in ("json", "fromjson", "fromdata"): + if data is None: + raise commands.UserInputError() + data = await JSON_CONVERTER.convert(ctx, argument=data) + elif conversion_type in ("yaml", "fromyaml"): + if data is None: + raise commands.UserInputError() + data = await YAML_CONVERTER.convert(ctx, argument=data) + elif conversion_type in ("fromfile", "jsonfile", "fromjsonfile", "fromdatafile"): + if not ctx.message.attachments or ctx.message.attachments[0].filename.split(".")[ + -1 + ] not in ("json", "txt"): + raise commands.UserInputError() + try: + argument = (await ctx.message.attachments[0].read()).decode(encoding="utf-8") + except UnicodeDecodeError: + raise commands.UserFeedbackCheckFailure(_("Unreadable attachment with `utf-8`.")) + data = await JSON_CONVERTER.convert(ctx, argument=argument) + elif conversion_type in ("yamlfile", "fromyamlfile"): + if not ctx.message.attachments or ctx.message.attachments[0].filename.split(".")[ + -1 + ] not in ("yaml", "txt"): + raise commands.UserInputError() + try: + argument = (await ctx.message.attachments[0].read()).decode(encoding="utf-8") + except UnicodeDecodeError: + raise commands.UserFeedbackCheckFailure(_("Unreadable attachment with `utf-8`.")) + data = await YAML_CONVERTER.convert(ctx, argument=argument) + elif conversion_type in ("gist", "pastebin", "hastebin"): + if data is None: + raise commands.UserInputError() + data = await PASTEBIN_CONVERTER.convert(ctx, argument=data) + elif conversion_type in ("message", "frommessage", "msg", "frommsg"): + if data is not None: + message = await commands.MessageConverter().convert(ctx, argument=data) + elif ctx.message.reference is not None and isinstance( + (reference := ctx.message.reference.resolved), discord.Message + ): + message = reference + else: + raise commands.UserInputError() + if not message.embeds: + raise commands.UserInputError() + data = {"embed": message.embeds[0]} + embed = data["embed"] + try: + await ctx.channel.send(embed=embed) + except discord.HTTPException as error: + return await StringToEmbed.embed_convert_error(ctx, _("Embed Sending Error"), error) + + async with ( + self.config if global_level else self.config.guild(ctx.guild) + ).stored_embeds() as stored_embeds: + total_embeds = set(stored_embeds) + total_embeds.add(name) + # If the user provides a name that's already used as an embed, it won't increment the embed count, which is why total embeds is converted to a set to calculate length to prevent duplicate names. + embed_limit = 100 + if not global_level and len(total_embeds) > embed_limit: + raise commands.UserFeedbackCheckFailure( + _( + "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." + ).format(embed_limit=embed_limit, ctx=ctx) + ) + stored_embeds[name] = { + "author": ctx.author.id, + "embed": embed.to_dict(), + "locked": locked, + "uses": 0, + } + + @commands.mod_or_permissions(manage_guild=True) + @embed.command(name="unstore", aliases=["unstoreembed"], usage="[global_level=False] ") + async def embed_unstore( + self, + ctx: commands.Context, + global_level: typing.Optional[bool], + name: str, + ): + """Remove a stored embed.""" + if global_level is None: + global_level = False + elif global_level and ctx.author.id not in ctx.bot.owner_ids: + raise commands.UserFeedbackCheckFailure(_("You can't manage global stored embeds.")) + async with ( + self.config if global_level else self.config.guild(ctx.guild) + ).stored_embeds() as stored_embeds: + if name not in stored_embeds: + raise commands.UserFeedbackCheckFailure( + _("This is not a stored embed at this level.") + ) + del stored_embeds[name] + + @commands.mod_or_permissions(manage_guild=True) + @embed.command(name="list", aliases=["liststored", "liststoredembeds"]) + async def embed_list(self, ctx: commands.Context, global_level: bool = False): + """Get info about a stored embed.""" + if global_level and ctx.author.id not in ctx.bot.owner_ids: + raise commands.UserFeedbackCheckFailure(_("You can't manage global stored embeds.")) + stored_embeds = await ( + self.config if global_level else self.config.guild(ctx.guild) + ).stored_embeds() + if not stored_embeds: + raise commands.UserFeedbackCheckFailure( + _("No stored embeds is configured at this level.") + ) + description = "\n".join(f"- `{name}`" for name in stored_embeds) + embed: discord.Embed = discord.Embed( + title=(_("Global ") if global_level else "") + _("Stored Embeds"), + color=await ctx.embed_color(), + ) + embed.set_author(name=ctx.me.display_name, icon_url=ctx.me.display_avatar) + embeds = [] + for page in pagify(description): + e = embed.copy() + e.description = page + embeds.append(e) + await Menu(pages=embeds).start(ctx) + + @commands.mod_or_permissions(manage_guild=True) + @embed.command( + name="info", aliases=["infostored", "infostoredembed"], usage="[global_level=False] " + ) + async def embed_info( + self, ctx: commands.Context, global_level: typing.Optional[bool], name: str + ): + """Get info about a stored embed.""" + if global_level is None: + global_level = False + elif global_level and ctx.author.id not in ctx.bot.owner_ids: + raise commands.UserFeedbackCheckFailure(_("You can't manage global stored embeds.")) + stored_embeds = await ( + self.config if global_level else self.config.guild(ctx.guild) + ).stored_embeds() + if name not in stored_embeds: + raise commands.UserFeedbackCheckFailure(_("This is not a stored embed at this level.")) + stored_embed = stored_embeds[name] + description = [ + f"• **Author:** <@{stored_embed['author']}> ({stored_embed['author']})", + f"• **Uses:** {stored_embed['uses']}", + f"• **Length:** {len(stored_embed['embed'])}", + f"• **Locked:** {stored_embed['locked']}", + ] + embed: discord.Embed = discord.Embed( + title=f"Info about `{name}`", + description="\n".join(description), + color=await ctx.embed_color(), + ) + embed.set_author(name=ctx.me.display_name, icon_url=ctx.me.display_avatar) + await ctx.send(embed=embed, allowed_mentions=discord.AllowedMentions(users=False)) + + @commands.mod_or_permissions(manage_guild=True) + @embed.command( + name="downloadstored", aliases=["downloadstoredembed"], usage="[global_level=False] " + ) + async def embed_download_stored( + self, ctx: commands.Context, global_level: typing.Optional[bool], name: str + ): + """Download a JSON file for a stored embed.""" + if global_level is None: + global_level = False + elif global_level and ctx.author.id not in ctx.bot.owner_ids: + raise commands.UserFeedbackCheckFailure(_("You can't manage global stored embeds.")) + stored_embeds = await ( + self.config if global_level else self.config.guild(ctx.guild) + ).stored_embeds() + if name not in stored_embeds: + raise commands.UserFeedbackCheckFailure(_("This is not a stored embed at this level.")) + stored_embed = stored_embeds[name] + await ctx.send( + file=text_to_file( + text=json.dumps({"embed": stored_embed["embed"]}, indent=4), filename="embed.json" + ) + ) + + @embed.command( + name="poststored", + aliases=["poststoredembed", "post"], + usage="[channel_or_message=] [global_level=False] ", + ) + async def embed_post_stored( + self, + ctx: commands.Context, + channel_or_message: typing.Optional[MessageableOrMessageConverter], + global_level: typing.Optional[bool], + names: commands.Greedy[StrConverter], + ): + """Post stored embeds.""" + if global_level is None: + global_level = False + embeds = [] + async with ( + self.config if global_level else self.config.guild(ctx.guild) + ).stored_embeds() as stored_embeds: + for name in names: + if ( + name not in stored_embeds + or ( + global_level + and stored_embeds[name]["locked"] + and ctx.author.id not in ctx.bot.owner_ids + ) + or ( + not global_level + and stored_embeds[name]["locked"] + and await ctx.bot.is_mod(ctx.author) + ) + ): + raise commands.UserFeedbackCheckFailure( + _("`{name}` is not a stored embed at this level.").format(name=name) + ) + embeds.append(discord.Embed.from_dict(stored_embeds[name]["embed"])) + stored_embeds[name]["uses"] += 1 + try: + if not isinstance(channel_or_message, discord.Message): + channel = ( + channel_or_message + if channel_or_message is not None + else ( + ctx.channel + if ctx.interaction is None or ctx.interaction.is_guild_integration() + else ctx + ) + ) + await channel.send(embeds=embeds) + else: + await channel_or_message.edit(embeds=embeds) + except discord.HTTPException as error: + return await StringToEmbed.embed_convert_error(ctx, _("Embed Sending Error"), error) + + @commands.mod_or_permissions(manage_webhooks=True) + @commands.bot_has_permissions(manage_webhooks=True) + @embed.command( + name="postwebhook", + aliases=["webhook"], + usage="[channel_or_message=] [global_level=False] ", + ) + async def embed_post_webhook( + self, + ctx: commands.Context, + channel: typing.Optional[MessageableConverter], + username: commands.Range[str, 1, 80], + avatar_url: str, + global_level: typing.Optional[bool], + names: commands.Greedy[StrConverter], + ): + """Post stored embeds with a webhook.""" + if global_level is None: + global_level = False + embeds = [] + async with ( + self.config if global_level else self.config.guild(ctx.guild) + ).stored_embeds() as stored_embeds: + for name in names: + if ( + name not in stored_embeds + or ( + global_level + and stored_embeds[name]["locked"] + and ctx.author.id not in ctx.bot.owner_ids + ) + or ( + not global_level + and stored_embeds[name]["locked"] + and await ctx.bot.is_mod(ctx.author) + ) + ): + raise commands.UserFeedbackCheckFailure( + _("`{name}` is not a stored embed at this level.").format(name=name) + ) + embeds.append(discord.Embed.from_dict(stored_embeds[name]["embed"])) + stored_embeds[name]["uses"] += 1 + try: + channel = channel or ctx.channel + hook: discord.Webhook = await CogsUtils.get_hook(bot=self.bot, channel=channel) + await hook.send( + embeds=embeds, + username=username, + avatar_url=avatar_url, + wait=True, + ) + except discord.HTTPException as error: + return await StringToEmbed.embed_convert_error(ctx, _("Embed Sending Error"), error) + + @embed.command() + async def dashboard( + self, + ctx: commands.Context, + conversion_type: typing.Optional[ + typing.Literal[ + "json", + "fromjson", + "fromdata", + "yaml", + "fromyaml", + "fromfile", + "jsonfile", + "fromjsonfile", + "fromdatafile", + "yamlfile", + "fromyamlfile", + "gist", + "pastebin", + "hastebin", + "message", + "frommessage", + "msg", + "frommsg", + ] + ] = None, + *, + data: str = None, + ) -> None: + """Get the link to the Dashboard.""" + if (dashboard_url := getattr(ctx.bot, "dashboard_url", None)) is None: + raise commands.UserFeedbackCheckFailure( + _( + "Red-Web-Dashboard is not installed. Check ." + ) + ) + if not dashboard_url[1] and ctx.author.id not in ctx.bot.owner_ids: + raise commands.UserFeedbackCheckFailure(_("You can't access the Dashboard.")) + if ( + self.qualified_name + in await self.bot.get_cog("Dashboard").config.webserver.disabled_third_parties() + ): + raise commands.UserFeedbackCheckFailure( + _("This third party is disabled on the Dashboard.") + ) + url = ( + f"{dashboard_url[0]}/dashboard/{ctx.guild.id}/third-party/{self.qualified_name}/guild" + ) + + if conversion_type is not None: + if conversion_type in ("json", "fromjson", "fromdata"): + if data is None: + raise commands.UserInputError() + data = await JSON_LIST_CONVERTER.convert(ctx, argument=data) + elif conversion_type in ("yaml", "fromyaml"): + if data is None: + raise commands.UserInputError() + data = await YAML_LIST_CONVERTER.convert(ctx, argument=data) + elif conversion_type in ("fromfile", "jsonfile", "fromjsonfile", "fromdatafile"): + if not ctx.message.attachments or ctx.message.attachments[0].filename.split(".")[ + -1 + ] not in ("json", "txt"): + raise commands.UserInputError() + try: + argument = (await ctx.message.attachments[0].read()).decode(encoding="utf-8") + except UnicodeDecodeError: + raise commands.UserFeedbackCheckFailure( + _("Unreadable attachment with `utf-8`.") + ) + data = await JSON_LIST_CONVERTER.convert(ctx, argument=argument) + elif conversion_type in ("yamlfile", "fromyamlfile"): + if not ctx.message.attachments or ctx.message.attachments[0].filename.split(".")[ + -1 + ] not in ("yaml", "txt"): + raise commands.UserInputError() + try: + argument = (await ctx.message.attachments[0].read()).decode(encoding="utf-8") + except UnicodeDecodeError: + raise commands.UserFeedbackCheckFailure( + _("Unreadable attachment with `utf-8`.") + ) + data = await YAML_LIST_CONVERTER.convert(ctx, argument=argument) + elif conversion_type in ("gist", "pastebin", "hastebin"): + if data is None: + raise commands.UserInputError() + data = await PASTEBIN_LIST_CONVERTER.convert(ctx, argument=data) + elif conversion_type in ("message", "frommessage", "msg", "frommsg"): + if data is not None: + message = await commands.MessageConverter().convert(ctx, argument=data) + elif ctx.message.reference is not None and isinstance( + (reference := ctx.message.reference.resolved), discord.Message + ): + message = reference + else: + raise commands.UserInputError() + data = {} + if message.content: + data["content"] = message.content + if message.embeds: + data["embeds"] = message.embeds.copy() + if not data: + raise commands.UserInputError() + + if data["embeds"]: + data["embeds"] = [embed.to_dict() for embed in data["embeds"]] + url += f"?data={base64.b64encode(quote(json.dumps(data)).encode()).decode()}" + + embed: discord.Embed = discord.Embed( + title=_("Dashboard - ") + self.qualified_name, + color=await ctx.embed_color(), + ) + file = None + if len(url) <= 2048: + embed.description = _( + "You can create and send rich embeds directly from the Dashboard!" + ) + embed.url = url + elif len(url) <= 4096 - 15: + embed.description = f"[Click here!]({url})" + else: + embed.description = _("The URL is too long to be displayed.") + file = text_to_file(text=url, filename="dashboard_url.txt") + await ctx.send( + embed=embed, + file=file, + ) + + @commands.is_owner() + @embed.command(aliases=["migratefromembedutils"]) + async def migratefromphen(self, ctx: commands.Context) -> None: + """Migrate stored embeds from EmbedUtils by Phen.""" + old_config: Config = Config.get_conf( + "EmbedUtils", + identifier=43248937299564234735284, + force_registration=True, + cog_name="EmbedUtils", + ) + old_global_data = await old_config.all() + new_global_group = self.config._get_base_group(self.config.GLOBAL) + async with new_global_group.all() as new_global_data: + if "embeds" in old_global_data: + if "stored_embeds" not in new_global_data: + new_global_data["stored_embeds"] = {} + _stored_embeds = new_global_data["stored_embeds"] + new_global_data["stored_embeds"] = { + name: { + "author": data["author"], + "embed": data["embed"], + "locked": data.get("locked", False), + "uses": data["uses"], + } + for name, data in old_global_data["embeds"].items() + } + new_global_data["stored_embeds"].update(**_stored_embeds) + new_guild_group = self.config._get_base_group(self.config.GUILD) + old_guilds_data = await old_config.all_guilds() + async with new_guild_group.all() as new_guilds_data: + for guild_id in old_guilds_data: + if "embeds" in old_guilds_data[guild_id]: + if str(guild_id) not in new_guilds_data: + new_guilds_data[str(guild_id)] = {} + if "stored_embeds" not in new_guilds_data[str(guild_id)]: + new_guilds_data[str(guild_id)]["stored_embeds"] = {} + _stored_embeds = new_guilds_data[str(guild_id)]["stored_embeds"] + new_guilds_data[str(guild_id)]["stored_embeds"] = { + name: { + "author": data["author"], + "embed": data["embed"], + "locked": data.get("locked", False), + "uses": data["uses"], + } + for name, data in old_guilds_data[guild_id]["embeds"].items() + } + new_guilds_data[str(guild_id)]["stored_embeds"].update(**_stored_embeds) + await ctx.send(_("Data successfully migrated from EmbedUtils by Phen.")) diff --git a/embedutils/info.json b/embedutils/info.json new file mode 100644 index 0000000..c8d25a1 --- /dev/null +++ b/embedutils/info.json @@ -0,0 +1,20 @@ +{ + "author": ["PhenoM4n4n", "AAA3A"], + "name": "EmbedUtils", + "install_msg": "Thank you for installing this cog!\nDo `[p]help CogName` to get the list of commands and their description. If you enjoy my work, please consider donating on [Buy Me a Coffee]() or [Ko-Fi]()!\nIf you previously used Phen's cog, please run `[p]embed migratefromphen`.", + "short": "Create, send, and store rich embeds, from Red-Web-Dashboard too!", + "description": "Create, send, and store rich embeds, from Red-Web-Dashboard too!", + "tags": [ + "embed", + "creator", + "content", + "advanced", + "json", + "yaml", + "rich", + "dashboard" + ], + "requirements": ["git+https://github.com/AAA3A-AAA3A/AAA3A_utils.git"], + "min_bot_version": "3.5.0", + "end_user_data_statement": "This cog does not persistently store data or metadata about users." +} \ No newline at end of file diff --git a/embedutils/locales/de-DE.po b/embedutils/locales/de-DE.po new file mode 100644 index 0000000..0971074 --- /dev/null +++ b/embedutils/locales/de-DE.po @@ -0,0 +1,339 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: German\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: de\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/embedutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 314\n" +"Language: de_DE\n" + +#: embedutils\converters.py:66 embedutils\converters.py:81 +msgid "This doesn't seem to be properly formatted embed {conversion_type}. Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "Dies scheint nicht richtig formatiert zu sein, um {conversion_type}einzubetten. Siehe den Link auf `{ctx.clean_prefix}Hilfe {ctx.command.qualified_name}`." + +#: embedutils\converters.py:93 +msgid "JSON Parse Error" +msgstr "JSON-Parsing-Fehler" + +#: embedutils\converters.py:104 +msgid "YAML Parse Error" +msgstr "YAML-Parsing-Fehler" + +#: embedutils\converters.py:114 +msgid "The `content` field is not supported for this command." +msgstr "Das Feld `Inhalt` wird für diesen Befehl nicht unterstützt." + +#: embedutils\converters.py:130 +msgid "Embed Parse Error" +msgstr "Parse-Fehler einbetten" + +#: embedutils\converters.py:135 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "Die Größe der Einbettung überschreitet das Discord-Limit von 6000 Zeichen ({length})." + +#: embedutils\converters.py:147 embedutils\embedutils.py:100 +#: embedutils\embedutils.py:133 embedutils\embedutils.py:165 +#: embedutils\embedutils.py:200 embedutils\embedutils.py:231 +#: embedutils\embedutils.py:262 embedutils\embedutils.py:314 +#: embedutils\embedutils.py:440 embedutils\embedutils.py:537 +#: embedutils\embedutils.py:708 embedutils\embedutils.py:762 +msgid "Embed Send Error" +msgstr "Sendefehler einbetten" + +#: embedutils\converters.py:162 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "Verwenden Sie `{ctx.prefix}help {ctx.command.qualified_name}`, um ein Beispiel zu sehen." + +#: embedutils\converters.py:204 +msgid "Embed limit reached ({limit})." +msgstr "Einbettungsgrenze erreicht ({limit})." + +#: embedutils\converters.py:209 +msgid "Failed to convert input into embeds." +msgstr "Konnte die Eingabe nicht in Einbettungen umwandeln." + +#: embedutils\converters.py:228 +msgid "It's not a valid channel or thread." +msgstr "Es handelt sich nicht um einen gültigen Kanal oder Thread." + +#: embedutils\converters.py:232 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "Ich habe keine Berechtigung zum Senden von Einbettungen in {channel.mention}." + +#: embedutils\converters.py:241 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "Sie haben nicht die Berechtigung, Einbettungen in {channel.mention}zu senden." + +#: embedutils\converters.py:253 +msgid "I have to be the author of the message. You can use the command without providing a message to send one." +msgstr "Ich muss der Verfasser der Nachricht sein. Sie können den Befehl auch ohne eine Nachricht verwenden, um eine zu senden." + +#: embedutils\converters.py:263 +msgid "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "Es ist nicht erlaubt, Einbettungen einer bestehenden Nachricht zu bearbeiten (der Bot-Besitzer kann die Berechtigungen mit dem Menüpunkt Berechtigungen auf dem Befehl `[p]embed edit` einstellen)." + +#: embedutils\dashboard_integration.py:53 +msgid "You don't have permissions to access this page." +msgstr "Sie haben keine Berechtigung für den Zugriff auf diese Seite." + +#: embedutils\dashboard_integration.py:60 +msgid "I or you don't have permissions to send messages or embeds in any channel in this guild." +msgstr "Ich oder du haben keine Berechtigung, Nachrichten oder Einbettungen in irgendeinem Kanal dieser Gilde zu senden." + +#: embedutils\dashboard_integration.py:76 +msgid "Username:" +msgstr "Benutzername:" + +#: embedutils\dashboard_integration.py:80 +msgid "Avatar URL:" +msgstr "Avatar-URL:" + +#: embedutils\dashboard_integration.py:84 +msgid "Data" +msgstr "Daten" + +#: embedutils\dashboard_integration.py:91 +msgid "Channels:" +msgstr "Kanäle:" + +#: embedutils\dashboard_integration.py:100 +msgid "Send Message(s)" +msgstr "Nachricht(en) senden" + +#: embedutils\dashboard_integration.py:164 +msgid "Message(s) sent successfully!" +msgstr "Nachricht(en) erfolgreich gesendet!" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "Erstellen, senden und speichern Sie umfangreiche Einbettungen, auch von Red-Web-Dashboard aus!" + +#: embedutils\embedutils.py:110 +#, docstring +msgid "Post embeds from valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Posten Sie Einbettungen aus gültigem JSON.\n\n" +" Dies muss in dem Format sein, das von [**dieser Discord-Dokumentation**] (https://discord.com/developers/docs/resources/channel#embed-object) erwartet wird.\n" +" Hier ist ein Beispiel: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Sie können einen [**embeds creator**](https://embedutils.com/) verwenden, um eine JSON-Nutzlast zu erhalten.\n\n" +" Wenn Sie eine Nachricht angeben, wird diese bearbeitet.\n" +" Sie können einen Anhang verwenden und der Befehl `[p]embed yamlfile` wird automatisch aufgerufen.\n" +" " + +#: embedutils\embedutils.py:143 +#, docstring +msgid "Post embeds from valid YAML.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Post-Embeds aus gültigem YAML.\n\n" +" Dies muss in dem Format sein, das von [**dieser Discord-Dokumentation**] (https://discord.com/developers/docs/resources/channel#embed-object) erwartet wird.\n" +" Hier ist ein Beispiel: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Wenn Sie eine Nachricht angeben, wird diese bearbeitet.\n" +" Sie können einen Anhang verwenden und der Befehl `[p]embed yamlfile` wird automatisch aufgerufen.\n" +" " + +#: embedutils\embedutils.py:171 +#, docstring +msgid "Post an embed from a valid JSON file (upload it).\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Poste eine Einbettung aus einer gültigen JSON-Datei (lade sie hoch).\n\n" +" Diese muss das von [**dieser Discord-Dokumentation**] erwartete Format haben (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Hier ist ein Beispiel: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Sie können einen [**embeds creator**](https://embedutils.com/) verwenden, um eine JSON-Nutzlast zu erhalten.\n\n" +" Wenn Sie eine Nachricht angeben, wird diese bearbeitet.\n" +" " + +#: embedutils\embedutils.py:186 embedutils\embedutils.py:217 +#: embedutils\embedutils.py:405 embedutils\embedutils.py:415 +#: embedutils\embedutils.py:505 embedutils\embedutils.py:515 +#: embedutils\embedutils.py:831 embedutils\embedutils.py:843 +msgid "Unreadable attachment with `utf-8`." +msgstr "Unleserlicher Anhang mit `utf-8`." + +#: embedutils\embedutils.py:206 +#, docstring +msgid "Post an embed from a valid YAML file (upload it).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Posten Sie eine Einbettung aus einer gültigen YAML-Datei (laden Sie sie hoch).\n\n" +" Wenn Sie eine Nachricht angeben, wird diese bearbeitet.\n" +" " + +#: embedutils\embedutils.py:243 +#, docstring +msgid "Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Posten Sie Einbettungen von einem GitHub/Gist/Pastebin/Hastebin-Link mit gültigem JSON.\n\n" +" Dies muss in dem Format sein, das von [**dieser Discord-Dokumentation**] (https://discord.com/developers/docs/resources/channel#embed-object) erwartet wird.\n" +" Hier ist ein Beispiel: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Wenn Sie eine Nachricht angeben, wird diese bearbeitet.\n" +" " + +#: embedutils\embedutils.py:273 +#, docstring +msgid "Post embed(s) from an existing message.\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Einbettung(en) aus einer vorhandenen Nachricht posten.\n\n" +" Die Nachricht muss mindestens eine Einbettung enthalten.\n" +" Sie können einen Index angeben (beginnend mit 0), wenn Sie nur eine der Einbettungen senden möchten.\n" +" Wenn kein Index angegeben wird, wird der Inhalt der bereits gesendeten Nachricht übernommen.\n\n" +" Wenn Sie eine Nachricht angeben, wird diese bearbeitet.\n" +" " + +#: embedutils\embedutils.py:325 +#, docstring +msgid "Download a JSON file for a message's embed(s).\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "Laden Sie eine JSON-Datei für die Einbettung(en) einer Nachricht herunter.\n\n" +" Die Nachricht muss mindestens eine Einbettung enthalten.\n" +" Sie können einen Index angeben (beginnend mit 0), wenn Sie nur eine der Einbettungen einschließen möchten.\n" +" Der Inhalt der bereits gesendeten Nachricht wird einbezogen, wenn kein Index angegeben wird.\n" +" " + +#: embedutils\embedutils.py:385 +#, docstring +msgid "Edit a message sent by [botname].\n\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "Eine von [botname] gesendete Nachricht bearbeiten.\n\n" +" Es wäre besser, den Parameter `Meldung` aller anderen Befehle zu verwenden.\n" +" " + +#: embedutils\embedutils.py:477 +#, docstring +msgid "Store an embed.\n\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "Speichern Sie eine Einbettung.\n\n" +" Setze den Namen in Anführungszeichen, wenn er aus mehreren Wörtern besteht.\n" +" Das Argument `locked` gibt an, ob die Einbettung nur für Mods und Vorgesetzte (Gildenebene) oder nur für Bot-Besitzer (globale Ebene) gesperrt werden soll.\n" +" " + +#: embedutils\embedutils.py:485 embedutils\embedutils.py:571 +#: embedutils\embedutils.py:586 embedutils\embedutils.py:618 +#: embedutils\embedutils.py:650 +msgid "You can't manage global stored embeds." +msgstr "Sie können keine global gespeicherten Einbettungen verwalten." + +#: embedutils\embedutils.py:548 +msgid "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "Dieser Server hat die Einbettungsgrenze von {embed_limit}erreicht. Sie müssen eine Einbettung mit \"{ctx.clean_prefix}embed unstore\" entfernen, bevor Sie eine neue Einbettung hinzufügen können." + +#: embedutils\embedutils.py:567 +#, docstring +msgid "Remove a stored embed." +msgstr "Eine gespeicherte Einbettung entfernen." + +#: embedutils\embedutils.py:577 embedutils\embedutils.py:623 +#: embedutils\embedutils.py:655 +msgid "This is not a stored embed at this level." +msgstr "Dies ist keine gespeicherte Einbettung auf dieser Ebene." + +#: embedutils\embedutils.py:584 embedutils\embedutils.py:614 +#, docstring +msgid "Get info about a stored embed." +msgstr "Informationen über eine gespeicherte Einbettung abrufen." + +#: embedutils\embedutils.py:592 +msgid "No stored embeds is configured at this level." +msgstr "Auf dieser Ebene sind keine gespeicherten Einbettungen konfiguriert." + +#: embedutils\embedutils.py:596 +msgid "Global " +msgstr "Global " + +#: embedutils\embedutils.py:596 +msgid "Stored Embeds" +msgstr "Gespeicherte Einbettungen" + +#: embedutils\embedutils.py:646 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "Laden Sie eine JSON-Datei für eine gespeicherte Einbettung herunter." + +#: embedutils\embedutils.py:675 +#, docstring +msgid "Post stored embeds." +msgstr "Gespeicherte Einbettungen buchen." + +#: embedutils\embedutils.py:697 embedutils\embedutils.py:748 +msgid "`{name}` is not a stored embed at this level." +msgstr "{name}\" ist keine gespeicherte Einbettung auf dieser Ebene." + +#: embedutils\embedutils.py:726 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "Posten Sie gespeicherte Einbettungen mit einem Webhook." + +#: embedutils\embedutils.py:793 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "Holen Sie sich den Link zum Dashboard." + +#: embedutils\embedutils.py:796 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard ist nicht installiert. Prüfen Sie ." + +#: embedutils\embedutils.py:801 +msgid "You can't access the Dashboard." +msgstr "Sie können nicht auf das Dashboard zugreifen." + +#: embedutils\embedutils.py:807 +msgid "This third party is disabled on the Dashboard." +msgstr "Diese dritte Partei ist auf dem Dashboard deaktiviert." + +#: embedutils\embedutils.py:872 +msgid "Dashboard - " +msgstr "Armaturenbrett - " + +#: embedutils\embedutils.py:876 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "Sie können Rich Embeds direkt vom Dashboard aus erstellen und versenden!" + +#: embedutils\embedutils.py:883 +msgid "The URL is too long to be displayed." +msgstr "Die URL ist zu lang, um angezeigt zu werden." + +#: embedutils\embedutils.py:889 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "Migrieren Sie gespeicherte Einbettungen aus EmbedUtils von Phen." + +#: embedutils\embedutils.py:933 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "Daten erfolgreich aus EmbedUtils von Phen migriert." + diff --git a/embedutils/locales/el-GR.po b/embedutils/locales/el-GR.po new file mode 100644 index 0000000..aa22fd3 --- /dev/null +++ b/embedutils/locales/el-GR.po @@ -0,0 +1,339 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Greek\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: el\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/embedutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 314\n" +"Language: el_GR\n" + +#: embedutils\converters.py:66 embedutils\converters.py:81 +msgid "This doesn't seem to be properly formatted embed {conversion_type}. Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "Αυτό δεν φαίνεται να είναι σωστά διαμορφωμένο embed {conversion_type}. Ανατρέξτε στον σύνδεσμο στο `{ctx.clean_prefix}βοήθεια {ctx.command.qualified_name}`." + +#: embedutils\converters.py:93 +msgid "JSON Parse Error" +msgstr "Σφάλμα ανάλυσης JSON" + +#: embedutils\converters.py:104 +msgid "YAML Parse Error" +msgstr "Σφάλμα ανάλυσης YAML" + +#: embedutils\converters.py:114 +msgid "The `content` field is not supported for this command." +msgstr "Το πεδίο \"περιεχόμενο\" δεν υποστηρίζεται για αυτή την εντολή." + +#: embedutils\converters.py:130 +msgid "Embed Parse Error" +msgstr "Ενσωμάτωση σφάλματος ανάλυσης" + +#: embedutils\converters.py:135 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "Το μέγεθος της ενσωμάτωσης υπερβαίνει το όριο των 6000 χαρακτήρων του Discord ({length})." + +#: embedutils\converters.py:147 embedutils\embedutils.py:100 +#: embedutils\embedutils.py:133 embedutils\embedutils.py:165 +#: embedutils\embedutils.py:200 embedutils\embedutils.py:231 +#: embedutils\embedutils.py:262 embedutils\embedutils.py:314 +#: embedutils\embedutils.py:440 embedutils\embedutils.py:537 +#: embedutils\embedutils.py:708 embedutils\embedutils.py:762 +msgid "Embed Send Error" +msgstr "Ενσωμάτωση Σφάλμα αποστολής" + +#: embedutils\converters.py:162 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "Χρησιμοποιήστε `{ctx.prefix}help {ctx.command.qualified_name}` για να δείτε ένα παράδειγμα." + +#: embedutils\converters.py:204 +msgid "Embed limit reached ({limit})." +msgstr "Επίτευξη ορίου ενσωμάτωσης ({limit})." + +#: embedutils\converters.py:209 +msgid "Failed to convert input into embeds." +msgstr "Απέτυχε να μετατρέψει την είσοδο σε embeds." + +#: embedutils\converters.py:228 +msgid "It's not a valid channel or thread." +msgstr "Δεν είναι έγκυρο κανάλι ή νήμα." + +#: embedutils\converters.py:232 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "Δεν έχω δικαιώματα για να στείλω ενσωματώσεις στο {channel.mention}." + +#: embedutils\converters.py:241 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "Δεν έχετε δικαιώματα για να στείλετε ενσωματώσεις στο {channel.mention}." + +#: embedutils\converters.py:253 +msgid "I have to be the author of the message. You can use the command without providing a message to send one." +msgstr "Εγώ πρέπει να είμαι ο συντάκτης του μηνύματος. Μπορείτε να χρησιμοποιήσετε την εντολή χωρίς να δώσετε ένα μήνυμα για να στείλετε ένα μήνυμα." + +#: embedutils\converters.py:263 +msgid "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "Δεν επιτρέπεται να επεξεργαστείτε τα embeds ενός υπάρχοντος μηνύματος (ο ιδιοκτήτης του bot μπορεί να ορίσει τα δικαιώματα με το γρανάζι Permissions στην εντολή `[p]embed edit`)." + +#: embedutils\dashboard_integration.py:53 +msgid "You don't have permissions to access this page." +msgstr "Δεν έχετε δικαιώματα πρόσβασης σε αυτή τη σελίδα." + +#: embedutils\dashboard_integration.py:60 +msgid "I or you don't have permissions to send messages or embeds in any channel in this guild." +msgstr "Εγώ ή εσείς δεν έχετε δικαιώματα να στέλνετε μηνύματα ή ενσωματώσεις σε κανένα κανάλι αυτής της συντεχνίας." + +#: embedutils\dashboard_integration.py:76 +msgid "Username:" +msgstr "Όνομα χρήστη:" + +#: embedutils\dashboard_integration.py:80 +msgid "Avatar URL:" +msgstr "URL του άβαταρ:" + +#: embedutils\dashboard_integration.py:84 +msgid "Data" +msgstr "Δεδομένα" + +#: embedutils\dashboard_integration.py:91 +msgid "Channels:" +msgstr "Κανάλια:" + +#: embedutils\dashboard_integration.py:100 +msgid "Send Message(s)" +msgstr "Αποστολή μηνύματος(-ων)" + +#: embedutils\dashboard_integration.py:164 +msgid "Message(s) sent successfully!" +msgstr "Μήνυμα(-α) εστάλη(-αν) με επιτυχία!" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "Δημιουργήστε, στείλτε και αποθηκεύστε πλούσιες ενσωματώσεις και από το Red-Web-Dashboard!" + +#: embedutils\embedutils.py:110 +#, docstring +msgid "Post embeds from valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Δημοσιεύστε ενσωματώσεις από έγκυρο JSON.\n\n" +" Αυτό πρέπει να είναι στη μορφή που αναμένεται από [**αυτή την τεκμηρίωση του Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Εδώ είναι ένα παράδειγμα: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Μπορείτε να χρησιμοποιήσετε ένα [**embeds creator**](https://embedutils.com/) για να λάβετε ένα ωφέλιμο φορτίο JSON.\n\n" +" Εάν δώσετε ένα μήνυμα, αυτό θα επεξεργαστεί.\n" +" Μπορείτε να χρησιμοποιήσετε ένα συνημμένο και η εντολή `[p]embed yamlfile` θα κληθεί αυτόματα.\n" +" " + +#: embedutils\embedutils.py:143 +#, docstring +msgid "Post embeds from valid YAML.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Δημοσίευση ενσωματώσεων από έγκυρο YAML.\n\n" +" Αυτή πρέπει να έχει τη μορφή που αναμένεται από [**αυτή την τεκμηρίωση του Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Εδώ είναι ένα παράδειγμα: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Εάν παρέχετε ένα μήνυμα, αυτό θα επεξεργαστεί.\n" +" Μπορείτε να χρησιμοποιήσετε ένα συνημμένο και η εντολή `[p]embed yamlfile` θα κληθεί αυτόματα.\n" +" " + +#: embedutils\embedutils.py:171 +#, docstring +msgid "Post an embed from a valid JSON file (upload it).\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Δημοσιεύστε μια ενσωμάτωση από ένα έγκυρο αρχείο JSON (ανεβάστε το).\n\n" +" Αυτό πρέπει να έχει τη μορφή που αναμένεται από [**αυτή την τεκμηρίωση του Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Εδώ είναι ένα παράδειγμα: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Μπορείτε να χρησιμοποιήσετε ένα [**embeds creator**](https://embedutils.com/) για να λάβετε ένα ωφέλιμο φορτίο JSON.\n\n" +" Εάν δώσετε ένα μήνυμα, αυτό θα επεξεργαστεί.\n" +" " + +#: embedutils\embedutils.py:186 embedutils\embedutils.py:217 +#: embedutils\embedutils.py:405 embedutils\embedutils.py:415 +#: embedutils\embedutils.py:505 embedutils\embedutils.py:515 +#: embedutils\embedutils.py:831 embedutils\embedutils.py:843 +msgid "Unreadable attachment with `utf-8`." +msgstr "Μη αναγνώσιμο συνημμένο με `utf-8`." + +#: embedutils\embedutils.py:206 +#, docstring +msgid "Post an embed from a valid YAML file (upload it).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Δημοσιεύστε μια ενσωμάτωση από ένα έγκυρο αρχείο YAML (ανεβάστε το).\n\n" +" Εάν δώσετε ένα μήνυμα, θα γίνει επεξεργασία.\n" +" " + +#: embedutils\embedutils.py:243 +#, docstring +msgid "Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Δημοσιεύστε ενσωματώσεις από έναν σύνδεσμο GitHub/Gist/Pastebin/Hastebin που περιέχει έγκυρο JSON.\n\n" +" Αυτό πρέπει να έχει τη μορφή που αναμένεται από [**αυτή την τεκμηρίωση του Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Εδώ είναι ένα παράδειγμα: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Εάν παρέχετε ένα μήνυμα, αυτό θα επεξεργαστεί.\n" +" " + +#: embedutils\embedutils.py:273 +#, docstring +msgid "Post embed(s) from an existing message.\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Δημοσιεύστε embed(s) από ένα υπάρχον μήνυμα.\n\n" +" Το μήνυμα πρέπει να έχει τουλάχιστον μία ενσωμάτωση.\n" +" Μπορείτε να καθορίσετε έναν δείκτη (ξεκινώντας από το 0), αν θέλετε να στείλετε μόνο μία από τις ενσωματώσεις.\n" +" Το περιεχόμενο του μηνύματος που έχει ήδη αποσταλεί συμπεριλαμβάνεται εάν δεν έχει καθοριστεί δείκτης.\n\n" +" Εάν δώσετε ένα μήνυμα, αυτό θα επεξεργαστεί.\n" +" " + +#: embedutils\embedutils.py:325 +#, docstring +msgid "Download a JSON file for a message's embed(s).\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "Λήψη ενός αρχείου JSON για την ενσωμάτωση ενός μηνύματος.\n\n" +" Το μήνυμα πρέπει να έχει τουλάχιστον μία ενσωμάτωση.\n" +" Μπορείτε να καθορίσετε έναν δείκτη (ξεκινώντας από το 0), αν θέλετε να συμπεριλάβετε μόνο μία από τις ενσωματώσεις.\n" +" Το περιεχόμενο του μηνύματος που έχει ήδη αποσταλεί συμπεριλαμβάνεται εάν δεν έχει καθοριστεί δείκτης.\n" +" " + +#: embedutils\embedutils.py:385 +#, docstring +msgid "Edit a message sent by [botname].\n\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "Επεξεργαστείτε ένα μήνυμα που έχει σταλεί από τον [botname].\n\n" +" Θα ήταν καλύτερα να χρησιμοποιείτε την παράμετρο `message` όλων των άλλων εντολών.\n" +" " + +#: embedutils\embedutils.py:477 +#, docstring +msgid "Store an embed.\n\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "Αποθηκεύστε μια ενσωμάτωση.\n\n" +" Βάλτε το όνομα σε εισαγωγικά αν πρόκειται για πολλές λέξεις.\n" +" Το όρισμα `locked` καθορίζει αν η ενσωμάτωση θα πρέπει να είναι κλειδωμένη μόνο για τους mod και τους ανώτερους (επίπεδο guild) ή μόνο για τους ιδιοκτήτες των bot (παγκόσμιο επίπεδο).\n" +" " + +#: embedutils\embedutils.py:485 embedutils\embedutils.py:571 +#: embedutils\embedutils.py:586 embedutils\embedutils.py:618 +#: embedutils\embedutils.py:650 +msgid "You can't manage global stored embeds." +msgstr "Δεν μπορείτε να διαχειρίζεστε τα παγκόσμια αποθηκευμένα embeds." + +#: embedutils\embedutils.py:548 +msgid "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "Αυτός ο διακομιστής έχει φτάσει στο όριο ενσωμάτωσης του {embed_limit}. Πρέπει να αφαιρέσετε μια ενσωμάτωση με την εντολή `{ctx.clean_prefix}embed unstore` πριν προσθέσετε μια νέα." + +#: embedutils\embedutils.py:567 +#, docstring +msgid "Remove a stored embed." +msgstr "Κατάργηση μιας αποθηκευμένης ενσωμάτωσης." + +#: embedutils\embedutils.py:577 embedutils\embedutils.py:623 +#: embedutils\embedutils.py:655 +msgid "This is not a stored embed at this level." +msgstr "Αυτό δεν είναι μια αποθηκευμένη ενσωμάτωση σε αυτό το επίπεδο." + +#: embedutils\embedutils.py:584 embedutils\embedutils.py:614 +#, docstring +msgid "Get info about a stored embed." +msgstr "Λήψη πληροφοριών σχετικά με ένα αποθηκευμένο embed." + +#: embedutils\embedutils.py:592 +msgid "No stored embeds is configured at this level." +msgstr "Σε αυτό το επίπεδο δεν έχει ρυθμιστεί καμία αποθηκευμένη ενσωμάτωση." + +#: embedutils\embedutils.py:596 +msgid "Global " +msgstr "Παγκόσμια " + +#: embedutils\embedutils.py:596 +msgid "Stored Embeds" +msgstr "Αποθηκευμένα Embeds" + +#: embedutils\embedutils.py:646 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "Λήψη ενός αρχείου JSON για μια αποθηκευμένη ενσωμάτωση." + +#: embedutils\embedutils.py:675 +#, docstring +msgid "Post stored embeds." +msgstr "Δημοσίευση αποθηκευμένων ενσωματώσεων." + +#: embedutils\embedutils.py:697 embedutils\embedutils.py:748 +msgid "`{name}` is not a stored embed at this level." +msgstr "Το \"{name}\" δεν είναι ένα αποθηκευμένο embed σε αυτό το επίπεδο." + +#: embedutils\embedutils.py:726 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "Δημοσιεύστε αποθηκευμένες ενσωματώσεις με ένα webhook." + +#: embedutils\embedutils.py:793 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "Λάβετε το σύνδεσμο για το ταμπλό." + +#: embedutils\embedutils.py:796 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Το Red-Web-Dashboard δεν είναι εγκατεστημένο. Ελέγξτε το ." + +#: embedutils\embedutils.py:801 +msgid "You can't access the Dashboard." +msgstr "Δεν μπορείτε να έχετε πρόσβαση στον πίνακα ελέγχου." + +#: embedutils\embedutils.py:807 +msgid "This third party is disabled on the Dashboard." +msgstr "Αυτό το τρίτο μέρος είναι απενεργοποιημένο στον πίνακα ελέγχου." + +#: embedutils\embedutils.py:872 +msgid "Dashboard - " +msgstr "Ταμπλό - " + +#: embedutils\embedutils.py:876 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "Μπορείτε να δημιουργείτε και να στέλνετε πλούσιες ενσωματώσεις απευθείας από το Dashboard!" + +#: embedutils\embedutils.py:883 +msgid "The URL is too long to be displayed." +msgstr "Η διεύθυνση URL είναι πολύ μεγάλη για να εμφανιστεί." + +#: embedutils\embedutils.py:889 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "Μεταφορά αποθηκευμένων ενσωματώσεων από το EmbedUtils by Phen." + +#: embedutils\embedutils.py:933 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "Τα δεδομένα μεταφέρθηκαν επιτυχώς από την EmbedUtils by Phen." + diff --git a/embedutils/locales/es-ES.po b/embedutils/locales/es-ES.po new file mode 100644 index 0000000..9e9dcea --- /dev/null +++ b/embedutils/locales/es-ES.po @@ -0,0 +1,339 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Spanish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: es-ES\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/embedutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 314\n" +"Language: es_ES\n" + +#: embedutils\converters.py:66 embedutils\converters.py:81 +msgid "This doesn't seem to be properly formatted embed {conversion_type}. Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "Esto no parece estar correctamente formateado incrustar {conversion_type}. Consulte el enlace en `{ctx.clean_prefix}ayuda {ctx.command.qualified_name}`." + +#: embedutils\converters.py:93 +msgid "JSON Parse Error" +msgstr "Error de análisis JSON" + +#: embedutils\converters.py:104 +msgid "YAML Parse Error" +msgstr "Error de análisis de YAML" + +#: embedutils\converters.py:114 +msgid "The `content` field is not supported for this command." +msgstr "El campo `content` no es compatible con este comando." + +#: embedutils\converters.py:130 +msgid "Embed Parse Error" +msgstr "Embed Parse Error" + +#: embedutils\converters.py:135 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "El tamaño de la incrustación excede el límite de Discord de 6000 caracteres ({length})." + +#: embedutils\converters.py:147 embedutils\embedutils.py:100 +#: embedutils\embedutils.py:133 embedutils\embedutils.py:165 +#: embedutils\embedutils.py:200 embedutils\embedutils.py:231 +#: embedutils\embedutils.py:262 embedutils\embedutils.py:314 +#: embedutils\embedutils.py:440 embedutils\embedutils.py:537 +#: embedutils\embedutils.py:708 embedutils\embedutils.py:762 +msgid "Embed Send Error" +msgstr "Embed Enviar error" + +#: embedutils\converters.py:162 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "Utilice `{ctx.prefix}help {ctx.command.qualified_name}` para ver un ejemplo." + +#: embedutils\converters.py:204 +msgid "Embed limit reached ({limit})." +msgstr "Límite de incrustación alcanzado ({limit})." + +#: embedutils\converters.py:209 +msgid "Failed to convert input into embeds." +msgstr "Fallo al convertir la entrada en incrustaciones." + +#: embedutils\converters.py:228 +msgid "It's not a valid channel or thread." +msgstr "No es un canal o hilo válido." + +#: embedutils\converters.py:232 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "No tengo permisos para enviar incrustaciones en {channel.mention}." + +#: embedutils\converters.py:241 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "No tienes permisos para enviar incrustaciones en {channel.mention}." + +#: embedutils\converters.py:253 +msgid "I have to be the author of the message. You can use the command without providing a message to send one." +msgstr "Tengo que ser el autor del mensaje. Puede utilizar el comando sin proporcionar un mensaje para enviar uno." + +#: embedutils\converters.py:263 +msgid "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "No está permitido editar incrustaciones de un mensaje existente (el propietario del bot puede establecer los permisos con la rueda Permisos en el comando `[p]embed edit`)." + +#: embedutils\dashboard_integration.py:53 +msgid "You don't have permissions to access this page." +msgstr "No tienes permisos para acceder a esta página." + +#: embedutils\dashboard_integration.py:60 +msgid "I or you don't have permissions to send messages or embeds in any channel in this guild." +msgstr "Ni tú ni yo tenemos permisos para enviar mensajes o incrustaciones en ningún canal de este gremio." + +#: embedutils\dashboard_integration.py:76 +msgid "Username:" +msgstr "Nombre de usuario:" + +#: embedutils\dashboard_integration.py:80 +msgid "Avatar URL:" +msgstr "URL del avatar:" + +#: embedutils\dashboard_integration.py:84 +msgid "Data" +msgstr "Datos" + +#: embedutils\dashboard_integration.py:91 +msgid "Channels:" +msgstr "Canales:" + +#: embedutils\dashboard_integration.py:100 +msgid "Send Message(s)" +msgstr "Enviar mensaje(s)" + +#: embedutils\dashboard_integration.py:164 +msgid "Message(s) sent successfully!" +msgstr "¡Mensaje(s) enviado(s) con éxito!" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "Cree, envíe y almacene incrustaciones enriquecidas, ¡también desde Red-Web-Dashboard!" + +#: embedutils\embedutils.py:110 +#, docstring +msgid "Post embeds from valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Publicar incrustaciones de JSON válido.\n\n" +" Debe estar en el formato esperado por [**esta documentación de Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Aquí tienes un ejemplo: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Puedes usar un [**embeds creator**](https://embedutils.com/) para obtener una carga JSON.\n\n" +" Si proporcionas un mensaje, se editará.\n" +" Puedes usar un adjunto y el comando `[p]embed yamlfile` será invocado automáticamente.\n" +" " + +#: embedutils\embedutils.py:143 +#, docstring +msgid "Post embeds from valid YAML.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Publicar incrustaciones de YAML válidas.\n\n" +" Debe estar en el formato esperado por [**esta documentación de Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Aquí tienes un ejemplo: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Si proporcionas un mensaje, será editado.\n" +" Puedes utilizar un archivo adjunto y el comando `[p]embed yamlfile` será invocado automáticamente.\n" +" " + +#: embedutils\embedutils.py:171 +#, docstring +msgid "Post an embed from a valid JSON file (upload it).\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Publica un embed de un archivo JSON válido (súbelo).\n\n" +" Debe estar en el formato esperado por [**esta documentación de Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Aquí tienes un ejemplo: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Puedes usar un [**embeds creator**](https://embedutils.com/) para obtener una carga JSON.\n\n" +" Si proporcionas un mensaje, se editará.\n" +" " + +#: embedutils\embedutils.py:186 embedutils\embedutils.py:217 +#: embedutils\embedutils.py:405 embedutils\embedutils.py:415 +#: embedutils\embedutils.py:505 embedutils\embedutils.py:515 +#: embedutils\embedutils.py:831 embedutils\embedutils.py:843 +msgid "Unreadable attachment with `utf-8`." +msgstr "Archivo adjunto ilegible con `utf-8`." + +#: embedutils\embedutils.py:206 +#, docstring +msgid "Post an embed from a valid YAML file (upload it).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Publica un embed desde un archivo YAML válido (súbelo).\n\n" +" Si proporciona un mensaje, se editará.\n" +" " + +#: embedutils\embedutils.py:243 +#, docstring +msgid "Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Publica incrustaciones desde un enlace GitHub/Gist/Pastebin/Hastebin que contenga JSON válido.\n\n" +" Debe tener el formato esperado por [**esta documentación de Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Aquí tienes un ejemplo: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Si proporcionas un mensaje, será editado.\n" +" " + +#: embedutils\embedutils.py:273 +#, docstring +msgid "Post embed(s) from an existing message.\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Publicar incrustado(s) de un mensaje existente.\n\n" +" El mensaje debe tener al menos un embed.\n" +" Puede especificar un índice (empezando por 0) si desea enviar sólo uno de los incrustados.\n" +" Si no se especifica ningún índice, se incluirá el contenido del mensaje ya enviado.\n\n" +" Si proporciona un mensaje, éste se editará.\n" +" " + +#: embedutils\embedutils.py:325 +#, docstring +msgid "Download a JSON file for a message's embed(s).\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "Descargar un archivo JSON para los archivos incrustados de un mensaje.\n\n" +" El mensaje debe tener al menos una incrustación.\n" +" Puede especificar un índice (empezando por 0) si desea incluir sólo uno de los incrustados.\n" +" Si no se especifica ningún índice, se incluye el contenido del mensaje ya enviado.\n" +" " + +#: embedutils\embedutils.py:385 +#, docstring +msgid "Edit a message sent by [botname].\n\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "Editar un mensaje enviado por [botname].\n\n" +" Sería mejor utilizar el parámetro `message` de todos los demás comandos.\n" +" " + +#: embedutils\embedutils.py:477 +#, docstring +msgid "Store an embed.\n\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "Almacena una incrustación.\n\n" +" Pon el nombre entre comillas si tiene varias palabras.\n" +" El argumento `locked` especifica si la incrustación debe estar bloqueada sólo para mods y superiores (nivel de gremio) o sólo para propietarios de bots (nivel global).\n" +" " + +#: embedutils\embedutils.py:485 embedutils\embedutils.py:571 +#: embedutils\embedutils.py:586 embedutils\embedutils.py:618 +#: embedutils\embedutils.py:650 +msgid "You can't manage global stored embeds." +msgstr "No se pueden gestionar incrustaciones almacenadas globalmente." + +#: embedutils\embedutils.py:548 +msgid "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "Este servidor ha alcanzado el límite de {embed_limit}. Debe eliminar un embed con `{ctx.clean_prefix}embed unstore` antes de poder añadir uno nuevo." + +#: embedutils\embedutils.py:567 +#, docstring +msgid "Remove a stored embed." +msgstr "Eliminar una incrustación almacenada." + +#: embedutils\embedutils.py:577 embedutils\embedutils.py:623 +#: embedutils\embedutils.py:655 +msgid "This is not a stored embed at this level." +msgstr "No se trata de una incrustación almacenada a este nivel." + +#: embedutils\embedutils.py:584 embedutils\embedutils.py:614 +#, docstring +msgid "Get info about a stored embed." +msgstr "Obtener información sobre un embed almacenado." + +#: embedutils\embedutils.py:592 +msgid "No stored embeds is configured at this level." +msgstr "En este nivel no se configura ninguna incrustación almacenada." + +#: embedutils\embedutils.py:596 +msgid "Global " +msgstr "Global " + +#: embedutils\embedutils.py:596 +msgid "Stored Embeds" +msgstr "Incrustaciones almacenadas" + +#: embedutils\embedutils.py:646 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "Descargar un archivo JSON para una incrustación almacenada." + +#: embedutils\embedutils.py:675 +#, docstring +msgid "Post stored embeds." +msgstr "Publicar incrustaciones almacenadas." + +#: embedutils\embedutils.py:697 embedutils\embedutils.py:748 +msgid "`{name}` is not a stored embed at this level." +msgstr "`{name}` no es un embed almacenado a este nivel." + +#: embedutils\embedutils.py:726 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "Publicar incrustaciones almacenadas con un webhook." + +#: embedutils\embedutils.py:793 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "Obtenga el enlace al panel de control." + +#: embedutils\embedutils.py:796 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard no está instalado. Compruebe ." + +#: embedutils\embedutils.py:801 +msgid "You can't access the Dashboard." +msgstr "No puedes acceder al Panel de control." + +#: embedutils\embedutils.py:807 +msgid "This third party is disabled on the Dashboard." +msgstr "Este tercero está desactivado en el Panel de control." + +#: embedutils\embedutils.py:872 +msgid "Dashboard - " +msgstr "Cuadro de mandos - " + +#: embedutils\embedutils.py:876 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "Puede crear y enviar incrustaciones enriquecidas directamente desde el panel de control!" + +#: embedutils\embedutils.py:883 +msgid "The URL is too long to be displayed." +msgstr "La URL es demasiado larga para mostrarse." + +#: embedutils\embedutils.py:889 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "Migrar incrustaciones almacenadas de EmbedUtils por Phen." + +#: embedutils\embedutils.py:933 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "Datos migrados con éxito desde EmbedUtils por Phen." + diff --git a/embedutils/locales/fi-FI.po b/embedutils/locales/fi-FI.po new file mode 100644 index 0000000..e1e9bc1 --- /dev/null +++ b/embedutils/locales/fi-FI.po @@ -0,0 +1,339 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Finnish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fi\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/embedutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 314\n" +"Language: fi_FI\n" + +#: embedutils\converters.py:66 embedutils\converters.py:81 +msgid "This doesn't seem to be properly formatted embed {conversion_type}. Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "Tämä ei näytä olevan oikein muotoiltu upottaa {conversion_type}. Katso linkki `{ctx.clean_prefix}help {ctx.command.qualified_name}`." + +#: embedutils\converters.py:93 +msgid "JSON Parse Error" +msgstr "JSON Parse -virhe" + +#: embedutils\converters.py:104 +msgid "YAML Parse Error" +msgstr "YAML Parse -virhe" + +#: embedutils\converters.py:114 +msgid "The `content` field is not supported for this command." +msgstr "Sisältö-kenttää ei tueta tässä komennossa." + +#: embedutils\converters.py:130 +msgid "Embed Parse Error" +msgstr "Upota Parse-virhe" + +#: embedutils\converters.py:135 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "Upotuskoko ylittää Discordin 6000 merkin rajan ({length})." + +#: embedutils\converters.py:147 embedutils\embedutils.py:100 +#: embedutils\embedutils.py:133 embedutils\embedutils.py:165 +#: embedutils\embedutils.py:200 embedutils\embedutils.py:231 +#: embedutils\embedutils.py:262 embedutils\embedutils.py:314 +#: embedutils\embedutils.py:440 embedutils\embedutils.py:537 +#: embedutils\embedutils.py:708 embedutils\embedutils.py:762 +msgid "Embed Send Error" +msgstr "Upota lähetysvirhe" + +#: embedutils\converters.py:162 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "Käytä `{ctx.prefix}help {ctx.command.qualified_name}` nähdäksesi esimerkin." + +#: embedutils\converters.py:204 +msgid "Embed limit reached ({limit})." +msgstr "Sulauttamisraja saavutettu ({limit})." + +#: embedutils\converters.py:209 +msgid "Failed to convert input into embeds." +msgstr "Syötteen muuntaminen upotuksiksi epäonnistui." + +#: embedutils\converters.py:228 +msgid "It's not a valid channel or thread." +msgstr "Se ei ole kelvollinen kanava tai säie." + +#: embedutils\converters.py:232 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "Minulla ei ole oikeuksia lähettää upotuksia osoitteessa {channel.mention}." + +#: embedutils\converters.py:241 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "Sinulla ei ole oikeuksia lähettää upotuksia osoitteessa {channel.mention}." + +#: embedutils\converters.py:253 +msgid "I have to be the author of the message. You can use the command without providing a message to send one." +msgstr "Minun on oltava viestin kirjoittaja. Voit käyttää komentoa ilman viestin antamista viestin lähettämiseen." + +#: embedutils\converters.py:263 +msgid "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "Et saa muokata olemassa olevan viestin upotuksia (bottiomistaja voi asettaa oikeudet komennon `[p]embed edit` (upota muokkaa) kohdassa Permissions)." + +#: embedutils\dashboard_integration.py:53 +msgid "You don't have permissions to access this page." +msgstr "Sinulla ei ole oikeuksia käyttää tätä sivua." + +#: embedutils\dashboard_integration.py:60 +msgid "I or you don't have permissions to send messages or embeds in any channel in this guild." +msgstr "Minulla tai sinulla ei ole oikeuksia lähettää viestejä tai upotuksia millään tämän killan kanavalla." + +#: embedutils\dashboard_integration.py:76 +msgid "Username:" +msgstr "Käyttäjätunnus:" + +#: embedutils\dashboard_integration.py:80 +msgid "Avatar URL:" +msgstr "Avatarin URL-osoite:" + +#: embedutils\dashboard_integration.py:84 +msgid "Data" +msgstr "Tiedot" + +#: embedutils\dashboard_integration.py:91 +msgid "Channels:" +msgstr "Kanavat:" + +#: embedutils\dashboard_integration.py:100 +msgid "Send Message(s)" +msgstr "Lähetä viesti(t)" + +#: embedutils\dashboard_integration.py:164 +msgid "Message(s) sent successfully!" +msgstr "Viesti(t) lähetetty onnistuneesti!" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "Luo, lähetä ja tallenna runsaasti upotuksia myös Red-Web-Dashboardista!" + +#: embedutils\embedutils.py:110 +#, docstring +msgid "Post embeds from valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Postita upotuksia validista JSONista.\n\n" +" Tämän on oltava [**tämän Discordin dokumentaation**](https://discord.com/developers/docs/resources/channel#embed-object) odottamassa muodossa.\n" +" Tässä on esimerkki: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Voit käyttää [**embedien luojaa**](https://embedutils.com/) saadaksesi JSON-hyötykuorman.\n\n" +" Jos annat viestin, sitä muokataan.\n" +" Voit käyttää liitetiedostoa, jolloin komento `[p]embed yamlfile` kutsutaan automaattisesti.\n" +" " + +#: embedutils\embedutils.py:143 +#, docstring +msgid "Post embeds from valid YAML.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Postita upotukset kelvollisesta YAML:stä.\n\n" +" Tämän on oltava [**tämän Discordin dokumentaation**](https://discord.com/developers/docs/resources/channel#embed-object) odottamassa muodossa.\n" +" Tässä on esimerkki: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Jos annat viestin, sitä muokataan.\n" +" Voit käyttää liitetiedostoa, jolloin komento `[p]embed yamlfile` kutsutaan automaattisesti.\n" +" " + +#: embedutils\embedutils.py:171 +#, docstring +msgid "Post an embed from a valid JSON file (upload it).\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Lähetä upotus kelvollisesta JSON-tiedostosta (lataa se).\n\n" +" Sen on oltava [**tämän Discordin dokumentaation**] (https://discord.com/developers/docs/resources/channel#embed-object) odottamassa muodossa.\n" +" Tässä on esimerkki: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Voit käyttää [**embedien luojaa**](https://embedutils.com/) saadaksesi JSON-hyötykuorman.\n\n" +" Jos annat viestin, sitä muokataan.\n" +" " + +#: embedutils\embedutils.py:186 embedutils\embedutils.py:217 +#: embedutils\embedutils.py:405 embedutils\embedutils.py:415 +#: embedutils\embedutils.py:505 embedutils\embedutils.py:515 +#: embedutils\embedutils.py:831 embedutils\embedutils.py:843 +msgid "Unreadable attachment with `utf-8`." +msgstr "Lukukelvoton liitetiedosto `utf-8`:lla." + +#: embedutils\embedutils.py:206 +#, docstring +msgid "Post an embed from a valid YAML file (upload it).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Lähetä upotus kelvollisesta YAML-tiedostosta (lataa se).\n\n" +" Jos annat viestin, sitä muokataan.\n" +" " + +#: embedutils\embedutils.py:243 +#, docstring +msgid "Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Postita upotuksia GitHub/Gist/Pastebin/Hastebin-linkistä, joka sisältää validin JSONin.\n\n" +" Tämän on oltava [**tämän Discordin dokumentaation**](https://discord.com/developers/docs/resources/channel#embed-object) odottamassa muodossa.\n" +" Tässä on esimerkki: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Jos annat viestin, sitä muokataan.\n" +" " + +#: embedutils\embedutils.py:273 +#, docstring +msgid "Post embed(s) from an existing message.\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Postaa upotukset olemassa olevasta viestistä.\n\n" +" Viestissä on oltava vähintään yksi upotus.\n" +" Voit määrittää indeksin (alkaen 0:sta), jos haluat lähettää vain yhden upotuksista.\n" +" Jo lähetetyn viestin sisältö sisällytetään, jos indeksiä ei ole määritetty.\n\n" +" Jos annat viestin, sitä muokataan.\n" +" " + +#: embedutils\embedutils.py:325 +#, docstring +msgid "Download a JSON file for a message's embed(s).\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "Lataa JSON-tiedosto viestin upotetusta viestistä (upotetuista viesteistä).\n\n" +" Viestissä on oltava vähintään yksi upotus.\n" +" Voit määrittää indeksin (alkaen 0), jos haluat sisällyttää vain yhden upotuksista.\n" +" Jo lähetetyn viestin sisältö sisällytetään, jos indeksiä ei ole määritetty.\n" +" " + +#: embedutils\embedutils.py:385 +#, docstring +msgid "Edit a message sent by [botname].\n\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "Muokkaa [botname]n lähettämää viestiä.\n\n" +" Olisi parempi käyttää kaikkien muiden komentojen `message`-parametria.\n" +" " + +#: embedutils\embedutils.py:477 +#, docstring +msgid "Store an embed.\n\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "Tallenna upotus.\n\n" +" Laita nimi lainausmerkkeihin, jos se koostuu useista sanoista.\n" +" Argumentti `locked` määrittää, lukitaanko upotus vain modille ja esimiehelle (kiltataso) vai vain botin omistajille (globaali taso).\n" +" " + +#: embedutils\embedutils.py:485 embedutils\embedutils.py:571 +#: embedutils\embedutils.py:586 embedutils\embedutils.py:618 +#: embedutils\embedutils.py:650 +msgid "You can't manage global stored embeds." +msgstr "Et voi hallita globaalisti tallennettuja upotuksia." + +#: embedutils\embedutils.py:548 +msgid "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "Tämä palvelin on saavuttanut {embed_limit}-sisällytysrajan. Sinun on poistettava upotus komennolla `{ctx.clean_prefix}embed unstore` ennen kuin voit lisätä uuden." + +#: embedutils\embedutils.py:567 +#, docstring +msgid "Remove a stored embed." +msgstr "Poista tallennettu upotus." + +#: embedutils\embedutils.py:577 embedutils\embedutils.py:623 +#: embedutils\embedutils.py:655 +msgid "This is not a stored embed at this level." +msgstr "Tämä ei ole tallennettu upotus tällä tasolla." + +#: embedutils\embedutils.py:584 embedutils\embedutils.py:614 +#, docstring +msgid "Get info about a stored embed." +msgstr "Hae tietoja tallennetusta upotuksesta." + +#: embedutils\embedutils.py:592 +msgid "No stored embeds is configured at this level." +msgstr "Tällä tasolla ei ole määritetty tallennettuja upotuksia." + +#: embedutils\embedutils.py:596 +msgid "Global " +msgstr "Maailmanlaajuinen " + +#: embedutils\embedutils.py:596 +msgid "Stored Embeds" +msgstr "Tallennetut embeds" + +#: embedutils\embedutils.py:646 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "Lataa JSON-tiedosto tallennettua upotusta varten." + +#: embedutils\embedutils.py:675 +#, docstring +msgid "Post stored embeds." +msgstr "Postitse tallennetut upotukset." + +#: embedutils\embedutils.py:697 embedutils\embedutils.py:748 +msgid "`{name}` is not a stored embed at this level." +msgstr "`{name}` ei ole tallennettu upotus tällä tasolla." + +#: embedutils\embedutils.py:726 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "Lähetä tallennetut upotukset verkkokoukulla." + +#: embedutils\embedutils.py:793 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "Hae linkki kojelautaan." + +#: embedutils\embedutils.py:796 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboardia ei ole asennettu. Tarkista ." + +#: embedutils\embedutils.py:801 +msgid "You can't access the Dashboard." +msgstr "Et pääse kojelautaan." + +#: embedutils\embedutils.py:807 +msgid "This third party is disabled on the Dashboard." +msgstr "Tämä kolmas osapuoli on poistettu käytöstä kojelaudalla." + +#: embedutils\embedutils.py:872 +msgid "Dashboard - " +msgstr "Kojelauta - " + +#: embedutils\embedutils.py:876 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "Voit luoda ja lähettää runsaasti upotuksia suoraan Dashboardista!" + +#: embedutils\embedutils.py:883 +msgid "The URL is too long to be displayed." +msgstr "URL-osoite on liian pitkä näytettäväksi." + +#: embedutils\embedutils.py:889 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "Siirrä tallennetut upotukset EmbedUtilsista Phen." + +#: embedutils\embedutils.py:933 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "Tiedot siirretty onnistuneesti EmbedUtilsista Phen." + diff --git a/embedutils/locales/fr-FR.po b/embedutils/locales/fr-FR.po new file mode 100644 index 0000000..423d1b7 --- /dev/null +++ b/embedutils/locales/fr-FR.po @@ -0,0 +1,339 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: French\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: fr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/embedutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 314\n" +"Language: fr_FR\n" + +#: embedutils\converters.py:66 embedutils\converters.py:81 +msgid "This doesn't seem to be properly formatted embed {conversion_type}. Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "Ceci ne semble pas être correctement formaté embed {conversion_type}. Référez-vous au lien sur `{ctx.clean_prefix}help {ctx.command.qualified_name}`." + +#: embedutils\converters.py:93 +msgid "JSON Parse Error" +msgstr "Erreur d'analyse JSON" + +#: embedutils\converters.py:104 +msgid "YAML Parse Error" +msgstr "Erreur d'analyse YAML" + +#: embedutils\converters.py:114 +msgid "The `content` field is not supported for this command." +msgstr "Le champ `content` n'est pas pris en charge par cette commande." + +#: embedutils\converters.py:130 +msgid "Embed Parse Error" +msgstr "Embed Parse Error (erreur d'analyse)" + +#: embedutils\converters.py:135 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "La taille de l'insertion dépasse la limite de 6000 caractères de Discord ({length})." + +#: embedutils\converters.py:147 embedutils\embedutils.py:100 +#: embedutils\embedutils.py:133 embedutils\embedutils.py:165 +#: embedutils\embedutils.py:200 embedutils\embedutils.py:231 +#: embedutils\embedutils.py:262 embedutils\embedutils.py:314 +#: embedutils\embedutils.py:440 embedutils\embedutils.py:537 +#: embedutils\embedutils.py:708 embedutils\embedutils.py:762 +msgid "Embed Send Error" +msgstr "Embed Send Error (erreur d'envoi)" + +#: embedutils\converters.py:162 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "Utilisez `{ctx.prefix}help {ctx.command.qualified_name}` pour voir un exemple." + +#: embedutils\converters.py:204 +msgid "Embed limit reached ({limit})." +msgstr "Limite d'intégration atteinte ({limit})." + +#: embedutils\converters.py:209 +msgid "Failed to convert input into embeds." +msgstr "Échec de la conversion de l'entrée en liens." + +#: embedutils\converters.py:228 +msgid "It's not a valid channel or thread." +msgstr "Il ne s'agit pas d'un canal ou d'un fil de discussion valable." + +#: embedutils\converters.py:232 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "Je n'ai pas l'autorisation d'envoyer des liens dans {channel.mention}." + +#: embedutils\converters.py:241 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "Vous n'avez pas l'autorisation d'envoyer des images dans {channel.mention}." + +#: embedutils\converters.py:253 +msgid "I have to be the author of the message. You can use the command without providing a message to send one." +msgstr "Je dois être l'auteur du message. Vous pouvez utiliser la commande sans fournir de message pour en envoyer un." + +#: embedutils\converters.py:263 +msgid "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "Vous n'êtes pas autorisé à éditer les embeds d'un message existant (le propriétaire du bot peut définir les permissions avec le champ Permissions de la commande `[p]embed edit`)." + +#: embedutils\dashboard_integration.py:53 +msgid "You don't have permissions to access this page." +msgstr "Vous n'avez pas les autorisations nécessaires pour accéder à cette page." + +#: embedutils\dashboard_integration.py:60 +msgid "I or you don't have permissions to send messages or embeds in any channel in this guild." +msgstr "Je ou tu n'as pas la permission d'envoyer des messages ou des embeds dans aucun canal de cette guilde." + +#: embedutils\dashboard_integration.py:76 +msgid "Username:" +msgstr "Nom d'utilisateur :" + +#: embedutils\dashboard_integration.py:80 +msgid "Avatar URL:" +msgstr "URL de l'avatar :" + +#: embedutils\dashboard_integration.py:84 +msgid "Data" +msgstr "Données" + +#: embedutils\dashboard_integration.py:91 +msgid "Channels:" +msgstr "Chaînes :" + +#: embedutils\dashboard_integration.py:100 +msgid "Send Message(s)" +msgstr "Envoyer le(s) message(s)" + +#: embedutils\dashboard_integration.py:164 +msgid "Message(s) sent successfully!" +msgstr "Message(s) envoyé(s) avec succès !" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "Créez, envoyez et stockez des images enrichies, même à partir de Red-Web-Dashboard !" + +#: embedutils\embedutils.py:110 +#, docstring +msgid "Post embeds from valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Postez des embeds à partir de JSON valides.\n\n" +" Cela doit être dans le format attendu par [**cette documentation Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Voici un exemple : [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Vous pouvez utiliser un [**créateur d'embeds**](https://embedutils.com/) pour obtenir une charge utile JSON.\n\n" +" Si vous fournissez un message, il sera édité.\n" +" Vous pouvez utiliser une pièce jointe et la commande `[p]embed yamlfile` sera invoquée automatiquement.\n" +" " + +#: embedutils\embedutils.py:143 +#, docstring +msgid "Post embeds from valid YAML.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Poster des embeds à partir d'un YAML valide.\n\n" +" Cela doit être dans le format attendu par [**cette documentation Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Voici un exemple : [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Si vous fournissez un message, il sera édité.\n" +" Vous pouvez utiliser une pièce jointe et la commande `[p]embed yamlfile` sera invoquée automatiquement.\n" +" " + +#: embedutils\embedutils.py:171 +#, docstring +msgid "Post an embed from a valid JSON file (upload it).\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Postez un embed à partir d'un fichier JSON valide (téléchargez-le).\n\n" +" Il doit être au format attendu par [**cette documentation Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Voici un exemple : [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Vous pouvez utiliser un [**créateur d'embeds**](https://embedutils.com/) pour obtenir une charge utile JSON.\n\n" +" Si vous fournissez un message, il sera édité.\n" +" " + +#: embedutils\embedutils.py:186 embedutils\embedutils.py:217 +#: embedutils\embedutils.py:405 embedutils\embedutils.py:415 +#: embedutils\embedutils.py:505 embedutils\embedutils.py:515 +#: embedutils\embedutils.py:831 embedutils\embedutils.py:843 +msgid "Unreadable attachment with `utf-8`." +msgstr "Pièce jointe illisible avec `utf-8`." + +#: embedutils\embedutils.py:206 +#, docstring +msgid "Post an embed from a valid YAML file (upload it).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Postez un embed à partir d'un fichier YAML valide (téléchargez-le).\n\n" +" Si vous fournissez un message, il sera édité.\n" +" " + +#: embedutils\embedutils.py:243 +#, docstring +msgid "Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Postez des embeds à partir d'un lien GitHub/Gist/Pastebin/Hastebin contenant du JSON valide.\n\n" +" Le format doit être celui attendu par [**cette documentation Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Voici un exemple : [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Si vous fournissez un message, il sera édité.\n" +" " + +#: embedutils\embedutils.py:273 +#, docstring +msgid "Post embed(s) from an existing message.\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Publier un ou plusieurs éléments d'un message existant.\n\n" +" Le message doit comporter au moins un élément intégré.\n" +" Vous pouvez spécifier un index (commençant par 0) si vous souhaitez n'envoyer qu'un seul des éléments incorporés.\n" +" Le contenu du message déjà envoyé est inclus si aucun index n'est spécifié.\n\n" +" Si vous fournissez un message, il sera édité.\n" +" " + +#: embedutils\embedutils.py:325 +#, docstring +msgid "Download a JSON file for a message's embed(s).\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "Télécharger un fichier JSON pour le(s) élément(s) intégré(s) d'un message.\n\n" +" Le message doit comporter au moins un élément intégré.\n" +" Vous pouvez spécifier un index (commençant par 0) si vous souhaitez n'inclure qu'un seul des éléments incorporés.\n" +" Le contenu du message déjà envoyé est inclus si aucun index n'est spécifié.\n" +" " + +#: embedutils\embedutils.py:385 +#, docstring +msgid "Edit a message sent by [botname].\n\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "Modifie un message envoyé par [botname].\n\n" +" Il serait préférable d'utiliser le paramètre `message` de toutes les autres commandes.\n" +" " + +#: embedutils\embedutils.py:477 +#, docstring +msgid "Store an embed.\n\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "Stocker un embed.\n\n" +" Mettez le nom entre guillemets s'il s'agit de plusieurs mots.\n" +" L'argument `locked` spécifie si l'embed doit être verrouillé pour les mods et supérieurs uniquement (niveau guilde) ou pour les propriétaires de bot uniquement (niveau global).\n" +" " + +#: embedutils\embedutils.py:485 embedutils\embedutils.py:571 +#: embedutils\embedutils.py:586 embedutils\embedutils.py:618 +#: embedutils\embedutils.py:650 +msgid "You can't manage global stored embeds." +msgstr "Vous ne pouvez pas gérer les liens stockés au niveau mondial." + +#: embedutils\embedutils.py:548 +msgid "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "Ce serveur a atteint la limite d'incorporation de {embed_limit}. Vous devez supprimer un contenu avec `{ctx.clean_prefix}embed unstore` avant de pouvoir en ajouter un nouveau." + +#: embedutils\embedutils.py:567 +#, docstring +msgid "Remove a stored embed." +msgstr "Suppression d'un embed stocké." + +#: embedutils\embedutils.py:577 embedutils\embedutils.py:623 +#: embedutils\embedutils.py:655 +msgid "This is not a stored embed at this level." +msgstr "Il ne s'agit pas d'une incorporation stockée à ce niveau." + +#: embedutils\embedutils.py:584 embedutils\embedutils.py:614 +#, docstring +msgid "Get info about a stored embed." +msgstr "Obtenir des informations sur un élément stocké." + +#: embedutils\embedutils.py:592 +msgid "No stored embeds is configured at this level." +msgstr "Aucun élément stocké n'est configuré à ce niveau." + +#: embedutils\embedutils.py:596 +msgid "Global " +msgstr "Mondial " + +#: embedutils\embedutils.py:596 +msgid "Stored Embeds" +msgstr "Emblèmes stockés" + +#: embedutils\embedutils.py:646 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "Télécharger un fichier JSON pour un embed stocké." + +#: embedutils\embedutils.py:675 +#, docstring +msgid "Post stored embeds." +msgstr "Afficher les liens stockés." + +#: embedutils\embedutils.py:697 embedutils\embedutils.py:748 +msgid "`{name}` is not a stored embed at this level." +msgstr "`{name}` n'est pas un embed stocké à ce niveau." + +#: embedutils\embedutils.py:726 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "Publier les liens stockés avec un webhook." + +#: embedutils\embedutils.py:793 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "Obtenir le lien vers le tableau de bord." + +#: embedutils\embedutils.py:796 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard n'est pas installé. Vérifiez ." + +#: embedutils\embedutils.py:801 +msgid "You can't access the Dashboard." +msgstr "Vous ne pouvez pas accéder au tableau de bord." + +#: embedutils\embedutils.py:807 +msgid "This third party is disabled on the Dashboard." +msgstr "Ce tiers est désactivé dans le tableau de bord." + +#: embedutils\embedutils.py:872 +msgid "Dashboard - " +msgstr "Tableau de bord - " + +#: embedutils\embedutils.py:876 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "Vous pouvez créer et envoyer des liens riches directement à partir du tableau de bord !" + +#: embedutils\embedutils.py:883 +msgid "The URL is too long to be displayed." +msgstr "L'URL est trop longue pour être affichée." + +#: embedutils\embedutils.py:889 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "Migration des embeds stockés depuis EmbedUtils by Phen." + +#: embedutils\embedutils.py:933 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "Données migrées avec succès depuis EmbedUtils par Phen." + diff --git a/embedutils/locales/it-IT.po b/embedutils/locales/it-IT.po new file mode 100644 index 0000000..7001942 --- /dev/null +++ b/embedutils/locales/it-IT.po @@ -0,0 +1,339 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Italian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: it\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/embedutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 314\n" +"Language: it_IT\n" + +#: embedutils\converters.py:66 embedutils\converters.py:81 +msgid "This doesn't seem to be properly formatted embed {conversion_type}. Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "Questo non sembra essere formattato in modo appropriato {conversion_type}. Fare riferimento al link su `{ctx.clean_prefix}help {ctx.command.qualified_name}`." + +#: embedutils\converters.py:93 +msgid "JSON Parse Error" +msgstr "Errore di analisi JSON" + +#: embedutils\converters.py:104 +msgid "YAML Parse Error" +msgstr "Errore di analisi YAML" + +#: embedutils\converters.py:114 +msgid "The `content` field is not supported for this command." +msgstr "Il campo `contenuto' non è supportato per questo comando." + +#: embedutils\converters.py:130 +msgid "Embed Parse Error" +msgstr "Errore di parsing dell'embed" + +#: embedutils\converters.py:135 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "La dimensione del messaggio supera il limite di 6000 caratteri di Discord ({length})." + +#: embedutils\converters.py:147 embedutils\embedutils.py:100 +#: embedutils\embedutils.py:133 embedutils\embedutils.py:165 +#: embedutils\embedutils.py:200 embedutils\embedutils.py:231 +#: embedutils\embedutils.py:262 embedutils\embedutils.py:314 +#: embedutils\embedutils.py:440 embedutils\embedutils.py:537 +#: embedutils\embedutils.py:708 embedutils\embedutils.py:762 +msgid "Embed Send Error" +msgstr "Errore di invio dell'embed" + +#: embedutils\converters.py:162 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "Utilizzare `{ctx.prefix}help {ctx.command.qualified_name}` per vedere un esempio." + +#: embedutils\converters.py:204 +msgid "Embed limit reached ({limit})." +msgstr "Limite di inclusione raggiunto ({limit})." + +#: embedutils\converters.py:209 +msgid "Failed to convert input into embeds." +msgstr "Impossibile convertire l'input in embed." + +#: embedutils\converters.py:228 +msgid "It's not a valid channel or thread." +msgstr "Non è un canale o un thread valido." + +#: embedutils\converters.py:232 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "Non ho i permessi per inviare gli embed in {channel.mention}." + +#: embedutils\converters.py:241 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "Non hai i permessi per inviare gli embed in {channel.mention}." + +#: embedutils\converters.py:253 +msgid "I have to be the author of the message. You can use the command without providing a message to send one." +msgstr "Devo essere l'autore del messaggio. È possibile utilizzare il comando senza fornire un messaggio per inviarne uno." + +#: embedutils\converters.py:263 +msgid "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "Non è consentito modificare gli embed di un messaggio esistente (il proprietario del bot può impostare i permessi con la voce Permessi del comando `[p]embed edit`)." + +#: embedutils\dashboard_integration.py:53 +msgid "You don't have permissions to access this page." +msgstr "Non hai i permessi per accedere a questa pagina." + +#: embedutils\dashboard_integration.py:60 +msgid "I or you don't have permissions to send messages or embeds in any channel in this guild." +msgstr "Io o tu non abbiamo il permesso di inviare messaggi o embed in nessun canale di questa gilda." + +#: embedutils\dashboard_integration.py:76 +msgid "Username:" +msgstr "Nome utente:" + +#: embedutils\dashboard_integration.py:80 +msgid "Avatar URL:" +msgstr "URL dell'avatar:" + +#: embedutils\dashboard_integration.py:84 +msgid "Data" +msgstr "Dati" + +#: embedutils\dashboard_integration.py:91 +msgid "Channels:" +msgstr "Canali:" + +#: embedutils\dashboard_integration.py:100 +msgid "Send Message(s)" +msgstr "Inviare i messaggi" + +#: embedutils\dashboard_integration.py:164 +msgid "Message(s) sent successfully!" +msgstr "Messaggio/i inviato/i con successo!" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "Create, inviate e memorizzate rich embed anche da Red-Web-Dashboard!" + +#: embedutils\embedutils.py:110 +#, docstring +msgid "Post embeds from valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Pubblicare embed da JSON validi.\n\n" +" Questo deve essere nel formato previsto da [**questa documentazione di Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Ecco un esempio: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" È possibile utilizzare un [**creatore di embed**](https://embedutils.com/) per ottenere un payload JSON.\n\n" +" Se si fornisce un messaggio, questo verrà modificato.\n" +" Si può usare un allegato e il comando `[p]embed yamlfile` sarà invocato automaticamente.\n" +" " + +#: embedutils\embedutils.py:143 +#, docstring +msgid "Post embeds from valid YAML.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Pubblicare i file incorporati da YAML valido.\n\n" +" Questo deve essere nel formato previsto da [**questa documentazione di Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Ecco un esempio: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Se si fornisce un messaggio, questo verrà modificato.\n" +" È possibile utilizzare un allegato e il comando `[p]embed yamlfile` sarà invocato automaticamente.\n" +" " + +#: embedutils\embedutils.py:171 +#, docstring +msgid "Post an embed from a valid JSON file (upload it).\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Pubblicare un embed da un file JSON valido (caricarlo).\n\n" +" Deve essere nel formato previsto da [**questa documentazione di Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Ecco un esempio: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" È possibile utilizzare un [**creatore di allegati**](https://embedutils.com/) per ottenere un payload JSON.\n\n" +" Se si fornisce un messaggio, questo verrà modificato.\n" +" " + +#: embedutils\embedutils.py:186 embedutils\embedutils.py:217 +#: embedutils\embedutils.py:405 embedutils\embedutils.py:415 +#: embedutils\embedutils.py:505 embedutils\embedutils.py:515 +#: embedutils\embedutils.py:831 embedutils\embedutils.py:843 +msgid "Unreadable attachment with `utf-8`." +msgstr "Allegato illeggibile con `utf-8`." + +#: embedutils\embedutils.py:206 +#, docstring +msgid "Post an embed from a valid YAML file (upload it).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Pubblicare un embed da un file YAML valido (caricarlo).\n\n" +" Se si fornisce un messaggio, questo verrà modificato.\n" +" " + +#: embedutils\embedutils.py:243 +#, docstring +msgid "Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Pubblicare le incorporazioni da un link GitHub/Gist/Pastebin/Hastebin contenente un JSON valido.\n\n" +" Questo deve essere nel formato previsto da [**questa documentazione di Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Ecco un esempio: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Se si fornisce un messaggio, questo verrà modificato.\n" +" " + +#: embedutils\embedutils.py:273 +#, docstring +msgid "Post embed(s) from an existing message.\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Pubblicare gli embed da un messaggio esistente.\n\n" +" Il messaggio deve avere almeno un embed.\n" +" È possibile specificare un indice (a partire da 0) se si desidera inviare solo uno degli incorporamenti.\n" +" Se non viene specificato alcun indice, viene incluso il contenuto del messaggio già inviato.\n\n" +" Se si fornisce un messaggio, questo verrà modificato.\n" +" " + +#: embedutils\embedutils.py:325 +#, docstring +msgid "Download a JSON file for a message's embed(s).\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "Scarica un file JSON per gli incorporamenti di un messaggio.\n\n" +" Il messaggio deve avere almeno un incorporamento.\n" +" È possibile specificare un indice (a partire da 0) se si desidera includere solo uno degli incorporamenti.\n" +" Se non viene specificato alcun indice, viene incluso il contenuto del messaggio già inviato.\n" +" " + +#: embedutils\embedutils.py:385 +#, docstring +msgid "Edit a message sent by [botname].\n\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "Modifica un messaggio inviato da [botname].\n\n" +" Sarebbe meglio usare il parametro `messaggio` di tutti gli altri comandi.\n" +" " + +#: embedutils\embedutils.py:477 +#, docstring +msgid "Store an embed.\n\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "Memorizzare un embed.\n\n" +" Mettere il nome tra virgolette se è composto da più parole.\n" +" L'argomento `locked` specifica se l'embed deve essere bloccato solo per i mod e i superiori (livello gilda) o solo per i proprietari di bot (livello globale).\n" +" " + +#: embedutils\embedutils.py:485 embedutils\embedutils.py:571 +#: embedutils\embedutils.py:586 embedutils\embedutils.py:618 +#: embedutils\embedutils.py:650 +msgid "You can't manage global stored embeds." +msgstr "Non è possibile gestire gli embed memorizzati a livello globale." + +#: embedutils\embedutils.py:548 +msgid "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "Questo server ha raggiunto il limite di incorporazioni di {embed_limit}. È necessario rimuovere un embed con `{ctx.clean_prefix}embed unstore` prima di poterne aggiungere uno nuovo." + +#: embedutils\embedutils.py:567 +#, docstring +msgid "Remove a stored embed." +msgstr "Rimuovere un embed memorizzato." + +#: embedutils\embedutils.py:577 embedutils\embedutils.py:623 +#: embedutils\embedutils.py:655 +msgid "This is not a stored embed at this level." +msgstr "Non si tratta di un embed memorizzato a questo livello." + +#: embedutils\embedutils.py:584 embedutils\embedutils.py:614 +#, docstring +msgid "Get info about a stored embed." +msgstr "Ottenere informazioni su un embed memorizzato." + +#: embedutils\embedutils.py:592 +msgid "No stored embeds is configured at this level." +msgstr "A questo livello non è configurato alcun embed memorizzato." + +#: embedutils\embedutils.py:596 +msgid "Global " +msgstr "Globale " + +#: embedutils\embedutils.py:596 +msgid "Stored Embeds" +msgstr "Embed memorizzati" + +#: embedutils\embedutils.py:646 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "Scarica un file JSON per un embed memorizzato." + +#: embedutils\embedutils.py:675 +#, docstring +msgid "Post stored embeds." +msgstr "Pubblicare gli embed memorizzati." + +#: embedutils\embedutils.py:697 embedutils\embedutils.py:748 +msgid "`{name}` is not a stored embed at this level." +msgstr "`{name}` non è un embed memorizzato a questo livello." + +#: embedutils\embedutils.py:726 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "Pubblicare gli embed memorizzati con un webhook." + +#: embedutils\embedutils.py:793 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "Ottenere il link al cruscotto." + +#: embedutils\embedutils.py:796 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard non è installato. Controllare ." + +#: embedutils\embedutils.py:801 +msgid "You can't access the Dashboard." +msgstr "Non è possibile accedere al Dashboard." + +#: embedutils\embedutils.py:807 +msgid "This third party is disabled on the Dashboard." +msgstr "Questa terza parte è disattivata nella Dashboard." + +#: embedutils\embedutils.py:872 +msgid "Dashboard - " +msgstr "Cruscotto - " + +#: embedutils\embedutils.py:876 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "È possibile creare e inviare rich embed direttamente dalla Dashboard!" + +#: embedutils\embedutils.py:883 +msgid "The URL is too long to be displayed." +msgstr "L'URL è troppo lungo per essere visualizzato." + +#: embedutils\embedutils.py:889 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "Migrare gli embed memorizzati da EmbedUtils di Phen." + +#: embedutils\embedutils.py:933 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "I dati sono stati migrati con successo da EmbedUtils da Phen." + diff --git a/embedutils/locales/ja-JP.po b/embedutils/locales/ja-JP.po new file mode 100644 index 0000000..a625619 --- /dev/null +++ b/embedutils/locales/ja-JP.po @@ -0,0 +1,339 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Japanese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ja\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/embedutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 314\n" +"Language: ja_JP\n" + +#: embedutils\converters.py:66 embedutils\converters.py:81 +msgid "This doesn't seem to be properly formatted embed {conversion_type}. Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "これは適切にフォーマットされていないようです 埋め込み {conversion_type}。{ctx.clean_prefix}help {ctx.command.qualified_name}`のリンクを参照してください。" + +#: embedutils\converters.py:93 +msgid "JSON Parse Error" +msgstr "JSONパースエラー" + +#: embedutils\converters.py:104 +msgid "YAML Parse Error" +msgstr "YAMLパースエラー" + +#: embedutils\converters.py:114 +msgid "The `content` field is not supported for this command." +msgstr "このコマンドでは`content`フィールドはサポートされていません。" + +#: embedutils\converters.py:130 +msgid "Embed Parse Error" +msgstr "エンベッドパースエラー" + +#: embedutils\converters.py:135 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "埋め込みサイズがDiscordの制限である6000文字を超えています ({length})。" + +#: embedutils\converters.py:147 embedutils\embedutils.py:100 +#: embedutils\embedutils.py:133 embedutils\embedutils.py:165 +#: embedutils\embedutils.py:200 embedutils\embedutils.py:231 +#: embedutils\embedutils.py:262 embedutils\embedutils.py:314 +#: embedutils\embedutils.py:440 embedutils\embedutils.py:537 +#: embedutils\embedutils.py:708 embedutils\embedutils.py:762 +msgid "Embed Send Error" +msgstr "埋め込み送信エラー" + +#: embedutils\converters.py:162 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "{ctx.prefix}help {ctx.command.qualified_name}` で例を見ることができる。" + +#: embedutils\converters.py:204 +msgid "Embed limit reached ({limit})." +msgstr "エンベッドリミットに達しました ({limit})。" + +#: embedutils\converters.py:209 +msgid "Failed to convert input into embeds." +msgstr "入力を埋め込みに変換できませんでした。" + +#: embedutils\converters.py:228 +msgid "It's not a valid channel or thread." +msgstr "有効なチャンネルでもスレッドでもない。" + +#: embedutils\converters.py:232 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "{channel.mention}でエンベッドを送信する権限がありません。" + +#: embedutils\converters.py:241 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "{channel.mention}でエンベッドを送信する権限がありません。" + +#: embedutils\converters.py:253 +msgid "I have to be the author of the message. You can use the command without providing a message to send one." +msgstr "私はメッセージの作者でなければなりません。メッセージを送らなくても、このコマンドは使えます。" + +#: embedutils\converters.py:263 +msgid "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "既存のメッセージの埋め込みを編集することはできません(ボットのオーナーはコマンド `[p]embed edit` の Permissions コグで権限を設定できます)。" + +#: embedutils\dashboard_integration.py:53 +msgid "You don't have permissions to access this page." +msgstr "このページにアクセスする権限がありません。" + +#: embedutils\dashboard_integration.py:60 +msgid "I or you don't have permissions to send messages or embeds in any channel in this guild." +msgstr "私またはあなたは、このギルドのどのチャンネルでもメッセージやエンベッドを送信する権限を持っていません。" + +#: embedutils\dashboard_integration.py:76 +msgid "Username:" +msgstr "ユーザー名" + +#: embedutils\dashboard_integration.py:80 +msgid "Avatar URL:" +msgstr "アバターのURL:" + +#: embedutils\dashboard_integration.py:84 +msgid "Data" +msgstr "データ" + +#: embedutils\dashboard_integration.py:91 +msgid "Channels:" +msgstr "チャンネル" + +#: embedutils\dashboard_integration.py:100 +msgid "Send Message(s)" +msgstr "メッセージを送る" + +#: embedutils\dashboard_integration.py:164 +msgid "Message(s) sent successfully!" +msgstr "メッセージは正常に送信されました!" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "Red-Web-Dashboardからもリッチエンベッドを作成、送信、保存できます!" + +#: embedutils\embedutils.py:110 +#, docstring +msgid "Post embeds from valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "有効なJSONから埋め込みを投稿してください。\n\n" +" これは、[**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object) が期待するフォーマットでなければなりません。\n" +" 以下に例を示します:[**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" JSON ペイロードを取得するには、[**embeds creator**](https://embedutils.com/) を使用します。\n\n" +" メッセージを指定すると、それが編集されます。\n" +" 添付ファイルを使用すると、`[p]embed yamlfile`コマンドが自動的に呼び出されます。\n" +" " + +#: embedutils\embedutils.py:143 +#, docstring +msgid "Post embeds from valid YAML.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "有効なYAMLから埋め込みを投稿してください。\n\n" +" これは[**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object)が期待するフォーマットでなければなりません。\n" +" 以下に例を示します:[**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" メッセージを提供すると、編集されます。\n" +" 添付ファイルを使用すると、コマンド `[p]embed yamlfile` が自動的に呼び出されます。\n" +" " + +#: embedutils\embedutils.py:171 +#, docstring +msgid "Post an embed from a valid JSON file (upload it).\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "有効なJSONファイルから埋め込みを投稿してください(アップロードしてください)。\n\n" +" これは、[**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object)が期待するフォーマットでなければなりません。\n" +" 以下に例を示します:[**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" JSON ペイロードを取得するには、[**embeds creator**](https://embedutils.com/) を使用します。\n\n" +" メッセージを提供すると、それが編集されます。\n" +" " + +#: embedutils\embedutils.py:186 embedutils\embedutils.py:217 +#: embedutils\embedutils.py:405 embedutils\embedutils.py:415 +#: embedutils\embedutils.py:505 embedutils\embedutils.py:515 +#: embedutils\embedutils.py:831 embedutils\embedutils.py:843 +msgid "Unreadable attachment with `utf-8`." +msgstr "utf-8`の添付ファイルが読めない。" + +#: embedutils\embedutils.py:206 +#, docstring +msgid "Post an embed from a valid YAML file (upload it).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "有効なYAMLファイルから埋め込みを投稿する(アップロードする)。\n\n" +" メッセージを記入すると編集されます。\n" +" " + +#: embedutils\embedutils.py:243 +#, docstring +msgid "Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "有効なJSONを含むGitHub/Gist/Pastebin/Hastebinリンクから埋め込みを投稿してください。\n\n" +" これは、[**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object) が期待するフォーマットでなければなりません。\n" +" 以下に例を示します:[**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" メッセージを提供した場合、そのメッセージは編集されます。\n" +" " + +#: embedutils\embedutils.py:273 +#, docstring +msgid "Post embed(s) from an existing message.\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "既存のメッセージから埋め込みを投稿する。\n\n" +" メッセージには少なくとも一つの埋め込みが必要です。\n" +" つの埋め込みだけを送信したい場合は、インデックス(0から始まる)を指定することができます。\n" +" インデックスを指定しない場合は、すでに送信されたメッセージの内容が含まれます。\n\n" +" メッセージを指定した場合、そのメッセージは編集されます。\n" +" " + +#: embedutils\embedutils.py:325 +#, docstring +msgid "Download a JSON file for a message's embed(s).\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "メッセージの埋め込み用の JSON ファイルをダウンロードします。\n\n" +" メッセージには少なくとも1つの埋め込みが必要です。\n" +" 埋め込みを1つだけ含めたい場合は、インデックス(0から始まる)を指定できます。\n" +" インデックスを指定しない場合は、すでに送信されたメッセージの内容が含まれます。\n" +" " + +#: embedutils\embedutils.py:385 +#, docstring +msgid "Edit a message sent by [botname].\n\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "botname]が送信したメッセージを編集する。\n\n" +" 他のコマンドの `message` パラメータを使用する方が良いでしょう。\n" +" " + +#: embedutils\embedutils.py:477 +#, docstring +msgid "Store an embed.\n\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "埋め込みを保存する。\n\n" +" 複数単語の場合は引用符で囲む。\n" +" locked`引数は、埋め込みをMODと上長のみ(ギルドレベル)にロックするか、ボットオーナーのみ(グローバルレベル)にロックするかを指定します。\n" +" " + +#: embedutils\embedutils.py:485 embedutils\embedutils.py:571 +#: embedutils\embedutils.py:586 embedutils\embedutils.py:618 +#: embedutils\embedutils.py:650 +msgid "You can't manage global stored embeds." +msgstr "グローバルに保存されたエンベッドを管理することはできません。" + +#: embedutils\embedutils.py:548 +msgid "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "このサーバーは {embed_limit}の埋め込み制限に達しました。新しいembedを追加する前に、`{ctx.clean_prefix}embed unstore`でembedを削除する必要があります。" + +#: embedutils\embedutils.py:567 +#, docstring +msgid "Remove a stored embed." +msgstr "保存されている埋め込みを削除する。" + +#: embedutils\embedutils.py:577 embedutils\embedutils.py:623 +#: embedutils\embedutils.py:655 +msgid "This is not a stored embed at this level." +msgstr "このレベルではストアドの埋め込みはできない。" + +#: embedutils\embedutils.py:584 embedutils\embedutils.py:614 +#, docstring +msgid "Get info about a stored embed." +msgstr "保存された埋め込みに関する情報を取得する。" + +#: embedutils\embedutils.py:592 +msgid "No stored embeds is configured at this level." +msgstr "このレベルではストアド・エンベッドは設定されない。" + +#: embedutils\embedutils.py:596 +msgid "Global " +msgstr "グローバル " + +#: embedutils\embedutils.py:596 +msgid "Stored Embeds" +msgstr "保存されたエンベッド" + +#: embedutils\embedutils.py:646 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "ストアドエンベッドのJSONファイルをダウンロードする。" + +#: embedutils\embedutils.py:675 +#, docstring +msgid "Post stored embeds." +msgstr "保存されたエンベッドを投稿する。" + +#: embedutils\embedutils.py:697 embedutils\embedutils.py:748 +msgid "`{name}` is not a stored embed at this level." +msgstr "{name}`は、このレベルではストアド・エンベッドではない。" + +#: embedutils\embedutils.py:726 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "保存されたエンベッドをウェブフックで投稿する。" + +#: embedutils\embedutils.py:793 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "ダッシュボードへのリンクを取得する。" + +#: embedutils\embedutils.py:796 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard がインストールされていません。を確認してください。" + +#: embedutils\embedutils.py:801 +msgid "You can't access the Dashboard." +msgstr "ダッシュボードにアクセスできません。" + +#: embedutils\embedutils.py:807 +msgid "This third party is disabled on the Dashboard." +msgstr "このサードパーティはダッシュボードでは無効になっている。" + +#: embedutils\embedutils.py:872 +msgid "Dashboard - " +msgstr "ダッシュボード " + +#: embedutils\embedutils.py:876 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "ダッシュボードから直接リッチエンベッドを作成して送信できます!" + +#: embedutils\embedutils.py:883 +msgid "The URL is too long to be displayed." +msgstr "URLが長すぎて表示できません。" + +#: embedutils\embedutils.py:889 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "PhenによってEmbedUtilsから保存されたエンベッドを移行する。" + +#: embedutils\embedutils.py:933 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "PhenによるEmbedUtilsからのデータ移行に成功。" + diff --git a/embedutils/locales/messages.pot b/embedutils/locales/messages.pot new file mode 100644 index 0000000..22d6de0 --- /dev/null +++ b/embedutils/locales/messages.pot @@ -0,0 +1,335 @@ +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2025-03-15 23:04+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" + +#: embedutils\converters.py:68 embedutils\converters.py:83 +msgid "" +"This doesn't seem to be properly formatted embed {conversion_type}. Refer to" +" the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "" + +#: embedutils\converters.py:95 +msgid "JSON Parse Error" +msgstr "" + +#: embedutils\converters.py:106 +msgid "YAML Parse Error" +msgstr "" + +#: embedutils\converters.py:116 +msgid "The `content` field is not supported for this command." +msgstr "" + +#: embedutils\converters.py:136 +msgid "Embed Parse Error" +msgstr "" + +#: embedutils\converters.py:141 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "" + +#: embedutils\converters.py:153 +msgid "Embed Send Error" +msgstr "" + +#: embedutils\converters.py:168 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "" + +#: embedutils\converters.py:210 +msgid "Embed limit reached ({limit})." +msgstr "" + +#: embedutils\converters.py:215 +msgid "Failed to convert input into embeds." +msgstr "" + +#: embedutils\converters.py:234 +msgid "It's not a valid channel or thread." +msgstr "" + +#: embedutils\converters.py:238 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "" + +#: embedutils\converters.py:247 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "" + +#: embedutils\converters.py:259 +msgid "" +"I have to be the author of the message. You can use the command without " +"providing a message to send one." +msgstr "" + +#: embedutils\converters.py:269 +msgid "" +"You are not allowed to edit embeds of an existing message (bot owner can set" +" the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "" + +#: embedutils\dashboard_integration.py:55 +msgid "You don't have permissions to access this page." +msgstr "" + +#: embedutils\dashboard_integration.py:62 +msgid "" +"I or you don't have permissions to send messages or embeds in any channel in" +" this guild." +msgstr "" + +#: embedutils\dashboard_integration.py:78 +msgid "Username:" +msgstr "" + +#: embedutils\dashboard_integration.py:82 +msgid "Avatar URL:" +msgstr "" + +#: embedutils\dashboard_integration.py:86 +msgid "Data" +msgstr "" + +#: embedutils\dashboard_integration.py:93 +msgid "Channels:" +msgstr "" + +#: embedutils\dashboard_integration.py:102 +msgid "Send Message(s)" +msgstr "" + +#: embedutils\dashboard_integration.py:168 +msgid "Message{s} sent successfully!" +msgstr "" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "" + +#: embedutils\embedutils.py:109 embedutils\embedutils.py:152 +#: embedutils\embedutils.py:194 embedutils\embedutils.py:239 +#: embedutils\embedutils.py:280 embedutils\embedutils.py:321 +#: embedutils\embedutils.py:383 embedutils\embedutils.py:509 +#: embedutils\embedutils.py:606 embedutils\embedutils.py:785 +#: embedutils\embedutils.py:839 +msgid "Embed Sending Error" +msgstr "" + +#: embedutils\embedutils.py:119 +#, docstring +msgid "" +"Post embeds from valid JSON.\n" +"\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n" +"\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed fromfile` will be invoked automatically.\n" +" " +msgstr "" + +#: embedutils\embedutils.py:162 +#, docstring +msgid "" +"Post embeds from valid YAML.\n" +"\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +"\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "" + +#: embedutils\embedutils.py:200 +#, docstring +msgid "" +"Post an embed from a valid JSON file (upload it).\n" +"\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n" +"\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "" + +#: embedutils\embedutils.py:215 embedutils\embedutils.py:256 +#: embedutils\embedutils.py:474 embedutils\embedutils.py:484 +#: embedutils\embedutils.py:574 embedutils\embedutils.py:584 +#: embedutils\embedutils.py:908 embedutils\embedutils.py:920 +msgid "Unreadable attachment with `utf-8`." +msgstr "" + +#: embedutils\embedutils.py:245 +#, docstring +msgid "" +"Post an embed from a valid YAML file (upload it).\n" +"\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "" + +#: embedutils\embedutils.py:292 +#, docstring +msgid "" +"Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n" +"\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +"\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "" + +#: embedutils\embedutils.py:332 +#, docstring +msgid "" +"Post embed(s) from an existing message.\n" +"\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +"\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "" + +#: embedutils\embedutils.py:394 +#, docstring +msgid "" +"Download a JSON file for a message's embed(s).\n" +"\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "" + +#: embedutils\embedutils.py:454 +#, docstring +msgid "" +"Edit a message sent by [botname].\n" +"\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "" + +#: embedutils\embedutils.py:546 +#, docstring +msgid "" +"Store an embed.\n" +"\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "" + +#: embedutils\embedutils.py:554 embedutils\embedutils.py:640 +#: embedutils\embedutils.py:655 embedutils\embedutils.py:687 +#: embedutils\embedutils.py:719 +msgid "You can't manage global stored embeds." +msgstr "" + +#: embedutils\embedutils.py:617 +msgid "" +"This server has reached the embed limit of {embed_limit}. You must remove an" +" embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "" + +#: embedutils\embedutils.py:636 +#, docstring +msgid "Remove a stored embed." +msgstr "" + +#: embedutils\embedutils.py:646 embedutils\embedutils.py:692 +#: embedutils\embedutils.py:724 +msgid "This is not a stored embed at this level." +msgstr "" + +#: embedutils\embedutils.py:653 embedutils\embedutils.py:683 +#, docstring +msgid "Get info about a stored embed." +msgstr "" + +#: embedutils\embedutils.py:661 +msgid "No stored embeds is configured at this level." +msgstr "" + +#: embedutils\embedutils.py:665 +msgid "Global " +msgstr "" + +#: embedutils\embedutils.py:665 +msgid "Stored Embeds" +msgstr "" + +#: embedutils\embedutils.py:715 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "" + +#: embedutils\embedutils.py:744 +#, docstring +msgid "Post stored embeds." +msgstr "" + +#: embedutils\embedutils.py:766 embedutils\embedutils.py:825 +msgid "`{name}` is not a stored embed at this level." +msgstr "" + +#: embedutils\embedutils.py:803 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "" + +#: embedutils\embedutils.py:870 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "" + +#: embedutils\embedutils.py:873 +msgid "" +"Red-Web-Dashboard is not installed. Check ." +msgstr "" + +#: embedutils\embedutils.py:878 +msgid "You can't access the Dashboard." +msgstr "" + +#: embedutils\embedutils.py:884 +msgid "This third party is disabled on the Dashboard." +msgstr "" + +#: embedutils\embedutils.py:949 +msgid "Dashboard - " +msgstr "" + +#: embedutils\embedutils.py:954 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "" + +#: embedutils\embedutils.py:961 +msgid "The URL is too long to be displayed." +msgstr "" + +#: embedutils\embedutils.py:971 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "" + +#: embedutils\embedutils.py:1015 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "" diff --git a/embedutils/locales/nl-NL.po b/embedutils/locales/nl-NL.po new file mode 100644 index 0000000..0d1d210 --- /dev/null +++ b/embedutils/locales/nl-NL.po @@ -0,0 +1,339 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Dutch\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: nl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/embedutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 314\n" +"Language: nl_NL\n" + +#: embedutils\converters.py:66 embedutils\converters.py:81 +msgid "This doesn't seem to be properly formatted embed {conversion_type}. Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "Dit lijkt niet goed te zijn geformatteerd embed {conversion_type}. Zie de link op `{ctx.clean_prefix}help {ctx.command.qualified_name}`." + +#: embedutils\converters.py:93 +msgid "JSON Parse Error" +msgstr "Fout bij het parsen van JSON" + +#: embedutils\converters.py:104 +msgid "YAML Parse Error" +msgstr "Fout bij YAML parse" + +#: embedutils\converters.py:114 +msgid "The `content` field is not supported for this command." +msgstr "Het veld `content` wordt niet ondersteund voor deze opdracht." + +#: embedutils\converters.py:130 +msgid "Embed Parse Error" +msgstr "Parseerfout insluiten" + +#: embedutils\converters.py:135 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "Embedgrootte overschrijdt Discord limiet van 6000 tekens ({length})." + +#: embedutils\converters.py:147 embedutils\embedutils.py:100 +#: embedutils\embedutils.py:133 embedutils\embedutils.py:165 +#: embedutils\embedutils.py:200 embedutils\embedutils.py:231 +#: embedutils\embedutils.py:262 embedutils\embedutils.py:314 +#: embedutils\embedutils.py:440 embedutils\embedutils.py:537 +#: embedutils\embedutils.py:708 embedutils\embedutils.py:762 +msgid "Embed Send Error" +msgstr "Embed Verzendfout" + +#: embedutils\converters.py:162 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "Gebruik `{ctx.prefix}help {ctx.command.qualified_name}` om een voorbeeld te zien." + +#: embedutils\converters.py:204 +msgid "Embed limit reached ({limit})." +msgstr "Embedlimiet bereikt ({limit})." + +#: embedutils\converters.py:209 +msgid "Failed to convert input into embeds." +msgstr "Invoer is niet geconverteerd naar embeds." + +#: embedutils\converters.py:228 +msgid "It's not a valid channel or thread." +msgstr "Het is geen geldig kanaal of draad." + +#: embedutils\converters.py:232 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "Ik heb geen rechten om embeds te verzenden in {channel.mention}." + +#: embedutils\converters.py:241 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "Je hebt geen rechten om embeds te verzenden in {channel.mention}." + +#: embedutils\converters.py:253 +msgid "I have to be the author of the message. You can use the command without providing a message to send one." +msgstr "Ik moet de auteur van het bericht zijn. Je kunt het commando gebruiken zonder een bericht op te geven om er een te versturen." + +#: embedutils\converters.py:263 +msgid "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "Het is niet toegestaan om embeds van een bestaand bericht te bewerken (de eigenaar van de bot kan de rechten instellen met het tandwiel Machtigingen op het commando `[p]embed bewerken`)." + +#: embedutils\dashboard_integration.py:53 +msgid "You don't have permissions to access this page." +msgstr "Je hebt geen rechten om deze pagina te openen." + +#: embedutils\dashboard_integration.py:60 +msgid "I or you don't have permissions to send messages or embeds in any channel in this guild." +msgstr "Ik of jij hebben geen rechten om berichten of embeds te sturen in een kanaal in dit gilde." + +#: embedutils\dashboard_integration.py:76 +msgid "Username:" +msgstr "Gebruikersnaam:" + +#: embedutils\dashboard_integration.py:80 +msgid "Avatar URL:" +msgstr "Avatar URL:" + +#: embedutils\dashboard_integration.py:84 +msgid "Data" +msgstr "Gegevens" + +#: embedutils\dashboard_integration.py:91 +msgid "Channels:" +msgstr "Kanalen:" + +#: embedutils\dashboard_integration.py:100 +msgid "Send Message(s)" +msgstr "Bericht(en) verzenden" + +#: embedutils\dashboard_integration.py:164 +msgid "Message(s) sent successfully!" +msgstr "Bericht(en) succesvol verzonden!" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "Maak, verzend en sla rijke embeds op, ook vanuit Red-Web-Dashboard!" + +#: embedutils\embedutils.py:110 +#, docstring +msgid "Post embeds from valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Post embeds van geldige JSON.\n\n" +" Dit moet in het formaat zijn dat verwacht wordt door [**deze Discord documentatie**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Hier is een voorbeeld: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Je kunt een [**embeds creator**](https://embedutils.com/) gebruiken om een JSON payload te krijgen.\n\n" +" Als je een bericht opgeeft, wordt het bewerkt.\n" +" Je kunt een bijlage gebruiken en het commando `[p]embed yamlfile` wordt automatisch aangeroepen.\n" +" " + +#: embedutils\embedutils.py:143 +#, docstring +msgid "Post embeds from valid YAML.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Post embeds van geldige YAML.\n\n" +" Dit moet in het formaat zijn dat verwacht wordt door [**deze Discord documentatie**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Hier is een voorbeeld: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Als je een bericht opgeeft, wordt het bewerkt.\n" +" Je kunt een bijlage gebruiken en het commando `[p]embed yamlfile` wordt automatisch aangeroepen.\n" +" " + +#: embedutils\embedutils.py:171 +#, docstring +msgid "Post an embed from a valid JSON file (upload it).\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Post een embed van een geldig JSON-bestand (upload het).\n\n" +" Dit moet in het formaat zijn dat verwacht wordt door [**deze Discord documentatie**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Hier is een voorbeeld: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Je kunt een [**embeds creator**](https://embedutils.com/) gebruiken om een JSON payload te krijgen.\n\n" +" Als je een bericht opgeeft, wordt het bewerkt.\n" +" " + +#: embedutils\embedutils.py:186 embedutils\embedutils.py:217 +#: embedutils\embedutils.py:405 embedutils\embedutils.py:415 +#: embedutils\embedutils.py:505 embedutils\embedutils.py:515 +#: embedutils\embedutils.py:831 embedutils\embedutils.py:843 +msgid "Unreadable attachment with `utf-8`." +msgstr "Onleesbare bijlage met `utf-8`." + +#: embedutils\embedutils.py:206 +#, docstring +msgid "Post an embed from a valid YAML file (upload it).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Plaats een insluiting vanuit een geldig YAML-bestand (upload het).\n\n" +" Als je een bericht opgeeft, wordt het bewerkt.\n" +" " + +#: embedutils\embedutils.py:243 +#, docstring +msgid "Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Post embeds van een GitHub/Gist/Pastebin/Hastebin link die geldige JSON bevat.\n\n" +" Dit moet in het formaat zijn dat verwacht wordt door [**deze Discord documentatie**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Hier is een voorbeeld: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Als je een bericht opgeeft, wordt het bewerkt.\n" +" " + +#: embedutils\embedutils.py:273 +#, docstring +msgid "Post embed(s) from an existing message.\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Plaats embed(s) van een bestaand bericht.\n\n" +" Het bericht moet ten minste één embed hebben.\n" +" Je kunt een index opgeven (beginnend met 0) als je slechts één van de embedden wilt verzenden.\n" +" De inhoud van het reeds verzonden bericht wordt meegenomen als er geen index is opgegeven.\n\n" +" Als je een bericht opgeeft, wordt het bewerkt.\n" +" " + +#: embedutils\embedutils.py:325 +#, docstring +msgid "Download a JSON file for a message's embed(s).\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "Download een JSON-bestand voor de insluitingen van een bericht.\n\n" +" Het bericht moet ten minste één embed hebben.\n" +" Je kunt een index opgeven (beginnend met 0) als je slechts één van de embedden wilt opnemen.\n" +" De inhoud van het reeds verzonden bericht wordt meegenomen als er geen index is opgegeven.\n" +" " + +#: embedutils\embedutils.py:385 +#, docstring +msgid "Edit a message sent by [botname].\n\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "Bewerk een bericht verzonden door [botnaam].\n\n" +" Het is beter om de `message` parameter van alle andere commando's te gebruiken.\n" +" " + +#: embedutils\embedutils.py:477 +#, docstring +msgid "Store an embed.\n\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "Sla een insluiting op.\n\n" +" Zet de naam tussen aanhalingstekens als het uit meerdere woorden bestaat.\n" +" Het `vergrendeld` argument geeft aan of de insluiting alleen voor mod en overste (gilde niveau) of alleen voor bot eigenaren (globaal niveau) vergrendeld moet worden.\n" +" " + +#: embedutils\embedutils.py:485 embedutils\embedutils.py:571 +#: embedutils\embedutils.py:586 embedutils\embedutils.py:618 +#: embedutils\embedutils.py:650 +msgid "You can't manage global stored embeds." +msgstr "Je kunt wereldwijd opgeslagen embeds niet beheren." + +#: embedutils\embedutils.py:548 +msgid "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "Deze server heeft de embedlimiet van {embed_limit}bereikt. Je moet een embed verwijderen met `{ctx.clean_prefix}embed unstore` voordat je een nieuwe kunt toevoegen." + +#: embedutils\embedutils.py:567 +#, docstring +msgid "Remove a stored embed." +msgstr "Een opgeslagen insluiting verwijderen." + +#: embedutils\embedutils.py:577 embedutils\embedutils.py:623 +#: embedutils\embedutils.py:655 +msgid "This is not a stored embed at this level." +msgstr "Dit is geen opgeslagen insluiting op dit niveau." + +#: embedutils\embedutils.py:584 embedutils\embedutils.py:614 +#, docstring +msgid "Get info about a stored embed." +msgstr "Info ophalen over een opgeslagen embed." + +#: embedutils\embedutils.py:592 +msgid "No stored embeds is configured at this level." +msgstr "Op dit niveau zijn geen opgeslagen embeds geconfigureerd." + +#: embedutils\embedutils.py:596 +msgid "Global " +msgstr "Wereldwijd " + +#: embedutils\embedutils.py:596 +msgid "Stored Embeds" +msgstr "Opgeslagen insluitingen" + +#: embedutils\embedutils.py:646 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "Download een JSON-bestand voor een opgeslagen insluiting." + +#: embedutils\embedutils.py:675 +#, docstring +msgid "Post stored embeds." +msgstr "Opgeslagen embeds plaatsen." + +#: embedutils\embedutils.py:697 embedutils\embedutils.py:748 +msgid "`{name}` is not a stored embed at this level." +msgstr "`{name}` is geen opgeslagen insluiting op dit niveau." + +#: embedutils\embedutils.py:726 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "Post opgeslagen embeds met een webhook." + +#: embedutils\embedutils.py:793 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "Krijg de link naar het dashboard." + +#: embedutils\embedutils.py:796 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard is niet geïnstalleerd. Controleer ." + +#: embedutils\embedutils.py:801 +msgid "You can't access the Dashboard." +msgstr "Je hebt geen toegang tot het dashboard." + +#: embedutils\embedutils.py:807 +msgid "This third party is disabled on the Dashboard." +msgstr "Deze derde partij is uitgeschakeld op het dashboard." + +#: embedutils\embedutils.py:872 +msgid "Dashboard - " +msgstr "Dashboard - " + +#: embedutils\embedutils.py:876 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "Je kunt rijke embeds rechtstreeks vanuit het dashboard maken en verzenden!" + +#: embedutils\embedutils.py:883 +msgid "The URL is too long to be displayed." +msgstr "De URL is te lang om te worden weergegeven." + +#: embedutils\embedutils.py:889 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "Migreer opgeslagen embeds van EmbedUtils door Phen." + +#: embedutils\embedutils.py:933 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "Gegevens succesvol gemigreerd van EmbedUtils door Phen." + diff --git a/embedutils/locales/pl-PL.po b/embedutils/locales/pl-PL.po new file mode 100644 index 0000000..5b3dcec --- /dev/null +++ b/embedutils/locales/pl-PL.po @@ -0,0 +1,339 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Polish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pl\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/embedutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 314\n" +"Language: pl_PL\n" + +#: embedutils\converters.py:66 embedutils\converters.py:81 +msgid "This doesn't seem to be properly formatted embed {conversion_type}. Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "Wydaje się, że nie jest to poprawnie sformatowany embed {conversion_type}. Odnieś się do linku na stronie `{ctx.clean_prefix}help {ctx.command.qualified_name}`." + +#: embedutils\converters.py:93 +msgid "JSON Parse Error" +msgstr "Błąd analizy JSON" + +#: embedutils\converters.py:104 +msgid "YAML Parse Error" +msgstr "Błąd analizy YAML" + +#: embedutils\converters.py:114 +msgid "The `content` field is not supported for this command." +msgstr "Pole `content` nie jest obsługiwane dla tej komendy." + +#: embedutils\converters.py:130 +msgid "Embed Parse Error" +msgstr "Błąd parsowania osadzania" + +#: embedutils\converters.py:135 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "Rozmiar osadzenia przekracza limit Discord wynoszący 6000 znaków ({length})." + +#: embedutils\converters.py:147 embedutils\embedutils.py:100 +#: embedutils\embedutils.py:133 embedutils\embedutils.py:165 +#: embedutils\embedutils.py:200 embedutils\embedutils.py:231 +#: embedutils\embedutils.py:262 embedutils\embedutils.py:314 +#: embedutils\embedutils.py:440 embedutils\embedutils.py:537 +#: embedutils\embedutils.py:708 embedutils\embedutils.py:762 +msgid "Embed Send Error" +msgstr "Błąd wysyłania osadzania" + +#: embedutils\converters.py:162 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "Użyj `{ctx.prefix}help {ctx.command.qualified_name}`, aby zobaczyć przykład." + +#: embedutils\converters.py:204 +msgid "Embed limit reached ({limit})." +msgstr "Osiągnięto limit osadzania ({limit})." + +#: embedutils\converters.py:209 +msgid "Failed to convert input into embeds." +msgstr "Nie udało się przekonwertować danych wejściowych na embedy." + +#: embedutils\converters.py:228 +msgid "It's not a valid channel or thread." +msgstr "To nie jest ważny kanał ani wątek." + +#: embedutils\converters.py:232 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "Nie mam uprawnień do wysyłania embedów w {channel.mention}." + +#: embedutils\converters.py:241 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "Nie masz uprawnień do wysyłania embedów w {channel.mention}." + +#: embedutils\converters.py:253 +msgid "I have to be the author of the message. You can use the command without providing a message to send one." +msgstr "Muszę być autorem wiadomości. Możesz użyć polecenia bez podawania wiadomości, aby ją wysłać." + +#: embedutils\converters.py:263 +msgid "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "Nie możesz edytować embedów istniejącej wiadomości (właściciel bota może ustawić uprawnienia za pomocą opcji Uprawnienia na komendzie `[p]embed edit`)." + +#: embedutils\dashboard_integration.py:53 +msgid "You don't have permissions to access this page." +msgstr "Nie masz uprawnień dostępu do tej strony." + +#: embedutils\dashboard_integration.py:60 +msgid "I or you don't have permissions to send messages or embeds in any channel in this guild." +msgstr "Ani ja, ani ty nie mamy uprawnień do wysyłania wiadomości lub embedów na żadnym kanale w tej gildii." + +#: embedutils\dashboard_integration.py:76 +msgid "Username:" +msgstr "Nazwa użytkownika:" + +#: embedutils\dashboard_integration.py:80 +msgid "Avatar URL:" +msgstr "Adres URL awatara:" + +#: embedutils\dashboard_integration.py:84 +msgid "Data" +msgstr "Dane" + +#: embedutils\dashboard_integration.py:91 +msgid "Channels:" +msgstr "Kanały:" + +#: embedutils\dashboard_integration.py:100 +msgid "Send Message(s)" +msgstr "Wyślij wiadomość(i)" + +#: embedutils\dashboard_integration.py:164 +msgid "Message(s) sent successfully!" +msgstr "Wiadomość wysłana pomyślnie!" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "Twórz, wysyłaj i przechowuj bogate osadzenia również z Red-Web-Dashboard!" + +#: embedutils\embedutils.py:110 +#, docstring +msgid "Post embeds from valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Osadzanie postów z poprawnego JSON.\n\n" +" Musi to być format oczekiwany przez [**dokumentację Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Oto przykład: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Możesz użyć [**embeds creator**](https://embedutils.com/), aby uzyskać ładunek JSON.\n\n" +" Jeśli podasz wiadomość, zostanie ona edytowana.\n" +" Możesz użyć załącznika, a polecenie `[p]embed yamlfile` zostanie wywołane automatycznie.\n" +" " + +#: embedutils\embedutils.py:143 +#, docstring +msgid "Post embeds from valid YAML.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Osadzanie postów z poprawnego YAML.\n\n" +" Musi to być format oczekiwany przez [**dokumentację Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Oto przykład: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Jeśli podasz wiadomość, zostanie ona edytowana.\n" +" Możesz użyć załącznika, a polecenie `[p]embed yamlfile` zostanie wywołane automatycznie.\n" +" " + +#: embedutils\embedutils.py:171 +#, docstring +msgid "Post an embed from a valid JSON file (upload it).\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Opublikuj osadzenie z poprawnego pliku JSON (prześlij go).\n\n" +" Musi on być w formacie oczekiwanym przez [**dokumentację Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Oto przykład: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Możesz użyć [**embeds creator**](https://embedutils.com/), aby uzyskać ładunek JSON.\n\n" +" Jeśli podasz wiadomość, zostanie ona edytowana.\n" +" " + +#: embedutils\embedutils.py:186 embedutils\embedutils.py:217 +#: embedutils\embedutils.py:405 embedutils\embedutils.py:415 +#: embedutils\embedutils.py:505 embedutils\embedutils.py:515 +#: embedutils\embedutils.py:831 embedutils\embedutils.py:843 +msgid "Unreadable attachment with `utf-8`." +msgstr "Nieczytelny załącznik z `utf-8`." + +#: embedutils\embedutils.py:206 +#, docstring +msgid "Post an embed from a valid YAML file (upload it).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Opublikuj osadzenie z ważnego pliku YAML (prześlij go).\n\n" +" Jeśli podasz wiadomość, zostanie ona edytowana.\n" +" " + +#: embedutils\embedutils.py:243 +#, docstring +msgid "Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Opublikuj osadzanie z linku GitHub/Gist/Pastebin/Hastebin zawierającego prawidłowy JSON.\n\n" +" Musi on być w formacie oczekiwanym przez [**dokumentację Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Oto przykład: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Jeśli podasz wiadomość, zostanie ona edytowana.\n" +" " + +#: embedutils\embedutils.py:273 +#, docstring +msgid "Post embed(s) from an existing message.\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Opublikuj osadzone elementy z istniejącej wiadomości.\n\n" +" Wiadomość musi zawierać co najmniej jedno osadzenie.\n" +" Możesz określić indeks (zaczynając od 0), jeśli chcesz wysłać tylko jeden z osadzonych elementów.\n" +" Treść już wysłanej wiadomości jest dołączana, jeśli nie określono indeksu.\n\n" +" Jeśli podasz wiadomość, zostanie ona edytowana.\n" +" " + +#: embedutils\embedutils.py:325 +#, docstring +msgid "Download a JSON file for a message's embed(s).\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "Pobiera plik JSON dla osadzonych elementów wiadomości.\n\n" +" Wiadomość musi mieć co najmniej jedno osadzenie.\n" +" Możesz określić indeks (zaczynając od 0), jeśli chcesz dołączyć tylko jedno z osadzeń.\n" +" Treść już wysłanej wiadomości jest dołączana, jeśli nie określono indeksu.\n" +" " + +#: embedutils\embedutils.py:385 +#, docstring +msgid "Edit a message sent by [botname].\n\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "Edytuj wiadomość wysłaną przez [botname].\n\n" +" Lepiej byłoby użyć parametru `message` we wszystkich innych poleceniach.\n" +" " + +#: embedutils\embedutils.py:477 +#, docstring +msgid "Store an embed.\n\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "Zapisz osadzenie.\n\n" +" Umieść nazwę w cudzysłowie, jeśli składa się z wielu słów.\n" +" Argument `locked` określa, czy osadzenie powinno być zablokowane tylko dla modów i przełożonych (poziom gildii), czy tylko dla właścicieli botów (poziom globalny).\n" +" " + +#: embedutils\embedutils.py:485 embedutils\embedutils.py:571 +#: embedutils\embedutils.py:586 embedutils\embedutils.py:618 +#: embedutils\embedutils.py:650 +msgid "You can't manage global stored embeds." +msgstr "Nie można zarządzać globalnymi zapisanymi osadzeniami." + +#: embedutils\embedutils.py:548 +msgid "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "Ten serwer osiągnął limit osadzania {embed_limit}. Musisz usunąć osadzenie za pomocą `{ctx.clean_prefix}embed unstore` zanim będziesz mógł dodać nowe." + +#: embedutils\embedutils.py:567 +#, docstring +msgid "Remove a stored embed." +msgstr "Usunięcie zapisanego osadzenia." + +#: embedutils\embedutils.py:577 embedutils\embedutils.py:623 +#: embedutils\embedutils.py:655 +msgid "This is not a stored embed at this level." +msgstr "Na tym poziomie nie jest to przechowywane osadzenie." + +#: embedutils\embedutils.py:584 embedutils\embedutils.py:614 +#, docstring +msgid "Get info about a stored embed." +msgstr "Uzyskaj informacje o zapisanym osadzeniu." + +#: embedutils\embedutils.py:592 +msgid "No stored embeds is configured at this level." +msgstr "Na tym poziomie nie skonfigurowano żadnych zapisanych osadzeń." + +#: embedutils\embedutils.py:596 +msgid "Global " +msgstr "Globalny " + +#: embedutils\embedutils.py:596 +msgid "Stored Embeds" +msgstr "Zapisane osadzenia" + +#: embedutils\embedutils.py:646 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "Pobranie pliku JSON dla zapisanego osadzenia." + +#: embedutils\embedutils.py:675 +#, docstring +msgid "Post stored embeds." +msgstr "Publikowanie zapisanych osadzeń." + +#: embedutils\embedutils.py:697 embedutils\embedutils.py:748 +msgid "`{name}` is not a stored embed at this level." +msgstr "`{name}` nie jest przechowywanym embedem na tym poziomie." + +#: embedutils\embedutils.py:726 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "Publikowanie zapisanych elementów za pomocą webhooka." + +#: embedutils\embedutils.py:793 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "Pobierz łącze do pulpitu nawigacyjnego." + +#: embedutils\embedutils.py:796 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard nie jest zainstalowany. Sprawdź ." + +#: embedutils\embedutils.py:801 +msgid "You can't access the Dashboard." +msgstr "Nie można uzyskać dostępu do pulpitu nawigacyjnego." + +#: embedutils\embedutils.py:807 +msgid "This third party is disabled on the Dashboard." +msgstr "Ta strona trzecia jest wyłączona na pulpicie nawigacyjnym." + +#: embedutils\embedutils.py:872 +msgid "Dashboard - " +msgstr "Dashboard - " + +#: embedutils\embedutils.py:876 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "Możesz tworzyć i wysyłać bogate osadzenia bezpośrednio z pulpitu nawigacyjnego!" + +#: embedutils\embedutils.py:883 +msgid "The URL is too long to be displayed." +msgstr "Adres URL jest zbyt długi, aby go wyświetlić." + +#: embedutils\embedutils.py:889 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "Migracja zapisanych osadzeń z EmbedUtils przez Phen." + +#: embedutils\embedutils.py:933 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "Dane zostały pomyślnie zmigrowane z EmbedUtils przez Phen." + diff --git a/embedutils/locales/pt-BR.po b/embedutils/locales/pt-BR.po new file mode 100644 index 0000000..76a8e42 --- /dev/null +++ b/embedutils/locales/pt-BR.po @@ -0,0 +1,339 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:17\n" +"Last-Translator: \n" +"Language-Team: Portuguese, Brazilian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-BR\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/embedutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 314\n" +"Language: pt_BR\n" + +#: embedutils\converters.py:66 embedutils\converters.py:81 +msgid "This doesn't seem to be properly formatted embed {conversion_type}. Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "Parece que essa incorporação não está formatada corretamente {conversion_type}. Consulte o link em `{ctx.clean_prefix}help {ctx.command.qualified_name}`." + +#: embedutils\converters.py:93 +msgid "JSON Parse Error" +msgstr "Erro de análise de JSON" + +#: embedutils\converters.py:104 +msgid "YAML Parse Error" +msgstr "Erro de análise de YAML" + +#: embedutils\converters.py:114 +msgid "The `content` field is not supported for this command." +msgstr "O campo `content` não é compatível com esse comando." + +#: embedutils\converters.py:130 +msgid "Embed Parse Error" +msgstr "Erro de análise de incorporação" + +#: embedutils\converters.py:135 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "O tamanho da incorporação excede o limite do Discord de 6000 caracteres ({length})." + +#: embedutils\converters.py:147 embedutils\embedutils.py:100 +#: embedutils\embedutils.py:133 embedutils\embedutils.py:165 +#: embedutils\embedutils.py:200 embedutils\embedutils.py:231 +#: embedutils\embedutils.py:262 embedutils\embedutils.py:314 +#: embedutils\embedutils.py:440 embedutils\embedutils.py:537 +#: embedutils\embedutils.py:708 embedutils\embedutils.py:762 +msgid "Embed Send Error" +msgstr "Erro de envio de incorporação" + +#: embedutils\converters.py:162 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "Use `{ctx.prefix}help {ctx.command.qualified_name}` para ver um exemplo." + +#: embedutils\converters.py:204 +msgid "Embed limit reached ({limit})." +msgstr "Limite de incorporação atingido ({limit})." + +#: embedutils\converters.py:209 +msgid "Failed to convert input into embeds." +msgstr "Falha ao converter a entrada em incorporações." + +#: embedutils\converters.py:228 +msgid "It's not a valid channel or thread." +msgstr "Não é um canal ou tópico válido." + +#: embedutils\converters.py:232 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "Não tenho permissões para enviar incorporações em {channel.mention}." + +#: embedutils\converters.py:241 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "Você não tem permissão para enviar incorporações em {channel.mention}." + +#: embedutils\converters.py:253 +msgid "I have to be the author of the message. You can use the command without providing a message to send one." +msgstr "Eu preciso ser o autor da mensagem. Você pode usar o comando sem fornecer uma mensagem para enviar uma." + +#: embedutils\converters.py:263 +msgid "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "Você não tem permissão para editar incorporações de uma mensagem existente (o proprietário do bot pode definir as permissões com a engrenagem Permissions no comando `[p]embed edit`)." + +#: embedutils\dashboard_integration.py:53 +msgid "You don't have permissions to access this page." +msgstr "Você não tem permissão para acessar esta página." + +#: embedutils\dashboard_integration.py:60 +msgid "I or you don't have permissions to send messages or embeds in any channel in this guild." +msgstr "Eu ou você não temos permissões para enviar mensagens ou incorporações em nenhum canal desta guilda." + +#: embedutils\dashboard_integration.py:76 +msgid "Username:" +msgstr "Nome de usuário:" + +#: embedutils\dashboard_integration.py:80 +msgid "Avatar URL:" +msgstr "URL do avatar:" + +#: embedutils\dashboard_integration.py:84 +msgid "Data" +msgstr "Dados" + +#: embedutils\dashboard_integration.py:91 +msgid "Channels:" +msgstr "Canais:" + +#: embedutils\dashboard_integration.py:100 +msgid "Send Message(s)" +msgstr "Enviar mensagem(ns)" + +#: embedutils\dashboard_integration.py:164 +msgid "Message(s) sent successfully!" +msgstr "Mensagem(s) enviada(s) com sucesso!" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "Crie, envie e armazene incorporações avançadas também a partir do Red-Web-Dashboard!" + +#: embedutils\embedutils.py:110 +#, docstring +msgid "Post embeds from valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Publique incorporações de JSON válido.\n\n" +" Isso deve estar no formato esperado por [**esta documentação do Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Aqui está um exemplo: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Você pode usar um [**embeds creator**] (https://embedutils.com/) para obter uma carga útil JSON.\n\n" +" Se você fornecer uma mensagem, ela será editada.\n" +" Você pode usar um anexo e o comando `[p]embed yamlfile` será chamado automaticamente.\n" +" " + +#: embedutils\embedutils.py:143 +#, docstring +msgid "Post embeds from valid YAML.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Postar incorporações de YAML válido.\n\n" +" Isso deve estar no formato esperado por [**esta documentação do Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Aqui está um exemplo: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Se você fornecer uma mensagem, ela será editada.\n" +" Você pode usar um anexo e o comando `[p]embed yamlfile` será chamado automaticamente.\n" +" " + +#: embedutils\embedutils.py:171 +#, docstring +msgid "Post an embed from a valid JSON file (upload it).\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Publique uma incorporação de um arquivo JSON válido (carregue-o).\n\n" +" Ele deve estar no formato esperado por [**esta documentação do Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Aqui está um exemplo: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Você pode usar um [**embeds creator**] (https://embedutils.com/) para obter uma carga útil JSON.\n\n" +" Se você fornecer uma mensagem, ela será editada.\n" +" " + +#: embedutils\embedutils.py:186 embedutils\embedutils.py:217 +#: embedutils\embedutils.py:405 embedutils\embedutils.py:415 +#: embedutils\embedutils.py:505 embedutils\embedutils.py:515 +#: embedutils\embedutils.py:831 embedutils\embedutils.py:843 +msgid "Unreadable attachment with `utf-8`." +msgstr "Anexo ilegível com `utf-8`." + +#: embedutils\embedutils.py:206 +#, docstring +msgid "Post an embed from a valid YAML file (upload it).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Publique uma incorporação de um arquivo YAML válido (carregue-o).\n\n" +" Se você fornecer uma mensagem, ela será editada.\n" +" " + +#: embedutils\embedutils.py:243 +#, docstring +msgid "Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Publique incorporações de um link do GitHub/Gist/Pastebin/Hastebin contendo JSON válido.\n\n" +" Ele deve estar no formato esperado por [**esta documentação do Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Aqui está um exemplo: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Se você fornecer uma mensagem, ela será editada.\n" +" " + +#: embedutils\embedutils.py:273 +#, docstring +msgid "Post embed(s) from an existing message.\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Publique incorporação(ões) de uma mensagem existente.\n\n" +" A mensagem deve ter pelo menos uma incorporação.\n" +" Você pode especificar um índice (começando por 0) se quiser enviar apenas uma das incorporações.\n" +" O conteúdo da mensagem já enviada será incluído se nenhum índice for especificado.\n\n" +" Se você fornecer uma mensagem, ela será editada.\n" +" " + +#: embedutils\embedutils.py:325 +#, docstring +msgid "Download a JSON file for a message's embed(s).\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "Baixe um arquivo JSON para a(s) incorporação(ões) de uma mensagem.\n\n" +" A mensagem deve ter pelo menos uma incorporação.\n" +" Você pode especificar um índice (começando por 0) se quiser incluir apenas uma das incorporações.\n" +" O conteúdo da mensagem já enviada será incluído se nenhum índice for especificado.\n" +" " + +#: embedutils\embedutils.py:385 +#, docstring +msgid "Edit a message sent by [botname].\n\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "Edita uma mensagem enviada por [botname].\n\n" +" Seria melhor usar o parâmetro `message` de todos os outros comandos.\n" +" " + +#: embedutils\embedutils.py:477 +#, docstring +msgid "Store an embed.\n\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "Armazene uma incorporação.\n\n" +" Coloque o nome entre aspas se ele tiver várias palavras.\n" +" O argumento `locked` especifica se a incorporação deve ser bloqueada apenas para mods e superiores (nível de guilda) ou apenas para proprietários de bots (nível global).\n" +" " + +#: embedutils\embedutils.py:485 embedutils\embedutils.py:571 +#: embedutils\embedutils.py:586 embedutils\embedutils.py:618 +#: embedutils\embedutils.py:650 +msgid "You can't manage global stored embeds." +msgstr "Não é possível gerenciar incorporações globais armazenadas." + +#: embedutils\embedutils.py:548 +msgid "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "Este servidor atingiu o limite de incorporações de {embed_limit}. Você deve remover uma incorporação com `{ctx.clean_prefix}embed unstore` antes de poder adicionar uma nova." + +#: embedutils\embedutils.py:567 +#, docstring +msgid "Remove a stored embed." +msgstr "Remover uma incorporação armazenada." + +#: embedutils\embedutils.py:577 embedutils\embedutils.py:623 +#: embedutils\embedutils.py:655 +msgid "This is not a stored embed at this level." +msgstr "Essa não é uma incorporação armazenada nesse nível." + +#: embedutils\embedutils.py:584 embedutils\embedutils.py:614 +#, docstring +msgid "Get info about a stored embed." +msgstr "Obter informações sobre uma incorporação armazenada." + +#: embedutils\embedutils.py:592 +msgid "No stored embeds is configured at this level." +msgstr "Nenhuma incorporação armazenada é configurada nesse nível." + +#: embedutils\embedutils.py:596 +msgid "Global " +msgstr "Global " + +#: embedutils\embedutils.py:596 +msgid "Stored Embeds" +msgstr "Incorporações armazenadas" + +#: embedutils\embedutils.py:646 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "Baixe um arquivo JSON para uma incorporação armazenada." + +#: embedutils\embedutils.py:675 +#, docstring +msgid "Post stored embeds." +msgstr "Publique incorporações armazenadas." + +#: embedutils\embedutils.py:697 embedutils\embedutils.py:748 +msgid "`{name}` is not a stored embed at this level." +msgstr "`{name}` não é uma incorporação armazenada nesse nível." + +#: embedutils\embedutils.py:726 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "Publique incorporações armazenadas com um webhook." + +#: embedutils\embedutils.py:793 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "Obter o link para o Dashboard." + +#: embedutils\embedutils.py:796 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "O Red-Web-Dashboard não está instalado. Verifique ." + +#: embedutils\embedutils.py:801 +msgid "You can't access the Dashboard." +msgstr "Não é possível acessar o Dashboard." + +#: embedutils\embedutils.py:807 +msgid "This third party is disabled on the Dashboard." +msgstr "Esse terceiro está desativado no Dashboard." + +#: embedutils\embedutils.py:872 +msgid "Dashboard - " +msgstr "Painel de controle - " + +#: embedutils\embedutils.py:876 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "Você pode criar e enviar incorporações avançadas diretamente do Dashboard!" + +#: embedutils\embedutils.py:883 +msgid "The URL is too long to be displayed." +msgstr "O URL é muito longo para ser exibido." + +#: embedutils\embedutils.py:889 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "Migrar incorporações armazenadas do EmbedUtils por Phen." + +#: embedutils\embedutils.py:933 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "Dados migrados com sucesso do EmbedUtils pelo Phen." + diff --git a/embedutils/locales/pt-PT.po b/embedutils/locales/pt-PT.po new file mode 100644 index 0000000..988d92e --- /dev/null +++ b/embedutils/locales/pt-PT.po @@ -0,0 +1,339 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:17\n" +"Last-Translator: \n" +"Language-Team: Portuguese\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: pt-PT\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/embedutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 314\n" +"Language: pt_PT\n" + +#: embedutils\converters.py:66 embedutils\converters.py:81 +msgid "This doesn't seem to be properly formatted embed {conversion_type}. Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "Parece que esta incorporação não está corretamente formatada {conversion_type}. Consulte a ligação em `{ctx.clean_prefix}help {ctx.command.qualified_name}`." + +#: embedutils\converters.py:93 +msgid "JSON Parse Error" +msgstr "Erro de análise JSON" + +#: embedutils\converters.py:104 +msgid "YAML Parse Error" +msgstr "Erro de análise YAML" + +#: embedutils\converters.py:114 +msgid "The `content` field is not supported for this command." +msgstr "O campo `content` não é suportado por este comando." + +#: embedutils\converters.py:130 +msgid "Embed Parse Error" +msgstr "Incorporar erro de análise" + +#: embedutils\converters.py:135 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "O tamanho da incorporação excede o limite de 6000 caracteres do Discord ({length})." + +#: embedutils\converters.py:147 embedutils\embedutils.py:100 +#: embedutils\embedutils.py:133 embedutils\embedutils.py:165 +#: embedutils\embedutils.py:200 embedutils\embedutils.py:231 +#: embedutils\embedutils.py:262 embedutils\embedutils.py:314 +#: embedutils\embedutils.py:440 embedutils\embedutils.py:537 +#: embedutils\embedutils.py:708 embedutils\embedutils.py:762 +msgid "Embed Send Error" +msgstr "Erro de envio de incorporação" + +#: embedutils\converters.py:162 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "Utilize `{ctx.prefix}help {ctx.command.qualified_name}` para ver um exemplo." + +#: embedutils\converters.py:204 +msgid "Embed limit reached ({limit})." +msgstr "Limite de incorporação atingido ({limit})." + +#: embedutils\converters.py:209 +msgid "Failed to convert input into embeds." +msgstr "Falha ao converter a entrada em incorporações." + +#: embedutils\converters.py:228 +msgid "It's not a valid channel or thread." +msgstr "Não é um canal ou um tópico válido." + +#: embedutils\converters.py:232 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "Não tenho permissões para enviar ficheiros incorporados em {channel.mention}." + +#: embedutils\converters.py:241 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "Não tem permissões para enviar incorporações em {channel.mention}." + +#: embedutils\converters.py:253 +msgid "I have to be the author of the message. You can use the command without providing a message to send one." +msgstr "Tenho de ser o autor da mensagem. Pode utilizar o comando sem fornecer uma mensagem para a enviar." + +#: embedutils\converters.py:263 +msgid "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "Não é permitido editar incorporações de uma mensagem existente (o proprietário do bot pode definir as permissões com a engrenagem Permissões no comando `[p]embed edit`)." + +#: embedutils\dashboard_integration.py:53 +msgid "You don't have permissions to access this page." +msgstr "Não tem permissões para aceder a esta página." + +#: embedutils\dashboard_integration.py:60 +msgid "I or you don't have permissions to send messages or embeds in any channel in this guild." +msgstr "Eu ou tu não temos permissões para enviar mensagens ou embeds em nenhum canal desta guilda." + +#: embedutils\dashboard_integration.py:76 +msgid "Username:" +msgstr "Nome de utilizador:" + +#: embedutils\dashboard_integration.py:80 +msgid "Avatar URL:" +msgstr "URL do avatar:" + +#: embedutils\dashboard_integration.py:84 +msgid "Data" +msgstr "Dados" + +#: embedutils\dashboard_integration.py:91 +msgid "Channels:" +msgstr "Canais:" + +#: embedutils\dashboard_integration.py:100 +msgid "Send Message(s)" +msgstr "Enviar mensagem(ns)" + +#: embedutils\dashboard_integration.py:164 +msgid "Message(s) sent successfully!" +msgstr "Mensagem(s) enviada(s) com sucesso!" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "Crie, envie e armazene incorporações avançadas também a partir do Red-Web-Dashboard!" + +#: embedutils\embedutils.py:110 +#, docstring +msgid "Post embeds from valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Publique incorporações de JSON válido.\n\n" +" Isso deve estar no formato esperado por [**esta documentação do Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Aqui está um exemplo: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Pode utilizar um [**embeds creator**](https://embedutils.com/) para obter um payload JSON.\n\n" +" Se fornecer uma mensagem, esta será editada.\n" +" Pode utilizar um anexo e o comando `[p]embed yamlfile` será invocado automaticamente.\n" +" " + +#: embedutils\embedutils.py:143 +#, docstring +msgid "Post embeds from valid YAML.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Publica incorporações a partir de YAML válido.\n\n" +" Isso deve estar no formato esperado por [**esta documentação do Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Aqui está um exemplo: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Se fornecer uma mensagem, esta será editada.\n" +" Pode usar um anexo e o comando `[p]embed yamlfile` será invocado automaticamente.\n" +" " + +#: embedutils\embedutils.py:171 +#, docstring +msgid "Post an embed from a valid JSON file (upload it).\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Publique uma incorporação a partir de um ficheiro JSON válido (carregue-o).\n\n" +" Ele deve estar no formato esperado por [**esta documentação do Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Aqui está um exemplo: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Pode utilizar um [**embeds creator**](https://embedutils.com/) para obter um payload JSON.\n\n" +" Se forneceres uma mensagem, esta será editada.\n" +" " + +#: embedutils\embedutils.py:186 embedutils\embedutils.py:217 +#: embedutils\embedutils.py:405 embedutils\embedutils.py:415 +#: embedutils\embedutils.py:505 embedutils\embedutils.py:515 +#: embedutils\embedutils.py:831 embedutils\embedutils.py:843 +msgid "Unreadable attachment with `utf-8`." +msgstr "Anexo ilegível com `utf-8`." + +#: embedutils\embedutils.py:206 +#, docstring +msgid "Post an embed from a valid YAML file (upload it).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Publique uma incorporação a partir de um ficheiro YAML válido (carregue-o).\n\n" +" Se fornecer uma mensagem, esta será editada.\n" +" " + +#: embedutils\embedutils.py:243 +#, docstring +msgid "Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Publique incorporações de um link GitHub/Gist/Pastebin/Hastebin contendo JSON válido.\n\n" +" Isso deve estar no formato esperado por [**esta documentação do Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Aqui está um exemplo: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Se forneceres uma mensagem, ela será editada.\n" +" " + +#: embedutils\embedutils.py:273 +#, docstring +msgid "Post embed(s) from an existing message.\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Publicar incorporação(ões) de uma mensagem existente.\n\n" +" A mensagem deve ter pelo menos uma incorporação.\n" +" Pode especificar um índice (começando por 0) se pretender enviar apenas uma das incorporações.\n" +" O conteúdo da mensagem já enviada é incluído se não for especificado um índice.\n\n" +" Se fornecer uma mensagem, esta será editada.\n" +" " + +#: embedutils\embedutils.py:325 +#, docstring +msgid "Download a JSON file for a message's embed(s).\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "Descarrega um ficheiro JSON para a(s) incorporação(ões) de uma mensagem.\n\n" +" A mensagem deve ter pelo menos uma incorporação.\n" +" Pode especificar um índice (começando por 0) se pretender incluir apenas uma das incorporações.\n" +" O conteúdo da mensagem já enviada é incluído se não for especificado um índice.\n" +" " + +#: embedutils\embedutils.py:385 +#, docstring +msgid "Edit a message sent by [botname].\n\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "Edita uma mensagem enviada por [botname].\n\n" +" Seria melhor usar o parâmetro `message` de todos os outros comandos.\n" +" " + +#: embedutils\embedutils.py:477 +#, docstring +msgid "Store an embed.\n\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "Guardar uma incorporação.\n\n" +" Coloca o nome entre aspas se ele tiver várias palavras.\n" +" O argumento `locked` especifica se o embed deve ser bloqueado apenas para mod e superior (nível de guilda) ou apenas para donos de bots (nível global).\n" +" " + +#: embedutils\embedutils.py:485 embedutils\embedutils.py:571 +#: embedutils\embedutils.py:586 embedutils\embedutils.py:618 +#: embedutils\embedutils.py:650 +msgid "You can't manage global stored embeds." +msgstr "Não é possível gerir incorporações globais armazenadas." + +#: embedutils\embedutils.py:548 +msgid "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "Este servidor atingiu o limite de incorporações de {embed_limit}. É necessário remover um embed com `{ctx.clean_prefix}embed unstore` antes de poder adicionar um novo embed." + +#: embedutils\embedutils.py:567 +#, docstring +msgid "Remove a stored embed." +msgstr "Remover uma incorporação armazenada." + +#: embedutils\embedutils.py:577 embedutils\embedutils.py:623 +#: embedutils\embedutils.py:655 +msgid "This is not a stored embed at this level." +msgstr "Não se trata de uma incorporação armazenada a este nível." + +#: embedutils\embedutils.py:584 embedutils\embedutils.py:614 +#, docstring +msgid "Get info about a stored embed." +msgstr "Obter informações sobre uma embed armazenada." + +#: embedutils\embedutils.py:592 +msgid "No stored embeds is configured at this level." +msgstr "Não são configuradas incorporações armazenadas a este nível." + +#: embedutils\embedutils.py:596 +msgid "Global " +msgstr "Mundial " + +#: embedutils\embedutils.py:596 +msgid "Stored Embeds" +msgstr "Embutidos armazenados" + +#: embedutils\embedutils.py:646 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "Descarregar um ficheiro JSON para uma incorporação armazenada." + +#: embedutils\embedutils.py:675 +#, docstring +msgid "Post stored embeds." +msgstr "Lançar incorporações armazenadas." + +#: embedutils\embedutils.py:697 embedutils\embedutils.py:748 +msgid "`{name}` is not a stored embed at this level." +msgstr "`{name}` não é um embed armazenado a este nível." + +#: embedutils\embedutils.py:726 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "Publique incorporações armazenadas com um webhook." + +#: embedutils\embedutils.py:793 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "Obter a ligação para o painel de controlo." + +#: embedutils\embedutils.py:796 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "O Red-Web-Dashboard não está instalado. Verifique ." + +#: embedutils\embedutils.py:801 +msgid "You can't access the Dashboard." +msgstr "Não é possível aceder ao painel de controlo." + +#: embedutils\embedutils.py:807 +msgid "This third party is disabled on the Dashboard." +msgstr "Este terceiro está desativado no painel de controlo." + +#: embedutils\embedutils.py:872 +msgid "Dashboard - " +msgstr "Painel de controlo - " + +#: embedutils\embedutils.py:876 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "Pode criar e enviar incorporações avançadas diretamente a partir do Painel de Controlo!" + +#: embedutils\embedutils.py:883 +msgid "The URL is too long to be displayed." +msgstr "O URL é demasiado longo para ser apresentado." + +#: embedutils\embedutils.py:889 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "Migrar incorporações armazenadas de EmbedUtils por Phen." + +#: embedutils\embedutils.py:933 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "Dados migrados com sucesso do EmbedUtils por Phen." + diff --git a/embedutils/locales/ro-RO.po b/embedutils/locales/ro-RO.po new file mode 100644 index 0000000..4d699bd --- /dev/null +++ b/embedutils/locales/ro-RO.po @@ -0,0 +1,339 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:16\n" +"Last-Translator: \n" +"Language-Team: Romanian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ro\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/embedutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 314\n" +"Language: ro_RO\n" + +#: embedutils\converters.py:66 embedutils\converters.py:81 +msgid "This doesn't seem to be properly formatted embed {conversion_type}. Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "Acest lucru nu pare a fi formatat corespunzător embed {conversion_type}. Consultați linkul de pe `{ctx.clean_prefix}help {ctx.command.qualified_name}`." + +#: embedutils\converters.py:93 +msgid "JSON Parse Error" +msgstr "Eroare JSON Parse Error" + +#: embedutils\converters.py:104 +msgid "YAML Parse Error" +msgstr "Eroare YAML Parse Error" + +#: embedutils\converters.py:114 +msgid "The `content` field is not supported for this command." +msgstr "Câmpul `content` nu este acceptat pentru această comandă." + +#: embedutils\converters.py:130 +msgid "Embed Parse Error" +msgstr "Eroare de analiză Embed Parse" + +#: embedutils\converters.py:135 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "Mărimea mesajului depășește limita Discord de 6000 de caractere ({length})." + +#: embedutils\converters.py:147 embedutils\embedutils.py:100 +#: embedutils\embedutils.py:133 embedutils\embedutils.py:165 +#: embedutils\embedutils.py:200 embedutils\embedutils.py:231 +#: embedutils\embedutils.py:262 embedutils\embedutils.py:314 +#: embedutils\embedutils.py:440 embedutils\embedutils.py:537 +#: embedutils\embedutils.py:708 embedutils\embedutils.py:762 +msgid "Embed Send Error" +msgstr "Înscrieți trimiteți eroare" + +#: embedutils\converters.py:162 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "Utilizați `{ctx.prefix}help {ctx.command.qualified_name}` pentru a vedea un exemplu." + +#: embedutils\converters.py:204 +msgid "Embed limit reached ({limit})." +msgstr "Limita de încorporare a fost atinsă ({limit})." + +#: embedutils\converters.py:209 +msgid "Failed to convert input into embeds." +msgstr "Nu a reușit să convertească datele de intrare în embed-uri." + +#: embedutils\converters.py:228 +msgid "It's not a valid channel or thread." +msgstr "Nu este un canal sau un subiect valid." + +#: embedutils\converters.py:232 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "Nu am permisiuni pentru a trimite embed-uri în {channel.mention}." + +#: embedutils\converters.py:241 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "Nu aveți permisiuni pentru a trimite embed-uri în {channel.mention}." + +#: embedutils\converters.py:253 +msgid "I have to be the author of the message. You can use the command without providing a message to send one." +msgstr "Eu trebuie să fiu autorul mesajului. Puteți utiliza comanda fără a furniza un mesaj pentru a trimite unul." + +#: embedutils\converters.py:263 +msgid "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "Nu aveți permisiunea de a edita embed-urile unui mesaj existent (proprietarul bot-ului poate seta permisiunile cu ajutorul butonului Permissions din comanda `[p]embed edit`)." + +#: embedutils\dashboard_integration.py:53 +msgid "You don't have permissions to access this page." +msgstr "Nu aveți permisiuni pentru a accesa această pagină." + +#: embedutils\dashboard_integration.py:60 +msgid "I or you don't have permissions to send messages or embeds in any channel in this guild." +msgstr "Eu sau tu nu avem permisiunea de a trimite mesaje sau embed-uri pe niciun canal din această breaslă." + +#: embedutils\dashboard_integration.py:76 +msgid "Username:" +msgstr "Nume utilizator:" + +#: embedutils\dashboard_integration.py:80 +msgid "Avatar URL:" +msgstr "Avatar URL:" + +#: embedutils\dashboard_integration.py:84 +msgid "Data" +msgstr "Date" + +#: embedutils\dashboard_integration.py:91 +msgid "Channels:" +msgstr "Canale:" + +#: embedutils\dashboard_integration.py:100 +msgid "Send Message(s)" +msgstr "Trimiteți mesaj(e)" + +#: embedutils\dashboard_integration.py:164 +msgid "Message(s) sent successfully!" +msgstr "Mesaj(e) trimis(e) cu succes!" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "Creați, trimiteți și stocați încorporări bogate, inclusiv din Red-Web-Dashboard!" + +#: embedutils\embedutils.py:110 +#, docstring +msgid "Post embeds from valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Postați încorporări din JSON valid.\n\n" +" Acesta trebuie să fie în formatul așteptat de [**această documentație Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Iată un exemplu: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Puteți utiliza un [**embeds creator**](https://embedutils.com/) pentru a obține o sarcină utilă JSON.\n\n" +" Dacă furnizați un mesaj, acesta va fi editat.\n" +" Puteți utiliza un atașament și comanda `[p]embed yamlfile` va fi invocată automat.\n" +" " + +#: embedutils\embedutils.py:143 +#, docstring +msgid "Post embeds from valid YAML.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Postați încorporări din YAML valid.\n\n" +" Acesta trebuie să fie în formatul așteptat de [**această documentație Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Iată un exemplu: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Dacă furnizați un mesaj, acesta va fi editat.\n" +" Puteți folosi un atașament și comanda `[p]embed yamlfile` va fi invocată automat.\n" +" " + +#: embedutils\embedutils.py:171 +#, docstring +msgid "Post an embed from a valid JSON file (upload it).\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Postați un embed dintr-un fișier JSON valid (încărcați-l).\n\n" +" Acesta trebuie să fie în formatul așteptat de [**această documentație Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Iată un exemplu: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Puteți utiliza un [**embeds creator**](https://embedutils.com/) pentru a obține o sarcină utilă JSON.\n\n" +" Dacă furnizați un mesaj, acesta va fi editat.\n" +" " + +#: embedutils\embedutils.py:186 embedutils\embedutils.py:217 +#: embedutils\embedutils.py:405 embedutils\embedutils.py:415 +#: embedutils\embedutils.py:505 embedutils\embedutils.py:515 +#: embedutils\embedutils.py:831 embedutils\embedutils.py:843 +msgid "Unreadable attachment with `utf-8`." +msgstr "Atașament ilizibil cu `utf-8`." + +#: embedutils\embedutils.py:206 +#, docstring +msgid "Post an embed from a valid YAML file (upload it).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Postați o inserție dintr-un fișier YAML valid (încărcați-l).\n\n" +" Dacă furnizați un mesaj, acesta va fi editat.\n" +" " + +#: embedutils\embedutils.py:243 +#, docstring +msgid "Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Postați încorporări dintr-un link GitHub/Gist/Pastebin/Hastebin care conține un JSON valid.\n\n" +" Acesta trebuie să fie în formatul așteptat de [**această documentație Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Iată un exemplu: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Dacă furnizați un mesaj, acesta va fi editat.\n" +" " + +#: embedutils\embedutils.py:273 +#, docstring +msgid "Post embed(s) from an existing message.\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Postați embed(uri) dintr-un mesaj existent.\n\n" +" Mesajul trebuie să aibă cel puțin un embed.\n" +" Puteți specifica un index (începând cu 0) dacă doriți să trimiteți doar unul dintre embed-uri.\n" +" Conținutul mesajului deja trimis este inclus dacă nu este specificat niciun index.\n\n" +" Dacă furnizați un mesaj, acesta va fi editat.\n" +" " + +#: embedutils\embedutils.py:325 +#, docstring +msgid "Download a JSON file for a message's embed(s).\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "Descărcați un fișier JSON pentru încorporarea (încorporările) unui mesaj.\n\n" +" Mesajul trebuie să aibă cel puțin un embed.\n" +" Puteți specifica un index (începând cu 0) dacă doriți să includeți doar unul dintre embed-uri.\n" +" Conținutul mesajului deja trimis este inclus dacă nu este specificat niciun index.\n" +" " + +#: embedutils\embedutils.py:385 +#, docstring +msgid "Edit a message sent by [botname].\n\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "Editați un mesaj trimis de [botname].\n\n" +" Ar fi mai bine să folosiți parametrul `message` din toate celelalte comenzi.\n" +" " + +#: embedutils\embedutils.py:477 +#, docstring +msgid "Store an embed.\n\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "Stocați un embed.\n\n" +" Puneți numele între ghilimele dacă este format din mai multe cuvinte.\n" +" Argumentul `locked` specifică dacă embed-ul trebuie să fie blocat doar pentru mod și superior (nivel de breaslă) sau doar pentru proprietarii de roboți (nivel global).\n" +" " + +#: embedutils\embedutils.py:485 embedutils\embedutils.py:571 +#: embedutils\embedutils.py:586 embedutils\embedutils.py:618 +#: embedutils\embedutils.py:650 +msgid "You can't manage global stored embeds." +msgstr "Nu puteți gestiona încorporările stocate la nivel global." + +#: embedutils\embedutils.py:548 +msgid "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "Acest server a atins limita de încorporare a {embed_limit}. Trebuie să eliminați un embed cu `{ctx.clean_prefix}embed unstore` înainte de a putea adăuga unul nou." + +#: embedutils\embedutils.py:567 +#, docstring +msgid "Remove a stored embed." +msgstr "Îndepărtarea unei încorporări stocate." + +#: embedutils\embedutils.py:577 embedutils\embedutils.py:623 +#: embedutils\embedutils.py:655 +msgid "This is not a stored embed at this level." +msgstr "Aceasta nu este o încorporare stocată la acest nivel." + +#: embedutils\embedutils.py:584 embedutils\embedutils.py:614 +#, docstring +msgid "Get info about a stored embed." +msgstr "Obține informații despre o încorporare stocată." + +#: embedutils\embedutils.py:592 +msgid "No stored embeds is configured at this level." +msgstr "La acest nivel nu este configurată nicio încorporare stocată." + +#: embedutils\embedutils.py:596 +msgid "Global " +msgstr "Global " + +#: embedutils\embedutils.py:596 +msgid "Stored Embeds" +msgstr "Embed-uri stocate" + +#: embedutils\embedutils.py:646 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "Descărcați un fișier JSON pentru o încorporare stocată." + +#: embedutils\embedutils.py:675 +#, docstring +msgid "Post stored embeds." +msgstr "Postați încorporări stocate." + +#: embedutils\embedutils.py:697 embedutils\embedutils.py:748 +msgid "`{name}` is not a stored embed at this level." +msgstr "`{name}` nu este o încorporare stocată la acest nivel." + +#: embedutils\embedutils.py:726 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "Postați încorporări stocate cu un webhook." + +#: embedutils\embedutils.py:793 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "Obțineți link-ul către tabloul de bord." + +#: embedutils\embedutils.py:796 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard nu este instalat. Verificați ." + +#: embedutils\embedutils.py:801 +msgid "You can't access the Dashboard." +msgstr "Nu puteți accesa tabloul de bord." + +#: embedutils\embedutils.py:807 +msgid "This third party is disabled on the Dashboard." +msgstr "Această terță parte este dezactivată pe tabloul de bord." + +#: embedutils\embedutils.py:872 +msgid "Dashboard - " +msgstr "Tablou de bord - " + +#: embedutils\embedutils.py:876 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "Puteți crea și trimite încorporări bogate direct din tabloul de bord!" + +#: embedutils\embedutils.py:883 +msgid "The URL is too long to be displayed." +msgstr "URL-ul este prea lung pentru a fi afișat." + +#: embedutils\embedutils.py:889 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "Migrează embed-urile stocate din EmbedUtils by Phen." + +#: embedutils\embedutils.py:933 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "Datele au fost migrate cu succes din EmbedUtils de către Phen." + diff --git a/embedutils/locales/ru-RU.po b/embedutils/locales/ru-RU.po new file mode 100644 index 0000000..1591af8 --- /dev/null +++ b/embedutils/locales/ru-RU.po @@ -0,0 +1,339 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:17\n" +"Last-Translator: \n" +"Language-Team: Russian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: ru\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/embedutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 314\n" +"Language: ru_RU\n" + +#: embedutils\converters.py:66 embedutils\converters.py:81 +msgid "This doesn't seem to be properly formatted embed {conversion_type}. Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "Похоже, это неправильно отформатированное вложение {conversion_type}. Обратитесь к ссылке на `{ctx.clean_prefix}help {ctx.command.qualified_name}`." + +#: embedutils\converters.py:93 +msgid "JSON Parse Error" +msgstr "Ошибка разбора JSON" + +#: embedutils\converters.py:104 +msgid "YAML Parse Error" +msgstr "Ошибка разбора YAML" + +#: embedutils\converters.py:114 +msgid "The `content` field is not supported for this command." +msgstr "Поле `content` для этой команды не поддерживается." + +#: embedutils\converters.py:130 +msgid "Embed Parse Error" +msgstr "Ошибка разбора эмбеда" + +#: embedutils\converters.py:135 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "Размер вставки превышает ограничение Discord в 6000 символов ({length})." + +#: embedutils\converters.py:147 embedutils\embedutils.py:100 +#: embedutils\embedutils.py:133 embedutils\embedutils.py:165 +#: embedutils\embedutils.py:200 embedutils\embedutils.py:231 +#: embedutils\embedutils.py:262 embedutils\embedutils.py:314 +#: embedutils\embedutils.py:440 embedutils\embedutils.py:537 +#: embedutils\embedutils.py:708 embedutils\embedutils.py:762 +msgid "Embed Send Error" +msgstr "Ошибка отправки эмбеда" + +#: embedutils\converters.py:162 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "Используйте `{ctx.prefix}help {ctx.command.qualified_name}`, чтобы посмотреть пример." + +#: embedutils\converters.py:204 +msgid "Embed limit reached ({limit})." +msgstr "Достигнут предел вложения ({limit})." + +#: embedutils\converters.py:209 +msgid "Failed to convert input into embeds." +msgstr "Не удалось преобразовать вводимые данные в эмбеды." + +#: embedutils\converters.py:228 +msgid "It's not a valid channel or thread." +msgstr "Это не правильный канал или поток." + +#: embedutils\converters.py:232 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "У меня нет прав на отправку вложений в {channel.mention}." + +#: embedutils\converters.py:241 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "У вас нет прав для отправки вложений в {channel.mention}." + +#: embedutils\converters.py:253 +msgid "I have to be the author of the message. You can use the command without providing a message to send one." +msgstr "Я должен быть автором сообщения. Для отправки сообщения можно использовать команду без указания сообщения." + +#: embedutils\converters.py:263 +msgid "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "Вам не разрешено редактировать вставки в существующее сообщение (владелец бота может установить права с помощью пункта Разрешения в команде `[p]embed edit`)." + +#: embedutils\dashboard_integration.py:53 +msgid "You don't have permissions to access this page." +msgstr "У вас нет прав для доступа к этой странице." + +#: embedutils\dashboard_integration.py:60 +msgid "I or you don't have permissions to send messages or embeds in any channel in this guild." +msgstr "У меня или у вас нет прав на отправку сообщений или вложений в любом канале в этой гильдии." + +#: embedutils\dashboard_integration.py:76 +msgid "Username:" +msgstr "Имя пользователя:" + +#: embedutils\dashboard_integration.py:80 +msgid "Avatar URL:" +msgstr "Аватар URL:" + +#: embedutils\dashboard_integration.py:84 +msgid "Data" +msgstr "Данные" + +#: embedutils\dashboard_integration.py:91 +msgid "Channels:" +msgstr "Каналы:" + +#: embedutils\dashboard_integration.py:100 +msgid "Send Message(s)" +msgstr "Отправить сообщение(я)" + +#: embedutils\dashboard_integration.py:164 +msgid "Message(s) sent successfully!" +msgstr "Сообщение(я) отправлено успешно!" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "Создавайте, отправляйте и храните богатые вставки, в том числе из Red-Web-Dashboard!" + +#: embedutils\embedutils.py:110 +#, docstring +msgid "Post embeds from valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Публикуйте вставки из корректного JSON.\n\n" +" Он должен быть в формате, ожидаемом [**этой документацией Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Вот пример: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Вы можете использовать [**создатель семян**](https://embedutils.com/), чтобы получить полезную нагрузку в формате JSON.\n\n" +" Если вы укажете сообщение, оно будет отредактировано.\n" +" Вы можете использовать вложение, и команда `[p]embed yamlfile` будет вызвана автоматически.\n" +" " + +#: embedutils\embedutils.py:143 +#, docstring +msgid "Post embeds from valid YAML.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Опубликуйте вставки из корректного YAML.\n\n" +" Он должен быть в формате, ожидаемом [**этой документацией Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Вот пример: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Если вы предоставите сообщение, оно будет отредактировано.\n" +" Вы можете использовать вложение, и команда `[p]embed yamlfile` будет вызвана автоматически.\n" +" " + +#: embedutils\embedutils.py:171 +#, docstring +msgid "Post an embed from a valid JSON file (upload it).\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Опубликуйте вставку из корректного JSON-файла (загрузите его).\n\n" +" Он должен быть в формате, ожидаемом [**этой документацией Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Вот пример: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Вы можете использовать [**создатель семян**](https://embedutils.com/), чтобы получить полезную нагрузку в формате JSON.\n\n" +" Если вы предоставите сообщение, оно будет отредактировано.\n" +" " + +#: embedutils\embedutils.py:186 embedutils\embedutils.py:217 +#: embedutils\embedutils.py:405 embedutils\embedutils.py:415 +#: embedutils\embedutils.py:505 embedutils\embedutils.py:515 +#: embedutils\embedutils.py:831 embedutils\embedutils.py:843 +msgid "Unreadable attachment with `utf-8`." +msgstr "Нечитаемое вложение с `utf-8`." + +#: embedutils\embedutils.py:206 +#, docstring +msgid "Post an embed from a valid YAML file (upload it).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Опубликуйте вставку из корректного YAML-файла (загрузите его).\n\n" +" Если вы укажете сообщение, оно будет отредактировано.\n" +" " + +#: embedutils\embedutils.py:243 +#, docstring +msgid "Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Опубликуйте вложения из ссылки GitHub/Gist/Pastebin/Hastebin, содержащей корректный JSON.\n\n" +" Он должен быть в формате, ожидаемом [**этой документацией Discord**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Вот пример: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Если вы предоставите сообщение, оно будет отредактировано.\n" +" " + +#: embedutils\embedutils.py:273 +#, docstring +msgid "Post embed(s) from an existing message.\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Опубликовать вставку(и) из существующего сообщения.\n\n" +" Сообщение должно содержать хотя бы одну вставку.\n" +" Вы можете указать индекс (начиная с 0), если хотите отправить только один из вкраплений.\n" +" Если индекс не указан, в сообщение будет включено содержимое уже отправленного сообщения.\n\n" +" Если вы указали сообщение, оно будет отредактировано.\n" +" " + +#: embedutils\embedutils.py:325 +#, docstring +msgid "Download a JSON file for a message's embed(s).\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "Загрузите JSON-файл для вставки(ей) сообщения.\n\n" +" Сообщение должно содержать хотя бы одну вставку.\n" +" Вы можете указать индекс (начиная с 0), если хотите включить только одну из вложений.\n" +" Если индекс не указан, в файл включается содержимое уже отправленного сообщения.\n" +" " + +#: embedutils\embedutils.py:385 +#, docstring +msgid "Edit a message sent by [botname].\n\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "Редактирование сообщения, отправленного [botname].\n\n" +" Лучше использовать параметр `message` во всех остальных командах.\n" +" " + +#: embedutils\embedutils.py:477 +#, docstring +msgid "Store an embed.\n\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "Сохраните вставку.\n\n" +" Поместите имя в кавычки, если оно состоит из нескольких слов.\n" +" Аргумент `locked` указывает, должна ли вставка быть заблокирована только для модов и начальников (уровень гильдии) или только для владельцев ботов (глобальный уровень).\n" +" " + +#: embedutils\embedutils.py:485 embedutils\embedutils.py:571 +#: embedutils\embedutils.py:586 embedutils\embedutils.py:618 +#: embedutils\embedutils.py:650 +msgid "You can't manage global stored embeds." +msgstr "Вы не можете управлять глобальными сохраненными вставками." + +#: embedutils\embedutils.py:548 +msgid "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "Этот сервер достиг лимита встраивания {embed_limit}. Вы должны удалить вставку с помощью `{ctx.clean_prefix}embed unstore`, прежде чем сможете добавить новую." + +#: embedutils\embedutils.py:567 +#, docstring +msgid "Remove a stored embed." +msgstr "Удалить сохраненную вставку." + +#: embedutils\embedutils.py:577 embedutils\embedutils.py:623 +#: embedutils\embedutils.py:655 +msgid "This is not a stored embed at this level." +msgstr "На этом уровне не хранится в памяти." + +#: embedutils\embedutils.py:584 embedutils\embedutils.py:614 +#, docstring +msgid "Get info about a stored embed." +msgstr "Получите информацию о хранимой вставке." + +#: embedutils\embedutils.py:592 +msgid "No stored embeds is configured at this level." +msgstr "На этом уровне не настраиваются хранимые эмбеды." + +#: embedutils\embedutils.py:596 +msgid "Global " +msgstr "Глобальная " + +#: embedutils\embedutils.py:596 +msgid "Stored Embeds" +msgstr "Сохраненные эмбеды" + +#: embedutils\embedutils.py:646 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "Загрузите JSON-файл для хранимой вставки." + +#: embedutils\embedutils.py:675 +#, docstring +msgid "Post stored embeds." +msgstr "Разместите сохраненные вставки." + +#: embedutils\embedutils.py:697 embedutils\embedutils.py:748 +msgid "`{name}` is not a stored embed at this level." +msgstr "`{name}` не является хранимой вставкой на этом уровне." + +#: embedutils\embedutils.py:726 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "Публикуйте сохраненные эмбеды с помощью веб-хука." + +#: embedutils\embedutils.py:793 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "Получите ссылку на приборную панель." + +#: embedutils\embedutils.py:796 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard не установлен. Проверьте ." + +#: embedutils\embedutils.py:801 +msgid "You can't access the Dashboard." +msgstr "Вы не можете получить доступ к панели управления." + +#: embedutils\embedutils.py:807 +msgid "This third party is disabled on the Dashboard." +msgstr "Эта третья сторона отключена на панели управления." + +#: embedutils\embedutils.py:872 +msgid "Dashboard - " +msgstr "Приборная панель - " + +#: embedutils\embedutils.py:876 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "Вы можете создавать и отправлять насыщенные вставки прямо из панели управления!" + +#: embedutils\embedutils.py:883 +msgid "The URL is too long to be displayed." +msgstr "URL-адрес слишком длинный для отображения." + +#: embedutils\embedutils.py:889 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "Перенесите сохраненные эмбеды из EmbedUtils с помощью Phen." + +#: embedutils\embedutils.py:933 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "Данные успешно перенесены из EmbedUtils с помощью Phen." + diff --git a/embedutils/locales/tr-TR.po b/embedutils/locales/tr-TR.po new file mode 100644 index 0000000..24116cc --- /dev/null +++ b/embedutils/locales/tr-TR.po @@ -0,0 +1,339 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 13:27\n" +"Last-Translator: \n" +"Language-Team: Turkish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: tr\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/embedutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 314\n" +"Language: tr_TR\n" + +#: embedutils\converters.py:66 embedutils\converters.py:81 +msgid "This doesn't seem to be properly formatted embed {conversion_type}. Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "Bu, düzgün biçimlendirilmiş bir embed {conversion_type} gibi görünmüyor. `{ctx.clean_prefix}help {ctx.command.qualified_name}` bağlantısına bakın." + +#: embedutils\converters.py:93 +msgid "JSON Parse Error" +msgstr "JSON Ayrıştırma Hatası" + +#: embedutils\converters.py:104 +msgid "YAML Parse Error" +msgstr "YAML Ayrıştırma Hatası" + +#: embedutils\converters.py:114 +msgid "The `content` field is not supported for this command." +msgstr "`content` alanı bu komut için desteklenmiyor." + +#: embedutils\converters.py:130 +msgid "Embed Parse Error" +msgstr "Embed Ayrıştırma Hatası" + +#: embedutils\converters.py:135 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "Embed boyutu, Discord'un 6000 karakter sınırını aşıyor ({length})." + +#: embedutils\converters.py:147 embedutils\embedutils.py:100 +#: embedutils\embedutils.py:133 embedutils\embedutils.py:165 +#: embedutils\embedutils.py:200 embedutils\embedutils.py:231 +#: embedutils\embedutils.py:262 embedutils\embedutils.py:314 +#: embedutils\embedutils.py:440 embedutils\embedutils.py:537 +#: embedutils\embedutils.py:708 embedutils\embedutils.py:762 +msgid "Embed Send Error" +msgstr "Embed Gönderme Hatası" + +#: embedutils\converters.py:162 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "Bir örnek görmek için `{ctx.prefix}help {ctx.command.qualified_name}` komutunu kullanın." + +#: embedutils\converters.py:204 +msgid "Embed limit reached ({limit})." +msgstr "Embed sınırına ulaşıldı ({limit})." + +#: embedutils\converters.py:209 +msgid "Failed to convert input into embeds." +msgstr "Girdiyi embede dönüştürme başarısız oldu." + +#: embedutils\converters.py:228 +msgid "It's not a valid channel or thread." +msgstr "Geçerli bir kanal veya konu değil." + +#: embedutils\converters.py:232 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "{channel.mention} kanalında embed göndermek için iznim yok." + +#: embedutils\converters.py:241 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "{channel.mention} kanalında embed göndermek için izniniz yok." + +#: embedutils\converters.py:253 +msgid "I have to be the author of the message. You can use the command without providing a message to send one." +msgstr "Mesajın yazarı olmam gerekiyor. Mesaj göndermek için komutu mesaj vermeden kullanabilirsiniz." + +#: embedutils\converters.py:263 +msgid "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "Mevcut bir mesajın embedlerini düzenleme izniniz yok (bot sahibi, `[p]embed edit` komutuyla izinleri ayarlayabilir)." + +#: embedutils\dashboard_integration.py:53 +msgid "You don't have permissions to access this page." +msgstr "Bu sayfaya erişim izniniz yok." + +#: embedutils\dashboard_integration.py:60 +msgid "I or you don't have permissions to send messages or embeds in any channel in this guild." +msgstr "Benim veya sizin, bu sunucuda herhangi bir kanalda mesaj veya embed göndermek için izniniz yok." + +#: embedutils\dashboard_integration.py:76 +msgid "Username:" +msgstr "Kullanıcı adı:" + +#: embedutils\dashboard_integration.py:80 +msgid "Avatar URL:" +msgstr "Avatar URL'si:" + +#: embedutils\dashboard_integration.py:84 +msgid "Data" +msgstr "Veri" + +#: embedutils\dashboard_integration.py:91 +msgid "Channels:" +msgstr "Kanallar:" + +#: embedutils\dashboard_integration.py:100 +msgid "Send Message(s)" +msgstr "Mesaj(lar) Gönder" + +#: embedutils\dashboard_integration.py:164 +msgid "Message(s) sent successfully!" +msgstr "Mesaj(lar) başarıyla gönderildi!" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "Red-Web-Dashboard'dan da zengin embedler oluşturun, gönderin ve saklayın!" + +#: embedutils\embedutils.py:110 +#, docstring +msgid "Post embeds from valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Geçerli JSON'dan embedler gönderin.\n\n" +" Bu, [**bu Discord belgesi**](https://discord.com/developers/docs/resources/channel#embed-object) tarafından beklenen formatta olmalıdır.\n" +" İşte bir örnek: [**bu örnek**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" JSON yükü almak için bir [**embed oluşturucu**](https://embedutils.com/) kullanabilirsiniz.\n\n" +" Bir mesaj sağlarsanız, düzenlenecektir.\n" +" Bir ek kullanabilirsiniz ve `[p]embed yamlfile` komutu otomatik olarak çağrılacaktır.\n" +" " + +#: embedutils\embedutils.py:143 +#, docstring +msgid "Post embeds from valid YAML.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Geçerli YAML'den embedler gönderin.\n\n" +" Bu, [**bu Discord belgesi**](https://discord.com/developers/docs/resources/channel#embed-object) tarafından beklenen formatta olmalıdır.\n" +" İşte bir örnek: [**bu örnek**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Bir mesaj sağlarsanız, düzenlenecektir.\n" +" Bir ek kullanabilirsiniz ve `[p]embed yamlfile` komutu otomatik olarak çağrılacaktır.\n" +" " + +#: embedutils\embedutils.py:171 +#, docstring +msgid "Post an embed from a valid JSON file (upload it).\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Geçerli bir JSON dosyasından embed gönderin (yükleyin).\n\n" +" Bu, [**bu Discord belgesi**](https://discord.com/developers/docs/resources/channel#embed-object) tarafından beklenen formatta olmalıdır.\n" +" İşte bir örnek: [**bu örnek**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" JSON yükü almak için bir [**embed oluşturucu**](https://embedutils.com/) kullanabilirsiniz.\n\n" +" Bir mesaj sağlarsanız, düzenlenecektir.\n" +" " + +#: embedutils\embedutils.py:186 embedutils\embedutils.py:217 +#: embedutils\embedutils.py:405 embedutils\embedutils.py:415 +#: embedutils\embedutils.py:505 embedutils\embedutils.py:515 +#: embedutils\embedutils.py:831 embedutils\embedutils.py:843 +msgid "Unreadable attachment with `utf-8`." +msgstr "`utf-8` ile okunamayan ek." + +#: embedutils\embedutils.py:206 +#, docstring +msgid "Post an embed from a valid YAML file (upload it).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Geçerli bir YAML dosyasından bir yerleştirme gönderin (yükleyin).\n\n" +" Bir mesaj verirseniz, bu mesaj düzenlenecektir.\n" +" " + +#: embedutils\embedutils.py:243 +#, docstring +msgid "Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Geçerli JSON içeren bir GitHub/Gist/Pastebin/Hastebin bağlantısından yerleştirmeler gönderin.\n\n" +" Bu, [**bu Discord dokümantasyonu**] (https://discord.com/developers/docs/resources/channel#embed-object) tarafından beklenen formatta olmalıdır.\n" +" İşte bir örnek: [**bu örnek**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Bir mesaj verirseniz, bu mesaj düzenlenecektir.\n" +" " + +#: embedutils\embedutils.py:273 +#, docstring +msgid "Post embed(s) from an existing message.\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Mevcut bir mesajdan yerleştirme(ler) gönderin.\n\n" +" Mesajda en az bir katıştırma olmalıdır.\n" +" Yerleştirmelerden yalnızca birini göndermek istiyorsanız bir dizin (0 ile başlayan) belirtebilirsiniz.\n" +" Herhangi bir dizin belirtilmemişse, zaten gönderilmiş olan mesajın içeriği dahil edilir.\n\n" +" Bir mesaj verirseniz, bu mesaj düzenlenecektir.\n" +" " + +#: embedutils\embedutils.py:325 +#, docstring +msgid "Download a JSON file for a message's embed(s).\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "Bir iletinin katıştırma(lar) ı için bir JSON dosyası indirin.\n\n" +" Mesajda en az bir katıştırma olmalıdır.\n" +" Yerleştirmelerden yalnızca birini dahil etmek istiyorsanız bir dizin (0 ile başlayan) belirtebilirsiniz.\n" +" Herhangi bir dizin belirtilmemişse, zaten gönderilmiş olan mesajın içeriği dahil edilir.\n" +" " + +#: embedutils\embedutils.py:385 +#, docstring +msgid "Edit a message sent by [botname].\n\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "[botname] tarafından gönderilen bir mesajı düzenleyin.\n\n" +" Diğer tüm komutların `message` parametresini kullanmak daha iyi olacaktır.\n" +" " + +#: embedutils\embedutils.py:477 +#, docstring +msgid "Store an embed.\n\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "Bir yerleştirmeyi saklayın.\n\n" +" Birden fazla sözcük varsa adı tırnak içine alın.\n" +" `locked` argümanı katıştırmanın sadece mod ve üstlerine mi (sunucu seviyesi) yoksa sadece bot sahiplerine mi (global seviye) kilitleneceğini belirtir.\n" +" " + +#: embedutils\embedutils.py:485 embedutils\embedutils.py:571 +#: embedutils\embedutils.py:586 embedutils\embedutils.py:618 +#: embedutils\embedutils.py:650 +msgid "You can't manage global stored embeds." +msgstr "Küresel olarak saklanan embedleri yönetemezsiniz." + +#: embedutils\embedutils.py:548 +msgid "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "Bu sunucu {embed_limit} embed sınırına ulaştı. Yeni bir tane ekleyebilmek için `{ctx.clean_prefix}embed unstore` ile bir embedi kaldırmanız gerekmektedir." + +#: embedutils\embedutils.py:567 +#, docstring +msgid "Remove a stored embed." +msgstr "Saklanan bir embedi kaldırın." + +#: embedutils\embedutils.py:577 embedutils\embedutils.py:623 +#: embedutils\embedutils.py:655 +msgid "This is not a stored embed at this level." +msgstr "Bu düzeyde saklanan bir embed değil." + +#: embedutils\embedutils.py:584 embedutils\embedutils.py:614 +#, docstring +msgid "Get info about a stored embed." +msgstr "Saklanan bir embed hakkında bilgi alın." + +#: embedutils\embedutils.py:592 +msgid "No stored embeds is configured at this level." +msgstr "Bu düzeyde yapılandırılmış saklanan embed yok." + +#: embedutils\embedutils.py:596 +msgid "Global " +msgstr "Küresel" + +#: embedutils\embedutils.py:596 +msgid "Stored Embeds" +msgstr "Saklanan Embedler" + +#: embedutils\embedutils.py:646 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "Saklanan bir embed için bir JSON dosyası indirin." + +#: embedutils\embedutils.py:675 +#, docstring +msgid "Post stored embeds." +msgstr "Saklanan embedleri gönderin." + +#: embedutils\embedutils.py:697 embedutils\embedutils.py:748 +msgid "`{name}` is not a stored embed at this level." +msgstr "`{name}` bu düzeyde saklanan bir embed değil." + +#: embedutils\embedutils.py:726 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "Webhook ile saklanan embedleri gönderin." + +#: embedutils\embedutils.py:793 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "Kontrol Paneli bağlantısını alın." + +#: embedutils\embedutils.py:796 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard yüklü değil. adresini kontrol edin." + +#: embedutils\embedutils.py:801 +msgid "You can't access the Dashboard." +msgstr "Dashboard'a erişemezsiniz." + +#: embedutils\embedutils.py:807 +msgid "This third party is disabled on the Dashboard." +msgstr "Bu üçüncü parti Kontrol Panelinde devre dışı bırakıldı." + +#: embedutils\embedutils.py:872 +msgid "Dashboard - " +msgstr "Kontrol Paneli -" + +#: embedutils\embedutils.py:876 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "Kontrol Panelinden doğrudan zengin embedler oluşturabilir ve gönderebilirsiniz!" + +#: embedutils\embedutils.py:883 +msgid "The URL is too long to be displayed." +msgstr "URL görüntülenemeyecek kadar uzun." + +#: embedutils\embedutils.py:889 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "Phen tarafından EmbedUtils'ten saklanan embedleri taşıyın." + +#: embedutils\embedutils.py:933 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "Phen tarafından EmbedUtils'ten veriler başarıyla taşındı." + diff --git a/embedutils/locales/uk-UA.po b/embedutils/locales/uk-UA.po new file mode 100644 index 0000000..e3914f6 --- /dev/null +++ b/embedutils/locales/uk-UA.po @@ -0,0 +1,339 @@ +msgid "" +msgstr "" +"Project-Id-Version: aaa3a-cogs\n" +"POT-Creation-Date: 2024-07-20 22:14+0200\n" +"PO-Revision-Date: 2024-07-21 15:17\n" +"Last-Translator: \n" +"Language-Team: Ukrainian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" +"X-Crowdin-Project: aaa3a-cogs\n" +"X-Crowdin-Project-ID: 531090\n" +"X-Crowdin-Language: uk\n" +"X-Crowdin-File: /[AAA3A-AAA3A.AAA3A-cogs] main/embedutils/locales/messages.pot\n" +"X-Crowdin-File-ID: 314\n" +"Language: uk_UA\n" + +#: embedutils\converters.py:66 embedutils\converters.py:81 +msgid "This doesn't seem to be properly formatted embed {conversion_type}. Refer to the link on `{ctx.clean_prefix}help {ctx.command.qualified_name}`." +msgstr "Здається, це неправильно відформатоване вбудовування {conversion_type}. Зверніться до посилання на `{ctx.clean_prefix}довідки {ctx.command.qualified_name}`." + +#: embedutils\converters.py:93 +msgid "JSON Parse Error" +msgstr "Помилка розбору JSON" + +#: embedutils\converters.py:104 +msgid "YAML Parse Error" +msgstr "Помилка розбору YAML" + +#: embedutils\converters.py:114 +msgid "The `content` field is not supported for this command." +msgstr "Поле `content` не підтримується для цієї команди." + +#: embedutils\converters.py:130 +msgid "Embed Parse Error" +msgstr "Помилка розбору вбудовування" + +#: embedutils\converters.py:135 +msgid "Embed size exceeds Discord limit of 6000 characters ({length})." +msgstr "Розмір вбудовування перевищує обмеження Discord у 6000 символів ({length})." + +#: embedutils\converters.py:147 embedutils\embedutils.py:100 +#: embedutils\embedutils.py:133 embedutils\embedutils.py:165 +#: embedutils\embedutils.py:200 embedutils\embedutils.py:231 +#: embedutils\embedutils.py:262 embedutils\embedutils.py:314 +#: embedutils\embedutils.py:440 embedutils\embedutils.py:537 +#: embedutils\embedutils.py:708 embedutils\embedutils.py:762 +msgid "Embed Send Error" +msgstr "Помилка надсилання вбудовування" + +#: embedutils\converters.py:162 +msgid "Use `{ctx.prefix}help {ctx.command.qualified_name}` to see an example." +msgstr "Використовуйте `{ctx.prefix}help {ctx.command.qualified_name}`, щоб побачити приклад." + +#: embedutils\converters.py:204 +msgid "Embed limit reached ({limit})." +msgstr "Ліміт вбудовування досягнуто ({limit})." + +#: embedutils\converters.py:209 +msgid "Failed to convert input into embeds." +msgstr "Не вдалося перетворити вхідні дані у вбудовування." + +#: embedutils\converters.py:228 +msgid "It's not a valid channel or thread." +msgstr "Це не дійсний канал або потік." + +#: embedutils\converters.py:232 +msgid "I do not have permissions to send embeds in {channel.mention}." +msgstr "Я не маю дозволу надсилати вбудовування в {channel.mention}." + +#: embedutils\converters.py:241 +msgid "You do not have permissions to send embeds in {channel.mention}." +msgstr "Ви не маєте дозволів для надсилання вбудовуваних файлів до {channel.mention}." + +#: embedutils\converters.py:253 +msgid "I have to be the author of the message. You can use the command without providing a message to send one." +msgstr "Я маю бути автором повідомлення. Ви можете скористатися командою, не надаючи повідомлення для надсилання." + +#: embedutils\converters.py:263 +msgid "You are not allowed to edit embeds of an existing message (bot owner can set the permissions with the cog Permissions on the command `[p]embed edit`)." +msgstr "Ви не маєте права редагувати вбудовування існуючих повідомлень (власник бота може встановити дозволи за допомогою гвинтика Permissions у команді `[p]embed edit`)." + +#: embedutils\dashboard_integration.py:53 +msgid "You don't have permissions to access this page." +msgstr "У вас немає прав для доступу до цієї сторінки." + +#: embedutils\dashboard_integration.py:60 +msgid "I or you don't have permissions to send messages or embeds in any channel in this guild." +msgstr "У мене або у вас немає дозволів на надсилання повідомлень або вбудовування в будь-який канал у цій гільдії." + +#: embedutils\dashboard_integration.py:76 +msgid "Username:" +msgstr "Ім'я користувача:" + +#: embedutils\dashboard_integration.py:80 +msgid "Avatar URL:" +msgstr "URL-адреса аватара:" + +#: embedutils\dashboard_integration.py:84 +msgid "Data" +msgstr "Дані" + +#: embedutils\dashboard_integration.py:91 +msgid "Channels:" +msgstr "Канали:" + +#: embedutils\dashboard_integration.py:100 +msgid "Send Message(s)" +msgstr "Надіслати повідомлення(я)" + +#: embedutils\dashboard_integration.py:164 +msgid "Message(s) sent successfully!" +msgstr "Повідомлення успішно відправлено!" + +#: embedutils\embedutils.py:47 +#, docstring +msgid "Create, send, and store rich embeds, from Red-Web-Dashboard too!" +msgstr "Створюйте, надсилайте та зберігайте розширені вбудовування з Red-Web-Dashboard!" + +#: embedutils\embedutils.py:110 +#, docstring +msgid "Post embeds from valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Публікуйте вбудовування з коректного JSON.\n\n" +" Вони мають бути у форматі, очікуваному [**цією документацією Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Ось приклад: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Ви можете скористатися [**створювачем вбудовувань**](https://embedutils.com/), щоб отримати JSON-дані.\n\n" +" Якщо ви надасте повідомлення, його буде відредаговано.\n" +" Ви можете використовувати вкладення, і команда `[p]embed yamlfile` буде викликана автоматично.\n" +" " + +#: embedutils\embedutils.py:143 +#, docstring +msgid "Post embeds from valid YAML.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" You can use an attachment and the command `[p]embed yamlfile` will be invoked automatically.\n" +" " +msgstr "Публікуйте вбудовування з дійсного YAML.\n\n" +" Вони мають бути у форматі, передбаченому [**цією документацією Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Ось приклад: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Якщо ви надішлете повідомлення, його буде відредаговано.\n" +" Ви можете використати вкладення, і команда `[p]embed yamlfile` буде викликана автоматично.\n" +" " + +#: embedutils\embedutils.py:171 +#, docstring +msgid "Post an embed from a valid JSON file (upload it).\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" You can use an [**embeds creator**](https://embedutils.com/) to get a JSON payload.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Опублікуйте вбудовування з коректного JSON-файлу (завантажте його).\n\n" +" Він має бути у форматі, передбаченому [**цією документацією Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Ось приклад: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n" +" Ви можете скористатися [**творцем вбудованих програм**](https://embedutils.com/) для отримання корисного навантаження у форматі JSON.\n\n" +" Якщо ви надасте повідомлення, його буде відредаговано.\n" +" " + +#: embedutils\embedutils.py:186 embedutils\embedutils.py:217 +#: embedutils\embedutils.py:405 embedutils\embedutils.py:415 +#: embedutils\embedutils.py:505 embedutils\embedutils.py:515 +#: embedutils\embedutils.py:831 embedutils\embedutils.py:843 +msgid "Unreadable attachment with `utf-8`." +msgstr "Нечитабельне вкладення з `utf-8`." + +#: embedutils\embedutils.py:206 +#, docstring +msgid "Post an embed from a valid YAML file (upload it).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Опублікуйте вбудовування з коректного YAML-файлу (завантажте його).\n\n" +" Якщо ви надасте повідомлення, воно буде відредаговано.\n" +" " + +#: embedutils\embedutils.py:243 +#, docstring +msgid "Post embeds from a GitHub/Gist/Pastebin/Hastebin link containing valid JSON.\n\n" +" This must be in the format expected by [**this Discord documentation**](https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Here's an example: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Публікуйте вбудовування за посиланням GitHub/Gist/Pastebin/Hastebin, що містить коректний JSON.\n\n" +" Воно має бути у форматі, очікуваному [**цією документацією Discord**] (https://discord.com/developers/docs/resources/channel#embed-object).\n" +" Ось приклад: [**this example**](https://gist.github.com/AAA3A-AAA3A/3c9772b34a8ebc09b3b10018185f4cd4).\n\n" +" Якщо ви надасте повідомлення, його буде відредаговано.\n" +" " + +#: embedutils\embedutils.py:273 +#, docstring +msgid "Post embed(s) from an existing message.\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to send only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n\n" +" If you provide a message, it will be edited.\n" +" " +msgstr "Опублікуйте вбудовування з існуючого повідомлення.\n\n" +" Повідомлення повинно мати принаймні одне вбудовування.\n" +" Ви можете вказати індекс (починаючи з 0), якщо ви хочете надіслати лише одне з вбудовувань.\n" +" Якщо індекс не вказано, буде включено вміст вже надісланого повідомлення.\n\n" +" Якщо ви надішлете повідомлення, його буде відредаговано.\n" +" " + +#: embedutils\embedutils.py:325 +#, docstring +msgid "Download a JSON file for a message's embed(s).\n\n" +" The message must have at least one embed.\n" +" You can specify an index (starting by 0) if you want to include only one of the embeds.\n" +" The content of the message already sent is included if no index is specified.\n" +" " +msgstr "Завантажте JSON-файл для вбудовування повідомлення.\n\n" +" Повідомлення повинно мати принаймні одне вбудовування.\n" +" Ви можете вказати індекс (починаючи з 0), якщо хочете включити лише одне з вбудовувань.\n" +" Якщо індекс не вказано, буде включено вміст вже надісланого повідомлення.\n" +" " + +#: embedutils\embedutils.py:385 +#, docstring +msgid "Edit a message sent by [botname].\n\n" +" It would be better to use the `message` parameter of all the other commands.\n" +" " +msgstr "Відредагувати повідомлення, надіслане [ім'я бота].\n\n" +" Було б краще використовувати параметр `message` для всіх інших команд.\n" +" " + +#: embedutils\embedutils.py:477 +#, docstring +msgid "Store an embed.\n\n" +" Put the name in quotes if it is multiple words.\n" +" The `locked` argument specifies whether the embed should be locked to mod and superior only (guild level) or bot owners only (global level).\n" +" " +msgstr "Збережіть вбудовування.\n\n" +" Візьміть назву в лапки, якщо вона складається з кількох слів.\n" +" Аргумент `locked` вказує, чи має вбудовування бути доступним лише для модераторів і вищих за рангом (рівень гільдії) або лише для власників ботів (глобальний рівень).\n" +" " + +#: embedutils\embedutils.py:485 embedutils\embedutils.py:571 +#: embedutils\embedutils.py:586 embedutils\embedutils.py:618 +#: embedutils\embedutils.py:650 +msgid "You can't manage global stored embeds." +msgstr "Ви не можете керувати глобальними збереженими вбудовуваннями." + +#: embedutils\embedutils.py:548 +msgid "This server has reached the embed limit of {embed_limit}. You must remove an embed with `{ctx.clean_prefix}embed unstore` before you can add a new one." +msgstr "Цей сервер досягнув ліміту вбудовування {embed_limit}. Ви повинні видалити вбудовування за допомогою `{ctx.clean_prefix}embed unstore` перед тим, як додати нове." + +#: embedutils\embedutils.py:567 +#, docstring +msgid "Remove a stored embed." +msgstr "Видалити збережене вбудовування." + +#: embedutils\embedutils.py:577 embedutils\embedutils.py:623 +#: embedutils\embedutils.py:655 +msgid "This is not a stored embed at this level." +msgstr "На цьому рівні це не є збереженим вбудовуванням." + +#: embedutils\embedutils.py:584 embedutils\embedutils.py:614 +#, docstring +msgid "Get info about a stored embed." +msgstr "Отримати інформацію про збережене вбудовування." + +#: embedutils\embedutils.py:592 +msgid "No stored embeds is configured at this level." +msgstr "На цьому рівні не налаштовуються збережені вбудовування." + +#: embedutils\embedutils.py:596 +msgid "Global " +msgstr "Глобальний " + +#: embedutils\embedutils.py:596 +msgid "Stored Embeds" +msgstr "Збережені вставки" + +#: embedutils\embedutils.py:646 +#, docstring +msgid "Download a JSON file for a stored embed." +msgstr "Завантажте JSON-файл для збереженого вбудовування." + +#: embedutils\embedutils.py:675 +#, docstring +msgid "Post stored embeds." +msgstr "Публікуйте збережені вбудовування." + +#: embedutils\embedutils.py:697 embedutils\embedutils.py:748 +msgid "`{name}` is not a stored embed at this level." +msgstr "`{name}` не є збереженим вбудовуванням на цьому рівні." + +#: embedutils\embedutils.py:726 +#, docstring +msgid "Post stored embeds with a webhook." +msgstr "Публікуйте збережені вбудовування за допомогою веб-хука." + +#: embedutils\embedutils.py:793 +#, docstring +msgid "Get the link to the Dashboard." +msgstr "Отримайте посилання на Dashboard." + +#: embedutils\embedutils.py:796 +msgid "Red-Web-Dashboard is not installed. Check ." +msgstr "Red-Web-Dashboard не встановлено. Перевірте ." + +#: embedutils\embedutils.py:801 +msgid "You can't access the Dashboard." +msgstr "Ви не маєте доступу до інформаційної панелі." + +#: embedutils\embedutils.py:807 +msgid "This third party is disabled on the Dashboard." +msgstr "Ця третя сторона відключена на Панелі керування." + +#: embedutils\embedutils.py:872 +msgid "Dashboard - " +msgstr "Приладова панель - " + +#: embedutils\embedutils.py:876 +msgid "You can create and send rich embeds directly from the Dashboard!" +msgstr "Ви можете створювати та надсилати розширені вбудовування безпосередньо з Dashboard!" + +#: embedutils\embedutils.py:883 +msgid "The URL is too long to be displayed." +msgstr "URL-адреса занадто довга для відображення." + +#: embedutils\embedutils.py:889 +#, docstring +msgid "Migrate stored embeds from EmbedUtils by Phen." +msgstr "Перенесіть збережені вбудовування з EmbedUtils від Phen." + +#: embedutils\embedutils.py:933 +msgid "Data successfully migrated from EmbedUtils by Phen." +msgstr "Дані успішно перенесено з EmbedUtils від Phen." + diff --git a/embedutils/utils_version.json b/embedutils/utils_version.json new file mode 100644 index 0000000..bfab002 --- /dev/null +++ b/embedutils/utils_version.json @@ -0,0 +1 @@ +{"needed_utils_version": 7.0} \ No newline at end of file
+
+ +
+
+
+

Sender

+
+
+
+
+ + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + +

+
+
+
+

Message content

+
+
+ +
+
+ Embed 1 + + + + + + + + + + + + + +
+
+
+

Author

+
+
+
+
+ + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + +

+
+
+
+

Title

+
+
+ +
+
+

Description

+
+
+ +
+
+

Fields

+
+
+
+
+
+
+
+ +
+
+ +
+
+
+ +
+
+ + + + + + Remove +
+
+
+
+

New Field

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Thumbnail

+
+
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + +

+
+
+
+
+
+
+

Image

+
+
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + +

+
+
+
+
+
+ +
+
+
+ + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + +

+
+
+ + + + + +

+ +
+
+
+
+
+ + + +
+
Add Embed
+
+
+
+
+
+
+
+
+ + # + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + + + + + +
+
+

Embed Color

+

Pick the embed color

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if send_form %} +
+
+ {{ send_form|safe }} +
+
+ {% endif %} +
+
+
+
+
+  +

+ Discord Bot + + + + + APP + + +

+
+
+
+
+
+
+
+
+
+
+ +
+ + + + + + +
+
+
+ +
+ + + + + + +
+
+
+
+
+
+
NOTHING HERE
+
+
+
There is an error
+
+
+