mtaaz commited on
Commit
2b5eba7
·
verified ·
1 Parent(s): a87155c

Upload 91 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -0
  2. bot/__pycache__/__init__.cpython-311.pyc +0 -0
  3. bot/__pycache__/config.cpython-311.pyc +0 -0
  4. bot/__pycache__/database.cpython-311.pyc +0 -0
  5. bot/__pycache__/emojis.cpython-311.pyc +0 -0
  6. bot/__pycache__/i18n.cpython-311.pyc +0 -0
  7. bot/__pycache__/main.cpython-311.pyc +0 -0
  8. bot/__pycache__/server.cpython-311.pyc +0 -0
  9. bot/__pycache__/theme.cpython-311.pyc +0 -0
  10. bot/cogs/__pycache__/__init__.cpython-311.pyc +0 -0
  11. bot/cogs/__pycache__/admin.cpython-311.pyc +0 -0
  12. bot/cogs/__pycache__/ai_admin.cpython-311.pyc +0 -0
  13. bot/cogs/__pycache__/ai_suite.cpython-311.pyc +2 -2
  14. bot/cogs/__pycache__/banner_manager.cpython-311.pyc +0 -0
  15. bot/cogs/__pycache__/board_games.cpython-311.pyc +0 -0
  16. bot/cogs/__pycache__/community.cpython-311.pyc +2 -2
  17. bot/cogs/__pycache__/configuration.cpython-311.pyc +0 -0
  18. bot/cogs/__pycache__/developer.cpython-311.pyc +0 -0
  19. bot/cogs/__pycache__/engagement.cpython-311.pyc +2 -2
  20. bot/cogs/__pycache__/events.cpython-311.pyc +0 -0
  21. bot/cogs/__pycache__/fun.cpython-311.pyc +0 -0
  22. bot/cogs/__pycache__/gambling.cpython-311.pyc +0 -0
  23. bot/cogs/__pycache__/language.cpython-311.pyc +0 -0
  24. bot/cogs/__pycache__/media.cpython-311.pyc +2 -2
  25. bot/cogs/__pycache__/media_helpers.cpython-311.pyc +0 -0
  26. bot/cogs/__pycache__/menu.cpython-311.pyc +0 -0
  27. bot/cogs/__pycache__/observability.cpython-311.pyc +0 -0
  28. bot/cogs/__pycache__/server_manager.cpython-311.pyc +0 -0
  29. bot/cogs/__pycache__/utility.cpython-311.pyc +0 -0
  30. bot/cogs/__pycache__/verification.cpython-311.pyc +0 -0
  31. bot/cogs/admin.py +0 -0
  32. bot/cogs/ai_admin.py +847 -395
  33. bot/cogs/ai_suite.py +5 -6
  34. bot/cogs/banner_manager.py +38 -4
  35. bot/cogs/board_games.py +1 -1
  36. bot/cogs/community.py +131 -5
  37. bot/cogs/configuration.py +5 -5
  38. bot/cogs/engagement.py +149 -27
  39. bot/cogs/events.py +494 -18
  40. bot/cogs/fun.py +9 -4
  41. bot/cogs/gambling.py +688 -770
  42. bot/cogs/language.py +22 -3
  43. bot/cogs/media.py +261 -85
  44. bot/cogs/media_helpers.py +12 -3
  45. bot/cogs/menu.py +805 -754
  46. bot/cogs/observability.py +74 -10
  47. bot/database.py +25 -10
  48. bot/emojis.py +26 -10
  49. bot/i18n.py +15 -2
  50. bot/locales/ar.json +178 -3
.gitattributes CHANGED
@@ -38,3 +38,5 @@ bot/cogs/__pycache__/community.cpython-311.pyc filter=lfs diff=lfs merge=lfs -te
38
  bot/cogs/__pycache__/engagement.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
39
  bot/cogs/__pycache__/media.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
40
  database.db filter=lfs diff=lfs merge=lfs -text
 
 
 
38
  bot/cogs/__pycache__/engagement.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
39
  bot/cogs/__pycache__/media.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
40
  database.db filter=lfs diff=lfs merge=lfs -text
41
+ bot/cogs/__pycache__/admin.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
42
+ bot/cogs/__pycache__/events.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
bot/__pycache__/__init__.cpython-311.pyc CHANGED
Binary files a/bot/__pycache__/__init__.cpython-311.pyc and b/bot/__pycache__/__init__.cpython-311.pyc differ
 
bot/__pycache__/config.cpython-311.pyc CHANGED
Binary files a/bot/__pycache__/config.cpython-311.pyc and b/bot/__pycache__/config.cpython-311.pyc differ
 
bot/__pycache__/database.cpython-311.pyc CHANGED
Binary files a/bot/__pycache__/database.cpython-311.pyc and b/bot/__pycache__/database.cpython-311.pyc differ
 
bot/__pycache__/emojis.cpython-311.pyc CHANGED
Binary files a/bot/__pycache__/emojis.cpython-311.pyc and b/bot/__pycache__/emojis.cpython-311.pyc differ
 
bot/__pycache__/i18n.cpython-311.pyc CHANGED
Binary files a/bot/__pycache__/i18n.cpython-311.pyc and b/bot/__pycache__/i18n.cpython-311.pyc differ
 
bot/__pycache__/main.cpython-311.pyc CHANGED
Binary files a/bot/__pycache__/main.cpython-311.pyc and b/bot/__pycache__/main.cpython-311.pyc differ
 
bot/__pycache__/server.cpython-311.pyc ADDED
Binary file (968 Bytes). View file
 
bot/__pycache__/theme.cpython-311.pyc CHANGED
Binary files a/bot/__pycache__/theme.cpython-311.pyc and b/bot/__pycache__/theme.cpython-311.pyc differ
 
bot/cogs/__pycache__/__init__.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/__init__.cpython-311.pyc and b/bot/cogs/__pycache__/__init__.cpython-311.pyc differ
 
bot/cogs/__pycache__/admin.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/admin.cpython-311.pyc and b/bot/cogs/__pycache__/admin.cpython-311.pyc differ
 
bot/cogs/__pycache__/ai_admin.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/ai_admin.cpython-311.pyc and b/bot/cogs/__pycache__/ai_admin.cpython-311.pyc differ
 
bot/cogs/__pycache__/ai_suite.cpython-311.pyc CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:cf125ae44086772a36fa22b27791a407f2e63f0fc7f9bf8b356e9e54695a1968
3
- size 143051
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e3d87a3a72d573124f17d8e63596d2f74a90fe1a464d7c3e483a0a7a1b97fae8
3
+ size 142795
bot/cogs/__pycache__/banner_manager.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/banner_manager.cpython-311.pyc and b/bot/cogs/__pycache__/banner_manager.cpython-311.pyc differ
 
bot/cogs/__pycache__/board_games.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/board_games.cpython-311.pyc and b/bot/cogs/__pycache__/board_games.cpython-311.pyc differ
 
bot/cogs/__pycache__/community.cpython-311.pyc CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:a34e59b7f605ed55c2ecfed7028c01ccf315cac7aea0be8ac3452cb8e3530c26
3
- size 119508
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:87a05d115d6c14cd3607f3ce9d415d52e370582ddf9817c707a8002eb0f39d66
3
+ size 131193
bot/cogs/__pycache__/configuration.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/configuration.cpython-311.pyc and b/bot/cogs/__pycache__/configuration.cpython-311.pyc differ
 
bot/cogs/__pycache__/developer.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/developer.cpython-311.pyc and b/bot/cogs/__pycache__/developer.cpython-311.pyc differ
 
bot/cogs/__pycache__/engagement.cpython-311.pyc CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:fbc07318ec00e7f522658a482610aa30df29d4ef3f95836f5d8eb7b0a6e794ca
3
- size 115797
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2dab296a18802c5fb56ad3d9d1e26cfcd915adc3b82be3c4dd50c7b294a781a2
3
+ size 120855
bot/cogs/__pycache__/events.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/events.cpython-311.pyc and b/bot/cogs/__pycache__/events.cpython-311.pyc differ
 
bot/cogs/__pycache__/fun.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/fun.cpython-311.pyc and b/bot/cogs/__pycache__/fun.cpython-311.pyc differ
 
bot/cogs/__pycache__/gambling.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/gambling.cpython-311.pyc and b/bot/cogs/__pycache__/gambling.cpython-311.pyc differ
 
bot/cogs/__pycache__/language.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/language.cpython-311.pyc and b/bot/cogs/__pycache__/language.cpython-311.pyc differ
 
bot/cogs/__pycache__/media.cpython-311.pyc CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:9cd726a7fc89417a87ada78320e9485c5113bbbf0a053c4d9efbeb9c1ac8960a
3
- size 209239
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ca5f86939d327aa5b764e849e35198a8f0881667d5202772e915f0a92899da33
3
+ size 220295
bot/cogs/__pycache__/media_helpers.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/media_helpers.cpython-311.pyc and b/bot/cogs/__pycache__/media_helpers.cpython-311.pyc differ
 
bot/cogs/__pycache__/menu.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/menu.cpython-311.pyc and b/bot/cogs/__pycache__/menu.cpython-311.pyc differ
 
bot/cogs/__pycache__/observability.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/observability.cpython-311.pyc and b/bot/cogs/__pycache__/observability.cpython-311.pyc differ
 
bot/cogs/__pycache__/server_manager.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/server_manager.cpython-311.pyc and b/bot/cogs/__pycache__/server_manager.cpython-311.pyc differ
 
bot/cogs/__pycache__/utility.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/utility.cpython-311.pyc and b/bot/cogs/__pycache__/utility.cpython-311.pyc differ
 
bot/cogs/__pycache__/verification.cpython-311.pyc CHANGED
Binary files a/bot/cogs/__pycache__/verification.cpython-311.pyc and b/bot/cogs/__pycache__/verification.cpython-311.pyc differ
 
bot/cogs/admin.py CHANGED
The diff for this file is too large to render. See raw diff
 
bot/cogs/ai_admin.py CHANGED
@@ -1,479 +1,931 @@
1
- """
2
- Autonomous AI Administrator - Senior Backend Engineer Implementation
3
- Permission Guard + Intelligence Layer + Execution Engine + Global Language Support
4
- """
5
-
6
  from __future__ import annotations
7
 
 
8
  import json
9
  import re
10
  from typing import Any
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- import discord
13
- from discord.ext import commands
14
-
15
- try:
16
- import aiohttp
17
- except Exception:
18
- aiohttp = None
19
-
20
-
21
- class PermissionGuard:
22
- """Validates hierarchy before any AI admin action."""
23
-
24
- @staticmethod
25
- def check(ctx: commands.Context) -> tuple[bool, str]:
26
- if not ctx.guild:
27
- return False, "Server only."
28
-
29
- if ctx.author.id == ctx.guild.owner_id:
30
- return True, ""
31
-
32
- if not ctx.author.guild_permissions.manage_guild:
33
- return False, "Manage Server permission required."
34
-
35
- member = ctx.guild.get_member(ctx.author.id)
36
- bot_member = ctx.guild.me
37
-
38
- if member and member.top_role.position >= bot_member.top_role.position:
39
- return False, "Insufficient Hierarchy: My role must be higher than yours."
40
-
41
- return True, ""
42
-
43
-
44
- class IntelligenceLayer:
45
- """OpenRouter integration for AI decision making."""
46
-
47
- def __init__(self, bot: commands.Bot) -> None:
48
- self.bot = bot
49
- self._session: aiohttp.ClientSession | None = None
50
-
51
- async def _get_session(self) -> aiohttp.ClientSession:
52
- if self._session is None or self._session.closed:
53
- self._session = aiohttp.ClientSession()
54
- return self._session
55
-
56
- async def ask_ai(self, prompt: str) -> list[dict[str, Any]] | None:
57
- settings = self.bot.settings
58
- api_key = getattr(settings, "openrouter_api_key", None)
59
- if not api_key:
60
- return None
61
-
62
- model = getattr(settings, "openrouter_model", "openai/gpt-4o-mini") or "openai/gpt-4o-mini"
63
-
64
- system_message = """You are an Autonomous AI Administrator for a Discord server.
65
- Output ONLY a valid JSON array of actions. Do not include any other text.
66
 
67
- Supported actions:
 
 
 
 
 
68
 
69
- 1. CREATE ROLE:
70
  {
71
- "action": "create_role",
72
- "name": "Role Name",
73
- "color": "#FF0000",
74
- "hoist": true,
75
- "reason": "Why this role is needed"
76
  }
77
 
78
- 2. CREATE CHANNEL:
79
  {
80
- "action": "create_channel",
81
- "name": "channel-name",
82
- "type": "text",
83
- "category": "Category Name",
84
- "locked_to_roles": ["Role1", "Role2"],
85
- "reason": "Why this channel is needed"
86
  }
87
 
88
- 3. ANNOUNCE:
89
  {
90
- "action": "announce",
91
  "channel": "channel-name",
92
- "title": "Announcement Title",
93
- "description": "Announcement content",
94
- "color": "#00FFFF"
95
  }
96
 
97
- 4. CREATE GIVEAWAY:
98
  {
99
- "action": "create_giveaway",
100
- "prize": "Prize description",
101
- "duration_minutes": 60,
102
- "winners": 1,
103
- "channel": "channel-name"
104
  }
105
 
106
- 5. CREATE TOURNAMENT:
107
  {
108
- "action": "create_tournament",
109
- "name": "Tournament Name",
110
- "game": "Game name",
111
- "max_participants": 16,
112
- "channel": "channel-name"
113
  }
114
 
115
- 6. CREATE POLL:
116
  {
117
- "action": "create_poll",
118
- "question": "Poll question",
119
- "options": ["Option 1", "Option 2", "Option 3"],
120
- "duration_minutes": 30
121
  }
122
 
123
- 7. RUN COMMAND (for any other bot command):
124
  {
125
- "action": "run_command",
126
- "command": "command_name",
127
- "args": {"arg1": "value1", "arg2": "value2"}
128
  }
129
 
130
- ALWAYS include a "response_to_user" field in the SAME language as the input.
131
- Example: If input is Arabic, response_to_user must be in Arabic.
132
-
133
- 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."""
 
 
 
134
 
135
- payload = {
136
- "model": model,
137
- "messages": [
138
- {"role": "system", "content": system_message},
139
- {"role": "user", "content": prompt}
140
- ],
141
- "temperature": 0.3,
142
- "max_tokens": 2000
143
- }
144
-
145
- headers = {
146
- "Authorization": f"Bearer {api_key}",
147
- "Content-Type": "application/json",
148
- "HTTP-Referer": "https://github.com/mega-bot",
149
- }
150
-
151
- try:
152
- session = await self._get_session()
153
- async with session.post(
154
- "https://openrouter.ai/api/v1/chat/completions",
155
- json=payload,
156
- headers=headers
157
- ) as resp:
158
- if resp.status != 200:
159
- return None
160
- data = await resp.json()
161
-
162
- content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
163
- return self._parse_json_response(content)
164
- except Exception:
165
- return None
166
-
167
- def _parse_json_response(self, content: str) -> list[dict[str, Any]] | None:
168
- try:
169
- content = content.strip()
170
- json_match = re.search(r'\[.*\]', content, re.DOTALL)
171
- if json_match:
172
- content = json_match.group(0)
173
- return json.loads(content)
174
- except Exception:
175
- return None
176
-
177
- async def close(self) -> None:
178
- if self._session and not self._session.closed:
179
- await self._session.close()
180
 
 
 
 
 
 
 
 
181
 
182
- class ExecutionEngine:
183
- """Executes AI decisions with proper error handling."""
184
-
185
- def __init__(self, bot: commands.Bot) -> None:
186
- self.bot = bot
187
-
188
- async def execute(self, actions: list[dict[str, Any]], ctx: commands.Context) -> list[str]:
189
- results = []
190
- for action in actions:
191
- action_type = action.get("action", "")
192
- try:
193
- if action_type == "create_role":
194
- result = await self._create_role(action, ctx)
195
- elif action_type == "create_channel":
196
- result = await self._create_channel(action, ctx)
197
- elif action_type == "announce":
198
- result = await self._announce(action, ctx)
199
- elif action_type == "create_giveaway":
200
- result = await self._create_giveaway(action, ctx)
201
- elif action_type == "create_tournament":
202
- result = await self._create_tournament(action, ctx)
203
- elif action_type == "create_poll":
204
- result = await self._create_poll(action, ctx)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  elif action_type == "run_command":
206
  result = await self._run_command(action, ctx)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  else:
208
  result = f"Unknown action: {action_type}"
209
  results.append(result)
210
  except Exception as e:
211
  results.append(f"Error executing {action_type}: {str(e)}")
212
  return results
213
-
214
- async def _create_role(self, action: dict[str, Any], ctx: commands.Context) -> str:
215
- name = action.get("name", "New Role")
216
- color_str = action.get("color", "#99AAB5")
217
- hoist = action.get("hoist", False)
218
- reason = action.get("reason", "AI Admin")
219
-
220
- try:
221
- color = discord.Color(int(color_str.lstrip("#"), 16))
222
- except ValueError:
223
- color = discord.Color.default()
224
-
225
- role = await ctx.guild.create_role(name=name, color=color, hoist=hoist, reason=reason)
226
- return f"Created role: {role.mention}"
227
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  async def _create_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
 
 
229
  name = action.get("name", "new-channel")
230
  channel_type = action.get("type", "text")
231
- category_name = action.get("category")
232
- locked_roles = action.get("locked_to_roles", [])
233
- reason = action.get("reason", "AI Admin")
234
-
235
- overwrites = {
236
- ctx.guild.default_role: discord.PermissionOverwrite(view_channel=False),
237
- ctx.guild.me: discord.PermissionOverwrite(view_channel=True, send_messages=True, manage_channels=True),
238
- }
239
-
240
- for role_name in locked_roles:
241
- role = discord.utils.get(ctx.guild.roles, name=role_name)
242
- if role:
243
- overwrites[role] = discord.PermissionOverwrite(view_channel=True, send_messages=True)
244
-
245
- category = None
246
- if category_name:
247
- category = discord.utils.get(ctx.guild.categories, name=category_name)
248
-
249
- if channel_type == "text":
250
- channel = await ctx.guild.create_text_channel(
251
- name=name, category=category, overwrites=overwrites, reason=reason
252
- )
253
- elif channel_type == "voice":
254
- channel = await ctx.guild.create_voice_channel(
255
- name=name, category=category, overwrites=overwrites, reason=reason
256
- )
257
- else:
258
- return f"Unknown channel type: {channel_type}"
259
-
260
- return f"Created channel: {channel.mention}"
261
-
262
- async def _announce(self, action: dict[str, Any], ctx: commands.Context) -> str:
263
- channel_name = action.get("channel")
264
- title = action.get("title", "Announcement")
265
- description = action.get("description", "")
266
- color_str = action.get("color", "#00FFFF")
267
-
268
- try:
269
- color = discord.Color(int(color_str.lstrip("#"), 16))
270
- except ValueError:
271
- color = discord.Color.blue()
272
-
273
- channel = None
274
- if channel_name:
275
- channel = discord.utils.get(ctx.guild.text_channels, name=channel_name)
276
-
277
- if not channel:
278
- channel = ctx.channel
279
-
280
- embed = discord.Embed(title=title, description=description, color=color)
281
- embed.timestamp = discord.utils.utcnow()
282
- await channel.send(embed=embed)
283
- return f"Announcement sent to {channel.mention}"
284
-
285
- async def _create_giveaway(self, action: dict[str, Any], ctx: commands.Context) -> str:
286
- prize = action.get("prize", "Giveaway Prize")
287
- duration = action.get("duration_minutes", 60)
288
- winners = action.get("winners", 1)
289
- channel_name = action.get("channel")
290
-
291
- channel = ctx.channel
292
- if channel_name:
293
- ch = discord.utils.get(ctx.guild.text_channels, name=channel_name)
294
- if ch:
295
- channel = ch
296
-
297
- cog = self.bot.get_cog("Community")
298
- if not cog:
299
- return "Community cog not found."
300
-
301
- await ctx.invoke(cog.giveaway, prize=prize, minutes=duration, winners=winners, channel=channel)
302
- return f"Giveaway created: {prize}"
303
-
304
- async def _create_tournament(self, action: dict[str, Any], ctx: commands.Context) -> str:
305
- name = action.get("name", "Tournament")
306
- game = action.get("game", "Game")
307
- max_participants = action.get("max_participants", 16)
308
- channel_name = action.get("channel")
309
-
310
- channel = ctx.channel
311
- if channel_name:
312
- ch = discord.utils.get(ctx.guild.text_channels, name=channel_name)
313
- if ch:
314
- channel = ch
315
-
316
- cog = self.bot.get_cog("Engagement")
317
- if not cog:
318
- return "Engagement cog not found."
319
-
320
- await ctx.invoke(cog.tournament_create, name=name, game=game, max=max_participants, channel=channel)
321
- return f"Tournament created: {name}"
322
-
323
- async def _create_poll(self, action: dict[str, Any], ctx: commands.Context) -> str:
324
- question = action.get("question", "Poll")
325
- options = action.get("options", ["Yes", "No"])
326
- duration = action.get("duration_minutes", 30)
327
-
328
- cog = self.bot.get_cog("Community")
329
- if not cog:
330
- return "Community cog not found."
331
-
332
- options_str = ",".join(options)
333
- await ctx.invoke(cog.poll, question=question, options=options_str, duration=duration)
334
- return f"Poll created: {question}"
335
-
336
- async def _run_command(self, action: dict[str, Any], ctx: commands.Context) -> str:
337
- command_name = action.get("command", "")
338
- args = action.get("args", {})
339
-
340
- command = self.bot.get_command(command_name)
341
- if not command:
342
- return f"Command not found: {command_name}"
343
-
344
- try:
345
- await ctx.invoke(command, **args)
346
- return f"Command executed: {command_name}"
347
- except Exception as e:
348
- return f"Error running command: {str(e)}"
349
-
350
- async def _create_giveaway(self, action: dict[str, Any], ctx: commands.Context) -> str:
351
- prize = action.get("prize", "Giveaway Prize")
352
- duration = action.get("duration_minutes", 60)
353
- winners = action.get("winners", 1)
354
- channel_name = action.get("channel")
355
-
356
- channel = ctx.channel
357
- if channel_name:
358
- ch = discord.utils.get(ctx.guild.text_channels, name=channel_name)
359
- if ch:
360
- channel = ch
361
-
362
- cog = self.bot.get_cog("Community")
363
- if not cog:
364
- return "Community cog not found."
365
-
366
- await ctx.invoke(cog.giveaway, prize=prize, minutes=duration, winners=winners, channel=channel)
367
- return f"Giveaway created: {prize}"
368
-
369
  async def _create_tournament(self, action: dict[str, Any], ctx: commands.Context) -> str:
370
  name = action.get("name", "Tournament")
371
  game = action.get("game", "Game")
372
  max_participants = action.get("max_participants", 16)
373
- channel_name = action.get("channel")
374
-
375
- channel = ctx.channel
376
- if channel_name:
377
- ch = discord.utils.get(ctx.guild.text_channels, name=channel_name)
378
- if ch:
379
- channel = ch
380
 
381
  cog = self.bot.get_cog("Engagement")
382
  if not cog:
383
  return "Engagement cog not found."
384
-
385
- await ctx.invoke(cog.tournament_create, name=name, game=game, max=max_participants, channel=channel)
 
 
386
  return f"Tournament created: {name}"
387
 
388
  async def _create_poll(self, action: dict[str, Any], ctx: commands.Context) -> str:
389
  question = action.get("question", "Poll")
390
  options = action.get("options", ["Yes", "No"])
391
- duration = action.get("duration_minutes", 30)
392
 
393
- cog = self.bot.get_cog("Community")
394
  if not cog:
395
- return "Community cog not found."
396
 
397
- options_str = ",".join(options)
398
- await ctx.invoke(cog.poll, question=question, options=options_str, duration=duration)
 
399
  return f"Poll created: {question}"
400
-
401
  async def _run_command(self, action: dict[str, Any], ctx: commands.Context) -> str:
402
  command_name = action.get("command", "")
403
  args = action.get("args", {})
404
-
405
- command = self.bot.get_command(command_name)
406
- if not command:
407
- return f"Command not found: {command_name}"
408
-
409
  try:
410
  await ctx.invoke(command, **args)
411
  return f"Command executed: {command_name}"
412
  except Exception as e:
413
  return f"Error running command: {str(e)}"
414
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  class AIAdmin(commands.Cog):
417
- """Autonomous AI Administrator Cog."""
418
-
419
  def __init__(self, bot: commands.Bot) -> None:
420
  self.bot = bot
421
  self.permission_guard = PermissionGuard()
422
  self.intelligence = IntelligenceLayer(bot)
423
  self.execution = ExecutionEngine(bot)
424
-
425
- async def cog_unload(self) -> None:
426
- await self.intelligence.close()
427
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  @commands.hybrid_command(name="ai_admin", description="Let AI administrate the server")
429
  async def ai_admin(self, ctx: commands.Context, *, request: str) -> None:
430
- if not self.permission_guard.check(ctx)[0]:
431
- await ctx.send(self.permission_guard.check(ctx)[1], ephemeral=True)
432
- return
433
-
434
- await ctx.defer()
435
-
436
- actions = await self.intelligence.ask_ai(request)
437
- if not actions:
438
- await ctx.send("AI failed to generate actions. Try again.", ephemeral=True)
439
  return
440
-
441
- response_text = None
442
- for action in actions:
443
- if "response_to_user" in action:
444
- response_text = action.pop("response_to_user")
445
- break
446
-
447
- results = await self.execution.execute(actions, ctx)
448
-
449
- if response_text:
450
- await ctx.send(response_text)
451
- else:
452
- await ctx.send("\n".join(results))
453
-
454
- @commands.hybrid_command(name="ai_help", description="Show AI Admin capabilities")
455
- async def ai_help(self, ctx: commands.Context) -> None:
456
- embed = discord.Embed(
457
- title="AI Administrator Help",
458
- description=(
459
- "I can help manage your server using AI. Examples:\n\n"
460
- "**Create a role:**\n"
461
- "`/ai_admin create a moderator role with purple color`\n\n"
462
- "**Create a channel:**\n"
463
- "`/ai_admin make a private channel for staff`\n\n"
464
- "**Create a giveaway:**\n"
465
- "`/ai_admin create a giveaway for Discord Nitro 24 hours`\n\n"
466
- "**Create a tournament:**\n"
467
- "`/ai_admin setup a Valorant tournament for 16 players`\n\n"
468
- "**Create a poll:**\n"
469
- "`/ai_admin make a poll about server events`\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  "**Announce:**\n"
471
- "`/ai_admin announce server maintenance in 1 hour`"
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  ),
473
  color=discord.Color.blue()
474
  )
475
- await ctx.send(embed=embed, ephemeral=True)
476
-
477
-
478
- async def setup(bot: commands.Bot) -> None:
479
- await bot.add_cog(AIAdmin(bot))
 
1
+ """
2
+ Autonomous AI Administrator - Senior Backend Engineer Implementation
3
+ Permission Guard + Intelligence Layer + Execution Engine + Global Language Support
4
+ """
5
+
6
  from __future__ import annotations
7
 
8
+ import datetime as dt
9
  import json
10
  import re
11
  from typing import Any
12
+
13
+ import discord
14
+ from discord.ext import commands
15
+
16
+ try:
17
+ import aiohttp
18
+ except Exception:
19
+ aiohttp = None
20
+
21
+
22
+ class PermissionGuard:
23
+ """Validates hierarchy before any AI admin action."""
24
+
25
+ @staticmethod
26
+ def check(ctx: commands.Context) -> tuple[bool, str]:
27
+ if not ctx.guild:
28
+ return False, "Server only."
29
+
30
+ if ctx.author.id == ctx.guild.owner_id:
31
+ return True, ""
32
+
33
+ if not ctx.author.guild_permissions.manage_guild:
34
+ return False, "Manage Server permission required."
35
+
36
+ member = ctx.guild.get_member(ctx.author.id)
37
+ bot_member = ctx.guild.me
38
+
39
+ if member and member.top_role.position >= bot_member.top_role.position:
40
+ return False, "Insufficient Hierarchy: My role must be higher than yours."
41
+
42
+ return True, ""
43
+
44
+
45
+ class IntelligenceLayer:
46
+ """OpenRouter integration for AI decision making."""
47
+
48
+ def __init__(self, bot: commands.Bot) -> None:
49
+ self.bot = bot
50
+ self._session: aiohttp.ClientSession | None = None
51
+
52
+ async def _get_session(self) -> aiohttp.ClientSession:
53
+ if self._session is None or self._session.closed:
54
+ self._session = aiohttp.ClientSession()
55
+ return self._session
56
+
57
+ async def ask_ai(self, prompt: str) -> list[dict[str, Any]] | None:
58
+ settings = self.bot.settings
59
+ api_key = getattr(settings, "openrouter_api_key", None)
60
+ if not api_key:
61
+ return None
62
+
63
+ model = getattr(settings, "openrouter_model", "openai/gpt-4o-mini") or "openai/gpt-4o-mini"
64
+
65
+ system_message = """You are an Autonomous AI Administrator for a Discord server.
66
+ Output ONLY a valid JSON array of actions. Do not include any other text.
67
+
68
+ Supported actions:
69
+
70
+ 1. CREATE ROLE:
71
+ {
72
+ "action": "create_role",
73
+ "name": "Role Name",
74
+ "color": "#FF0000",
75
+ "hoist": true,
76
+ "reason": "Why this role is needed"
77
+ }
78
+
79
+ 2. CREATE CHANNEL:
80
+ {
81
+ "action": "create_channel",
82
+ "name": "channel-name",
83
+ "type": "text",
84
+ "category": "Category Name",
85
+ "locked_to_roles": ["Role1", "Role2"],
86
+ "reason": "Why this channel is needed"
87
+ }
88
+
89
+ 3. ANNOUNCE:
90
+ {
91
+ "action": "announce",
92
+ "channel": "channel-name",
93
+ "title": "Announcement Title",
94
+ "description": "Announcement content",
95
+ "color": "#00FFFF"
96
+ }
97
+
98
+ 4. CREATE GIVEAWAY:
99
+ {
100
+ "action": "create_giveaway",
101
+ "prize": "Prize description",
102
+ "duration_minutes": 60,
103
+ "winners": 1,
104
+ "channel": "channel-name"
105
+ }
106
+
107
+ 5. CREATE TOURNAMENT:
108
+ {
109
+ "action": "create_tournament",
110
+ "name": "Tournament Name",
111
+ "game": "Game name",
112
+ "max_participants": 16,
113
+ "channel": "channel-name"
114
+ }
115
+
116
+ 6. CREATE POLL:
117
+ {
118
+ "action": "create_poll",
119
+ "question": "Poll question",
120
+ "options": ["Option 1", "Option 2", "Option 3"],
121
+ "duration_minutes": 30
122
+ }
123
+
124
+ 7. RUN COMMAND (for any other bot command):
125
+ {
126
+ "action": "run_command",
127
+ "command": "command_name",
128
+ "args": {"arg1": "value1", "arg2": "value2"}
129
+ }
130
 
131
+ 8. TIMEOUT MEMBER:
132
+ {
133
+ "action": "timeout_member",
134
+ "member": "@user or user_id",
135
+ "minutes": 30,
136
+ "reason": "Why timeout is needed"
137
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
+ 9. UNTIMEOUT MEMBER:
140
+ {
141
+ "action": "untimeout_member",
142
+ "member": "@user or user_id",
143
+ "reason": "Why timeout is removed"
144
+ }
145
 
146
+ 10. ADD ROLE:
147
  {
148
+ "action": "add_role",
149
+ "member": "@user or user_id",
150
+ "role": "Role Name",
151
+ "reason": "Why role is added"
 
152
  }
153
 
154
+ 11. REMOVE ROLE:
155
  {
156
+ "action": "remove_role",
157
+ "member": "@user or user_id",
158
+ "role": "Role Name",
159
+ "reason": "Why role is removed"
 
 
160
  }
161
 
162
+ 12. LOCK CHANNEL:
163
  {
164
+ "action": "lock_channel",
165
  "channel": "channel-name",
166
+ "reason": "Why channel is locked"
 
 
167
  }
168
 
169
+ 13. UNLOCK CHANNEL:
170
  {
171
+ "action": "unlock_channel",
172
+ "channel": "channel-name",
173
+ "reason": "Why channel is unlocked"
 
 
174
  }
175
 
176
+ 14. SLOWMODE:
177
  {
178
+ "action": "set_slowmode",
179
+ "channel": "channel-name",
180
+ "seconds": 10,
181
+ "reason": "Why slowmode is set"
 
182
  }
183
 
184
+ 15. PURGE MESSAGES:
185
  {
186
+ "action": "purge_messages",
187
+ "channel": "channel-name",
188
+ "amount": 25,
189
+ "reason": "Why messages are purged"
190
  }
191
 
192
+ 16. DELETE CHANNEL:
193
  {
194
+ "action": "delete_channel",
195
+ "channel": "channel-name",
196
+ "reason": "Why channel is deleted"
197
  }
198
 
199
+ 17. RENAME CHANNEL:
200
+ {
201
+ "action": "rename_channel",
202
+ "channel": "old-channel-name",
203
+ "new_name": "new-channel-name",
204
+ "reason": "Why channel is renamed"
205
+ }
206
 
207
+ 18. CREATE CATEGORY:
208
+ {
209
+ "action": "create_category",
210
+ "name": "Category Name",
211
+ "reason": "Why this category is needed"
212
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
+ 19. RENAME CATEGORY:
215
+ {
216
+ "action": "rename_category",
217
+ "category": "Old Category Name",
218
+ "new_name": "New Category Name",
219
+ "reason": "Why category is renamed"
220
+ }
221
 
222
+ 20. DELETE CATEGORY:
223
+ {
224
+ "action": "delete_category",
225
+ "category": "Category Name",
226
+ "reason": "Why category is deleted"
227
+ }
228
+
229
+ ALWAYS include a "response_to_user" field in the SAME language as the input.
230
+ Example: If input is Arabic, response_to_user must be in Arabic.
231
+
232
+ 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."""
233
+
234
+ payload = {
235
+ "model": model,
236
+ "messages": [
237
+ {"role": "system", "content": system_message},
238
+ {"role": "user", "content": prompt}
239
+ ],
240
+ "temperature": 0.3,
241
+ "max_tokens": 2000
242
+ }
243
+
244
+ headers = {
245
+ "Authorization": f"Bearer {api_key}",
246
+ "Content-Type": "application/json",
247
+ "HTTP-Referer": "https://github.com/mega-bot",
248
+ }
249
+
250
+ try:
251
+ session = await self._get_session()
252
+ async with session.post(
253
+ "https://openrouter.ai/api/v1/chat/completions",
254
+ json=payload,
255
+ headers=headers
256
+ ) as resp:
257
+ if resp.status != 200:
258
+ return None
259
+ data = await resp.json()
260
+
261
+ content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
262
+ return self._parse_json_response(content)
263
+ except Exception:
264
+ return None
265
+
266
+ def _parse_json_response(self, content: str) -> list[dict[str, Any]] | None:
267
+ try:
268
+ content = content.strip()
269
+ json_match = re.search(r'\[.*\]', content, re.DOTALL)
270
+ if json_match:
271
+ content = json_match.group(0)
272
+ return json.loads(content)
273
+ except Exception:
274
+ return None
275
+
276
+ async def close(self) -> None:
277
+ if self._session and not self._session.closed:
278
+ await self._session.close()
279
+
280
+
281
+ class ExecutionEngine:
282
+ """Executes AI decisions with proper error handling."""
283
+
284
+ def __init__(self, bot: commands.Bot) -> None:
285
+ self.bot = bot
286
+
287
+ async def execute(self, actions: list[dict[str, Any]], ctx: commands.Context) -> list[str]:
288
+ results = []
289
+ for action in actions:
290
+ action_type = action.get("action", "")
291
+ try:
292
+ if action_type == "create_role":
293
+ result = await self._create_role(action, ctx)
294
+ elif action_type == "create_channel":
295
+ result = await self._create_channel(action, ctx)
296
+ elif action_type == "announce":
297
+ result = await self._announce(action, ctx)
298
+ elif action_type == "create_giveaway":
299
+ result = await self._create_giveaway(action, ctx)
300
+ elif action_type == "create_tournament":
301
+ result = await self._create_tournament(action, ctx)
302
+ elif action_type == "create_poll":
303
+ result = await self._create_poll(action, ctx)
304
  elif action_type == "run_command":
305
  result = await self._run_command(action, ctx)
306
+ elif action_type == "timeout_member":
307
+ result = await self._timeout_member(action, ctx)
308
+ elif action_type == "untimeout_member":
309
+ result = await self._untimeout_member(action, ctx)
310
+ elif action_type == "add_role":
311
+ result = await self._add_role(action, ctx)
312
+ elif action_type == "remove_role":
313
+ result = await self._remove_role(action, ctx)
314
+ elif action_type == "lock_channel":
315
+ result = await self._lock_channel(action, ctx)
316
+ elif action_type == "unlock_channel":
317
+ result = await self._unlock_channel(action, ctx)
318
+ elif action_type == "set_slowmode":
319
+ result = await self._set_slowmode(action, ctx)
320
+ elif action_type == "purge_messages":
321
+ result = await self._purge_messages(action, ctx)
322
+ elif action_type == "delete_channel":
323
+ result = await self._delete_channel(action, ctx)
324
+ elif action_type == "rename_channel":
325
+ result = await self._rename_channel(action, ctx)
326
+ elif action_type == "create_category":
327
+ result = await self._create_category(action, ctx)
328
+ elif action_type == "rename_category":
329
+ result = await self._rename_category(action, ctx)
330
+ elif action_type == "delete_category":
331
+ result = await self._delete_category(action, ctx)
332
  else:
333
  result = f"Unknown action: {action_type}"
334
  results.append(result)
335
  except Exception as e:
336
  results.append(f"Error executing {action_type}: {str(e)}")
337
  return results
338
+
339
+ @staticmethod
340
+ def _resolve_member(guild: discord.Guild, ref: str | int | None) -> discord.Member | None:
341
+ if ref is None:
342
+ return None
343
+ text = str(ref).strip()
344
+ mention = re.search(r"<@!?(\d+)>", text)
345
+ if mention:
346
+ return guild.get_member(int(mention.group(1)))
347
+ if text.isdigit():
348
+ return guild.get_member(int(text))
349
+ return discord.utils.find(lambda m: m.name.lower() == text.lower() or m.display_name.lower() == text.lower(), guild.members)
350
+
351
+ @staticmethod
352
+ def _resolve_channel(guild: discord.Guild, ref: str | None, fallback: discord.abc.GuildChannel | None = None) -> discord.TextChannel | None:
353
+ if ref:
354
+ text = str(ref).strip()
355
+ mention = re.search(r"<#(\d+)>", text)
356
+ if mention:
357
+ ch = guild.get_channel(int(mention.group(1)))
358
+ return ch if isinstance(ch, discord.TextChannel) else None
359
+ by_name = discord.utils.get(guild.text_channels, name=text)
360
+ if by_name:
361
+ return by_name
362
+ if isinstance(fallback, discord.TextChannel):
363
+ return fallback
364
+ return None
365
+
366
+ @staticmethod
367
+ def _resolve_guild_channel(
368
+ guild: discord.Guild,
369
+ ref: str | int | None,
370
+ fallback: discord.abc.GuildChannel | None = None,
371
+ ) -> discord.abc.GuildChannel | None:
372
+ if ref is not None:
373
+ text = str(ref).strip()
374
+ mention = re.search(r"<#(\d+)>", text)
375
+ if mention:
376
+ return guild.get_channel(int(mention.group(1)))
377
+ if text.isdigit():
378
+ return guild.get_channel(int(text))
379
+ by_name = discord.utils.find(lambda c: c.name.lower() == text.lower(), guild.channels)
380
+ if by_name:
381
+ return by_name
382
+ if isinstance(fallback, discord.abc.GuildChannel):
383
+ return fallback
384
+ return None
385
+
386
+ @staticmethod
387
+ def _resolve_category(guild: discord.Guild, ref: str | int | None) -> discord.CategoryChannel | None:
388
+ if ref is None:
389
+ return None
390
+ text = str(ref).strip()
391
+ mention = re.search(r"<#(\d+)>", text)
392
+ if mention:
393
+ ch = guild.get_channel(int(mention.group(1)))
394
+ return ch if isinstance(ch, discord.CategoryChannel) else None
395
+ if text.isdigit():
396
+ ch = guild.get_channel(int(text))
397
+ return ch if isinstance(ch, discord.CategoryChannel) else None
398
+ return discord.utils.find(lambda c: c.name.lower() == text.lower(), guild.categories)
399
+
400
+ async def _create_role(self, action: dict[str, Any], ctx: commands.Context) -> str:
401
+ name = action.get("name", "New Role")
402
+ color_str = action.get("color", "#99AAB5")
403
+ hoist = action.get("hoist", False)
404
+ reason = action.get("reason", "AI Admin")
405
+
406
+ try:
407
+ color = discord.Color(int(color_str.lstrip("#"), 16))
408
+ except ValueError:
409
+ color = discord.Color.default()
410
+
411
+ role = await ctx.guild.create_role(name=name, color=color, hoist=hoist, reason=reason)
412
+ return f"Created role: {role.mention}"
413
+
414
  async def _create_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
415
+ if not ctx.author.guild_permissions.manage_channels:
416
+ return "Manage Channels permission required."
417
  name = action.get("name", "new-channel")
418
  channel_type = action.get("type", "text")
419
+ category_name = action.get("category")
420
+ locked_roles = action.get("locked_to_roles", [])
421
+ reason = action.get("reason", "AI Admin")
422
+
423
+ overwrites = {
424
+ ctx.guild.default_role: discord.PermissionOverwrite(view_channel=False),
425
+ ctx.guild.me: discord.PermissionOverwrite(view_channel=True, send_messages=True, manage_channels=True),
426
+ }
427
+
428
+ for role_name in locked_roles:
429
+ role = discord.utils.get(ctx.guild.roles, name=role_name)
430
+ if role:
431
+ overwrites[role] = discord.PermissionOverwrite(view_channel=True, send_messages=True)
432
+
433
+ category = None
434
+ if category_name:
435
+ category = discord.utils.get(ctx.guild.categories, name=category_name)
436
+
437
+ if channel_type == "text":
438
+ channel = await ctx.guild.create_text_channel(
439
+ name=name, category=category, overwrites=overwrites, reason=reason
440
+ )
441
+ elif channel_type == "voice":
442
+ channel = await ctx.guild.create_voice_channel(
443
+ name=name, category=category, overwrites=overwrites, reason=reason
444
+ )
445
+ else:
446
+ return f"Unknown channel type: {channel_type}"
447
+
448
+ return f"Created channel: {channel.mention}"
449
+
450
+ async def _announce(self, action: dict[str, Any], ctx: commands.Context) -> str:
451
+ channel_name = action.get("channel")
452
+ title = action.get("title", "Announcement")
453
+ description = action.get("description", "")
454
+ color_str = action.get("color", "#00FFFF")
455
+
456
+ try:
457
+ color = discord.Color(int(color_str.lstrip("#"), 16))
458
+ except ValueError:
459
+ color = discord.Color.blue()
460
+
461
+ channel = None
462
+ if channel_name:
463
+ channel = discord.utils.get(ctx.guild.text_channels, name=channel_name)
464
+
465
+ if not channel:
466
+ channel = ctx.channel
467
+
468
+ embed = discord.Embed(title=title, description=description, color=color)
469
+ embed.timestamp = discord.utils.utcnow()
470
+ await channel.send(embed=embed)
471
+ return f"Announcement sent to {channel.mention}"
472
+
473
+ async def _create_giveaway(self, action: dict[str, Any], ctx: commands.Context) -> str:
474
+ prize = action.get("prize", "Giveaway Prize")
475
+ duration = action.get("duration_minutes", 60)
476
+ winners = action.get("winners", 1)
477
+ channel_name = action.get("channel")
478
+
479
+ channel = ctx.channel
480
+ if channel_name:
481
+ ch = discord.utils.get(ctx.guild.text_channels, name=channel_name)
482
+ if ch:
483
+ channel = ch
484
+
485
+ cog = self.bot.get_cog("Community")
486
+ if not cog:
487
+ return "Community cog not found."
488
+
489
+ await cog.giveaway_create(ctx, int(duration), int(winners), prize=prize)
490
+ return f"Giveaway created: {prize}"
491
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
  async def _create_tournament(self, action: dict[str, Any], ctx: commands.Context) -> str:
493
  name = action.get("name", "Tournament")
494
  game = action.get("game", "Game")
495
  max_participants = action.get("max_participants", 16)
 
 
 
 
 
 
 
496
 
497
  cog = self.bot.get_cog("Engagement")
498
  if not cog:
499
  return "Engagement cog not found."
500
+
501
+ # Engagement exposes tournament creation via the base `/tournament` command.
502
+ games = f"{game}" if game else "chess, checkers, connect4, othello"
503
+ await ctx.invoke(cog.tournament, name=name, games=games)
504
  return f"Tournament created: {name}"
505
 
506
  async def _create_poll(self, action: dict[str, Any], ctx: commands.Context) -> str:
507
  question = action.get("question", "Poll")
508
  options = action.get("options", ["Yes", "No"])
 
509
 
510
+ cog = self.bot.get_cog("Utility")
511
  if not cog:
512
+ return "Utility cog not found."
513
 
514
+ # Utility poll command accepts options as a "|" separated string.
515
+ options_str = "|".join(str(o) for o in options)
516
+ await ctx.invoke(cog.poll, question=question, options=options_str)
517
  return f"Poll created: {question}"
518
+
519
  async def _run_command(self, action: dict[str, Any], ctx: commands.Context) -> str:
520
  command_name = action.get("command", "")
521
  args = action.get("args", {})
522
+
523
+ command = self.bot.get_command(command_name)
524
+ if not command:
525
+ return f"Command not found: {command_name}"
526
+
527
  try:
528
  await ctx.invoke(command, **args)
529
  return f"Command executed: {command_name}"
530
  except Exception as e:
531
  return f"Error running command: {str(e)}"
532
 
533
+ async def _timeout_member(self, action: dict[str, Any], ctx: commands.Context) -> str:
534
+ member = self._resolve_member(ctx.guild, action.get("member"))
535
+ if not member:
536
+ return "Member not found."
537
+ minutes = max(1, min(int(action.get("minutes", 10)), 40320))
538
+ reason = action.get("reason", "AI Admin timeout")
539
+ until = discord.utils.utcnow() + dt.timedelta(minutes=minutes)
540
+ await member.timeout(until, reason=reason)
541
+ return f"Timed out {member.mention} for {minutes} minute(s)."
542
+
543
+ async def _untimeout_member(self, action: dict[str, Any], ctx: commands.Context) -> str:
544
+ member = self._resolve_member(ctx.guild, action.get("member"))
545
+ if not member:
546
+ return "Member not found."
547
+ reason = action.get("reason", "AI Admin untimeout")
548
+ await member.timeout(None, reason=reason)
549
+ return f"Removed timeout from {member.mention}."
550
+
551
+ async def _add_role(self, action: dict[str, Any], ctx: commands.Context) -> str:
552
+ member = self._resolve_member(ctx.guild, action.get("member"))
553
+ if not member:
554
+ return "Member not found."
555
+ role_name = str(action.get("role", "")).strip()
556
+ role = discord.utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles)
557
+ if not role:
558
+ return f"Role not found: {role_name}"
559
+ reason = action.get("reason", "AI Admin add role")
560
+ await member.add_roles(role, reason=reason)
561
+ return f"Added role **{role.name}** to {member.mention}."
562
+
563
+ async def _remove_role(self, action: dict[str, Any], ctx: commands.Context) -> str:
564
+ member = self._resolve_member(ctx.guild, action.get("member"))
565
+ if not member:
566
+ return "Member not found."
567
+ role_name = str(action.get("role", "")).strip()
568
+ role = discord.utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles)
569
+ if not role:
570
+ return f"Role not found: {role_name}"
571
+ reason = action.get("reason", "AI Admin remove role")
572
+ await member.remove_roles(role, reason=reason)
573
+ return f"Removed role **{role.name}** from {member.mention}."
574
+
575
+ async def _lock_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
576
+ if not ctx.author.guild_permissions.manage_channels:
577
+ return "Manage Channels permission required."
578
+ channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel)
579
+ if not channel:
580
+ return "Channel not found."
581
+ reason = action.get("reason", "AI Admin lock channel")
582
+ await channel.set_permissions(ctx.guild.default_role, send_messages=False, reason=reason)
583
+ return f"Locked channel {channel.mention}."
584
+
585
+ async def _unlock_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
586
+ if not ctx.author.guild_permissions.manage_channels:
587
+ return "Manage Channels permission required."
588
+ channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel)
589
+ if not channel:
590
+ return "Channel not found."
591
+ reason = action.get("reason", "AI Admin unlock channel")
592
+ await channel.set_permissions(ctx.guild.default_role, send_messages=True, reason=reason)
593
+ return f"Unlocked channel {channel.mention}."
594
+
595
+ async def _set_slowmode(self, action: dict[str, Any], ctx: commands.Context) -> str:
596
+ if not ctx.author.guild_permissions.manage_channels:
597
+ return "Manage Channels permission required."
598
+ channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel)
599
+ if not channel:
600
+ return "Channel not found."
601
+ seconds = max(0, min(int(action.get("seconds", 0)), 21600))
602
+ reason = action.get("reason", "AI Admin slowmode")
603
+ await channel.edit(slowmode_delay=seconds, reason=reason)
604
+ return f"Set slowmode in {channel.mention} to {seconds}s."
605
+
606
+ async def _purge_messages(self, action: dict[str, Any], ctx: commands.Context) -> str:
607
+ if not ctx.author.guild_permissions.manage_messages:
608
+ return "Manage Messages permission required."
609
+ channel = self._resolve_channel(ctx.guild, action.get("channel"), ctx.channel)
610
+ if not channel:
611
+ return "Channel not found."
612
+ amount = max(1, min(int(action.get("amount", 10)), 200))
613
+ deleted = await channel.purge(limit=amount)
614
+ return f"Purged {len(deleted)} message(s) in {channel.mention}."
615
 
616
+ async def _delete_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
617
+ if not ctx.author.guild_permissions.manage_channels:
618
+ return "Manage Channels permission required."
619
+ channel = self._resolve_guild_channel(ctx.guild, action.get("channel"), None)
620
+ if not channel:
621
+ return "Channel not found."
622
+ if ctx.channel and channel.id == ctx.channel.id:
623
+ return "Refusing to delete the channel currently being used for command execution."
624
+ reason = action.get("reason", "AI Admin delete channel")
625
+ name = channel.name
626
+ await channel.delete(reason=reason)
627
+ return f"Deleted channel #{name}."
628
+
629
+ async def _rename_channel(self, action: dict[str, Any], ctx: commands.Context) -> str:
630
+ if not ctx.author.guild_permissions.manage_channels:
631
+ return "Manage Channels permission required."
632
+ channel = self._resolve_guild_channel(ctx.guild, action.get("channel"), None)
633
+ if not channel:
634
+ return "Channel not found."
635
+ new_name = str(action.get("new_name", "")).strip()
636
+ if not new_name:
637
+ return "New channel name is required."
638
+ reason = action.get("reason", "AI Admin rename channel")
639
+ old_name = channel.name
640
+ await channel.edit(name=new_name[:100], reason=reason)
641
+ return f"Renamed channel **{old_name}** -> **{new_name[:100]}**."
642
+
643
+ async def _create_category(self, action: dict[str, Any], ctx: commands.Context) -> str:
644
+ if not ctx.author.guild_permissions.manage_channels:
645
+ return "Manage Channels permission required."
646
+ name = str(action.get("name", "new-category")).strip()[:100]
647
+ if not name:
648
+ return "Category name is required."
649
+ reason = action.get("reason", "AI Admin create category")
650
+ category = await ctx.guild.create_category(name=name, reason=reason)
651
+ return f"Created category: **{category.name}**."
652
+
653
+ async def _rename_category(self, action: dict[str, Any], ctx: commands.Context) -> str:
654
+ if not ctx.author.guild_permissions.manage_channels:
655
+ return "Manage Channels permission required."
656
+ category = self._resolve_category(ctx.guild, action.get("category"))
657
+ if not category:
658
+ return "Category not found."
659
+ new_name = str(action.get("new_name", "")).strip()[:100]
660
+ if not new_name:
661
+ return "New category name is required."
662
+ reason = action.get("reason", "AI Admin rename category")
663
+ old_name = category.name
664
+ await category.edit(name=new_name, reason=reason)
665
+ return f"Renamed category **{old_name}** -> **{new_name}**."
666
+
667
+ async def _delete_category(self, action: dict[str, Any], ctx: commands.Context) -> str:
668
+ if not ctx.author.guild_permissions.manage_channels:
669
+ return "Manage Channels permission required."
670
+ category = self._resolve_category(ctx.guild, action.get("category"))
671
+ if not category:
672
+ return "Category not found."
673
+ reason = action.get("reason", "AI Admin delete category")
674
+ name = category.name
675
+ channels_inside = len(category.channels)
676
+ await category.delete(reason=reason)
677
+ return f"Deleted category **{name}** (had {channels_inside} channel(s))."
678
+
679
  class AIAdmin(commands.Cog):
680
+ """Autonomous AI Administrator Cog."""
681
+
682
  def __init__(self, bot: commands.Bot) -> None:
683
  self.bot = bot
684
  self.permission_guard = PermissionGuard()
685
  self.intelligence = IntelligenceLayer(bot)
686
  self.execution = ExecutionEngine(bot)
687
+
688
+ @staticmethod
689
+ def _parse_duration_minutes(text: str) -> int | None:
690
+ match = re.search(r"(\d+)\s*(m|min|mins|minute|minutes|h|hr|hour|hours|d|day|days)?", text, re.IGNORECASE)
691
+ if not match:
692
+ return None
693
+ value = int(match.group(1))
694
+ unit = (match.group(2) or "m").lower()
695
+ if unit.startswith("h"):
696
+ value *= 60
697
+ elif unit.startswith("d"):
698
+ value *= 60 * 24
699
+ return max(1, min(value, 40320))
700
+
701
+ async def _try_direct_moderation(self, ctx: commands.Context, request: str) -> str | None:
702
+ if not ctx.guild:
703
+ return "Server only."
704
+ text = (request or "").strip()
705
+ lower = text.lower()
706
+
707
+ target = None
708
+ if ctx.message and ctx.message.mentions:
709
+ target = ctx.message.mentions[0]
710
+ else:
711
+ match = re.search(r"<@!?(\d+)>", text)
712
+ if match:
713
+ target = ctx.guild.get_member(int(match.group(1)))
714
+
715
+ if lower.startswith("kick ") and target:
716
+ if not ctx.author.guild_permissions.kick_members:
717
+ return "You need Kick Members permission."
718
+ await target.kick(reason=f"AI Admin by {ctx.author}")
719
+ return f"OK: Kicked {target.mention}"
720
+
721
+ if lower.startswith("ban ") and target:
722
+ if not ctx.author.guild_permissions.ban_members:
723
+ return "You need Ban Members permission."
724
+ await target.ban(reason=f"AI Admin by {ctx.author}", delete_message_days=0)
725
+ return f"OK: Banned {target.mention}"
726
+
727
+ if (lower.startswith("mute ") or lower.startswith("timeout ")) and target:
728
+ if not ctx.author.guild_permissions.moderate_members:
729
+ return "You need Moderate Members permission."
730
+ minutes = self._parse_duration_minutes(text) or 10
731
+ until = discord.utils.utcnow() + dt.timedelta(minutes=minutes)
732
+ await target.timeout(until, reason=f"AI Admin by {ctx.author}")
733
+ return f"OK: Timed out {target.mention} for {minutes} minute(s)"
734
+
735
+ if (lower.startswith("unmute ") or lower.startswith("untimeout ")) and target:
736
+ if not ctx.author.guild_permissions.moderate_members:
737
+ return "You need Moderate Members permission."
738
+ await target.timeout(None, reason=f"AI Admin by {ctx.author}")
739
+ return f"OK: Removed timeout from {target.mention}"
740
+
741
+ role_match = re.search(r"(?:give|add)\s+role\s+(.+?)\s+(?:to|for)\s+<@!?(\d+)>", text, re.IGNORECASE)
742
+ if role_match:
743
+ role_name = role_match.group(1).strip(" \"'")
744
+ member = ctx.guild.get_member(int(role_match.group(2)))
745
+ if not member:
746
+ return "Target member not found."
747
+ role = discord.utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles)
748
+ if not role:
749
+ return f"Role not found: {role_name}"
750
+ if not ctx.author.guild_permissions.manage_roles:
751
+ return "You need Manage Roles permission."
752
+ await member.add_roles(role, reason=f"AI Admin by {ctx.author}")
753
+ return f"OK: Added role **{role.name}** to {member.mention}"
754
+
755
+ remove_role_match = re.search(r"(?:remove)\s+role\s+(.+?)\s+(?:from)\s+<@!?(\d+)>", text, re.IGNORECASE)
756
+ if remove_role_match:
757
+ role_name = remove_role_match.group(1).strip(" \"'")
758
+ member = ctx.guild.get_member(int(remove_role_match.group(2)))
759
+ if not member:
760
+ return "Target member not found."
761
+ role = discord.utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles)
762
+ if not role:
763
+ return f"Role not found: {role_name}"
764
+ if not ctx.author.guild_permissions.manage_roles:
765
+ return "You need Manage Roles permission."
766
+ await member.remove_roles(role, reason=f"AI Admin by {ctx.author}")
767
+ return f"OK: Removed role **{role.name}** from {member.mention}"
768
+
769
+ lock_match = re.search(r"^lock(?:\s+<#(\d+)>)?", lower)
770
+ if lock_match:
771
+ if not ctx.author.guild_permissions.manage_channels:
772
+ return "You need Manage Channels permission."
773
+ channel = ctx.guild.get_channel(int(lock_match.group(1))) if lock_match.group(1) else ctx.channel
774
+ if isinstance(channel, discord.TextChannel):
775
+ await channel.set_permissions(ctx.guild.default_role, send_messages=False, reason=f"AI Admin by {ctx.author}")
776
+ return f"OK: Locked {channel.mention}"
777
+
778
+ unlock_match = re.search(r"^unlock(?:\s+<#(\d+)>)?", lower)
779
+ if unlock_match:
780
+ if not ctx.author.guild_permissions.manage_channels:
781
+ return "You need Manage Channels permission."
782
+ channel = ctx.guild.get_channel(int(unlock_match.group(1))) if unlock_match.group(1) else ctx.channel
783
+ if isinstance(channel, discord.TextChannel):
784
+ await channel.set_permissions(ctx.guild.default_role, send_messages=True, reason=f"AI Admin by {ctx.author}")
785
+ return f"OK: Unlocked {channel.mention}"
786
+
787
+ rename_channel_match = re.search(
788
+ r"(?:rename)\s+channel\s+(.+?)\s+(?:to|->)\s+(.+)$",
789
+ text,
790
+ re.IGNORECASE,
791
+ )
792
+ if rename_channel_match:
793
+ if not ctx.author.guild_permissions.manage_channels:
794
+ return "You need Manage Channels permission."
795
+ old_ref = rename_channel_match.group(1).strip(" \"'")
796
+ new_name = rename_channel_match.group(2).strip(" \"'")
797
+ channel = ExecutionEngine._resolve_guild_channel(ctx.guild, old_ref)
798
+ if not channel:
799
+ return f"Channel not found: {old_ref}"
800
+ if not new_name:
801
+ return "New channel name is required."
802
+ old_name = channel.name
803
+ await channel.edit(name=new_name[:100], reason=f"AI Admin by {ctx.author}")
804
+ return f"OK: Renamed channel **{old_name}** -> **{new_name[:100]}**"
805
+
806
+ delete_channel_match = re.search(r"(?:delete|remove)\s+channel\s+(.+)$", text, re.IGNORECASE)
807
+ if delete_channel_match:
808
+ if not ctx.author.guild_permissions.manage_channels:
809
+ return "You need Manage Channels permission."
810
+ ref = delete_channel_match.group(1).strip(" \"'")
811
+ channel = ExecutionEngine._resolve_guild_channel(ctx.guild, ref)
812
+ if not channel:
813
+ return f"Channel not found: {ref}"
814
+ if ctx.channel and channel.id == ctx.channel.id:
815
+ return "Refusing to delete the channel currently being used for command execution."
816
+ name = channel.name
817
+ await channel.delete(reason=f"AI Admin by {ctx.author}")
818
+ return f"OK: Deleted channel **{name}**"
819
+
820
+ rename_category_match = re.search(
821
+ r"(?:rename)\s+(?:category|directory)\s+(.+?)\s+(?:to|->)\s+(.+)$",
822
+ text,
823
+ re.IGNORECASE,
824
+ )
825
+ if rename_category_match:
826
+ if not ctx.author.guild_permissions.manage_channels:
827
+ return "You need Manage Channels permission."
828
+ old_ref = rename_category_match.group(1).strip(" \"'")
829
+ new_name = rename_category_match.group(2).strip(" \"'")
830
+ category = ExecutionEngine._resolve_category(ctx.guild, old_ref)
831
+ if not category:
832
+ return f"Category not found: {old_ref}"
833
+ if not new_name:
834
+ return "New category name is required."
835
+ old_name = category.name
836
+ await category.edit(name=new_name[:100], reason=f"AI Admin by {ctx.author}")
837
+ return f"OK: Renamed category **{old_name}** -> **{new_name[:100]}**"
838
+
839
+ delete_category_match = re.search(r"(?:delete|remove)\s+(?:category|directory)\s+(.+)$", text, re.IGNORECASE)
840
+ if delete_category_match:
841
+ if not ctx.author.guild_permissions.manage_channels:
842
+ return "You need Manage Channels permission."
843
+ ref = delete_category_match.group(1).strip(" \"'")
844
+ category = ExecutionEngine._resolve_category(ctx.guild, ref)
845
+ if not category:
846
+ return f"Category not found: {ref}"
847
+ name = category.name
848
+ channels_inside = len(category.channels)
849
+ await category.delete(reason=f"AI Admin by {ctx.author}")
850
+ return f"OK: Deleted category **{name}** (had {channels_inside} channel(s))"
851
+
852
+ return None
853
+
854
+ async def cog_unload(self) -> None:
855
+ await self.intelligence.close()
856
+
857
  @commands.hybrid_command(name="ai_admin", description="Let AI administrate the server")
858
  async def ai_admin(self, ctx: commands.Context, *, request: str) -> None:
859
+ allowed, deny_reason = self.permission_guard.check(ctx)
860
+ if not allowed:
861
+ await ctx.send(deny_reason, ephemeral=True)
 
 
 
 
 
 
862
  return
863
+
864
+ direct_result = await self._try_direct_moderation(ctx, request)
865
+ if direct_result is not None:
866
+ await ctx.send(direct_result, ephemeral=True)
867
+ return
868
+
869
+ if ctx.interaction and not ctx.interaction.response.is_done():
870
+ try:
871
+ await ctx.defer()
872
+ except discord.InteractionResponded:
873
+ pass
874
+
875
+ actions = await self.intelligence.ask_ai(request)
876
+ if not actions:
877
+ await ctx.send("AI failed to generate actions. Try again.", ephemeral=True)
878
+ return
879
+
880
+ response_text = None
881
+ for action in actions:
882
+ if "response_to_user" in action:
883
+ response_text = action.pop("response_to_user")
884
+ break
885
+
886
+ results = await self.execution.execute(actions, ctx)
887
+
888
+ if response_text:
889
+ await ctx.send(response_text)
890
+ else:
891
+ await ctx.send("\n".join(results))
892
+
893
+ @commands.hybrid_command(name="ai_help", description="Show AI Admin capabilities")
894
+ async def ai_help(self, ctx: commands.Context) -> None:
895
+ embed = discord.Embed(
896
+ title="AI Administrator Help",
897
+ description=(
898
+ "I can help manage your server using AI. Examples:\n\n"
899
+ "**Create a role:**\n"
900
+ "`/ai_admin create a moderator role with purple color`\n\n"
901
+ "**Create a channel:**\n"
902
+ "`/ai_admin make a private channel for staff`\n\n"
903
+ "**Create a giveaway:**\n"
904
+ "`/ai_admin create a giveaway for Discord Nitro 24 hours`\n\n"
905
+ "**Create a tournament:**\n"
906
+ "`/ai_admin setup a Valorant tournament for 16 players`\n\n"
907
+ "**Create a poll:**\n"
908
+ "`/ai_admin make a poll about server events`\n\n"
909
  "**Announce:**\n"
910
+ "`/ai_admin announce server maintenance in 1 hour`\n\n"
911
+ "**Direct moderation shortcuts:**\n"
912
+ "`/ai_admin kick @user`\n"
913
+ "`/ai_admin ban @user`\n"
914
+ "`/ai_admin mute @user 30m`\n"
915
+ "`/ai_admin unmute @user`\n"
916
+ "`/ai_admin add role VIP to @user`\n"
917
+ "`/ai_admin remove role VIP from @user`\n"
918
+ "`/ai_admin lock #channel`\n"
919
+ "`/ai_admin unlock #channel`\n"
920
+ "`/ai_admin rename channel old-name to new-name`\n"
921
+ "`/ai_admin delete channel old-name`\n"
922
+ "`/ai_admin rename category Staff to Team-Staff`\n"
923
+ "`/ai_admin delete category Team-Staff`"
924
  ),
925
  color=discord.Color.blue()
926
  )
927
+ await ctx.send(embed=embed, ephemeral=True)
928
+
929
+
930
+ async def setup(bot: commands.Bot) -> None:
931
+ await bot.add_cog(AIAdmin(bot))
bot/cogs/ai_suite.py CHANGED
@@ -1354,12 +1354,11 @@ class AISuite(commands.Cog):
1354
  # Fallback mappings for panel bootstrapping when planner is unavailable.
1355
  lowered = text.lower()
1356
  if any(k in lowered for k in ("اقتراح", "suggestion panel", "suggest panel", "suggestions panel")):
1357
- if ctx:
1358
- cmd = self.bot.get_command("suggestion_panel")
1359
- if cmd:
1360
- await ctx.invoke(cmd)
1361
- return "✅ تم نشر لوحة الاقتراحات." if guild_lang == "ar" else "✅ Suggestion panel requested."
1362
- return "استخدم `/suggestion_panel` لنشر لوحة الاقتراحات التفاعلية." if guild_lang == "ar" else "Use `/suggestion_panel` to deploy the interactive suggestions UI."
1363
  if any(k in lowered for k in ("تذكرة", "tickets", "ticket panel", "دعم")):
1364
  if ctx:
1365
  cmd = self.bot.get_command("ticket_panel")
 
1354
  # Fallback mappings for panel bootstrapping when planner is unavailable.
1355
  lowered = text.lower()
1356
  if any(k in lowered for k in ("اقتراح", "suggestion panel", "suggest panel", "suggestions panel")):
1357
+ return (
1358
+ "نظام الاقتراحات يعمل عبر قناة مخصصة. استخدم `/setsuggestionchannel #channel` ثم أرسل اقتراحك هناك."
1359
+ if guild_lang == "ar"
1360
+ else "Suggestions use a dedicated channel. Run `/setsuggestionchannel #channel` then post suggestions there."
1361
+ )
 
1362
  if any(k in lowered for k in ("تذكرة", "tickets", "ticket panel", "دعم")):
1363
  if ctx:
1364
  cmd = self.bot.get_command("ticket_panel")
bot/cogs/banner_manager.py CHANGED
@@ -4,6 +4,9 @@ Server Banner Management Cog - Allows servers to set custom banners
4
 
5
  from __future__ import annotations
6
 
 
 
 
7
  import discord
8
  from discord.ext import commands
9
  from discord import app_commands
@@ -26,15 +29,19 @@ class SetBannerModal(discord.ui.Modal, title="🖼️ Set Server Banner"):
26
  self.cog = cog
27
 
28
  async def on_submit(self, interaction: discord.Interaction) -> None:
29
- url = self.banner_url.value.strip()
30
 
31
  # Validate URL
32
- if not url.startswith(("http://", "https://")):
33
- await interaction.response.send_message("❌ Invalid URL. Must start with http:// or https://", ephemeral=True)
 
 
 
34
  return
35
 
36
  guild_id = interaction.guild.id if interaction.guild else 0
37
-
 
38
  # Save to database
39
  await self.cog.bot.db.execute(
40
  "INSERT INTO guild_config(guild_id, custom_banner_url) VALUES (?, ?) "
@@ -56,6 +63,30 @@ class SetBannerModal(discord.ui.Modal, title="🖼️ Set Server Banner"):
56
  class BannerManager(commands.Cog):
57
  def __init__(self, bot: commands.Bot) -> None:
58
  self.bot = bot
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  @app_commands.command(name="set_banner", description="Set a custom banner for this server")
61
  @app_commands.checks.has_permissions(manage_guild=True)
@@ -65,6 +96,7 @@ class BannerManager(commands.Cog):
65
  await interaction.response.send_message("❌ This command can only be used in a server.", ephemeral=True)
66
  return
67
 
 
68
  await interaction.response.send_modal(SetBannerModal(self))
69
 
70
  @app_commands.command(name="remove_banner", description="Remove the custom banner from this server")
@@ -75,6 +107,7 @@ class BannerManager(commands.Cog):
75
  await interaction.response.send_message("❌ This command can only be used in a server.", ephemeral=True)
76
  return
77
 
 
78
  guild_id = interaction.guild.id
79
 
80
  await self.bot.db.execute(
@@ -97,6 +130,7 @@ class BannerManager(commands.Cog):
97
  await interaction.response.send_message("❌ This command can only be used in a server.", ephemeral=True)
98
  return
99
 
 
100
  guild_id = interaction.guild.id
101
 
102
  row = await self.bot.db.fetchone(
 
4
 
5
  from __future__ import annotations
6
 
7
+ import sqlite3
8
+ from urllib.parse import urlparse
9
+
10
  import discord
11
  from discord.ext import commands
12
  from discord import app_commands
 
29
  self.cog = cog
30
 
31
  async def on_submit(self, interaction: discord.Interaction) -> None:
32
+ url = self.banner_url.value.strip().strip("<>")
33
 
34
  # Validate URL
35
+ if not self.cog._looks_like_image_url(url):
36
+ await interaction.response.send_message(
37
+ "Invalid image URL. Use a public http(s) link ending with png/jpg/jpeg/webp/gif.",
38
+ ephemeral=True,
39
+ )
40
  return
41
 
42
  guild_id = interaction.guild.id if interaction.guild else 0
43
+
44
+ await self.cog._ensure_schema()
45
  # Save to database
46
  await self.cog.bot.db.execute(
47
  "INSERT INTO guild_config(guild_id, custom_banner_url) VALUES (?, ?) "
 
63
  class BannerManager(commands.Cog):
64
  def __init__(self, bot: commands.Bot) -> None:
65
  self.bot = bot
66
+ self._schema_checked = False
67
+
68
+ async def _ensure_schema(self) -> None:
69
+ if self._schema_checked:
70
+ return
71
+ try:
72
+ await self.bot.db.execute(
73
+ "ALTER TABLE guild_config ADD COLUMN custom_banner_url TEXT"
74
+ )
75
+ except sqlite3.OperationalError as exc:
76
+ if "duplicate column name" not in str(exc).lower():
77
+ raise
78
+ self._schema_checked = True
79
+
80
+ @staticmethod
81
+ def _looks_like_image_url(url: str) -> bool:
82
+ try:
83
+ parsed = urlparse(url)
84
+ except Exception:
85
+ return False
86
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
87
+ return False
88
+ path = (parsed.path or "").lower()
89
+ return path.endswith((".png", ".jpg", ".jpeg", ".webp", ".gif"))
90
 
91
  @app_commands.command(name="set_banner", description="Set a custom banner for this server")
92
  @app_commands.checks.has_permissions(manage_guild=True)
 
96
  await interaction.response.send_message("❌ This command can only be used in a server.", ephemeral=True)
97
  return
98
 
99
+ await self._ensure_schema()
100
  await interaction.response.send_modal(SetBannerModal(self))
101
 
102
  @app_commands.command(name="remove_banner", description="Remove the custom banner from this server")
 
107
  await interaction.response.send_message("❌ This command can only be used in a server.", ephemeral=True)
108
  return
109
 
110
+ await self._ensure_schema()
111
  guild_id = interaction.guild.id
112
 
113
  await self.bot.db.execute(
 
130
  await interaction.response.send_message("❌ This command can only be used in a server.", ephemeral=True)
131
  return
132
 
133
+ await self._ensure_schema()
134
  guild_id = interaction.guild.id
135
 
136
  row = await self.bot.db.fetchone(
bot/cogs/board_games.py CHANGED
@@ -808,7 +808,7 @@ class BoardGames(commands.Cog):
808
 
809
  @commands.hybrid_command(name="games_panel", hidden=True, description="Deprecated: use /gamehub", with_app_command=False)
810
  async def games_panel(self, ctx: commands.Context) -> None:
811
- await ctx.reply("🟢 This panel is deprecated. Use `/gamehub` for the improved game experience.")
812
 
813
  @commands.hybrid_command(name="board_forfeit", hidden=True, description="Forfeit active board game", with_app_command=False)
814
  async def board_forfeit(self, ctx: commands.Context) -> None:
 
808
 
809
  @commands.hybrid_command(name="games_panel", hidden=True, description="Deprecated: use /gamehub", with_app_command=False)
810
  async def games_panel(self, ctx: commands.Context) -> None:
811
+ await ctx.reply("<:animatedarrowgreen:1477261279428087979> This panel is deprecated. Use `/gamehub` for the improved game experience.")
812
 
813
  @commands.hybrid_command(name="board_forfeit", hidden=True, description="Forfeit active board game", with_app_command=False)
814
  async def board_forfeit(self, ctx: commands.Context) -> None:
bot/cogs/community.py CHANGED
@@ -474,7 +474,7 @@ class QuickOptionsView(discord.ui.View):
474
 
475
  @discord.ui.button(label="Priority", style=discord.ButtonStyle.primary, row=2)
476
  async def priority(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
477
- self.parent.options = ["🔴 High", "🟡 Medium", "🟢 Low"]
478
  await self._apply_options(interaction)
479
 
480
  async def _apply_options(self, interaction: discord.Interaction) -> None:
@@ -933,9 +933,9 @@ class Community(commands.Cog):
933
  await msg.edit(embed=embed, view=None)
934
  if winner_id:
935
  await channel.send(f"🎉 Giveaway **#{giveaway_id}** winner: <@{winner_id}> • Prize: **{prize}**")
936
- log_channel_id = await self.bot.db.fetchone("SELECT log_channel_id FROM guild_config WHERE guild_id = ?", ctx.guild.id)
937
  if log_channel_id and log_channel_id[0]:
938
- log_channel = ctx.guild.get_channel(log_channel_id[0])
939
  if log_channel:
940
  log_embed = discord.Embed(
941
  title="🎁 Giveaway Ended",
@@ -946,8 +946,6 @@ class Community(commands.Cog):
946
  log_embed.add_field(name="Prize", value=prize, inline=True)
947
  log_embed.add_field(name="Giveaway ID", value=str(giveaway_id), inline=True)
948
  await log_channel.send(embed=log_embed)
949
- else:
950
- await ctx.reply(f"Winner: <@{winner_id}>")
951
 
952
  @commands.hybrid_command(name="setsuggestionchannel", description="Set suggestion channel")
953
  @commands.has_permissions(manage_guild=True)
@@ -1007,6 +1005,134 @@ class Community(commands.Cog):
1007
  )
1008
  await ctx.reply(embed=embed, view=view)
1009
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1010
 
1011
  # ═══════════════════════════════════════════════════════════════════════════════
1012
  # GLOBAL TICKETS + GIVEAWAYS
 
474
 
475
  @discord.ui.button(label="Priority", style=discord.ButtonStyle.primary, row=2)
476
  async def priority(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
477
+ self.parent.options = ["🔴 High", "<:animatedarrowyellow:1477261257592668271> Medium", "<:animatedarrowgreen:1477261279428087979> Low"]
478
  await self._apply_options(interaction)
479
 
480
  async def _apply_options(self, interaction: discord.Interaction) -> None:
 
933
  await msg.edit(embed=embed, view=None)
934
  if winner_id:
935
  await channel.send(f"🎉 Giveaway **#{giveaway_id}** winner: <@{winner_id}> • Prize: **{prize}**")
936
+ log_channel_id = await self.bot.db.fetchone("SELECT log_channel_id FROM guild_config WHERE guild_id = ?", guild.id)
937
  if log_channel_id and log_channel_id[0]:
938
+ log_channel = guild.get_channel(log_channel_id[0])
939
  if log_channel:
940
  log_embed = discord.Embed(
941
  title="🎁 Giveaway Ended",
 
946
  log_embed.add_field(name="Prize", value=prize, inline=True)
947
  log_embed.add_field(name="Giveaway ID", value=str(giveaway_id), inline=True)
948
  await log_channel.send(embed=log_embed)
 
 
949
 
950
  @commands.hybrid_command(name="setsuggestionchannel", description="Set suggestion channel")
951
  @commands.has_permissions(manage_guild=True)
 
1005
  )
1006
  await ctx.reply(embed=embed, view=view)
1007
 
1008
+ @commands.hybrid_group(name="giveaway", description="Giveaway commands")
1009
+ @commands.has_permissions(manage_guild=True)
1010
+ async def giveaway_group(self, ctx: commands.Context) -> None:
1011
+ if ctx.invoked_subcommand is None:
1012
+ await ctx.reply("Use: `/giveaway start`, `/giveaway end`, `/giveaway reroll`.")
1013
+
1014
+ async def giveaway_create(self, ctx_or_interaction: commands.Context | discord.Interaction, minutes: int, winner_count: int, *, prize: str) -> None:
1015
+ guild = ctx_or_interaction.guild
1016
+ channel = ctx_or_interaction.channel
1017
+ if not guild or not isinstance(channel, discord.TextChannel):
1018
+ if isinstance(ctx_or_interaction, commands.Context):
1019
+ await ctx_or_interaction.reply("Server text channel only.")
1020
+ else:
1021
+ await ctx_or_interaction.followup.send("Server text channel only.", ephemeral=True)
1022
+ return
1023
+ end_time = int(time.time()) + max(1, minutes) * 60
1024
+ view = GiveawayJoinView(self, guild.id)
1025
+ view.winners_count = max(1, winner_count)
1026
+ embed = view.build_embed(prize=prize, end_time=end_time, winners=view.winners_count, entrants=0, ended=False)
1027
+ msg = await channel.send(embed=embed, view=view)
1028
+ await self.bot.db.execute(
1029
+ "INSERT INTO giveaways(guild_id, channel_id, message_id, prize, end_time, winner_id, ended) VALUES (?, ?, ?, ?, ?, NULL, 0)",
1030
+ guild.id,
1031
+ channel.id,
1032
+ msg.id,
1033
+ prize[:250],
1034
+ end_time,
1035
+ )
1036
+ row = await self.bot.db.fetchone("SELECT id FROM giveaways WHERE message_id = ?", msg.id)
1037
+ view.giveaway_id = int(row[0]) if row else 0
1038
+ if view.giveaway_id:
1039
+ await msg.edit(embed=view.build_embed(prize=prize, end_time=end_time, winners=view.winners_count, entrants=0, ended=False), view=view)
1040
+ response = f"✅ Giveaway created (ID: `{view.giveaway_id}`) • ends <t:{end_time}:R>"
1041
+ if isinstance(ctx_or_interaction, commands.Context):
1042
+ await ctx_or_interaction.reply(response)
1043
+ else:
1044
+ await ctx_or_interaction.followup.send(response, ephemeral=True)
1045
+
1046
+ async def giveaway_end(self, ctx_or_interaction: commands.Context | discord.Interaction, giveaway_id: int) -> None:
1047
+ row = await self.bot.db.fetchone(
1048
+ "SELECT guild_id, channel_id, message_id, prize, ended FROM giveaways WHERE id = ?",
1049
+ giveaway_id,
1050
+ )
1051
+ if not row:
1052
+ msg = "Giveaway not found."
1053
+ if isinstance(ctx_or_interaction, commands.Context):
1054
+ await ctx_or_interaction.reply(msg)
1055
+ else:
1056
+ await ctx_or_interaction.followup.send(msg, ephemeral=True)
1057
+ return
1058
+ guild_id, channel_id, message_id, prize, ended = row
1059
+ if int(ended or 0) == 1:
1060
+ msg = "Giveaway already ended."
1061
+ if isinstance(ctx_or_interaction, commands.Context):
1062
+ await ctx_or_interaction.reply(msg)
1063
+ else:
1064
+ await ctx_or_interaction.followup.send(msg, ephemeral=True)
1065
+ return
1066
+ entries = await self.bot.db.fetchall("SELECT user_id FROM giveaway_entries WHERE giveaway_id = ?", giveaway_id)
1067
+ users = [uid for (uid,) in entries]
1068
+ winner_id = random.choice(users) if users else None
1069
+ await self.bot.db.execute("UPDATE giveaways SET ended = 1, winner_id = ? WHERE id = ?", winner_id, giveaway_id)
1070
+ guild = self.bot.get_guild(int(guild_id))
1071
+ channel = guild.get_channel(int(channel_id)) if guild else None
1072
+ if isinstance(channel, discord.TextChannel):
1073
+ try:
1074
+ msg_obj = await channel.fetch_message(int(message_id))
1075
+ embed = msg_obj.embeds[0] if msg_obj.embeds else discord.Embed(title="🎁 Giveaway")
1076
+ embed.color = NEON_ORANGE
1077
+ embed.add_field(name="Winner", value=(f"<@{winner_id}>" if winner_id else "No entrants"), inline=False)
1078
+ await msg_obj.edit(embed=embed, view=None)
1079
+ except Exception:
1080
+ pass
1081
+ if winner_id:
1082
+ await channel.send(f"🎉 Giveaway **#{giveaway_id}** winner: <@{winner_id}> • Prize: **{prize}**")
1083
+ else:
1084
+ await channel.send(f"🎉 Giveaway **#{giveaway_id}** ended with no entrants.")
1085
+ reply = f"✅ Giveaway `{giveaway_id}` ended."
1086
+ if isinstance(ctx_or_interaction, commands.Context):
1087
+ await ctx_or_interaction.reply(reply)
1088
+ else:
1089
+ await ctx_or_interaction.followup.send(reply, ephemeral=True)
1090
+
1091
+ @giveaway_group.command(name="start", description="Start a giveaway in current channel")
1092
+ async def giveaway_start(self, ctx: commands.Context, minutes: int, winners: int, *, prize: str) -> None:
1093
+ await self.giveaway_create(ctx, minutes, winners, prize=prize)
1094
+
1095
+ @giveaway_group.command(name="end", description="End a giveaway by ID")
1096
+ async def giveaway_end_cmd(self, ctx: commands.Context, giveaway_id: int) -> None:
1097
+ await self.giveaway_end(ctx, giveaway_id)
1098
+
1099
+ @giveaway_group.command(name="reroll", description="Reroll winner for ended giveaway")
1100
+ async def giveaway_reroll(self, ctx: commands.Context, giveaway_id: int) -> None:
1101
+ row = await self.bot.db.fetchone("SELECT ended FROM giveaways WHERE id = ?", giveaway_id)
1102
+ if not row or int(row[0] or 0) != 1:
1103
+ await ctx.reply("Giveaway must be ended before reroll.")
1104
+ return
1105
+ await self.bot.db.execute("UPDATE giveaways SET ended = 0, winner_id = NULL WHERE id = ?", giveaway_id)
1106
+ await self.giveaway_end(ctx, giveaway_id)
1107
+
1108
+ @commands.hybrid_group(name="ticket", description="Ticket commands")
1109
+ async def ticket_group(self, ctx: commands.Context) -> None:
1110
+ if ctx.invoked_subcommand is None:
1111
+ await ctx.reply("Use: `/ticket close` or `/ticket delete` in a ticket channel.")
1112
+
1113
+ @ticket_group.command(name="close", description="Close current ticket channel")
1114
+ async def ticket_close_cmd(self, ctx: commands.Context) -> None:
1115
+ channel = ctx.channel
1116
+ if not isinstance(channel, discord.TextChannel) or not channel.name.startswith("ticket-"):
1117
+ await ctx.reply("This is not a ticket channel.")
1118
+ return
1119
+ await self.bot.db.execute("UPDATE tickets SET status = 'closed' WHERE channel_id = ?", channel.id)
1120
+ await ctx.reply("🔒 Closing ticket in 5 seconds...")
1121
+ await asyncio.sleep(5)
1122
+ await channel.delete(reason=f"Closed by {ctx.author}")
1123
+
1124
+ @ticket_group.command(name="delete", description="Delete current ticket channel")
1125
+ @commands.has_permissions(manage_channels=True)
1126
+ async def ticket_delete_cmd(self, ctx: commands.Context) -> None:
1127
+ channel = ctx.channel
1128
+ if not isinstance(channel, discord.TextChannel) or not channel.name.startswith("ticket-"):
1129
+ await ctx.reply("This is not a ticket channel.")
1130
+ return
1131
+ await self.bot.db.execute("DELETE FROM tickets WHERE channel_id = ?", channel.id)
1132
+ await ctx.reply("🗑️ Deleting ticket in 3 seconds...")
1133
+ await asyncio.sleep(3)
1134
+ await channel.delete(reason=f"Deleted by {ctx.author}")
1135
+
1136
 
1137
  # ═══════════════════════════════════════════════════════════════════════════════
1138
  # GLOBAL TICKETS + GIVEAWAYS
bot/cogs/configuration.py CHANGED
@@ -273,15 +273,15 @@ class FreeGameSetupView(discord.ui.View):
273
  min_values=1,
274
  max_values=3,
275
  options=[
276
- discord.SelectOption(label="Epic Games", value="epic", emoji="🟢"),
277
- discord.SelectOption(label="Steam", value="steam", emoji="🟢"),
278
- discord.SelectOption(label="GOG", value="gog", emoji="🟢"),
279
  ],
280
  )
281
  async def stores(self, interaction: discord.Interaction, select: discord.ui.Select) -> None:
282
  self.platforms = set(select.values)
283
  await self._save(interaction)
284
- await interaction.response.send_message("🟢 Free games stores updated.", ephemeral=True)
285
 
286
  @discord.ui.select(
287
  placeholder="Choose ping role (optional)",
@@ -620,7 +620,7 @@ class Configuration(commands.Cog):
620
  )
621
  view = FreeGameSetupView(self, ctx.guild.id, channel.id, role.id if role else None)
622
  embed = discord.Embed(
623
- title="🟢 Free Games Setup",
624
  description=(
625
  f"Channel: {channel.mention}\n"
626
  f"Role ping: {role.mention if role else 'None'}\n"
 
273
  min_values=1,
274
  max_values=3,
275
  options=[
276
+ discord.SelectOption(label="Epic Games", value="epic", emoji="<:animatedarrowgreen:1477261279428087979>"),
277
+ discord.SelectOption(label="Steam", value="steam", emoji="<:animatedarrowgreen:1477261279428087979>"),
278
+ discord.SelectOption(label="GOG", value="gog", emoji="<:animatedarrowgreen:1477261279428087979>"),
279
  ],
280
  )
281
  async def stores(self, interaction: discord.Interaction, select: discord.ui.Select) -> None:
282
  self.platforms = set(select.values)
283
  await self._save(interaction)
284
+ await interaction.response.send_message("<:animatedarrowgreen:1477261279428087979> Free games stores updated.", ephemeral=True)
285
 
286
  @discord.ui.select(
287
  placeholder="Choose ping role (optional)",
 
620
  )
621
  view = FreeGameSetupView(self, ctx.guild.id, channel.id, role.id if role else None)
622
  embed = discord.Embed(
623
+ title="<:animatedarrowgreen:1477261279428087979> Free Games Setup",
624
  description=(
625
  f"Channel: {channel.mention}\n"
626
  f"Role ping: {role.mention if role else 'None'}\n"
bot/cogs/engagement.py CHANGED
@@ -15,7 +15,7 @@ from bot.theme import (
15
  level_display, economy_embed, success_embed, error_embed, info_embed,
16
  leaderboard_embed, profile_embed, gaming_embed, tournament_embed,
17
  format_leaderboard, money_display, timestamp_display, double_line, triple_line,
18
- pick_neon_color, shimmer
19
  )
20
  from bot.emojis import (
21
  ui, E_DIAMOND, E_STAR, E_CATJAM, E_ARROW_BLUE, E_ARROW_GREEN,
@@ -189,8 +189,27 @@ class EconomyPanelView(discord.ui.View):
189
  if not interaction.guild:
190
  await interaction.response.send_message("Server only.", ephemeral=True)
191
  return
192
- guild_id = interaction.guild.id
193
- await interaction.response.send_modal(GambleModal(self.cog, guild_id, interaction.user.id))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
  @discord.ui.button(label="Rob", emoji=ui("gun"), style=discord.ButtonStyle.danger, custom_id="economy:rob", row=1)
196
  async def rob_btn(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
@@ -224,7 +243,15 @@ class EconomyPanelView(discord.ui.View):
224
  guild_id,
225
  )
226
  if not rows:
227
- await interaction.followup.send("No economy data yet.", ephemeral=True)
 
 
 
 
 
 
 
 
228
  return
229
  lines = []
230
  for rank, (uid, total) in enumerate(rows, 1):
@@ -461,9 +488,9 @@ class GambleModal(discord.ui.Modal, title="🎲 Gamble Coins"):
461
 
462
  class RobModal(discord.ui.Modal, title="🔫 Rob a User"):
463
  target = discord.ui.TextInput(
464
- label="User ID or Mention",
465
- placeholder="123456789 or @user",
466
- required=True,
467
  max_length=64,
468
  )
469
 
@@ -475,11 +502,23 @@ class RobModal(discord.ui.Modal, title="🔫 Rob a User"):
475
 
476
  async def on_submit(self, interaction: discord.Interaction) -> None:
477
  await interaction.response.defer(ephemeral=True)
 
478
  val = self.target.value.strip().replace("<", "").replace(">", "").replace("@", "").replace("!", "")
479
- try:
480
- tid = int(val)
481
- except ValueError:
482
- await interaction.followup.send("Invalid user ID.", ephemeral=True)
 
 
 
 
 
 
 
 
 
 
 
483
  return
484
  if tid == self.user_id:
485
  await interaction.followup.send("You can't rob yourself!", ephemeral=True)
@@ -721,9 +760,19 @@ class Engagement(commands.Cog):
721
  )
722
  if not row:
723
  if lang == "ar":
724
- await ctx.reply("📊 لا توجد بيانات مستوى بعد. ابدأ الدردشة لكسب XP!")
 
 
 
 
 
725
  else:
726
- await ctx.reply("📊 No rank data yet. Start chatting to earn XP!")
 
 
 
 
 
727
  return
728
 
729
  xp, level = row
@@ -766,9 +815,19 @@ class Engagement(commands.Cog):
766
  )
767
  if not rows:
768
  if lang == "ar":
769
- await ctx.reply("📊 لا توجد بيانات بعد. ابدأ الدردشة لكسب XP!")
 
 
 
 
 
770
  else:
771
- await ctx.reply("📊 No data yet. Start chatting to earn XP!")
 
 
 
 
 
772
  return
773
 
774
  lines = []
@@ -969,10 +1028,20 @@ class Engagement(commands.Cog):
969
  await ctx.reply(embed=embed)
970
 
971
  @commands.hybrid_command(name="rob", description="Attempt to rob another user (risky!)", hidden=True, with_app_command=False)
972
- async def rob(self, ctx: commands.Context, target: discord.Member) -> None:
973
  guild_id = ctx.guild.id
974
  lang = await self.bot.get_guild_language(guild_id)
975
-
 
 
 
 
 
 
 
 
 
 
976
  if target.id == ctx.author.id or target.bot:
977
  if lang == "ar":
978
  await ctx.reply("❌ هدف غير صالح.")
@@ -1082,7 +1151,7 @@ class Engagement(commands.Cog):
1082
  await self.gamble(ctx, bet)
1083
 
1084
  @economy.command(name="rob", description="Attempt to rob another user")
1085
- async def economy_rob(self, ctx: commands.Context, target: discord.Member) -> None:
1086
  await self.rob(ctx, target)
1087
 
1088
  @commands.hybrid_command(name="profile")
@@ -1486,9 +1555,19 @@ class Engagement(commands.Cog):
1486
  )
1487
  if not row:
1488
  if lang == "ar":
1489
- await ctx.reply("❌ البطولة غير موجودة.")
 
 
 
 
 
1490
  else:
1491
- await ctx.reply("❌ Tournament not found.")
 
 
 
 
 
1492
  return
1493
 
1494
  _, status, winner_id = row
@@ -1542,9 +1621,19 @@ class Engagement(commands.Cog):
1542
  )
1543
  if not rows:
1544
  if lang == "ar":
1545
- await ctx.reply("❌ لا توجد ألعاب محددة لهذه البطولة.")
 
 
 
 
 
1546
  else:
1547
- await ctx.reply("❌ No games set for this tournament.")
 
 
 
 
 
1548
  return
1549
 
1550
  games = "🎮 " + " • ".join(game for (game,) in rows)
@@ -1579,9 +1668,19 @@ class Engagement(commands.Cog):
1579
  )
1580
  if not row:
1581
  if lang == "ar":
1582
- await ctx.reply("❌ البطولة غير موجودة.")
 
 
 
 
 
1583
  else:
1584
- await ctx.reply("❌ Tournament not found.")
 
 
 
 
 
1585
  return
1586
 
1587
  game_rows = await self.bot.db.fetchall(
@@ -1635,7 +1734,20 @@ class Engagement(commands.Cog):
1635
  name,
1636
  )
1637
  if not row:
1638
- await ctx.reply("❌ Tournament not found." if lang != "ar" else "❌ البطولة غير موجودة.")
 
 
 
 
 
 
 
 
 
 
 
 
 
1639
  return
1640
  if lang == "ar":
1641
  msg = (
@@ -1662,9 +1774,19 @@ class Engagement(commands.Cog):
1662
  )
1663
  if not rows:
1664
  if lang == "ar":
1665
- await ctx.reply("🏆 لا يوجد فائزون بالبطولات بعد.")
 
 
 
 
 
1666
  else:
1667
- await ctx.reply("🏆 No tournament winners yet.")
 
 
 
 
 
1668
  return
1669
 
1670
  lines = []
 
15
  level_display, economy_embed, success_embed, error_embed, info_embed,
16
  leaderboard_embed, profile_embed, gaming_embed, tournament_embed,
17
  format_leaderboard, money_display, timestamp_display, double_line, triple_line,
18
+ pick_neon_color, shimmer, idle_embed_for_guild
19
  )
20
  from bot.emojis import (
21
  ui, E_DIAMOND, E_STAR, E_CATJAM, E_ARROW_BLUE, E_ARROW_GREEN,
 
189
  if not interaction.guild:
190
  await interaction.response.send_message("Server only.", ephemeral=True)
191
  return
192
+ gambling_cog = interaction.client.get_cog("Gambling")
193
+ if gambling_cog is None:
194
+ guild_id = interaction.guild.id
195
+ await interaction.response.send_modal(GambleModal(self.cog, guild_id, interaction.user.id))
196
+ return
197
+ from bot.cogs.gambling import GamblingPanelView
198
+ embed = discord.Embed(
199
+ title="Casino & Gambling Panel",
200
+ description=(
201
+ "Choose a game:\n\n"
202
+ "Blackjack\n"
203
+ "Roulette\n"
204
+ "RPG Adventure"
205
+ ),
206
+ color=NEON_ORANGE,
207
+ )
208
+ await interaction.response.send_message(
209
+ embed=embed,
210
+ view=GamblingPanelView(gambling_cog, interaction.guild.id, interaction.user.id),
211
+ ephemeral=True,
212
+ )
213
 
214
  @discord.ui.button(label="Rob", emoji=ui("gun"), style=discord.ButtonStyle.danger, custom_id="economy:rob", row=1)
215
  async def rob_btn(self, interaction: discord.Interaction, _: discord.ui.Button) -> None:
 
243
  guild_id,
244
  )
245
  if not rows:
246
+ await interaction.followup.send(
247
+ embed=await idle_embed_for_guild(
248
+ "Economy Leaderboard Idle",
249
+ "No economy data is available yet.",
250
+ guild=interaction.guild,
251
+ bot=self.cog.bot,
252
+ ),
253
+ ephemeral=True,
254
+ )
255
  return
256
  lines = []
257
  for rank, (uid, total) in enumerate(rows, 1):
 
488
 
489
  class RobModal(discord.ui.Modal, title="🔫 Rob a User"):
490
  target = discord.ui.TextInput(
491
+ label="User ID or Mention (optional)",
492
+ placeholder="Leave empty for random target",
493
+ required=False,
494
  max_length=64,
495
  )
496
 
 
502
 
503
  async def on_submit(self, interaction: discord.Interaction) -> None:
504
  await interaction.response.defer(ephemeral=True)
505
+ tid: int | None = None
506
  val = self.target.value.strip().replace("<", "").replace(">", "").replace("@", "").replace("!", "")
507
+ if val:
508
+ try:
509
+ tid = int(val)
510
+ except ValueError:
511
+ await interaction.followup.send("Invalid user ID.", ephemeral=True)
512
+ return
513
+ elif interaction.guild:
514
+ eligible = [
515
+ m for m in interaction.guild.members
516
+ if (not m.bot) and m.id != self.user_id and m.guild_permissions.send_messages
517
+ ]
518
+ if eligible:
519
+ tid = random.choice(eligible).id
520
+ if tid is None:
521
+ await interaction.followup.send("No valid robbery target found.", ephemeral=True)
522
  return
523
  if tid == self.user_id:
524
  await interaction.followup.send("You can't rob yourself!", ephemeral=True)
 
760
  )
761
  if not row:
762
  if lang == "ar":
763
+ await ctx.reply(embed=await idle_embed_for_guild(
764
+ "حالة الرتبة الخاملة",
765
+ "لا توجد بيانات مستوى بعد. ابدأ الدردشة لكسب XP!",
766
+ guild=ctx.guild,
767
+ bot=self.bot,
768
+ ))
769
  else:
770
+ await ctx.reply(embed=await idle_embed_for_guild(
771
+ "Rank Idle",
772
+ "No rank data yet. Start chatting to earn XP!",
773
+ guild=ctx.guild,
774
+ bot=self.bot,
775
+ ))
776
  return
777
 
778
  xp, level = row
 
815
  )
816
  if not rows:
817
  if lang == "ar":
818
+ await ctx.reply(embed=await idle_embed_for_guild(
819
+ "اللوحة الخاملة",
820
+ "لا توجد بيانات بعد. ابدأ الدردشة لكسب XP!",
821
+ guild=ctx.guild,
822
+ bot=self.bot,
823
+ ))
824
  else:
825
+ await ctx.reply(embed=await idle_embed_for_guild(
826
+ "Leaderboard Idle",
827
+ "No data yet. Start chatting to earn XP!",
828
+ guild=ctx.guild,
829
+ bot=self.bot,
830
+ ))
831
  return
832
 
833
  lines = []
 
1028
  await ctx.reply(embed=embed)
1029
 
1030
  @commands.hybrid_command(name="rob", description="Attempt to rob another user (risky!)", hidden=True, with_app_command=False)
1031
+ async def rob(self, ctx: commands.Context, target: discord.Member | None = None) -> None:
1032
  guild_id = ctx.guild.id
1033
  lang = await self.bot.get_guild_language(guild_id)
1034
+
1035
+ if target is None:
1036
+ eligible = [
1037
+ member for member in ctx.guild.members
1038
+ if (not member.bot) and member.id != ctx.author.id and member.guild_permissions.send_messages
1039
+ ]
1040
+ target = random.choice(eligible) if eligible else None
1041
+ if target is None:
1042
+ await ctx.reply("❌ No valid robbery targets found.")
1043
+ return
1044
+
1045
  if target.id == ctx.author.id or target.bot:
1046
  if lang == "ar":
1047
  await ctx.reply("❌ هدف غير صالح.")
 
1151
  await self.gamble(ctx, bet)
1152
 
1153
  @economy.command(name="rob", description="Attempt to rob another user")
1154
+ async def economy_rob(self, ctx: commands.Context, target: discord.Member | None = None) -> None:
1155
  await self.rob(ctx, target)
1156
 
1157
  @commands.hybrid_command(name="profile")
 
1555
  )
1556
  if not row:
1557
  if lang == "ar":
1558
+ await ctx.reply(embed=await idle_embed_for_guild(
1559
+ "البطولة غير متاحة",
1560
+ "لا توجد بطولة بهذا الاسم حالياً.",
1561
+ guild=ctx.guild,
1562
+ bot=self.bot,
1563
+ ))
1564
  else:
1565
+ await ctx.reply(embed=await idle_embed_for_guild(
1566
+ "Tournament Not Found",
1567
+ "No tournament exists with that name right now.",
1568
+ guild=ctx.guild,
1569
+ bot=self.bot,
1570
+ ))
1571
  return
1572
 
1573
  _, status, winner_id = row
 
1621
  )
1622
  if not rows:
1623
  if lang == "ar":
1624
+ await ctx.reply(embed=await idle_embed_for_guild(
1625
+ "قائمة الألعاب فارغة",
1626
+ "لم يتم إعداد ألعاب لهذه البطولة بعد.",
1627
+ guild=ctx.guild,
1628
+ bot=self.bot,
1629
+ ))
1630
  else:
1631
+ await ctx.reply(embed=await idle_embed_for_guild(
1632
+ "Tournament Games Idle",
1633
+ "No games are configured for this tournament yet.",
1634
+ guild=ctx.guild,
1635
+ bot=self.bot,
1636
+ ))
1637
  return
1638
 
1639
  games = "🎮 " + " • ".join(game for (game,) in rows)
 
1668
  )
1669
  if not row:
1670
  if lang == "ar":
1671
+ await ctx.reply(embed=await idle_embed_for_guild(
1672
+ "البطولة غير متاحة",
1673
+ "لا توجد بطولة بهذا الاسم حالياً.",
1674
+ guild=ctx.guild,
1675
+ bot=self.bot,
1676
+ ))
1677
  else:
1678
+ await ctx.reply(embed=await idle_embed_for_guild(
1679
+ "Tournament Not Found",
1680
+ "No tournament exists with that name right now.",
1681
+ guild=ctx.guild,
1682
+ bot=self.bot,
1683
+ ))
1684
  return
1685
 
1686
  game_rows = await self.bot.db.fetchall(
 
1734
  name,
1735
  )
1736
  if not row:
1737
+ if lang == "ar":
1738
+ await ctx.reply(embed=await idle_embed_for_guild(
1739
+ "البطولة غير متاحة",
1740
+ "لا توجد بطولة بهذا الاسم حالياً.",
1741
+ guild=ctx.guild,
1742
+ bot=self.bot,
1743
+ ))
1744
+ else:
1745
+ await ctx.reply(embed=await idle_embed_for_guild(
1746
+ "Tournament Not Found",
1747
+ "No tournament exists with that name right now.",
1748
+ guild=ctx.guild,
1749
+ bot=self.bot,
1750
+ ))
1751
  return
1752
  if lang == "ar":
1753
  msg = (
 
1774
  )
1775
  if not rows:
1776
  if lang == "ar":
1777
+ await ctx.reply(embed=await idle_embed_for_guild(
1778
+ "لوحة البطولات الخاملة",
1779
+ "لا يوجد فائزون مسجلون حتى الآن.",
1780
+ guild=ctx.guild,
1781
+ bot=self.bot,
1782
+ ))
1783
  else:
1784
+ await ctx.reply(embed=await idle_embed_for_guild(
1785
+ "Tournament Leaderboard Idle",
1786
+ "No tournament winners are recorded yet.",
1787
+ guild=ctx.guild,
1788
+ bot=self.bot,
1789
+ ))
1790
  return
1791
 
1792
  lines = []
bot/cogs/events.py CHANGED
@@ -4,6 +4,7 @@ import asyncio
4
  import base64
5
  import datetime as dt
6
  import hashlib
 
7
  import json
8
  import os
9
  import re
@@ -28,15 +29,23 @@ try:
28
  except Exception: # pragma: no cover
29
  genai = None
30
 
 
 
 
 
 
 
 
31
  BAD_WORDS = {"badword1", "badword2", "badword3"}
32
  SCAM_LINK_RE = re.compile(r"(https?://\S+|discord\.gg/\S+|t\.me/\S+)", re.IGNORECASE)
33
  SCAM_SYSTEM_PROMPT = """You are a strict Discord anti-scam classifier specialized in detecting:
34
  1. Low-quality scam images (blurry, pixelated, poor compression)
35
  2. Fake giveaways and fraudulent offers
36
- 3. Phishing attempts and social engineering
37
- 4. Misleading or deceptive content
 
38
 
39
- Analyze the image quality, text content, and context carefully.
40
  Reply ONLY TRUE (if scam) or FALSE (if safe)."""
41
 
42
  SCAM_CALL_TO_ACTION_RE = re.compile(
@@ -45,7 +54,7 @@ SCAM_CALL_TO_ACTION_RE = re.compile(
45
  re.IGNORECASE,
46
  )
47
  SCAM_KEYWORDS_RE = re.compile(
48
- r"\b(free money|free nitro|crypto|airdrop|giveaway|gift card|usdt|btc|eth|investment|"
49
  r"ربح مجاني|نيترو مجاني|كريبتو|هدية|هدايا|قسيمة|استثمار|محفظة)\b",
50
  re.IGNORECASE,
51
  )
@@ -64,6 +73,21 @@ BENIGN_DM_RE = re.compile(
64
  r"راسلني إذا|راسلني للمساعدة|خاص للمساعدة)\b",
65
  re.IGNORECASE,
66
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
 
69
 
@@ -72,6 +96,8 @@ class Events(commands.Cog):
72
  self.bot = bot
73
  self._scam_scan_semaphore = asyncio.Semaphore(4)
74
  self._recent_user_messages: dict[tuple[int, int], deque[tuple[float, str]]] = defaultdict(lambda: deque(maxlen=8))
 
 
75
  self._gemini_key = (os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY") or "").strip()
76
  if self._gemini_key and genai is not None:
77
  try:
@@ -197,6 +223,149 @@ class Events(commands.Cog):
197
  reasons.append("benign dm-context detected")
198
  return (score, reasons)
199
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  async def _log_image_scan(self, message: discord.Message, result: str, detail: str) -> None:
201
  try:
202
  await self.send_log(
@@ -208,17 +377,30 @@ class Events(commands.Cog):
208
  except Exception:
209
  return
210
 
211
- async def _image_bytes_from_text_urls(self, content: str) -> list[bytes]:
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  if aiohttp is None:
213
  return []
214
  urls = re.findall(r"https?://\S+", content or "")
215
  image_urls = [u.rstrip(").,!?") for u in urls if re.search(r"\.(png|jpg|jpeg|webp|gif)(\?|$)", u, re.IGNORECASE)]
216
  if not image_urls:
217
  return []
218
- out: list[bytes] = []
219
  timeout = aiohttp.ClientTimeout(total=8)
220
  async with aiohttp.ClientSession(timeout=timeout) as session:
221
- for url in image_urls[:2]:
222
  try:
223
  async with session.get(url) as resp:
224
  if resp.status >= 400:
@@ -227,12 +409,169 @@ class Events(commands.Cog):
227
  if "image/" not in ctype:
228
  continue
229
  data = await resp.read()
230
- if data:
231
- out.append(data[:2_000_000])
 
 
 
232
  except Exception:
233
  continue
234
  return out
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  async def _analyze_scam_with_openrouter(self, message: discord.Message) -> bool:
237
  if aiohttp is None or not self.bot.settings.openrouter_api_key:
238
  return False
@@ -242,7 +581,18 @@ class Events(commands.Cog):
242
  "meta-llama/llama-3.2-11b-vision-instruct:free",
243
  "nvidia/llama-3.1-nemotron-nano-vl-8b-v1:free",
244
  ]
245
- score, reasons = self._scam_heuristics(message.content or "", has_images=bool(message.attachments))
 
 
 
 
 
 
 
 
 
 
 
246
  base_text = (
247
  "You are an advanced scam detector for Discord specialized in identifying:\n"
248
  "1. LOW-QUALITY IMAGES: Blurry, pixelated, heavily compressed, or distorted images are HIGH RISK\n"
@@ -251,7 +601,9 @@ class Events(commands.Cog):
251
  "4. SOCIAL ENGINEERING: Impersonation, urgency tactics, pressure to act quickly\n\n"
252
  "IMPORTANT: Low image quality + suspicious content = ALMOST ALWAYS SCAM\n\n"
253
  f"Heuristic analysis: score={score}/10, reasons: {', '.join(reasons) if reasons else 'none'}.\n\n"
254
- f"Message content:\n{(message.content or '')[:2000]}"
 
 
255
  )
256
  content_parts: list[dict] = [{"type": "text", "text": base_text}]
257
  image_count = 0
@@ -265,6 +617,14 @@ class Events(commands.Cog):
265
  image_count += 1
266
  except Exception:
267
  continue
 
 
 
 
 
 
 
 
268
  if image_count == 0 and not message.content.strip():
269
  return False
270
 
@@ -302,11 +662,23 @@ class Events(commands.Cog):
302
  return False
303
  try:
304
  model = genai.GenerativeModel("gemini-1.5-flash")
 
 
 
 
 
 
 
 
 
 
305
  prompt = (
306
  "Classify if this Discord message is a scam/phishing attempt.\n"
307
  "Use context. Do NOT flag harmless 'dm me' alone.\n"
308
  "Reply with TRUE or FALSE only.\n\n"
309
- f"Message: {(message.content or '')[:2000]}"
 
 
310
  )
311
  parts: list = [prompt]
312
  for att in message.attachments[:3]:
@@ -790,6 +1162,7 @@ class Events(commands.Cog):
790
  if message.author.bot or not message.guild:
791
  return
792
  text_content = message.content or ""
 
793
  await self.send_log(
794
  message.guild,
795
  "💬 Message Sent",
@@ -801,6 +1174,23 @@ class Events(commands.Cog):
801
  bucket = self._recent_user_messages[key]
802
  bucket.append((now_ts, text_content.strip()))
803
  stitched = " ".join(chunk for ts, chunk in bucket if now_ts - ts <= 90 and chunk)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
804
 
805
  if stitched and self._is_high_risk_scam_text(stitched):
806
  violation = await self._openrouter_shield_violation(stitched)
@@ -811,6 +1201,8 @@ class Events(commands.Cog):
811
  if message.attachments:
812
  has_image = any((att.content_type or "").startswith("image/") for att in message.attachments)
813
  if has_image:
 
 
814
  for att in message.attachments:
815
  if not (att.content_type or "").startswith("image/"):
816
  continue
@@ -819,6 +1211,8 @@ class Events(commands.Cog):
819
  except Exception:
820
  continue
821
  digest = hashlib.sha256(raw).hexdigest()
 
 
822
  row = await self.bot.db.fetchone(
823
  "SELECT image_hash FROM scam_images WHERE guild_id = ? AND image_hash = ?",
824
  message.guild.id,
@@ -829,24 +1223,90 @@ class Events(commands.Cog):
829
  deleted = await message.channel.purge(limit=15, check=lambda m: m.author.id == message.author.id and m.attachments)
830
  await self.send_log(message.guild, "🧹 Scam Image Purge", f"Deleted {len(deleted)} image messages from {message.author.mention}")
831
  return
 
 
 
 
 
 
 
 
 
832
  is_image_scam = await self._analyze_scam_with_openrouter(message)
833
  if not is_image_scam:
834
  is_image_scam = await self._analyze_scam_with_gemini(message)
835
- score, reasons = self._scam_heuristics(message.content or "", has_images=True)
836
- detail = f"heuristic_score={score}; reasons={', '.join(reasons) if reasons else 'none'}; ai_scam={is_image_scam}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
837
  await self._log_image_scan(message, "SCAM" if is_image_scam else "SAFE", detail)
838
  if is_image_scam:
 
 
839
  await self._apply_ai_shield(message, "SCAM")
840
  return
841
  elif re.search(r"https?://\S+\.(png|jpg|jpeg|webp|gif)(\?|$)", message.content or "", re.IGNORECASE):
 
 
 
 
 
 
 
 
 
 
 
 
842
  proxy_message = message
 
 
 
 
 
 
 
 
 
843
  is_image_scam = await self._analyze_scam_with_openrouter(proxy_message)
844
  if not is_image_scam:
845
  is_image_scam = await self._analyze_scam_with_gemini(proxy_message)
846
- score, reasons = self._scam_heuristics(message.content or "", has_images=True)
847
- detail = f"url_image_scan=1; heuristic_score={score}; reasons={', '.join(reasons) if reasons else 'none'}; ai_scam={is_image_scam}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
848
  await self._log_image_scan(message, "SCAM" if is_image_scam else "SAFE", detail)
849
  if is_image_scam:
 
 
850
  await self._apply_ai_shield(message, "SCAM")
851
  return
852
  if self._is_high_risk_scam_text(text_content):
@@ -919,10 +1379,26 @@ class Events(commands.Cog):
919
  elif mention_type == "role" and role_id:
920
  mention = f"<@&{role_id}> "
921
  for item in fresh[:3]:
922
- embed = discord.Embed(title=item.get("title", ""), url=item.get("url", ""), color=NEON_CYAN)
 
 
 
 
 
 
 
 
 
923
  if item.get("image"):
924
  embed.set_image(url=item["image"])
925
- view = None
 
 
 
 
 
 
 
926
  await channel.send(content=mention or None, embed=embed, view=view)
927
  latest_ids = ",".join(item["id"] for item in items[:20])
928
  await self.bot.db.execute(
 
4
  import base64
5
  import datetime as dt
6
  import hashlib
7
+ import io
8
  import json
9
  import os
10
  import re
 
29
  except Exception: # pragma: no cover
30
  genai = None
31
 
32
+ try:
33
+ from PIL import Image, ImageFilter, ImageStat
34
+ except Exception: # pragma: no cover
35
+ Image = None
36
+ ImageFilter = None
37
+ ImageStat = None
38
+
39
  BAD_WORDS = {"badword1", "badword2", "badword3"}
40
  SCAM_LINK_RE = re.compile(r"(https?://\S+|discord\.gg/\S+|t\.me/\S+)", re.IGNORECASE)
41
  SCAM_SYSTEM_PROMPT = """You are a strict Discord anti-scam classifier specialized in detecting:
42
  1. Low-quality scam images (blurry, pixelated, poor compression)
43
  2. Fake giveaways and fraudulent offers
44
+ 3. Celebrity/creator impersonation scams (example: MrBeast giveaway, fake verified pages)
45
+ 4. Phishing attempts and social engineering
46
+ 5. Misleading/deceptive content (wallet verification, Telegram/WhatsApp contact, urgent claim flow)
47
 
48
+ Analyze image text, logos, badges, links/handles, and context carefully.
49
  Reply ONLY TRUE (if scam) or FALSE (if safe)."""
50
 
51
  SCAM_CALL_TO_ACTION_RE = re.compile(
 
54
  re.IGNORECASE,
55
  )
56
  SCAM_KEYWORDS_RE = re.compile(
57
+ r"\b(free money|free nitro|crypto|airdrop|giveaway|gift card|usdt|btc|eth|investment|mrbeast|mr beast|"
58
  r"ربح مجاني|نيترو مجاني|كريبتو|هدية|هدايا|قسيمة|استثمار|محفظة)\b",
59
  re.IGNORECASE,
60
  )
 
73
  r"راسلني إذا|راسلني للمساعدة|خاص للمساعدة)\b",
74
  re.IGNORECASE,
75
  )
76
+ CUSTOM_EMOJI_RE = re.compile(r"<a?:[A-Za-z0-9_~\-]{2,32}:\d{6,}>")
77
+ UNICODE_EMOJI_RE = re.compile(
78
+ r"[\U0001F1E6-\U0001F1FF]" # flags
79
+ r"|[\U0001F300-\U0001FAFF]" # symbols & pictographs
80
+ r"|[\u2600-\u27BF]" # dingbats/misc
81
+ )
82
+ SCAM_LURE_EMOJIS = {
83
+ "🎁", "💰", "💸", "🏆", "🚀", "🔥", "✅", "💎", "🔗", "🎉", "🪙", "📈", "💳", "💵", "🤑"
84
+ }
85
+ SCAM_PRESSURE_EMOJIS = {"⚠️", "⏰", "🚨", "❗", "‼️", "⌛"}
86
+ OCR_SCAM_RE = re.compile(
87
+ r"\b(mrbeast|mr beast|giveaway|airdrop|free|claim|winner|verify wallet|wallet|crypto|usdt|btc|eth|"
88
+ r"telegram|whatsapp|dm me|join now|limited time|urgent|bit\.ly|tinyurl)\b",
89
+ re.IGNORECASE,
90
+ )
91
 
92
 
93
 
 
96
  self.bot = bot
97
  self._scam_scan_semaphore = asyncio.Semaphore(4)
98
  self._recent_user_messages: dict[tuple[int, int], deque[tuple[float, str]]] = defaultdict(lambda: deque(maxlen=8))
99
+ self._recent_user_context: dict[tuple[int, int], deque[tuple[float, int, str, list[str]]]] = defaultdict(lambda: deque(maxlen=40))
100
+ self._last_context_ai_check: dict[tuple[int, int], float] = {}
101
  self._gemini_key = (os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY") or "").strip()
102
  if self._gemini_key and genai is not None:
103
  try:
 
223
  reasons.append("benign dm-context detected")
224
  return (score, reasons)
225
 
226
+ @staticmethod
227
+ def _extract_emojis(content: str) -> list[str]:
228
+ text = (content or "").strip()
229
+ if not text:
230
+ return []
231
+ custom = CUSTOM_EMOJI_RE.findall(text)
232
+ # Keep unicode emoji extraction approximate but fast.
233
+ unicode_hits = UNICODE_EMOJI_RE.findall(text)
234
+ return custom + unicode_hits
235
+
236
+ def _emoji_scam_heuristics(self, content: str, emojis: list[str]) -> tuple[int, list[str]]:
237
+ if not emojis:
238
+ return (0, [])
239
+ text = (content or "").strip().lower()
240
+ score = 0
241
+ reasons: list[str] = []
242
+
243
+ lure_count = sum(1 for e in emojis if e in SCAM_LURE_EMOJIS)
244
+ pressure_count = sum(1 for e in emojis if e in SCAM_PRESSURE_EMOJIS)
245
+ has_link = bool(SCAM_LINK_RE.search(text))
246
+ has_cta = bool(SCAM_CALL_TO_ACTION_RE.search(text))
247
+ has_keyword = bool(SCAM_KEYWORDS_RE.search(text))
248
+
249
+ if lure_count >= 2:
250
+ score += 2
251
+ reasons.append("multiple lure emojis")
252
+ if pressure_count >= 1:
253
+ score += 1
254
+ reasons.append("urgency/pressure emojis")
255
+ if len(emojis) >= 6:
256
+ score += 1
257
+ reasons.append("emoji-heavy promotional style")
258
+ if has_link and (lure_count >= 1 or pressure_count >= 1):
259
+ score += 2
260
+ reasons.append("link + lure/pressure emojis")
261
+ if has_cta and (lure_count >= 1 or has_keyword):
262
+ score += 2
263
+ reasons.append("cta + emoji bait")
264
+ if has_keyword and lure_count >= 1:
265
+ score += 2
266
+ reasons.append("financial/scam keywords + emoji bait")
267
+ return (score, reasons)
268
+
269
+ async def _shield_level(self, guild_id: int) -> str:
270
+ row = await self.bot.db.fetchone("SELECT level FROM shield_settings WHERE guild_id = ?", guild_id)
271
+ level = str(row[0]).strip().lower() if row and row[0] else "medium"
272
+ if level not in {"low", "medium", "high"}:
273
+ return "medium"
274
+ return level
275
+
276
+ @staticmethod
277
+ def _context_window_seconds(level: str) -> int:
278
+ if level == "high":
279
+ return 6 * 60 * 60
280
+ if level == "low":
281
+ return 90 * 60
282
+ return 3 * 60 * 60
283
+
284
+ @staticmethod
285
+ def _context_check_cooldown_seconds(level: str) -> int:
286
+ if level == "high":
287
+ return 8
288
+ if level == "low":
289
+ return 25
290
+ return 15
291
+
292
+ def _build_recent_context(self, guild_id: int, user_id: int, *, now_ts: float, horizon_seconds: int) -> list[tuple[float, int, str, list[str]]]:
293
+ key = (guild_id, user_id)
294
+ bucket = self._recent_user_context[key]
295
+ return [entry for entry in bucket if (now_ts - entry[0]) <= horizon_seconds]
296
+
297
+ async def _openrouter_contextual_scam_verdict(
298
+ self,
299
+ message: discord.Message,
300
+ *,
301
+ emojis: list[str],
302
+ stitched_text: str,
303
+ emoji_score: int,
304
+ emoji_reasons: list[str],
305
+ ) -> bool:
306
+ api_key = (self.bot.settings.openrouter_api_key or "").strip()
307
+ if not api_key or aiohttp is None:
308
+ return False
309
+
310
+ level = await self._shield_level(message.guild.id)
311
+ now_ts = time.time()
312
+ key = (message.guild.id, message.author.id)
313
+ cooldown = self._context_check_cooldown_seconds(level)
314
+ if (now_ts - self._last_context_ai_check.get(key, 0.0)) < cooldown:
315
+ return False
316
+ self._last_context_ai_check[key] = now_ts
317
+
318
+ horizon = self._context_window_seconds(level)
319
+ context_rows = self._build_recent_context(message.guild.id, message.author.id, now_ts=now_ts, horizon_seconds=horizon)
320
+ context_rows = context_rows[-20:] # keep prompt bounded
321
+
322
+ context_lines: list[str] = []
323
+ for ts, channel_id, text, row_emojis in context_rows:
324
+ delta_min = int(max(0, now_ts - ts) // 60)
325
+ compact_text = (text or "").replace("\n", " ").strip()[:220]
326
+ emoji_str = " ".join(row_emojis[:10]) if row_emojis else "-"
327
+ context_lines.append(f"[{delta_min}m ago][#{channel_id}] text={compact_text} | emojis={emoji_str}")
328
+
329
+ prompt = (
330
+ "You are a Discord anti-scam contextual classifier.\n"
331
+ "Goal: detect hacked-account scam campaigns and phishing even when messages are separated over time.\n"
332
+ "Treat repeated lure emojis + links/CTA + fake rewards/wallet verification as high risk.\n"
333
+ "Do not flag harmless emoji-only chatting.\n"
334
+ "Reply with one word only: SCAM or SAFE.\n\n"
335
+ f"Guild shield level: {level}\n"
336
+ f"Current message text: {(message.content or '').strip()[:1200]}\n"
337
+ f"Current message emojis: {' '.join(emojis[:30]) if emojis else '-'}\n"
338
+ f"Emoji heuristic score: {emoji_score} ({', '.join(emoji_reasons) if emoji_reasons else 'none'})\n"
339
+ f"Recent stitched text: {stitched_text[:1200] if stitched_text else '-'}\n\n"
340
+ "Recent same-user context (can be different channels/time):\n"
341
+ + ("\n".join(context_lines) if context_lines else "-")
342
+ )
343
+
344
+ payload = {
345
+ "model": (self.bot.settings.openrouter_model or "").strip() or "meta-llama/llama-3.3-70b-instruct:free",
346
+ "messages": [
347
+ {"role": "system", "content": "Classify as SCAM or SAFE only."},
348
+ {"role": "user", "content": prompt},
349
+ ],
350
+ "temperature": 0,
351
+ }
352
+ headers = {
353
+ "Authorization": f"Bearer {api_key}",
354
+ "Content-Type": "application/json",
355
+ }
356
+
357
+ try:
358
+ async with self._scam_scan_semaphore:
359
+ async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=18)) as session:
360
+ async with session.post("https://openrouter.ai/api/v1/chat/completions", json=payload, headers=headers) as resp:
361
+ if resp.status >= 400:
362
+ return False
363
+ data = await resp.json(content_type=None)
364
+ verdict = str((((data.get("choices") or [{}])[0].get("message") or {}).get("content") or "")).strip().upper()
365
+ return "SCAM" in verdict and "SAFE" not in verdict
366
+ except Exception:
367
+ return False
368
+
369
  async def _log_image_scan(self, message: discord.Message, result: str, detail: str) -> None:
370
  try:
371
  await self.send_log(
 
377
  except Exception:
378
  return
379
 
380
+ async def _store_scam_hash(self, guild_id: int, digest: str, *, created_by: int = 0) -> None:
381
+ if not digest:
382
+ return
383
+ try:
384
+ await self.bot.db.execute(
385
+ "INSERT OR IGNORE INTO scam_images(guild_id, image_hash, created_by) VALUES (?, ?, ?)",
386
+ guild_id,
387
+ digest,
388
+ created_by,
389
+ )
390
+ except Exception:
391
+ return
392
+
393
+ async def _image_items_from_text_urls(self, content: str) -> list[tuple[str, bytes, str]]:
394
  if aiohttp is None:
395
  return []
396
  urls = re.findall(r"https?://\S+", content or "")
397
  image_urls = [u.rstrip(").,!?") for u in urls if re.search(r"\.(png|jpg|jpeg|webp|gif)(\?|$)", u, re.IGNORECASE)]
398
  if not image_urls:
399
  return []
400
+ out: list[tuple[str, bytes, str]] = []
401
  timeout = aiohttp.ClientTimeout(total=8)
402
  async with aiohttp.ClientSession(timeout=timeout) as session:
403
+ for url in image_urls[:3]:
404
  try:
405
  async with session.get(url) as resp:
406
  if resp.status >= 400:
 
409
  if "image/" not in ctype:
410
  continue
411
  data = await resp.read()
412
+ if not data:
413
+ continue
414
+ raw = data[:2_000_000]
415
+ digest = hashlib.sha256(raw).hexdigest()
416
+ out.append((url, raw, digest))
417
  except Exception:
418
  continue
419
  return out
420
 
421
+ async def _image_bytes_from_text_urls(self, content: str) -> list[bytes]:
422
+ return [raw for _, raw, _ in await self._image_items_from_text_urls(content)]
423
+
424
+ @staticmethod
425
+ def _image_quality_heuristics(images: list[bytes]) -> tuple[int, list[str], dict[str, float | int]]:
426
+ if not images:
427
+ return (0, [], {})
428
+ if Image is None or ImageFilter is None or ImageStat is None:
429
+ return (0, [], {})
430
+
431
+ score = 0
432
+ reasons: list[str] = []
433
+ total_pixels = 0
434
+ total_bytes = 0
435
+ min_dim = None
436
+ edge_mean_avg = 0.0
437
+ analyzed = 0
438
+
439
+ for raw in images[:4]:
440
+ try:
441
+ with Image.open(io.BytesIO(raw)) as img:
442
+ img.load()
443
+ w, h = img.size
444
+ if w <= 0 or h <= 0:
445
+ continue
446
+ analyzed += 1
447
+ total_pixels += (w * h)
448
+ total_bytes += len(raw)
449
+ md = min(w, h)
450
+ min_dim = md if min_dim is None else min(min_dim, md)
451
+
452
+ gray = img.convert("L")
453
+ edges = gray.filter(ImageFilter.FIND_EDGES)
454
+ st = ImageStat.Stat(edges)
455
+ edge_mean = float(st.mean[0]) if st.mean else 0.0
456
+ edge_mean_avg += edge_mean
457
+ except Exception:
458
+ continue
459
+
460
+ if analyzed == 0 or total_pixels <= 0:
461
+ return (0, [], {})
462
+
463
+ mp = total_pixels / 1_000_000.0
464
+ bpp = total_bytes / float(total_pixels)
465
+ edge_mean_avg = edge_mean_avg / analyzed if analyzed else 0.0
466
+
467
+ if mp < 0.45 or (min_dim is not None and min_dim < 520):
468
+ score += 2
469
+ reasons.append("low resolution image quality")
470
+ if edge_mean_avg < 11.0:
471
+ score += 2
472
+ reasons.append("blurry/soft edges (likely low quality)")
473
+ if bpp < 0.16:
474
+ score += 1
475
+ reasons.append("high compression artifacts likely")
476
+
477
+ metrics: dict[str, float | int] = {
478
+ "analyzed_images": analyzed,
479
+ "megapixels": round(mp, 3),
480
+ "bytes_per_pixel": round(bpp, 4),
481
+ "edge_mean": round(edge_mean_avg, 3),
482
+ "min_dimension": int(min_dim or 0),
483
+ }
484
+ return (score, reasons, metrics)
485
+
486
+ @staticmethod
487
+ def _ocr_scam_heuristics(ocr_text: str) -> tuple[int, list[str]]:
488
+ text = (ocr_text or "").strip().lower()
489
+ if not text:
490
+ return (0, [])
491
+ reasons: list[str] = []
492
+ score = 0
493
+
494
+ hits = OCR_SCAM_RE.findall(text)
495
+ hit_count = len(hits)
496
+ if hit_count >= 2:
497
+ score += 2
498
+ reasons.append("ocr contains multiple scam terms")
499
+ if ("verify" in text and "wallet" in text) or "verify wallet" in text:
500
+ score += 2
501
+ reasons.append("ocr indicates wallet verification flow")
502
+ if ("mrbeast" in text or "mr beast" in text) and ("giveaway" in text or "free" in text or "winner" in text):
503
+ score += 2
504
+ reasons.append("ocr indicates creator impersonation giveaway")
505
+ if ("bit.ly" in text or "tinyurl" in text) and ("claim" in text or "winner" in text or "free" in text):
506
+ score += 1
507
+ reasons.append("short-link + prize phrasing in ocr")
508
+
509
+ return (score, reasons)
510
+
511
+ async def _openrouter_extract_image_text(self, message: discord.Message, *, preloaded_images: list[bytes] | None = None) -> str:
512
+ api_key = (self.bot.settings.openrouter_api_key or "").strip()
513
+ if not api_key or aiohttp is None:
514
+ return ""
515
+
516
+ images: list[bytes] = []
517
+ if preloaded_images:
518
+ images.extend(preloaded_images[:3])
519
+ else:
520
+ for att in message.attachments[:3]:
521
+ if not ((att.content_type or "").startswith("image/")):
522
+ continue
523
+ try:
524
+ raw = await att.read(use_cached=True)
525
+ if raw:
526
+ images.append(raw[:2_000_000])
527
+ except Exception:
528
+ continue
529
+ if not images:
530
+ images.extend((await self._image_bytes_from_text_urls(message.content or ""))[:3])
531
+
532
+ if not images:
533
+ return ""
534
+
535
+ content_parts: list[dict] = [
536
+ {
537
+ "type": "text",
538
+ "text": (
539
+ "Extract all visible text from these images (OCR). "
540
+ "Return plain text only. Keep URLs, @handles, numbers, and symbols exactly if visible."
541
+ ),
542
+ }
543
+ ]
544
+ for raw in images[:3]:
545
+ try:
546
+ data_url = "data:image/png;base64," + base64.b64encode(raw[:2_000_000]).decode("utf-8")
547
+ content_parts.append({"type": "image_url", "image_url": {"url": data_url}})
548
+ except Exception:
549
+ continue
550
+
551
+ payload = {
552
+ "model": "meta-llama/llama-3.2-11b-vision-instruct:free",
553
+ "messages": [
554
+ {"role": "system", "content": "You are OCR. Return extracted text only."},
555
+ {"role": "user", "content": content_parts},
556
+ ],
557
+ "temperature": 0,
558
+ }
559
+ headers = {
560
+ "Authorization": f"Bearer {api_key}",
561
+ "Content-Type": "application/json",
562
+ }
563
+
564
+ try:
565
+ async with self._scam_scan_semaphore:
566
+ async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=20)) as session:
567
+ async with session.post("https://openrouter.ai/api/v1/chat/completions", json=payload, headers=headers) as resp:
568
+ if resp.status >= 400:
569
+ return ""
570
+ data = await resp.json(content_type=None)
571
+ return str((((data.get("choices") or [{}])[0].get("message") or {}).get("content") or "")).strip()
572
+ except Exception:
573
+ return ""
574
+
575
  async def _analyze_scam_with_openrouter(self, message: discord.Message) -> bool:
576
  if aiohttp is None or not self.bot.settings.openrouter_api_key:
577
  return False
 
581
  "meta-llama/llama-3.2-11b-vision-instruct:free",
582
  "nvidia/llama-3.1-nemotron-nano-vl-8b-v1:free",
583
  ]
584
+ has_url_images = bool(re.search(r"https?://\S+\.(png|jpg|jpeg|webp|gif)(\?|$)", message.content or "", re.IGNORECASE))
585
+ score, reasons = self._scam_heuristics(message.content or "", has_images=bool(message.attachments) or has_url_images)
586
+ level = await self._shield_level(message.guild.id)
587
+ now_ts = time.time()
588
+ horizon = self._context_window_seconds(level)
589
+ recent_rows = self._build_recent_context(message.guild.id, message.author.id, now_ts=now_ts, horizon_seconds=horizon)
590
+ recent_lines: list[str] = []
591
+ for ts, channel_id, text, row_emojis in recent_rows[-12:]:
592
+ delta_min = int(max(0, now_ts - ts) // 60)
593
+ compact_text = (text or "").replace("\n", " ").strip()[:180]
594
+ emoji_str = " ".join(row_emojis[:8]) if row_emojis else "-"
595
+ recent_lines.append(f"[{delta_min}m][#{channel_id}] {compact_text} | emojis={emoji_str}")
596
  base_text = (
597
  "You are an advanced scam detector for Discord specialized in identifying:\n"
598
  "1. LOW-QUALITY IMAGES: Blurry, pixelated, heavily compressed, or distorted images are HIGH RISK\n"
 
601
  "4. SOCIAL ENGINEERING: Impersonation, urgency tactics, pressure to act quickly\n\n"
602
  "IMPORTANT: Low image quality + suspicious content = ALMOST ALWAYS SCAM\n\n"
603
  f"Heuristic analysis: score={score}/10, reasons: {', '.join(reasons) if reasons else 'none'}.\n\n"
604
+ f"Message content:\n{(message.content or '')[:2000]}\n\n"
605
+ "Recent same-user context (for delayed scam campaigns):\n"
606
+ f"{chr(10).join(recent_lines) if recent_lines else '-'}"
607
  )
608
  content_parts: list[dict] = [{"type": "text", "text": base_text}]
609
  image_count = 0
 
617
  image_count += 1
618
  except Exception:
619
  continue
620
+ url_images = await self._image_bytes_from_text_urls(message.content or "")
621
+ for raw in url_images[:3]:
622
+ try:
623
+ data_url = "data:image/png;base64," + base64.b64encode(raw[:2_000_000]).decode("utf-8")
624
+ content_parts.append({"type": "image_url", "image_url": {"url": data_url}})
625
+ image_count += 1
626
+ except Exception:
627
+ continue
628
  if image_count == 0 and not message.content.strip():
629
  return False
630
 
 
662
  return False
663
  try:
664
  model = genai.GenerativeModel("gemini-1.5-flash")
665
+ level = await self._shield_level(message.guild.id)
666
+ now_ts = time.time()
667
+ horizon = self._context_window_seconds(level)
668
+ recent_rows = self._build_recent_context(message.guild.id, message.author.id, now_ts=now_ts, horizon_seconds=horizon)
669
+ recent_lines: list[str] = []
670
+ for ts, channel_id, text, row_emojis in recent_rows[-12:]:
671
+ delta_min = int(max(0, now_ts - ts) // 60)
672
+ compact_text = (text or "").replace("\n", " ").strip()[:180]
673
+ emoji_str = " ".join(row_emojis[:8]) if row_emojis else "-"
674
+ recent_lines.append(f"[{delta_min}m][#{channel_id}] {compact_text} | emojis={emoji_str}")
675
  prompt = (
676
  "Classify if this Discord message is a scam/phishing attempt.\n"
677
  "Use context. Do NOT flag harmless 'dm me' alone.\n"
678
  "Reply with TRUE or FALSE only.\n\n"
679
+ f"Message: {(message.content or '')[:2000]}\n\n"
680
+ "Recent same-user context (for delayed scam campaigns):\n"
681
+ f"{chr(10).join(recent_lines) if recent_lines else '-'}"
682
  )
683
  parts: list = [prompt]
684
  for att in message.attachments[:3]:
 
1162
  if message.author.bot or not message.guild:
1163
  return
1164
  text_content = message.content or ""
1165
+ emoji_tokens = self._extract_emojis(text_content)
1166
  await self.send_log(
1167
  message.guild,
1168
  "💬 Message Sent",
 
1174
  bucket = self._recent_user_messages[key]
1175
  bucket.append((now_ts, text_content.strip()))
1176
  stitched = " ".join(chunk for ts, chunk in bucket if now_ts - ts <= 90 and chunk)
1177
+ context_bucket = self._recent_user_context[key]
1178
+ context_bucket.append((now_ts, message.channel.id, text_content.strip()[:350], emoji_tokens[:20]))
1179
+
1180
+ level = await self._shield_level(message.guild.id)
1181
+ emoji_score, emoji_reasons = self._emoji_scam_heuristics(text_content, emoji_tokens)
1182
+ emoji_threshold = 3 if level == "low" else (2 if level == "medium" else 1)
1183
+ if emoji_tokens and emoji_score >= emoji_threshold:
1184
+ contextual_scam = await self._openrouter_contextual_scam_verdict(
1185
+ message,
1186
+ emojis=emoji_tokens,
1187
+ stitched_text=stitched,
1188
+ emoji_score=emoji_score,
1189
+ emoji_reasons=emoji_reasons,
1190
+ )
1191
+ if contextual_scam:
1192
+ await self._apply_ai_shield(message, "SCAM")
1193
+ return
1194
 
1195
  if stitched and self._is_high_risk_scam_text(stitched):
1196
  violation = await self._openrouter_shield_violation(stitched)
 
1201
  if message.attachments:
1202
  has_image = any((att.content_type or "").startswith("image/") for att in message.attachments)
1203
  if has_image:
1204
+ attachment_hashes: list[str] = []
1205
+ attachment_images: list[bytes] = []
1206
  for att in message.attachments:
1207
  if not (att.content_type or "").startswith("image/"):
1208
  continue
 
1211
  except Exception:
1212
  continue
1213
  digest = hashlib.sha256(raw).hexdigest()
1214
+ attachment_hashes.append(digest)
1215
+ attachment_images.append(raw[:2_000_000])
1216
  row = await self.bot.db.fetchone(
1217
  "SELECT image_hash FROM scam_images WHERE guild_id = ? AND image_hash = ?",
1218
  message.guild.id,
 
1223
  deleted = await message.channel.purge(limit=15, check=lambda m: m.author.id == message.author.id and m.attachments)
1224
  await self.send_log(message.guild, "🧹 Scam Image Purge", f"Deleted {len(deleted)} image messages from {message.author.mention}")
1225
  return
1226
+ quality_score, quality_reasons, quality_metrics = self._image_quality_heuristics(attachment_images)
1227
+ score, reasons = self._scam_heuristics(message.content or "", has_images=True)
1228
+ ocr_text = ""
1229
+ ocr_score = 0
1230
+ ocr_reasons: list[str] = []
1231
+ if score >= 2 or quality_score >= 2:
1232
+ ocr_text = await self._openrouter_extract_image_text(message, preloaded_images=attachment_images)
1233
+ ocr_score, ocr_reasons = self._ocr_scam_heuristics(ocr_text)
1234
+
1235
  is_image_scam = await self._analyze_scam_with_openrouter(message)
1236
  if not is_image_scam:
1237
  is_image_scam = await self._analyze_scam_with_gemini(message)
1238
+
1239
+ combined_score = score + quality_score + ocr_score
1240
+ has_direct_scam_cue = bool(
1241
+ SCAM_LINK_RE.search(message.content or "")
1242
+ or SCAM_KEYWORDS_RE.search((message.content or "") + " " + (ocr_text or ""))
1243
+ or ocr_score >= 2
1244
+ )
1245
+ if not is_image_scam and combined_score >= 7 and has_direct_scam_cue:
1246
+ is_image_scam = True
1247
+
1248
+ quality_text = ", ".join(quality_reasons) if quality_reasons else "none"
1249
+ ocr_snippet = (ocr_text or "").replace("\n", " ")[:180]
1250
+ detail = (
1251
+ f"heuristic_score={score}; heuristic_reasons={', '.join(reasons) if reasons else 'none'}; "
1252
+ f"quality_score={quality_score}; quality_reasons={quality_text}; quality_metrics={quality_metrics}; "
1253
+ f"ocr_score={ocr_score}; ocr_reasons={', '.join(ocr_reasons) if ocr_reasons else 'none'}; "
1254
+ f"ocr_text={ocr_snippet or 'none'}; combined_score={combined_score}; ai_scam={is_image_scam}"
1255
+ )
1256
  await self._log_image_scan(message, "SCAM" if is_image_scam else "SAFE", detail)
1257
  if is_image_scam:
1258
+ for digest in attachment_hashes:
1259
+ await self._store_scam_hash(message.guild.id, digest, created_by=(self.bot.user.id if self.bot.user else 0))
1260
  await self._apply_ai_shield(message, "SCAM")
1261
  return
1262
  elif re.search(r"https?://\S+\.(png|jpg|jpeg|webp|gif)(\?|$)", message.content or "", re.IGNORECASE):
1263
+ url_items = await self._image_items_from_text_urls(message.content or "")
1264
+ url_hashes = [digest for _, _, digest in url_items]
1265
+ url_images = [raw for _, raw, _ in url_items]
1266
+ for digest in url_hashes:
1267
+ row = await self.bot.db.fetchone(
1268
+ "SELECT image_hash FROM scam_images WHERE guild_id = ? AND image_hash = ?",
1269
+ message.guild.id,
1270
+ digest,
1271
+ )
1272
+ if row:
1273
+ await self._apply_ai_shield(message, "SCAM")
1274
+ return
1275
  proxy_message = message
1276
+ quality_score, quality_reasons, quality_metrics = self._image_quality_heuristics(url_images)
1277
+ score, reasons = self._scam_heuristics(message.content or "", has_images=True)
1278
+ ocr_text = ""
1279
+ ocr_score = 0
1280
+ ocr_reasons: list[str] = []
1281
+ if score >= 2 or quality_score >= 2:
1282
+ ocr_text = await self._openrouter_extract_image_text(proxy_message, preloaded_images=url_images)
1283
+ ocr_score, ocr_reasons = self._ocr_scam_heuristics(ocr_text)
1284
+
1285
  is_image_scam = await self._analyze_scam_with_openrouter(proxy_message)
1286
  if not is_image_scam:
1287
  is_image_scam = await self._analyze_scam_with_gemini(proxy_message)
1288
+
1289
+ combined_score = score + quality_score + ocr_score
1290
+ has_direct_scam_cue = bool(
1291
+ SCAM_LINK_RE.search(message.content or "")
1292
+ or SCAM_KEYWORDS_RE.search((message.content or "") + " " + (ocr_text or ""))
1293
+ or ocr_score >= 2
1294
+ )
1295
+ if not is_image_scam and combined_score >= 7 and has_direct_scam_cue:
1296
+ is_image_scam = True
1297
+
1298
+ quality_text = ", ".join(quality_reasons) if quality_reasons else "none"
1299
+ ocr_snippet = (ocr_text or "").replace("\n", " ")[:180]
1300
+ detail = (
1301
+ f"url_image_scan=1; heuristic_score={score}; heuristic_reasons={', '.join(reasons) if reasons else 'none'}; "
1302
+ f"quality_score={quality_score}; quality_reasons={quality_text}; quality_metrics={quality_metrics}; "
1303
+ f"ocr_score={ocr_score}; ocr_reasons={', '.join(ocr_reasons) if ocr_reasons else 'none'}; "
1304
+ f"ocr_text={ocr_snippet or 'none'}; combined_score={combined_score}; ai_scam={is_image_scam}"
1305
+ )
1306
  await self._log_image_scan(message, "SCAM" if is_image_scam else "SAFE", detail)
1307
  if is_image_scam:
1308
+ for digest in url_hashes:
1309
+ await self._store_scam_hash(message.guild.id, digest, created_by=(self.bot.user.id if self.bot.user else 0))
1310
  await self._apply_ai_shield(message, "SCAM")
1311
  return
1312
  if self._is_high_risk_scam_text(text_content):
 
1379
  elif mention_type == "role" and role_id:
1380
  mention = f"<@&{role_id}> "
1381
  for item in fresh[:3]:
1382
+ link = item.get("link", "")
1383
+ embed = discord.Embed(
1384
+ title=f"🎁 Free Game Drop: {item.get('title', '')}",
1385
+ url=link,
1386
+ color=NEON_CYAN,
1387
+ description=(
1388
+ f"**{item.get('description', 'Limited-time free game offer!')}**\n\n"
1389
+ f"Grab it before the offer expires."
1390
+ ),
1391
+ )
1392
  if item.get("image"):
1393
  embed.set_image(url=item["image"])
1394
+ embed.set_thumbnail(url=self._store_icon(item.get("platform", "")))
1395
+ embed.add_field(name="🏪 Platform", value=item.get("platform", "Unknown"), inline=True)
1396
+ embed.add_field(name="🧩 Type", value=item.get("game_type", "Game"), inline=True)
1397
+ embed.add_field(name="💸 Previous Price", value=item.get("original_price", "N/A"), inline=True)
1398
+ embed.add_field(name="⏳ Offer Ends", value=item.get("end_date", "N/A"), inline=True)
1399
+ embed.add_field(name="🔗 Claim Link", value=f"[Open Giveaway]({link})" if link else "N/A", inline=True)
1400
+ embed.set_footer(text="BOT- Free Games Radar • Epic / Steam / GOG")
1401
+ view = FreeGameClaimView(link, item.get("id", "")) if link else None
1402
  await channel.send(content=mention or None, embed=embed, view=view)
1403
  latest_ids = ",".join(item["id"] for item in items[:20])
1404
  await self.bot.db.execute(
bot/cogs/fun.py CHANGED
@@ -605,14 +605,19 @@ class Fun(commands.Cog):
605
  link = item["link"]
606
  embed = ImperialMotaz.craft_embed(
607
  title=f"[FREE GAME] | {title}",
608
- description="Live free-game deal from trusted store.",
 
 
 
609
  color=NEON_PINK,
610
  footer="Free Games Tracker",
611
  )
612
  embed.url = link
613
  embed.add_field(name="Platform", value=f"「 {item.get('platform', 'Unknown')} 」", inline=True)
 
614
  embed.add_field(name="Original Price", value=f"「 {item.get('original_price', 'N/A')} 」", inline=True)
615
  embed.add_field(name="Ends On", value=f"「 {item.get('end_date', 'N/A')} 」", inline=True)
 
616
  if item.get("image"):
617
  embed.set_image(url=item["image"])
618
  embed.set_thumbnail(url=self._store_icon(item.get("platform", "")))
@@ -760,7 +765,7 @@ class Fun(commands.Cog):
760
 
761
  @gamehub.command(name="trivia", description="Gaming/Movies/Series trivia")
762
  async def gamehub_trivia(self, ctx: commands.Context, category: str = "gaming", difficulty: str = "medium") -> None:
763
- await self.trivia(ctx, category=category, difficulty=difficulty)
764
 
765
  @commands.hybrid_command(name="xo", description="TicTacToe vs member or bot", hidden=True, with_app_command=False)
766
  async def xo(self, ctx: commands.Context, opponent: discord.Member | None = None) -> None:
@@ -845,7 +850,7 @@ class Fun(commands.Cog):
845
  ),
846
  color=NEON_ORANGE,
847
  )
848
- embed.add_field(name="🎁 مكافأة", value="تم فتح combo باك مان! 🟡", inline=False)
849
  else:
850
  embed = discord.Embed(
851
  title="🍄 Arcade Challenge",
@@ -857,7 +862,7 @@ class Fun(commands.Cog):
857
  ),
858
  color=NEON_ORANGE,
859
  )
860
- embed.add_field(name="🎁 Bonus", value="Pac-Man combo unlocked! 🟡", inline=False)
861
  embed.set_thumbnail(url="https://upload.wikimedia.org/wikipedia/en/a/a9/MarioNSMBUDeluxe.png")
862
  await ctx.reply(embed=embed)
863
 
 
605
  link = item["link"]
606
  embed = ImperialMotaz.craft_embed(
607
  title=f"[FREE GAME] | {title}",
608
+ description=(
609
+ f"**{item.get('description', 'Limited-time free game offer!')}**\n\n"
610
+ "Claim it now from the official store page before it expires."
611
+ ),
612
  color=NEON_PINK,
613
  footer="Free Games Tracker",
614
  )
615
  embed.url = link
616
  embed.add_field(name="Platform", value=f"「 {item.get('platform', 'Unknown')} 」", inline=True)
617
+ embed.add_field(name="Type", value=f"「 {item.get('game_type', 'Game')} 」", inline=True)
618
  embed.add_field(name="Original Price", value=f"「 {item.get('original_price', 'N/A')} 」", inline=True)
619
  embed.add_field(name="Ends On", value=f"「 {item.get('end_date', 'N/A')} 」", inline=True)
620
+ embed.add_field(name="Link", value=f"[Open Giveaway]({link})", inline=False)
621
  if item.get("image"):
622
  embed.set_image(url=item["image"])
623
  embed.set_thumbnail(url=self._store_icon(item.get("platform", "")))
 
765
 
766
  @gamehub.command(name="trivia", description="Gaming/Movies/Series trivia")
767
  async def gamehub_trivia(self, ctx: commands.Context, category: str = "gaming", difficulty: str = "medium") -> None:
768
+ await self.trivia(ctx, category=category)
769
 
770
  @commands.hybrid_command(name="xo", description="TicTacToe vs member or bot", hidden=True, with_app_command=False)
771
  async def xo(self, ctx: commands.Context, opponent: discord.Member | None = None) -> None:
 
850
  ),
851
  color=NEON_ORANGE,
852
  )
853
+ embed.add_field(name="🎁 مكافأة", value="تم فتح combo باك مان! <:animatedarrowyellow:1477261257592668271>", inline=False)
854
  else:
855
  embed = discord.Embed(
856
  title="🍄 Arcade Challenge",
 
862
  ),
863
  color=NEON_ORANGE,
864
  )
865
+ embed.add_field(name="🎁 Bonus", value="Pac-Man combo unlocked! <:animatedarrowyellow:1477261257592668271>", inline=False)
866
  embed.set_thumbnail(url="https://upload.wikimedia.org/wikipedia/en/a/a9/MarioNSMBUDeluxe.png")
867
  await ctx.reply(embed=embed)
868
 
bot/cogs/gambling.py CHANGED
@@ -1,771 +1,689 @@
1
- """
2
- Interactive Gambling & RPG Cog - Casino Games with UI and Full RPG System
3
- """
4
-
5
- from __future__ import annotations
6
-
7
- import random
8
- import asyncio
9
- from typing import List, Optional, Dict
10
-
11
- import discord
12
- from discord.ext import commands
13
-
14
- from bot.theme import NEON_CYAN, NEON_LIME, NEON_RED, NEON_GOLD, NEON_PURPLE, NEON_PINK, NEON_BLUE, panel_divider, add_banner_to_embed
15
- from bot.emojis import ui
16
-
17
-
18
- class Card:
19
- def __init__(self, suit: str, rank: str):
20
- self.suit = suit
21
- self.rank = rank
22
- self.value = self._get_value()
23
-
24
- def _get_value(self) -> int:
25
- if self.rank in ['J', 'Q', 'K']:
26
- return 10
27
- elif self.rank == 'A':
28
- return 11
29
- else:
30
- return int(self.rank)
31
-
32
- def __str__(self) -> str:
33
- return f"{self.rank}{self.suit}"
34
-
35
-
36
- class Deck:
37
- def __init__(self):
38
- self.cards = []
39
- suits = ['♠', '♥', '♦', '♣']
40
- ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
41
- for suit in suits:
42
- for rank in ranks:
43
- self.cards.append(Card(suit, rank))
44
- random.shuffle(self.cards)
45
-
46
- def draw(self) -> Card:
47
- return self.cards.pop()
48
-
49
-
50
- class BlackjackView(discord.ui.View):
51
- def __init__(self, cog, guild_id: int, user_id: int, bet: int):
52
- super().__init__(timeout=300)
53
- self.cog = cog
54
- self.guild_id = guild_id
55
- self.user_id = user_id
56
- self.bet = bet
57
- self.deck = Deck()
58
- self.player_hand = [self.deck.draw(), self.deck.draw()]
59
- self.dealer_hand = [self.deck.draw(), self.deck.draw()]
60
- self.game_over = False
61
- self.message: Optional[discord.Message] = None
62
-
63
- def hand_value(self, hand: List[Card]) -> int:
64
- value = sum(card.value for card in hand)
65
- aces = sum(1 for card in hand if card.rank == 'A')
66
- while value > 21 and aces:
67
- value -= 10
68
- aces -= 1
69
- return value
70
-
71
- def format_hand(self, hand: List[Card], hide_first: bool = False) -> str:
72
- if hide_first and len(hand) >= 2:
73
- return f"[❓❓] [{hand[1]}]"
74
- return ' '.join([f"[{str(card)}]" for card in hand])
75
-
76
- def build_embed(self, show_dealer_card: bool = True) -> discord.Embed:
77
- player_value = self.hand_value(self.player_hand)
78
- dealer_value = self.hand_value(self.dealer_hand) if show_dealer_card else '?'
79
-
80
- embed = discord.Embed(
81
- title="🃏 Blackjack Casino",
82
- description=panel_divider('purple'),
83
- color=NEON_PURPLE,
84
- )
85
-
86
- dealer_display = self.format_hand(self.dealer_hand, hide_first=not show_dealer_card)
87
- embed.add_field(
88
- name="🎩 Dealer's Hand",
89
- value=f"{dealer_display}\n**Value:** {dealer_value}",
90
- inline=False,
91
- )
92
-
93
- player_display = self.format_hand(self.player_hand)
94
- embed.add_field(
95
- name="👤 Your Hand",
96
- value=f"{player_display}\n**Value:** {player_value}",
97
- inline=False,
98
- )
99
-
100
- embed.add_field(name="💰 Bet", value=f"`{self.bet:,}` coins", inline=False)
101
-
102
- if self.game_over:
103
- if player_value > 21:
104
- embed.description = f"❌ **BUST!** You lost `{self.bet:,}` coins."
105
- embed.color = NEON_RED
106
- elif dealer_value == 21 and len(self.dealer_hand) == 2:
107
- embed.description = f"❌ **Dealer Blackjack!** You lost `{self.bet:,}` coins."
108
- embed.color = NEON_RED
109
- elif dealer_value > 21:
110
- winnings = self.bet
111
- embed.description = f"✅ **Dealer Busts! You Win!** +`{winnings:,}` coins!"
112
- embed.color = NEON_LIME
113
- elif player_value > dealer_value:
114
- winnings = self.bet
115
- embed.description = f"✅ **You Win!** +`{winnings:,}` coins!"
116
- embed.color = NEON_LIME
117
- elif player_value < dealer_value:
118
- embed.description = f"❌ **Dealer Wins!** You lost `{self.bet:,}` coins."
119
- embed.color = NEON_RED
120
- else:
121
- embed.description = f"🤝 **Push!** Bet returned."
122
- embed.color = NEON_GOLD
123
-
124
- return embed
125
-
126
- async def end_game(self, interaction: discord.Interaction):
127
- self.game_over = True
128
-
129
- engagement_cog = interaction.bot.get_cog("Engagement")
130
- if not engagement_cog:
131
- return
132
-
133
- player_value = self.hand_value(self.player_hand)
134
- dealer_value = self.hand_value(self.dealer_hand)
135
-
136
- if player_value > 21:
137
- await engagement_cog._remove_coins(self.guild_id, self.user_id, self.bet)
138
- elif dealer_value == 21 and len(self.dealer_hand) == 2:
139
- await engagement_cog._remove_coins(self.guild_id, self.user_id, self.bet)
140
- elif dealer_value > 21 or player_value > dealer_value:
141
- await engagement_cog._add_coins(self.guild_id, self.user_id, self.bet)
142
- elif player_value < dealer_value:
143
- await engagement_cog._remove_coins(self.guild_id, self.user_id, self.bet)
144
-
145
- embed = self.build_embed(show_dealer_card=True)
146
- for child in self.children:
147
- child.disabled = True
148
- if self.message:
149
- await self.message.edit(embed=embed, view=self)
150
-
151
- @discord.ui.button(label="Hit", emoji=ui("plus"), style=discord.ButtonStyle.primary, custom_id="bj_hit")
152
- async def hit(self, interaction: discord.Interaction, button: discord.ui.Button):
153
- if self.game_over or interaction.user.id != self.user_id:
154
- await interaction.response.send_message("❌ Not your game.", ephemeral=True)
155
- return
156
-
157
- self.player_hand.append(self.deck.draw())
158
- player_value = self.hand_value(self.player_hand)
159
-
160
- if player_value > 21:
161
- await self.end_game(interaction)
162
- else:
163
- embed = self.build_embed(show_dealer_card=False)
164
- await interaction.response.edit_message(embed=embed, view=self)
165
-
166
- @discord.ui.button(label="Stand", emoji=ui("stop"), style=discord.ButtonStyle.danger, custom_id="bj_stand")
167
- async def stand(self, interaction: discord.Interaction, button: discord.ui.Button):
168
- if self.game_over or interaction.user.id != self.user_id:
169
- await interaction.response.send_message("❌ Not your game.", ephemeral=True)
170
- return
171
-
172
- while self.hand_value(self.dealer_hand) < 17:
173
- self.dealer_hand.append(self.deck.draw())
174
-
175
- await self.end_game(interaction)
176
-
177
- @discord.ui.button(label="Double Down", emoji=ui("star"), style=discord.ButtonStyle.success, custom_id="bj_double")
178
- async def double_down(self, interaction: discord.Interaction, button: discord.ui.Button):
179
- if self.game_over or interaction.user.id != self.user_id:
180
- await interaction.response.send_message("❌ Not your game.", ephemeral=True)
181
- return
182
-
183
- if len(self.player_hand) != 2:
184
- await interaction.response.send_message("❌ Can only double on first turn.", ephemeral=True)
185
- return
186
-
187
- engagement_cog = interaction.bot.get_cog("Engagement")
188
- if not engagement_cog:
189
- return
190
-
191
- row = await interaction.bot.db.fetchone(
192
- "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?",
193
- self.guild_id, self.user_id,
194
- )
195
- wallet = row[0] if row else 0
196
-
197
- if wallet < self.bet:
198
- await interaction.response.send_message("❌ Insufficient funds to double.", ephemeral=True)
199
- return
200
-
201
- self.bet *= 2
202
- self.player_hand.append(self.deck.draw())
203
-
204
- player_value = self.hand_value(self.player_hand)
205
- if player_value > 21:
206
- await self.end_game(interaction)
207
- else:
208
- while self.hand_value(self.dealer_hand) < 17:
209
- self.dealer_hand.append(self.deck.draw())
210
- await self.end_game(interaction)
211
-
212
-
213
- class RouletteGameView(discord.ui.View):
214
- def __init__(self, cog, guild_id: int, user_id: int):
215
- super().__init__(timeout=300)
216
- self.cog = cog
217
- self.guild_id = guild_id
218
- self.user_id = user_id
219
- self.bet_amount = 0
220
- self.bet_type = ""
221
- self.spinning = False
222
-
223
- RED_NUMBERS = [1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36]
224
- BLACK_NUMBERS = [2, 4, 6, 8, 10, 11, 13, 15, 17, 20, 22, 24, 26, 28, 29, 31, 33, 35]
225
-
226
- def get_color(self, number: int) -> str:
227
- if number == 0:
228
- return "🟢"
229
- elif number in self.RED_NUMBERS:
230
- return "🔴"
231
- else:
232
- return "⚫"
233
-
234
- def build_embed(self, number: Optional[int] = None, won: bool = False, winnings: int = 0) -> discord.Embed:
235
- embed = discord.Embed(
236
- title="🎰 Roulette Casino",
237
- description=panel_divider('gold'),
238
- color=NEON_GOLD,
239
- )
240
-
241
- if number is not None:
242
- color_emoji = self.get_color(number)
243
- embed.add_field(name="🎯 Result", value=f"{color_emoji} **{number}**", inline=False)
244
- embed.add_field(name="💰 Bet", value=f"`{self.bet_amount:,}` on {self.bet_type}", inline=True)
245
- embed.add_field(name="💵 Result", value=f"`{'+' if won else '-'}{abs(winnings):,}` coins", inline=True)
246
-
247
- if won:
248
- embed.description = f"✅ **You Won!** +`{winnings:,}` coins!"
249
- embed.color = NEON_LIME
250
- else:
251
- embed.description = f"❌ **You Lost!** -`{abs(winnings):,}` coins."
252
- embed.color = NEON_RED
253
- else:
254
- embed.description = "Place your bet and spin the wheel!"
255
- grid = ""
256
- for i in range(0, 37, 3):
257
- if i == 0:
258
- grid += "🟢 **0** "
259
- else:
260
- c1 = "🔴" if i in self.RED_NUMBERS else ""
261
- c2 = "🔴" if (i+1) in self.RED_NUMBERS else "⚫"
262
- c3 = "🔴" if (i+2) in self.RED_NUMBERS else "⚫"
263
- grid += f"{c1} {i} {c2} {i+1} {c3} {i+2}\n"
264
- embed.add_field(name="🎡 Roulette Wheel", value=f"```\n{grid}```", inline=False)
265
-
266
- return embed
267
-
268
- @discord.ui.button(label="Bet Red 🔴", style=discord.ButtonStyle.danger, custom_id="roulette_red")
269
- async def bet_red(self, interaction: discord.Interaction, button: discord.ui.Button):
270
- await self.prompt_bet(interaction, "red")
271
-
272
- @discord.ui.button(label="Bet Black ⚫", style=discord.ButtonStyle.gray, custom_id="roulette_black")
273
- async def bet_black(self, interaction: discord.Interaction, button: discord.ui.Button):
274
- await self.prompt_bet(interaction, "black")
275
-
276
- @discord.ui.button(label="Bet Even", style=discord.ButtonStyle.primary, custom_id="roulette_even")
277
- async def bet_even(self, interaction: discord.Interaction, button: discord.ui.Button):
278
- await self.prompt_bet(interaction, "even")
279
-
280
- @discord.ui.button(label="Bet Odd", style=discord.ButtonStyle.primary, custom_id="roulette_odd")
281
- async def bet_odd(self, interaction: discord.Interaction, button: discord.ui.Button):
282
- await self.prompt_bet(interaction, "odd")
283
-
284
- async def prompt_bet(self, interaction: discord.Interaction, bet_type: str):
285
- if interaction.user.id != self.user_id:
286
- return
287
- await interaction.response.send_modal(RouletteBetModal(self.cog, self.guild_id, self.user_id, bet_type))
288
-
289
-
290
- class RouletteBetModal(discord.ui.Modal, title="🎰 Place Your Bet"):
291
- bet_amount = discord.ui.TextInput(
292
- label="Bet Amount",
293
- placeholder="Enter amount (min 10)",
294
- required=True,
295
- min_length=1,
296
- max_length=10,
297
- )
298
-
299
- def __init__(self, cog, guild_id: int, user_id: int, bet_type: str):
300
- super().__init__()
301
- self.cog = cog
302
- self.guild_id = guild_id
303
- self.user_id = user_id
304
- self.bet_type = bet_type
305
-
306
- async def on_submit(self, interaction: discord.Interaction) -> None:
307
- try:
308
- bet = int(self.bet_amount.value.strip())
309
- except ValueError:
310
- await interaction.response.send_message("❌ Invalid bet amount.", ephemeral=True)
311
- return
312
-
313
- if bet < 10:
314
- await interaction.response.send_message("❌ Minimum bet is 10 coins.", ephemeral=True)
315
- return
316
-
317
- engagement_cog = interaction.bot.get_cog("Engagement")
318
- if not engagement_cog:
319
- await interaction.response.send_message("❌ Economy not available.", ephemeral=True)
320
- return
321
-
322
- row = await interaction.bot.db.fetchone(
323
- "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?",
324
- self.guild_id, self.user_id,
325
- )
326
- wallet = row[0] if row else 0
327
-
328
- if wallet < bet:
329
- await interaction.response.send_message(f"❌ Insufficient funds. You have {wallet:,} coins.", ephemeral=True)
330
- return
331
-
332
- await interaction.response.defer()
333
-
334
- number = random.randint(0, 36)
335
- is_red = number in RouletteGameView.RED_NUMBERS
336
- is_black = number in RouletteGameView.BLACK_NUMBERS
337
- is_even = number != 0 and number % 2 == 0
338
- is_odd = number != 0 and number % 2 == 1
339
-
340
- won = False
341
- multiplier = 2
342
-
343
- if self.bet_type == 'red' and is_red:
344
- won = True
345
- elif self.bet_type == 'black' and is_black:
346
- won = True
347
- elif self.bet_type == 'even' and is_even:
348
- won = True
349
- elif self.bet_type == 'odd' and is_odd:
350
- won = True
351
-
352
- winnings = bet * multiplier if won else -bet
353
-
354
- if won:
355
- await engagement_cog._add_coins(self.guild_id, self.user_id, winnings)
356
- else:
357
- await engagement_cog._remove_coins(self.guild_id, self.user_id, bet)
358
-
359
- view = RouletteGameView(self.cog, self.guild_id, self.user_id)
360
- view.bet_amount = bet
361
- view.bet_type = self.bet_type
362
- embed = view.build_embed(number=number, won=won, winnings=winnings)
363
-
364
- for child in view.children:
365
- child.disabled = False
366
-
367
- await interaction.followup.send(embed=embed, view=view)
368
-
369
-
370
- class RPGView(discord.ui.View):
371
- def __init__(self, cog, guild_id: int, user_id: int):
372
- super().__init__(timeout=None)
373
- self.cog = cog
374
- self.guild_id = guild_id
375
- self.user_id = user_id
376
- self.adventure_image = "https://images.unsplash.com/photo-1519074069444-1ba4fff66d16?w=500"
377
-
378
- def build_embed(self, title: str, description: str, color: discord.Color) -> discord.Embed:
379
- embed = discord.Embed(
380
- title=title,
381
- description=description,
382
- color=color,
383
- )
384
- embed.set_image(url=self.adventure_image)
385
- return embed
386
-
387
- @discord.ui.button(label="⚔️ Fight Monster", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="rpg_fight")
388
- async def fight_monster(self, interaction: discord.Interaction, button: discord.ui.Button):
389
- if interaction.user.id != self.user_id:
390
- return
391
-
392
- engagement_cog = interaction.bot.get_cog("Engagement")
393
- if not engagement_cog:
394
- await interaction.response.send_message("❌ Economy not available.", ephemeral=True)
395
- return
396
-
397
- row = await interaction.bot.db.fetchone(
398
- "SELECT level FROM user_xp WHERE guild_id = ? AND user_id = ?",
399
- self.guild_id, self.user_id,
400
- )
401
- level = row[0] if row else 1
402
-
403
- monsters = [
404
- ("Goblin", 50, 100, 20, 40),
405
- ("Dragon", 200, 500, 50, 100),
406
- ("Skeleton", 30, 80, 15, 30),
407
- ("Orc", 80, 200, 25, 50),
408
- ("Troll", 150, 350, 40, 80),
409
- ("Demon", 300, 600, 60, 120),
410
- ]
411
-
412
- monster_name, min_reward, max_reward, min_xp, max_xp = random.choice(monsters)
413
- win_chance = min(0.8, 0.3 + (level * 0.05))
414
- won = random.random() < win_chance
415
-
416
- if won:
417
- reward = random.randint(min_reward, max_reward) + (level * 10)
418
- xp_gain = random.randint(min_xp, max_xp)
419
- await engagement_cog._add_coins(self.guild_id, self.user_id, reward)
420
- await engagement_cog.add_xp(self.guild_id, self.user_id, xp_gain)
421
-
422
- embed = discord.Embed(
423
- title="⚔️ RPG Adventure - Victory!",
424
- 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)}",
425
- color=NEON_LIME,
426
- )
427
- embed.set_image(url="https://images.unsplash.com/photo-1535905557558-afc4877a26fc?w=500")
428
- else:
429
- penalty = random.randint(10, 50) + (level * 5)
430
- await engagement_cog._remove_coins(self.guild_id, self.user_id, penalty)
431
-
432
- embed = discord.Embed(
433
- title="⚔️ RPG Adventure - Defeat!",
434
- description=f"❌ **The {monster_name} defeated you!**\n\n💀 **-{penalty:,} coins**\n\n💡 Tip: Level up to increase win chance!",
435
- color=NEON_RED,
436
- )
437
- embed.set_image(url="https://images.unsplash.com/photo-1519791883288-dc8bd696e667?w=500")
438
-
439
- if interaction.guild:
440
- await add_banner_to_embed(embed, interaction.guild)
441
-
442
- await interaction.response.send_message(embed=embed, ephemeral=True)
443
-
444
- @discord.ui.button(label="🎁 Find Treasure", emoji=ui("gift"), style=discord.ButtonStyle.success, custom_id="rpg_treasure")
445
- async def find_treasure(self, interaction: discord.Interaction, button: discord.ui.Button):
446
- if interaction.user.id != self.user_id:
447
- return
448
-
449
- engagement_cog = interaction.bot.get_cog("Engagement")
450
- if not engagement_cog:
451
- await interaction.response.send_message("❌ Economy not available.", ephemeral=True)
452
- return
453
-
454
- found = random.random() < 0.4
455
-
456
- if found:
457
- reward = random.randint(30, 150)
458
- await engagement_cog._add_coins(self.guild_id, self.user_id, reward)
459
-
460
- embed = discord.Embed(
461
- title="🎁 Treasure Hunt - Success!",
462
- description=f"✅ **You found a hidden treasure!**\n\n💰 **+{reward:,} coins**\n\n🗺️ The treasure map led you to riches!",
463
- color=NEON_GOLD,
464
- )
465
- embed.set_image(url="https://images.unsplash.com/photo-1518709268805-4e9042af9f23?w=500")
466
- else:
467
- embed = discord.Embed(
468
- title="🎁 Treasure Hunt - Nothing Found",
469
- description="❌ **No treasure found in this area...**\n\n🗺️ Try exploring a different location next time!",
470
- color=NEON_RED,
471
- )
472
- embed.set_image(url="https://images.unsplash.com/photo-1500353391678-d7b57970d9b2?w=500")
473
-
474
- if interaction.guild:
475
- await add_banner_to_embed(embed, interaction.guild)
476
-
477
- await interaction.response.send_message(embed=embed, ephemeral=True)
478
-
479
- @discord.ui.button(label="🏪 Shop", emoji=ui("cart"), style=discord.ButtonStyle.blurple, custom_id="rpg_shop")
480
- async def shop(self, interaction: discord.Interaction, button: discord.ui.Button):
481
- if interaction.user.id != self.user_id:
482
- return
483
-
484
- embed = discord.Embed(
485
- title="🏪 RPG Shop",
486
- description=(
487
- "Welcome to the shop! Buy items to help in your adventures.\n\n"
488
- "🗡️ **Sword** - Increase win chance (Coming Soon)\n"
489
- "🛡️ **Shield** - Reduce coin loss on defeat (Coming Soon)\n"
490
- "🧪 **Potion** - Heal after battle (Coming Soon)\n"
491
- "📜 **Map** - Better treasure finds (Coming Soon)"
492
- ),
493
- color=NEON_CYAN,
494
- )
495
- embed.set_image(url="https://images.unsplash.com/photo-1589829545856-d10d557cf95f?w=500")
496
-
497
- if interaction.guild:
498
- await add_banner_to_embed(embed, interaction.guild)
499
-
500
- await interaction.response.send_message(embed=embed, ephemeral=True)
501
-
502
- @discord.ui.button(label="📊 Stats", emoji=ui("stats"), style=discord.ButtonStyle.secondary, custom_id="rpg_stats")
503
- async def stats(self, interaction: discord.Interaction, button: discord.ui.Button):
504
- if interaction.user.id != self.user_id:
505
- return
506
-
507
- engagement_cog = interaction.bot.get_cog("Engagement")
508
- if not engagement_cog:
509
- return
510
-
511
- row = await interaction.bot.db.fetchone(
512
- "SELECT wallet, bank FROM user_balance WHERE guild_id = ? AND user_id = ?",
513
- self.guild_id, self.user_id,
514
- )
515
- wallet, bank = row if row else (0, 0)
516
-
517
- xp_row = await interaction.bot.db.fetchone(
518
- "SELECT xp, level FROM user_xp WHERE guild_id = ? AND user_id = ?",
519
- self.guild_id, self.user_id,
520
- )
521
- xp, level = xp_row if xp_row else (0, 1)
522
-
523
- embed = discord.Embed(
524
- title="📊 RPG Stats",
525
- description=(
526
- f"**Level:** {level}\n"
527
- f"**XP:** {xp:,}\n"
528
- f"**Wallet:** {wallet:,} coins\n"
529
- f"**Bank:** {bank:,} coins\n"
530
- f"**Total Wealth:** {wallet + bank:,} coins"
531
- ),
532
- color=NEON_PURPLE,
533
- )
534
-
535
- if interaction.guild:
536
- await add_banner_to_embed(embed, interaction.guild)
537
-
538
- await interaction.response.send_message(embed=embed, ephemeral=True)
539
-
540
-
541
- class GamblingPanelView(discord.ui.View):
542
- def __init__(self, cog, guild_id: int, user_id: int):
543
- super().__init__(timeout=None)
544
- self.cog = cog
545
- self.guild_id = guild_id
546
- self.user_id = user_id
547
-
548
- @discord.ui.button(label="🃏 Play Blackjack", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="gambling_blackjack")
549
- async def play_blackjack(self, interaction: discord.Interaction, button: discord.ui.Button):
550
- if interaction.user.id != self.user_id:
551
- return
552
- await interaction.response.send_modal(BlackjackBetModal(self.cog, self.guild_id, self.user_id))
553
-
554
- @discord.ui.button(label="🎰 Play Roulette", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="gambling_roulette")
555
- async def play_roulette(self, interaction: discord.Interaction, button: discord.ui.Button):
556
- if interaction.user.id != self.user_id:
557
- return
558
- view = RouletteGameView(self.cog, self.guild_id, self.user_id)
559
- embed = view.build_embed()
560
- await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
561
-
562
- @discord.ui.button(label="⚔️ RPG Adventure", emoji=ui("game"), style=discord.ButtonStyle.success, custom_id="gambling_rpg")
563
- async def play_rpg(self, interaction: discord.Interaction, button: discord.ui.Button):
564
- if interaction.user.id != self.user_id:
565
- return
566
- view = RPGView(self.cog, self.guild_id, self.user_id)
567
- embed = view.build_embed(
568
- title="⚔️ RPG Adventure",
569
- description="Choose your action below to begin your adventure!",
570
- color=NEON_PURPLE,
571
- )
572
- await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
573
-
574
-
575
- class BlackjackBetModal(discord.ui.Modal, title="🃏 Place Your Bet"):
576
- bet_amount = discord.ui.TextInput(
577
- label="Bet Amount",
578
- placeholder="Enter amount (min 10)",
579
- required=True,
580
- min_length=1,
581
- max_length=10,
582
- )
583
-
584
- def __init__(self, cog, guild_id: int, user_id: int):
585
- super().__init__()
586
- self.cog = cog
587
- self.guild_id = guild_id
588
- self.user_id = user_id
589
-
590
- async def on_submit(self, interaction: discord.Interaction) -> None:
591
- try:
592
- bet = int(self.bet_amount.value.strip())
593
- except ValueError:
594
- await interaction.response.send_message("❌ Invalid bet amount.", ephemeral=True)
595
- return
596
-
597
- if bet < 10:
598
- await interaction.response.send_message("❌ Minimum bet is 10 coins.", ephemeral=True)
599
- return
600
-
601
- engagement_cog = interaction.bot.get_cog("Engagement")
602
- if not engagement_cog:
603
- await interaction.response.send_message("❌ Economy not available.", ephemeral=True)
604
- return
605
-
606
- row = await interaction.bot.db.fetchone(
607
- "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?",
608
- self.guild_id, self.user_id,
609
- )
610
- wallet = row[0] if row else 0
611
-
612
- if wallet < bet:
613
- await interaction.response.send_message(f"❌ Insufficient funds. You have {wallet:,} coins.", ephemeral=True)
614
- return
615
-
616
- view = BlackjackView(self.cog, self.guild_id, self.user_id, bet)
617
- embed = view.build_embed(show_dealer_card=False)
618
- message = await interaction.channel.send(embed=embed, view=view)
619
- view.message = message
620
- await interaction.response.send_message("✅ Blackjack game started!", ephemeral=True)
621
-
622
-
623
- class GamblingPanelView(discord.ui.View):
624
- def __init__(self, cog, guild_id: int, user_id: int):
625
- super().__init__(timeout=None)
626
- self.cog = cog
627
- self.guild_id = guild_id
628
- self.user_id = user_id
629
-
630
- @discord.ui.button(label="🃏 Play Blackjack", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="gambling_blackjack")
631
- async def play_blackjack(self, interaction: discord.Interaction, button: discord.ui.Button):
632
- if interaction.user.id != self.user_id:
633
- return
634
- await interaction.response.send_modal(BlackjackBetModal(self.cog, self.guild_id, self.user_id))
635
-
636
- @discord.ui.button(label="🎰 Play Roulette", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="gambling_roulette")
637
- async def play_roulette(self, interaction: discord.Interaction, button: discord.ui.Button):
638
- if interaction.user.id != self.user_id:
639
- return
640
- view = RouletteGameView(self.cog, self.guild_id, self.user_id)
641
- embed = view.build_embed()
642
- await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
643
-
644
- @discord.ui.button(label="⚔️ RPG Adventure", emoji=ui("game"), style=discord.ButtonStyle.success, custom_id="gambling_rpg")
645
- async def play_rpg(self, interaction: discord.Interaction, button: discord.ui.Button):
646
- if interaction.user.id != self.user_id:
647
- return
648
- view = RPGView(self.cog, self.guild_id, self.user_id)
649
- embed = view.build_embed(
650
- title="⚔️ RPG Adventure",
651
- description="Choose your action below to begin your adventure!",
652
- color=NEON_PURPLE,
653
- )
654
- await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
655
-
656
-
657
- class BlackjackBetModal(discord.ui.Modal, title="🃏 Place Your Bet"):
658
- bet_amount = discord.ui.TextInput(
659
- label="Bet Amount",
660
- placeholder="Enter amount (min 10)",
661
- required=True,
662
- min_length=1,
663
- max_length=10,
664
- )
665
-
666
- def __init__(self, cog, guild_id: int, user_id: int):
667
- super().__init__()
668
- self.cog = cog
669
- self.guild_id = guild_id
670
- self.user_id = user_id
671
-
672
- async def on_submit(self, interaction: discord.Interaction) -> None:
673
- try:
674
- bet = int(self.bet_amount.value.strip())
675
- except ValueError:
676
- await interaction.response.send_message("❌ Invalid bet amount.", ephemeral=True)
677
- return
678
-
679
- if bet < 10:
680
- await interaction.response.send_message("❌ Minimum bet is 10 coins.", ephemeral=True)
681
- return
682
-
683
- engagement_cog = interaction.bot.get_cog("Engagement")
684
- if not engagement_cog:
685
- await interaction.response.send_message("❌ Economy not available.", ephemeral=True)
686
- return
687
-
688
- row = await interaction.bot.db.fetchone(
689
- "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?",
690
- self.guild_id, self.user_id,
691
- )
692
- wallet = row[0] if row else 0
693
-
694
- if wallet < bet:
695
- await interaction.response.send_message(f"❌ Insufficient funds. You have {wallet:,} coins.", ephemeral=True)
696
- return
697
-
698
- view = BlackjackView(self.cog, self.guild_id, self.user_id, bet)
699
- embed = view.build_embed(show_dealer_card=False)
700
- message = await interaction.channel.send(embed=embed, view=view)
701
- view.message = message
702
- await interaction.response.send_message("✅ Blackjack game started!", ephemeral=True)
703
-
704
-
705
  class Gambling(commands.Cog):
706
- def __init__(self, bot: commands.Bot) -> None:
707
- self.bot = bot
708
-
709
- @commands.hybrid_command(name="blackjack", description="Play interactive Blackjack")
710
- async def blackjack(self, ctx: commands.Context, bet: int) -> None:
711
- if bet < 10:
712
- await ctx.send("❌ Minimum bet is 10 coins.", ephemeral=True)
713
- return
714
-
715
- engagement_cog = self.bot.get_cog("Engagement")
716
- if not engagement_cog:
717
- await ctx.send("❌ Economy not available.", ephemeral=True)
718
- return
719
-
720
- row = await self.bot.db.fetchone(
721
- "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?",
722
- ctx.guild.id if ctx.guild else 0, ctx.author.id,
723
- )
724
- wallet = row[0] if row else 0
725
-
726
- if wallet < bet:
727
- await ctx.send(f"❌ Insufficient funds. You have {wallet:,} coins.", ephemeral=True)
728
- return
729
-
730
- view = BlackjackView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id, bet)
731
- embed = view.build_embed(show_dealer_card=False)
732
- message = await ctx.send(embed=embed, view=view)
733
- view.message = message
734
-
735
- @commands.hybrid_command(name="roulette", description="Play interactive Roulette")
736
- async def roulette(self, ctx: commands.Context) -> None:
737
- view = RouletteGameView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id)
738
- embed = view.build_embed()
739
- await ctx.send(embed=embed, view=view)
740
-
741
- @commands.hybrid_command(name="rpg", description="Start an RPG adventure")
742
- async def rpg(self, ctx: commands.Context) -> None:
743
- view = RPGView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id)
744
- embed = view.build_embed(
745
- title="⚔️ RPG Adventure",
746
- description="Choose your action below to begin your adventure!",
747
- color=NEON_PURPLE,
748
- )
749
- await ctx.send(embed=embed, view=view, ephemeral=True)
750
-
751
- @commands.hybrid_command(name="gambling_panel", description="Open the gambling panel")
752
- async def gambling_panel(self, ctx: commands.Context) -> None:
753
- view = GamblingPanelView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id)
754
- embed = discord.Embed(
755
- title="🎰 Casino & Gambling Panel",
756
- description=(
757
- "Welcome to the Casino! Choose a game to play:\n\n"
758
- "🃏 **Blackjack** - Beat the dealer to 21\n"
759
- "🎰 **Roulette** - Bet on numbers and colors\n"
760
- "⚔️ **RPG Adventure** - Fight monsters and find treasure"
761
- ),
762
- color=NEON_GOLD,
763
- )
764
- if ctx.guild:
765
- from bot.theme import add_banner_to_embed
766
- await add_banner_to_embed(embed, ctx.guild)
767
- await ctx.send(embed=embed, view=view, ephemeral=True)
768
-
769
-
770
- async def setup(bot: commands.Bot) -> None:
771
- await bot.add_cog(Gambling(bot))
 
1
+ """
2
+ Interactive Gambling & RPG Cog - Casino Games with UI and Full RPG System
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import random
8
+ import asyncio
9
+ from typing import List, Optional, Dict
10
+
11
+ import discord
12
+ from discord.ext import commands
13
+
14
+ from bot.theme import NEON_CYAN, NEON_LIME, NEON_RED, NEON_GOLD, NEON_PURPLE, NEON_PINK, NEON_BLUE, panel_divider, add_banner_to_embed
15
+ from bot.emojis import ui
16
+
17
+
18
+ class Card:
19
+ def __init__(self, suit: str, rank: str):
20
+ self.suit = suit
21
+ self.rank = rank
22
+ self.value = self._get_value()
23
+
24
+ def _get_value(self) -> int:
25
+ if self.rank in ['J', 'Q', 'K']:
26
+ return 10
27
+ elif self.rank == 'A':
28
+ return 11
29
+ else:
30
+ return int(self.rank)
31
+
32
+ def __str__(self) -> str:
33
+ return f"{self.rank}{self.suit}"
34
+
35
+
36
+ class Deck:
37
+ def __init__(self):
38
+ self.cards = []
39
+ suits = ['♠', '♥', '♦', '♣']
40
+ ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
41
+ for suit in suits:
42
+ for rank in ranks:
43
+ self.cards.append(Card(suit, rank))
44
+ random.shuffle(self.cards)
45
+
46
+ def draw(self) -> Card:
47
+ return self.cards.pop()
48
+
49
+
50
+ class BlackjackView(discord.ui.View):
51
+ def __init__(self, cog, guild_id: int, user_id: int, bet: int):
52
+ super().__init__(timeout=300)
53
+ self.cog = cog
54
+ self.guild_id = guild_id
55
+ self.user_id = user_id
56
+ self.bet = bet
57
+ self.deck = Deck()
58
+ self.player_hand = [self.deck.draw(), self.deck.draw()]
59
+ self.dealer_hand = [self.deck.draw(), self.deck.draw()]
60
+ self.game_over = False
61
+ self.message: Optional[discord.Message] = None
62
+
63
+ def hand_value(self, hand: List[Card]) -> int:
64
+ value = sum(card.value for card in hand)
65
+ aces = sum(1 for card in hand if card.rank == 'A')
66
+ while value > 21 and aces:
67
+ value -= 10
68
+ aces -= 1
69
+ return value
70
+
71
+ def format_hand(self, hand: List[Card], hide_first: bool = False) -> str:
72
+ if hide_first and len(hand) >= 2:
73
+ return f"[❓❓] [{hand[1]}]"
74
+ return ' '.join([f"[{str(card)}]" for card in hand])
75
+
76
+ def build_embed(self, show_dealer_card: bool = True) -> discord.Embed:
77
+ player_value = self.hand_value(self.player_hand)
78
+ dealer_value = self.hand_value(self.dealer_hand) if show_dealer_card else '?'
79
+
80
+ embed = discord.Embed(
81
+ title="🃏 Blackjack Casino",
82
+ description=panel_divider('purple'),
83
+ color=NEON_PURPLE,
84
+ )
85
+
86
+ dealer_display = self.format_hand(self.dealer_hand, hide_first=not show_dealer_card)
87
+ embed.add_field(
88
+ name="🎩 Dealer's Hand",
89
+ value=f"{dealer_display}\n**Value:** {dealer_value}",
90
+ inline=False,
91
+ )
92
+
93
+ player_display = self.format_hand(self.player_hand)
94
+ embed.add_field(
95
+ name="👤 Your Hand",
96
+ value=f"{player_display}\n**Value:** {player_value}",
97
+ inline=False,
98
+ )
99
+
100
+ embed.add_field(name="💰 Bet", value=f"`{self.bet:,}` coins", inline=False)
101
+
102
+ if self.game_over:
103
+ if player_value > 21:
104
+ embed.description = f"❌ **BUST!** You lost `{self.bet:,}` coins."
105
+ embed.color = NEON_RED
106
+ elif dealer_value == 21 and len(self.dealer_hand) == 2:
107
+ embed.description = f"❌ **Dealer Blackjack!** You lost `{self.bet:,}` coins."
108
+ embed.color = NEON_RED
109
+ elif dealer_value > 21:
110
+ winnings = self.bet
111
+ embed.description = f"✅ **Dealer Busts! You Win!** +`{winnings:,}` coins!"
112
+ embed.color = NEON_LIME
113
+ elif player_value > dealer_value:
114
+ winnings = self.bet
115
+ embed.description = f"✅ **You Win!** +`{winnings:,}` coins!"
116
+ embed.color = NEON_LIME
117
+ elif player_value < dealer_value:
118
+ embed.description = f"❌ **Dealer Wins!** You lost `{self.bet:,}` coins."
119
+ embed.color = NEON_RED
120
+ else:
121
+ embed.description = f"🤝 **Push!** Bet returned."
122
+ embed.color = NEON_GOLD
123
+
124
+ return embed
125
+
126
+ async def end_game(self, interaction: discord.Interaction):
127
+ self.game_over = True
128
+
129
+ engagement_cog = interaction.client.get_cog("Engagement")
130
+ if not engagement_cog:
131
+ return
132
+
133
+ player_value = self.hand_value(self.player_hand)
134
+ dealer_value = self.hand_value(self.dealer_hand)
135
+
136
+ if player_value > 21:
137
+ await engagement_cog._remove_coins(self.guild_id, self.user_id, self.bet)
138
+ elif dealer_value == 21 and len(self.dealer_hand) == 2:
139
+ await engagement_cog._remove_coins(self.guild_id, self.user_id, self.bet)
140
+ elif dealer_value > 21 or player_value > dealer_value:
141
+ await engagement_cog._add_coins(self.guild_id, self.user_id, self.bet)
142
+ elif player_value < dealer_value:
143
+ await engagement_cog._remove_coins(self.guild_id, self.user_id, self.bet)
144
+
145
+ embed = self.build_embed(show_dealer_card=True)
146
+ for child in self.children:
147
+ child.disabled = True
148
+ if self.message:
149
+ await self.message.edit(embed=embed, view=self)
150
+
151
+ @discord.ui.button(label="Hit", emoji=ui("plus"), style=discord.ButtonStyle.primary, custom_id="bj_hit")
152
+ async def hit(self, interaction: discord.Interaction, button: discord.ui.Button):
153
+ if self.game_over or interaction.user.id != self.user_id:
154
+ await interaction.response.send_message("❌ Not your game.", ephemeral=True)
155
+ return
156
+
157
+ self.player_hand.append(self.deck.draw())
158
+ player_value = self.hand_value(self.player_hand)
159
+
160
+ if player_value > 21:
161
+ await self.end_game(interaction)
162
+ else:
163
+ embed = self.build_embed(show_dealer_card=False)
164
+ await interaction.response.edit_message(embed=embed, view=self)
165
+
166
+ @discord.ui.button(label="Stand", emoji=ui("stop"), style=discord.ButtonStyle.danger, custom_id="bj_stand")
167
+ async def stand(self, interaction: discord.Interaction, button: discord.ui.Button):
168
+ if self.game_over or interaction.user.id != self.user_id:
169
+ await interaction.response.send_message("❌ Not your game.", ephemeral=True)
170
+ return
171
+
172
+ while self.hand_value(self.dealer_hand) < 17:
173
+ self.dealer_hand.append(self.deck.draw())
174
+
175
+ await self.end_game(interaction)
176
+
177
+ @discord.ui.button(label="Double Down", emoji=ui("star"), style=discord.ButtonStyle.success, custom_id="bj_double")
178
+ async def double_down(self, interaction: discord.Interaction, button: discord.ui.Button):
179
+ if self.game_over or interaction.user.id != self.user_id:
180
+ await interaction.response.send_message("❌ Not your game.", ephemeral=True)
181
+ return
182
+
183
+ if len(self.player_hand) != 2:
184
+ await interaction.response.send_message("❌ Can only double on first turn.", ephemeral=True)
185
+ return
186
+
187
+ engagement_cog = interaction.client.get_cog("Engagement")
188
+ if not engagement_cog:
189
+ return
190
+
191
+ row = await interaction.client.db.fetchone(
192
+ "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?",
193
+ self.guild_id, self.user_id,
194
+ )
195
+ wallet = row[0] if row else 0
196
+
197
+ if wallet < self.bet:
198
+ await interaction.response.send_message("❌ Insufficient funds to double.", ephemeral=True)
199
+ return
200
+
201
+ self.bet *= 2
202
+ self.player_hand.append(self.deck.draw())
203
+
204
+ player_value = self.hand_value(self.player_hand)
205
+ if player_value > 21:
206
+ await self.end_game(interaction)
207
+ else:
208
+ while self.hand_value(self.dealer_hand) < 17:
209
+ self.dealer_hand.append(self.deck.draw())
210
+ await self.end_game(interaction)
211
+
212
+
213
+ class RouletteGameView(discord.ui.View):
214
+ def __init__(self, cog, guild_id: int, user_id: int):
215
+ super().__init__(timeout=300)
216
+ self.cog = cog
217
+ self.guild_id = guild_id
218
+ self.user_id = user_id
219
+ self.bet_amount = 0
220
+ self.bet_type = ""
221
+ self.spinning = False
222
+
223
+ RED_NUMBERS = [1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36]
224
+ BLACK_NUMBERS = [2, 4, 6, 8, 10, 11, 13, 15, 17, 20, 22, 24, 26, 28, 29, 31, 33, 35]
225
+
226
+ def get_color(self, number: int) -> str:
227
+ if number == 0:
228
+ return "<:animatedarrowgreen:1477261279428087979>"
229
+ elif number in self.RED_NUMBERS:
230
+ return "🔴"
231
+ else:
232
+ return "⚫"
233
+
234
+ def build_embed(self, number: Optional[int] = None, won: bool = False, winnings: int = 0) -> discord.Embed:
235
+ embed = discord.Embed(
236
+ title="🎰 Roulette Casino",
237
+ description=panel_divider('gold'),
238
+ color=NEON_GOLD,
239
+ )
240
+
241
+ if number is not None:
242
+ color_emoji = self.get_color(number)
243
+ embed.add_field(name="🎯 Result", value=f"{color_emoji} **{number}**", inline=False)
244
+ embed.add_field(name="💰 Bet", value=f"`{self.bet_amount:,}` on {self.bet_type}", inline=True)
245
+ embed.add_field(name="💵 Result", value=f"`{'+' if won else '-'}{abs(winnings):,}` coins", inline=True)
246
+
247
+ if won:
248
+ embed.description = f"✅ **You Won!** +`{winnings:,}` coins!"
249
+ embed.color = NEON_LIME
250
+ else:
251
+ embed.description = f"❌ **You Lost!** -`{abs(winnings):,}` coins."
252
+ embed.color = NEON_RED
253
+ else:
254
+ embed.description = "Place your bet and spin the wheel!"
255
+ grid = ""
256
+ for i in range(0, 37, 3):
257
+ if i == 0:
258
+ grid += "<:animatedarrowgreen:1477261279428087979> **0** "
259
+ else:
260
+ c1 = "🔴" if i in self.RED_NUMBERS else "��"
261
+ c2 = "🔴" if (i+1) in self.RED_NUMBERS else "⚫"
262
+ c3 = "🔴" if (i+2) in self.RED_NUMBERS else "⚫"
263
+ grid += f"{c1} {i} {c2} {i+1} {c3} {i+2}\n"
264
+ embed.add_field(name="🎡 Roulette Wheel", value=f"```\n{grid}```", inline=False)
265
+
266
+ return embed
267
+
268
+ @discord.ui.button(label="Bet Red 🔴", style=discord.ButtonStyle.danger, custom_id="roulette_red")
269
+ async def bet_red(self, interaction: discord.Interaction, button: discord.ui.Button):
270
+ await self.prompt_bet(interaction, "red")
271
+
272
+ @discord.ui.button(label="Bet Black ⚫", style=discord.ButtonStyle.gray, custom_id="roulette_black")
273
+ async def bet_black(self, interaction: discord.Interaction, button: discord.ui.Button):
274
+ await self.prompt_bet(interaction, "black")
275
+
276
+ @discord.ui.button(label="Bet Even", style=discord.ButtonStyle.primary, custom_id="roulette_even")
277
+ async def bet_even(self, interaction: discord.Interaction, button: discord.ui.Button):
278
+ await self.prompt_bet(interaction, "even")
279
+
280
+ @discord.ui.button(label="Bet Odd", style=discord.ButtonStyle.primary, custom_id="roulette_odd")
281
+ async def bet_odd(self, interaction: discord.Interaction, button: discord.ui.Button):
282
+ await self.prompt_bet(interaction, "odd")
283
+
284
+ async def prompt_bet(self, interaction: discord.Interaction, bet_type: str):
285
+ if self.user_id and interaction.user.id != self.user_id:
286
+ return
287
+ await interaction.response.send_modal(RouletteBetModal(self.cog, self.guild_id, self.user_id, bet_type))
288
+
289
+
290
+ class RouletteBetModal(discord.ui.Modal, title="🎰 Place Your Bet"):
291
+ bet_amount = discord.ui.TextInput(
292
+ label="Bet Amount",
293
+ placeholder="Enter amount (min 10)",
294
+ required=True,
295
+ min_length=1,
296
+ max_length=10,
297
+ )
298
+
299
+ def __init__(self, cog, guild_id: int, user_id: int, bet_type: str):
300
+ super().__init__()
301
+ self.cog = cog
302
+ self.guild_id = guild_id
303
+ self.user_id = user_id
304
+ self.bet_type = bet_type
305
+
306
+ async def on_submit(self, interaction: discord.Interaction) -> None:
307
+ try:
308
+ bet = int(self.bet_amount.value.strip())
309
+ except ValueError:
310
+ await interaction.response.send_message("❌ Invalid bet amount.", ephemeral=True)
311
+ return
312
+
313
+ if bet < 10:
314
+ await interaction.response.send_message("❌ Minimum bet is 10 coins.", ephemeral=True)
315
+ return
316
+
317
+ engagement_cog = interaction.client.get_cog("Engagement")
318
+ if not engagement_cog:
319
+ await interaction.response.send_message("❌ Economy not available.", ephemeral=True)
320
+ return
321
+
322
+ row = await interaction.client.db.fetchone(
323
+ "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?",
324
+ self.guild_id, self.user_id,
325
+ )
326
+ wallet = row[0] if row else 0
327
+
328
+ if wallet < bet:
329
+ await interaction.response.send_message(f"❌ Insufficient funds. You have {wallet:,} coins.", ephemeral=True)
330
+ return
331
+
332
+ await interaction.response.defer()
333
+
334
+ number = random.randint(0, 36)
335
+ is_red = number in RouletteGameView.RED_NUMBERS
336
+ is_black = number in RouletteGameView.BLACK_NUMBERS
337
+ is_even = number != 0 and number % 2 == 0
338
+ is_odd = number != 0 and number % 2 == 1
339
+
340
+ won = False
341
+ multiplier = 2
342
+
343
+ if self.bet_type == 'red' and is_red:
344
+ won = True
345
+ elif self.bet_type == 'black' and is_black:
346
+ won = True
347
+ elif self.bet_type == 'even' and is_even:
348
+ won = True
349
+ elif self.bet_type == 'odd' and is_odd:
350
+ won = True
351
+
352
+ winnings = bet * multiplier if won else -bet
353
+
354
+ if won:
355
+ await engagement_cog._add_coins(self.guild_id, self.user_id, winnings)
356
+ else:
357
+ await engagement_cog._remove_coins(self.guild_id, self.user_id, bet)
358
+
359
+ view = RouletteGameView(self.cog, self.guild_id, self.user_id)
360
+ view.bet_amount = bet
361
+ view.bet_type = self.bet_type
362
+ embed = view.build_embed(number=number, won=won, winnings=winnings)
363
+
364
+ for child in view.children:
365
+ child.disabled = False
366
+
367
+ await interaction.followup.send(embed=embed, view=view)
368
+
369
+
370
+ class RPGView(discord.ui.View):
371
+ def __init__(self, cog, guild_id: int, user_id: int):
372
+ super().__init__(timeout=None)
373
+ self.cog = cog
374
+ self.guild_id = guild_id
375
+ self.user_id = user_id
376
+ self.adventure_image = "https://images.unsplash.com/photo-1519074069444-1ba4fff66d16?w=500"
377
+
378
+ def build_embed(self, title: str, description: str, color: discord.Color) -> discord.Embed:
379
+ embed = discord.Embed(
380
+ title=title,
381
+ description=description,
382
+ color=color,
383
+ )
384
+ embed.set_image(url=self.adventure_image)
385
+ return embed
386
+
387
+ @discord.ui.button(label="⚔️ Fight Monster", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="rpg_fight")
388
+ async def fight_monster(self, interaction: discord.Interaction, button: discord.ui.Button):
389
+ if self.user_id and interaction.user.id != self.user_id:
390
+ return
391
+
392
+ engagement_cog = interaction.client.get_cog("Engagement")
393
+ if not engagement_cog:
394
+ await interaction.response.send_message("❌ Economy not available.", ephemeral=True)
395
+ return
396
+
397
+ row = await interaction.client.db.fetchone(
398
+ "SELECT level FROM user_xp WHERE guild_id = ? AND user_id = ?",
399
+ self.guild_id, self.user_id,
400
+ )
401
+ level = row[0] if row else 1
402
+
403
+ monsters = [
404
+ ("Goblin", 50, 100, 20, 40),
405
+ ("Dragon", 200, 500, 50, 100),
406
+ ("Skeleton", 30, 80, 15, 30),
407
+ ("Orc", 80, 200, 25, 50),
408
+ ("Troll", 150, 350, 40, 80),
409
+ ("Demon", 300, 600, 60, 120),
410
+ ]
411
+
412
+ monster_name, min_reward, max_reward, min_xp, max_xp = random.choice(monsters)
413
+ win_chance = min(0.8, 0.3 + (level * 0.05))
414
+ won = random.random() < win_chance
415
+
416
+ if won:
417
+ reward = random.randint(min_reward, max_reward) + (level * 10)
418
+ xp_gain = random.randint(min_xp, max_xp)
419
+ await engagement_cog._add_coins(self.guild_id, self.user_id, reward)
420
+ await engagement_cog.add_xp(self.guild_id, self.user_id, xp_gain)
421
+
422
+ embed = discord.Embed(
423
+ title="⚔️ RPG Adventure - Victory!",
424
+ 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)}",
425
+ color=NEON_LIME,
426
+ )
427
+ embed.set_image(url="https://images.unsplash.com/photo-1535905557558-afc4877a26fc?w=500")
428
+ else:
429
+ penalty = random.randint(10, 50) + (level * 5)
430
+ await engagement_cog._remove_coins(self.guild_id, self.user_id, penalty)
431
+
432
+ embed = discord.Embed(
433
+ title="⚔️ RPG Adventure - Defeat!",
434
+ description=f"❌ **The {monster_name} defeated you!**\n\n💀 **-{penalty:,} coins**\n\n💡 Tip: Level up to increase win chance!",
435
+ color=NEON_RED,
436
+ )
437
+ embed.set_image(url="https://images.unsplash.com/photo-1519791883288-dc8bd696e667?w=500")
438
+
439
+ if interaction.guild:
440
+ await add_banner_to_embed(embed, interaction.guild)
441
+
442
+ await interaction.response.send_message(embed=embed, ephemeral=True)
443
+
444
+ @discord.ui.button(label="🎁 Find Treasure", emoji=ui("gift"), style=discord.ButtonStyle.success, custom_id="rpg_treasure")
445
+ async def find_treasure(self, interaction: discord.Interaction, button: discord.ui.Button):
446
+ if self.user_id and interaction.user.id != self.user_id:
447
+ return
448
+
449
+ engagement_cog = interaction.client.get_cog("Engagement")
450
+ if not engagement_cog:
451
+ await interaction.response.send_message("❌ Economy not available.", ephemeral=True)
452
+ return
453
+
454
+ found = random.random() < 0.4
455
+
456
+ if found:
457
+ reward = random.randint(30, 150)
458
+ await engagement_cog._add_coins(self.guild_id, self.user_id, reward)
459
+
460
+ embed = discord.Embed(
461
+ title="🎁 Treasure Hunt - Success!",
462
+ description=f"✅ **You found a hidden treasure!**\n\n💰 **+{reward:,} coins**\n\n🗺️ The treasure map led you to riches!",
463
+ color=NEON_GOLD,
464
+ )
465
+ embed.set_image(url="https://images.unsplash.com/photo-1518709268805-4e9042af9f23?w=500")
466
+ else:
467
+ embed = discord.Embed(
468
+ title="🎁 Treasure Hunt - Nothing Found",
469
+ description="❌ **No treasure found in this area...**\n\n🗺️ Try exploring a different location next time!",
470
+ color=NEON_RED,
471
+ )
472
+ embed.set_image(url="https://images.unsplash.com/photo-1500353391678-d7b57970d9b2?w=500")
473
+
474
+ if interaction.guild:
475
+ await add_banner_to_embed(embed, interaction.guild)
476
+
477
+ await interaction.response.send_message(embed=embed, ephemeral=True)
478
+
479
+ @discord.ui.button(label="🏪 Shop", emoji=ui("cart"), style=discord.ButtonStyle.blurple, custom_id="rpg_shop")
480
+ async def shop(self, interaction: discord.Interaction, button: discord.ui.Button):
481
+ if interaction.user.id != self.user_id:
482
+ return
483
+
484
+ embed = discord.Embed(
485
+ title="🏪 RPG Shop",
486
+ description=(
487
+ "Welcome to the shop! Buy items to help in your adventures.\n\n"
488
+ "🗡️ **Sword** - Increase win chance (Coming Soon)\n"
489
+ "🛡️ **Shield** - Reduce coin loss on defeat (Coming Soon)\n"
490
+ "🧪 **Potion** - Heal after battle (Coming Soon)\n"
491
+ "📜 **Map** - Better treasure finds (Coming Soon)"
492
+ ),
493
+ color=NEON_CYAN,
494
+ )
495
+ embed.set_image(url="https://images.unsplash.com/photo-1589829545856-d10d557cf95f?w=500")
496
+
497
+ if interaction.guild:
498
+ await add_banner_to_embed(embed, interaction.guild)
499
+
500
+ await interaction.response.send_message(embed=embed, ephemeral=True)
501
+
502
+ @discord.ui.button(label="📊 Stats", emoji=ui("stats"), style=discord.ButtonStyle.secondary, custom_id="rpg_stats")
503
+ async def stats(self, interaction: discord.Interaction, button: discord.ui.Button):
504
+ if interaction.user.id != self.user_id:
505
+ return
506
+
507
+ engagement_cog = interaction.client.get_cog("Engagement")
508
+ if not engagement_cog:
509
+ return
510
+
511
+ row = await interaction.client.db.fetchone(
512
+ "SELECT wallet, bank FROM user_balance WHERE guild_id = ? AND user_id = ?",
513
+ self.guild_id, self.user_id,
514
+ )
515
+ wallet, bank = row if row else (0, 0)
516
+
517
+ xp_row = await interaction.client.db.fetchone(
518
+ "SELECT xp, level FROM user_xp WHERE guild_id = ? AND user_id = ?",
519
+ self.guild_id, self.user_id,
520
+ )
521
+ xp, level = xp_row if xp_row else (0, 1)
522
+
523
+ embed = discord.Embed(
524
+ title="📊 RPG Stats",
525
+ description=(
526
+ f"**Level:** {level}\n"
527
+ f"**XP:** {xp:,}\n"
528
+ f"**Wallet:** {wallet:,} coins\n"
529
+ f"**Bank:** {bank:,} coins\n"
530
+ f"**Total Wealth:** {wallet + bank:,} coins"
531
+ ),
532
+ color=NEON_PURPLE,
533
+ )
534
+
535
+ if interaction.guild:
536
+ await add_banner_to_embed(embed, interaction.guild)
537
+
538
+ await interaction.response.send_message(embed=embed, ephemeral=True)
539
+
540
+
541
+ class GamblingPanelView(discord.ui.View):
542
+ def __init__(self, cog, guild_id: int, user_id: int):
543
+ super().__init__(timeout=None)
544
+ self.cog = cog
545
+ self.guild_id = guild_id
546
+ self.user_id = user_id
547
+
548
+ @discord.ui.button(label="🃏 Play Blackjack", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="gambling_blackjack")
549
+ async def play_blackjack(self, interaction: discord.Interaction, button: discord.ui.Button):
550
+ if interaction.user.id != self.user_id:
551
+ return
552
+ await interaction.response.send_modal(BlackjackBetModal(self.cog, self.guild_id, self.user_id))
553
+
554
+ @discord.ui.button(label="🎰 Play Roulette", emoji=ui("game"), style=discord.ButtonStyle.danger, custom_id="gambling_roulette")
555
+ async def play_roulette(self, interaction: discord.Interaction, button: discord.ui.Button):
556
+ if interaction.user.id != self.user_id:
557
+ return
558
+ view = RouletteGameView(self.cog, self.guild_id, self.user_id)
559
+ embed = view.build_embed()
560
+ await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
561
+
562
+ @discord.ui.button(label="⚔️ RPG Adventure", emoji=ui("game"), style=discord.ButtonStyle.success, custom_id="gambling_rpg")
563
+ async def play_rpg(self, interaction: discord.Interaction, button: discord.ui.Button):
564
+ if interaction.user.id != self.user_id:
565
+ return
566
+ view = RPGView(self.cog, self.guild_id, self.user_id)
567
+ embed = view.build_embed(
568
+ title="⚔️ RPG Adventure",
569
+ description="Choose your action below to begin your adventure!",
570
+ color=NEON_PURPLE,
571
+ )
572
+ await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
573
+
574
+
575
+ class BlackjackBetModal(discord.ui.Modal, title="🃏 Place Your Bet"):
576
+ bet_amount = discord.ui.TextInput(
577
+ label="Bet Amount",
578
+ placeholder="Enter amount (min 10)",
579
+ required=True,
580
+ min_length=1,
581
+ max_length=10,
582
+ )
583
+
584
+ def __init__(self, cog, guild_id: int, user_id: int):
585
+ super().__init__()
586
+ self.cog = cog
587
+ self.guild_id = guild_id
588
+ self.user_id = user_id
589
+
590
+ async def on_submit(self, interaction: discord.Interaction) -> None:
591
+ try:
592
+ bet = int(self.bet_amount.value.strip())
593
+ except ValueError:
594
+ await interaction.response.send_message("❌ Invalid bet amount.", ephemeral=True)
595
+ return
596
+
597
+ if bet < 10:
598
+ await interaction.response.send_message("❌ Minimum bet is 10 coins.", ephemeral=True)
599
+ return
600
+
601
+ engagement_cog = interaction.client.get_cog("Engagement")
602
+ if not engagement_cog:
603
+ await interaction.response.send_message("❌ Economy not available.", ephemeral=True)
604
+ return
605
+
606
+ row = await interaction.client.db.fetchone(
607
+ "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?",
608
+ self.guild_id, self.user_id,
609
+ )
610
+ wallet = row[0] if row else 0
611
+
612
+ if wallet < bet:
613
+ await interaction.response.send_message(f"❌ Insufficient funds. You have {wallet:,} coins.", ephemeral=True)
614
+ return
615
+
616
+ view = BlackjackView(self.cog, self.guild_id, self.user_id, bet)
617
+ embed = view.build_embed(show_dealer_card=False)
618
+ message = await interaction.channel.send(embed=embed, view=view)
619
+ view.message = message
620
+ await interaction.response.send_message("✅ Blackjack game started!", ephemeral=True)
621
+
622
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
  class Gambling(commands.Cog):
624
+ def __init__(self, bot: commands.Bot) -> None:
625
+ self.bot = bot
626
+
627
+ @commands.hybrid_command(name="blackjack", description="Play interactive Blackjack")
628
+ async def blackjack(self, ctx: commands.Context, bet: int) -> None:
629
+ if bet < 10:
630
+ await ctx.send("❌ Minimum bet is 10 coins.", ephemeral=True)
631
+ return
632
+
633
+ engagement_cog = self.bot.get_cog("Engagement")
634
+ if not engagement_cog:
635
+ await ctx.send("❌ Economy not available.", ephemeral=True)
636
+ return
637
+
638
+ row = await self.bot.db.fetchone(
639
+ "SELECT wallet FROM user_balance WHERE guild_id = ? AND user_id = ?",
640
+ ctx.guild.id if ctx.guild else 0, ctx.author.id,
641
+ )
642
+ wallet = row[0] if row else 0
643
+
644
+ if wallet < bet:
645
+ await ctx.send(f"❌ Insufficient funds. You have {wallet:,} coins.", ephemeral=True)
646
+ return
647
+
648
+ view = BlackjackView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id, bet)
649
+ embed = view.build_embed(show_dealer_card=False)
650
+ message = await ctx.send(embed=embed, view=view)
651
+ view.message = message
652
+
653
+ @commands.hybrid_command(name="roulette", description="Play interactive Roulette")
654
+ async def roulette(self, ctx: commands.Context) -> None:
655
+ view = RouletteGameView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id)
656
+ embed = view.build_embed()
657
+ await ctx.send(embed=embed, view=view)
658
+
659
+ @commands.hybrid_command(name="rpg", description="Start an RPG adventure")
660
+ async def rpg(self, ctx: commands.Context) -> None:
661
+ view = RPGView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id)
662
+ embed = view.build_embed(
663
+ title="⚔️ RPG Adventure",
664
+ description="Choose your action below to begin your adventure!",
665
+ color=NEON_PURPLE,
666
+ )
667
+ await ctx.send(embed=embed, view=view, ephemeral=True)
668
+
669
+ @commands.hybrid_command(name="gambling_panel", description="Open the gambling panel")
670
+ async def gambling_panel(self, ctx: commands.Context) -> None:
671
+ view = GamblingPanelView(self, ctx.guild.id if ctx.guild else 0, ctx.author.id)
672
+ embed = discord.Embed(
673
+ title="🎰 Casino & Gambling Panel",
674
+ description=(
675
+ "Welcome to the Casino! Choose a game to play:\n\n"
676
+ "🃏 **Blackjack** - Beat the dealer to 21\n"
677
+ "🎰 **Roulette** - Bet on numbers and colors\n"
678
+ "⚔️ **RPG Adventure** - Fight monsters and find treasure"
679
+ ),
680
+ color=NEON_GOLD,
681
+ )
682
+ if ctx.guild:
683
+ from bot.theme import add_banner_to_embed
684
+ await add_banner_to_embed(embed, ctx.guild)
685
+ await ctx.send(embed=embed, view=view, ephemeral=True)
686
+
687
+
688
+ async def setup(bot: commands.Bot) -> None:
689
+ await bot.add_cog(Gambling(bot))
bot/cogs/language.py CHANGED
@@ -23,6 +23,7 @@ LANGUAGE_META: dict[str, tuple[str, str]] = {
23
  "id": ("Indonesia", "🇮🇩"),
24
  "ja": ("日本語", "🇯🇵"),
25
  "zh": ("中文", "🇨🇳"),
 
26
  }
27
 
28
 
@@ -32,7 +33,10 @@ class LanguageSelect(discord.ui.Select):
32
  self.guild_id = guild_id
33
 
34
  options: list[discord.SelectOption] = []
35
- for code in SUPPORTED_LANGUAGES:
 
 
 
36
  label, emoji = LANGUAGE_META.get(code, (code.upper(), "🌐"))
37
  options.append(
38
  discord.SelectOption(
@@ -58,6 +62,17 @@ class LanguageSelect(discord.ui.Select):
58
  return
59
 
60
  selected = self.values[0]
 
 
 
 
 
 
 
 
 
 
 
61
  await self.cog.bot.db.execute(
62
  "INSERT INTO guild_config(guild_id, guild_language) VALUES (?, ?) "
63
  "ON CONFLICT(guild_id) DO UPDATE SET guild_language = excluded.guild_language",
@@ -98,14 +113,18 @@ class Language(commands.Cog):
98
 
99
  async def _current_code(self, guild_id: int) -> str:
100
  row = await self.bot.db.fetchone("SELECT guild_language FROM guild_config WHERE guild_id = ?", guild_id)
101
- return row[0] if row and row[0] in LANGUAGE_META else "ar"
 
102
 
103
  async def _language_embed(self, guild_id: int, current_code: str) -> discord.Embed:
104
  current_name = LANGUAGE_META.get(current_code, (current_code.upper(), "🌐"))[0]
105
  current_emoji = LANGUAGE_META.get(current_code, (current_code.upper(), "🌐"))[1]
106
 
107
  lines = []
108
- for code in LANGUAGE_META:
 
 
 
109
  name, emoji = LANGUAGE_META.get(code, (code.upper(), "🌐"))
110
  marker = "✅" if code == current_code else "▫️"
111
  lines.append(f"{marker} {emoji} **{name}** `({code})`")
 
23
  "id": ("Indonesia", "🇮🇩"),
24
  "ja": ("日本語", "🇯🇵"),
25
  "zh": ("中文", "🇨🇳"),
26
+ "he": ("עברית", "🇮🇱"),
27
  }
28
 
29
 
 
33
  self.guild_id = guild_id
34
 
35
  options: list[discord.SelectOption] = []
36
+ dynamic_supported = sorted(
37
+ getattr(getattr(cog.bot, "translator", None), "supported_languages", set(SUPPORTED_LANGUAGES))
38
+ )
39
+ for code in dynamic_supported:
40
  label, emoji = LANGUAGE_META.get(code, (code.upper(), "🌐"))
41
  options.append(
42
  discord.SelectOption(
 
62
  return
63
 
64
  selected = self.values[0]
65
+
66
+ # Special rule requested by owner: Hebrew is visible but cannot be set.
67
+ if selected == "he":
68
+ current_code = await self.cog._current_code(interaction.guild.id)
69
+ for option in self.options:
70
+ option.default = option.value == current_code
71
+ embed = await self.cog._language_embed(interaction.guild.id, current_code)
72
+ await interaction.response.edit_message(embed=embed, view=self.view)
73
+ await interaction.followup.send("only you know this language", ephemeral=True)
74
+ return
75
+
76
  await self.cog.bot.db.execute(
77
  "INSERT INTO guild_config(guild_id, guild_language) VALUES (?, ?) "
78
  "ON CONFLICT(guild_id) DO UPDATE SET guild_language = excluded.guild_language",
 
113
 
114
  async def _current_code(self, guild_id: int) -> str:
115
  row = await self.bot.db.fetchone("SELECT guild_language FROM guild_config WHERE guild_id = ?", guild_id)
116
+ supported = set(getattr(getattr(self.bot, "translator", None), "supported_languages", set(SUPPORTED_LANGUAGES)))
117
+ return row[0] if row and row[0] in supported else "ar"
118
 
119
  async def _language_embed(self, guild_id: int, current_code: str) -> discord.Embed:
120
  current_name = LANGUAGE_META.get(current_code, (current_code.upper(), "🌐"))[0]
121
  current_emoji = LANGUAGE_META.get(current_code, (current_code.upper(), "🌐"))[1]
122
 
123
  lines = []
124
+ supported = sorted(
125
+ getattr(getattr(self.bot, "translator", None), "supported_languages", set(SUPPORTED_LANGUAGES))
126
+ )
127
+ for code in supported:
128
  name, emoji = LANGUAGE_META.get(code, (code.upper(), "🌐"))
129
  marker = "✅" if code == current_code else "▫️"
130
  lines.append(f"{marker} {emoji} **{name}** `({code})`")
bot/cogs/media.py CHANGED
@@ -87,12 +87,21 @@ if wavelink is not None:
87
  else:
88
  self._connection_event.set()
89
 
90
- from bot.theme import NEON_CYAN, NEON_ORANGE, NEON_LIME, NEON_PURPLE, panel_divider
 
 
 
 
 
 
 
 
 
91
  from bot.emojis import resolve_emoji_value, set_emoji_bot
92
 
93
  # Import views from separate module
94
  from .media_helpers import (
95
- MusicPanelView, QueueView, FiltersView, FiltersPanelView,
96
  AUDIO_FILTERS, get_filter_emoji, safe_defer, safe_send, safe_edit,
97
  safe_interaction
98
  )
@@ -469,44 +478,6 @@ class SuggestionSelect(discord.ui.Select):
469
  return
470
 
471
  selected = self.suggestions[selected_index]
472
-
473
- # Get guild and player
474
- guild = interaction.guild
475
- if not guild:
476
- await interaction.followup.send("Server only.", ephemeral=True)
477
- return
478
-
479
- player = guild.voice_client
480
- if not player or not isinstance(player, wavelink.Player):
481
- await interaction.followup.send("Not connected to voice.", ephemeral=True)
482
- return
483
-
484
- # Add ALL suggestions to wavelink queue first (in order)
485
- for idx, item in enumerate(self.suggestions):
486
- try:
487
- search_q = item.query if item.query else f"ytsearch1:{item.title}"
488
- results = await wavelink.Playable.search(search_q)
489
- if results:
490
- await player.queue.put_wait(results[0])
491
- except Exception:
492
- continue
493
-
494
- # Now play the selected track immediately by moving it to front
495
- # Get the selected track from queue and move to front
496
- queue_list = list(player.queue)
497
- if selected_index < len(queue_list):
498
- selected_track = queue_list.pop(selected_index)
499
- # Clear queue and re-add with selected first
500
- player.queue._queue.clear()
501
- player.queue._queue.append(selected_track)
502
- for track in queue_list:
503
- player.queue._queue.append(track)
504
-
505
- # Play the selected track
506
- if player.queue:
507
- track_to_play = player.queue.get()
508
- await player.play(track_to_play)
509
-
510
  choice = (selected.query or selected.title).strip()
511
  result = await self.cog.play_from_query(interaction, choice)
512
  await interaction.followup.send(
@@ -741,6 +712,9 @@ class Media(commands.Cog):
741
  or os.getenv("YT_API_KEY", "").strip()
742
  )
743
  self._youtube_region_code = (os.getenv("YOUTUBE_REGION_CODE", "US").strip() or "US").upper()
 
 
 
744
 
745
  if hasattr(self.bot, "logger"):
746
  if self._ffmpeg_path:
@@ -751,6 +725,7 @@ class Media(commands.Cog):
751
  async def cog_load(self) -> None:
752
  """Set up event listeners for wavelink."""
753
  self.bot.add_view(MusicPanelView(self))
 
754
 
755
  # Register wavelink event listeners
756
  if wavelink is not None:
@@ -1182,6 +1157,7 @@ class Media(commands.Cog):
1182
 
1183
  def _to_lavalink_identifier(self, query: str) -> str:
1184
  normalized = self._sanitize_query(query)
 
1185
  prefixes = ("ytsearch1:", "ytsearch:", "ytmsearch:")
1186
  while True:
1187
  lowered = normalized.casefold()
@@ -1189,19 +1165,124 @@ class Media(commands.Cog):
1189
  for prefix in prefixes:
1190
  if lowered.startswith(prefix):
1191
  normalized = normalized[len(prefix):].strip()
 
1192
  matched = True
1193
  break
1194
  if not matched:
1195
  break
 
 
1196
  if self._looks_like_url(normalized):
 
 
 
1197
  return normalized
1198
  return f"ytsearch:{normalized}"
1199
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1200
  def _guild_state(self, guild_id: int) -> GuildPlaybackState:
1201
  if guild_id not in self.state:
1202
  self.state[guild_id] = GuildPlaybackState()
1203
  return self.state[guild_id]
1204
 
 
 
 
 
 
 
 
 
 
 
 
1205
  async def _dj_permitted(self, ctx_or_interaction: commands.Context | discord.Interaction) -> bool:
1206
  return True
1207
 
@@ -1535,7 +1616,13 @@ class Media(commands.Cog):
1535
  continue
1536
  direct = str(entry.get("url") or entry.get("webpage_url") or "").strip()
1537
  vid = str(entry.get("id") or "").strip()
 
 
1538
  if direct and direct.startswith("http"):
 
 
 
 
1539
  urls.append(direct)
1540
  continue
1541
  if direct and not direct.startswith("http") and "youtube" in playlist_url:
@@ -1543,9 +1630,14 @@ class Media(commands.Cog):
1543
  continue
1544
  if vid:
1545
  urls.append(f"https://www.youtube.com/watch?v={vid}")
 
 
 
 
1546
  return urls
1547
 
1548
- return await asyncio.to_thread(_run)
 
1549
 
1550
  async def _resolve_query_with_ytdlp(self, query: str) -> str | None:
1551
  """Resolve a text query to a direct video URL using yt-dlp search."""
@@ -1656,7 +1748,11 @@ class Media(commands.Cog):
1656
  lines.append(f"*...and {len(queue) - 5} more*")
1657
  embed.add_field(name=up_next, value="\n".join(lines), inline=False)
1658
  else:
1659
- embed.add_field(name=up_next, value="Queue is empty.", inline=False)
 
 
 
 
1660
 
1661
  status_icon = "📡"
1662
  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):
1674
 
1675
  server_name = guild.name if guild else "Server"
1676
  embed.set_footer(text=f"⛩️ 〣 🔄 Auto-refreshing every 10s • {server_name} 〣 🏮")
 
 
1677
 
1678
  return embed
1679
 
@@ -1807,7 +1905,7 @@ class Media(commands.Cog):
1807
  self.now_playing.pop(guild.id, None)
1808
  if lang == "ar":
1809
  return "⏭️ تم التخطي. الطابور فارغ."
1810
- return "⏭️ Skipped. Queue is empty."
1811
 
1812
  # For non-wavelink players
1813
  if not self._voice_is_playing(player):
@@ -1982,7 +2080,7 @@ class Media(commands.Cog):
1982
  tracks = ([now] if now else []) + list(state_queue)
1983
  uris = [t.webpage_url for t in tracks if t and t.webpage_url]
1984
  if not uris:
1985
- return "Queue is empty."
1986
  cleaned_name = (playlist_name or "quicksave").strip()[:40]
1987
  await self.bot.db.execute(
1988
  "INSERT INTO saved_playlists(user_id, name, tracks_json) VALUES (?, ?, ?) "
@@ -2000,7 +2098,7 @@ class Media(commands.Cog):
2000
  )
2001
  embed = discord.Embed(title="💾 Saved Playlists", color=NEON_CYAN)
2002
  if not rows:
2003
- embed.description = "No playlists saved yet. Use the **Save Queue** button."
2004
  return embed
2005
  lines: list[str] = []
2006
  for name, tracks_json, created_at in rows:
@@ -2057,16 +2155,17 @@ class Media(commands.Cog):
2057
  self.queues.setdefault(guild.id, [])
2058
  count = 0
2059
  first_track: str | None = None
2060
- for url in urls[:100]:
2061
  try:
2062
- results = await wavelink.Playable.search(url)
 
2063
  except Exception as exc:
2064
  await self._log_media_issue(guild, "saved_playlist_play_search", url, exc)
2065
  continue
2066
  if not results:
2067
  continue
2068
  wl_items = list(results.tracks) if (wavelink is not None and isinstance(results, wavelink.Playlist)) else list(results)
2069
- for wl_track in wl_items[:100]:
2070
  track = self._wavelink_to_track(wl_track, actor.id)
2071
  if count == 0 and not player.playing and not player.paused:
2072
  await player.play(wl_track, volume=self._guild_state(guild.id).volume)
@@ -2076,6 +2175,11 @@ class Media(commands.Cog):
2076
  self.queues[guild.id].append(track)
2077
  await player.queue.put_wait(wl_track)
2078
  count += 1
 
 
 
 
 
2079
 
2080
  if count == 0:
2081
  if lang == "ar":
@@ -2180,7 +2284,32 @@ class Media(commands.Cog):
2180
 
2181
  urls: list[str] = []
2182
  if self._looks_like_playlist(source):
2183
- urls = await self._extract_playlist_entries(source, limit=100)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2184
  else:
2185
  chosen = source
2186
  if not self._looks_like_url(source):
@@ -2190,7 +2319,7 @@ class Media(commands.Cog):
2190
  if chosen:
2191
  urls = [chosen]
2192
 
2193
- urls = [u for u in urls if u][:100]
2194
  if not urls:
2195
  if lang == "ar":
2196
  return "تعذر استخراج مقاطع للحفظ."
@@ -2252,7 +2381,7 @@ class Media(commands.Cog):
2252
  tracks = self._get_saved_playlist(guild_id)
2253
  embed = discord.Embed(title="📚 Saved Playlist", color=NEON_CYAN)
2254
  if not tracks:
2255
- embed.description = "No saved tracks yet. Use `/music playlist_add <query>`."
2256
  return embed
2257
  lines = [
2258
  f"**{idx}.** {track.title[:70]} • `{track.format_duration()}`"
@@ -2467,20 +2596,22 @@ class Media(commands.Cog):
2467
  )
2468
  if is_supported_playlist:
2469
  try:
2470
- pre_extracted_urls = await self._extract_playlist_entries(playlist_query, limit=100)
2471
  except Exception:
2472
  pre_extracted_urls = []
2473
 
2474
  try:
2475
  if pre_extracted_urls:
2476
  results = []
2477
- for url in pre_extracted_urls:
2478
- found = await wavelink.Playable.search(url)
2479
  if found:
2480
  results.append(found[0])
 
 
2481
  else:
2482
  # Search for playlist
2483
- results = await wavelink.Playable.search(playlist_query)
2484
  except Exception:
2485
  if lang == "ar":
2486
  return "تعذر تحميل قائمة التشغيل."
@@ -2505,7 +2636,7 @@ class Media(commands.Cog):
2505
  first = True
2506
 
2507
  # Process tracks in order
2508
- for wl_track in tracks_list[:100]: # Limit to 100 tracks
2509
  try:
2510
  track = self._wavelink_to_track(wl_track, actor.id)
2511
 
@@ -2552,8 +2683,7 @@ class Media(commands.Cog):
2552
 
2553
  @commands.hybrid_group(name="music", fallback="panel", description="Music controls", with_app_command=False)
2554
  async def music_group(self, ctx: commands.Context) -> None:
2555
- if ctx.interaction and not ctx.interaction.response.is_done():
2556
- await ctx.interaction.response.defer(ephemeral=True)
2557
  guild_id = ctx.guild.id if ctx.guild else None
2558
  embed = await self._music_panel_embed(guild_id)
2559
  panel_view = MusicPanelView(self, guild_id)
@@ -2563,8 +2693,7 @@ class Media(commands.Cog):
2563
 
2564
  @commands.hybrid_command(name="music_panel", description="Open interactive music panel")
2565
  async def music_panel(self, ctx: commands.Context) -> None:
2566
- if ctx.interaction and not ctx.interaction.response.is_done():
2567
- await ctx.interaction.response.defer(ephemeral=True)
2568
  guild_id = ctx.guild.id if ctx.guild else None
2569
  embed = await self._music_panel_embed(guild_id)
2570
  panel_view = MusicPanelView(self, guild_id)
@@ -2586,14 +2715,12 @@ class Media(commands.Cog):
2586
  guild_id = ctx.guild.id if ctx.guild else None
2587
  embed = await self._music_panel_embed(guild_id)
2588
  panel_view = MusicPanelView(self, guild_id)
2589
- if ctx.interaction and not ctx.interaction.response.is_done():
2590
- await ctx.interaction.response.defer(ephemeral=True)
2591
  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))
2592
  if isinstance(panel_msg, discord.Message):
2593
  await panel_view.start_auto_refresh(panel_msg)
2594
  return
2595
- if ctx.interaction and not ctx.interaction.response.is_done():
2596
- await ctx.defer()
2597
  if not self._looks_like_url(query):
2598
  suggestions = await self._unified_play_search(query, limit=10)
2599
  if suggestions:
@@ -2678,22 +2805,19 @@ class Media(commands.Cog):
2678
 
2679
  @music_group.command(name="playlist_add", description="Add a track to saved playlist only")
2680
  async def music_playlist_add(self, ctx: commands.Context, *, query: str) -> None:
2681
- if ctx.interaction and not ctx.interaction.response.is_done():
2682
- await ctx.defer()
2683
  result = await self.add_query_to_saved_playlist(ctx, query)
2684
  await ctx.reply(result)
2685
 
2686
  @music_group.command(name="playlist_play", description="Play one of your saved playlists by name")
2687
  async def music_playlist_play(self, ctx: commands.Context, *, name: str) -> None:
2688
- if ctx.interaction and not ctx.interaction.response.is_done():
2689
- await ctx.defer()
2690
  result = await self.play_user_saved_playlist(ctx, name)
2691
  await ctx.reply(result)
2692
 
2693
  @music_group.command(name="playlist_save", description="Save URL/query (track or playlist) into named playlist")
2694
  async def music_playlist_save(self, ctx: commands.Context, name: str, *, source: str) -> None:
2695
- if ctx.interaction and not ctx.interaction.response.is_done():
2696
- await ctx.defer(ephemeral=True)
2697
  result = await self.save_query_to_named_playlist(ctx, name, source)
2698
  if ctx.interaction:
2699
  await ctx.interaction.followup.send(result, ephemeral=True)
@@ -2702,8 +2826,7 @@ class Media(commands.Cog):
2702
 
2703
  @music_group.command(name="playlist_delete", description="Delete one of your saved playlists by name")
2704
  async def music_playlist_delete(self, ctx: commands.Context, *, name: str) -> None:
2705
- if ctx.interaction and not ctx.interaction.response.is_done():
2706
- await ctx.defer(ephemeral=True)
2707
  result = await self.delete_user_playlist(ctx, name)
2708
  if ctx.interaction:
2709
  await ctx.interaction.followup.send(result, ephemeral=True)
@@ -2712,8 +2835,7 @@ class Media(commands.Cog):
2712
 
2713
  @music_group.command(name="playlist_rename", description="Rename one of your saved playlists")
2714
  async def music_playlist_rename(self, ctx: commands.Context, old_name: str, *, new_name: str) -> None:
2715
- if ctx.interaction and not ctx.interaction.response.is_done():
2716
- await ctx.defer(ephemeral=True)
2717
  result = await self.rename_user_playlist(ctx, old_name, new_name)
2718
  if ctx.interaction:
2719
  await ctx.interaction.followup.send(result, ephemeral=True)
@@ -2725,11 +2847,18 @@ class Media(commands.Cog):
2725
  if not ctx.guild:
2726
  await ctx.reply("Server only.")
2727
  return
2728
- if ctx.interaction and not ctx.interaction.response.is_done():
2729
- await ctx.defer()
2730
  normalized = self._sanitize_query(query)
2731
  if not normalized:
2732
- await ctx.reply("Type a search query.")
 
 
 
 
 
 
 
 
2733
  return
2734
 
2735
  used_api = bool((self._youtube_api_key or "").strip())
@@ -2749,7 +2878,15 @@ class Media(commands.Cog):
2749
  )
2750
  await ctx.reply("No direct YouTube results found. Try one of these:", embed=fallback_embed)
2751
  return
2752
- await ctx.reply("No YouTube results found.")
 
 
 
 
 
 
 
 
2753
  return
2754
 
2755
  embed = discord.Embed(
@@ -2771,8 +2908,7 @@ class Media(commands.Cog):
2771
 
2772
  @commands.hybrid_command(name="playlists", description="View your saved playlists")
2773
  async def playlists(self, ctx: commands.Context) -> None:
2774
- if ctx.interaction and not ctx.interaction.response.is_done():
2775
- await ctx.defer(ephemeral=True)
2776
  embed = await self.user_playlists_embed(ctx.author.id)
2777
  if ctx.interaction:
2778
  await ctx.interaction.followup.send(embed=embed, ephemeral=True)
@@ -2792,7 +2928,15 @@ class Media(commands.Cog):
2792
  return
2793
  queue = self.queues.get(ctx.guild.id, [])
2794
  if len(queue) < 2:
2795
- await ctx.reply("Need at least 2 tracks to shuffle.")
 
 
 
 
 
 
 
 
2796
  return
2797
  random.shuffle(queue)
2798
 
@@ -3007,7 +3151,15 @@ class Media(commands.Cog):
3007
  if lang == "ar":
3008
  await ctx.reply("❌ الطابور فارغ.")
3009
  else:
3010
- await ctx.reply("❌ Queue is empty.")
 
 
 
 
 
 
 
 
3011
  return
3012
 
3013
  # Convert to 0-indexed
@@ -3062,7 +3214,15 @@ class Media(commands.Cog):
3062
  if lang == "ar":
3063
  await ctx.reply("❌ الطابور فارغ.")
3064
  else:
3065
- await ctx.reply("❌ Queue is empty.")
 
 
 
 
 
 
 
 
3066
  return
3067
 
3068
  # Convert to 0-indexed
@@ -3115,7 +3275,15 @@ class Media(commands.Cog):
3115
  if lang == "ar":
3116
  await ctx.reply("❌ الطابور فارغ.")
3117
  else:
3118
- await ctx.reply("❌ Queue is empty.")
 
 
 
 
 
 
 
 
3119
  return
3120
 
3121
  idx = position - 1
@@ -3164,7 +3332,15 @@ class Media(commands.Cog):
3164
  if lang == "ar":
3165
  await ctx.reply("❌ الطابور فارغ.")
3166
  else:
3167
- await ctx.reply("❌ Queue is empty.")
 
 
 
 
 
 
 
 
3168
  return
3169
 
3170
  idx = position - 1
 
87
  else:
88
  self._connection_event.set()
89
 
90
+ from bot.theme import (
91
+ NEON_CYAN,
92
+ NEON_ORANGE,
93
+ NEON_LIME,
94
+ NEON_PURPLE,
95
+ panel_divider,
96
+ idle_embed_for_guild,
97
+ idle_text,
98
+ add_banner_to_embed,
99
+ )
100
  from bot.emojis import resolve_emoji_value, set_emoji_bot
101
 
102
  # Import views from separate module
103
  from .media_helpers import (
104
+ MusicPanelView, QueueView, FiltersView, FiltersPanelView, AudioActionsView,
105
  AUDIO_FILTERS, get_filter_emoji, safe_defer, safe_send, safe_edit,
106
  safe_interaction
107
  )
 
478
  return
479
 
480
  selected = self.suggestions[selected_index]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  choice = (selected.query or selected.title).strip()
482
  result = await self.cog.play_from_query(interaction, choice)
483
  await interaction.followup.send(
 
712
  or os.getenv("YT_API_KEY", "").strip()
713
  )
714
  self._youtube_region_code = (os.getenv("YOUTUBE_REGION_CODE", "US").strip() or "US").upper()
715
+ self._playlist_track_limit = max(50, min(500, int((os.getenv("PLAYLIST_TRACK_LIMIT", "300").strip() or "300"))))
716
+ self._playlist_batch_sleep = max(0.05, min(1.0, float((os.getenv("PLAYLIST_BATCH_SLEEP", "0.35").strip() or "0.35"))))
717
+ self._resolved_url_query_cache: dict[str, str] = {}
718
 
719
  if hasattr(self.bot, "logger"):
720
  if self._ffmpeg_path:
 
725
  async def cog_load(self) -> None:
726
  """Set up event listeners for wavelink."""
727
  self.bot.add_view(MusicPanelView(self))
728
+ self.bot.add_view(AudioActionsView(self))
729
 
730
  # Register wavelink event listeners
731
  if wavelink is not None:
 
1157
 
1158
  def _to_lavalink_identifier(self, query: str) -> str:
1159
  normalized = self._sanitize_query(query)
1160
+ forced_search = False
1161
  prefixes = ("ytsearch1:", "ytsearch:", "ytmsearch:")
1162
  while True:
1163
  lowered = normalized.casefold()
 
1165
  for prefix in prefixes:
1166
  if lowered.startswith(prefix):
1167
  normalized = normalized[len(prefix):].strip()
1168
+ forced_search = True
1169
  matched = True
1170
  break
1171
  if not matched:
1172
  break
1173
+ if forced_search:
1174
+ return f"ytsearch:{normalized}"
1175
  if self._looks_like_url(normalized):
1176
+ lowered = normalized.casefold()
1177
+ if "open.spotify.com/track/" in lowered or "music.apple.com/" in lowered:
1178
+ return f"ytsearch:{normalized}"
1179
  return normalized
1180
  return f"ytsearch:{normalized}"
1181
 
1182
+ async def _resolve_music_url_to_search_term(self, raw_url: str) -> str | None:
1183
+ url = (raw_url or "").strip()
1184
+ if not url:
1185
+ return None
1186
+ cached = self._resolved_url_query_cache.get(url)
1187
+ if cached:
1188
+ return cached
1189
+
1190
+ lowered = url.casefold()
1191
+ timeout = aiohttp.ClientTimeout(total=5)
1192
+
1193
+ # Spotify: lightweight oEmbed endpoint (no auth token needed for title/artist string).
1194
+ if "open.spotify.com/track/" in lowered:
1195
+ try:
1196
+ oembed = f"https://open.spotify.com/oembed?url={quote_plus(url)}"
1197
+ async with aiohttp.ClientSession(timeout=timeout) as session:
1198
+ async with session.get(oembed) as resp:
1199
+ payload = await resp.json(content_type=None) if resp.status == 200 else None
1200
+ if isinstance(payload, dict):
1201
+ title = str(payload.get("title") or "").strip()
1202
+ author = str(payload.get("author_name") or "").strip()
1203
+ term = f"{author} - {title}".strip(" -")
1204
+ if term:
1205
+ self._resolved_url_query_cache[url] = term
1206
+ return term
1207
+ except Exception:
1208
+ pass
1209
+
1210
+ # Apple Music track URLs usually include iTunes id-like token (/id123456789).
1211
+ if "music.apple.com/" in lowered:
1212
+ match = re.search(r"/id(\d+)", url)
1213
+ if match:
1214
+ try:
1215
+ lookup = f"https://itunes.apple.com/lookup?id={match.group(1)}"
1216
+ async with aiohttp.ClientSession(timeout=timeout) as session:
1217
+ async with session.get(lookup) as resp:
1218
+ payload = await resp.json(content_type=None) if resp.status == 200 else None
1219
+ if isinstance(payload, dict):
1220
+ rows = payload.get("results") or []
1221
+ if rows and isinstance(rows[0], dict):
1222
+ artist = str(rows[0].get("artistName") or "").strip()
1223
+ title = str(rows[0].get("trackName") or rows[0].get("collectionName") or "").strip()
1224
+ term = f"{artist} - {title}".strip(" -")
1225
+ if term:
1226
+ self._resolved_url_query_cache[url] = term
1227
+ return term
1228
+ except Exception:
1229
+ pass
1230
+ return None
1231
+
1232
+ async def _identifier_fallbacks(self, identifier: str) -> list[str]:
1233
+ primary = (identifier or "").strip()
1234
+ if not primary:
1235
+ return []
1236
+ candidates: list[str] = [primary]
1237
+
1238
+ plain = primary
1239
+ if plain.lower().startswith("ytsearch:"):
1240
+ plain = plain[len("ytsearch:") :].strip()
1241
+
1242
+ if self._looks_like_url(plain):
1243
+ term = await self._resolve_music_url_to_search_term(plain)
1244
+ if term:
1245
+ alt = f"ytsearch:{term}"
1246
+ if alt not in candidates:
1247
+ candidates.append(alt)
1248
+
1249
+ return candidates
1250
+
1251
+ async def _search_playable_with_retry(self, identifier: str, *, attempts: int = 3) -> list[object]:
1252
+ if wavelink is None:
1253
+ return []
1254
+ last_error: Exception | None = None
1255
+ candidates = await self._identifier_fallbacks(identifier)
1256
+ for candidate in candidates:
1257
+ for attempt in range(1, max(1, attempts) + 1):
1258
+ try:
1259
+ return await wavelink.Playable.search(candidate)
1260
+ except Exception as exc:
1261
+ last_error = exc
1262
+ text = str(exc).lower()
1263
+ if "429" not in text and "rate" not in text:
1264
+ break
1265
+ await asyncio.sleep((0.35 * attempt) + random.uniform(0.05, 0.2))
1266
+ if last_error is not None:
1267
+ raise last_error
1268
+ return []
1269
+
1270
  def _guild_state(self, guild_id: int) -> GuildPlaybackState:
1271
  if guild_id not in self.state:
1272
  self.state[guild_id] = GuildPlaybackState()
1273
  return self.state[guild_id]
1274
 
1275
+ async def _safe_ctx_defer(self, ctx: commands.Context, *, ephemeral: bool = False) -> None:
1276
+ interaction = getattr(ctx, "interaction", None)
1277
+ if not interaction:
1278
+ return
1279
+ try:
1280
+ if interaction.response.is_done():
1281
+ return
1282
+ await interaction.response.defer(ephemeral=ephemeral)
1283
+ except (discord.InteractionResponded, discord.NotFound, discord.HTTPException):
1284
+ return
1285
+
1286
  async def _dj_permitted(self, ctx_or_interaction: commands.Context | discord.Interaction) -> bool:
1287
  return True
1288
 
 
1616
  continue
1617
  direct = str(entry.get("url") or entry.get("webpage_url") or "").strip()
1618
  vid = str(entry.get("id") or "").strip()
1619
+ title = str(entry.get("title") or "").strip()
1620
+ uploader = str(entry.get("uploader") or entry.get("channel") or "").strip()
1621
  if direct and direct.startswith("http"):
1622
+ if "open.spotify.com/track/" in direct and title:
1623
+ query = f"{uploader} - {title}" if uploader else title
1624
+ urls.append(f"ytsearch1:{query}")
1625
+ continue
1626
  urls.append(direct)
1627
  continue
1628
  if direct and not direct.startswith("http") and "youtube" in playlist_url:
 
1630
  continue
1631
  if vid:
1632
  urls.append(f"https://www.youtube.com/watch?v={vid}")
1633
+ continue
1634
+ if title:
1635
+ query = f"{uploader} - {title}" if uploader else title
1636
+ urls.append(f"ytsearch1:{query}")
1637
  return urls
1638
 
1639
+ urls = await asyncio.to_thread(_run)
1640
+ return [u for u in urls if u][: max(1, min(limit, self._playlist_track_limit))]
1641
 
1642
  async def _resolve_query_with_ytdlp(self, query: str) -> str | None:
1643
  """Resolve a text query to a direct video URL using yt-dlp search."""
 
1748
  lines.append(f"*...and {len(queue) - 5} more*")
1749
  embed.add_field(name=up_next, value="\n".join(lines), inline=False)
1750
  else:
1751
+ embed.add_field(
1752
+ name=up_next,
1753
+ value=idle_text("Queue is empty.", "Add tracks with `/music play` or the music panel."),
1754
+ inline=False,
1755
+ )
1756
 
1757
  status_icon = "📡"
1758
  loop_text = "Off" if state.loop_mode == "off" else ("Track" if state.loop_mode == "track" else "Queue")
 
1770
 
1771
  server_name = guild.name if guild else "Server"
1772
  embed.set_footer(text=f"⛩️ 〣 🔄 Auto-refreshing every 10s • {server_name} 〣 🏮")
1773
+ if guild:
1774
+ await add_banner_to_embed(embed, guild, self.bot)
1775
 
1776
  return embed
1777
 
 
1905
  self.now_playing.pop(guild.id, None)
1906
  if lang == "ar":
1907
  return "⏭️ تم التخطي. الطابور فارغ."
1908
+ return idle_text("Queue is empty.", "Skipped current track. Nothing is queued.")
1909
 
1910
  # For non-wavelink players
1911
  if not self._voice_is_playing(player):
 
2080
  tracks = ([now] if now else []) + list(state_queue)
2081
  uris = [t.webpage_url for t in tracks if t and t.webpage_url]
2082
  if not uris:
2083
+ return idle_text("Queue is empty.", "Play songs first, then try saving again.")
2084
  cleaned_name = (playlist_name or "quicksave").strip()[:40]
2085
  await self.bot.db.execute(
2086
  "INSERT INTO saved_playlists(user_id, name, tracks_json) VALUES (?, ?, ?) "
 
2098
  )
2099
  embed = discord.Embed(title="💾 Saved Playlists", color=NEON_CYAN)
2100
  if not rows:
2101
+ embed.description = idle_text("No playlists saved yet.", "Use the **Save Queue** button.")
2102
  return embed
2103
  lines: list[str] = []
2104
  for name, tracks_json, created_at in rows:
 
2155
  self.queues.setdefault(guild.id, [])
2156
  count = 0
2157
  first_track: str | None = None
2158
+ for idx, url in enumerate(urls[: self._playlist_track_limit], start=1):
2159
  try:
2160
+ search_identifier = self._to_lavalink_identifier(url)
2161
+ results = await self._search_playable_with_retry(search_identifier, attempts=3)
2162
  except Exception as exc:
2163
  await self._log_media_issue(guild, "saved_playlist_play_search", url, exc)
2164
  continue
2165
  if not results:
2166
  continue
2167
  wl_items = list(results.tracks) if (wavelink is not None and isinstance(results, wavelink.Playlist)) else list(results)
2168
+ for wl_track in wl_items[: self._playlist_track_limit]:
2169
  track = self._wavelink_to_track(wl_track, actor.id)
2170
  if count == 0 and not player.playing and not player.paused:
2171
  await player.play(wl_track, volume=self._guild_state(guild.id).volume)
 
2175
  self.queues[guild.id].append(track)
2176
  await player.queue.put_wait(wl_track)
2177
  count += 1
2178
+ # Smooth out provider/API bursts for large playlist loads.
2179
+ if count % 5 == 0:
2180
+ await asyncio.sleep(self._playlist_batch_sleep)
2181
+ if idx % 3 == 0:
2182
+ await asyncio.sleep(self._playlist_batch_sleep)
2183
 
2184
  if count == 0:
2185
  if lang == "ar":
 
2284
 
2285
  urls: list[str] = []
2286
  if self._looks_like_playlist(source):
2287
+ urls = await self._extract_playlist_entries(source, limit=self._playlist_track_limit)
2288
+ if not urls:
2289
+ # Secondary fallback: try Lavalink playlist parsing and store track URIs.
2290
+ try:
2291
+ lavalink_results = await self._search_playable_with_retry(
2292
+ self._to_lavalink_identifier(source),
2293
+ attempts=2,
2294
+ )
2295
+ except Exception:
2296
+ lavalink_results = []
2297
+ if lavalink_results:
2298
+ items = (
2299
+ list(lavalink_results.tracks)
2300
+ if (wavelink is not None and isinstance(lavalink_results, wavelink.Playlist))
2301
+ else list(lavalink_results)
2302
+ )
2303
+ for item in items[: self._playlist_track_limit]:
2304
+ uri = str(getattr(item, "uri", "") or getattr(item, "url", "") or "").strip()
2305
+ if uri:
2306
+ urls.append(uri)
2307
+ else:
2308
+ title = str(getattr(item, "title", "") or "").strip()
2309
+ author = str(getattr(item, "author", "") or "").strip()
2310
+ if title:
2311
+ term = f"{author} - {title}".strip(" -")
2312
+ urls.append(f"ytsearch:{term}")
2313
  else:
2314
  chosen = source
2315
  if not self._looks_like_url(source):
 
2319
  if chosen:
2320
  urls = [chosen]
2321
 
2322
+ urls = [u for u in urls if u][: self._playlist_track_limit]
2323
  if not urls:
2324
  if lang == "ar":
2325
  return "تعذر استخراج مقاطع للحفظ."
 
2381
  tracks = self._get_saved_playlist(guild_id)
2382
  embed = discord.Embed(title="📚 Saved Playlist", color=NEON_CYAN)
2383
  if not tracks:
2384
+ embed.description = idle_text("No saved tracks yet.", "Use `/music playlist_add <query>`.")
2385
  return embed
2386
  lines = [
2387
  f"**{idx}.** {track.title[:70]} • `{track.format_duration()}`"
 
2596
  )
2597
  if is_supported_playlist:
2598
  try:
2599
+ pre_extracted_urls = await self._extract_playlist_entries(playlist_query, limit=self._playlist_track_limit)
2600
  except Exception:
2601
  pre_extracted_urls = []
2602
 
2603
  try:
2604
  if pre_extracted_urls:
2605
  results = []
2606
+ for index, url in enumerate(pre_extracted_urls, start=1):
2607
+ found = await self._search_playable_with_retry(self._to_lavalink_identifier(url), attempts=3)
2608
  if found:
2609
  results.append(found[0])
2610
+ if index % 3 == 0:
2611
+ await asyncio.sleep(self._playlist_batch_sleep)
2612
  else:
2613
  # Search for playlist
2614
+ results = await self._search_playable_with_retry(self._to_lavalink_identifier(playlist_query), attempts=3)
2615
  except Exception:
2616
  if lang == "ar":
2617
  return "تعذر تحميل قائمة التشغيل."
 
2636
  first = True
2637
 
2638
  # Process tracks in order
2639
+ for wl_track in tracks_list[: self._playlist_track_limit]:
2640
  try:
2641
  track = self._wavelink_to_track(wl_track, actor.id)
2642
 
 
2683
 
2684
  @commands.hybrid_group(name="music", fallback="panel", description="Music controls", with_app_command=False)
2685
  async def music_group(self, ctx: commands.Context) -> None:
2686
+ await self._safe_ctx_defer(ctx, ephemeral=True)
 
2687
  guild_id = ctx.guild.id if ctx.guild else None
2688
  embed = await self._music_panel_embed(guild_id)
2689
  panel_view = MusicPanelView(self, guild_id)
 
2693
 
2694
  @commands.hybrid_command(name="music_panel", description="Open interactive music panel")
2695
  async def music_panel(self, ctx: commands.Context) -> None:
2696
+ await self._safe_ctx_defer(ctx, ephemeral=True)
 
2697
  guild_id = ctx.guild.id if ctx.guild else None
2698
  embed = await self._music_panel_embed(guild_id)
2699
  panel_view = MusicPanelView(self, guild_id)
 
2715
  guild_id = ctx.guild.id if ctx.guild else None
2716
  embed = await self._music_panel_embed(guild_id)
2717
  panel_view = MusicPanelView(self, guild_id)
2718
+ await self._safe_ctx_defer(ctx, ephemeral=True)
 
2719
  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))
2720
  if isinstance(panel_msg, discord.Message):
2721
  await panel_view.start_auto_refresh(panel_msg)
2722
  return
2723
+ await self._safe_ctx_defer(ctx)
 
2724
  if not self._looks_like_url(query):
2725
  suggestions = await self._unified_play_search(query, limit=10)
2726
  if suggestions:
 
2805
 
2806
  @music_group.command(name="playlist_add", description="Add a track to saved playlist only")
2807
  async def music_playlist_add(self, ctx: commands.Context, *, query: str) -> None:
2808
+ await self._safe_ctx_defer(ctx)
 
2809
  result = await self.add_query_to_saved_playlist(ctx, query)
2810
  await ctx.reply(result)
2811
 
2812
  @music_group.command(name="playlist_play", description="Play one of your saved playlists by name")
2813
  async def music_playlist_play(self, ctx: commands.Context, *, name: str) -> None:
2814
+ await self._safe_ctx_defer(ctx)
 
2815
  result = await self.play_user_saved_playlist(ctx, name)
2816
  await ctx.reply(result)
2817
 
2818
  @music_group.command(name="playlist_save", description="Save URL/query (track or playlist) into named playlist")
2819
  async def music_playlist_save(self, ctx: commands.Context, name: str, *, source: str) -> None:
2820
+ await self._safe_ctx_defer(ctx, ephemeral=True)
 
2821
  result = await self.save_query_to_named_playlist(ctx, name, source)
2822
  if ctx.interaction:
2823
  await ctx.interaction.followup.send(result, ephemeral=True)
 
2826
 
2827
  @music_group.command(name="playlist_delete", description="Delete one of your saved playlists by name")
2828
  async def music_playlist_delete(self, ctx: commands.Context, *, name: str) -> None:
2829
+ await self._safe_ctx_defer(ctx, ephemeral=True)
 
2830
  result = await self.delete_user_playlist(ctx, name)
2831
  if ctx.interaction:
2832
  await ctx.interaction.followup.send(result, ephemeral=True)
 
2835
 
2836
  @music_group.command(name="playlist_rename", description="Rename one of your saved playlists")
2837
  async def music_playlist_rename(self, ctx: commands.Context, old_name: str, *, new_name: str) -> None:
2838
+ await self._safe_ctx_defer(ctx, ephemeral=True)
 
2839
  result = await self.rename_user_playlist(ctx, old_name, new_name)
2840
  if ctx.interaction:
2841
  await ctx.interaction.followup.send(result, ephemeral=True)
 
2847
  if not ctx.guild:
2848
  await ctx.reply("Server only.")
2849
  return
2850
+ await self._safe_ctx_defer(ctx)
 
2851
  normalized = self._sanitize_query(query)
2852
  if not normalized:
2853
+ await ctx.reply(
2854
+ embed=await idle_embed_for_guild(
2855
+ "YouTube Search Idle",
2856
+ "Type a search query first.",
2857
+ "Example: /music yt_search weeknd blinding lights",
2858
+ guild=ctx.guild,
2859
+ bot=self.bot,
2860
+ )
2861
+ )
2862
  return
2863
 
2864
  used_api = bool((self._youtube_api_key or "").strip())
 
2878
  )
2879
  await ctx.reply("No direct YouTube results found. Try one of these:", embed=fallback_embed)
2880
  return
2881
+ await ctx.reply(
2882
+ embed=await idle_embed_for_guild(
2883
+ "No Results",
2884
+ "No YouTube results were found for this query.",
2885
+ "Try a different title, artist name, or direct URL.",
2886
+ guild=ctx.guild,
2887
+ bot=self.bot,
2888
+ )
2889
+ )
2890
  return
2891
 
2892
  embed = discord.Embed(
 
2908
 
2909
  @commands.hybrid_command(name="playlists", description="View your saved playlists")
2910
  async def playlists(self, ctx: commands.Context) -> None:
2911
+ await self._safe_ctx_defer(ctx, ephemeral=True)
 
2912
  embed = await self.user_playlists_embed(ctx.author.id)
2913
  if ctx.interaction:
2914
  await ctx.interaction.followup.send(embed=embed, ephemeral=True)
 
2928
  return
2929
  queue = self.queues.get(ctx.guild.id, [])
2930
  if len(queue) < 2:
2931
+ await ctx.reply(
2932
+ embed=await idle_embed_for_guild(
2933
+ "Shuffle Idle",
2934
+ "Need at least 2 tracks in queue to shuffle.",
2935
+ "Add more tracks first, then run /music shuffle.",
2936
+ guild=ctx.guild,
2937
+ bot=self.bot,
2938
+ )
2939
+ )
2940
  return
2941
  random.shuffle(queue)
2942
 
 
3151
  if lang == "ar":
3152
  await ctx.reply("❌ الطابور فارغ.")
3153
  else:
3154
+ await ctx.reply(
3155
+ embed=await idle_embed_for_guild(
3156
+ "Queue Idle",
3157
+ "Queue is empty. Nothing to move.",
3158
+ "Use /music play to add tracks.",
3159
+ guild=ctx.guild,
3160
+ bot=self.bot,
3161
+ )
3162
+ )
3163
  return
3164
 
3165
  # Convert to 0-indexed
 
3214
  if lang == "ar":
3215
  await ctx.reply("❌ الطابور فارغ.")
3216
  else:
3217
+ await ctx.reply(
3218
+ embed=await idle_embed_for_guild(
3219
+ "Queue Idle",
3220
+ "Queue is empty. Nothing to swap.",
3221
+ "Use /music play to add tracks.",
3222
+ guild=ctx.guild,
3223
+ bot=self.bot,
3224
+ )
3225
+ )
3226
  return
3227
 
3228
  # Convert to 0-indexed
 
3275
  if lang == "ar":
3276
  await ctx.reply("❌ الطابور فارغ.")
3277
  else:
3278
+ await ctx.reply(
3279
+ embed=await idle_embed_for_guild(
3280
+ "Queue Idle",
3281
+ "Queue is empty. Nothing to remove.",
3282
+ "Use /music play to add tracks.",
3283
+ guild=ctx.guild,
3284
+ bot=self.bot,
3285
+ )
3286
+ )
3287
  return
3288
 
3289
  idx = position - 1
 
3332
  if lang == "ar":
3333
  await ctx.reply("❌ الطابور فارغ.")
3334
  else:
3335
+ await ctx.reply(
3336
+ embed=await idle_embed_for_guild(
3337
+ "Queue Idle",
3338
+ "Queue is empty. Nothing to jump to.",
3339
+ "Use /music play to add tracks.",
3340
+ guild=ctx.guild,
3341
+ bot=self.bot,
3342
+ )
3343
+ )
3344
  return
3345
 
3346
  idx = position - 1
bot/cogs/media_helpers.py CHANGED
@@ -22,7 +22,7 @@ except Exception:
22
  from bot.theme import (
23
  NEON_CYAN, NEON_PURPLE, NEON_ORANGE, NEON_LIME, NEON_PINK,
24
  panel_divider, fancy_divider, fancy_header, progress_bar,
25
- music_embed, music_now_playing, music_status, beautiful_list
26
  )
27
 
28
  def _emoji(key: str, default: str) -> str:
@@ -896,7 +896,13 @@ class MusicPanelView(discord.ui.View, AutoRefreshMixin):
896
  return
897
  queue = self.cog.queues.get(interaction.guild.id, [])
898
  if len(queue) < 2:
899
- await safe_send(interaction, "Need at least 2 tracks to shuffle.")
 
 
 
 
 
 
900
  return
901
  random.shuffle(queue)
902
  player = interaction.guild.voice_client
@@ -1175,9 +1181,12 @@ class QueueView(discord.ui.View, AutoRefreshMixin):
1175
  else:
1176
  embed.add_field(
1177
  name="📋 Up Next",
1178
- value="Add tracks with `/play`!\n" + fancy_divider('music'),
1179
  inline=False
1180
  )
 
 
 
1181
 
1182
  return embed
1183
 
 
22
  from bot.theme import (
23
  NEON_CYAN, NEON_PURPLE, NEON_ORANGE, NEON_LIME, NEON_PINK,
24
  panel_divider, fancy_divider, fancy_header, progress_bar,
25
+ music_embed, music_now_playing, music_status, beautiful_list, idle_embed_for_guild, idle_text, add_banner_to_embed
26
  )
27
 
28
  def _emoji(key: str, default: str) -> str:
 
896
  return
897
  queue = self.cog.queues.get(interaction.guild.id, [])
898
  if len(queue) < 2:
899
+ await safe_send(interaction, embed=await idle_embed_for_guild(
900
+ "Shuffle Idle",
901
+ "Need at least 2 tracks in queue to shuffle.",
902
+ "Add tracks from Play/Search, then retry.",
903
+ guild=interaction.guild,
904
+ bot=self.cog.bot,
905
+ ))
906
  return
907
  random.shuffle(queue)
908
  player = interaction.guild.voice_client
 
1181
  else:
1182
  embed.add_field(
1183
  name="📋 Up Next",
1184
+ value=idle_text("Queue is empty.", "Add tracks with `/music play`.\n" + fancy_divider('music')),
1185
  inline=False
1186
  )
1187
+ guild = self.cog.bot.get_guild(self.guild_id) if hasattr(self.cog, "bot") else None
1188
+ if guild:
1189
+ await add_banner_to_embed(embed, guild, self.cog.bot)
1190
 
1191
  return embed
1192
 
bot/cogs/menu.py CHANGED
@@ -1,768 +1,819 @@
1
- """
2
- Menu cog: Interactive command explorer with beautiful panels.
3
- Enhanced with stunning decorations, rich formatting, and multi-language support.
4
- """
5
-
6
- import random
7
-
8
- import discord
9
- from discord.ext import commands
10
-
11
- from bot.cogs.ai_suite import ImperialMotaz
12
- from bot.emojis import ui, E_DIAMOND, E_STAR, E_FIRE, E_SPARKLE, E_GEM, E_CROWN
13
- from bot.theme import (
14
- fancy_header, pick_neon_color, progress_bar, shimmer, panel_divider,
15
- NEON_CYAN, NEON_PINK, NEON_PURPLE, NEON_LIME, NEON_YELLOW, NEON_BLUE,
16
- NEON_GOLD, NEON_MAGENTA,
17
- double_line, triple_line, mega_title, fancy_divider, animated_header,
18
- gradient_header, panel_title, section_header, quick_stats_grid
19
- )
20
- from bot.i18n import translate, t
21
-
22
-
23
- # Beautiful unicode emojis for select menu categories
24
- _CATEGORY_EMOJIS = {
25
- "Music": "🎵",
26
- "Admin": "🛡️",
27
- "Fun": "🎮",
28
- "AI": "🤖",
29
- "Utility": "🔧",
30
- "Config": "⚙️",
31
- "Economy": "💰",
32
- "Moderation": "⚔️",
33
- "Tickets": "🎫",
34
- "Welcome": "👋",
35
- "Giveaway": "🎁",
36
- "Verification": "",
37
- "Tournament": "🏆",
38
- "Games": "🎲",
39
- "Level": "",
40
- "AutoMod": "🤖",
41
- "Logs": "📋",
42
- "DJ": "🎧",
43
- "Developer": "💻",
44
- "default": "📁",
45
- }
46
-
47
- # Category descriptions with emojis
48
- _CATEGORY_DESCRIPTIONS = {
49
- "Music": "🎵 Music commands for playback control",
50
- "Admin": "🛡️ Server administration tools",
51
- "Fun": "🎮 Fun games and entertainment",
52
- "AI": "🤖 AI-powered features",
53
- "Utility": "🔧 Useful utility commands",
54
- "Config": "⚙️ Bot configuration settings",
55
- "Economy": "💰 Economy and currency system",
56
- "Moderation": "⚔️ Moderation commands",
57
- "Tickets": "🎫 Ticket support system",
58
- "Welcome": "👋 Welcome and goodbye messages",
59
- "Giveaway": "🎁 Giveaway management",
60
- "Verification": " Member verification",
61
- "Tournament": "🏆 Tournament brackets",
62
- "Games": "🎲 Mini games and fun",
63
- "Level": " XP and leveling system",
64
- "AutoMod": "🤖 Auto-moderation features",
65
- "Logs": "📋 Logging configuration",
66
- "DJ": "🎧 DJ music controls",
67
- "Developer": "💻 Developer tools",
68
- }
69
-
70
- _CATEGORY_BILINGUAL = {
71
- "__all__": "📚 All Commands | جميع الأوامر",
72
- "__ai__": "🤖 AI | الذكاء الاصطناعي",
73
- "Music": "🎵 Music | الموسيقى",
74
- "Admin": "🛡️ Admin | الإدارة",
75
- "Fun": "🎮 Games | الألعاب",
76
- "AI": "🤖 AI | الذكاء الاصطناعي",
77
- "Utility": "🔧 Utility | الأدوات",
78
- "Config": "⚙️ Config | الإعدادات",
79
- "Economy": "💰 Economy | الاقتصاد",
80
- "Moderation": "⚔️ Moderation | الإشراف",
81
- "Community": "💡 Community | المجتمع",
82
- "AISuite": "🤖 AI Suite | الذكاء",
83
- "Configuration": "⚙️ Configuration | الإعدادات",
84
- "Events": "📋 Events | الأحداث",
85
- "Verification": "✅ Verification | التحقق",
86
- }
87
-
88
-
89
- def _bilingual_category(name: str) -> str:
90
- return _CATEGORY_BILINGUAL.get(name, f"📁 {name} | {name}")
91
-
92
-
93
- class CommandsMenuSelect(discord.ui.Select):
94
- """Beautiful dropdown menu for selecting command categories."""
95
-
96
- def __init__(self, cog_names: list[str], all_label: str, ai_label: str, lang: str = "en", selected_cog: str = "__all__") -> None:
97
- self.lang = lang
98
-
99
- # Localized descriptions
100
- all_desc = translate(lang, "menu.select_all_desc")
101
- ai_desc = translate(lang, "menu.select_ai_desc")
102
-
103
- options = [
104
- discord.SelectOption(
105
- label=_bilingual_category("__all__"),
106
- description=all_desc,
107
- emoji=ui("book"),
108
- value="__all__",
109
- default=selected_cog == "__all__",
110
- ),
111
- discord.SelectOption(
112
- label=_bilingual_category("__ai__"),
113
- description=ai_desc,
114
- emoji=ui("robot"),
115
- value="__ai__",
116
- default=selected_cog == "__ai__",
117
- ),
118
- ]
119
-
120
- # Add cog options with beautiful emojis
121
- for name in sorted(cog_names):
122
- emoji = _CATEGORY_EMOJIS.get(name, _CATEGORY_EMOJIS["default"])
123
- desc = translate(lang, f"menu.category_desc.{name.lower()}")
124
- if desc == f"menu.category_desc.{name.lower()}":
125
- desc = _CATEGORY_DESCRIPTIONS.get(name, f"📁 {name} commands")
126
- # Truncate description to fit Discord's limits
127
- desc = desc[:100]
128
- options.append(
129
- discord.SelectOption(
130
- label=_bilingual_category(name)[:100],
131
- description=desc,
132
- emoji=emoji,
133
- value=name,
134
- default=selected_cog == name,
135
- )
136
- )
137
-
138
- placeholder = translate(lang, "menu.select_placeholder")
139
- super().__init__(
140
- placeholder=placeholder,
141
- min_values=1,
142
- max_values=1,
143
- options=options
144
- )
145
-
146
- async def callback(self, interaction: discord.Interaction) -> None:
147
- await interaction.response.defer()
148
- view = self.view
149
- if not isinstance(view, CommandsMenuView):
150
- return
151
- selected = self.values[0]
152
- view.selected_cog = selected
153
- view.page = 0
154
- await view.setup_items()
155
- embed = await view.build_embed(interaction.guild.id if interaction.guild else None, selected)
156
- await interaction.followup.edit_message(interaction.message.id, embed=embed, view=view)
157
-
158
-
159
- class CommandsMenuView(discord.ui.View):
160
- """Beautiful command menu view with rich decorations."""
161
-
162
- def __init__(self, bot: commands.Bot, guild_id: int | None = None) -> None:
163
- super().__init__(timeout=None)
164
- self.bot = bot
165
- self.guild_id = guild_id
166
- self.selected_cog = "__all__"
167
- self.page = 0
168
- self.page_size = 18
169
- self._accent_seed = random.randrange(0, 1024)
170
- self._color_palette = [NEON_CYAN, NEON_PINK, NEON_PURPLE, NEON_LIME, NEON_GOLD, NEON_MAGENTA]
171
-
172
- async def setup_items(self) -> None:
173
- """Setup the menu items."""
174
- cog_names = list(self.bot.cogs.keys())
175
- lang = await self.bot.get_guild_language(self.guild_id)
176
- all_label = translate(lang, "menu.all")
177
- ai_label = translate(lang, "menu.ai")
178
- self.clear_items()
179
- self.add_item(CommandsMenuSelect(cog_names, all_label, ai_label, lang, self.selected_cog))
180
-
181
- refresh_label = translate(lang, "menu.refresh")
182
- invite_label = translate(lang, "menu.invite_button")
183
- self.add_item(RefreshButton(self, refresh_label))
184
- self.add_item(QuickCategoryButton(self, "Economy | الاقتصاد", "Economy", "💰", row=1))
185
- self.add_item(QuickCategoryButton(self, "Music | الموسيقى", "Music", "🎵", row=1))
186
- self.add_item(QuickCategoryButton(self, "Admin | الإدارة", "Admin", "🛡️", row=1))
187
- self.add_item(QuickCategoryButton(self, "Utility | الأدوات", "Utility", "ℹ️", row=1))
188
- self.add_item(QuickCategoryButton(self, "Community | المجتمع", "Community", "💡", row=2))
189
- self.add_item(QuickCategoryButton(self, "AI | الذكاء", "AISuite", "🤖", row=2))
190
- self.add_item(QuickCategoryButton(self, "Config | الإعداد", "Configuration", "⚙️", row=2))
191
- if self.page > 0:
192
- self.add_item(PageButton(self, "prev"))
193
- if self._has_next_page(self.selected_cog):
194
- self.add_item(PageButton(self, "next"))
195
-
196
- if self.bot.user:
197
- self.add_item(InviteButton(invite_label, self.bot.user.id))
198
-
199
- def _desc_key(self, qualified_name: str) -> str:
200
- """Get the translation key for a command description."""
201
- norm = qualified_name.strip().lower().replace(" ", "_")
202
- key_mapping = {
203
- # Music commands
204
- "play": "menu.cmd.play",
205
- "music_play": "menu.cmd.music_play",
206
- "music_panel": "menu.cmd.music_panel",
207
- "music_skip": "menu.cmd.music_skip",
208
- "music_stop": "menu.cmd.music_stop",
209
- "music_pause": "menu.cmd.music_pause",
210
- "music_resume": "menu.cmd.music_resume",
211
- "music_queue": "menu.cmd.music_queue",
212
- "music_playlist": "menu.cmd.music_playlist",
213
- "music_playlist_save": "menu.cmd.music_playlist_save",
214
- "music_playlist_rename": "menu.cmd.music_playlist_rename",
215
- "music_playlist_delete": "menu.cmd.music_playlist_delete",
216
- "music_volume": "menu.cmd.music_volume",
217
- "music_nowplaying": "menu.cmd.music_nowplaying",
218
- "music_filter": "menu.cmd.music_filter",
219
- "music_loop": "menu.cmd.music_loop",
220
- "music_shuffle": "menu.cmd.music_shuffle",
221
- "music_247": "menu.cmd.music_247",
222
- "music_previous": "menu.cmd.music_previous",
223
- "music_seek": "menu.cmd.music_seek",
224
- "music_clear": "menu.cmd.music_clear",
225
- "music_remove": "menu.cmd.music_remove",
226
- "music_move": "menu.cmd.music_move",
227
- "music_jump": "menu.cmd.music_jump",
228
- "music_lyrics": "menu.cmd.music_lyrics",
229
-
230
- # Admin commands
231
- "purge": "menu.cmd.purge",
232
- "ban": "menu.cmd.ban",
233
- "unban": "menu.cmd.unban",
234
- "kick": "menu.cmd.kick",
235
- "mute": "menu.cmd.mute",
236
- "unmute": "menu.cmd.unmute",
237
- "warn": "menu.cmd.warn",
238
- "warnings": "menu.cmd.warnings",
239
- "clearwarn": "menu.cmd.clearwarn",
240
- "slowmode": "menu.cmd.slowmode",
241
- "lock": "menu.cmd.lock",
242
- "unlock": "menu.cmd.unlock",
243
- "cloneemoji": "menu.cmd.cloneemoji",
244
- "awesomeroles": "menu.cmd.awesomeroles",
245
- "backupserver": "menu.cmd.backupserver",
246
-
247
- # Fun commands
248
- "8ball": "menu.cmd.8ball",
249
- "meme": "menu.cmd.meme",
250
- "trivia": "menu.cmd.trivia",
251
- "gaming_news": "menu.cmd.gaming_news",
252
- "free_games": "menu.cmd.free_games",
253
- "gamehub": "menu.cmd.gamehub",
254
- "xo": "menu.cmd.xo",
255
- "choose": "menu.cmd.choose",
256
- "mario": "menu.cmd.mario",
257
- "dice": "menu.cmd.dice",
258
- "slots": "menu.cmd.slots",
259
- "coinflip": "menu.cmd.coinflip",
260
- "roll": "menu.cmd.roll",
261
-
262
- # AI commands
263
- "chat": "menu.cmd.chat",
264
- "ask_image": "menu.cmd.ask_image",
265
- "imagine": "menu.cmd.imagine",
266
- "image_gen": "menu.cmd.image_gen",
267
- "upscale": "menu.cmd.upscale",
268
- "summarize": "menu.cmd.summarize",
269
- "ai": "menu.cmd.ai",
270
- "ai_chat": "menu.cmd.chat",
271
- "ai_ask_image": "menu.cmd.ask_image",
272
- "ai_imagine": "menu.cmd.imagine",
273
- "ai_image_gen": "menu.cmd.image_gen",
274
- "ai_upscale": "menu.cmd.upscale",
275
- "ai_summarize": "menu.cmd.summarize",
276
- "ai_debug": "menu.cmd.debug",
277
- "ai_code_gen": "menu.cmd.code_gen",
278
- "ai_voice": "menu.cmd.speak",
279
- "ai_speak": "menu.cmd.speak",
280
- "ai_tts": "menu.cmd.tts",
281
- "ai_translate_voice": "menu.cmd.translate_voice",
282
- "ai_model": "menu.cmd.ai_model",
283
- "ai_channel": "menu.cmd.ai_channel",
284
- "ai_auto": "menu.cmd.ai_auto",
285
- "ai_setup": "menu.cmd.ai",
286
- "ai_execute": "menu.cmd.ai_execute",
287
-
288
- # Utility commands
289
- "serverinfo": "menu.cmd.serverinfo",
290
- "userinfo": "menu.cmd.userinfo",
291
- "botstats": "menu.cmd.botstats",
292
- "poll": "menu.cmd.poll",
293
- "remind": "menu.cmd.remind",
294
- "avatar": "menu.cmd.avatar",
295
- "banner": "menu.cmd.banner",
296
- "translate": "menu.cmd.translate",
297
-
298
- # Config commands
299
- "set": "menu.cmd.set",
300
- "set_log": "menu.cmd.set_log",
301
- "set_welcome": "menu.cmd.set_welcome",
302
- "set_suggestions": "menu.cmd.set_suggestions",
303
- "set_automod": "menu.cmd.set_automod",
304
- "set_dailymessage": "menu.cmd.set_dailymessage",
305
- "set_dailychannel": "menu.cmd.set_dailychannel",
306
- "set_dailytime": "menu.cmd.set_dailytime",
307
- "set_dailytitle": "menu.cmd.set_dailytitle",
308
- "set_dailyimage": "menu.cmd.set_dailyimage",
309
- "set_dailybutton": "menu.cmd.set_dailybutton",
310
- "set_dailytoggle": "menu.cmd.set_dailytoggle",
311
- "set_pollchannel": "menu.cmd.set_pollchannel",
312
- "set_freegames": "menu.cmd.set_freegames",
313
- "set_gamenews": "menu.cmd.set_gamenews",
314
- "set_supportai": "menu.cmd.set_supportai",
315
- "set_wisdom": "menu.cmd.set_wisdom",
316
-
317
- # Language commands
318
- "language": "menu.cmd.language",
319
- "languages": "menu.cmd.languages",
320
-
321
- # Menu commands
322
- "menu": "menu.cmd.menu",
323
- "ping": "menu.cmd.ping",
324
- "load": "menu.cmd.load",
325
- "unload": "menu.cmd.unload",
326
- "reload": "menu.cmd.reload",
327
- "sync": "menu.cmd.sync",
328
- "shutdown": "menu.cmd.shutdown",
329
-
330
- # Verification commands
331
- "verify": "menu.cmd.verify",
332
- "verifysetup": "menu.cmd.verifysetup",
333
- "verify_config": "menu.cmd.verify",
334
- "verify_config_panel": "menu.cmd.verify",
335
- "verify_config_setup": "menu.cmd.verifysetup",
336
-
337
- # Economy commands
338
- "economy": "menu.cmd.economy",
339
- "economy_deposit": "menu.cmd.economy_deposit",
340
- "economy_withdraw": "menu.cmd.economy_withdraw",
341
- "economy_gamble": "menu.cmd.economy_gamble",
342
- "economy_rob": "menu.cmd.economy_rob",
343
- "balance": "menu.cmd.balance",
344
- "daily": "menu.cmd.daily",
345
- "work": "menu.cmd.work",
346
- "gamble": "menu.cmd.gamble",
347
- "rob": "menu.cmd.rob",
348
- "leaderboard": "menu.cmd.leaderboard",
349
- "shop": "menu.cmd.shop",
350
- "buy": "menu.cmd.buy",
351
- "inventory": "menu.cmd.inventory",
352
- "transfer": "menu.cmd.transfer",
353
-
354
- # Engagement commands
355
- "xp": "menu.cmd.xp",
356
- "level": "menu.cmd.level",
357
- "rank": "menu.cmd.rank",
358
- "suggest": "menu.cmd.suggest",
359
- "suggestion": "menu.cmd.suggestion",
360
- "suggestion_show": "menu.cmd.suggestion_show",
361
- "suggestion_panel": "menu.cmd.suggestion_panel",
362
- "suggestion_voters": "menu.cmd.suggestion_voters",
363
- "giveaway_create": "menu.cmd.giveaway_create",
364
- "giveaway_end": "menu.cmd.giveaway_end",
365
- "ticket_panel": "menu.cmd.ticket_panel",
366
-
367
- # Tournament commands
368
- "tournament": "menu.cmd.tournament",
369
- "tournament_panel": "menu.cmd.tournament_panel",
370
- "tournament_create": "menu.cmd.tournament_create",
371
- "tournament_join": "menu.cmd.tournament_join",
372
- "tournament_start": "menu.cmd.tournament_start",
373
- "tournament_end": "menu.cmd.tournament_end",
374
- "tournament_gamehub": "menu.cmd.tournament_gamehub",
375
-
376
- # Gambling commands
377
- "blackjack": "menu.cmd.blackjack",
378
- "roulette": "menu.cmd.roulette",
379
- "rpg": "menu.cmd.rpg",
380
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  # AI Admin commands
382
  "ai_admin": "menu.cmd.ai_admin",
383
  "ai_help": "menu.cmd.ai_help",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  }
385
- return key_mapping.get(norm, f"menu.cmd.{norm}")
386
-
387
- async def _format_commands(self, guild_id: int | None, cmds: list[commands.Command], max_lines: int = 20, max_chars: int = 980) -> str:
388
- """Format commands list with beautiful decorations."""
389
- lang = await self.bot.get_guild_language(guild_id)
390
- lines: list[str] = []
391
- used = 0
392
-
393
- for idx, cmd in enumerate(sorted(cmds, key=lambda c: c.qualified_name), start=1):
394
- if cmd.hidden:
395
- continue
396
-
397
- # Get translated description
398
- desc_key = self._desc_key(cmd.qualified_name)
399
- translated_desc = translate(lang, desc_key)
400
-
401
- if translated_desc == desc_key:
402
- fallback_desc = (cmd.description or cmd.help or "").strip()
403
- desc = fallback_desc if fallback_desc else "—"
404
- else:
405
- desc = translated_desc
406
-
407
- # Hybrid commands are shown with slash syntax; prefix-only commands use !
408
- is_hybrid = isinstance(cmd, commands.HybridCommand)
409
- if is_hybrid:
410
- invoke = f"`/{cmd.qualified_name}`"
411
- else:
412
- invoke = f"`!{cmd.qualified_name}`"
413
- line = f" {invoke} {desc[:78]}"
414
- projected = used + len(line) + (1 if lines else 0)
415
-
416
- if projected > max_chars:
417
- break
418
- lines.append(line)
419
- used = projected
420
-
421
- if len(lines) >= max_lines:
422
- break
423
-
424
  if len(lines) < len([c for c in cmds if not c.hidden]):
425
- suffix = "\n ┈┈┈┈┈┈┈┈┈┈┈┈"
426
  if used + len(suffix) <= max_chars:
427
- lines.append(" ┈┈┈┈┈┈┈┈┈┈┈┈")
428
-
429
- return "\n".join(lines)
430
-
431
- def _visible_commands_page(self, selected_cog: str) -> tuple[list[commands.Command], int]:
432
- commands_list = sorted(self._selected_commands(selected_cog), key=lambda c: c.qualified_name)
433
- if not commands_list:
434
- return [], 1
435
- total_pages = max(1, (len(commands_list) + self.page_size - 1) // self.page_size)
436
- if self.page >= total_pages:
437
- self.page = total_pages - 1
438
- start = self.page * self.page_size
439
- return commands_list[start:start + self.page_size], total_pages
440
-
441
- def _has_next_page(self, selected_cog: str) -> bool:
442
- commands_list = self._selected_commands(selected_cog)
443
- total_pages = max(1, (len(commands_list) + self.page_size - 1) // self.page_size)
444
- return self.page + 1 < total_pages
445
-
446
- def _category_stats(self) -> list[tuple[str, int]]:
447
- """Get category statistics."""
448
- stats: list[tuple[str, int]] = []
449
- for name, cog in self.bot.cogs.items():
450
- cog_commands = [c for c in (cog.get_commands() if cog else []) if not c.hidden]
451
- if cog_commands:
452
- stats.append((name, len(cog_commands)))
453
- return sorted(stats, key=lambda pair: (-pair[1], pair[0]))
454
 
455
- async def _category_value(self, stats: list[tuple[str, int]], guild_id: int | None) -> str:
456
- """Format category statistics with beautiful decorations."""
457
- lang = await self.bot.get_guild_language(guild_id)
458
- if not stats:
459
- return translate(lang, "menu.none")
460
-
461
- lines: list[str] = []
462
- for index, (name, count) in enumerate(stats[:8], start=1):
463
- badge = {1: "🥇", 2: "🥈", 3: "🥉"}.get(index, f"#{index}")
464
- emoji = _CATEGORY_EMOJIS.get(name, "📁")
465
-
466
- if lang == "ar":
467
- lines.append(f"{badge} {emoji} **{name}** — `{count}` أمر")
468
- else:
469
- lines.append(f"{badge} {emoji} **{name}** — `{count}` cmds")
470
-
471
- if len(stats) > 8:
472
- lines.append("✦ ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈")
473
-
474
  return "\n".join(lines)
475
-
476
- async def _stats_text(
477
- self,
478
- guild_id: int | None,
479
- total_commands: int,
480
- category_count: int,
481
- selection_label: str,
482
- visible_commands: int,
483
- ) -> str:
484
- """Format statistics with beautiful decorations."""
485
- lang = await self.bot.get_guild_language(guild_id)
486
-
487
- if lang == "ar":
488
- pieces = [
489
- f"📊 **إجمالي الأوامر:** `{total_commands}`",
490
- f"📁 **الفئات:** `{category_count}`",
491
- f"✨ **المحدد:** {selection_label} (`{visible_commands}` أمر)",
492
- f"📈 {progress_bar(visible_commands, total_commands, size=14)}",
493
- ]
494
- else:
495
- pieces = [
496
- f"📊 **Total Commands:** `{total_commands}`",
497
- f"📁 **Categories:** `{category_count}`",
498
- f"✨ **Selected:** {selection_label} (`{visible_commands}` cmds)",
499
- f"📈 {progress_bar(visible_commands, total_commands, size=14)}",
500
- ]
501
- return "\n".join(pieces)
502
-
503
- async def _selected_label(self, guild_id: int | None, selected_cog: str) -> str:
504
- """Get the label for the selected category."""
505
- return _bilingual_category(selected_cog)
506
-
507
- def _selected_commands(self, selected_cog: str) -> list[commands.Command]:
508
- """Get commands for the selected category."""
509
- def _flatten(cmds: list[commands.Command]) -> list[commands.Command]:
510
- collected: list[commands.Command] = []
511
- for command in cmds:
512
- if command.hidden:
513
- continue
514
- collected.append(command)
515
- if isinstance(command, commands.Group):
516
- collected.extend([sub for sub in command.walk_commands() if not sub.hidden])
517
- unique: dict[str, commands.Command] = {c.qualified_name: c for c in collected}
518
- return list(unique.values())
519
-
520
- if selected_cog == "__all__":
521
- return _flatten(list(self.bot.commands))
522
- if selected_cog == "__ai__":
523
- ai_cog = self.bot.get_cog("AISuite")
524
- return _flatten(list(ai_cog.get_commands() if ai_cog else []))
525
- cog = self.bot.get_cog(selected_cog)
526
- return _flatten(list(cog.get_commands() if cog else []))
527
-
528
- def _top_level_slash_count(self) -> int:
529
- """Count top-level slash-registered hybrid commands/groups (Discord limit: 100)."""
530
- total = 0
531
- for cmd in self.bot.commands:
532
- if not isinstance(cmd, commands.HybridCommand):
533
- continue
534
- if getattr(cmd, "with_app_command", True):
535
- total += 1
536
- return total
537
-
538
- async def build_embed(self, guild_id: int | None, selected_cog: str) -> discord.Embed:
539
- """Build the beautiful embed for the menu."""
540
- lang = await self.bot.get_guild_language(guild_id)
541
- color = discord.Color(0x2B2D31)
542
-
543
- title = "BOT- AI System"
544
- desc = (
545
- f"{translate(lang, 'menu.hub_welcome')}\n"
546
- f"{translate(lang, 'menu.hub_explore')}\n"
547
- f"{translate(lang, 'menu.hub_usage')}"
548
- )
549
- stats_title = translate(lang, "menu.stats_heading")
550
- categories_title = translate(lang, "menu.categories_heading")
551
- tips_title = translate(lang, "menu.tips_heading")
552
- quick_title = translate(lang, "menu.quick_heading")
553
- updates_title = translate(lang, "menu.updates_heading")
554
- tips = (
555
- f"• {translate(lang, 'menu.tip_line_1')}\n"
556
- f"• {translate(lang, 'menu.tip_line_2')}\n"
557
- f"• {translate(lang, 'menu.tip_line_3')}"
558
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
  quick = (
560
- f"🎵 `/music_panel` {translate(lang, 'menu.quick_music')}\n"
561
- f"🎮 `/gamehub` {translate(lang, 'menu.quick_gamehub')}\n"
562
- f"🏆 `/tournament panel` {translate(lang, 'menu.quick_tournament')}\n"
563
- f"💰 `/economy` {translate(lang, 'menu.quick_economy')}"
564
  )
 
565
  updates = (
566
- f"🧩 `/admin emoji clone` {translate(lang, 'menu.update_cloneemoji')}\n"
567
- f"🛡️ `/admin shield add_image` {translate(lang, 'menu.update_shield_image')}\n"
568
- f"🗳️ `/poll create` {translate(lang, 'menu.update_poll_group')}\n"
569
- f"💸 `/economy deposit` {translate(lang, 'menu.update_economy_group')}\n"
570
- f"🤖 `/ai execute` {translate(lang, 'menu.quick_ai') if translate(lang, 'menu.quick_ai') != 'menu.quick_ai' else 'AI admin request'}"
571
- )
572
- footer = "🏮 Powered by BOT- AI Suite 🏮"
573
-
574
- top_divider = panel_divider("cyan")
575
- mid_divider = panel_divider("purple")
576
- embed = ImperialMotaz.craft_embed(
577
- title=title,
578
- description=f"{top_divider}\n{desc}\n{mid_divider}",
579
- color=color,
580
- footer=footer,
581
- )
582
-
583
- if self.bot.user:
584
- embed.set_thumbnail(url=self.bot.user.display_avatar.url)
585
-
586
- all_commands = self._selected_commands("__all__")
587
- category_stats = self._category_stats()
588
- selected_label = await self._selected_label(guild_id, selected_cog)
589
- selected_cmds = self._selected_commands(selected_cog)
590
- visible_cmds, total_pages = self._visible_commands_page(selected_cog)
591
- max_lines = 18
592
- content = await self._format_commands(guild_id, visible_cmds, max_lines=max_lines)
593
-
594
- # Add fields with beautiful decorations
595
- total_guilds = len(self.bot.guilds)
596
- total_members = sum((g.member_count or 0) for g in self.bot.guilds)
597
- latency_ms = f"{round(self.bot.latency * 1000)}ms"
598
- slash_used = self._top_level_slash_count()
599
- slash_budget = f"{slash_used}/100"
600
- stats_grid = quick_stats_grid(
601
- [
602
- ("Guilds", str(total_guilds), "🌐"),
603
- ("Members", f"{total_members:,}", "👥"),
604
- ("Latency", latency_ms, "⚡"),
605
- ("Slash", slash_budget, "🧮"),
606
- ],
607
- columns=2,
608
- )
609
- embed.add_field(name=stats_title, value=stats_grid, inline=False)
610
-
611
- embed.add_field(
612
- name=categories_title,
613
- value=await self._category_value(category_stats, guild_id),
614
- inline=True,
615
- )
616
-
617
- emoji = _CATEGORY_EMOJIS.get(selected_cog, "📁")
618
- embed.add_field(
619
- name=f"{emoji} {selected_label}",
620
- value=content or translate(lang, "menu.none"),
621
- inline=False,
622
- )
623
-
624
- embed.add_field(
625
- name=quick_title,
626
- value=quick,
627
- inline=True,
628
- )
629
-
630
- embed.add_field(
631
- name=tips_title,
632
- value=tips,
633
- inline=True,
634
- )
635
-
636
- embed.add_field(
637
- name=updates_title,
638
- value=updates,
639
- inline=False,
640
  )
641
-
642
- embed.set_footer(text=footer)
643
- embed.description = f"{embed.description}\n\n📄 Page {self.page + 1}/{total_pages}"
644
- return embed
645
-
646
-
647
- class MainMenuView(CommandsMenuView):
648
- """Named alias for persistent registration."""
649
-
650
-
651
- class InviteButton(discord.ui.Button):
652
- """Beautiful invite button."""
653
-
654
- def __init__(self, label: str, client_id: int) -> None:
655
- url = f"https://discord.com/api/oauth2/authorize?client_id={client_id}&permissions=0&scope=applications.commands%20bot"
656
- super().__init__(
657
- label=label,
658
- style=discord.ButtonStyle.link,
659
- url=url,
660
- emoji=ui("link")
661
- )
662
-
663
-
664
- class RefreshButton(discord.ui.Button):
665
- """Beautiful refresh button."""
666
-
667
- def __init__(self, parent: CommandsMenuView, label: str) -> None:
668
- super().__init__(
669
- label=label,
670
- emoji=ui("refresh"),
671
- style=discord.ButtonStyle.secondary
672
- )
673
- self.parent_view = parent
674
-
675
- async def callback(self, interaction: discord.Interaction) -> None:
676
- await interaction.response.defer()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  guild_id = interaction.guild.id if interaction.guild else None
678
- lang = await self.parent_view.bot.get_guild_language(guild_id)
679
- self.label = translate(lang, "menu.refresh")
680
- await self.parent_view.setup_items()
681
- embed = await self.parent_view.build_embed(guild_id, "__all__")
682
- await interaction.followup.edit_message(interaction.message.id, embed=embed, view=self.parent_view)
683
-
684
-
685
- class QuickCategoryButton(discord.ui.Button):
686
- def __init__(self, parent: CommandsMenuView, label: str, category: str, emoji: str, *, row: int = 1) -> None:
687
- super().__init__(
688
- label=label,
689
- emoji=emoji,
690
- style=discord.ButtonStyle.secondary,
691
- custom_id=f"menu:quick:{label.lower()}",
692
- row=row,
693
- )
694
- self.parent_view = parent
695
- self.category = category
696
-
697
- async def callback(self, interaction: discord.Interaction) -> None:
698
- await interaction.response.defer()
699
- self.parent_view.selected_cog = self.category
700
- self.parent_view.page = 0
701
- await self.parent_view.setup_items()
702
- embed = await self.parent_view.build_embed(interaction.guild.id if interaction.guild else None, self.category)
703
- await interaction.followup.edit_message(interaction.message.id, embed=embed, view=self.parent_view)
704
-
705
-
706
- class PageButton(discord.ui.Button):
707
- def __init__(self, parent: CommandsMenuView, direction: str) -> None:
708
- self.parent_view = parent
709
- self.direction = direction
710
- label = "Previous" if direction == "prev" else "Next"
711
- emoji = "⬅️" if direction == "prev" else "➡️"
712
- super().__init__(label=label, emoji=emoji, style=discord.ButtonStyle.secondary)
713
-
714
- async def callback(self, interaction: discord.Interaction) -> None:
715
- await interaction.response.defer()
716
- if self.direction == "prev":
717
- self.parent_view.page = max(0, self.parent_view.page - 1)
718
- else:
719
- if self.parent_view._has_next_page(self.parent_view.selected_cog):
720
- self.parent_view.page += 1
721
- await self.parent_view.setup_items()
722
- embed = await self.parent_view.build_embed(interaction.guild.id if interaction.guild else None, self.parent_view.selected_cog)
723
- await interaction.followup.edit_message(interaction.message.id, embed=embed, view=self.parent_view)
724
-
725
-
726
- class Menu(commands.Cog):
727
- """Interactive command menu with beautiful panels and multi-language support."""
728
-
729
- def __init__(self, bot: commands.Bot) -> None:
730
- self.bot = bot
731
-
732
  async def cog_load(self) -> None:
733
- self.bot.add_view(CommandsMenuView(self))
734
-
735
- @commands.hybrid_command(name="menu", description="Bot menu | قائمة أوامر واضحة")
736
- async def menu(self, ctx: commands.Context) -> None:
737
- """Display the beautiful command menu."""
738
- if ctx.interaction and not ctx.interaction.response.is_done():
739
- await ctx.interaction.response.defer()
740
- guild_id = ctx.guild.id if ctx.guild else None
741
- view = CommandsMenuView(self.bot, guild_id)
742
- await view.setup_items()
743
- embed = await view.build_embed(guild_id, "__all__")
744
- if ctx.interaction:
745
- await ctx.interaction.followup.send(embed=embed, view=view)
746
- else:
747
- await ctx.reply(embed=embed, view=view)
748
-
749
- @commands.hybrid_command(name="start", description="Start menu | القائمة الرئيسية", with_app_command=False)
750
- async def start_menu(self, ctx: commands.Context) -> None:
751
- await self.menu(ctx)
752
-
753
- async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None:
754
- embed = ImperialMotaz.craft_embed(
755
- title="⚠️ Command Error | خطأ في الأمر",
756
- description=f" {str(error)[:1000]} ",
757
- color=discord.Color(0x2B2D31),
758
- footer="🏮 Powered by BOT- AI Suite 🏮",
759
- )
760
- try:
761
- await ctx.reply(embed=embed)
762
- except Exception:
763
- if ctx.channel:
764
- await ctx.channel.send(embed=embed)
765
-
766
-
767
- async def setup(bot: commands.Bot) -> None:
768
- await bot.add_cog(Menu(bot))
 
 
 
1
+ """
2
+ Menu cog: Interactive command explorer with beautiful panels.
3
+ Enhanced with stunning decorations, rich formatting, and multi-language support.
4
+ """
5
+
6
+ import random
7
+
8
+ import discord
9
+ from discord.ext import commands
10
+
11
+ from bot.cogs.ai_suite import ImperialMotaz
12
+ from bot.emojis import ui, E_DIAMOND, E_STAR, E_FIRE, E_SPARKLE, E_GEM, E_CROWN
13
+ from bot.theme import (
14
+ fancy_header, pick_neon_color, progress_bar, shimmer, panel_divider,
15
+ NEON_CYAN, NEON_PINK, NEON_PURPLE, NEON_LIME, NEON_YELLOW, NEON_BLUE,
16
+ NEON_GOLD, NEON_MAGENTA,
17
+ double_line, triple_line, mega_title, fancy_divider, animated_header,
18
+ gradient_header, panel_title, section_header, quick_stats_grid
19
+ )
20
+
21
+
22
+ # Beautiful unicode emojis for select menu categories
23
+ _CATEGORY_EMOJIS = {
24
+ "Music": "🎵",
25
+ "Admin": "🛡️",
26
+ "Fun": "🎮",
27
+ "AI": "🤖",
28
+ "Utility": "🔧",
29
+ "Config": "⚙️",
30
+ "Economy": "💰",
31
+ "Moderation": "⚔️",
32
+ "Tickets": "🎫",
33
+ "Welcome": "👋",
34
+ "Giveaway": "🎁",
35
+ "Verification": "✅",
36
+ "Tournament": "🏆",
37
+ "Games": "🎲",
38
+ "Level": "✅",
39
+ "AutoMod": "🤖",
40
+ "Logs": "📋",
41
+ "DJ": "🎧",
42
+ "Developer": "💻",
43
+ "default": "📁",
44
+ }
45
+
46
+ # Category descriptions with emojis
47
+ _CATEGORY_DESCRIPTIONS = {
48
+ "Music": "🎵 Music commands for playback control",
49
+ "Admin": "🛡️ Server administration tools",
50
+ "Fun": "🎮 Fun games and entertainment",
51
+ "AI": "🤖 AI-powered features",
52
+ "Utility": "🔧 Useful utility commands",
53
+ "Config": "⚙️ Bot configuration settings",
54
+ "Economy": "💰 Economy and currency system",
55
+ "Moderation": "⚔️ Moderation commands",
56
+ "Tickets": "🎫 Ticket support system",
57
+ "Welcome": "👋 Welcome and goodbye messages",
58
+ "Giveaway": "🎁 Giveaway management",
59
+ "Verification": "✅ Member verification",
60
+ "Tournament": "🏆 Tournament brackets",
61
+ "Games": "🎲 Mini games and fun",
62
+ "Level": "✅ XP and leveling system",
63
+ "AutoMod": "🤖 Auto-moderation features",
64
+ "Logs": "📋 Logging configuration",
65
+ "DJ": "🎧 DJ music controls",
66
+ "Developer": "💻 Developer tools",
67
+ }
68
+
69
+ _CATEGORY_BILINGUAL = {
70
+ "__all__": "📚 All Commands | جميع الأوامر",
71
+ "__ai__": "🤖 AI | الذكاء الاصطناعي",
72
+ "Music": "🎵 Music | الموسيقى",
73
+ "Admin": "🛡️ Admin | الإدارة",
74
+ "Fun": "🎮 Games | الألعاب",
75
+ "AI": "🤖 AI | الذكاء الاصطناعي",
76
+ "Utility": "🔧 Utility | الأدوات",
77
+ "Config": "⚙️ Config | الإعدادات",
78
+ "Economy": "💰 Economy | الاقتصاد",
79
+ "Moderation": "⚔️ Moderation | الإشراف",
80
+ "Community": "💡 Community | المجتمع",
81
+ "AISuite": "🤖 AI Suite | الذكاء",
82
+ "Configuration": "⚙️ Configuration | الإعدادات",
83
+ "Events": "📋 Events | الأحداث",
84
+ "Verification": "✅ Verification | التحقق",
85
+ }
86
+
87
+
88
+ def _bilingual_category(name: str) -> str:
89
+ return _CATEGORY_BILINGUAL.get(name, f"📁 {name} | {name}")
90
+
91
+
92
+ class CommandsMenuSelect(discord.ui.Select):
93
+ """Beautiful dropdown menu for selecting command categories."""
94
+
95
+ def __init__(
96
+ self,
97
+ cog_names: list[str],
98
+ all_label: str,
99
+ ai_label: str,
100
+ *,
101
+ placeholder: str,
102
+ all_desc: str,
103
+ ai_desc: str,
104
+ cog_desc_map: dict[str, str],
105
+ selected_cog: str = "__all__",
106
+ ) -> None:
107
+
108
+ options = [
109
+ discord.SelectOption(
110
+ label=_bilingual_category("__all__"),
111
+ description=all_desc,
112
+ emoji=ui("book"),
113
+ value="__all__",
114
+ default=selected_cog == "__all__",
115
+ ),
116
+ discord.SelectOption(
117
+ label=_bilingual_category("__ai__"),
118
+ description=ai_desc,
119
+ emoji=ui("robot"),
120
+ value="__ai__",
121
+ default=selected_cog == "__ai__",
122
+ ),
123
+ ]
124
+
125
+ # Add cog options with beautiful emojis
126
+ for name in sorted(cog_names):
127
+ emoji = _CATEGORY_EMOJIS.get(name, _CATEGORY_EMOJIS["default"])
128
+ desc = cog_desc_map.get(name, _CATEGORY_DESCRIPTIONS.get(name, f"{name} commands"))
129
+ # Truncate description to fit Discord's limits
130
+ desc = desc[:100]
131
+ options.append(
132
+ discord.SelectOption(
133
+ label=_bilingual_category(name)[:100],
134
+ description=desc,
135
+ emoji=emoji,
136
+ value=name,
137
+ default=selected_cog == name,
138
+ )
139
+ )
140
+
141
+ super().__init__(
142
+ placeholder=placeholder,
143
+ min_values=1,
144
+ max_values=1,
145
+ options=options
146
+ )
147
+
148
+ async def callback(self, interaction: discord.Interaction) -> None:
149
+ await interaction.response.defer()
150
+ view = self.view
151
+ if not isinstance(view, CommandsMenuView):
152
+ return
153
+ selected = self.values[0]
154
+ view.selected_cog = selected
155
+ view.page = 0
156
+ await view.setup_items()
157
+ embed = await view.build_embed(interaction.guild.id if interaction.guild else None, selected)
158
+ await interaction.followup.edit_message(interaction.message.id, embed=embed, view=view)
159
+
160
+
161
+ class CommandsMenuView(discord.ui.View):
162
+ """Beautiful command menu view with rich decorations."""
163
+
164
+ def __init__(self, bot: commands.Bot, guild_id: int | None = None) -> None:
165
+ super().__init__(timeout=None)
166
+ self.bot = bot
167
+ self.guild_id = guild_id
168
+ self.selected_cog = "__all__"
169
+ self.page = 0
170
+ self.page_size = 18
171
+ self._accent_seed = random.randrange(0, 1024)
172
+ self._color_palette = [NEON_CYAN, NEON_PINK, NEON_PURPLE, NEON_LIME, NEON_GOLD, NEON_MAGENTA]
173
+
174
+ async def setup_items(self) -> None:
175
+ """Setup the menu items."""
176
+ cog_names = list(self.bot.cogs.keys())
177
+ all_label = await self.bot.get_text(self.guild_id, "menu.all")
178
+ ai_label = await self.bot.get_text(self.guild_id, "menu.ai")
179
+ placeholder = await self.bot.get_text(self.guild_id, "menu.select_placeholder")
180
+ all_desc = await self.bot.get_text(self.guild_id, "menu.select_all_desc")
181
+ ai_desc = await self.bot.get_text(self.guild_id, "menu.select_ai_desc")
182
+ cog_desc_map: dict[str, str] = {}
183
+ for name in cog_names:
184
+ key = f"menu.category_desc.{name.lower()}"
185
+ desc = await self.bot.get_text(self.guild_id, key)
186
+ if desc == key:
187
+ desc = _CATEGORY_DESCRIPTIONS.get(name, f"📁 {name} commands")
188
+ cog_desc_map[name] = desc
189
+ self.clear_items()
190
+ self.add_item(
191
+ CommandsMenuSelect(
192
+ cog_names,
193
+ all_label,
194
+ ai_label,
195
+ placeholder=placeholder,
196
+ all_desc=all_desc,
197
+ ai_desc=ai_desc,
198
+ cog_desc_map=cog_desc_map,
199
+ selected_cog=self.selected_cog,
200
+ )
201
+ )
202
+
203
+ refresh_label = await self.bot.get_text(self.guild_id, "menu.refresh")
204
+ invite_label = await self.bot.get_text(self.guild_id, "menu.invite_button")
205
+ self.add_item(RefreshButton(self, refresh_label))
206
+ self.add_item(QuickCategoryButton(self, "Economy | الاقتصاد", "Economy", "💰", row=1))
207
+ self.add_item(QuickCategoryButton(self, "Music | الموسيقى", "Music", "🎵", row=1))
208
+ self.add_item(QuickCategoryButton(self, "Admin | الإدارة", "Admin", "🛡️", row=1))
209
+ self.add_item(QuickCategoryButton(self, "Utility | الأدوات", "Utility", "ℹ️", row=1))
210
+ self.add_item(QuickCategoryButton(self, "Community | المجتمع", "Community", "💡", row=2))
211
+ self.add_item(QuickCategoryButton(self, "AI | الذكاء", "AISuite", "🤖", row=2))
212
+ self.add_item(QuickCategoryButton(self, "Config | الإعداد", "Configuration", "⚙️", row=2))
213
+ if self.page > 0:
214
+ self.add_item(PageButton(self, "prev"))
215
+ if self._has_next_page(self.selected_cog):
216
+ self.add_item(PageButton(self, "next"))
217
+
218
+ if self.bot.user:
219
+ self.add_item(InviteButton(invite_label, self.bot.user.id))
220
+
221
+ def _desc_key(self, qualified_name: str) -> str:
222
+ """Get the translation key for a command description."""
223
+ norm = qualified_name.strip().lower().replace(" ", "_")
224
+ key_mapping = {
225
+ # Music commands
226
+ "play": "menu.cmd.play",
227
+ "music_play": "menu.cmd.music_play",
228
+ "music_panel": "menu.cmd.music_panel",
229
+ "music_skip": "menu.cmd.music_skip",
230
+ "music_stop": "menu.cmd.music_stop",
231
+ "music_pause": "menu.cmd.music_pause",
232
+ "music_resume": "menu.cmd.music_resume",
233
+ "music_queue": "menu.cmd.music_queue",
234
+ "music_playlist": "menu.cmd.music_playlist",
235
+ "music_playlist_save": "menu.cmd.music_playlist_save",
236
+ "music_playlist_rename": "menu.cmd.music_playlist_rename",
237
+ "music_playlist_delete": "menu.cmd.music_playlist_delete",
238
+ "music_volume": "menu.cmd.music_volume",
239
+ "music_nowplaying": "menu.cmd.music_nowplaying",
240
+ "music_filter": "menu.cmd.music_filter",
241
+ "music_loop": "menu.cmd.music_loop",
242
+ "music_shuffle": "menu.cmd.music_shuffle",
243
+ "music_247": "menu.cmd.music_247",
244
+ "music_previous": "menu.cmd.music_previous",
245
+ "music_seek": "menu.cmd.music_seek",
246
+ "music_clear": "menu.cmd.music_clear",
247
+ "music_remove": "menu.cmd.music_remove",
248
+ "music_move": "menu.cmd.music_move",
249
+ "music_jump": "menu.cmd.music_jump",
250
+ "music_lyrics": "menu.cmd.music_lyrics",
251
+
252
+ # Admin commands
253
+ "purge": "menu.cmd.purge",
254
+ "ban": "menu.cmd.ban",
255
+ "unban": "menu.cmd.unban",
256
+ "kick": "menu.cmd.kick",
257
+ "mute": "menu.cmd.mute",
258
+ "unmute": "menu.cmd.unmute",
259
+ "warn": "menu.cmd.warn",
260
+ "warnings": "menu.cmd.warnings",
261
+ "clearwarn": "menu.cmd.clearwarn",
262
+ "slowmode": "menu.cmd.slowmode",
263
+ "lock": "menu.cmd.lock",
264
+ "unlock": "menu.cmd.unlock",
265
+ "cloneemoji": "menu.cmd.cloneemoji",
266
+ "awesomeroles": "menu.cmd.awesomeroles",
267
+ "backupserver": "menu.cmd.backupserver",
268
+
269
+ # Fun commands
270
+ "8ball": "menu.cmd.8ball",
271
+ "meme": "menu.cmd.meme",
272
+ "trivia": "menu.cmd.trivia",
273
+ "gaming_news": "menu.cmd.gaming_news",
274
+ "free_games": "menu.cmd.free_games",
275
+ "gamehub": "menu.cmd.gamehub",
276
+ "xo": "menu.cmd.xo",
277
+ "choose": "menu.cmd.choose",
278
+ "mario": "menu.cmd.mario",
279
+ "dice": "menu.cmd.dice",
280
+ "slots": "menu.cmd.slots",
281
+ "coinflip": "menu.cmd.coinflip",
282
+ "roll": "menu.cmd.roll",
283
+
284
+ # AI commands
285
+ "chat": "menu.cmd.chat",
286
+ "ask_image": "menu.cmd.ask_image",
287
+ "imagine": "menu.cmd.imagine",
288
+ "image_gen": "menu.cmd.image_gen",
289
+ "upscale": "menu.cmd.upscale",
290
+ "summarize": "menu.cmd.summarize",
291
+ "ai": "menu.cmd.ai",
292
+ "ai_chat": "menu.cmd.chat",
293
+ "ai_ask_image": "menu.cmd.ask_image",
294
+ "ai_imagine": "menu.cmd.imagine",
295
+ "ai_image_gen": "menu.cmd.image_gen",
296
+ "ai_upscale": "menu.cmd.upscale",
297
+ "ai_summarize": "menu.cmd.summarize",
298
+ "ai_debug": "menu.cmd.debug",
299
+ "ai_code_gen": "menu.cmd.code_gen",
300
+ "ai_voice": "menu.cmd.speak",
301
+ "ai_speak": "menu.cmd.speak",
302
+ "ai_tts": "menu.cmd.tts",
303
+ "ai_translate_voice": "menu.cmd.translate_voice",
304
+ "ai_model": "menu.cmd.ai_model",
305
+ "ai_channel": "menu.cmd.ai_channel",
306
+ "ai_auto": "menu.cmd.ai_auto",
307
+ "ai_setup": "menu.cmd.ai",
308
+ "ai_execute": "menu.cmd.ai_execute",
309
+
310
+ # Utility commands
311
+ "serverinfo": "menu.cmd.serverinfo",
312
+ "userinfo": "menu.cmd.userinfo",
313
+ "botstats": "menu.cmd.botstats",
314
+ "poll": "menu.cmd.poll",
315
+ "remind": "menu.cmd.remind",
316
+ "avatar": "menu.cmd.avatar",
317
+ "banner": "menu.cmd.banner",
318
+ "translate": "menu.cmd.translate",
319
+
320
+ # Config commands
321
+ "set": "menu.cmd.set",
322
+ "set_log": "menu.cmd.set_log",
323
+ "set_welcome": "menu.cmd.set_welcome",
324
+ "set_suggestions": "menu.cmd.set_suggestions",
325
+ "set_automod": "menu.cmd.set_automod",
326
+ "set_dailymessage": "menu.cmd.set_dailymessage",
327
+ "set_dailychannel": "menu.cmd.set_dailychannel",
328
+ "set_dailytime": "menu.cmd.set_dailytime",
329
+ "set_dailytitle": "menu.cmd.set_dailytitle",
330
+ "set_dailyimage": "menu.cmd.set_dailyimage",
331
+ "set_dailybutton": "menu.cmd.set_dailybutton",
332
+ "set_dailytoggle": "menu.cmd.set_dailytoggle",
333
+ "set_pollchannel": "menu.cmd.set_pollchannel",
334
+ "set_freegames": "menu.cmd.set_freegames",
335
+ "set_gamenews": "menu.cmd.set_gamenews",
336
+ "set_supportai": "menu.cmd.set_supportai",
337
+ "set_wisdom": "menu.cmd.set_wisdom",
338
+
339
+ # Language commands
340
+ "language": "menu.cmd.language",
341
+ "languages": "menu.cmd.languages",
342
+
343
+ # Menu commands
344
+ "menu": "menu.cmd.menu",
345
+ "ping": "menu.cmd.ping",
346
+ "load": "menu.cmd.load",
347
+ "unload": "menu.cmd.unload",
348
+ "reload": "menu.cmd.reload",
349
+ "sync": "menu.cmd.sync",
350
+ "shutdown": "menu.cmd.shutdown",
351
+
352
+ # Verification commands
353
+ "verify": "menu.cmd.verify",
354
+ "verifysetup": "menu.cmd.verifysetup",
355
+ "verify_config": "menu.cmd.verify",
356
+ "verify_config_panel": "menu.cmd.verify",
357
+ "verify_config_setup": "menu.cmd.verifysetup",
358
+
359
+ # Economy commands
360
+ "economy": "menu.cmd.economy",
361
+ "economy_deposit": "menu.cmd.economy_deposit",
362
+ "economy_withdraw": "menu.cmd.economy_withdraw",
363
+ "economy_gamble": "menu.cmd.economy_gamble",
364
+ "economy_rob": "menu.cmd.economy_rob",
365
+ "balance": "menu.cmd.balance",
366
+ "daily": "menu.cmd.daily",
367
+ "work": "menu.cmd.work",
368
+ "gamble": "menu.cmd.gamble",
369
+ "rob": "menu.cmd.rob",
370
+ "leaderboard": "menu.cmd.leaderboard",
371
+ "shop": "menu.cmd.shop",
372
+ "buy": "menu.cmd.buy",
373
+ "inventory": "menu.cmd.inventory",
374
+ "transfer": "menu.cmd.transfer",
375
+
376
+ # Engagement commands
377
+ "xp": "menu.cmd.xp",
378
+ "level": "menu.cmd.level",
379
+ "rank": "menu.cmd.rank",
380
+ "suggest": "menu.cmd.suggest",
381
+ "suggestion": "menu.cmd.suggestion",
382
+ "suggestion_show": "menu.cmd.suggestion_show",
383
+ "suggestion_panel": "menu.cmd.suggestion_panel",
384
+ "suggestion_voters": "menu.cmd.suggestion_voters",
385
+ "giveaway_create": "menu.cmd.giveaway_create",
386
+ "giveaway_end": "menu.cmd.giveaway_end",
387
+ "ticket_panel": "menu.cmd.ticket_panel",
388
+
389
+ # Tournament commands
390
+ "tournament": "menu.cmd.tournament",
391
+ "tournament_panel": "menu.cmd.tournament_panel",
392
+ "tournament_create": "menu.cmd.tournament_create",
393
+ "tournament_join": "menu.cmd.tournament_join",
394
+ "tournament_start": "menu.cmd.tournament_start",
395
+ "tournament_end": "menu.cmd.tournament_end",
396
+ "tournament_gamehub": "menu.cmd.tournament_gamehub",
397
+
398
+ # Gambling commands
399
+ "blackjack": "menu.cmd.blackjack",
400
+ "roulette": "menu.cmd.roulette",
401
+ "rpg": "menu.cmd.rpg",
402
+
403
  # AI Admin commands
404
  "ai_admin": "menu.cmd.ai_admin",
405
  "ai_help": "menu.cmd.ai_help",
406
+ # Additional commands/groups to keep menu fully mapped
407
+ "admin": "menu.cmd.admin_panel",
408
+ "admin_panel": "menu.cmd.admin_panel",
409
+ "shield_level": "menu.cmd.shield_level",
410
+ "shield_state": "menu.cmd.shield_state",
411
+ "econ_admin": "menu.cmd.econ_admin",
412
+ "econadmin": "menu.cmd.econ_admin",
413
+ "economy_admin": "menu.cmd.econ_admin",
414
+ "giveaway": "menu.cmd.giveaway",
415
+ "ticket": "menu.cmd.ticket",
416
+ "poll_legacy": "menu.cmd.poll",
417
+ "setpollchannel": "menu.cmd.set_pollchannel",
418
+ "setsuggestionchannel": "menu.cmd.set_suggestions",
419
+ "set_freegame": "menu.cmd.set_freegames",
420
+ "setupserver": "menu.cmd.setupserver",
421
+ "organizechannels": "menu.cmd.organizechannels",
422
+ "backup_panel": "menu.cmd.backup_panel",
423
+ "system_audit": "menu.cmd.system_audit",
424
+ "wisdom_today": "menu.cmd.wisdom_today",
425
+ "freegames": "menu.cmd.free_games",
426
+ "playlists": "menu.cmd.music_playlist",
427
+ "music": "menu.cmd.music_panel",
428
+ "profile": "menu.cmd.profile",
429
+ "rps": "menu.cmd.rps",
430
+ "guess": "menu.cmd.guess",
431
+ "make_event": "menu.cmd.make_event",
432
+ "gambling_panel": "menu.cmd.gambling_panel",
433
+ "add_scam_image": "menu.cmd.add_scam_image",
434
+ "set_banner": "menu.cmd.set_banner",
435
+ "view_banner": "menu.cmd.view_banner",
436
+ "remove_banner": "menu.cmd.remove_banner",
437
+ "banner_help": "menu.cmd.banner_help",
438
+ "boardgames": "menu.cmd.boardgames",
439
+ "board_start": "menu.cmd.board_start",
440
+ "board_move": "menu.cmd.board_move",
441
+ "board_forfeit": "menu.cmd.board_forfeit",
442
+ "games_panel": "menu.cmd.games_panel",
443
+ "chess": "menu.cmd.chess",
444
+ "checkers": "menu.cmd.checkers",
445
+ "connect4": "menu.cmd.connect4",
446
+ "othello": "menu.cmd.othello",
447
+ "start": "menu.cmd.menu",
448
+ "command_fill": "menu.cmd.menu",
449
+ "tournament_lb": "menu.cmd.tournament_lb",
450
+ "aisetup": "menu.cmd.ai",
451
  }
452
+ return key_mapping.get(norm, f"menu.cmd.{norm}")
453
+
454
+ async def _format_commands(self, guild_id: int | None, cmds: list[commands.Command], max_lines: int = 20, max_chars: int = 980) -> str:
455
+ """Format commands list with beautiful decorations."""
456
+ lines: list[str] = []
457
+ used = 0
458
+
459
+ for idx, cmd in enumerate(sorted(cmds, key=lambda c: c.qualified_name), start=1):
460
+ if cmd.hidden:
461
+ continue
462
+
463
+ desc_key = self._desc_key(cmd.qualified_name)
464
+ translated_desc = await self.bot.get_text(guild_id, desc_key)
465
+ json_key = ""
466
+ if translated_desc == desc_key:
467
+ cog_key = (cmd.cog_name or "").lower()
468
+ json_key = f"commands.{cog_key}.{cmd.name}_desc" if cog_key else ""
469
+ if json_key:
470
+ translated_desc = await self.bot.get_text(guild_id, json_key)
471
+
472
+ if translated_desc == desc_key or (json_key and translated_desc == json_key):
473
+ fallback_desc = (cmd.description or cmd.help or "").strip()
474
+ desc = fallback_desc if fallback_desc else "-"
475
+ else:
476
+ desc = translated_desc
477
+
478
+ is_hybrid = isinstance(cmd, commands.HybridCommand)
479
+ invoke = f"`/{cmd.qualified_name}`" if is_hybrid else f"`!{cmd.qualified_name}`"
480
+ line = f"- {invoke} - {desc[:78]}"
481
+ projected = used + len(line) + (1 if lines else 0)
482
+
483
+ if projected > max_chars:
484
+ break
485
+ lines.append(line)
486
+ used = projected
487
+
488
+ if len(lines) >= max_lines:
489
+ break
490
+
491
  if len(lines) < len([c for c in cmds if not c.hidden]):
492
+ suffix = "\n- ------------"
493
  if used + len(suffix) <= max_chars:
494
+ lines.append("- ------------")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
  return "\n".join(lines)
497
+
498
+ def _visible_commands_page(self, selected_cog: str) -> tuple[list[commands.Command], int]:
499
+ commands_list = sorted(self._selected_commands(selected_cog), key=lambda c: c.qualified_name)
500
+ if not commands_list:
501
+ return [], 1
502
+ total_pages = max(1, (len(commands_list) + self.page_size - 1) // self.page_size)
503
+ if self.page >= total_pages:
504
+ self.page = total_pages - 1
505
+ start = self.page * self.page_size
506
+ return commands_list[start:start + self.page_size], total_pages
507
+
508
+ def _has_next_page(self, selected_cog: str) -> bool:
509
+ commands_list = self._selected_commands(selected_cog)
510
+ total_pages = max(1, (len(commands_list) + self.page_size - 1) // self.page_size)
511
+ return self.page + 1 < total_pages
512
+
513
+ def _category_stats(self) -> list[tuple[str, int]]:
514
+ """Get category statistics."""
515
+ stats: list[tuple[str, int]] = []
516
+ for name, cog in self.bot.cogs.items():
517
+ cog_commands = [c for c in (cog.get_commands() if cog else []) if not c.hidden]
518
+ if cog_commands:
519
+ stats.append((name, len(cog_commands)))
520
+ return sorted(stats, key=lambda pair: (-pair[1], pair[0]))
521
+
522
+ async def _category_value(self, stats: list[tuple[str, int]], guild_id: int | None) -> str:
523
+ """Format category statistics with beautiful decorations."""
524
+ lang = await self.bot.get_guild_language(guild_id)
525
+ if not stats:
526
+ return await self.bot.get_text(guild_id, "menu.none")
527
+
528
+ lines: list[str] = []
529
+ for index, (name, count) in enumerate(stats[:8], start=1):
530
+ badge = {1: "🥇", 2: "🥈", 3: "🥉"}.get(index, f"#{index}")
531
+ emoji = _CATEGORY_EMOJIS.get(name, "📁")
532
+
533
+ if lang == "ar":
534
+ lines.append(f"{badge} {emoji} **{name}** — `{count}` أمر")
535
+ else:
536
+ lines.append(f"{badge} {emoji} **{name}** — `{count}` cmds")
537
+
538
+ if len(stats) > 8:
539
+ lines.append("✦ ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈")
540
+
541
+ return "\n".join(lines)
542
+
543
+ async def _stats_text(
544
+ self,
545
+ guild_id: int | None,
546
+ total_commands: int,
547
+ category_count: int,
548
+ selection_label: str,
549
+ visible_commands: int,
550
+ ) -> str:
551
+ """Format statistics with beautiful decorations."""
552
+ lang = await self.bot.get_guild_language(guild_id)
553
+
554
+ if lang == "ar":
555
+ pieces = [
556
+ f"📊 **إجمالي الأوامر:** `{total_commands}`",
557
+ f"📁 **الفئات:** `{category_count}`",
558
+ f"✨ **المحدد:** {selection_label} (`{visible_commands}` أمر)",
559
+ f"📈 {progress_bar(visible_commands, total_commands, size=14)}",
560
+ ]
561
+ else:
562
+ pieces = [
563
+ f"📊 **Total Commands:** `{total_commands}`",
564
+ f"📁 **Categories:** `{category_count}`",
565
+ f"✨ **Selected:** {selection_label} (`{visible_commands}` cmds)",
566
+ f"📈 {progress_bar(visible_commands, total_commands, size=14)}",
567
+ ]
568
+ return "\n".join(pieces)
569
+
570
+ async def _selected_label(self, guild_id: int | None, selected_cog: str) -> str:
571
+ """Get the label for the selected category."""
572
+ return _bilingual_category(selected_cog)
573
+
574
+ def _selected_commands(self, selected_cog: str) -> list[commands.Command]:
575
+ """Get commands for the selected category."""
576
+ def _flatten(cmds: list[commands.Command]) -> list[commands.Command]:
577
+ collected: list[commands.Command] = []
578
+ for command in cmds:
579
+ if command.hidden:
580
+ continue
581
+ collected.append(command)
582
+ if isinstance(command, commands.Group):
583
+ collected.extend([sub for sub in command.walk_commands() if not sub.hidden])
584
+ unique: dict[str, commands.Command] = {c.qualified_name: c for c in collected}
585
+ return list(unique.values())
586
+
587
+ if selected_cog == "__all__":
588
+ return _flatten(list(self.bot.commands))
589
+ if selected_cog == "__ai__":
590
+ ai_cog = self.bot.get_cog("AISuite")
591
+ return _flatten(list(ai_cog.get_commands() if ai_cog else []))
592
+ cog = self.bot.get_cog(selected_cog)
593
+ return _flatten(list(cog.get_commands() if cog else []))
594
+
595
+ def _top_level_slash_count(self) -> int:
596
+ """Count top-level slash-registered hybrid commands/groups (Discord limit: 100)."""
597
+ total = 0
598
+ for cmd in self.bot.commands:
599
+ if not isinstance(cmd, commands.HybridCommand):
600
+ continue
601
+ if getattr(cmd, "with_app_command", True):
602
+ total += 1
603
+ return total
604
+
605
+ async def build_embed(self, guild_id: int | None, selected_cog: str) -> discord.Embed:
606
+ """Build the beautiful embed for the menu."""
607
+ color = discord.Color(0x2B2D31)
608
+
609
+ hub_welcome = await self.bot.get_text(guild_id, "menu.hub_welcome")
610
+ hub_explore = await self.bot.get_text(guild_id, "menu.hub_explore")
611
+ hub_usage = await self.bot.get_text(guild_id, "menu.hub_usage")
612
+ stats_title = await self.bot.get_text(guild_id, "menu.stats_heading")
613
+ categories_title = await self.bot.get_text(guild_id, "menu.categories_heading")
614
+ tips_title = await self.bot.get_text(guild_id, "menu.tips_heading")
615
+ quick_title = await self.bot.get_text(guild_id, "menu.quick_heading")
616
+ updates_title = await self.bot.get_text(guild_id, "menu.updates_heading")
617
+
618
+ title = "BOT- AI System"
619
+ desc = f"{hub_welcome}\n{hub_explore}\n{hub_usage}"
620
+ tips = (
621
+ f"? {await self.bot.get_text(guild_id, 'menu.tip_line_1')}\n"
622
+ f"? {await self.bot.get_text(guild_id, 'menu.tip_line_2')}\n"
623
+ f"? {await self.bot.get_text(guild_id, 'menu.tip_line_3')}"
624
+ )
625
  quick = (
626
+ f"- `/music_panel` - {await self.bot.get_text(guild_id, 'menu.quick_music')}\n"
627
+ f"- `/gamehub` - {await self.bot.get_text(guild_id, 'menu.quick_gamehub')}\n"
628
+ f"- `/tournament panel` - {await self.bot.get_text(guild_id, 'menu.quick_tournament')}\n"
629
+ f"- `/economy` - {await self.bot.get_text(guild_id, 'menu.quick_economy')}"
630
  )
631
+ quick_ai = await self.bot.get_text(guild_id, 'menu.quick_ai')
632
  updates = (
633
+ f"- `/admin emoji clone` - {await self.bot.get_text(guild_id, 'menu.update_cloneemoji')}\n"
634
+ f"- `/admin shield add_image` - {await self.bot.get_text(guild_id, 'menu.update_shield_image')}\n"
635
+ f"- `/poll create` - {await self.bot.get_text(guild_id, 'menu.update_poll_group')}\n"
636
+ f"- `/economy deposit` - {await self.bot.get_text(guild_id, 'menu.update_economy_group')}\n"
637
+ f"- `/ai execute` - {quick_ai if quick_ai != 'menu.quick_ai' else 'AI admin request'}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
638
  )
639
+ footer = "Powered by BOT- AI Suite"
640
+
641
+ top_divider = panel_divider("cyan")
642
+ mid_divider = panel_divider("purple")
643
+ embed = ImperialMotaz.craft_embed(
644
+ title=title,
645
+ description=f"{top_divider}\n{desc}\n{mid_divider}",
646
+ color=color,
647
+ footer=footer,
648
+ )
649
+
650
+ if self.bot.user:
651
+ embed.set_thumbnail(url=self.bot.user.display_avatar.url)
652
+
653
+ category_stats = self._category_stats()
654
+ selected_label = await self._selected_label(guild_id, selected_cog)
655
+ visible_cmds, total_pages = self._visible_commands_page(selected_cog)
656
+ content = await self._format_commands(guild_id, visible_cmds, max_lines=18)
657
+
658
+ total_guilds = len(self.bot.guilds)
659
+ total_members = sum((g.member_count or 0) for g in self.bot.guilds)
660
+ latency_ms = f"{round(self.bot.latency * 1000)}ms"
661
+ slash_used = self._top_level_slash_count()
662
+ slash_budget = f"{slash_used}/100"
663
+ stats_grid = quick_stats_grid(
664
+ [
665
+ ("Guilds", str(total_guilds), "G"),
666
+ ("Members", f"{total_members:,}", "M"),
667
+ ("Latency", latency_ms, "L"),
668
+ ("Slash", slash_budget, "S"),
669
+ ],
670
+ columns=2,
671
+ )
672
+ embed.add_field(name=stats_title, value=stats_grid, inline=False)
673
+
674
+ embed.add_field(
675
+ name=categories_title,
676
+ value=await self._category_value(category_stats, guild_id),
677
+ inline=True,
678
+ )
679
+
680
+ emoji = _CATEGORY_EMOJIS.get(selected_cog, "C")
681
+ none_text = await self.bot.get_text(guild_id, "menu.none")
682
+ embed.add_field(
683
+ name=f"{emoji} {selected_label}",
684
+ value=content or none_text,
685
+ inline=False,
686
+ )
687
+
688
+ embed.add_field(name=quick_title, value=quick, inline=True)
689
+ embed.add_field(name=tips_title, value=tips, inline=True)
690
+ embed.add_field(name=updates_title, value=updates, inline=False)
691
+
692
+ embed.set_footer(text=footer)
693
+ embed.description = f"{embed.description}\n\nPage {self.page + 1}/{total_pages}"
694
+ return embed
695
+
696
+
697
+ class MainMenuView(CommandsMenuView):
698
+ """Named alias for persistent registration."""
699
+
700
+
701
+ class InviteButton(discord.ui.Button):
702
+ """Beautiful invite button."""
703
+
704
+ def __init__(self, label: str, client_id: int) -> None:
705
+ url = f"https://discord.com/api/oauth2/authorize?client_id={client_id}&permissions=0&scope=applications.commands%20bot"
706
+ super().__init__(
707
+ label=label,
708
+ style=discord.ButtonStyle.link,
709
+ url=url,
710
+ emoji=ui("link")
711
+ )
712
+
713
+
714
+ class RefreshButton(discord.ui.Button):
715
+ """Beautiful refresh button."""
716
+
717
+ def __init__(self, parent: CommandsMenuView, label: str) -> None:
718
+ super().__init__(
719
+ label=label,
720
+ emoji=ui("refresh"),
721
+ style=discord.ButtonStyle.secondary
722
+ )
723
+ self.parent_view = parent
724
+
725
+ async def callback(self, interaction: discord.Interaction) -> None:
726
+ await interaction.response.defer()
727
  guild_id = interaction.guild.id if interaction.guild else None
728
+ self.label = await self.parent_view.bot.get_text(guild_id, "menu.refresh")
729
+ await self.parent_view.setup_items()
730
+ embed = await self.parent_view.build_embed(guild_id, "__all__")
731
+ await interaction.followup.edit_message(interaction.message.id, embed=embed, view=self.parent_view)
732
+
733
+
734
+ class QuickCategoryButton(discord.ui.Button):
735
+ def __init__(self, parent: CommandsMenuView, label: str, category: str, emoji: str, *, row: int = 1) -> None:
736
+ super().__init__(
737
+ label=label,
738
+ emoji=emoji,
739
+ style=discord.ButtonStyle.secondary,
740
+ custom_id=f"menu:quick:{label.lower()}",
741
+ row=row,
742
+ )
743
+ self.parent_view = parent
744
+ self.category = category
745
+
746
+ async def callback(self, interaction: discord.Interaction) -> None:
747
+ await interaction.response.defer()
748
+ self.parent_view.selected_cog = self.category
749
+ self.parent_view.page = 0
750
+ await self.parent_view.setup_items()
751
+ embed = await self.parent_view.build_embed(interaction.guild.id if interaction.guild else None, self.category)
752
+ await interaction.followup.edit_message(interaction.message.id, embed=embed, view=self.parent_view)
753
+
754
+
755
+ class PageButton(discord.ui.Button):
756
+ def __init__(self, parent: CommandsMenuView, direction: str) -> None:
757
+ self.parent_view = parent
758
+ self.direction = direction
759
+ label = "Previous" if direction == "prev" else "Next"
760
+ emoji = "⬅️" if direction == "prev" else "➡️"
761
+ super().__init__(label=label, emoji=emoji, style=discord.ButtonStyle.secondary)
762
+
763
+ async def callback(self, interaction: discord.Interaction) -> None:
764
+ await interaction.response.defer()
765
+ if self.direction == "prev":
766
+ self.parent_view.page = max(0, self.parent_view.page - 1)
767
+ else:
768
+ if self.parent_view._has_next_page(self.parent_view.selected_cog):
769
+ self.parent_view.page += 1
770
+ await self.parent_view.setup_items()
771
+ embed = await self.parent_view.build_embed(interaction.guild.id if interaction.guild else None, self.parent_view.selected_cog)
772
+ await interaction.followup.edit_message(interaction.message.id, embed=embed, view=self.parent_view)
773
+
774
+
775
+ class Menu(commands.Cog):
776
+ """Interactive command menu with beautiful panels and multi-language support."""
777
+
778
+ def __init__(self, bot: commands.Bot) -> None:
779
+ self.bot = bot
780
+
 
781
  async def cog_load(self) -> None:
782
+ self.bot.add_view(CommandsMenuView(self.bot))
783
+
784
+ @commands.hybrid_command(name="menu", description="Bot menu | قائمة أوامر واضحة")
785
+ async def menu(self, ctx: commands.Context) -> None:
786
+ """Display the beautiful command menu."""
787
+ if ctx.interaction and not ctx.interaction.response.is_done():
788
+ await ctx.interaction.response.defer()
789
+ guild_id = ctx.guild.id if ctx.guild else None
790
+ view = CommandsMenuView(self.bot, guild_id)
791
+ await view.setup_items()
792
+ embed = await view.build_embed(guild_id, "__all__")
793
+ if ctx.interaction:
794
+ await ctx.interaction.followup.send(embed=embed, view=view)
795
+ else:
796
+ await ctx.reply(embed=embed, view=view)
797
+
798
+ @commands.hybrid_command(name="start", description="Start menu | القائمة الرئيسية", with_app_command=False)
799
+ async def start_menu(self, ctx: commands.Context) -> None:
800
+ await self.menu(ctx)
801
+
802
+ async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None:
803
+ embed = ImperialMotaz.craft_embed(
804
+ title="⚠️ Command Error | خطأ في الأمر",
805
+ description=f"「 {str(error)[:1000]} 」",
806
+ color=discord.Color(0x2B2D31),
807
+ footer="🏮 Powered by BOT- AI Suite 🏮",
808
+ )
809
+ try:
810
+ await ctx.reply(embed=embed)
811
+ except Exception:
812
+ if ctx.channel:
813
+ await ctx.channel.send(embed=embed)
814
+
815
+
816
+ async def setup(bot: commands.Bot) -> None:
817
+ await bot.add_cog(Menu(bot))
818
+
819
+
bot/cogs/observability.py CHANGED
@@ -1,14 +1,16 @@
1
- from __future__ import annotations
2
-
3
- import difflib
4
- import traceback
5
- from typing import get_args, get_origin
 
 
6
 
7
  import discord
8
  from discord.ext import commands
9
 
10
 
11
- class Observability(commands.Cog):
12
  def __init__(self, bot: commands.Bot) -> None:
13
  self.bot = bot
14
 
@@ -122,8 +124,8 @@ class Observability(commands.Cog):
122
  embed.add_field(name="Error", value=f"```py\n{tb[-900:]}\n```", inline=False)
123
  await channel.send(embed=embed)
124
 
125
- @commands.hybrid_command(name="command_fill", description="Show how to fill command arguments")
126
- async def command_fill(self, ctx: commands.Context, *, command_name: str) -> None:
127
  command = self.bot.get_command(command_name.strip())
128
  if not command:
129
  close = self._closest_commands(command_name.strip())
@@ -141,8 +143,70 @@ class Observability(commands.Cog):
141
  usage = f"{ctx.prefix}{command.qualified_name} " + " ".join(pieces)
142
  embed = discord.Embed(title="🧩 Command Fill Helper", color=discord.Color.blurple())
143
  embed.add_field(name="Usage", value=f"`{usage.strip()}`", inline=False)
144
- embed.add_field(name="Argument hints", value="\n".join(details) if details else "No arguments", inline=False)
145
- await ctx.reply(embed=embed)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
 
148
  async def setup(bot: commands.Bot) -> None:
 
1
+ from __future__ import annotations
2
+
3
+ import difflib
4
+ import json
5
+ import traceback
6
+ from pathlib import Path
7
+ from typing import get_args, get_origin
8
 
9
  import discord
10
  from discord.ext import commands
11
 
12
 
13
+ class Observability(commands.Cog):
14
  def __init__(self, bot: commands.Bot) -> None:
15
  self.bot = bot
16
 
 
124
  embed.add_field(name="Error", value=f"```py\n{tb[-900:]}\n```", inline=False)
125
  await channel.send(embed=embed)
126
 
127
+ @commands.hybrid_command(name="command_fill", description="Show how to fill command arguments")
128
+ async def command_fill(self, ctx: commands.Context, *, command_name: str) -> None:
129
  command = self.bot.get_command(command_name.strip())
130
  if not command:
131
  close = self._closest_commands(command_name.strip())
 
143
  usage = f"{ctx.prefix}{command.qualified_name} " + " ".join(pieces)
144
  embed = discord.Embed(title="🧩 Command Fill Helper", color=discord.Color.blurple())
145
  embed.add_field(name="Usage", value=f"`{usage.strip()}`", inline=False)
146
+ embed.add_field(name="Argument hints", value="\n".join(details) if details else "No arguments", inline=False)
147
+ await ctx.reply(embed=embed)
148
+
149
+ @commands.hybrid_command(name="system_audit", description="Run a quick runtime health audit")
150
+ @commands.has_permissions(manage_guild=True)
151
+ async def system_audit(self, ctx: commands.Context) -> None:
152
+ await self._safe_reply(ctx, "Running system audit...")
153
+
154
+ total_commands = len(self.bot.commands)
155
+ loaded_cogs = len(self.bot.cogs)
156
+
157
+ row = await self.bot.db.fetchone("PRAGMA table_info(guild_config)")
158
+ has_guild_config = bool(row)
159
+ cols = await self.bot.db.fetchall("PRAGMA table_info(guild_config)")
160
+ col_names = {str(r[1]) for r in cols} if cols else set()
161
+ has_banner_col = "custom_banner_url" in col_names
162
+ has_lang_col = "guild_language" in col_names
163
+
164
+ locale_dir = Path("bot/locales")
165
+ locale_files = sorted(p.name for p in locale_dir.glob("*.json")) if locale_dir.exists() else []
166
+ locale_issues = 0
167
+ if locale_files:
168
+ try:
169
+ base = json.loads((locale_dir / "en.json").read_text(encoding="utf-8"))
170
+ def flatten(obj, prefix=""):
171
+ out = {}
172
+ if isinstance(obj, dict):
173
+ for k, v in obj.items():
174
+ key = f"{prefix}.{k}" if prefix else k
175
+ out.update(flatten(v, key))
176
+ else:
177
+ out[prefix] = obj
178
+ return out
179
+ base_keys = set(flatten(base).keys())
180
+ for name in locale_files:
181
+ data = json.loads((locale_dir / name).read_text(encoding="utf-8"))
182
+ keys = set(flatten(data).keys())
183
+ if base_keys - keys:
184
+ locale_issues += 1
185
+ except Exception:
186
+ locale_issues += 1
187
+
188
+ checks = [
189
+ ("Commands registered", total_commands >= 50, str(total_commands)),
190
+ ("Cogs loaded", loaded_cogs >= 8, str(loaded_cogs)),
191
+ ("guild_config table", has_guild_config, "ok" if has_guild_config else "missing"),
192
+ ("guild_language column", has_lang_col, "ok" if has_lang_col else "missing"),
193
+ ("custom_banner_url column", has_banner_col, "ok" if has_banner_col else "missing"),
194
+ ("Locale files", bool(locale_files), f"{len(locale_files)} files"),
195
+ ("Locale consistency", locale_issues == 0, "ok" if locale_issues == 0 else f"{locale_issues} issue(s)"),
196
+ ]
197
+
198
+ passed = sum(1 for _, ok, _ in checks if ok)
199
+ status = "HEALTHY" if passed == len(checks) else "NEEDS ATTENTION"
200
+ color = discord.Color.green() if passed == len(checks) else discord.Color.orange()
201
+ lines = [f"{'✅' if ok else '⚠️'} {name}: `{detail}`" for name, ok, detail in checks]
202
+
203
+ embed = discord.Embed(
204
+ title=f"System Audit: {status}",
205
+ description="\n".join(lines),
206
+ color=color,
207
+ )
208
+ embed.set_footer(text=f"Passed {passed}/{len(checks)} checks")
209
+ await ctx.reply(embed=embed)
210
 
211
 
212
  async def setup(bot: commands.Bot) -> None:
bot/database.py CHANGED
@@ -59,6 +59,8 @@ class Database:
59
  Path(self.path).parent.mkdir(parents=True, exist_ok=True)
60
  await asyncio.to_thread(shutil.copy2, local_file, self.path)
61
  except Exception:
 
 
62
  return
63
 
64
  async def _push_remote_db(self) -> None:
@@ -75,6 +77,8 @@ class Database:
75
  commit_message="Bot DB sync",
76
  )
77
  except Exception:
 
 
78
  return
79
 
80
  async def _ensure_column(
@@ -337,6 +341,15 @@ class Database:
337
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
338
  PRIMARY KEY (guild_id, image_hash)
339
  );
 
 
 
 
 
 
 
 
 
340
  """
341
  )
342
 
@@ -375,6 +388,7 @@ class Database:
375
  await self._ensure_column(db, "guild_config", "game_news_channel_id", "INTEGER")
376
  await self._ensure_column(db, "guild_config", "game_news_role_id", "INTEGER")
377
  await self._ensure_column(db, "guild_config", "game_news_last_ids", "TEXT")
 
378
 
379
  await db.commit()
380
  await self._push_remote_db()
@@ -405,15 +419,16 @@ class Database:
405
  """Clean up all data for a guild (for when bot leaves a server)."""
406
  async with self._lock:
407
  db = await self._get_connection()
408
- await db.execute("DELETE FROM guild_config WHERE guild_id = ?", guild_id)
409
- await db.execute("DELETE FROM user_xp WHERE guild_id = ?", guild_id)
410
- await db.execute("DELETE FROM user_balance WHERE guild_id = ?", guild_id)
411
- await db.execute("DELETE FROM user_daily_claim WHERE guild_id = ?", guild_id)
412
- await db.execute("DELETE FROM user_work_cooldown WHERE guild_id = ?", guild_id)
413
- await db.execute("DELETE FROM warns WHERE guild_id = ?", guild_id)
414
- await db.execute("DELETE FROM tournaments WHERE guild_id = ?", guild_id)
415
- await db.execute("DELETE FROM tournament_participants WHERE guild_id = ?", guild_id)
416
- await db.execute("DELETE FROM tournament_games WHERE guild_id = ?", guild_id)
417
- await db.execute("DELETE FROM voice_sessions WHERE guild_id = ?", guild_id)
 
418
  await db.commit()
419
  await self._push_remote_db()
 
59
  Path(self.path).parent.mkdir(parents=True, exist_ok=True)
60
  await asyncio.to_thread(shutil.copy2, local_file, self.path)
61
  except Exception:
62
+ # Disable further sync attempts to avoid repeated noisy auth/network failures.
63
+ self._hf_sync_enabled = False
64
  return
65
 
66
  async def _push_remote_db(self) -> None:
 
77
  commit_message="Bot DB sync",
78
  )
79
  except Exception:
80
+ # Disable further sync attempts to avoid repeated noisy auth/network failures.
81
+ self._hf_sync_enabled = False
82
  return
83
 
84
  async def _ensure_column(
 
341
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
342
  PRIMARY KEY (guild_id, image_hash)
343
  );
344
+
345
+ CREATE TABLE IF NOT EXISTS bot_presence_config (
346
+ id INTEGER PRIMARY KEY CHECK (id = 1),
347
+ status TEXT DEFAULT 'online',
348
+ activity_type TEXT DEFAULT 'playing',
349
+ activity_text TEXT DEFAULT 'CYBER // GRID',
350
+ updated_by INTEGER,
351
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
352
+ );
353
  """
354
  )
355
 
 
388
  await self._ensure_column(db, "guild_config", "game_news_channel_id", "INTEGER")
389
  await self._ensure_column(db, "guild_config", "game_news_role_id", "INTEGER")
390
  await self._ensure_column(db, "guild_config", "game_news_last_ids", "TEXT")
391
+ await self._ensure_column(db, "guild_config", "custom_banner_url", "TEXT")
392
 
393
  await db.commit()
394
  await self._push_remote_db()
 
419
  """Clean up all data for a guild (for when bot leaves a server)."""
420
  async with self._lock:
421
  db = await self._get_connection()
422
+ params = (guild_id,)
423
+ await db.execute("DELETE FROM guild_config WHERE guild_id = ?", params)
424
+ await db.execute("DELETE FROM user_xp WHERE guild_id = ?", params)
425
+ await db.execute("DELETE FROM user_balance WHERE guild_id = ?", params)
426
+ await db.execute("DELETE FROM user_daily_claim WHERE guild_id = ?", params)
427
+ await db.execute("DELETE FROM user_work_cooldown WHERE guild_id = ?", params)
428
+ await db.execute("DELETE FROM warns WHERE guild_id = ?", params)
429
+ await db.execute("DELETE FROM tournaments WHERE guild_id = ?", params)
430
+ await db.execute("DELETE FROM tournament_participants WHERE guild_id = ?", params)
431
+ await db.execute("DELETE FROM tournament_games WHERE guild_id = ?", params)
432
+ await db.execute("DELETE FROM voice_sessions WHERE guild_id = ?", params)
433
  await db.commit()
434
  await self._push_remote_db()
bot/emojis.py CHANGED
@@ -33,8 +33,8 @@ FALLBACK_EMOJIS: dict[str, str] = {
33
  "catjam": "😺", "djpeepo": "🎧", "spotify": "🎵", "partytime": "🎉", "letsgo": "🚀",
34
 
35
  # Arrows & decorations
36
- "arrow_green": "🟢", "arrow_pink": "✅", "arrow_blue": "🔵", "arrow_purple": "🟢",
37
- "arrow_orange": "🟠", "arrow_yellow": "🟡", "shield": "🛡️", "gift": "🎁", "hype": "🔥",
38
 
39
  # UI elements
40
  "notebook": "📓", "ticket": "🎫", "verified": "✅", "error": "❌", "loading": "⏳",
@@ -44,8 +44,8 @@ FALLBACK_EMOJIS: dict[str, str] = {
44
  "num_6": "6️⃣", "num_7": "7️⃣", "num_8": "8️⃣", "num_9": "9️⃣", "num_10": "🔟",
45
 
46
  # Enhanced decorations
47
- "sparkle": "✨", "fire": "🔥", "heart": "✅", "purple_heart": "🟢", "blue_heart": "🔵",
48
- "green_heart": "🟢", "yellow_heart": "🟡", "orange_heart": "🟠", "pink_heart": "✅",
49
  "crown": "👑", "trophy": "🏆", "medal": "🎖️", "ribbon": "🎀", "gem": "💎",
50
  "crystal_ball": "🔮", "comet": "☄️", "star2": "✅", "star3": "✅", "dizzy": "💫",
51
  "boom": "💥", "zap": "⚡", "warning": "⚠️", "information": "ℹ️", "question": "❓",
@@ -67,7 +67,7 @@ FALLBACK_EMOJIS: dict[str, str] = {
67
  "credit_card": "💳", "chart": "📈", "bank": "🏦", "atm": "🏧", "receipt": "🧾",
68
 
69
  # Decorative borders
70
- "line": "▬", "dash": "—", "dot": "•", "bullet": "•", "circle": "🟢",
71
  "square": "■", "diamond_shape": "◆", "triangle": "▲", "star_border": "✅",
72
  }
73
 
@@ -96,6 +96,16 @@ def _normalize_key(value: str) -> str:
96
  return (value or "").strip().lower().replace(" ", "_")
97
 
98
 
 
 
 
 
 
 
 
 
 
 
99
  def set_emoji_bot(bot: discord.Client) -> None:
100
  """Register bot/client instance for dynamic custom emoji lookup."""
101
  global _EMOJI_BOT
@@ -198,11 +208,17 @@ def _parse_emoji_file() -> dict[str, str]:
198
  if key and value:
199
  # Check if value is already in full format
200
  if (value.startswith("<:") or value.startswith("<a:")) and value.endswith(">"):
201
- # Already full format, use as-is
202
- parsed[key] = value
 
 
 
 
 
 
203
  elif value.isdigit():
204
  # Just the ID, create full format
205
- parsed[key] = f"<:{name.strip()}:{value}>"
206
  else:
207
  # Some other format, try to use as-is
208
  parsed[key] = value
@@ -663,7 +679,7 @@ E_DIZZY = "💫"
663
  E_BOOM = "💥"
664
  E_ZAP = "⚡"
665
  E_HEART = "✅"
666
- E_PURPLE_HEART = "🟢"
667
  E_BLUE_HEART = "💙"
668
- E_GREEN_HEART = "💚"
669
  E_YELLOW_HEART = "💛"
 
33
  "catjam": "😺", "djpeepo": "🎧", "spotify": "🎵", "partytime": "🎉", "letsgo": "🚀",
34
 
35
  # Arrows & decorations
36
+ "arrow_green": "<:animatedarrowgreen:1477261279428087979>", "arrow_pink": "✅", "arrow_blue": "🔵", "arrow_purple": "<:animatedarrowgreen:1477261279428087979>",
37
+ "arrow_orange": "🟠", "arrow_yellow": "<:animatedarrowyellow:1477261257592668271>", "shield": "🛡️", "gift": "🎁", "hype": "🔥",
38
 
39
  # UI elements
40
  "notebook": "📓", "ticket": "🎫", "verified": "✅", "error": "❌", "loading": "⏳",
 
44
  "num_6": "6️⃣", "num_7": "7️⃣", "num_8": "8️⃣", "num_9": "9️⃣", "num_10": "🔟",
45
 
46
  # Enhanced decorations
47
+ "sparkle": "✨", "fire": "🔥", "heart": "✅", "purple_heart": "<:animatedarrowgreen:1477261279428087979>", "blue_heart": "🔵",
48
+ "green_heart": "<:animatedarrowgreen:1477261279428087979>", "yellow_heart": "<:animatedarrowyellow:1477261257592668271>", "orange_heart": "🟠", "pink_heart": "✅",
49
  "crown": "👑", "trophy": "🏆", "medal": "🎖️", "ribbon": "🎀", "gem": "💎",
50
  "crystal_ball": "🔮", "comet": "☄️", "star2": "✅", "star3": "✅", "dizzy": "💫",
51
  "boom": "💥", "zap": "⚡", "warning": "⚠️", "information": "ℹ️", "question": "❓",
 
67
  "credit_card": "💳", "chart": "📈", "bank": "🏦", "atm": "🏧", "receipt": "🧾",
68
 
69
  # Decorative borders
70
+ "line": "▬", "dash": "—", "dot": "•", "bullet": "•", "circle": "<:animatedarrowgreen:1477261279428087979>",
71
  "square": "■", "diamond_shape": "◆", "triangle": "▲", "star_border": "✅",
72
  }
73
 
 
96
  return (value or "").strip().lower().replace(" ", "_")
97
 
98
 
99
+ def _sanitize_emoji_name(name: str) -> str:
100
+ cleaned = re.sub(r"[^A-Za-z0-9_]", "_", (name or "").strip())
101
+ cleaned = re.sub(r"_+", "_", cleaned).strip("_")
102
+ if not cleaned:
103
+ return "emoji"
104
+ if len(cleaned) < 2:
105
+ return f"{cleaned}_x"
106
+ return cleaned[:32]
107
+
108
+
109
  def set_emoji_bot(bot: discord.Client) -> None:
110
  """Register bot/client instance for dynamic custom emoji lookup."""
111
  global _EMOJI_BOT
 
208
  if key and value:
209
  # Check if value is already in full format
210
  if (value.startswith("<:") or value.startswith("<a:")) and value.endswith(">"):
211
+ cfg = _parse_custom_emoji_config(value)
212
+ if cfg is not None:
213
+ cfg_name, emoji_id, animated = cfg
214
+ parsed[key] = _build_custom_emoji_code(
215
+ _sanitize_emoji_name(cfg_name), emoji_id, animated
216
+ )
217
+ else:
218
+ parsed[key] = value
219
  elif value.isdigit():
220
  # Just the ID, create full format
221
+ parsed[key] = f"<:{_sanitize_emoji_name(name)}:{value}>"
222
  else:
223
  # Some other format, try to use as-is
224
  parsed[key] = value
 
679
  E_BOOM = "💥"
680
  E_ZAP = "⚡"
681
  E_HEART = "✅"
682
+ E_PURPLE_HEART = "<:animatedarrowgreen:1477261279428087979>"
683
  E_BLUE_HEART = "💙"
684
+ E_GREEN_HEART = "<:animatedarrowgreen:1477261279428087979>"
685
  E_YELLOW_HEART = "💛"
bot/i18n.py CHANGED
@@ -20,7 +20,7 @@ TRANSLATIONS["ar"] = {
20
  # === Language Commands ===
21
  "lang.current": "{globe} لغة السيرفر الحالية: **العربية**.",
22
  "lang.updated": "{ok} تم تغيير لغة البوت إلى **العربية**. استخدم `/menu` لرؤية الأوامر باللغة المحددة.",
23
- "lang.invalid": "{no} لغة غير مدعومة. استخدم: `ar`, `en`, `es`, `fr`, `de`, `tr`, `it`, `pt`, `ru`, `hi`, `id`, `ja`, `he`.",
24
 
25
  # === Menu System ===
26
  "menu.title": "{menu} قائمة أوامر البوت",
@@ -629,7 +629,7 @@ TRANSLATIONS["en"] = {
629
  # === Language Commands ===
630
  "lang.current": "{globe} Current server language: **English**.",
631
  "lang.updated": "{ok} Bot language changed to **English**. Use `/menu` to view commands in the selected language.",
632
- "lang.invalid": "{no} Unsupported language. Use: `ar`, `en`, `es`, `fr`, `de`, `tr`, `it`, `pt`, `ru`, `hi`, `id`, `ja`, `he`.",
633
 
634
  # === Menu System ===
635
  "menu.title": "{menu} Bot Command Menu",
@@ -1445,6 +1445,19 @@ TRANSLATIONS["he"] = {
1445
  "ping.desc": "זמן תגובה: **{latency}ms**",
1446
  }
1447
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1448
  # ═══════════════════════════════════════════════════════════════════════════════
1449
  # HELPER FUNCTIONS
1450
  # ═══════════════════════════════════════════════════════════════════════════════
 
20
  # === Language Commands ===
21
  "lang.current": "{globe} لغة السيرفر الحالية: **العربية**.",
22
  "lang.updated": "{ok} تم تغيير لغة البوت إلى **العربية**. استخدم `/menu` لرؤية الأوامر باللغة المحددة.",
23
+ "lang.invalid": "{no} لغة غير مدعومة. استخدم: `ar`, `de`, `en`, `es`, `fr`, `he`, `hi`, `id`, `it`, `ja`, `pt`, `ru`, `tr`, `zh`.",
24
 
25
  # === Menu System ===
26
  "menu.title": "{menu} قائمة أوامر البوت",
 
629
  # === Language Commands ===
630
  "lang.current": "{globe} Current server language: **English**.",
631
  "lang.updated": "{ok} Bot language changed to **English**. Use `/menu` to view commands in the selected language.",
632
+ "lang.invalid": "{no} Unsupported language. Use: `ar`, `de`, `en`, `es`, `fr`, `he`, `hi`, `id`, `it`, `ja`, `pt`, `ru`, `tr`, `zh`.",
633
 
634
  # === Menu System ===
635
  "menu.title": "{menu} Bot Command Menu",
 
1445
  "ping.desc": "זמן תגובה: **{latency}ms**",
1446
  }
1447
 
1448
+ # Chinese (fallback pack; missing keys are filled from English below)
1449
+ TRANSLATIONS["zh"] = {
1450
+ "lang.current": "{globe} 当前服务器语言: **中文**。",
1451
+ "lang.updated": "{ok} 机器人语言已切换为 **中文**。",
1452
+ "lang.invalid": "{no} 不支持的语言。",
1453
+ "menu.title": "{menu} 命令菜单",
1454
+ "menu.all": "全部",
1455
+ "menu.ai": "AI 面板",
1456
+ "menu.refresh": "刷新",
1457
+ "ping.title": "{ping} Pong",
1458
+ "ping.desc": "延迟: **{latency}ms**",
1459
+ }
1460
+
1461
  # ═══════════════════════════════════════════════════════════════════════════════
1462
  # HELPER FUNCTIONS
1463
  # ═══════════════════════════════════════════════════════════════════════════════
bot/locales/ar.json CHANGED
@@ -89,6 +89,12 @@
89
  },
90
  "ai": {
91
  "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』"
 
 
 
 
 
 
92
  }
93
  },
94
  "errors": {
@@ -267,8 +273,163 @@
267
  "set_banner": "تعيين بنر مخصص لهذا السيرفر",
268
  "remove_banner": "إزالة البنر المخصص من هذا السيرفر",
269
  "view_banner": "عرض البنر المخصص الحالي لهذا السيرفر",
270
- "banner_help": "تعلم كيفية تعيين بنر مخصص"
271
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  },
273
  "gambling": {
274
  "blackjack": {
@@ -396,5 +557,19 @@
396
  "trivia_title": "سؤال مسابقة",
397
  "trivia_correct": "إجابة صحيحة!",
398
  "trivia_wrong": "إجابة خاطئة!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  }
400
- }
 
89
  },
90
  "ai": {
91
  "header": "『 <:birdmusic:1476278789251268824> 𝔸𝕀 𝕆𝕣𝕒𝕔𝕝𝕖 〣 』"
92
+ },
93
+ "ai_architect": {
94
+ "title": "?? ????? ????? ????? ?????? ?????????",
95
+ "proposal": "??????? ???????? ???????: {sections}",
96
+ "analyzing": "???? ????? ??????? ???????...",
97
+ "success": "????? ????? ????? ?????? ????????? ?????."
98
  }
99
  },
100
  "errors": {
 
273
  "set_banner": "تعيين بنر مخصص لهذا السيرفر",
274
  "remove_banner": "إزالة البنر المخصص من هذا السيرفر",
275
  "view_banner": "عرض البنر المخصص الحالي لهذا السيرفر",
276
+ "banner_help": "تعلم كيفية تعيين بنر مخصص",
277
+ "8ball": "8ball",
278
+ "add_scam_image": "Save scam image signature",
279
+ "admin_panel": "Open admin control panel",
280
+ "ai": "Ai",
281
+ "ai_auto": "Ai auto",
282
+ "ai_channel": "Ai channel",
283
+ "ai_model": "Ai model",
284
+ "ask_image": "Ask image",
285
+ "avatar": "Avatar",
286
+ "awesomeroles": "Awesomeroles",
287
+ "backup_panel": "Open backup management panel",
288
+ "backupserver": "Backupserver",
289
+ "balance": "Balance",
290
+ "ban": "Ban",
291
+ "banner": "Banner",
292
+ "board_forfeit": "Board forfeit",
293
+ "board_move": "Board move",
294
+ "board_start": "Board start",
295
+ "boardgames": "Boardgames",
296
+ "botstats": "Botstats",
297
+ "buy": "Buy",
298
+ "chat": "Chat",
299
+ "checkers": "Checkers",
300
+ "chess": "Chess",
301
+ "choose": "Choose",
302
+ "clearwarn": "Clearwarn",
303
+ "cloneemoji": "Cloneemoji",
304
+ "code_gen": "Code gen",
305
+ "coinflip": "Coinflip",
306
+ "connect4": "Connect4",
307
+ "daily": "Daily",
308
+ "debug": "Debug",
309
+ "dice": "Dice",
310
+ "econ_admin": "Manage user economy balances",
311
+ "economy": "Economy",
312
+ "economy_deposit": "Economy deposit",
313
+ "economy_gamble": "Economy gamble",
314
+ "economy_rob": "Economy rob",
315
+ "economy_withdraw": "Economy withdraw",
316
+ "free_games": "Free games",
317
+ "gamble": "Gamble",
318
+ "gamehub": "Gamehub",
319
+ "games_panel": "Games panel",
320
+ "gaming_news": "Gaming news",
321
+ "giveaway": "Giveaway command group",
322
+ "guess": "Guess",
323
+ "image_gen": "Image gen",
324
+ "imagine": "Imagine",
325
+ "inventory": "Inventory",
326
+ "kick": "Kick",
327
+ "language": "Language",
328
+ "languages": "Languages",
329
+ "leaderboard": "Leaderboard",
330
+ "level": "Level",
331
+ "load": "Load",
332
+ "lock": "Lock",
333
+ "make_event": "Make event",
334
+ "mario": "Mario",
335
+ "meme": "Meme",
336
+ "menu": "Menu",
337
+ "music_247": "Music 247",
338
+ "music_clear": "Music clear",
339
+ "music_filter": "Music filter",
340
+ "music_jump": "Music jump",
341
+ "music_loop": "Music loop",
342
+ "music_lyrics": "Music lyrics",
343
+ "music_move": "Music move",
344
+ "music_nowplaying": "Music nowplaying",
345
+ "music_panel": "Music panel",
346
+ "music_pause": "Music pause",
347
+ "music_play": "Music play",
348
+ "music_playlist_delete": "Music playlist delete",
349
+ "music_playlist_rename": "Music playlist rename",
350
+ "music_playlist_save": "Music playlist save",
351
+ "music_previous": "Music previous",
352
+ "music_queue": "Music queue",
353
+ "music_remove": "Music remove",
354
+ "music_resume": "Music resume",
355
+ "music_seek": "Music seek",
356
+ "music_shuffle": "Music shuffle",
357
+ "music_skip": "Music skip",
358
+ "music_stop": "Music stop",
359
+ "music_volume": "Music volume",
360
+ "mute": "Mute",
361
+ "organizechannels": "Organizechannels",
362
+ "othello": "Othello",
363
+ "ping": "Ping",
364
+ "play": "Play",
365
+ "poll": "Poll",
366
+ "profile": "Profile",
367
+ "purge": "Purge",
368
+ "rank": "Rank",
369
+ "reload": "Reload",
370
+ "remind": "Remind",
371
+ "rob": "Rob",
372
+ "roll": "Roll",
373
+ "rps": "Rps",
374
+ "serverinfo": "Serverinfo",
375
+ "set": "Set",
376
+ "set_automod": "Set automod",
377
+ "set_dailybutton": "Set dailybutton",
378
+ "set_dailychannel": "Set dailychannel",
379
+ "set_dailyimage": "Set dailyimage",
380
+ "set_dailymessage": "Set dailymessage",
381
+ "set_dailytime": "Set dailytime",
382
+ "set_dailytitle": "Set dailytitle",
383
+ "set_dailytoggle": "Set dailytoggle",
384
+ "set_freegames": "Set freegames",
385
+ "set_gamenews": "Set gamenews",
386
+ "set_log": "Set log",
387
+ "set_pollchannel": "Set pollchannel",
388
+ "set_suggestions": "Set suggestions",
389
+ "set_supportai": "Set supportai",
390
+ "set_welcome": "Set welcome",
391
+ "set_wisdom": "Set wisdom",
392
+ "setupserver": "Setupserver",
393
+ "shield_level": "Set shield sensitivity level",
394
+ "shield_state": "Show current shield state",
395
+ "shop": "Shop",
396
+ "shutdown": "Shutdown",
397
+ "slots": "Slots",
398
+ "slowmode": "Slowmode",
399
+ "speak": "Speak",
400
+ "summarize": "Summarize",
401
+ "sync": "Sync",
402
+ "system_audit": "Show system audit and diagnostics",
403
+ "ticket": "Ticket command group",
404
+ "tournament": "Tournament",
405
+ "tournament_create": "Tournament create",
406
+ "tournament_end": "Tournament end",
407
+ "tournament_gamehub": "Tournament gamehub",
408
+ "tournament_join": "Tournament join",
409
+ "tournament_lb": "Tournament lb",
410
+ "tournament_panel": "Tournament panel",
411
+ "tournament_start": "Tournament start",
412
+ "transfer": "Transfer",
413
+ "translate": "Translate",
414
+ "translate_voice": "Translate voice",
415
+ "trivia": "Trivia",
416
+ "tts": "Tts",
417
+ "unban": "Unban",
418
+ "unload": "Unload",
419
+ "unlock": "Unlock",
420
+ "unmute": "Unmute",
421
+ "upscale": "Upscale",
422
+ "userinfo": "Userinfo",
423
+ "verify": "Verify",
424
+ "verifysetup": "Verifysetup",
425
+ "warn": "Warn",
426
+ "warnings": "Warnings",
427
+ "wisdom_today": "Wisdom today",
428
+ "work": "Work",
429
+ "xo": "Xo",
430
+ "xp": "Xp"
431
+ },
432
+ "quick_ai": "??? ????? ??????? ?????????"
433
  },
434
  "gambling": {
435
  "blackjack": {
 
557
  "trivia_title": "سؤال مسابقة",
558
  "trivia_correct": "إجابة صحيحة!",
559
  "trivia_wrong": "إجابة خاطئة!"
560
+ },
561
+ "config": {
562
+ "visuals": {
563
+ "divider": "<:editprofilewhite:1476278814894981200> ??? ???????? ????????? ??? <:editprofilewhite:1476278814894981200>"
564
+ }
565
+ },
566
+ "ping": {
567
+ "title": "?? ????",
568
+ "desc": "??? ?????????: **{latency}ms**"
569
+ },
570
+ "roll": "{user} ??? ????? **{value}** (1-{limit}).",
571
+ "lang": {
572
+ "current": "Current server language: **English**.",
573
+ "updated": "Bot language changed successfully."
574
  }
575
+ }