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 | 🟩 | ⚠️ |
|
| - Slowmode | 🟩 | ⚠️ |
|
||||||
| **Roles & Permissions** | | |
|
| **Roles & Permissions** | | |
|
||||||
| - Roles Cloning | 🟩 | 🟩 |
|
| - Roles Cloning | 🟩 | 🟩 |
|
||||||
| - Roles Permissions | 🟩 | ⏳ |
|
| - Roles Permissions | 🟩 | 🟩 |
|
||||||
| - Category Permissions | 🟩 | ⚠️ |
|
| - Category Permissions | 🟩 | ⚠️ |
|
||||||
| - Channel Permissions | 🟩 | ⏳ |
|
| - Channel Permissions | 🟩 | ⏳to be done |
|
||||||
| **Emojis & Stickers** | | |
|
| **Emojis & Stickers** | | |
|
||||||
| - Copy Emojis | 🟩 | 🟩 |
|
| - Copy Emojis | 🟩 | 🟩 |
|
||||||
| - Copy Stickers | 🟩 | ⚠️ |
|
| - Copy Stickers | 🟩 | ⚠️ |
|
||||||
|
|
@ -33,14 +33,17 @@ Fluxer Reaper is a simple tool to help you move an entire Discord server over to
|
||||||
| - Threads | 🟩 | 🟩 |
|
| - Threads | 🟩 | 🟩 |
|
||||||
| - Forums | ⚠️ | ⚠️ |
|
| - 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.
|
* **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.
|
* **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.
|
* **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.
|
* **Audit Channel**: Logs migration progress and errors to a logs channel in the target community.
|
||||||
|
|
||||||
### Danger Zone
|
### Danger Zone
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import discord
|
||||||
from typing import Callable, Awaitable
|
from typing import Callable, Awaitable
|
||||||
|
|
||||||
from src.core.base import MigrationContext
|
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:
|
async def sync_permissions(context: MigrationContext, progress_callback: Callable[[str, int, int], Awaitable[None]] | None = None) -> dict:
|
||||||
"""Syncs category and channel role overrides/permissions."""
|
"""Syncs category and channel role overrides/permissions."""
|
||||||
categories = await context.discord_reader.get_categories()
|
discord_categories = await context.discord_reader.get_categories()
|
||||||
channels = await context.discord_reader.get_channels()
|
discord_channels = await context.discord_reader.get_channels()
|
||||||
|
|
||||||
# Only sync for items that are already mapped
|
# Only sync for items that are already mapped
|
||||||
categories = [c for c in categories if context.state.get_target_category_id(str(c.id))]
|
mapped_categories = [c for c in discord_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_channels = [c for c in discord_channels if context.state.get_target_channel_id(str(c.id))]
|
||||||
|
|
||||||
synced_info = {
|
synced_info = {
|
||||||
"categories_synced": [],
|
"categories_synced": [],
|
||||||
|
|
@ -53,75 +54,111 @@ async def sync_permissions(context: MigrationContext, progress_callback: Callabl
|
||||||
"structure": {} # category_name -> [channel_names]
|
"structure": {} # category_name -> [channel_names]
|
||||||
}
|
}
|
||||||
|
|
||||||
total = len(categories) + len(channels)
|
total = len(mapped_categories) + len(mapped_channels)
|
||||||
current_idx = 0
|
current_idx = 0
|
||||||
|
|
||||||
if total == 0:
|
if total == 0:
|
||||||
return synced_info
|
return synced_info
|
||||||
|
|
||||||
async def _sync_overwrites(discord_item, stoat_id):
|
cat_map = {cat.id: cat for cat in discord_categories}
|
||||||
"""Helper to sync role overwrites for a given channel or category."""
|
cat_names = {cat.id: cat.name for cat in discord_categories}
|
||||||
for target, overwrite in discord_item.overwrites.items():
|
|
||||||
|
# 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":
|
if type(target).__name__ == "Role":
|
||||||
discord_role_id = str(target.id)
|
if target.id in final_overwrites:
|
||||||
# Handle @everyone role special case
|
# Merge logic: channel overwrites specific settings, but
|
||||||
if discord_role_id == str(context.config.discord_server_id):
|
# keeps inherited settings for any permission marked as 'None' in channel
|
||||||
# In Stoat, @everyone is usually the community ID
|
cat_ow = final_overwrites[target.id]
|
||||||
stoat_role_id = str(context.stoat_writer.community_id)
|
merged = discord.PermissionOverwrite()
|
||||||
is_role = False # Use set_default_permissions logic
|
# 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:
|
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
|
is_role = True
|
||||||
|
|
||||||
if not stoat_role_id:
|
if not stoat_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
allow_val, deny_val = overwrite.pair()
|
allow_val, deny_val = merged_ov.pair()
|
||||||
await context.stoat_writer.set_channel_permission(
|
await context.stoat_writer.set_channel_permission(
|
||||||
channel_id=stoat_id,
|
channel_id=stoat_channel_id,
|
||||||
overwrite_id=stoat_role_id,
|
overwrite_id=stoat_id,
|
||||||
allow=allow_val.value,
|
allow=allow_val.value,
|
||||||
deny=deny_val.value,
|
deny=deny_val.value,
|
||||||
is_role=is_role
|
is_role=is_role
|
||||||
)
|
)
|
||||||
|
|
||||||
# Dictionary to map category names to their synced channels
|
synced_info["channels_synced"].append(channel.name)
|
||||||
cat_name_map = {str(cat.id): cat.name for cat in (await context.discord_reader.get_categories())}
|
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)
|
except Exception as e:
|
||||||
for cat in categories:
|
logger.error(f"Failed syncing permissions for channel {channel.name}: {e}")
|
||||||
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}")
|
|
||||||
|
|
||||||
current_idx += 1
|
current_idx += 1
|
||||||
if progress_callback: await progress_callback(channel.name, current_idx, total)
|
if progress_callback: await progress_callback(channel.name, current_idx, total)
|
||||||
|
await asyncio.sleep(context.config.migration.rate_limit_delay_seconds)
|
||||||
|
|
||||||
|
|
||||||
return synced_info
|
return synced_info
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -515,9 +515,14 @@ class StoatWriter:
|
||||||
# User-specific overrides are currently skipped for Stoat
|
# User-specific overrides are currently skipped for Stoat
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
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}")
|
logger.error(f"Failed to set Stoat channel permission for {overwrite_id} on {channel_id}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_all_roles(self, **kwargs) -> int:
|
async def delete_all_roles(self, **kwargs) -> int:
|
||||||
server = await self._get_server()
|
server = await self._get_server()
|
||||||
# Stoat roles are in server.roles (dict) or fetch_roles()
|
# Stoat roles are in server.roles (dict) or fetch_roles()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue