update readme
This commit is contained in:
parent
9d8f236fb8
commit
77fabe3e2b
3 changed files with 109 additions and 64 deletions
11
README.md
11
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue