mtaaz commited on
Commit
e699b46
·
verified ·
1 Parent(s): 91c7f83

Upload 93 files

Browse files
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/cogs/__pycache__/admin.cpython-311.pyc CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:4e65407e3a0ee2ebbaee76d78967fa971bfa4460c8922ad8f4b67abac8670f4e
3
- size 125088
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e270a16b470f278e4e28ec544b4f0d41f7cbd9b779e86c5895e69332c12a2c40
3
+ size 128139
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:a59d6c99f1b5da223c1cd0cc22d7e3d6200ea5f6c00df433181311389f2b584c
3
- size 142874
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:87f01bf9a440357d17580e66c2efa381504f8bf1e56d21d7441c36247ea810cb
3
+ size 147711
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__/events.cpython-311.pyc CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:58e4643fd664dcdb77bde357abcf03ef9507fb718cb6a8aa0a575b041c7a7102
3
- size 141527
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f44117d125c62595e8d6e4275e863dff84411cb9d94d3c2771ff7d0f03d10ecf
3
+ size 141960
bot/cogs/__pycache__/media.cpython-311.pyc CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:4a4041ede6a563d0c1605ccde48c9572c66d76bbbaa74f4a7e64abc807651d3a
3
- size 223657
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e2754334025d4695b6eb3daaf2e259b9733692c25e981f290c2434c627816f1d
3
+ size 224730
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__/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
@@ -904,10 +904,50 @@ class Admin(commands.Cog):
904
  "🛡️ Shield State",
905
  f"Current level: **`{level}`**\n"
906
  f"Profile: {profile}\n\n"
907
- "Use `/shield_level low|medium|high` to change it.",
 
 
908
  )
909
  await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction))
910
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
911
  @commands.hybrid_command(name="econ_admin", description=get_cmd_desc("commands.admin.econ_admin_desc"))
912
  @commands.has_permissions(administrator=True)
913
  async def econ_admin(self, ctx: commands.Context, member: discord.Member, action: str, amount: int) -> None:
 
904
  "🛡️ Shield State",
905
  f"Current level: **`{level}`**\n"
906
  f"Profile: {profile}\n\n"
907
+ "Use `/shield_level low|medium|high` to change it.\n"
908
+ "Use `/shield_restrict <text>` to add custom restrictions.\n"
909
+ "Use `/shield_keywords <words>` to add strict trigger keywords."
910
  )
911
  await self._safe_ctx_send(ctx, embed=embed, ephemeral=bool(ctx.interaction))
912
 
913
+ @commands.hybrid_command(name="shield_restrict", description="Set custom AI shield restrictions")
914
+ @commands.has_permissions(manage_guild=True)
915
+ async def shield_restrict(self, ctx: commands.Context, *, restrictions: str) -> None:
916
+ """Tell the AI shield what to be strict on. Example: 'no crypto links, no dm requests'."""
917
+ if not ctx.guild:
918
+ await ctx.send("Server only.", ephemeral=True)
919
+ return
920
+ await self.bot.db.execute(
921
+ "INSERT INTO shield_settings(guild_id, level, custom_restrictions) VALUES (?, ?, ?) "
922
+ "ON CONFLICT(guild_id) DO UPDATE SET custom_restrictions = excluded.custom_restrictions",
923
+ ctx.guild.id, "medium", restrictions[:500],
924
+ )
925
+ embed = discord.Embed(
926
+ title="🛡️ Shield Restrictions Updated",
927
+ description=f"Custom restrictions set to:\n`{restrictions[:200]}`",
928
+ color=discord.Color.green(),
929
+ )
930
+ await ctx.send(embed=embed, ephemeral=True)
931
+
932
+ @commands.hybrid_command(name="shield_keywords", description="Add keywords that trigger instant shield action")
933
+ @commands.has_permissions(manage_guild=True)
934
+ async def shield_keywords(self, ctx: commands.Context, *, keywords: str) -> None:
935
+ """Add custom keywords for the shield. Example: 'crypto, investment, dm me, wallet, airdrop'."""
936
+ if not ctx.guild:
937
+ await ctx.send("Server only.", ephemeral=True)
938
+ return
939
+ await self.bot.db.execute(
940
+ "INSERT INTO shield_settings(guild_id, level, strict_keywords) VALUES (?, ?, ?) "
941
+ "ON CONFLICT(guild_id) DO UPDATE SET strict_keywords = excluded.strict_keywords",
942
+ ctx.guild.id, "medium", keywords[:500],
943
+ )
944
+ embed = discord.Embed(
945
+ title="🔑 Shield Keywords Updated",
946
+ description=f"Strict keywords set to:\n`{keywords[:200]}`",
947
+ color=discord.Color.green(),
948
+ )
949
+ await ctx.send(embed=embed, ephemeral=True)
950
+
951
  @commands.hybrid_command(name="econ_admin", description=get_cmd_desc("commands.admin.econ_admin_desc"))
952
  @commands.has_permissions(administrator=True)
953
  async def econ_admin(self, ctx: commands.Context, member: discord.Member, action: str, amount: int) -> None:
bot/cogs/ai_admin.py CHANGED
@@ -67,6 +67,11 @@ class IntelligenceLayer:
67
 
68
  You receive natural language requests (Arabic, English, or mixed) and output ONLY a valid JSON array of actions. Do NOT include any other text.
69
 
 
 
 
 
 
70
  ═══════════════════════════════════════════════════════
71
  YOUR CAPABILITIES
72
  ═══════════════════════════════════════════════════════
@@ -880,6 +885,38 @@ class ExecutionEngine:
880
  f"**Top 5:**\n" + "\n".join(lines)
881
  )
882
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
883
  class AIAdmin(commands.Cog):
884
  """Autonomous AI Administrator Cog."""
885
 
@@ -891,7 +928,7 @@ class AIAdmin(commands.Cog):
891
 
892
  @staticmethod
893
  def _parse_duration_minutes(text: str) -> int | None:
894
- match = re.search(r"(\d+)\s*(m|min|mins|minute|minutes|h|hr|hour|hours|d|day|days)?", text, re.IGNORECASE)
895
  if not match:
896
  return None
897
  value = int(match.group(1))
@@ -900,6 +937,8 @@ class AIAdmin(commands.Cog):
900
  value *= 60
901
  elif unit.startswith("d"):
902
  value *= 60 * 24
 
 
903
  return max(1, min(value, 40320))
904
 
905
  async def _try_direct_moderation(self, ctx: commands.Context, request: str) -> str | None:
@@ -1124,21 +1163,44 @@ class AIAdmin(commands.Cog):
1124
  if "response_to_user" in action:
1125
  response_text = action.pop("response_to_user")
1126
  break
1127
-
1128
- results = await self.execution.execute(actions, ctx)
1129
-
1130
- try:
1131
- if response_text:
1132
- await ctx.send(response_text)
 
1133
  else:
1134
- await ctx.send("\n".join(results))
1135
- except discord.NotFound:
1136
- # Interaction message may have been deleted
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1137
  try:
 
 
1138
  if ctx.channel:
1139
- await ctx.channel.send(response_text or "\n".join(results))
1140
- except Exception:
1141
- pass
1142
 
1143
  @commands.hybrid_command(name="ai_help", description=get_cmd_desc("commands.ai.ai_help_desc"))
1144
  async def ai_help(self, ctx: commands.Context) -> None:
 
67
 
68
  You receive natural language requests (Arabic, English, or mixed) and output ONLY a valid JSON array of actions. Do NOT include any other text.
69
 
70
+ SAFETY & CONFIRMATION RULES:
71
+ If the user requests a DESTRUCTIVE action (e.g., "delete messages"/"حذف الرسائل", "purge"/"مسح", "ban"/"حظر", "kick"/"طرد", "delete channel"), you MUST:
72
+ 1. Add `"requires_confirmation": true` to the action JSON object.
73
+ 2. In `"response_to_user"`, ask the user for confirmation (e.g., "⚠️ This will delete 50 messages in #general. Click Confirm to proceed." or "⚠️ سيتم حذف 50 رسالة. اضغط 'تأكيد' للمتابعة.").
74
+
75
  ═══════════════════════════════════════════════════════
76
  YOUR CAPABILITIES
77
  ═══════════════════════════════════════════════════════
 
885
  f"**Top 5:**\n" + "\n".join(lines)
886
  )
887
 
888
+ class AIAdminConfirmationView(discord.ui.View):
889
+ """View for confirming destructive AI Admin actions."""
890
+ def __init__(self, cog: "AIAdmin", ctx: commands.Context, action: dict, response_text: str) -> None:
891
+ super().__init__(timeout=60.0)
892
+ self.cog = cog
893
+ self.ctx = ctx
894
+ self.action = action
895
+ self.response_text = response_text
896
+
897
+ @discord.ui.button(label="Confirm", emoji="✅", style=discord.ButtonStyle.success)
898
+ async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
899
+ if interaction.user.id != self.ctx.author.id:
900
+ await interaction.response.send_message("❌ Only the command author can confirm.", ephemeral=True)
901
+ return
902
+
903
+ await interaction.response.defer(thinking=True)
904
+ try:
905
+ # Remove the confirmation flag so it executes normally
906
+ self.action.pop("requires_confirmation", None)
907
+ results = await self.cog.execution.execute([self.action], self.ctx)
908
+ result_text = "\n".join(results)
909
+ await interaction.followup.send(f"✅ **Action Executed:**\n{result_text[:1800]}")
910
+ except Exception as e:
911
+ await interaction.followup.send(f"❌ **Execution Error:** {str(e)[:1000]}")
912
+
913
+ @discord.ui.button(label="Cancel", emoji="❌", style=discord.ButtonStyle.danger)
914
+ async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
915
+ if interaction.user.id != self.ctx.author.id:
916
+ await interaction.response.send_message("❌ Only the command author can cancel.", ephemeral=True)
917
+ return
918
+ await interaction.response.edit_message(content="❌ Action cancelled by user.", view=None)
919
+
920
  class AIAdmin(commands.Cog):
921
  """Autonomous AI Administrator Cog."""
922
 
 
928
 
929
  @staticmethod
930
  def _parse_duration_minutes(text: str) -> int | None:
931
+ match = re.search(r"(\d+)\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hour|hours|d|day|days)?", text, re.IGNORECASE)
932
  if not match:
933
  return None
934
  value = int(match.group(1))
 
937
  value *= 60
938
  elif unit.startswith("d"):
939
  value *= 60 * 24
940
+ elif unit.startswith("s"):
941
+ value = max(1, value // 60) # Convert seconds to minutes (min 1 min)
942
  return max(1, min(value, 40320))
943
 
944
  async def _try_direct_moderation(self, ctx: commands.Context, request: str) -> str | None:
 
1163
  if "response_to_user" in action:
1164
  response_text = action.pop("response_to_user")
1165
  break
1166
+
1167
+ # Split actions into safe (execute now) and unsafe (require confirmation)
1168
+ safe_actions = []
1169
+ unsafe_actions = []
1170
+ for action in actions:
1171
+ if action.get("requires_confirmation"):
1172
+ unsafe_actions.append(action)
1173
  else:
1174
+ safe_actions.append(action)
1175
+
1176
+ # Execute safe actions immediately
1177
+ safe_results = []
1178
+ if safe_actions:
1179
+ safe_results = await self.execution.execute(safe_actions, ctx)
1180
+
1181
+ # Handle unsafe actions (Confirmation Flow)
1182
+ if unsafe_actions:
1183
+ # Take the first unsafe action to confirm
1184
+ action_to_confirm = unsafe_actions[0]
1185
+ # Remove the flag so it executes when confirmed
1186
+ action_to_confirm.pop("requires_confirmation", None)
1187
+
1188
+ # Construct a message if the AI didn't provide a specific one
1189
+ confirm_msg = response_text if response_text else f"⚠️ **Action Requires Confirmation:**\n`{action_to_confirm.get('action')}`"
1190
+
1191
+ view = AIAdminConfirmationView(self, ctx, action_to_confirm, confirm_msg)
1192
+ await ctx.send(confirm_msg, view=view)
1193
+ else:
1194
+ # All actions were safe, send normal response
1195
+ final_response = response_text
1196
+ if safe_results:
1197
+ final_response = f"{response_text or ''}\n\n{' '.join(safe_results)}".strip()
1198
+
1199
  try:
1200
+ await ctx.send(final_response)
1201
+ except discord.NotFound:
1202
  if ctx.channel:
1203
+ await ctx.channel.send(final_response)
 
 
1204
 
1205
  @commands.hybrid_command(name="ai_help", description=get_cmd_desc("commands.ai.ai_help_desc"))
1206
  async def ai_help(self, ctx: commands.Context) -> None:
bot/cogs/ai_suite.py CHANGED
@@ -36,10 +36,30 @@ try:
36
  except Exception: # pragma: no cover
37
  edge_tts = None
38
 
 
 
 
 
 
 
 
39
  from bot.i18n import get_cmd_desc
40
  from bot.emojis import resolve_emoji_value
41
 
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  PERSONALITY_INSTRUCTIONS = {
44
  "wise": "Respond as a wise mentor: calm, clear, and practical.",
45
  "sarcastic": "Respond with light sarcasm only, never insulting or abusive.",
@@ -55,6 +75,7 @@ class PromptPayload:
55
  image_bytes: bytes | None = None
56
  is_code_result: bool = False
57
  memory_context: str = ""
 
58
 
59
 
60
  class ImperialMotaz:
@@ -448,6 +469,43 @@ class AISuite(commands.Cog):
448
  self.memory_icon = resolve_emoji_value("🧠", fallback="🧠", bot=bot)
449
  self.spark_icon = resolve_emoji_value("⚡", fallback="⚡", bot=bot)
450
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  async def cog_load(self) -> None:
452
  # Persistent registration for AI settings panel buttons/select.
453
  self.bot.add_view(AISettingsView(self))
@@ -871,6 +929,7 @@ class AISuite(commands.Cog):
871
  image_bytes: bytes | None = None,
872
  model: str | None = None,
873
  memory_context: str = "",
 
874
  ) -> dict:
875
  # Kept method name for minimal code changes; implementation now uses OpenRouter.
876
  api_key = self.bot.settings.openrouter_api_key
@@ -884,12 +943,27 @@ class AISuite(commands.Cog):
884
  + self._personality(guild)
885
  )
886
 
 
 
 
 
 
 
 
 
 
 
 
887
  if memory_context.strip():
888
  prompt = (
889
  f"Channel short-term memory (last 10 messages):\n{memory_context[:5000]}\n\n"
890
  f"Current user request:\n{prompt}"
891
  )
892
 
 
 
 
 
893
  user_content: list[dict] = [{"type": "text", "text": prompt}]
894
  if image_bytes:
895
  data_url = "data:image/png;base64," + base64.b64encode(image_bytes).decode("utf-8")
@@ -958,6 +1032,7 @@ class AISuite(commands.Cog):
958
  payload.image_bytes,
959
  model=model,
960
  memory_context=payload.memory_context,
 
961
  )
962
  return self._extract_text(data), model
963
  except Exception as e:
@@ -1410,24 +1485,47 @@ class AISuite(commands.Cog):
1410
  await self._send_error(ctx, err)
1411
  return
1412
  await self._safe_defer(ctx)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1413
  progress_msg = await self._send_progressive_embed(
1414
  ctx,
1415
  title=f"{self.ai_icon} Thinking...",
1416
- description="Analyzing your request.",
1417
  )
1418
  try:
1419
  memory = await self._channel_memory_text(ctx.channel, limit=self.MEMORY_WINDOW_SIZE)
1420
  await self._update_progressive_embed(
1421
  progress_msg,
1422
  title=f"{self.memory_icon} Writing...",
1423
- description="Using short-term channel memory for better context.",
 
 
 
 
 
 
1424
  )
1425
- payload = PromptPayload(command_name="chat", prompt=prompt, memory_context=memory)
1426
  await self.run_payload(ctx, payload)
1427
  await self._update_progressive_embed(
1428
  progress_msg,
1429
  title=f"{self.spark_icon} Finalizing...",
1430
- description="Response delivered successfully.",
1431
  )
1432
  finally:
1433
  if progress_msg:
@@ -1837,11 +1935,25 @@ class AISuite(commands.Cog):
1837
  )
1838
 
1839
  memory = await self._channel_memory_text(message.channel, limit=10)
1840
- payload = PromptPayload(command_name="auto_chat", prompt=prompt, memory_context=memory)
 
 
 
 
 
 
 
 
 
 
 
1841
  try:
1842
  async with message.channel.typing():
1843
  text, model_used = await self._generate_with_failover(message.guild, payload)
1844
- await message.reply(f"{text[:1800]}\n\n{self.memory_icon} Model: `{model_used}`", mention_author=False)
 
 
 
1845
  except Exception:
1846
  await message.reply(
1847
  "❌ I couldn't process the AI request right now. Please try again in a moment.",
 
36
  except Exception: # pragma: no cover
37
  edge_tts = None
38
 
39
+ try:
40
+ from duckduckgo_search import DDGS
41
+ HAS_DDG = True
42
+ except Exception: # pragma: no cover
43
+ DDGS = None
44
+ HAS_DDG = False
45
+
46
  from bot.i18n import get_cmd_desc
47
  from bot.emojis import resolve_emoji_value
48
 
49
 
50
+ # ═══════════════════════════════════════════════════════════════════════════════
51
+ # WEB SEARCH — RAG-based live context injection
52
+ # ═══════════════════════════════════════════════════════════════════════════════
53
+
54
+ # Keywords that trigger automatic web search
55
+ _WEB_SEARCH_TRIGGERS = re.compile(
56
+ r"\b(today|now|current|latest|recent|news|live|right now|this (week|month|year)|"
57
+ r"2025|2026|price|rate|stock|score|result|winner|election|update|breaking|event|"
58
+ r"اليوم|الآن|الحالي|أحدث|أخبار|مباشر|سعر|معدل|نتيجة|فائز|حدث)"
59
+ r"\b",
60
+ re.IGNORECASE,
61
+ )
62
+
63
  PERSONALITY_INSTRUCTIONS = {
64
  "wise": "Respond as a wise mentor: calm, clear, and practical.",
65
  "sarcastic": "Respond with light sarcasm only, never insulting or abusive.",
 
75
  image_bytes: bytes | None = None
76
  is_code_result: bool = False
77
  memory_context: str = ""
78
+ web_context: str = "" # Injected web search results for RAG
79
 
80
 
81
  class ImperialMotaz:
 
469
  self.memory_icon = resolve_emoji_value("🧠", fallback="🧠", bot=bot)
470
  self.spark_icon = resolve_emoji_value("⚡", fallback="⚡", bot=bot)
471
 
472
+ # ═══════════════════════════════════════════════════════════════════════════════
473
+ # WEB SEARCH — RAG-based live context injection via DuckDuckGo
474
+ # ═══════════════════════════════════════════════════════════════════════════════
475
+
476
+ @staticmethod
477
+ def _needs_web_search(text: str) -> bool:
478
+ """Check if the prompt contains keywords that suggest live info is needed."""
479
+ return bool(_WEB_SEARCH_TRIGGERS.search(text))
480
+
481
+ async def _web_search_context(self, query: str, max_results: int = 5) -> str:
482
+ """Search the web and return top results as context for the AI.
483
+
484
+ Returns a formatted string with title, snippet, and URL for each result.
485
+ Runs in a thread pool to avoid blocking the event loop.
486
+ """
487
+ if not HAS_DDG or DDGS is None:
488
+ return ""
489
+
490
+ def _do_search() -> str:
491
+ try:
492
+ with DDGS() as ddgs:
493
+ results = list(ddgs.text(query, max_results=max_results))
494
+ if not results:
495
+ return ""
496
+ parts = []
497
+ for i, r in enumerate(results[:max_results], 1):
498
+ title = r.get("title", "")
499
+ body = r.get("body", r.get("snippet", ""))
500
+ url = r.get("href", r.get("url", ""))
501
+ parts.append(f"[{i}] {title}\n {body}\n Source: {url}")
502
+ return "\n\n".join(parts)
503
+ except Exception:
504
+ return ""
505
+
506
+ # Run in thread pool to avoid blocking the event loop
507
+ return await asyncio.get_event_loop().run_in_executor(None, _do_search)
508
+
509
  async def cog_load(self) -> None:
510
  # Persistent registration for AI settings panel buttons/select.
511
  self.bot.add_view(AISettingsView(self))
 
929
  image_bytes: bytes | None = None,
930
  model: str | None = None,
931
  memory_context: str = "",
932
+ web_context: str = "",
933
  ) -> dict:
934
  # Kept method name for minimal code changes; implementation now uses OpenRouter.
935
  api_key = self.bot.settings.openrouter_api_key
 
943
  + self._personality(guild)
944
  )
945
 
946
+ # Inject web search results as context (RAG)
947
+ web_prefix = ""
948
+ if web_context.strip():
949
+ web_prefix = (
950
+ "You have access to LIVE WEB SEARCH results below. "
951
+ "Use these results to provide accurate, up-to-date answers. "
952
+ "If the search results contain the answer, prioritize them over your training data. "
953
+ "Mention that your info is from a live search.\n\n"
954
+ f"═══ LIVE WEB SEARCH RESULTS ═══\n{web_context[:6000]}\n═══ END SEARCH RESULTS ═══\n\n"
955
+ )
956
+
957
  if memory_context.strip():
958
  prompt = (
959
  f"Channel short-term memory (last 10 messages):\n{memory_context[:5000]}\n\n"
960
  f"Current user request:\n{prompt}"
961
  )
962
 
963
+ # Prepend web context to the prompt
964
+ if web_prefix:
965
+ prompt = web_prefix + prompt
966
+
967
  user_content: list[dict] = [{"type": "text", "text": prompt}]
968
  if image_bytes:
969
  data_url = "data:image/png;base64," + base64.b64encode(image_bytes).decode("utf-8")
 
1032
  payload.image_bytes,
1033
  model=model,
1034
  memory_context=payload.memory_context,
1035
+ web_context=payload.web_context,
1036
  )
1037
  return self._extract_text(data), model
1038
  except Exception as e:
 
1485
  await self._send_error(ctx, err)
1486
  return
1487
  await self._safe_defer(ctx)
1488
+
1489
+ # ─── Web Search Detection & Execution ───
1490
+ web_context = ""
1491
+ progress_msg = None
1492
+ if self._needs_web_search(prompt):
1493
+ progress_msg = await self._send_progressive_embed(
1494
+ ctx,
1495
+ title="🔍 Searching the web...",
1496
+ description="Looking for the latest information online.",
1497
+ )
1498
+ web_context = await self._web_search_context(prompt)
1499
+ if progress_msg:
1500
+ try:
1501
+ await progress_msg.delete()
1502
+ except Exception:
1503
+ pass
1504
+ progress_msg = None
1505
+
1506
  progress_msg = await self._send_progressive_embed(
1507
  ctx,
1508
  title=f"{self.ai_icon} Thinking...",
1509
+ description="Analyzing your request." + (" (with web results)" if web_context else ""),
1510
  )
1511
  try:
1512
  memory = await self._channel_memory_text(ctx.channel, limit=self.MEMORY_WINDOW_SIZE)
1513
  await self._update_progressive_embed(
1514
  progress_msg,
1515
  title=f"{self.memory_icon} Writing...",
1516
+ description="Using short-term channel memory for better context." + (" + live web search" if web_context else ""),
1517
+ )
1518
+ payload = PromptPayload(
1519
+ command_name="chat",
1520
+ prompt=prompt,
1521
+ memory_context=memory,
1522
+ web_context=web_context,
1523
  )
 
1524
  await self.run_payload(ctx, payload)
1525
  await self._update_progressive_embed(
1526
  progress_msg,
1527
  title=f"{self.spark_icon} Finalizing...",
1528
+ description="Response delivered successfully." + (" 🔍 Source: Live Web Search" if web_context else ""),
1529
  )
1530
  finally:
1531
  if progress_msg:
 
1935
  )
1936
 
1937
  memory = await self._channel_memory_text(message.channel, limit=10)
1938
+
1939
+ # Web search for auto chat if keywords detected
1940
+ web_context = ""
1941
+ if self._needs_web_search(content):
1942
+ web_context = await self._web_search_context(content, max_results=3)
1943
+
1944
+ payload = PromptPayload(
1945
+ command_name="auto_chat",
1946
+ prompt=prompt,
1947
+ memory_context=memory,
1948
+ web_context=web_context,
1949
+ )
1950
  try:
1951
  async with message.channel.typing():
1952
  text, model_used = await self._generate_with_failover(message.guild, payload)
1953
+ footer = f"\n\n{self.memory_icon} Model: `{model_used}`"
1954
+ if web_context:
1955
+ footer += " 🔍 Source: Live Web Search"
1956
+ await message.reply(f"{text[:1800]}{footer}", mention_author=False)
1957
  except Exception:
1958
  await message.reply(
1959
  "❌ I couldn't process the AI request right now. Please try again in a moment.",
bot/cogs/developer.py CHANGED
@@ -16,8 +16,8 @@ class Developer(commands.Cog):
16
  def __init__(self, bot: commands.Bot) -> None:
17
  self.bot = bot
18
 
19
- async def cog_check(self, ctx: commands.Context) -> bool:
20
- return await self.bot.is_owner(ctx.author)
21
 
22
  @commands.command(name="load", help="Load a cog extension by dotted path.")
23
  async def load(self, ctx: commands.Context, extension: str) -> None:
@@ -48,7 +48,7 @@ class Developer(commands.Cog):
48
  async def emoji_scan(self, ctx: commands.Context) -> None:
49
  """Scan all configured custom emojis and show which ones are broken."""
50
  if not self.bot.is_ready() or not self.bot.user:
51
- await ctx.reply("⏳ Bot is not ready yet.", ephemeral=True)
52
  return
53
 
54
  bot_emojis = {e.id: e for e in self.bot.emojis}
@@ -125,10 +125,17 @@ class Developer(commands.Cog):
125
 
126
  embed.set_footer(text="Run this after the bot joins new servers to refresh emoji cache")
127
 
128
- if ctx.interaction:
129
- await ctx.interaction.response.send_message(embed=embed, ephemeral=True)
130
- else:
131
- await ctx.reply(embed=embed, ephemeral=True)
 
 
 
 
 
 
 
132
 
133
  @staticmethod
134
  def _extract_id(value: str) -> int | None:
 
16
  def __init__(self, bot: commands.Bot) -> None:
17
  self.bot = bot
18
 
19
+ # Removed global cog_check to allow more granular permissions
20
+ # Individual commands can have their own checks if needed
21
 
22
  @commands.command(name="load", help="Load a cog extension by dotted path.")
23
  async def load(self, ctx: commands.Context, extension: str) -> None:
 
48
  async def emoji_scan(self, ctx: commands.Context) -> None:
49
  """Scan all configured custom emojis and show which ones are broken."""
50
  if not self.bot.is_ready() or not self.bot.user:
51
+ await ctx.send("⏳ Bot is not ready yet.", ephemeral=True)
52
  return
53
 
54
  bot_emojis = {e.id: e for e in self.bot.emojis}
 
125
 
126
  embed.set_footer(text="Run this after the bot joins new servers to refresh emoji cache")
127
 
128
+ try:
129
+ if ctx.interaction:
130
+ if ctx.interaction.response.is_done():
131
+ await ctx.interaction.followup.send(embed=embed, ephemeral=True)
132
+ else:
133
+ await ctx.interaction.response.send_message(embed=embed, ephemeral=True)
134
+ else:
135
+ await ctx.send(embed=embed, ephemeral=True)
136
+ except discord.InteractionResponded:
137
+ if ctx.interaction:
138
+ await ctx.interaction.followup.send(embed=embed, ephemeral=True)
139
 
140
  @staticmethod
141
  def _extract_id(value: str) -> int | None:
bot/cogs/events.py CHANGED
@@ -532,76 +532,97 @@ class Events(commands.Cog):
532
  async def _check_multi_vector_scam(self, message: discord.Message) -> tuple[bool, str, int]:
533
  """Multi-Vector Scam Detection.
534
 
535
- Combines suspicion score, link inspection, and contextual chain analysis.
 
 
 
 
 
536
  Returns: (is_scam, reason, total_score)
537
  """
538
  guild_id = message.guild.id if message.guild else 0
539
  user_id = message.author.id
540
  content = message.content or ""
 
541
  has_images = bool(message.attachments)
542
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
  total_score = 0
544
  reasons: list[str] = []
545
 
546
- # --- Stage 1: Check for invite/scam keywords ---
547
- is_stage1, stage1_reasons = self._detect_invite_chain(content)
548
- if is_stage1:
549
- score = await self.suspicion.add_points(guild_id, user_id, 5, "invite_text")
550
  total_score += 5
551
- reasons.extend(stage1_reasons)
552
-
553
- # --- Stage 2: Check for link or image ---
554
- is_stage2, stage2_reasons = self._detect_link_stage(content, has_images)
555
- if is_stage2:
556
- score = await self.suspicion.add_points(guild_id, user_id, 10, "link_or_image")
557
- total_score += 10
558
- reasons.extend(stage2_reasons)
559
-
560
- # Link + invite combo = High-Risk Scam
561
- link_risk, _ = self._classify_link_risk(content)
562
- if SCAM_INVITE_RE.search(content) and link_risk >= 2:
563
  total_score += 5
564
- reasons.append("CRITICAL: invite keywords + suspicious link")
 
 
 
565
 
566
- # --- Check existing suspicion score ---
 
 
 
567
  existing_score = await self.suspicion.get_score(guild_id, user_id)
568
  total_score = max(total_score, existing_score)
569
 
570
- # --- Action: If score > 10 → trigger Warning System ---
571
  if total_score > SuspicionTracker.ACTION_THRESHOLD:
572
  reason_str = " | ".join(reasons[:3])
573
- return True, f"Multi-vector scam (score: {total_score}): {reason_str}", total_score
574
-
575
- # --- Link Inspection: suspicious domain without prior context ---
576
- if SCAM_LINK_RE.search(content) and not self._is_trusted_domain(content):
577
- link_risk, link_reasons = self._classify_link_risk(content)
578
- if link_risk >= 3 and SCAM_INVITE_RE.search(content):
579
- return True, f"Immediate scam: {link_reasons}", total_score + 5
580
 
581
  return False, "", total_score
582
 
583
- def _is_high_risk_scam_text(self, content: str) -> bool:
584
- """Legacy compatibility — now uses multi-vector detection."""
585
- is_scam, _, _ = asyncio.get_event_loop().run_until_complete(
586
- self._check_multi_vector_scam_discord_mock(content)
587
- ) if hasattr(self, '_check_multi_vector_scam_discord_mock') else False
588
- # Fallback to basic heuristic for sync contexts
589
- text = (content or "").strip().lower()
590
- if not text:
591
- return False
592
- if BENIGN_DM_RE.search(text) and not SCAM_LINK_RE.search(text):
593
- return False
594
- has_link = bool(SCAM_LINK_RE.search(text))
595
- has_cta = bool(SCAM_CALL_TO_ACTION_RE.search(text))
596
- has_keyword = bool(SCAM_KEYWORDS_RE.search(text))
597
- has_official = bool(SCAM_OFFICIAL_RE.search(text))
598
- has_urgent = bool(SCAM_URGENT_RE.search(text))
599
- if has_link and (has_cta or has_keyword or has_official or has_urgent):
600
- return True
601
- if has_cta and (has_keyword or has_official) and not BENIGN_DM_RE.search(text):
602
- return True
603
- return False
604
-
605
  def _scam_heuristics(self, content: str, has_images: bool = False) -> tuple[int, list[str]]:
606
  """Legacy compatibility — replaced by multi-vector detection."""
607
  text = (content or "").strip().lower()
@@ -1216,6 +1237,15 @@ class Events(commands.Cog):
1216
  except Exception:
1217
  pass
1218
 
 
 
 
 
 
 
 
 
 
1219
  # Log to shield_logs
1220
  await self.bot.db.execute(
1221
  "INSERT INTO shield_logs(guild_id, user_id, reason, message_content) VALUES (?, ?, ?, ?)",
 
532
  async def _check_multi_vector_scam(self, message: discord.Message) -> tuple[bool, str, int]:
533
  """Multi-Vector Scam Detection.
534
 
535
+ Checks in order:
536
+ 1. Custom admin-set keywords (instant block)
537
+ 2. Any scam keyword + link → instant block
538
+ 3. Pure scam text (high-risk patterns)
539
+ 4. Any link in message (suspicion tracking)
540
+ 5. Suspicion score threshold (repeat offender)
541
  Returns: (is_scam, reason, total_score)
542
  """
543
  guild_id = message.guild.id if message.guild else 0
544
  user_id = message.author.id
545
  content = message.content or ""
546
+ text_lower = content.strip().lower()
547
  has_images = bool(message.attachments)
548
 
549
+ if not text_lower and not has_images:
550
+ return False, "", 0
551
+
552
+ # --- CHECK 1: Custom admin-set strict keywords (instant block) ---
553
+ custom_row = await self.bot.db.fetchone(
554
+ "SELECT custom_restrictions, strict_keywords FROM shield_settings WHERE guild_id = ?",
555
+ guild_id,
556
+ )
557
+ strict_keywords = (custom_row[1] or "").lower() if custom_row else ""
558
+
559
+ if strict_keywords:
560
+ for kw in strict_keywords.split(","):
561
+ kw = kw.strip()
562
+ if kw and kw in text_lower:
563
+ return True, f"Custom keyword: `{kw}`", 20
564
+
565
+ # --- CHECK 2: Any scam keyword + ANY link → instant block ---
566
+ has_any_scam_keyword = bool(
567
+ SCAM_INVITE_RE.search(text_lower) or
568
+ SCAM_CALL_TO_ACTION_RE.search(text_lower) or
569
+ SCAM_KEYWORDS_RE.search(text_lower) or
570
+ SCAM_OFFICIAL_RE.search(text_lower) or
571
+ SCAM_URGENT_RE.search(text_lower)
572
+ )
573
+ has_link = bool(SCAM_LINK_RE.search(content))
574
+
575
+ if has_any_scam_keyword and has_link:
576
+ link_risk, link_reasons = self._classify_link_risk(content)
577
+ return True, f"Scam keywords + link ({link_reasons or 'detected'})", 15
578
+
579
+ # --- CHECK 3: Pure high-risk scam text (no link needed) ---
580
+ has_cta = bool(SCAM_CALL_TO_ACTION_RE.search(text_lower))
581
+ has_keyword = bool(SCAM_KEYWORDS_RE.search(text_lower))
582
+ has_official = bool(SCAM_OFFICIAL_RE.search(text_lower))
583
+ has_urgent = bool(SCAM_URGENT_RE.search(text_lower))
584
+
585
+ # CTA alone is enough if it matches scam patterns
586
+ if has_cta and has_keyword:
587
+ return True, "Scam CTA + keywords", 15
588
+
589
+ # Official impersonation + urgency
590
+ if has_official and (has_urgent or has_keyword):
591
+ return True, "Official impersonation scam", 15
592
+
593
+ # Any single strong scam signal (prevents first-time scammers)
594
+ if has_official and has_link:
595
+ return True, "Official + link", 15
596
+
597
+ # --- CHECK 4: Suspicion tracking for repeat offenders ---
598
  total_score = 0
599
  reasons: list[str] = []
600
 
601
+ if has_any_scam_keyword:
 
 
 
602
  total_score += 5
603
+ reasons.append("scam keywords")
604
+ if has_link:
605
+ link_risk, link_reasons = self._classify_link_risk(content)
606
+ if link_risk >= 2:
 
 
 
 
 
 
 
 
607
  total_score += 5
608
+ reasons.append(f"suspicious link ({link_reasons})")
609
+ elif link_risk >= 1:
610
+ total_score += 3
611
+ reasons.append("link detected")
612
 
613
+ if has_any_scam_keyword or (has_link and link_risk >= 2): # noqa: F821
614
+ await self.suspicion.add_points(guild_id, user_id, total_score, "scam_signal")
615
+
616
+ # --- CHECK 5: Repeat offender threshold ---
617
  existing_score = await self.suspicion.get_score(guild_id, user_id)
618
  total_score = max(total_score, existing_score)
619
 
 
620
  if total_score > SuspicionTracker.ACTION_THRESHOLD:
621
  reason_str = " | ".join(reasons[:3])
622
+ return True, f"Repeat scam (score: {total_score}): {reason_str}", total_score
 
 
 
 
 
 
623
 
624
  return False, "", total_score
625
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
  def _scam_heuristics(self, content: str, has_images: bool = False) -> tuple[int, list[str]]:
627
  """Legacy compatibility — replaced by multi-vector detection."""
628
  text = (content or "").strip().lower()
 
1237
  except Exception:
1238
  pass
1239
 
1240
+ # Log to warns table (connects to admin warning system)
1241
+ await self.bot.db.execute(
1242
+ "INSERT INTO warns(guild_id, user_id, moderator_id, reason) VALUES (?, ?, ?, ?)",
1243
+ guild.id,
1244
+ member.id if member else 0,
1245
+ self.bot.user.id if self.bot.user else 0,
1246
+ f"Multi-Vector Scam (score: {score}): {reason[:180]}",
1247
+ )
1248
+
1249
  # Log to shield_logs
1250
  await self.bot.db.execute(
1251
  "INSERT INTO shield_logs(guild_id, user_id, reason, message_content) VALUES (?, ?, ?, ?)",
bot/cogs/media.py CHANGED
@@ -154,14 +154,22 @@ def _build_emoji_markup(emoji_obj: discord.Emoji) -> str:
154
 
155
 
156
  def _resolve_emoji_value(raw_value: str, default: str) -> str:
157
- """Resolve emoji by ID using bot cache, then fallback to unicode default."""
 
 
 
 
 
 
 
 
 
158
  emoji_id = _extract_emoji_id(raw_value)
159
  if emoji_id is not None and _EMOJI_BOT is not None:
160
  found = _EMOJI_BOT.get_emoji(emoji_id)
161
  if found is not None:
162
  return _build_emoji_markup(found)
163
- # Emoji not available in cache — return default unicode to avoid raw :name: display
164
- return default
165
 
166
 
167
  def _load_emoji_config() -> dict[str, str]:
@@ -391,6 +399,7 @@ class GuildPlaybackState:
391
  volume: int = 80
392
  stay_247: bool = False
393
  filter_preset: str = "none"
 
394
 
395
 
396
  @dataclass
@@ -832,14 +841,33 @@ class Media(commands.Cog):
832
  )
833
 
834
  async def _autoplay_next(self, guild: discord.Guild, player: wavelink.Player) -> None:
835
- """Get autoplay recommendation and play it."""
 
 
 
 
 
 
836
  try:
 
 
 
 
 
 
 
 
 
 
 
 
837
  # Search for similar music
838
  current = self.now_playing.get(guild.id)
839
  seed = current.title if current else "top hits"
840
  results = await wavelink.Playable.search(f"ytsearch:{seed}")
841
  if results:
842
  track = random.choice(results[:5]) # Pick from top 5
 
843
  await player.queue.put_wait(track)
844
  next_track = player.queue.get()
845
  self.now_playing[guild.id] = self._wavelink_to_track(next_track, 0)
 
154
 
155
 
156
  def _resolve_emoji_value(raw_value: str, default: str) -> str:
157
+ """Resolve emoji for media panel display.
158
+
159
+ Returns the full custom emoji tag so Discord can render it.
160
+ Only uses default if the value is empty/invalid.
161
+ """
162
+ if not raw_value:
163
+ return default
164
+ # Check if it's a custom emoji tag
165
+ if raw_value.startswith("<") and raw_value.endswith(">"):
166
+ return raw_value # Return as-is for Discord to render
167
  emoji_id = _extract_emoji_id(raw_value)
168
  if emoji_id is not None and _EMOJI_BOT is not None:
169
  found = _EMOJI_BOT.get_emoji(emoji_id)
170
  if found is not None:
171
  return _build_emoji_markup(found)
172
+ return raw_value if raw_value.strip() else default
 
173
 
174
 
175
  def _load_emoji_config() -> dict[str, str]:
 
399
  volume: int = 80
400
  stay_247: bool = False
401
  filter_preset: str = "none"
402
+ _autoplay_chain: int = 0 # Consecutive autoplay tracks (resets on human interaction)
403
 
404
 
405
  @dataclass
 
841
  )
842
 
843
  async def _autoplay_next(self, guild: discord.Guild, player: wavelink.Player) -> None:
844
+ """Get autoplay recommendation and play it.
845
+
846
+ Safety limits:
847
+ - Won't autoplay if queue already has 10+ tracks
848
+ - Won't autoplay more than 3 consecutive recommended tracks
849
+ - Skips if a human-added track is already playing
850
+ """
851
  try:
852
+ state = self._guild_state(guild.id)
853
+
854
+ # Don't autoplay if queue is already full
855
+ queue_size = player.queue.count if hasattr(player.queue, 'count') else 0
856
+ if queue_size >= 10:
857
+ return
858
+
859
+ # Don't chain more than 3 autoplay tracks in a row
860
+ if state._autoplay_chain >= 3:
861
+ state._autoplay_chain = 0
862
+ return
863
+
864
  # Search for similar music
865
  current = self.now_playing.get(guild.id)
866
  seed = current.title if current else "top hits"
867
  results = await wavelink.Playable.search(f"ytsearch:{seed}")
868
  if results:
869
  track = random.choice(results[:5]) # Pick from top 5
870
+ state._autoplay_chain = getattr(state, '_autoplay_chain', 0) + 1
871
  await player.queue.put_wait(track)
872
  next_track = player.queue.get()
873
  self.now_playing[guild.id] = self._wavelink_to_track(next_track, 0)
bot/cogs/media_helpers.py CHANGED
@@ -31,15 +31,18 @@ def _emoji(key: str, default: str) -> str:
31
  return resolve_emoji(key, default)
32
 
33
 
34
- def _button_emoji(key: str, default: str) -> str | discord.PartialEmoji:
35
- """Build a button-safe emoji (supports raw custom emoji tags)."""
36
  value = _emoji(key, default)
37
- try:
38
- if isinstance(value, str) and value.startswith("<") and value.endswith(">"):
 
 
 
39
  return discord.PartialEmoji.from_str(value)
40
- except Exception:
41
- pass
42
- return value
43
 
44
 
45
  # ═══════════════════════════════════════════════════════════════════════════════
 
31
  return resolve_emoji(key, default)
32
 
33
 
34
+ def _button_emoji(key: str, default: str) -> str | discord.PartialEmoji | None:
35
+ """Return emoji for button labels. Returns PartialEmoji for custom tags."""
36
  value = _emoji(key, default)
37
+ if not value:
38
+ return default
39
+ # Try to parse as custom emoji tag
40
+ if isinstance(value, str) and value.startswith("<") and value.endswith(">"):
41
+ try:
42
  return discord.PartialEmoji.from_str(value)
43
+ except Exception:
44
+ pass
45
+ return value if isinstance(value, str) else default
46
 
47
 
48
  # ═══════════════════════════════════════════════════════════════════════════════
bot/cogs/menu.py CHANGED
@@ -709,6 +709,12 @@ class CommandsMenuView(discord.ui.View):
709
  embed.add_field(name=tips_title, value=tips, inline=True)
710
  embed.add_field(name=updates_title, value=updates, inline=False)
711
 
 
 
 
 
 
 
712
  embed.set_footer(text=footer)
713
  embed.description = f"{embed.description}\n\nPage {self.page + 1}/{total_pages}"
714
  return embed
 
709
  embed.add_field(name=tips_title, value=tips, inline=True)
710
  embed.add_field(name=updates_title, value=updates, inline=False)
711
 
712
+ # ═══ What's New section ═══
713
+ whats_new = await self.bot.get_text(guild_id, "menu.whats_new_heading")
714
+ whats_new_content = await self.bot.get_text(guild_id, "menu.whats_new_content")
715
+ if whats_new and whats_new != "menu.whats_new_heading" and whats_new_content and whats_new_content != "menu.whats_new_content":
716
+ embed.add_field(name=whats_new, value=whats_new_content[:900], inline=False)
717
+
718
  embed.set_footer(text=footer)
719
  embed.description = f"{embed.description}\n\nPage {self.page + 1}/{total_pages}"
720
  return embed
bot/cogs/utility.py CHANGED
@@ -338,6 +338,7 @@ class Utility(commands.Cog):
338
  await channel.send(f"⏰ <@{user_id}> reminder: {msg}")
339
  await self.bot.db.execute("DELETE FROM reminders WHERE id = ?", rid)
340
 
 
341
  @commands.hybrid_command(name="search", description=get_cmd_desc("commands.tools.search_desc"))
342
  @discord.app_commands.describe(query="Search query | بحث")
343
  async def search(self, ctx: commands.Context, query: str) -> None:
@@ -529,11 +530,6 @@ async def _yt_search(query: str, max_results: int = 25) -> list[dict]:
529
  return results
530
 
531
 
532
- class Utility(commands.Cog):
533
- """Utility commands including YouTube search."""
534
-
535
- def __init__(self, bot: commands.Bot) -> None:
536
- self.bot = bot
537
 
538
  @commands.hybrid_command(name="botstats", description=get_cmd_desc("commands.tools.botstats_desc"))
539
  async def botstats(self, ctx: commands.Context) -> None:
@@ -553,4 +549,9 @@ class Utility(commands.Cog):
553
 
554
 
555
  async def setup(bot: commands.Bot) -> None:
556
- await bot.add_cog(Utility(bot))
 
 
 
 
 
 
338
  await channel.send(f"⏰ <@{user_id}> reminder: {msg}")
339
  await self.bot.db.execute("DELETE FROM reminders WHERE id = ?", rid)
340
 
341
+
342
  @commands.hybrid_command(name="search", description=get_cmd_desc("commands.tools.search_desc"))
343
  @discord.app_commands.describe(query="Search query | بحث")
344
  async def search(self, ctx: commands.Context, query: str) -> None:
 
530
  return results
531
 
532
 
 
 
 
 
 
533
 
534
  @commands.hybrid_command(name="botstats", description=get_cmd_desc("commands.tools.botstats_desc"))
535
  async def botstats(self, ctx: commands.Context) -> None:
 
549
 
550
 
551
  async def setup(bot: commands.Bot) -> None:
552
+ cog = Utility(bot)
553
+ # Wire autocomplete after the class is fully defined
554
+ if hasattr(cog, 'search') and cog.search is not None:
555
+ yt_ac = YouTubeAutocomplete()
556
+ cog.search.autocomplete = yt_ac.autocomplete
557
+ await bot.add_cog(cog)
bot/cogs/verification.py CHANGED
@@ -47,7 +47,17 @@ class VerifyView(discord.ui.View):
47
  return
48
 
49
  await member.add_roles(role, reason="Member completed verification")
50
- await interaction.followup.send(await self.bot.get_text(guild_id, "welcome.verify_success"), ephemeral=True)
 
 
 
 
 
 
 
 
 
 
51
 
52
  await self.bot.log_to_guild(
53
  interaction.guild,
 
47
  return
48
 
49
  await member.add_roles(role, reason="Member completed verification")
50
+
51
+ # Send success message with redirect link to welcome channel
52
+ welcome_channel_id = row[1] if row else None
53
+ redirect_text = ""
54
+ if welcome_channel_id:
55
+ welcome_ch = interaction.guild.get_channel(welcome_channel_id)
56
+ if welcome_ch:
57
+ redirect_text = f"\n\n➡️ {await self.bot.get_text(guild_id, 'welcome.redirect_text', channel=welcome_ch.mention)}"
58
+
59
+ success_msg = await self.bot.get_text(guild_id, "welcome.verify_success")
60
+ await interaction.followup.send(f"{success_msg}{redirect_text}", ephemeral=True)
61
 
62
  await self.bot.log_to_guild(
63
  interaction.guild,
bot/emojis.py CHANGED
@@ -141,21 +141,17 @@ def _ensure_unescaped_emoji(value: str) -> str:
141
 
142
 
143
  def resolve_emoji_value(value: str, fallback: str = "✨", *, bot: discord.Client | None = None) -> str:
144
- """Resolve emoji config with a hybrid resolver (cache first, then fallback to unicode).
145
 
146
- If the custom emoji is available in the bot's cache, it returns the full tag.
147
- Otherwise it falls back to the unicode fallback to avoid showing raw :name: text.
 
148
  """
149
  parsed = _parse_custom_emoji_config(value)
150
  if parsed is not None:
151
  config_name, emoji_id, animated = parsed
152
- active_bot = bot or _EMOJI_BOT
153
- if active_bot is not None:
154
- resolved = active_bot.get_emoji(emoji_id)
155
- if resolved is not None:
156
- return _ensure_unescaped_emoji(_format_custom_emoji(resolved))
157
- # Emoji not in cache — return fallback unicode to avoid raw :name: display
158
- return fallback
159
 
160
  emoji_id = _extract_emoji_id(value)
161
  active_bot = bot or _EMOJI_BOT
@@ -163,8 +159,7 @@ def resolve_emoji_value(value: str, fallback: str = "✨", *, bot: discord.Clien
163
  resolved = active_bot.get_emoji(emoji_id)
164
  if resolved is not None:
165
  return _ensure_unescaped_emoji(_format_custom_emoji(resolved))
166
- return fallback
167
- # Value might be a plain unicode emoji or already-resolved string
168
  return _ensure_unescaped_emoji(value or fallback)
169
 
170
 
 
141
 
142
 
143
  def resolve_emoji_value(value: str, fallback: str = "✨", *, bot: discord.Client | None = None) -> str:
144
+ """Resolve emoji config for display in embeds/messages.
145
 
146
+ Returns the full custom emoji tag <:name:id> so Discord can render it.
147
+ Only falls back to unicode if the value is invalid or empty.
148
+ Custom emoji tags work in embed descriptions if the bot has access to the emoji.
149
  """
150
  parsed = _parse_custom_emoji_config(value)
151
  if parsed is not None:
152
  config_name, emoji_id, animated = parsed
153
+ # Return the full custom emoji tag — Discord renders it if valid
154
+ return _ensure_unescaped_emoji(_build_custom_emoji_code(config_name, emoji_id, animated))
 
 
 
 
 
155
 
156
  emoji_id = _extract_emoji_id(value)
157
  active_bot = bot or _EMOJI_BOT
 
159
  resolved = active_bot.get_emoji(emoji_id)
160
  if resolved is not None:
161
  return _ensure_unescaped_emoji(_format_custom_emoji(resolved))
162
+ # Return as-is (might be unicode emoji or already-resolved tag)
 
163
  return _ensure_unescaped_emoji(value or fallback)
164
 
165
 
bot_test.log ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0
  ./database.db : 100%|██████████| 184kB / 184kB 
 
 
1
  ./database.db : 100%|██████████| 184kB / 184kB 
 
 
2
  ./database.db : 100%|██████████| 184kB / 184kB 
 
 
3
  ./database.db : 100%|██████████| 184kB / 184kB 
 
 
4
  ./database.db : 100%|██████████| 184kB / 184kB 
 
 
5
  ./database.db : 100%|██████████| 184kB / 184kB 
 
 
6
  ./database.db : 100%|██████████| 184kB / 184kB 
 
 
7
  ./database.db : 100%|██████████| 184kB / 184kB 
 
 
8
  ./database.db : 100%|██████████| 184kB / 184kB
 
 
 
 
 
 
 
 
 
 
1
+ [OK] Keep-alive HTTP server started on port 10000
2
+ [OK] Flask lifeline started on 0.0.0.0:7860
3
+ 2026-04-09 22:59:01,847 | INFO | mega-bot.config | Loaded 1 owner ID(s).
4
+ * Serving Flask app 'bot-lifeline'
5
+ * Debug mode: off
6
+ 2026-04-09 22:59:01,865 | INFO | discord.client | logging in using static token
7
+ 2026-04-09 22:59:03,974 | INFO | httpx | HTTP Request: HEAD https://huggingface.co/datasets/mtaaz/db/resolve/main/database.db "HTTP/1.1 302 Found"
8
+ 2026-04-09 22:59:04,302 | INFO | httpx | HTTP Request: POST https://huggingface.co/api/datasets/mtaaz/db/preupload/main "HTTP/1.1 200 OK"
9
+ 2026-04-09 22:59:04,514 | INFO | httpx | HTTP Request: POST https://huggingface.co/datasets/mtaaz/db.git/info/lfs/objects/batch "HTTP/1.1 200 OK"
10
+ 2026-04-09 22:59:04,734 | INFO | httpx | HTTP Request: GET https://huggingface.co/api/datasets/mtaaz/db/xet-write-token/main "HTTP/1.1 200 OK"
11
+
12
+
13
+
14
+
15
  ./database.db : 100%|██████████| 184kB / 184kB 
16
+
17
+
18
  ./database.db : 100%|██████████| 184kB / 184kB 
19
+
20
+
21
  ./database.db : 100%|██████████| 184kB / 184kB 
22
+
23
+
24
  ./database.db : 100%|██████████| 184kB / 184kB 
25
+
26
+
27
  ./database.db : 100%|██████████| 184kB / 184kB 
28
+
29
+
30
  ./database.db : 100%|██████████| 184kB / 184kB 
31
+
32
+
33
  ./database.db : 100%|██████████| 184kB / 184kB 
34
+
35
+
36
  ./database.db : 100%|██████████| 184kB / 184kB 
37
+
38
+
39
  ./database.db : 100%|██████████| 184kB / 184kB
40
+ No files have been modified since last commit. Skipping to prevent empty commit.
41
+ 2026-04-09 22:59:05,980 | WARNING | huggingface_hub.hf_api | No files have been modified since last commit. Skipping to prevent empty commit.
42
+ 2026-04-09 22:59:06,197 | INFO | httpx | HTTP Request: GET https://huggingface.co/api/datasets/mtaaz/db/revision/main "HTTP/1.1 200 OK"
43
+ 2026-04-09 22:59:07,050 | WARNING | mega-bot | FFmpeg binary not found in PATH.
44
+ 2026-04-09 22:59:07,115 | WARNING | mega-bot | MainMenuView registration skipped: View is not persistent. Items need to have a custom_id set and View must have no timeout
45
+ 2026-04-09 22:59:07,948 | INFO | mega-bot | Loaded all cogs and synced slash commands
46
+ 2026-04-09 22:59:08,825 | INFO | discord.gateway | Shard ID None has connected to Gateway (Session ID: a9741b4159f3677909c8df43062d7271).
47
+ 2026-04-09 22:59:10,840 | INFO | mega-bot | Logged in as Ultimate bot#9259 (1475803247313682472)
48
+ ^C
database.db-shm CHANGED
Binary files a/database.db-shm and b/database.db-shm differ
 
database.db-wal CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:5e4aebadf9918d28b332f92b113c88b7bc344f2f3a6cc093b22d32b4ee1eac2b
3
- size 111272
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:770eff54c07d26e4599ea95a1c9f833f74a157aad1e19700d8335897e3ea30dd
3
+ size 218392
requirements.txt CHANGED
@@ -12,3 +12,4 @@ Pillow>=10.4.0
12
  gTTS>=2.5.3
13
  edge-tts>=6.1.13
14
  huggingface_hub>=0.25.0
 
 
12
  gTTS>=2.5.3
13
  edge-tts>=6.1.13
14
  huggingface_hub>=0.25.0
15
+ duckduckgo-search>=6.3.0