Upload 93 files
Browse files- bot/__pycache__/database.cpython-311.pyc +0 -0
- bot/__pycache__/emojis.cpython-311.pyc +0 -0
- bot/__pycache__/i18n.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/admin.cpython-311.pyc +2 -2
- bot/cogs/__pycache__/ai_admin.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/ai_suite.cpython-311.pyc +2 -2
- bot/cogs/__pycache__/developer.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/events.cpython-311.pyc +2 -2
- bot/cogs/__pycache__/media.cpython-311.pyc +2 -2
- bot/cogs/__pycache__/media_helpers.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/menu.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/utility.cpython-311.pyc +0 -0
- bot/cogs/__pycache__/verification.cpython-311.pyc +0 -0
- bot/cogs/admin.py +41 -1
- bot/cogs/ai_admin.py +75 -13
- bot/cogs/ai_suite.py +118 -6
- bot/cogs/developer.py +14 -7
- bot/cogs/events.py +79 -49
- bot/cogs/media.py +32 -4
- bot/cogs/media_helpers.py +10 -7
- bot/cogs/menu.py +6 -0
- bot/cogs/utility.py +7 -6
- bot/cogs/verification.py +11 -1
- bot/emojis.py +7 -12
- bot_test.log +39 -0
- database.db-shm +0 -0
- database.db-wal +2 -2
- requirements.txt +1 -0
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:
|
| 3 |
-
size
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
|
|
|
| 1133 |
else:
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1137 |
try:
|
|
|
|
|
|
|
| 1138 |
if ctx.channel:
|
| 1139 |
-
await ctx.channel.send(
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1841 |
try:
|
| 1842 |
async with message.channel.typing():
|
| 1843 |
text, model_used = await self._generate_with_failover(message.guild, payload)
|
| 1844 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 20 |
-
|
| 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.
|
| 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 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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.
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 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("
|
|
|
|
|
|
|
|
|
|
| 565 |
|
| 566 |
-
|
|
|
|
|
|
|
|
|
|
| 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"
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
"""
|
| 36 |
value = _emoji(key, default)
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
| 39 |
return discord.PartialEmoji.from_str(value)
|
| 40 |
-
|
| 41 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
|
|
|
| 148 |
"""
|
| 149 |
parsed = _parse_custom_emoji_config(value)
|
| 150 |
if parsed is not None:
|
| 151 |
config_name, emoji_id, animated = parsed
|
| 152 |
-
|
| 153 |
-
|
| 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 |
-
|
| 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 [A[A
|
|
|
|
|
|
|
| 1 |
./database.db : 100%|██████████| 184kB / 184kB [A[A
|
|
|
|
|
|
|
| 2 |
./database.db : 100%|██████████| 184kB / 184kB [A[A
|
|
|
|
|
|
|
| 3 |
./database.db : 100%|██████████| 184kB / 184kB [A[A
|
|
|
|
|
|
|
| 4 |
./database.db : 100%|██████████| 184kB / 184kB [A[A
|
|
|
|
|
|
|
| 5 |
./database.db : 100%|██████████| 184kB / 184kB [A[A
|
|
|
|
|
|
|
| 6 |
./database.db : 100%|██████████| 184kB / 184kB [A[A
|
|
|
|
|
|
|
| 7 |
./database.db : 100%|██████████| 184kB / 184kB [A[A
|
|
|
|
|
|
|
| 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 [A[A
|
| 16 |
+
|
| 17 |
+
|
| 18 |
./database.db : 100%|██████████| 184kB / 184kB [A[A
|
| 19 |
+
|
| 20 |
+
|
| 21 |
./database.db : 100%|██████████| 184kB / 184kB [A[A
|
| 22 |
+
|
| 23 |
+
|
| 24 |
./database.db : 100%|██████████| 184kB / 184kB [A[A
|
| 25 |
+
|
| 26 |
+
|
| 27 |
./database.db : 100%|██████████| 184kB / 184kB [A[A
|
| 28 |
+
|
| 29 |
+
|
| 30 |
./database.db : 100%|██████████| 184kB / 184kB [A[A
|
| 31 |
+
|
| 32 |
+
|
| 33 |
./database.db : 100%|██████████| 184kB / 184kB [A[A
|
| 34 |
+
|
| 35 |
+
|
| 36 |
./database.db : 100%|██████████| 184kB / 184kB [A[A
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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
|