diff --git a/.gitattributes b/.gitattributes index 165616c780154ad34db899b8981e633bac89ee27..ddae0087c363e2ea08c06083aaea6d8b8727335c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -38,3 +38,5 @@ bot/cogs/__pycache__/community.cpython-311.pyc filter=lfs diff=lfs merge=lfs -te bot/cogs/__pycache__/engagement.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text bot/cogs/__pycache__/media.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text database.db filter=lfs diff=lfs merge=lfs -text +bot/cogs/__pycache__/admin.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text +bot/cogs/__pycache__/events.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text diff --git a/bot/__pycache__/__init__.cpython-311.pyc b/bot/__pycache__/__init__.cpython-311.pyc index adaae6d76c0fa641b6eecfc51d636cd5fbd08dde..191d9d9597da34c0a3f0ec088fc8404ba2dcc084 100644 Binary files a/bot/__pycache__/__init__.cpython-311.pyc and b/bot/__pycache__/__init__.cpython-311.pyc differ diff --git a/bot/__pycache__/config.cpython-311.pyc b/bot/__pycache__/config.cpython-311.pyc index 9a8e89522b31ca843ca86ef0861dbba1592971c3..7e7530e13b6c42fee5b7d899416f5779869fa941 100644 Binary files a/bot/__pycache__/config.cpython-311.pyc and b/bot/__pycache__/config.cpython-311.pyc differ diff --git a/bot/__pycache__/database.cpython-311.pyc b/bot/__pycache__/database.cpython-311.pyc index c0bfb490b5ceaa7bcc2aef65aad950fa5c819124..32dd7382747ce50347b2f4286727ad7607d803a3 100644 Binary files a/bot/__pycache__/database.cpython-311.pyc and b/bot/__pycache__/database.cpython-311.pyc differ diff --git a/bot/__pycache__/emojis.cpython-311.pyc b/bot/__pycache__/emojis.cpython-311.pyc index 8144800f392f97d95b2190abded926faf17975f8..484461a0b901ca42135ee1621c6c1c24bd055540 100644 Binary files a/bot/__pycache__/emojis.cpython-311.pyc and b/bot/__pycache__/emojis.cpython-311.pyc differ diff --git a/bot/__pycache__/i18n.cpython-311.pyc b/bot/__pycache__/i18n.cpython-311.pyc index 1df97307bef794b8b91d407b60a68446aa49fba6..72bf17a536ed99fe43ff9866833d22e8ac2f0ea9 100644 Binary files a/bot/__pycache__/i18n.cpython-311.pyc and b/bot/__pycache__/i18n.cpython-311.pyc differ diff --git a/bot/__pycache__/main.cpython-311.pyc b/bot/__pycache__/main.cpython-311.pyc index c92e92ef652b961b7e2114471bf005c4c4431eae..cf3a9333cff84975f7664c52adce40874891e2b3 100644 Binary files a/bot/__pycache__/main.cpython-311.pyc and b/bot/__pycache__/main.cpython-311.pyc differ diff --git a/bot/__pycache__/server.cpython-311.pyc b/bot/__pycache__/server.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3969517dd6fd19d3e45442216c8e042d5c4d0895 Binary files /dev/null and b/bot/__pycache__/server.cpython-311.pyc differ diff --git a/bot/__pycache__/theme.cpython-311.pyc b/bot/__pycache__/theme.cpython-311.pyc index 3fe26300b0be7ef8e0c0ba081e52e53fff96963a..69de51e443710e0c57944e1ef8f6e644a5ee9055 100644 Binary files a/bot/__pycache__/theme.cpython-311.pyc and b/bot/__pycache__/theme.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/__init__.cpython-311.pyc b/bot/cogs/__pycache__/__init__.cpython-311.pyc index 1686ebaa76c0fc94871a1c6a67808e7d7a175a77..63eecbfc9ce9de66510b300822183fed93a67d93 100644 Binary files a/bot/cogs/__pycache__/__init__.cpython-311.pyc and b/bot/cogs/__pycache__/__init__.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/admin.cpython-311.pyc b/bot/cogs/__pycache__/admin.cpython-311.pyc index 10dac5eee8b9e7773323ba77730534f359ceea7e..bc81a21e0d8e2250ebb991ab18b6c45be0795700 100644 Binary files a/bot/cogs/__pycache__/admin.cpython-311.pyc and b/bot/cogs/__pycache__/admin.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/ai_admin.cpython-311.pyc b/bot/cogs/__pycache__/ai_admin.cpython-311.pyc index 4f7c19177a456efcb44a9f933662f2c0f3cb95a6..fc3d22c7d4529dae00f833cc3feededb444690de 100644 Binary files a/bot/cogs/__pycache__/ai_admin.cpython-311.pyc and b/bot/cogs/__pycache__/ai_admin.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/ai_suite.cpython-311.pyc b/bot/cogs/__pycache__/ai_suite.cpython-311.pyc index 5df44b37f1ab37ad5c82044545d7ddbdd8a9436d..912fe8ee49e7c53f6bcac866ca6b845f654e68ae 100644 --- a/bot/cogs/__pycache__/ai_suite.cpython-311.pyc +++ b/bot/cogs/__pycache__/ai_suite.cpython-311.pyc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf125ae44086772a36fa22b27791a407f2e63f0fc7f9bf8b356e9e54695a1968 -size 143051 +oid sha256:e3d87a3a72d573124f17d8e63596d2f74a90fe1a464d7c3e483a0a7a1b97fae8 +size 142795 diff --git a/bot/cogs/__pycache__/banner_manager.cpython-311.pyc b/bot/cogs/__pycache__/banner_manager.cpython-311.pyc index 6fd790ede87293d937a1e3733dc9b882ea2fa6be..8c14828159d4c5c8e0a4ec2b3e209929aa4f2baa 100644 Binary files a/bot/cogs/__pycache__/banner_manager.cpython-311.pyc and b/bot/cogs/__pycache__/banner_manager.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/board_games.cpython-311.pyc b/bot/cogs/__pycache__/board_games.cpython-311.pyc index 1464fac6b695bf5f047a5aef7a1140e7d5f3f0b2..e674d2079bf37edfe7d3cc009df0c806247e1038 100644 Binary files a/bot/cogs/__pycache__/board_games.cpython-311.pyc and b/bot/cogs/__pycache__/board_games.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/community.cpython-311.pyc b/bot/cogs/__pycache__/community.cpython-311.pyc index 81640491a4297b4d362917cf90a207e41a9dfb7e..6924849ca3dc20a32d0f4fa8ed2af0093b9e43c9 100644 --- a/bot/cogs/__pycache__/community.cpython-311.pyc +++ b/bot/cogs/__pycache__/community.cpython-311.pyc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a34e59b7f605ed55c2ecfed7028c01ccf315cac7aea0be8ac3452cb8e3530c26 -size 119508 +oid sha256:87a05d115d6c14cd3607f3ce9d415d52e370582ddf9817c707a8002eb0f39d66 +size 131193 diff --git a/bot/cogs/__pycache__/configuration.cpython-311.pyc b/bot/cogs/__pycache__/configuration.cpython-311.pyc index 0d2b4f4f681bec725bf907f22670dd5a68494400..ac8e311543985b2e72791e902279b67f1637e394 100644 Binary files a/bot/cogs/__pycache__/configuration.cpython-311.pyc and b/bot/cogs/__pycache__/configuration.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/developer.cpython-311.pyc b/bot/cogs/__pycache__/developer.cpython-311.pyc index 46af9ea47ea168bf936b52f67e6e126b77191b28..af3e09c78212e29ef7779c40dfad32f9a61adf73 100644 Binary files a/bot/cogs/__pycache__/developer.cpython-311.pyc and b/bot/cogs/__pycache__/developer.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/engagement.cpython-311.pyc b/bot/cogs/__pycache__/engagement.cpython-311.pyc index 67cf66360b3731ed7d88c27f54ddac0f44612b9a..94eacc87e94f75ad68799235f76ea3a3b6a930e9 100644 --- a/bot/cogs/__pycache__/engagement.cpython-311.pyc +++ b/bot/cogs/__pycache__/engagement.cpython-311.pyc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbc07318ec00e7f522658a482610aa30df29d4ef3f95836f5d8eb7b0a6e794ca -size 115797 +oid sha256:2dab296a18802c5fb56ad3d9d1e26cfcd915adc3b82be3c4dd50c7b294a781a2 +size 120855 diff --git a/bot/cogs/__pycache__/events.cpython-311.pyc b/bot/cogs/__pycache__/events.cpython-311.pyc index 582af6a1832946df73f5e634f60ddcb11d216f7f..4664e9365088c70393731e9d9fe146ff0daa04d1 100644 Binary files a/bot/cogs/__pycache__/events.cpython-311.pyc and b/bot/cogs/__pycache__/events.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/fun.cpython-311.pyc b/bot/cogs/__pycache__/fun.cpython-311.pyc index 4fbdbda9dc1a95232b16201c3670cad708f23a00..0678b3bab7934c1ed8d6d272c868e87463bda20a 100644 Binary files a/bot/cogs/__pycache__/fun.cpython-311.pyc and b/bot/cogs/__pycache__/fun.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/gambling.cpython-311.pyc b/bot/cogs/__pycache__/gambling.cpython-311.pyc index ed295c0e09ca5dc3a2c454ecc653455d30ca5bb2..2759538f49bac3cef9a3f4d3dc2bcaa0f0f37beb 100644 Binary files a/bot/cogs/__pycache__/gambling.cpython-311.pyc and b/bot/cogs/__pycache__/gambling.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/language.cpython-311.pyc b/bot/cogs/__pycache__/language.cpython-311.pyc index 3f813c1e562312a335af917a19569dc64ae25982..1a8f223ef502151635791861cb54916e0848bc22 100644 Binary files a/bot/cogs/__pycache__/language.cpython-311.pyc and b/bot/cogs/__pycache__/language.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/media.cpython-311.pyc b/bot/cogs/__pycache__/media.cpython-311.pyc index c0e05672a0ef3fdd4cffd895353c888fce82f6ec..5f559367c5035da3eca63d979b39daf0f10e773e 100644 --- a/bot/cogs/__pycache__/media.cpython-311.pyc +++ b/bot/cogs/__pycache__/media.cpython-311.pyc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9cd726a7fc89417a87ada78320e9485c5113bbbf0a053c4d9efbeb9c1ac8960a -size 209239 +oid sha256:ca5f86939d327aa5b764e849e35198a8f0881667d5202772e915f0a92899da33 +size 220295 diff --git a/bot/cogs/__pycache__/media_helpers.cpython-311.pyc b/bot/cogs/__pycache__/media_helpers.cpython-311.pyc index 27b140972878f8b17aff4096e606e39768e19d0d..92c971129769ad4f9e68a751a5494f4d569a4967 100644 Binary files a/bot/cogs/__pycache__/media_helpers.cpython-311.pyc and b/bot/cogs/__pycache__/media_helpers.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/menu.cpython-311.pyc b/bot/cogs/__pycache__/menu.cpython-311.pyc index 37c6269f1215890321a656f132c8aa5c6486295e..3dac427d6184947ae1752e002dfccd0141eeb368 100644 Binary files a/bot/cogs/__pycache__/menu.cpython-311.pyc and b/bot/cogs/__pycache__/menu.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/observability.cpython-311.pyc b/bot/cogs/__pycache__/observability.cpython-311.pyc index b1c710ac29c910b72ccda88132ba85c4074bd040..e24a59cc46ad6f1c95d451a6eb79f85eb86cdc24 100644 Binary files a/bot/cogs/__pycache__/observability.cpython-311.pyc and b/bot/cogs/__pycache__/observability.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/server_manager.cpython-311.pyc b/bot/cogs/__pycache__/server_manager.cpython-311.pyc index c69c855b1cbf1557fa5a1ce089a0e6f5743a78d8..efaecf0f2500e139a4c989fb7ce056f29d68758f 100644 Binary files a/bot/cogs/__pycache__/server_manager.cpython-311.pyc and b/bot/cogs/__pycache__/server_manager.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/utility.cpython-311.pyc b/bot/cogs/__pycache__/utility.cpython-311.pyc index 1c730715fef950166366db785d281045b0107be2..3d48ff76a2e036e97310a4ef8a5081a8ac318752 100644 Binary files a/bot/cogs/__pycache__/utility.cpython-311.pyc and b/bot/cogs/__pycache__/utility.cpython-311.pyc differ diff --git a/bot/cogs/__pycache__/verification.cpython-311.pyc b/bot/cogs/__pycache__/verification.cpython-311.pyc index e593f7049265356aa39ff24c0625b7cf3bd35e54..4996d67d4c16d141191cdeeaff02bc0a274962f7 100644 Binary files a/bot/cogs/__pycache__/verification.cpython-311.pyc and b/bot/cogs/__pycache__/verification.cpython-311.pyc differ diff --git a/bot/cogs/admin.py b/bot/cogs/admin.py index ef64a77904464119151bc9342fec6c287ce698b2..a7d49f4cea05c19d1d2b8ebff8cadbf21c4d06f5 100644 --- a/bot/cogs/admin.py +++ b/bot/cogs/admin.py @@ -1,1443 +1,1820 @@ -""" -Admin cog: Moderation and server management commands. -Enhanced with rich emoji decorations and beautiful formatting. -""" - -import datetime as dt -import hashlib -import json -import random -import re -from typing import Optional - -import discord -from discord.ext import commands - -from bot.theme import ( - fancy_header, NEON_CYAN, NEON_PINK, NEON_PURPLE, NEON_LIME, NEON_ORANGE, NEON_RED, - NEON_BLUE, NEON_YELLOW, panel_divider, success_embed, error_embed, warning_embed, info_embed, - double_line, triple_line, shimmer, pick_neon_color, add_banner_to_embed -) -from bot.emojis import ( - ui, E_SHIELD, E_CROWN, E_TROPHY, E_FIRE, E_SPARKLE, E_LOCK, E_KEY, - E_ARROW_BLUE, E_ARROW_GREEN, E_ARROW_PINK, E_ARROW_PURPLE, E_GEM, E_STAR -) - - -# ═══════════════════════════════════════════════════════════════════════════════ -# AWESOME ROLES DEFINITIONS -# ═══════════════════════════════════════════════════════════════════════════════ - +""" +Admin cog: Moderation and server management commands. +Enhanced with rich emoji decorations and beautiful formatting. +""" + +import datetime as dt +import hashlib +import json +import random +import re +from typing import Optional + +import aiohttp +import discord +from discord.ext import commands + +from bot.theme import ( + fancy_header, NEON_CYAN, NEON_PINK, NEON_PURPLE, NEON_LIME, NEON_ORANGE, NEON_RED, + NEON_BLUE, NEON_YELLOW, panel_divider, success_embed, error_embed, warning_embed, info_embed, + double_line, triple_line, shimmer, pick_neon_color, add_banner_to_embed +) +from bot.emojis import ( + ui, E_SHIELD, E_CROWN, E_TROPHY, E_FIRE, E_SPARKLE, E_LOCK, E_KEY, + E_ARROW_BLUE, E_ARROW_GREEN, E_ARROW_PINK, E_ARROW_PURPLE, E_GEM, E_STAR +) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# AWESOME ROLES DEFINITIONS +# ═══════════════════════════════════════════════════════════════════════════════ + AWESOME_ROLES = [ - { - "name": "✨ Cyan Legend", - "color": NEON_CYAN, - "permissions": ["view_channel", "send_messages", "embed_links", "attach_files", "read_message_history"], - "hoist": True, - "description": "Elite member with cyan glow" - }, - { - "name": "💗 Pink Master", - "color": NEON_PINK, - "permissions": ["view_channel", "send_messages", "embed_links", "attach_files", "read_message_history", "add_reactions"], - "hoist": True, - "description": "Creative member with pink style" - }, - { - "name": "🟢 Purple Elite", - "color": NEON_PURPLE, - "permissions": ["view_channel", "send_messages", "embed_links", "attach_files", "read_message_history", "use_external_emojis"], - "hoist": True, - "description": "Premium member with purple flair" - }, - { - "name": "💚 Green Champion", - "color": NEON_LIME, - "permissions": ["view_channel", "send_messages", "embed_links", "attach_files", "read_message_history", "stream"], - "hoist": True, - "description": "Active member with green energy" - }, - { - "name": "🧡 Orange Hero", - "color": NEON_ORANGE, - "permissions": ["view_channel", "send_messages", "embed_links", "attach_files", "read_message_history", "connect", "speak"], - "hoist": True, - "description": "Helpful member with orange vibe" - }, - { - "name": "💛 Golden Star", - "color": NEON_YELLOW, - "permissions": ["view_channel", "send_messages", "embed_links", "attach_files", "read_message_history", "change_nickname"], - "hoist": True, - "description": "Valued member with golden touch" - }, - { - "name": "💙 Azure Guardian", - "color": NEON_BLUE, - "permissions": ["view_channel", "send_messages", "embed_links", "attach_files", "read_message_history", "manage_nicknames"], - "hoist": True, - "description": "Trusted member with azure power" - }, + { + "name": "✨ Cyan Legend", + "color": NEON_CYAN, + "permissions": ["view_channel", "send_messages", "embed_links", "attach_files", "read_message_history"], + "hoist": True, + "description": "Elite member with cyan glow" + }, + { + "name": "<:animatedarrowpink:1477261266690113651> Pink Master", + "color": NEON_PINK, + "permissions": ["view_channel", "send_messages", "embed_links", "attach_files", "read_message_history", "add_reactions"], + "hoist": True, + "description": "Creative member with pink style" + }, + { + "name": "<:animatedarrowgreen:1477261279428087979> Purple Elite", + "color": NEON_PURPLE, + "permissions": ["view_channel", "send_messages", "embed_links", "attach_files", "read_message_history", "use_external_emojis"], + "hoist": True, + "description": "Premium member with purple flair" + }, + { + "name": "<:animatedarrowgreen:1477261279428087979> Green Champion", + "color": NEON_LIME, + "permissions": ["view_channel", "send_messages", "embed_links", "attach_files", "read_message_history", "stream"], + "hoist": True, + "description": "Active member with green energy" + }, + { + "name": "🧡 Orange Hero", + "color": NEON_ORANGE, + "permissions": ["view_channel", "send_messages", "embed_links", "attach_files", "read_message_history", "connect", "speak"], + "hoist": True, + "description": "Helpful member with orange vibe" + }, + { + "name": "💛 Golden Star", + "color": NEON_YELLOW, + "permissions": ["view_channel", "send_messages", "embed_links", "attach_files", "read_message_history", "change_nickname"], + "hoist": True, + "description": "Valued member with golden touch" + }, + { + "name": "💙 Azure Guardian", + "color": NEON_BLUE, + "permissions": ["view_channel", "send_messages", "embed_links", "attach_files", "read_message_history", "manage_nicknames"], + "hoist": True, + "description": "Trusted member with azure power" + }, ] +PRESENCE_STATUS_MAP: dict[str, discord.Status] = { + "online": discord.Status.online, + "idle": discord.Status.idle, + "dnd": discord.Status.dnd, + "invisible": discord.Status.invisible, + "offline": discord.Status.invisible, +} -class AdminMasterPanel(discord.ui.View): - def __init__(self, cog: "Admin") -> None: - super().__init__(timeout=None) - self.cog = cog +PRESENCE_ACTIVITY_MAP: dict[str, discord.ActivityType] = { + "playing": discord.ActivityType.playing, + "watching": discord.ActivityType.watching, + "listening": discord.ActivityType.listening, + "competing": discord.ActivityType.competing, +} - @discord.ui.select( - placeholder="Admin Panel | لوحة الإدارة", - min_values=1, - max_values=1, - custom_id="admin_master_panel_select", - options=[ - discord.SelectOption(label="💰 Economy Admin | إدارة الاقتصاد", value="economy"), - discord.SelectOption(label="🛡️ Shield Control | تحكم الدرع", value="shield"), - discord.SelectOption(label="📊 System Status | حالة النظام", value="status"), - discord.SelectOption(label="🎁 Giveaways | القيافاوي", value="giveaways"), - discord.SelectOption(label="🏆 Tournaments | البطولات", value="tournaments"), - discord.SelectOption(label="📊 Polls | التصويتات", value="polls"), - discord.SelectOption(label="🎫 Tickets | التذاكر", value="tickets"), - discord.SelectOption(label="🤖 AI Admin | الذكاء الاصطناعي", value="ai_admin"), - discord.SelectOption(label="🔧 Server Config | الإعدادات", value="config"), - ], - ) - async def select_action(self, interaction: discord.Interaction, select: discord.ui.Select) -> None: - choice = select.values[0] - if choice == "economy": - await interaction.response.edit_message( - embed=await self.cog.build_economy_admin_embed(interaction.guild), - view=EconomyAdminPanel(self.cog), - ) - return - if choice == "shield": - await interaction.response.send_message("Use: `/shield_level low|medium|high`", ephemeral=True) - return + +class AdminMasterPanel(discord.ui.View): + def __init__(self, cog: "Admin") -> None: + super().__init__(timeout=None) + self.cog = cog + + @discord.ui.select( + placeholder="Admin Panel | لوحة الإدارة", + min_values=1, + max_values=1, + custom_id="admin_master_panel_select", + options=[ + discord.SelectOption(label="💰 Economy Admin | إدارة الاقتصاد", value="economy"), + discord.SelectOption(label="🛡️ Shield Control | تحكم الدرع", value="shield"), + discord.SelectOption(label="📊 System Status | حالة النظام", value="status"), + discord.SelectOption(label="🎁 Giveaways | القيافاوي", value="giveaways"), + discord.SelectOption(label="🏆 Tournaments | البطولات", value="tournaments"), + discord.SelectOption(label="📊 Polls | التصويتات", value="polls"), + discord.SelectOption(label="🎫 Tickets | التذاكر", value="tickets"), + discord.SelectOption(label="🤖 AI Admin | الذكاء الاصطناعي", value="ai_admin"), + discord.SelectOption(label="🔧 Server Config | الإعدادات", value="config"), + ], + ) + async def select_action(self, interaction: discord.Interaction, select: discord.ui.Select) -> None: + choice = select.values[0] + if choice == "economy": + await interaction.response.edit_message( + embed=await self.cog.build_economy_admin_embed(interaction.guild), + view=EconomyAdminPanel(self.cog), + ) + return + if choice == "shield": + await interaction.response.send_message("Use: `/shield_state` or `/shield_level low|medium|high`", ephemeral=True) + return if choice == "status": synced = "Enabled" if self.cog.bot.db._hf_sync_enabled else "Disabled" - await interaction.response.send_message(f"HF DB Sync: **{synced}**", ephemeral=True) - return - if choice == "giveaways": - await interaction.response.send_message( - "**Giveaway Commands:**\n" - "`/giveaway start` - Start a new giveaway\n" - "`/giveaway end ` - End a giveaway\n" - "`/giveaway reroll ` - Reroll a giveaway", - ephemeral=True - ) - return - if choice == "tournaments": - await interaction.response.send_message( - "**Tournament Commands:**\n" - "`/tournament create` - Create a tournament\n" - "`/tournament join` - Join a tournament\n" - "`/tournament start` - Start a tournament\n" - "`/tournament end` - End a tournament", - ephemeral=True - ) - return - if choice == "polls": - await interaction.response.send_message( - "**Poll Commands:**\n" - "`/poll create` - Create a poll\n" - "`/poll end ` - End a poll\n" - "`/poll results ` - View poll results", - ephemeral=True - ) - return - if choice == "tickets": + activity_obj = getattr(self.cog.bot, "activity", None) + activity_name = getattr(activity_obj, "name", None) or "CYBER // GRID" + activity_type = str(getattr(activity_obj, "type", "playing")).replace("ActivityType.", "") + status_name = str(getattr(self.cog.bot, "status", "online")).replace("Status.", "") await interaction.response.send_message( - "**Ticket Commands:**\n" - "`/ticket_panel setup` - Setup ticket panel\n" - "`/ticket close` - Close current ticket\n" - "`/ticket delete` - Delete current ticket", - ephemeral=True + ( + f"HF DB Sync: **{synced}**\n" + f"Bot Status: **{status_name}**\n" + f"Bot Activity: **{activity_type}** • **{activity_name}**\n\n" + "Owner commands:\n" + "`/set_bot_status `\n" + "`/reset_bot_status`\n" + "`/bot_status`" + ), + ephemeral=True, ) return - if choice == "ai_admin": - await interaction.response.send_message( - "**AI Admin Commands:**\n" - "`/ai_admin ` - Let AI manage the server\n" - "`/ai_help` - Show AI capabilities\n\n" - "Examples:\n" - "- Create a giveaway for Nitro\n" - "- Setup a Valorant tournament\n" - "- Create a moderator role", - ephemeral=True - ) - return - if choice == "config": - await interaction.response.send_message( - "**Config Commands:**\n" - "`/config panel` - Open config panel\n" - "`/language set` - Set server language\n" - "`/prefix set` - Set bot prefix", - ephemeral=True - ) - return - return - if choice == "giveaways": - await interaction.response.send_message( - "**Giveaway Commands:**\n" - "`/giveaway start` - Start a new giveaway\n" - "`/giveaway end ` - End a giveaway\n" - "`/giveaway reroll ` - Reroll a giveaway", - ephemeral=True - ) - return - if choice == "tournaments": - await interaction.response.send_message( - "**Tournament Commands:**\n" - "`/tournament create` - Create a tournament\n" - "`/tournament join` - Join a tournament\n" - "`/tournament start` - Start a tournament\n" - "`/tournament end` - End a tournament", - ephemeral=True - ) - return - if choice == "polls": - await interaction.response.send_message( - "**Poll Commands:**\n" - "`/poll create` - Create a poll\n" - "`/poll end ` - End a poll\n" - "`/poll results ` - View poll results", - ephemeral=True - ) - return - if choice == "tickets": - await interaction.response.send_message( - "**Ticket Commands:**\n" - "`/ticket_panel setup` - Setup ticket panel\n" - "`/ticket close` - Close current ticket\n" - "`/ticket delete` - Delete current ticket", - ephemeral=True - ) - return - if choice == "ai_admin": - await interaction.response.send_message( - "**AI Admin Commands:**\n" - "`/ai_admin ` - Let AI manage the server\n" - "`/ai_help` - Show AI capabilities\n\n" - "Examples:\n" - "- Create a giveaway for Nitro\n" - "- Setup a Valorant tournament\n" - "- Create a moderator role", - ephemeral=True - ) - return - if choice == "config": - await interaction.response.send_message( - "**Config Commands:**\n" - "`/config panel` - Open config panel\n" - "`/language set` - Set server language\n" - "`/prefix set` - Set bot prefix", - ephemeral=True - ) - return - - -class EconomyAdminModalBase(discord.ui.Modal): - member_id = discord.ui.TextInput( - label="User ID / Mention", - placeholder="123456789012345678 or @user", - required=True, - max_length=32, - ) - - def __init__(self, cog: "Admin", title: str) -> None: - super().__init__(title=title) - self.cog = cog - - async def _resolve_member_or_reply(self, interaction: discord.Interaction) -> discord.Member | None: - guild = interaction.guild - if guild is None: - await interaction.response.send_message("This action must be used in a server.", ephemeral=True) - return None - member = await self.cog.resolve_member(guild, str(self.member_id)) - if member is None: - await interaction.response.send_message("Could not find that member.", ephemeral=True) - return None - return member - - -class AddCoinsModal(EconomyAdminModalBase): - amount = discord.ui.TextInput(label="Coins To Add", placeholder="250", required=True, max_length=12) - - def __init__(self, cog: "Admin") -> None: - super().__init__(cog, "➕ Add Coins") - - async def on_submit(self, interaction: discord.Interaction) -> None: - member = await self._resolve_member_or_reply(interaction) - if member is None: - return - amount = self.cog.parse_positive_int(str(self.amount)) - if amount is None: - await interaction.response.send_message("Amount must be a positive number.", ephemeral=True) - return - before, after = await self.cog.adjust_wallet(interaction.guild.id, member.id, amount) - await interaction.response.send_message( - f"✅ Added `{amount}` coins for {member.mention}. Wallet `{before}` → `{after}`.", - ephemeral=True, - ) - - -class RemoveCoinsModal(EconomyAdminModalBase): - amount = discord.ui.TextInput(label="Coins To Remove", placeholder="150", required=True, max_length=12) - - def __init__(self, cog: "Admin") -> None: - super().__init__(cog, "➖ Remove Coins") - - async def on_submit(self, interaction: discord.Interaction) -> None: - member = await self._resolve_member_or_reply(interaction) - if member is None: - return - amount = self.cog.parse_positive_int(str(self.amount)) - if amount is None: - await interaction.response.send_message("Amount must be a positive number.", ephemeral=True) - return - before, after = await self.cog.adjust_wallet(interaction.guild.id, member.id, -amount) - await interaction.response.send_message( - f"✅ Removed `{amount}` coins for {member.mention}. Wallet `{before}` → `{after}`.", - ephemeral=True, - ) - - -class SetBalanceModal(EconomyAdminModalBase): - amount = discord.ui.TextInput(label="New Wallet Balance", placeholder="5000", required=True, max_length=12) - - def __init__(self, cog: "Admin") -> None: - super().__init__(cog, "⚖️ Set Balance") - - async def on_submit(self, interaction: discord.Interaction) -> None: - member = await self._resolve_member_or_reply(interaction) - if member is None: - return - target = self.cog.parse_non_negative_int(str(self.amount)) - if target is None: - await interaction.response.send_message("Balance must be zero or more.", ephemeral=True) - return - before, after = await self.cog.set_wallet(interaction.guild.id, member.id, target) - await interaction.response.send_message( - f"✅ Set wallet for {member.mention}. `{before}` → `{after}`.", - ephemeral=True, - ) - - -class SetSalaryModal(discord.ui.Modal): - min_salary = discord.ui.TextInput(label="Minimum Work Salary", placeholder="50", required=True, max_length=12) - max_salary = discord.ui.TextInput(label="Maximum Work Salary", placeholder="150", required=True, max_length=12) - - def __init__(self, cog: "Admin") -> None: - super().__init__(title="💸 Set Salary") - self.cog = cog - - async def on_submit(self, interaction: discord.Interaction) -> None: - if interaction.guild is None: - await interaction.response.send_message("This action must be used in a server.", ephemeral=True) - return - min_val = self.cog.parse_non_negative_int(str(self.min_salary)) - max_val = self.cog.parse_non_negative_int(str(self.max_salary)) - if min_val is None or max_val is None or min_val > max_val: - await interaction.response.send_message("Use valid integers with min <= max.", ephemeral=True) - return - await self.cog.bot.db.execute( - "INSERT INTO economy_salaries(guild_id, min_salary, max_salary) VALUES (?, ?, ?) " - "ON CONFLICT(guild_id) DO UPDATE SET min_salary = excluded.min_salary, max_salary = excluded.max_salary", - interaction.guild.id, - min_val, - max_val, - ) - await interaction.response.send_message( - f"✅ Work salary updated to `{min_val}` - `{max_val}`.", - ephemeral=True, - ) - - -class SetDailyModal(discord.ui.Modal): - daily_min = discord.ui.TextInput(label="Minimum Daily Reward", placeholder="100", required=True, max_length=12) - daily_max = discord.ui.TextInput(label="Maximum Daily Reward", placeholder="250", required=True, max_length=12) - - def __init__(self, cog: "Admin") -> None: - super().__init__(title="📆 Set Daily") - self.cog = cog - - async def on_submit(self, interaction: discord.Interaction) -> None: - if interaction.guild is None: - await interaction.response.send_message("This action must be used in a server.", ephemeral=True) - return - min_val = self.cog.parse_non_negative_int(str(self.daily_min)) - max_val = self.cog.parse_non_negative_int(str(self.daily_max)) - if min_val is None or max_val is None or min_val > max_val: - await interaction.response.send_message("Use valid integers with min <= max.", ephemeral=True) - return - await self.cog.bot.db.execute( - "INSERT INTO economy_salaries(guild_id, daily_min, daily_max) VALUES (?, ?, ?) " - "ON CONFLICT(guild_id) DO UPDATE SET daily_min = excluded.daily_min, daily_max = excluded.daily_max", - interaction.guild.id, - min_val, - max_val, - ) - await interaction.response.send_message( - f"✅ Daily reward updated to `{min_val}` - `{max_val}`.", - ephemeral=True, - ) - - -class EconomyAdminPanel(discord.ui.View): - def __init__(self, cog: "Admin") -> None: - super().__init__(timeout=300) - self.cog = cog - - @discord.ui.button(label="➕ Add Coins", style=discord.ButtonStyle.success, row=0) - async def add_coins(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: - await interaction.response.send_modal(AddCoinsModal(self.cog)) - - @discord.ui.button(label="💸 Set Salary", style=discord.ButtonStyle.primary, row=0) - async def set_salary(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: - await interaction.response.send_modal(SetSalaryModal(self.cog)) - - @discord.ui.button(label="➖ Remove", style=discord.ButtonStyle.danger, row=1) - async def remove_coins(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: - await interaction.response.send_modal(RemoveCoinsModal(self.cog)) - - @discord.ui.button(label="📆 Set Daily", style=discord.ButtonStyle.secondary, row=1) - async def set_daily(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: - await interaction.response.send_modal(SetDailyModal(self.cog)) - - @discord.ui.button(label="⚖️ Set Balance", style=discord.ButtonStyle.primary, row=2) - async def set_balance(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: - await interaction.response.send_modal(SetBalanceModal(self.cog)) - - @discord.ui.button(label="⬅️ Back", style=discord.ButtonStyle.secondary, row=2) - async def back(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: - await interaction.response.edit_message(embed=await self.cog.build_admin_home_embed(interaction.guild), view=AdminMasterPanel(self.cog)) - - -class Admin(commands.Cog): - """Moderation commands with beautiful panels.""" - - def __init__(self, bot: commands.Bot) -> None: - self.bot = bot - - async def cog_load(self) -> None: - self.bot.add_view(AdminMasterPanel(self)) - - async def build_admin_home_embed(self, guild: discord.Guild | None = None) -> discord.Embed: - embed = success_embed( - "꧁⫷ 𝕄𝕠تآز 𝕊𝕪𝕤𝕥𝕖𝕞 ⫸꧂", - "╔════╗\n💰 Economy Admin | إدارة الاقتصاد\n🛡️ Shield Control | تحكم الدرع\n📊 System Status | حالة النظام\n╚════╝", - ) - embed.set_footer(text="🏮 Powered by BOT- AI Suite 🏮") - if guild: - await add_banner_to_embed(embed, guild) - return embed - - async def build_economy_admin_embed(self, guild: discord.Guild | None = None) -> discord.Embed: - embed = info_embed( - f"{ui('economy')} Economy Admin", - "Choose one action below:\n" - "➕ Add Coins\n" - "💸 Set Salary\n" - "➖ Remove Coins\n" - "📆 Set Daily\n" - "⚖️ Set Balance", - ) - if guild: - await add_banner_to_embed(embed, guild) - return embed - - @staticmethod - def parse_positive_int(value: str) -> int | None: - try: - parsed = int(value.strip()) - except (TypeError, ValueError): - return None - return parsed if parsed > 0 else None - - @staticmethod - def parse_non_negative_int(value: str) -> int | None: - try: - parsed = int(value.strip()) - except (TypeError, ValueError): - return None - return parsed if parsed >= 0 else None - - async def resolve_member(self, guild: discord.Guild, raw: str) -> discord.Member | None: - match = re.search(r"\d{15,22}", raw or "") - if not match: - return None - member_id = int(match.group(0)) - member = guild.get_member(member_id) - if member is not None: - return member - try: - return await guild.fetch_member(member_id) - except discord.HTTPException: - return None - - async def get_wallet(self, guild_id: int, user_id: int) -> int: - row = await self.bot.db.fetchone( - "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?", - guild_id, - user_id, - ) - return int(row[0]) if row else 0 - - async def adjust_wallet(self, guild_id: int, user_id: int, delta: int) -> tuple[int, int]: - before = await self.get_wallet(guild_id, user_id) - after = max(0, before + delta) - await self.bot.db.execute( - "INSERT INTO user_balance(guild_id, user_id, wallet, bank) VALUES (?, ?, ?, 0) " - "ON CONFLICT(guild_id, user_id) DO UPDATE SET wallet = excluded.wallet", - guild_id, - user_id, - after, - ) - return before, after - - async def set_wallet(self, guild_id: int, user_id: int, target: int) -> tuple[int, int]: - before = await self.get_wallet(guild_id, user_id) - after = max(0, target) - await self.bot.db.execute( - "INSERT INTO user_balance(guild_id, user_id, wallet, bank) VALUES (?, ?, ?, 0) " - "ON CONFLICT(guild_id, user_id) DO UPDATE SET wallet = excluded.wallet", - guild_id, - user_id, - after, - ) - return before, after - + if choice == "giveaways": + await interaction.response.send_message( + "**Giveaway Commands:**\n" + "`/giveaway start` - Start a new giveaway\n" + "`/giveaway end ` - End a giveaway\n" + "`/giveaway reroll ` - Reroll a giveaway", + ephemeral=True + ) + return + if choice == "tournaments": + await interaction.response.send_message( + "**Tournament Commands:**\n" + "`/tournament create` - Create a tournament\n" + "`/tournament join` - Join a tournament\n" + "`/tournament start` - Start a tournament\n" + "`/tournament end` - End a tournament", + ephemeral=True + ) + return + if choice == "polls": + await interaction.response.send_message( + "**Poll Commands:**\n" + "`/poll create` - Create a poll\n" + "`/poll end ` - End a poll\n" + "`/poll results ` - View poll results", + ephemeral=True + ) + return + if choice == "tickets": + await interaction.response.send_message( + "**Ticket Commands:**\n" + "`/ticket_panel setup` - Setup ticket panel\n" + "`/ticket close` - Close current ticket\n" + "`/ticket delete` - Delete current ticket", + ephemeral=True + ) + return + if choice == "ai_admin": + await interaction.response.send_message( + "**AI Admin Commands:**\n" + "`/ai_admin ` - Let AI manage the server\n" + "`/ai_help` - Show AI capabilities\n\n" + "Examples:\n" + "- Create a giveaway for Nitro\n" + "- Setup a Valorant tournament\n" + "- Create a moderator role", + ephemeral=True + ) + return + if choice == "config": + await interaction.response.send_message( + "**Config Commands:**\n" + "`/config panel` - Open config panel\n" + "`/language set` - Set server language\n" + "`/prefix set` - Set bot prefix", + ephemeral=True + ) + return + + +class EconomyAdminModalBase(discord.ui.Modal): + member_id = discord.ui.TextInput( + label="User ID / Mention", + placeholder="123456789012345678 or @user", + required=True, + max_length=32, + ) + + def __init__(self, cog: "Admin", title: str) -> None: + super().__init__(title=title) + self.cog = cog + + async def _resolve_member_or_reply(self, interaction: discord.Interaction) -> discord.Member | None: + guild = interaction.guild + if guild is None: + await interaction.response.send_message("This action must be used in a server.", ephemeral=True) + return None + member = await self.cog.resolve_member(guild, str(self.member_id)) + if member is None: + await interaction.response.send_message("Could not find that member.", ephemeral=True) + return None + return member + + +class AddCoinsModal(EconomyAdminModalBase): + amount = discord.ui.TextInput(label="Coins To Add", placeholder="250", required=True, max_length=12) + + def __init__(self, cog: "Admin") -> None: + super().__init__(cog, "➕ Add Coins") + + async def on_submit(self, interaction: discord.Interaction) -> None: + member = await self._resolve_member_or_reply(interaction) + if member is None: + return + amount = self.cog.parse_positive_int(str(self.amount)) + if amount is None: + await interaction.response.send_message("Amount must be a positive number.", ephemeral=True) + return + before, after = await self.cog.adjust_wallet(interaction.guild.id, member.id, amount) + await interaction.response.send_message( + f"✅ Added `{amount}` coins for {member.mention}. Wallet `{before}` → `{after}`.", + ephemeral=True, + ) + + +class RemoveCoinsModal(EconomyAdminModalBase): + amount = discord.ui.TextInput(label="Coins To Remove", placeholder="150", required=True, max_length=12) + + def __init__(self, cog: "Admin") -> None: + super().__init__(cog, "➖ Remove Coins") + + async def on_submit(self, interaction: discord.Interaction) -> None: + member = await self._resolve_member_or_reply(interaction) + if member is None: + return + amount = self.cog.parse_positive_int(str(self.amount)) + if amount is None: + await interaction.response.send_message("Amount must be a positive number.", ephemeral=True) + return + before, after = await self.cog.adjust_wallet(interaction.guild.id, member.id, -amount) + await interaction.response.send_message( + f"✅ Removed `{amount}` coins for {member.mention}. Wallet `{before}` → `{after}`.", + ephemeral=True, + ) + + +class SetBalanceModal(EconomyAdminModalBase): + amount = discord.ui.TextInput(label="New Wallet Balance", placeholder="5000", required=True, max_length=12) + + def __init__(self, cog: "Admin") -> None: + super().__init__(cog, "⚖️ Set Balance") + + async def on_submit(self, interaction: discord.Interaction) -> None: + member = await self._resolve_member_or_reply(interaction) + if member is None: + return + target = self.cog.parse_non_negative_int(str(self.amount)) + if target is None: + await interaction.response.send_message("Balance must be zero or more.", ephemeral=True) + return + before, after = await self.cog.set_wallet(interaction.guild.id, member.id, target) + await interaction.response.send_message( + f"✅ Set wallet for {member.mention}. `{before}` → `{after}`.", + ephemeral=True, + ) + + +class SetSalaryModal(discord.ui.Modal): + min_salary = discord.ui.TextInput(label="Minimum Work Salary", placeholder="50", required=True, max_length=12) + max_salary = discord.ui.TextInput(label="Maximum Work Salary", placeholder="150", required=True, max_length=12) + + def __init__(self, cog: "Admin") -> None: + super().__init__(title="💸 Set Salary") + self.cog = cog + + async def on_submit(self, interaction: discord.Interaction) -> None: + if interaction.guild is None: + await interaction.response.send_message("This action must be used in a server.", ephemeral=True) + return + min_val = self.cog.parse_non_negative_int(str(self.min_salary)) + max_val = self.cog.parse_non_negative_int(str(self.max_salary)) + if min_val is None or max_val is None or min_val > max_val: + await interaction.response.send_message("Use valid integers with min <= max.", ephemeral=True) + return + await self.cog.bot.db.execute( + "INSERT INTO economy_salaries(guild_id, min_salary, max_salary) VALUES (?, ?, ?) " + "ON CONFLICT(guild_id) DO UPDATE SET min_salary = excluded.min_salary, max_salary = excluded.max_salary", + interaction.guild.id, + min_val, + max_val, + ) + await interaction.response.send_message( + f"✅ Work salary updated to `{min_val}` - `{max_val}`.", + ephemeral=True, + ) + + +class SetDailyModal(discord.ui.Modal): + daily_min = discord.ui.TextInput(label="Minimum Daily Reward", placeholder="100", required=True, max_length=12) + daily_max = discord.ui.TextInput(label="Maximum Daily Reward", placeholder="250", required=True, max_length=12) + + def __init__(self, cog: "Admin") -> None: + super().__init__(title="📆 Set Daily") + self.cog = cog + + async def on_submit(self, interaction: discord.Interaction) -> None: + if interaction.guild is None: + await interaction.response.send_message("This action must be used in a server.", ephemeral=True) + return + min_val = self.cog.parse_non_negative_int(str(self.daily_min)) + max_val = self.cog.parse_non_negative_int(str(self.daily_max)) + if min_val is None or max_val is None or min_val > max_val: + await interaction.response.send_message("Use valid integers with min <= max.", ephemeral=True) + return + await self.cog.bot.db.execute( + "INSERT INTO economy_salaries(guild_id, daily_min, daily_max) VALUES (?, ?, ?) " + "ON CONFLICT(guild_id) DO UPDATE SET daily_min = excluded.daily_min, daily_max = excluded.daily_max", + interaction.guild.id, + min_val, + max_val, + ) + await interaction.response.send_message( + f"✅ Daily reward updated to `{min_val}` - `{max_val}`.", + ephemeral=True, + ) + + +class EconomyAdminPanel(discord.ui.View): + def __init__(self, cog: "Admin") -> None: + super().__init__(timeout=300) + self.cog = cog + + @discord.ui.button(label="➕ Add Coins", style=discord.ButtonStyle.success, row=0) + async def add_coins(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: + await interaction.response.send_modal(AddCoinsModal(self.cog)) + + @discord.ui.button(label="💸 Set Salary", style=discord.ButtonStyle.primary, row=0) + async def set_salary(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: + await interaction.response.send_modal(SetSalaryModal(self.cog)) + + @discord.ui.button(label="➖ Remove", style=discord.ButtonStyle.danger, row=1) + async def remove_coins(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: + await interaction.response.send_modal(RemoveCoinsModal(self.cog)) + + @discord.ui.button(label="📆 Set Daily", style=discord.ButtonStyle.secondary, row=1) + async def set_daily(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: + await interaction.response.send_modal(SetDailyModal(self.cog)) + + @discord.ui.button(label="⚖️ Set Balance", style=discord.ButtonStyle.primary, row=2) + async def set_balance(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: + await interaction.response.send_modal(SetBalanceModal(self.cog)) + + @discord.ui.button(label="⬅️ Back", style=discord.ButtonStyle.secondary, row=2) + async def back(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: + await interaction.response.edit_message(embed=await self.cog.build_admin_home_embed(interaction.guild), view=AdminMasterPanel(self.cog)) + + +class EmojiCloneModal(discord.ui.Modal): + source = discord.ui.TextInput( + label="Source Emoji (tag/id/url/name)", + placeholder="<:emoji:123...> or 123... or https://... or emoji_name", + required=True, + max_length=500, + ) + target_name = discord.ui.TextInput( + label="New Name (optional)", + placeholder="leave empty to auto-detect", + required=False, + max_length=32, + ) + + def __init__(self, cog: "Admin") -> None: + super().__init__(title="Clone Emoji") + self.cog = cog + + async def on_submit(self, interaction: discord.Interaction) -> None: + if not interaction.guild: + await interaction.response.send_message("Server only.", ephemeral=True) + return + if not interaction.user.guild_permissions.manage_emojis: + await interaction.response.send_message("Manage Emojis permission required.", ephemeral=True) + return + source = str(self.source.value).strip() + name = str(self.target_name.value).strip() or None + created, error_text = await self.cog._clone_emoji_to_guild( + interaction.guild, + interaction.user, + source=source, + name=name, + ) + if error_text: + await interaction.response.send_message(error_text, ephemeral=True) + return + embed = success_embed( + "Emoji Cloned", + f"Created {created} as `:{created.name}:`", + ) + await interaction.response.send_message(embed=embed, ephemeral=True) + + +class EmojiClonePanelView(discord.ui.View): + def __init__(self, cog: "Admin") -> None: + super().__init__(timeout=300) + self.cog = cog + + async def _ensure_perm(self, interaction: discord.Interaction) -> bool: + if not interaction.guild: + await interaction.response.send_message("Server only.", ephemeral=True) + return False + if not interaction.user.guild_permissions.manage_emojis: + await interaction.response.send_message("Manage Emojis permission required.", ephemeral=True) + return False + return True + + @discord.ui.button(label="Clone Emoji", style=discord.ButtonStyle.success, emoji="🧬", row=0) + async def clone_emoji(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: + if not await self._ensure_perm(interaction): + return + await interaction.response.send_modal(EmojiCloneModal(self.cog)) + + @discord.ui.button(label="Server Emojis", style=discord.ButtonStyle.primary, emoji="📋", row=0) + async def list_server_emojis(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: + if not await self._ensure_perm(interaction): + return + emojis = interaction.guild.emojis if interaction.guild else [] + if not emojis: + await interaction.response.send_message("No custom emojis in this server.", ephemeral=True) + return + lines = [f"{e} `:{e.name}:` `{str(e)}`" for e in emojis[:60]] + embed = info_embed("Server Emoji List", "\n".join(lines)) + await interaction.response.send_message(embed=embed, ephemeral=True) + + @discord.ui.button(label="Bot Emoji Picker", style=discord.ButtonStyle.secondary, emoji="🧩", row=1) + async def picker(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: + if not await self._ensure_perm(interaction): + return + custom_names = sorted({emoji.name for emoji in getattr(self.cog.bot, "emojis", []) if getattr(emoji, "name", None)}) + if not custom_names: + await interaction.response.send_message("No bot emoji catalog available.", ephemeral=True) + return + lines = [] + for name in custom_names[:60]: + emoji_obj = discord.utils.get(self.cog.bot.emojis, name=name) + rendered = str(emoji_obj) if emoji_obj else name + lines.append(f"{rendered} `{name}`") + embed = info_embed( + "Emoji Picker", + "\n".join(lines) + "\n\nUse Clone Emoji button and paste any name/tag/URL/ID.", + ) + await interaction.response.send_message(embed=embed, ephemeral=True) + + @discord.ui.button(label="Close", style=discord.ButtonStyle.danger, emoji="✖", row=1) + async def close_panel(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: + if not await self._ensure_perm(interaction): + return + for item in self.children: + item.disabled = True + await interaction.response.edit_message(view=self) + + +class Admin(commands.Cog): + """Moderation commands with beautiful panels.""" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + async def cog_load(self) -> None: + self.bot.add_view(AdminMasterPanel(self)) + + async def build_admin_home_embed(self, guild: discord.Guild | None = None) -> discord.Embed: + embed = success_embed( + "꧁⫷ 𝕄𝕠تآز 𝕊𝕪𝕤𝕥𝕖𝕞 ⫸꧂", + "╔════╗\n💰 Economy Admin | إدارة الاقتصاد\n🛡️ Shield Control | تحكم الدرع\n📊 System Status | حالة النظام\n╚════╝", + ) + embed.set_footer(text="🏮 Powered by BOT- AI Suite 🏮") + if guild: + await add_banner_to_embed(embed, guild) + return embed + + async def build_economy_admin_embed(self, guild: discord.Guild | None = None) -> discord.Embed: + embed = info_embed( + f"{ui('economy')} Economy Admin", + "Choose one action below:\n" + "➕ Add Coins\n" + "💸 Set Salary\n" + "➖ Remove Coins\n" + "📆 Set Daily\n" + "⚖️ Set Balance", + ) + if guild: + await add_banner_to_embed(embed, guild) + return embed + + @staticmethod + def parse_positive_int(value: str) -> int | None: + try: + parsed = int(value.strip()) + except (TypeError, ValueError): + return None + return parsed if parsed > 0 else None + + @staticmethod + def parse_non_negative_int(value: str) -> int | None: + try: + parsed = int(value.strip()) + except (TypeError, ValueError): + return None + return parsed if parsed >= 0 else None + + async def resolve_member(self, guild: discord.Guild, raw: str) -> discord.Member | None: + match = re.search(r"\d{15,22}", raw or "") + if not match: + return None + member_id = int(match.group(0)) + member = guild.get_member(member_id) + if member is not None: + return member + try: + return await guild.fetch_member(member_id) + except discord.HTTPException: + return None + + async def get_wallet(self, guild_id: int, user_id: int) -> int: + row = await self.bot.db.fetchone( + "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?", + guild_id, + user_id, + ) + return int(row[0]) if row else 0 + + async def adjust_wallet(self, guild_id: int, user_id: int, delta: int) -> tuple[int, int]: + before = await self.get_wallet(guild_id, user_id) + after = max(0, before + delta) + await self.bot.db.execute( + "INSERT INTO user_balance(guild_id, user_id, wallet, bank) VALUES (?, ?, ?, 0) " + "ON CONFLICT(guild_id, user_id) DO UPDATE SET wallet = excluded.wallet", + guild_id, + user_id, + after, + ) + return before, after + + async def set_wallet(self, guild_id: int, user_id: int, target: int) -> tuple[int, int]: + before = await self.get_wallet(guild_id, user_id) + after = max(0, target) + await self.bot.db.execute( + "INSERT INTO user_balance(guild_id, user_id, wallet, bank) VALUES (?, ?, ?, 0) " + "ON CONFLICT(guild_id, user_id) DO UPDATE SET wallet = excluded.wallet", + guild_id, + user_id, + after, + ) + return before, after + async def _safe_ctx_send( self, ctx: commands.Context, - *, - content: str | None = None, - embed: discord.Embed | None = None, - view: discord.ui.View | None = None, - ephemeral: bool = False, - delete_after: float | None = None, - ) -> None: - kwargs: dict[str, object] = {} - if content is not None: - kwargs["content"] = content - if embed is not None: - kwargs["embed"] = embed - if view is not None: - kwargs["view"] = view - kwargs["ephemeral"] = ephemeral - - if ctx.interaction: - try: - if not ctx.interaction.response.is_done(): - await ctx.interaction.response.send_message(**kwargs) - return - await ctx.interaction.followup.send(**kwargs) - return - except (discord.NotFound, discord.InteractionResponded): - pass - except discord.HTTPException as exc: - if exc.code not in {10062, 40060, 10008}: - raise - - if ctx.channel: - channel_kwargs = {k: v for k, v in kwargs.items() if k != "ephemeral"} + *, + content: str | None = None, + embed: discord.Embed | None = None, + view: discord.ui.View | None = None, + ephemeral: bool = False, + delete_after: float | None = None, + ) -> None: + kwargs: dict[str, object] = {} + if content is not None: + kwargs["content"] = content + if embed is not None: + kwargs["embed"] = embed + if view is not None: + kwargs["view"] = view + kwargs["ephemeral"] = ephemeral + + if ctx.interaction: + try: + if not ctx.interaction.response.is_done(): + await ctx.interaction.response.send_message(**kwargs) + return + await ctx.interaction.followup.send(**kwargs) + return + except (discord.NotFound, discord.InteractionResponded): + pass + except discord.HTTPException as exc: + if exc.code not in {10062, 40060, 10008}: + raise + + if ctx.channel: + channel_kwargs = {k: v for k, v in kwargs.items() if k != "ephemeral"} msg = await ctx.channel.send(**channel_kwargs) if delete_after: await msg.delete(delay=delete_after) - @commands.hybrid_command(name="purge") - @commands.has_permissions(manage_messages=True) - async def purge(self, ctx: commands.Context, amount: int) -> None: - """Delete a number of messages from the channel.""" - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.interaction.response.defer(ephemeral=True, thinking=True) - guild_id = ctx.guild.id if ctx.guild else None - lang = await self.bot.get_guild_language(guild_id) - - amount = max(1, min(amount, 100)) - deleted = await ctx.channel.purge(limit=amount + 1) - - if lang == "ar": - desc = f"📝 تم حذف **{len(deleted) - 1}** رسالة.\n👤 المشرف: {ctx.author.mention}" - else: - desc = f"📝 Deleted **{len(deleted) - 1}** messages.\n👤 Moderator: {ctx.author.mention}" - - embed = success_embed("🗑️ Messages Purged", f"{panel_divider('green')}\n{desc}\n{panel_divider('green')}") - await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction), delete_after=5 if not ctx.interaction else None) - - @commands.hybrid_command(name="admin_panel", description="Economy/Shield/Status management panel", with_app_command=False) - @commands.has_permissions(administrator=True) - async def admin_panel(self, ctx: commands.Context) -> None: - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.interaction.response.defer(ephemeral=True, thinking=True) - await self._safe_ctx_send( - ctx, - embed=await self.build_admin_home_embed(ctx.guild), - view=AdminMasterPanel(self), - ephemeral=bool(ctx.interaction), - ) - - @commands.hybrid_group(name="admin", fallback="panel", description="Administrative grouped controls") - @commands.has_permissions(administrator=True) - async def admin_group(self, ctx: commands.Context) -> None: - await self.admin_panel(ctx) - - @admin_group.group(name="shield", invoke_without_command=True) - @commands.has_permissions(administrator=True) - async def admin_shield_group(self, ctx: commands.Context) -> None: - await self._safe_ctx_send( - ctx, - content="Use `/admin shield set_level ` or `/admin shield add_image `", - ephemeral=bool(ctx.interaction), - ) - - @admin_shield_group.command(name="set_level") - @commands.has_permissions(administrator=True) - async def admin_shield_set_level(self, ctx: commands.Context, level: str) -> None: - await self.shield_level(ctx, level) - - @admin_shield_group.command(name="add_image") - @commands.has_permissions(manage_messages=True) - async def admin_shield_add_image(self, ctx: commands.Context, image: discord.Attachment) -> None: - if not ctx.guild: - await self._safe_ctx_send(ctx, content="Server only.", ephemeral=bool(ctx.interaction)) - return - if not (image.content_type or "").startswith("image/"): - await self._safe_ctx_send(ctx, content="Please attach an image file.", ephemeral=bool(ctx.interaction)) - return - digest = hashlib.sha256(await image.read()).hexdigest() - await self.bot.db.execute( - "INSERT OR IGNORE INTO scam_images(guild_id, image_hash, created_by) VALUES (?, ?, ?)", - ctx.guild.id, - digest, - ctx.author.id, - ) - await self._safe_ctx_send(ctx, content="✅ Scam image signature saved.", ephemeral=bool(ctx.interaction)) - - @commands.hybrid_command(name="shield_level", description="Set AI shield level low|medium|high", hidden=True, with_app_command=False) - @commands.has_permissions(administrator=True) - async def shield_level(self, ctx: commands.Context, level: str) -> None: - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.interaction.response.defer(ephemeral=True, thinking=True) - normalized = level.strip().lower() - if normalized not in {"low", "medium", "high"}: - await self._safe_ctx_send(ctx, content="Use one of: low, medium, high", ephemeral=bool(ctx.interaction)) - return - await self.bot.db.execute( - "INSERT INTO shield_settings(guild_id, level) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET level = excluded.level", - ctx.guild.id if ctx.guild else 0, - normalized, - ) - await self._safe_ctx_send(ctx, content=f"🛡️ Shield level set to `{normalized}`", ephemeral=bool(ctx.interaction)) - - @commands.hybrid_command(name="econ_admin", description="Add/remove coins from user") - @commands.has_permissions(administrator=True) - async def econ_admin(self, ctx: commands.Context, member: discord.Member, action: str, amount: int) -> None: - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.interaction.response.defer(ephemeral=True, thinking=True) - action_n = action.strip().lower() - if action_n not in {"add", "remove"}: - await self._safe_ctx_send(ctx, content="Use action: add/remove", ephemeral=bool(ctx.interaction)) - return - amount = max(0, amount) - row = await self.bot.db.fetchone("SELECT coins, xp FROM economy WHERE user_id = ?", member.id) - coins = int(row[0]) if row else 0 - xp = int(row[1]) if row else 0 - new_coins = coins + amount if action_n == "add" else max(0, coins - amount) - await self.bot.db.execute( - "INSERT INTO economy(user_id, coins, xp) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET coins = excluded.coins, xp = excluded.xp", - member.id, - new_coins, - xp, + def _presence_status_label(self, status: discord.Status | str | None) -> str: + value = str(status or "online").lower() + return { + "online": "online", + "idle": "idle", + "dnd": "dnd", + "invisible": "invisible", + "offline": "invisible", + }.get(value, "online") + + def _presence_activity_label(self, activity_type: discord.ActivityType | str | None) -> str: + value = str(activity_type or "playing").lower() + return { + "playing": "playing", + "watching": "watching", + "listening": "listening", + "competing": "competing", + }.get(value, "playing") + + async def _set_bot_presence( + self, + actor_id: int, + *, + status_name: str, + activity_type_name: str, + activity_text: str, + ) -> None: + resolved_status = PRESENCE_STATUS_MAP.get(status_name, discord.Status.online) + resolved_activity_type = PRESENCE_ACTIVITY_MAP.get(activity_type_name, discord.ActivityType.playing) + await self.bot.change_presence( + status=resolved_status, + activity=discord.Activity(type=resolved_activity_type, name=activity_text), ) - await self._safe_ctx_send(ctx, content=f"💰 {member.mention} coins: `{coins}` → `{new_coins}`", ephemeral=bool(ctx.interaction)) - - @commands.hybrid_command(name="warn") - @commands.has_permissions(manage_roles=True) - async def warn(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None: - """Warn a member.""" - guild_id = ctx.guild.id if ctx.guild else None - lang = await self.bot.get_guild_language(guild_id) - await self.bot.db.execute( - "INSERT INTO user_warnings(guild_id, user_id, moderator_id, reason, timestamp) VALUES (?, ?, ?, ?, ?)", - ctx.guild.id, - member.id, - ctx.author.id, - reason, + "INSERT INTO bot_presence_config(id, status, activity_type, activity_text, updated_by, updated_at) " + "VALUES (1, ?, ?, ?, ?, ?) " + "ON CONFLICT(id) DO UPDATE SET " + "status = excluded.status, " + "activity_type = excluded.activity_type, " + "activity_text = excluded.activity_text, " + "updated_by = excluded.updated_by, " + "updated_at = excluded.updated_at", + status_name, + activity_type_name, + activity_text, + actor_id, dt.datetime.utcnow().isoformat(), ) - - if lang == "ar": - title = "⚠️ تحذير للعضو" - desc = ( - f"{panel_divider('orange')}\n" - f"👤 **المستخدم:** {member.mention}\n" - f"🛡️ **المشرف:** {ctx.author.mention}\n" - f"📝 **السبب:** {reason}\n" - f"{panel_divider('orange')}" - ) - dm_title = "⚠️ تم تحذيرك" - dm_desc = f"🏛️ **السيرفر:** {ctx.guild.name}\n📝 **السبب:** {reason}\n🛡️ **المشرف:** {ctx.author}" - else: - title = "⚠️ Member Warned" - desc = ( - f"{panel_divider('orange')}\n" - f"👤 **User:** {member.mention}\n" - f"🛡️ **Moderator:** {ctx.author.mention}\n" - f"📝 **Reason:** {reason}\n" - f"{panel_divider('orange')}" - ) - dm_title = "⚠️ You have been warned" - dm_desc = f"🏛️ **Server:** {ctx.guild.name}\n📝 **Reason:** {reason}\n🛡️ **Moderator:** {ctx.author}" - - embed = warning_embed(title, desc) - await ctx.reply(embed=embed) - - try: - dm_embed = warning_embed(dm_title, dm_desc) - await member.send(embed=dm_embed) - except discord.Forbidden: - pass - @commands.hybrid_command(name="warnings") - @commands.has_permissions(manage_roles=True) - async def warnings(self, ctx: commands.Context, member: discord.Member) -> None: - """View warnings for a member.""" - guild_id = ctx.guild.id if ctx.guild else None - lang = await self.bot.get_guild_language(guild_id) - - rows = await self.bot.db.fetchall( - "SELECT moderator_id, reason, timestamp FROM user_warnings WHERE guild_id = ? AND user_id = ? ORDER BY timestamp DESC LIMIT 10", - ctx.guild.id, - member.id, + async def _presence_embed(self) -> discord.Embed: + current_activity = getattr(self.bot, "activity", None) + activity_text = getattr(current_activity, "name", None) or "CYBER // GRID" + activity_type_name = self._presence_activity_label(getattr(current_activity, "type", None)) + status_name = self._presence_status_label(getattr(self.bot, "status", None)) + row = await self.bot.db.fetchone( + "SELECT updated_by, updated_at FROM bot_presence_config WHERE id = 1" ) - if not rows: - if lang == "ar": - embed = info_embed("📋 التحذيرات", f"{member.mention} لا توجد تحذيرات. ✅") - else: - embed = info_embed("📋 Warnings", f"{member.mention} has no warnings. ✅") - await ctx.reply(embed=embed) - return - - lines = [] - for idx, (mod_id, reason, timestamp) in enumerate(rows, 1): - mod = ctx.guild.get_member(mod_id) - mod_name = mod.display_name if mod else f"<@{mod_id}>" - lines.append(f"`{idx}.` 📝 {reason[:50]}{'...' if len(reason) > 50 else ''}\n 🛡️ by {mod_name}") - - title = f"📋 تحذيرات {member.display_name}" if lang == "ar" else f"📋 Warnings for {member.display_name}" - embed = discord.Embed( - title=title, - description=f"{panel_divider('orange')}\n" + "\n".join(lines) + f"\n{panel_divider('orange')}", - color=NEON_ORANGE, + updated_by = f"<@{row[0]}>" if row and row[0] else "Unknown" + updated_at = row[1] if row and row[1] else "Not saved yet" + embed = info_embed( + "Bot Presence", + ( + f"Status: **{status_name}**\n" + f"Activity: **{activity_type_name}**\n" + f"Text: **{activity_text}**\n\n" + f"Updated by: {updated_by}\n" + f"Updated at: `{updated_at}`" + ), ) - embed.set_thumbnail(url=member.display_avatar.url) - await ctx.reply(embed=embed) + return embed + + @commands.hybrid_command(name="purge") + @commands.has_permissions(manage_messages=True) + async def purge(self, ctx: commands.Context, amount: int) -> None: + """Delete a number of messages from the channel.""" + if ctx.interaction and not ctx.interaction.response.is_done(): + await ctx.interaction.response.defer(ephemeral=True, thinking=True) + guild_id = ctx.guild.id if ctx.guild else None + lang = await self.bot.get_guild_language(guild_id) + + amount = max(1, min(amount, 100)) + deleted = await ctx.channel.purge(limit=amount + 1) + + if lang == "ar": + desc = f"📝 تم حذف **{len(deleted) - 1}** رسالة.\n👤 المشرف: {ctx.author.mention}" + else: + desc = f"📝 Deleted **{len(deleted) - 1}** messages.\n👤 Moderator: {ctx.author.mention}" + + embed = success_embed("🗑️ Messages Purged", f"{panel_divider('green')}\n{desc}\n{panel_divider('green')}") + await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction), delete_after=5 if not ctx.interaction else None) + + @commands.hybrid_command(name="admin_panel", description="Economy/Shield/Status management panel", with_app_command=False) + @commands.has_permissions(administrator=True) + async def admin_panel(self, ctx: commands.Context) -> None: + if ctx.interaction and not ctx.interaction.response.is_done(): + await ctx.interaction.response.defer(ephemeral=True, thinking=True) + await self._safe_ctx_send( + ctx, + embed=await self.build_admin_home_embed(ctx.guild), + view=AdminMasterPanel(self), + ephemeral=bool(ctx.interaction), + ) + + @commands.hybrid_group(name="admin", fallback="panel", description="Administrative grouped controls") + @commands.has_permissions(administrator=True) + async def admin_group(self, ctx: commands.Context) -> None: + await self.admin_panel(ctx) - @commands.hybrid_command(name="clearwarn") - @commands.has_permissions(manage_roles=True) - async def clearwarn(self, ctx: commands.Context, member: discord.Member) -> None: - """Clear all warnings for a member.""" - guild_id = ctx.guild.id if ctx.guild else None - lang = await self.bot.get_guild_language(guild_id) - - await self.bot.db.execute( - "DELETE FROM user_warnings WHERE guild_id = ? AND user_id = ?", - ctx.guild.id, - member.id, - ) - - if lang == "ar": - embed = success_embed("✅ تم مسح التحذيرات", f"🗑️ تم إزالة جميع تحذيرات {member.mention}.") - else: - embed = success_embed("✅ Warnings Cleared", f"🗑️ All warnings for {member.mention} have been removed.") - await ctx.reply(embed=embed) + @admin_group.group(name="botstatus", invoke_without_command=True) + @commands.is_owner() + async def admin_botstatus_group(self, ctx: commands.Context) -> None: + embed = await self._presence_embed() + await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction)) - @commands.hybrid_command(name="kick") - @commands.has_permissions(kick_members=True) - async def kick(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None: - """Kick a member from the server.""" - guild_id = ctx.guild.id if ctx.guild else None - lang = await self.bot.get_guild_language(guild_id) - - if member.top_role >= ctx.author.top_role and ctx.author.id != ctx.guild.owner_id: - if lang == "ar": - await ctx.reply("❌ لا يمكنك طرد شخص برتبة أعلى أو مساوية.") - else: - await ctx.reply("❌ You cannot kick someone with a higher or equal role.") - return - - try: - if lang == "ar": - dm_embed = warning_embed("👢 تم طردك", f"🏛️ **السيرفر:** {ctx.guild.name}\n📝 **السبب:** {reason}\n🛡️ **المشرف:** {ctx.author}") - else: - dm_embed = warning_embed("👢 You have been kicked", f"🏛️ **Server:** {ctx.guild.name}\n📝 **Reason:** {reason}\n🛡️ **Moderator:** {ctx.author}") - await member.send(embed=dm_embed) - except discord.Forbidden: - pass - - await member.kick(reason=reason) - - if lang == "ar": - embed = success_embed( - "👢 تم طرد العضو", - f"{panel_divider('green')}\n" - f"👤 **المستخدم:** {member.mention}\n" - f"🛡️ **المشرف:** {ctx.author.mention}\n" - f"📝 **السبب:** {reason}\n" - f"{panel_divider('green')}" - ) - else: - embed = success_embed( - "👢 Member Kicked", - f"{panel_divider('green')}\n" - f"👤 **User:** {member.mention}\n" - f"🛡️ **Moderator:** {ctx.author.mention}\n" - f"📝 **Reason:** {reason}\n" - f"{panel_divider('green')}" + @admin_botstatus_group.command(name="set") + @commands.is_owner() + async def admin_botstatus_set( + self, + ctx: commands.Context, + status: str, + activity_type: str, + *, + text: str, + ) -> None: + status_name = (status or "").strip().lower() + activity_type_name = (activity_type or "").strip().lower() + activity_text = (text or "").strip()[:128] + + if status_name not in PRESENCE_STATUS_MAP: + await self._safe_ctx_send( + ctx, + content="Invalid status. Use: `online`, `idle`, `dnd`, `invisible`", + ephemeral=bool(ctx.interaction), ) - await ctx.reply(embed=embed) - - @commands.hybrid_command(name="ban") - @commands.has_permissions(ban_members=True) - async def ban(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None: - """Ban a member from the server.""" - guild_id = ctx.guild.id if ctx.guild else None - lang = await self.bot.get_guild_language(guild_id) - - if member.top_role >= ctx.author.top_role and ctx.author.id != ctx.guild.owner_id: - if lang == "ar": - await ctx.reply("❌ لا يمكنك حظر شخص برتبة أعلى أو مساوية.") - else: - await ctx.reply("❌ You cannot ban someone with a higher or equal role.") return - - try: - if lang == "ar": - dm_embed = error_embed("🔨 تم حظرك", f"🏛️ **السيرفر:** {ctx.guild.name}\n📝 **السبب:** {reason}\n🛡️ **المشرف:** {ctx.author}") - else: - dm_embed = error_embed("🔨 You have been banned", f"🏛️ **Server:** {ctx.guild.name}\n📝 **Reason:** {reason}\n🛡️ **Moderator:** {ctx.author}") - await member.send(embed=dm_embed) - except discord.Forbidden: - pass - - await member.ban(reason=reason) - - if lang == "ar": - embed = success_embed( - "🔨 تم حظر العضو", - f"{panel_divider('pink')}\n" - f"👤 **المستخدم:** {member.mention}\n" - f"🛡️ **المشرف:** {ctx.author.mention}\n" - f"📝 **السبب:** {reason}\n" - f"{panel_divider('pink')}" - ) - else: - embed = success_embed( - "🔨 Member Banned", - f"{panel_divider('pink')}\n" - f"👤 **User:** {member.mention}\n" - f"🛡️ **Moderator:** {ctx.author.mention}\n" - f"📝 **Reason:** {reason}\n" - f"{panel_divider('pink')}" + if activity_type_name not in PRESENCE_ACTIVITY_MAP: + await self._safe_ctx_send( + ctx, + content="Invalid activity type. Use: `playing`, `watching`, `listening`, `competing`", + ephemeral=bool(ctx.interaction), ) - await ctx.reply(embed=embed) - await self.bot.log_to_guild( - ctx.guild, - "🔨 Moderation: Ban", - f"{member.mention} was banned by {ctx.author.mention}\nReason: {reason}", - color=discord.Color.red(), - ) - - @commands.hybrid_command(name="unban") - @commands.has_permissions(ban_members=True) - async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = "No reason provided") -> None: - """Unban a user by their ID.""" - guild_id = ctx.guild.id if ctx.guild else None - lang = await self.bot.get_guild_language(guild_id) - - try: - user = await self.bot.fetch_user(user_id) - except discord.NotFound: - if lang == "ar": - await ctx.reply("❌ المستخدم غير موجود.") - else: - await ctx.reply("❌ User not found.") return - except discord.HTTPException: - if lang == "ar": - await ctx.reply("❌ تعذر جلب المستخدم.") - else: - await ctx.reply("❌ Could not fetch user.") + if not activity_text: + await self._safe_ctx_send(ctx, content="Activity text cannot be empty.", ephemeral=bool(ctx.interaction)) return - bans = [b async for b in ctx.guild.bans()] - if user not in [b.user for b in bans]: - if lang == "ar": - await ctx.reply(f"❌ {user.mention} غير محظور.") - else: - await ctx.reply(f"❌ {user.mention} is not banned.") - return - - await ctx.guild.unban(user, reason=reason) - - if lang == "ar": - embed = success_embed( - "🔓 تم فك الحظر", - f"{panel_divider('green')}\n" - f"👤 **المستخدم:** {user.mention}\n" - f"🛡️ **المشرف:** {ctx.author.mention}\n" - f"📝 **السبب:** {reason}\n" - f"{panel_divider('green')}" - ) - else: - embed = success_embed( - "🔓 Member Unbanned", - f"{panel_divider('green')}\n" - f"👤 **User:** {user.mention}\n" - f"🛡️ **Moderator:** {ctx.author.mention}\n" - f"📝 **Reason:** {reason}\n" - f"{panel_divider('green')}" - ) - await ctx.reply(embed=embed) - await self.bot.log_to_guild( - ctx.guild, - "🔓 Moderation: Unban", - f"{user.mention} was unbanned by {ctx.author.mention}\nReason: {reason}", - color=discord.Color.green(), + await self._set_bot_presence( + ctx.author.id, + status_name=status_name, + activity_type_name=activity_type_name, + activity_text=activity_text, ) - - @commands.hybrid_command(name="mute") - @commands.has_permissions(moderate_members=True) - async def mute(self, ctx: commands.Context, member: discord.Member, duration: int = 10, *, reason: str = "No reason provided") -> None: - """Timeout a member for a specified duration in minutes.""" - guild_id = ctx.guild.id if ctx.guild else None - lang = await self.bot.get_guild_language(guild_id) - - if member.top_role >= ctx.author.top_role and ctx.author.id != ctx.guild.owner_id: - if lang == "ar": - await ctx.reply("❌ لا يمكنك كتم شخص برتبة أعلى أو مساوية.") - else: - await ctx.reply("❌ You cannot mute someone with a higher or equal role.") - return - - duration = max(1, min(duration, 40320)) # Max 28 days - until = dt.datetime.utcnow() + dt.timedelta(minutes=duration) - - await member.timeout(until, reason=reason) - - if lang == "ar": - embed = success_embed( - "🔇 تم كتم العضو", - f"{panel_divider('orange')}\n" - f"👤 **المستخدم:** {member.mention}\n" - f"⏰ **المدة:** {duration} دقيقة\n" - f"🛡️ **المشرف:** {ctx.author.mention}\n" - f"📝 **السبب:** {reason}\n" - f"{panel_divider('orange')}" - ) - else: - embed = success_embed( - "🔇 Member Muted", - f"{panel_divider('orange')}\n" - f"👤 **User:** {member.mention}\n" - f"⏰ **Duration:** {duration} minutes\n" - f"🛡️ **Moderator:** {ctx.author.mention}\n" - f"📝 **Reason:** {reason}\n" - f"{panel_divider('orange')}" - ) - await ctx.reply(embed=embed) - await self.bot.log_to_guild( - ctx.guild, - "🔇 Moderation: Mute", - f"{member.mention} was muted by {ctx.author.mention} for {duration} minute(s)\nReason: {reason}", - color=discord.Color.red(), + embed = success_embed( + "Bot Status Updated", + ( + f"Status: **{status_name}**\n" + f"Activity: **{activity_type_name}**\n" + f"Text: **{activity_text}**" + ), ) - - @commands.hybrid_command(name="unmute") - @commands.has_permissions(moderate_members=True) - async def unmute(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None: - """Remove timeout from a member.""" - guild_id = ctx.guild.id if ctx.guild else None - lang = await self.bot.get_guild_language(guild_id) - - await member.timeout(None, reason=reason) - - if lang == "ar": - embed = success_embed("🔊 تم فك الكتم", f"👤 **المستخدم:** {member.mention}\n🛡️ **المشرف:** {ctx.author.mention}\n📝 **السبب:** {reason}") - else: - embed = success_embed("🔊 Member Unmuted", f"👤 **User:** {member.mention}\n🛡️ **Moderator:** {ctx.author.mention}\n📝 **Reason:** {reason}") - await ctx.reply(embed=embed) - - @admin_group.group(name="emoji", invoke_without_command=True) - @commands.has_permissions(manage_emojis=True) - async def admin_emoji_group(self, ctx: commands.Context) -> None: - emojis = ctx.guild.emojis if ctx.guild else [] - if not emojis: - await self._safe_ctx_send(ctx, content="No custom emojis in this server.", ephemeral=bool(ctx.interaction)) - return - lines = [f"{e} `:{e.name}:` `{str(e)}`" for e in emojis[:40]] - embed = info_embed("Emoji List", "\n".join(lines)) - await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction)) - - @admin_emoji_group.command(name="clone") - @commands.has_permissions(manage_emojis=True) - async def admin_emoji_clone(self, ctx: commands.Context) -> None: - if not ctx.guild: - await self._safe_ctx_send(ctx, content="Server only.", ephemeral=bool(ctx.interaction)) - return - custom_names = sorted(self.bot.emojis.keys()) if hasattr(self.bot, "emojis") else [] - if not custom_names: - await self._safe_ctx_send(ctx, content="No bot emoji catalog available.", ephemeral=bool(ctx.interaction)) - return - lines = [] - for name in custom_names[:40]: - emoji_obj = discord.utils.get(self.bot.emojis, name=name) - rendered = str(emoji_obj) if emoji_obj else name - lines.append(f"{rendered} `{name}`") - embed = info_embed("Emoji Picker", "\n".join(lines)) await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction)) - @commands.hybrid_command(name="awesomeroles") - @commands.has_permissions(manage_roles=True) - async def awesomeroles(self, ctx: commands.Context) -> None: - """Create awesome roles for the server with proper permissions.""" - guild_id = ctx.guild.id if ctx.guild else None - lang = await self.bot.get_guild_language(guild_id) - - created = [] - skipped = [] - - for role_data in AWESOME_ROLES: - existing = discord.utils.get(ctx.guild.roles, name=role_data["name"]) - if existing: - skipped.append(role_data["name"]) - continue - - # Build permissions - permissions = discord.Permissions() - for perm_name in role_data["permissions"]: - setattr(permissions, perm_name, True) - - try: - role = await ctx.guild.create_role( - name=role_data["name"], - color=role_data["color"], - permissions=permissions, - hoist=role_data["hoist"], - reason=f"Created by {ctx.author} via /awesomeroles" - ) - created.append((role, role_data["description"])) - except discord.Forbidden: - continue - - embed = discord.Embed( - title=f"✨ {'الأدوار الرائعة' if lang == 'ar' else 'Awesome Roles'}", - description=f"{panel_divider('purple')}", - color=NEON_PURPLE, - ) - - if created: - lines = [] - for role, desc in created: - lines.append(f"{role.mention}\n└─ *{desc}*") - embed.add_field( - name=f"✅ {'تم إنشاؤها' if lang == 'ar' else 'Created'} ({len(created)})", - value="\n\n".join(lines), - inline=False - ) - - if skipped: - embed.add_field( - name=f"⏭️ {'موجودة مسبقاً' if lang == 'ar' else 'Already Exist'} ({len(skipped)})", - value="\n".join(f"• {name}" for name in skipped), - inline=False - ) - - embed.add_field( - name="💡 {'نصيحة' if lang == 'ar' else 'Tip'}", - value="Drag roles in server settings to set their position!" if lang != "ar" else "اسحب الأدوار في إعدادات السيرفر لتغيير ترتيبها!", - inline=False - ) - - embed.set_footer(text=f"{E_STAR} Created by {ctx.author.display_name}") - await ctx.reply(embed=embed) - - @commands.hybrid_command(name="backupserver") - @commands.has_permissions(administrator=True) - async def backupserver(self, ctx: commands.Context) -> None: - """Create a backup of server structure.""" - if not ctx.guild: - await ctx.reply("Server only.") - return - if ctx.interaction and not ctx.interaction.response.is_done(): - try: - await ctx.interaction.response.defer(ephemeral=True, thinking=True) - except (discord.NotFound, discord.HTTPException, discord.InteractionResponded): - # If interaction token is already invalid, continue and fallback to channel sends via resilient ctx.reply. - pass - - guild_id = ctx.guild.id if ctx.guild else None - lang = await self.bot.get_guild_language(guild_id) - - # Collect server data - roles = [] - for r in ctx.guild.roles: - if not r.is_default() and not r.managed: - roles.append({ - "name": r.name, - "color": r.color.value, - "permissions": r.permissions.value, - "hoist": r.hoist, - "mentionable": r.mentionable, - "position": r.position - }) - - channels = [] - for c in ctx.guild.channels: - channels.append({ - "name": c.name, - "type": str(c.type), - "position": c.position, - "category": c.category.name if c.category else None - }) - - backup_data = { - "roles": roles, - "channels": channels, - "name": ctx.guild.name, - "icon_url": str(ctx.guild.icon.url) if ctx.guild.icon else None - } - - await self.bot.db.execute( - "INSERT INTO server_backups(guild_id, backup_data, created_by, created_at) VALUES (?, ?, ?, ?)", - ctx.guild.id, - json.dumps(backup_data, ensure_ascii=False), + @admin_botstatus_group.command(name="reset") + @commands.is_owner() + async def admin_botstatus_reset(self, ctx: commands.Context) -> None: + await self._set_bot_presence( ctx.author.id, - dt.datetime.utcnow().isoformat(), - ) - - if lang == "ar": - embed = success_embed( - "💾 تم إنشاء نسخة احتياطية", - f"{panel_divider('blue')}\n" - f"🎭 **الأدوار:** {len(roles)}\n" - f"📺 **القنوات:** {len(channels)}\n" - f"👤 **أنشئت بواسطة:** {ctx.author.mention}\n" - f"{panel_divider('blue')}" - ) - else: - embed = success_embed( - "💾 Server Backup Created", - f"{panel_divider('blue')}\n" - f"🎭 **Roles:** {len(roles)}\n" - f"📺 **Channels:** {len(channels)}\n" - f"👤 **Created by:** {ctx.author.mention}\n" - f"{panel_divider('blue')}" - ) - await ctx.reply(embed=embed) - - @commands.hybrid_command(name="backup_panel", description="Interactive backup management panel") - @commands.has_permissions(administrator=True) - async def backup_panel(self, ctx: commands.Context) -> None: - """Open the interactive backup management panel.""" - if not ctx.guild: - await ctx.reply("Server only.") - return - guild_id = ctx.guild.id - lang = await self.bot.get_guild_language(guild_id) - rows = await self.bot.db.fetchall( - "SELECT id, created_by, created_at, backup_data FROM server_backups WHERE guild_id = ? ORDER BY id DESC LIMIT 10", - guild_id, - ) - embed = self._build_backup_embed(guild_id, lang, rows) - await ctx.reply(embed=embed, view=BackupPanelView(self, guild_id)) - - def _build_backup_embed(self, guild_id: int, lang: str, rows: list) -> discord.Embed: - if lang == "ar": - title = "💾 إدارة النسخ الاحتياطية" - desc = f"{panel_divider('blue')}\nإدارة النسخ الاحتياطية للسيرفر\n{panel_divider('blue')}" - else: - title = "💾 Backup Management" - desc = f"{panel_divider('blue')}\nManage server backups\n{panel_divider('blue')}" - embed = discord.Embed(title=title, description=desc, color=NEON_CYAN) - if not rows: - embed.add_field(name="📦" if lang == "ar" else "📦 No backups", value="No backups found." if lang != "ar" else "لا توجد نسخ احتياطية.", inline=False) - else: - lines = [] - for bid, creator_id, created_at, data_str in rows: - try: - data = json.loads(data_str) - roles_count = len(data.get("roles", [])) - channels_count = len(data.get("channels", [])) - except Exception: - roles_count = "?" - channels_count = "?" - lines.append(f"**#{bid}** — {created_at[:19]} | 🎭{roles_count} 📺{channels_count}") - embed.add_field( - name="📦 Recent Backups" if lang != "ar" else "📦 النسخ الأخيرة", - value="\n".join(lines[:10]), - inline=False, - ) - return embed - - -class BackupPanelView(discord.ui.View): - def __init__(self, cog: "Admin", guild_id: int) -> None: - super().__init__(timeout=None) - self.cog = cog - self.guild_id = guild_id - - @discord.ui.button(label="Refresh", style=discord.ButtonStyle.blurple, emoji=ui("refresh"), row=2) - async def refresh(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: - if not interaction.guild: - await interaction.response.send_message("Server only.", ephemeral=True) - return - guild_id = interaction.guild.id - lang = await self.cog.bot.get_guild_language(guild_id) - rows = await self.cog.bot.db.fetchall( - "SELECT id, created_by, created_at, backup_data FROM server_backups WHERE guild_id = ? ORDER BY id DESC LIMIT 10", - guild_id, - ) - embed = self.cog._build_backup_embed(guild_id, lang, rows) - await interaction.response.edit_message(embed=embed, view=self) - - @discord.ui.button(label="List Backups", style=discord.ButtonStyle.primary, emoji=ui("notebook"), row=0) - async def list_backups(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: - if not interaction.guild: - await interaction.response.send_message("Server only.", ephemeral=True) - return - await interaction.response.defer(ephemeral=True) - lang = await self.cog.bot.get_guild_language(interaction.guild.id) - rows = await self.cog.bot.db.fetchall( - "SELECT id, created_by, created_at, backup_data FROM server_backups WHERE guild_id = ? ORDER BY id DESC", - interaction.guild.id, - ) - if not rows: - await interaction.followup.send("No backups found." if lang != "ar" else "لا توجد نسخ احتياطية.", ephemeral=True) - return - lines = [] - for bid, creator_id, created_at, data_str in rows: - try: - data = json.loads(data_str) - roles_count = len(data.get("roles", [])) - channels_count = len(data.get("channels", [])) - except Exception: - roles_count = "?" - channels_count = "?" - lines.append(f"**#{bid}** — {created_at[:19]} | 🎭{roles_count} 📺{channels_count} | By: <@{creator_id}>") - embed = discord.Embed( - title="📦 All Backups" if lang != "ar" else "📦 جميع النسخ", - description="\n".join(lines[:20]), - color=NEON_CYAN, - ) - await interaction.followup.send(embed=embed, ephemeral=True) - - @discord.ui.button(label="Restore Backup", style=discord.ButtonStyle.success, emoji=ui("ok"), row=0) - async def restore_backup(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: - if not interaction.guild: - await interaction.response.send_message("Server only.", ephemeral=True) - return - if not interaction.user.guild_permissions.administrator: - await interaction.response.send_message("Administrator permission required.", ephemeral=True) - return - await interaction.response.send_modal(RestoreBackupModal(self.cog, interaction.guild.id)) - - @discord.ui.button(label="Delete Backup", style=discord.ButtonStyle.danger, emoji=ui("trash"), row=0) - async def delete_backup(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: - if not interaction.guild: - await interaction.response.send_message("Server only.", ephemeral=True) - return - if not interaction.user.guild_permissions.administrator: - await interaction.response.send_message("Administrator permission required.", ephemeral=True) - return - await interaction.response.send_modal(DeleteBackupModal(self.cog, interaction.guild.id)) - - -class RestoreBackupModal(discord.ui.Modal, title="♻️ Restore Backup"): - backup_id = discord.ui.TextInput( - label="Backup ID", - placeholder="1", - required=True, - max_length=16, - ) - - def __init__(self, cog: "Admin", guild_id: int) -> None: - super().__init__(timeout=None) - self.cog = cog - self.guild_id = guild_id - - async def on_submit(self, interaction: discord.Interaction) -> None: - await interaction.response.defer(ephemeral=True) - try: - bid = int(self.backup_id.value.strip()) - except ValueError: - await interaction.followup.send("Invalid backup ID.", ephemeral=True) - return - row = await self.cog.bot.db.fetchone( - "SELECT backup_data FROM server_backups WHERE guild_id = ? AND id = ?", - self.guild_id, bid, + status_name="online", + activity_type_name="playing", + activity_text="CYBER // GRID", ) - if not row: - await interaction.followup.send("Backup not found.", ephemeral=True) - return - try: - data = json.loads(row[0]) - except Exception: - await interaction.followup.send("Corrupted backup data.", ephemeral=True) - return - guild = interaction.guild - if not guild: - await interaction.followup.send("Server only.", ephemeral=True) - return - lang = await self.cog.bot.get_guild_language(self.guild_id) - created_roles = 0 - created_channels = 0 - for role_data in data.get("roles", []): - name = role_data.get("name", "") - if not name or guild.get_role_next_id() is None: - continue - existing = discord.utils.get(guild.roles, name=name) - if existing: - continue - try: - color = discord.Color(role_data.get("color", 0)) - perms = discord.Permissions(role_data.get("permissions", 0)) - await guild.create_role( - name=name, - color=color, - permissions=perms, - hoist=role_data.get("hoist", False), - mentionable=role_data.get("mentionable", False), - ) - created_roles += 1 - except Exception: - pass - for ch_data in data.get("channels", []): - name = ch_data.get("name", "") - ch_type = ch_data.get("type", "") - if not name: - continue - existing = discord.utils.get(guild.channels, name=name) - if existing: - continue - try: - if "text" in ch_type.lower(): - await guild.create_text_channel(name) - elif "voice" in ch_type.lower(): - await guild.create_voice_channel(name) - elif "category" in ch_type.lower(): - await guild.create_category(name) - created_channels += 1 - except Exception: - pass - if lang == "ar": - embed = success_embed( - "♻️ تم استعادة النسخة الاحتياطية", - f"{panel_divider('blue')}\n🎭 **أدوار:** {created_roles}\n📺 **قنوات:** {created_channels}\n{panel_divider('blue')}" - ) - else: - embed = success_embed( - "♻️ Backup Restored", - f"{panel_divider('blue')}\n🎭 **Roles:** {created_roles}\n📺 **Channels:** {created_channels}\n{panel_divider('blue')}" - ) - await interaction.followup.send(embed=embed, ephemeral=True) + embed = success_embed("Bot Status Reset", "Restored default presence.") + await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction)) + @commands.hybrid_command(name="bot_status", description="Show current bot presence", with_app_command=True) + @commands.is_owner() + async def bot_status(self, ctx: commands.Context) -> None: + embed = await self._presence_embed() + await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction)) -class DeleteBackupModal(discord.ui.Modal, title="🗑️ Delete Backup"): - backup_id = discord.ui.TextInput( - label="Backup ID", - placeholder="1", - required=True, - max_length=16, - ) + @commands.hybrid_command(name="set_bot_status", description="Set bot presence", with_app_command=True) + @commands.is_owner() + async def set_bot_status( + self, + ctx: commands.Context, + status: str, + activity_type: str, + *, + text: str, + ) -> None: + await self.admin_botstatus_set(ctx, status, activity_type, text=text) - def __init__(self, cog: "Admin", guild_id: int) -> None: - super().__init__(timeout=None) - self.cog = cog - self.guild_id = guild_id + @commands.hybrid_command(name="reset_bot_status", description="Reset bot presence to default", with_app_command=True) + @commands.is_owner() + async def reset_bot_status(self, ctx: commands.Context) -> None: + await self.admin_botstatus_reset(ctx) - async def on_submit(self, interaction: discord.Interaction) -> None: - await interaction.response.defer(ephemeral=True) - try: - bid = int(self.backup_id.value.strip()) - except ValueError: - await interaction.followup.send("Invalid backup ID.", ephemeral=True) - return - await self.cog.bot.db.execute( - "DELETE FROM server_backups WHERE guild_id = ? AND id = ?", - self.guild_id, bid, + @admin_group.group(name="shield", invoke_without_command=True) + @commands.has_permissions(administrator=True) + async def admin_shield_group(self, ctx: commands.Context) -> None: + await self._safe_ctx_send( + ctx, + content="Use `/admin shield state`, `/admin shield set_level `, or `/admin shield add_image `", + ephemeral=bool(ctx.interaction), + ) + + @admin_shield_group.command(name="set_level") + @commands.has_permissions(administrator=True) + async def admin_shield_set_level(self, ctx: commands.Context, level: str) -> None: + await self.shield_level(ctx, level) + + @admin_shield_group.command(name="state") + @commands.has_permissions(administrator=True) + async def admin_shield_state(self, ctx: commands.Context) -> None: + await self.shield_state(ctx) + + @admin_shield_group.command(name="add_image") + @commands.has_permissions(manage_messages=True) + async def admin_shield_add_image(self, ctx: commands.Context, image: discord.Attachment) -> None: + if not ctx.guild: + await self._safe_ctx_send(ctx, content="Server only.", ephemeral=bool(ctx.interaction)) + return + if not (image.content_type or "").startswith("image/"): + await self._safe_ctx_send(ctx, content="Please attach an image file.", ephemeral=bool(ctx.interaction)) + return + digest = hashlib.sha256(await image.read()).hexdigest() + await self.bot.db.execute( + "INSERT OR IGNORE INTO scam_images(guild_id, image_hash, created_by) VALUES (?, ?, ?)", + ctx.guild.id, + digest, + ctx.author.id, + ) + await self._safe_ctx_send(ctx, content="✅ Scam image signature saved.", ephemeral=bool(ctx.interaction)) + + @commands.hybrid_command(name="shield_level", description="Set AI shield level low|medium|high", hidden=True, with_app_command=False) + @commands.has_permissions(administrator=True) + async def shield_level(self, ctx: commands.Context, level: str) -> None: + if ctx.interaction and not ctx.interaction.response.is_done(): + await ctx.interaction.response.defer(ephemeral=True, thinking=True) + normalized = level.strip().lower() + if normalized not in {"low", "medium", "high"}: + await self._safe_ctx_send(ctx, content="Use one of: low, medium, high", ephemeral=bool(ctx.interaction)) + return + await self.bot.db.execute( + "INSERT INTO shield_settings(guild_id, level) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET level = excluded.level", + ctx.guild.id if ctx.guild else 0, + normalized, + ) + await self._safe_ctx_send(ctx, content=f"🛡️ Shield level set to `{normalized}`", ephemeral=bool(ctx.interaction)) + + @commands.hybrid_command(name="shield_state", description="Show current AI shield state", with_app_command=True) + @commands.has_permissions(administrator=True) + async def shield_state(self, ctx: commands.Context) -> None: + if not ctx.guild: + await self._safe_ctx_send(ctx, content="Server only.", ephemeral=bool(ctx.interaction)) + return + row = await self.bot.db.fetchone( + "SELECT level FROM shield_settings WHERE guild_id = ?", + ctx.guild.id, + ) + level = str(row[0]).strip().lower() if row and row[0] else "medium" + if level not in {"low", "medium", "high"}: + level = "medium" + + if level == "high": + profile = "Very strict. Fast context checks, highest sensitivity, strongest anti-scam posture." + elif level == "low": + profile = "Relaxed. Fewer interventions and lower sensitivity." + else: + profile = "Balanced. Good protection with moderate sensitivity." + + embed = info_embed( + "🛡️ Shield State", + f"Current level: **`{level}`**\n" + f"Profile: {profile}\n\n" + "Use `/shield_level low|medium|high` to change it.", + ) + await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction)) + + @commands.hybrid_command(name="econ_admin", description="Add/remove coins from user") + @commands.has_permissions(administrator=True) + async def econ_admin(self, ctx: commands.Context, member: discord.Member, action: str, amount: int) -> None: + if ctx.interaction and not ctx.interaction.response.is_done(): + await ctx.interaction.response.defer(ephemeral=True, thinking=True) + action_n = action.strip().lower() + if action_n not in {"add", "remove"}: + await self._safe_ctx_send(ctx, content="Use action: add/remove", ephemeral=bool(ctx.interaction)) + return + amount = max(0, amount) + row = await self.bot.db.fetchone("SELECT coins, xp FROM economy WHERE user_id = ?", member.id) + coins = int(row[0]) if row else 0 + xp = int(row[1]) if row else 0 + new_coins = coins + amount if action_n == "add" else max(0, coins - amount) + await self.bot.db.execute( + "INSERT INTO economy(user_id, coins, xp) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET coins = excluded.coins, xp = excluded.xp", + member.id, + new_coins, + xp, + ) + await self._safe_ctx_send(ctx, content=f"💰 {member.mention} coins: `{coins}` → `{new_coins}`", ephemeral=bool(ctx.interaction)) + + @commands.hybrid_command(name="warn") + @commands.has_permissions(manage_roles=True) + async def warn(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None: + """Warn a member.""" + guild_id = ctx.guild.id if ctx.guild else None + lang = await self.bot.get_guild_language(guild_id) + + await self.bot.db.execute( + "INSERT INTO user_warnings(guild_id, user_id, moderator_id, reason, timestamp) VALUES (?, ?, ?, ?, ?)", + ctx.guild.id, + member.id, + ctx.author.id, + reason, + dt.datetime.utcnow().isoformat(), + ) + + if lang == "ar": + title = "⚠️ تحذير للعضو" + desc = ( + f"{panel_divider('orange')}\n" + f"👤 **المستخدم:** {member.mention}\n" + f"🛡️ **المشرف:** {ctx.author.mention}\n" + f"📝 **السبب:** {reason}\n" + f"{panel_divider('orange')}" + ) + dm_title = "⚠️ تم تحذيرك" + dm_desc = f"🏛️ **السيرفر:** {ctx.guild.name}\n📝 **السبب:** {reason}\n🛡️ **المشرف:** {ctx.author}" + else: + title = "⚠️ Member Warned" + desc = ( + f"{panel_divider('orange')}\n" + f"👤 **User:** {member.mention}\n" + f"🛡️ **Moderator:** {ctx.author.mention}\n" + f"📝 **Reason:** {reason}\n" + f"{panel_divider('orange')}" + ) + dm_title = "⚠️ You have been warned" + dm_desc = f"🏛️ **Server:** {ctx.guild.name}\n📝 **Reason:** {reason}\n🛡️ **Moderator:** {ctx.author}" + + embed = warning_embed(title, desc) + await ctx.reply(embed=embed) + + try: + dm_embed = warning_embed(dm_title, dm_desc) + await member.send(embed=dm_embed) + except discord.Forbidden: + pass + + @commands.hybrid_command(name="warnings") + @commands.has_permissions(manage_roles=True) + async def warnings(self, ctx: commands.Context, member: discord.Member) -> None: + """View warnings for a member.""" + guild_id = ctx.guild.id if ctx.guild else None + lang = await self.bot.get_guild_language(guild_id) + + rows = await self.bot.db.fetchall( + "SELECT moderator_id, reason, timestamp FROM user_warnings WHERE guild_id = ? AND user_id = ? ORDER BY timestamp DESC LIMIT 10", + ctx.guild.id, + member.id, + ) + if not rows: + if lang == "ar": + embed = info_embed("📋 التحذيرات", f"{member.mention} لا توجد تحذيرات. ✅") + else: + embed = info_embed("📋 Warnings", f"{member.mention} has no warnings. ✅") + await ctx.reply(embed=embed) + return + + lines = [] + for idx, (mod_id, reason, timestamp) in enumerate(rows, 1): + mod = ctx.guild.get_member(mod_id) + mod_name = mod.display_name if mod else f"<@{mod_id}>" + lines.append(f"`{idx}.` 📝 {reason[:50]}{'...' if len(reason) > 50 else ''}\n 🛡️ by {mod_name}") + + title = f"📋 تحذيرات {member.display_name}" if lang == "ar" else f"📋 Warnings for {member.display_name}" + embed = discord.Embed( + title=title, + description=f"{panel_divider('orange')}\n" + "\n".join(lines) + f"\n{panel_divider('orange')}", + color=NEON_ORANGE, + ) + embed.set_thumbnail(url=member.display_avatar.url) + await ctx.reply(embed=embed) + + @commands.hybrid_command(name="clearwarn") + @commands.has_permissions(manage_roles=True) + async def clearwarn(self, ctx: commands.Context, member: discord.Member) -> None: + """Clear all warnings for a member.""" + guild_id = ctx.guild.id if ctx.guild else None + lang = await self.bot.get_guild_language(guild_id) + + await self.bot.db.execute( + "DELETE FROM user_warnings WHERE guild_id = ? AND user_id = ?", + ctx.guild.id, + member.id, + ) + + if lang == "ar": + embed = success_embed("✅ تم مسح التحذيرات", f"🗑️ تم إزالة جميع تحذيرات {member.mention}.") + else: + embed = success_embed("✅ Warnings Cleared", f"🗑️ All warnings for {member.mention} have been removed.") + await ctx.reply(embed=embed) + + @commands.hybrid_command(name="kick") + @commands.has_permissions(kick_members=True) + async def kick(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None: + """Kick a member from the server.""" + guild_id = ctx.guild.id if ctx.guild else None + lang = await self.bot.get_guild_language(guild_id) + + if member.top_role >= ctx.author.top_role and ctx.author.id != ctx.guild.owner_id: + if lang == "ar": + await ctx.reply("❌ لا يمكنك طرد شخص برتبة أعلى أو مساوية.") + else: + await ctx.reply("❌ You cannot kick someone with a higher or equal role.") + return + + try: + if lang == "ar": + dm_embed = warning_embed("👢 تم طردك", f"🏛️ **السيرفر:** {ctx.guild.name}\n📝 **السبب:** {reason}\n🛡️ **المشرف:** {ctx.author}") + else: + dm_embed = warning_embed("👢 You have been kicked", f"🏛️ **Server:** {ctx.guild.name}\n📝 **Reason:** {reason}\n🛡️ **Moderator:** {ctx.author}") + await member.send(embed=dm_embed) + except discord.Forbidden: + pass + + await member.kick(reason=reason) + + if lang == "ar": + embed = success_embed( + "👢 تم طرد العضو", + f"{panel_divider('green')}\n" + f"👤 **المستخدم:** {member.mention}\n" + f"🛡️ **المشرف:** {ctx.author.mention}\n" + f"📝 **السبب:** {reason}\n" + f"{panel_divider('green')}" + ) + else: + embed = success_embed( + "👢 Member Kicked", + f"{panel_divider('green')}\n" + f"👤 **User:** {member.mention}\n" + f"🛡️ **Moderator:** {ctx.author.mention}\n" + f"📝 **Reason:** {reason}\n" + f"{panel_divider('green')}" + ) + await ctx.reply(embed=embed) + + @commands.hybrid_command(name="ban") + @commands.has_permissions(ban_members=True) + async def ban(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None: + """Ban a member from the server.""" + guild_id = ctx.guild.id if ctx.guild else None + lang = await self.bot.get_guild_language(guild_id) + + if member.top_role >= ctx.author.top_role and ctx.author.id != ctx.guild.owner_id: + if lang == "ar": + await ctx.reply("❌ لا يمكنك حظر شخص برتبة أعلى أو مساوية.") + else: + await ctx.reply("❌ You cannot ban someone with a higher or equal role.") + return + + try: + if lang == "ar": + dm_embed = error_embed("🔨 تم حظرك", f"🏛️ **السيرفر:** {ctx.guild.name}\n📝 **السبب:** {reason}\n🛡️ **المشرف:** {ctx.author}") + else: + dm_embed = error_embed("🔨 You have been banned", f"🏛️ **Server:** {ctx.guild.name}\n📝 **Reason:** {reason}\n🛡️ **Moderator:** {ctx.author}") + await member.send(embed=dm_embed) + except discord.Forbidden: + pass + + await member.ban(reason=reason) + + if lang == "ar": + embed = success_embed( + "🔨 تم حظر العضو", + f"{panel_divider('pink')}\n" + f"👤 **المستخدم:** {member.mention}\n" + f"🛡️ **المشرف:** {ctx.author.mention}\n" + f"📝 **السبب:** {reason}\n" + f"{panel_divider('pink')}" + ) + else: + embed = success_embed( + "🔨 Member Banned", + f"{panel_divider('pink')}\n" + f"👤 **User:** {member.mention}\n" + f"🛡️ **Moderator:** {ctx.author.mention}\n" + f"📝 **Reason:** {reason}\n" + f"{panel_divider('pink')}" + ) + await ctx.reply(embed=embed) + await self.bot.log_to_guild( + ctx.guild, + "🔨 Moderation: Ban", + f"{member.mention} was banned by {ctx.author.mention}\nReason: {reason}", + color=discord.Color.red(), + ) + + @commands.hybrid_command(name="unban") + @commands.has_permissions(ban_members=True) + async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = "No reason provided") -> None: + """Unban a user by their ID.""" + guild_id = ctx.guild.id if ctx.guild else None + lang = await self.bot.get_guild_language(guild_id) + + try: + user = await self.bot.fetch_user(user_id) + except discord.NotFound: + if lang == "ar": + await ctx.reply("❌ المستخدم غير موجود.") + else: + await ctx.reply("❌ User not found.") + return + except discord.HTTPException: + if lang == "ar": + await ctx.reply("❌ تعذر جلب المستخدم.") + else: + await ctx.reply("❌ Could not fetch user.") + return + + bans = [b async for b in ctx.guild.bans()] + if user not in [b.user for b in bans]: + if lang == "ar": + await ctx.reply(f"❌ {user.mention} غير محظور.") + else: + await ctx.reply(f"❌ {user.mention} is not banned.") + return + + await ctx.guild.unban(user, reason=reason) + + if lang == "ar": + embed = success_embed( + "🔓 تم فك الحظر", + f"{panel_divider('green')}\n" + f"👤 **المستخدم:** {user.mention}\n" + f"🛡️ **المشرف:** {ctx.author.mention}\n" + f"📝 **السبب:** {reason}\n" + f"{panel_divider('green')}" + ) + else: + embed = success_embed( + "🔓 Member Unbanned", + f"{panel_divider('green')}\n" + f"👤 **User:** {user.mention}\n" + f"🛡️ **Moderator:** {ctx.author.mention}\n" + f"📝 **Reason:** {reason}\n" + f"{panel_divider('green')}" + ) + await ctx.reply(embed=embed) + await self.bot.log_to_guild( + ctx.guild, + "🔓 Moderation: Unban", + f"{user.mention} was unbanned by {ctx.author.mention}\nReason: {reason}", + color=discord.Color.green(), + ) + + @commands.hybrid_command(name="mute") + @commands.has_permissions(moderate_members=True) + async def mute(self, ctx: commands.Context, member: discord.Member, duration: int = 10, *, reason: str = "No reason provided") -> None: + """Timeout a member for a specified duration in minutes.""" + guild_id = ctx.guild.id if ctx.guild else None + lang = await self.bot.get_guild_language(guild_id) + + if member.top_role >= ctx.author.top_role and ctx.author.id != ctx.guild.owner_id: + if lang == "ar": + await ctx.reply("❌ لا يمكنك كتم شخص برتبة أعلى أو مساوية.") + else: + await ctx.reply("❌ You cannot mute someone with a higher or equal role.") + return + + duration = max(1, min(duration, 40320)) # Max 28 days + until = dt.datetime.utcnow() + dt.timedelta(minutes=duration) + + await member.timeout(until, reason=reason) + + if lang == "ar": + embed = success_embed( + "🔇 تم كتم العضو", + f"{panel_divider('orange')}\n" + f"👤 **المستخدم:** {member.mention}\n" + f"⏰ **المدة:** {duration} دقيقة\n" + f"🛡️ **المشرف:** {ctx.author.mention}\n" + f"📝 **السبب:** {reason}\n" + f"{panel_divider('orange')}" + ) + else: + embed = success_embed( + "🔇 Member Muted", + f"{panel_divider('orange')}\n" + f"👤 **User:** {member.mention}\n" + f"⏰ **Duration:** {duration} minutes\n" + f"🛡️ **Moderator:** {ctx.author.mention}\n" + f"📝 **Reason:** {reason}\n" + f"{panel_divider('orange')}" + ) + await ctx.reply(embed=embed) + await self.bot.log_to_guild( + ctx.guild, + "🔇 Moderation: Mute", + f"{member.mention} was muted by {ctx.author.mention} for {duration} minute(s)\nReason: {reason}", + color=discord.Color.red(), + ) + + @commands.hybrid_command(name="unmute") + @commands.has_permissions(moderate_members=True) + async def unmute(self, ctx: commands.Context, member: discord.Member, *, reason: str = "No reason provided") -> None: + """Remove timeout from a member.""" + guild_id = ctx.guild.id if ctx.guild else None + lang = await self.bot.get_guild_language(guild_id) + + await member.timeout(None, reason=reason) + + if lang == "ar": + embed = success_embed("🔊 تم فك الكتم", f"👤 **المستخدم:** {member.mention}\n🛡️ **المشرف:** {ctx.author.mention}\n📝 **السبب:** {reason}") + else: + embed = success_embed("🔊 Member Unmuted", f"👤 **User:** {member.mention}\n🛡️ **Moderator:** {ctx.author.mention}\n📝 **Reason:** {reason}") + await ctx.reply(embed=embed) + + + async def _clone_emoji_to_guild( + self, + guild: discord.Guild, + actor: discord.abc.User, + *, + source: str, + name: str | None = None, + ) -> tuple[discord.Emoji | None, str]: + source_clean = (source or "").strip() + if not source_clean: + return None, "Provide a valid emoji tag, emoji ID, emoji URL, or known emoji name." + + emoji_url: str | None = None + detected_name: str | None = None + tag_match = re.fullmatch(r"<(a?):([A-Za-z0-9_]{2,32}):(\d+)>", source_clean) + if tag_match: + is_animated = bool(tag_match.group(1)) + detected_name = tag_match.group(2) + eid = tag_match.group(3) + ext = "gif" if is_animated else "png" + emoji_url = f"https://cdn.discordapp.com/emojis/{eid}.{ext}?quality=lossless" + elif source_clean.isdigit(): + emoji_url = f"https://cdn.discordapp.com/emojis/{source_clean}.png?quality=lossless" + elif source_clean.startswith(("http://", "https://")): + emoji_url = source_clean + else: + found = discord.utils.get(self.bot.emojis, name=source_clean) + if found: + detected_name = found.name + emoji_url = str(found.url) + + if not emoji_url: + return None, "Provide a valid emoji tag, emoji ID, emoji URL, or known emoji name." + + target_name_raw = (name or detected_name or "cloned_emoji").strip().lower() + target_name = re.sub(r"[^a-z0-9_]", "_", target_name_raw) + target_name = re.sub(r"_+", "_", target_name).strip("_") + if len(target_name) < 2: + target_name = "cloned_emoji" + target_name = target_name[:32] + + try: + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=20)) as session: + async with session.get(emoji_url) as resp: + if resp.status != 200: + return None, f"Failed to fetch emoji image (HTTP {resp.status})." + image_bytes = await resp.read() + except Exception as exc: + return None, f"Failed to fetch emoji image: {exc}" + + if not image_bytes: + return None, "Fetched emoji image is empty." + if len(image_bytes) > 256 * 1024: + return None, "Emoji image is too large. Discord custom emojis must be <= 256 KB." + + try: + created = await guild.create_custom_emoji( + name=target_name, + image=image_bytes, + reason=f"Emoji cloned by {actor} via /admin emoji clone", + ) + except discord.Forbidden: + return None, "Missing permission to create emojis." + except discord.HTTPException as exc: + return None, f"Could not create emoji: {exc}" + + return created, "" + + @admin_group.group(name="emoji", invoke_without_command=True) + @commands.has_permissions(manage_emojis=True) + async def admin_emoji_group(self, ctx: commands.Context) -> None: + await self.admin_emoji_panel(ctx) + + @admin_emoji_group.command(name="panel") + @commands.has_permissions(manage_emojis=True) + async def admin_emoji_panel(self, ctx: commands.Context) -> None: + if not ctx.guild: + await self._safe_ctx_send(ctx, content="Server only.", ephemeral=bool(ctx.interaction)) + return + embed = info_embed( + "Emoji Clone Panel", + "Quick actions:\n- Clone from tag/ID/URL/name\n- View server emojis\n- Open bot emoji picker", ) - lang = await self.cog.bot.get_guild_language(self.guild_id) - msg = "✅ Backup deleted." if lang != "ar" else "✅ تم حذف النسخة الاحتياطية." - await interaction.followup.send(msg, ephemeral=True) - - -class SlowmodeCommandMixin: - """Shared slowmode/lock/unlock commands.""" - pass - - - @commands.hybrid_command(name="slowmode") - @commands.has_permissions(manage_channels=True) - async def slowmode(self, ctx: commands.Context, seconds: int = 0) -> None: - """Set slowmode for the current channel.""" - guild_id = ctx.guild.id if ctx.guild else None - lang = await self.bot.get_guild_language(guild_id) - - seconds = max(0, min(seconds, 21600)) # Max 6 hours - await ctx.channel.edit(slowmode_delay=seconds) - - if seconds > 0: - if lang == "ar": - embed = success_embed("⏱️ تم تفعيل الوضع البطيء", f"🕒 **المدة:** {seconds} ثانية\n📺 **القناة:** {ctx.channel.mention}") - else: - embed = success_embed("⏱️ Slowmode Enabled", f"🕒 **Duration:** {seconds} seconds\n📺 **Channel:** {ctx.channel.mention}") - else: - if lang == "ar": - embed = success_embed("⏱️ تم إيقاف الوضع البطيء", f"📺 **القناة:** {ctx.channel.mention}") - else: - embed = success_embed("⏱️ Slowmode Disabled", f"📺 **Channel:** {ctx.channel.mention}") - await ctx.reply(embed=embed) - - @commands.hybrid_command(name="lock") - @commands.has_permissions(manage_channels=True) - async def lock(self, ctx: commands.Context, channel: discord.TextChannel | None = None) -> None: - """Lock a channel to prevent messages.""" - guild_id = ctx.guild.id if ctx.guild else None - lang = await self.bot.get_guild_language(guild_id) - - channel = channel or ctx.channel - overwrite = channel.overwrites_for(ctx.guild.default_role) - overwrite.send_messages = False - await channel.set_permissions(ctx.guild.default_role, overwrite=overwrite) - - if lang == "ar": - embed = success_embed("🔒 تم قفل القناة", f"📺 **القناة:** {channel.mention}") - else: - embed = success_embed("🔒 Channel Locked", f"📺 **Channel:** {channel.mention}") - await ctx.reply(embed=embed) - - @commands.hybrid_command(name="unlock") - @commands.has_permissions(manage_channels=True) - async def unlock(self, ctx: commands.Context, channel: discord.TextChannel | None = None) -> None: - """Unlock a channel to allow messages.""" - guild_id = ctx.guild.id if ctx.guild else None - lang = await self.bot.get_guild_language(guild_id) - - channel = channel or ctx.channel - overwrite = channel.overwrites_for(ctx.guild.default_role) - overwrite.send_messages = True - await channel.set_permissions(ctx.guild.default_role, overwrite=overwrite) - - if lang == "ar": - embed = success_embed("🔓 تم فتح القناة", f"📺 **القناة:** {channel.mention}") - else: - embed = success_embed("🔓 Channel Unlocked", f"📺 **Channel:** {channel.mention}") - await ctx.reply(embed=embed) - - -async def setup(bot: commands.Bot) -> None: - await bot.add_cog(Admin(bot)) + await self._safe_ctx_send(ctx, embed=embed, view=EmojiClonePanelView(self), ephemeral=bool(ctx.interaction)) + + @commands.hybrid_command(name="emoji_clone_panel", description="Open the emoji clone panel", with_app_command=True) + @commands.has_permissions(manage_emojis=True) + async def emoji_clone_panel(self, ctx: commands.Context) -> None: + await self.admin_emoji_panel(ctx) + + @admin_emoji_group.command(name="clone") + @commands.has_permissions(manage_emojis=True) + async def admin_emoji_clone(self, ctx: commands.Context, source: str | None = None, name: str | None = None) -> None: + if not ctx.guild: + await self._safe_ctx_send(ctx, content="Server only.", ephemeral=bool(ctx.interaction)) + return + if source: + if ctx.interaction and not ctx.interaction.response.is_done(): + try: + await ctx.interaction.response.defer(ephemeral=True, thinking=True) + except (discord.NotFound, discord.HTTPException, discord.InteractionResponded): + pass + + created, error_text = await self._clone_emoji_to_guild( + ctx.guild, + ctx.author, + source=source, + name=name, + ) + if error_text: + await self._safe_ctx_send(ctx, content=error_text, ephemeral=True) + return + + embed = success_embed( + "Emoji Cloned", + f"Created {created} as `:{created.name}:`", + ) + await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction)) + return + + custom_names = sorted({emoji.name for emoji in getattr(self.bot, "emojis", []) if getattr(emoji, "name", None)}) + if not custom_names: + await self._safe_ctx_send(ctx, content="No bot emoji catalog available.", ephemeral=bool(ctx.interaction)) + return + lines = [] + for em_name in custom_names[:40]: + emoji_obj = discord.utils.get(self.bot.emojis, name=em_name) + rendered = str(emoji_obj) if emoji_obj else em_name + lines.append(f"{rendered} `{em_name}`") + embed = info_embed( + "Emoji Picker", + "\n".join(lines) + + "\n\nUse: `/admin emoji clone [name]`", + ) + await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction)) + + @commands.hybrid_command(name="awesomeroles") + @commands.has_permissions(manage_roles=True) + async def awesomeroles(self, ctx: commands.Context) -> None: + """Create awesome roles for the server with proper permissions.""" + guild_id = ctx.guild.id if ctx.guild else None + lang = await self.bot.get_guild_language(guild_id) + + created = [] + skipped = [] + + for role_data in AWESOME_ROLES: + existing = discord.utils.get(ctx.guild.roles, name=role_data["name"]) + if existing: + skipped.append(role_data["name"]) + continue + + # Build permissions + permissions = discord.Permissions() + for perm_name in role_data["permissions"]: + setattr(permissions, perm_name, True) + + try: + role = await ctx.guild.create_role( + name=role_data["name"], + color=role_data["color"], + permissions=permissions, + hoist=role_data["hoist"], + reason=f"Created by {ctx.author} via /awesomeroles" + ) + created.append((role, role_data["description"])) + except discord.Forbidden: + continue + + embed = discord.Embed( + title=f"✨ {'الأدوار الرائعة' if lang == 'ar' else 'Awesome Roles'}", + description=f"{panel_divider('purple')}", + color=NEON_PURPLE, + ) + + if created: + lines = [] + for role, desc in created: + lines.append(f"{role.mention}\n└─ *{desc}*") + embed.add_field( + name=f"✅ {'تم إنشاؤها' if lang == 'ar' else 'Created'} ({len(created)})", + value="\n\n".join(lines), + inline=False + ) + + if skipped: + embed.add_field( + name=f"⏭️ {'موجودة مسبقاً' if lang == 'ar' else 'Already Exist'} ({len(skipped)})", + value="\n".join(f"• {name}" for name in skipped), + inline=False + ) + + embed.add_field( + name="💡 {'نصيحة' if lang == 'ar' else 'Tip'}", + value="Drag roles in server settings to set their position!" if lang != "ar" else "اسحب الأدوار في إعدادات السيرفر لتغيير ترتيبها!", + inline=False + ) + + embed.set_footer(text=f"{E_STAR} Created by {ctx.author.display_name}") + await ctx.reply(embed=embed) + + @commands.hybrid_command(name="backupserver") + @commands.has_permissions(administrator=True) + async def backupserver(self, ctx: commands.Context) -> None: + """Create a backup of server structure.""" + if not ctx.guild: + await ctx.reply("Server only.") + return + if ctx.interaction and not ctx.interaction.response.is_done(): + try: + await ctx.interaction.response.defer(ephemeral=True, thinking=True) + except (discord.NotFound, discord.HTTPException, discord.InteractionResponded): + # If interaction token is already invalid, continue and fallback to channel sends via resilient ctx.reply. + pass + + guild_id = ctx.guild.id if ctx.guild else None + lang = await self.bot.get_guild_language(guild_id) + + # Collect server data + roles = [] + for r in ctx.guild.roles: + if not r.is_default() and not r.managed: + roles.append({ + "name": r.name, + "color": r.color.value, + "permissions": r.permissions.value, + "hoist": r.hoist, + "mentionable": r.mentionable, + "position": r.position + }) + + channels = [] + for c in ctx.guild.channels: + channels.append({ + "name": c.name, + "type": str(c.type), + "position": c.position, + "category": c.category.name if c.category else None + }) + + backup_data = { + "roles": roles, + "channels": channels, + "name": ctx.guild.name, + "icon_url": str(ctx.guild.icon.url) if ctx.guild.icon else None + } + + await self.bot.db.execute( + "INSERT INTO server_backups(guild_id, backup_data, created_by, created_at) VALUES (?, ?, ?, ?)", + ctx.guild.id, + json.dumps(backup_data, ensure_ascii=False), + ctx.author.id, + dt.datetime.utcnow().isoformat(), + ) + + if lang == "ar": + embed = success_embed( + "💾 تم إنشاء نسخة احتياطية", + f"{panel_divider('blue')}\n" + f"🎭 **الأدوار:** {len(roles)}\n" + f"📺 **القنوات:** {len(channels)}\n" + f"👤 **أنشئت بواسطة:** {ctx.author.mention}\n" + f"{panel_divider('blue')}" + ) + else: + embed = success_embed( + "💾 Server Backup Created", + f"{panel_divider('blue')}\n" + f"🎭 **Roles:** {len(roles)}\n" + f"📺 **Channels:** {len(channels)}\n" + f"👤 **Created by:** {ctx.author.mention}\n" + f"{panel_divider('blue')}" + ) + await ctx.reply(embed=embed) + + @commands.hybrid_command(name="backup_panel", description="Interactive backup management panel") + @commands.has_permissions(administrator=True) + async def backup_panel(self, ctx: commands.Context) -> None: + """Open the interactive backup management panel.""" + if not ctx.guild: + await ctx.reply("Server only.") + return + guild_id = ctx.guild.id + lang = await self.bot.get_guild_language(guild_id) + rows = await self.bot.db.fetchall( + "SELECT id, created_by, created_at, backup_data FROM server_backups WHERE guild_id = ? ORDER BY id DESC LIMIT 10", + guild_id, + ) + embed = self._build_backup_embed(guild_id, lang, rows) + await ctx.reply(embed=embed, view=BackupPanelView(self, guild_id)) + + def _build_backup_embed(self, guild_id: int, lang: str, rows: list) -> discord.Embed: + if lang == "ar": + title = "💾 إدارة النسخ الاحتياطية" + desc = f"{panel_divider('blue')}\nإدارة النسخ الاحتياطية للسيرفر\n{panel_divider('blue')}" + else: + title = "💾 Backup Management" + desc = f"{panel_divider('blue')}\nManage server backups\n{panel_divider('blue')}" + embed = discord.Embed(title=title, description=desc, color=NEON_CYAN) + if not rows: + embed.add_field(name="📦" if lang == "ar" else "📦 No backups", value="No backups found." if lang != "ar" else "لا توجد نسخ احتياطية.", inline=False) + else: + lines = [] + for bid, creator_id, created_at, data_str in rows: + try: + data = json.loads(data_str) + roles_count = len(data.get("roles", [])) + channels_count = len(data.get("channels", [])) + except Exception: + roles_count = "?" + channels_count = "?" + lines.append(f"**#{bid}** — {created_at[:19]} | 🎭{roles_count} 📺{channels_count}") + embed.add_field( + name="📦 Recent Backups" if lang != "ar" else "📦 النسخ الأخيرة", + value="\n".join(lines[:10]), + inline=False, + ) + return embed + + +class BackupPanelView(discord.ui.View): + def __init__(self, cog: "Admin", guild_id: int) -> None: + super().__init__(timeout=None) + self.cog = cog + self.guild_id = guild_id + + @discord.ui.button(label="Refresh", style=discord.ButtonStyle.blurple, emoji=ui("refresh"), row=2) + async def refresh(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: + if not interaction.guild: + await interaction.response.send_message("Server only.", ephemeral=True) + return + guild_id = interaction.guild.id + lang = await self.cog.bot.get_guild_language(guild_id) + rows = await self.cog.bot.db.fetchall( + "SELECT id, created_by, created_at, backup_data FROM server_backups WHERE guild_id = ? ORDER BY id DESC LIMIT 10", + guild_id, + ) + embed = self.cog._build_backup_embed(guild_id, lang, rows) + await interaction.response.edit_message(embed=embed, view=self) + + @discord.ui.button(label="List Backups", style=discord.ButtonStyle.primary, emoji=ui("notebook"), row=0) + async def list_backups(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: + if not interaction.guild: + await interaction.response.send_message("Server only.", ephemeral=True) + return + await interaction.response.defer(ephemeral=True) + lang = await self.cog.bot.get_guild_language(interaction.guild.id) + rows = await self.cog.bot.db.fetchall( + "SELECT id, created_by, created_at, backup_data FROM server_backups WHERE guild_id = ? ORDER BY id DESC", + interaction.guild.id, + ) + if not rows: + await interaction.followup.send("No backups found." if lang != "ar" else "لا توجد نسخ احتياطية.", ephemeral=True) + return + lines = [] + for bid, creator_id, created_at, data_str in rows: + try: + data = json.loads(data_str) + roles_count = len(data.get("roles", [])) + channels_count = len(data.get("channels", [])) + except Exception: + roles_count = "?" + channels_count = "?" + lines.append(f"**#{bid}** — {created_at[:19]} | 🎭{roles_count} 📺{channels_count} | By: <@{creator_id}>") + embed = discord.Embed( + title="📦 All Backups" if lang != "ar" else "📦 جميع النسخ", + description="\n".join(lines[:20]), + color=NEON_CYAN, + ) + await interaction.followup.send(embed=embed, ephemeral=True) + + @discord.ui.button(label="Restore Backup", style=discord.ButtonStyle.success, emoji=ui("ok"), row=0) + async def restore_backup(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: + if not interaction.guild: + await interaction.response.send_message("Server only.", ephemeral=True) + return + if not interaction.user.guild_permissions.administrator: + await interaction.response.send_message("Administrator permission required.", ephemeral=True) + return + await interaction.response.send_modal(RestoreBackupModal(self.cog, interaction.guild.id)) + + @discord.ui.button(label="Delete Backup", style=discord.ButtonStyle.danger, emoji=ui("trash"), row=0) + async def delete_backup(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: + if not interaction.guild: + await interaction.response.send_message("Server only.", ephemeral=True) + return + if not interaction.user.guild_permissions.administrator: + await interaction.response.send_message("Administrator permission required.", ephemeral=True) + return + await interaction.response.send_modal(DeleteBackupModal(self.cog, interaction.guild.id)) + + +class RestoreBackupModal(discord.ui.Modal, title="♻️ Restore Backup"): + backup_id = discord.ui.TextInput( + label="Backup ID", + placeholder="1", + required=True, + max_length=16, + ) + + def __init__(self, cog: "Admin", guild_id: int) -> None: + super().__init__(timeout=None) + self.cog = cog + self.guild_id = guild_id + + async def on_submit(self, interaction: discord.Interaction) -> None: + await interaction.response.defer(ephemeral=True) + try: + bid = int(self.backup_id.value.strip()) + except ValueError: + await interaction.followup.send("Invalid backup ID.", ephemeral=True) + return + row = await self.cog.bot.db.fetchone( + "SELECT backup_data FROM server_backups WHERE guild_id = ? AND id = ?", + self.guild_id, bid, + ) + if not row: + await interaction.followup.send("Backup not found.", ephemeral=True) + return + try: + data = json.loads(row[0]) + except Exception: + await interaction.followup.send("Corrupted backup data.", ephemeral=True) + return + guild = interaction.guild + if not guild: + await interaction.followup.send("Server only.", ephemeral=True) + return + lang = await self.cog.bot.get_guild_language(self.guild_id) + created_roles = 0 + created_channels = 0 + for role_data in data.get("roles", []): + name = role_data.get("name", "") + if not name or guild.get_role_next_id() is None: + continue + existing = discord.utils.get(guild.roles, name=name) + if existing: + continue + try: + color = discord.Color(role_data.get("color", 0)) + perms = discord.Permissions(role_data.get("permissions", 0)) + await guild.create_role( + name=name, + color=color, + permissions=perms, + hoist=role_data.get("hoist", False), + mentionable=role_data.get("mentionable", False), + ) + created_roles += 1 + except Exception: + pass + for ch_data in data.get("channels", []): + name = ch_data.get("name", "") + ch_type = ch_data.get("type", "") + if not name: + continue + existing = discord.utils.get(guild.channels, name=name) + if existing: + continue + try: + if "text" in ch_type.lower(): + await guild.create_text_channel(name) + elif "voice" in ch_type.lower(): + await guild.create_voice_channel(name) + elif "category" in ch_type.lower(): + await guild.create_category(name) + created_channels += 1 + except Exception: + pass + if lang == "ar": + embed = success_embed( + "♻️ تم استعادة النسخة الاحتياطية", + f"{panel_divider('blue')}\n🎭 **أدوار:** {created_roles}\n📺 **قنوات:** {created_channels}\n{panel_divider('blue')}" + ) + else: + embed = success_embed( + "♻️ Backup Restored", + f"{panel_divider('blue')}\n🎭 **Roles:** {created_roles}\n📺 **Channels:** {created_channels}\n{panel_divider('blue')}" + ) + await interaction.followup.send(embed=embed, ephemeral=True) + + +class DeleteBackupModal(discord.ui.Modal, title="🗑️ Delete Backup"): + backup_id = discord.ui.TextInput( + label="Backup ID", + placeholder="1", + required=True, + max_length=16, + ) + + def __init__(self, cog: "Admin", guild_id: int) -> None: + super().__init__(timeout=None) + self.cog = cog + self.guild_id = guild_id + + async def on_submit(self, interaction: discord.Interaction) -> None: + await interaction.response.defer(ephemeral=True) + try: + bid = int(self.backup_id.value.strip()) + except ValueError: + await interaction.followup.send("Invalid backup ID.", ephemeral=True) + return + await self.cog.bot.db.execute( + "DELETE FROM server_backups WHERE guild_id = ? AND id = ?", + self.guild_id, bid, + ) + lang = await self.cog.bot.get_guild_language(self.guild_id) + msg = "✅ Backup deleted." if lang != "ar" else "✅ تم حذف النسخة الاحتياطية." + await interaction.followup.send(msg, ephemeral=True) + + +class SlowmodeCommandMixin: + """Shared slowmode/lock/unlock commands.""" + pass + + + @commands.hybrid_command(name="slowmode") + @commands.has_permissions(manage_channels=True) + async def slowmode(self, ctx: commands.Context, seconds: int = 0) -> None: + """Set slowmode for the current channel.""" + guild_id = ctx.guild.id if ctx.guild else None + lang = await self.bot.get_guild_language(guild_id) + + seconds = max(0, min(seconds, 21600)) # Max 6 hours + await ctx.channel.edit(slowmode_delay=seconds) + + if seconds > 0: + if lang == "ar": + embed = success_embed("⏱️ تم تفعيل الوضع البطيء", f"🕒 **المدة:** {seconds} ثانية\n📺 **القناة:** {ctx.channel.mention}") + else: + embed = success_embed("⏱️ Slowmode Enabled", f"🕒 **Duration:** {seconds} seconds\n📺 **Channel:** {ctx.channel.mention}") + else: + if lang == "ar": + embed = success_embed("⏱️ تم إيقاف الوضع البطيء", f"📺 **القناة:** {ctx.channel.mention}") + else: + embed = success_embed("⏱️ Slowmode Disabled", f"📺 **Channel:** {ctx.channel.mention}") + await ctx.reply(embed=embed) + + @commands.hybrid_command(name="lock") + @commands.has_permissions(manage_channels=True) + async def lock(self, ctx: commands.Context, channel: discord.TextChannel | None = None) -> None: + """Lock a channel to prevent messages.""" + guild_id = ctx.guild.id if ctx.guild else None + lang = await self.bot.get_guild_language(guild_id) + + channel = channel or ctx.channel + overwrite = channel.overwrites_for(ctx.guild.default_role) + overwrite.send_messages = False + await channel.set_permissions(ctx.guild.default_role, overwrite=overwrite) + + if lang == "ar": + embed = success_embed("🔒 تم قفل القناة", f"📺 **القناة:** {channel.mention}") + else: + embed = success_embed("🔒 Channel Locked", f"📺 **Channel:** {channel.mention}") + await ctx.reply(embed=embed) + + @commands.hybrid_command(name="unlock") + @commands.has_permissions(manage_channels=True) + async def unlock(self, ctx: commands.Context, channel: discord.TextChannel | None = None) -> None: + """Unlock a channel to allow messages.""" + guild_id = ctx.guild.id if ctx.guild else None + lang = await self.bot.get_guild_language(guild_id) + + channel = channel or ctx.channel + overwrite = channel.overwrites_for(ctx.guild.default_role) + overwrite.send_messages = True + await channel.set_permissions(ctx.guild.default_role, overwrite=overwrite) + + if lang == "ar": + embed = success_embed("🔓 تم فتح القناة", f"📺 **القناة:** {channel.mention}") + else: + embed = success_embed("🔓 Channel Unlocked", f"📺 **Channel:** {channel.mention}") + await ctx.reply(embed=embed) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Admin(bot)) diff --git a/bot/cogs/ai_admin.py b/bot/cogs/ai_admin.py index 0813b134a435a6782a1d63332b2b4d8b15055521..8d9bfc9f13529ca3b9c05fda91a512dc6919529e 100644 --- a/bot/cogs/ai_admin.py +++ b/bot/cogs/ai_admin.py @@ -1,479 +1,931 @@ -""" -Autonomous AI Administrator - Senior Backend Engineer Implementation -Permission Guard + Intelligence Layer + Execution Engine + Global Language Support -""" - +""" +Autonomous AI Administrator - Senior Backend Engineer Implementation +Permission Guard + Intelligence Layer + Execution Engine + Global Language Support +""" + from __future__ import annotations +import datetime as dt import json import re from typing import Any + +import discord +from discord.ext import commands + +try: + import aiohttp +except Exception: + aiohttp = None + + +class PermissionGuard: + """Validates hierarchy before any AI admin action.""" + + @staticmethod + def check(ctx: commands.Context) -> tuple[bool, str]: + if not ctx.guild: + return False, "Server only." + + if ctx.author.id == ctx.guild.owner_id: + return True, "" + + if not ctx.author.guild_permissions.manage_guild: + return False, "Manage Server permission required." + + member = ctx.guild.get_member(ctx.author.id) + bot_member = ctx.guild.me + + if member and member.top_role.position >= bot_member.top_role.position: + return False, "Insufficient Hierarchy: My role must be higher than yours." + + return True, "" + + +class IntelligenceLayer: + """OpenRouter integration for AI decision making.""" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + self._session: aiohttp.ClientSession | None = None + + async def _get_session(self) -> aiohttp.ClientSession: + if self._session is None or self._session.closed: + self._session = aiohttp.ClientSession() + return self._session + + async def ask_ai(self, prompt: str) -> list[dict[str, Any]] | None: + settings = self.bot.settings + api_key = getattr(settings, "openrouter_api_key", None) + if not api_key: + return None + + model = getattr(settings, "openrouter_model", "openai/gpt-4o-mini") or "openai/gpt-4o-mini" + + system_message = """You are an Autonomous AI Administrator for a Discord server. +Output ONLY a valid JSON array of actions. Do not include any other text. + +Supported actions: + +1. CREATE ROLE: +{ + "action": "create_role", + "name": "Role Name", + "color": "#FF0000", + "hoist": true, + "reason": "Why this role is needed" +} + +2. CREATE CHANNEL: +{ + "action": "create_channel", + "name": "channel-name", + "type": "text", + "category": "Category Name", + "locked_to_roles": ["Role1", "Role2"], + "reason": "Why this channel is needed" +} + +3. ANNOUNCE: +{ + "action": "announce", + "channel": "channel-name", + "title": "Announcement Title", + "description": "Announcement content", + "color": "#00FFFF" +} + +4. CREATE GIVEAWAY: +{ + "action": "create_giveaway", + "prize": "Prize description", + "duration_minutes": 60, + "winners": 1, + "channel": "channel-name" +} + +5. CREATE TOURNAMENT: +{ + "action": "create_tournament", + "name": "Tournament Name", + "game": "Game name", + "max_participants": 16, + "channel": "channel-name" +} + +6. CREATE POLL: +{ + "action": "create_poll", + "question": "Poll question", + "options": ["Option 1", "Option 2", "Option 3"], + "duration_minutes": 30 +} + +7. RUN COMMAND (for any other bot command): +{ + "action": "run_command", + "command": "command_name", + "args": {"arg1": "value1", "arg2": "value2"} +} -import discord -from discord.ext import commands - -try: - import aiohttp -except Exception: - aiohttp = None - - -class PermissionGuard: - """Validates hierarchy before any AI admin action.""" - - @staticmethod - def check(ctx: commands.Context) -> tuple[bool, str]: - if not ctx.guild: - return False, "Server only." - - if ctx.author.id == ctx.guild.owner_id: - return True, "" - - if not ctx.author.guild_permissions.manage_guild: - return False, "Manage Server permission required." - - member = ctx.guild.get_member(ctx.author.id) - bot_member = ctx.guild.me - - if member and member.top_role.position >= bot_member.top_role.position: - return False, "Insufficient Hierarchy: My role must be higher than yours." - - return True, "" - - -class IntelligenceLayer: - """OpenRouter integration for AI decision making.""" - - def __init__(self, bot: commands.Bot) -> None: - self.bot = bot - self._session: aiohttp.ClientSession | None = None - - async def _get_session(self) -> aiohttp.ClientSession: - if self._session is None or self._session.closed: - self._session = aiohttp.ClientSession() - return self._session - - async def ask_ai(self, prompt: str) -> list[dict[str, Any]] | None: - settings = self.bot.settings - api_key = getattr(settings, "openrouter_api_key", None) - if not api_key: - return None - - model = getattr(settings, "openrouter_model", "openai/gpt-4o-mini") or "openai/gpt-4o-mini" - - system_message = """You are an Autonomous AI Administrator for a Discord server. -Output ONLY a valid JSON array of actions. Do not include any other text. +8. TIMEOUT MEMBER: +{ + "action": "timeout_member", + "member": "@user or user_id", + "minutes": 30, + "reason": "Why timeout is needed" +} -Supported actions: +9. UNTIMEOUT MEMBER: +{ + "action": "untimeout_member", + "member": "@user or user_id", + "reason": "Why timeout is removed" +} -1. CREATE ROLE: +10. ADD ROLE: { - "action": "create_role", - "name": "Role Name", - "color": "#FF0000", - "hoist": true, - "reason": "Why this role is needed" + "action": "add_role", + "member": "@user or user_id", + "role": "Role Name", + "reason": "Why role is added" } -2. CREATE CHANNEL: +11. REMOVE ROLE: { - "action": "create_channel", - "name": "channel-name", - "type": "text", - "category": "Category Name", - "locked_to_roles": ["Role1", "Role2"], - "reason": "Why this channel is needed" + "action": "remove_role", + "member": "@user or user_id", + "role": "Role Name", + "reason": "Why role is removed" } -3. ANNOUNCE: +12. LOCK CHANNEL: { - "action": "announce", + "action": "lock_channel", "channel": "channel-name", - "title": "Announcement Title", - "description": "Announcement content", - "color": "#00FFFF" + "reason": "Why channel is locked" } -4. CREATE GIVEAWAY: +13. UNLOCK CHANNEL: { - "action": "create_giveaway", - "prize": "Prize description", - "duration_minutes": 60, - "winners": 1, - "channel": "channel-name" + "action": "unlock_channel", + "channel": "channel-name", + "reason": "Why channel is unlocked" } -5. CREATE TOURNAMENT: +14. SLOWMODE: { - "action": "create_tournament", - "name": "Tournament Name", - "game": "Game name", - "max_participants": 16, - "channel": "channel-name" + "action": "set_slowmode", + "channel": "channel-name", + "seconds": 10, + "reason": "Why slowmode is set" } -6. CREATE POLL: +15. PURGE MESSAGES: { - "action": "create_poll", - "question": "Poll question", - "options": ["Option 1", "Option 2", "Option 3"], - "duration_minutes": 30 + "action": "purge_messages", + "channel": "channel-name", + "amount": 25, + "reason": "Why messages are purged" } -7. RUN COMMAND (for any other bot command): +16. DELETE CHANNEL: { - "action": "run_command", - "command": "command_name", - "args": {"arg1": "value1", "arg2": "value2"} + "action": "delete_channel", + "channel": "channel-name", + "reason": "Why channel is deleted" } -ALWAYS include a "response_to_user" field in the SAME language as the input. -Example: If input is Arabic, response_to_user must be in Arabic. - -IMPORTANT: For any request that involves creating events, giveaways, tournaments, polls, or other bot features, use the appropriate action above. If unsure, use run_command.""" +17. RENAME CHANNEL: +{ + "action": "rename_channel", + "channel": "old-channel-name", + "new_name": "new-channel-name", + "reason": "Why channel is renamed" +} - payload = { - "model": model, - "messages": [ - {"role": "system", "content": system_message}, - {"role": "user", "content": prompt} - ], - "temperature": 0.3, - "max_tokens": 2000 - } - - headers = { - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", - "HTTP-Referer": "https://github.com/mega-bot", - } - - try: - session = await self._get_session() - async with session.post( - "https://openrouter.ai/api/v1/chat/completions", - json=payload, - headers=headers - ) as resp: - if resp.status != 200: - return None - data = await resp.json() - - content = data.get("choices", [{}])[0].get("message", {}).get("content", "") - return self._parse_json_response(content) - except Exception: - return None - - def _parse_json_response(self, content: str) -> list[dict[str, Any]] | None: - try: - content = content.strip() - json_match = re.search(r'\[.*\]', content, re.DOTALL) - if json_match: - content = json_match.group(0) - return json.loads(content) - except Exception: - return None - - async def close(self) -> None: - if self._session and not self._session.closed: - await self._session.close() +18. CREATE CATEGORY: +{ + "action": "create_category", + "name": "Category Name", + "reason": "Why this category is needed" +} +19. RENAME CATEGORY: +{ + "action": "rename_category", + "category": "Old Category Name", + "new_name": "New Category Name", + "reason": "Why category is renamed" +} -class ExecutionEngine: - """Executes AI decisions with proper error handling.""" - - def __init__(self, bot: commands.Bot) -> None: - self.bot = bot - - async def execute(self, actions: list[dict[str, Any]], ctx: commands.Context) -> list[str]: - results = [] - for action in actions: - action_type = action.get("action", "") - try: - if action_type == "create_role": - result = await self._create_role(action, ctx) - elif action_type == "create_channel": - result = await self._create_channel(action, ctx) - elif action_type == "announce": - result = await self._announce(action, ctx) - elif action_type == "create_giveaway": - result = await self._create_giveaway(action, ctx) - elif action_type == "create_tournament": - result = await self._create_tournament(action, ctx) - elif action_type == "create_poll": - result = await self._create_poll(action, ctx) +20. DELETE CATEGORY: +{ + "action": "delete_category", + "category": "Category Name", + "reason": "Why category is deleted" +} + +ALWAYS include a "response_to_user" field in the SAME language as the input. +Example: If input is Arabic, response_to_user must be in Arabic. + +IMPORTANT: For any request that involves creating events, giveaways, tournaments, polls, or other bot features, use the appropriate action above. If unsure, use run_command.""" + + payload = { + "model": model, + "messages": [ + {"role": "system", "content": system_message}, + {"role": "user", "content": prompt} + ], + "temperature": 0.3, + "max_tokens": 2000 + } + + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "HTTP-Referer": "https://github.com/mega-bot", + } + + try: + session = await self._get_session() + async with session.post( + "https://openrouter.ai/api/v1/chat/completions", + json=payload, + headers=headers + ) as resp: + if resp.status != 200: + return None + data = await resp.json() + + content = data.get("choices", [{}])[0].get("message", {}).get("content", "") + return self._parse_json_response(content) + except Exception: + return None + + def _parse_json_response(self, content: str) -> list[dict[str, Any]] | None: + try: + content = content.strip() + json_match = re.search(r'\[.*\]', content, re.DOTALL) + if json_match: + content = json_match.group(0) + return json.loads(content) + except Exception: + return None + + async def close(self) -> None: + if self._session and not self._session.closed: + await self._session.close() + + +class ExecutionEngine: + """Executes AI decisions with proper error handling.""" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + async def execute(self, actions: list[dict[str, Any]], ctx: commands.Context) -> list[str]: + results = [] + for action in actions: + action_type = action.get("action", "") + try: + if action_type == "create_role": + result = await self._create_role(action, ctx) + elif action_type == "create_channel": + result = await self._create_channel(action, ctx) + elif action_type == "announce": + result = await self._announce(action, ctx) + elif action_type == "create_giveaway": + result = await self._create_giveaway(action, ctx) + elif action_type == "create_tournament": + result = await self._create_tournament(action, ctx) + elif action_type == "create_poll": + result = await self._create_poll(action, ctx) elif action_type == "run_command": result = await self._run_command(action, ctx) + elif action_type == "timeout_member": + result = await self._timeout_member(action, ctx) + elif action_type == "untimeout_member": + result = await self._untimeout_member(action, ctx) + elif action_type == "add_role": + result = await self._add_role(action, ctx) + elif action_type == "remove_role": + result = await self._remove_role(action, ctx) + elif action_type == "lock_channel": + result = await self._lock_channel(action, ctx) + elif action_type == "unlock_channel": + result = await self._unlock_channel(action, ctx) + elif action_type == "set_slowmode": + result = await self._set_slowmode(action, ctx) + elif action_type == "purge_messages": + result = await self._purge_messages(action, ctx) + elif action_type == "delete_channel": + result = await self._delete_channel(action, ctx) + elif action_type == "rename_channel": + result = await self._rename_channel(action, ctx) + elif action_type == "create_category": + result = await self._create_category(action, ctx) + elif action_type == "rename_category": + result = await self._rename_category(action, ctx) + elif action_type == "delete_category": + result = await self._delete_category(action, ctx) else: result = f"Unknown action: {action_type}" results.append(result) except Exception as e: results.append(f"Error executing {action_type}: {str(e)}") return results - - async def _create_role(self, action: dict[str, Any], ctx: commands.Context) -> str: - name = action.get("name", "New Role") - color_str = action.get("color", "#99AAB5") - hoist = action.get("hoist", False) - reason = action.get("reason", "AI Admin") - - try: - color = discord.Color(int(color_str.lstrip("#"), 16)) - except ValueError: - color = discord.Color.default() - - role = await ctx.guild.create_role(name=name, color=color, hoist=hoist, reason=reason) - return f"Created role: {role.mention}" - + + @staticmethod + def _resolve_member(guild: discord.Guild, ref: str | int | None) -> discord.Member | None: + if ref is None: + return None + text = str(ref).strip() + mention = re.search(r"<@!?(\d+)>", text) + if mention: + return guild.get_member(int(mention.group(1))) + if text.isdigit(): + return guild.get_member(int(text)) + return discord.utils.find(lambda m: m.name.lower() == text.lower() or m.display_name.lower() == text.lower(), guild.members) + + @staticmethod + def _resolve_channel(guild: discord.Guild, ref: str | None, fallback: discord.abc.GuildChannel | None = None) -> discord.TextChannel | None: + if ref: + text = str(ref).strip() + mention = re.search(r"<#(\d+)>", text) + if mention: + ch = guild.get_channel(int(mention.group(1))) + return ch if isinstance(ch, discord.TextChannel) else None + by_name = discord.utils.get(guild.text_channels, name=text) + if by_name: + return by_name + if isinstance(fallback, discord.TextChannel): + return fallback + return None + + @staticmethod + def _resolve_guild_channel( + guild: discord.Guild, + ref: str | int | None, + fallback: discord.abc.GuildChannel | None = None, + ) -> discord.abc.GuildChannel | None: + if ref is not None: + text = str(ref).strip() + mention = re.search(r"<#(\d+)>", text) + if mention: + return guild.get_channel(int(mention.group(1))) + if text.isdigit(): + return guild.get_channel(int(text)) + by_name = discord.utils.find(lambda c: c.name.lower() == text.lower(), guild.channels) + if by_name: + return by_name + if isinstance(fallback, discord.abc.GuildChannel): + return fallback + return None + + @staticmethod + def _resolve_category(guild: discord.Guild, ref: str | int | None) -> discord.CategoryChannel | None: + if ref is None: + return None + text = str(ref).strip() + mention = re.search(r"<#(\d+)>", text) + if mention: + ch = guild.get_channel(int(mention.group(1))) + return ch if isinstance(ch, discord.CategoryChannel) else None + if text.isdigit(): + ch = guild.get_channel(int(text)) + return ch if isinstance(ch, discord.CategoryChannel) else None + return discord.utils.find(lambda c: c.name.lower() == text.lower(), guild.categories) + + async def _create_role(self, action: dict[str, Any], ctx: commands.Context) -> str: + name = action.get("name", "New Role") + color_str = action.get("color", "#99AAB5") + hoist = action.get("hoist", False) + reason = action.get("reason", "AI Admin") + + try: + color = discord.Color(int(color_str.lstrip("#"), 16)) + except ValueError: + color = discord.Color.default() + + role = await ctx.guild.create_role(name=name, color=color, hoist=hoist, reason=reason) + return f"Created role: {role.mention}" + async def _create_channel(self, action: dict[str, Any], ctx: commands.Context) -> str: + if not ctx.author.guild_permissions.manage_channels: + return "Manage Channels permission required." name = action.get("name", "new-channel") channel_type = action.get("type", "text") - category_name = action.get("category") - locked_roles = action.get("locked_to_roles", []) - reason = action.get("reason", "AI Admin") - - overwrites = { - ctx.guild.default_role: discord.PermissionOverwrite(view_channel=False), - ctx.guild.me: discord.PermissionOverwrite(view_channel=True, send_messages=True, manage_channels=True), - } - - for role_name in locked_roles: - role = discord.utils.get(ctx.guild.roles, name=role_name) - if role: - overwrites[role] = discord.PermissionOverwrite(view_channel=True, send_messages=True) - - category = None - if category_name: - category = discord.utils.get(ctx.guild.categories, name=category_name) - - if channel_type == "text": - channel = await ctx.guild.create_text_channel( - name=name, category=category, overwrites=overwrites, reason=reason - ) - elif channel_type == "voice": - channel = await ctx.guild.create_voice_channel( - name=name, category=category, overwrites=overwrites, reason=reason - ) - else: - return f"Unknown channel type: {channel_type}" - - return f"Created channel: {channel.mention}" - - async def _announce(self, action: dict[str, Any], ctx: commands.Context) -> str: - channel_name = action.get("channel") - title = action.get("title", "Announcement") - description = action.get("description", "") - color_str = action.get("color", "#00FFFF") - - try: - color = discord.Color(int(color_str.lstrip("#"), 16)) - except ValueError: - color = discord.Color.blue() - - channel = None - if channel_name: - channel = discord.utils.get(ctx.guild.text_channels, name=channel_name) - - if not channel: - channel = ctx.channel - - embed = discord.Embed(title=title, description=description, color=color) - embed.timestamp = discord.utils.utcnow() - await channel.send(embed=embed) - return f"Announcement sent to {channel.mention}" - - async def _create_giveaway(self, action: dict[str, Any], ctx: commands.Context) -> str: - prize = action.get("prize", "Giveaway Prize") - duration = action.get("duration_minutes", 60) - winners = action.get("winners", 1) - channel_name = action.get("channel") - - channel = ctx.channel - if channel_name: - ch = discord.utils.get(ctx.guild.text_channels, name=channel_name) - if ch: - channel = ch - - cog = self.bot.get_cog("Community") - if not cog: - return "Community cog not found." - - await ctx.invoke(cog.giveaway, prize=prize, minutes=duration, winners=winners, channel=channel) - return f"Giveaway created: {prize}" - - async def _create_tournament(self, action: dict[str, Any], ctx: commands.Context) -> str: - name = action.get("name", "Tournament") - game = action.get("game", "Game") - max_participants = action.get("max_participants", 16) - channel_name = action.get("channel") - - channel = ctx.channel - if channel_name: - ch = discord.utils.get(ctx.guild.text_channels, name=channel_name) - if ch: - channel = ch - - cog = self.bot.get_cog("Engagement") - if not cog: - return "Engagement cog not found." - - await ctx.invoke(cog.tournament_create, name=name, game=game, max=max_participants, channel=channel) - return f"Tournament created: {name}" - - async def _create_poll(self, action: dict[str, Any], ctx: commands.Context) -> str: - question = action.get("question", "Poll") - options = action.get("options", ["Yes", "No"]) - duration = action.get("duration_minutes", 30) - - cog = self.bot.get_cog("Community") - if not cog: - return "Community cog not found." - - options_str = ",".join(options) - await ctx.invoke(cog.poll, question=question, options=options_str, duration=duration) - return f"Poll created: {question}" - - async def _run_command(self, action: dict[str, Any], ctx: commands.Context) -> str: - command_name = action.get("command", "") - args = action.get("args", {}) - - command = self.bot.get_command(command_name) - if not command: - return f"Command not found: {command_name}" - - try: - await ctx.invoke(command, **args) - return f"Command executed: {command_name}" - except Exception as e: - return f"Error running command: {str(e)}" - - async def _create_giveaway(self, action: dict[str, Any], ctx: commands.Context) -> str: - prize = action.get("prize", "Giveaway Prize") - duration = action.get("duration_minutes", 60) - winners = action.get("winners", 1) - channel_name = action.get("channel") - - channel = ctx.channel - if channel_name: - ch = discord.utils.get(ctx.guild.text_channels, name=channel_name) - if ch: - channel = ch - - cog = self.bot.get_cog("Community") - if not cog: - return "Community cog not found." - - await ctx.invoke(cog.giveaway, prize=prize, minutes=duration, winners=winners, channel=channel) - return f"Giveaway created: {prize}" - + category_name = action.get("category") + locked_roles = action.get("locked_to_roles", []) + reason = action.get("reason", "AI Admin") + + overwrites = { + ctx.guild.default_role: discord.PermissionOverwrite(view_channel=False), + ctx.guild.me: discord.PermissionOverwrite(view_channel=True, send_messages=True, manage_channels=True), + } + + for role_name in locked_roles: + role = discord.utils.get(ctx.guild.roles, name=role_name) + if role: + overwrites[role] = discord.PermissionOverwrite(view_channel=True, send_messages=True) + + category = None + if category_name: + category = discord.utils.get(ctx.guild.categories, name=category_name) + + if channel_type == "text": + channel = await ctx.guild.create_text_channel( + name=name, category=category, overwrites=overwrites, reason=reason + ) + elif channel_type == "voice": + channel = await ctx.guild.create_voice_channel( + name=name, category=category, overwrites=overwrites, reason=reason + ) + else: + return f"Unknown channel type: {channel_type}" + + return f"Created channel: {channel.mention}" + + async def _announce(self, action: dict[str, Any], ctx: commands.Context) -> str: + channel_name = action.get("channel") + title = action.get("title", "Announcement") + description = action.get("description", "") + color_str = action.get("color", "#00FFFF") + + try: + color = discord.Color(int(color_str.lstrip("#"), 16)) + except ValueError: + color = discord.Color.blue() + + channel = None + if channel_name: + channel = discord.utils.get(ctx.guild.text_channels, name=channel_name) + + if not channel: + channel = ctx.channel + + embed = discord.Embed(title=title, description=description, color=color) + embed.timestamp = discord.utils.utcnow() + await channel.send(embed=embed) + return f"Announcement sent to {channel.mention}" + + async def _create_giveaway(self, action: dict[str, Any], ctx: commands.Context) -> str: + prize = action.get("prize", "Giveaway Prize") + duration = action.get("duration_minutes", 60) + winners = action.get("winners", 1) + channel_name = action.get("channel") + + channel = ctx.channel + if channel_name: + ch = discord.utils.get(ctx.guild.text_channels, name=channel_name) + if ch: + channel = ch + + cog = self.bot.get_cog("Community") + if not cog: + return "Community cog not found." + + await cog.giveaway_create(ctx, int(duration), int(winners), prize=prize) + return f"Giveaway created: {prize}" + async def _create_tournament(self, action: dict[str, Any], ctx: commands.Context) -> str: name = action.get("name", "Tournament") game = action.get("game", "Game") max_participants = action.get("max_participants", 16) - channel_name = action.get("channel") - - channel = ctx.channel - if channel_name: - ch = discord.utils.get(ctx.guild.text_channels, name=channel_name) - if ch: - channel = ch cog = self.bot.get_cog("Engagement") if not cog: return "Engagement cog not found." - - await ctx.invoke(cog.tournament_create, name=name, game=game, max=max_participants, channel=channel) + + # Engagement exposes tournament creation via the base `/tournament` command. + games = f"{game}" if game else "chess, checkers, connect4, othello" + await ctx.invoke(cog.tournament, name=name, games=games) return f"Tournament created: {name}" async def _create_poll(self, action: dict[str, Any], ctx: commands.Context) -> str: question = action.get("question", "Poll") options = action.get("options", ["Yes", "No"]) - duration = action.get("duration_minutes", 30) - cog = self.bot.get_cog("Community") + cog = self.bot.get_cog("Utility") if not cog: - return "Community cog not found." + return "Utility cog not found." - options_str = ",".join(options) - await ctx.invoke(cog.poll, question=question, options=options_str, duration=duration) + # Utility poll command accepts options as a "|" separated string. + options_str = "|".join(str(o) for o in options) + await ctx.invoke(cog.poll, question=question, options=options_str) return f"Poll created: {question}" - + async def _run_command(self, action: dict[str, Any], ctx: commands.Context) -> str: command_name = action.get("command", "") args = action.get("args", {}) - - command = self.bot.get_command(command_name) - if not command: - return f"Command not found: {command_name}" - + + command = self.bot.get_command(command_name) + if not command: + return f"Command not found: {command_name}" + try: await ctx.invoke(command, **args) return f"Command executed: {command_name}" except Exception as e: return f"Error running command: {str(e)}" + async def _timeout_member(self, action: dict[str, Any], ctx: commands.Context) -> str: + member = self._resolve_member(ctx.guild, action.get("member")) + if not member: + return "Member not found." + minutes = max(1, min(int(action.get("minutes", 10)), 40320)) + reason = action.get("reason", "AI Admin timeout") + until = discord.utils.utcnow() + dt.timedelta(minutes=minutes) + await member.timeout(until, reason=reason) + return f"Timed out {member.mention} for {minutes} minute(s)." + + async def _untimeout_member(self, action: dict[str, Any], ctx: commands.Context) -> str: + member = self._resolve_member(ctx.guild, action.get("member")) + if not member: + return "Member not found." + reason = action.get("reason", "AI Admin untimeout") + await member.timeout(None, reason=reason) + return f"Removed timeout from {member.mention}." + + async def _add_role(self, action: dict[str, Any], ctx: commands.Context) -> str: + member = self._resolve_member(ctx.guild, action.get("member")) + if not member: + return "Member not found." + role_name = str(action.get("role", "")).strip() + role = discord.utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles) + if not role: + return f"Role not found: {role_name}" + reason = action.get("reason", "AI Admin add role") + await member.add_roles(role, reason=reason) + return f"Added role **{role.name}** to {member.mention}." + + async def _remove_role(self, action: dict[str, Any], ctx: commands.Context) -> str: + member = self._resolve_member(ctx.guild, action.get("member")) + if not member: + return "Member not found." + role_name = str(action.get("role", "")).strip() + role = discord.utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles) + if not role: + return f"Role not found: {role_name}" + reason = action.get("reason", "AI Admin remove role") + await member.remove_roles(role, reason=reason) + return f"Removed role **{role.name}** from {member.mention}." + + async def _lock_channel(self, action: dict[str, Any], ctx: commands.Context) -> str: + if not ctx.author.guild_permissions.manage_channels: + return "Manage Channels permission required." + channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel) + if not channel: + return "Channel not found." + reason = action.get("reason", "AI Admin lock channel") + await channel.set_permissions(ctx.guild.default_role, send_messages=False, reason=reason) + return f"Locked channel {channel.mention}." + + async def _unlock_channel(self, action: dict[str, Any], ctx: commands.Context) -> str: + if not ctx.author.guild_permissions.manage_channels: + return "Manage Channels permission required." + channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel) + if not channel: + return "Channel not found." + reason = action.get("reason", "AI Admin unlock channel") + await channel.set_permissions(ctx.guild.default_role, send_messages=True, reason=reason) + return f"Unlocked channel {channel.mention}." + + async def _set_slowmode(self, action: dict[str, Any], ctx: commands.Context) -> str: + if not ctx.author.guild_permissions.manage_channels: + return "Manage Channels permission required." + channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel) + if not channel: + return "Channel not found." + seconds = max(0, min(int(action.get("seconds", 0)), 21600)) + reason = action.get("reason", "AI Admin slowmode") + await channel.edit(slowmode_delay=seconds, reason=reason) + return f"Set slowmode in {channel.mention} to {seconds}s." + + async def _purge_messages(self, action: dict[str, Any], ctx: commands.Context) -> str: + if not ctx.author.guild_permissions.manage_messages: + return "Manage Messages permission required." + channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel) + if not channel: + return "Channel not found." + amount = max(1, min(int(action.get("amount", 10)), 200)) + deleted = await channel.purge(limit=amount) + return f"Purged {len(deleted)} message(s) in {channel.mention}." + async def _delete_channel(self, action: dict[str, Any], ctx: commands.Context) -> str: + if not ctx.author.guild_permissions.manage_channels: + return "Manage Channels permission required." + channel = self._resolve_guild_channel(ctx.guild, action.get("channel"), None) + if not channel: + return "Channel not found." + if ctx.channel and channel.id == ctx.channel.id: + return "Refusing to delete the channel currently being used for command execution." + reason = action.get("reason", "AI Admin delete channel") + name = channel.name + await channel.delete(reason=reason) + return f"Deleted channel #{name}." + + async def _rename_channel(self, action: dict[str, Any], ctx: commands.Context) -> str: + if not ctx.author.guild_permissions.manage_channels: + return "Manage Channels permission required." + channel = self._resolve_guild_channel(ctx.guild, action.get("channel"), None) + if not channel: + return "Channel not found." + new_name = str(action.get("new_name", "")).strip() + if not new_name: + return "New channel name is required." + reason = action.get("reason", "AI Admin rename channel") + old_name = channel.name + await channel.edit(name=new_name[:100], reason=reason) + return f"Renamed channel **{old_name}** -> **{new_name[:100]}**." + + async def _create_category(self, action: dict[str, Any], ctx: commands.Context) -> str: + if not ctx.author.guild_permissions.manage_channels: + return "Manage Channels permission required." + name = str(action.get("name", "new-category")).strip()[:100] + if not name: + return "Category name is required." + reason = action.get("reason", "AI Admin create category") + category = await ctx.guild.create_category(name=name, reason=reason) + return f"Created category: **{category.name}**." + + async def _rename_category(self, action: dict[str, Any], ctx: commands.Context) -> str: + if not ctx.author.guild_permissions.manage_channels: + return "Manage Channels permission required." + category = self._resolve_category(ctx.guild, action.get("category")) + if not category: + return "Category not found." + new_name = str(action.get("new_name", "")).strip()[:100] + if not new_name: + return "New category name is required." + reason = action.get("reason", "AI Admin rename category") + old_name = category.name + await category.edit(name=new_name, reason=reason) + return f"Renamed category **{old_name}** -> **{new_name}**." + + async def _delete_category(self, action: dict[str, Any], ctx: commands.Context) -> str: + if not ctx.author.guild_permissions.manage_channels: + return "Manage Channels permission required." + category = self._resolve_category(ctx.guild, action.get("category")) + if not category: + return "Category not found." + reason = action.get("reason", "AI Admin delete category") + name = category.name + channels_inside = len(category.channels) + await category.delete(reason=reason) + return f"Deleted category **{name}** (had {channels_inside} channel(s))." + class AIAdmin(commands.Cog): - """Autonomous AI Administrator Cog.""" - + """Autonomous AI Administrator Cog.""" + def __init__(self, bot: commands.Bot) -> None: self.bot = bot self.permission_guard = PermissionGuard() self.intelligence = IntelligenceLayer(bot) self.execution = ExecutionEngine(bot) - - async def cog_unload(self) -> None: - await self.intelligence.close() - + + @staticmethod + def _parse_duration_minutes(text: str) -> int | None: + match = re.search(r"(\d+)\s*(m|min|mins|minute|minutes|h|hr|hour|hours|d|day|days)?", text, re.IGNORECASE) + if not match: + return None + value = int(match.group(1)) + unit = (match.group(2) or "m").lower() + if unit.startswith("h"): + value *= 60 + elif unit.startswith("d"): + value *= 60 * 24 + return max(1, min(value, 40320)) + + async def _try_direct_moderation(self, ctx: commands.Context, request: str) -> str | None: + if not ctx.guild: + return "Server only." + text = (request or "").strip() + lower = text.lower() + + target = None + if ctx.message and ctx.message.mentions: + target = ctx.message.mentions[0] + else: + match = re.search(r"<@!?(\d+)>", text) + if match: + target = ctx.guild.get_member(int(match.group(1))) + + if lower.startswith("kick ") and target: + if not ctx.author.guild_permissions.kick_members: + return "You need Kick Members permission." + await target.kick(reason=f"AI Admin by {ctx.author}") + return f"OK: Kicked {target.mention}" + + if lower.startswith("ban ") and target: + if not ctx.author.guild_permissions.ban_members: + return "You need Ban Members permission." + await target.ban(reason=f"AI Admin by {ctx.author}", delete_message_days=0) + return f"OK: Banned {target.mention}" + + if (lower.startswith("mute ") or lower.startswith("timeout ")) and target: + if not ctx.author.guild_permissions.moderate_members: + return "You need Moderate Members permission." + minutes = self._parse_duration_minutes(text) or 10 + until = discord.utils.utcnow() + dt.timedelta(minutes=minutes) + await target.timeout(until, reason=f"AI Admin by {ctx.author}") + return f"OK: Timed out {target.mention} for {minutes} minute(s)" + + if (lower.startswith("unmute ") or lower.startswith("untimeout ")) and target: + if not ctx.author.guild_permissions.moderate_members: + return "You need Moderate Members permission." + await target.timeout(None, reason=f"AI Admin by {ctx.author}") + return f"OK: Removed timeout from {target.mention}" + + role_match = re.search(r"(?:give|add)\s+role\s+(.+?)\s+(?:to|for)\s+<@!?(\d+)>", text, re.IGNORECASE) + if role_match: + role_name = role_match.group(1).strip(" \"'") + member = ctx.guild.get_member(int(role_match.group(2))) + if not member: + return "Target member not found." + role = discord.utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles) + if not role: + return f"Role not found: {role_name}" + if not ctx.author.guild_permissions.manage_roles: + return "You need Manage Roles permission." + await member.add_roles(role, reason=f"AI Admin by {ctx.author}") + return f"OK: Added role **{role.name}** to {member.mention}" + + remove_role_match = re.search(r"(?:remove)\s+role\s+(.+?)\s+(?:from)\s+<@!?(\d+)>", text, re.IGNORECASE) + if remove_role_match: + role_name = remove_role_match.group(1).strip(" \"'") + member = ctx.guild.get_member(int(remove_role_match.group(2))) + if not member: + return "Target member not found." + role = discord.utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles) + if not role: + return f"Role not found: {role_name}" + if not ctx.author.guild_permissions.manage_roles: + return "You need Manage Roles permission." + await member.remove_roles(role, reason=f"AI Admin by {ctx.author}") + return f"OK: Removed role **{role.name}** from {member.mention}" + + lock_match = re.search(r"^lock(?:\s+<#(\d+)>)?", lower) + if lock_match: + if not ctx.author.guild_permissions.manage_channels: + return "You need Manage Channels permission." + channel = ctx.guild.get_channel(int(lock_match.group(1))) if lock_match.group(1) else ctx.channel + if isinstance(channel, discord.TextChannel): + await channel.set_permissions(ctx.guild.default_role, send_messages=False, reason=f"AI Admin by {ctx.author}") + return f"OK: Locked {channel.mention}" + + unlock_match = re.search(r"^unlock(?:\s+<#(\d+)>)?", lower) + if unlock_match: + if not ctx.author.guild_permissions.manage_channels: + return "You need Manage Channels permission." + channel = ctx.guild.get_channel(int(unlock_match.group(1))) if unlock_match.group(1) else ctx.channel + if isinstance(channel, discord.TextChannel): + await channel.set_permissions(ctx.guild.default_role, send_messages=True, reason=f"AI Admin by {ctx.author}") + return f"OK: Unlocked {channel.mention}" + + rename_channel_match = re.search( + r"(?:rename)\s+channel\s+(.+?)\s+(?:to|->)\s+(.+)$", + text, + re.IGNORECASE, + ) + if rename_channel_match: + if not ctx.author.guild_permissions.manage_channels: + return "You need Manage Channels permission." + old_ref = rename_channel_match.group(1).strip(" \"'") + new_name = rename_channel_match.group(2).strip(" \"'") + channel = ExecutionEngine._resolve_guild_channel(ctx.guild, old_ref) + if not channel: + return f"Channel not found: {old_ref}" + if not new_name: + return "New channel name is required." + old_name = channel.name + await channel.edit(name=new_name[:100], reason=f"AI Admin by {ctx.author}") + return f"OK: Renamed channel **{old_name}** -> **{new_name[:100]}**" + + delete_channel_match = re.search(r"(?:delete|remove)\s+channel\s+(.+)$", text, re.IGNORECASE) + if delete_channel_match: + if not ctx.author.guild_permissions.manage_channels: + return "You need Manage Channels permission." + ref = delete_channel_match.group(1).strip(" \"'") + channel = ExecutionEngine._resolve_guild_channel(ctx.guild, ref) + if not channel: + return f"Channel not found: {ref}" + if ctx.channel and channel.id == ctx.channel.id: + return "Refusing to delete the channel currently being used for command execution." + name = channel.name + await channel.delete(reason=f"AI Admin by {ctx.author}") + return f"OK: Deleted channel **{name}**" + + rename_category_match = re.search( + r"(?:rename)\s+(?:category|directory)\s+(.+?)\s+(?:to|->)\s+(.+)$", + text, + re.IGNORECASE, + ) + if rename_category_match: + if not ctx.author.guild_permissions.manage_channels: + return "You need Manage Channels permission." + old_ref = rename_category_match.group(1).strip(" \"'") + new_name = rename_category_match.group(2).strip(" \"'") + category = ExecutionEngine._resolve_category(ctx.guild, old_ref) + if not category: + return f"Category not found: {old_ref}" + if not new_name: + return "New category name is required." + old_name = category.name + await category.edit(name=new_name[:100], reason=f"AI Admin by {ctx.author}") + return f"OK: Renamed category **{old_name}** -> **{new_name[:100]}**" + + delete_category_match = re.search(r"(?:delete|remove)\s+(?:category|directory)\s+(.+)$", text, re.IGNORECASE) + if delete_category_match: + if not ctx.author.guild_permissions.manage_channels: + return "You need Manage Channels permission." + ref = delete_category_match.group(1).strip(" \"'") + category = ExecutionEngine._resolve_category(ctx.guild, ref) + if not category: + return f"Category not found: {ref}" + name = category.name + channels_inside = len(category.channels) + await category.delete(reason=f"AI Admin by {ctx.author}") + return f"OK: Deleted category **{name}** (had {channels_inside} channel(s))" + + return None + + async def cog_unload(self) -> None: + await self.intelligence.close() + @commands.hybrid_command(name="ai_admin", description="Let AI administrate the server") async def ai_admin(self, ctx: commands.Context, *, request: str) -> None: - if not self.permission_guard.check(ctx)[0]: - await ctx.send(self.permission_guard.check(ctx)[1], ephemeral=True) - return - - await ctx.defer() - - actions = await self.intelligence.ask_ai(request) - if not actions: - await ctx.send("AI failed to generate actions. Try again.", ephemeral=True) + allowed, deny_reason = self.permission_guard.check(ctx) + if not allowed: + await ctx.send(deny_reason, ephemeral=True) return - - response_text = None - for action in actions: - if "response_to_user" in action: - response_text = action.pop("response_to_user") - break - - results = await self.execution.execute(actions, ctx) - - if response_text: - await ctx.send(response_text) - else: - await ctx.send("\n".join(results)) - - @commands.hybrid_command(name="ai_help", description="Show AI Admin capabilities") - async def ai_help(self, ctx: commands.Context) -> None: - embed = discord.Embed( - title="AI Administrator Help", - description=( - "I can help manage your server using AI. Examples:\n\n" - "**Create a role:**\n" - "`/ai_admin create a moderator role with purple color`\n\n" - "**Create a channel:**\n" - "`/ai_admin make a private channel for staff`\n\n" - "**Create a giveaway:**\n" - "`/ai_admin create a giveaway for Discord Nitro 24 hours`\n\n" - "**Create a tournament:**\n" - "`/ai_admin setup a Valorant tournament for 16 players`\n\n" - "**Create a poll:**\n" - "`/ai_admin make a poll about server events`\n\n" + + direct_result = await self._try_direct_moderation(ctx, request) + if direct_result is not None: + await ctx.send(direct_result, ephemeral=True) + return + + if ctx.interaction and not ctx.interaction.response.is_done(): + try: + await ctx.defer() + except discord.InteractionResponded: + pass + + actions = await self.intelligence.ask_ai(request) + if not actions: + await ctx.send("AI failed to generate actions. Try again.", ephemeral=True) + return + + response_text = None + for action in actions: + if "response_to_user" in action: + response_text = action.pop("response_to_user") + break + + results = await self.execution.execute(actions, ctx) + + if response_text: + await ctx.send(response_text) + else: + await ctx.send("\n".join(results)) + + @commands.hybrid_command(name="ai_help", description="Show AI Admin capabilities") + async def ai_help(self, ctx: commands.Context) -> None: + embed = discord.Embed( + title="AI Administrator Help", + description=( + "I can help manage your server using AI. Examples:\n\n" + "**Create a role:**\n" + "`/ai_admin create a moderator role with purple color`\n\n" + "**Create a channel:**\n" + "`/ai_admin make a private channel for staff`\n\n" + "**Create a giveaway:**\n" + "`/ai_admin create a giveaway for Discord Nitro 24 hours`\n\n" + "**Create a tournament:**\n" + "`/ai_admin setup a Valorant tournament for 16 players`\n\n" + "**Create a poll:**\n" + "`/ai_admin make a poll about server events`\n\n" "**Announce:**\n" - "`/ai_admin announce server maintenance in 1 hour`" + "`/ai_admin announce server maintenance in 1 hour`\n\n" + "**Direct moderation shortcuts:**\n" + "`/ai_admin kick @user`\n" + "`/ai_admin ban @user`\n" + "`/ai_admin mute @user 30m`\n" + "`/ai_admin unmute @user`\n" + "`/ai_admin add role VIP to @user`\n" + "`/ai_admin remove role VIP from @user`\n" + "`/ai_admin lock #channel`\n" + "`/ai_admin unlock #channel`\n" + "`/ai_admin rename channel old-name to new-name`\n" + "`/ai_admin delete channel old-name`\n" + "`/ai_admin rename category Staff to Team-Staff`\n" + "`/ai_admin delete category Team-Staff`" ), color=discord.Color.blue() ) - await ctx.send(embed=embed, ephemeral=True) - - -async def setup(bot: commands.Bot) -> None: - await bot.add_cog(AIAdmin(bot)) + await ctx.send(embed=embed, ephemeral=True) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(AIAdmin(bot)) diff --git a/bot/cogs/ai_suite.py b/bot/cogs/ai_suite.py index 71595784f0d9ecdf1c1f2e1c7bf6d4b54bd36f95..07d934423f8c7d49285a702121ef3818e86c9b07 100644 --- a/bot/cogs/ai_suite.py +++ b/bot/cogs/ai_suite.py @@ -1354,12 +1354,11 @@ class AISuite(commands.Cog): # Fallback mappings for panel bootstrapping when planner is unavailable. lowered = text.lower() if any(k in lowered for k in ("اقتراح", "suggestion panel", "suggest panel", "suggestions panel")): - if ctx: - cmd = self.bot.get_command("suggestion_panel") - if cmd: - await ctx.invoke(cmd) - return "✅ تم نشر لوحة الاقتراحات." if guild_lang == "ar" else "✅ Suggestion panel requested." - return "استخدم `/suggestion_panel` لنشر لوحة الاقتراحات التفاعلية." if guild_lang == "ar" else "Use `/suggestion_panel` to deploy the interactive suggestions UI." + return ( + "نظام الاقتراحات يعمل عبر قناة مخصصة. استخدم `/setsuggestionchannel #channel` ثم أرسل اقتراحك هناك." + if guild_lang == "ar" + else "Suggestions use a dedicated channel. Run `/setsuggestionchannel #channel` then post suggestions there." + ) if any(k in lowered for k in ("تذكرة", "tickets", "ticket panel", "دعم")): if ctx: cmd = self.bot.get_command("ticket_panel") diff --git a/bot/cogs/banner_manager.py b/bot/cogs/banner_manager.py index eb07347341522aa797dcc0c784f6105152e9ead4..635190e7d363774d3fb68d5666f9cc5cd025cd8f 100644 --- a/bot/cogs/banner_manager.py +++ b/bot/cogs/banner_manager.py @@ -4,6 +4,9 @@ Server Banner Management Cog - Allows servers to set custom banners from __future__ import annotations +import sqlite3 +from urllib.parse import urlparse + import discord from discord.ext import commands from discord import app_commands @@ -26,15 +29,19 @@ class SetBannerModal(discord.ui.Modal, title="🖼️ Set Server Banner"): self.cog = cog async def on_submit(self, interaction: discord.Interaction) -> None: - url = self.banner_url.value.strip() + url = self.banner_url.value.strip().strip("<>") # Validate URL - if not url.startswith(("http://", "https://")): - await interaction.response.send_message("❌ Invalid URL. Must start with http:// or https://", ephemeral=True) + if not self.cog._looks_like_image_url(url): + await interaction.response.send_message( + "Invalid image URL. Use a public http(s) link ending with png/jpg/jpeg/webp/gif.", + ephemeral=True, + ) return guild_id = interaction.guild.id if interaction.guild else 0 - + + await self.cog._ensure_schema() # Save to database await self.cog.bot.db.execute( "INSERT INTO guild_config(guild_id, custom_banner_url) VALUES (?, ?) " @@ -56,6 +63,30 @@ class SetBannerModal(discord.ui.Modal, title="🖼️ Set Server Banner"): class BannerManager(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot + self._schema_checked = False + + async def _ensure_schema(self) -> None: + if self._schema_checked: + return + try: + await self.bot.db.execute( + "ALTER TABLE guild_config ADD COLUMN custom_banner_url TEXT" + ) + except sqlite3.OperationalError as exc: + if "duplicate column name" not in str(exc).lower(): + raise + self._schema_checked = True + + @staticmethod + def _looks_like_image_url(url: str) -> bool: + try: + parsed = urlparse(url) + except Exception: + return False + if parsed.scheme not in {"http", "https"} or not parsed.netloc: + return False + path = (parsed.path or "").lower() + return path.endswith((".png", ".jpg", ".jpeg", ".webp", ".gif")) @app_commands.command(name="set_banner", description="Set a custom banner for this server") @app_commands.checks.has_permissions(manage_guild=True) @@ -65,6 +96,7 @@ class BannerManager(commands.Cog): await interaction.response.send_message("❌ This command can only be used in a server.", ephemeral=True) return + await self._ensure_schema() await interaction.response.send_modal(SetBannerModal(self)) @app_commands.command(name="remove_banner", description="Remove the custom banner from this server") @@ -75,6 +107,7 @@ class BannerManager(commands.Cog): await interaction.response.send_message("❌ This command can only be used in a server.", ephemeral=True) return + await self._ensure_schema() guild_id = interaction.guild.id await self.bot.db.execute( @@ -97,6 +130,7 @@ class BannerManager(commands.Cog): await interaction.response.send_message("❌ This command can only be used in a server.", ephemeral=True) return + await self._ensure_schema() guild_id = interaction.guild.id row = await self.bot.db.fetchone( diff --git a/bot/cogs/board_games.py b/bot/cogs/board_games.py index 768d63aa9e7d6b1412ea5dec230210786395ce98..598580bdf3d028bc4793dec120750f9aee38682b 100644 --- a/bot/cogs/board_games.py +++ b/bot/cogs/board_games.py @@ -808,7 +808,7 @@ class BoardGames(commands.Cog): @commands.hybrid_command(name="games_panel", hidden=True, description="Deprecated: use /gamehub", with_app_command=False) async def games_panel(self, ctx: commands.Context) -> None: - await ctx.reply("🟢 This panel is deprecated. Use `/gamehub` for the improved game experience.") + await ctx.reply("<:animatedarrowgreen:1477261279428087979> This panel is deprecated. Use `/gamehub` for the improved game experience.") @commands.hybrid_command(name="board_forfeit", hidden=True, description="Forfeit active board game", with_app_command=False) async def board_forfeit(self, ctx: commands.Context) -> None: diff --git a/bot/cogs/community.py b/bot/cogs/community.py index 2e4334e4b16ff0c3bc0f043550a0c68125949640..c4255828acba62a7442bb7ae5a6780b56ca36855 100644 --- a/bot/cogs/community.py +++ b/bot/cogs/community.py @@ -474,7 +474,7 @@ class QuickOptionsView(discord.ui.View): @discord.ui.button(label="Priority", style=discord.ButtonStyle.primary, row=2) async def priority(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: - self.parent.options = ["🔴 High", "🟡 Medium", "🟢 Low"] + self.parent.options = ["🔴 High", "<:animatedarrowyellow:1477261257592668271> Medium", "<:animatedarrowgreen:1477261279428087979> Low"] await self._apply_options(interaction) async def _apply_options(self, interaction: discord.Interaction) -> None: @@ -933,9 +933,9 @@ class Community(commands.Cog): await msg.edit(embed=embed, view=None) if winner_id: await channel.send(f"🎉 Giveaway **#{giveaway_id}** winner: <@{winner_id}> • Prize: **{prize}**") - log_channel_id = await self.bot.db.fetchone("SELECT log_channel_id FROM guild_config WHERE guild_id = ?", ctx.guild.id) + log_channel_id = await self.bot.db.fetchone("SELECT log_channel_id FROM guild_config WHERE guild_id = ?", guild.id) if log_channel_id and log_channel_id[0]: - log_channel = ctx.guild.get_channel(log_channel_id[0]) + log_channel = guild.get_channel(log_channel_id[0]) if log_channel: log_embed = discord.Embed( title="🎁 Giveaway Ended", @@ -946,8 +946,6 @@ class Community(commands.Cog): log_embed.add_field(name="Prize", value=prize, inline=True) log_embed.add_field(name="Giveaway ID", value=str(giveaway_id), inline=True) await log_channel.send(embed=log_embed) - else: - await ctx.reply(f"Winner: <@{winner_id}>") @commands.hybrid_command(name="setsuggestionchannel", description="Set suggestion channel") @commands.has_permissions(manage_guild=True) @@ -1007,6 +1005,134 @@ class Community(commands.Cog): ) await ctx.reply(embed=embed, view=view) + @commands.hybrid_group(name="giveaway", description="Giveaway commands") + @commands.has_permissions(manage_guild=True) + async def giveaway_group(self, ctx: commands.Context) -> None: + if ctx.invoked_subcommand is None: + await ctx.reply("Use: `/giveaway start`, `/giveaway end`, `/giveaway reroll`.") + + async def giveaway_create(self, ctx_or_interaction: commands.Context | discord.Interaction, minutes: int, winner_count: int, *, prize: str) -> None: + guild = ctx_or_interaction.guild + channel = ctx_or_interaction.channel + if not guild or not isinstance(channel, discord.TextChannel): + if isinstance(ctx_or_interaction, commands.Context): + await ctx_or_interaction.reply("Server text channel only.") + else: + await ctx_or_interaction.followup.send("Server text channel only.", ephemeral=True) + return + end_time = int(time.time()) + max(1, minutes) * 60 + view = GiveawayJoinView(self, guild.id) + view.winners_count = max(1, winner_count) + embed = view.build_embed(prize=prize, end_time=end_time, winners=view.winners_count, entrants=0, ended=False) + msg = await channel.send(embed=embed, view=view) + await self.bot.db.execute( + "INSERT INTO giveaways(guild_id, channel_id, message_id, prize, end_time, winner_id, ended) VALUES (?, ?, ?, ?, ?, NULL, 0)", + guild.id, + channel.id, + msg.id, + prize[:250], + end_time, + ) + row = await self.bot.db.fetchone("SELECT id FROM giveaways WHERE message_id = ?", msg.id) + view.giveaway_id = int(row[0]) if row else 0 + if view.giveaway_id: + await msg.edit(embed=view.build_embed(prize=prize, end_time=end_time, winners=view.winners_count, entrants=0, ended=False), view=view) + response = f"✅ Giveaway created (ID: `{view.giveaway_id}`) • ends " + if isinstance(ctx_or_interaction, commands.Context): + await ctx_or_interaction.reply(response) + else: + await ctx_or_interaction.followup.send(response, ephemeral=True) + + async def giveaway_end(self, ctx_or_interaction: commands.Context | discord.Interaction, giveaway_id: int) -> None: + row = await self.bot.db.fetchone( + "SELECT guild_id, channel_id, message_id, prize, ended FROM giveaways WHERE id = ?", + giveaway_id, + ) + if not row: + msg = "Giveaway not found." + if isinstance(ctx_or_interaction, commands.Context): + await ctx_or_interaction.reply(msg) + else: + await ctx_or_interaction.followup.send(msg, ephemeral=True) + return + guild_id, channel_id, message_id, prize, ended = row + if int(ended or 0) == 1: + msg = "Giveaway already ended." + if isinstance(ctx_or_interaction, commands.Context): + await ctx_or_interaction.reply(msg) + else: + await ctx_or_interaction.followup.send(msg, ephemeral=True) + return + entries = await self.bot.db.fetchall("SELECT user_id FROM giveaway_entries WHERE giveaway_id = ?", giveaway_id) + users = [uid for (uid,) in entries] + winner_id = random.choice(users) if users else None + await self.bot.db.execute("UPDATE giveaways SET ended = 1, winner_id = ? WHERE id = ?", winner_id, giveaway_id) + guild = self.bot.get_guild(int(guild_id)) + channel = guild.get_channel(int(channel_id)) if guild else None + if isinstance(channel, discord.TextChannel): + try: + msg_obj = await channel.fetch_message(int(message_id)) + embed = msg_obj.embeds[0] if msg_obj.embeds else discord.Embed(title="🎁 Giveaway") + embed.color = NEON_ORANGE + embed.add_field(name="Winner", value=(f"<@{winner_id}>" if winner_id else "No entrants"), inline=False) + await msg_obj.edit(embed=embed, view=None) + except Exception: + pass + if winner_id: + await channel.send(f"🎉 Giveaway **#{giveaway_id}** winner: <@{winner_id}> • Prize: **{prize}**") + else: + await channel.send(f"🎉 Giveaway **#{giveaway_id}** ended with no entrants.") + reply = f"✅ Giveaway `{giveaway_id}` ended." + if isinstance(ctx_or_interaction, commands.Context): + await ctx_or_interaction.reply(reply) + else: + await ctx_or_interaction.followup.send(reply, ephemeral=True) + + @giveaway_group.command(name="start", description="Start a giveaway in current channel") + async def giveaway_start(self, ctx: commands.Context, minutes: int, winners: int, *, prize: str) -> None: + await self.giveaway_create(ctx, minutes, winners, prize=prize) + + @giveaway_group.command(name="end", description="End a giveaway by ID") + async def giveaway_end_cmd(self, ctx: commands.Context, giveaway_id: int) -> None: + await self.giveaway_end(ctx, giveaway_id) + + @giveaway_group.command(name="reroll", description="Reroll winner for ended giveaway") + async def giveaway_reroll(self, ctx: commands.Context, giveaway_id: int) -> None: + row = await self.bot.db.fetchone("SELECT ended FROM giveaways WHERE id = ?", giveaway_id) + if not row or int(row[0] or 0) != 1: + await ctx.reply("Giveaway must be ended before reroll.") + return + await self.bot.db.execute("UPDATE giveaways SET ended = 0, winner_id = NULL WHERE id = ?", giveaway_id) + await self.giveaway_end(ctx, giveaway_id) + + @commands.hybrid_group(name="ticket", description="Ticket commands") + async def ticket_group(self, ctx: commands.Context) -> None: + if ctx.invoked_subcommand is None: + await ctx.reply("Use: `/ticket close` or `/ticket delete` in a ticket channel.") + + @ticket_group.command(name="close", description="Close current ticket channel") + async def ticket_close_cmd(self, ctx: commands.Context) -> None: + channel = ctx.channel + if not isinstance(channel, discord.TextChannel) or not channel.name.startswith("ticket-"): + await ctx.reply("This is not a ticket channel.") + return + await self.bot.db.execute("UPDATE tickets SET status = 'closed' WHERE channel_id = ?", channel.id) + await ctx.reply("🔒 Closing ticket in 5 seconds...") + await asyncio.sleep(5) + await channel.delete(reason=f"Closed by {ctx.author}") + + @ticket_group.command(name="delete", description="Delete current ticket channel") + @commands.has_permissions(manage_channels=True) + async def ticket_delete_cmd(self, ctx: commands.Context) -> None: + channel = ctx.channel + if not isinstance(channel, discord.TextChannel) or not channel.name.startswith("ticket-"): + await ctx.reply("This is not a ticket channel.") + return + await self.bot.db.execute("DELETE FROM tickets WHERE channel_id = ?", channel.id) + await ctx.reply("🗑️ Deleting ticket in 3 seconds...") + await asyncio.sleep(3) + await channel.delete(reason=f"Deleted by {ctx.author}") + # ═══════════════════════════════════════════════════════════════════════════════ # GLOBAL TICKETS + GIVEAWAYS diff --git a/bot/cogs/configuration.py b/bot/cogs/configuration.py index 2758a053970d445e7a158e469396af8cd78368a8..cea0b83d94d87b952994c5f3b878896716ba2634 100644 --- a/bot/cogs/configuration.py +++ b/bot/cogs/configuration.py @@ -273,15 +273,15 @@ class FreeGameSetupView(discord.ui.View): min_values=1, max_values=3, options=[ - discord.SelectOption(label="Epic Games", value="epic", emoji="🟢"), - discord.SelectOption(label="Steam", value="steam", emoji="🟢"), - discord.SelectOption(label="GOG", value="gog", emoji="🟢"), + discord.SelectOption(label="Epic Games", value="epic", emoji="<:animatedarrowgreen:1477261279428087979>"), + discord.SelectOption(label="Steam", value="steam", emoji="<:animatedarrowgreen:1477261279428087979>"), + discord.SelectOption(label="GOG", value="gog", emoji="<:animatedarrowgreen:1477261279428087979>"), ], ) async def stores(self, interaction: discord.Interaction, select: discord.ui.Select) -> None: self.platforms = set(select.values) await self._save(interaction) - await interaction.response.send_message("🟢 Free games stores updated.", ephemeral=True) + await interaction.response.send_message("<:animatedarrowgreen:1477261279428087979> Free games stores updated.", ephemeral=True) @discord.ui.select( placeholder="Choose ping role (optional)", @@ -620,7 +620,7 @@ class Configuration(commands.Cog): ) view = FreeGameSetupView(self, ctx.guild.id, channel.id, role.id if role else None) embed = discord.Embed( - title="🟢 Free Games Setup", + title="<:animatedarrowgreen:1477261279428087979> Free Games Setup", description=( f"Channel: {channel.mention}\n" f"Role ping: {role.mention if role else 'None'}\n" diff --git a/bot/cogs/engagement.py b/bot/cogs/engagement.py index 77b58db5c7382bf347eaaa30b2a478f2b745f970..9e71471ec14e67e35ed3e518af58e5b536c87e1e 100644 --- a/bot/cogs/engagement.py +++ b/bot/cogs/engagement.py @@ -15,7 +15,7 @@ from bot.theme import ( level_display, economy_embed, success_embed, error_embed, info_embed, leaderboard_embed, profile_embed, gaming_embed, tournament_embed, format_leaderboard, money_display, timestamp_display, double_line, triple_line, - pick_neon_color, shimmer + pick_neon_color, shimmer, idle_embed_for_guild ) from bot.emojis import ( ui, E_DIAMOND, E_STAR, E_CATJAM, E_ARROW_BLUE, E_ARROW_GREEN, @@ -189,8 +189,27 @@ class EconomyPanelView(discord.ui.View): if not interaction.guild: await interaction.response.send_message("Server only.", ephemeral=True) return - guild_id = interaction.guild.id - await interaction.response.send_modal(GambleModal(self.cog, guild_id, interaction.user.id)) + gambling_cog = interaction.client.get_cog("Gambling") + if gambling_cog is None: + guild_id = interaction.guild.id + await interaction.response.send_modal(GambleModal(self.cog, guild_id, interaction.user.id)) + return + from bot.cogs.gambling import GamblingPanelView + embed = discord.Embed( + title="Casino & Gambling Panel", + description=( + "Choose a game:\n\n" + "Blackjack\n" + "Roulette\n" + "RPG Adventure" + ), + color=NEON_ORANGE, + ) + await interaction.response.send_message( + embed=embed, + view=GamblingPanelView(gambling_cog, interaction.guild.id, interaction.user.id), + ephemeral=True, + ) @discord.ui.button(label="Rob", emoji=ui("gun"), style=discord.ButtonStyle.danger, custom_id="economy:rob", row=1) async def rob_btn(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: @@ -224,7 +243,15 @@ class EconomyPanelView(discord.ui.View): guild_id, ) if not rows: - await interaction.followup.send("No economy data yet.", ephemeral=True) + await interaction.followup.send( + embed=await idle_embed_for_guild( + "Economy Leaderboard Idle", + "No economy data is available yet.", + guild=interaction.guild, + bot=self.cog.bot, + ), + ephemeral=True, + ) return lines = [] for rank, (uid, total) in enumerate(rows, 1): @@ -461,9 +488,9 @@ class GambleModal(discord.ui.Modal, title="🎲 Gamble Coins"): class RobModal(discord.ui.Modal, title="🔫 Rob a User"): target = discord.ui.TextInput( - label="User ID or Mention", - placeholder="123456789 or @user", - required=True, + label="User ID or Mention (optional)", + placeholder="Leave empty for random target", + required=False, max_length=64, ) @@ -475,11 +502,23 @@ class RobModal(discord.ui.Modal, title="🔫 Rob a User"): async def on_submit(self, interaction: discord.Interaction) -> None: await interaction.response.defer(ephemeral=True) + tid: int | None = None val = self.target.value.strip().replace("<", "").replace(">", "").replace("@", "").replace("!", "") - try: - tid = int(val) - except ValueError: - await interaction.followup.send("Invalid user ID.", ephemeral=True) + if val: + try: + tid = int(val) + except ValueError: + await interaction.followup.send("Invalid user ID.", ephemeral=True) + return + elif interaction.guild: + eligible = [ + m for m in interaction.guild.members + if (not m.bot) and m.id != self.user_id and m.guild_permissions.send_messages + ] + if eligible: + tid = random.choice(eligible).id + if tid is None: + await interaction.followup.send("No valid robbery target found.", ephemeral=True) return if tid == self.user_id: await interaction.followup.send("You can't rob yourself!", ephemeral=True) @@ -721,9 +760,19 @@ class Engagement(commands.Cog): ) if not row: if lang == "ar": - await ctx.reply("📊 لا توجد بيانات مستوى بعد. ابدأ الدردشة لكسب XP!") + await ctx.reply(embed=await idle_embed_for_guild( + "حالة الرتبة الخاملة", + "لا توجد بيانات مستوى بعد. ابدأ الدردشة لكسب XP!", + guild=ctx.guild, + bot=self.bot, + )) else: - await ctx.reply("📊 No rank data yet. Start chatting to earn XP!") + await ctx.reply(embed=await idle_embed_for_guild( + "Rank Idle", + "No rank data yet. Start chatting to earn XP!", + guild=ctx.guild, + bot=self.bot, + )) return xp, level = row @@ -766,9 +815,19 @@ class Engagement(commands.Cog): ) if not rows: if lang == "ar": - await ctx.reply("📊 لا توجد بيانات بعد. ابدأ الدردشة لكسب XP!") + await ctx.reply(embed=await idle_embed_for_guild( + "اللوحة الخاملة", + "لا توجد بيانات بعد. ابدأ الدردشة لكسب XP!", + guild=ctx.guild, + bot=self.bot, + )) else: - await ctx.reply("📊 No data yet. Start chatting to earn XP!") + await ctx.reply(embed=await idle_embed_for_guild( + "Leaderboard Idle", + "No data yet. Start chatting to earn XP!", + guild=ctx.guild, + bot=self.bot, + )) return lines = [] @@ -969,10 +1028,20 @@ class Engagement(commands.Cog): await ctx.reply(embed=embed) @commands.hybrid_command(name="rob", description="Attempt to rob another user (risky!)", hidden=True, with_app_command=False) - async def rob(self, ctx: commands.Context, target: discord.Member) -> None: + async def rob(self, ctx: commands.Context, target: discord.Member | None = None) -> None: guild_id = ctx.guild.id lang = await self.bot.get_guild_language(guild_id) - + + if target is None: + eligible = [ + member for member in ctx.guild.members + if (not member.bot) and member.id != ctx.author.id and member.guild_permissions.send_messages + ] + target = random.choice(eligible) if eligible else None + if target is None: + await ctx.reply("❌ No valid robbery targets found.") + return + if target.id == ctx.author.id or target.bot: if lang == "ar": await ctx.reply("❌ هدف غير صالح.") @@ -1082,7 +1151,7 @@ class Engagement(commands.Cog): await self.gamble(ctx, bet) @economy.command(name="rob", description="Attempt to rob another user") - async def economy_rob(self, ctx: commands.Context, target: discord.Member) -> None: + async def economy_rob(self, ctx: commands.Context, target: discord.Member | None = None) -> None: await self.rob(ctx, target) @commands.hybrid_command(name="profile") @@ -1486,9 +1555,19 @@ class Engagement(commands.Cog): ) if not row: if lang == "ar": - await ctx.reply("❌ البطولة غير موجودة.") + await ctx.reply(embed=await idle_embed_for_guild( + "البطولة غير متاحة", + "لا توجد بطولة بهذا الاسم حالياً.", + guild=ctx.guild, + bot=self.bot, + )) else: - await ctx.reply("❌ Tournament not found.") + await ctx.reply(embed=await idle_embed_for_guild( + "Tournament Not Found", + "No tournament exists with that name right now.", + guild=ctx.guild, + bot=self.bot, + )) return _, status, winner_id = row @@ -1542,9 +1621,19 @@ class Engagement(commands.Cog): ) if not rows: if lang == "ar": - await ctx.reply("❌ لا توجد ألعاب محددة لهذه البطولة.") + await ctx.reply(embed=await idle_embed_for_guild( + "قائمة الألعاب فارغة", + "لم يتم إعداد ألعاب لهذه البطولة بعد.", + guild=ctx.guild, + bot=self.bot, + )) else: - await ctx.reply("❌ No games set for this tournament.") + await ctx.reply(embed=await idle_embed_for_guild( + "Tournament Games Idle", + "No games are configured for this tournament yet.", + guild=ctx.guild, + bot=self.bot, + )) return games = "🎮 " + " • ".join(game for (game,) in rows) @@ -1579,9 +1668,19 @@ class Engagement(commands.Cog): ) if not row: if lang == "ar": - await ctx.reply("❌ البطولة غير موجودة.") + await ctx.reply(embed=await idle_embed_for_guild( + "البطولة غير متاحة", + "لا توجد بطولة بهذا الاسم حالياً.", + guild=ctx.guild, + bot=self.bot, + )) else: - await ctx.reply("❌ Tournament not found.") + await ctx.reply(embed=await idle_embed_for_guild( + "Tournament Not Found", + "No tournament exists with that name right now.", + guild=ctx.guild, + bot=self.bot, + )) return game_rows = await self.bot.db.fetchall( @@ -1635,7 +1734,20 @@ class Engagement(commands.Cog): name, ) if not row: - await ctx.reply("❌ Tournament not found." if lang != "ar" else "❌ البطولة غير موجودة.") + if lang == "ar": + await ctx.reply(embed=await idle_embed_for_guild( + "البطولة غير متاحة", + "لا توجد بطولة بهذا الاسم حالياً.", + guild=ctx.guild, + bot=self.bot, + )) + else: + await ctx.reply(embed=await idle_embed_for_guild( + "Tournament Not Found", + "No tournament exists with that name right now.", + guild=ctx.guild, + bot=self.bot, + )) return if lang == "ar": msg = ( @@ -1662,9 +1774,19 @@ class Engagement(commands.Cog): ) if not rows: if lang == "ar": - await ctx.reply("🏆 لا يوجد فائزون بالبطولات بعد.") + await ctx.reply(embed=await idle_embed_for_guild( + "لوحة البطولات الخاملة", + "لا يوجد فائزون مسجلون حتى الآن.", + guild=ctx.guild, + bot=self.bot, + )) else: - await ctx.reply("🏆 No tournament winners yet.") + await ctx.reply(embed=await idle_embed_for_guild( + "Tournament Leaderboard Idle", + "No tournament winners are recorded yet.", + guild=ctx.guild, + bot=self.bot, + )) return lines = [] diff --git a/bot/cogs/events.py b/bot/cogs/events.py index 1179bce8a52b3de7abc895f353216dc294d6d6d2..04dfcf49584d3004045ba005739e90fde4203676 100644 --- a/bot/cogs/events.py +++ b/bot/cogs/events.py @@ -4,6 +4,7 @@ import asyncio import base64 import datetime as dt import hashlib +import io import json import os import re @@ -28,15 +29,23 @@ try: except Exception: # pragma: no cover genai = None +try: + from PIL import Image, ImageFilter, ImageStat +except Exception: # pragma: no cover + Image = None + ImageFilter = None + ImageStat = None + BAD_WORDS = {"badword1", "badword2", "badword3"} SCAM_LINK_RE = re.compile(r"(https?://\S+|discord\.gg/\S+|t\.me/\S+)", re.IGNORECASE) SCAM_SYSTEM_PROMPT = """You are a strict Discord anti-scam classifier specialized in detecting: 1. Low-quality scam images (blurry, pixelated, poor compression) 2. Fake giveaways and fraudulent offers -3. Phishing attempts and social engineering -4. Misleading or deceptive content +3. Celebrity/creator impersonation scams (example: MrBeast giveaway, fake verified pages) +4. Phishing attempts and social engineering +5. Misleading/deceptive content (wallet verification, Telegram/WhatsApp contact, urgent claim flow) -Analyze the image quality, text content, and context carefully. +Analyze image text, logos, badges, links/handles, and context carefully. Reply ONLY TRUE (if scam) or FALSE (if safe).""" SCAM_CALL_TO_ACTION_RE = re.compile( @@ -45,7 +54,7 @@ SCAM_CALL_TO_ACTION_RE = re.compile( re.IGNORECASE, ) SCAM_KEYWORDS_RE = re.compile( - r"\b(free money|free nitro|crypto|airdrop|giveaway|gift card|usdt|btc|eth|investment|" + r"\b(free money|free nitro|crypto|airdrop|giveaway|gift card|usdt|btc|eth|investment|mrbeast|mr beast|" r"ربح مجاني|نيترو مجاني|كريبتو|هدية|هدايا|قسيمة|استثمار|محفظة)\b", re.IGNORECASE, ) @@ -64,6 +73,21 @@ BENIGN_DM_RE = re.compile( r"راسلني إذا|راسلني للمساعدة|خاص للمساعدة)\b", re.IGNORECASE, ) +CUSTOM_EMOJI_RE = re.compile(r"") +UNICODE_EMOJI_RE = re.compile( + r"[\U0001F1E6-\U0001F1FF]" # flags + r"|[\U0001F300-\U0001FAFF]" # symbols & pictographs + r"|[\u2600-\u27BF]" # dingbats/misc +) +SCAM_LURE_EMOJIS = { + "🎁", "💰", "💸", "🏆", "🚀", "🔥", "✅", "💎", "🔗", "🎉", "🪙", "📈", "💳", "💵", "🤑" +} +SCAM_PRESSURE_EMOJIS = {"⚠️", "⏰", "🚨", "❗", "‼️", "⌛"} +OCR_SCAM_RE = re.compile( + r"\b(mrbeast|mr beast|giveaway|airdrop|free|claim|winner|verify wallet|wallet|crypto|usdt|btc|eth|" + r"telegram|whatsapp|dm me|join now|limited time|urgent|bit\.ly|tinyurl)\b", + re.IGNORECASE, +) @@ -72,6 +96,8 @@ class Events(commands.Cog): self.bot = bot self._scam_scan_semaphore = asyncio.Semaphore(4) self._recent_user_messages: dict[tuple[int, int], deque[tuple[float, str]]] = defaultdict(lambda: deque(maxlen=8)) + self._recent_user_context: dict[tuple[int, int], deque[tuple[float, int, str, list[str]]]] = defaultdict(lambda: deque(maxlen=40)) + self._last_context_ai_check: dict[tuple[int, int], float] = {} self._gemini_key = (os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY") or "").strip() if self._gemini_key and genai is not None: try: @@ -197,6 +223,149 @@ class Events(commands.Cog): reasons.append("benign dm-context detected") return (score, reasons) + @staticmethod + def _extract_emojis(content: str) -> list[str]: + text = (content or "").strip() + if not text: + return [] + custom = CUSTOM_EMOJI_RE.findall(text) + # Keep unicode emoji extraction approximate but fast. + unicode_hits = UNICODE_EMOJI_RE.findall(text) + return custom + unicode_hits + + def _emoji_scam_heuristics(self, content: str, emojis: list[str]) -> tuple[int, list[str]]: + if not emojis: + return (0, []) + text = (content or "").strip().lower() + score = 0 + reasons: list[str] = [] + + lure_count = sum(1 for e in emojis if e in SCAM_LURE_EMOJIS) + pressure_count = sum(1 for e in emojis if e in SCAM_PRESSURE_EMOJIS) + has_link = bool(SCAM_LINK_RE.search(text)) + has_cta = bool(SCAM_CALL_TO_ACTION_RE.search(text)) + has_keyword = bool(SCAM_KEYWORDS_RE.search(text)) + + if lure_count >= 2: + score += 2 + reasons.append("multiple lure emojis") + if pressure_count >= 1: + score += 1 + reasons.append("urgency/pressure emojis") + if len(emojis) >= 6: + score += 1 + reasons.append("emoji-heavy promotional style") + if has_link and (lure_count >= 1 or pressure_count >= 1): + score += 2 + reasons.append("link + lure/pressure emojis") + if has_cta and (lure_count >= 1 or has_keyword): + score += 2 + reasons.append("cta + emoji bait") + if has_keyword and lure_count >= 1: + score += 2 + reasons.append("financial/scam keywords + emoji bait") + return (score, reasons) + + async def _shield_level(self, guild_id: int) -> str: + row = await self.bot.db.fetchone("SELECT level FROM shield_settings WHERE guild_id = ?", guild_id) + level = str(row[0]).strip().lower() if row and row[0] else "medium" + if level not in {"low", "medium", "high"}: + return "medium" + return level + + @staticmethod + def _context_window_seconds(level: str) -> int: + if level == "high": + return 6 * 60 * 60 + if level == "low": + return 90 * 60 + return 3 * 60 * 60 + + @staticmethod + def _context_check_cooldown_seconds(level: str) -> int: + if level == "high": + return 8 + if level == "low": + return 25 + return 15 + + def _build_recent_context(self, guild_id: int, user_id: int, *, now_ts: float, horizon_seconds: int) -> list[tuple[float, int, str, list[str]]]: + key = (guild_id, user_id) + bucket = self._recent_user_context[key] + return [entry for entry in bucket if (now_ts - entry[0]) <= horizon_seconds] + + async def _openrouter_contextual_scam_verdict( + self, + message: discord.Message, + *, + emojis: list[str], + stitched_text: str, + emoji_score: int, + emoji_reasons: list[str], + ) -> bool: + api_key = (self.bot.settings.openrouter_api_key or "").strip() + if not api_key or aiohttp is None: + return False + + level = await self._shield_level(message.guild.id) + now_ts = time.time() + key = (message.guild.id, message.author.id) + cooldown = self._context_check_cooldown_seconds(level) + if (now_ts - self._last_context_ai_check.get(key, 0.0)) < cooldown: + return False + self._last_context_ai_check[key] = now_ts + + horizon = self._context_window_seconds(level) + context_rows = self._build_recent_context(message.guild.id, message.author.id, now_ts=now_ts, horizon_seconds=horizon) + context_rows = context_rows[-20:] # keep prompt bounded + + context_lines: list[str] = [] + for ts, channel_id, text, row_emojis in context_rows: + delta_min = int(max(0, now_ts - ts) // 60) + compact_text = (text or "").replace("\n", " ").strip()[:220] + emoji_str = " ".join(row_emojis[:10]) if row_emojis else "-" + context_lines.append(f"[{delta_min}m ago][#{channel_id}] text={compact_text} | emojis={emoji_str}") + + prompt = ( + "You are a Discord anti-scam contextual classifier.\n" + "Goal: detect hacked-account scam campaigns and phishing even when messages are separated over time.\n" + "Treat repeated lure emojis + links/CTA + fake rewards/wallet verification as high risk.\n" + "Do not flag harmless emoji-only chatting.\n" + "Reply with one word only: SCAM or SAFE.\n\n" + f"Guild shield level: {level}\n" + f"Current message text: {(message.content or '').strip()[:1200]}\n" + f"Current message emojis: {' '.join(emojis[:30]) if emojis else '-'}\n" + f"Emoji heuristic score: {emoji_score} ({', '.join(emoji_reasons) if emoji_reasons else 'none'})\n" + f"Recent stitched text: {stitched_text[:1200] if stitched_text else '-'}\n\n" + "Recent same-user context (can be different channels/time):\n" + + ("\n".join(context_lines) if context_lines else "-") + ) + + payload = { + "model": (self.bot.settings.openrouter_model or "").strip() or "meta-llama/llama-3.3-70b-instruct:free", + "messages": [ + {"role": "system", "content": "Classify as SCAM or SAFE only."}, + {"role": "user", "content": prompt}, + ], + "temperature": 0, + } + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + + try: + async with self._scam_scan_semaphore: + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=18)) as session: + async with session.post("https://openrouter.ai/api/v1/chat/completions", json=payload, headers=headers) as resp: + if resp.status >= 400: + return False + data = await resp.json(content_type=None) + verdict = str((((data.get("choices") or [{}])[0].get("message") or {}).get("content") or "")).strip().upper() + return "SCAM" in verdict and "SAFE" not in verdict + except Exception: + return False + async def _log_image_scan(self, message: discord.Message, result: str, detail: str) -> None: try: await self.send_log( @@ -208,17 +377,30 @@ class Events(commands.Cog): except Exception: return - async def _image_bytes_from_text_urls(self, content: str) -> list[bytes]: + async def _store_scam_hash(self, guild_id: int, digest: str, *, created_by: int = 0) -> None: + if not digest: + return + try: + await self.bot.db.execute( + "INSERT OR IGNORE INTO scam_images(guild_id, image_hash, created_by) VALUES (?, ?, ?)", + guild_id, + digest, + created_by, + ) + except Exception: + return + + async def _image_items_from_text_urls(self, content: str) -> list[tuple[str, bytes, str]]: if aiohttp is None: return [] urls = re.findall(r"https?://\S+", content or "") image_urls = [u.rstrip(").,!?") for u in urls if re.search(r"\.(png|jpg|jpeg|webp|gif)(\?|$)", u, re.IGNORECASE)] if not image_urls: return [] - out: list[bytes] = [] + out: list[tuple[str, bytes, str]] = [] timeout = aiohttp.ClientTimeout(total=8) async with aiohttp.ClientSession(timeout=timeout) as session: - for url in image_urls[:2]: + for url in image_urls[:3]: try: async with session.get(url) as resp: if resp.status >= 400: @@ -227,12 +409,169 @@ class Events(commands.Cog): if "image/" not in ctype: continue data = await resp.read() - if data: - out.append(data[:2_000_000]) + if not data: + continue + raw = data[:2_000_000] + digest = hashlib.sha256(raw).hexdigest() + out.append((url, raw, digest)) except Exception: continue return out + async def _image_bytes_from_text_urls(self, content: str) -> list[bytes]: + return [raw for _, raw, _ in await self._image_items_from_text_urls(content)] + + @staticmethod + def _image_quality_heuristics(images: list[bytes]) -> tuple[int, list[str], dict[str, float | int]]: + if not images: + return (0, [], {}) + if Image is None or ImageFilter is None or ImageStat is None: + return (0, [], {}) + + score = 0 + reasons: list[str] = [] + total_pixels = 0 + total_bytes = 0 + min_dim = None + edge_mean_avg = 0.0 + analyzed = 0 + + for raw in images[:4]: + try: + with Image.open(io.BytesIO(raw)) as img: + img.load() + w, h = img.size + if w <= 0 or h <= 0: + continue + analyzed += 1 + total_pixels += (w * h) + total_bytes += len(raw) + md = min(w, h) + min_dim = md if min_dim is None else min(min_dim, md) + + gray = img.convert("L") + edges = gray.filter(ImageFilter.FIND_EDGES) + st = ImageStat.Stat(edges) + edge_mean = float(st.mean[0]) if st.mean else 0.0 + edge_mean_avg += edge_mean + except Exception: + continue + + if analyzed == 0 or total_pixels <= 0: + return (0, [], {}) + + mp = total_pixels / 1_000_000.0 + bpp = total_bytes / float(total_pixels) + edge_mean_avg = edge_mean_avg / analyzed if analyzed else 0.0 + + if mp < 0.45 or (min_dim is not None and min_dim < 520): + score += 2 + reasons.append("low resolution image quality") + if edge_mean_avg < 11.0: + score += 2 + reasons.append("blurry/soft edges (likely low quality)") + if bpp < 0.16: + score += 1 + reasons.append("high compression artifacts likely") + + metrics: dict[str, float | int] = { + "analyzed_images": analyzed, + "megapixels": round(mp, 3), + "bytes_per_pixel": round(bpp, 4), + "edge_mean": round(edge_mean_avg, 3), + "min_dimension": int(min_dim or 0), + } + return (score, reasons, metrics) + + @staticmethod + def _ocr_scam_heuristics(ocr_text: str) -> tuple[int, list[str]]: + text = (ocr_text or "").strip().lower() + if not text: + return (0, []) + reasons: list[str] = [] + score = 0 + + hits = OCR_SCAM_RE.findall(text) + hit_count = len(hits) + if hit_count >= 2: + score += 2 + reasons.append("ocr contains multiple scam terms") + if ("verify" in text and "wallet" in text) or "verify wallet" in text: + score += 2 + reasons.append("ocr indicates wallet verification flow") + if ("mrbeast" in text or "mr beast" in text) and ("giveaway" in text or "free" in text or "winner" in text): + score += 2 + reasons.append("ocr indicates creator impersonation giveaway") + if ("bit.ly" in text or "tinyurl" in text) and ("claim" in text or "winner" in text or "free" in text): + score += 1 + reasons.append("short-link + prize phrasing in ocr") + + return (score, reasons) + + async def _openrouter_extract_image_text(self, message: discord.Message, *, preloaded_images: list[bytes] | None = None) -> str: + api_key = (self.bot.settings.openrouter_api_key or "").strip() + if not api_key or aiohttp is None: + return "" + + images: list[bytes] = [] + if preloaded_images: + images.extend(preloaded_images[:3]) + else: + for att in message.attachments[:3]: + if not ((att.content_type or "").startswith("image/")): + continue + try: + raw = await att.read(use_cached=True) + if raw: + images.append(raw[:2_000_000]) + except Exception: + continue + if not images: + images.extend((await self._image_bytes_from_text_urls(message.content or ""))[:3]) + + if not images: + return "" + + content_parts: list[dict] = [ + { + "type": "text", + "text": ( + "Extract all visible text from these images (OCR). " + "Return plain text only. Keep URLs, @handles, numbers, and symbols exactly if visible." + ), + } + ] + for raw in images[:3]: + try: + data_url = "data:image/png;base64," + base64.b64encode(raw[:2_000_000]).decode("utf-8") + content_parts.append({"type": "image_url", "image_url": {"url": data_url}}) + except Exception: + continue + + payload = { + "model": "meta-llama/llama-3.2-11b-vision-instruct:free", + "messages": [ + {"role": "system", "content": "You are OCR. Return extracted text only."}, + {"role": "user", "content": content_parts}, + ], + "temperature": 0, + } + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + + try: + async with self._scam_scan_semaphore: + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=20)) as session: + async with session.post("https://openrouter.ai/api/v1/chat/completions", json=payload, headers=headers) as resp: + if resp.status >= 400: + return "" + data = await resp.json(content_type=None) + return str((((data.get("choices") or [{}])[0].get("message") or {}).get("content") or "")).strip() + except Exception: + return "" + async def _analyze_scam_with_openrouter(self, message: discord.Message) -> bool: if aiohttp is None or not self.bot.settings.openrouter_api_key: return False @@ -242,7 +581,18 @@ class Events(commands.Cog): "meta-llama/llama-3.2-11b-vision-instruct:free", "nvidia/llama-3.1-nemotron-nano-vl-8b-v1:free", ] - score, reasons = self._scam_heuristics(message.content or "", has_images=bool(message.attachments)) + has_url_images = bool(re.search(r"https?://\S+\.(png|jpg|jpeg|webp|gif)(\?|$)", message.content or "", re.IGNORECASE)) + score, reasons = self._scam_heuristics(message.content or "", has_images=bool(message.attachments) or has_url_images) + level = await self._shield_level(message.guild.id) + now_ts = time.time() + horizon = self._context_window_seconds(level) + recent_rows = self._build_recent_context(message.guild.id, message.author.id, now_ts=now_ts, horizon_seconds=horizon) + recent_lines: list[str] = [] + for ts, channel_id, text, row_emojis in recent_rows[-12:]: + delta_min = int(max(0, now_ts - ts) // 60) + compact_text = (text or "").replace("\n", " ").strip()[:180] + emoji_str = " ".join(row_emojis[:8]) if row_emojis else "-" + recent_lines.append(f"[{delta_min}m][#{channel_id}] {compact_text} | emojis={emoji_str}") base_text = ( "You are an advanced scam detector for Discord specialized in identifying:\n" "1. LOW-QUALITY IMAGES: Blurry, pixelated, heavily compressed, or distorted images are HIGH RISK\n" @@ -251,7 +601,9 @@ class Events(commands.Cog): "4. SOCIAL ENGINEERING: Impersonation, urgency tactics, pressure to act quickly\n\n" "IMPORTANT: Low image quality + suspicious content = ALMOST ALWAYS SCAM\n\n" f"Heuristic analysis: score={score}/10, reasons: {', '.join(reasons) if reasons else 'none'}.\n\n" - f"Message content:\n{(message.content or '')[:2000]}" + f"Message content:\n{(message.content or '')[:2000]}\n\n" + "Recent same-user context (for delayed scam campaigns):\n" + f"{chr(10).join(recent_lines) if recent_lines else '-'}" ) content_parts: list[dict] = [{"type": "text", "text": base_text}] image_count = 0 @@ -265,6 +617,14 @@ class Events(commands.Cog): image_count += 1 except Exception: continue + url_images = await self._image_bytes_from_text_urls(message.content or "") + for raw in url_images[:3]: + try: + data_url = "data:image/png;base64," + base64.b64encode(raw[:2_000_000]).decode("utf-8") + content_parts.append({"type": "image_url", "image_url": {"url": data_url}}) + image_count += 1 + except Exception: + continue if image_count == 0 and not message.content.strip(): return False @@ -302,11 +662,23 @@ class Events(commands.Cog): return False try: model = genai.GenerativeModel("gemini-1.5-flash") + level = await self._shield_level(message.guild.id) + now_ts = time.time() + horizon = self._context_window_seconds(level) + recent_rows = self._build_recent_context(message.guild.id, message.author.id, now_ts=now_ts, horizon_seconds=horizon) + recent_lines: list[str] = [] + for ts, channel_id, text, row_emojis in recent_rows[-12:]: + delta_min = int(max(0, now_ts - ts) // 60) + compact_text = (text or "").replace("\n", " ").strip()[:180] + emoji_str = " ".join(row_emojis[:8]) if row_emojis else "-" + recent_lines.append(f"[{delta_min}m][#{channel_id}] {compact_text} | emojis={emoji_str}") prompt = ( "Classify if this Discord message is a scam/phishing attempt.\n" "Use context. Do NOT flag harmless 'dm me' alone.\n" "Reply with TRUE or FALSE only.\n\n" - f"Message: {(message.content or '')[:2000]}" + f"Message: {(message.content or '')[:2000]}\n\n" + "Recent same-user context (for delayed scam campaigns):\n" + f"{chr(10).join(recent_lines) if recent_lines else '-'}" ) parts: list = [prompt] for att in message.attachments[:3]: @@ -790,6 +1162,7 @@ class Events(commands.Cog): if message.author.bot or not message.guild: return text_content = message.content or "" + emoji_tokens = self._extract_emojis(text_content) await self.send_log( message.guild, "💬 Message Sent", @@ -801,6 +1174,23 @@ class Events(commands.Cog): bucket = self._recent_user_messages[key] bucket.append((now_ts, text_content.strip())) stitched = " ".join(chunk for ts, chunk in bucket if now_ts - ts <= 90 and chunk) + context_bucket = self._recent_user_context[key] + context_bucket.append((now_ts, message.channel.id, text_content.strip()[:350], emoji_tokens[:20])) + + level = await self._shield_level(message.guild.id) + emoji_score, emoji_reasons = self._emoji_scam_heuristics(text_content, emoji_tokens) + emoji_threshold = 3 if level == "low" else (2 if level == "medium" else 1) + if emoji_tokens and emoji_score >= emoji_threshold: + contextual_scam = await self._openrouter_contextual_scam_verdict( + message, + emojis=emoji_tokens, + stitched_text=stitched, + emoji_score=emoji_score, + emoji_reasons=emoji_reasons, + ) + if contextual_scam: + await self._apply_ai_shield(message, "SCAM") + return if stitched and self._is_high_risk_scam_text(stitched): violation = await self._openrouter_shield_violation(stitched) @@ -811,6 +1201,8 @@ class Events(commands.Cog): if message.attachments: has_image = any((att.content_type or "").startswith("image/") for att in message.attachments) if has_image: + attachment_hashes: list[str] = [] + attachment_images: list[bytes] = [] for att in message.attachments: if not (att.content_type or "").startswith("image/"): continue @@ -819,6 +1211,8 @@ class Events(commands.Cog): except Exception: continue digest = hashlib.sha256(raw).hexdigest() + attachment_hashes.append(digest) + attachment_images.append(raw[:2_000_000]) row = await self.bot.db.fetchone( "SELECT image_hash FROM scam_images WHERE guild_id = ? AND image_hash = ?", message.guild.id, @@ -829,24 +1223,90 @@ class Events(commands.Cog): deleted = await message.channel.purge(limit=15, check=lambda m: m.author.id == message.author.id and m.attachments) await self.send_log(message.guild, "🧹 Scam Image Purge", f"Deleted {len(deleted)} image messages from {message.author.mention}") return + quality_score, quality_reasons, quality_metrics = self._image_quality_heuristics(attachment_images) + score, reasons = self._scam_heuristics(message.content or "", has_images=True) + ocr_text = "" + ocr_score = 0 + ocr_reasons: list[str] = [] + if score >= 2 or quality_score >= 2: + ocr_text = await self._openrouter_extract_image_text(message, preloaded_images=attachment_images) + ocr_score, ocr_reasons = self._ocr_scam_heuristics(ocr_text) + is_image_scam = await self._analyze_scam_with_openrouter(message) if not is_image_scam: is_image_scam = await self._analyze_scam_with_gemini(message) - score, reasons = self._scam_heuristics(message.content or "", has_images=True) - detail = f"heuristic_score={score}; reasons={', '.join(reasons) if reasons else 'none'}; ai_scam={is_image_scam}" + + combined_score = score + quality_score + ocr_score + has_direct_scam_cue = bool( + SCAM_LINK_RE.search(message.content or "") + or SCAM_KEYWORDS_RE.search((message.content or "") + " " + (ocr_text or "")) + or ocr_score >= 2 + ) + if not is_image_scam and combined_score >= 7 and has_direct_scam_cue: + is_image_scam = True + + quality_text = ", ".join(quality_reasons) if quality_reasons else "none" + ocr_snippet = (ocr_text or "").replace("\n", " ")[:180] + detail = ( + f"heuristic_score={score}; heuristic_reasons={', '.join(reasons) if reasons else 'none'}; " + f"quality_score={quality_score}; quality_reasons={quality_text}; quality_metrics={quality_metrics}; " + f"ocr_score={ocr_score}; ocr_reasons={', '.join(ocr_reasons) if ocr_reasons else 'none'}; " + f"ocr_text={ocr_snippet or 'none'}; combined_score={combined_score}; ai_scam={is_image_scam}" + ) await self._log_image_scan(message, "SCAM" if is_image_scam else "SAFE", detail) if is_image_scam: + for digest in attachment_hashes: + await self._store_scam_hash(message.guild.id, digest, created_by=(self.bot.user.id if self.bot.user else 0)) await self._apply_ai_shield(message, "SCAM") return elif re.search(r"https?://\S+\.(png|jpg|jpeg|webp|gif)(\?|$)", message.content or "", re.IGNORECASE): + url_items = await self._image_items_from_text_urls(message.content or "") + url_hashes = [digest for _, _, digest in url_items] + url_images = [raw for _, raw, _ in url_items] + for digest in url_hashes: + row = await self.bot.db.fetchone( + "SELECT image_hash FROM scam_images WHERE guild_id = ? AND image_hash = ?", + message.guild.id, + digest, + ) + if row: + await self._apply_ai_shield(message, "SCAM") + return proxy_message = message + quality_score, quality_reasons, quality_metrics = self._image_quality_heuristics(url_images) + score, reasons = self._scam_heuristics(message.content or "", has_images=True) + ocr_text = "" + ocr_score = 0 + ocr_reasons: list[str] = [] + if score >= 2 or quality_score >= 2: + ocr_text = await self._openrouter_extract_image_text(proxy_message, preloaded_images=url_images) + ocr_score, ocr_reasons = self._ocr_scam_heuristics(ocr_text) + is_image_scam = await self._analyze_scam_with_openrouter(proxy_message) if not is_image_scam: is_image_scam = await self._analyze_scam_with_gemini(proxy_message) - score, reasons = self._scam_heuristics(message.content or "", has_images=True) - detail = f"url_image_scan=1; heuristic_score={score}; reasons={', '.join(reasons) if reasons else 'none'}; ai_scam={is_image_scam}" + + combined_score = score + quality_score + ocr_score + has_direct_scam_cue = bool( + SCAM_LINK_RE.search(message.content or "") + or SCAM_KEYWORDS_RE.search((message.content or "") + " " + (ocr_text or "")) + or ocr_score >= 2 + ) + if not is_image_scam and combined_score >= 7 and has_direct_scam_cue: + is_image_scam = True + + quality_text = ", ".join(quality_reasons) if quality_reasons else "none" + ocr_snippet = (ocr_text or "").replace("\n", " ")[:180] + detail = ( + f"url_image_scan=1; heuristic_score={score}; heuristic_reasons={', '.join(reasons) if reasons else 'none'}; " + f"quality_score={quality_score}; quality_reasons={quality_text}; quality_metrics={quality_metrics}; " + f"ocr_score={ocr_score}; ocr_reasons={', '.join(ocr_reasons) if ocr_reasons else 'none'}; " + f"ocr_text={ocr_snippet or 'none'}; combined_score={combined_score}; ai_scam={is_image_scam}" + ) await self._log_image_scan(message, "SCAM" if is_image_scam else "SAFE", detail) if is_image_scam: + for digest in url_hashes: + await self._store_scam_hash(message.guild.id, digest, created_by=(self.bot.user.id if self.bot.user else 0)) await self._apply_ai_shield(message, "SCAM") return if self._is_high_risk_scam_text(text_content): @@ -919,10 +1379,26 @@ class Events(commands.Cog): elif mention_type == "role" and role_id: mention = f"<@&{role_id}> " for item in fresh[:3]: - embed = discord.Embed(title=item.get("title", ""), url=item.get("url", ""), color=NEON_CYAN) + link = item.get("link", "") + embed = discord.Embed( + title=f"🎁 Free Game Drop: {item.get('title', '')}", + url=link, + color=NEON_CYAN, + description=( + f"**{item.get('description', 'Limited-time free game offer!')}**\n\n" + f"Grab it before the offer expires." + ), + ) if item.get("image"): embed.set_image(url=item["image"]) - view = None + embed.set_thumbnail(url=self._store_icon(item.get("platform", ""))) + embed.add_field(name="🏪 Platform", value=item.get("platform", "Unknown"), inline=True) + embed.add_field(name="🧩 Type", value=item.get("game_type", "Game"), inline=True) + embed.add_field(name="💸 Previous Price", value=item.get("original_price", "N/A"), inline=True) + embed.add_field(name="⏳ Offer Ends", value=item.get("end_date", "N/A"), inline=True) + embed.add_field(name="🔗 Claim Link", value=f"[Open Giveaway]({link})" if link else "N/A", inline=True) + embed.set_footer(text="BOT- Free Games Radar • Epic / Steam / GOG") + view = FreeGameClaimView(link, item.get("id", "")) if link else None await channel.send(content=mention or None, embed=embed, view=view) latest_ids = ",".join(item["id"] for item in items[:20]) await self.bot.db.execute( diff --git a/bot/cogs/fun.py b/bot/cogs/fun.py index 0b80e47622cffaa375c122fdfb17da3c62dec6b9..9a87a32d4807109e66ba72dc32e0b0ba168a9a21 100644 --- a/bot/cogs/fun.py +++ b/bot/cogs/fun.py @@ -605,14 +605,19 @@ class Fun(commands.Cog): link = item["link"] embed = ImperialMotaz.craft_embed( title=f"[FREE GAME] | {title}", - description="Live free-game deal from trusted store.", + description=( + f"**{item.get('description', 'Limited-time free game offer!')}**\n\n" + "Claim it now from the official store page before it expires." + ), color=NEON_PINK, footer="Free Games Tracker", ) embed.url = link embed.add_field(name="Platform", value=f"「 {item.get('platform', 'Unknown')} 」", inline=True) + embed.add_field(name="Type", value=f"「 {item.get('game_type', 'Game')} 」", inline=True) embed.add_field(name="Original Price", value=f"「 {item.get('original_price', 'N/A')} 」", inline=True) embed.add_field(name="Ends On", value=f"「 {item.get('end_date', 'N/A')} 」", inline=True) + embed.add_field(name="Link", value=f"[Open Giveaway]({link})", inline=False) if item.get("image"): embed.set_image(url=item["image"]) embed.set_thumbnail(url=self._store_icon(item.get("platform", ""))) @@ -760,7 +765,7 @@ class Fun(commands.Cog): @gamehub.command(name="trivia", description="Gaming/Movies/Series trivia") async def gamehub_trivia(self, ctx: commands.Context, category: str = "gaming", difficulty: str = "medium") -> None: - await self.trivia(ctx, category=category, difficulty=difficulty) + await self.trivia(ctx, category=category) @commands.hybrid_command(name="xo", description="TicTacToe vs member or bot", hidden=True, with_app_command=False) async def xo(self, ctx: commands.Context, opponent: discord.Member | None = None) -> None: @@ -845,7 +850,7 @@ class Fun(commands.Cog): ), color=NEON_ORANGE, ) - embed.add_field(name="🎁 مكافأة", value="تم فتح combo باك مان! 🟡", inline=False) + embed.add_field(name="🎁 مكافأة", value="تم فتح combo باك مان! <:animatedarrowyellow:1477261257592668271>", inline=False) else: embed = discord.Embed( title="🍄 Arcade Challenge", @@ -857,7 +862,7 @@ class Fun(commands.Cog): ), color=NEON_ORANGE, ) - embed.add_field(name="🎁 Bonus", value="Pac-Man combo unlocked! 🟡", inline=False) + embed.add_field(name="🎁 Bonus", value="Pac-Man combo unlocked! <:animatedarrowyellow:1477261257592668271>", inline=False) embed.set_thumbnail(url="https://upload.wikimedia.org/wikipedia/en/a/a9/MarioNSMBUDeluxe.png") await ctx.reply(embed=embed) diff --git a/bot/cogs/gambling.py b/bot/cogs/gambling.py index ca9f7071cbbfaa8ef7287686ee878c80ad118851..6ca63359ffcf7dc039ea724c5e6471f9e532c6e8 100644 --- a/bot/cogs/gambling.py +++ b/bot/cogs/gambling.py @@ -1,771 +1,689 @@ -""" -Interactive Gambling & RPG Cog - Casino Games with UI and Full RPG System -""" - -from __future__ import annotations - -import random -import asyncio -from typing import List, Optional, Dict - -import discord -from discord.ext import commands - -from bot.theme import NEON_CYAN, NEON_LIME, NEON_RED, NEON_GOLD, NEON_PURPLE, NEON_PINK, NEON_BLUE, panel_divider, add_banner_to_embed -from bot.emojis import ui - - -class Card: - def __init__(self, suit: str, rank: str): - self.suit = suit - self.rank = rank - self.value = self._get_value() - - def _get_value(self) -> int: - if self.rank in ['J', 'Q', 'K']: - return 10 - elif self.rank == 'A': - return 11 - else: - return int(self.rank) - - def __str__(self) -> str: - return f"{self.rank}{self.suit}" - - -class Deck: - def __init__(self): - self.cards = [] - suits = ['♠', '♥', '♦', '♣'] - ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'] - for suit in suits: - for rank in ranks: - self.cards.append(Card(suit, rank)) - random.shuffle(self.cards) - - def draw(self) -> Card: - return self.cards.pop() - - -class BlackjackView(discord.ui.View): - def __init__(self, cog, guild_id: int, user_id: int, bet: int): - super().__init__(timeout=300) - self.cog = cog - self.guild_id = guild_id - self.user_id = user_id - self.bet = bet - self.deck = Deck() - self.player_hand = [self.deck.draw(), self.deck.draw()] - self.dealer_hand = [self.deck.draw(), self.deck.draw()] - self.game_over = False - self.message: Optional[discord.Message] = None - - def hand_value(self, hand: List[Card]) -> int: - value = sum(card.value for card in hand) - aces = sum(1 for card in hand if card.rank == 'A') - while value > 21 and aces: - value -= 10 - aces -= 1 - return value - - def format_hand(self, hand: List[Card], hide_first: bool = False) -> str: - if hide_first and len(hand) >= 2: - return f"[❓❓] [{hand[1]}]" - return ' '.join([f"[{str(card)}]" for card in hand]) - - def build_embed(self, show_dealer_card: bool = True) -> discord.Embed: - player_value = self.hand_value(self.player_hand) - dealer_value = self.hand_value(self.dealer_hand) if show_dealer_card else '?' - - embed = discord.Embed( - title="🃏 Blackjack Casino", - description=panel_divider('purple'), - color=NEON_PURPLE, - ) - - dealer_display = self.format_hand(self.dealer_hand, hide_first=not show_dealer_card) - embed.add_field( - name="🎩 Dealer's Hand", - value=f"{dealer_display}\n**Value:** {dealer_value}", - inline=False, - ) - - player_display = self.format_hand(self.player_hand) - embed.add_field( - name="👤 Your Hand", - value=f"{player_display}\n**Value:** {player_value}", - inline=False, - ) - - embed.add_field(name="💰 Bet", value=f"`{self.bet:,}` coins", inline=False) - - if self.game_over: - if player_value > 21: - embed.description = f"❌ **BUST!** You lost `{self.bet:,}` coins." - embed.color = NEON_RED - elif dealer_value == 21 and len(self.dealer_hand) == 2: - embed.description = f"❌ **Dealer Blackjack!** You lost `{self.bet:,}` coins." - embed.color = NEON_RED - elif dealer_value > 21: - winnings = self.bet - embed.description = f"✅ **Dealer Busts! You Win!** +`{winnings:,}` coins!" - embed.color = NEON_LIME - elif player_value > dealer_value: - winnings = self.bet - embed.description = f"✅ **You Win!** +`{winnings:,}` coins!" - embed.color = NEON_LIME - elif player_value < dealer_value: - embed.description = f"❌ **Dealer Wins!** You lost `{self.bet:,}` coins." - embed.color = NEON_RED - else: - embed.description = f"🤝 **Push!** Bet returned." - embed.color = NEON_GOLD - - return embed - - async def end_game(self, interaction: discord.Interaction): - self.game_over = True - - engagement_cog = interaction.bot.get_cog("Engagement") - if not engagement_cog: - return - - player_value = self.hand_value(self.player_hand) - dealer_value = self.hand_value(self.dealer_hand) - - if player_value > 21: - await engagement_cog._remove_coins(self.guild_id, self.user_id, self.bet) - elif dealer_value == 21 and len(self.dealer_hand) == 2: - await engagement_cog._remove_coins(self.guild_id, self.user_id, self.bet) - elif dealer_value > 21 or player_value > dealer_value: - await engagement_cog._add_coins(self.guild_id, self.user_id, self.bet) - elif player_value < dealer_value: - await engagement_cog._remove_coins(self.guild_id, self.user_id, self.bet) - - embed = self.build_embed(show_dealer_card=True) - for child in self.children: - child.disabled = True - if self.message: - await self.message.edit(embed=embed, view=self) - - @discord.ui.button(label="Hit", emoji=ui("plus"), style=discord.ButtonStyle.primary, custom_id="bj_hit") - async def hit(self, interaction: discord.Interaction, button: discord.ui.Button): - if self.game_over or interaction.user.id != self.user_id: - await interaction.response.send_message("❌ Not your game.", ephemeral=True) - return - - self.player_hand.append(self.deck.draw()) - player_value = self.hand_value(self.player_hand) - - if player_value > 21: - await self.end_game(interaction) - else: - embed = self.build_embed(show_dealer_card=False) - await interaction.response.edit_message(embed=embed, view=self) - - @discord.ui.button(label="Stand", emoji=ui("stop"), style=discord.ButtonStyle.danger, custom_id="bj_stand") - async def stand(self, interaction: discord.Interaction, button: discord.ui.Button): - if self.game_over or interaction.user.id != self.user_id: - await interaction.response.send_message("❌ Not your game.", ephemeral=True) - return - - while self.hand_value(self.dealer_hand) < 17: - self.dealer_hand.append(self.deck.draw()) - - await self.end_game(interaction) - - @discord.ui.button(label="Double Down", emoji=ui("star"), style=discord.ButtonStyle.success, custom_id="bj_double") - async def double_down(self, interaction: discord.Interaction, button: discord.ui.Button): - if self.game_over or interaction.user.id != self.user_id: - await interaction.response.send_message("❌ Not your game.", ephemeral=True) - return - - if len(self.player_hand) != 2: - await interaction.response.send_message("❌ Can only double on first turn.", ephemeral=True) - return - - engagement_cog = interaction.bot.get_cog("Engagement") - if not engagement_cog: - return - - row = await interaction.bot.db.fetchone( - "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?", - self.guild_id, self.user_id, - ) - wallet = row[0] if row else 0 - - if wallet < self.bet: - await interaction.response.send_message("❌ Insufficient funds to double.", ephemeral=True) - return - - self.bet *= 2 - self.player_hand.append(self.deck.draw()) - - player_value = self.hand_value(self.player_hand) - if player_value > 21: - await self.end_game(interaction) - else: - while self.hand_value(self.dealer_hand) < 17: - self.dealer_hand.append(self.deck.draw()) - await self.end_game(interaction) - - -class RouletteGameView(discord.ui.View): - def __init__(self, cog, guild_id: int, user_id: int): - super().__init__(timeout=300) - self.cog = cog - self.guild_id = guild_id - self.user_id = user_id - self.bet_amount = 0 - self.bet_type = "" - self.spinning = False - - RED_NUMBERS = [1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36] - BLACK_NUMBERS = [2, 4, 6, 8, 10, 11, 13, 15, 17, 20, 22, 24, 26, 28, 29, 31, 33, 35] - - def get_color(self, number: int) -> str: - if number == 0: - return "🟢" - elif number in self.RED_NUMBERS: - return "🔴" - else: - return "⚫" - - def build_embed(self, number: Optional[int] = None, won: bool = False, winnings: int = 0) -> discord.Embed: - embed = discord.Embed( - title="🎰 Roulette Casino", - description=panel_divider('gold'), - color=NEON_GOLD, - ) - - if number is not None: - color_emoji = self.get_color(number) - embed.add_field(name="🎯 Result", value=f"{color_emoji} **{number}**", inline=False) - embed.add_field(name="💰 Bet", value=f"`{self.bet_amount:,}` on {self.bet_type}", inline=True) - embed.add_field(name="💵 Result", value=f"`{'+' if won else '-'}{abs(winnings):,}` coins", inline=True) - - if won: - embed.description = f"✅ **You Won!** +`{winnings:,}` coins!" - embed.color = NEON_LIME - else: - embed.description = f"❌ **You Lost!** -`{abs(winnings):,}` coins." - embed.color = NEON_RED - else: - embed.description = "Place your bet and spin the wheel!" - grid = "" - for i in range(0, 37, 3): - if i == 0: - grid += "🟢 **0** " - else: - c1 = "🔴" if i in self.RED_NUMBERS else "⚫" - c2 = "🔴" if (i+1) in self.RED_NUMBERS else "⚫" - c3 = "🔴" if (i+2) in self.RED_NUMBERS else "⚫" - grid += f"{c1} {i} {c2} {i+1} {c3} {i+2}\n" - embed.add_field(name="🎡 Roulette Wheel", value=f"```\n{grid}```", inline=False) - - return embed - - @discord.ui.button(label="Bet Red 🔴", style=discord.ButtonStyle.danger, custom_id="roulette_red") - async def bet_red(self, interaction: discord.Interaction, button: discord.ui.Button): - await self.prompt_bet(interaction, "red") - - @discord.ui.button(label="Bet Black ⚫", style=discord.ButtonStyle.gray, custom_id="roulette_black") - async def bet_black(self, interaction: discord.Interaction, button: discord.ui.Button): - await self.prompt_bet(interaction, "black") - - @discord.ui.button(label="Bet Even", style=discord.ButtonStyle.primary, custom_id="roulette_even") - async def bet_even(self, interaction: discord.Interaction, button: discord.ui.Button): - await self.prompt_bet(interaction, "even") - - @discord.ui.button(label="Bet Odd", style=discord.ButtonStyle.primary, custom_id="roulette_odd") - async def bet_odd(self, interaction: discord.Interaction, button: discord.ui.Button): - await self.prompt_bet(interaction, "odd") - - async def prompt_bet(self, interaction: discord.Interaction, bet_type: str): - if interaction.user.id != self.user_id: - return - await interaction.response.send_modal(RouletteBetModal(self.cog, self.guild_id, self.user_id, bet_type)) - - -class RouletteBetModal(discord.ui.Modal, title="🎰 Place Your Bet"): - bet_amount = discord.ui.TextInput( - label="Bet Amount", - placeholder="Enter amount (min 10)", - required=True, - min_length=1, - max_length=10, - ) - - def __init__(self, cog, guild_id: int, user_id: int, bet_type: str): - super().__init__() - self.cog = cog - self.guild_id = guild_id - self.user_id = user_id - self.bet_type = bet_type - - async def on_submit(self, interaction: discord.Interaction) -> None: - try: - bet = int(self.bet_amount.value.strip()) - except ValueError: - await interaction.response.send_message("❌ Invalid bet amount.", ephemeral=True) - return - - if bet < 10: - await interaction.response.send_message("❌ Minimum bet is 10 coins.", ephemeral=True) - return - - engagement_cog = interaction.bot.get_cog("Engagement") - if not engagement_cog: - await interaction.response.send_message("❌ Economy not available.", ephemeral=True) - return - - row = await interaction.bot.db.fetchone( - "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?", - self.guild_id, self.user_id, - ) - wallet = row[0] if row else 0 - - if wallet < bet: - await interaction.response.send_message(f"❌ Insufficient funds. You have {wallet:,} coins.", ephemeral=True) - return - - await interaction.response.defer() - - number = random.randint(0, 36) - is_red = number in RouletteGameView.RED_NUMBERS - is_black = number in RouletteGameView.BLACK_NUMBERS - is_even = number != 0 and number % 2 == 0 - is_odd = number != 0 and number % 2 == 1 - - won = False - multiplier = 2 - - if self.bet_type == 'red' and is_red: - won = True - elif self.bet_type == 'black' and is_black: - won = True - elif self.bet_type == 'even' and is_even: - won = True - elif self.bet_type == 'odd' and is_odd: - won = True - - winnings = bet * multiplier if won else -bet - - if won: - await engagement_cog._add_coins(self.guild_id, self.user_id, winnings) - else: - await engagement_cog._remove_coins(self.guild_id, self.user_id, bet) - - view = RouletteGameView(self.cog, self.guild_id, self.user_id) - view.bet_amount = bet - view.bet_type = self.bet_type - embed = view.build_embed(number=number, won=won, winnings=winnings) - - for child in view.children: - child.disabled = False - - await interaction.followup.send(embed=embed, view=view) - - -class RPGView(discord.ui.View): - def __init__(self, cog, guild_id: int, user_id: int): - super().__init__(timeout=None) - self.cog = cog - self.guild_id = guild_id - self.user_id = user_id - self.adventure_image = "https://images.unsplash.com/photo-1519074069444-1ba4fff66d16?w=500" - - def build_embed(self, title: str, description: str, color: discord.Color) -> discord.Embed: - embed = discord.Embed( - title=title, - description=description, - color=color, - ) - embed.set_image(url=self.adventure_image) - return embed - - @discord.ui.button(label="⚔️ Fight Monster", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="rpg_fight") - async def fight_monster(self, interaction: discord.Interaction, button: discord.ui.Button): - if interaction.user.id != self.user_id: - return - - engagement_cog = interaction.bot.get_cog("Engagement") - if not engagement_cog: - await interaction.response.send_message("❌ Economy not available.", ephemeral=True) - return - - row = await interaction.bot.db.fetchone( - "SELECT level FROM user_xp WHERE guild_id = ? AND user_id = ?", - self.guild_id, self.user_id, - ) - level = row[0] if row else 1 - - monsters = [ - ("Goblin", 50, 100, 20, 40), - ("Dragon", 200, 500, 50, 100), - ("Skeleton", 30, 80, 15, 30), - ("Orc", 80, 200, 25, 50), - ("Troll", 150, 350, 40, 80), - ("Demon", 300, 600, 60, 120), - ] - - monster_name, min_reward, max_reward, min_xp, max_xp = random.choice(monsters) - win_chance = min(0.8, 0.3 + (level * 0.05)) - won = random.random() < win_chance - - if won: - reward = random.randint(min_reward, max_reward) + (level * 10) - xp_gain = random.randint(min_xp, max_xp) - await engagement_cog._add_coins(self.guild_id, self.user_id, reward) - await engagement_cog.add_xp(self.guild_id, self.user_id, xp_gain) - - embed = discord.Embed( - title="⚔️ RPG Adventure - Victory!", - description=f"✅ **You defeated the {monster_name}!**\n\n💰 **+{reward:,} coins**\n⭐ **+{xp_gain} XP**\n📈 Level {level} → {level + (1 if xp_gain > 40 else 0)}", - color=NEON_LIME, - ) - embed.set_image(url="https://images.unsplash.com/photo-1535905557558-afc4877a26fc?w=500") - else: - penalty = random.randint(10, 50) + (level * 5) - await engagement_cog._remove_coins(self.guild_id, self.user_id, penalty) - - embed = discord.Embed( - title="⚔️ RPG Adventure - Defeat!", - description=f"❌ **The {monster_name} defeated you!**\n\n💀 **-{penalty:,} coins**\n\n💡 Tip: Level up to increase win chance!", - color=NEON_RED, - ) - embed.set_image(url="https://images.unsplash.com/photo-1519791883288-dc8bd696e667?w=500") - - if interaction.guild: - await add_banner_to_embed(embed, interaction.guild) - - await interaction.response.send_message(embed=embed, ephemeral=True) - - @discord.ui.button(label="🎁 Find Treasure", emoji=ui("gift"), style=discord.ButtonStyle.success, custom_id="rpg_treasure") - async def find_treasure(self, interaction: discord.Interaction, button: discord.ui.Button): - if interaction.user.id != self.user_id: - return - - engagement_cog = interaction.bot.get_cog("Engagement") - if not engagement_cog: - await interaction.response.send_message("❌ Economy not available.", ephemeral=True) - return - - found = random.random() < 0.4 - - if found: - reward = random.randint(30, 150) - await engagement_cog._add_coins(self.guild_id, self.user_id, reward) - - embed = discord.Embed( - title="🎁 Treasure Hunt - Success!", - description=f"✅ **You found a hidden treasure!**\n\n💰 **+{reward:,} coins**\n\n🗺️ The treasure map led you to riches!", - color=NEON_GOLD, - ) - embed.set_image(url="https://images.unsplash.com/photo-1518709268805-4e9042af9f23?w=500") - else: - embed = discord.Embed( - title="🎁 Treasure Hunt - Nothing Found", - description="❌ **No treasure found in this area...**\n\n🗺️ Try exploring a different location next time!", - color=NEON_RED, - ) - embed.set_image(url="https://images.unsplash.com/photo-1500353391678-d7b57970d9b2?w=500") - - if interaction.guild: - await add_banner_to_embed(embed, interaction.guild) - - await interaction.response.send_message(embed=embed, ephemeral=True) - - @discord.ui.button(label="🏪 Shop", emoji=ui("cart"), style=discord.ButtonStyle.blurple, custom_id="rpg_shop") - async def shop(self, interaction: discord.Interaction, button: discord.ui.Button): - if interaction.user.id != self.user_id: - return - - embed = discord.Embed( - title="🏪 RPG Shop", - description=( - "Welcome to the shop! Buy items to help in your adventures.\n\n" - "🗡️ **Sword** - Increase win chance (Coming Soon)\n" - "🛡️ **Shield** - Reduce coin loss on defeat (Coming Soon)\n" - "🧪 **Potion** - Heal after battle (Coming Soon)\n" - "📜 **Map** - Better treasure finds (Coming Soon)" - ), - color=NEON_CYAN, - ) - embed.set_image(url="https://images.unsplash.com/photo-1589829545856-d10d557cf95f?w=500") - - if interaction.guild: - await add_banner_to_embed(embed, interaction.guild) - - await interaction.response.send_message(embed=embed, ephemeral=True) - - @discord.ui.button(label="📊 Stats", emoji=ui("stats"), style=discord.ButtonStyle.secondary, custom_id="rpg_stats") - async def stats(self, interaction: discord.Interaction, button: discord.ui.Button): - if interaction.user.id != self.user_id: - return - - engagement_cog = interaction.bot.get_cog("Engagement") - if not engagement_cog: - return - - row = await interaction.bot.db.fetchone( - "SELECT wallet, bank FROM user_balance WHERE guild_id = ? AND user_id = ?", - self.guild_id, self.user_id, - ) - wallet, bank = row if row else (0, 0) - - xp_row = await interaction.bot.db.fetchone( - "SELECT xp, level FROM user_xp WHERE guild_id = ? AND user_id = ?", - self.guild_id, self.user_id, - ) - xp, level = xp_row if xp_row else (0, 1) - - embed = discord.Embed( - title="📊 RPG Stats", - description=( - f"**Level:** {level}\n" - f"**XP:** {xp:,}\n" - f"**Wallet:** {wallet:,} coins\n" - f"**Bank:** {bank:,} coins\n" - f"**Total Wealth:** {wallet + bank:,} coins" - ), - color=NEON_PURPLE, - ) - - if interaction.guild: - await add_banner_to_embed(embed, interaction.guild) - - await interaction.response.send_message(embed=embed, ephemeral=True) - - -class GamblingPanelView(discord.ui.View): - def __init__(self, cog, guild_id: int, user_id: int): - super().__init__(timeout=None) - self.cog = cog - self.guild_id = guild_id - self.user_id = user_id - - @discord.ui.button(label="🃏 Play Blackjack", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="gambling_blackjack") - async def play_blackjack(self, interaction: discord.Interaction, button: discord.ui.Button): - if interaction.user.id != self.user_id: - return - await interaction.response.send_modal(BlackjackBetModal(self.cog, self.guild_id, self.user_id)) - - @discord.ui.button(label="🎰 Play Roulette", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="gambling_roulette") - async def play_roulette(self, interaction: discord.Interaction, button: discord.ui.Button): - if interaction.user.id != self.user_id: - return - view = RouletteGameView(self.cog, self.guild_id, self.user_id) - embed = view.build_embed() - await interaction.response.send_message(embed=embed, view=view, ephemeral=True) - - @discord.ui.button(label="⚔️ RPG Adventure", emoji=ui("game"), style=discord.ButtonStyle.success, custom_id="gambling_rpg") - async def play_rpg(self, interaction: discord.Interaction, button: discord.ui.Button): - if interaction.user.id != self.user_id: - return - view = RPGView(self.cog, self.guild_id, self.user_id) - embed = view.build_embed( - title="⚔️ RPG Adventure", - description="Choose your action below to begin your adventure!", - color=NEON_PURPLE, - ) - await interaction.response.send_message(embed=embed, view=view, ephemeral=True) - - -class BlackjackBetModal(discord.ui.Modal, title="🃏 Place Your Bet"): - bet_amount = discord.ui.TextInput( - label="Bet Amount", - placeholder="Enter amount (min 10)", - required=True, - min_length=1, - max_length=10, - ) - - def __init__(self, cog, guild_id: int, user_id: int): - super().__init__() - self.cog = cog - self.guild_id = guild_id - self.user_id = user_id - - async def on_submit(self, interaction: discord.Interaction) -> None: - try: - bet = int(self.bet_amount.value.strip()) - except ValueError: - await interaction.response.send_message("❌ Invalid bet amount.", ephemeral=True) - return - - if bet < 10: - await interaction.response.send_message("❌ Minimum bet is 10 coins.", ephemeral=True) - return - - engagement_cog = interaction.bot.get_cog("Engagement") - if not engagement_cog: - await interaction.response.send_message("❌ Economy not available.", ephemeral=True) - return - - row = await interaction.bot.db.fetchone( - "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?", - self.guild_id, self.user_id, - ) - wallet = row[0] if row else 0 - - if wallet < bet: - await interaction.response.send_message(f"❌ Insufficient funds. You have {wallet:,} coins.", ephemeral=True) - return - - view = BlackjackView(self.cog, self.guild_id, self.user_id, bet) - embed = view.build_embed(show_dealer_card=False) - message = await interaction.channel.send(embed=embed, view=view) - view.message = message - await interaction.response.send_message("✅ Blackjack game started!", ephemeral=True) - - -class GamblingPanelView(discord.ui.View): - def __init__(self, cog, guild_id: int, user_id: int): - super().__init__(timeout=None) - self.cog = cog - self.guild_id = guild_id - self.user_id = user_id - - @discord.ui.button(label="🃏 Play Blackjack", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="gambling_blackjack") - async def play_blackjack(self, interaction: discord.Interaction, button: discord.ui.Button): - if interaction.user.id != self.user_id: - return - await interaction.response.send_modal(BlackjackBetModal(self.cog, self.guild_id, self.user_id)) - - @discord.ui.button(label="🎰 Play Roulette", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="gambling_roulette") - async def play_roulette(self, interaction: discord.Interaction, button: discord.ui.Button): - if interaction.user.id != self.user_id: - return - view = RouletteGameView(self.cog, self.guild_id, self.user_id) - embed = view.build_embed() - await interaction.response.send_message(embed=embed, view=view, ephemeral=True) - - @discord.ui.button(label="⚔️ RPG Adventure", emoji=ui("game"), style=discord.ButtonStyle.success, custom_id="gambling_rpg") - async def play_rpg(self, interaction: discord.Interaction, button: discord.ui.Button): - if interaction.user.id != self.user_id: - return - view = RPGView(self.cog, self.guild_id, self.user_id) - embed = view.build_embed( - title="⚔️ RPG Adventure", - description="Choose your action below to begin your adventure!", - color=NEON_PURPLE, - ) - await interaction.response.send_message(embed=embed, view=view, ephemeral=True) - - -class BlackjackBetModal(discord.ui.Modal, title="🃏 Place Your Bet"): - bet_amount = discord.ui.TextInput( - label="Bet Amount", - placeholder="Enter amount (min 10)", - required=True, - min_length=1, - max_length=10, - ) - - def __init__(self, cog, guild_id: int, user_id: int): - super().__init__() - self.cog = cog - self.guild_id = guild_id - self.user_id = user_id - - async def on_submit(self, interaction: discord.Interaction) -> None: - try: - bet = int(self.bet_amount.value.strip()) - except ValueError: - await interaction.response.send_message("❌ Invalid bet amount.", ephemeral=True) - return - - if bet < 10: - await interaction.response.send_message("❌ Minimum bet is 10 coins.", ephemeral=True) - return - - engagement_cog = interaction.bot.get_cog("Engagement") - if not engagement_cog: - await interaction.response.send_message("❌ Economy not available.", ephemeral=True) - return - - row = await interaction.bot.db.fetchone( - "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?", - self.guild_id, self.user_id, - ) - wallet = row[0] if row else 0 - - if wallet < bet: - await interaction.response.send_message(f"❌ Insufficient funds. You have {wallet:,} coins.", ephemeral=True) - return - - view = BlackjackView(self.cog, self.guild_id, self.user_id, bet) - embed = view.build_embed(show_dealer_card=False) - message = await interaction.channel.send(embed=embed, view=view) - view.message = message - await interaction.response.send_message("✅ Blackjack game started!", ephemeral=True) - - +""" +Interactive Gambling & RPG Cog - Casino Games with UI and Full RPG System +""" + +from __future__ import annotations + +import random +import asyncio +from typing import List, Optional, Dict + +import discord +from discord.ext import commands + +from bot.theme import NEON_CYAN, NEON_LIME, NEON_RED, NEON_GOLD, NEON_PURPLE, NEON_PINK, NEON_BLUE, panel_divider, add_banner_to_embed +from bot.emojis import ui + + +class Card: + def __init__(self, suit: str, rank: str): + self.suit = suit + self.rank = rank + self.value = self._get_value() + + def _get_value(self) -> int: + if self.rank in ['J', 'Q', 'K']: + return 10 + elif self.rank == 'A': + return 11 + else: + return int(self.rank) + + def __str__(self) -> str: + return f"{self.rank}{self.suit}" + + +class Deck: + def __init__(self): + self.cards = [] + suits = ['♠', '♥', '♦', '♣'] + ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'] + for suit in suits: + for rank in ranks: + self.cards.append(Card(suit, rank)) + random.shuffle(self.cards) + + def draw(self) -> Card: + return self.cards.pop() + + +class BlackjackView(discord.ui.View): + def __init__(self, cog, guild_id: int, user_id: int, bet: int): + super().__init__(timeout=300) + self.cog = cog + self.guild_id = guild_id + self.user_id = user_id + self.bet = bet + self.deck = Deck() + self.player_hand = [self.deck.draw(), self.deck.draw()] + self.dealer_hand = [self.deck.draw(), self.deck.draw()] + self.game_over = False + self.message: Optional[discord.Message] = None + + def hand_value(self, hand: List[Card]) -> int: + value = sum(card.value for card in hand) + aces = sum(1 for card in hand if card.rank == 'A') + while value > 21 and aces: + value -= 10 + aces -= 1 + return value + + def format_hand(self, hand: List[Card], hide_first: bool = False) -> str: + if hide_first and len(hand) >= 2: + return f"[❓❓] [{hand[1]}]" + return ' '.join([f"[{str(card)}]" for card in hand]) + + def build_embed(self, show_dealer_card: bool = True) -> discord.Embed: + player_value = self.hand_value(self.player_hand) + dealer_value = self.hand_value(self.dealer_hand) if show_dealer_card else '?' + + embed = discord.Embed( + title="🃏 Blackjack Casino", + description=panel_divider('purple'), + color=NEON_PURPLE, + ) + + dealer_display = self.format_hand(self.dealer_hand, hide_first=not show_dealer_card) + embed.add_field( + name="🎩 Dealer's Hand", + value=f"{dealer_display}\n**Value:** {dealer_value}", + inline=False, + ) + + player_display = self.format_hand(self.player_hand) + embed.add_field( + name="👤 Your Hand", + value=f"{player_display}\n**Value:** {player_value}", + inline=False, + ) + + embed.add_field(name="💰 Bet", value=f"`{self.bet:,}` coins", inline=False) + + if self.game_over: + if player_value > 21: + embed.description = f"❌ **BUST!** You lost `{self.bet:,}` coins." + embed.color = NEON_RED + elif dealer_value == 21 and len(self.dealer_hand) == 2: + embed.description = f"❌ **Dealer Blackjack!** You lost `{self.bet:,}` coins." + embed.color = NEON_RED + elif dealer_value > 21: + winnings = self.bet + embed.description = f"✅ **Dealer Busts! You Win!** +`{winnings:,}` coins!" + embed.color = NEON_LIME + elif player_value > dealer_value: + winnings = self.bet + embed.description = f"✅ **You Win!** +`{winnings:,}` coins!" + embed.color = NEON_LIME + elif player_value < dealer_value: + embed.description = f"❌ **Dealer Wins!** You lost `{self.bet:,}` coins." + embed.color = NEON_RED + else: + embed.description = f"🤝 **Push!** Bet returned." + embed.color = NEON_GOLD + + return embed + + async def end_game(self, interaction: discord.Interaction): + self.game_over = True + + engagement_cog = interaction.client.get_cog("Engagement") + if not engagement_cog: + return + + player_value = self.hand_value(self.player_hand) + dealer_value = self.hand_value(self.dealer_hand) + + if player_value > 21: + await engagement_cog._remove_coins(self.guild_id, self.user_id, self.bet) + elif dealer_value == 21 and len(self.dealer_hand) == 2: + await engagement_cog._remove_coins(self.guild_id, self.user_id, self.bet) + elif dealer_value > 21 or player_value > dealer_value: + await engagement_cog._add_coins(self.guild_id, self.user_id, self.bet) + elif player_value < dealer_value: + await engagement_cog._remove_coins(self.guild_id, self.user_id, self.bet) + + embed = self.build_embed(show_dealer_card=True) + for child in self.children: + child.disabled = True + if self.message: + await self.message.edit(embed=embed, view=self) + + @discord.ui.button(label="Hit", emoji=ui("plus"), style=discord.ButtonStyle.primary, custom_id="bj_hit") + async def hit(self, interaction: discord.Interaction, button: discord.ui.Button): + if self.game_over or interaction.user.id != self.user_id: + await interaction.response.send_message("❌ Not your game.", ephemeral=True) + return + + self.player_hand.append(self.deck.draw()) + player_value = self.hand_value(self.player_hand) + + if player_value > 21: + await self.end_game(interaction) + else: + embed = self.build_embed(show_dealer_card=False) + await interaction.response.edit_message(embed=embed, view=self) + + @discord.ui.button(label="Stand", emoji=ui("stop"), style=discord.ButtonStyle.danger, custom_id="bj_stand") + async def stand(self, interaction: discord.Interaction, button: discord.ui.Button): + if self.game_over or interaction.user.id != self.user_id: + await interaction.response.send_message("❌ Not your game.", ephemeral=True) + return + + while self.hand_value(self.dealer_hand) < 17: + self.dealer_hand.append(self.deck.draw()) + + await self.end_game(interaction) + + @discord.ui.button(label="Double Down", emoji=ui("star"), style=discord.ButtonStyle.success, custom_id="bj_double") + async def double_down(self, interaction: discord.Interaction, button: discord.ui.Button): + if self.game_over or interaction.user.id != self.user_id: + await interaction.response.send_message("❌ Not your game.", ephemeral=True) + return + + if len(self.player_hand) != 2: + await interaction.response.send_message("❌ Can only double on first turn.", ephemeral=True) + return + + engagement_cog = interaction.client.get_cog("Engagement") + if not engagement_cog: + return + + row = await interaction.client.db.fetchone( + "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?", + self.guild_id, self.user_id, + ) + wallet = row[0] if row else 0 + + if wallet < self.bet: + await interaction.response.send_message("❌ Insufficient funds to double.", ephemeral=True) + return + + self.bet *= 2 + self.player_hand.append(self.deck.draw()) + + player_value = self.hand_value(self.player_hand) + if player_value > 21: + await self.end_game(interaction) + else: + while self.hand_value(self.dealer_hand) < 17: + self.dealer_hand.append(self.deck.draw()) + await self.end_game(interaction) + + +class RouletteGameView(discord.ui.View): + def __init__(self, cog, guild_id: int, user_id: int): + super().__init__(timeout=300) + self.cog = cog + self.guild_id = guild_id + self.user_id = user_id + self.bet_amount = 0 + self.bet_type = "" + self.spinning = False + + RED_NUMBERS = [1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36] + BLACK_NUMBERS = [2, 4, 6, 8, 10, 11, 13, 15, 17, 20, 22, 24, 26, 28, 29, 31, 33, 35] + + def get_color(self, number: int) -> str: + if number == 0: + return "<:animatedarrowgreen:1477261279428087979>" + elif number in self.RED_NUMBERS: + return "🔴" + else: + return "⚫" + + def build_embed(self, number: Optional[int] = None, won: bool = False, winnings: int = 0) -> discord.Embed: + embed = discord.Embed( + title="🎰 Roulette Casino", + description=panel_divider('gold'), + color=NEON_GOLD, + ) + + if number is not None: + color_emoji = self.get_color(number) + embed.add_field(name="🎯 Result", value=f"{color_emoji} **{number}**", inline=False) + embed.add_field(name="💰 Bet", value=f"`{self.bet_amount:,}` on {self.bet_type}", inline=True) + embed.add_field(name="💵 Result", value=f"`{'+' if won else '-'}{abs(winnings):,}` coins", inline=True) + + if won: + embed.description = f"✅ **You Won!** +`{winnings:,}` coins!" + embed.color = NEON_LIME + else: + embed.description = f"❌ **You Lost!** -`{abs(winnings):,}` coins." + embed.color = NEON_RED + else: + embed.description = "Place your bet and spin the wheel!" + grid = "" + for i in range(0, 37, 3): + if i == 0: + grid += "<:animatedarrowgreen:1477261279428087979> **0** " + else: + c1 = "🔴" if i in self.RED_NUMBERS else "⚫" + c2 = "🔴" if (i+1) in self.RED_NUMBERS else "⚫" + c3 = "🔴" if (i+2) in self.RED_NUMBERS else "⚫" + grid += f"{c1} {i} {c2} {i+1} {c3} {i+2}\n" + embed.add_field(name="🎡 Roulette Wheel", value=f"```\n{grid}```", inline=False) + + return embed + + @discord.ui.button(label="Bet Red 🔴", style=discord.ButtonStyle.danger, custom_id="roulette_red") + async def bet_red(self, interaction: discord.Interaction, button: discord.ui.Button): + await self.prompt_bet(interaction, "red") + + @discord.ui.button(label="Bet Black ⚫", style=discord.ButtonStyle.gray, custom_id="roulette_black") + async def bet_black(self, interaction: discord.Interaction, button: discord.ui.Button): + await self.prompt_bet(interaction, "black") + + @discord.ui.button(label="Bet Even", style=discord.ButtonStyle.primary, custom_id="roulette_even") + async def bet_even(self, interaction: discord.Interaction, button: discord.ui.Button): + await self.prompt_bet(interaction, "even") + + @discord.ui.button(label="Bet Odd", style=discord.ButtonStyle.primary, custom_id="roulette_odd") + async def bet_odd(self, interaction: discord.Interaction, button: discord.ui.Button): + await self.prompt_bet(interaction, "odd") + + async def prompt_bet(self, interaction: discord.Interaction, bet_type: str): + if self.user_id and interaction.user.id != self.user_id: + return + await interaction.response.send_modal(RouletteBetModal(self.cog, self.guild_id, self.user_id, bet_type)) + + +class RouletteBetModal(discord.ui.Modal, title="🎰 Place Your Bet"): + bet_amount = discord.ui.TextInput( + label="Bet Amount", + placeholder="Enter amount (min 10)", + required=True, + min_length=1, + max_length=10, + ) + + def __init__(self, cog, guild_id: int, user_id: int, bet_type: str): + super().__init__() + self.cog = cog + self.guild_id = guild_id + self.user_id = user_id + self.bet_type = bet_type + + async def on_submit(self, interaction: discord.Interaction) -> None: + try: + bet = int(self.bet_amount.value.strip()) + except ValueError: + await interaction.response.send_message("❌ Invalid bet amount.", ephemeral=True) + return + + if bet < 10: + await interaction.response.send_message("❌ Minimum bet is 10 coins.", ephemeral=True) + return + + engagement_cog = interaction.client.get_cog("Engagement") + if not engagement_cog: + await interaction.response.send_message("❌ Economy not available.", ephemeral=True) + return + + row = await interaction.client.db.fetchone( + "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?", + self.guild_id, self.user_id, + ) + wallet = row[0] if row else 0 + + if wallet < bet: + await interaction.response.send_message(f"❌ Insufficient funds. You have {wallet:,} coins.", ephemeral=True) + return + + await interaction.response.defer() + + number = random.randint(0, 36) + is_red = number in RouletteGameView.RED_NUMBERS + is_black = number in RouletteGameView.BLACK_NUMBERS + is_even = number != 0 and number % 2 == 0 + is_odd = number != 0 and number % 2 == 1 + + won = False + multiplier = 2 + + if self.bet_type == 'red' and is_red: + won = True + elif self.bet_type == 'black' and is_black: + won = True + elif self.bet_type == 'even' and is_even: + won = True + elif self.bet_type == 'odd' and is_odd: + won = True + + winnings = bet * multiplier if won else -bet + + if won: + await engagement_cog._add_coins(self.guild_id, self.user_id, winnings) + else: + await engagement_cog._remove_coins(self.guild_id, self.user_id, bet) + + view = RouletteGameView(self.cog, self.guild_id, self.user_id) + view.bet_amount = bet + view.bet_type = self.bet_type + embed = view.build_embed(number=number, won=won, winnings=winnings) + + for child in view.children: + child.disabled = False + + await interaction.followup.send(embed=embed, view=view) + + +class RPGView(discord.ui.View): + def __init__(self, cog, guild_id: int, user_id: int): + super().__init__(timeout=None) + self.cog = cog + self.guild_id = guild_id + self.user_id = user_id + self.adventure_image = "https://images.unsplash.com/photo-1519074069444-1ba4fff66d16?w=500" + + def build_embed(self, title: str, description: str, color: discord.Color) -> discord.Embed: + embed = discord.Embed( + title=title, + description=description, + color=color, + ) + embed.set_image(url=self.adventure_image) + return embed + + @discord.ui.button(label="⚔️ Fight Monster", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="rpg_fight") + async def fight_monster(self, interaction: discord.Interaction, button: discord.ui.Button): + if self.user_id and interaction.user.id != self.user_id: + return + + engagement_cog = interaction.client.get_cog("Engagement") + if not engagement_cog: + await interaction.response.send_message("❌ Economy not available.", ephemeral=True) + return + + row = await interaction.client.db.fetchone( + "SELECT level FROM user_xp WHERE guild_id = ? AND user_id = ?", + self.guild_id, self.user_id, + ) + level = row[0] if row else 1 + + monsters = [ + ("Goblin", 50, 100, 20, 40), + ("Dragon", 200, 500, 50, 100), + ("Skeleton", 30, 80, 15, 30), + ("Orc", 80, 200, 25, 50), + ("Troll", 150, 350, 40, 80), + ("Demon", 300, 600, 60, 120), + ] + + monster_name, min_reward, max_reward, min_xp, max_xp = random.choice(monsters) + win_chance = min(0.8, 0.3 + (level * 0.05)) + won = random.random() < win_chance + + if won: + reward = random.randint(min_reward, max_reward) + (level * 10) + xp_gain = random.randint(min_xp, max_xp) + await engagement_cog._add_coins(self.guild_id, self.user_id, reward) + await engagement_cog.add_xp(self.guild_id, self.user_id, xp_gain) + + embed = discord.Embed( + title="⚔️ RPG Adventure - Victory!", + description=f"✅ **You defeated the {monster_name}!**\n\n💰 **+{reward:,} coins**\n⭐ **+{xp_gain} XP**\n📈 Level {level} → {level + (1 if xp_gain > 40 else 0)}", + color=NEON_LIME, + ) + embed.set_image(url="https://images.unsplash.com/photo-1535905557558-afc4877a26fc?w=500") + else: + penalty = random.randint(10, 50) + (level * 5) + await engagement_cog._remove_coins(self.guild_id, self.user_id, penalty) + + embed = discord.Embed( + title="⚔️ RPG Adventure - Defeat!", + description=f"❌ **The {monster_name} defeated you!**\n\n💀 **-{penalty:,} coins**\n\n💡 Tip: Level up to increase win chance!", + color=NEON_RED, + ) + embed.set_image(url="https://images.unsplash.com/photo-1519791883288-dc8bd696e667?w=500") + + if interaction.guild: + await add_banner_to_embed(embed, interaction.guild) + + await interaction.response.send_message(embed=embed, ephemeral=True) + + @discord.ui.button(label="🎁 Find Treasure", emoji=ui("gift"), style=discord.ButtonStyle.success, custom_id="rpg_treasure") + async def find_treasure(self, interaction: discord.Interaction, button: discord.ui.Button): + if self.user_id and interaction.user.id != self.user_id: + return + + engagement_cog = interaction.client.get_cog("Engagement") + if not engagement_cog: + await interaction.response.send_message("❌ Economy not available.", ephemeral=True) + return + + found = random.random() < 0.4 + + if found: + reward = random.randint(30, 150) + await engagement_cog._add_coins(self.guild_id, self.user_id, reward) + + embed = discord.Embed( + title="🎁 Treasure Hunt - Success!", + description=f"✅ **You found a hidden treasure!**\n\n💰 **+{reward:,} coins**\n\n🗺️ The treasure map led you to riches!", + color=NEON_GOLD, + ) + embed.set_image(url="https://images.unsplash.com/photo-1518709268805-4e9042af9f23?w=500") + else: + embed = discord.Embed( + title="🎁 Treasure Hunt - Nothing Found", + description="❌ **No treasure found in this area...**\n\n🗺️ Try exploring a different location next time!", + color=NEON_RED, + ) + embed.set_image(url="https://images.unsplash.com/photo-1500353391678-d7b57970d9b2?w=500") + + if interaction.guild: + await add_banner_to_embed(embed, interaction.guild) + + await interaction.response.send_message(embed=embed, ephemeral=True) + + @discord.ui.button(label="🏪 Shop", emoji=ui("cart"), style=discord.ButtonStyle.blurple, custom_id="rpg_shop") + async def shop(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user.id != self.user_id: + return + + embed = discord.Embed( + title="🏪 RPG Shop", + description=( + "Welcome to the shop! Buy items to help in your adventures.\n\n" + "🗡️ **Sword** - Increase win chance (Coming Soon)\n" + "🛡️ **Shield** - Reduce coin loss on defeat (Coming Soon)\n" + "🧪 **Potion** - Heal after battle (Coming Soon)\n" + "📜 **Map** - Better treasure finds (Coming Soon)" + ), + color=NEON_CYAN, + ) + embed.set_image(url="https://images.unsplash.com/photo-1589829545856-d10d557cf95f?w=500") + + if interaction.guild: + await add_banner_to_embed(embed, interaction.guild) + + await interaction.response.send_message(embed=embed, ephemeral=True) + + @discord.ui.button(label="📊 Stats", emoji=ui("stats"), style=discord.ButtonStyle.secondary, custom_id="rpg_stats") + async def stats(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user.id != self.user_id: + return + + engagement_cog = interaction.client.get_cog("Engagement") + if not engagement_cog: + return + + row = await interaction.client.db.fetchone( + "SELECT wallet, bank FROM user_balance WHERE guild_id = ? AND user_id = ?", + self.guild_id, self.user_id, + ) + wallet, bank = row if row else (0, 0) + + xp_row = await interaction.client.db.fetchone( + "SELECT xp, level FROM user_xp WHERE guild_id = ? AND user_id = ?", + self.guild_id, self.user_id, + ) + xp, level = xp_row if xp_row else (0, 1) + + embed = discord.Embed( + title="📊 RPG Stats", + description=( + f"**Level:** {level}\n" + f"**XP:** {xp:,}\n" + f"**Wallet:** {wallet:,} coins\n" + f"**Bank:** {bank:,} coins\n" + f"**Total Wealth:** {wallet + bank:,} coins" + ), + color=NEON_PURPLE, + ) + + if interaction.guild: + await add_banner_to_embed(embed, interaction.guild) + + await interaction.response.send_message(embed=embed, ephemeral=True) + + +class GamblingPanelView(discord.ui.View): + def __init__(self, cog, guild_id: int, user_id: int): + super().__init__(timeout=None) + self.cog = cog + self.guild_id = guild_id + self.user_id = user_id + + @discord.ui.button(label="🃏 Play Blackjack", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="gambling_blackjack") + async def play_blackjack(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user.id != self.user_id: + return + await interaction.response.send_modal(BlackjackBetModal(self.cog, self.guild_id, self.user_id)) + + @discord.ui.button(label="🎰 Play Roulette", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="gambling_roulette") + async def play_roulette(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user.id != self.user_id: + return + view = RouletteGameView(self.cog, self.guild_id, self.user_id) + embed = view.build_embed() + await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + + @discord.ui.button(label="⚔️ RPG Adventure", emoji=ui("game"), style=discord.ButtonStyle.success, custom_id="gambling_rpg") + async def play_rpg(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user.id != self.user_id: + return + view = RPGView(self.cog, self.guild_id, self.user_id) + embed = view.build_embed( + title="⚔️ RPG Adventure", + description="Choose your action below to begin your adventure!", + color=NEON_PURPLE, + ) + await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + + +class BlackjackBetModal(discord.ui.Modal, title="🃏 Place Your Bet"): + bet_amount = discord.ui.TextInput( + label="Bet Amount", + placeholder="Enter amount (min 10)", + required=True, + min_length=1, + max_length=10, + ) + + def __init__(self, cog, guild_id: int, user_id: int): + super().__init__() + self.cog = cog + self.guild_id = guild_id + self.user_id = user_id + + async def on_submit(self, interaction: discord.Interaction) -> None: + try: + bet = int(self.bet_amount.value.strip()) + except ValueError: + await interaction.response.send_message("❌ Invalid bet amount.", ephemeral=True) + return + + if bet < 10: + await interaction.response.send_message("❌ Minimum bet is 10 coins.", ephemeral=True) + return + + engagement_cog = interaction.client.get_cog("Engagement") + if not engagement_cog: + await interaction.response.send_message("❌ Economy not available.", ephemeral=True) + return + + row = await interaction.client.db.fetchone( + "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?", + self.guild_id, self.user_id, + ) + wallet = row[0] if row else 0 + + if wallet < bet: + await interaction.response.send_message(f"❌ Insufficient funds. You have {wallet:,} coins.", ephemeral=True) + return + + view = BlackjackView(self.cog, self.guild_id, self.user_id, bet) + embed = view.build_embed(show_dealer_card=False) + message = await interaction.channel.send(embed=embed, view=view) + view.message = message + await interaction.response.send_message("✅ Blackjack game started!", ephemeral=True) + + class Gambling(commands.Cog): - def __init__(self, bot: commands.Bot) -> None: - self.bot = bot - - @commands.hybrid_command(name="blackjack", description="Play interactive Blackjack") - async def blackjack(self, ctx: commands.Context, bet: int) -> None: - if bet < 10: - await ctx.send("❌ Minimum bet is 10 coins.", ephemeral=True) - return - - engagement_cog = self.bot.get_cog("Engagement") - if not engagement_cog: - await ctx.send("❌ Economy not available.", ephemeral=True) - return - - row = await self.bot.db.fetchone( - "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?", - ctx.guild.id if ctx.guild else 0, ctx.author.id, - ) - wallet = row[0] if row else 0 - - if wallet < bet: - await ctx.send(f"❌ Insufficient funds. You have {wallet:,} coins.", ephemeral=True) - return - - view = BlackjackView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id, bet) - embed = view.build_embed(show_dealer_card=False) - message = await ctx.send(embed=embed, view=view) - view.message = message - - @commands.hybrid_command(name="roulette", description="Play interactive Roulette") - async def roulette(self, ctx: commands.Context) -> None: - view = RouletteGameView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id) - embed = view.build_embed() - await ctx.send(embed=embed, view=view) - - @commands.hybrid_command(name="rpg", description="Start an RPG adventure") - async def rpg(self, ctx: commands.Context) -> None: - view = RPGView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id) - embed = view.build_embed( - title="⚔️ RPG Adventure", - description="Choose your action below to begin your adventure!", - color=NEON_PURPLE, - ) - await ctx.send(embed=embed, view=view, ephemeral=True) - - @commands.hybrid_command(name="gambling_panel", description="Open the gambling panel") - async def gambling_panel(self, ctx: commands.Context) -> None: - view = GamblingPanelView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id) - embed = discord.Embed( - title="🎰 Casino & Gambling Panel", - description=( - "Welcome to the Casino! Choose a game to play:\n\n" - "🃏 **Blackjack** - Beat the dealer to 21\n" - "🎰 **Roulette** - Bet on numbers and colors\n" - "⚔️ **RPG Adventure** - Fight monsters and find treasure" - ), - color=NEON_GOLD, - ) - if ctx.guild: - from bot.theme import add_banner_to_embed - await add_banner_to_embed(embed, ctx.guild) - await ctx.send(embed=embed, view=view, ephemeral=True) - - -async def setup(bot: commands.Bot) -> None: - await bot.add_cog(Gambling(bot)) \ No newline at end of file + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + @commands.hybrid_command(name="blackjack", description="Play interactive Blackjack") + async def blackjack(self, ctx: commands.Context, bet: int) -> None: + if bet < 10: + await ctx.send("❌ Minimum bet is 10 coins.", ephemeral=True) + return + + engagement_cog = self.bot.get_cog("Engagement") + if not engagement_cog: + await ctx.send("❌ Economy not available.", ephemeral=True) + return + + row = await self.bot.db.fetchone( + "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?", + ctx.guild.id if ctx.guild else 0, ctx.author.id, + ) + wallet = row[0] if row else 0 + + if wallet < bet: + await ctx.send(f"❌ Insufficient funds. You have {wallet:,} coins.", ephemeral=True) + return + + view = BlackjackView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id, bet) + embed = view.build_embed(show_dealer_card=False) + message = await ctx.send(embed=embed, view=view) + view.message = message + + @commands.hybrid_command(name="roulette", description="Play interactive Roulette") + async def roulette(self, ctx: commands.Context) -> None: + view = RouletteGameView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id) + embed = view.build_embed() + await ctx.send(embed=embed, view=view) + + @commands.hybrid_command(name="rpg", description="Start an RPG adventure") + async def rpg(self, ctx: commands.Context) -> None: + view = RPGView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id) + embed = view.build_embed( + title="⚔️ RPG Adventure", + description="Choose your action below to begin your adventure!", + color=NEON_PURPLE, + ) + await ctx.send(embed=embed, view=view, ephemeral=True) + + @commands.hybrid_command(name="gambling_panel", description="Open the gambling panel") + async def gambling_panel(self, ctx: commands.Context) -> None: + view = GamblingPanelView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id) + embed = discord.Embed( + title="🎰 Casino & Gambling Panel", + description=( + "Welcome to the Casino! Choose a game to play:\n\n" + "🃏 **Blackjack** - Beat the dealer to 21\n" + "🎰 **Roulette** - Bet on numbers and colors\n" + "⚔️ **RPG Adventure** - Fight monsters and find treasure" + ), + color=NEON_GOLD, + ) + if ctx.guild: + from bot.theme import add_banner_to_embed + await add_banner_to_embed(embed, ctx.guild) + await ctx.send(embed=embed, view=view, ephemeral=True) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Gambling(bot)) diff --git a/bot/cogs/language.py b/bot/cogs/language.py index 6b641c53c44f8b8ff3f9befd4d219bcadef9b848..aa5e2d84f579331858095170eff8bdd58c564716 100644 --- a/bot/cogs/language.py +++ b/bot/cogs/language.py @@ -23,6 +23,7 @@ LANGUAGE_META: dict[str, tuple[str, str]] = { "id": ("Indonesia", "🇮🇩"), "ja": ("日本語", "🇯🇵"), "zh": ("中文", "🇨🇳"), + "he": ("עברית", "🇮🇱"), } @@ -32,7 +33,10 @@ class LanguageSelect(discord.ui.Select): self.guild_id = guild_id options: list[discord.SelectOption] = [] - for code in SUPPORTED_LANGUAGES: + dynamic_supported = sorted( + getattr(getattr(cog.bot, "translator", None), "supported_languages", set(SUPPORTED_LANGUAGES)) + ) + for code in dynamic_supported: label, emoji = LANGUAGE_META.get(code, (code.upper(), "🌐")) options.append( discord.SelectOption( @@ -58,6 +62,17 @@ class LanguageSelect(discord.ui.Select): return selected = self.values[0] + + # Special rule requested by owner: Hebrew is visible but cannot be set. + if selected == "he": + current_code = await self.cog._current_code(interaction.guild.id) + for option in self.options: + option.default = option.value == current_code + embed = await self.cog._language_embed(interaction.guild.id, current_code) + await interaction.response.edit_message(embed=embed, view=self.view) + await interaction.followup.send("only you know this language", ephemeral=True) + return + await self.cog.bot.db.execute( "INSERT INTO guild_config(guild_id, guild_language) VALUES (?, ?) " "ON CONFLICT(guild_id) DO UPDATE SET guild_language = excluded.guild_language", @@ -98,14 +113,18 @@ class Language(commands.Cog): async def _current_code(self, guild_id: int) -> str: row = await self.bot.db.fetchone("SELECT guild_language FROM guild_config WHERE guild_id = ?", guild_id) - return row[0] if row and row[0] in LANGUAGE_META else "ar" + supported = set(getattr(getattr(self.bot, "translator", None), "supported_languages", set(SUPPORTED_LANGUAGES))) + return row[0] if row and row[0] in supported else "ar" async def _language_embed(self, guild_id: int, current_code: str) -> discord.Embed: current_name = LANGUAGE_META.get(current_code, (current_code.upper(), "🌐"))[0] current_emoji = LANGUAGE_META.get(current_code, (current_code.upper(), "🌐"))[1] lines = [] - for code in LANGUAGE_META: + supported = sorted( + getattr(getattr(self.bot, "translator", None), "supported_languages", set(SUPPORTED_LANGUAGES)) + ) + for code in supported: name, emoji = LANGUAGE_META.get(code, (code.upper(), "🌐")) marker = "✅" if code == current_code else "▫️" lines.append(f"{marker} {emoji} **{name}** `({code})`") diff --git a/bot/cogs/media.py b/bot/cogs/media.py index 2fde109f1a6cc643e3a9b26955cb9e19cfca52d7..30501c5e37bd7a5bc43dd021e45006189aed1ac9 100644 --- a/bot/cogs/media.py +++ b/bot/cogs/media.py @@ -87,12 +87,21 @@ if wavelink is not None: else: self._connection_event.set() -from bot.theme import NEON_CYAN, NEON_ORANGE, NEON_LIME, NEON_PURPLE, panel_divider +from bot.theme import ( + NEON_CYAN, + NEON_ORANGE, + NEON_LIME, + NEON_PURPLE, + panel_divider, + idle_embed_for_guild, + idle_text, + add_banner_to_embed, +) from bot.emojis import resolve_emoji_value, set_emoji_bot # Import views from separate module from .media_helpers import ( - MusicPanelView, QueueView, FiltersView, FiltersPanelView, + MusicPanelView, QueueView, FiltersView, FiltersPanelView, AudioActionsView, AUDIO_FILTERS, get_filter_emoji, safe_defer, safe_send, safe_edit, safe_interaction ) @@ -469,44 +478,6 @@ class SuggestionSelect(discord.ui.Select): return selected = self.suggestions[selected_index] - - # Get guild and player - guild = interaction.guild - if not guild: - await interaction.followup.send("Server only.", ephemeral=True) - return - - player = guild.voice_client - if not player or not isinstance(player, wavelink.Player): - await interaction.followup.send("Not connected to voice.", ephemeral=True) - return - - # Add ALL suggestions to wavelink queue first (in order) - for idx, item in enumerate(self.suggestions): - try: - search_q = item.query if item.query else f"ytsearch1:{item.title}" - results = await wavelink.Playable.search(search_q) - if results: - await player.queue.put_wait(results[0]) - except Exception: - continue - - # Now play the selected track immediately by moving it to front - # Get the selected track from queue and move to front - queue_list = list(player.queue) - if selected_index < len(queue_list): - selected_track = queue_list.pop(selected_index) - # Clear queue and re-add with selected first - player.queue._queue.clear() - player.queue._queue.append(selected_track) - for track in queue_list: - player.queue._queue.append(track) - - # Play the selected track - if player.queue: - track_to_play = player.queue.get() - await player.play(track_to_play) - choice = (selected.query or selected.title).strip() result = await self.cog.play_from_query(interaction, choice) await interaction.followup.send( @@ -741,6 +712,9 @@ class Media(commands.Cog): or os.getenv("YT_API_KEY", "").strip() ) self._youtube_region_code = (os.getenv("YOUTUBE_REGION_CODE", "US").strip() or "US").upper() + self._playlist_track_limit = max(50, min(500, int((os.getenv("PLAYLIST_TRACK_LIMIT", "300").strip() or "300")))) + self._playlist_batch_sleep = max(0.05, min(1.0, float((os.getenv("PLAYLIST_BATCH_SLEEP", "0.35").strip() or "0.35")))) + self._resolved_url_query_cache: dict[str, str] = {} if hasattr(self.bot, "logger"): if self._ffmpeg_path: @@ -751,6 +725,7 @@ class Media(commands.Cog): async def cog_load(self) -> None: """Set up event listeners for wavelink.""" self.bot.add_view(MusicPanelView(self)) + self.bot.add_view(AudioActionsView(self)) # Register wavelink event listeners if wavelink is not None: @@ -1182,6 +1157,7 @@ class Media(commands.Cog): def _to_lavalink_identifier(self, query: str) -> str: normalized = self._sanitize_query(query) + forced_search = False prefixes = ("ytsearch1:", "ytsearch:", "ytmsearch:") while True: lowered = normalized.casefold() @@ -1189,19 +1165,124 @@ class Media(commands.Cog): for prefix in prefixes: if lowered.startswith(prefix): normalized = normalized[len(prefix):].strip() + forced_search = True matched = True break if not matched: break + if forced_search: + return f"ytsearch:{normalized}" if self._looks_like_url(normalized): + lowered = normalized.casefold() + if "open.spotify.com/track/" in lowered or "music.apple.com/" in lowered: + return f"ytsearch:{normalized}" return normalized return f"ytsearch:{normalized}" + async def _resolve_music_url_to_search_term(self, raw_url: str) -> str | None: + url = (raw_url or "").strip() + if not url: + return None + cached = self._resolved_url_query_cache.get(url) + if cached: + return cached + + lowered = url.casefold() + timeout = aiohttp.ClientTimeout(total=5) + + # Spotify: lightweight oEmbed endpoint (no auth token needed for title/artist string). + if "open.spotify.com/track/" in lowered: + try: + oembed = f"https://open.spotify.com/oembed?url={quote_plus(url)}" + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.get(oembed) as resp: + payload = await resp.json(content_type=None) if resp.status == 200 else None + if isinstance(payload, dict): + title = str(payload.get("title") or "").strip() + author = str(payload.get("author_name") or "").strip() + term = f"{author} - {title}".strip(" -") + if term: + self._resolved_url_query_cache[url] = term + return term + except Exception: + pass + + # Apple Music track URLs usually include iTunes id-like token (/id123456789). + if "music.apple.com/" in lowered: + match = re.search(r"/id(\d+)", url) + if match: + try: + lookup = f"https://itunes.apple.com/lookup?id={match.group(1)}" + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.get(lookup) as resp: + payload = await resp.json(content_type=None) if resp.status == 200 else None + if isinstance(payload, dict): + rows = payload.get("results") or [] + if rows and isinstance(rows[0], dict): + artist = str(rows[0].get("artistName") or "").strip() + title = str(rows[0].get("trackName") or rows[0].get("collectionName") or "").strip() + term = f"{artist} - {title}".strip(" -") + if term: + self._resolved_url_query_cache[url] = term + return term + except Exception: + pass + return None + + async def _identifier_fallbacks(self, identifier: str) -> list[str]: + primary = (identifier or "").strip() + if not primary: + return [] + candidates: list[str] = [primary] + + plain = primary + if plain.lower().startswith("ytsearch:"): + plain = plain[len("ytsearch:") :].strip() + + if self._looks_like_url(plain): + term = await self._resolve_music_url_to_search_term(plain) + if term: + alt = f"ytsearch:{term}" + if alt not in candidates: + candidates.append(alt) + + return candidates + + async def _search_playable_with_retry(self, identifier: str, *, attempts: int = 3) -> list[object]: + if wavelink is None: + return [] + last_error: Exception | None = None + candidates = await self._identifier_fallbacks(identifier) + for candidate in candidates: + for attempt in range(1, max(1, attempts) + 1): + try: + return await wavelink.Playable.search(candidate) + except Exception as exc: + last_error = exc + text = str(exc).lower() + if "429" not in text and "rate" not in text: + break + await asyncio.sleep((0.35 * attempt) + random.uniform(0.05, 0.2)) + if last_error is not None: + raise last_error + return [] + def _guild_state(self, guild_id: int) -> GuildPlaybackState: if guild_id not in self.state: self.state[guild_id] = GuildPlaybackState() return self.state[guild_id] + async def _safe_ctx_defer(self, ctx: commands.Context, *, ephemeral: bool = False) -> None: + interaction = getattr(ctx, "interaction", None) + if not interaction: + return + try: + if interaction.response.is_done(): + return + await interaction.response.defer(ephemeral=ephemeral) + except (discord.InteractionResponded, discord.NotFound, discord.HTTPException): + return + async def _dj_permitted(self, ctx_or_interaction: commands.Context | discord.Interaction) -> bool: return True @@ -1535,7 +1616,13 @@ class Media(commands.Cog): continue direct = str(entry.get("url") or entry.get("webpage_url") or "").strip() vid = str(entry.get("id") or "").strip() + title = str(entry.get("title") or "").strip() + uploader = str(entry.get("uploader") or entry.get("channel") or "").strip() if direct and direct.startswith("http"): + if "open.spotify.com/track/" in direct and title: + query = f"{uploader} - {title}" if uploader else title + urls.append(f"ytsearch1:{query}") + continue urls.append(direct) continue if direct and not direct.startswith("http") and "youtube" in playlist_url: @@ -1543,9 +1630,14 @@ class Media(commands.Cog): continue if vid: urls.append(f"https://www.youtube.com/watch?v={vid}") + continue + if title: + query = f"{uploader} - {title}" if uploader else title + urls.append(f"ytsearch1:{query}") return urls - return await asyncio.to_thread(_run) + urls = await asyncio.to_thread(_run) + return [u for u in urls if u][: max(1, min(limit, self._playlist_track_limit))] async def _resolve_query_with_ytdlp(self, query: str) -> str | None: """Resolve a text query to a direct video URL using yt-dlp search.""" @@ -1656,7 +1748,11 @@ class Media(commands.Cog): lines.append(f"*...and {len(queue) - 5} more*") embed.add_field(name=up_next, value="\n".join(lines), inline=False) else: - embed.add_field(name=up_next, value="Queue is empty.", inline=False) + embed.add_field( + name=up_next, + value=idle_text("Queue is empty.", "Add tracks with `/music play` or the music panel."), + inline=False, + ) status_icon = "📡" loop_text = "Off" if state.loop_mode == "off" else ("Track" if state.loop_mode == "track" else "Queue") @@ -1674,6 +1770,8 @@ class Media(commands.Cog): server_name = guild.name if guild else "Server" embed.set_footer(text=f"⛩️ 〣 🔄 Auto-refreshing every 10s • {server_name} 〣 🏮") + if guild: + await add_banner_to_embed(embed, guild, self.bot) return embed @@ -1807,7 +1905,7 @@ class Media(commands.Cog): self.now_playing.pop(guild.id, None) if lang == "ar": return "⏭️ تم التخطي. الطابور فارغ." - return "⏭️ Skipped. Queue is empty." + return idle_text("Queue is empty.", "Skipped current track. Nothing is queued.") # For non-wavelink players if not self._voice_is_playing(player): @@ -1982,7 +2080,7 @@ class Media(commands.Cog): tracks = ([now] if now else []) + list(state_queue) uris = [t.webpage_url for t in tracks if t and t.webpage_url] if not uris: - return "Queue is empty." + return idle_text("Queue is empty.", "Play songs first, then try saving again.") cleaned_name = (playlist_name or "quicksave").strip()[:40] await self.bot.db.execute( "INSERT INTO saved_playlists(user_id, name, tracks_json) VALUES (?, ?, ?) " @@ -2000,7 +2098,7 @@ class Media(commands.Cog): ) embed = discord.Embed(title="💾 Saved Playlists", color=NEON_CYAN) if not rows: - embed.description = "No playlists saved yet. Use the **Save Queue** button." + embed.description = idle_text("No playlists saved yet.", "Use the **Save Queue** button.") return embed lines: list[str] = [] for name, tracks_json, created_at in rows: @@ -2057,16 +2155,17 @@ class Media(commands.Cog): self.queues.setdefault(guild.id, []) count = 0 first_track: str | None = None - for url in urls[:100]: + for idx, url in enumerate(urls[: self._playlist_track_limit], start=1): try: - results = await wavelink.Playable.search(url) + search_identifier = self._to_lavalink_identifier(url) + results = await self._search_playable_with_retry(search_identifier, attempts=3) except Exception as exc: await self._log_media_issue(guild, "saved_playlist_play_search", url, exc) continue if not results: continue wl_items = list(results.tracks) if (wavelink is not None and isinstance(results, wavelink.Playlist)) else list(results) - for wl_track in wl_items[:100]: + for wl_track in wl_items[: self._playlist_track_limit]: track = self._wavelink_to_track(wl_track, actor.id) if count == 0 and not player.playing and not player.paused: await player.play(wl_track, volume=self._guild_state(guild.id).volume) @@ -2076,6 +2175,11 @@ class Media(commands.Cog): self.queues[guild.id].append(track) await player.queue.put_wait(wl_track) count += 1 + # Smooth out provider/API bursts for large playlist loads. + if count % 5 == 0: + await asyncio.sleep(self._playlist_batch_sleep) + if idx % 3 == 0: + await asyncio.sleep(self._playlist_batch_sleep) if count == 0: if lang == "ar": @@ -2180,7 +2284,32 @@ class Media(commands.Cog): urls: list[str] = [] if self._looks_like_playlist(source): - urls = await self._extract_playlist_entries(source, limit=100) + urls = await self._extract_playlist_entries(source, limit=self._playlist_track_limit) + if not urls: + # Secondary fallback: try Lavalink playlist parsing and store track URIs. + try: + lavalink_results = await self._search_playable_with_retry( + self._to_lavalink_identifier(source), + attempts=2, + ) + except Exception: + lavalink_results = [] + if lavalink_results: + items = ( + list(lavalink_results.tracks) + if (wavelink is not None and isinstance(lavalink_results, wavelink.Playlist)) + else list(lavalink_results) + ) + for item in items[: self._playlist_track_limit]: + uri = str(getattr(item, "uri", "") or getattr(item, "url", "") or "").strip() + if uri: + urls.append(uri) + else: + title = str(getattr(item, "title", "") or "").strip() + author = str(getattr(item, "author", "") or "").strip() + if title: + term = f"{author} - {title}".strip(" -") + urls.append(f"ytsearch:{term}") else: chosen = source if not self._looks_like_url(source): @@ -2190,7 +2319,7 @@ class Media(commands.Cog): if chosen: urls = [chosen] - urls = [u for u in urls if u][:100] + urls = [u for u in urls if u][: self._playlist_track_limit] if not urls: if lang == "ar": return "تعذر استخراج مقاطع للحفظ." @@ -2252,7 +2381,7 @@ class Media(commands.Cog): tracks = self._get_saved_playlist(guild_id) embed = discord.Embed(title="📚 Saved Playlist", color=NEON_CYAN) if not tracks: - embed.description = "No saved tracks yet. Use `/music playlist_add `." + embed.description = idle_text("No saved tracks yet.", "Use `/music playlist_add `.") return embed lines = [ f"**{idx}.** {track.title[:70]} • `{track.format_duration()}`" @@ -2467,20 +2596,22 @@ class Media(commands.Cog): ) if is_supported_playlist: try: - pre_extracted_urls = await self._extract_playlist_entries(playlist_query, limit=100) + pre_extracted_urls = await self._extract_playlist_entries(playlist_query, limit=self._playlist_track_limit) except Exception: pre_extracted_urls = [] try: if pre_extracted_urls: results = [] - for url in pre_extracted_urls: - found = await wavelink.Playable.search(url) + for index, url in enumerate(pre_extracted_urls, start=1): + found = await self._search_playable_with_retry(self._to_lavalink_identifier(url), attempts=3) if found: results.append(found[0]) + if index % 3 == 0: + await asyncio.sleep(self._playlist_batch_sleep) else: # Search for playlist - results = await wavelink.Playable.search(playlist_query) + results = await self._search_playable_with_retry(self._to_lavalink_identifier(playlist_query), attempts=3) except Exception: if lang == "ar": return "تعذر تحميل قائمة التشغيل." @@ -2505,7 +2636,7 @@ class Media(commands.Cog): first = True # Process tracks in order - for wl_track in tracks_list[:100]: # Limit to 100 tracks + for wl_track in tracks_list[: self._playlist_track_limit]: try: track = self._wavelink_to_track(wl_track, actor.id) @@ -2552,8 +2683,7 @@ class Media(commands.Cog): @commands.hybrid_group(name="music", fallback="panel", description="Music controls", with_app_command=False) async def music_group(self, ctx: commands.Context) -> None: - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.interaction.response.defer(ephemeral=True) + await self._safe_ctx_defer(ctx, ephemeral=True) guild_id = ctx.guild.id if ctx.guild else None embed = await self._music_panel_embed(guild_id) panel_view = MusicPanelView(self, guild_id) @@ -2563,8 +2693,7 @@ class Media(commands.Cog): @commands.hybrid_command(name="music_panel", description="Open interactive music panel") async def music_panel(self, ctx: commands.Context) -> None: - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.interaction.response.defer(ephemeral=True) + await self._safe_ctx_defer(ctx, ephemeral=True) guild_id = ctx.guild.id if ctx.guild else None embed = await self._music_panel_embed(guild_id) panel_view = MusicPanelView(self, guild_id) @@ -2586,14 +2715,12 @@ class Media(commands.Cog): guild_id = ctx.guild.id if ctx.guild else None embed = await self._music_panel_embed(guild_id) panel_view = MusicPanelView(self, guild_id) - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.interaction.response.defer(ephemeral=True) + await self._safe_ctx_defer(ctx, ephemeral=True) panel_msg = await (ctx.interaction.followup.send("Use the panel below or provide a query:", embed=embed, view=panel_view, wait=True) if ctx.interaction else ctx.reply("Use the panel below or provide a query:", embed=embed, view=panel_view)) if isinstance(panel_msg, discord.Message): await panel_view.start_auto_refresh(panel_msg) return - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.defer() + await self._safe_ctx_defer(ctx) if not self._looks_like_url(query): suggestions = await self._unified_play_search(query, limit=10) if suggestions: @@ -2678,22 +2805,19 @@ class Media(commands.Cog): @music_group.command(name="playlist_add", description="Add a track to saved playlist only") async def music_playlist_add(self, ctx: commands.Context, *, query: str) -> None: - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.defer() + await self._safe_ctx_defer(ctx) result = await self.add_query_to_saved_playlist(ctx, query) await ctx.reply(result) @music_group.command(name="playlist_play", description="Play one of your saved playlists by name") async def music_playlist_play(self, ctx: commands.Context, *, name: str) -> None: - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.defer() + await self._safe_ctx_defer(ctx) result = await self.play_user_saved_playlist(ctx, name) await ctx.reply(result) @music_group.command(name="playlist_save", description="Save URL/query (track or playlist) into named playlist") async def music_playlist_save(self, ctx: commands.Context, name: str, *, source: str) -> None: - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.defer(ephemeral=True) + await self._safe_ctx_defer(ctx, ephemeral=True) result = await self.save_query_to_named_playlist(ctx, name, source) if ctx.interaction: await ctx.interaction.followup.send(result, ephemeral=True) @@ -2702,8 +2826,7 @@ class Media(commands.Cog): @music_group.command(name="playlist_delete", description="Delete one of your saved playlists by name") async def music_playlist_delete(self, ctx: commands.Context, *, name: str) -> None: - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.defer(ephemeral=True) + await self._safe_ctx_defer(ctx, ephemeral=True) result = await self.delete_user_playlist(ctx, name) if ctx.interaction: await ctx.interaction.followup.send(result, ephemeral=True) @@ -2712,8 +2835,7 @@ class Media(commands.Cog): @music_group.command(name="playlist_rename", description="Rename one of your saved playlists") async def music_playlist_rename(self, ctx: commands.Context, old_name: str, *, new_name: str) -> None: - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.defer(ephemeral=True) + await self._safe_ctx_defer(ctx, ephemeral=True) result = await self.rename_user_playlist(ctx, old_name, new_name) if ctx.interaction: await ctx.interaction.followup.send(result, ephemeral=True) @@ -2725,11 +2847,18 @@ class Media(commands.Cog): if not ctx.guild: await ctx.reply("Server only.") return - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.defer() + await self._safe_ctx_defer(ctx) normalized = self._sanitize_query(query) if not normalized: - await ctx.reply("Type a search query.") + await ctx.reply( + embed=await idle_embed_for_guild( + "YouTube Search Idle", + "Type a search query first.", + "Example: /music yt_search weeknd blinding lights", + guild=ctx.guild, + bot=self.bot, + ) + ) return used_api = bool((self._youtube_api_key or "").strip()) @@ -2749,7 +2878,15 @@ class Media(commands.Cog): ) await ctx.reply("No direct YouTube results found. Try one of these:", embed=fallback_embed) return - await ctx.reply("No YouTube results found.") + await ctx.reply( + embed=await idle_embed_for_guild( + "No Results", + "No YouTube results were found for this query.", + "Try a different title, artist name, or direct URL.", + guild=ctx.guild, + bot=self.bot, + ) + ) return embed = discord.Embed( @@ -2771,8 +2908,7 @@ class Media(commands.Cog): @commands.hybrid_command(name="playlists", description="View your saved playlists") async def playlists(self, ctx: commands.Context) -> None: - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.defer(ephemeral=True) + await self._safe_ctx_defer(ctx, ephemeral=True) embed = await self.user_playlists_embed(ctx.author.id) if ctx.interaction: await ctx.interaction.followup.send(embed=embed, ephemeral=True) @@ -2792,7 +2928,15 @@ class Media(commands.Cog): return queue = self.queues.get(ctx.guild.id, []) if len(queue) < 2: - await ctx.reply("Need at least 2 tracks to shuffle.") + await ctx.reply( + embed=await idle_embed_for_guild( + "Shuffle Idle", + "Need at least 2 tracks in queue to shuffle.", + "Add more tracks first, then run /music shuffle.", + guild=ctx.guild, + bot=self.bot, + ) + ) return random.shuffle(queue) @@ -3007,7 +3151,15 @@ class Media(commands.Cog): if lang == "ar": await ctx.reply("❌ الطابور فارغ.") else: - await ctx.reply("❌ Queue is empty.") + await ctx.reply( + embed=await idle_embed_for_guild( + "Queue Idle", + "Queue is empty. Nothing to move.", + "Use /music play to add tracks.", + guild=ctx.guild, + bot=self.bot, + ) + ) return # Convert to 0-indexed @@ -3062,7 +3214,15 @@ class Media(commands.Cog): if lang == "ar": await ctx.reply("❌ الطابور فارغ.") else: - await ctx.reply("❌ Queue is empty.") + await ctx.reply( + embed=await idle_embed_for_guild( + "Queue Idle", + "Queue is empty. Nothing to swap.", + "Use /music play to add tracks.", + guild=ctx.guild, + bot=self.bot, + ) + ) return # Convert to 0-indexed @@ -3115,7 +3275,15 @@ class Media(commands.Cog): if lang == "ar": await ctx.reply("❌ الطابور فارغ.") else: - await ctx.reply("❌ Queue is empty.") + await ctx.reply( + embed=await idle_embed_for_guild( + "Queue Idle", + "Queue is empty. Nothing to remove.", + "Use /music play to add tracks.", + guild=ctx.guild, + bot=self.bot, + ) + ) return idx = position - 1 @@ -3164,7 +3332,15 @@ class Media(commands.Cog): if lang == "ar": await ctx.reply("❌ الطابور فارغ.") else: - await ctx.reply("❌ Queue is empty.") + await ctx.reply( + embed=await idle_embed_for_guild( + "Queue Idle", + "Queue is empty. Nothing to jump to.", + "Use /music play to add tracks.", + guild=ctx.guild, + bot=self.bot, + ) + ) return idx = position - 1 diff --git a/bot/cogs/media_helpers.py b/bot/cogs/media_helpers.py index d7d0f32e07c188614c104909739eb58e89879742..7dae852e3d8e7de44cb2bde8339b641f1d8bff18 100644 --- a/bot/cogs/media_helpers.py +++ b/bot/cogs/media_helpers.py @@ -22,7 +22,7 @@ except Exception: from bot.theme import ( NEON_CYAN, NEON_PURPLE, NEON_ORANGE, NEON_LIME, NEON_PINK, panel_divider, fancy_divider, fancy_header, progress_bar, - music_embed, music_now_playing, music_status, beautiful_list + music_embed, music_now_playing, music_status, beautiful_list, idle_embed_for_guild, idle_text, add_banner_to_embed ) def _emoji(key: str, default: str) -> str: @@ -896,7 +896,13 @@ class MusicPanelView(discord.ui.View, AutoRefreshMixin): return queue = self.cog.queues.get(interaction.guild.id, []) if len(queue) < 2: - await safe_send(interaction, "Need at least 2 tracks to shuffle.") + await safe_send(interaction, embed=await idle_embed_for_guild( + "Shuffle Idle", + "Need at least 2 tracks in queue to shuffle.", + "Add tracks from Play/Search, then retry.", + guild=interaction.guild, + bot=self.cog.bot, + )) return random.shuffle(queue) player = interaction.guild.voice_client @@ -1175,9 +1181,12 @@ class QueueView(discord.ui.View, AutoRefreshMixin): else: embed.add_field( name="📋 Up Next", - value="Add tracks with `/play`!\n" + fancy_divider('music'), + value=idle_text("Queue is empty.", "Add tracks with `/music play`.\n" + fancy_divider('music')), inline=False ) + guild = self.cog.bot.get_guild(self.guild_id) if hasattr(self.cog, "bot") else None + if guild: + await add_banner_to_embed(embed, guild, self.cog.bot) return embed diff --git a/bot/cogs/menu.py b/bot/cogs/menu.py index dcc0e394bc548b3a6461fd37fc80c1fb71f8e9da..8a8f1501612066bfb37ba7ab86c4656c775c7008 100644 --- a/bot/cogs/menu.py +++ b/bot/cogs/menu.py @@ -1,768 +1,819 @@ -""" -Menu cog: Interactive command explorer with beautiful panels. -Enhanced with stunning decorations, rich formatting, and multi-language support. -""" - -import random - -import discord -from discord.ext import commands - -from bot.cogs.ai_suite import ImperialMotaz -from bot.emojis import ui, E_DIAMOND, E_STAR, E_FIRE, E_SPARKLE, E_GEM, E_CROWN -from bot.theme import ( - fancy_header, pick_neon_color, progress_bar, shimmer, panel_divider, - NEON_CYAN, NEON_PINK, NEON_PURPLE, NEON_LIME, NEON_YELLOW, NEON_BLUE, - NEON_GOLD, NEON_MAGENTA, - double_line, triple_line, mega_title, fancy_divider, animated_header, - gradient_header, panel_title, section_header, quick_stats_grid -) -from bot.i18n import translate, t - - -# Beautiful unicode emojis for select menu categories -_CATEGORY_EMOJIS = { - "Music": "🎵", - "Admin": "🛡️", - "Fun": "🎮", - "AI": "🤖", - "Utility": "🔧", - "Config": "⚙️", - "Economy": "💰", - "Moderation": "⚔️", - "Tickets": "🎫", - "Welcome": "👋", - "Giveaway": "🎁", - "Verification": "✅", - "Tournament": "🏆", - "Games": "🎲", - "Level": "✅", - "AutoMod": "🤖", - "Logs": "📋", - "DJ": "🎧", - "Developer": "💻", - "default": "📁", -} - -# Category descriptions with emojis -_CATEGORY_DESCRIPTIONS = { - "Music": "🎵 Music commands for playback control", - "Admin": "🛡️ Server administration tools", - "Fun": "🎮 Fun games and entertainment", - "AI": "🤖 AI-powered features", - "Utility": "🔧 Useful utility commands", - "Config": "⚙️ Bot configuration settings", - "Economy": "💰 Economy and currency system", - "Moderation": "⚔️ Moderation commands", - "Tickets": "🎫 Ticket support system", - "Welcome": "👋 Welcome and goodbye messages", - "Giveaway": "🎁 Giveaway management", - "Verification": "✅ Member verification", - "Tournament": "🏆 Tournament brackets", - "Games": "🎲 Mini games and fun", - "Level": "✅ XP and leveling system", - "AutoMod": "🤖 Auto-moderation features", - "Logs": "📋 Logging configuration", - "DJ": "🎧 DJ music controls", - "Developer": "💻 Developer tools", -} - -_CATEGORY_BILINGUAL = { - "__all__": "📚 All Commands | جميع الأوامر", - "__ai__": "🤖 AI | الذكاء الاصطناعي", - "Music": "🎵 Music | الموسيقى", - "Admin": "🛡️ Admin | الإدارة", - "Fun": "🎮 Games | الألعاب", - "AI": "🤖 AI | الذكاء الاصطناعي", - "Utility": "🔧 Utility | الأدوات", - "Config": "⚙️ Config | الإعدادات", - "Economy": "💰 Economy | الاقتصاد", - "Moderation": "⚔️ Moderation | الإشراف", - "Community": "💡 Community | المجتمع", - "AISuite": "🤖 AI Suite | الذكاء", - "Configuration": "⚙️ Configuration | الإعدادات", - "Events": "📋 Events | الأحداث", - "Verification": "✅ Verification | التحقق", -} - - -def _bilingual_category(name: str) -> str: - return _CATEGORY_BILINGUAL.get(name, f"📁 {name} | {name}") - - -class CommandsMenuSelect(discord.ui.Select): - """Beautiful dropdown menu for selecting command categories.""" - - def __init__(self, cog_names: list[str], all_label: str, ai_label: str, lang: str = "en", selected_cog: str = "__all__") -> None: - self.lang = lang - - # Localized descriptions - all_desc = translate(lang, "menu.select_all_desc") - ai_desc = translate(lang, "menu.select_ai_desc") - - options = [ - discord.SelectOption( - label=_bilingual_category("__all__"), - description=all_desc, - emoji=ui("book"), - value="__all__", - default=selected_cog == "__all__", - ), - discord.SelectOption( - label=_bilingual_category("__ai__"), - description=ai_desc, - emoji=ui("robot"), - value="__ai__", - default=selected_cog == "__ai__", - ), - ] - - # Add cog options with beautiful emojis - for name in sorted(cog_names): - emoji = _CATEGORY_EMOJIS.get(name, _CATEGORY_EMOJIS["default"]) - desc = translate(lang, f"menu.category_desc.{name.lower()}") - if desc == f"menu.category_desc.{name.lower()}": - desc = _CATEGORY_DESCRIPTIONS.get(name, f"📁 {name} commands") - # Truncate description to fit Discord's limits - desc = desc[:100] - options.append( - discord.SelectOption( - label=_bilingual_category(name)[:100], - description=desc, - emoji=emoji, - value=name, - default=selected_cog == name, - ) - ) - - placeholder = translate(lang, "menu.select_placeholder") - super().__init__( - placeholder=placeholder, - min_values=1, - max_values=1, - options=options - ) - - async def callback(self, interaction: discord.Interaction) -> None: - await interaction.response.defer() - view = self.view - if not isinstance(view, CommandsMenuView): - return - selected = self.values[0] - view.selected_cog = selected - view.page = 0 - await view.setup_items() - embed = await view.build_embed(interaction.guild.id if interaction.guild else None, selected) - await interaction.followup.edit_message(interaction.message.id, embed=embed, view=view) - - -class CommandsMenuView(discord.ui.View): - """Beautiful command menu view with rich decorations.""" - - def __init__(self, bot: commands.Bot, guild_id: int | None = None) -> None: - super().__init__(timeout=None) - self.bot = bot - self.guild_id = guild_id - self.selected_cog = "__all__" - self.page = 0 - self.page_size = 18 - self._accent_seed = random.randrange(0, 1024) - self._color_palette = [NEON_CYAN, NEON_PINK, NEON_PURPLE, NEON_LIME, NEON_GOLD, NEON_MAGENTA] - - async def setup_items(self) -> None: - """Setup the menu items.""" - cog_names = list(self.bot.cogs.keys()) - lang = await self.bot.get_guild_language(self.guild_id) - all_label = translate(lang, "menu.all") - ai_label = translate(lang, "menu.ai") - self.clear_items() - self.add_item(CommandsMenuSelect(cog_names, all_label, ai_label, lang, self.selected_cog)) - - refresh_label = translate(lang, "menu.refresh") - invite_label = translate(lang, "menu.invite_button") - self.add_item(RefreshButton(self, refresh_label)) - self.add_item(QuickCategoryButton(self, "Economy | الاقتصاد", "Economy", "💰", row=1)) - self.add_item(QuickCategoryButton(self, "Music | الموسيقى", "Music", "🎵", row=1)) - self.add_item(QuickCategoryButton(self, "Admin | الإدارة", "Admin", "🛡️", row=1)) - self.add_item(QuickCategoryButton(self, "Utility | الأدوات", "Utility", "ℹ️", row=1)) - self.add_item(QuickCategoryButton(self, "Community | المجتمع", "Community", "💡", row=2)) - self.add_item(QuickCategoryButton(self, "AI | الذكاء", "AISuite", "🤖", row=2)) - self.add_item(QuickCategoryButton(self, "Config | الإعداد", "Configuration", "⚙️", row=2)) - if self.page > 0: - self.add_item(PageButton(self, "prev")) - if self._has_next_page(self.selected_cog): - self.add_item(PageButton(self, "next")) - - if self.bot.user: - self.add_item(InviteButton(invite_label, self.bot.user.id)) - - def _desc_key(self, qualified_name: str) -> str: - """Get the translation key for a command description.""" - norm = qualified_name.strip().lower().replace(" ", "_") - key_mapping = { - # Music commands - "play": "menu.cmd.play", - "music_play": "menu.cmd.music_play", - "music_panel": "menu.cmd.music_panel", - "music_skip": "menu.cmd.music_skip", - "music_stop": "menu.cmd.music_stop", - "music_pause": "menu.cmd.music_pause", - "music_resume": "menu.cmd.music_resume", - "music_queue": "menu.cmd.music_queue", - "music_playlist": "menu.cmd.music_playlist", - "music_playlist_save": "menu.cmd.music_playlist_save", - "music_playlist_rename": "menu.cmd.music_playlist_rename", - "music_playlist_delete": "menu.cmd.music_playlist_delete", - "music_volume": "menu.cmd.music_volume", - "music_nowplaying": "menu.cmd.music_nowplaying", - "music_filter": "menu.cmd.music_filter", - "music_loop": "menu.cmd.music_loop", - "music_shuffle": "menu.cmd.music_shuffle", - "music_247": "menu.cmd.music_247", - "music_previous": "menu.cmd.music_previous", - "music_seek": "menu.cmd.music_seek", - "music_clear": "menu.cmd.music_clear", - "music_remove": "menu.cmd.music_remove", - "music_move": "menu.cmd.music_move", - "music_jump": "menu.cmd.music_jump", - "music_lyrics": "menu.cmd.music_lyrics", - - # Admin commands - "purge": "menu.cmd.purge", - "ban": "menu.cmd.ban", - "unban": "menu.cmd.unban", - "kick": "menu.cmd.kick", - "mute": "menu.cmd.mute", - "unmute": "menu.cmd.unmute", - "warn": "menu.cmd.warn", - "warnings": "menu.cmd.warnings", - "clearwarn": "menu.cmd.clearwarn", - "slowmode": "menu.cmd.slowmode", - "lock": "menu.cmd.lock", - "unlock": "menu.cmd.unlock", - "cloneemoji": "menu.cmd.cloneemoji", - "awesomeroles": "menu.cmd.awesomeroles", - "backupserver": "menu.cmd.backupserver", - - # Fun commands - "8ball": "menu.cmd.8ball", - "meme": "menu.cmd.meme", - "trivia": "menu.cmd.trivia", - "gaming_news": "menu.cmd.gaming_news", - "free_games": "menu.cmd.free_games", - "gamehub": "menu.cmd.gamehub", - "xo": "menu.cmd.xo", - "choose": "menu.cmd.choose", - "mario": "menu.cmd.mario", - "dice": "menu.cmd.dice", - "slots": "menu.cmd.slots", - "coinflip": "menu.cmd.coinflip", - "roll": "menu.cmd.roll", - - # AI commands - "chat": "menu.cmd.chat", - "ask_image": "menu.cmd.ask_image", - "imagine": "menu.cmd.imagine", - "image_gen": "menu.cmd.image_gen", - "upscale": "menu.cmd.upscale", - "summarize": "menu.cmd.summarize", - "ai": "menu.cmd.ai", - "ai_chat": "menu.cmd.chat", - "ai_ask_image": "menu.cmd.ask_image", - "ai_imagine": "menu.cmd.imagine", - "ai_image_gen": "menu.cmd.image_gen", - "ai_upscale": "menu.cmd.upscale", - "ai_summarize": "menu.cmd.summarize", - "ai_debug": "menu.cmd.debug", - "ai_code_gen": "menu.cmd.code_gen", - "ai_voice": "menu.cmd.speak", - "ai_speak": "menu.cmd.speak", - "ai_tts": "menu.cmd.tts", - "ai_translate_voice": "menu.cmd.translate_voice", - "ai_model": "menu.cmd.ai_model", - "ai_channel": "menu.cmd.ai_channel", - "ai_auto": "menu.cmd.ai_auto", - "ai_setup": "menu.cmd.ai", - "ai_execute": "menu.cmd.ai_execute", - - # Utility commands - "serverinfo": "menu.cmd.serverinfo", - "userinfo": "menu.cmd.userinfo", - "botstats": "menu.cmd.botstats", - "poll": "menu.cmd.poll", - "remind": "menu.cmd.remind", - "avatar": "menu.cmd.avatar", - "banner": "menu.cmd.banner", - "translate": "menu.cmd.translate", - - # Config commands - "set": "menu.cmd.set", - "set_log": "menu.cmd.set_log", - "set_welcome": "menu.cmd.set_welcome", - "set_suggestions": "menu.cmd.set_suggestions", - "set_automod": "menu.cmd.set_automod", - "set_dailymessage": "menu.cmd.set_dailymessage", - "set_dailychannel": "menu.cmd.set_dailychannel", - "set_dailytime": "menu.cmd.set_dailytime", - "set_dailytitle": "menu.cmd.set_dailytitle", - "set_dailyimage": "menu.cmd.set_dailyimage", - "set_dailybutton": "menu.cmd.set_dailybutton", - "set_dailytoggle": "menu.cmd.set_dailytoggle", - "set_pollchannel": "menu.cmd.set_pollchannel", - "set_freegames": "menu.cmd.set_freegames", - "set_gamenews": "menu.cmd.set_gamenews", - "set_supportai": "menu.cmd.set_supportai", - "set_wisdom": "menu.cmd.set_wisdom", - - # Language commands - "language": "menu.cmd.language", - "languages": "menu.cmd.languages", - - # Menu commands - "menu": "menu.cmd.menu", - "ping": "menu.cmd.ping", - "load": "menu.cmd.load", - "unload": "menu.cmd.unload", - "reload": "menu.cmd.reload", - "sync": "menu.cmd.sync", - "shutdown": "menu.cmd.shutdown", - - # Verification commands - "verify": "menu.cmd.verify", - "verifysetup": "menu.cmd.verifysetup", - "verify_config": "menu.cmd.verify", - "verify_config_panel": "menu.cmd.verify", - "verify_config_setup": "menu.cmd.verifysetup", - - # Economy commands - "economy": "menu.cmd.economy", - "economy_deposit": "menu.cmd.economy_deposit", - "economy_withdraw": "menu.cmd.economy_withdraw", - "economy_gamble": "menu.cmd.economy_gamble", - "economy_rob": "menu.cmd.economy_rob", - "balance": "menu.cmd.balance", - "daily": "menu.cmd.daily", - "work": "menu.cmd.work", - "gamble": "menu.cmd.gamble", - "rob": "menu.cmd.rob", - "leaderboard": "menu.cmd.leaderboard", - "shop": "menu.cmd.shop", - "buy": "menu.cmd.buy", - "inventory": "menu.cmd.inventory", - "transfer": "menu.cmd.transfer", - - # Engagement commands - "xp": "menu.cmd.xp", - "level": "menu.cmd.level", - "rank": "menu.cmd.rank", - "suggest": "menu.cmd.suggest", - "suggestion": "menu.cmd.suggestion", - "suggestion_show": "menu.cmd.suggestion_show", - "suggestion_panel": "menu.cmd.suggestion_panel", - "suggestion_voters": "menu.cmd.suggestion_voters", - "giveaway_create": "menu.cmd.giveaway_create", - "giveaway_end": "menu.cmd.giveaway_end", - "ticket_panel": "menu.cmd.ticket_panel", - - # Tournament commands - "tournament": "menu.cmd.tournament", - "tournament_panel": "menu.cmd.tournament_panel", - "tournament_create": "menu.cmd.tournament_create", - "tournament_join": "menu.cmd.tournament_join", - "tournament_start": "menu.cmd.tournament_start", - "tournament_end": "menu.cmd.tournament_end", - "tournament_gamehub": "menu.cmd.tournament_gamehub", - - # Gambling commands - "blackjack": "menu.cmd.blackjack", - "roulette": "menu.cmd.roulette", - "rpg": "menu.cmd.rpg", - +""" +Menu cog: Interactive command explorer with beautiful panels. +Enhanced with stunning decorations, rich formatting, and multi-language support. +""" + +import random + +import discord +from discord.ext import commands + +from bot.cogs.ai_suite import ImperialMotaz +from bot.emojis import ui, E_DIAMOND, E_STAR, E_FIRE, E_SPARKLE, E_GEM, E_CROWN +from bot.theme import ( + fancy_header, pick_neon_color, progress_bar, shimmer, panel_divider, + NEON_CYAN, NEON_PINK, NEON_PURPLE, NEON_LIME, NEON_YELLOW, NEON_BLUE, + NEON_GOLD, NEON_MAGENTA, + double_line, triple_line, mega_title, fancy_divider, animated_header, + gradient_header, panel_title, section_header, quick_stats_grid +) + + +# Beautiful unicode emojis for select menu categories +_CATEGORY_EMOJIS = { + "Music": "🎵", + "Admin": "🛡️", + "Fun": "🎮", + "AI": "🤖", + "Utility": "🔧", + "Config": "⚙️", + "Economy": "💰", + "Moderation": "⚔️", + "Tickets": "🎫", + "Welcome": "👋", + "Giveaway": "🎁", + "Verification": "✅", + "Tournament": "🏆", + "Games": "🎲", + "Level": "✅", + "AutoMod": "🤖", + "Logs": "📋", + "DJ": "🎧", + "Developer": "💻", + "default": "📁", +} + +# Category descriptions with emojis +_CATEGORY_DESCRIPTIONS = { + "Music": "🎵 Music commands for playback control", + "Admin": "🛡️ Server administration tools", + "Fun": "🎮 Fun games and entertainment", + "AI": "🤖 AI-powered features", + "Utility": "🔧 Useful utility commands", + "Config": "⚙️ Bot configuration settings", + "Economy": "💰 Economy and currency system", + "Moderation": "⚔️ Moderation commands", + "Tickets": "🎫 Ticket support system", + "Welcome": "👋 Welcome and goodbye messages", + "Giveaway": "🎁 Giveaway management", + "Verification": "✅ Member verification", + "Tournament": "🏆 Tournament brackets", + "Games": "🎲 Mini games and fun", + "Level": "✅ XP and leveling system", + "AutoMod": "🤖 Auto-moderation features", + "Logs": "📋 Logging configuration", + "DJ": "🎧 DJ music controls", + "Developer": "💻 Developer tools", +} + +_CATEGORY_BILINGUAL = { + "__all__": "📚 All Commands | جميع الأوامر", + "__ai__": "🤖 AI | الذكاء الاصطناعي", + "Music": "🎵 Music | الموسيقى", + "Admin": "🛡️ Admin | الإدارة", + "Fun": "🎮 Games | الألعاب", + "AI": "🤖 AI | الذكاء الاصطناعي", + "Utility": "🔧 Utility | الأدوات", + "Config": "⚙️ Config | الإعدادات", + "Economy": "💰 Economy | الاقتصاد", + "Moderation": "⚔️ Moderation | الإشراف", + "Community": "💡 Community | المجتمع", + "AISuite": "🤖 AI Suite | الذكاء", + "Configuration": "⚙️ Configuration | الإعدادات", + "Events": "📋 Events | الأحداث", + "Verification": "✅ Verification | التحقق", +} + + +def _bilingual_category(name: str) -> str: + return _CATEGORY_BILINGUAL.get(name, f"📁 {name} | {name}") + + +class CommandsMenuSelect(discord.ui.Select): + """Beautiful dropdown menu for selecting command categories.""" + + def __init__( + self, + cog_names: list[str], + all_label: str, + ai_label: str, + *, + placeholder: str, + all_desc: str, + ai_desc: str, + cog_desc_map: dict[str, str], + selected_cog: str = "__all__", + ) -> None: + + options = [ + discord.SelectOption( + label=_bilingual_category("__all__"), + description=all_desc, + emoji=ui("book"), + value="__all__", + default=selected_cog == "__all__", + ), + discord.SelectOption( + label=_bilingual_category("__ai__"), + description=ai_desc, + emoji=ui("robot"), + value="__ai__", + default=selected_cog == "__ai__", + ), + ] + + # Add cog options with beautiful emojis + for name in sorted(cog_names): + emoji = _CATEGORY_EMOJIS.get(name, _CATEGORY_EMOJIS["default"]) + desc = cog_desc_map.get(name, _CATEGORY_DESCRIPTIONS.get(name, f"{name} commands")) + # Truncate description to fit Discord's limits + desc = desc[:100] + options.append( + discord.SelectOption( + label=_bilingual_category(name)[:100], + description=desc, + emoji=emoji, + value=name, + default=selected_cog == name, + ) + ) + + super().__init__( + placeholder=placeholder, + min_values=1, + max_values=1, + options=options + ) + + async def callback(self, interaction: discord.Interaction) -> None: + await interaction.response.defer() + view = self.view + if not isinstance(view, CommandsMenuView): + return + selected = self.values[0] + view.selected_cog = selected + view.page = 0 + await view.setup_items() + embed = await view.build_embed(interaction.guild.id if interaction.guild else None, selected) + await interaction.followup.edit_message(interaction.message.id, embed=embed, view=view) + + +class CommandsMenuView(discord.ui.View): + """Beautiful command menu view with rich decorations.""" + + def __init__(self, bot: commands.Bot, guild_id: int | None = None) -> None: + super().__init__(timeout=None) + self.bot = bot + self.guild_id = guild_id + self.selected_cog = "__all__" + self.page = 0 + self.page_size = 18 + self._accent_seed = random.randrange(0, 1024) + self._color_palette = [NEON_CYAN, NEON_PINK, NEON_PURPLE, NEON_LIME, NEON_GOLD, NEON_MAGENTA] + + async def setup_items(self) -> None: + """Setup the menu items.""" + cog_names = list(self.bot.cogs.keys()) + all_label = await self.bot.get_text(self.guild_id, "menu.all") + ai_label = await self.bot.get_text(self.guild_id, "menu.ai") + placeholder = await self.bot.get_text(self.guild_id, "menu.select_placeholder") + all_desc = await self.bot.get_text(self.guild_id, "menu.select_all_desc") + ai_desc = await self.bot.get_text(self.guild_id, "menu.select_ai_desc") + cog_desc_map: dict[str, str] = {} + for name in cog_names: + key = f"menu.category_desc.{name.lower()}" + desc = await self.bot.get_text(self.guild_id, key) + if desc == key: + desc = _CATEGORY_DESCRIPTIONS.get(name, f"📁 {name} commands") + cog_desc_map[name] = desc + self.clear_items() + self.add_item( + CommandsMenuSelect( + cog_names, + all_label, + ai_label, + placeholder=placeholder, + all_desc=all_desc, + ai_desc=ai_desc, + cog_desc_map=cog_desc_map, + selected_cog=self.selected_cog, + ) + ) + + refresh_label = await self.bot.get_text(self.guild_id, "menu.refresh") + invite_label = await self.bot.get_text(self.guild_id, "menu.invite_button") + self.add_item(RefreshButton(self, refresh_label)) + self.add_item(QuickCategoryButton(self, "Economy | الاقتصاد", "Economy", "💰", row=1)) + self.add_item(QuickCategoryButton(self, "Music | الموسيقى", "Music", "🎵", row=1)) + self.add_item(QuickCategoryButton(self, "Admin | الإدارة", "Admin", "🛡️", row=1)) + self.add_item(QuickCategoryButton(self, "Utility | الأدوات", "Utility", "ℹ️", row=1)) + self.add_item(QuickCategoryButton(self, "Community | المجتمع", "Community", "💡", row=2)) + self.add_item(QuickCategoryButton(self, "AI | الذكاء", "AISuite", "🤖", row=2)) + self.add_item(QuickCategoryButton(self, "Config | الإعداد", "Configuration", "⚙️", row=2)) + if self.page > 0: + self.add_item(PageButton(self, "prev")) + if self._has_next_page(self.selected_cog): + self.add_item(PageButton(self, "next")) + + if self.bot.user: + self.add_item(InviteButton(invite_label, self.bot.user.id)) + + def _desc_key(self, qualified_name: str) -> str: + """Get the translation key for a command description.""" + norm = qualified_name.strip().lower().replace(" ", "_") + key_mapping = { + # Music commands + "play": "menu.cmd.play", + "music_play": "menu.cmd.music_play", + "music_panel": "menu.cmd.music_panel", + "music_skip": "menu.cmd.music_skip", + "music_stop": "menu.cmd.music_stop", + "music_pause": "menu.cmd.music_pause", + "music_resume": "menu.cmd.music_resume", + "music_queue": "menu.cmd.music_queue", + "music_playlist": "menu.cmd.music_playlist", + "music_playlist_save": "menu.cmd.music_playlist_save", + "music_playlist_rename": "menu.cmd.music_playlist_rename", + "music_playlist_delete": "menu.cmd.music_playlist_delete", + "music_volume": "menu.cmd.music_volume", + "music_nowplaying": "menu.cmd.music_nowplaying", + "music_filter": "menu.cmd.music_filter", + "music_loop": "menu.cmd.music_loop", + "music_shuffle": "menu.cmd.music_shuffle", + "music_247": "menu.cmd.music_247", + "music_previous": "menu.cmd.music_previous", + "music_seek": "menu.cmd.music_seek", + "music_clear": "menu.cmd.music_clear", + "music_remove": "menu.cmd.music_remove", + "music_move": "menu.cmd.music_move", + "music_jump": "menu.cmd.music_jump", + "music_lyrics": "menu.cmd.music_lyrics", + + # Admin commands + "purge": "menu.cmd.purge", + "ban": "menu.cmd.ban", + "unban": "menu.cmd.unban", + "kick": "menu.cmd.kick", + "mute": "menu.cmd.mute", + "unmute": "menu.cmd.unmute", + "warn": "menu.cmd.warn", + "warnings": "menu.cmd.warnings", + "clearwarn": "menu.cmd.clearwarn", + "slowmode": "menu.cmd.slowmode", + "lock": "menu.cmd.lock", + "unlock": "menu.cmd.unlock", + "cloneemoji": "menu.cmd.cloneemoji", + "awesomeroles": "menu.cmd.awesomeroles", + "backupserver": "menu.cmd.backupserver", + + # Fun commands + "8ball": "menu.cmd.8ball", + "meme": "menu.cmd.meme", + "trivia": "menu.cmd.trivia", + "gaming_news": "menu.cmd.gaming_news", + "free_games": "menu.cmd.free_games", + "gamehub": "menu.cmd.gamehub", + "xo": "menu.cmd.xo", + "choose": "menu.cmd.choose", + "mario": "menu.cmd.mario", + "dice": "menu.cmd.dice", + "slots": "menu.cmd.slots", + "coinflip": "menu.cmd.coinflip", + "roll": "menu.cmd.roll", + + # AI commands + "chat": "menu.cmd.chat", + "ask_image": "menu.cmd.ask_image", + "imagine": "menu.cmd.imagine", + "image_gen": "menu.cmd.image_gen", + "upscale": "menu.cmd.upscale", + "summarize": "menu.cmd.summarize", + "ai": "menu.cmd.ai", + "ai_chat": "menu.cmd.chat", + "ai_ask_image": "menu.cmd.ask_image", + "ai_imagine": "menu.cmd.imagine", + "ai_image_gen": "menu.cmd.image_gen", + "ai_upscale": "menu.cmd.upscale", + "ai_summarize": "menu.cmd.summarize", + "ai_debug": "menu.cmd.debug", + "ai_code_gen": "menu.cmd.code_gen", + "ai_voice": "menu.cmd.speak", + "ai_speak": "menu.cmd.speak", + "ai_tts": "menu.cmd.tts", + "ai_translate_voice": "menu.cmd.translate_voice", + "ai_model": "menu.cmd.ai_model", + "ai_channel": "menu.cmd.ai_channel", + "ai_auto": "menu.cmd.ai_auto", + "ai_setup": "menu.cmd.ai", + "ai_execute": "menu.cmd.ai_execute", + + # Utility commands + "serverinfo": "menu.cmd.serverinfo", + "userinfo": "menu.cmd.userinfo", + "botstats": "menu.cmd.botstats", + "poll": "menu.cmd.poll", + "remind": "menu.cmd.remind", + "avatar": "menu.cmd.avatar", + "banner": "menu.cmd.banner", + "translate": "menu.cmd.translate", + + # Config commands + "set": "menu.cmd.set", + "set_log": "menu.cmd.set_log", + "set_welcome": "menu.cmd.set_welcome", + "set_suggestions": "menu.cmd.set_suggestions", + "set_automod": "menu.cmd.set_automod", + "set_dailymessage": "menu.cmd.set_dailymessage", + "set_dailychannel": "menu.cmd.set_dailychannel", + "set_dailytime": "menu.cmd.set_dailytime", + "set_dailytitle": "menu.cmd.set_dailytitle", + "set_dailyimage": "menu.cmd.set_dailyimage", + "set_dailybutton": "menu.cmd.set_dailybutton", + "set_dailytoggle": "menu.cmd.set_dailytoggle", + "set_pollchannel": "menu.cmd.set_pollchannel", + "set_freegames": "menu.cmd.set_freegames", + "set_gamenews": "menu.cmd.set_gamenews", + "set_supportai": "menu.cmd.set_supportai", + "set_wisdom": "menu.cmd.set_wisdom", + + # Language commands + "language": "menu.cmd.language", + "languages": "menu.cmd.languages", + + # Menu commands + "menu": "menu.cmd.menu", + "ping": "menu.cmd.ping", + "load": "menu.cmd.load", + "unload": "menu.cmd.unload", + "reload": "menu.cmd.reload", + "sync": "menu.cmd.sync", + "shutdown": "menu.cmd.shutdown", + + # Verification commands + "verify": "menu.cmd.verify", + "verifysetup": "menu.cmd.verifysetup", + "verify_config": "menu.cmd.verify", + "verify_config_panel": "menu.cmd.verify", + "verify_config_setup": "menu.cmd.verifysetup", + + # Economy commands + "economy": "menu.cmd.economy", + "economy_deposit": "menu.cmd.economy_deposit", + "economy_withdraw": "menu.cmd.economy_withdraw", + "economy_gamble": "menu.cmd.economy_gamble", + "economy_rob": "menu.cmd.economy_rob", + "balance": "menu.cmd.balance", + "daily": "menu.cmd.daily", + "work": "menu.cmd.work", + "gamble": "menu.cmd.gamble", + "rob": "menu.cmd.rob", + "leaderboard": "menu.cmd.leaderboard", + "shop": "menu.cmd.shop", + "buy": "menu.cmd.buy", + "inventory": "menu.cmd.inventory", + "transfer": "menu.cmd.transfer", + + # Engagement commands + "xp": "menu.cmd.xp", + "level": "menu.cmd.level", + "rank": "menu.cmd.rank", + "suggest": "menu.cmd.suggest", + "suggestion": "menu.cmd.suggestion", + "suggestion_show": "menu.cmd.suggestion_show", + "suggestion_panel": "menu.cmd.suggestion_panel", + "suggestion_voters": "menu.cmd.suggestion_voters", + "giveaway_create": "menu.cmd.giveaway_create", + "giveaway_end": "menu.cmd.giveaway_end", + "ticket_panel": "menu.cmd.ticket_panel", + + # Tournament commands + "tournament": "menu.cmd.tournament", + "tournament_panel": "menu.cmd.tournament_panel", + "tournament_create": "menu.cmd.tournament_create", + "tournament_join": "menu.cmd.tournament_join", + "tournament_start": "menu.cmd.tournament_start", + "tournament_end": "menu.cmd.tournament_end", + "tournament_gamehub": "menu.cmd.tournament_gamehub", + + # Gambling commands + "blackjack": "menu.cmd.blackjack", + "roulette": "menu.cmd.roulette", + "rpg": "menu.cmd.rpg", + # AI Admin commands "ai_admin": "menu.cmd.ai_admin", "ai_help": "menu.cmd.ai_help", + # Additional commands/groups to keep menu fully mapped + "admin": "menu.cmd.admin_panel", + "admin_panel": "menu.cmd.admin_panel", + "shield_level": "menu.cmd.shield_level", + "shield_state": "menu.cmd.shield_state", + "econ_admin": "menu.cmd.econ_admin", + "econadmin": "menu.cmd.econ_admin", + "economy_admin": "menu.cmd.econ_admin", + "giveaway": "menu.cmd.giveaway", + "ticket": "menu.cmd.ticket", + "poll_legacy": "menu.cmd.poll", + "setpollchannel": "menu.cmd.set_pollchannel", + "setsuggestionchannel": "menu.cmd.set_suggestions", + "set_freegame": "menu.cmd.set_freegames", + "setupserver": "menu.cmd.setupserver", + "organizechannels": "menu.cmd.organizechannels", + "backup_panel": "menu.cmd.backup_panel", + "system_audit": "menu.cmd.system_audit", + "wisdom_today": "menu.cmd.wisdom_today", + "freegames": "menu.cmd.free_games", + "playlists": "menu.cmd.music_playlist", + "music": "menu.cmd.music_panel", + "profile": "menu.cmd.profile", + "rps": "menu.cmd.rps", + "guess": "menu.cmd.guess", + "make_event": "menu.cmd.make_event", + "gambling_panel": "menu.cmd.gambling_panel", + "add_scam_image": "menu.cmd.add_scam_image", + "set_banner": "menu.cmd.set_banner", + "view_banner": "menu.cmd.view_banner", + "remove_banner": "menu.cmd.remove_banner", + "banner_help": "menu.cmd.banner_help", + "boardgames": "menu.cmd.boardgames", + "board_start": "menu.cmd.board_start", + "board_move": "menu.cmd.board_move", + "board_forfeit": "menu.cmd.board_forfeit", + "games_panel": "menu.cmd.games_panel", + "chess": "menu.cmd.chess", + "checkers": "menu.cmd.checkers", + "connect4": "menu.cmd.connect4", + "othello": "menu.cmd.othello", + "start": "menu.cmd.menu", + "command_fill": "menu.cmd.menu", + "tournament_lb": "menu.cmd.tournament_lb", + "aisetup": "menu.cmd.ai", } - return key_mapping.get(norm, f"menu.cmd.{norm}") - - async def _format_commands(self, guild_id: int | None, cmds: list[commands.Command], max_lines: int = 20, max_chars: int = 980) -> str: - """Format commands list with beautiful decorations.""" - lang = await self.bot.get_guild_language(guild_id) - lines: list[str] = [] - used = 0 - - for idx, cmd in enumerate(sorted(cmds, key=lambda c: c.qualified_name), start=1): - if cmd.hidden: - continue - - # Get translated description - desc_key = self._desc_key(cmd.qualified_name) - translated_desc = translate(lang, desc_key) - - if translated_desc == desc_key: - fallback_desc = (cmd.description or cmd.help or "").strip() - desc = fallback_desc if fallback_desc else "—" - else: - desc = translated_desc - - # Hybrid commands are shown with slash syntax; prefix-only commands use ! - is_hybrid = isinstance(cmd, commands.HybridCommand) - if is_hybrid: - invoke = f"`/{cmd.qualified_name}`" - else: - invoke = f"`!{cmd.qualified_name}`" - line = f"• {invoke} — {desc[:78]}" - projected = used + len(line) + (1 if lines else 0) - - if projected > max_chars: - break - lines.append(line) - used = projected - - if len(lines) >= max_lines: - break - + return key_mapping.get(norm, f"menu.cmd.{norm}") + + async def _format_commands(self, guild_id: int | None, cmds: list[commands.Command], max_lines: int = 20, max_chars: int = 980) -> str: + """Format commands list with beautiful decorations.""" + lines: list[str] = [] + used = 0 + + for idx, cmd in enumerate(sorted(cmds, key=lambda c: c.qualified_name), start=1): + if cmd.hidden: + continue + + desc_key = self._desc_key(cmd.qualified_name) + translated_desc = await self.bot.get_text(guild_id, desc_key) + json_key = "" + if translated_desc == desc_key: + cog_key = (cmd.cog_name or "").lower() + json_key = f"commands.{cog_key}.{cmd.name}_desc" if cog_key else "" + if json_key: + translated_desc = await self.bot.get_text(guild_id, json_key) + + if translated_desc == desc_key or (json_key and translated_desc == json_key): + fallback_desc = (cmd.description or cmd.help or "").strip() + desc = fallback_desc if fallback_desc else "-" + else: + desc = translated_desc + + is_hybrid = isinstance(cmd, commands.HybridCommand) + invoke = f"`/{cmd.qualified_name}`" if is_hybrid else f"`!{cmd.qualified_name}`" + line = f"- {invoke} - {desc[:78]}" + projected = used + len(line) + (1 if lines else 0) + + if projected > max_chars: + break + lines.append(line) + used = projected + + if len(lines) >= max_lines: + break + if len(lines) < len([c for c in cmds if not c.hidden]): - suffix = "\n• ┈┈┈┈┈┈┈┈┈┈┈┈" + suffix = "\n- ------------" if used + len(suffix) <= max_chars: - lines.append("• ┈┈┈┈┈┈┈┈┈┈┈┈") - - return "\n".join(lines) - - def _visible_commands_page(self, selected_cog: str) -> tuple[list[commands.Command], int]: - commands_list = sorted(self._selected_commands(selected_cog), key=lambda c: c.qualified_name) - if not commands_list: - return [], 1 - total_pages = max(1, (len(commands_list) + self.page_size - 1) // self.page_size) - if self.page >= total_pages: - self.page = total_pages - 1 - start = self.page * self.page_size - return commands_list[start:start + self.page_size], total_pages - - def _has_next_page(self, selected_cog: str) -> bool: - commands_list = self._selected_commands(selected_cog) - total_pages = max(1, (len(commands_list) + self.page_size - 1) // self.page_size) - return self.page + 1 < total_pages - - def _category_stats(self) -> list[tuple[str, int]]: - """Get category statistics.""" - stats: list[tuple[str, int]] = [] - for name, cog in self.bot.cogs.items(): - cog_commands = [c for c in (cog.get_commands() if cog else []) if not c.hidden] - if cog_commands: - stats.append((name, len(cog_commands))) - return sorted(stats, key=lambda pair: (-pair[1], pair[0])) + lines.append("- ------------") - async def _category_value(self, stats: list[tuple[str, int]], guild_id: int | None) -> str: - """Format category statistics with beautiful decorations.""" - lang = await self.bot.get_guild_language(guild_id) - if not stats: - return translate(lang, "menu.none") - - lines: list[str] = [] - for index, (name, count) in enumerate(stats[:8], start=1): - badge = {1: "🥇", 2: "🥈", 3: "🥉"}.get(index, f"#{index}") - emoji = _CATEGORY_EMOJIS.get(name, "📁") - - if lang == "ar": - lines.append(f"{badge} {emoji} **{name}** — `{count}` أمر") - else: - lines.append(f"{badge} {emoji} **{name}** — `{count}` cmds") - - if len(stats) > 8: - lines.append("✦ ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈") - return "\n".join(lines) - - async def _stats_text( - self, - guild_id: int | None, - total_commands: int, - category_count: int, - selection_label: str, - visible_commands: int, - ) -> str: - """Format statistics with beautiful decorations.""" - lang = await self.bot.get_guild_language(guild_id) - - if lang == "ar": - pieces = [ - f"📊 **إجمالي الأوامر:** `{total_commands}`", - f"📁 **الفئات:** `{category_count}`", - f"✨ **المحدد:** {selection_label} (`{visible_commands}` أمر)", - f"📈 {progress_bar(visible_commands, total_commands, size=14)}", - ] - else: - pieces = [ - f"📊 **Total Commands:** `{total_commands}`", - f"📁 **Categories:** `{category_count}`", - f"✨ **Selected:** {selection_label} (`{visible_commands}` cmds)", - f"📈 {progress_bar(visible_commands, total_commands, size=14)}", - ] - return "\n".join(pieces) - - async def _selected_label(self, guild_id: int | None, selected_cog: str) -> str: - """Get the label for the selected category.""" - return _bilingual_category(selected_cog) - - def _selected_commands(self, selected_cog: str) -> list[commands.Command]: - """Get commands for the selected category.""" - def _flatten(cmds: list[commands.Command]) -> list[commands.Command]: - collected: list[commands.Command] = [] - for command in cmds: - if command.hidden: - continue - collected.append(command) - if isinstance(command, commands.Group): - collected.extend([sub for sub in command.walk_commands() if not sub.hidden]) - unique: dict[str, commands.Command] = {c.qualified_name: c for c in collected} - return list(unique.values()) - - if selected_cog == "__all__": - return _flatten(list(self.bot.commands)) - if selected_cog == "__ai__": - ai_cog = self.bot.get_cog("AISuite") - return _flatten(list(ai_cog.get_commands() if ai_cog else [])) - cog = self.bot.get_cog(selected_cog) - return _flatten(list(cog.get_commands() if cog else [])) - - def _top_level_slash_count(self) -> int: - """Count top-level slash-registered hybrid commands/groups (Discord limit: 100).""" - total = 0 - for cmd in self.bot.commands: - if not isinstance(cmd, commands.HybridCommand): - continue - if getattr(cmd, "with_app_command", True): - total += 1 - return total - - async def build_embed(self, guild_id: int | None, selected_cog: str) -> discord.Embed: - """Build the beautiful embed for the menu.""" - lang = await self.bot.get_guild_language(guild_id) - color = discord.Color(0x2B2D31) - - title = "BOT- AI System" - desc = ( - f"{translate(lang, 'menu.hub_welcome')}\n" - f"{translate(lang, 'menu.hub_explore')}\n" - f"{translate(lang, 'menu.hub_usage')}" - ) - stats_title = translate(lang, "menu.stats_heading") - categories_title = translate(lang, "menu.categories_heading") - tips_title = translate(lang, "menu.tips_heading") - quick_title = translate(lang, "menu.quick_heading") - updates_title = translate(lang, "menu.updates_heading") - tips = ( - f"• {translate(lang, 'menu.tip_line_1')}\n" - f"• {translate(lang, 'menu.tip_line_2')}\n" - f"• {translate(lang, 'menu.tip_line_3')}" - ) + + def _visible_commands_page(self, selected_cog: str) -> tuple[list[commands.Command], int]: + commands_list = sorted(self._selected_commands(selected_cog), key=lambda c: c.qualified_name) + if not commands_list: + return [], 1 + total_pages = max(1, (len(commands_list) + self.page_size - 1) // self.page_size) + if self.page >= total_pages: + self.page = total_pages - 1 + start = self.page * self.page_size + return commands_list[start:start + self.page_size], total_pages + + def _has_next_page(self, selected_cog: str) -> bool: + commands_list = self._selected_commands(selected_cog) + total_pages = max(1, (len(commands_list) + self.page_size - 1) // self.page_size) + return self.page + 1 < total_pages + + def _category_stats(self) -> list[tuple[str, int]]: + """Get category statistics.""" + stats: list[tuple[str, int]] = [] + for name, cog in self.bot.cogs.items(): + cog_commands = [c for c in (cog.get_commands() if cog else []) if not c.hidden] + if cog_commands: + stats.append((name, len(cog_commands))) + return sorted(stats, key=lambda pair: (-pair[1], pair[0])) + + async def _category_value(self, stats: list[tuple[str, int]], guild_id: int | None) -> str: + """Format category statistics with beautiful decorations.""" + lang = await self.bot.get_guild_language(guild_id) + if not stats: + return await self.bot.get_text(guild_id, "menu.none") + + lines: list[str] = [] + for index, (name, count) in enumerate(stats[:8], start=1): + badge = {1: "🥇", 2: "🥈", 3: "🥉"}.get(index, f"#{index}") + emoji = _CATEGORY_EMOJIS.get(name, "📁") + + if lang == "ar": + lines.append(f"{badge} {emoji} **{name}** — `{count}` أمر") + else: + lines.append(f"{badge} {emoji} **{name}** — `{count}` cmds") + + if len(stats) > 8: + lines.append("✦ ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈") + + return "\n".join(lines) + + async def _stats_text( + self, + guild_id: int | None, + total_commands: int, + category_count: int, + selection_label: str, + visible_commands: int, + ) -> str: + """Format statistics with beautiful decorations.""" + lang = await self.bot.get_guild_language(guild_id) + + if lang == "ar": + pieces = [ + f"📊 **إجمالي الأوامر:** `{total_commands}`", + f"📁 **الفئات:** `{category_count}`", + f"✨ **المحدد:** {selection_label} (`{visible_commands}` أمر)", + f"📈 {progress_bar(visible_commands, total_commands, size=14)}", + ] + else: + pieces = [ + f"📊 **Total Commands:** `{total_commands}`", + f"📁 **Categories:** `{category_count}`", + f"✨ **Selected:** {selection_label} (`{visible_commands}` cmds)", + f"📈 {progress_bar(visible_commands, total_commands, size=14)}", + ] + return "\n".join(pieces) + + async def _selected_label(self, guild_id: int | None, selected_cog: str) -> str: + """Get the label for the selected category.""" + return _bilingual_category(selected_cog) + + def _selected_commands(self, selected_cog: str) -> list[commands.Command]: + """Get commands for the selected category.""" + def _flatten(cmds: list[commands.Command]) -> list[commands.Command]: + collected: list[commands.Command] = [] + for command in cmds: + if command.hidden: + continue + collected.append(command) + if isinstance(command, commands.Group): + collected.extend([sub for sub in command.walk_commands() if not sub.hidden]) + unique: dict[str, commands.Command] = {c.qualified_name: c for c in collected} + return list(unique.values()) + + if selected_cog == "__all__": + return _flatten(list(self.bot.commands)) + if selected_cog == "__ai__": + ai_cog = self.bot.get_cog("AISuite") + return _flatten(list(ai_cog.get_commands() if ai_cog else [])) + cog = self.bot.get_cog(selected_cog) + return _flatten(list(cog.get_commands() if cog else [])) + + def _top_level_slash_count(self) -> int: + """Count top-level slash-registered hybrid commands/groups (Discord limit: 100).""" + total = 0 + for cmd in self.bot.commands: + if not isinstance(cmd, commands.HybridCommand): + continue + if getattr(cmd, "with_app_command", True): + total += 1 + return total + + async def build_embed(self, guild_id: int | None, selected_cog: str) -> discord.Embed: + """Build the beautiful embed for the menu.""" + color = discord.Color(0x2B2D31) + + hub_welcome = await self.bot.get_text(guild_id, "menu.hub_welcome") + hub_explore = await self.bot.get_text(guild_id, "menu.hub_explore") + hub_usage = await self.bot.get_text(guild_id, "menu.hub_usage") + stats_title = await self.bot.get_text(guild_id, "menu.stats_heading") + categories_title = await self.bot.get_text(guild_id, "menu.categories_heading") + tips_title = await self.bot.get_text(guild_id, "menu.tips_heading") + quick_title = await self.bot.get_text(guild_id, "menu.quick_heading") + updates_title = await self.bot.get_text(guild_id, "menu.updates_heading") + + title = "BOT- AI System" + desc = f"{hub_welcome}\n{hub_explore}\n{hub_usage}" + tips = ( + f"? {await self.bot.get_text(guild_id, 'menu.tip_line_1')}\n" + f"? {await self.bot.get_text(guild_id, 'menu.tip_line_2')}\n" + f"? {await self.bot.get_text(guild_id, 'menu.tip_line_3')}" + ) quick = ( - f"🎵 `/music_panel` — {translate(lang, 'menu.quick_music')}\n" - f"🎮 `/gamehub` — {translate(lang, 'menu.quick_gamehub')}\n" - f"🏆 `/tournament panel` — {translate(lang, 'menu.quick_tournament')}\n" - f"💰 `/economy` — {translate(lang, 'menu.quick_economy')}" + f"- `/music_panel` - {await self.bot.get_text(guild_id, 'menu.quick_music')}\n" + f"- `/gamehub` - {await self.bot.get_text(guild_id, 'menu.quick_gamehub')}\n" + f"- `/tournament panel` - {await self.bot.get_text(guild_id, 'menu.quick_tournament')}\n" + f"- `/economy` - {await self.bot.get_text(guild_id, 'menu.quick_economy')}" ) + quick_ai = await self.bot.get_text(guild_id, 'menu.quick_ai') updates = ( - f"🧩 `/admin emoji clone` — {translate(lang, 'menu.update_cloneemoji')}\n" - f"🛡️ `/admin shield add_image` — {translate(lang, 'menu.update_shield_image')}\n" - f"🗳️ `/poll create` — {translate(lang, 'menu.update_poll_group')}\n" - f"💸 `/economy deposit` — {translate(lang, 'menu.update_economy_group')}\n" - f"🤖 `/ai execute` — {translate(lang, 'menu.quick_ai') if translate(lang, 'menu.quick_ai') != 'menu.quick_ai' else 'AI admin request'}" - ) - footer = "🏮 Powered by BOT- AI Suite 🏮" - - top_divider = panel_divider("cyan") - mid_divider = panel_divider("purple") - embed = ImperialMotaz.craft_embed( - title=title, - description=f"{top_divider}\n{desc}\n{mid_divider}", - color=color, - footer=footer, - ) - - if self.bot.user: - embed.set_thumbnail(url=self.bot.user.display_avatar.url) - - all_commands = self._selected_commands("__all__") - category_stats = self._category_stats() - selected_label = await self._selected_label(guild_id, selected_cog) - selected_cmds = self._selected_commands(selected_cog) - visible_cmds, total_pages = self._visible_commands_page(selected_cog) - max_lines = 18 - content = await self._format_commands(guild_id, visible_cmds, max_lines=max_lines) - - # Add fields with beautiful decorations - total_guilds = len(self.bot.guilds) - total_members = sum((g.member_count or 0) for g in self.bot.guilds) - latency_ms = f"{round(self.bot.latency * 1000)}ms" - slash_used = self._top_level_slash_count() - slash_budget = f"{slash_used}/100" - stats_grid = quick_stats_grid( - [ - ("Guilds", str(total_guilds), "🌐"), - ("Members", f"{total_members:,}", "👥"), - ("Latency", latency_ms, "⚡"), - ("Slash", slash_budget, "🧮"), - ], - columns=2, - ) - embed.add_field(name=stats_title, value=stats_grid, inline=False) - - embed.add_field( - name=categories_title, - value=await self._category_value(category_stats, guild_id), - inline=True, - ) - - emoji = _CATEGORY_EMOJIS.get(selected_cog, "📁") - embed.add_field( - name=f"{emoji} {selected_label}", - value=content or translate(lang, "menu.none"), - inline=False, - ) - - embed.add_field( - name=quick_title, - value=quick, - inline=True, - ) - - embed.add_field( - name=tips_title, - value=tips, - inline=True, - ) - - embed.add_field( - name=updates_title, - value=updates, - inline=False, + f"- `/admin emoji clone` - {await self.bot.get_text(guild_id, 'menu.update_cloneemoji')}\n" + f"- `/admin shield add_image` - {await self.bot.get_text(guild_id, 'menu.update_shield_image')}\n" + f"- `/poll create` - {await self.bot.get_text(guild_id, 'menu.update_poll_group')}\n" + f"- `/economy deposit` - {await self.bot.get_text(guild_id, 'menu.update_economy_group')}\n" + f"- `/ai execute` - {quick_ai if quick_ai != 'menu.quick_ai' else 'AI admin request'}" ) - - embed.set_footer(text=footer) - embed.description = f"{embed.description}\n\n📄 Page {self.page + 1}/{total_pages}" - return embed - - -class MainMenuView(CommandsMenuView): - """Named alias for persistent registration.""" - - -class InviteButton(discord.ui.Button): - """Beautiful invite button.""" - - def __init__(self, label: str, client_id: int) -> None: - url = f"https://discord.com/api/oauth2/authorize?client_id={client_id}&permissions=0&scope=applications.commands%20bot" - super().__init__( - label=label, - style=discord.ButtonStyle.link, - url=url, - emoji=ui("link") - ) - - -class RefreshButton(discord.ui.Button): - """Beautiful refresh button.""" - - def __init__(self, parent: CommandsMenuView, label: str) -> None: - super().__init__( - label=label, - emoji=ui("refresh"), - style=discord.ButtonStyle.secondary - ) - self.parent_view = parent - - async def callback(self, interaction: discord.Interaction) -> None: - await interaction.response.defer() + footer = "Powered by BOT- AI Suite" + + top_divider = panel_divider("cyan") + mid_divider = panel_divider("purple") + embed = ImperialMotaz.craft_embed( + title=title, + description=f"{top_divider}\n{desc}\n{mid_divider}", + color=color, + footer=footer, + ) + + if self.bot.user: + embed.set_thumbnail(url=self.bot.user.display_avatar.url) + + category_stats = self._category_stats() + selected_label = await self._selected_label(guild_id, selected_cog) + visible_cmds, total_pages = self._visible_commands_page(selected_cog) + content = await self._format_commands(guild_id, visible_cmds, max_lines=18) + + total_guilds = len(self.bot.guilds) + total_members = sum((g.member_count or 0) for g in self.bot.guilds) + latency_ms = f"{round(self.bot.latency * 1000)}ms" + slash_used = self._top_level_slash_count() + slash_budget = f"{slash_used}/100" + stats_grid = quick_stats_grid( + [ + ("Guilds", str(total_guilds), "G"), + ("Members", f"{total_members:,}", "M"), + ("Latency", latency_ms, "L"), + ("Slash", slash_budget, "S"), + ], + columns=2, + ) + embed.add_field(name=stats_title, value=stats_grid, inline=False) + + embed.add_field( + name=categories_title, + value=await self._category_value(category_stats, guild_id), + inline=True, + ) + + emoji = _CATEGORY_EMOJIS.get(selected_cog, "C") + none_text = await self.bot.get_text(guild_id, "menu.none") + embed.add_field( + name=f"{emoji} {selected_label}", + value=content or none_text, + inline=False, + ) + + embed.add_field(name=quick_title, value=quick, inline=True) + embed.add_field(name=tips_title, value=tips, inline=True) + embed.add_field(name=updates_title, value=updates, inline=False) + + embed.set_footer(text=footer) + embed.description = f"{embed.description}\n\nPage {self.page + 1}/{total_pages}" + return embed + + +class MainMenuView(CommandsMenuView): + """Named alias for persistent registration.""" + + +class InviteButton(discord.ui.Button): + """Beautiful invite button.""" + + def __init__(self, label: str, client_id: int) -> None: + url = f"https://discord.com/api/oauth2/authorize?client_id={client_id}&permissions=0&scope=applications.commands%20bot" + super().__init__( + label=label, + style=discord.ButtonStyle.link, + url=url, + emoji=ui("link") + ) + + +class RefreshButton(discord.ui.Button): + """Beautiful refresh button.""" + + def __init__(self, parent: CommandsMenuView, label: str) -> None: + super().__init__( + label=label, + emoji=ui("refresh"), + style=discord.ButtonStyle.secondary + ) + self.parent_view = parent + + async def callback(self, interaction: discord.Interaction) -> None: + await interaction.response.defer() guild_id = interaction.guild.id if interaction.guild else None - lang = await self.parent_view.bot.get_guild_language(guild_id) - self.label = translate(lang, "menu.refresh") - await self.parent_view.setup_items() - embed = await self.parent_view.build_embed(guild_id, "__all__") - await interaction.followup.edit_message(interaction.message.id, embed=embed, view=self.parent_view) - - -class QuickCategoryButton(discord.ui.Button): - def __init__(self, parent: CommandsMenuView, label: str, category: str, emoji: str, *, row: int = 1) -> None: - super().__init__( - label=label, - emoji=emoji, - style=discord.ButtonStyle.secondary, - custom_id=f"menu:quick:{label.lower()}", - row=row, - ) - self.parent_view = parent - self.category = category - - async def callback(self, interaction: discord.Interaction) -> None: - await interaction.response.defer() - self.parent_view.selected_cog = self.category - self.parent_view.page = 0 - await self.parent_view.setup_items() - embed = await self.parent_view.build_embed(interaction.guild.id if interaction.guild else None, self.category) - await interaction.followup.edit_message(interaction.message.id, embed=embed, view=self.parent_view) - - -class PageButton(discord.ui.Button): - def __init__(self, parent: CommandsMenuView, direction: str) -> None: - self.parent_view = parent - self.direction = direction - label = "Previous" if direction == "prev" else "Next" - emoji = "⬅️" if direction == "prev" else "➡️" - super().__init__(label=label, emoji=emoji, style=discord.ButtonStyle.secondary) - - async def callback(self, interaction: discord.Interaction) -> None: - await interaction.response.defer() - if self.direction == "prev": - self.parent_view.page = max(0, self.parent_view.page - 1) - else: - if self.parent_view._has_next_page(self.parent_view.selected_cog): - self.parent_view.page += 1 - await self.parent_view.setup_items() - embed = await self.parent_view.build_embed(interaction.guild.id if interaction.guild else None, self.parent_view.selected_cog) - await interaction.followup.edit_message(interaction.message.id, embed=embed, view=self.parent_view) - - -class Menu(commands.Cog): - """Interactive command menu with beautiful panels and multi-language support.""" - - def __init__(self, bot: commands.Bot) -> None: - self.bot = bot - + self.label = await self.parent_view.bot.get_text(guild_id, "menu.refresh") + await self.parent_view.setup_items() + embed = await self.parent_view.build_embed(guild_id, "__all__") + await interaction.followup.edit_message(interaction.message.id, embed=embed, view=self.parent_view) + + +class QuickCategoryButton(discord.ui.Button): + def __init__(self, parent: CommandsMenuView, label: str, category: str, emoji: str, *, row: int = 1) -> None: + super().__init__( + label=label, + emoji=emoji, + style=discord.ButtonStyle.secondary, + custom_id=f"menu:quick:{label.lower()}", + row=row, + ) + self.parent_view = parent + self.category = category + + async def callback(self, interaction: discord.Interaction) -> None: + await interaction.response.defer() + self.parent_view.selected_cog = self.category + self.parent_view.page = 0 + await self.parent_view.setup_items() + embed = await self.parent_view.build_embed(interaction.guild.id if interaction.guild else None, self.category) + await interaction.followup.edit_message(interaction.message.id, embed=embed, view=self.parent_view) + + +class PageButton(discord.ui.Button): + def __init__(self, parent: CommandsMenuView, direction: str) -> None: + self.parent_view = parent + self.direction = direction + label = "Previous" if direction == "prev" else "Next" + emoji = "⬅️" if direction == "prev" else "➡️" + super().__init__(label=label, emoji=emoji, style=discord.ButtonStyle.secondary) + + async def callback(self, interaction: discord.Interaction) -> None: + await interaction.response.defer() + if self.direction == "prev": + self.parent_view.page = max(0, self.parent_view.page - 1) + else: + if self.parent_view._has_next_page(self.parent_view.selected_cog): + self.parent_view.page += 1 + await self.parent_view.setup_items() + embed = await self.parent_view.build_embed(interaction.guild.id if interaction.guild else None, self.parent_view.selected_cog) + await interaction.followup.edit_message(interaction.message.id, embed=embed, view=self.parent_view) + + +class Menu(commands.Cog): + """Interactive command menu with beautiful panels and multi-language support.""" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + async def cog_load(self) -> None: - self.bot.add_view(CommandsMenuView(self)) - - @commands.hybrid_command(name="menu", description="Bot menu | قائمة أوامر واضحة") - async def menu(self, ctx: commands.Context) -> None: - """Display the beautiful command menu.""" - if ctx.interaction and not ctx.interaction.response.is_done(): - await ctx.interaction.response.defer() - guild_id = ctx.guild.id if ctx.guild else None - view = CommandsMenuView(self.bot, guild_id) - await view.setup_items() - embed = await view.build_embed(guild_id, "__all__") - if ctx.interaction: - await ctx.interaction.followup.send(embed=embed, view=view) - else: - await ctx.reply(embed=embed, view=view) - - @commands.hybrid_command(name="start", description="Start menu | القائمة الرئيسية", with_app_command=False) - async def start_menu(self, ctx: commands.Context) -> None: - await self.menu(ctx) - - async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None: - embed = ImperialMotaz.craft_embed( - title="⚠️ Command Error | خطأ في الأمر", - description=f"「 {str(error)[:1000]} 」", - color=discord.Color(0x2B2D31), - footer="🏮 Powered by BOT- AI Suite 🏮", - ) - try: - await ctx.reply(embed=embed) - except Exception: - if ctx.channel: - await ctx.channel.send(embed=embed) - - -async def setup(bot: commands.Bot) -> None: - await bot.add_cog(Menu(bot)) + self.bot.add_view(CommandsMenuView(self.bot)) + + @commands.hybrid_command(name="menu", description="Bot menu | قائمة أوامر واضحة") + async def menu(self, ctx: commands.Context) -> None: + """Display the beautiful command menu.""" + if ctx.interaction and not ctx.interaction.response.is_done(): + await ctx.interaction.response.defer() + guild_id = ctx.guild.id if ctx.guild else None + view = CommandsMenuView(self.bot, guild_id) + await view.setup_items() + embed = await view.build_embed(guild_id, "__all__") + if ctx.interaction: + await ctx.interaction.followup.send(embed=embed, view=view) + else: + await ctx.reply(embed=embed, view=view) + + @commands.hybrid_command(name="start", description="Start menu | القائمة الرئيسية", with_app_command=False) + async def start_menu(self, ctx: commands.Context) -> None: + await self.menu(ctx) + + async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None: + embed = ImperialMotaz.craft_embed( + title="⚠️ Command Error | خطأ في الأمر", + description=f"「 {str(error)[:1000]} 」", + color=discord.Color(0x2B2D31), + footer="🏮 Powered by BOT- AI Suite 🏮", + ) + try: + await ctx.reply(embed=embed) + except Exception: + if ctx.channel: + await ctx.channel.send(embed=embed) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Menu(bot)) + + diff --git a/bot/cogs/observability.py b/bot/cogs/observability.py index 0c728314c1c388081db66cadf51579b0c3e59bd6..1ab923291383c7ce8350564ea24cc9eca34e4cad 100644 --- a/bot/cogs/observability.py +++ b/bot/cogs/observability.py @@ -1,14 +1,16 @@ -from __future__ import annotations - -import difflib -import traceback -from typing import get_args, get_origin +from __future__ import annotations + +import difflib +import json +import traceback +from pathlib import Path +from typing import get_args, get_origin import discord from discord.ext import commands -class Observability(commands.Cog): +class Observability(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot @@ -122,8 +124,8 @@ class Observability(commands.Cog): embed.add_field(name="Error", value=f"```py\n{tb[-900:]}\n```", inline=False) await channel.send(embed=embed) - @commands.hybrid_command(name="command_fill", description="Show how to fill command arguments") - async def command_fill(self, ctx: commands.Context, *, command_name: str) -> None: + @commands.hybrid_command(name="command_fill", description="Show how to fill command arguments") + async def command_fill(self, ctx: commands.Context, *, command_name: str) -> None: command = self.bot.get_command(command_name.strip()) if not command: close = self._closest_commands(command_name.strip()) @@ -141,8 +143,70 @@ class Observability(commands.Cog): usage = f"{ctx.prefix}{command.qualified_name} " + " ".join(pieces) embed = discord.Embed(title="🧩 Command Fill Helper", color=discord.Color.blurple()) embed.add_field(name="Usage", value=f"`{usage.strip()}`", inline=False) - embed.add_field(name="Argument hints", value="\n".join(details) if details else "No arguments", inline=False) - await ctx.reply(embed=embed) + embed.add_field(name="Argument hints", value="\n".join(details) if details else "No arguments", inline=False) + await ctx.reply(embed=embed) + + @commands.hybrid_command(name="system_audit", description="Run a quick runtime health audit") + @commands.has_permissions(manage_guild=True) + async def system_audit(self, ctx: commands.Context) -> None: + await self._safe_reply(ctx, "Running system audit...") + + total_commands = len(self.bot.commands) + loaded_cogs = len(self.bot.cogs) + + row = await self.bot.db.fetchone("PRAGMA table_info(guild_config)") + has_guild_config = bool(row) + cols = await self.bot.db.fetchall("PRAGMA table_info(guild_config)") + col_names = {str(r[1]) for r in cols} if cols else set() + has_banner_col = "custom_banner_url" in col_names + has_lang_col = "guild_language" in col_names + + locale_dir = Path("bot/locales") + locale_files = sorted(p.name for p in locale_dir.glob("*.json")) if locale_dir.exists() else [] + locale_issues = 0 + if locale_files: + try: + base = json.loads((locale_dir / "en.json").read_text(encoding="utf-8")) + def flatten(obj, prefix=""): + out = {} + if isinstance(obj, dict): + for k, v in obj.items(): + key = f"{prefix}.{k}" if prefix else k + out.update(flatten(v, key)) + else: + out[prefix] = obj + return out + base_keys = set(flatten(base).keys()) + for name in locale_files: + data = json.loads((locale_dir / name).read_text(encoding="utf-8")) + keys = set(flatten(data).keys()) + if base_keys - keys: + locale_issues += 1 + except Exception: + locale_issues += 1 + + checks = [ + ("Commands registered", total_commands >= 50, str(total_commands)), + ("Cogs loaded", loaded_cogs >= 8, str(loaded_cogs)), + ("guild_config table", has_guild_config, "ok" if has_guild_config else "missing"), + ("guild_language column", has_lang_col, "ok" if has_lang_col else "missing"), + ("custom_banner_url column", has_banner_col, "ok" if has_banner_col else "missing"), + ("Locale files", bool(locale_files), f"{len(locale_files)} files"), + ("Locale consistency", locale_issues == 0, "ok" if locale_issues == 0 else f"{locale_issues} issue(s)"), + ] + + passed = sum(1 for _, ok, _ in checks if ok) + status = "HEALTHY" if passed == len(checks) else "NEEDS ATTENTION" + color = discord.Color.green() if passed == len(checks) else discord.Color.orange() + lines = [f"{'✅' if ok else '⚠️'} {name}: `{detail}`" for name, ok, detail in checks] + + embed = discord.Embed( + title=f"System Audit: {status}", + description="\n".join(lines), + color=color, + ) + embed.set_footer(text=f"Passed {passed}/{len(checks)} checks") + await ctx.reply(embed=embed) async def setup(bot: commands.Bot) -> None: diff --git a/bot/database.py b/bot/database.py index 1b6694493127a444392235b4c0376803185d1502..0d455c98e0b59f9c168f178d27bfe4df77ecbc52 100644 --- a/bot/database.py +++ b/bot/database.py @@ -59,6 +59,8 @@ class Database: Path(self.path).parent.mkdir(parents=True, exist_ok=True) await asyncio.to_thread(shutil.copy2, local_file, self.path) except Exception: + # Disable further sync attempts to avoid repeated noisy auth/network failures. + self._hf_sync_enabled = False return async def _push_remote_db(self) -> None: @@ -75,6 +77,8 @@ class Database: commit_message="Bot DB sync", ) except Exception: + # Disable further sync attempts to avoid repeated noisy auth/network failures. + self._hf_sync_enabled = False return async def _ensure_column( @@ -337,6 +341,15 @@ class Database: created_at TEXT DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (guild_id, image_hash) ); + + CREATE TABLE IF NOT EXISTS bot_presence_config ( + id INTEGER PRIMARY KEY CHECK (id = 1), + status TEXT DEFAULT 'online', + activity_type TEXT DEFAULT 'playing', + activity_text TEXT DEFAULT 'CYBER // GRID', + updated_by INTEGER, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ); """ ) @@ -375,6 +388,7 @@ class Database: await self._ensure_column(db, "guild_config", "game_news_channel_id", "INTEGER") await self._ensure_column(db, "guild_config", "game_news_role_id", "INTEGER") await self._ensure_column(db, "guild_config", "game_news_last_ids", "TEXT") + await self._ensure_column(db, "guild_config", "custom_banner_url", "TEXT") await db.commit() await self._push_remote_db() @@ -405,15 +419,16 @@ class Database: """Clean up all data for a guild (for when bot leaves a server).""" async with self._lock: db = await self._get_connection() - await db.execute("DELETE FROM guild_config WHERE guild_id = ?", guild_id) - await db.execute("DELETE FROM user_xp WHERE guild_id = ?", guild_id) - await db.execute("DELETE FROM user_balance WHERE guild_id = ?", guild_id) - await db.execute("DELETE FROM user_daily_claim WHERE guild_id = ?", guild_id) - await db.execute("DELETE FROM user_work_cooldown WHERE guild_id = ?", guild_id) - await db.execute("DELETE FROM warns WHERE guild_id = ?", guild_id) - await db.execute("DELETE FROM tournaments WHERE guild_id = ?", guild_id) - await db.execute("DELETE FROM tournament_participants WHERE guild_id = ?", guild_id) - await db.execute("DELETE FROM tournament_games WHERE guild_id = ?", guild_id) - await db.execute("DELETE FROM voice_sessions WHERE guild_id = ?", guild_id) + params = (guild_id,) + await db.execute("DELETE FROM guild_config WHERE guild_id = ?", params) + await db.execute("DELETE FROM user_xp WHERE guild_id = ?", params) + await db.execute("DELETE FROM user_balance WHERE guild_id = ?", params) + await db.execute("DELETE FROM user_daily_claim WHERE guild_id = ?", params) + await db.execute("DELETE FROM user_work_cooldown WHERE guild_id = ?", params) + await db.execute("DELETE FROM warns WHERE guild_id = ?", params) + await db.execute("DELETE FROM tournaments WHERE guild_id = ?", params) + await db.execute("DELETE FROM tournament_participants WHERE guild_id = ?", params) + await db.execute("DELETE FROM tournament_games WHERE guild_id = ?", params) + await db.execute("DELETE FROM voice_sessions WHERE guild_id = ?", params) await db.commit() await self._push_remote_db() diff --git a/bot/emojis.py b/bot/emojis.py index 2e04c4e00c7ab7689bc16fc061e9d729a0e36900..9a0c05ef5929bd84c20262e9b3bf4a9cadc715a7 100644 --- a/bot/emojis.py +++ b/bot/emojis.py @@ -33,8 +33,8 @@ FALLBACK_EMOJIS: dict[str, str] = { "catjam": "😺", "djpeepo": "🎧", "spotify": "🎵", "partytime": "🎉", "letsgo": "🚀", # Arrows & decorations - "arrow_green": "🟢", "arrow_pink": "✅", "arrow_blue": "🔵", "arrow_purple": "🟢", - "arrow_orange": "🟠", "arrow_yellow": "🟡", "shield": "🛡️", "gift": "🎁", "hype": "🔥", + "arrow_green": "<:animatedarrowgreen:1477261279428087979>", "arrow_pink": "✅", "arrow_blue": "🔵", "arrow_purple": "<:animatedarrowgreen:1477261279428087979>", + "arrow_orange": "🟠", "arrow_yellow": "<:animatedarrowyellow:1477261257592668271>", "shield": "🛡️", "gift": "🎁", "hype": "🔥", # UI elements "notebook": "📓", "ticket": "🎫", "verified": "✅", "error": "❌", "loading": "⏳", @@ -44,8 +44,8 @@ FALLBACK_EMOJIS: dict[str, str] = { "num_6": "6️⃣", "num_7": "7️⃣", "num_8": "8️⃣", "num_9": "9️⃣", "num_10": "🔟", # Enhanced decorations - "sparkle": "✨", "fire": "🔥", "heart": "✅", "purple_heart": "🟢", "blue_heart": "🔵", - "green_heart": "🟢", "yellow_heart": "🟡", "orange_heart": "🟠", "pink_heart": "✅", + "sparkle": "✨", "fire": "🔥", "heart": "✅", "purple_heart": "<:animatedarrowgreen:1477261279428087979>", "blue_heart": "🔵", + "green_heart": "<:animatedarrowgreen:1477261279428087979>", "yellow_heart": "<:animatedarrowyellow:1477261257592668271>", "orange_heart": "🟠", "pink_heart": "✅", "crown": "👑", "trophy": "🏆", "medal": "🎖️", "ribbon": "🎀", "gem": "💎", "crystal_ball": "🔮", "comet": "☄️", "star2": "✅", "star3": "✅", "dizzy": "💫", "boom": "💥", "zap": "⚡", "warning": "⚠️", "information": "ℹ️", "question": "❓", @@ -67,7 +67,7 @@ FALLBACK_EMOJIS: dict[str, str] = { "credit_card": "💳", "chart": "📈", "bank": "🏦", "atm": "🏧", "receipt": "🧾", # Decorative borders - "line": "▬", "dash": "—", "dot": "•", "bullet": "•", "circle": "🟢", + "line": "▬", "dash": "—", "dot": "•", "bullet": "•", "circle": "<:animatedarrowgreen:1477261279428087979>", "square": "■", "diamond_shape": "◆", "triangle": "▲", "star_border": "✅", } @@ -96,6 +96,16 @@ def _normalize_key(value: str) -> str: return (value or "").strip().lower().replace(" ", "_") +def _sanitize_emoji_name(name: str) -> str: + cleaned = re.sub(r"[^A-Za-z0-9_]", "_", (name or "").strip()) + cleaned = re.sub(r"_+", "_", cleaned).strip("_") + if not cleaned: + return "emoji" + if len(cleaned) < 2: + return f"{cleaned}_x" + return cleaned[:32] + + def set_emoji_bot(bot: discord.Client) -> None: """Register bot/client instance for dynamic custom emoji lookup.""" global _EMOJI_BOT @@ -198,11 +208,17 @@ def _parse_emoji_file() -> dict[str, str]: if key and value: # Check if value is already in full format if (value.startswith("<:") or value.startswith(""): - # Already full format, use as-is - parsed[key] = value + cfg = _parse_custom_emoji_config(value) + if cfg is not None: + cfg_name, emoji_id, animated = cfg + parsed[key] = _build_custom_emoji_code( + _sanitize_emoji_name(cfg_name), emoji_id, animated + ) + else: + parsed[key] = value elif value.isdigit(): # Just the ID, create full format - parsed[key] = f"<:{name.strip()}:{value}>" + parsed[key] = f"<:{_sanitize_emoji_name(name)}:{value}>" else: # Some other format, try to use as-is parsed[key] = value @@ -663,7 +679,7 @@ E_DIZZY = "💫" E_BOOM = "💥" E_ZAP = "⚡" E_HEART = "✅" -E_PURPLE_HEART = "🟢" +E_PURPLE_HEART = "<:animatedarrowgreen:1477261279428087979>" E_BLUE_HEART = "💙" -E_GREEN_HEART = "💚" +E_GREEN_HEART = "<:animatedarrowgreen:1477261279428087979>" E_YELLOW_HEART = "💛" diff --git a/bot/i18n.py b/bot/i18n.py index b1b00a1f329ed4cafd67801310dab4469268cabf..4dc1b1479426391e440c4541c679c6cdecb9208d 100644 --- a/bot/i18n.py +++ b/bot/i18n.py @@ -20,7 +20,7 @@ TRANSLATIONS["ar"] = { # === Language Commands === "lang.current": "{globe} لغة السيرفر الحالية: **العربية**.", "lang.updated": "{ok} تم تغيير لغة البوت إلى **العربية**. استخدم `/menu` لرؤية الأوامر باللغة المحددة.", - "lang.invalid": "{no} لغة غير مدعومة. استخدم: `ar`, `en`, `es`, `fr`, `de`, `tr`, `it`, `pt`, `ru`, `hi`, `id`, `ja`, `he`.", + "lang.invalid": "{no} لغة غير مدعومة. استخدم: `ar`, `de`, `en`, `es`, `fr`, `he`, `hi`, `id`, `it`, `ja`, `pt`, `ru`, `tr`, `zh`.", # === Menu System === "menu.title": "{menu} قائمة أوامر البوت", @@ -629,7 +629,7 @@ TRANSLATIONS["en"] = { # === Language Commands === "lang.current": "{globe} Current server language: **English**.", "lang.updated": "{ok} Bot language changed to **English**. Use `/menu` to view commands in the selected language.", - "lang.invalid": "{no} Unsupported language. Use: `ar`, `en`, `es`, `fr`, `de`, `tr`, `it`, `pt`, `ru`, `hi`, `id`, `ja`, `he`.", + "lang.invalid": "{no} Unsupported language. Use: `ar`, `de`, `en`, `es`, `fr`, `he`, `hi`, `id`, `it`, `ja`, `pt`, `ru`, `tr`, `zh`.", # === Menu System === "menu.title": "{menu} Bot Command Menu", @@ -1445,6 +1445,19 @@ TRANSLATIONS["he"] = { "ping.desc": "זמן תגובה: **{latency}ms**", } +# Chinese (fallback pack; missing keys are filled from English below) +TRANSLATIONS["zh"] = { + "lang.current": "{globe} 当前服务器语言: **中文**。", + "lang.updated": "{ok} 机器人语言已切换为 **中文**。", + "lang.invalid": "{no} 不支持的语言。", + "menu.title": "{menu} 命令菜单", + "menu.all": "全部", + "menu.ai": "AI 面板", + "menu.refresh": "刷新", + "ping.title": "{ping} Pong", + "ping.desc": "延迟: **{latency}ms**", +} + # ═══════════════════════════════════════════════════════════════════════════════ # HELPER FUNCTIONS # ═══════════════════════════════════════════════════════════════════════════════ diff --git a/bot/locales/ar.json b/bot/locales/ar.json index 00c424f4b1972b033d6338a614f8fd794013e319..9d0c555734b8135927b5e74c58f7aa82715c02a7 100644 --- a/bot/locales/ar.json +++ b/bot/locales/ar.json @@ -89,6 +89,12 @@ }, "ai": { "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』" + }, + "ai_architect": { + "title": "?? ????? ????? ????? ?????? ?????????", + "proposal": "??????? ???????? ???????: {sections}", + "analyzing": "???? ????? ??????? ???????...", + "success": "????? ????? ????? ?????? ????????? ?????." } }, "errors": { @@ -267,8 +273,163 @@ "set_banner": "تعيين بنر مخصص لهذا السيرفر", "remove_banner": "إزالة البنر المخصص من هذا السيرفر", "view_banner": "عرض البنر المخصص الحالي لهذا السيرفر", - "banner_help": "تعلم كيفية تعيين بنر مخصص" - } + "banner_help": "تعلم كيفية تعيين بنر مخصص", + "8ball": "8ball", + "add_scam_image": "Save scam image signature", + "admin_panel": "Open admin control panel", + "ai": "Ai", + "ai_auto": "Ai auto", + "ai_channel": "Ai channel", + "ai_model": "Ai model", + "ask_image": "Ask image", + "avatar": "Avatar", + "awesomeroles": "Awesomeroles", + "backup_panel": "Open backup management panel", + "backupserver": "Backupserver", + "balance": "Balance", + "ban": "Ban", + "banner": "Banner", + "board_forfeit": "Board forfeit", + "board_move": "Board move", + "board_start": "Board start", + "boardgames": "Boardgames", + "botstats": "Botstats", + "buy": "Buy", + "chat": "Chat", + "checkers": "Checkers", + "chess": "Chess", + "choose": "Choose", + "clearwarn": "Clearwarn", + "cloneemoji": "Cloneemoji", + "code_gen": "Code gen", + "coinflip": "Coinflip", + "connect4": "Connect4", + "daily": "Daily", + "debug": "Debug", + "dice": "Dice", + "econ_admin": "Manage user economy balances", + "economy": "Economy", + "economy_deposit": "Economy deposit", + "economy_gamble": "Economy gamble", + "economy_rob": "Economy rob", + "economy_withdraw": "Economy withdraw", + "free_games": "Free games", + "gamble": "Gamble", + "gamehub": "Gamehub", + "games_panel": "Games panel", + "gaming_news": "Gaming news", + "giveaway": "Giveaway command group", + "guess": "Guess", + "image_gen": "Image gen", + "imagine": "Imagine", + "inventory": "Inventory", + "kick": "Kick", + "language": "Language", + "languages": "Languages", + "leaderboard": "Leaderboard", + "level": "Level", + "load": "Load", + "lock": "Lock", + "make_event": "Make event", + "mario": "Mario", + "meme": "Meme", + "menu": "Menu", + "music_247": "Music 247", + "music_clear": "Music clear", + "music_filter": "Music filter", + "music_jump": "Music jump", + "music_loop": "Music loop", + "music_lyrics": "Music lyrics", + "music_move": "Music move", + "music_nowplaying": "Music nowplaying", + "music_panel": "Music panel", + "music_pause": "Music pause", + "music_play": "Music play", + "music_playlist_delete": "Music playlist delete", + "music_playlist_rename": "Music playlist rename", + "music_playlist_save": "Music playlist save", + "music_previous": "Music previous", + "music_queue": "Music queue", + "music_remove": "Music remove", + "music_resume": "Music resume", + "music_seek": "Music seek", + "music_shuffle": "Music shuffle", + "music_skip": "Music skip", + "music_stop": "Music stop", + "music_volume": "Music volume", + "mute": "Mute", + "organizechannels": "Organizechannels", + "othello": "Othello", + "ping": "Ping", + "play": "Play", + "poll": "Poll", + "profile": "Profile", + "purge": "Purge", + "rank": "Rank", + "reload": "Reload", + "remind": "Remind", + "rob": "Rob", + "roll": "Roll", + "rps": "Rps", + "serverinfo": "Serverinfo", + "set": "Set", + "set_automod": "Set automod", + "set_dailybutton": "Set dailybutton", + "set_dailychannel": "Set dailychannel", + "set_dailyimage": "Set dailyimage", + "set_dailymessage": "Set dailymessage", + "set_dailytime": "Set dailytime", + "set_dailytitle": "Set dailytitle", + "set_dailytoggle": "Set dailytoggle", + "set_freegames": "Set freegames", + "set_gamenews": "Set gamenews", + "set_log": "Set log", + "set_pollchannel": "Set pollchannel", + "set_suggestions": "Set suggestions", + "set_supportai": "Set supportai", + "set_welcome": "Set welcome", + "set_wisdom": "Set wisdom", + "setupserver": "Setupserver", + "shield_level": "Set shield sensitivity level", + "shield_state": "Show current shield state", + "shop": "Shop", + "shutdown": "Shutdown", + "slots": "Slots", + "slowmode": "Slowmode", + "speak": "Speak", + "summarize": "Summarize", + "sync": "Sync", + "system_audit": "Show system audit and diagnostics", + "ticket": "Ticket command group", + "tournament": "Tournament", + "tournament_create": "Tournament create", + "tournament_end": "Tournament end", + "tournament_gamehub": "Tournament gamehub", + "tournament_join": "Tournament join", + "tournament_lb": "Tournament lb", + "tournament_panel": "Tournament panel", + "tournament_start": "Tournament start", + "transfer": "Transfer", + "translate": "Translate", + "translate_voice": "Translate voice", + "trivia": "Trivia", + "tts": "Tts", + "unban": "Unban", + "unload": "Unload", + "unlock": "Unlock", + "unmute": "Unmute", + "upscale": "Upscale", + "userinfo": "Userinfo", + "verify": "Verify", + "verifysetup": "Verifysetup", + "warn": "Warn", + "warnings": "Warnings", + "wisdom_today": "Wisdom today", + "work": "Work", + "xo": "Xo", + "xp": "Xp" + }, + "quick_ai": "??? ????? ??????? ?????????" }, "gambling": { "blackjack": { @@ -396,5 +557,19 @@ "trivia_title": "سؤال مسابقة", "trivia_correct": "إجابة صحيحة!", "trivia_wrong": "إجابة خاطئة!" + }, + "config": { + "visuals": { + "divider": "<:editprofilewhite:1476278814894981200> ??? ???????? ????????? ??? <:editprofilewhite:1476278814894981200>" + } + }, + "ping": { + "title": "?? ????", + "desc": "??? ?????????: **{latency}ms**" + }, + "roll": "{user} ??? ????? **{value}** (1-{limit}).", + "lang": { + "current": "Current server language: **English**.", + "updated": "Bot language changed successfully." } -} \ No newline at end of file +} diff --git a/bot/locales/de.json b/bot/locales/de.json index 1260d7a356925e035ddb83b4b8a270d5aaf9c424..9cbe92ded49ddc2f9243fe22a03d8a9dc138c78e 100644 --- a/bot/locales/de.json +++ b/bot/locales/de.json @@ -89,6 +89,12 @@ }, "ai": { "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』" + }, + "ai_architect": { + "title": "?? AI Architect Setup Complete", + "proposal": "Proposed setup sections: {sections}", + "analyzing": "Analyzing your server requirements...", + "success": "AI Architect finished successfully." } }, "errors": { @@ -267,8 +273,163 @@ "set_banner": "Benutzerdefiniertes Banner für diesen Server festlegen", "remove_banner": "Benutzerdefiniertes Banner von diesem Server entfernen", "view_banner": "Aktuelles benutzerdefiniertes Banner dieses Servers anzeigen", - "banner_help": "Erfahren, wie man ein benutzerdefiniertes Banner festlegt" - } + "banner_help": "Erfahren, wie man ein benutzerdefiniertes Banner festlegt", + "8ball": "8ball", + "add_scam_image": "Save scam image signature", + "admin_panel": "Open admin control panel", + "ai": "Ai", + "ai_auto": "Ai auto", + "ai_channel": "Ai channel", + "ai_model": "Ai model", + "ask_image": "Ask image", + "avatar": "Avatar", + "awesomeroles": "Awesomeroles", + "backup_panel": "Open backup management panel", + "backupserver": "Backupserver", + "balance": "Balance", + "ban": "Ban", + "banner": "Banner", + "board_forfeit": "Board forfeit", + "board_move": "Board move", + "board_start": "Board start", + "boardgames": "Boardgames", + "botstats": "Botstats", + "buy": "Buy", + "chat": "Chat", + "checkers": "Checkers", + "chess": "Chess", + "choose": "Choose", + "clearwarn": "Clearwarn", + "cloneemoji": "Cloneemoji", + "code_gen": "Code gen", + "coinflip": "Coinflip", + "connect4": "Connect4", + "daily": "Daily", + "debug": "Debug", + "dice": "Dice", + "econ_admin": "Manage user economy balances", + "economy": "Economy", + "economy_deposit": "Economy deposit", + "economy_gamble": "Economy gamble", + "economy_rob": "Economy rob", + "economy_withdraw": "Economy withdraw", + "free_games": "Free games", + "gamble": "Gamble", + "gamehub": "Gamehub", + "games_panel": "Games panel", + "gaming_news": "Gaming news", + "giveaway": "Giveaway command group", + "guess": "Guess", + "image_gen": "Image gen", + "imagine": "Imagine", + "inventory": "Inventory", + "kick": "Kick", + "language": "Language", + "languages": "Languages", + "leaderboard": "Leaderboard", + "level": "Level", + "load": "Load", + "lock": "Lock", + "make_event": "Make event", + "mario": "Mario", + "meme": "Meme", + "menu": "Menu", + "music_247": "Music 247", + "music_clear": "Music clear", + "music_filter": "Music filter", + "music_jump": "Music jump", + "music_loop": "Music loop", + "music_lyrics": "Music lyrics", + "music_move": "Music move", + "music_nowplaying": "Music nowplaying", + "music_panel": "Music panel", + "music_pause": "Music pause", + "music_play": "Music play", + "music_playlist_delete": "Music playlist delete", + "music_playlist_rename": "Music playlist rename", + "music_playlist_save": "Music playlist save", + "music_previous": "Music previous", + "music_queue": "Music queue", + "music_remove": "Music remove", + "music_resume": "Music resume", + "music_seek": "Music seek", + "music_shuffle": "Music shuffle", + "music_skip": "Music skip", + "music_stop": "Music stop", + "music_volume": "Music volume", + "mute": "Mute", + "organizechannels": "Organizechannels", + "othello": "Othello", + "ping": "Ping", + "play": "Play", + "poll": "Poll", + "profile": "Profile", + "purge": "Purge", + "rank": "Rank", + "reload": "Reload", + "remind": "Remind", + "rob": "Rob", + "roll": "Roll", + "rps": "Rps", + "serverinfo": "Serverinfo", + "set": "Set", + "set_automod": "Set automod", + "set_dailybutton": "Set dailybutton", + "set_dailychannel": "Set dailychannel", + "set_dailyimage": "Set dailyimage", + "set_dailymessage": "Set dailymessage", + "set_dailytime": "Set dailytime", + "set_dailytitle": "Set dailytitle", + "set_dailytoggle": "Set dailytoggle", + "set_freegames": "Set freegames", + "set_gamenews": "Set gamenews", + "set_log": "Set log", + "set_pollchannel": "Set pollchannel", + "set_suggestions": "Set suggestions", + "set_supportai": "Set supportai", + "set_welcome": "Set welcome", + "set_wisdom": "Set wisdom", + "setupserver": "Setupserver", + "shield_level": "Set shield sensitivity level", + "shield_state": "Show current shield state", + "shop": "Shop", + "shutdown": "Shutdown", + "slots": "Slots", + "slowmode": "Slowmode", + "speak": "Speak", + "summarize": "Summarize", + "sync": "Sync", + "system_audit": "Show system audit and diagnostics", + "ticket": "Ticket command group", + "tournament": "Tournament", + "tournament_create": "Tournament create", + "tournament_end": "Tournament end", + "tournament_gamehub": "Tournament gamehub", + "tournament_join": "Tournament join", + "tournament_lb": "Tournament lb", + "tournament_panel": "Tournament panel", + "tournament_start": "Tournament start", + "transfer": "Transfer", + "translate": "Translate", + "translate_voice": "Translate voice", + "trivia": "Trivia", + "tts": "Tts", + "unban": "Unban", + "unload": "Unload", + "unlock": "Unlock", + "unmute": "Unmute", + "upscale": "Upscale", + "userinfo": "Userinfo", + "verify": "Verify", + "verifysetup": "Verifysetup", + "warn": "Warn", + "warnings": "Warnings", + "wisdom_today": "Wisdom today", + "work": "Work", + "xo": "Xo", + "xp": "Xp" + }, + "quick_ai": "AI admin request" }, "gambling": { "blackjack": { @@ -345,5 +506,70 @@ "play_roulette": "<:catjam:1476278750378201260> Roulette spielen", "play_rpg": "<:therock:1476278719994794054> RPG-Abenteuer" } + }, + "admin": { + "panel_title": "Admin Control Panel", + "economy_admin": "Economy Administration", + "shield_control": "Shield Control", + "system_status": "System Status", + "backup_created": "Backup created successfully.", + "backup_restored": "Backup restored successfully.", + "backup_deleted": "Backup deleted.", + "no_backups": "No backups found." + }, + "verification": { + "panel_title": "Verification Panel", + "verify_button": "Verify Me", + "verified_role": "Verified", + "already_verified": "You are already verified.", + "verification_success": "Verification successful! Welcome to the server." + }, + "board_games": { + "panel_title": "Board Games Panel", + "chess": "Chess", + "checkers": "Checkers", + "connect4": "Connect 4", + "othello": "Othello", + "tictactoe": "Tic Tac Toe", + "rps": "Rock Paper Scissors", + "game_started": "Game started!", + "your_turn": "Your turn!", + "waiting": "Waiting for opponent..." + }, + "media": { + "now_playing": "Now Playing", + "queue_added": "Added to queue", + "no_music": "No music playing", + "volume_set": "Volume set to {volume}%", + "skipped": "Skipped track", + "stopped": "Stopped playback", + "paused": "Paused playback", + "resumed": "Resumed playback" + }, + "fun": { + "rps_title": "Rock Paper Scissors", + "rps_rock": "Rock", + "rps_paper": "Paper", + "rps_scissors": "Scissors", + "you_win": "You win!", + "you_lose": "You lose!", + "draw": "Draw!", + "trivia_title": "Trivia Question", + "trivia_correct": "Correct!", + "trivia_wrong": "Wrong!" + }, + "config": { + "visuals": { + "divider": "<:editprofilewhite:1476278814894981200> ??? ???????? ????????? ??? <:editprofilewhite:1476278814894981200>" + } + }, + "ping": { + "title": "?? Pong", + "desc": "Latency: **{latency}ms**" + }, + "roll": "{user} rolled **{value}** (1-{limit}).", + "lang": { + "current": "Current server language: **English**.", + "updated": "Bot language changed successfully." } -} \ No newline at end of file +} diff --git a/bot/locales/en.json b/bot/locales/en.json index a6cf37f57ed80cbedbf8c1ac91d9248a315e1b87..290dd95554bf28ee081ff7ea0d38e8c0991e9706 100644 --- a/bot/locales/en.json +++ b/bot/locales/en.json @@ -89,6 +89,12 @@ }, "ai": { "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』" + }, + "ai_architect": { + "title": "?? AI Architect Setup Complete", + "proposal": "Proposed setup sections: {sections}", + "analyzing": "Analyzing your server requirements...", + "success": "AI Architect finished successfully." } }, "errors": { @@ -267,8 +273,163 @@ "set_banner": "Set a custom banner for this server", "remove_banner": "Remove the custom banner from this server", "view_banner": "View the current custom banner for this server", - "banner_help": "Learn how to set a custom banner" - } + "banner_help": "Learn how to set a custom banner", + "8ball": "8ball", + "add_scam_image": "Save scam image signature", + "admin_panel": "Open admin control panel", + "ai": "Ai", + "ai_auto": "Ai auto", + "ai_channel": "Ai channel", + "ai_model": "Ai model", + "ask_image": "Ask image", + "avatar": "Avatar", + "awesomeroles": "Awesomeroles", + "backup_panel": "Open backup management panel", + "backupserver": "Backupserver", + "balance": "Balance", + "ban": "Ban", + "banner": "Banner", + "board_forfeit": "Board forfeit", + "board_move": "Board move", + "board_start": "Board start", + "boardgames": "Boardgames", + "botstats": "Botstats", + "buy": "Buy", + "chat": "Chat", + "checkers": "Checkers", + "chess": "Chess", + "choose": "Choose", + "clearwarn": "Clearwarn", + "cloneemoji": "Cloneemoji", + "code_gen": "Code gen", + "coinflip": "Coinflip", + "connect4": "Connect4", + "daily": "Daily", + "debug": "Debug", + "dice": "Dice", + "econ_admin": "Manage user economy balances", + "economy": "Economy", + "economy_deposit": "Economy deposit", + "economy_gamble": "Economy gamble", + "economy_rob": "Economy rob", + "economy_withdraw": "Economy withdraw", + "free_games": "Free games", + "gamble": "Gamble", + "gamehub": "Gamehub", + "games_panel": "Games panel", + "gaming_news": "Gaming news", + "giveaway": "Giveaway command group", + "guess": "Guess", + "image_gen": "Image gen", + "imagine": "Imagine", + "inventory": "Inventory", + "kick": "Kick", + "language": "Language", + "languages": "Languages", + "leaderboard": "Leaderboard", + "level": "Level", + "load": "Load", + "lock": "Lock", + "make_event": "Make event", + "mario": "Mario", + "meme": "Meme", + "menu": "Menu", + "music_247": "Music 247", + "music_clear": "Music clear", + "music_filter": "Music filter", + "music_jump": "Music jump", + "music_loop": "Music loop", + "music_lyrics": "Music lyrics", + "music_move": "Music move", + "music_nowplaying": "Music nowplaying", + "music_panel": "Music panel", + "music_pause": "Music pause", + "music_play": "Music play", + "music_playlist_delete": "Music playlist delete", + "music_playlist_rename": "Music playlist rename", + "music_playlist_save": "Music playlist save", + "music_previous": "Music previous", + "music_queue": "Music queue", + "music_remove": "Music remove", + "music_resume": "Music resume", + "music_seek": "Music seek", + "music_shuffle": "Music shuffle", + "music_skip": "Music skip", + "music_stop": "Music stop", + "music_volume": "Music volume", + "mute": "Mute", + "organizechannels": "Organizechannels", + "othello": "Othello", + "ping": "Ping", + "play": "Play", + "poll": "Poll", + "profile": "Profile", + "purge": "Purge", + "rank": "Rank", + "reload": "Reload", + "remind": "Remind", + "rob": "Rob", + "roll": "Roll", + "rps": "Rps", + "serverinfo": "Serverinfo", + "set": "Set", + "set_automod": "Set automod", + "set_dailybutton": "Set dailybutton", + "set_dailychannel": "Set dailychannel", + "set_dailyimage": "Set dailyimage", + "set_dailymessage": "Set dailymessage", + "set_dailytime": "Set dailytime", + "set_dailytitle": "Set dailytitle", + "set_dailytoggle": "Set dailytoggle", + "set_freegames": "Set freegames", + "set_gamenews": "Set gamenews", + "set_log": "Set log", + "set_pollchannel": "Set pollchannel", + "set_suggestions": "Set suggestions", + "set_supportai": "Set supportai", + "set_welcome": "Set welcome", + "set_wisdom": "Set wisdom", + "setupserver": "Setupserver", + "shield_level": "Set shield sensitivity level", + "shield_state": "Show current shield state", + "shop": "Shop", + "shutdown": "Shutdown", + "slots": "Slots", + "slowmode": "Slowmode", + "speak": "Speak", + "summarize": "Summarize", + "sync": "Sync", + "system_audit": "Show system audit and diagnostics", + "ticket": "Ticket command group", + "tournament": "Tournament", + "tournament_create": "Tournament create", + "tournament_end": "Tournament end", + "tournament_gamehub": "Tournament gamehub", + "tournament_join": "Tournament join", + "tournament_lb": "Tournament lb", + "tournament_panel": "Tournament panel", + "tournament_start": "Tournament start", + "transfer": "Transfer", + "translate": "Translate", + "translate_voice": "Translate voice", + "trivia": "Trivia", + "tts": "Tts", + "unban": "Unban", + "unload": "Unload", + "unlock": "Unlock", + "unmute": "Unmute", + "upscale": "Upscale", + "userinfo": "Userinfo", + "verify": "Verify", + "verifysetup": "Verifysetup", + "warn": "Warn", + "warnings": "Warnings", + "wisdom_today": "Wisdom today", + "work": "Work", + "xo": "Xo", + "xp": "Xp" + }, + "quick_ai": "AI admin request" }, "gambling": { "blackjack": { @@ -396,5 +557,19 @@ "trivia_title": "Trivia Question", "trivia_correct": "Correct!", "trivia_wrong": "Wrong!" + }, + "config": { + "visuals": { + "divider": "<:editprofilewhite:1476278814894981200> ??? ???????? ????????? ??? <:editprofilewhite:1476278814894981200>" + } + }, + "ping": { + "title": "?? Pong", + "desc": "Latency: **{latency}ms**" + }, + "roll": "{user} rolled **{value}** (1-{limit}).", + "lang": { + "current": "Current server language: **English**.", + "updated": "Bot language changed successfully." } -} \ No newline at end of file +} diff --git a/bot/locales/es.json b/bot/locales/es.json index b4760c074f3fe813babe7f58b3f2109f4fc0baad..84f85758bb57a493448f31b5f5735f2b759caf65 100644 --- a/bot/locales/es.json +++ b/bot/locales/es.json @@ -89,6 +89,12 @@ }, "ai": { "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』" + }, + "ai_architect": { + "title": "?? AI Architect Setup Complete", + "proposal": "Proposed setup sections: {sections}", + "analyzing": "Analyzing your server requirements...", + "success": "AI Architect finished successfully." } }, "errors": { @@ -267,8 +273,163 @@ "set_banner": "Configurar un banner personalizado para este servidor", "remove_banner": "Eliminar el banner personalizado de este servidor", "view_banner": "Ver el banner personalizado actual de este servidor", - "banner_help": "Aprender a configurar un banner personalizado" - } + "banner_help": "Aprender a configurar un banner personalizado", + "8ball": "8ball", + "add_scam_image": "Save scam image signature", + "admin_panel": "Open admin control panel", + "ai": "Ai", + "ai_auto": "Ai auto", + "ai_channel": "Ai channel", + "ai_model": "Ai model", + "ask_image": "Ask image", + "avatar": "Avatar", + "awesomeroles": "Awesomeroles", + "backup_panel": "Open backup management panel", + "backupserver": "Backupserver", + "balance": "Balance", + "ban": "Ban", + "banner": "Banner", + "board_forfeit": "Board forfeit", + "board_move": "Board move", + "board_start": "Board start", + "boardgames": "Boardgames", + "botstats": "Botstats", + "buy": "Buy", + "chat": "Chat", + "checkers": "Checkers", + "chess": "Chess", + "choose": "Choose", + "clearwarn": "Clearwarn", + "cloneemoji": "Cloneemoji", + "code_gen": "Code gen", + "coinflip": "Coinflip", + "connect4": "Connect4", + "daily": "Daily", + "debug": "Debug", + "dice": "Dice", + "econ_admin": "Manage user economy balances", + "economy": "Economy", + "economy_deposit": "Economy deposit", + "economy_gamble": "Economy gamble", + "economy_rob": "Economy rob", + "economy_withdraw": "Economy withdraw", + "free_games": "Free games", + "gamble": "Gamble", + "gamehub": "Gamehub", + "games_panel": "Games panel", + "gaming_news": "Gaming news", + "giveaway": "Giveaway command group", + "guess": "Guess", + "image_gen": "Image gen", + "imagine": "Imagine", + "inventory": "Inventory", + "kick": "Kick", + "language": "Language", + "languages": "Languages", + "leaderboard": "Leaderboard", + "level": "Level", + "load": "Load", + "lock": "Lock", + "make_event": "Make event", + "mario": "Mario", + "meme": "Meme", + "menu": "Menu", + "music_247": "Music 247", + "music_clear": "Music clear", + "music_filter": "Music filter", + "music_jump": "Music jump", + "music_loop": "Music loop", + "music_lyrics": "Music lyrics", + "music_move": "Music move", + "music_nowplaying": "Music nowplaying", + "music_panel": "Music panel", + "music_pause": "Music pause", + "music_play": "Music play", + "music_playlist_delete": "Music playlist delete", + "music_playlist_rename": "Music playlist rename", + "music_playlist_save": "Music playlist save", + "music_previous": "Music previous", + "music_queue": "Music queue", + "music_remove": "Music remove", + "music_resume": "Music resume", + "music_seek": "Music seek", + "music_shuffle": "Music shuffle", + "music_skip": "Music skip", + "music_stop": "Music stop", + "music_volume": "Music volume", + "mute": "Mute", + "organizechannels": "Organizechannels", + "othello": "Othello", + "ping": "Ping", + "play": "Play", + "poll": "Poll", + "profile": "Profile", + "purge": "Purge", + "rank": "Rank", + "reload": "Reload", + "remind": "Remind", + "rob": "Rob", + "roll": "Roll", + "rps": "Rps", + "serverinfo": "Serverinfo", + "set": "Set", + "set_automod": "Set automod", + "set_dailybutton": "Set dailybutton", + "set_dailychannel": "Set dailychannel", + "set_dailyimage": "Set dailyimage", + "set_dailymessage": "Set dailymessage", + "set_dailytime": "Set dailytime", + "set_dailytitle": "Set dailytitle", + "set_dailytoggle": "Set dailytoggle", + "set_freegames": "Set freegames", + "set_gamenews": "Set gamenews", + "set_log": "Set log", + "set_pollchannel": "Set pollchannel", + "set_suggestions": "Set suggestions", + "set_supportai": "Set supportai", + "set_welcome": "Set welcome", + "set_wisdom": "Set wisdom", + "setupserver": "Setupserver", + "shield_level": "Set shield sensitivity level", + "shield_state": "Show current shield state", + "shop": "Shop", + "shutdown": "Shutdown", + "slots": "Slots", + "slowmode": "Slowmode", + "speak": "Speak", + "summarize": "Summarize", + "sync": "Sync", + "system_audit": "Show system audit and diagnostics", + "ticket": "Ticket command group", + "tournament": "Tournament", + "tournament_create": "Tournament create", + "tournament_end": "Tournament end", + "tournament_gamehub": "Tournament gamehub", + "tournament_join": "Tournament join", + "tournament_lb": "Tournament lb", + "tournament_panel": "Tournament panel", + "tournament_start": "Tournament start", + "transfer": "Transfer", + "translate": "Translate", + "translate_voice": "Translate voice", + "trivia": "Trivia", + "tts": "Tts", + "unban": "Unban", + "unload": "Unload", + "unlock": "Unlock", + "unmute": "Unmute", + "upscale": "Upscale", + "userinfo": "Userinfo", + "verify": "Verify", + "verifysetup": "Verifysetup", + "warn": "Warn", + "warnings": "Warnings", + "wisdom_today": "Wisdom today", + "work": "Work", + "xo": "Xo", + "xp": "Xp" + }, + "quick_ai": "AI admin request" }, "gambling": { "blackjack": { @@ -345,5 +506,70 @@ "play_roulette": "<:catjam:1476278750378201260> Jugar Ruleta", "play_rpg": "<:therock:1476278719994794054> Aventura RPG" } + }, + "admin": { + "panel_title": "Admin Control Panel", + "economy_admin": "Economy Administration", + "shield_control": "Shield Control", + "system_status": "System Status", + "backup_created": "Backup created successfully.", + "backup_restored": "Backup restored successfully.", + "backup_deleted": "Backup deleted.", + "no_backups": "No backups found." + }, + "verification": { + "panel_title": "Verification Panel", + "verify_button": "Verify Me", + "verified_role": "Verified", + "already_verified": "You are already verified.", + "verification_success": "Verification successful! Welcome to the server." + }, + "board_games": { + "panel_title": "Board Games Panel", + "chess": "Chess", + "checkers": "Checkers", + "connect4": "Connect 4", + "othello": "Othello", + "tictactoe": "Tic Tac Toe", + "rps": "Rock Paper Scissors", + "game_started": "Game started!", + "your_turn": "Your turn!", + "waiting": "Waiting for opponent..." + }, + "media": { + "now_playing": "Now Playing", + "queue_added": "Added to queue", + "no_music": "No music playing", + "volume_set": "Volume set to {volume}%", + "skipped": "Skipped track", + "stopped": "Stopped playback", + "paused": "Paused playback", + "resumed": "Resumed playback" + }, + "fun": { + "rps_title": "Rock Paper Scissors", + "rps_rock": "Rock", + "rps_paper": "Paper", + "rps_scissors": "Scissors", + "you_win": "You win!", + "you_lose": "You lose!", + "draw": "Draw!", + "trivia_title": "Trivia Question", + "trivia_correct": "Correct!", + "trivia_wrong": "Wrong!" + }, + "config": { + "visuals": { + "divider": "<:editprofilewhite:1476278814894981200> ??? ???????? ????????? ??? <:editprofilewhite:1476278814894981200>" + } + }, + "ping": { + "title": "?? Pong", + "desc": "Latency: **{latency}ms**" + }, + "roll": "{user} rolled **{value}** (1-{limit}).", + "lang": { + "current": "Current server language: **English**.", + "updated": "Bot language changed successfully." } } diff --git a/bot/locales/fr.json b/bot/locales/fr.json index 486770809f3525eee29ae29be6fef28cf40fd108..d6d6b09576580f1e31bbe0c5701d256c045966c2 100644 --- a/bot/locales/fr.json +++ b/bot/locales/fr.json @@ -89,6 +89,12 @@ }, "ai": { "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』" + }, + "ai_architect": { + "title": "?? AI Architect Setup Complete", + "proposal": "Proposed setup sections: {sections}", + "analyzing": "Analyzing your server requirements...", + "success": "AI Architect finished successfully." } }, "errors": { @@ -267,8 +273,163 @@ "set_banner": "Définir une bannière personnalisée pour ce serveur", "remove_banner": "Supprimer la bannière personnalisée de ce serveur", "view_banner": "Voir la bannière personnalisée actuelle de ce serveur", - "banner_help": "Apprendre à définir une bannière personnalisée" - } + "banner_help": "Apprendre à définir une bannière personnalisée", + "8ball": "8ball", + "add_scam_image": "Save scam image signature", + "admin_panel": "Open admin control panel", + "ai": "Ai", + "ai_auto": "Ai auto", + "ai_channel": "Ai channel", + "ai_model": "Ai model", + "ask_image": "Ask image", + "avatar": "Avatar", + "awesomeroles": "Awesomeroles", + "backup_panel": "Open backup management panel", + "backupserver": "Backupserver", + "balance": "Balance", + "ban": "Ban", + "banner": "Banner", + "board_forfeit": "Board forfeit", + "board_move": "Board move", + "board_start": "Board start", + "boardgames": "Boardgames", + "botstats": "Botstats", + "buy": "Buy", + "chat": "Chat", + "checkers": "Checkers", + "chess": "Chess", + "choose": "Choose", + "clearwarn": "Clearwarn", + "cloneemoji": "Cloneemoji", + "code_gen": "Code gen", + "coinflip": "Coinflip", + "connect4": "Connect4", + "daily": "Daily", + "debug": "Debug", + "dice": "Dice", + "econ_admin": "Manage user economy balances", + "economy": "Economy", + "economy_deposit": "Economy deposit", + "economy_gamble": "Economy gamble", + "economy_rob": "Economy rob", + "economy_withdraw": "Economy withdraw", + "free_games": "Free games", + "gamble": "Gamble", + "gamehub": "Gamehub", + "games_panel": "Games panel", + "gaming_news": "Gaming news", + "giveaway": "Giveaway command group", + "guess": "Guess", + "image_gen": "Image gen", + "imagine": "Imagine", + "inventory": "Inventory", + "kick": "Kick", + "language": "Language", + "languages": "Languages", + "leaderboard": "Leaderboard", + "level": "Level", + "load": "Load", + "lock": "Lock", + "make_event": "Make event", + "mario": "Mario", + "meme": "Meme", + "menu": "Menu", + "music_247": "Music 247", + "music_clear": "Music clear", + "music_filter": "Music filter", + "music_jump": "Music jump", + "music_loop": "Music loop", + "music_lyrics": "Music lyrics", + "music_move": "Music move", + "music_nowplaying": "Music nowplaying", + "music_panel": "Music panel", + "music_pause": "Music pause", + "music_play": "Music play", + "music_playlist_delete": "Music playlist delete", + "music_playlist_rename": "Music playlist rename", + "music_playlist_save": "Music playlist save", + "music_previous": "Music previous", + "music_queue": "Music queue", + "music_remove": "Music remove", + "music_resume": "Music resume", + "music_seek": "Music seek", + "music_shuffle": "Music shuffle", + "music_skip": "Music skip", + "music_stop": "Music stop", + "music_volume": "Music volume", + "mute": "Mute", + "organizechannels": "Organizechannels", + "othello": "Othello", + "ping": "Ping", + "play": "Play", + "poll": "Poll", + "profile": "Profile", + "purge": "Purge", + "rank": "Rank", + "reload": "Reload", + "remind": "Remind", + "rob": "Rob", + "roll": "Roll", + "rps": "Rps", + "serverinfo": "Serverinfo", + "set": "Set", + "set_automod": "Set automod", + "set_dailybutton": "Set dailybutton", + "set_dailychannel": "Set dailychannel", + "set_dailyimage": "Set dailyimage", + "set_dailymessage": "Set dailymessage", + "set_dailytime": "Set dailytime", + "set_dailytitle": "Set dailytitle", + "set_dailytoggle": "Set dailytoggle", + "set_freegames": "Set freegames", + "set_gamenews": "Set gamenews", + "set_log": "Set log", + "set_pollchannel": "Set pollchannel", + "set_suggestions": "Set suggestions", + "set_supportai": "Set supportai", + "set_welcome": "Set welcome", + "set_wisdom": "Set wisdom", + "setupserver": "Setupserver", + "shield_level": "Set shield sensitivity level", + "shield_state": "Show current shield state", + "shop": "Shop", + "shutdown": "Shutdown", + "slots": "Slots", + "slowmode": "Slowmode", + "speak": "Speak", + "summarize": "Summarize", + "sync": "Sync", + "system_audit": "Show system audit and diagnostics", + "ticket": "Ticket command group", + "tournament": "Tournament", + "tournament_create": "Tournament create", + "tournament_end": "Tournament end", + "tournament_gamehub": "Tournament gamehub", + "tournament_join": "Tournament join", + "tournament_lb": "Tournament lb", + "tournament_panel": "Tournament panel", + "tournament_start": "Tournament start", + "transfer": "Transfer", + "translate": "Translate", + "translate_voice": "Translate voice", + "trivia": "Trivia", + "tts": "Tts", + "unban": "Unban", + "unload": "Unload", + "unlock": "Unlock", + "unmute": "Unmute", + "upscale": "Upscale", + "userinfo": "Userinfo", + "verify": "Verify", + "verifysetup": "Verifysetup", + "warn": "Warn", + "warnings": "Warnings", + "wisdom_today": "Wisdom today", + "work": "Work", + "xo": "Xo", + "xp": "Xp" + }, + "quick_ai": "AI admin request" }, "gambling": { "blackjack": { @@ -345,5 +506,70 @@ "play_roulette": "<:catjam:1476278750378201260> Jouer à la Roulette", "play_rpg": "<:therock:1476278719994794054> Aventure RPG" } + }, + "admin": { + "panel_title": "Admin Control Panel", + "economy_admin": "Economy Administration", + "shield_control": "Shield Control", + "system_status": "System Status", + "backup_created": "Backup created successfully.", + "backup_restored": "Backup restored successfully.", + "backup_deleted": "Backup deleted.", + "no_backups": "No backups found." + }, + "verification": { + "panel_title": "Verification Panel", + "verify_button": "Verify Me", + "verified_role": "Verified", + "already_verified": "You are already verified.", + "verification_success": "Verification successful! Welcome to the server." + }, + "board_games": { + "panel_title": "Board Games Panel", + "chess": "Chess", + "checkers": "Checkers", + "connect4": "Connect 4", + "othello": "Othello", + "tictactoe": "Tic Tac Toe", + "rps": "Rock Paper Scissors", + "game_started": "Game started!", + "your_turn": "Your turn!", + "waiting": "Waiting for opponent..." + }, + "media": { + "now_playing": "Now Playing", + "queue_added": "Added to queue", + "no_music": "No music playing", + "volume_set": "Volume set to {volume}%", + "skipped": "Skipped track", + "stopped": "Stopped playback", + "paused": "Paused playback", + "resumed": "Resumed playback" + }, + "fun": { + "rps_title": "Rock Paper Scissors", + "rps_rock": "Rock", + "rps_paper": "Paper", + "rps_scissors": "Scissors", + "you_win": "You win!", + "you_lose": "You lose!", + "draw": "Draw!", + "trivia_title": "Trivia Question", + "trivia_correct": "Correct!", + "trivia_wrong": "Wrong!" + }, + "config": { + "visuals": { + "divider": "<:editprofilewhite:1476278814894981200> ??? ???????? ????????? ??? <:editprofilewhite:1476278814894981200>" + } + }, + "ping": { + "title": "?? Pong", + "desc": "Latency: **{latency}ms**" + }, + "roll": "{user} rolled **{value}** (1-{limit}).", + "lang": { + "current": "Current server language: **English**.", + "updated": "Bot language changed successfully." } -} \ No newline at end of file +} diff --git a/bot/locales/he.json b/bot/locales/he.json new file mode 100644 index 0000000000000000000000000000000000000000..ddc0d94cb130f98213de1c95ee78b81ca48bdce8 --- /dev/null +++ b/bot/locales/he.json @@ -0,0 +1,575 @@ +{ + "commands": { + "admin": { + "purge_desc": "Delete multiple messages.", + "ban_desc": "Ban a member from the server.", + "unban_desc": "Unban a previously banned user.", + "kick_desc": "Kick a member from the server.", + "mute_desc": "Mute a member for a period.", + "unmute_desc": "Unmute a muted member.", + "warn_desc": "Warn a member.", + "warnings_desc": "Show a member's warnings.", + "clearwarn_desc": "Clear a member's warnings.", + "slowmode_desc": "Set channel slowmode.", + "lock_desc": "Lock the current channel.", + "unlock_desc": "Unlock the current channel." + }, + "ai": { + "chat_desc": "Chat with AI.", + "ask_image_desc": "Analyze an image with AI.", + "imagine_desc": "Generate image prompts.", + "image_gen_desc": "Create an image from text.", + "upscale_desc": "Upscale an image.", + "summarize_desc": "Summarize recent channel messages.", + "speak_desc": "Speak AI response in voice channel.", + "model_desc": "Change the AI model.", + "channel_desc": "Set the auto AI chat channel.", + "auto_desc": "Enable or disable auto AI replies." + }, + "boardgames": { + "games_panel_desc": "Open the interactive board games panel.", + "board_start_desc": "Start a board game.", + "board_move_desc": "Play a move in a board game.", + "board_forfeit_desc": "Forfeit the current board game." + }, + "economy": { + "economy_desc": "Open the economy panel.", + "balance_desc": "Show your balance.", + "daily_desc": "Claim daily reward.", + "work_desc": "Work to earn coins.", + "gamble_desc": "Gamble your coins.", + "rob_desc": "Attempt to rob another user.", + "leaderboard_desc": "Show economy leaderboard.", + "shop_desc": "Open the item shop.", + "buy_desc": "Buy an item.", + "inventory_desc": "Show your inventory.", + "transfer_desc": "Transfer money to another member." + }, + "music": { + "panel_desc": "Open the music control panel.", + "play_desc": "Play a track by URL or query.", + "pause_desc": "Pause or resume playback.", + "skip_desc": "Skip the current track.", + "stop_desc": "Stop playback and clear queue.", + "queue_desc": "Show the queue.", + "volume_desc": "Adjust playback volume.", + "playlist_desc": "Open saved playlist panel." + }, + "tools": { + "menu_desc": "Open the command hub.", + "language_desc": "Change server language.", + "serverinfo_desc": "Show detailed server info.", + "userinfo_desc": "Show detailed user info.", + "botstats_desc": "Show bot statistics.", + "translate_desc": "Translate text instantly.", + "poll_desc": "Create a poll.", + "remind_desc": "Set a reminder." + } + }, + "panels": { + "global": { + "prefix": "", + "divider": "<:editprofilewhite:1476278814894981200> ━━━ 𝕄𝕒𝕚𝕟 ℕ𝕖𝕩𝕦𝕤 ━━━ <:editprofilewhite:1476278814894981200>", + "bullet": "〣" + }, + "menu": { + "header": "『 <:editprofilewhite:1476278814894981200> 𝕄𝕒𝕚𝕟 ℕ𝕖𝕩𝕦𝕤 〣 』", + "menu_title": "『 𝕄𝕒𝕤𝕥𝕖𝕣 ℂ𝕠𝕞𝕞𝕒𝕟𝕕𝕤 』", + "title": "『 <:editprofilewhite:1476278814894981200> Main Control Nexus 〣 』", + "desc": "Welcome to the integrated system. Select a category below:" + }, + "economy": { + "header": "『 ㊙️ 𝕀𝕞𝕡𝕖𝕣𝕚𝕒𝕝 𝔹𝕒𝕟𝕜 〣 』" + }, + "games": { + "header": "『 𝕄𝕒𝕚𝕟 ℕ𝕖𝕩𝕦𝕤 𝔾𝕒𝕞𝕚𝕟𝕘 𝔻𝕠𝕛𝕠 〣 』" + }, + "music": { + "header": "『 <:zap:1476278712914804777> 𝕄𝕦𝕤𝕚𝕔 ℙ𝕒𝕧𝕚𝕝𝕚𝕠𝕟 〣 』" + }, + "ai": { + "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』" + }, + "ai_architect": { + "title": "?? AI Architect Setup Complete", + "proposal": "Proposed setup sections: {sections}", + "analyzing": "Analyzing your server requirements...", + "success": "AI Architect finished successfully." + } + }, + "errors": { + "common": "Something went wrong. Please try again.", + "server_only": "This command works in servers only.", + "music": { + "lavalink_unavailable": "Lavalink node is not connected. Check configuration.", + "player_unavailable": "Lavalink player not available." + }, + "ai": { + "rate_limited": "The AI provider is rate-limiting requests right now (429). Please retry in about a minute.", + "model_unavailable": "The selected model is temporarily unavailable. Please choose another OpenRouter model and try again.", + "memory_error": "The upstream model ran out of memory. Please retry with a shorter request.", + "generic_error": "I couldn't complete this request due to a temporary AI service issue. Please try again shortly." + } + }, + "success": { + "panel_refreshed": "Panel refreshed successfully.", + "purge_done": "Messages were deleted successfully.", + "language_updated": "???? ?????? ??????." + }, + "common": { + "server_only": "????? ?? ????? ?? ???? ???.", + "error": "????? ????? ?????. ??? ???." + }, + "music": { + "lavalink_unavailable": "Lavalink node is not connected. Check configuration.", + "player_unavailable": "Lavalink player not available.", + "queue_shuffled": "Queue shuffled!", + "loop_mode": "Loop mode: **{mode}**" + }, + "ai": { + "rate_limited": "The AI provider is rate-limiting requests right now (429). Please retry in about a minute.", + "model_unavailable": "The selected model is temporarily unavailable. Please choose another OpenRouter model and try again.", + "memory_error": "The upstream model ran out of memory. Please retry with a shorter request.", + "generic_error": "I couldn't complete this request due to a temporary AI service issue. Please try again shortly." + }, + "economy": { + "daily": { + "already_claimed": "<:hellokittyjam:1476278701984317512> You already claimed your daily reward. Come back tomorrow!", + "reward_received": "<:hellokittyjam:1476278701984317512> Daily reward received: **{reward}** coins." + }, + "work": { + "earned": "<:rubberhosebongocat:1476278733534007448> Work completed: **{reward}** coins earned.", + "cooldown": "<:birdvibetriangle:1476278751741345873> Come back in **{mins}** minutes." + }, + "panel": { + "line_one": "Earn coins via Daily and Work actions.", + "line_two": "Use Bank snapshot and /deposit /withdraw to manage funds.", + "footer": "Work • Daily • Bank" + }, + "labels": { + "wallet": "<:milk10:1476278762168520809> Wallet", + "bank": "<:basedsigma:1476278736692183061> Bank", + "level": "<:letsgo:1476278740232442012> Level", + "progress": "<:settingswhite:1476278810985894111> Progress" + }, + "bank": { + "snapshot": "<:basedsigma:1476278736692183061> Wallet: `{wallet:,}` • Bank: `{bank:,}`" + } + }, + "security": { + "scam": { + "alert": "AI Scam Radar removed scam content by {user} in {channel} and applied a 1-hour timeout.", + "user_notice": "Your message was removed by AI Scam Radar due to suspected phishing/scam content." + } + }, + "welcome": { + "verify_title": "<:accountisconnected:1476278715012088011> Imperial Verification Gate", + "verify_body": "Press the verify button below to unlock full server access instantly.", + "verify_button": "Unlock Access", + "verify_benefits_title": "After Verification", + "verify_benefits_body": "• Access all channels\n• Use every command\n• Join events and systems", + "verify_footer": "<:editprofilewhite:1476278814894981200> Security check powered by BOT-", + "verify_role_missing": "Verification role is not configured.", + "verify_role_deleted": "Configured verification role no longer exists.", + "verify_member_missing": "Could not find your member profile.", + "verify_already_done": "You are already verified <:letsgo:1476278740232442012>", + "verify_success": "<:letsgo:1476278740232442012> Verification complete! Welcome to the server.", + "verify_log_title": "<:letsgo:1476278740232442012> Verification Success", + "verify_log_desc": "{member} passed verification and received {role}.", + "post_verify_title": "## 𝕎𝕖𝕝𝕔𝕠𝕞𝕖 𝕥𝕠 𝕥𝕙𝕖 𝕀𝕟𝕟𝕖𝕣 𝕊𝕒𝕟𝕔𝕥𝕦𝕒𝕣𝕪 〣", + "post_verify_msg": "Warrior {user.mention} has passed the trials!" + }, + "ai_architect": { + "categories": { + "admin": "〖 𝔸𝕕𝕞𝕚𝕟 𝕊𝕦𝕚𝕥𝕖 〗", + "ai": "〖 𝔸𝕀 𝕊𝕖𝕔𝕥𝕚𝕠𝕟 〗", + "community": "〖 𝕔𝕠𝕞𝕞𝕦𝕟𝕚𝕥𝕪 〗" + }, + "channels": { + "admin_logs": "〖 𝔸𝕕𝕞𝕚𝕟-𝕃𝕠𝕘𝕤 〗", + "ai_chat": "〖 𝔸𝕀-𝕔𝕙𝕒𝕥 〗", + "ai_lab": "〖 𝔸𝕀-𝕝𝕒𝕓 〗", + "welcome": "〖 𝕨𝕖𝕝𝕔𝕠𝕞𝕖 〗" + }, + "topics": { + "admin_logs": "Administrative and security logs.", + "ai_chat": "Primary AI auto-chat channel.", + "ai_lab": "AI experiments and prompts.", + "welcome": "Welcome and onboarding messages." + }, + "embed": { + "title": "㊙️ AI Architect Setup Complete", + "description": "Professional AI server structure has been provisioned.", + "field_created": "Created Channels" + } + }, + "menu": { + "select_all_desc": "Browse every available command.", + "select_ai_desc": "Show AI-only commands.", + "select_placeholder": "Choose a category...", + "all": "?? ???????", + "ai": "?????? AI", + "refresh": "?????", + "invite_button": "Invite Bot", + "hub_welcome": "Welcome to BOT- control hub.", + "hub_explore": "Pick a category to view commands and descriptions.", + "hub_usage": "Use buttons for quick navigation.", + "stats_heading": "<:settingswhite:1476278810985894111> Stats", + "categories_heading": "<:editprofiledark:1476278812923658432> Categories", + "tips_heading": "<:spotifyfavourite:1476278718287708391> Tips", + "quick_heading": "<:zap:1476278712914804777> Quick Start", + "tip_line_1": "Use /menu anytime to refresh this panel.", + "tip_line_2": "Use slash commands for autocomplete support.", + "tip_line_3": "Set language with /language for localized help.", + "quick_music": "Open music controls", + "quick_gamehub": "Open game utilities", + "quick_tournament": "Manage tournaments", + "quick_economy": "Open economy panel", + "updates_heading": "<:partytime:1476278712914804777> Latest Updates", + "update_cloneemoji": "Clone emojis from source servers", + "update_shield_image": "Save scam-image signatures quickly", + "update_poll_group": "Use unified poll commands", + "update_economy_group": "Use grouped economy actions", + "none": "No data", + "page_footer": "Page {page}/{total} • BOT- AI Suite", + "powered_by": "Powered by BOT- AI Suite", + "category_desc": { + "aisuite": "AI setup, automation and chat tools", + "community": "Suggestions, giveaways and tickets", + "media": "Music playback and playlists", + "configuration": "Server bot configuration", + "engagement": "Economy, leveling and tournaments", + "utility": "Utility and information commands", + "events": "Logs, automations and alerts", + "admin": "Moderation and admin tools", + "language": "Language controls", + "verification": "Verification system", + "fun": "Fun and mini-games", + "developer": "Developer commands", + "menu": "Command menu", + "server_manager": "Server structuring tools", + "board_games": "Board games", + "gambling": "Casino games and RPG adventures" + }, + "cmd": { + "suggest": "Submit a suggestion", + "suggestion": "Moderate suggestions", + "suggestion_show": "Show suggestion by ID", + "suggestion_panel": "Suggestions control panel", + "suggestion_voters": "Show suggestion voters", + "giveaway_create": "Create giveaway", + "giveaway_end": "End giveaway", + "ticket_panel": "Open global ticket panel", + "ai_execute": "Execute practical AI task", + "ai_setup": "AI server setup", + "music_playlist": "Open saved playlists", + "music_youtube_search": "Interactive YouTube search", + "blackjack": "Play interactive Blackjack casino game", + "roulette": "Play interactive Roulette casino game", + "rpg": "Start an RPG adventure with monsters and treasures", + "ai_admin": "Let AI administrate the server", + "ai_help": "Show AI Admin capabilities", + "gambling_panel": "Open the gambling panel", + "set_banner": "Set a custom banner for this server", + "remove_banner": "Remove the custom banner from this server", + "view_banner": "View the current custom banner for this server", + "banner_help": "Learn how to set a custom banner", + "8ball": "8ball", + "add_scam_image": "Save scam image signature", + "admin_panel": "Open admin control panel", + "ai": "Ai", + "ai_auto": "Ai auto", + "ai_channel": "Ai channel", + "ai_model": "Ai model", + "ask_image": "Ask image", + "avatar": "Avatar", + "awesomeroles": "Awesomeroles", + "backup_panel": "Open backup management panel", + "backupserver": "Backupserver", + "balance": "Balance", + "ban": "Ban", + "banner": "Banner", + "board_forfeit": "Board forfeit", + "board_move": "Board move", + "board_start": "Board start", + "boardgames": "Boardgames", + "botstats": "Botstats", + "buy": "Buy", + "chat": "Chat", + "checkers": "Checkers", + "chess": "Chess", + "choose": "Choose", + "clearwarn": "Clearwarn", + "cloneemoji": "Cloneemoji", + "code_gen": "Code gen", + "coinflip": "Coinflip", + "connect4": "Connect4", + "daily": "Daily", + "debug": "Debug", + "dice": "Dice", + "econ_admin": "Manage user economy balances", + "economy": "Economy", + "economy_deposit": "Economy deposit", + "economy_gamble": "Economy gamble", + "economy_rob": "Economy rob", + "economy_withdraw": "Economy withdraw", + "free_games": "Free games", + "gamble": "Gamble", + "gamehub": "Gamehub", + "games_panel": "Games panel", + "gaming_news": "Gaming news", + "giveaway": "Giveaway command group", + "guess": "Guess", + "image_gen": "Image gen", + "imagine": "Imagine", + "inventory": "Inventory", + "kick": "Kick", + "language": "Language", + "languages": "Languages", + "leaderboard": "Leaderboard", + "level": "Level", + "load": "Load", + "lock": "Lock", + "make_event": "Make event", + "mario": "Mario", + "meme": "Meme", + "menu": "Menu", + "music_247": "Music 247", + "music_clear": "Music clear", + "music_filter": "Music filter", + "music_jump": "Music jump", + "music_loop": "Music loop", + "music_lyrics": "Music lyrics", + "music_move": "Music move", + "music_nowplaying": "Music nowplaying", + "music_panel": "Music panel", + "music_pause": "Music pause", + "music_play": "Music play", + "music_playlist_delete": "Music playlist delete", + "music_playlist_rename": "Music playlist rename", + "music_playlist_save": "Music playlist save", + "music_previous": "Music previous", + "music_queue": "Music queue", + "music_remove": "Music remove", + "music_resume": "Music resume", + "music_seek": "Music seek", + "music_shuffle": "Music shuffle", + "music_skip": "Music skip", + "music_stop": "Music stop", + "music_volume": "Music volume", + "mute": "Mute", + "organizechannels": "Organizechannels", + "othello": "Othello", + "ping": "Ping", + "play": "Play", + "poll": "Poll", + "profile": "Profile", + "purge": "Purge", + "rank": "Rank", + "reload": "Reload", + "remind": "Remind", + "rob": "Rob", + "roll": "Roll", + "rps": "Rps", + "serverinfo": "Serverinfo", + "set": "Set", + "set_automod": "Set automod", + "set_dailybutton": "Set dailybutton", + "set_dailychannel": "Set dailychannel", + "set_dailyimage": "Set dailyimage", + "set_dailymessage": "Set dailymessage", + "set_dailytime": "Set dailytime", + "set_dailytitle": "Set dailytitle", + "set_dailytoggle": "Set dailytoggle", + "set_freegames": "Set freegames", + "set_gamenews": "Set gamenews", + "set_log": "Set log", + "set_pollchannel": "Set pollchannel", + "set_suggestions": "Set suggestions", + "set_supportai": "Set supportai", + "set_welcome": "Set welcome", + "set_wisdom": "Set wisdom", + "setupserver": "Setupserver", + "shield_level": "Set shield sensitivity level", + "shield_state": "Show current shield state", + "shop": "Shop", + "shutdown": "Shutdown", + "slots": "Slots", + "slowmode": "Slowmode", + "speak": "Speak", + "summarize": "Summarize", + "sync": "Sync", + "system_audit": "Show system audit and diagnostics", + "ticket": "Ticket command group", + "tournament": "Tournament", + "tournament_create": "Tournament create", + "tournament_end": "Tournament end", + "tournament_gamehub": "Tournament gamehub", + "tournament_join": "Tournament join", + "tournament_lb": "Tournament lb", + "tournament_panel": "Tournament panel", + "tournament_start": "Tournament start", + "transfer": "Transfer", + "translate": "Translate", + "translate_voice": "Translate voice", + "trivia": "Trivia", + "tts": "Tts", + "unban": "Unban", + "unload": "Unload", + "unlock": "Unlock", + "unmute": "Unmute", + "upscale": "Upscale", + "userinfo": "Userinfo", + "verify": "Verify", + "verifysetup": "Verifysetup", + "warn": "Warn", + "warnings": "Warnings", + "wisdom_today": "Wisdom today", + "work": "Work", + "xo": "Xo", + "xp": "Xp" + }, + "quick_ai": "AI admin request" + }, + "gambling": { + "blackjack": { + "title": "<:djpeepo:1476278742920724663> Blackjack Casino", + "dealer_hand": "<:gigachad:1476278801485926483> Dealer's Hand", + "your_hand": "<:therock:1476278719994794054> Your Hand", + "bet": "<:milk10:1476278762168520809> Bet", + "bust": "<:boomer:1476278723907948746> **BUST!** You lost `{bet:,}` coins.", + "dealer_blackjack": "<:boomer:1476278723907948746> **Dealer Blackjack!** You lost `{bet:,}` coins.", + "dealer_busts": "<:letsgo:1476278740232442012> **Dealer Busts! You Win!** +`{winnings:,}` coins!", + "you_win": "<:letsgo:1476278740232442012> **You Win!** +`{winnings:,}` coins!", + "dealer_wins": "<:boomer:1476278723907948746> **Dealer Wins!** You lost `{bet:,}` coins.", + "push": "🤝 **Push!** Bet returned.", + "not_your_game": "<:boomer:1476278723907948746> Not your game.", + "insufficient_funds": "<:boomer:1476278723907948746> Insufficient funds to double.", + "min_bet": "<:boomer:1476278723907948746> Minimum bet is 10 coins.", + "invalid_bet": "<:boomer:1476278723907948746> Invalid bet amount.", + "economy_not_available": "<:boomer:1476278723907948746> Economy not available.", + "insufficient_wallet": "<:boomer:1476278723907948746> Insufficient funds. You have {wallet:,} coins.", + "game_started": "<:letsgo:1476278740232442012> Blackjack game started!" + }, + "roulette": { + "title": "<:catjam:1476278750378201260> Roulette Casino", + "result": "<:target:1476278712914804777> Result", + "bet": "<:milk10:1476278762168520809> Bet", + "winnings": "💵 Result", + "you_won": "<:letsgo:1476278740232442012> **You Won!** +`{winnings:,}` coins!", + "you_lost": "<:boomer:1476278723907948746> **You Lost!** -`{losses:,}` coins.", + "place_bet": "Place your bet and spin the wheel!", + "wheel": "<:catjam:1476278750378201260> Roulette Wheel", + "bet_red": "Bet Red <:boomer:1476278723907948746>", + "bet_black": "Bet Black <:basedsigma:1476278736692183061>", + "bet_even": "Bet Even", + "bet_odd": "Bet Odd", + "place_bet_modal": "<:catjam:1476278750378201260> Place Your Bet", + "bet_amount_label": "Bet Amount", + "bet_amount_placeholder": "Enter amount (min 10)" + }, + "rpg": { + "title": "<:therock:1476278719994794054> RPG Adventure", + "description": "Choose your action below to begin your adventure!", + "fight_monster": "<:therock:1476278719994794054> Fight Monster", + "find_treasure": "<:cinnamusic:1476278738357456926> Find Treasure", + "shop": "<:bongocatbmo:1476278804535312519> Shop", + "stats": "<:settingswhite:1476278810985894111> Stats", + "victory": "<:therock:1476278719994794054> RPG Adventure - Victory!", + "defeat": "<:therock:1476278719994794054> RPG Adventure - Defeat!", + "defeated_monster": "<:letsgo:1476278740232442012> **You defeated the {monster}!**", + "monster_defeated_you": "<:boomer:1476278723907948746> **The {monster} defeated you!**", + "coins_gain": "<:milk10:1476278762168520809> **+{reward:,} coins**", + "xp_gain": "<:wingedmusicnote:1476278698033287270> **+{xp_gain} XP**", + "coins_loss": "<:hamstermeme:1476278727926349958> **-{penalty:,} coins**", + "level_up": "<:settingsdark:1476278809580802058> Level {old_level} → {new_level}", + "tip": "<:spotifyfavourite:1476278718287708391> Tip: Level up to increase win chance!", + "treasure_success": "<:cinnamusic:1476278738357456926> Treasure Hunt - Success!", + "treasure_fail": "<:cinnamusic:1476278738357456926> Treasure Hunt - Nothing Found", + "found_treasure": "<:letsgo:1476278740232442012> **You found a hidden treasure!**", + "no_treasure": "<:boomer:1476278723907948746> **No treasure found in this area...**", + "treasure_map": "<:whalebongocat:1476278816744669227> The treasure map led you to riches!", + "try_different": "<:whalebongocat:1476278816744669227> Try exploring a different location next time!", + "shop_title": "<:bongocatbmo:1476278804535312519> RPG Shop", + "shop_desc": "Welcome to the shop! Buy items to help in your adventures.\n\n<:scottpilgrimrock:1476278716718911489> **Sword** - Increase win chance (Coming Soon)\n<:gigachad:1476278801485926483> **Shield** - Reduce coin loss on defeat (Coming Soon)\n<:aggretsuko:1476278807425056863> **Potion** - Heal after battle (Coming Soon)\n<:maintainscommands:1476278705473982494> **Map** - Better treasure finds (Coming Soon)", + "stats_title": "<:settingswhite:1476278810985894111> RPG Stats", + "level": "**Level:** {level}", + "xp": "**XP:** {xp:,}", + "wallet": "**Wallet:** {wallet:,} coins", + "bank": "**Bank:** {bank:,} coins", + "total_wealth": "**Total Wealth:** {total:,} coins" + }, + "panel": { + "title": "<:catjam:1476278750378201260> Casino & Gambling Panel", + "description": "Welcome to the Casino! Choose a game to play:\n\n<:djpeepo:1476278742920724663> **Blackjack** - Beat the dealer to 21\n<:catjam:1476278750378201260> **Roulette** - Bet on numbers and colors\n<:therock:1476278719994794054> **RPG Adventure** - Fight monsters and find treasure", + "play_blackjack": "<:djpeepo:1476278742920724663> Play Blackjack", + "play_roulette": "<:catjam:1476278750378201260> Play Roulette", + "play_rpg": "<:therock:1476278719994794054> RPG Adventure" + } + }, + "admin": { + "panel_title": "Admin Control Panel", + "economy_admin": "Economy Administration", + "shield_control": "Shield Control", + "system_status": "System Status", + "backup_created": "Backup created successfully.", + "backup_restored": "Backup restored successfully.", + "backup_deleted": "Backup deleted.", + "no_backups": "No backups found." + }, + "verification": { + "panel_title": "Verification Panel", + "verify_button": "Verify Me", + "verified_role": "Verified", + "already_verified": "You are already verified.", + "verification_success": "Verification successful! Welcome to the server." + }, + "board_games": { + "panel_title": "Board Games Panel", + "chess": "Chess", + "checkers": "Checkers", + "connect4": "Connect 4", + "othello": "Othello", + "tictactoe": "Tic Tac Toe", + "rps": "Rock Paper Scissors", + "game_started": "Game started!", + "your_turn": "Your turn!", + "waiting": "Waiting for opponent..." + }, + "media": { + "now_playing": "Now Playing", + "queue_added": "Added to queue", + "no_music": "No music playing", + "volume_set": "Volume set to {volume}%", + "skipped": "Skipped track", + "stopped": "Stopped playback", + "paused": "Paused playback", + "resumed": "Resumed playback" + }, + "fun": { + "rps_title": "Rock Paper Scissors", + "rps_rock": "Rock", + "rps_paper": "Paper", + "rps_scissors": "Scissors", + "you_win": "You win!", + "you_lose": "You lose!", + "draw": "Draw!", + "trivia_title": "Trivia Question", + "trivia_correct": "Correct!", + "trivia_wrong": "Wrong!" + }, + "config": { + "visuals": { + "divider": "<:editprofilewhite:1476278814894981200> ??? ???????? ????????? ??? <:editprofilewhite:1476278814894981200>" + } + }, + "ping": { + "title": "?? Pong", + "desc": "Latency: **{latency}ms**" + }, + "roll": "{user} rolled **{value}** (1-{limit}).", + "lang": { + "current": "Current server language: **English**.", + "updated": "Bot language changed successfully." + } +} diff --git a/bot/locales/hi.json b/bot/locales/hi.json index 5f3755365321e13e1d2a2c43efe0958e3d2f6a28..f0358b3d56a82678e77709d879f31e0471f8d915 100644 --- a/bot/locales/hi.json +++ b/bot/locales/hi.json @@ -89,6 +89,12 @@ }, "ai": { "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』" + }, + "ai_architect": { + "title": "?? AI Architect Setup Complete", + "proposal": "Proposed setup sections: {sections}", + "analyzing": "Analyzing your server requirements...", + "success": "AI Architect finished successfully." } }, "errors": { @@ -267,8 +273,163 @@ "set_banner": "इस सर्वर के लिए कस्टम बैनर सेट करें", "remove_banner": "इस सर्वर से कस्टम बैनर हटाएं", "view_banner": "इस सर्वर का वर्तमान कस्टम बैनर देखें", - "banner_help": "कस्टम बैनर सेट करना सीखें" - } + "banner_help": "कस्टम बैनर सेट करना सीखें", + "8ball": "8ball", + "add_scam_image": "Save scam image signature", + "admin_panel": "Open admin control panel", + "ai": "Ai", + "ai_auto": "Ai auto", + "ai_channel": "Ai channel", + "ai_model": "Ai model", + "ask_image": "Ask image", + "avatar": "Avatar", + "awesomeroles": "Awesomeroles", + "backup_panel": "Open backup management panel", + "backupserver": "Backupserver", + "balance": "Balance", + "ban": "Ban", + "banner": "Banner", + "board_forfeit": "Board forfeit", + "board_move": "Board move", + "board_start": "Board start", + "boardgames": "Boardgames", + "botstats": "Botstats", + "buy": "Buy", + "chat": "Chat", + "checkers": "Checkers", + "chess": "Chess", + "choose": "Choose", + "clearwarn": "Clearwarn", + "cloneemoji": "Cloneemoji", + "code_gen": "Code gen", + "coinflip": "Coinflip", + "connect4": "Connect4", + "daily": "Daily", + "debug": "Debug", + "dice": "Dice", + "econ_admin": "Manage user economy balances", + "economy": "Economy", + "economy_deposit": "Economy deposit", + "economy_gamble": "Economy gamble", + "economy_rob": "Economy rob", + "economy_withdraw": "Economy withdraw", + "free_games": "Free games", + "gamble": "Gamble", + "gamehub": "Gamehub", + "games_panel": "Games panel", + "gaming_news": "Gaming news", + "giveaway": "Giveaway command group", + "guess": "Guess", + "image_gen": "Image gen", + "imagine": "Imagine", + "inventory": "Inventory", + "kick": "Kick", + "language": "Language", + "languages": "Languages", + "leaderboard": "Leaderboard", + "level": "Level", + "load": "Load", + "lock": "Lock", + "make_event": "Make event", + "mario": "Mario", + "meme": "Meme", + "menu": "Menu", + "music_247": "Music 247", + "music_clear": "Music clear", + "music_filter": "Music filter", + "music_jump": "Music jump", + "music_loop": "Music loop", + "music_lyrics": "Music lyrics", + "music_move": "Music move", + "music_nowplaying": "Music nowplaying", + "music_panel": "Music panel", + "music_pause": "Music pause", + "music_play": "Music play", + "music_playlist_delete": "Music playlist delete", + "music_playlist_rename": "Music playlist rename", + "music_playlist_save": "Music playlist save", + "music_previous": "Music previous", + "music_queue": "Music queue", + "music_remove": "Music remove", + "music_resume": "Music resume", + "music_seek": "Music seek", + "music_shuffle": "Music shuffle", + "music_skip": "Music skip", + "music_stop": "Music stop", + "music_volume": "Music volume", + "mute": "Mute", + "organizechannels": "Organizechannels", + "othello": "Othello", + "ping": "Ping", + "play": "Play", + "poll": "Poll", + "profile": "Profile", + "purge": "Purge", + "rank": "Rank", + "reload": "Reload", + "remind": "Remind", + "rob": "Rob", + "roll": "Roll", + "rps": "Rps", + "serverinfo": "Serverinfo", + "set": "Set", + "set_automod": "Set automod", + "set_dailybutton": "Set dailybutton", + "set_dailychannel": "Set dailychannel", + "set_dailyimage": "Set dailyimage", + "set_dailymessage": "Set dailymessage", + "set_dailytime": "Set dailytime", + "set_dailytitle": "Set dailytitle", + "set_dailytoggle": "Set dailytoggle", + "set_freegames": "Set freegames", + "set_gamenews": "Set gamenews", + "set_log": "Set log", + "set_pollchannel": "Set pollchannel", + "set_suggestions": "Set suggestions", + "set_supportai": "Set supportai", + "set_welcome": "Set welcome", + "set_wisdom": "Set wisdom", + "setupserver": "Setupserver", + "shield_level": "Set shield sensitivity level", + "shield_state": "Show current shield state", + "shop": "Shop", + "shutdown": "Shutdown", + "slots": "Slots", + "slowmode": "Slowmode", + "speak": "Speak", + "summarize": "Summarize", + "sync": "Sync", + "system_audit": "Show system audit and diagnostics", + "ticket": "Ticket command group", + "tournament": "Tournament", + "tournament_create": "Tournament create", + "tournament_end": "Tournament end", + "tournament_gamehub": "Tournament gamehub", + "tournament_join": "Tournament join", + "tournament_lb": "Tournament lb", + "tournament_panel": "Tournament panel", + "tournament_start": "Tournament start", + "transfer": "Transfer", + "translate": "Translate", + "translate_voice": "Translate voice", + "trivia": "Trivia", + "tts": "Tts", + "unban": "Unban", + "unload": "Unload", + "unlock": "Unlock", + "unmute": "Unmute", + "upscale": "Upscale", + "userinfo": "Userinfo", + "verify": "Verify", + "verifysetup": "Verifysetup", + "warn": "Warn", + "warnings": "Warnings", + "wisdom_today": "Wisdom today", + "work": "Work", + "xo": "Xo", + "xp": "Xp" + }, + "quick_ai": "AI admin request" }, "gambling": { "blackjack": { @@ -345,5 +506,70 @@ "play_roulette": "<:catjam:1476278750378201260> रूलेट खेलें", "play_rpg": "<:therock:1476278719994794054> RPG साहसिक कार्य" } + }, + "admin": { + "panel_title": "Admin Control Panel", + "economy_admin": "Economy Administration", + "shield_control": "Shield Control", + "system_status": "System Status", + "backup_created": "Backup created successfully.", + "backup_restored": "Backup restored successfully.", + "backup_deleted": "Backup deleted.", + "no_backups": "No backups found." + }, + "verification": { + "panel_title": "Verification Panel", + "verify_button": "Verify Me", + "verified_role": "Verified", + "already_verified": "You are already verified.", + "verification_success": "Verification successful! Welcome to the server." + }, + "board_games": { + "panel_title": "Board Games Panel", + "chess": "Chess", + "checkers": "Checkers", + "connect4": "Connect 4", + "othello": "Othello", + "tictactoe": "Tic Tac Toe", + "rps": "Rock Paper Scissors", + "game_started": "Game started!", + "your_turn": "Your turn!", + "waiting": "Waiting for opponent..." + }, + "media": { + "now_playing": "Now Playing", + "queue_added": "Added to queue", + "no_music": "No music playing", + "volume_set": "Volume set to {volume}%", + "skipped": "Skipped track", + "stopped": "Stopped playback", + "paused": "Paused playback", + "resumed": "Resumed playback" + }, + "fun": { + "rps_title": "Rock Paper Scissors", + "rps_rock": "Rock", + "rps_paper": "Paper", + "rps_scissors": "Scissors", + "you_win": "You win!", + "you_lose": "You lose!", + "draw": "Draw!", + "trivia_title": "Trivia Question", + "trivia_correct": "Correct!", + "trivia_wrong": "Wrong!" + }, + "config": { + "visuals": { + "divider": "<:editprofilewhite:1476278814894981200> ??? ???????? ????????? ??? <:editprofilewhite:1476278814894981200>" + } + }, + "ping": { + "title": "?? Pong", + "desc": "Latency: **{latency}ms**" + }, + "roll": "{user} rolled **{value}** (1-{limit}).", + "lang": { + "current": "Current server language: **English**.", + "updated": "Bot language changed successfully." } -} \ No newline at end of file +} diff --git a/bot/locales/id.json b/bot/locales/id.json index fa402ce9c3469a157ea090b195fa3118d03409ee..704a0948dc741aa5b2ce00b2cf1fac0422bc81c9 100644 --- a/bot/locales/id.json +++ b/bot/locales/id.json @@ -89,6 +89,12 @@ }, "ai": { "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』" + }, + "ai_architect": { + "title": "?? AI Architect Setup Complete", + "proposal": "Proposed setup sections: {sections}", + "analyzing": "Analyzing your server requirements...", + "success": "AI Architect finished successfully." } }, "errors": { @@ -267,8 +273,163 @@ "set_banner": "Atur banner kustom untuk server ini", "remove_banner": "Hapus banner kustom dari server ini", "view_banner": "Lihat banner kustom saat ini dari server ini", - "banner_help": "Pelajari cara mengatur banner kustom" - } + "banner_help": "Pelajari cara mengatur banner kustom", + "8ball": "8ball", + "add_scam_image": "Save scam image signature", + "admin_panel": "Open admin control panel", + "ai": "Ai", + "ai_auto": "Ai auto", + "ai_channel": "Ai channel", + "ai_model": "Ai model", + "ask_image": "Ask image", + "avatar": "Avatar", + "awesomeroles": "Awesomeroles", + "backup_panel": "Open backup management panel", + "backupserver": "Backupserver", + "balance": "Balance", + "ban": "Ban", + "banner": "Banner", + "board_forfeit": "Board forfeit", + "board_move": "Board move", + "board_start": "Board start", + "boardgames": "Boardgames", + "botstats": "Botstats", + "buy": "Buy", + "chat": "Chat", + "checkers": "Checkers", + "chess": "Chess", + "choose": "Choose", + "clearwarn": "Clearwarn", + "cloneemoji": "Cloneemoji", + "code_gen": "Code gen", + "coinflip": "Coinflip", + "connect4": "Connect4", + "daily": "Daily", + "debug": "Debug", + "dice": "Dice", + "econ_admin": "Manage user economy balances", + "economy": "Economy", + "economy_deposit": "Economy deposit", + "economy_gamble": "Economy gamble", + "economy_rob": "Economy rob", + "economy_withdraw": "Economy withdraw", + "free_games": "Free games", + "gamble": "Gamble", + "gamehub": "Gamehub", + "games_panel": "Games panel", + "gaming_news": "Gaming news", + "giveaway": "Giveaway command group", + "guess": "Guess", + "image_gen": "Image gen", + "imagine": "Imagine", + "inventory": "Inventory", + "kick": "Kick", + "language": "Language", + "languages": "Languages", + "leaderboard": "Leaderboard", + "level": "Level", + "load": "Load", + "lock": "Lock", + "make_event": "Make event", + "mario": "Mario", + "meme": "Meme", + "menu": "Menu", + "music_247": "Music 247", + "music_clear": "Music clear", + "music_filter": "Music filter", + "music_jump": "Music jump", + "music_loop": "Music loop", + "music_lyrics": "Music lyrics", + "music_move": "Music move", + "music_nowplaying": "Music nowplaying", + "music_panel": "Music panel", + "music_pause": "Music pause", + "music_play": "Music play", + "music_playlist_delete": "Music playlist delete", + "music_playlist_rename": "Music playlist rename", + "music_playlist_save": "Music playlist save", + "music_previous": "Music previous", + "music_queue": "Music queue", + "music_remove": "Music remove", + "music_resume": "Music resume", + "music_seek": "Music seek", + "music_shuffle": "Music shuffle", + "music_skip": "Music skip", + "music_stop": "Music stop", + "music_volume": "Music volume", + "mute": "Mute", + "organizechannels": "Organizechannels", + "othello": "Othello", + "ping": "Ping", + "play": "Play", + "poll": "Poll", + "profile": "Profile", + "purge": "Purge", + "rank": "Rank", + "reload": "Reload", + "remind": "Remind", + "rob": "Rob", + "roll": "Roll", + "rps": "Rps", + "serverinfo": "Serverinfo", + "set": "Set", + "set_automod": "Set automod", + "set_dailybutton": "Set dailybutton", + "set_dailychannel": "Set dailychannel", + "set_dailyimage": "Set dailyimage", + "set_dailymessage": "Set dailymessage", + "set_dailytime": "Set dailytime", + "set_dailytitle": "Set dailytitle", + "set_dailytoggle": "Set dailytoggle", + "set_freegames": "Set freegames", + "set_gamenews": "Set gamenews", + "set_log": "Set log", + "set_pollchannel": "Set pollchannel", + "set_suggestions": "Set suggestions", + "set_supportai": "Set supportai", + "set_welcome": "Set welcome", + "set_wisdom": "Set wisdom", + "setupserver": "Setupserver", + "shield_level": "Set shield sensitivity level", + "shield_state": "Show current shield state", + "shop": "Shop", + "shutdown": "Shutdown", + "slots": "Slots", + "slowmode": "Slowmode", + "speak": "Speak", + "summarize": "Summarize", + "sync": "Sync", + "system_audit": "Show system audit and diagnostics", + "ticket": "Ticket command group", + "tournament": "Tournament", + "tournament_create": "Tournament create", + "tournament_end": "Tournament end", + "tournament_gamehub": "Tournament gamehub", + "tournament_join": "Tournament join", + "tournament_lb": "Tournament lb", + "tournament_panel": "Tournament panel", + "tournament_start": "Tournament start", + "transfer": "Transfer", + "translate": "Translate", + "translate_voice": "Translate voice", + "trivia": "Trivia", + "tts": "Tts", + "unban": "Unban", + "unload": "Unload", + "unlock": "Unlock", + "unmute": "Unmute", + "upscale": "Upscale", + "userinfo": "Userinfo", + "verify": "Verify", + "verifysetup": "Verifysetup", + "warn": "Warn", + "warnings": "Warnings", + "wisdom_today": "Wisdom today", + "work": "Work", + "xo": "Xo", + "xp": "Xp" + }, + "quick_ai": "AI admin request" }, "gambling": { "blackjack": { @@ -345,5 +506,70 @@ "play_roulette": "<:catjam:1476278750378201260> Main Roulette", "play_rpg": "<:therock:1476278719994794054> Petualangan RPG" } + }, + "admin": { + "panel_title": "Admin Control Panel", + "economy_admin": "Economy Administration", + "shield_control": "Shield Control", + "system_status": "System Status", + "backup_created": "Backup created successfully.", + "backup_restored": "Backup restored successfully.", + "backup_deleted": "Backup deleted.", + "no_backups": "No backups found." + }, + "verification": { + "panel_title": "Verification Panel", + "verify_button": "Verify Me", + "verified_role": "Verified", + "already_verified": "You are already verified.", + "verification_success": "Verification successful! Welcome to the server." + }, + "board_games": { + "panel_title": "Board Games Panel", + "chess": "Chess", + "checkers": "Checkers", + "connect4": "Connect 4", + "othello": "Othello", + "tictactoe": "Tic Tac Toe", + "rps": "Rock Paper Scissors", + "game_started": "Game started!", + "your_turn": "Your turn!", + "waiting": "Waiting for opponent..." + }, + "media": { + "now_playing": "Now Playing", + "queue_added": "Added to queue", + "no_music": "No music playing", + "volume_set": "Volume set to {volume}%", + "skipped": "Skipped track", + "stopped": "Stopped playback", + "paused": "Paused playback", + "resumed": "Resumed playback" + }, + "fun": { + "rps_title": "Rock Paper Scissors", + "rps_rock": "Rock", + "rps_paper": "Paper", + "rps_scissors": "Scissors", + "you_win": "You win!", + "you_lose": "You lose!", + "draw": "Draw!", + "trivia_title": "Trivia Question", + "trivia_correct": "Correct!", + "trivia_wrong": "Wrong!" + }, + "config": { + "visuals": { + "divider": "<:editprofilewhite:1476278814894981200> ??? ???????? ????????? ??? <:editprofilewhite:1476278814894981200>" + } + }, + "ping": { + "title": "?? Pong", + "desc": "Latency: **{latency}ms**" + }, + "roll": "{user} rolled **{value}** (1-{limit}).", + "lang": { + "current": "Current server language: **English**.", + "updated": "Bot language changed successfully." } -} \ No newline at end of file +} diff --git a/bot/locales/it.json b/bot/locales/it.json index b505d4a04a69b5d043fc870e9c081e1f13b5854c..cc954bd4a483c8f63ee38b93772cd6b72df2af82 100644 --- a/bot/locales/it.json +++ b/bot/locales/it.json @@ -89,6 +89,12 @@ }, "ai": { "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』" + }, + "ai_architect": { + "title": "?? AI Architect Setup Complete", + "proposal": "Proposed setup sections: {sections}", + "analyzing": "Analyzing your server requirements...", + "success": "AI Architect finished successfully." } }, "errors": { @@ -267,8 +273,163 @@ "set_banner": "Imposta un banner personalizzato per questo server", "remove_banner": "Rimuovi il banner personalizzato da questo server", "view_banner": "Visualizza il banner personalizzato attuale di questo server", - "banner_help": "Scopri come impostare un banner personalizzato" - } + "banner_help": "Scopri come impostare un banner personalizzato", + "8ball": "8ball", + "add_scam_image": "Save scam image signature", + "admin_panel": "Open admin control panel", + "ai": "Ai", + "ai_auto": "Ai auto", + "ai_channel": "Ai channel", + "ai_model": "Ai model", + "ask_image": "Ask image", + "avatar": "Avatar", + "awesomeroles": "Awesomeroles", + "backup_panel": "Open backup management panel", + "backupserver": "Backupserver", + "balance": "Balance", + "ban": "Ban", + "banner": "Banner", + "board_forfeit": "Board forfeit", + "board_move": "Board move", + "board_start": "Board start", + "boardgames": "Boardgames", + "botstats": "Botstats", + "buy": "Buy", + "chat": "Chat", + "checkers": "Checkers", + "chess": "Chess", + "choose": "Choose", + "clearwarn": "Clearwarn", + "cloneemoji": "Cloneemoji", + "code_gen": "Code gen", + "coinflip": "Coinflip", + "connect4": "Connect4", + "daily": "Daily", + "debug": "Debug", + "dice": "Dice", + "econ_admin": "Manage user economy balances", + "economy": "Economy", + "economy_deposit": "Economy deposit", + "economy_gamble": "Economy gamble", + "economy_rob": "Economy rob", + "economy_withdraw": "Economy withdraw", + "free_games": "Free games", + "gamble": "Gamble", + "gamehub": "Gamehub", + "games_panel": "Games panel", + "gaming_news": "Gaming news", + "giveaway": "Giveaway command group", + "guess": "Guess", + "image_gen": "Image gen", + "imagine": "Imagine", + "inventory": "Inventory", + "kick": "Kick", + "language": "Language", + "languages": "Languages", + "leaderboard": "Leaderboard", + "level": "Level", + "load": "Load", + "lock": "Lock", + "make_event": "Make event", + "mario": "Mario", + "meme": "Meme", + "menu": "Menu", + "music_247": "Music 247", + "music_clear": "Music clear", + "music_filter": "Music filter", + "music_jump": "Music jump", + "music_loop": "Music loop", + "music_lyrics": "Music lyrics", + "music_move": "Music move", + "music_nowplaying": "Music nowplaying", + "music_panel": "Music panel", + "music_pause": "Music pause", + "music_play": "Music play", + "music_playlist_delete": "Music playlist delete", + "music_playlist_rename": "Music playlist rename", + "music_playlist_save": "Music playlist save", + "music_previous": "Music previous", + "music_queue": "Music queue", + "music_remove": "Music remove", + "music_resume": "Music resume", + "music_seek": "Music seek", + "music_shuffle": "Music shuffle", + "music_skip": "Music skip", + "music_stop": "Music stop", + "music_volume": "Music volume", + "mute": "Mute", + "organizechannels": "Organizechannels", + "othello": "Othello", + "ping": "Ping", + "play": "Play", + "poll": "Poll", + "profile": "Profile", + "purge": "Purge", + "rank": "Rank", + "reload": "Reload", + "remind": "Remind", + "rob": "Rob", + "roll": "Roll", + "rps": "Rps", + "serverinfo": "Serverinfo", + "set": "Set", + "set_automod": "Set automod", + "set_dailybutton": "Set dailybutton", + "set_dailychannel": "Set dailychannel", + "set_dailyimage": "Set dailyimage", + "set_dailymessage": "Set dailymessage", + "set_dailytime": "Set dailytime", + "set_dailytitle": "Set dailytitle", + "set_dailytoggle": "Set dailytoggle", + "set_freegames": "Set freegames", + "set_gamenews": "Set gamenews", + "set_log": "Set log", + "set_pollchannel": "Set pollchannel", + "set_suggestions": "Set suggestions", + "set_supportai": "Set supportai", + "set_welcome": "Set welcome", + "set_wisdom": "Set wisdom", + "setupserver": "Setupserver", + "shield_level": "Set shield sensitivity level", + "shield_state": "Show current shield state", + "shop": "Shop", + "shutdown": "Shutdown", + "slots": "Slots", + "slowmode": "Slowmode", + "speak": "Speak", + "summarize": "Summarize", + "sync": "Sync", + "system_audit": "Show system audit and diagnostics", + "ticket": "Ticket command group", + "tournament": "Tournament", + "tournament_create": "Tournament create", + "tournament_end": "Tournament end", + "tournament_gamehub": "Tournament gamehub", + "tournament_join": "Tournament join", + "tournament_lb": "Tournament lb", + "tournament_panel": "Tournament panel", + "tournament_start": "Tournament start", + "transfer": "Transfer", + "translate": "Translate", + "translate_voice": "Translate voice", + "trivia": "Trivia", + "tts": "Tts", + "unban": "Unban", + "unload": "Unload", + "unlock": "Unlock", + "unmute": "Unmute", + "upscale": "Upscale", + "userinfo": "Userinfo", + "verify": "Verify", + "verifysetup": "Verifysetup", + "warn": "Warn", + "warnings": "Warnings", + "wisdom_today": "Wisdom today", + "work": "Work", + "xo": "Xo", + "xp": "Xp" + }, + "quick_ai": "AI admin request" }, "gambling": { "blackjack": { @@ -345,5 +506,70 @@ "play_roulette": "<:catjam:1476278750378201260> Gioca alla Roulette", "play_rpg": "<:therock:1476278719994794054> Avventura RPG" } + }, + "admin": { + "panel_title": "Admin Control Panel", + "economy_admin": "Economy Administration", + "shield_control": "Shield Control", + "system_status": "System Status", + "backup_created": "Backup created successfully.", + "backup_restored": "Backup restored successfully.", + "backup_deleted": "Backup deleted.", + "no_backups": "No backups found." + }, + "verification": { + "panel_title": "Verification Panel", + "verify_button": "Verify Me", + "verified_role": "Verified", + "already_verified": "You are already verified.", + "verification_success": "Verification successful! Welcome to the server." + }, + "board_games": { + "panel_title": "Board Games Panel", + "chess": "Chess", + "checkers": "Checkers", + "connect4": "Connect 4", + "othello": "Othello", + "tictactoe": "Tic Tac Toe", + "rps": "Rock Paper Scissors", + "game_started": "Game started!", + "your_turn": "Your turn!", + "waiting": "Waiting for opponent..." + }, + "media": { + "now_playing": "Now Playing", + "queue_added": "Added to queue", + "no_music": "No music playing", + "volume_set": "Volume set to {volume}%", + "skipped": "Skipped track", + "stopped": "Stopped playback", + "paused": "Paused playback", + "resumed": "Resumed playback" + }, + "fun": { + "rps_title": "Rock Paper Scissors", + "rps_rock": "Rock", + "rps_paper": "Paper", + "rps_scissors": "Scissors", + "you_win": "You win!", + "you_lose": "You lose!", + "draw": "Draw!", + "trivia_title": "Trivia Question", + "trivia_correct": "Correct!", + "trivia_wrong": "Wrong!" + }, + "config": { + "visuals": { + "divider": "<:editprofilewhite:1476278814894981200> ??? ???????? ????????? ??? <:editprofilewhite:1476278814894981200>" + } + }, + "ping": { + "title": "?? Pong", + "desc": "Latency: **{latency}ms**" + }, + "roll": "{user} rolled **{value}** (1-{limit}).", + "lang": { + "current": "Current server language: **English**.", + "updated": "Bot language changed successfully." } -} \ No newline at end of file +} diff --git a/bot/locales/ja.json b/bot/locales/ja.json index baf6cb040ad5553189fb56eedd2ea7e507991af0..51a001ce9a1e6fe40d5b8dd9bae43f14a471f810 100644 --- a/bot/locales/ja.json +++ b/bot/locales/ja.json @@ -89,6 +89,12 @@ }, "ai": { "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』" + }, + "ai_architect": { + "title": "?? AI Architect Setup Complete", + "proposal": "Proposed setup sections: {sections}", + "analyzing": "Analyzing your server requirements...", + "success": "AI Architect finished successfully." } }, "errors": { @@ -267,8 +273,163 @@ "set_banner": "このサーバーのカスタムバナーを設定", "remove_banner": "このサーバーからカスタムバナーを削除", "view_banner": "このサーバーの現在のカスタムバナーを表示", - "banner_help": "カスタムバナーの設定方法を学ぶ" - } + "banner_help": "カスタムバナーの設定方法を学ぶ", + "8ball": "8ball", + "add_scam_image": "Save scam image signature", + "admin_panel": "Open admin control panel", + "ai": "Ai", + "ai_auto": "Ai auto", + "ai_channel": "Ai channel", + "ai_model": "Ai model", + "ask_image": "Ask image", + "avatar": "Avatar", + "awesomeroles": "Awesomeroles", + "backup_panel": "Open backup management panel", + "backupserver": "Backupserver", + "balance": "Balance", + "ban": "Ban", + "banner": "Banner", + "board_forfeit": "Board forfeit", + "board_move": "Board move", + "board_start": "Board start", + "boardgames": "Boardgames", + "botstats": "Botstats", + "buy": "Buy", + "chat": "Chat", + "checkers": "Checkers", + "chess": "Chess", + "choose": "Choose", + "clearwarn": "Clearwarn", + "cloneemoji": "Cloneemoji", + "code_gen": "Code gen", + "coinflip": "Coinflip", + "connect4": "Connect4", + "daily": "Daily", + "debug": "Debug", + "dice": "Dice", + "econ_admin": "Manage user economy balances", + "economy": "Economy", + "economy_deposit": "Economy deposit", + "economy_gamble": "Economy gamble", + "economy_rob": "Economy rob", + "economy_withdraw": "Economy withdraw", + "free_games": "Free games", + "gamble": "Gamble", + "gamehub": "Gamehub", + "games_panel": "Games panel", + "gaming_news": "Gaming news", + "giveaway": "Giveaway command group", + "guess": "Guess", + "image_gen": "Image gen", + "imagine": "Imagine", + "inventory": "Inventory", + "kick": "Kick", + "language": "Language", + "languages": "Languages", + "leaderboard": "Leaderboard", + "level": "Level", + "load": "Load", + "lock": "Lock", + "make_event": "Make event", + "mario": "Mario", + "meme": "Meme", + "menu": "Menu", + "music_247": "Music 247", + "music_clear": "Music clear", + "music_filter": "Music filter", + "music_jump": "Music jump", + "music_loop": "Music loop", + "music_lyrics": "Music lyrics", + "music_move": "Music move", + "music_nowplaying": "Music nowplaying", + "music_panel": "Music panel", + "music_pause": "Music pause", + "music_play": "Music play", + "music_playlist_delete": "Music playlist delete", + "music_playlist_rename": "Music playlist rename", + "music_playlist_save": "Music playlist save", + "music_previous": "Music previous", + "music_queue": "Music queue", + "music_remove": "Music remove", + "music_resume": "Music resume", + "music_seek": "Music seek", + "music_shuffle": "Music shuffle", + "music_skip": "Music skip", + "music_stop": "Music stop", + "music_volume": "Music volume", + "mute": "Mute", + "organizechannels": "Organizechannels", + "othello": "Othello", + "ping": "Ping", + "play": "Play", + "poll": "Poll", + "profile": "Profile", + "purge": "Purge", + "rank": "Rank", + "reload": "Reload", + "remind": "Remind", + "rob": "Rob", + "roll": "Roll", + "rps": "Rps", + "serverinfo": "Serverinfo", + "set": "Set", + "set_automod": "Set automod", + "set_dailybutton": "Set dailybutton", + "set_dailychannel": "Set dailychannel", + "set_dailyimage": "Set dailyimage", + "set_dailymessage": "Set dailymessage", + "set_dailytime": "Set dailytime", + "set_dailytitle": "Set dailytitle", + "set_dailytoggle": "Set dailytoggle", + "set_freegames": "Set freegames", + "set_gamenews": "Set gamenews", + "set_log": "Set log", + "set_pollchannel": "Set pollchannel", + "set_suggestions": "Set suggestions", + "set_supportai": "Set supportai", + "set_welcome": "Set welcome", + "set_wisdom": "Set wisdom", + "setupserver": "Setupserver", + "shield_level": "Set shield sensitivity level", + "shield_state": "Show current shield state", + "shop": "Shop", + "shutdown": "Shutdown", + "slots": "Slots", + "slowmode": "Slowmode", + "speak": "Speak", + "summarize": "Summarize", + "sync": "Sync", + "system_audit": "Show system audit and diagnostics", + "ticket": "Ticket command group", + "tournament": "Tournament", + "tournament_create": "Tournament create", + "tournament_end": "Tournament end", + "tournament_gamehub": "Tournament gamehub", + "tournament_join": "Tournament join", + "tournament_lb": "Tournament lb", + "tournament_panel": "Tournament panel", + "tournament_start": "Tournament start", + "transfer": "Transfer", + "translate": "Translate", + "translate_voice": "Translate voice", + "trivia": "Trivia", + "tts": "Tts", + "unban": "Unban", + "unload": "Unload", + "unlock": "Unlock", + "unmute": "Unmute", + "upscale": "Upscale", + "userinfo": "Userinfo", + "verify": "Verify", + "verifysetup": "Verifysetup", + "warn": "Warn", + "warnings": "Warnings", + "wisdom_today": "Wisdom today", + "work": "Work", + "xo": "Xo", + "xp": "Xp" + }, + "quick_ai": "AI admin request" }, "gambling": { "blackjack": { @@ -345,5 +506,70 @@ "play_roulette": "<:catjam:1476278750378201260> ルーレットをプレイ", "play_rpg": "<:therock:1476278719994794054> RPGアドベンチャー" } + }, + "admin": { + "panel_title": "Admin Control Panel", + "economy_admin": "Economy Administration", + "shield_control": "Shield Control", + "system_status": "System Status", + "backup_created": "Backup created successfully.", + "backup_restored": "Backup restored successfully.", + "backup_deleted": "Backup deleted.", + "no_backups": "No backups found." + }, + "verification": { + "panel_title": "Verification Panel", + "verify_button": "Verify Me", + "verified_role": "Verified", + "already_verified": "You are already verified.", + "verification_success": "Verification successful! Welcome to the server." + }, + "board_games": { + "panel_title": "Board Games Panel", + "chess": "Chess", + "checkers": "Checkers", + "connect4": "Connect 4", + "othello": "Othello", + "tictactoe": "Tic Tac Toe", + "rps": "Rock Paper Scissors", + "game_started": "Game started!", + "your_turn": "Your turn!", + "waiting": "Waiting for opponent..." + }, + "media": { + "now_playing": "Now Playing", + "queue_added": "Added to queue", + "no_music": "No music playing", + "volume_set": "Volume set to {volume}%", + "skipped": "Skipped track", + "stopped": "Stopped playback", + "paused": "Paused playback", + "resumed": "Resumed playback" + }, + "fun": { + "rps_title": "Rock Paper Scissors", + "rps_rock": "Rock", + "rps_paper": "Paper", + "rps_scissors": "Scissors", + "you_win": "You win!", + "you_lose": "You lose!", + "draw": "Draw!", + "trivia_title": "Trivia Question", + "trivia_correct": "Correct!", + "trivia_wrong": "Wrong!" + }, + "config": { + "visuals": { + "divider": "<:editprofilewhite:1476278814894981200> ??? ???????? ????????? ??? <:editprofilewhite:1476278814894981200>" + } + }, + "ping": { + "title": "?? Pong", + "desc": "Latency: **{latency}ms**" + }, + "roll": "{user} rolled **{value}** (1-{limit}).", + "lang": { + "current": "Current server language: **English**.", + "updated": "Bot language changed successfully." } -} \ No newline at end of file +} diff --git a/bot/locales/pt.json b/bot/locales/pt.json index 67f5c4a5780d8bf2b8d1122bef9639bbe8637ce9..290dd95554bf28ee081ff7ea0d38e8c0991e9706 100644 --- a/bot/locales/pt.json +++ b/bot/locales/pt.json @@ -89,6 +89,12 @@ }, "ai": { "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』" + }, + "ai_architect": { + "title": "?? AI Architect Setup Complete", + "proposal": "Proposed setup sections: {sections}", + "analyzing": "Analyzing your server requirements...", + "success": "AI Architect finished successfully." } }, "errors": { @@ -267,8 +273,163 @@ "set_banner": "Set a custom banner for this server", "remove_banner": "Remove the custom banner from this server", "view_banner": "View the current custom banner for this server", - "banner_help": "Learn how to set a custom banner" - } + "banner_help": "Learn how to set a custom banner", + "8ball": "8ball", + "add_scam_image": "Save scam image signature", + "admin_panel": "Open admin control panel", + "ai": "Ai", + "ai_auto": "Ai auto", + "ai_channel": "Ai channel", + "ai_model": "Ai model", + "ask_image": "Ask image", + "avatar": "Avatar", + "awesomeroles": "Awesomeroles", + "backup_panel": "Open backup management panel", + "backupserver": "Backupserver", + "balance": "Balance", + "ban": "Ban", + "banner": "Banner", + "board_forfeit": "Board forfeit", + "board_move": "Board move", + "board_start": "Board start", + "boardgames": "Boardgames", + "botstats": "Botstats", + "buy": "Buy", + "chat": "Chat", + "checkers": "Checkers", + "chess": "Chess", + "choose": "Choose", + "clearwarn": "Clearwarn", + "cloneemoji": "Cloneemoji", + "code_gen": "Code gen", + "coinflip": "Coinflip", + "connect4": "Connect4", + "daily": "Daily", + "debug": "Debug", + "dice": "Dice", + "econ_admin": "Manage user economy balances", + "economy": "Economy", + "economy_deposit": "Economy deposit", + "economy_gamble": "Economy gamble", + "economy_rob": "Economy rob", + "economy_withdraw": "Economy withdraw", + "free_games": "Free games", + "gamble": "Gamble", + "gamehub": "Gamehub", + "games_panel": "Games panel", + "gaming_news": "Gaming news", + "giveaway": "Giveaway command group", + "guess": "Guess", + "image_gen": "Image gen", + "imagine": "Imagine", + "inventory": "Inventory", + "kick": "Kick", + "language": "Language", + "languages": "Languages", + "leaderboard": "Leaderboard", + "level": "Level", + "load": "Load", + "lock": "Lock", + "make_event": "Make event", + "mario": "Mario", + "meme": "Meme", + "menu": "Menu", + "music_247": "Music 247", + "music_clear": "Music clear", + "music_filter": "Music filter", + "music_jump": "Music jump", + "music_loop": "Music loop", + "music_lyrics": "Music lyrics", + "music_move": "Music move", + "music_nowplaying": "Music nowplaying", + "music_panel": "Music panel", + "music_pause": "Music pause", + "music_play": "Music play", + "music_playlist_delete": "Music playlist delete", + "music_playlist_rename": "Music playlist rename", + "music_playlist_save": "Music playlist save", + "music_previous": "Music previous", + "music_queue": "Music queue", + "music_remove": "Music remove", + "music_resume": "Music resume", + "music_seek": "Music seek", + "music_shuffle": "Music shuffle", + "music_skip": "Music skip", + "music_stop": "Music stop", + "music_volume": "Music volume", + "mute": "Mute", + "organizechannels": "Organizechannels", + "othello": "Othello", + "ping": "Ping", + "play": "Play", + "poll": "Poll", + "profile": "Profile", + "purge": "Purge", + "rank": "Rank", + "reload": "Reload", + "remind": "Remind", + "rob": "Rob", + "roll": "Roll", + "rps": "Rps", + "serverinfo": "Serverinfo", + "set": "Set", + "set_automod": "Set automod", + "set_dailybutton": "Set dailybutton", + "set_dailychannel": "Set dailychannel", + "set_dailyimage": "Set dailyimage", + "set_dailymessage": "Set dailymessage", + "set_dailytime": "Set dailytime", + "set_dailytitle": "Set dailytitle", + "set_dailytoggle": "Set dailytoggle", + "set_freegames": "Set freegames", + "set_gamenews": "Set gamenews", + "set_log": "Set log", + "set_pollchannel": "Set pollchannel", + "set_suggestions": "Set suggestions", + "set_supportai": "Set supportai", + "set_welcome": "Set welcome", + "set_wisdom": "Set wisdom", + "setupserver": "Setupserver", + "shield_level": "Set shield sensitivity level", + "shield_state": "Show current shield state", + "shop": "Shop", + "shutdown": "Shutdown", + "slots": "Slots", + "slowmode": "Slowmode", + "speak": "Speak", + "summarize": "Summarize", + "sync": "Sync", + "system_audit": "Show system audit and diagnostics", + "ticket": "Ticket command group", + "tournament": "Tournament", + "tournament_create": "Tournament create", + "tournament_end": "Tournament end", + "tournament_gamehub": "Tournament gamehub", + "tournament_join": "Tournament join", + "tournament_lb": "Tournament lb", + "tournament_panel": "Tournament panel", + "tournament_start": "Tournament start", + "transfer": "Transfer", + "translate": "Translate", + "translate_voice": "Translate voice", + "trivia": "Trivia", + "tts": "Tts", + "unban": "Unban", + "unload": "Unload", + "unlock": "Unlock", + "unmute": "Unmute", + "upscale": "Upscale", + "userinfo": "Userinfo", + "verify": "Verify", + "verifysetup": "Verifysetup", + "warn": "Warn", + "warnings": "Warnings", + "wisdom_today": "Wisdom today", + "work": "Work", + "xo": "Xo", + "xp": "Xp" + }, + "quick_ai": "AI admin request" }, "gambling": { "blackjack": { @@ -345,5 +506,70 @@ "play_roulette": "<:catjam:1476278750378201260> Play Roulette", "play_rpg": "<:therock:1476278719994794054> RPG Adventure" } + }, + "admin": { + "panel_title": "Admin Control Panel", + "economy_admin": "Economy Administration", + "shield_control": "Shield Control", + "system_status": "System Status", + "backup_created": "Backup created successfully.", + "backup_restored": "Backup restored successfully.", + "backup_deleted": "Backup deleted.", + "no_backups": "No backups found." + }, + "verification": { + "panel_title": "Verification Panel", + "verify_button": "Verify Me", + "verified_role": "Verified", + "already_verified": "You are already verified.", + "verification_success": "Verification successful! Welcome to the server." + }, + "board_games": { + "panel_title": "Board Games Panel", + "chess": "Chess", + "checkers": "Checkers", + "connect4": "Connect 4", + "othello": "Othello", + "tictactoe": "Tic Tac Toe", + "rps": "Rock Paper Scissors", + "game_started": "Game started!", + "your_turn": "Your turn!", + "waiting": "Waiting for opponent..." + }, + "media": { + "now_playing": "Now Playing", + "queue_added": "Added to queue", + "no_music": "No music playing", + "volume_set": "Volume set to {volume}%", + "skipped": "Skipped track", + "stopped": "Stopped playback", + "paused": "Paused playback", + "resumed": "Resumed playback" + }, + "fun": { + "rps_title": "Rock Paper Scissors", + "rps_rock": "Rock", + "rps_paper": "Paper", + "rps_scissors": "Scissors", + "you_win": "You win!", + "you_lose": "You lose!", + "draw": "Draw!", + "trivia_title": "Trivia Question", + "trivia_correct": "Correct!", + "trivia_wrong": "Wrong!" + }, + "config": { + "visuals": { + "divider": "<:editprofilewhite:1476278814894981200> ??? ???????? ????????? ??? <:editprofilewhite:1476278814894981200>" + } + }, + "ping": { + "title": "?? Pong", + "desc": "Latency: **{latency}ms**" + }, + "roll": "{user} rolled **{value}** (1-{limit}).", + "lang": { + "current": "Current server language: **English**.", + "updated": "Bot language changed successfully." } -} \ No newline at end of file +} diff --git a/bot/locales/ru.json b/bot/locales/ru.json index 453bc3ad48485d05b3f1c18ea4b6d487fb391bb3..0a3c36f990716a6aede45cedbb5ac21118ea5cef 100644 --- a/bot/locales/ru.json +++ b/bot/locales/ru.json @@ -89,6 +89,12 @@ }, "ai": { "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』" + }, + "ai_architect": { + "title": "?? AI Architect Setup Complete", + "proposal": "Proposed setup sections: {sections}", + "analyzing": "Analyzing your server requirements...", + "success": "AI Architect finished successfully." } }, "errors": { @@ -267,8 +273,163 @@ "set_banner": "Установить пользовательский баннер для этого сервера", "remove_banner": "Удалить пользовательский баннер с этого сервера", "view_banner": "Просмотреть текущий пользовательский баннер этого сервера", - "banner_help": "Узнать, как установить пользовательский баннер" - } + "banner_help": "Узнать, как установить пользовательский баннер", + "8ball": "8ball", + "add_scam_image": "Save scam image signature", + "admin_panel": "Open admin control panel", + "ai": "Ai", + "ai_auto": "Ai auto", + "ai_channel": "Ai channel", + "ai_model": "Ai model", + "ask_image": "Ask image", + "avatar": "Avatar", + "awesomeroles": "Awesomeroles", + "backup_panel": "Open backup management panel", + "backupserver": "Backupserver", + "balance": "Balance", + "ban": "Ban", + "banner": "Banner", + "board_forfeit": "Board forfeit", + "board_move": "Board move", + "board_start": "Board start", + "boardgames": "Boardgames", + "botstats": "Botstats", + "buy": "Buy", + "chat": "Chat", + "checkers": "Checkers", + "chess": "Chess", + "choose": "Choose", + "clearwarn": "Clearwarn", + "cloneemoji": "Cloneemoji", + "code_gen": "Code gen", + "coinflip": "Coinflip", + "connect4": "Connect4", + "daily": "Daily", + "debug": "Debug", + "dice": "Dice", + "econ_admin": "Manage user economy balances", + "economy": "Economy", + "economy_deposit": "Economy deposit", + "economy_gamble": "Economy gamble", + "economy_rob": "Economy rob", + "economy_withdraw": "Economy withdraw", + "free_games": "Free games", + "gamble": "Gamble", + "gamehub": "Gamehub", + "games_panel": "Games panel", + "gaming_news": "Gaming news", + "giveaway": "Giveaway command group", + "guess": "Guess", + "image_gen": "Image gen", + "imagine": "Imagine", + "inventory": "Inventory", + "kick": "Kick", + "language": "Language", + "languages": "Languages", + "leaderboard": "Leaderboard", + "level": "Level", + "load": "Load", + "lock": "Lock", + "make_event": "Make event", + "mario": "Mario", + "meme": "Meme", + "menu": "Menu", + "music_247": "Music 247", + "music_clear": "Music clear", + "music_filter": "Music filter", + "music_jump": "Music jump", + "music_loop": "Music loop", + "music_lyrics": "Music lyrics", + "music_move": "Music move", + "music_nowplaying": "Music nowplaying", + "music_panel": "Music panel", + "music_pause": "Music pause", + "music_play": "Music play", + "music_playlist_delete": "Music playlist delete", + "music_playlist_rename": "Music playlist rename", + "music_playlist_save": "Music playlist save", + "music_previous": "Music previous", + "music_queue": "Music queue", + "music_remove": "Music remove", + "music_resume": "Music resume", + "music_seek": "Music seek", + "music_shuffle": "Music shuffle", + "music_skip": "Music skip", + "music_stop": "Music stop", + "music_volume": "Music volume", + "mute": "Mute", + "organizechannels": "Organizechannels", + "othello": "Othello", + "ping": "Ping", + "play": "Play", + "poll": "Poll", + "profile": "Profile", + "purge": "Purge", + "rank": "Rank", + "reload": "Reload", + "remind": "Remind", + "rob": "Rob", + "roll": "Roll", + "rps": "Rps", + "serverinfo": "Serverinfo", + "set": "Set", + "set_automod": "Set automod", + "set_dailybutton": "Set dailybutton", + "set_dailychannel": "Set dailychannel", + "set_dailyimage": "Set dailyimage", + "set_dailymessage": "Set dailymessage", + "set_dailytime": "Set dailytime", + "set_dailytitle": "Set dailytitle", + "set_dailytoggle": "Set dailytoggle", + "set_freegames": "Set freegames", + "set_gamenews": "Set gamenews", + "set_log": "Set log", + "set_pollchannel": "Set pollchannel", + "set_suggestions": "Set suggestions", + "set_supportai": "Set supportai", + "set_welcome": "Set welcome", + "set_wisdom": "Set wisdom", + "setupserver": "Setupserver", + "shield_level": "Set shield sensitivity level", + "shield_state": "Show current shield state", + "shop": "Shop", + "shutdown": "Shutdown", + "slots": "Slots", + "slowmode": "Slowmode", + "speak": "Speak", + "summarize": "Summarize", + "sync": "Sync", + "system_audit": "Show system audit and diagnostics", + "ticket": "Ticket command group", + "tournament": "Tournament", + "tournament_create": "Tournament create", + "tournament_end": "Tournament end", + "tournament_gamehub": "Tournament gamehub", + "tournament_join": "Tournament join", + "tournament_lb": "Tournament lb", + "tournament_panel": "Tournament panel", + "tournament_start": "Tournament start", + "transfer": "Transfer", + "translate": "Translate", + "translate_voice": "Translate voice", + "trivia": "Trivia", + "tts": "Tts", + "unban": "Unban", + "unload": "Unload", + "unlock": "Unlock", + "unmute": "Unmute", + "upscale": "Upscale", + "userinfo": "Userinfo", + "verify": "Verify", + "verifysetup": "Verifysetup", + "warn": "Warn", + "warnings": "Warnings", + "wisdom_today": "Wisdom today", + "work": "Work", + "xo": "Xo", + "xp": "Xp" + }, + "quick_ai": "AI admin request" }, "gambling": { "blackjack": { @@ -345,5 +506,70 @@ "play_roulette": "<:catjam:1476278750378201260> Играть в рулетку", "play_rpg": "<:therock:1476278719994794054> RPG-приключение" } + }, + "admin": { + "panel_title": "Admin Control Panel", + "economy_admin": "Economy Administration", + "shield_control": "Shield Control", + "system_status": "System Status", + "backup_created": "Backup created successfully.", + "backup_restored": "Backup restored successfully.", + "backup_deleted": "Backup deleted.", + "no_backups": "No backups found." + }, + "verification": { + "panel_title": "Verification Panel", + "verify_button": "Verify Me", + "verified_role": "Verified", + "already_verified": "You are already verified.", + "verification_success": "Verification successful! Welcome to the server." + }, + "board_games": { + "panel_title": "Board Games Panel", + "chess": "Chess", + "checkers": "Checkers", + "connect4": "Connect 4", + "othello": "Othello", + "tictactoe": "Tic Tac Toe", + "rps": "Rock Paper Scissors", + "game_started": "Game started!", + "your_turn": "Your turn!", + "waiting": "Waiting for opponent..." + }, + "media": { + "now_playing": "Now Playing", + "queue_added": "Added to queue", + "no_music": "No music playing", + "volume_set": "Volume set to {volume}%", + "skipped": "Skipped track", + "stopped": "Stopped playback", + "paused": "Paused playback", + "resumed": "Resumed playback" + }, + "fun": { + "rps_title": "Rock Paper Scissors", + "rps_rock": "Rock", + "rps_paper": "Paper", + "rps_scissors": "Scissors", + "you_win": "You win!", + "you_lose": "You lose!", + "draw": "Draw!", + "trivia_title": "Trivia Question", + "trivia_correct": "Correct!", + "trivia_wrong": "Wrong!" + }, + "config": { + "visuals": { + "divider": "<:editprofilewhite:1476278814894981200> ??? ???????? ????????? ??? <:editprofilewhite:1476278814894981200>" + } + }, + "ping": { + "title": "?? Pong", + "desc": "Latency: **{latency}ms**" + }, + "roll": "{user} rolled **{value}** (1-{limit}).", + "lang": { + "current": "Current server language: **English**.", + "updated": "Bot language changed successfully." } -} \ No newline at end of file +} diff --git a/bot/locales/tr.json b/bot/locales/tr.json index 2ffaab6b3a553fc9fcc924a9a5a93a9ef4d238dc..b51ad193608d7d271413bdf90da3e83aac838d8e 100644 --- a/bot/locales/tr.json +++ b/bot/locales/tr.json @@ -89,6 +89,12 @@ }, "ai": { "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』" + }, + "ai_architect": { + "title": "?? AI Architect Setup Complete", + "proposal": "Proposed setup sections: {sections}", + "analyzing": "Analyzing your server requirements...", + "success": "AI Architect finished successfully." } }, "errors": { @@ -267,8 +273,163 @@ "set_banner": "Bu sunucu için özel bir afiş ayarla", "remove_banner": "Bu sunucudan özel afişi kaldır", "view_banner": "Bu sunucunun mevcut özel afişini görüntüle", - "banner_help": "Özel afişin nasıl ayarlanacağını öğren" - } + "banner_help": "Özel afişin nasıl ayarlanacağını öğren", + "8ball": "8ball", + "add_scam_image": "Save scam image signature", + "admin_panel": "Open admin control panel", + "ai": "Ai", + "ai_auto": "Ai auto", + "ai_channel": "Ai channel", + "ai_model": "Ai model", + "ask_image": "Ask image", + "avatar": "Avatar", + "awesomeroles": "Awesomeroles", + "backup_panel": "Open backup management panel", + "backupserver": "Backupserver", + "balance": "Balance", + "ban": "Ban", + "banner": "Banner", + "board_forfeit": "Board forfeit", + "board_move": "Board move", + "board_start": "Board start", + "boardgames": "Boardgames", + "botstats": "Botstats", + "buy": "Buy", + "chat": "Chat", + "checkers": "Checkers", + "chess": "Chess", + "choose": "Choose", + "clearwarn": "Clearwarn", + "cloneemoji": "Cloneemoji", + "code_gen": "Code gen", + "coinflip": "Coinflip", + "connect4": "Connect4", + "daily": "Daily", + "debug": "Debug", + "dice": "Dice", + "econ_admin": "Manage user economy balances", + "economy": "Economy", + "economy_deposit": "Economy deposit", + "economy_gamble": "Economy gamble", + "economy_rob": "Economy rob", + "economy_withdraw": "Economy withdraw", + "free_games": "Free games", + "gamble": "Gamble", + "gamehub": "Gamehub", + "games_panel": "Games panel", + "gaming_news": "Gaming news", + "giveaway": "Giveaway command group", + "guess": "Guess", + "image_gen": "Image gen", + "imagine": "Imagine", + "inventory": "Inventory", + "kick": "Kick", + "language": "Language", + "languages": "Languages", + "leaderboard": "Leaderboard", + "level": "Level", + "load": "Load", + "lock": "Lock", + "make_event": "Make event", + "mario": "Mario", + "meme": "Meme", + "menu": "Menu", + "music_247": "Music 247", + "music_clear": "Music clear", + "music_filter": "Music filter", + "music_jump": "Music jump", + "music_loop": "Music loop", + "music_lyrics": "Music lyrics", + "music_move": "Music move", + "music_nowplaying": "Music nowplaying", + "music_panel": "Music panel", + "music_pause": "Music pause", + "music_play": "Music play", + "music_playlist_delete": "Music playlist delete", + "music_playlist_rename": "Music playlist rename", + "music_playlist_save": "Music playlist save", + "music_previous": "Music previous", + "music_queue": "Music queue", + "music_remove": "Music remove", + "music_resume": "Music resume", + "music_seek": "Music seek", + "music_shuffle": "Music shuffle", + "music_skip": "Music skip", + "music_stop": "Music stop", + "music_volume": "Music volume", + "mute": "Mute", + "organizechannels": "Organizechannels", + "othello": "Othello", + "ping": "Ping", + "play": "Play", + "poll": "Poll", + "profile": "Profile", + "purge": "Purge", + "rank": "Rank", + "reload": "Reload", + "remind": "Remind", + "rob": "Rob", + "roll": "Roll", + "rps": "Rps", + "serverinfo": "Serverinfo", + "set": "Set", + "set_automod": "Set automod", + "set_dailybutton": "Set dailybutton", + "set_dailychannel": "Set dailychannel", + "set_dailyimage": "Set dailyimage", + "set_dailymessage": "Set dailymessage", + "set_dailytime": "Set dailytime", + "set_dailytitle": "Set dailytitle", + "set_dailytoggle": "Set dailytoggle", + "set_freegames": "Set freegames", + "set_gamenews": "Set gamenews", + "set_log": "Set log", + "set_pollchannel": "Set pollchannel", + "set_suggestions": "Set suggestions", + "set_supportai": "Set supportai", + "set_welcome": "Set welcome", + "set_wisdom": "Set wisdom", + "setupserver": "Setupserver", + "shield_level": "Set shield sensitivity level", + "shield_state": "Show current shield state", + "shop": "Shop", + "shutdown": "Shutdown", + "slots": "Slots", + "slowmode": "Slowmode", + "speak": "Speak", + "summarize": "Summarize", + "sync": "Sync", + "system_audit": "Show system audit and diagnostics", + "ticket": "Ticket command group", + "tournament": "Tournament", + "tournament_create": "Tournament create", + "tournament_end": "Tournament end", + "tournament_gamehub": "Tournament gamehub", + "tournament_join": "Tournament join", + "tournament_lb": "Tournament lb", + "tournament_panel": "Tournament panel", + "tournament_start": "Tournament start", + "transfer": "Transfer", + "translate": "Translate", + "translate_voice": "Translate voice", + "trivia": "Trivia", + "tts": "Tts", + "unban": "Unban", + "unload": "Unload", + "unlock": "Unlock", + "unmute": "Unmute", + "upscale": "Upscale", + "userinfo": "Userinfo", + "verify": "Verify", + "verifysetup": "Verifysetup", + "warn": "Warn", + "warnings": "Warnings", + "wisdom_today": "Wisdom today", + "work": "Work", + "xo": "Xo", + "xp": "Xp" + }, + "quick_ai": "AI admin request" }, "gambling": { "blackjack": { @@ -345,5 +506,70 @@ "play_roulette": "<:catjam:1476278750378201260> Rulet Oyna", "play_rpg": "<:therock:1476278719994794054> RPG Macera" } + }, + "admin": { + "panel_title": "Admin Control Panel", + "economy_admin": "Economy Administration", + "shield_control": "Shield Control", + "system_status": "System Status", + "backup_created": "Backup created successfully.", + "backup_restored": "Backup restored successfully.", + "backup_deleted": "Backup deleted.", + "no_backups": "No backups found." + }, + "verification": { + "panel_title": "Verification Panel", + "verify_button": "Verify Me", + "verified_role": "Verified", + "already_verified": "You are already verified.", + "verification_success": "Verification successful! Welcome to the server." + }, + "board_games": { + "panel_title": "Board Games Panel", + "chess": "Chess", + "checkers": "Checkers", + "connect4": "Connect 4", + "othello": "Othello", + "tictactoe": "Tic Tac Toe", + "rps": "Rock Paper Scissors", + "game_started": "Game started!", + "your_turn": "Your turn!", + "waiting": "Waiting for opponent..." + }, + "media": { + "now_playing": "Now Playing", + "queue_added": "Added to queue", + "no_music": "No music playing", + "volume_set": "Volume set to {volume}%", + "skipped": "Skipped track", + "stopped": "Stopped playback", + "paused": "Paused playback", + "resumed": "Resumed playback" + }, + "fun": { + "rps_title": "Rock Paper Scissors", + "rps_rock": "Rock", + "rps_paper": "Paper", + "rps_scissors": "Scissors", + "you_win": "You win!", + "you_lose": "You lose!", + "draw": "Draw!", + "trivia_title": "Trivia Question", + "trivia_correct": "Correct!", + "trivia_wrong": "Wrong!" + }, + "config": { + "visuals": { + "divider": "<:editprofilewhite:1476278814894981200> ??? ???????? ????????? ??? <:editprofilewhite:1476278814894981200>" + } + }, + "ping": { + "title": "?? Pong", + "desc": "Latency: **{latency}ms**" + }, + "roll": "{user} rolled **{value}** (1-{limit}).", + "lang": { + "current": "Current server language: **English**.", + "updated": "Bot language changed successfully." } -} \ No newline at end of file +} diff --git a/bot/locales/zh.json b/bot/locales/zh.json index 68a6304589d51fc7704d1d8b8e3c3e2e697384e5..4d87a66040c8681de3ba70eb98cd817659f2e9e2 100644 --- a/bot/locales/zh.json +++ b/bot/locales/zh.json @@ -89,6 +89,12 @@ }, "ai": { "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』" + }, + "ai_architect": { + "title": "?? AI Architect Setup Complete", + "proposal": "Proposed setup sections: {sections}", + "analyzing": "Analyzing your server requirements...", + "success": "AI Architect finished successfully." } }, "errors": { @@ -267,8 +273,163 @@ "set_banner": "为此服务器设置自定义横幅", "remove_banner": "从此服务器移除自定义横幅", "view_banner": "查看此服务器当前的自定义横幅", - "banner_help": "了解如何设置自定义横幅" - } + "banner_help": "了解如何设置自定义横幅", + "8ball": "8ball", + "add_scam_image": "Save scam image signature", + "admin_panel": "Open admin control panel", + "ai": "Ai", + "ai_auto": "Ai auto", + "ai_channel": "Ai channel", + "ai_model": "Ai model", + "ask_image": "Ask image", + "avatar": "Avatar", + "awesomeroles": "Awesomeroles", + "backup_panel": "Open backup management panel", + "backupserver": "Backupserver", + "balance": "Balance", + "ban": "Ban", + "banner": "Banner", + "board_forfeit": "Board forfeit", + "board_move": "Board move", + "board_start": "Board start", + "boardgames": "Boardgames", + "botstats": "Botstats", + "buy": "Buy", + "chat": "Chat", + "checkers": "Checkers", + "chess": "Chess", + "choose": "Choose", + "clearwarn": "Clearwarn", + "cloneemoji": "Cloneemoji", + "code_gen": "Code gen", + "coinflip": "Coinflip", + "connect4": "Connect4", + "daily": "Daily", + "debug": "Debug", + "dice": "Dice", + "econ_admin": "Manage user economy balances", + "economy": "Economy", + "economy_deposit": "Economy deposit", + "economy_gamble": "Economy gamble", + "economy_rob": "Economy rob", + "economy_withdraw": "Economy withdraw", + "free_games": "Free games", + "gamble": "Gamble", + "gamehub": "Gamehub", + "games_panel": "Games panel", + "gaming_news": "Gaming news", + "giveaway": "Giveaway command group", + "guess": "Guess", + "image_gen": "Image gen", + "imagine": "Imagine", + "inventory": "Inventory", + "kick": "Kick", + "language": "Language", + "languages": "Languages", + "leaderboard": "Leaderboard", + "level": "Level", + "load": "Load", + "lock": "Lock", + "make_event": "Make event", + "mario": "Mario", + "meme": "Meme", + "menu": "Menu", + "music_247": "Music 247", + "music_clear": "Music clear", + "music_filter": "Music filter", + "music_jump": "Music jump", + "music_loop": "Music loop", + "music_lyrics": "Music lyrics", + "music_move": "Music move", + "music_nowplaying": "Music nowplaying", + "music_panel": "Music panel", + "music_pause": "Music pause", + "music_play": "Music play", + "music_playlist_delete": "Music playlist delete", + "music_playlist_rename": "Music playlist rename", + "music_playlist_save": "Music playlist save", + "music_previous": "Music previous", + "music_queue": "Music queue", + "music_remove": "Music remove", + "music_resume": "Music resume", + "music_seek": "Music seek", + "music_shuffle": "Music shuffle", + "music_skip": "Music skip", + "music_stop": "Music stop", + "music_volume": "Music volume", + "mute": "Mute", + "organizechannels": "Organizechannels", + "othello": "Othello", + "ping": "Ping", + "play": "Play", + "poll": "Poll", + "profile": "Profile", + "purge": "Purge", + "rank": "Rank", + "reload": "Reload", + "remind": "Remind", + "rob": "Rob", + "roll": "Roll", + "rps": "Rps", + "serverinfo": "Serverinfo", + "set": "Set", + "set_automod": "Set automod", + "set_dailybutton": "Set dailybutton", + "set_dailychannel": "Set dailychannel", + "set_dailyimage": "Set dailyimage", + "set_dailymessage": "Set dailymessage", + "set_dailytime": "Set dailytime", + "set_dailytitle": "Set dailytitle", + "set_dailytoggle": "Set dailytoggle", + "set_freegames": "Set freegames", + "set_gamenews": "Set gamenews", + "set_log": "Set log", + "set_pollchannel": "Set pollchannel", + "set_suggestions": "Set suggestions", + "set_supportai": "Set supportai", + "set_welcome": "Set welcome", + "set_wisdom": "Set wisdom", + "setupserver": "Setupserver", + "shield_level": "Set shield sensitivity level", + "shield_state": "Show current shield state", + "shop": "Shop", + "shutdown": "Shutdown", + "slots": "Slots", + "slowmode": "Slowmode", + "speak": "Speak", + "summarize": "Summarize", + "sync": "Sync", + "system_audit": "Show system audit and diagnostics", + "ticket": "Ticket command group", + "tournament": "Tournament", + "tournament_create": "Tournament create", + "tournament_end": "Tournament end", + "tournament_gamehub": "Tournament gamehub", + "tournament_join": "Tournament join", + "tournament_lb": "Tournament lb", + "tournament_panel": "Tournament panel", + "tournament_start": "Tournament start", + "transfer": "Transfer", + "translate": "Translate", + "translate_voice": "Translate voice", + "trivia": "Trivia", + "tts": "Tts", + "unban": "Unban", + "unload": "Unload", + "unlock": "Unlock", + "unmute": "Unmute", + "upscale": "Upscale", + "userinfo": "Userinfo", + "verify": "Verify", + "verifysetup": "Verifysetup", + "warn": "Warn", + "warnings": "Warnings", + "wisdom_today": "Wisdom today", + "work": "Work", + "xo": "Xo", + "xp": "Xp" + }, + "quick_ai": "AI admin request" }, "gambling": { "blackjack": { @@ -345,5 +506,70 @@ "play_roulette": "<:catjam:1476278750378201260> 玩轮盘", "play_rpg": "<:therock:1476278719994794054> RPG冒险" } + }, + "admin": { + "panel_title": "Admin Control Panel", + "economy_admin": "Economy Administration", + "shield_control": "Shield Control", + "system_status": "System Status", + "backup_created": "Backup created successfully.", + "backup_restored": "Backup restored successfully.", + "backup_deleted": "Backup deleted.", + "no_backups": "No backups found." + }, + "verification": { + "panel_title": "Verification Panel", + "verify_button": "Verify Me", + "verified_role": "Verified", + "already_verified": "You are already verified.", + "verification_success": "Verification successful! Welcome to the server." + }, + "board_games": { + "panel_title": "Board Games Panel", + "chess": "Chess", + "checkers": "Checkers", + "connect4": "Connect 4", + "othello": "Othello", + "tictactoe": "Tic Tac Toe", + "rps": "Rock Paper Scissors", + "game_started": "Game started!", + "your_turn": "Your turn!", + "waiting": "Waiting for opponent..." + }, + "media": { + "now_playing": "Now Playing", + "queue_added": "Added to queue", + "no_music": "No music playing", + "volume_set": "Volume set to {volume}%", + "skipped": "Skipped track", + "stopped": "Stopped playback", + "paused": "Paused playback", + "resumed": "Resumed playback" + }, + "fun": { + "rps_title": "Rock Paper Scissors", + "rps_rock": "Rock", + "rps_paper": "Paper", + "rps_scissors": "Scissors", + "you_win": "You win!", + "you_lose": "You lose!", + "draw": "Draw!", + "trivia_title": "Trivia Question", + "trivia_correct": "Correct!", + "trivia_wrong": "Wrong!" + }, + "config": { + "visuals": { + "divider": "<:editprofilewhite:1476278814894981200> ??? ???????? ????????? ??? <:editprofilewhite:1476278814894981200>" + } + }, + "ping": { + "title": "?? Pong", + "desc": "Latency: **{latency}ms**" + }, + "roll": "{user} rolled **{value}** (1-{limit}).", + "lang": { + "current": "Current server language: **English**.", + "updated": "Bot language changed successfully." } -} \ No newline at end of file +} diff --git a/bot/main.py b/bot/main.py index fa544eae7a0966cc8ceca9e5bcc62ee59415b2ec..a079fbd2b0623ea99d5b16c23514afbf78440356 100644 --- a/bot/main.py +++ b/bot/main.py @@ -13,16 +13,10 @@ import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path -# Fix Windows console encoding -if sys.platform == "win32": - sys.stdout.reconfigure(encoding="utf-8") - sys.stderr.reconfigure(encoding="utf-8") - - -# Fix Windows console encoding -if sys.platform == "win32": - sys.stdout.reconfigure(encoding="utf-8") - sys.stderr.reconfigure(encoding="utf-8") +# Fix Windows console encoding +if sys.platform == "win32": + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") import aiohttp @@ -157,7 +151,7 @@ class ImperialContext(commands.Context): class MegaDiscordBot(commands.Bot): - def __init__(self) -> None: + def __init__(self) -> None: settings = load_settings() intents = discord.Intents.default() intents.members = True @@ -183,6 +177,7 @@ class MegaDiscordBot(commands.Bot): self.before_invoke(self._auto_defer_for_interaction) self.context_class = ImperialContext + self._presence_loaded = False async def _auto_defer_for_interaction(self, ctx: commands.Context) -> None: """Acknowledge hybrid slash interactions early to avoid timeout banners.""" @@ -219,21 +214,54 @@ class MegaDiscordBot(commands.Bot): await self.load_extension(extension_name) loaded_extensions.add(extension_name) - # Register persistent views so buttons/select menus survive restarts. - try: - from bot.cogs.verification import VerifyView - from bot.cogs.menu import MainMenuView - from bot.cogs.engagement import EconomyPanelView - - self.add_view(VerifyView(self)) - menu_view = MainMenuView(self, None) - await menu_view.setup_items() - self.add_view(menu_view) - engagement_cog = self.get_cog("Engagement") - if engagement_cog is not None: - self.add_view(EconomyPanelView(engagement_cog, 0)) - except Exception as exc: - self.logger.warning("Persistent view registration skipped: %s", exc) + # Register persistent views so buttons/select menus survive restarts. + try: + from bot.cogs.verification import VerifyView + self.add_view(VerifyView(self)) + except Exception as exc: + self.logger.warning("VerifyView registration skipped: %s", exc) + + try: + from bot.cogs.menu import MainMenuView + menu_view = MainMenuView(self, None) + await menu_view.setup_items() + self.add_view(menu_view) + except Exception as exc: + self.logger.warning("MainMenuView registration skipped: %s", exc) + + try: + from bot.cogs.engagement import EconomyPanelView + engagement_cog = self.get_cog("Engagement") + if engagement_cog is not None: + self.add_view(EconomyPanelView(engagement_cog, 0)) + except Exception as exc: + self.logger.warning("EconomyPanelView registration skipped: %s", exc) + + try: + from bot.cogs.media_helpers import MusicPanelView, AudioActionsView + media_cog = self.get_cog("Media") + if media_cog is not None: + self.add_view(MusicPanelView(media_cog, 0)) + self.add_view(AudioActionsView(media_cog, 0)) + except Exception as exc: + self.logger.warning("Media persistent views registration skipped: %s", exc) + + try: + from bot.cogs.gambling import GamblingPanelView + gambling_cog = self.get_cog("Gambling") + if gambling_cog is not None: + self.add_view(GamblingPanelView(gambling_cog, 0, 0)) + except Exception as exc: + self.logger.warning("Gambling persistent views registration skipped: %s", exc) + + try: + from bot.cogs.community import TicketCloseView, GiveawayJoinView + community_cog = self.get_cog("Community") + if community_cog is not None: + self.add_view(TicketCloseView()) + self.add_view(GiveawayJoinView(community_cog, 0)) + except Exception as exc: + self.logger.warning("Community persistent views registration skipped: %s", exc) await self.tree.sync() self.logger.info("Loaded all cogs and synced slash commands") @@ -303,9 +331,52 @@ class MegaDiscordBot(commands.Bot): """Resolve runtime custom emoji for a UI category.""" return resolve_dynamic_custom_emoji(category_key, fallback=fallback) - async def on_ready(self) -> None: - """Log when bot is ready.""" - self.logger.info("Logged in as %s (%s)", self.user, self.user.id) + async def on_ready(self) -> None: + """Log when bot is ready.""" + if not self._presence_loaded: + await self.apply_saved_presence() + self._presence_loaded = True + self.logger.info("Logged in as %s (%s)", self.user, self.user.id) + + async def apply_saved_presence(self) -> None: + """Apply persisted bot presence from database if configured.""" + try: + row = await self.db.fetchone( + "SELECT status, activity_type, activity_text FROM bot_presence_config WHERE id = 1" + ) + if not row: + return + + status_name = str(row[0] or "online").strip().lower() + activity_type_name = str(row[1] or "playing").strip().lower() + activity_text = str(row[2] or "CYBER // GRID").strip() or "CYBER // GRID" + + status_map = { + "online": discord.Status.online, + "idle": discord.Status.idle, + "dnd": discord.Status.dnd, + "invisible": discord.Status.invisible, + "offline": discord.Status.invisible, + } + activity_type_map = { + "playing": discord.ActivityType.playing, + "watching": discord.ActivityType.watching, + "listening": discord.ActivityType.listening, + "competing": discord.ActivityType.competing, + } + + resolved_status = status_map.get(status_name, discord.Status.online) + resolved_activity_type = activity_type_map.get(activity_type_name, discord.ActivityType.playing) + activity = discord.Activity(type=resolved_activity_type, name=activity_text) + await self.change_presence(status=resolved_status, activity=activity) + self.logger.info( + "Applied saved presence: status=%s activity_type=%s text=%s", + status_name, + activity_type_name, + activity_text, + ) + except Exception as exc: + self.logger.warning("Failed to apply saved presence: %s", exc) class _HealthHandler(BaseHTTPRequestHandler): diff --git a/bot/theme.py b/bot/theme.py index 9e7748f23d1cc5553377a3251dd2d58656e12035..e72c959d5b78bd36771b5e56b518eea377e7eaba 100644 --- a/bot/theme.py +++ b/bot/theme.py @@ -6,6 +6,7 @@ Version 2.0 - Improved dividers, colors, headers, and emojis import random from typing import Literal +from urllib.parse import urlparse import discord @@ -52,15 +53,15 @@ NEON_DARK_PALETTE = [NEON_INDIGO, NEON_VIOLET, NEON_CRIMSON, NEON_MAGENTA] GLOW_EMOJIS = ["✨", "⚡", "🌀", "🌐", "💫", "✅", "✅", "✦", "💠", "🔮"] SPARKLE_EMOJIS = ["✨", "❇️", "✴️", "❄️", "💠", "✅", "✅"] FIRE_EMOJIS = ["🔥", "💥", "⚡", "☄️", "✅", "🧡", "💛"] -HEART_EMOJIS = ["✅", "🟢", "💙", "💚", "💛", "🧡", "💗", "💖"] +HEART_EMOJIS = ["✅", "<:animatedarrowgreen:1477261279428087979>", "💙", "<:animatedarrowgreen:1477261279428087979>", "💛", "🧡", "<:animatedarrowpink:1477261266690113651>", "💖"] # Arrow emojis for dividers (Unicode - work everywhere) ARROW_BLUE = "🔷" -ARROW_GREEN = "🟢" -ARROW_PINK = "💗" -ARROW_PURPLE = "🟢" +ARROW_GREEN = "<:animatedarrowgreen:1477261279428087979>" +ARROW_PINK = "<:animatedarrowpink:1477261266690113651>" +ARROW_PURPLE = "<:animatedarrowgreen:1477261279428087979>" ARROW_ORANGE = "🟠" -ARROW_YELLOW = "🟡" +ARROW_YELLOW = "<:animatedarrowyellow:1477261257592668271>" ARROW_CYAN = "💠" ARROW_RED = "🔴" ARROW_WHITE = "⚪" @@ -292,17 +293,17 @@ def panel_divider(color: Literal["blue", "green", "pink", "purple", "orange", "y "orange": ARROW_ORANGE, "yellow": ARROW_YELLOW, "cyan": ARROW_CYAN, - "lime": "💚", + "lime": "<:animatedarrowgreen:1477261279428087979>", "red": ARROW_RED, "white": ARROW_WHITE, - "gold": "🟡", + "gold": "<:animatedarrowyellow:1477261257592668271>", "rainbow": None, # Special handling } length = max(4, min(12, length)) # Clamp between 4-12 if color == "rainbow": - colors = ["🔴", "🟠", "🟡", "🟢", "🔵", "🟣"] + colors = ["🔴", "🟠", "<:animatedarrowyellow:1477261257592668271>", "<:animatedarrowgreen:1477261279428087979>", "🔵", "🟣"] line1 = " ".join(colors[:length//2]) line2 = " ".join(colors[length//2:length]) return f"{line1}\n{line2}" @@ -383,9 +384,9 @@ def section_header(emoji: str, text: str, color: str = "default") -> str: colors = { "default": "", "cyan": "🩵", - "pink": "💗", - "purple": "🟢", - "green": "💚", + "pink": "<:animatedarrowpink:1477261266690113651>", + "purple": "<:animatedarrowgreen:1477261279428087979>", + "green": "<:animatedarrowgreen:1477261279428087979>", "yellow": "💛", "orange": "🧡", "red": "✅", @@ -418,10 +419,10 @@ def info_box(title: str, content: str, style: str = "info") -> str: """Create an info box with style.""" styles = { "info": ("ℹ️", "🔷"), - "success": ("✅", "🟢"), + "success": ("✅", "<:animatedarrowgreen:1477261279428087979>"), "warning": ("⚠️", "🟠"), "error": ("❌", "🔴"), - "tip": ("💡", "🟡"), + "tip": ("💡", "<:animatedarrowyellow:1477261257592668271>"), } emoji, border = styles.get(style, styles["info"]) return f"{border}{border}{border}{border}{border}\n{emoji} **{title}**\n{content}\n{border}{border}{border}{border}{border}" @@ -484,8 +485,8 @@ def timestamp_display(seconds: int) -> str: def status_display(status: str) -> str: """Display status with colored indicator.""" statuses = { - "online": "🟢 Online", - "idle": "🟡 Idle", + "online": "<:animatedarrowgreen:1477261279428087979> Online", + "idle": "<:animatedarrowyellow:1477261257592668271> Idle", "dnd": "🔴 Do Not Disturb", "offline": "⚫ Offline", "playing": "🎮 Playing", @@ -633,6 +634,46 @@ def info_embed(title: str, description: str = "") -> discord.Embed: return embed +def idle_text(summary: str, details: str = "") -> str: + """Format a compact idle-state text block for panels/commands.""" + body = f"{status_display('idle')} {summary}" + if details: + return f"{body}\n{panel_divider('orange')}\n{details}" + return body + + +def idle_embed(title: str = "Idle State", details: str = "", hint: str = "") -> discord.Embed: + """Create a standardized idle-state embed used across commands/panels.""" + content = details or "No active items yet." + embed = discord.Embed( + title=f"{status_display('idle')} 『 {title} 』", + description=( + f"{panel_divider('orange')}\n" + f"• {content}\n" + f"{panel_divider('orange')}" + ), + color=NEON_ORANGE, + ) + if hint: + embed.set_footer(text=hint) + return embed + + +async def idle_embed_for_guild( + title: str = "Idle State", + details: str = "", + hint: str = "", + *, + guild: discord.Guild | None = None, + bot = None, +) -> discord.Embed: + """Create idle embed and attach guild banner/icon when available.""" + embed = idle_embed(title, details, hint) + if guild is not None: + await add_banner_to_embed(embed, guild, bot) + return embed + + def loading_embed(title: str = "Loading...", description: str = "") -> discord.Embed: """Create a loading embed with animation indication.""" embed = discord.Embed( @@ -769,7 +810,7 @@ def goodbye_embed(username: str, server_name: str) -> discord.Embed: """Create a goodbye embed.""" embed = discord.Embed( title=f"👋 Goodbye!", - description=f"{fancy_divider('waves')}\n**{username}** has left {server_name}.\nWe hope to see you again! 🟢", + description=f"{fancy_divider('waves')}\n**{username}** has left {server_name}.\nWe hope to see you again! <:animatedarrowgreen:1477261279428087979>", color=NEON_ORANGE, ) return embed @@ -1095,9 +1136,9 @@ def status_box(status: str, message: str, details: str = "") -> str: Status types: success, error, warning, info, loading """ styles = { - "success": ("✅", "🟢", NEON_LIME), + "success": ("✅", "<:animatedarrowgreen:1477261279428087979>", NEON_LIME), "error": ("❌", "🔴", NEON_RED), - "warning": ("⚠️", "🟡", NEON_YELLOW), + "warning": ("⚠️", "<:animatedarrowyellow:1477261257592668271>", NEON_YELLOW), "info": ("ℹ️", "🔵", NEON_BLUE), "loading": ("⏳", "🟣", NEON_PURPLE), } @@ -1126,13 +1167,13 @@ def mini_card(title: str, value: str, emoji: str = "📊") -> str: def progress_ring(percentage: int, size: int = 10) -> str: """Create a circular-style progress indicator using emojis.""" if percentage >= 100: - return "🟢" * size + return "<:animatedarrowgreen:1477261279428087979>" * size elif percentage >= 75: - return "🟢" * (size - 2) + "🟡" * 2 + return "<:animatedarrowgreen:1477261279428087979>" * (size - 2) + "<:animatedarrowyellow:1477261257592668271>" * 2 elif percentage >= 50: - return "🟢" * (size // 2) + "🟡" * (size // 4) + "🔴" * (size - size // 2 - size // 4) + return "<:animatedarrowgreen:1477261279428087979>" * (size // 2) + "<:animatedarrowyellow:1477261257592668271>" * (size // 4) + "🔴" * (size - size // 2 - size // 4) elif percentage >= 25: - return "🟡" * (size // 4) + "🔴" * (size - size // 4) + return "<:animatedarrowyellow:1477261257592668271>" * (size // 4) + "🔴" * (size - size // 4) else: return "🔴" * size @@ -1199,7 +1240,7 @@ def glow_text(text: str, intensity: int = 1) -> str: def rainbow_text(text: str) -> str: """Add rainbow effect indication to text.""" - colors = ["🔴", "🟠", "🟡", "🟢", "🔵", "🟣"] + colors = ["🔴", "🟠", "<:animatedarrowyellow:1477261257592668271>", "<:animatedarrowgreen:1477261279428087979>", "🔵", "🟣"] return f"{colors[0]}{colors[1]}{colors[2]} **{text}** {colors[3]}{colors[4]}{colors[5]}" @@ -1295,15 +1336,31 @@ async def add_banner_to_embed(embed: discord.Embed, guild: discord.Guild | None """ if guild is None: return embed + + # Try to infer bot/client if not passed explicitly. + if bot is None: + state = getattr(guild, "_state", None) + get_client = getattr(state, "_get_client", None) + if callable(get_client): + try: + bot = get_client() + except Exception: + bot = None # Try custom banner from database first - if bot: + if bot and getattr(bot, "db", None): try: row = await bot.db.fetchone( "SELECT custom_banner_url FROM guild_config WHERE guild_id = ?", guild.id, ) custom_banner = row[0] if row else None + if custom_banner: + parsed = urlparse(str(custom_banner)) + valid = parsed.scheme in {"http", "https"} and bool(parsed.netloc) + valid = valid and str(parsed.path).lower().endswith((".png", ".jpg", ".jpeg", ".webp", ".gif")) + if not valid: + custom_banner = None if custom_banner: embed.set_image(url=custom_banner) return embed diff --git a/bot/utils/__pycache__/__init__.cpython-311.pyc b/bot/utils/__pycache__/__init__.cpython-311.pyc index d104f9fc977fbcb951d0aba8266783d9ed751cf6..c683f695374999462fe018d70fb2e36bfd0bed9d 100644 Binary files a/bot/utils/__pycache__/__init__.cpython-311.pyc and b/bot/utils/__pycache__/__init__.cpython-311.pyc differ diff --git a/bot/utils/__pycache__/shared.cpython-311.pyc b/bot/utils/__pycache__/shared.cpython-311.pyc index aae3d5d43570c99d56038e3242389965f7314b5f..6aefe1b13c24933ce993c2e8256c97d55285bbae 100644 Binary files a/bot/utils/__pycache__/shared.cpython-311.pyc and b/bot/utils/__pycache__/shared.cpython-311.pyc differ diff --git a/bot/utils/__pycache__/translator.cpython-311.pyc b/bot/utils/__pycache__/translator.cpython-311.pyc index 2bc527a2112c0ba4240c0fa5dc99a95e613cd95c..fb9c7669af9c05f8a38d490331dfe4dd2b23c56b 100644 Binary files a/bot/utils/__pycache__/translator.cpython-311.pyc and b/bot/utils/__pycache__/translator.cpython-311.pyc differ diff --git a/bot/utils/shared.py b/bot/utils/shared.py index f9a94fb792536d956aaf8554e164908432bc813f..19e9f3b37373f033886fa339db46c9608f830b53 100644 --- a/bot/utils/shared.py +++ b/bot/utils/shared.py @@ -52,6 +52,7 @@ async def fetch_free_games() -> list[dict[str, str]]: title = str(item.get("title", "")).strip() link = str(item.get("open_giveaway_url", "")).strip() platform = str(item.get("platforms", "Unknown")).strip() + game_type = str(item.get("type", "Game")).strip() or "Game" image = str(item.get("thumbnail", "")).strip() end_date = str(item.get("end_date", "N/A")).strip() original_price = str(item.get("worth", "N/A")).strip() or "N/A" @@ -63,6 +64,7 @@ async def fetch_free_games() -> list[dict[str, str]]: "title": title, "link": link, "platform": platform, + "game_type": game_type, "image": image, "end_date": end_date, "original_price": original_price, diff --git a/bot/utils/translator.py b/bot/utils/translator.py index 288c61b3392bf46b504abffe1948d08b872b9a3d..0b1a390895e1a982eacbc3727bb494d2ad8b5295 100644 --- a/bot/utils/translator.py +++ b/bot/utils/translator.py @@ -4,7 +4,7 @@ import json from pathlib import Path from typing import Any -from bot.i18n import translate as legacy_translate +from bot.i18n import SUPPORTED_LANGUAGES, translate as legacy_translate class Translator: @@ -15,10 +15,16 @@ class Translator: self.locales_dir = Path(locales_dir) self._cache: dict[str, dict[str, str]] = {} self.default_language = "en" - self.supported_languages = {"en", "ar"} + self.supported_languages = set(SUPPORTED_LANGUAGES) if SUPPORTED_LANGUAGES else {"en", "ar"} self._load_locales() def _load_locales(self) -> None: + # Include locale files that exist on disk, even if legacy i18n list is behind. + file_langs = { + p.stem for p in self.locales_dir.glob("*.json") + if p.is_file() + } + self.supported_languages = set(self.supported_languages) | file_langs for lang in self.supported_languages: path = self.locales_dir / f"{lang}.json" try: @@ -26,6 +32,31 @@ class Translator: except Exception: data = {} self._cache[lang] = data if isinstance(data, dict) else {} + self._merge_english_defaults() + + @staticmethod + def _deep_merge_defaults(defaults: dict[str, Any], target: dict[str, Any]) -> dict[str, Any]: + merged: dict[str, Any] = dict(target) + for key, value in defaults.items(): + if key not in merged: + merged[key] = value + continue + if isinstance(value, dict) and isinstance(merged.get(key), dict): + merged[key] = Translator._deep_merge_defaults(value, merged[key]) # type: ignore[arg-type] + return merged + + def _merge_english_defaults(self) -> None: + """Guarantee all locale packs have the full key tree using English as baseline.""" + en_pack = self._cache.get(self.default_language, {}) + if not isinstance(en_pack, dict): + return + for lang, pack in list(self._cache.items()): + if not isinstance(pack, dict): + self._cache[lang] = dict(en_pack) + continue + if lang == self.default_language: + continue + self._cache[lang] = self._deep_merge_defaults(en_pack, pack) def _lookup_key(self, lang: str, key: str) -> str | None: """Lookup a translation key supporting nested dotted paths.""" diff --git a/database.db b/database.db index ba91f22d50aceb1054ac8efc6b307147cf108e32..814b99f12423a9ad3f173b7d859b3a0758ab54bc 100644 --- a/database.db +++ b/database.db @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49992ae77d77166efa0aabac54ff6ee415988ee8f8c89f6f6ba3793e6b8d8bf5 +oid sha256:b0d1ddc7e39405a8bd77def3abed80e13b4498b25258aa63aa5a68301ddfbbbc size 184320