2026-05-06 17:07:09 +02:00
|
|
|
|
import disnake
|
|
|
|
|
|
from disnake.ext import commands
|
|
|
|
|
|
from disnake.ui import View, Button
|
2026-01-01 18:48:25 +01:00
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
from typing import List, Dict, Optional
|
|
|
|
|
|
|
|
|
|
|
|
class WhitelistMonitor(commands.Cog):
|
|
|
|
|
|
"""Monitors whitelist members for role changes and server leave events."""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, bot):
|
|
|
|
|
|
self.bot = bot
|
|
|
|
|
|
self.whitelist_file = "data/whitelist.json"
|
2026-01-04 00:01:15 +01:00
|
|
|
|
self.persistent_views_added = False
|
2026-01-01 18:48:25 +01:00
|
|
|
|
self.big_permissions = [
|
2026-05-16 20:02:53 +02:00
|
|
|
|
"administrator", "manage_guild", "manage_roles", "manage_channels",
|
|
|
|
|
|
"kick_members", "ban_members", "manage_messages", "moderate_members",
|
|
|
|
|
|
"manage_nicknames", "manage_emojis", "manage_webhooks", "view_audit_log"
|
2026-01-01 18:48:25 +01:00
|
|
|
|
]
|
|
|
|
|
|
|
2026-01-10 15:30:51 +01:00
|
|
|
|
def load_whitelist(self) -> Dict[str, List[str]]:
|
2026-01-01 18:48:25 +01:00
|
|
|
|
"""Load whitelist from JSON file."""
|
|
|
|
|
|
if not os.path.exists(self.whitelist_file):
|
2026-01-10 15:30:51 +01:00
|
|
|
|
return {}
|
2026-01-01 18:48:25 +01:00
|
|
|
|
try:
|
|
|
|
|
|
with open(self.whitelist_file, "r", encoding="utf-8") as f:
|
2026-01-10 15:30:51 +01:00
|
|
|
|
raw_data = json.load(f)
|
|
|
|
|
|
# Convert all user IDs to strings for consistent comparison
|
|
|
|
|
|
result = {}
|
|
|
|
|
|
for guild_id, user_ids in raw_data.items():
|
|
|
|
|
|
result[str(guild_id)] = [str(uid) for uid in user_ids]
|
|
|
|
|
|
return result
|
2026-01-01 18:48:25 +01:00
|
|
|
|
except (json.JSONDecodeError, FileNotFoundError):
|
2026-01-10 15:30:51 +01:00
|
|
|
|
return {}
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
2026-01-10 15:30:51 +01:00
|
|
|
|
def save_whitelist(self, whitelist: Dict[str, List[str]]):
|
2026-01-01 18:48:25 +01:00
|
|
|
|
"""Save whitelist to JSON file."""
|
|
|
|
|
|
with open(self.whitelist_file, "w", encoding="utf-8") as f:
|
|
|
|
|
|
json.dump(whitelist, f, ensure_ascii=False, indent=4)
|
|
|
|
|
|
|
2026-01-10 15:30:51 +01:00
|
|
|
|
def is_whitelisted(self, guild_id: int, user_id: int) -> bool:
|
|
|
|
|
|
"""Check if user is whitelisted for a specific guild."""
|
2026-01-01 18:48:25 +01:00
|
|
|
|
whitelist = self.load_whitelist()
|
2026-01-10 15:30:51 +01:00
|
|
|
|
guild_key = str(guild_id)
|
|
|
|
|
|
user_key = str(user_id)
|
|
|
|
|
|
return guild_key in whitelist and user_key in whitelist[guild_key]
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
2026-01-10 15:30:51 +01:00
|
|
|
|
def remove_from_whitelist(self, guild_id: int, user_id: int) -> bool:
|
|
|
|
|
|
"""Remove user from whitelist for a specific guild."""
|
2026-01-01 18:48:25 +01:00
|
|
|
|
whitelist = self.load_whitelist()
|
2026-01-10 15:30:51 +01:00
|
|
|
|
guild_key = str(guild_id)
|
|
|
|
|
|
user_key = str(user_id)
|
|
|
|
|
|
|
|
|
|
|
|
if guild_key in whitelist and user_key in whitelist[guild_key]:
|
|
|
|
|
|
whitelist[guild_key].remove(user_key)
|
|
|
|
|
|
# Clean up empty guild entries
|
|
|
|
|
|
if not whitelist[guild_key]:
|
|
|
|
|
|
del whitelist[guild_key]
|
2026-01-01 18:48:25 +01:00
|
|
|
|
self.save_whitelist(whitelist)
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
def has_big_permissions(self, role: disnake.Role) -> bool:
|
2026-01-01 18:48:25 +01:00
|
|
|
|
"""Check if role has significant permissions."""
|
2026-05-16 20:02:53 +02:00
|
|
|
|
perms = role.permissions
|
|
|
|
|
|
return any(getattr(perms, p, False) for p in self.big_permissions)
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def send_whitelist_notification(self, member: disnake.Member, removed_roles: List[disnake.Role]):
|
2026-01-01 18:48:25 +01:00
|
|
|
|
"""Send DM notification to user about whitelist member losing roles."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Find guild administrators to notify
|
|
|
|
|
|
admins = [m for m in member.guild.members if m.guild_permissions.administrator and not m.bot]
|
|
|
|
|
|
if not admins:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Create embed
|
2026-05-06 17:07:09 +02:00
|
|
|
|
embed = disnake.Embed(
|
2026-01-01 18:48:25 +01:00
|
|
|
|
title="⚠️ Whitelist Alert",
|
|
|
|
|
|
description=f"Un membre whitelisté a perdu des rôles importants dans **{member.guild.name}**.",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
color=disnake.Color.orange(),
|
2026-01-01 18:48:25 +01:00
|
|
|
|
timestamp=datetime.now()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
embed.add_field(
|
|
|
|
|
|
name="👤 Membre concerné",
|
|
|
|
|
|
value=f"{member.mention} ({member.display_name})",
|
|
|
|
|
|
inline=False
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if removed_roles:
|
|
|
|
|
|
roles_text = "\n".join([f"• {role.name}" for role in removed_roles])
|
|
|
|
|
|
embed.add_field(
|
|
|
|
|
|
name="🗑️ Rôles retirés",
|
|
|
|
|
|
value=roles_text,
|
|
|
|
|
|
inline=False
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
embed.add_field(
|
|
|
|
|
|
name="📋 Statut whitelist",
|
|
|
|
|
|
value="✅ Toujours whitelisté",
|
|
|
|
|
|
inline=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Create view with removal button
|
|
|
|
|
|
view = WhitelistRemovalView(member.id, member.guild.id)
|
|
|
|
|
|
|
|
|
|
|
|
# Send to all admins
|
|
|
|
|
|
for admin in admins:
|
|
|
|
|
|
try:
|
|
|
|
|
|
await admin.send(embed=embed, view=view)
|
2026-05-06 17:07:09 +02:00
|
|
|
|
except disnake.Forbidden:
|
2026-01-01 18:48:25 +01:00
|
|
|
|
continue # User has DMs disabled
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"Error sending whitelist notification: {e}")
|
|
|
|
|
|
|
2026-01-04 00:01:15 +01:00
|
|
|
|
async def cog_load(self):
|
|
|
|
|
|
"""Register persistent views when cog is loaded"""
|
|
|
|
|
|
await self._register_persistent_views()
|
|
|
|
|
|
|
|
|
|
|
|
async def _register_persistent_views(self):
|
|
|
|
|
|
"""Register all persistent views for guilds with whitelist alerts"""
|
|
|
|
|
|
if self.persistent_views_added:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
import json
|
|
|
|
|
|
security_file = "data/security_settings.json"
|
|
|
|
|
|
if os.path.exists(security_file):
|
|
|
|
|
|
with open(security_file, "r", encoding="utf-8") as f:
|
|
|
|
|
|
all_settings = json.load(f)
|
|
|
|
|
|
|
|
|
|
|
|
# For each guild with whitelist monitoring, register a default view
|
|
|
|
|
|
# We'll use a generic view that gets recreated when needed
|
|
|
|
|
|
# The key is that each unique custom_id needs to be registered once
|
|
|
|
|
|
view = WhitelistRemovalView(0, 0) # Placeholder with dummy IDs
|
|
|
|
|
|
self.bot.add_view(view)
|
|
|
|
|
|
print(f"[WhitelistMonitor] Persistent view registered")
|
|
|
|
|
|
|
|
|
|
|
|
self.persistent_views_added = True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[WhitelistMonitor] Error registering views: {e}")
|
|
|
|
|
|
|
2026-01-01 18:48:25 +01:00
|
|
|
|
@commands.Cog.listener()
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def on_member_update(self, before: disnake.Member, after: disnake.Member):
|
2026-01-01 18:48:25 +01:00
|
|
|
|
"""Monitor role changes for whitelist members."""
|
|
|
|
|
|
if before.roles == after.roles:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-01-10 15:30:51 +01:00
|
|
|
|
if not self.is_whitelisted(after.guild.id, after.id):
|
2026-01-01 18:48:25 +01:00
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Find removed roles
|
|
|
|
|
|
removed_roles = [role for role in before.roles if role not in after.roles]
|
|
|
|
|
|
|
|
|
|
|
|
# Check if any removed roles had big permissions
|
|
|
|
|
|
important_removed = [role for role in removed_roles if self.has_big_permissions(role)]
|
|
|
|
|
|
|
|
|
|
|
|
if important_removed:
|
|
|
|
|
|
await self.send_whitelist_notification(after, important_removed)
|
|
|
|
|
|
|
|
|
|
|
|
@commands.Cog.listener()
|
2026-05-06 17:07:09 +02:00
|
|
|
|
async def on_member_remove(self, member: disnake.Member):
|
2026-01-01 18:48:25 +01:00
|
|
|
|
"""Auto-remove from whitelist when member leaves server."""
|
2026-01-10 15:30:51 +01:00
|
|
|
|
if self.is_whitelisted(member.guild.id, member.id):
|
|
|
|
|
|
removed = self.remove_from_whitelist(member.guild.id, member.id)
|
2026-01-01 18:48:25 +01:00
|
|
|
|
if removed:
|
|
|
|
|
|
# Log the removal
|
|
|
|
|
|
try:
|
|
|
|
|
|
log_channel_id = self.get_log_channel(member.guild.id)
|
|
|
|
|
|
if log_channel_id:
|
|
|
|
|
|
log_channel = member.guild.get_channel(log_channel_id)
|
|
|
|
|
|
if log_channel:
|
2026-05-06 17:07:09 +02:00
|
|
|
|
embed = disnake.Embed(
|
2026-01-01 18:48:25 +01:00
|
|
|
|
title="🗑️ Auto-Whitelist Removal",
|
|
|
|
|
|
description=f"{member.mention} a été retiré automatiquement de la whitelist après avoir quitté le serveur.",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
color=disnake.Color.red(),
|
2026-01-01 18:48:25 +01:00
|
|
|
|
timestamp=datetime.now()
|
|
|
|
|
|
)
|
|
|
|
|
|
await log_channel.send(embed=embed)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"Error logging whitelist removal: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
def get_log_channel(self, guild_id: int) -> Optional[int]:
|
|
|
|
|
|
"""Get log channel ID from security settings."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
security_file = "data/security_settings.json"
|
|
|
|
|
|
if os.path.exists(security_file):
|
|
|
|
|
|
with open(security_file, "r") as f:
|
|
|
|
|
|
settings = json.load(f)
|
|
|
|
|
|
return settings.get(str(guild_id), {}).get("log_channel_id")
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
class WhitelistRemovalView(View):
|
|
|
|
|
|
"""View with button to remove user from whitelist."""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, user_id: int, guild_id: int):
|
2026-01-04 00:01:15 +01:00
|
|
|
|
super().__init__(timeout=None) # Persistent view
|
2026-01-01 18:48:25 +01:00
|
|
|
|
self.user_id = user_id
|
|
|
|
|
|
self.guild_id = guild_id
|
|
|
|
|
|
|
2026-05-06 17:07:09 +02:00
|
|
|
|
@disnake.ui.button(
|
2026-01-01 18:48:25 +01:00
|
|
|
|
label="Retirer de la whitelist",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
style=disnake.ButtonStyle.red,
|
2026-01-04 00:01:15 +01:00
|
|
|
|
emoji="🗑️",
|
|
|
|
|
|
custom_id="whitelist_remove_button"
|
2026-01-01 18:48:25 +01:00
|
|
|
|
)
|
|
|
|
|
|
async def remove_whitelist(
|
|
|
|
|
|
self,
|
2026-05-06 17:07:09 +02:00
|
|
|
|
interaction: disnake.Interaction,
|
2026-01-01 18:48:25 +01:00
|
|
|
|
button: Button
|
|
|
|
|
|
):
|
|
|
|
|
|
"""Remove user from whitelist when button is clicked."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Get the cog instance
|
|
|
|
|
|
cog = self.bot.get_cog("WhitelistMonitor")
|
|
|
|
|
|
if not cog:
|
|
|
|
|
|
await interaction.response.send_message(
|
|
|
|
|
|
"❌ Erreur: Système de whitelist non disponible.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Remove from whitelist
|
2026-01-10 15:30:51 +01:00
|
|
|
|
removed = cog.remove_from_whitelist(self.guild_id, self.user_id)
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
|
|
|
|
|
if removed:
|
|
|
|
|
|
# Get user info
|
|
|
|
|
|
try:
|
|
|
|
|
|
user = await self.bot.fetch_user(self.user_id)
|
|
|
|
|
|
user_mention = user.mention
|
|
|
|
|
|
except:
|
|
|
|
|
|
user_mention = f"<@{self.user_id}>"
|
|
|
|
|
|
|
|
|
|
|
|
# Update button
|
|
|
|
|
|
button.disabled = True
|
|
|
|
|
|
button.label = "✅ Retiré"
|
2026-05-06 17:07:09 +02:00
|
|
|
|
button.style = disnake.ButtonStyle.green
|
2026-01-01 18:48:25 +01:00
|
|
|
|
|
|
|
|
|
|
await interaction.response.edit_message(view=self)
|
|
|
|
|
|
|
|
|
|
|
|
# Send confirmation
|
2026-05-06 17:07:09 +02:00
|
|
|
|
embed = disnake.Embed(
|
2026-01-01 18:48:25 +01:00
|
|
|
|
title="✅ Retrait effectué",
|
|
|
|
|
|
description=f"{user_mention} a été retiré de la whitelist.",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
color=disnake.Color.green()
|
2026-01-01 18:48:25 +01:00
|
|
|
|
)
|
|
|
|
|
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
|
|
|
|
|
|
|
|
|
|
|
# Log the removal
|
|
|
|
|
|
try:
|
|
|
|
|
|
guild = self.bot.get_guild(self.guild_id)
|
|
|
|
|
|
if guild:
|
|
|
|
|
|
log_channel_id = cog.get_log_channel(self.guild_id)
|
|
|
|
|
|
if log_channel_id:
|
|
|
|
|
|
log_channel = guild.get_channel(log_channel_id)
|
|
|
|
|
|
if log_channel:
|
2026-05-06 17:07:09 +02:00
|
|
|
|
embed = disnake.Embed(
|
2026-01-01 18:48:25 +01:00
|
|
|
|
title="🗑️ Whitelist Manual Removal",
|
|
|
|
|
|
description=f"{user_mention} a été retiré manuellement de la whitelist.",
|
2026-05-06 17:07:09 +02:00
|
|
|
|
color=disnake.Color.green(),
|
2026-01-01 18:48:25 +01:00
|
|
|
|
timestamp=datetime.now()
|
|
|
|
|
|
)
|
|
|
|
|
|
embed.set_footer(text=f"Retiré par {interaction.user.display_name}")
|
|
|
|
|
|
await log_channel.send(embed=embed)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"Error logging manual removal: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
await interaction.response.send_message(
|
|
|
|
|
|
"ℹ️ L'utilisateur n'était pas dans la whitelist.",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
await interaction.response.send_message(
|
|
|
|
|
|
f"❌ Erreur lors du retrait: {str(e)}",
|
|
|
|
|
|
ephemeral=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-09 18:42:42 +02:00
|
|
|
|
def setup(bot):
|
|
|
|
|
|
bot.add_cog(WhitelistMonitor(bot))
|