diff --git a/src/stoat/roles_permissions.py b/src/stoat/roles_permissions.py index 6f074aa..af29558 100644 --- a/src/stoat/roles_permissions.py +++ b/src/stoat/roles_permissions.py @@ -128,6 +128,17 @@ async def sync_permissions(context: MigrationContext, progress_callback: Callabl async def migrate_roles(context: MigrationContext, progress_callback: Callable[[str, int, int], Awaitable[None]] | None = None, force: bool = False) -> list[str]: """Copies roles and their baseline permissions. Returns a list of cloned role names.""" + + # 1. Sync default permissions (@everyone) + try: + guild = context.discord_reader.guild + if guild: + default_role = guild.default_role + await context.stoat_writer.update_default_role_permissions(default_role.permissions.value) + logger.info("Synced default server permissions (@everyone).") + except Exception as e: + logger.error(f"Failed to sync default permissions: {e}") + roles = await context.discord_reader.get_roles() if not force: @@ -145,7 +156,8 @@ async def migrate_roles(context: MigrationContext, progress_callback: Callable[[ stoat_id = await context.stoat_writer.create_role( name=role.name, color=role.color.value, - hoist=role.hoist + hoist=role.hoist, + permissions=role.permissions.value ) if stoat_id: context.state.set_role_mapping(str(role.id), stoat_id) @@ -155,3 +167,4 @@ async def migrate_roles(context: MigrationContext, progress_callback: Callable[[ await asyncio.sleep(context.config.migration.rate_limit_delay_seconds) return cloned_role_names + diff --git a/src/stoat/writer.py b/src/stoat/writer.py index 5e3084e..66526c1 100644 --- a/src/stoat/writer.py +++ b/src/stoat/writer.py @@ -294,7 +294,7 @@ class StoatWriter: return None - async def create_role(self, name: str, color: int = 0, hoist: bool = False, **kwargs) -> str: + async def create_role(self, name: str, color: int = 0, hoist: bool = False, permissions: int = 0, **kwargs) -> str: server = await self._get_server() try: # Create role first (Stoat create_role only takes name and rank) @@ -310,11 +310,74 @@ class StoatWriter: color=hex_color if hex_color is not None else stoat.UNDEFINED, hoist=hoist ) + + # Set permissions + if permissions != 0: + s_perms = self._map_permissions(permissions) + await server.set_role_permissions(role, allow=s_perms) + return str(role.id) except Exception as e: logger.error(f"Failed to create Stoat role {name}: {e}") return "" + def _map_permissions(self, discord_perms_int: int) -> stoat.Permissions: + import discord + d_perms = discord.Permissions(discord_perms_int) + + s_perms = stoat.Permissions.none() + + mapping = { + "manage_channels": "manage_channels", + "manage_guild": "manage_server", + "manage_roles": "manage_roles", + "kick_members": "kick_members", + "ban_members": "ban_members", + "view_channel": "view_channel", + "send_messages": "send_messages", + "manage_messages": "manage_messages", + "embed_links": "send_embeds", + "attach_files": "upload_files", + "read_message_history": "read_message_history", + "mention_everyone": "mention_everyone", + "add_reactions": "react", + "connect": "connect", + "speak": "speak", + "stream": "video", + "mute_members": "mute_members", + "deafen_members": "deafen_members", + "move_members": "move_members", + "manage_nicknames": "manage_nicknames", + "manage_webhooks": "manage_webhooks", + "manage_emojis": "manage_customization", + "manage_stickers": "manage_customization", + "moderate_members": "timeout_members", + } + + for d_name, s_name in mapping.items(): + if getattr(d_perms, d_name, False): + try: + setattr(s_perms, s_name, True) + except Exception: + pass + + if d_perms.administrator: + return stoat.Permissions.all() + + return s_perms + + async def update_default_role_permissions(self, permissions: int): + """Sets the default server permissions from a Discord permissions bitfield.""" + server = await self._get_server() + try: + s_perms = self._map_permissions(permissions) + await server.set_default_permissions(s_perms) + return True + except Exception as e: + logger.error(f"Failed to update Stoat default permissions: {e}") + return False + + async def create_emoji(self, name: str, image_bytes: bytes, **kwargs) -> str: server = await self._get_server() try: @@ -437,9 +500,9 @@ class StoatWriter: try: channel = await self.client.fetch_channel(channel_id) - # Stoat Permissions objects are created from raw integers - allow_perms = stoat.Permissions(allow) - deny_perms = stoat.Permissions(deny) + # Stoat Permissions objects MUST be mapped from Discord bitfields + allow_perms = self._map_permissions(allow) + deny_perms = self._map_permissions(deny) # If overwrite_id is the community_id, it refers to the default permissions (@everyone) if str(overwrite_id) == self.community_id: @@ -449,12 +512,12 @@ class StoatWriter: # Stoat uses set_role_permissions(role_id, allow=..., deny=...) await channel.set_role_permissions(overwrite_id, allow=allow_perms, deny=deny_perms) else: - # This case might not be directly supported for individual users in the same way - # but we'll try something if needed. + # User-specific overrides are currently skipped for Stoat pass except Exception as e: logger.error(f"Failed to set Stoat channel permission for {overwrite_id} on {channel_id}: {e}") + async def delete_all_roles(self, **kwargs) -> int: server = await self._get_server() # Stoat roles are in server.roles (dict) or fetch_roles() diff --git a/src/ui/app.py b/src/ui/app.py index a06bef6..9068fa3 100644 --- a/src/ui/app.py +++ b/src/ui/app.py @@ -739,7 +739,7 @@ class MigrationCLI: role_task = progress.add_task("[cyan]Syncing Roles...", total=100) async def update_progress(item_name: str, current: int, total: int): - progress.update(role_task, total=total, completed=current, description=f"[cyan]Syncing Role: {item_name}") + progress.update(role_task, total=total, completed=current, description=f"[cyan]Copying Role: {item_name}") self.engine.is_running = True cloned_roles = await roles_mod.migrate_roles(self.engine, progress_callback=update_progress, force=force)