update readme

This commit is contained in:
rambros 2026-02-26 20:54:03 +05:30
parent 9d8f236fb8
commit 77fabe3e2b
3 changed files with 109 additions and 64 deletions

View file

@ -16,9 +16,9 @@ Fluxer Reaper is a simple tool to help you move an entire Discord server over to
| - Slowmode | 🟩 | ⚠️ |
| **Roles & Permissions** | | |
| - Roles Cloning | 🟩 | 🟩 |
| - Roles Permissions | 🟩 | |
| - Roles Permissions | 🟩 | 🟩 |
| - Category Permissions | 🟩 | ⚠️ |
| - Channel Permissions | 🟩 | ⏳ |
| - Channel Permissions | 🟩 | ⏳to be done |
| **Emojis & Stickers** | | |
| - Copy Emojis | 🟩 | 🟩 |
| - Copy Stickers | 🟩 | ⚠️ |
@ -33,14 +33,17 @@ Fluxer Reaper is a simple tool to help you move an entire Discord server over to
| - Threads | 🟩 | 🟩 |
| - Forums | ⚠️ | ⚠️ |
🟩fully functional | ⏳to be implemented | ⚠feature not available
- ⚠️**Fluxer/Stoat**: Threads & Forums type channels are not yet natively available. As a workaround, threads are migrated in their parent channels.
- ⚠️**Stoat**: doesnt have features like Category Permissions, Slowmode setting for channels.
- ⏳**Stoat**: permission sync for channels is not yet implemented since its permission management vastly different compared to Discord & Fluxer.
---
### Other Features
### Notable Features
* **Flexible Start Points**: Start from the oldest message, a specific custom message ID/link, or continue from where you left off.
* **Thread Support**: Copies threads with dedicated markers in the target channel. This is a workaround as the threads feature is not yet natively implemented in Fluxer/Stoat.
* **Permission Checks**: Checks if bots have the required Permissions.
* **Self-Hosted Instance Support**: Supports custom API url endpoint to use with your own Fluxer/Stoat instance.
* **Audit Channel**: Logs migration progress and errors to a logs channel in the target community.
### Danger Zone

View file

@ -1,5 +1,6 @@
import asyncio
import logging
import discord
from typing import Callable, Awaitable
from src.core.base import MigrationContext
@ -40,12 +41,12 @@ async def sync_roles_state(context: MigrationContext):
async def sync_permissions(context: MigrationContext, progress_callback: Callable[[str, int, int], Awaitable[None]] | None = None) -> dict:
"""Syncs category and channel role overrides/permissions."""
categories = await context.discord_reader.get_categories()
channels = await context.discord_reader.get_channels()
discord_categories = await context.discord_reader.get_categories()
discord_channels = await context.discord_reader.get_channels()
# Only sync for items that are already mapped
categories = [c for c in categories if context.state.get_target_category_id(str(c.id))]
channels = [c for c in channels if context.state.get_target_channel_id(str(c.id))]
mapped_categories = [c for c in discord_categories if context.state.get_target_category_id(str(c.id))]
mapped_channels = [c for c in discord_channels if context.state.get_target_channel_id(str(c.id))]
synced_info = {
"categories_synced": [],
@ -53,75 +54,111 @@ async def sync_permissions(context: MigrationContext, progress_callback: Callabl
"structure": {} # category_name -> [channel_names]
}
total = len(categories) + len(channels)
total = len(mapped_categories) + len(mapped_channels)
current_idx = 0
if total == 0:
return synced_info
async def _sync_overwrites(discord_item, stoat_id):
"""Helper to sync role overwrites for a given channel or category."""
for target, overwrite in discord_item.overwrites.items():
cat_map = {cat.id: cat for cat in discord_categories}
cat_names = {cat.id: cat.name for cat in discord_categories}
# 1. Sync Categories (In Stoat, categories/folders don't have permissions, so we just track them for structure)
for cat in mapped_categories:
if not context.is_running: break
synced_info["categories_synced"].append(cat.name)
if cat.name not in synced_info["structure"]:
synced_info["structure"][cat.name] = []
current_idx += 1
if progress_callback: await progress_callback(f"Cat: {cat.name}", current_idx, total)
# 2. Sync Channel Permissions with Category Inheritance logic
for channel in mapped_channels:
if not context.is_running: break
stoat_channel_id = context.state.get_target_channel_id(str(channel.id))
if not stoat_channel_id:
current_idx += 1
continue
# Flatten overwrites: start with category, then overlay channel overrides
final_overwrites = {} # role_id -> PermissionOverwrite
# A. Start with Category overwrites (Inheritance)
if channel.category_id and channel.category_id in cat_map:
cat = cat_map[channel.category_id]
for target, ow in cat.overwrites.items():
if type(target).__name__ == "Role":
# We store it by target.id to easily merge later
final_overwrites[target.id] = ow
# B. Apply Channel overrides on top (Merge)
for target, ow in channel.overwrites.items():
if type(target).__name__ == "Role":
discord_role_id = str(target.id)
# Handle @everyone role special case
if discord_role_id == str(context.config.discord_server_id):
# In Stoat, @everyone is usually the community ID
stoat_role_id = str(context.stoat_writer.community_id)
is_role = False # Use set_default_permissions logic
if target.id in final_overwrites:
# Merge logic: channel overwrites specific settings, but
# keeps inherited settings for any permission marked as 'None' in channel
cat_ow = final_overwrites[target.id]
merged = discord.PermissionOverwrite()
# Apply category settings
for name, value in cat_ow:
setattr(merged, name, value)
# Overwrite with channel settings
for name, value in ow:
if value is not None:
setattr(merged, name, value)
final_overwrites[target.id] = merged
else:
stoat_role_id = context.state.get_target_role_id(discord_role_id)
# Brand new override for this channel
final_overwrites[target.id] = ow
# C. Apply the merged overwrites to Stoat
try:
# Sort overrides to ensure @everyone (Community ID) is processed LAST.
# This prevents the bot from accidentally locking itself out of the channel
# (due to default denys) before it finishes setting other role permissions.
sorted_overrides = sorted(
final_overwrites.items(),
key=lambda x: str(x[0]) == str(context.discord_reader.server_id)
)
for d_role_id, merged_ov in sorted_overrides:
# Map Discord role to Stoat role
if str(d_role_id) == str(context.discord_reader.server_id):
# @everyone
stoat_id = str(context.stoat_writer.community_id)
is_role = False # set_default_permissions path
else:
stoat_id = context.state.get_target_role_id(str(d_role_id))
is_role = True
if not stoat_role_id:
if not stoat_id:
continue
allow_val, deny_val = overwrite.pair()
allow_val, deny_val = merged_ov.pair()
await context.stoat_writer.set_channel_permission(
channel_id=stoat_id,
overwrite_id=stoat_role_id,
channel_id=stoat_channel_id,
overwrite_id=stoat_id,
allow=allow_val.value,
deny=deny_val.value,
is_role=is_role
)
# Dictionary to map category names to their synced channels
cat_name_map = {str(cat.id): cat.name for cat in (await context.discord_reader.get_categories())}
synced_info["channels_synced"].append(channel.name)
parent_name = cat_names.get(channel.category_id, "No Category")
if parent_name not in synced_info["structure"]:
synced_info["structure"][parent_name] = []
synced_info["structure"][parent_name].append(channel.name)
# Sync Category Permissions (Role Overwrites)
for cat in categories:
if not context.is_running: break
stoat_id = context.state.get_target_category_id(str(cat.id))
if stoat_id:
try:
await _sync_overwrites(cat, stoat_id)
synced_info["categories_synced"].append(cat.name)
if cat.name not in synced_info["structure"]:
synced_info["structure"][cat.name] = []
except Exception as e:
logger.error(f"Failed syncing permissions for category {cat.name}: {e}")
current_idx += 1
if progress_callback: await progress_callback(f"Cat: {cat.name}", current_idx, total)
# Sync Channel Permissions
for channel in channels:
if not context.is_running: break
stoat_id = context.state.get_target_channel_id(str(channel.id))
if stoat_id:
try:
await _sync_overwrites(channel, stoat_id)
synced_info["channels_synced"].append(channel.name)
parent_name = cat_name_map.get(str(channel.category_id), "No Category") if channel.category_id else "No Category"
if parent_name not in synced_info["structure"]:
synced_info["structure"][parent_name] = []
synced_info["structure"][parent_name].append(channel.name)
except Exception as e:
logger.error(f"Failed syncing permissions for channel {channel.name}: {e}")
except Exception as e:
logger.error(f"Failed syncing permissions for channel {channel.name}: {e}")
current_idx += 1
if progress_callback: await progress_callback(channel.name, current_idx, total)
await asyncio.sleep(context.config.migration.rate_limit_delay_seconds)
return synced_info

View file

@ -515,9 +515,14 @@ class StoatWriter:
# User-specific overrides are currently skipped for Stoat
pass
except Exception as e:
err_msg = str(e)
if "MissingPermission" in err_msg and "ViewChannel" in err_msg:
logger.error(f"Stoat LOCKOUT: Bot lacks 'ViewChannel' to edit {channel_id}. "
"Ensure the bot has 'Manage Server' or a role with 'Allow View Channel' rank higher than @everyone.")
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()