functions refactor

This commit is contained in:
rambros 2026-02-23 13:12:36 +05:30
parent e5d844d77d
commit 6600be74c3
15 changed files with 719 additions and 682 deletions

View file

@ -2,7 +2,7 @@ import sys
import asyncio
import logging
from src.ui.app import run_cli
from src.config import load_config
from src.core.configuration import load_config
def setup_logging():
try:

74
src/core/base.py Normal file
View file

@ -0,0 +1,74 @@
import logging
from typing import Dict, Any
from src.core.configuration import AppConfig
from src.core.state import MigrationState
from src.core.discord_reader import DiscordReader
from src.core.fluxer_writer import FluxerWriter
logger = logging.getLogger(__name__)
class MigrationContext:
"""Holds state and connections for reading from Discord and writing to Fluxer."""
def __init__(self, config: AppConfig):
self.config = config
self.state = MigrationState()
self.discord_reader = DiscordReader(
token=config.discord_bot_token,
server_id=config.discord_server_id
)
self.fluxer_writer = FluxerWriter(
token=config.fluxer_bot_token,
community_id=config.fluxer_community_id
)
self.is_running = False
async def validate_all(self) -> Dict[str, Any]:
"""Returns connection validation status as a dictionary."""
try:
d_valid = await self.discord_reader.validate()
f_valid = await self.fluxer_writer.validate()
return {
"discord_token": d_valid.get("token", False),
"discord_bot_name": d_valid.get("bot_name"),
"discord_server": d_valid.get("server", False),
"discord_server_name": d_valid.get("server_name"),
"discord_intents": d_valid.get("intents", {}),
"discord_permissions": d_valid.get("permissions", {}),
"fluxer_token": f_valid.get("token", False),
"fluxer_bot_name": f_valid.get("bot_name"),
"fluxer_community": f_valid.get("community", False),
"fluxer_community_name": f_valid.get("community_name"),
"fluxer_permissions": f_valid.get("permissions", {})
}
except Exception as e:
logger.error(f"Validation failed with exception: {e}")
return {
"discord_token": False,
"discord_server": False,
"fluxer_token": False,
"fluxer_community": False
}
async def start_connections(self):
await self.discord_reader.start()
await self.fluxer_writer.start()
async def start_fluxer_only(self):
"""Starts only the Fluxer writer (used for Danger Zone operations that don't need Discord)."""
await self.fluxer_writer.start()
async def close_connections(self):
await self.discord_reader.close()
await self.fluxer_writer.close()
async def close_fluxer_only(self):
"""Closes only the Fluxer writer. Pair with start_fluxer_only()."""
await self.fluxer_writer.close()
def stop(self):
self.is_running = False

166
src/core/clone_server.py Normal file
View file

@ -0,0 +1,166 @@
import asyncio
import logging
from typing import Callable, Awaitable
from src.core.base import MigrationContext
logger = logging.getLogger(__name__)
async def sync_channel_state(context: MigrationContext):
"""
Scans Fluxer for channels matching Discord names and updates state.json mappings.
This prevents duplicate creation when the state.json is empty but channels exist in Fluxer.
"""
categories = await context.discord_reader.get_categories()
channels = await context.discord_reader.get_channels()
fluxer_channels = await context.fluxer_writer.get_channels()
# Build name -> id map and ID set for Fluxer for fast lookup
fluxer_name_map = {c.get("name"): str(c.get("id")) for c in fluxer_channels if c.get("name")}
fluxer_id_set = {str(c.get("id")) for c in fluxer_channels}
updates = 0
removals = 0
# 1. Verify and Sync Categories
for cat in categories:
discord_id = str(cat.id)
fluxer_id = context.state.get_fluxer_category_id(discord_id)
if fluxer_id:
if fluxer_id not in fluxer_id_set:
context.state.remove_category_mapping(discord_id)
removals += 1
elif cat.name in fluxer_name_map:
context.state.set_category_mapping(discord_id, fluxer_name_map[cat.name])
updates += 1
# 2. Verify and Sync Channels
for ch in channels:
discord_id = str(ch.id)
fluxer_id = context.state.get_fluxer_channel_id(discord_id)
if fluxer_id:
if fluxer_id not in fluxer_id_set:
context.state.remove_channel_mapping(discord_id)
removals += 1
elif ch.name in fluxer_name_map:
context.state.set_channel_mapping(discord_id, fluxer_name_map[ch.name])
updates += 1
if updates > 0 or removals > 0:
logger.info(f"Channel sync: {updates} mapped, {removals} stale mappings removed")
async def migrate_channels(context: MigrationContext, progress_callback: Callable[[str, str, int, int], Awaitable[None]] | None = None, force: bool = False):
"""Clones categories and text channels.
Args:
progress_callback: Optional callback receiving (item_name, status, current, total)
force: If True, re-create channels even if they exist in state.
"""
categories = await context.discord_reader.get_categories()
channels = await context.discord_reader.get_channels()
# 1. Identify categories to create
missing_categories = [cat for cat in categories if force or not context.state.get_fluxer_category_id(str(cat.id))]
missing_category_ids = {str(cat.id) for cat in missing_categories}
# 2. Identify channels to create or move
# Fetch current Fluxer state to check parent_ids
fluxer_channels = await context.fluxer_writer.get_channels()
fluxer_parent_map = {str(c["id"]): (str(c.get("parent_id")) if c.get("parent_id") else None) for c in fluxer_channels}
channels_to_create = []
channels_to_move = []
for ch in channels:
discord_id = str(ch.id)
fluxer_id = context.state.get_fluxer_channel_id(discord_id)
if force or not fluxer_id:
# We'll resolve the parent_id in the loop after categories are created
channels_to_create.append(ch)
else:
# Always add to move/sync list to ensure properties (topic, nsfw, slowmode) are synced
# even if the parent category is already correct.
channels_to_move.append((ch, fluxer_id))
total = len(missing_categories) + len(channels_to_create) + len(channels_to_move)
current_idx = 0
if total == 0:
return
# Migrate Categories first
for cat in missing_categories:
if not context.is_running: break
state_key = str(cat.id)
fluxer_id = await context.fluxer_writer.create_channel(cat.name, type=4)
context.state.set_category_mapping(state_key, fluxer_id)
current_idx += 1
if progress_callback: await progress_callback(f"Cat: {cat.name}", "Copying", current_idx, total)
await asyncio.sleep(context.config.migration.rate_limit_delay_seconds)
# Create missing channels
for channel in channels_to_create:
if not context.is_running: break
state_key = str(channel.id)
topic = getattr(channel, 'topic', "") or ""
nsfw = getattr(channel, 'nsfw', False)
slowmode = getattr(channel, 'slowmode_delay', 0)
logger.debug(f"Creating channel {channel.name}: topic={topic}, nsfw={nsfw}, slowmode={slowmode}")
parent_id = context.state.get_fluxer_category_id(str(channel.category_id)) if channel.category_id else None
fluxer_id = await context.fluxer_writer.create_channel(
name=channel.name,
topic=topic,
type=0,
parent_id=parent_id,
nsfw=nsfw,
slowmode_delay=slowmode
)
context.state.set_channel_mapping(state_key, fluxer_id)
# Sync again immediately because some properties (like slowmode) are ignored on creation
await context.fluxer_writer.modify_channel(
channel_id=fluxer_id,
parent_id=parent_id,
name=channel.name,
topic=topic,
nsfw=nsfw,
slowmode_delay=slowmode
)
current_idx += 1
if progress_callback: await progress_callback(channel.name, "Copying", current_idx, total)
await asyncio.sleep(context.config.migration.rate_limit_delay_seconds)
# Move/Sync existing channels
for channel, fluxer_id in channels_to_move:
if not context.is_running: break
parent_id = context.state.get_fluxer_category_id(str(channel.category_id)) if channel.category_id else None
nsfw = getattr(channel, 'nsfw', False)
slowmode = getattr(channel, 'slowmode_delay', 0)
topic = getattr(channel, 'topic', "") or ""
logger.debug(f"Syncing existing channel {channel.name} ({fluxer_id}): topic={topic}, nsfw={nsfw}, slowmode={slowmode}")
await context.fluxer_writer.modify_channel(
channel_id=fluxer_id,
parent_id=parent_id,
name=channel.name,
topic=topic,
nsfw=nsfw,
slowmode_delay=slowmode
)
current_idx += 1
if progress_callback: await progress_callback(channel.name, "Syncing", current_idx, total)
await asyncio.sleep(context.config.migration.rate_limit_delay_seconds)

33
src/core/danger_zone.py Normal file
View file

@ -0,0 +1,33 @@
import logging
from typing import Callable, Awaitable
from src.core.base import MigrationContext
logger = logging.getLogger(__name__)
async def danger_remove_logo_and_banner(context: MigrationContext) -> dict:
"""Removes the community logo and banner image. Returns per-field status."""
return await context.fluxer_writer.remove_community_logo_and_banner()
async def danger_delete_all_channels(context: MigrationContext, progress_callback=None) -> int:
"""Deletes every channel and category in the Fluxer community."""
count = await context.fluxer_writer.delete_all_channels(progress_callback=progress_callback)
context.state.clear_channel_mappings()
context.state.clear_message_history()
return count
async def danger_reset_channel_permissions(context: MigrationContext, progress_callback=None) -> int:
"""Resets all permission overwrites on every channel and category."""
return await context.fluxer_writer.reset_channel_permissions(progress_callback=progress_callback)
async def danger_delete_all_roles(context: MigrationContext, progress_callback=None) -> int:
"""Deletes all deletable roles (skips managed/bot roles and @everyone)."""
count = await context.fluxer_writer.delete_all_roles(progress_callback=progress_callback)
context.state.clear_role_mappings()
return count
async def danger_delete_all_emojis_and_stickers(context: MigrationContext, progress_callback=None) -> dict:
"""Deletes all custom emojis and stickers. Returns {"emojis": int, "stickers": int}."""
counts = await context.fluxer_writer.delete_all_emojis_and_stickers(progress_callback=progress_callback)
context.state.clear_asset_mappings()
return counts

106
src/core/emoji_stickers.py Normal file
View file

@ -0,0 +1,106 @@
import asyncio
import logging
from typing import Callable, Awaitable, List
from src.core.base import MigrationContext
logger = logging.getLogger(__name__)
async def sync_assets_state(context: MigrationContext):
"""
Scans Fluxer for emojis and stickers matching Discord names and updates state.json mappings.
"""
discord_emojis = await context.discord_reader.get_emojis()
discord_stickers = await context.discord_reader.get_stickers()
fluxer_emojis = await context.fluxer_writer.client.get_guild_emojis(context.config.fluxer_community_id)
fluxer_stickers = await context.fluxer_writer.client.get_guild_stickers(context.config.fluxer_community_id)
# Build name -> id maps and ID sets for Fluxer for fast lookup
fluxer_emoji_map = {e.get("name"): str(e.get("id")) for e in fluxer_emojis if e.get("name")}
fluxer_sticker_map = {s.get("name"): str(s.get("id")) for s in fluxer_stickers if s.get("name")}
fluxer_emoji_ids = {str(e.get("id")) for e in fluxer_emojis}
fluxer_sticker_ids = {str(s.get("id")) for s in fluxer_stickers}
updates = 0
removals = 0
# 1. Verify and Sync Emojis
for emoji in discord_emojis:
discord_id = str(emoji.id)
fluxer_id = context.state.get_fluxer_emoji_id(discord_id)
if fluxer_id:
if fluxer_id not in fluxer_emoji_ids:
context.state.remove_emoji_mapping(discord_id)
removals += 1
elif emoji.name in fluxer_emoji_map:
context.state.set_emoji_mapping(discord_id, fluxer_emoji_map[emoji.name])
updates += 1
# 2. Verify and Sync Stickers
for sticker in discord_stickers:
discord_id = str(sticker.id)
fluxer_id = context.state.get_fluxer_sticker_id(discord_id)
if fluxer_id:
if fluxer_id not in fluxer_sticker_ids:
context.state.remove_sticker_mapping(discord_id)
removals += 1
elif sticker.name in fluxer_sticker_map:
context.state.set_sticker_mapping(discord_id, fluxer_sticker_map[sticker.name])
updates += 1
if updates > 0 or removals > 0:
logger.info(f"Asset sync: {updates} mapped, {removals} stale mappings removed")
async def migrate_emojis(context: MigrationContext, progress_callback: Callable[[str, str, int, int], Awaitable[None]] | None = None, types_to_include: List[str] = ["Emoji", "Sticker"], force: bool = False):
"""Copies custom emojis and stickers.
Args:
force: If True, skip state cache and re-copy even if already migrated.
"""
objs = []
if "Emoji" in types_to_include:
emojis = await context.discord_reader.get_emojis()
objs.extend([(e, "Emoji") for e in emojis])
if "Sticker" in types_to_include:
stickers = await context.discord_reader.get_stickers()
objs.extend([(s, "Sticker") for s in stickers])
if not force:
objs = [(obj, obj_type) for obj, obj_type in objs if not (
context.state.get_fluxer_emoji_id(str(obj.id)) if obj_type == "Emoji" else context.state.get_fluxer_sticker_id(str(obj.id))
)]
total = len(objs)
if total == 0:
return
for idx, (obj, obj_type) in enumerate(objs):
if not context.is_running: break
try:
if obj_type == "Emoji":
img_data = await context.discord_reader.download_emoji(obj)
fluxer_id = await context.fluxer_writer.create_emoji(
name=obj.name,
image_bytes=img_data
)
if fluxer_id:
context.state.set_emoji_mapping(str(obj.id), fluxer_id)
else:
img_data = await context.discord_reader.download_sticker(obj)
fluxer_id = await context.fluxer_writer.create_sticker(
name=obj.name,
image_bytes=img_data
)
if fluxer_id:
context.state.set_sticker_mapping(str(obj.id), fluxer_id)
except Exception as e:
logger.error(f"Error downloading/uploading {obj_type.lower()} {obj.name}: {e}")
if progress_callback: await progress_callback(obj.name, obj_type, idx + 1, total)
await asyncio.sleep(context.config.migration.rate_limit_delay_seconds)

View file

@ -1,662 +0,0 @@
import asyncio
import logging
from typing import Callable, Awaitable, List, Dict, Any
from src.config import AppConfig
from src.core.state import MigrationState
from src.discord_bot.reader import DiscordReader
from src.fluxer_bot.writer import FluxerWriter
logger = logging.getLogger(__name__)
class MigrationEngine:
"""Orchestrates reading from Discord and writing to Fluxer."""
def __init__(self, config: AppConfig):
self.config = config
self.state = MigrationState()
self.discord_reader = DiscordReader(
token=config.discord_bot_token,
server_id=config.discord_server_id
)
self.fluxer_writer = FluxerWriter(
token=config.fluxer_bot_token,
community_id=config.fluxer_community_id
)
self.is_running = False
async def validate_all(self) -> Dict[str, Any]:
"""Returns True if both connections are valid."""
try:
d_valid = await self.discord_reader.validate()
f_valid = await self.fluxer_writer.validate()
return {
"discord_token": d_valid.get("token", False),
"discord_bot_name": d_valid.get("bot_name"),
"discord_server": d_valid.get("server", False),
"discord_server_name": d_valid.get("server_name"),
"discord_intents": d_valid.get("intents", {}),
"discord_permissions": d_valid.get("permissions", {}),
"fluxer_token": f_valid.get("token", False),
"fluxer_bot_name": f_valid.get("bot_name"),
"fluxer_community": f_valid.get("community", False),
"fluxer_community_name": f_valid.get("community_name"),
"fluxer_permissions": f_valid.get("permissions", {})
}
except Exception as e:
logger.error(f"Validation failed with exception: {e}")
return {
"discord_token": False,
"discord_server": False,
"fluxer_token": False,
"fluxer_community": False
}
async def start_connections(self):
await self.discord_reader.start()
await self.fluxer_writer.start()
async def start_fluxer_only(self):
"""Starts only the Fluxer writer (used for Danger Zone operations that don't need Discord)."""
await self.fluxer_writer.start()
async def close_connections(self):
await self.discord_reader.close()
await self.fluxer_writer.close()
async def close_fluxer_only(self):
"""Closes only the Fluxer writer. Pair with start_fluxer_only()."""
await self.fluxer_writer.close()
async def sync_server_metadata(self, progress_callback: Callable[[str, str], Awaitable[None]], components: List[str] = ["name", "icon", "banner"]):
"""Syncs the server name, logo and banner."""
metadata = await self.discord_reader.get_server_metadata()
# 1. Sync Name
if "name" in components:
try:
name = metadata.get("name")
await self.fluxer_writer.update_guild_metadata(name=name)
await progress_callback("Server Name", "DONE")
except Exception:
await progress_callback("Server Name", "ERROR")
# 2. Sync Icon
if "icon" in components:
try:
icon_bytes = None
if self.discord_reader.guild and self.discord_reader.guild.icon:
icon_bytes = await self.discord_reader.download_asset(self.discord_reader.guild.icon)
if icon_bytes:
await self.fluxer_writer.update_guild_metadata(icon=icon_bytes)
await progress_callback("Server Icon", "DONE")
else:
await progress_callback("Server Icon", "SKIP")
except Exception:
await progress_callback("Server Icon", "ERROR")
# 3. Sync Banner
if "banner" in components:
try:
banner_bytes = None
if self.discord_reader.guild and self.discord_reader.guild.banner:
banner_bytes = await self.discord_reader.download_asset(self.discord_reader.guild.banner)
if banner_bytes:
await self.fluxer_writer.update_guild_metadata(banner=banner_bytes)
await progress_callback("Server Banner", "DONE")
else:
await progress_callback("Server Banner", "SKIP")
except Exception:
await progress_callback("Server Banner", "ERROR")
async def sync_channel_state(self):
"""
Scans Fluxer for channels matching Discord names and updates state.json mappings.
This prevents duplicate creation when the state.json is empty but channels exist in Fluxer.
"""
categories = await self.discord_reader.get_categories()
channels = await self.discord_reader.get_channels()
fluxer_channels = await self.fluxer_writer.get_channels()
# Build name -> id map and ID set for Fluxer for fast lookup
fluxer_name_map = {c.get("name"): str(c.get("id")) for c in fluxer_channels if c.get("name")}
fluxer_id_set = {str(c.get("id")) for c in fluxer_channels}
updates = 0
removals = 0
# 1. Verify and Sync Categories
for cat in categories:
discord_id = str(cat.id)
fluxer_id = self.state.get_fluxer_category_id(discord_id)
if fluxer_id:
if fluxer_id not in fluxer_id_set:
self.state.remove_category_mapping(discord_id)
removals += 1
elif cat.name in fluxer_name_map:
self.state.set_category_mapping(discord_id, fluxer_name_map[cat.name])
updates += 1
# 2. Verify and Sync Channels
for ch in channels:
discord_id = str(ch.id)
fluxer_id = self.state.get_fluxer_channel_id(discord_id)
if fluxer_id:
if fluxer_id not in fluxer_id_set:
self.state.remove_channel_mapping(discord_id)
removals += 1
elif ch.name in fluxer_name_map:
self.state.set_channel_mapping(discord_id, fluxer_name_map[ch.name])
updates += 1
if updates > 0 or removals > 0:
logger.info(f"Channel sync: {updates} mapped, {removals} stale mappings removed")
async def sync_roles_state(self):
"""
Scans Fluxer for roles matching Discord names and updates state.json mappings.
"""
discord_roles = await self.discord_reader.get_roles()
fluxer_roles = await self.fluxer_writer.client.get_guild_roles(self.config.fluxer_community_id)
# Build name -> id maps and ID sets for Fluxer for fast lookup
fluxer_role_map = {r.get("name"): str(r.get("id")) for r in fluxer_roles if r.get("name")}
fluxer_role_ids = {str(r.get("id")) for r in fluxer_roles}
updates = 0
removals = 0
# Verify and Sync Roles
for role in discord_roles:
discord_id = str(role.id)
fluxer_id = self.state.get_fluxer_role_id(discord_id)
if fluxer_id:
if fluxer_id not in fluxer_role_ids:
self.state.remove_role_mapping(discord_id)
removals += 1
elif role.name in fluxer_role_map:
self.state.set_role_mapping(discord_id, fluxer_role_map[role.name])
updates += 1
if updates > 0 or removals > 0:
logger.info(f"Role sync: {updates} mapped, {removals} stale mappings removed")
async def sync_assets_state(self):
"""
Scans Fluxer for emojis and stickers matching Discord names and updates state.json mappings.
"""
discord_emojis = await self.discord_reader.get_emojis()
discord_stickers = await self.discord_reader.get_stickers()
fluxer_emojis = await self.fluxer_writer.client.get_guild_emojis(self.config.fluxer_community_id)
fluxer_stickers = await self.fluxer_writer.client.get_guild_stickers(self.config.fluxer_community_id)
# Build name -> id maps and ID sets for Fluxer for fast lookup
fluxer_emoji_map = {e.get("name"): str(e.get("id")) for e in fluxer_emojis if e.get("name")}
fluxer_sticker_map = {s.get("name"): str(s.get("id")) for s in fluxer_stickers if s.get("name")}
fluxer_emoji_ids = {str(e.get("id")) for e in fluxer_emojis}
fluxer_sticker_ids = {str(s.get("id")) for s in fluxer_stickers}
updates = 0
removals = 0
# 1. Verify and Sync Emojis
for emoji in discord_emojis:
discord_id = str(emoji.id)
fluxer_id = self.state.get_fluxer_emoji_id(discord_id)
if fluxer_id:
if fluxer_id not in fluxer_emoji_ids:
self.state.remove_emoji_mapping(discord_id)
removals += 1
elif emoji.name in fluxer_emoji_map:
self.state.set_emoji_mapping(discord_id, fluxer_emoji_map[emoji.name])
updates += 1
# 2. Verify and Sync Stickers
for sticker in discord_stickers:
discord_id = str(sticker.id)
fluxer_id = self.state.get_fluxer_sticker_id(discord_id)
if fluxer_id:
if fluxer_id not in fluxer_sticker_ids:
self.state.remove_sticker_mapping(discord_id)
removals += 1
elif sticker.name in fluxer_sticker_map:
self.state.set_sticker_mapping(discord_id, fluxer_sticker_map[sticker.name])
updates += 1
if updates > 0 or removals > 0:
logger.info(f"Asset sync: {updates} mapped, {removals} stale mappings removed")
async def migrate_channels(self, progress_callback: Callable[[str, str, int, int], Awaitable[None]] | None = None, force: bool = False):
"""Clones categories and text channels.
Args:
progress_callback: Optional callback receiving (item_name, status, current, total)
force: If True, re-create channels even if they exist in state.
"""
categories = await self.discord_reader.get_categories()
channels = await self.discord_reader.get_channels()
# 1. Identify categories to create
missing_categories = [cat for cat in categories if force or not self.state.get_fluxer_category_id(str(cat.id))]
missing_category_ids = {str(cat.id) for cat in missing_categories}
# 2. Identify channels to create or move
# Fetch current Fluxer state to check parent_ids
fluxer_channels = await self.fluxer_writer.get_channels()
fluxer_parent_map = {str(c["id"]): (str(c.get("parent_id")) if c.get("parent_id") else None) for c in fluxer_channels}
channels_to_create = []
channels_to_move = []
for ch in channels:
discord_id = str(ch.id)
fluxer_id = self.state.get_fluxer_channel_id(discord_id)
if force or not fluxer_id:
# We'll resolve the parent_id in the loop after categories are created
channels_to_create.append(ch)
else:
# Always add to move/sync list to ensure properties (topic, nsfw, slowmode) are synced
# even if the parent category is already correct.
channels_to_move.append((ch, fluxer_id))
total = len(missing_categories) + len(channels_to_create) + len(channels_to_move)
current_idx = 0
if total == 0:
return
# Migrate Categories first
for cat in missing_categories:
if not self.is_running: break
state_key = str(cat.id)
fluxer_id = await self.fluxer_writer.create_channel(cat.name, type=4)
self.state.set_category_mapping(state_key, fluxer_id)
current_idx += 1
if progress_callback: await progress_callback(f"Cat: {cat.name}", "Copying", current_idx, total)
await asyncio.sleep(self.config.migration.rate_limit_delay_seconds)
# Create missing channels
for channel in channels_to_create:
if not self.is_running: break
state_key = str(channel.id)
topic = getattr(channel, 'topic', "") or ""
nsfw = getattr(channel, 'nsfw', False)
slowmode = getattr(channel, 'slowmode_delay', 0)
logger.debug(f"Creating channel {channel.name}: topic={topic}, nsfw={nsfw}, slowmode={slowmode}")
parent_id = self.state.get_fluxer_category_id(str(channel.category_id)) if channel.category_id else None
fluxer_id = await self.fluxer_writer.create_channel(
name=channel.name,
topic=topic,
type=0,
parent_id=parent_id,
nsfw=nsfw,
slowmode_delay=slowmode
)
self.state.set_channel_mapping(state_key, fluxer_id)
# Sync again immediately because some properties (like slowmode) are ignored on creation
await self.fluxer_writer.modify_channel(
channel_id=fluxer_id,
parent_id=parent_id,
name=channel.name,
topic=topic,
nsfw=nsfw,
slowmode_delay=slowmode
)
current_idx += 1
if progress_callback: await progress_callback(channel.name, "Copying", current_idx, total)
await asyncio.sleep(self.config.migration.rate_limit_delay_seconds)
# Move/Sync existing channels
for channel, fluxer_id in channels_to_move:
if not self.is_running: break
parent_id = self.state.get_fluxer_category_id(str(channel.category_id)) if channel.category_id else None
nsfw = getattr(channel, 'nsfw', False)
slowmode = getattr(channel, 'slowmode_delay', 0)
topic = getattr(channel, 'topic', "") or ""
logger.debug(f"Syncing existing channel {channel.name} ({fluxer_id}): topic={topic}, nsfw={nsfw}, slowmode={slowmode}")
await self.fluxer_writer.modify_channel(
channel_id=fluxer_id,
parent_id=parent_id,
name=channel.name,
topic=topic,
nsfw=nsfw,
slowmode_delay=slowmode
)
current_idx += 1
if progress_callback: await progress_callback(channel.name, "Syncing", current_idx, total)
await asyncio.sleep(self.config.migration.rate_limit_delay_seconds)
async def sync_permissions(self, progress_callback: Callable[[str, int, int], Awaitable[None]] | None = None):
"""Syncs category and channel role overrides/permissions."""
categories = await self.discord_reader.get_categories()
channels = await self.discord_reader.get_channels()
# Only sync for items that are already mapped
categories = [c for c in categories if self.state.get_fluxer_category_id(str(c.id))]
channels = [c for c in channels if self.state.get_fluxer_channel_id(str(c.id))]
total = len(categories) + len(channels)
current_idx = 0
if total == 0:
return
async def _sync_overwrites(discord_item, fluxer_id):
"""Helper to sync role overwrites for a given channel or category."""
for target, overwrite in discord_item.overwrites.items():
if type(target).__name__ == "Role":
discord_role_id = str(target.id)
# Handle @everyone role special case
if discord_role_id == self.config.discord_server_id:
fluxer_role_id = self.config.fluxer_community_id
else:
fluxer_role_id = self.state.get_fluxer_role_id(discord_role_id)
if not fluxer_role_id:
continue
allow_val, deny_val = overwrite.pair()
await self.fluxer_writer.set_channel_permission(
channel_id=fluxer_id,
overwrite_id=fluxer_role_id,
allow=allow_val.value,
deny=deny_val.value,
is_role=True
)
# Sync Category Permissions (Role Overwrites)
for cat in categories:
if not self.is_running: break
fluxer_id = self.state.get_fluxer_category_id(str(cat.id))
if fluxer_id:
try:
await _sync_overwrites(cat, fluxer_id)
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 self.is_running: break
fluxer_id = self.state.get_fluxer_channel_id(str(channel.id))
if fluxer_id:
try:
await _sync_overwrites(channel, fluxer_id)
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)
async def analyze_migration(self, source_channel_id: int, after_message_id: int | None = None, progress_callback: Callable[[int], Awaitable[None]] | None = None) -> Dict[str, int]:
"""
Scans channel history to count messages, threads, and attachments.
"""
stats = {"messages": 0, "threads": 0, "attachments": 0}
async for msg in self.discord_reader.fetch_message_history(source_channel_id, after_id=after_message_id):
if not self.is_running:
break
stats["messages"] += 1
stats["attachments"] += len(msg.attachments)
# Count thread messages and markers
if hasattr(msg, 'thread') and msg.thread:
stats["threads"] += 1
# Recursively count thread content
thread_stats = await self.analyze_migration(msg.thread.id)
stats["messages"] += thread_stats["messages"]
stats["attachments"] += thread_stats["attachments"]
stats["threads"] += thread_stats["threads"] # Nested threads (rare in Discord but possible in forum channels)
if progress_callback and stats["messages"] % 10 == 0:
await progress_callback(stats["messages"])
return stats
async def migrate_messages(self, source_channel_id: int, target_channel_id: str, after_message_id: int | None = None, progress_callback: Callable[[int], Awaitable[None]] | None = None):
"""Migrate messages for a specific channel."""
message_count = 0
async for msg in self.discord_reader.fetch_message_history(source_channel_id, after_id=after_message_id):
if not self.is_running:
break
# Process attachments
files = []
attachments_to_process = list(msg.attachments)
# Check if this message is forwarded
# Discord flags: forwarded (is bit 28 / 0x10000000)
is_forwarded = False
if hasattr(msg.flags, 'forwarded'):
is_forwarded = msg.flags.forwarded
# If forwarded, the content and attachments might be in message_snapshots (discord.py 2.5+)
content = msg.content
if is_forwarded:
logger.debug(f"Detected forwarded message: ID={msg.id}, Flags={msg.flags.value}")
if hasattr(msg, 'message_snapshots') and msg.message_snapshots:
# For now we handle the first snapshot
snapshot = msg.message_snapshots[0]
if not content:
content = snapshot.content
# Add snapshot attachments to the list to process
attachments_to_process.extend(snapshot.attachments)
logger.debug(f"Found forwarded snapshot content: {content[:50]}... and {len(snapshot.attachments)} attachments")
for att in attachments_to_process:
try:
att_data = await self.discord_reader.download_attachment(att)
files.append({"filename": att.filename, "data": att_data})
except Exception as e:
logger.error(f"Failed to download attachment {att.filename}: {e}")
try:
# Check if this message is a reply
reply_to_fluxer_id = None
if msg.reference and msg.reference.message_id:
reply_to_fluxer_id = self.state.get_fluxer_message_id(str(msg.reference.message_id))
fluxer_msg_id = await self.fluxer_writer.send_message(
channel_id=target_channel_id,
author_name=msg.author.display_name,
author_avatar_url=str(msg.author.display_avatar.url),
content=content,
timestamp=msg.created_at.strftime("%Y-%m-%d %H:%M:%S"),
files=files if files else None,
reply_to_message_id=reply_to_fluxer_id,
is_forwarded=is_forwarded
)
if fluxer_msg_id:
self.state.set_message_mapping(str(msg.id), fluxer_msg_id)
# Check for associated thread
if hasattr(msg, 'thread') and msg.thread:
thread = msg.thread
logger.info(f"Detected thread '{thread.name}' on message {msg.id}")
# Send Start Marker
await self.fluxer_writer.send_marker(
channel_id=target_channel_id,
content=f"> <<< THREAD: **{thread.name}** >>>"
)
# Migrate thread messages
# We don't pass a progress callback here to avoid confusing the UI
# but we do want to track count if possible.
await self.migrate_messages(
source_channel_id=thread.id,
target_channel_id=target_channel_id
)
# Send End Marker
await self.fluxer_writer.send_marker(
channel_id=target_channel_id,
content=f"> <<< END OF THREAD >>>"
)
self.state.update_last_message_timestamp(str(source_channel_id), str(msg.created_at))
message_count += 1
if progress_callback:
await progress_callback(message_count)
except Exception as e:
logger.error(f"Failed to process message {msg.id}: {e}")
import traceback
logger.error(traceback.format_exc())
# Delay for rate limit safety
await asyncio.sleep(self.config.migration.rate_limit_delay_seconds)
return message_count
async def migrate_roles(self, progress_callback: Callable[[str, int, int], Awaitable[None]] | None = None, force: bool = False):
"""Copies roles and their baseline permissions."""
roles = await self.discord_reader.get_roles()
if not force:
roles = [r for r in roles if not self.state.get_fluxer_role_id(str(r.id))]
total = len(roles)
if total == 0:
return
for idx, role in enumerate(roles):
if not self.is_running: break
fluxer_id = await self.fluxer_writer.create_role(
name=role.name,
color=role.color.value,
hoist=role.hoist,
mentionable=role.mentionable
)
if fluxer_id:
self.state.set_role_mapping(str(role.id), fluxer_id)
if progress_callback: await progress_callback(role.name, idx + 1, total)
await asyncio.sleep(self.config.migration.rate_limit_delay_seconds)
async def migrate_emojis(self, progress_callback: Callable[[str, str, int, int], Awaitable[None]] | None = None, types_to_include: List[str] = ["Emoji", "Sticker"], force: bool = False):
"""Copies custom emojis and stickers.
Args:
force: If True, skip state cache and re-copy even if already migrated.
"""
objs = []
if "Emoji" in types_to_include:
emojis = await self.discord_reader.get_emojis()
objs.extend([(e, "Emoji") for e in emojis])
if "Sticker" in types_to_include:
stickers = await self.discord_reader.get_stickers()
objs.extend([(s, "Sticker") for s in stickers])
if not force:
objs = [(obj, obj_type) for obj, obj_type in objs if not (
self.state.get_fluxer_emoji_id(str(obj.id)) if obj_type == "Emoji" else self.state.get_fluxer_sticker_id(str(obj.id))
)]
total = len(objs)
if total == 0:
return
for idx, (obj, obj_type) in enumerate(objs):
if not self.is_running: break
try:
if obj_type == "Emoji":
img_data = await self.discord_reader.download_emoji(obj)
fluxer_id = await self.fluxer_writer.create_emoji(
name=obj.name,
image_bytes=img_data
)
if fluxer_id:
self.state.set_emoji_mapping(str(obj.id), fluxer_id)
else:
img_data = await self.discord_reader.download_sticker(obj)
fluxer_id = await self.fluxer_writer.create_sticker(
name=obj.name,
image_bytes=img_data
)
if fluxer_id:
self.state.set_sticker_mapping(str(obj.id), fluxer_id)
except Exception as e:
logger.error(f"Error downloading/uploading {obj_type.lower()} {obj.name}: {e}")
if progress_callback: await progress_callback(obj.name, obj_type, idx + 1, total)
await asyncio.sleep(self.config.migration.rate_limit_delay_seconds)
async def run_full_migration(self):
self.is_running = True
try:
await self.start_connections()
await self.migrate_channels()
# Example: just migrate one channel's messages to test
channels = await self.discord_reader.get_channels()
if channels:
await self.migrate_messages(channels[0].id)
finally:
await self.close_connections()
self.is_running = False
def stop(self):
self.is_running = False
# ──────────────── DANGER ZONE ────────────────
async def danger_remove_logo_and_banner(self) -> dict:
"""Removes the community logo and banner image. Returns per-field status."""
return await self.fluxer_writer.remove_community_logo_and_banner()
async def danger_delete_all_channels(self, progress_callback=None) -> int:
"""Deletes every channel and category in the Fluxer community."""
count = await self.fluxer_writer.delete_all_channels(progress_callback=progress_callback)
self.state.clear_channel_mappings()
self.state.clear_message_history()
return count
async def danger_reset_channel_permissions(self, progress_callback=None) -> int:
"""Resets all permission overwrites on every channel and category."""
return await self.fluxer_writer.reset_channel_permissions(progress_callback=progress_callback)
async def danger_delete_all_roles(self, progress_callback=None) -> int:
"""Deletes all deletable roles (skips managed/bot roles and @everyone)."""
count = await self.fluxer_writer.delete_all_roles(progress_callback=progress_callback)
self.state.clear_role_mappings()
return count
async def danger_delete_all_emojis_and_stickers(self, progress_callback=None) -> dict:
"""Deletes all custom emojis and stickers. Returns {"emojis": int, "stickers": int}."""
counts = await self.fluxer_writer.delete_all_emojis_and_stickers(progress_callback=progress_callback)
self.state.clear_asset_mappings()
return counts

View file

@ -246,7 +246,7 @@ class FluxerWriter:
if webhook and not files and not reply_to_message_id:
msg = await webhook.send(
content=final_content,
username=f"{author_name} (via Discord)",
username=f"{author_name} (discord)",
avatar_url=author_avatar_url,
wait=True
)

132
src/core/migrate_message.py Normal file
View file

@ -0,0 +1,132 @@
import asyncio
import logging
from typing import Callable, Awaitable, Dict
from src.core.base import MigrationContext
logger = logging.getLogger(__name__)
async def analyze_migration(context: MigrationContext, source_channel_id: int, after_message_id: int | None = None, progress_callback: Callable[[int], Awaitable[None]] | None = None) -> Dict[str, int]:
"""
Scans channel history to count messages, threads, and attachments.
"""
stats = {"messages": 0, "threads": 0, "attachments": 0}
async for msg in context.discord_reader.fetch_message_history(source_channel_id, after_id=after_message_id):
if not context.is_running:
break
stats["messages"] += 1
stats["attachments"] += len(msg.attachments)
# Count thread messages and markers
if hasattr(msg, 'thread') and msg.thread:
stats["threads"] += 1
# Recursively count thread content
thread_stats = await analyze_migration(context, msg.thread.id)
stats["messages"] += thread_stats["messages"]
stats["attachments"] += thread_stats["attachments"]
stats["threads"] += thread_stats["threads"] # Nested threads (rare in Discord but possible in forum channels)
if progress_callback and stats["messages"] % 10 == 0:
await progress_callback(stats["messages"])
return stats
async def migrate_messages(context: MigrationContext, source_channel_id: int, target_channel_id: str, after_message_id: int | None = None, progress_callback: Callable[[int], Awaitable[None]] | None = None):
"""Migrate messages for a specific channel."""
message_count = 0
async for msg in context.discord_reader.fetch_message_history(source_channel_id, after_id=after_message_id):
if not context.is_running:
break
# Process attachments
files = []
attachments_to_process = list(msg.attachments)
# Check if this message is forwarded
# Discord flags: forwarded (is bit 28 / 0x10000000)
is_forwarded = False
if hasattr(msg.flags, 'forwarded'):
is_forwarded = msg.flags.forwarded
# If forwarded, the content and attachments might be in message_snapshots (discord.py 2.5+)
content = msg.content
if is_forwarded:
logger.debug(f"Detected forwarded message: ID={msg.id}, Flags={msg.flags.value}")
if hasattr(msg, 'message_snapshots') and msg.message_snapshots:
# For now we handle the first snapshot
snapshot = msg.message_snapshots[0]
if not content:
content = snapshot.content
# Add snapshot attachments to the list to process
attachments_to_process.extend(snapshot.attachments)
logger.debug(f"Found forwarded snapshot content: {content[:50]}... and {len(snapshot.attachments)} attachments")
for att in attachments_to_process:
try:
att_data = await context.discord_reader.download_attachment(att)
files.append({"filename": att.filename, "data": att_data})
except Exception as e:
logger.error(f"Failed to download attachment {att.filename}: {e}")
try:
# Check if this message is a reply
reply_to_fluxer_id = None
if msg.reference and msg.reference.message_id:
reply_to_fluxer_id = context.state.get_fluxer_message_id(str(msg.reference.message_id))
fluxer_msg_id = await context.fluxer_writer.send_message(
channel_id=target_channel_id,
author_name=msg.author.display_name,
author_avatar_url=str(msg.author.display_avatar.url),
content=content,
timestamp=msg.created_at.strftime("%Y-%m-%d %H:%M:%S"),
files=files if files else None,
reply_to_message_id=reply_to_fluxer_id,
is_forwarded=is_forwarded
)
if fluxer_msg_id:
context.state.set_message_mapping(str(msg.id), fluxer_msg_id)
# Check for associated thread
if hasattr(msg, 'thread') and msg.thread:
thread = msg.thread
logger.info(f"Detected thread '{thread.name}' on message {msg.id}")
# Send Start Marker
await context.fluxer_writer.send_marker(
channel_id=target_channel_id,
content=f"> <<< THREAD: **{thread.name}** >>>"
)
# Migrate thread messages
# We don't pass a progress callback here to avoid confusing the UI
# but we do want to track count if possible.
await migrate_messages(
context=context,
source_channel_id=thread.id,
target_channel_id=target_channel_id
)
# Send End Marker
await context.fluxer_writer.send_marker(
channel_id=target_channel_id,
content=f"> <<< END OF THREAD >>>"
)
context.state.update_last_message_timestamp(str(source_channel_id), str(msg.created_at))
message_count += 1
if progress_callback:
await progress_callback(message_count)
except Exception as e:
logger.error(f"Failed to process message {msg.id}: {e}")
import traceback
logger.error(traceback.format_exc())
# Delay for rate limit safety
await asyncio.sleep(context.config.migration.rate_limit_delay_seconds)
return message_count

View file

@ -0,0 +1,130 @@
import asyncio
import logging
from typing import Callable, Awaitable
from src.core.base import MigrationContext
logger = logging.getLogger(__name__)
async def sync_roles_state(context: MigrationContext):
"""
Scans Fluxer for roles matching Discord names and updates state.json mappings.
"""
discord_roles = await context.discord_reader.get_roles()
fluxer_roles = await context.fluxer_writer.client.get_guild_roles(context.config.fluxer_community_id)
# Build name -> id maps and ID sets for Fluxer for fast lookup
fluxer_role_map = {r.get("name"): str(r.get("id")) for r in fluxer_roles if r.get("name")}
fluxer_role_ids = {str(r.get("id")) for r in fluxer_roles}
updates = 0
removals = 0
# Verify and Sync Roles
for role in discord_roles:
discord_id = str(role.id)
fluxer_id = context.state.get_fluxer_role_id(discord_id)
if fluxer_id:
if fluxer_id not in fluxer_role_ids:
context.state.remove_role_mapping(discord_id)
removals += 1
elif role.name in fluxer_role_map:
context.state.set_role_mapping(discord_id, fluxer_role_map[role.name])
updates += 1
if updates > 0 or removals > 0:
logger.info(f"Role sync: {updates} mapped, {removals} stale mappings removed")
async def sync_permissions(context: MigrationContext, progress_callback: Callable[[str, int, int], Awaitable[None]] | None = None):
"""Syncs category and channel role overrides/permissions."""
categories = await context.discord_reader.get_categories()
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_fluxer_category_id(str(c.id))]
channels = [c for c in channels if context.state.get_fluxer_channel_id(str(c.id))]
total = len(categories) + len(channels)
current_idx = 0
if total == 0:
return
async def _sync_overwrites(discord_item, fluxer_id):
"""Helper to sync role overwrites for a given channel or category."""
for target, overwrite in discord_item.overwrites.items():
if type(target).__name__ == "Role":
discord_role_id = str(target.id)
# Handle @everyone role special case
if discord_role_id == context.config.discord_server_id:
fluxer_role_id = context.config.fluxer_community_id
else:
fluxer_role_id = context.state.get_fluxer_role_id(discord_role_id)
if not fluxer_role_id:
continue
allow_val, deny_val = overwrite.pair()
await context.fluxer_writer.set_channel_permission(
channel_id=fluxer_id,
overwrite_id=fluxer_role_id,
allow=allow_val.value,
deny=deny_val.value,
is_role=True
)
# Sync Category Permissions (Role Overwrites)
for cat in categories:
if not context.is_running: break
fluxer_id = context.state.get_fluxer_category_id(str(cat.id))
if fluxer_id:
try:
await _sync_overwrites(cat, fluxer_id)
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
fluxer_id = context.state.get_fluxer_channel_id(str(channel.id))
if fluxer_id:
try:
await _sync_overwrites(channel, fluxer_id)
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)
async def migrate_roles(context: MigrationContext, progress_callback: Callable[[str, int, int], Awaitable[None]] | None = None, force: bool = False):
"""Copies roles and their baseline permissions."""
roles = await context.discord_reader.get_roles()
if not force:
roles = [r for r in roles if not context.state.get_fluxer_role_id(str(r.id))]
total = len(roles)
if total == 0:
return
for idx, role in enumerate(roles):
if not context.is_running: break
fluxer_id = await context.fluxer_writer.create_role(
name=role.name,
color=role.color.value,
hoist=role.hoist,
mentionable=role.mentionable
)
if fluxer_id:
context.state.set_role_mapping(str(role.id), fluxer_id)
if progress_callback: await progress_callback(role.name, idx + 1, total)
await asyncio.sleep(context.config.migration.rate_limit_delay_seconds)

View file

@ -0,0 +1,49 @@
import logging
from typing import Callable, Awaitable, List
from src.core.base import MigrationContext
logger = logging.getLogger(__name__)
async def sync_server_metadata(context: MigrationContext, progress_callback: Callable[[str, str], Awaitable[None]], components: List[str] = ["name", "icon", "banner"]):
"""Syncs the server name, logo and banner."""
metadata = await context.discord_reader.get_server_metadata()
# 1. Sync Name
if "name" in components:
try:
name = metadata.get("name")
await context.fluxer_writer.update_guild_metadata(name=name)
await progress_callback("Server Name", "DONE")
except Exception:
await progress_callback("Server Name", "ERROR")
# 2. Sync Icon
if "icon" in components:
try:
icon_bytes = None
if context.discord_reader.guild and context.discord_reader.guild.icon:
icon_bytes = await context.discord_reader.download_asset(context.discord_reader.guild.icon)
if icon_bytes:
await context.fluxer_writer.update_guild_metadata(icon=icon_bytes)
await progress_callback("Server Icon", "DONE")
else:
await progress_callback("Server Icon", "SKIP")
except Exception:
await progress_callback("Server Icon", "ERROR")
# 3. Sync Banner
if "banner" in components:
try:
banner_bytes = None
if context.discord_reader.guild and context.discord_reader.guild.banner:
banner_bytes = await context.discord_reader.download_asset(context.discord_reader.guild.banner)
if banner_bytes:
await context.fluxer_writer.update_guild_metadata(banner=banner_bytes)
await progress_callback("Server Banner", "DONE")
else:
await progress_callback("Server Banner", "SKIP")
except Exception:
await progress_callback("Server Banner", "ERROR")

View file

@ -7,8 +7,14 @@ from rich.prompt import Prompt, Confirm
from rich.table import Table
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
from src.config import load_config, save_config
from src.core.engine import MigrationEngine
from src.core.configuration import load_config, save_config
from src.core.base import MigrationContext
from src.core.clone_server import sync_channel_state, migrate_channels
from src.core.roles_permissions import sync_roles_state, sync_permissions, migrate_roles
from src.core.emoji_stickers import sync_assets_state, migrate_emojis
from src.core.server_metadata import sync_server_metadata
from src.core.migrate_message import analyze_migration, migrate_messages
from src.core.danger_zone import danger_remove_logo_and_banner, danger_delete_all_channels, danger_reset_channel_permissions, danger_delete_all_roles, danger_delete_all_emojis_and_stickers
class RateLimitHandler(logging.Handler):
"""Intersects library logs to print clean rate limit messages."""
@ -55,7 +61,7 @@ class MigrationCLI:
console.print(f"[bold red]Failed to load config: {e}[/bold red]")
sys.exit(1)
self.engine = MigrationEngine(self.config)
self.engine = MigrationContext(self.config)
self.progress_callback_task = None
self.tokens_valid = False
@ -284,7 +290,7 @@ class MigrationCLI:
save_config(self.config)
# Recreate engine with new config
self.engine = MigrationEngine(self.config)
self.engine = MigrationContext(self.config)
# Re-validate
console.print("[yellow]Validating new configuration...[/yellow]")
@ -309,7 +315,7 @@ class MigrationCLI:
try:
await self.engine.start_connections()
with console.status("[yellow]Syncing Fluxer channel state...[/yellow]"):
await self.engine.sync_channel_state()
await sync_channel_state(self.engine)
categories = await self.engine.discord_reader.get_categories()
channels = await self.engine.discord_reader.get_channels()
except Exception as e:
@ -408,7 +414,7 @@ class MigrationCLI:
progress.update(channel_task, total=total, completed=current, description=f"[{color}]{status} Channel: {item_name}")
self.engine.is_running = True
await self.engine.migrate_channels(progress_callback=update_progress, force=force)
await migrate_channels(self.engine, progress_callback=update_progress, force=force)
console.print("[bold green]Server Template cloned![/bold green]")
@ -434,7 +440,7 @@ class MigrationCLI:
await self.engine.start_connections()
with console.status("[yellow]Checking Fluxer for existing roles...[/yellow]"):
await self.engine.sync_roles_state()
await sync_roles_state(self.engine)
roles = await self.engine.discord_reader.get_roles()
@ -492,7 +498,7 @@ class MigrationCLI:
progress.update(role_task, total=total, completed=current, description=f"[cyan]Syncing Role: {item_name}")
self.engine.is_running = True
await self.engine.migrate_roles(progress_callback=update_progress, force=force)
await migrate_roles(self.engine, progress_callback=update_progress, force=force)
console.print("[bold green]Role migration complete![/bold green]")
@ -538,7 +544,7 @@ class MigrationCLI:
progress.update(perm_task, total=total, completed=current, description=f"[cyan]Syncing: {item_name}")
self.engine.is_running = True
await self.engine.sync_permissions(progress_callback=update_progress)
await sync_permissions(self.engine, progress_callback=update_progress)
console.print("[bold green]Permission synchronization complete![/bold green]")
@ -554,7 +560,7 @@ class MigrationCLI:
await self.engine.start_connections()
with console.status("[yellow]Checking Fluxer for existing emojis and stickers...[/yellow]"):
await self.engine.sync_assets_state()
await sync_assets_state(self.engine)
emojis = await self.engine.discord_reader.get_emojis()
stickers = await self.engine.discord_reader.get_stickers()
@ -642,7 +648,8 @@ class MigrationCLI:
progress.update(emoji_task, total=total, completed=current, description=f"[cyan]Copying {item_type}: {item_name}")
self.engine.is_running = True
await self.engine.migrate_emojis(
await migrate_emojis(
self.engine,
progress_callback=update_progress,
types_to_include=types_to_include,
force=force
@ -706,7 +713,7 @@ class MigrationCLI:
color = "green" if status == "DONE" else "red" if status == "ERROR" else "yellow"
console.print(f"{item} [[bold {color}]{status}[/bold {color}]]")
await self.engine.sync_server_metadata(progress_callback, components=components)
await sync_server_metadata(self.engine, progress_callback, components=components)
console.print("[bold green]Server metadata sync finished![/bold green]")
except Exception as e:
console.print(f"[bold red]Error during metadata sync: {str(e)}[/bold red]")
@ -876,7 +883,8 @@ class MigrationCLI:
async def update_scan_progress(count: int):
progress.update(task, description=f"[cyan]Scanned {count} items...")
stats = await self.engine.analyze_migration(
stats = await analyze_migration(
self.engine,
source_channel_id=source_channel.id,
after_message_id=after_id,
progress_callback=update_scan_progress
@ -928,7 +936,8 @@ class MigrationCLI:
async def update_msg_progress(count: int):
progress.update(task, description=f"[cyan]Migrated {count} messages...")
count = await self.engine.migrate_messages(
count = await migrate_messages(
self.engine,
source_channel_id=source_channel.id,
target_channel_id=target_channel.get("id"),
after_message_id=after_id,
@ -990,7 +999,7 @@ class MigrationCLI:
progress.update(del_task, total=total, completed=current,
description=f"[red]Deleting: {name}")
count = await self.engine.danger_delete_all_channels(progress_callback=on_channel_deleted)
count = await danger_delete_all_channels(self.engine, progress_callback=on_channel_deleted)
console.print(f"[bold green]{count} channels/categories deleted.[/bold green]")
console.print("[bold green]Done.[/bold green]")
except Exception as e:
@ -1021,7 +1030,7 @@ class MigrationCLI:
progress.update(perm_task, total=total, completed=current,
description=f"[red]Resetting: {name}")
count = await self.engine.danger_reset_channel_permissions(progress_callback=on_perm_reset)
count = await danger_reset_channel_permissions(self.engine, progress_callback=on_perm_reset)
console.print(f"[bold green]Permissions reset on {count} channels/categories.[/bold green]")
console.print("[bold green]Done.[/bold green]")
except Exception as e:
@ -1053,7 +1062,7 @@ class MigrationCLI:
progress.update(role_task, total=total, completed=current,
description=f"[red]Deleting role: {name}")
count = await self.engine.danger_delete_all_roles(progress_callback=on_role_deleted)
count = await danger_delete_all_roles(self.engine, progress_callback=on_role_deleted)
console.print(f"[bold green]{count} roles deleted.[/bold green]")
console.print("[bold green]Done.[/bold green]")
except Exception as e:
@ -1084,7 +1093,7 @@ class MigrationCLI:
progress.update(asset_task, total=total, completed=current,
description=f"[red]Deleting {asset_type}: {name}")
counts = await self.engine.danger_delete_all_emojis_and_stickers(progress_callback=on_asset_deleted)
counts = await danger_delete_all_emojis_and_stickers(self.engine, progress_callback=on_asset_deleted)
console.print(f"[bold green]{counts.get('emojis', 0)} emojis deleted.[/bold green]")
console.print(f"[bold green]{counts.get('stickers', 0)} stickers deleted.[/bold green]")
console.print("[bold green]Done.[/bold green]")