from __future__ import annotations import json from typing import Any import discord from storage import storage def _overwrite_to_dict(overwrite: discord.PermissionOverwrite) -> dict[str, bool | None]: allowed, denied = overwrite.pair() result: dict[str, bool | None] = {} for name, _ in discord.Permissions.all(): if getattr(allowed, name): result[name] = True elif getattr(denied, name): result[name] = False return result def _overwrites(channel: discord.abc.GuildChannel) -> list[dict[str, Any]]: rows: list[dict[str, Any]] = [] for target, overwrite in channel.overwrites.items(): rows.append( { "target_id": str(target.id), "target_type": "role" if isinstance(target, discord.Role) else "member", "permissions": _overwrite_to_dict(overwrite), } ) return rows def _channel_kind(channel: discord.abc.GuildChannel) -> str: if isinstance(channel, discord.CategoryChannel): return "category" if isinstance(channel, discord.TextChannel): return "text" if isinstance(channel, discord.VoiceChannel): return "voice" if isinstance(channel, discord.StageChannel): return "stage" if isinstance(channel, discord.ForumChannel): return "forum" return "other" async def create_backup(guild: discord.Guild, reason: str = "manual", created_by: str = "Sistema") -> int: roles = [] for role in guild.roles: if role.is_default(): continue roles.append( { "id": str(role.id), "name": role.name, "permissions": role.permissions.value, "color": role.color.value, "hoist": role.hoist, "mentionable": role.mentionable, "position": role.position, "managed": role.managed, } ) channels = [] for channel in guild.channels: item: dict[str, Any] = { "id": str(channel.id), "name": channel.name, "kind": _channel_kind(channel), "position": channel.position, "category_id": str(channel.category_id) if getattr(channel, "category_id", None) else None, "overwrites": _overwrites(channel), } if isinstance(channel, discord.TextChannel): item.update( { "topic": channel.topic, "nsfw": channel.nsfw, "slowmode_delay": channel.slowmode_delay, } ) elif isinstance(channel, (discord.VoiceChannel, discord.StageChannel)): item.update( { "bitrate": channel.bitrate, "user_limit": getattr(channel, "user_limit", 0), } ) channels.append(item) members = [] for member in guild.members: members.append( { "id": str(member.id), "roles": [str(r.id) for r in member.roles if not r.is_default()], } ) data = { "version": 1, "guild": { "id": str(guild.id), "name": guild.name, "owner_id": str(guild.owner_id or ""), }, "roles": roles, "channels": channels, "members": members, } backup_id = await storage.create_backup_record(guild.id, reason, data, created_by) settings = await storage.settings_for(guild.id) await storage.trim_backups(guild.id, int(settings.get("backup_keep", 30) or 30)) return backup_id async def restore_backup(guild: discord.Guild, backup_id: int) -> dict[str, int]: record = await storage.get_backup(guild.id, backup_id) if not record: raise RuntimeError("Backup no encontrado para este servidor.") data = record["backup"] bot_member = guild.me if not bot_member: raise RuntimeError("No se pudo resolver el usuario del bot en el servidor.") role_map: dict[str, discord.Role] = {} roles_created = 0 channels_created = 0 member_roles_restored = 0 # Se restauran roles que falten por nombre. Discord no permite recuperar IDs antiguos. existing_roles_by_name: dict[str, list[discord.Role]] = {} for role in guild.roles: existing_roles_by_name.setdefault(role.name, []).append(role) for saved in sorted(data.get("roles", []), key=lambda r: int(r.get("position", 0))): if saved.get("managed"): continue matches = existing_roles_by_name.get(saved.get("name", ""), []) role = matches[0] if matches else None if role is None: try: role = await guild.create_role( name=saved.get("name", "Restaurado"), permissions=discord.Permissions(int(saved.get("permissions", 0))), color=discord.Color(int(saved.get("color", 0))), hoist=bool(saved.get("hoist")), mentionable=bool(saved.get("mentionable")), reason="NovaShield restore", ) roles_created += 1 except (discord.Forbidden, discord.HTTPException): continue role_map[str(saved.get("id"))] = role category_map: dict[str, discord.CategoryChannel] = {} for saved in sorted(data.get("channels", []), key=lambda c: int(c.get("position", 0))): if saved.get("kind") != "category": continue existing = discord.utils.get(guild.categories, name=saved.get("name", "")) if existing: category_map[str(saved.get("id"))] = existing continue try: cat = await guild.create_category(saved.get("name", "Categoría restaurada"), reason="NovaShield restore") category_map[str(saved.get("id"))] = cat channels_created += 1 except (discord.Forbidden, discord.HTTPException): pass channel_names = {(c.name, _channel_kind(c)) for c in guild.channels} for saved in sorted(data.get("channels", []), key=lambda c: int(c.get("position", 0))): kind = saved.get("kind") if kind == "category": continue name = saved.get("name", "canal-restaurado") if (name, kind) in channel_names: continue category = category_map.get(str(saved.get("category_id"))) try: if kind == "text": await guild.create_text_channel( name, category=category, topic=saved.get("topic"), nsfw=bool(saved.get("nsfw")), slowmode_delay=int(saved.get("slowmode_delay", 0) or 0), reason="NovaShield restore", ) elif kind == "voice": await guild.create_voice_channel( name, category=category, bitrate=min(int(saved.get("bitrate", 64000) or 64000), guild.bitrate_limit), user_limit=int(saved.get("user_limit", 0) or 0), reason="NovaShield restore", ) elif kind == "stage": await guild.create_stage_channel(name, category=category, reason="NovaShield restore") elif kind == "forum": await guild.create_forum(name, category=category, reason="NovaShield restore") else: continue channels_created += 1 except (discord.Forbidden, discord.HTTPException): continue for saved_member in data.get("members", []): try: member_id = int(saved_member["id"]) except (ValueError, TypeError, KeyError): continue member = guild.get_member(member_id) if not member: continue roles = [ role_map[rid] for rid in saved_member.get("roles", []) if rid in role_map and role_map[rid] < bot_member.top_role ] if not roles: continue try: current = [r for r in member.roles if not r.is_default()] merged = list({r.id: r for r in current + roles}.values()) await member.edit(roles=merged, reason="NovaShield restore member roles") member_roles_restored += 1 except (discord.Forbidden, discord.HTTPException): pass return { "roles_created": roles_created, "channels_created": channels_created, "members_restored": member_roles_restored, }