dc-securex 2.29.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,792 @@
1
+ """
2
+ Complete backup and restoration manager.
3
+ Extracted from cogs/antinuke.py backup logic.
4
+ """
5
+ import discord
6
+ import json
7
+ import asyncio
8
+ from pathlib import Path
9
+ from datetime import datetime, timezone
10
+ from typing import Optional, Dict, List, Union
11
+ from ..models import BackupInfo, RestoreResult
12
+
13
+
14
+ class BackupManager:
15
+ """Manages server backups and restoration"""
16
+
17
+ def __init__(self, sdk):
18
+ self.sdk = sdk
19
+ self.bot = sdk.bot
20
+ self.backup_dir = sdk.backup_dir
21
+ self.backup_dir.mkdir(parents=True, exist_ok=True)
22
+
23
+
24
+ self._channel_cache: Dict[int, Dict] = {}
25
+ self._role_cache: Dict[int, Dict] = {}
26
+ self._guild_cache: Dict[int, Dict] = {}
27
+ self._cache_loaded = False
28
+ self._refresh_task = None
29
+ self._guild_locks: Dict[int, asyncio.Lock] = {}
30
+
31
+ def _get_guild_lock(self, guild_id: int) -> asyncio.Lock:
32
+ """Get or create an async lock for a specific guild"""
33
+ if guild_id not in self._guild_locks:
34
+ self._guild_locks[guild_id] = asyncio.Lock()
35
+ return self._guild_locks[guild_id]
36
+
37
+ async def create_backup(self, guild_id: int) -> BackupInfo:
38
+ """Create complete backup for a guild"""
39
+ try:
40
+ guild = self.bot.get_guild(guild_id)
41
+ if not guild:
42
+ return BackupInfo(
43
+ guild_id=guild_id,
44
+ timestamp=datetime.now(timezone.utc),
45
+ channel_count=0,
46
+ role_count=0,
47
+ backup_path="",
48
+ success=False
49
+ )
50
+
51
+
52
+ channel_count = await self._backup_channels(guild)
53
+
54
+
55
+ role_count = await self._backup_roles(guild)
56
+
57
+
58
+ await self._backup_guild_settings(guild)
59
+
60
+ backup_info = BackupInfo(
61
+ guild_id=guild_id,
62
+ timestamp=datetime.now(timezone.utc),
63
+ channel_count=channel_count,
64
+ role_count=role_count,
65
+ backup_path=str(self.backup_dir),
66
+ success=True
67
+ )
68
+
69
+
70
+ await self._update_guild_cache(guild_id)
71
+
72
+
73
+ await self.sdk._trigger_callbacks('backup_completed', backup_info)
74
+
75
+ return backup_info
76
+
77
+ except Exception as e:
78
+ print(f"Error creating backup: {e}")
79
+ return BackupInfo(
80
+ guild_id=guild_id,
81
+ timestamp=datetime.now(timezone.utc),
82
+ channel_count=0,
83
+ role_count=0,
84
+ backup_path="",
85
+ success=False
86
+ )
87
+
88
+ async def _backup_channels(self, guild: discord.Guild) -> int:
89
+ """Backup all channels"""
90
+ backup_data = {
91
+ "guild_id": guild.id,
92
+ "timestamp": datetime.now(timezone.utc).isoformat(),
93
+ "channels": []
94
+ }
95
+
96
+ for channel in guild.channels:
97
+ channel_data = {
98
+ "id": channel.id,
99
+ "name": channel.name,
100
+ "type": str(channel.type),
101
+ "position": channel.position,
102
+ "category_id": channel.category.id if channel.category else None,
103
+ "permissions": {}
104
+ }
105
+
106
+
107
+ for target, overwrite in channel.overwrites.items():
108
+ target_id = str(target.id)
109
+ channel_data["permissions"][target_id] = {
110
+ "type": "role" if isinstance(target, discord.Role) else "member",
111
+ "allow": overwrite.pair()[0].value,
112
+ "deny": overwrite.pair()[1].value
113
+ }
114
+ # Type-specific backup data
115
+ if channel.type == discord.ChannelType.text:
116
+ channel_data["text_properties"] = {
117
+ "topic": getattr(channel, 'topic', None),
118
+ "slowmode_delay": getattr(channel, 'slowmode_delay', 0),
119
+ "nsfw": getattr(channel, 'nsfw', False)
120
+ }
121
+ elif channel.type == discord.ChannelType.voice:
122
+ channel_data["voice_properties"] = {
123
+ "bitrate": getattr(channel, 'bitrate', 64000),
124
+ "user_limit": getattr(channel, 'user_limit', 0)
125
+ }
126
+ elif channel.type == discord.ChannelType.stage_voice:
127
+ channel_data["stage_properties"] = {
128
+ "bitrate": getattr(channel, 'bitrate', 64000)
129
+ }
130
+
131
+ backup_data["channels"].append(channel_data)
132
+
133
+
134
+ backup_file = self.backup_dir / f"channels_{guild.id}.json"
135
+ with open(backup_file, 'w') as f:
136
+ json.dump(backup_data, f, indent=2)
137
+
138
+ return len(backup_data["channels"])
139
+
140
+ async def _backup_roles(self, guild: discord.Guild) -> int:
141
+ """Backup all roles using fetch_roles() for correct ordering"""
142
+ backup_data = {
143
+ "guild_id": guild.id,
144
+ "timestamp": datetime.now(timezone.utc).isoformat(),
145
+ "roles": []
146
+ }
147
+
148
+
149
+ for role in guild.roles:
150
+ if role.is_default():
151
+ continue
152
+
153
+ role_data = {
154
+ "id": role.id,
155
+ "name": role.name,
156
+ "permissions": role.permissions.value,
157
+ "color": role.color.value,
158
+ "hoist": role.hoist,
159
+ "mentionable": role.mentionable,
160
+ "position": role.position
161
+ }
162
+
163
+ backup_data["roles"].append(role_data)
164
+
165
+
166
+ backup_file = self.backup_dir / f"roles_{guild.id}.json"
167
+ with open(backup_file, 'w') as f:
168
+ json.dump(backup_data, f, indent=2)
169
+
170
+ return len(backup_data["roles"])
171
+
172
+ async def restore_category_children(self, guild: discord.Guild, old_category_id: int, new_category: discord.CategoryChannel):
173
+ """
174
+ Restore all child channels that belonged to a category.
175
+ Called after a category is restored to recreate its children.
176
+
177
+ Args:
178
+ guild: The guild containing the category
179
+ old_category_id: The ID of the deleted category (from backup)
180
+ new_category: The newly created category object (to set as parent)
181
+ """
182
+ try:
183
+ backup_file = self.backup_dir / f"channels_{guild.id}.json"
184
+ if not backup_file.exists():
185
+ print(f"No channel backup found for guild {guild.id}")
186
+ return
187
+
188
+ with open(backup_file, 'r') as f:
189
+ backup = json.load(f)
190
+
191
+
192
+ child_channels = [
193
+ ch for ch in backup["channels"]
194
+ if ch.get("category_id") == old_category_id
195
+ ]
196
+
197
+ if not child_channels:
198
+ print(f"No child channels found for category {new_category.name} (old ID: {old_category_id})")
199
+ return
200
+
201
+
202
+ child_channels = sorted(child_channels, key=lambda x: x.get("position", 0))
203
+
204
+ print(f"Restoring {len(child_channels)} child channels for category: {new_category.name}")
205
+ print(f" Creating in sorted order (positions: {[ch.get('position') for ch in child_channels]})")
206
+
207
+
208
+ for ch_data in child_channels:
209
+ try:
210
+
211
+ existing = guild.get_channel(ch_data["id"])
212
+ if existing:
213
+
214
+ if existing.category != new_category:
215
+
216
+ await existing.edit(category=new_category, reason="SecureX: Move orphaned channel to restored category")
217
+ print(f"✅ Moved existing channel to category: {existing.name}")
218
+ else:
219
+ print(f"Channel {ch_data['name']} already in correct category, skipping")
220
+ continue
221
+
222
+
223
+ channel_type_str = ch_data["type"]
224
+
225
+
226
+ overwrites = {}
227
+ for target_id, perm_data in ch_data.get("permissions", {}).items():
228
+ try:
229
+ target_id = int(target_id)
230
+ if perm_data["type"] == "role":
231
+ target = guild.get_role(target_id)
232
+ else:
233
+ target = guild.get_member(target_id)
234
+
235
+ if target:
236
+ overwrites[target] = discord.PermissionOverwrite.from_pair(
237
+ discord.Permissions(perm_data["allow"]),
238
+ discord.Permissions(perm_data["deny"])
239
+ )
240
+ except Exception as e:
241
+ print(f"Error preparing permissions for {target_id}: {e}")
242
+
243
+
244
+ new_channel = None
245
+
246
+ if "text" in channel_type_str.lower():
247
+
248
+ props = ch_data.get("text_properties", {})
249
+ new_channel = await guild.create_text_channel(
250
+ name=ch_data["name"],
251
+ category=new_category,
252
+ topic=props.get("topic"),
253
+ slowmode_delay=props.get("slowmode_delay", 0),
254
+ nsfw=props.get("nsfw", False),
255
+ overwrites=overwrites,
256
+ reason="SecureX: Restore category child"
257
+ )
258
+
259
+ elif "voice" in channel_type_str.lower():
260
+
261
+ props = ch_data.get("voice_properties", {})
262
+ new_channel = await guild.create_voice_channel(
263
+ name=ch_data["name"],
264
+ category=new_category,
265
+ bitrate=props.get("bitrate", 64000),
266
+ user_limit=props.get("user_limit", 0),
267
+ overwrites=overwrites,
268
+ reason="SecureX: Restore category child"
269
+ )
270
+
271
+ elif "stage" in channel_type_str.lower():
272
+
273
+ props = ch_data.get("stage_properties", {})
274
+ new_channel = await guild.create_stage_channel(
275
+ name=ch_data["name"],
276
+ category=new_category,
277
+ overwrites=overwrites,
278
+ reason="SecureX: Restore category child"
279
+ )
280
+
281
+ if new_channel:
282
+ print(f"✅ Restored child channel: {new_channel.name} (auto-position: {new_channel.position})")
283
+
284
+
285
+ await asyncio.sleep(0.3)
286
+
287
+ except Exception as e:
288
+ print(f"Error restoring child channel {ch_data.get('name')}: {e}")
289
+
290
+
291
+
292
+ print(f"📝 Updating backup with new category ID: {old_category_id} → {new_category.id}")
293
+
294
+
295
+ for ch_data in backup["channels"]:
296
+ if ch_data.get("category_id") == old_category_id:
297
+
298
+ ch_data["category_id"] = new_category.id
299
+
300
+
301
+ existing = guild.get_channel(ch_data["id"])
302
+ if existing and existing.category == new_category:
303
+
304
+ ch_data["id"] = existing.id
305
+
306
+
307
+ with open(backup_file, 'w') as f:
308
+ json.dump(backup, f, indent=2)
309
+
310
+
311
+ if guild.id in self._channel_cache:
312
+ self._channel_cache[guild.id] = backup
313
+
314
+ print(f"✅ Backup updated - child channels now reference category {new_category.id}")
315
+ print(f"✅ Finished restoring children for category: {new_category.name}")
316
+
317
+ except Exception as e:
318
+ print(f"Error in restore_category_children: {e}")
319
+
320
+ def start_auto_backup(self, interval_minutes: int = 30):
321
+ """Start automatic backup task"""
322
+ async def auto_backup_task():
323
+ await self.bot.wait_until_ready()
324
+ while not self.bot.is_closed():
325
+ try:
326
+
327
+ for guild in self.bot.guilds:
328
+ await self.create_backup(guild.id)
329
+
330
+
331
+ await asyncio.sleep(interval_minutes * 60)
332
+ except Exception as e:
333
+ print(f"Error in auto backup: {e}")
334
+ await asyncio.sleep(60)
335
+
336
+
337
+ self.bot.loop.create_task(auto_backup_task())
338
+
339
+
340
+
341
+ async def restore_channel(self, guild: discord.Guild, channel: discord.abc.GuildChannel) -> Optional[discord.abc.GuildChannel]:
342
+ """Restore a deleted channel from backup"""
343
+ try:
344
+ print(f"🛠️ [Debug] Attempting to restore channel: {channel.name} ({channel.id})")
345
+
346
+ backup = self._channel_cache.get(guild.id)
347
+ if not backup:
348
+ print(f"📂 [Debug] Cache miss for guild {guild.id}. Loading from file...")
349
+
350
+ backup_file = self.backup_dir / f"channels_{guild.id}.json"
351
+ if not backup_file.exists():
352
+ print(f"❌ [Debug] Backup file not found: {backup_file}")
353
+ return None
354
+
355
+ with open(backup_file, 'r') as f:
356
+ backup = json.load(f)
357
+ self._channel_cache[guild.id] = backup
358
+
359
+
360
+ ch_data = None
361
+ channel_index = None
362
+ for i, data in enumerate(backup["channels"]):
363
+ if data["id"] == channel.id:
364
+ ch_data = data
365
+ channel_index = i
366
+ break
367
+
368
+ if not ch_data:
369
+ print(f"❌ [Debug] Channel ID {channel.id} not found in backup data.")
370
+ return None
371
+
372
+ print(f"📦 [Debug] Found channel data in backup. Type: {ch_data['type']}")
373
+
374
+
375
+ category = None
376
+ if ch_data.get("category_id"):
377
+ category = guild.get_channel(ch_data["category_id"])
378
+
379
+
380
+ new_channel = None
381
+ channel_type = ch_data["type"]
382
+
383
+ if "text" in channel_type.lower():
384
+ text_props = ch_data.get("text_properties", {})
385
+ new_channel = await guild.create_text_channel(
386
+ name=ch_data["name"],
387
+ category=category,
388
+ topic=text_props.get("topic"),
389
+ slowmode_delay=text_props.get("slowmode_delay", 0),
390
+ nsfw=text_props.get("nsfw", False),
391
+ reason="SecureX: Restoring deleted channel"
392
+ )
393
+ elif "voice" in channel_type.lower():
394
+ voice_props = ch_data.get("voice_properties", {})
395
+ new_channel = await guild.create_voice_channel(
396
+ name=ch_data["name"],
397
+ category=category,
398
+ bitrate=voice_props.get("bitrate", 64000),
399
+ user_limit=voice_props.get("user_limit", 0),
400
+ reason="SecureX: Restoring deleted channel"
401
+ )
402
+ elif "category" in channel_type.lower():
403
+ new_channel = await guild.create_category(
404
+ name=ch_data["name"],
405
+ reason="SecureX: Restoring deleted category"
406
+ )
407
+ elif "stage" in channel_type.lower():
408
+ stage_props = ch_data.get("stage_properties", {})
409
+ new_channel = await guild.create_stage_channel(
410
+ name=ch_data["name"],
411
+ category=category,
412
+ bitrate=stage_props.get("bitrate", 64000),
413
+ reason="SecureX: Restoring deleted channel"
414
+ )
415
+
416
+ if not new_channel:
417
+ return None
418
+
419
+
420
+ target_position = ch_data.get("order_index", ch_data["position"])
421
+
422
+
423
+ try:
424
+ await new_channel.edit(position=target_position)
425
+ print(f"✅ Restored channel: {new_channel.name} ({channel_type}) at order_index {target_position}")
426
+ except discord.Forbidden:
427
+ print(f"⚠️ Cannot set position for {new_channel.name}")
428
+ except Exception as e:
429
+ print(f"⚠️ Failed to set position for {new_channel.name}: {e}")
430
+
431
+
432
+ if ch_data.get("permissions"):
433
+ for target_id, perm_data in ch_data["permissions"].items():
434
+ try:
435
+ target_id = int(target_id)
436
+ target = guild.get_role(target_id) if perm_data["type"] == "role" else guild.get_member(target_id)
437
+
438
+ if target:
439
+ overwrite = discord.PermissionOverwrite.from_pair(
440
+ discord.Permissions(perm_data["allow"]),
441
+ discord.Permissions(perm_data["deny"])
442
+ )
443
+ await new_channel.set_permissions(target, overwrite=overwrite)
444
+ except Exception as e:
445
+ print(f"Error restoring permission: {e}")
446
+
447
+
448
+ if guild.id in self._channel_cache and channel_index is not None:
449
+ self._channel_cache[guild.id]["channels"][channel_index]["id"] = new_channel.id
450
+
451
+
452
+ backup_file = self.backup_dir / f"channels_{guild.id}.json"
453
+ with open(backup_file, 'w') as f:
454
+ json.dump(self._channel_cache[guild.id], f, indent=2)
455
+
456
+ return new_channel
457
+
458
+ except Exception as e:
459
+ print(f"Error restoring channel: {e}")
460
+ return None
461
+
462
+ async def restore_channel_permissions(self, guild: discord.Guild, channel_id: int) -> bool:
463
+ """Restore channel permissions from backup"""
464
+ try:
465
+ backup_file = self.backup_dir / f"channels_{guild.id}.json"
466
+ if not backup_file.exists():
467
+ return False
468
+
469
+ with open(backup_file, 'r') as f:
470
+ backup = json.load(f)
471
+
472
+
473
+ ch_data = None
474
+ for data in backup["channels"]:
475
+ if data["id"] == channel_id:
476
+ ch_data = data
477
+ break
478
+
479
+ if not ch_data:
480
+ return False
481
+
482
+ channel = guild.get_channel(channel_id)
483
+ if not channel:
484
+ return False
485
+
486
+
487
+ if ch_data.get("permissions"):
488
+ for target_id, perm_data in ch_data["permissions"].items():
489
+ try:
490
+ target_id = int(target_id)
491
+ target = guild.get_role(target_id) if perm_data["type"] == "role" else guild.get_member(target_id)
492
+
493
+ if target:
494
+ overwrite = discord.PermissionOverwrite.from_pair(
495
+ discord.Permissions(perm_data["allow"]),
496
+ discord.Permissions(perm_data["deny"])
497
+ )
498
+ await channel.set_permissions(target, overwrite=overwrite)
499
+ except Exception:
500
+ pass
501
+
502
+ return True
503
+
504
+ except Exception as e:
505
+ print(f"Error restoring channel permissions: {e}")
506
+ return False
507
+
508
+ async def restore_role(self, guild: discord.Guild, role_id: int) -> bool:
509
+ """Restore a deleted role from backup"""
510
+ try:
511
+
512
+ backup = self._role_cache.get(guild.id)
513
+ if not backup:
514
+
515
+ backup_file = self.backup_dir / f"roles_{guild.id}.json"
516
+ if not backup_file.exists():
517
+ return False
518
+
519
+ with open(backup_file, 'r') as f:
520
+ backup = json.load(f)
521
+ self._role_cache[guild.id] = backup
522
+
523
+
524
+ target_role_data = None
525
+ role_index = None
526
+ for i, role_data in enumerate(backup["roles"]):
527
+ if role_data["id"] == role_id:
528
+ target_role_data = role_data
529
+ role_index = i
530
+ break
531
+
532
+ if not target_role_data:
533
+ return False
534
+
535
+
536
+ new_role = await guild.create_role(
537
+ name=target_role_data["name"],
538
+ permissions=discord.Permissions(target_role_data["permissions"]),
539
+ color=discord.Color(target_role_data["color"]),
540
+ hoist=target_role_data["hoist"],
541
+ mentionable=target_role_data["mentionable"],
542
+ reason="SecureX: Restoring deleted role"
543
+ )
544
+
545
+
546
+ try:
547
+ await guild.edit_role_positions(positions={new_role: target_role_data["position"]})
548
+ print(f"✅ Restored role: {new_role.name} at position {target_role_data['position']}")
549
+ except discord.Forbidden:
550
+ print(f"⚠️ Cannot set position for {new_role.name} - bot hierarchy too low")
551
+ except Exception as e:
552
+ print(f"⚠️ Failed to set position for {new_role.name}: {e}")
553
+
554
+
555
+ if guild.id in self._role_cache and role_index is not None:
556
+
557
+ self._role_cache[guild.id]["roles"][role_index]["id"] = new_role.id
558
+
559
+
560
+ backup_file = self.backup_dir / f"roles_{guild.id}.json"
561
+ with open(backup_file, 'w') as f:
562
+ json.dump(self._role_cache[guild.id], f, indent=2)
563
+
564
+ return True
565
+ except Exception as e:
566
+ print(f"Error restoring role: {e}")
567
+ return False
568
+
569
+ async def restore_role_permissions(self, guild: discord.Guild, role_id: int) -> bool:
570
+ """Restore role permissions from backup"""
571
+ try:
572
+ backup_file = self.backup_dir / f"roles_{guild.id}.json"
573
+ if not backup_file.exists():
574
+ return False
575
+
576
+ with open(backup_file, 'r') as f:
577
+ backup = json.load(f)
578
+
579
+
580
+ for role_data in backup["roles"]:
581
+ if role_data["id"] == role_id:
582
+ role = guild.get_role(role_id)
583
+ if not role:
584
+ return False
585
+
586
+
587
+ await role.edit(
588
+ permissions=discord.Permissions(role_data["permissions"]),
589
+ position=role_data["position"],
590
+ reason="SecureX: Restoring role permissions"
591
+ )
592
+
593
+ return True
594
+
595
+ return False
596
+ except Exception as e:
597
+ print(f"Error restoring role permissions: {e}")
598
+ return False
599
+
600
+
601
+
602
+
603
+
604
+
605
+ async def preload_all(self):
606
+ """
607
+ Preload ALL backups into memory on startup.
608
+ Call this in enable() to warm the cache.
609
+ """
610
+ if self._cache_loaded:
611
+ return
612
+
613
+ print("🔄 Preloading backups into cache...")
614
+ channel_count = 0
615
+ role_count = 0
616
+
617
+
618
+ for file_path in self.backup_dir.glob("channels_*.json"):
619
+ try:
620
+ guild_id = int(file_path.stem.replace("channels_", ""))
621
+ with open(file_path, 'r') as f:
622
+ self._channel_cache[guild_id] = json.load(f)
623
+ channel_count += 1
624
+ except Exception as e:
625
+ print(f"Error loading channel backup {file_path}: {e}")
626
+
627
+
628
+ for file_path in self.backup_dir.glob("roles_*.json"):
629
+ try:
630
+ guild_id = int(file_path.stem.replace("roles_", ""))
631
+ with open(file_path, 'r') as f:
632
+ self._role_cache[guild_id] = json.load(f)
633
+ role_count += 1
634
+ except Exception as e:
635
+ print(f"Error loading role backup {file_path}: {e}")
636
+
637
+ self._cache_loaded = True
638
+ print(f"✅ Cached {channel_count} channel backups and {role_count} role backups")
639
+
640
+ async def _update_guild_cache(self, guild_id: int):
641
+ """Update cache for a specific guild after backup"""
642
+ try:
643
+
644
+ channel_file = self.backup_dir / f"channels_{guild_id}.json"
645
+ if channel_file.exists():
646
+ with open(channel_file, 'r') as f:
647
+ self._channel_cache[guild_id] = json.load(f)
648
+
649
+
650
+ role_file = self.backup_dir / f"roles_{guild_id}.json"
651
+ if role_file.exists():
652
+ with open(role_file, 'r') as f:
653
+ self._role_cache[guild_id] = json.load(f)
654
+ except Exception as e:
655
+ print(f"Error updating cache for guild {guild_id}: {e}")
656
+
657
+ """Start background task to refresh cache every 10 minutes"""
658
+ if self._refresh_task is None:
659
+ self._refresh_task = asyncio.create_task(self._auto_refresh_loop())
660
+ print("🔄 Started backup cache auto-refresh (every 10 minutes)")
661
+
662
+ async def _auto_refresh_loop(self):
663
+ """Background task: refresh cache every 10 minutes"""
664
+ while True:
665
+ try:
666
+ await asyncio.sleep(600)
667
+
668
+ print("🔄 Refreshing backup cache...")
669
+
670
+
671
+ for guild in self.bot.guilds:
672
+ await self._update_guild_cache(guild.id)
673
+
674
+ # Also refresh guild settings (vanity, name, etc.)
675
+ await self._backup_guild_settings(guild)
676
+
677
+ await asyncio.sleep(0.1)
678
+
679
+ print("✅ Backup cache refreshed")
680
+
681
+ except asyncio.CancelledError:
682
+ break
683
+ except Exception as e:
684
+ print(f"Error in auto-refresh loop: {e}")
685
+
686
+ def start_auto_refresh(self):
687
+ """Start background cache refresh task"""
688
+ if self._refresh_task is None or self._refresh_task.done():
689
+ self._refresh_task = asyncio.create_task(self._auto_refresh_loop())
690
+
691
+
692
+
693
+ async def _backup_guild_settings(self, guild: discord.Guild) -> Dict:
694
+ """Backup guild settings (vanity URL, name, etc.) to consolidated file"""
695
+ try:
696
+ backup_data = {
697
+ "guild_id": guild.id,
698
+ "vanity_url_code": guild.vanity_url_code,
699
+ "name": guild.name,
700
+ "icon": str(guild.icon) if guild.icon else None,
701
+ "banner": str(guild.banner) if guild.banner else None,
702
+ "description": guild.description,
703
+ "timestamp": datetime.now(timezone.utc).isoformat()
704
+ }
705
+
706
+ # Update cache
707
+ self._guild_cache[guild.id] = backup_data
708
+
709
+ # Save to consolidated file
710
+ await self._save_all_guild_settings()
711
+
712
+ return backup_data
713
+
714
+ except Exception as e:
715
+ print(f"Error backing up guild settings: {e}")
716
+ return {}
717
+
718
+ async def _save_all_guild_settings(self):
719
+ """Save all guild settings to single file"""
720
+ try:
721
+ settings_file = self.backup_dir / "guild_settings.json"
722
+ # Convert int keys to string for JSON
723
+ data = {str(k): v for k, v in self._guild_cache.items()}
724
+ with open(settings_file, 'w') as f:
725
+ json.dump(data, f, indent=2)
726
+ except Exception as e:
727
+ print(f"Error saving guild settings: {e}")
728
+
729
+ async def _load_all_guild_settings(self):
730
+ """Load all guild settings from single file into cache"""
731
+ try:
732
+ settings_file = self.backup_dir / "guild_settings.json"
733
+ if settings_file.exists():
734
+ with open(settings_file, 'r') as f:
735
+ data = json.load(f)
736
+ # Convert string keys back to int
737
+ self._guild_cache = {int(k): v for k, v in data.items()}
738
+ print(f"✅ Loaded guild settings for {len(self._guild_cache)} server(s)")
739
+ except Exception as e:
740
+ print(f"Error loading guild settings: {e}")
741
+
742
+ async def update_guild_vanity(self, guild_id: int, vanity_code: str):
743
+ """Update vanity URL in backup and cache (for authorized changes)"""
744
+ try:
745
+ if guild_id in self._guild_cache:
746
+ self._guild_cache[guild_id]["vanity_url_code"] = vanity_code
747
+ self._guild_cache[guild_id]["timestamp"] = datetime.now(timezone.utc).isoformat()
748
+ else:
749
+ self._guild_cache[guild_id] = {
750
+ "guild_id": guild_id,
751
+ "vanity_url_code": vanity_code,
752
+ "timestamp": datetime.now(timezone.utc).isoformat()
753
+ }
754
+
755
+ await self._save_all_guild_settings()
756
+ print(f"✅ Updated vanity backup: discord.gg/{vanity_code}")
757
+
758
+ except Exception as e:
759
+ print(f"Error updating guild vanity: {e}")
760
+
761
+ async def get_guild_vanity(self, guild_id: int) -> Optional[str]:
762
+ """Get backed up vanity URL"""
763
+ try:
764
+ if guild_id in self._guild_cache:
765
+ return self._guild_cache[guild_id].get("vanity_url_code")
766
+
767
+ await self._load_all_guild_settings()
768
+
769
+ if guild_id in self._guild_cache:
770
+ return self._guild_cache[guild_id].get("vanity_url_code")
771
+
772
+ return None
773
+
774
+ except Exception as e:
775
+ print(f"Error getting guild vanity: {e}")
776
+ return None
777
+
778
+ async def get_guild_settings(self, guild_id: int) -> Optional[Dict]:
779
+ """Get backed up guild settings"""
780
+ try:
781
+ if guild_id in self._guild_cache:
782
+ return self._guild_cache[guild_id]
783
+
784
+ await self._load_all_guild_settings()
785
+
786
+ if guild_id in self._guild_cache:
787
+ return self._guild_cache[guild_id]
788
+
789
+ return None
790
+
791
+ except Exception as e:
792
+ print(f"Error getting guild settings: {e}")