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.
Files changed (52) hide show
  1. {dc_securex-2.5.3 → dc_securex-2.9.6}/CHANGELOG.md +130 -0
  2. {dc_securex-2.5.3 → dc_securex-2.9.6}/PKG-INFO +1 -1
  3. {dc_securex-2.5.3 → dc_securex-2.9.6}/dc_securex.egg-info/PKG-INFO +1 -1
  4. {dc_securex-2.5.3 → dc_securex-2.9.6}/dc_securex.egg-info/SOURCES.txt +1 -2
  5. {dc_securex-2.5.3 → dc_securex-2.9.6}/pyproject.toml +1 -1
  6. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/backup/manager.py +318 -10
  7. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/client.py +5 -10
  8. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/channel.py +28 -0
  9. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/role.py +8 -0
  10. {dc_securex-2.5.3 → dc_securex-2.9.6}/setup.py +1 -1
  11. dc_securex-2.5.3/securex/handlers/channel_monitor.py +0 -128
  12. dc_securex-2.5.3/securex/handlers/role_monitor.py +0 -164
  13. {dc_securex-2.5.3 → dc_securex-2.9.6}/.DS_Store +0 -0
  14. {dc_securex-2.5.3 → dc_securex-2.9.6}/LICENSE +0 -0
  15. {dc_securex-2.5.3 → dc_securex-2.9.6}/MANIFEST.in +0 -0
  16. {dc_securex-2.5.3 → dc_securex-2.9.6}/README.md +0 -0
  17. {dc_securex-2.5.3 → dc_securex-2.9.6}/dc_securex.egg-info/dependency_links.txt +0 -0
  18. {dc_securex-2.5.3 → dc_securex-2.9.6}/dc_securex.egg-info/requires.txt +0 -0
  19. {dc_securex-2.5.3 → dc_securex-2.9.6}/dc_securex.egg-info/top_level.txt +0 -0
  20. {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/advanced_usage.py +0 -0
  21. {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/backup_management.py +0 -0
  22. {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/basic_usage.py +0 -0
  23. {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/channel_protection.py +0 -0
  24. {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/member_protection.py +0 -0
  25. {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/punishment_config.py +0 -0
  26. {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/role_protection.py +0 -0
  27. {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/webhook_protection.py +0 -0
  28. {dc_securex-2.5.3 → dc_securex-2.9.6}/examples/whitelist_management.py +0 -0
  29. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/__init__.py +0 -0
  30. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/__pycache__/__init__.cpython-312.pyc +0 -0
  31. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/__pycache__/client.cpython-312.pyc +0 -0
  32. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/__pycache__/models.cpython-312.pyc +0 -0
  33. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/backup/__init__.py +0 -0
  34. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/backup/__pycache__/__init__.cpython-312.pyc +0 -0
  35. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/backup/__pycache__/manager.cpython-312.pyc +0 -0
  36. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/__init__.py +0 -0
  37. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/__pycache__/__init__.cpython-312.pyc +0 -0
  38. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/__pycache__/channel.cpython-312.pyc +0 -0
  39. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/__pycache__/member.cpython-312.pyc +0 -0
  40. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/__pycache__/role.cpython-312.pyc +0 -0
  41. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/__pycache__/role_monitor.cpython-312.pyc +0 -0
  42. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/__pycache__/webhook.cpython-312.pyc +0 -0
  43. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/member.py +0 -0
  44. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/handlers/webhook.py +0 -0
  45. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/models.py +0 -0
  46. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/utils/__init__.py +0 -0
  47. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  48. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/utils/__pycache__/punishment.cpython-312.pyc +0 -0
  49. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/utils/__pycache__/whitelist.cpython-312.pyc +0 -0
  50. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/utils/punishment.py +0 -0
  51. {dc_securex-2.5.3 → dc_securex-2.9.6}/securex/utils/whitelist.py +0 -0
  52. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dc-securex
3
- Version: 2.5.3
3
+ Version: 2.9.6
4
4
  Summary: Backend-only Discord anti-nuke protection SDK
5
5
  Home-page: https://github.com/yourusername/securex-antinuke-sdk
6
6
  Author: SecureX Team
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dc-securex
3
- Version: 2.5.3
3
+ Version: 2.9.6
4
4
  Summary: Backend-only Discord anti-nuke protection SDK
5
5
  Home-page: https://github.com/yourusername/securex-antinuke-sdk
6
6
  Author: SecureX Team
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dc-securex"
7
- version = "2.5.3"
7
+ version = "2.9.6"
8
8
  description = "Backend-only Discord anti-nuke protection SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -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 open(backup_file, 'r') as f:
171
- backup = json.load(f)
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 open(backup_file, 'r') as f:
308
- backup = json.load(f)
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 will be corrected by RolePositionMonitor within 300ms
324
- print(f"✅ Restored role: {new_role.name} (position will be auto-corrected)")
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
- return True
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 False
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
- self.role_monitor = RolePositionMonitor(self)
111
- self.channel_monitor = ChannelPositionMonitor(self)
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.5.3",
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