dc-securex 2.5.3__tar.gz → 2.9.6__tar.gz
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.
- {dc_securex-2.5.3 → dc_securex-2.9.6}/CHANGELOG.md +130 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/PKG-INFO +1 -1
- {dc_securex-2.5.3 → dc_securex-2.9.6}/dc_securex.egg-info/PKG-INFO +1 -1
- {dc_securex-2.5.3 → dc_securex-2.9.6}/dc_securex.egg-info/SOURCES.txt +1 -2
- {dc_securex-2.5.3 → dc_securex-2.9.6}/pyproject.toml +1 -1
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/backup/manager.py +318 -10
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/client.py +5 -10
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/channel.py +28 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/role.py +8 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/setup.py +1 -1
- dc_securex-2.5.3/securex/handlers/channel_monitor.py +0 -128
- dc_securex-2.5.3/securex/handlers/role_monitor.py +0 -164
- {dc_securex-2.5.3 → dc_securex-2.9.6}/.DS_Store +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/LICENSE +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/MANIFEST.in +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/README.md +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/dc_securex.egg-info/dependency_links.txt +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/dc_securex.egg-info/requires.txt +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/dc_securex.egg-info/top_level.txt +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/advanced_usage.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/backup_management.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/basic_usage.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/channel_protection.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/member_protection.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/punishment_config.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/role_protection.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/webhook_protection.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/whitelist_management.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/__init__.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/__pycache__/__init__.cpython-312.pyc +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/__pycache__/client.cpython-312.pyc +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/__pycache__/models.cpython-312.pyc +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/backup/__init__.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/backup/__pycache__/__init__.cpython-312.pyc +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/backup/__pycache__/manager.cpython-312.pyc +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/__init__.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/__pycache__/__init__.cpython-312.pyc +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/__pycache__/channel.cpython-312.pyc +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/__pycache__/member.cpython-312.pyc +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/__pycache__/role.cpython-312.pyc +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/__pycache__/role_monitor.cpython-312.pyc +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/__pycache__/webhook.cpython-312.pyc +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/member.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/webhook.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/models.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/utils/__init__.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/utils/__pycache__/punishment.cpython-312.pyc +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/utils/__pycache__/whitelist.cpython-312.pyc +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/utils/punishment.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/utils/whitelist.py +0 -0
- {dc_securex-2.5.3 → dc_securex-2.9.6}/setup.cfg +0 -0
|
@@ -1,5 +1,135 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.9.6] - 2026-01-15
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Instant Position Restoration**: The `restore_role` function now attempts to set the correct position immediately after creation as a fast-track fallback.
|
|
7
|
+
- **Hierarchy Ground-Truth**: Re-added `fetch_member` to ensure the bot always uses its real-time rank when calculating legal position moves.
|
|
8
|
+
|
|
9
|
+
## [2.9.5] - 2026-01-15
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **Cache-Sync Restoration**: Ensured that the in-memory backup cache is updated instantly after an ID swap.
|
|
13
|
+
- This fixes issues where the repair logic would use stale role/channel IDs from memory even after the file was updated.
|
|
14
|
+
|
|
15
|
+
## [2.9.3] - 2026-01-15
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Exhaustive Structural Repair**: Maximized restoration success for role and channel positions.
|
|
19
|
+
- Implemented **Name-Based Fallback** (enables position repairs even before ID swaps propagate).
|
|
20
|
+
- Added **Strict Hierarchy Validation** (prevents illegal position edits and provides clear debug logging).
|
|
21
|
+
- Fixed a `SyntaxError` in `BackupManager` indentation from the previous build.
|
|
22
|
+
|
|
23
|
+
## [2.9.2] - 2026-01-15
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **Atomic Structural Repair**: Fixed a critical race condition where rapid-fire role/channel restorations would lose their "Smart ID Swaps".
|
|
27
|
+
- Implemented **Per-Guild Async Locks** in `BackupManager` to synchronize all backup file read/write operations.
|
|
28
|
+
- Ensures that ID mapping updates are atomic and never overwritten during high-concurrency events (nukes).
|
|
29
|
+
- Enhanced role matching logic for more robust structural repairs.
|
|
30
|
+
|
|
31
|
+
## [2.9.1] - 2026-01-15
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- **Highly Robust Structural Repair**: Fixed issues where role positions were not updating due to cache staleness and rapid-fire events.
|
|
35
|
+
- Added **Async Locking and Debouncing** to repair methods (prevents rate limits and race conditions during mass deletions).
|
|
36
|
+
- Switched to Ground-Truth fetching (`fetch_roles` and `fetch_channels`) to bypass cache issues.
|
|
37
|
+
- Increased event-settling delay to 2 seconds for guaranteed position application.
|
|
38
|
+
|
|
39
|
+
## [2.9.0] - 2026-01-15
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
- **Smart Role Restoration**: Restored roles now correctly have their positions updated.
|
|
43
|
+
- Implemented **Smart ID Swapping** for roles (ID mapping in backup).
|
|
44
|
+
- Enhanced `repair_role_structure` and `repair_channel_structure` with fresh cache fetching and event-settling delays.
|
|
45
|
+
- Ensures structural integrity is restored even when role IDs change.
|
|
46
|
+
|
|
47
|
+
## [2.8.9] - 2026-01-15
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
- Final cleanup of monitor-to-event architecture.
|
|
51
|
+
|
|
52
|
+
## [2.8.8] - 2026-01-15
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
- **Pure Event-Driven Architecture**: Completely removed background polling loops (`ChannelPositionMonitor`, `RolePositionMonitor`).
|
|
56
|
+
- **Integrated Structural Repairs**: Re-implemented the monitor's structural enforcement logic directly into `BackupManager` as `repair_channel_structure` and `repair_role_structure`.
|
|
57
|
+
- **Instant Response**: Structural repairs are now triggered reactively by event handlers (deletion/update) instead of waiting for a 300ms loop.
|
|
58
|
+
- **Zero Idle Overhead**: No background tasks are running during idle periods, saving CPU and API quota.
|
|
59
|
+
|
|
60
|
+
## [2.8.7] - 2026-01-15
|
|
61
|
+
|
|
62
|
+
### Improved
|
|
63
|
+
- **Reactive Structural Repairs**: Event handlers now instantly trigger the monitor's `_check_guild_roles` and `_check_guild_channels` logic after restoration.
|
|
64
|
+
- Fixes issues where role/channel positions were not updated fast enough by the background loop.
|
|
65
|
+
- Combines the speed of event-driven repairs with the robustness of background monitoring.
|
|
66
|
+
|
|
67
|
+
## [2.8.6] - 2026-01-15
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
- **Restored Background Monitoring**: Re-implemented `ChannelPositionMonitor` and `RolePositionMonitor` (300ms polling) to ensure 100% structural integrity during rapid nukes.
|
|
71
|
+
- **Coordination Logic**: Re-added `processing_channel_deletions` to prevent race conditions between handlers and monitors.
|
|
72
|
+
|
|
73
|
+
## [2.8.5] - 2026-01-14
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
- **Major Architecture Refactor**: This version supercedes all previous 2.x releases.
|
|
77
|
+
- **Removed Monitors**: Background polling loops (`ChannelPositionMonitor`, `RolePositionMonitor`) are completely removed.
|
|
78
|
+
- **Event-Driven Positioning**: Position restoration and structural repairs are now triggered directly by event handlers using bulk Discord APIs.
|
|
79
|
+
- **Efficiency**: Near-zero idle overhead. Structural integrity is now maintained reactively.
|
|
80
|
+
|
|
81
|
+
## [2.6.0] - 2026-01-14
|
|
82
|
+
|
|
83
|
+
### New Features
|
|
84
|
+
- **Full State Synchronization**: Authorized actions now instantly update the backup
|
|
85
|
+
- **Authorized Updates**: Manual channel edits (permissions, positions, names) by authorized users are now auto-updated in the backup.
|
|
86
|
+
- **Authorized Creations/Deletions**: Fully integrated in previous patches, now polished in 2.6.0.
|
|
87
|
+
- **Improved Monitoring**: The Channel Monitor now perfectly coordinates with event handlers, eliminating race conditions for authorized actions.
|
|
88
|
+
|
|
89
|
+
## [2.5.9] - 2026-01-14
|
|
90
|
+
|
|
91
|
+
### New Features
|
|
92
|
+
- **Auto-Backup for Authorized Creations**: When an authorized user creates a channel, it is immediately added to the backup
|
|
93
|
+
- Ensures the channel monitor knows about the new channel instantly
|
|
94
|
+
- Prevents any potential conflicts and keeps the backup up-to-date
|
|
95
|
+
- Compliments the Authorized Deletion fix from 2.5.8
|
|
96
|
+
|
|
97
|
+
## [2.5.8] - 2026-01-14
|
|
98
|
+
|
|
99
|
+
### Critical Fix
|
|
100
|
+
- **Race Condition Solved**: Fixed issue where Monitor restored "authorized" deletions before Handler could verify them
|
|
101
|
+
- Implemented `processing_channel_deletions` coordination set
|
|
102
|
+
- Monitor now pauses restoration if a channel is currently being processed by the ChannelHandler
|
|
103
|
+
- Ensures Authorized Deletions are correctly removed from backup BEFORE Monitor acts
|
|
104
|
+
|
|
105
|
+
## [2.5.7] - 2026-01-14
|
|
106
|
+
|
|
107
|
+
### Hotfix
|
|
108
|
+
- **Fixed Syntax Error**: Resolved indentation issue in `restore_channel` missing `except` block
|
|
109
|
+
- Confirmed valid syntax for 2.5.6 features (Authorized Deletion Handling)
|
|
110
|
+
|
|
111
|
+
## [2.5.6] - 2026-01-14
|
|
112
|
+
|
|
113
|
+
### Critical Fix
|
|
114
|
+
- **Authorized Deletion Handling**: Fixed issue where the monitor would restore channels deleted by authorized users
|
|
115
|
+
- When an authorized user deletes a channel, it is now explicitly removed from the backup
|
|
116
|
+
- This prevents the 300ms monitor from flagging it as "missing" and restoring it
|
|
117
|
+
|
|
118
|
+
## [2.5.5] - 2026-01-14
|
|
119
|
+
|
|
120
|
+
### Critical Fix
|
|
121
|
+
- **Channel Restoration Spam Fix**: Implemented ID swapping in `restore_channel`
|
|
122
|
+
- When a channel is restored (getting a new ID), the backup file is instantly updated with the new ID
|
|
123
|
+
- Prevents the monitor from continuously detecting the old ID as "missing" and creating duplicates
|
|
124
|
+
- Essential for the self-healing monitor to work correctly
|
|
125
|
+
|
|
126
|
+
## [2.5.4] - 2026-01-14
|
|
127
|
+
|
|
128
|
+
### Fixed
|
|
129
|
+
- **Startup Bug**: Fixed `ChannelPositionMonitor` not starting automatically
|
|
130
|
+
- Added `channel_position_monitoring` argument to `enable()` (defaults to True)
|
|
131
|
+
- Properly initializes the monitor loop on startup
|
|
132
|
+
|
|
3
133
|
## [2.5.3] - 2026-01-14
|
|
4
134
|
|
|
5
135
|
### Verified
|
|
@@ -4,6 +4,7 @@ LICENSE
|
|
|
4
4
|
MANIFEST.in
|
|
5
5
|
README.md
|
|
6
6
|
pyproject.toml
|
|
7
|
+
setup.cfg
|
|
7
8
|
setup.py
|
|
8
9
|
dc_securex.egg-info/PKG-INFO
|
|
9
10
|
dc_securex.egg-info/SOURCES.txt
|
|
@@ -31,10 +32,8 @@ securex/backup/__pycache__/__init__.cpython-312.pyc
|
|
|
31
32
|
securex/backup/__pycache__/manager.cpython-312.pyc
|
|
32
33
|
securex/handlers/__init__.py
|
|
33
34
|
securex/handlers/channel.py
|
|
34
|
-
securex/handlers/channel_monitor.py
|
|
35
35
|
securex/handlers/member.py
|
|
36
36
|
securex/handlers/role.py
|
|
37
|
-
securex/handlers/role_monitor.py
|
|
38
37
|
securex/handlers/webhook.py
|
|
39
38
|
securex/handlers/__pycache__/__init__.cpython-312.pyc
|
|
40
39
|
securex/handlers/__pycache__/channel.cpython-312.pyc
|
|
@@ -25,6 +25,13 @@ class BackupManager:
|
|
|
25
25
|
self._role_cache: Dict[int, Dict] = {} # {guild_id: role_backup_data}
|
|
26
26
|
self._cache_loaded = False
|
|
27
27
|
self._refresh_task = None
|
|
28
|
+
self._guild_locks: Dict[int, asyncio.Lock] = {}
|
|
29
|
+
|
|
30
|
+
def _get_guild_lock(self, guild_id: int) -> asyncio.Lock:
|
|
31
|
+
"""Get or create an async lock for a specific guild"""
|
|
32
|
+
if guild_id not in self._guild_locks:
|
|
33
|
+
self._guild_locks[guild_id] = asyncio.Lock()
|
|
34
|
+
return self._guild_locks[guild_id]
|
|
28
35
|
|
|
29
36
|
async def create_backup(self, guild_id: int) -> BackupInfo:
|
|
30
37
|
"""Create complete backup for a guild"""
|
|
@@ -167,8 +174,9 @@ class BackupManager:
|
|
|
167
174
|
if not backup_file.exists():
|
|
168
175
|
return None
|
|
169
176
|
|
|
170
|
-
with
|
|
171
|
-
|
|
177
|
+
async with self._get_guild_lock(guild.id):
|
|
178
|
+
with open(backup_file, 'r') as f:
|
|
179
|
+
backup = json.load(f)
|
|
172
180
|
|
|
173
181
|
# Find channel in backup
|
|
174
182
|
ch_data = None
|
|
@@ -245,12 +253,138 @@ class BackupManager:
|
|
|
245
253
|
except Exception as e:
|
|
246
254
|
print(f"Error restoring permission: {e}")
|
|
247
255
|
|
|
256
|
+
# CRITICAL: Update backup with NEW ID to prevent infinite restoration loop
|
|
257
|
+
# The monitor looks for IDs in the backup. If we don't update it,
|
|
258
|
+
# it will keep seeing the old ID as "missing" and restore it again.
|
|
259
|
+
try:
|
|
260
|
+
ch_data["id"] = new_channel.id
|
|
261
|
+
|
|
262
|
+
# Update the ID in the backup list in memory
|
|
263
|
+
for i, ch in enumerate(backup["channels"]):
|
|
264
|
+
if ch["id"] == target_id:
|
|
265
|
+
backup["channels"][i] = ch_data
|
|
266
|
+
break
|
|
267
|
+
|
|
268
|
+
# Save backup file
|
|
269
|
+
async with self._get_guild_lock(guild.id):
|
|
270
|
+
with open(backup_file, 'w') as f:
|
|
271
|
+
json.dump(backup, f, indent=2)
|
|
272
|
+
|
|
273
|
+
# Update in-memory cache
|
|
274
|
+
self._channel_cache[guild.id] = backup
|
|
275
|
+
except Exception as e:
|
|
276
|
+
print(f"Error updating backup ID: {e}")
|
|
277
|
+
|
|
248
278
|
return new_channel
|
|
249
279
|
|
|
250
280
|
except Exception as e:
|
|
251
281
|
print(f"Error restoring channel: {e}")
|
|
252
282
|
return None
|
|
253
|
-
|
|
283
|
+
|
|
284
|
+
async def remove_channel_from_backup(self, guild: discord.Guild, channel_id: int):
|
|
285
|
+
"""Remove a channel from backup (used when authorized user deletes it)"""
|
|
286
|
+
try:
|
|
287
|
+
backup_file = self.backup_dir / f"channels_{guild.id}.json"
|
|
288
|
+
if not backup_file.exists():
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
with open(backup_file, 'r') as f:
|
|
292
|
+
backup = json.load(f)
|
|
293
|
+
|
|
294
|
+
# Remove channel with matching ID
|
|
295
|
+
original_len = len(backup["channels"])
|
|
296
|
+
backup["channels"] = [ch for ch in backup["channels"] if ch["id"] != channel_id]
|
|
297
|
+
|
|
298
|
+
if len(backup["channels"]) < original_len:
|
|
299
|
+
# Save changes
|
|
300
|
+
with open(backup_file, 'w') as f:
|
|
301
|
+
json.dump(backup, f, indent=2)
|
|
302
|
+
# print(f"✅ Removed authorized deletion from backup: {channel_id}")
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
print(f"Error removing channel from backup: {e}")
|
|
306
|
+
|
|
307
|
+
async def add_channel_to_backup(self, guild: discord.Guild, channel: discord.abc.GuildChannel):
|
|
308
|
+
"""Add a new channel to backup (used when authorized user creates it)"""
|
|
309
|
+
try:
|
|
310
|
+
backup_file = self.backup_dir / f"channels_{guild.id}.json"
|
|
311
|
+
if not backup_file.exists():
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
with open(backup_file, 'r') as f:
|
|
315
|
+
backup = json.load(f)
|
|
316
|
+
|
|
317
|
+
# Create channel data object
|
|
318
|
+
ch_data = {
|
|
319
|
+
"id": channel.id,
|
|
320
|
+
"name": channel.name,
|
|
321
|
+
"type": str(channel.type),
|
|
322
|
+
"position": channel.position,
|
|
323
|
+
"category_id": channel.category_id if hasattr(channel, "category_id") else None
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# Add permissions
|
|
327
|
+
perms_data = {}
|
|
328
|
+
for target, overwrite in channel.overwrites.items():
|
|
329
|
+
perms_data[str(target.id)] = {
|
|
330
|
+
"type": "role" if isinstance(target, discord.Role) else "member",
|
|
331
|
+
"allow": overwrite.pair()[0].value,
|
|
332
|
+
"deny": overwrite.pair()[1].value
|
|
333
|
+
}
|
|
334
|
+
if perms_data:
|
|
335
|
+
ch_data["permissions"] = perms_data
|
|
336
|
+
|
|
337
|
+
# Add to backup list
|
|
338
|
+
backup["channels"].append(ch_data)
|
|
339
|
+
|
|
340
|
+
# Save changes
|
|
341
|
+
with open(backup_file, 'w') as f:
|
|
342
|
+
json.dump(backup, f, indent=2)
|
|
343
|
+
# print(f"✅ Added authorized creation to backup: {channel.name}")
|
|
344
|
+
|
|
345
|
+
except Exception as e:
|
|
346
|
+
print(f"Error adding channel to backup: {e}")
|
|
347
|
+
|
|
348
|
+
async def update_channel_in_backup(self, guild: discord.Guild, channel: discord.abc.GuildChannel):
|
|
349
|
+
"""Update channel in backup (used when authorized user updates it)"""
|
|
350
|
+
try:
|
|
351
|
+
backup_file = self.backup_dir / f"channels_{guild.id}.json"
|
|
352
|
+
if not backup_file.exists():
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
with open(backup_file, 'r') as f:
|
|
356
|
+
backup = json.load(f)
|
|
357
|
+
|
|
358
|
+
# Find and update channel
|
|
359
|
+
for i, ch_data in enumerate(backup["channels"]):
|
|
360
|
+
if ch_data["id"] == channel.id:
|
|
361
|
+
# Update fields
|
|
362
|
+
ch_data["name"] = channel.name
|
|
363
|
+
ch_data["position"] = channel.position
|
|
364
|
+
ch_data["category_id"] = channel.category_id if hasattr(channel, "category_id") else None
|
|
365
|
+
|
|
366
|
+
# Update permissions
|
|
367
|
+
perms_data = {}
|
|
368
|
+
for target, overwrite in channel.overwrites.items():
|
|
369
|
+
perms_data[str(target.id)] = {
|
|
370
|
+
"type": "role" if isinstance(target, discord.Role) else "member",
|
|
371
|
+
"allow": overwrite.pair()[0].value,
|
|
372
|
+
"deny": overwrite.pair()[1].value
|
|
373
|
+
}
|
|
374
|
+
if perms_data:
|
|
375
|
+
ch_data["permissions"] = perms_data
|
|
376
|
+
|
|
377
|
+
backup["channels"][i] = ch_data
|
|
378
|
+
break
|
|
379
|
+
|
|
380
|
+
# Save changes
|
|
381
|
+
with open(backup_file, 'w') as f:
|
|
382
|
+
json.dump(backup, f, indent=2)
|
|
383
|
+
# print(f"✅ Updated authorized changes in backup: {channel.name}")
|
|
384
|
+
|
|
385
|
+
except Exception as e:
|
|
386
|
+
print(f"Error updating channel in backup: {e}")
|
|
387
|
+
|
|
254
388
|
async def restore_channel_permissions(self, guild: discord.Guild, channel_id: int) -> bool:
|
|
255
389
|
"""Restore channel permissions from backup"""
|
|
256
390
|
try:
|
|
@@ -304,8 +438,9 @@ class BackupManager:
|
|
|
304
438
|
if not backup_file.exists():
|
|
305
439
|
return False
|
|
306
440
|
|
|
307
|
-
with
|
|
308
|
-
|
|
441
|
+
async with self._get_guild_lock(guild.id):
|
|
442
|
+
with open(backup_file, 'r') as f:
|
|
443
|
+
backup = json.load(f)
|
|
309
444
|
|
|
310
445
|
# Find role in backup
|
|
311
446
|
for role_data in backup["roles"]:
|
|
@@ -320,12 +455,41 @@ class BackupManager:
|
|
|
320
455
|
reason="SecureX: Restoring deleted role"
|
|
321
456
|
)
|
|
322
457
|
|
|
323
|
-
# Position
|
|
324
|
-
|
|
458
|
+
# Immediate Position Attempt (Optional/Safe fallback)
|
|
459
|
+
try:
|
|
460
|
+
me = await guild.fetch_member(self.bot.user.id)
|
|
461
|
+
if role_data["position"] < me.top_role.position:
|
|
462
|
+
await new_role.edit(position=role_data["position"], reason="SecureX: Immediate restoration")
|
|
463
|
+
print(f"✅ Restored role: {new_role.name} at position {role_data['position']}")
|
|
464
|
+
else:
|
|
465
|
+
print(f"✅ Restored role: {new_role.name} (Position correction deferred to repair logic)")
|
|
466
|
+
except Exception as e:
|
|
467
|
+
print(f"✅ Restored role: {new_role.name} (Position error: {e})")
|
|
325
468
|
|
|
326
|
-
|
|
469
|
+
# SMART ID SWAP: Update backup file with the new ID
|
|
470
|
+
try:
|
|
471
|
+
async with self._get_guild_lock(guild.id):
|
|
472
|
+
# Re-read to ensure we don't overwrite other parallel swaps
|
|
473
|
+
with open(backup_file, 'r') as f:
|
|
474
|
+
backup = json.load(f)
|
|
475
|
+
|
|
476
|
+
role_data["id"] = new_role.id
|
|
477
|
+
for i, r in enumerate(backup["roles"]):
|
|
478
|
+
if r["name"] == new_role.name: # Robust matching
|
|
479
|
+
backup["roles"][i]["id"] = new_role.id
|
|
480
|
+
break
|
|
481
|
+
|
|
482
|
+
with open(backup_file, 'w') as f:
|
|
483
|
+
json.dump(backup, f, indent=2)
|
|
484
|
+
|
|
485
|
+
# Update in-memory cache
|
|
486
|
+
self._role_cache[guild.id] = backup
|
|
487
|
+
except Exception as e:
|
|
488
|
+
print(f"Error updating backup role ID: {e}")
|
|
489
|
+
|
|
490
|
+
return new_role
|
|
327
491
|
|
|
328
|
-
return
|
|
492
|
+
return None
|
|
329
493
|
|
|
330
494
|
except Exception as e:
|
|
331
495
|
print(f"Error restoring role: {e}")
|
|
@@ -362,7 +526,151 @@ class BackupManager:
|
|
|
362
526
|
except Exception as e:
|
|
363
527
|
print(f"Error restoring role permissions: {e}")
|
|
364
528
|
return False
|
|
365
|
-
|
|
529
|
+
|
|
530
|
+
async def repair_channel_structure(self, guild: discord.Guild):
|
|
531
|
+
"""
|
|
532
|
+
Exhaustive channel structure repair with name-based fallback.
|
|
533
|
+
"""
|
|
534
|
+
if self.sdk._repair_channel_lock.locked():
|
|
535
|
+
return
|
|
536
|
+
|
|
537
|
+
async with self.sdk._repair_channel_lock:
|
|
538
|
+
try:
|
|
539
|
+
await asyncio.sleep(2) # Debounce
|
|
540
|
+
|
|
541
|
+
# Ground truth
|
|
542
|
+
channels = await guild.fetch_channels()
|
|
543
|
+
current_channels_by_id = {c.id: c for c in channels}
|
|
544
|
+
current_channels_by_name = {(type(c), c.name): c for c in channels}
|
|
545
|
+
|
|
546
|
+
backup_file = self.backup_dir / f"channels_{guild.id}.json"
|
|
547
|
+
if not backup_file.exists(): return
|
|
548
|
+
|
|
549
|
+
async with self._get_guild_lock(guild.id):
|
|
550
|
+
with open(backup_file, 'r') as f:
|
|
551
|
+
backup = json.load(f)
|
|
552
|
+
|
|
553
|
+
positions_to_fix = {}
|
|
554
|
+
|
|
555
|
+
for ch_data in backup["channels"]:
|
|
556
|
+
channel = current_channels_by_id.get(ch_data["id"])
|
|
557
|
+
|
|
558
|
+
# Name-based fallback (for just-restored channels whose ID mapping hasn't settled)
|
|
559
|
+
if not channel:
|
|
560
|
+
# Try matching by name and type
|
|
561
|
+
for c in channels:
|
|
562
|
+
if c.name == ch_data["name"] and ch_data["type"].lower() in str(type(c)).lower():
|
|
563
|
+
channel = c
|
|
564
|
+
break
|
|
565
|
+
|
|
566
|
+
if not channel: continue
|
|
567
|
+
|
|
568
|
+
# 1. Category Repair
|
|
569
|
+
target_cat_id = ch_data.get("category_id")
|
|
570
|
+
if target_cat_id:
|
|
571
|
+
current_cat_id = channel.category.id if channel.category else None
|
|
572
|
+
if current_cat_id != target_cat_id:
|
|
573
|
+
target_cat = current_channels_by_id.get(target_cat_id)
|
|
574
|
+
if target_cat and isinstance(target_cat, discord.CategoryChannel):
|
|
575
|
+
try:
|
|
576
|
+
await channel.edit(category=target_cat, reason="SecureX: Structural Repair")
|
|
577
|
+
except: pass
|
|
578
|
+
|
|
579
|
+
# 2. Position Track
|
|
580
|
+
if channel.position != ch_data["position"]:
|
|
581
|
+
positions_to_fix[channel] = ch_data["position"]
|
|
582
|
+
|
|
583
|
+
# 3. Permissions Repair
|
|
584
|
+
if ch_data.get("permissions"):
|
|
585
|
+
if len(channel.overwrites) != len(ch_data["permissions"]):
|
|
586
|
+
await self.restore_channel_permissions(guild, channel.id)
|
|
587
|
+
|
|
588
|
+
if positions_to_fix:
|
|
589
|
+
print(f"🔧 [Repair] Fixing {len(positions_to_fix)} channel positions in {guild.name}...")
|
|
590
|
+
await guild.edit(positions=positions_to_fix, reason="SecureX: Structural Repair")
|
|
591
|
+
|
|
592
|
+
except Exception as e:
|
|
593
|
+
print(f"Error in exhaustive repair_channel_structure: {e}")
|
|
594
|
+
|
|
595
|
+
async def repair_role_structure(self, guild: discord.Guild):
|
|
596
|
+
"""
|
|
597
|
+
Exhaustive role structure repair with name-based fallback and hierarchy checks.
|
|
598
|
+
"""
|
|
599
|
+
if self.sdk._repair_role_lock.locked():
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
async with self.sdk._repair_role_lock:
|
|
603
|
+
try:
|
|
604
|
+
await asyncio.sleep(2) # Debounce
|
|
605
|
+
|
|
606
|
+
roles = await guild.fetch_roles()
|
|
607
|
+
current_roles_by_id = {r.id: r for r in roles}
|
|
608
|
+
|
|
609
|
+
backup_file = self.backup_dir / f"roles_{guild.id}.json"
|
|
610
|
+
if not backup_file.exists(): return
|
|
611
|
+
|
|
612
|
+
async with self._get_guild_lock(guild.id):
|
|
613
|
+
with open(backup_file, 'r') as f:
|
|
614
|
+
backup = json.load(f)
|
|
615
|
+
|
|
616
|
+
positions_to_fix = {}
|
|
617
|
+
# GROUND TRUTH: Force fetch member to get latest top_role
|
|
618
|
+
me = await guild.fetch_member(self.bot.user.id)
|
|
619
|
+
bot_top_role = me.top_role
|
|
620
|
+
|
|
621
|
+
for role_data in backup["roles"]:
|
|
622
|
+
role = current_roles_by_id.get(role_data["id"])
|
|
623
|
+
|
|
624
|
+
# Name-based fallback
|
|
625
|
+
if not role:
|
|
626
|
+
for r in roles:
|
|
627
|
+
if r.name == role_data["name"] and not r.is_default():
|
|
628
|
+
role = r
|
|
629
|
+
break
|
|
630
|
+
|
|
631
|
+
if not role or role.is_default():
|
|
632
|
+
continue
|
|
633
|
+
|
|
634
|
+
# Check Hierarchy Power
|
|
635
|
+
if role >= bot_top_role:
|
|
636
|
+
# Log if we can't move it because but is too low
|
|
637
|
+
# print(f"⚠️ Cannot repair role '{role.name}' - Bot hierarchy too low.")
|
|
638
|
+
continue
|
|
639
|
+
|
|
640
|
+
# 1. Permissions Fix
|
|
641
|
+
if role.permissions.value != role_data["permissions"]:
|
|
642
|
+
try:
|
|
643
|
+
await role.edit(
|
|
644
|
+
permissions=discord.Permissions(role_data["permissions"]),
|
|
645
|
+
reason="SecureX: Structural Repair"
|
|
646
|
+
)
|
|
647
|
+
except: pass
|
|
648
|
+
|
|
649
|
+
# 2. Position Track
|
|
650
|
+
if role.position != role_data["position"]:
|
|
651
|
+
# Double check we don't try to move it above the bot
|
|
652
|
+
target_pos = role_data["position"]
|
|
653
|
+
if target_pos < bot_top_role.position:
|
|
654
|
+
positions_to_fix[role] = target_pos
|
|
655
|
+
|
|
656
|
+
if positions_to_fix:
|
|
657
|
+
print(f"🔧 [Repair] Fixing {len(positions_to_fix)} role positions in {guild.name}...")
|
|
658
|
+
try:
|
|
659
|
+
await guild.edit_role_positions(
|
|
660
|
+
positions=positions_to_fix,
|
|
661
|
+
reason="SecureX: Structural Repair"
|
|
662
|
+
)
|
|
663
|
+
print(f"✅ Successfully updated {len(positions_to_fix)} role positions.")
|
|
664
|
+
except discord.Forbidden:
|
|
665
|
+
print("❌ Failed to fix role positions: 403 Forbidden (Check bot permissions and hierarchy)")
|
|
666
|
+
except Exception as e:
|
|
667
|
+
print(f"❌ Error fixing role positions: {e}")
|
|
668
|
+
|
|
669
|
+
except Exception as e:
|
|
670
|
+
print(f"Error in exhaustive repair_role_structure: {e}")
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
|
|
366
674
|
async def restore_category_children(self, guild: discord.Guild, old_category_id: int, new_category: discord.CategoryChannel):
|
|
367
675
|
"""
|
|
368
676
|
Restore all child channels that belonged to a category.
|
|
@@ -11,8 +11,6 @@ from .handlers.channel import ChannelHandler
|
|
|
11
11
|
from .handlers.role import RoleHandler
|
|
12
12
|
from .handlers.member import MemberHandler
|
|
13
13
|
from .handlers.webhook import WebhookHandler
|
|
14
|
-
from .handlers.role_monitor import RolePositionMonitor
|
|
15
|
-
from .handlers.channel_monitor import ChannelPositionMonitor
|
|
16
14
|
from .utils.whitelist import WhitelistManager
|
|
17
15
|
from .utils.punishment import PunishmentExecutor
|
|
18
16
|
from .models import ThreatEvent, BackupInfo
|
|
@@ -107,8 +105,11 @@ class SecureX:
|
|
|
107
105
|
self.role_handler = RoleHandler(self)
|
|
108
106
|
self.member_handler = MemberHandler(self)
|
|
109
107
|
self.webhook_handler = WebhookHandler(self)
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
|
|
109
|
+
# Track channels being processed to prevent event overlap logic
|
|
110
|
+
self.processing_channel_deletions = set()
|
|
111
|
+
self._repair_role_lock = asyncio.Lock()
|
|
112
|
+
self._repair_channel_lock = asyncio.Lock()
|
|
112
113
|
|
|
113
114
|
# Register instant audit log event listener (v2.0) - for PUNISHMENT
|
|
114
115
|
self._register_audit_log_listener()
|
|
@@ -410,7 +411,6 @@ class SecureX:
|
|
|
410
411
|
guild_id: Optional[int] = None,
|
|
411
412
|
whitelist: Optional[List[int]] = None,
|
|
412
413
|
auto_backup: bool = True,
|
|
413
|
-
role_position_monitoring: bool = True,
|
|
414
414
|
punishments: Optional[Dict[str, str]] = None,
|
|
415
415
|
timeout_duration: int = 600,
|
|
416
416
|
notify_user: bool = True
|
|
@@ -432,7 +432,6 @@ class SecureX:
|
|
|
432
432
|
# Preload all caches into memory
|
|
433
433
|
await self.whitelist.preload_all()
|
|
434
434
|
await self.backup_manager.preload_all()
|
|
435
|
-
await self.role_monitor.preload_all()
|
|
436
435
|
|
|
437
436
|
# Populate whitelist cache for instant O(1) lookups
|
|
438
437
|
if guild_id:
|
|
@@ -467,10 +466,6 @@ class SecureX:
|
|
|
467
466
|
# Start cache auto-refresh (every 10 minutes)
|
|
468
467
|
self.backup_manager.start_auto_refresh()
|
|
469
468
|
|
|
470
|
-
if role_position_monitoring:
|
|
471
|
-
# Start role position monitoring
|
|
472
|
-
self.role_monitor.start()
|
|
473
|
-
|
|
474
469
|
self.timeout_duration = timeout_duration
|
|
475
470
|
self.notify_punished_user = notify_user
|
|
476
471
|
|
|
@@ -45,6 +45,10 @@ class ChannelHandler:
|
|
|
45
45
|
pass
|
|
46
46
|
except Exception as e:
|
|
47
47
|
print(f"Error deleting channel: {e}")
|
|
48
|
+
else:
|
|
49
|
+
print(f"✅ Authorized creation detected: #{channel.name}")
|
|
50
|
+
# CRITICAL: Add to backup immediately so Monitor knows about it
|
|
51
|
+
await self.backup_manager.add_channel_to_backup(guild, channel)
|
|
48
52
|
break
|
|
49
53
|
|
|
50
54
|
except Exception as e:
|
|
@@ -54,6 +58,10 @@ class ChannelHandler:
|
|
|
54
58
|
async def on_channel_delete(self, channel: discord.abc.GuildChannel):
|
|
55
59
|
"""Detect and restore unauthorized channel deletion"""
|
|
56
60
|
try:
|
|
61
|
+
# COORDINATION: Inform the monitor we are processing this channel
|
|
62
|
+
# This prevents the monitor from attempting a restore until we check audit logs
|
|
63
|
+
self.sdk.processing_channel_deletions.add(channel.id)
|
|
64
|
+
|
|
57
65
|
guild = channel.guild
|
|
58
66
|
print(f"🔍 Channel deleted: {channel.name} (ID: {channel.id})")
|
|
59
67
|
await asyncio.sleep(1)
|
|
@@ -71,6 +79,10 @@ class ChannelHandler:
|
|
|
71
79
|
# Restore the channel (returns new channel object or None)
|
|
72
80
|
new_channel = await self.backup_manager.restore_channel(guild, channel)
|
|
73
81
|
|
|
82
|
+
# Use the backup manager logic to restore structure immediately
|
|
83
|
+
if new_channel:
|
|
84
|
+
await self.backup_manager.repair_channel_structure(guild)
|
|
85
|
+
|
|
74
86
|
if new_channel:
|
|
75
87
|
print(f"✅ Restored: {new_channel.name}")
|
|
76
88
|
|
|
@@ -83,11 +95,19 @@ class ChannelHandler:
|
|
|
83
95
|
print(f"❌ Failed to restore channel: {channel.name} (not found in backup?)")
|
|
84
96
|
else:
|
|
85
97
|
print(f"⏭️ Skipping restoration - user is authorized")
|
|
98
|
+
# CRITICAL: Remove from backup so Monitor doesn't restore it
|
|
99
|
+
await self.backup_manager.remove_channel_from_backup(guild, channel.id)
|
|
86
100
|
break
|
|
87
101
|
else:
|
|
88
102
|
print(f"⚠️ Audit entry target mismatch: {entry.target.id} != {channel.id}")
|
|
103
|
+
|
|
89
104
|
except Exception as e:
|
|
90
105
|
print(f"Error in channel_delete handler: {e}")
|
|
106
|
+
finally:
|
|
107
|
+
# Always remove from processing set
|
|
108
|
+
if hasattr(self.sdk, 'processing_channel_deletions'):
|
|
109
|
+
self.sdk.processing_channel_deletions.discard(channel.id)
|
|
110
|
+
|
|
91
111
|
|
|
92
112
|
async def on_channel_update(self, before: discord.abc.GuildChannel, after: discord.abc.GuildChannel):
|
|
93
113
|
"""Detect unauthorized channel updates"""
|
|
@@ -108,6 +128,10 @@ class ChannelHandler:
|
|
|
108
128
|
# Restore channel permissions
|
|
109
129
|
restored = await self.backup_manager.restore_channel_permissions(guild, after.id)
|
|
110
130
|
|
|
131
|
+
# Use the backup manager logic to restore structure immediately
|
|
132
|
+
if restored:
|
|
133
|
+
await self.backup_manager.repair_channel_structure(guild)
|
|
134
|
+
|
|
111
135
|
# Create threat event
|
|
112
136
|
threat = ThreatEvent(
|
|
113
137
|
type="channel_update",
|
|
@@ -122,6 +146,10 @@ class ChannelHandler:
|
|
|
122
146
|
|
|
123
147
|
# Trigger callback
|
|
124
148
|
await self.sdk._trigger_callbacks('threat_detected', threat)
|
|
149
|
+
else:
|
|
150
|
+
# Authorized update - Update backup!
|
|
151
|
+
print(f"✅ Authorized update detected: #{after.name}")
|
|
152
|
+
await self.backup_manager.update_channel_in_backup(guild, after)
|
|
125
153
|
break
|
|
126
154
|
except Exception as e:
|
|
127
155
|
print(f"Error in channel_update handler: {e}")
|
|
@@ -62,6 +62,10 @@ class RoleHandler:
|
|
|
62
62
|
# Restore role from backup
|
|
63
63
|
restored = await self.backup_manager.restore_role(guild, role.id)
|
|
64
64
|
|
|
65
|
+
# Use the backup manager logic to restore structure immediately
|
|
66
|
+
if restored:
|
|
67
|
+
await self.backup_manager.repair_role_structure(guild)
|
|
68
|
+
|
|
65
69
|
threat = ThreatEvent(
|
|
66
70
|
type="role_delete",
|
|
67
71
|
guild_id=guild.id,
|
|
@@ -94,6 +98,10 @@ class RoleHandler:
|
|
|
94
98
|
# Restore role to backup state
|
|
95
99
|
restored = await self.backup_manager.restore_role_permissions(guild, after.id)
|
|
96
100
|
|
|
101
|
+
# Use the backup manager logic to restore structure immediately
|
|
102
|
+
if restored:
|
|
103
|
+
await self.backup_manager.repair_role_structure(guild)
|
|
104
|
+
|
|
97
105
|
threat = ThreatEvent(
|
|
98
106
|
type="role_update",
|
|
99
107
|
guild_id=guild.id,
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="dc-securex",
|
|
8
|
-
version="2.
|
|
8
|
+
version="2.9.6",
|
|
9
9
|
author="SecureX Team",
|
|
10
10
|
author_email="contact@securex.dev",
|
|
11
11
|
description="Backend-only Discord anti-nuke protection SDK - Build your own UI!",
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Channel position and permission monitoring - background task.
|
|
3
|
-
"""
|
|
4
|
-
import discord
|
|
5
|
-
import asyncio
|
|
6
|
-
import json
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import List, Tuple, Dict, Any, Union
|
|
9
|
-
|
|
10
|
-
class ChannelPositionMonitor:
|
|
11
|
-
"""
|
|
12
|
-
Background monitor that checks and restores channel positions, permissions,
|
|
13
|
-
and missing channels every 300ms.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
def __init__(self, sdk):
|
|
17
|
-
self.sdk = sdk
|
|
18
|
-
self.bot = sdk.bot
|
|
19
|
-
self.backup_dir = sdk.backup_dir
|
|
20
|
-
self.enabled = False
|
|
21
|
-
self._task = None
|
|
22
|
-
|
|
23
|
-
def start(self):
|
|
24
|
-
"""Start the channel monitoring task"""
|
|
25
|
-
if not self.enabled:
|
|
26
|
-
self.enabled = True
|
|
27
|
-
self._task = self.bot.loop.create_task(self._monitor_loop())
|
|
28
|
-
print("🔍 Channel Monitor: Started (300ms checks)")
|
|
29
|
-
|
|
30
|
-
def stop(self):
|
|
31
|
-
"""Stop the monitoring task"""
|
|
32
|
-
if self.enabled and self._task:
|
|
33
|
-
self.enabled = False
|
|
34
|
-
self._task.cancel()
|
|
35
|
-
print("🛑 Channel Monitor: Stopped")
|
|
36
|
-
|
|
37
|
-
async def _monitor_loop(self):
|
|
38
|
-
"""Main monitoring loop - runs every 300ms"""
|
|
39
|
-
await self.bot.wait_until_ready()
|
|
40
|
-
|
|
41
|
-
while self.enabled and not self.bot.is_closed():
|
|
42
|
-
try:
|
|
43
|
-
await self._check_all_guilds()
|
|
44
|
-
await asyncio.sleep(0.3)
|
|
45
|
-
except asyncio.CancelledError:
|
|
46
|
-
break
|
|
47
|
-
except Exception as e:
|
|
48
|
-
print(f"Error in channel monitor: {e}")
|
|
49
|
-
await asyncio.sleep(0.3)
|
|
50
|
-
|
|
51
|
-
async def _check_all_guilds(self):
|
|
52
|
-
"""Check channels for all guilds"""
|
|
53
|
-
for guild in self.bot.guilds:
|
|
54
|
-
try:
|
|
55
|
-
await self._check_guild_channels(guild)
|
|
56
|
-
except Exception as e:
|
|
57
|
-
print(f"Error checking channels for {guild.name}: {e}")
|
|
58
|
-
|
|
59
|
-
async def _check_guild_channels(self, guild: discord.Guild):
|
|
60
|
-
"""Check and restore channel positions/permissions/missing channels"""
|
|
61
|
-
try:
|
|
62
|
-
# Get backup file
|
|
63
|
-
backup_file = self.backup_dir / f"channels_{guild.id}.json"
|
|
64
|
-
if not backup_file.exists():
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
with open(backup_file, 'r') as f:
|
|
68
|
-
backup = json.load(f)
|
|
69
|
-
|
|
70
|
-
# Build current snapshot {channel_id: channel_obj}
|
|
71
|
-
current_channels = {c.id: c for c in guild.channels}
|
|
72
|
-
|
|
73
|
-
# Iterate through BACKUP channels (the source of truth)
|
|
74
|
-
for ch_data in backup["channels"]:
|
|
75
|
-
channel_id = ch_data["id"]
|
|
76
|
-
channel = current_channels.get(channel_id)
|
|
77
|
-
|
|
78
|
-
# 1. Missing Channel Detection
|
|
79
|
-
if not channel:
|
|
80
|
-
# Channel exists in backup but not in guild -> RESTORE IT!
|
|
81
|
-
# print(f"⚠️ Missing channel found in backup: {ch_data['name']} ({channel_id})")
|
|
82
|
-
try:
|
|
83
|
-
# Use the new restore_channel logic that accepts ID
|
|
84
|
-
await self.sdk.backup_manager.restore_channel(guild, channel_id)
|
|
85
|
-
# Slightly longer sleep on create to prevent rate limits
|
|
86
|
-
await asyncio.sleep(0.1)
|
|
87
|
-
except Exception as e:
|
|
88
|
-
print(f"Error restoring missing channel {channel_id}: {e}")
|
|
89
|
-
continue
|
|
90
|
-
|
|
91
|
-
# 2. Check Position
|
|
92
|
-
if channel.position != ch_data["position"]:
|
|
93
|
-
try:
|
|
94
|
-
await channel.edit(position=ch_data["position"], reason="SecureX: Auto-restoring position")
|
|
95
|
-
# print(f"🔧 Fixed position for #{channel.name}")
|
|
96
|
-
except Exception:
|
|
97
|
-
pass
|
|
98
|
-
|
|
99
|
-
# 3. Check Category
|
|
100
|
-
expected_cat_id = ch_data.get("category_id")
|
|
101
|
-
current_cat_id = channel.category.id if channel.category else None
|
|
102
|
-
|
|
103
|
-
if expected_cat_id != current_cat_id:
|
|
104
|
-
try:
|
|
105
|
-
if expected_cat_id:
|
|
106
|
-
category = guild.get_channel(expected_cat_id)
|
|
107
|
-
if category and isinstance(category, discord.CategoryChannel):
|
|
108
|
-
await channel.edit(category=category, reason="SecureX: Auto-restoring category")
|
|
109
|
-
else:
|
|
110
|
-
# Should be None (no category)
|
|
111
|
-
await channel.edit(category=None, reason="SecureX: Auto-restoring category")
|
|
112
|
-
except Exception:
|
|
113
|
-
pass
|
|
114
|
-
|
|
115
|
-
# 4. Check Permissions (Simplified)
|
|
116
|
-
# We check if the number of overwrites matches. Deep comparison is too heavy for 300ms.
|
|
117
|
-
# If they differ, we restore permissions.
|
|
118
|
-
# Note: Backup might have simplified perms, so this is a heuristic.
|
|
119
|
-
backup_perms = ch_data.get("permissions", {})
|
|
120
|
-
if len(channel.overwrites) != len(backup_perms):
|
|
121
|
-
try:
|
|
122
|
-
await self.sdk.backup_manager.restore_channel_permissions(guild, channel.id)
|
|
123
|
-
except Exception:
|
|
124
|
-
pass
|
|
125
|
-
|
|
126
|
-
except Exception as e:
|
|
127
|
-
# print(f"Error checking guild {guild.id}: {e}")
|
|
128
|
-
pass
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Role position monitoring - background task that checks every 5 seconds.
|
|
3
|
-
"""
|
|
4
|
-
import discord
|
|
5
|
-
import asyncio
|
|
6
|
-
import json
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import List, Tuple
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class RolePositionMonitor:
|
|
12
|
-
"""
|
|
13
|
-
Background monitor that checks and restores role positions every 5 seconds.
|
|
14
|
-
Uses Discord's bulk endpoint for efficient updates.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
def __init__(self, sdk):
|
|
18
|
-
self.sdk = sdk
|
|
19
|
-
self.bot = sdk.bot
|
|
20
|
-
self.backup_dir = sdk.backup_dir
|
|
21
|
-
self.enabled = False
|
|
22
|
-
self._task = None
|
|
23
|
-
|
|
24
|
-
# In-memory cache for role positions (instant lookups)
|
|
25
|
-
self._role_cache: dict = {} # {guild_id: {role_id: position}}
|
|
26
|
-
|
|
27
|
-
def start(self):
|
|
28
|
-
"""Start the role position monitoring task"""
|
|
29
|
-
if not self.enabled:
|
|
30
|
-
self.enabled = True
|
|
31
|
-
self._task = self.bot.loop.create_task(self._monitor_loop())
|
|
32
|
-
print("🔍 Role Position Monitor: Started (300ms checks)")
|
|
33
|
-
|
|
34
|
-
def stop(self):
|
|
35
|
-
"""Stop the monitoring task"""
|
|
36
|
-
if self.enabled and self._task:
|
|
37
|
-
self.enabled = False
|
|
38
|
-
self._task.cancel()
|
|
39
|
-
print("🛑 Role Position Monitor: Stopped")
|
|
40
|
-
|
|
41
|
-
async def _monitor_loop(self):
|
|
42
|
-
"""Main monitoring loop - runs every 300ms"""
|
|
43
|
-
await self.bot.wait_until_ready()
|
|
44
|
-
|
|
45
|
-
while self.enabled and not self.bot.is_closed():
|
|
46
|
-
try:
|
|
47
|
-
await self._check_all_guilds()
|
|
48
|
-
await asyncio.sleep(0.3) # Check every 300ms
|
|
49
|
-
except asyncio.CancelledError:
|
|
50
|
-
break
|
|
51
|
-
except Exception as e:
|
|
52
|
-
print(f"Error in role position monitor: {e}")
|
|
53
|
-
await asyncio.sleep(0.3)
|
|
54
|
-
|
|
55
|
-
async def _check_all_guilds(self):
|
|
56
|
-
"""Check role positions for all guilds"""
|
|
57
|
-
for guild in self.bot.guilds:
|
|
58
|
-
try:
|
|
59
|
-
await self._check_guild_positions(guild)
|
|
60
|
-
except Exception as e:
|
|
61
|
-
print(f"Error checking positions for {guild.name}: {e}")
|
|
62
|
-
|
|
63
|
-
async def _check_guild_positions(self, guild: discord.Guild):
|
|
64
|
-
"""Check and restore role positions for a specific guild"""
|
|
65
|
-
try:
|
|
66
|
-
# Get backup file
|
|
67
|
-
backup_file = self.backup_dir / f"roles_{guild.id}.json"
|
|
68
|
-
if not backup_file.exists():
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
with open(backup_file, 'r') as f:
|
|
72
|
-
backup = json.load(f)
|
|
73
|
-
|
|
74
|
-
# Use guild.roles snapshot for efficient lookup
|
|
75
|
-
# Build a dict: {role_id: role_object} from cached roles
|
|
76
|
-
current_roles = {role.id: role for role in guild.roles}
|
|
77
|
-
|
|
78
|
-
# Collect all mismatched roles
|
|
79
|
-
role_position_pairs: List[Tuple[discord.Role, int]] = []
|
|
80
|
-
|
|
81
|
-
for role_data in backup["roles"]:
|
|
82
|
-
role = current_roles.get(role_data["id"])
|
|
83
|
-
if not role:
|
|
84
|
-
continue # Role was deleted
|
|
85
|
-
|
|
86
|
-
# Check if position matches
|
|
87
|
-
if role.position != role_data["position"]:
|
|
88
|
-
role_position_pairs.append((role, role_data["position"]))
|
|
89
|
-
|
|
90
|
-
# If we found mismatches, restore using bulk endpoint
|
|
91
|
-
if role_position_pairs:
|
|
92
|
-
print(f"🔧 Restoring {len(role_position_pairs)} role positions in {guild.name}")
|
|
93
|
-
|
|
94
|
-
try:
|
|
95
|
-
# Build payload for bulk update
|
|
96
|
-
# Discord API expects: {role_object: new_position}
|
|
97
|
-
positions_dict = {
|
|
98
|
-
role: expected_pos
|
|
99
|
-
for role, expected_pos in role_position_pairs
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
# Use Discord's bulk role positions endpoint
|
|
103
|
-
await guild.edit_role_positions(
|
|
104
|
-
positions=positions_dict,
|
|
105
|
-
reason="SecureX: Bulk position restore"
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
print(f"✅ Restored {len(role_position_pairs)} role positions in {guild.name}")
|
|
109
|
-
|
|
110
|
-
except discord.Forbidden as e:
|
|
111
|
-
print(f"⚠️ Missing permissions to modify role positions: {e}")
|
|
112
|
-
except Exception as e:
|
|
113
|
-
print(f"Error restoring role positions: {e}")
|
|
114
|
-
# Fallback to individual updates
|
|
115
|
-
await self._fallback_individual_updates(guild, role_position_pairs)
|
|
116
|
-
|
|
117
|
-
except Exception as e:
|
|
118
|
-
print(f"Error in _check_guild_positions: {e}")
|
|
119
|
-
|
|
120
|
-
async def _fallback_individual_updates(self, guild: discord.Guild, role_position_pairs: List[Tuple[discord.Role, int]]):
|
|
121
|
-
"""Fallback to individual role updates if bulk fails"""
|
|
122
|
-
print(f"⚠️ Falling back to individual updates for {len(role_position_pairs)} roles")
|
|
123
|
-
|
|
124
|
-
for role, expected_pos in role_position_pairs:
|
|
125
|
-
try:
|
|
126
|
-
await role.edit(position=expected_pos, reason="SecureX: Position restore")
|
|
127
|
-
await asyncio.sleep(0.5) # Rate limit
|
|
128
|
-
except Exception as e:
|
|
129
|
-
print(f"Error updating {role.name}: {e}")
|
|
130
|
-
|
|
131
|
-
async def _load_guild_cache(self, guild_id: int):
|
|
132
|
-
"""Load role positions from backup into cache"""
|
|
133
|
-
try:
|
|
134
|
-
backup_file = self.backup_dir / f"roles_{guild_id}.json"
|
|
135
|
-
if backup_file.exists():
|
|
136
|
-
with open(backup_file, 'r') as f:
|
|
137
|
-
backup = json.load(f)
|
|
138
|
-
|
|
139
|
-
# Store positions in cache: {role_id: position}
|
|
140
|
-
self._role_cache[guild_id] = {
|
|
141
|
-
role_data["id"]: role_data["position"]
|
|
142
|
-
for role_data in backup.get("roles", [])
|
|
143
|
-
}
|
|
144
|
-
except Exception as e:
|
|
145
|
-
print(f"Error loading role cache for guild {guild_id}: {e}")
|
|
146
|
-
|
|
147
|
-
async def preload_all(self):
|
|
148
|
-
"""Preload all role positions into cache on startup"""
|
|
149
|
-
print("🔄 Preloading role positions into cache...")
|
|
150
|
-
count = 0
|
|
151
|
-
|
|
152
|
-
for backup_file in self.backup_dir.glob("roles_*.json"):
|
|
153
|
-
try:
|
|
154
|
-
guild_id = int(backup_file.stem.replace("roles_", ""))
|
|
155
|
-
await self._load_guild_cache(guild_id)
|
|
156
|
-
count += 1
|
|
157
|
-
except Exception as e:
|
|
158
|
-
print(f"Error preloading role backup {backup_file}: {e}")
|
|
159
|
-
|
|
160
|
-
print(f"✅ Cached role positions for {count} guilds")
|
|
161
|
-
|
|
162
|
-
def update_cache(self, guild_id: int):
|
|
163
|
-
"""Update cache after role backup (sync wrapper for async method)"""
|
|
164
|
-
asyncio.create_task(self._load_guild_cache(guild_id))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/__pycache__/role_monitor.cpython-312.pyc
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|