dc-securex 2.15.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dc-securex might be problematic. Click here for more details.
- dc_securex-2.15.3.dist-info/METADATA +653 -0
- dc_securex-2.15.3.dist-info/RECORD +17 -0
- dc_securex-2.15.3.dist-info/WHEEL +5 -0
- dc_securex-2.15.3.dist-info/licenses/LICENSE +21 -0
- dc_securex-2.15.3.dist-info/top_level.txt +1 -0
- securex/__init__.py +18 -0
- securex/backup/__init__.py +5 -0
- securex/backup/manager.py +693 -0
- securex/client.py +556 -0
- securex/handlers/__init__.py +8 -0
- securex/handlers/channel.py +97 -0
- securex/handlers/member.py +110 -0
- securex/handlers/role.py +74 -0
- securex/models.py +123 -0
- securex/utils/__init__.py +5 -0
- securex/utils/punishment.py +124 -0
- securex/utils/whitelist.py +129 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Member protection handler (bot/ban/kick protection).
|
|
3
|
+
"""
|
|
4
|
+
import discord
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MemberHandler:
|
|
9
|
+
"""Handles member-related protection"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DANGEROUS_PERMISSIONS = frozenset([
|
|
13
|
+
'administrator',
|
|
14
|
+
'kick_members',
|
|
15
|
+
'ban_members',
|
|
16
|
+
'manage_guild',
|
|
17
|
+
'manage_roles',
|
|
18
|
+
'manage_channels',
|
|
19
|
+
'manage_webhooks',
|
|
20
|
+
'manage_emojis',
|
|
21
|
+
'mention_everyone',
|
|
22
|
+
'manage_expressions'
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
def __init__(self, sdk):
|
|
26
|
+
self.sdk = sdk
|
|
27
|
+
self.bot = sdk.bot
|
|
28
|
+
self.whitelist = sdk.whitelist
|
|
29
|
+
|
|
30
|
+
async def _is_authorized(self, guild: discord.Guild, user_id: int) -> bool:
|
|
31
|
+
"""Check if user is authorized"""
|
|
32
|
+
if guild.owner_id == user_id:
|
|
33
|
+
return True
|
|
34
|
+
if user_id == self.bot.user.id:
|
|
35
|
+
return True
|
|
36
|
+
return await self.whitelist.is_whitelisted(guild.id, user_id)
|
|
37
|
+
|
|
38
|
+
async def on_member_ban(self, guild: discord.Guild, user: discord.User):
|
|
39
|
+
"""Restore banned victims (punishment handled by action worker)"""
|
|
40
|
+
try:
|
|
41
|
+
await asyncio.sleep(0.5)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
from datetime import datetime, timezone, timedelta
|
|
45
|
+
cutoff_time = datetime.now(timezone.utc) - timedelta(seconds=30)
|
|
46
|
+
|
|
47
|
+
async for entry in guild.audit_logs(limit=50, action=discord.AuditLogAction.ban):
|
|
48
|
+
if entry.created_at < cutoff_time:
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
if entry.target.id == user.id:
|
|
52
|
+
if not await self._is_authorized(guild, entry.user.id):
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
await guild.unban(user, reason="SecureX: Restoring banned member")
|
|
56
|
+
print(f"🔄 Unbanned unauthorized member ban: {user.name}")
|
|
57
|
+
except (discord.errors.Forbidden, discord.errors.HTTPException):
|
|
58
|
+
pass
|
|
59
|
+
break
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"Error in on_member_ban: {e}")
|
|
62
|
+
|
|
63
|
+
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
|
64
|
+
"""Check for dangerous permissions and remove unauthorized roles"""
|
|
65
|
+
try:
|
|
66
|
+
|
|
67
|
+
if before.roles == after.roles:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
guild = after.guild
|
|
71
|
+
await asyncio.sleep(0.5)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
from datetime import datetime, timezone, timedelta
|
|
75
|
+
cutoff_time = datetime.now(timezone.utc) - timedelta(seconds=30)
|
|
76
|
+
|
|
77
|
+
async for entry in guild.audit_logs(limit=50, action=discord.AuditLogAction.member_role_update):
|
|
78
|
+
if entry.created_at < cutoff_time:
|
|
79
|
+
break
|
|
80
|
+
if entry.target.id == after.id:
|
|
81
|
+
|
|
82
|
+
is_authorized = await self._is_authorized(guild, entry.user.id)
|
|
83
|
+
|
|
84
|
+
if not is_authorized:
|
|
85
|
+
|
|
86
|
+
dangerous_roles = []
|
|
87
|
+
|
|
88
|
+
for role in after.roles:
|
|
89
|
+
if role.is_default():
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
permissions = role.permissions
|
|
94
|
+
for perm_name in self.DANGEROUS_PERMISSIONS:
|
|
95
|
+
if getattr(permissions, perm_name, False):
|
|
96
|
+
dangerous_roles.append(role)
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if dangerous_roles:
|
|
101
|
+
try:
|
|
102
|
+
await after.remove_roles(*dangerous_roles, reason="SecureX: Removed dangerous roles (unauthorized update)")
|
|
103
|
+
role_names = ", ".join([r.name for r in dangerous_roles])
|
|
104
|
+
print(f"🛡️ Bulk removed {len(dangerous_roles)} dangerous role(s) from {after.name}: {role_names}")
|
|
105
|
+
except (discord.errors.Forbidden, discord.errors.HTTPException) as e:
|
|
106
|
+
print(f"⚠️ Failed to remove roles: {e}")
|
|
107
|
+
|
|
108
|
+
break
|
|
109
|
+
except Exception as e:
|
|
110
|
+
print(f"Error in on_member_update: {e}")
|
securex/handlers/role.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Role protection handler for SecureX SDK.
|
|
3
|
+
"""
|
|
4
|
+
import discord
|
|
5
|
+
import asyncio
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RoleHandler:
|
|
10
|
+
"""Handles role protection logic"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, sdk):
|
|
13
|
+
self.sdk = sdk
|
|
14
|
+
self.bot = sdk.bot
|
|
15
|
+
self.backup_manager = sdk.backup_manager
|
|
16
|
+
self.whitelist = sdk.whitelist
|
|
17
|
+
|
|
18
|
+
async def _is_authorized(self, guild: discord.Guild, user_id: int) -> bool:
|
|
19
|
+
"""Check if user is authorized"""
|
|
20
|
+
if guild.owner_id == user_id:
|
|
21
|
+
return True
|
|
22
|
+
if user_id == self.bot.user.id:
|
|
23
|
+
return True
|
|
24
|
+
return await self.whitelist.is_whitelisted(guild.id, user_id)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def on_role_delete(self, role: discord.Role):
|
|
28
|
+
"""Restore unauthorized role deletions"""
|
|
29
|
+
try:
|
|
30
|
+
guild = role.guild
|
|
31
|
+
await asyncio.sleep(1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
from datetime import timezone
|
|
35
|
+
cutoff_time = datetime.now(timezone.utc) - timedelta(seconds=30)
|
|
36
|
+
|
|
37
|
+
async for entry in guild.audit_logs(limit=50, action=discord.AuditLogAction.role_delete):
|
|
38
|
+
if entry.created_at < cutoff_time:
|
|
39
|
+
break
|
|
40
|
+
|
|
41
|
+
if entry.target.id == role.id:
|
|
42
|
+
if not await self._is_authorized(guild, entry.user.id):
|
|
43
|
+
if await self.backup_manager.restore_role(guild, role.id):
|
|
44
|
+
print(f"🔄 Restored unauthorized role deletion: {role.name}")
|
|
45
|
+
break
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
async def on_role_update(self, before: discord.Role, after: discord.Role):
|
|
50
|
+
"""Restore unauthorized role permission changes"""
|
|
51
|
+
try:
|
|
52
|
+
|
|
53
|
+
if before.position == after.position and before.permissions == after.permissions:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
guild = after.guild
|
|
57
|
+
await asyncio.sleep(0.3)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
from datetime import timezone
|
|
61
|
+
cutoff_time = datetime.now(timezone.utc) - timedelta(seconds=30)
|
|
62
|
+
|
|
63
|
+
async for entry in guild.audit_logs(limit=50, oldest_first=False):
|
|
64
|
+
if entry.created_at < cutoff_time:
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
if entry.action == discord.AuditLogAction.role_update:
|
|
68
|
+
if entry.target.id == after.id:
|
|
69
|
+
if not await self._is_authorized(guild, entry.user.id):
|
|
70
|
+
if await self.backup_manager.restore_role_permissions(guild, after.id):
|
|
71
|
+
print(f"🔄 Restored unauthorized role update: {after.name}")
|
|
72
|
+
break
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
securex/models.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for SecureX SDK.
|
|
3
|
+
These objects are returned to developers - no UI included.
|
|
4
|
+
"""
|
|
5
|
+
from dataclasses import dataclass, asdict, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Optional, List, Dict
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ThreatEvent:
|
|
13
|
+
"""
|
|
14
|
+
Represents a detected security threat.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
type: Type of threat (e.g., "channel_delete", "role_create")
|
|
18
|
+
guild_id: ID of the guild where threat occurred
|
|
19
|
+
actor_id: ID of user who performed the action
|
|
20
|
+
target_id: ID of the affected resource
|
|
21
|
+
target_name: Name of the affected resource
|
|
22
|
+
prevented: Whether the action was prevented
|
|
23
|
+
restored: Whether restoration was successful
|
|
24
|
+
timestamp: When the threat was detected
|
|
25
|
+
details: Additional context about the threat
|
|
26
|
+
"""
|
|
27
|
+
type: str
|
|
28
|
+
guild_id: int
|
|
29
|
+
actor_id: int
|
|
30
|
+
target_id: int
|
|
31
|
+
target_name: str
|
|
32
|
+
prevented: bool
|
|
33
|
+
restored: bool
|
|
34
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
35
|
+
details: Dict = field(default_factory=dict)
|
|
36
|
+
punishment_action: Optional[str] = None
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict:
|
|
39
|
+
"""Convert to dictionary for serialization"""
|
|
40
|
+
data = asdict(self)
|
|
41
|
+
data['timestamp'] = self.timestamp.isoformat()
|
|
42
|
+
return data
|
|
43
|
+
|
|
44
|
+
def to_json(self) -> str:
|
|
45
|
+
"""Convert to JSON string"""
|
|
46
|
+
return json.dumps(self.to_dict())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class BackupInfo:
|
|
51
|
+
"""
|
|
52
|
+
Information about a server backup.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
guild_id: ID of the backed up guild
|
|
56
|
+
timestamp: When backup was created
|
|
57
|
+
channel_count: Number of channels backed up
|
|
58
|
+
role_count: Number of roles backed up
|
|
59
|
+
backup_path: Path to backup file
|
|
60
|
+
success: Whether backup completed successfully
|
|
61
|
+
"""
|
|
62
|
+
guild_id: int
|
|
63
|
+
timestamp: datetime
|
|
64
|
+
channel_count: int
|
|
65
|
+
role_count: int
|
|
66
|
+
backup_path: str
|
|
67
|
+
success: bool = True
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict:
|
|
70
|
+
"""Convert to dictionary"""
|
|
71
|
+
data = asdict(self)
|
|
72
|
+
data['timestamp'] = self.timestamp.isoformat()
|
|
73
|
+
return data
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class RestoreResult:
|
|
78
|
+
"""
|
|
79
|
+
Result of a restoration operation.
|
|
80
|
+
|
|
81
|
+
Attributes:
|
|
82
|
+
success: Overall success status
|
|
83
|
+
items_restored: Number of items successfully restored
|
|
84
|
+
items_failed: Number of items that failed to restore
|
|
85
|
+
errors: List of error messages
|
|
86
|
+
duration: Time taken in seconds
|
|
87
|
+
details: Additional restoration details
|
|
88
|
+
"""
|
|
89
|
+
success: bool
|
|
90
|
+
items_restored: int
|
|
91
|
+
items_failed: int
|
|
92
|
+
errors: List[str] = field(default_factory=list)
|
|
93
|
+
duration: float = 0.0
|
|
94
|
+
details: Dict = field(default_factory=dict)
|
|
95
|
+
|
|
96
|
+
def to_dict(self) -> dict:
|
|
97
|
+
"""Convert to dictionary"""
|
|
98
|
+
return asdict(self)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class WhitelistChange:
|
|
103
|
+
"""
|
|
104
|
+
Represents a whitelist modification.
|
|
105
|
+
|
|
106
|
+
Attributes:
|
|
107
|
+
guild_id: Guild where change occurred
|
|
108
|
+
user_id: User added/removed from whitelist
|
|
109
|
+
action: "added" or "removed"
|
|
110
|
+
timestamp: When the change occurred
|
|
111
|
+
moderator_id: Who made the change
|
|
112
|
+
"""
|
|
113
|
+
guild_id: int
|
|
114
|
+
user_id: int
|
|
115
|
+
action: str
|
|
116
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
117
|
+
moderator_id: Optional[int] = None
|
|
118
|
+
|
|
119
|
+
def to_dict(self) -> dict:
|
|
120
|
+
"""Convert to dictionary"""
|
|
121
|
+
data = asdict(self)
|
|
122
|
+
data['timestamp'] = self.timestamp.isoformat()
|
|
123
|
+
return data
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Punishment executor for anti-nuke violations.
|
|
3
|
+
Handles kick, ban, timeout, and warn actions.
|
|
4
|
+
"""
|
|
5
|
+
import discord
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PunishmentExecutor:
|
|
11
|
+
"""Executes punishment actions on violators"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, bot):
|
|
14
|
+
"""Initialize with bot instance"""
|
|
15
|
+
self.bot = bot
|
|
16
|
+
|
|
17
|
+
async def punish(
|
|
18
|
+
self,
|
|
19
|
+
guild: discord.Guild,
|
|
20
|
+
user: discord.User,
|
|
21
|
+
violation_type: str,
|
|
22
|
+
details: str = None,
|
|
23
|
+
sdk = None
|
|
24
|
+
) -> str:
|
|
25
|
+
"""
|
|
26
|
+
Execute punishment on violator based on violation type.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
guild: The guild where violation occurred
|
|
30
|
+
user: The user who committed the violation
|
|
31
|
+
violation_type: Type of violation (e.g., "channel_delete")
|
|
32
|
+
details: Optional details about the violation
|
|
33
|
+
sdk: SDK instance for accessing punishment config
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The punishment action that was applied ("none", "warn", "timeout", "kick", "ban")
|
|
37
|
+
"""
|
|
38
|
+
if sdk is None:
|
|
39
|
+
return "none"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
action = sdk.punishments.get(violation_type, "none")
|
|
43
|
+
|
|
44
|
+
if action == "none":
|
|
45
|
+
return action
|
|
46
|
+
|
|
47
|
+
member = guild.get_member(user.id)
|
|
48
|
+
if not member:
|
|
49
|
+
print(f"Cannot punish {user}: not a member")
|
|
50
|
+
return action
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if member.id == guild.owner_id:
|
|
54
|
+
print(f"Cannot punish server owner {user}")
|
|
55
|
+
return action
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
if action == "warn":
|
|
61
|
+
|
|
62
|
+
print(f"⚠️ Warned {user} for {violation_type}")
|
|
63
|
+
|
|
64
|
+
elif action == "timeout":
|
|
65
|
+
duration = timedelta(seconds=sdk.timeout_duration)
|
|
66
|
+
await member.timeout(duration, reason=f"SecureX: {violation_type}")
|
|
67
|
+
print(f"⏱️ Timed out {user} for {sdk.timeout_duration}s ({violation_type})")
|
|
68
|
+
|
|
69
|
+
elif action == "kick":
|
|
70
|
+
await member.kick(reason=f"SecureX: {violation_type}")
|
|
71
|
+
print(f"👢 Kicked {user} for {violation_type}")
|
|
72
|
+
|
|
73
|
+
elif action == "ban":
|
|
74
|
+
await member.ban(reason=f"SecureX: {violation_type}", delete_message_days=0)
|
|
75
|
+
print(f"🔨 Banned {user} for {violation_type}")
|
|
76
|
+
|
|
77
|
+
if sdk.notify_punished_user:
|
|
78
|
+
await self._notify_user(member, violation_type, action, details, sdk)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
except discord.Forbidden:
|
|
82
|
+
print(f"❌ Missing permissions to {action} {user}")
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f"Error punishing {user}: {e}")
|
|
85
|
+
|
|
86
|
+
return action
|
|
87
|
+
|
|
88
|
+
async def _notify_user(
|
|
89
|
+
self,
|
|
90
|
+
member: discord.Member,
|
|
91
|
+
violation_type: str,
|
|
92
|
+
action: str,
|
|
93
|
+
details: Optional[str],
|
|
94
|
+
sdk
|
|
95
|
+
):
|
|
96
|
+
"""Send DM notification to punished user"""
|
|
97
|
+
try:
|
|
98
|
+
|
|
99
|
+
readable_type = violation_type.replace("_", " ").title()
|
|
100
|
+
|
|
101
|
+
embed = discord.Embed(
|
|
102
|
+
title="🚨 Anti-Nuke Violation Detected",
|
|
103
|
+
description=f"You triggered anti-nuke protection in **{member.guild.name}**",
|
|
104
|
+
color=discord.Color.red()
|
|
105
|
+
)
|
|
106
|
+
embed.add_field(name="Violation Type", value=readable_type, inline=False)
|
|
107
|
+
if details:
|
|
108
|
+
embed.add_field(name="Details", value=details, inline=False)
|
|
109
|
+
embed.add_field(
|
|
110
|
+
name="Action Taken",
|
|
111
|
+
value=f"**{action.title()}**",
|
|
112
|
+
inline=False
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if action == "timeout":
|
|
116
|
+
mins = sdk.timeout_duration // 60
|
|
117
|
+
embed.add_field(name="Duration", value=f"{mins} minutes", inline=False)
|
|
118
|
+
|
|
119
|
+
await member.send(embed=embed)
|
|
120
|
+
print(f"📨 Sent punishment notification to {member}")
|
|
121
|
+
except discord.Forbidden:
|
|
122
|
+
print(f"Cannot DM {member} (DMs disabled)")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
print(f"Error notifying {member}: {e}")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Whitelist management for SecureX SDK.
|
|
3
|
+
Strictly per-guild - no cross-server whitelisting.
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import aiofiles
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Set
|
|
9
|
+
from ..models import WhitelistChange
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WhitelistManager:
|
|
13
|
+
"""Manages whitelisted users per guild (guild-specific only)"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, sdk):
|
|
16
|
+
self.sdk = sdk
|
|
17
|
+
self.data_dir = Path("./data/whitelists")
|
|
18
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
self._whitelists: dict[int, Set[int]] = {}
|
|
22
|
+
self._cache_loaded = False
|
|
23
|
+
|
|
24
|
+
def _get_file_path(self, guild_id: int) -> Path:
|
|
25
|
+
"""Get whitelist file path for guild"""
|
|
26
|
+
return self.data_dir / f"{guild_id}.json"
|
|
27
|
+
|
|
28
|
+
async def preload_all(self):
|
|
29
|
+
"""
|
|
30
|
+
Preload ALL whitelists into memory on startup.
|
|
31
|
+
Call this in enable() to warm the cache.
|
|
32
|
+
"""
|
|
33
|
+
if self._cache_loaded:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
print("🔄 Preloading whitelists into cache...")
|
|
37
|
+
count = 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
for file_path in self.data_dir.glob("*.json"):
|
|
41
|
+
try:
|
|
42
|
+
guild_id = int(file_path.stem)
|
|
43
|
+
async with aiofiles.open(file_path, 'r') as f:
|
|
44
|
+
content = await f.read()
|
|
45
|
+
data = json.loads(content)
|
|
46
|
+
self._whitelists[guild_id] = set(data.get('users', []))
|
|
47
|
+
count += 1
|
|
48
|
+
except Exception as e:
|
|
49
|
+
print(f"Error loading whitelist {file_path}: {e}")
|
|
50
|
+
|
|
51
|
+
self._cache_loaded = True
|
|
52
|
+
print(f"✅ Cached {count} whitelists in memory")
|
|
53
|
+
|
|
54
|
+
async def _load_whitelist(self, guild_id: int):
|
|
55
|
+
"""Load whitelist from file (fallback if not preloaded)"""
|
|
56
|
+
file_path = self._get_file_path(guild_id)
|
|
57
|
+
if file_path.exists():
|
|
58
|
+
async with aiofiles.open(file_path, 'r') as f:
|
|
59
|
+
content = await f.read()
|
|
60
|
+
data = json.loads(content)
|
|
61
|
+
self._whitelists[guild_id] = set(data.get('users', []))
|
|
62
|
+
|
|
63
|
+
async def _save_whitelist(self, guild_id: int):
|
|
64
|
+
"""Save whitelist to file (async)"""
|
|
65
|
+
file_path = self._get_file_path(guild_id)
|
|
66
|
+
async with aiofiles.open(file_path, 'w') as f:
|
|
67
|
+
content = json.dumps({
|
|
68
|
+
'users': list(self._whitelists.get(guild_id, set()))
|
|
69
|
+
}, indent=2)
|
|
70
|
+
await f.write(content)
|
|
71
|
+
|
|
72
|
+
async def add(self, guild_id: int, user_id: int, moderator_id: int = None):
|
|
73
|
+
"""Add user to guild whitelist"""
|
|
74
|
+
if guild_id not in self._whitelists:
|
|
75
|
+
await self._load_whitelist(guild_id)
|
|
76
|
+
if guild_id not in self._whitelists:
|
|
77
|
+
self._whitelists[guild_id] = set()
|
|
78
|
+
|
|
79
|
+
self._whitelists[guild_id].add(user_id)
|
|
80
|
+
await self._save_whitelist(guild_id)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
change = WhitelistChange(
|
|
84
|
+
guild_id=guild_id,
|
|
85
|
+
user_id=user_id,
|
|
86
|
+
action="added",
|
|
87
|
+
moderator_id=moderator_id
|
|
88
|
+
)
|
|
89
|
+
await self.sdk._trigger_callbacks('whitelist_changed', change)
|
|
90
|
+
|
|
91
|
+
async def remove(self, guild_id: int, user_id: int, moderator_id: int = None):
|
|
92
|
+
"""Remove user from guild whitelist"""
|
|
93
|
+
if guild_id not in self._whitelists:
|
|
94
|
+
await self._load_whitelist(guild_id)
|
|
95
|
+
|
|
96
|
+
if guild_id in self._whitelists:
|
|
97
|
+
self._whitelists[guild_id].discard(user_id)
|
|
98
|
+
await self._save_whitelist(guild_id)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
change = WhitelistChange(
|
|
102
|
+
guild_id=guild_id,
|
|
103
|
+
user_id=user_id,
|
|
104
|
+
action="removed",
|
|
105
|
+
moderator_id=moderator_id
|
|
106
|
+
)
|
|
107
|
+
await self.sdk._trigger_callbacks('whitelist_changed', change)
|
|
108
|
+
|
|
109
|
+
async def get_all(self, guild_id: int) -> List[int]:
|
|
110
|
+
"""Get all whitelisted users for guild"""
|
|
111
|
+
if guild_id not in self._whitelists:
|
|
112
|
+
await self._load_whitelist(guild_id)
|
|
113
|
+
return list(self._whitelists.get(guild_id, set()))
|
|
114
|
+
|
|
115
|
+
async def is_whitelisted(self, guild_id: int, user_id: int) -> bool:
|
|
116
|
+
"""
|
|
117
|
+
Check if user is whitelisted in specific guild.
|
|
118
|
+
Uses cached data - instant response (no file I/O).
|
|
119
|
+
"""
|
|
120
|
+
if guild_id not in self._whitelists:
|
|
121
|
+
await self._load_whitelist(guild_id)
|
|
122
|
+
|
|
123
|
+
return user_id in self._whitelists.get(guild_id, set())
|
|
124
|
+
|
|
125
|
+
async def clear(self, guild_id: int):
|
|
126
|
+
"""Clear all whitelisted users for a guild"""
|
|
127
|
+
self._whitelists[guild_id] = set()
|
|
128
|
+
await self._save_whitelist(guild_id)
|
|
129
|
+
|