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 asyncio
import logging import logging
from src.ui.app import run_cli from src.ui.app import run_cli
from src.config import load_config from src.core.configuration import load_config
def setup_logging(): def setup_logging():
try: 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: if webhook and not files and not reply_to_message_id:
msg = await webhook.send( msg = await webhook.send(
content=final_content, content=final_content,
username=f"{author_name} (via Discord)", username=f"{author_name} (discord)",
avatar_url=author_avatar_url, avatar_url=author_avatar_url,
wait=True 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.table import Table
from rich.panel import Panel from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
from src.config import load_config, save_config from src.core.configuration import load_config, save_config
from src.core.engine import MigrationEngine 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): class RateLimitHandler(logging.Handler):
"""Intersects library logs to print clean rate limit messages.""" """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]") console.print(f"[bold red]Failed to load config: {e}[/bold red]")
sys.exit(1) sys.exit(1)
self.engine = MigrationEngine(self.config) self.engine = MigrationContext(self.config)
self.progress_callback_task = None self.progress_callback_task = None
self.tokens_valid = False self.tokens_valid = False
@ -284,7 +290,7 @@ class MigrationCLI:
save_config(self.config) save_config(self.config)
# Recreate engine with new config # Recreate engine with new config
self.engine = MigrationEngine(self.config) self.engine = MigrationContext(self.config)
# Re-validate # Re-validate
console.print("[yellow]Validating new configuration...[/yellow]") console.print("[yellow]Validating new configuration...[/yellow]")
@ -309,7 +315,7 @@ class MigrationCLI:
try: try:
await self.engine.start_connections() await self.engine.start_connections()
with console.status("[yellow]Syncing Fluxer channel state...[/yellow]"): 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() categories = await self.engine.discord_reader.get_categories()
channels = await self.engine.discord_reader.get_channels() channels = await self.engine.discord_reader.get_channels()
except Exception as e: 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}") progress.update(channel_task, total=total, completed=current, description=f"[{color}]{status} Channel: {item_name}")
self.engine.is_running = True 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]") console.print("[bold green]Server Template cloned![/bold green]")
@ -434,7 +440,7 @@ class MigrationCLI:
await self.engine.start_connections() await self.engine.start_connections()
with console.status("[yellow]Checking Fluxer for existing roles...[/yellow]"): 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() 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}") progress.update(role_task, total=total, completed=current, description=f"[cyan]Syncing Role: {item_name}")
self.engine.is_running = True 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]") 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}") progress.update(perm_task, total=total, completed=current, description=f"[cyan]Syncing: {item_name}")
self.engine.is_running = True 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]") console.print("[bold green]Permission synchronization complete![/bold green]")
@ -554,7 +560,7 @@ class MigrationCLI:
await self.engine.start_connections() await self.engine.start_connections()
with console.status("[yellow]Checking Fluxer for existing emojis and stickers...[/yellow]"): 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() emojis = await self.engine.discord_reader.get_emojis()
stickers = await self.engine.discord_reader.get_stickers() 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}") progress.update(emoji_task, total=total, completed=current, description=f"[cyan]Copying {item_type}: {item_name}")
self.engine.is_running = True self.engine.is_running = True
await self.engine.migrate_emojis( await migrate_emojis(
self.engine,
progress_callback=update_progress, progress_callback=update_progress,
types_to_include=types_to_include, types_to_include=types_to_include,
force=force force=force
@ -706,7 +713,7 @@ class MigrationCLI:
color = "green" if status == "DONE" else "red" if status == "ERROR" else "yellow" color = "green" if status == "DONE" else "red" if status == "ERROR" else "yellow"
console.print(f"{item} [[bold {color}]{status}[/bold {color}]]") 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]") console.print("[bold green]Server metadata sync finished![/bold green]")
except Exception as e: except Exception as e:
console.print(f"[bold red]Error during metadata sync: {str(e)}[/bold red]") 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): async def update_scan_progress(count: int):
progress.update(task, description=f"[cyan]Scanned {count} items...") 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, source_channel_id=source_channel.id,
after_message_id=after_id, after_message_id=after_id,
progress_callback=update_scan_progress progress_callback=update_scan_progress
@ -928,7 +936,8 @@ class MigrationCLI:
async def update_msg_progress(count: int): async def update_msg_progress(count: int):
progress.update(task, description=f"[cyan]Migrated {count} messages...") 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, source_channel_id=source_channel.id,
target_channel_id=target_channel.get("id"), target_channel_id=target_channel.get("id"),
after_message_id=after_id, after_message_id=after_id,
@ -990,7 +999,7 @@ class MigrationCLI:
progress.update(del_task, total=total, completed=current, progress.update(del_task, total=total, completed=current,
description=f"[red]Deleting: {name}") 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(f"[bold green]{count} channels/categories deleted.[/bold green]")
console.print("[bold green]Done.[/bold green]") console.print("[bold green]Done.[/bold green]")
except Exception as e: except Exception as e:
@ -1021,7 +1030,7 @@ class MigrationCLI:
progress.update(perm_task, total=total, completed=current, progress.update(perm_task, total=total, completed=current,
description=f"[red]Resetting: {name}") 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(f"[bold green]Permissions reset on {count} channels/categories.[/bold green]")
console.print("[bold green]Done.[/bold green]") console.print("[bold green]Done.[/bold green]")
except Exception as e: except Exception as e:
@ -1053,7 +1062,7 @@ class MigrationCLI:
progress.update(role_task, total=total, completed=current, progress.update(role_task, total=total, completed=current,
description=f"[red]Deleting role: {name}") 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(f"[bold green]{count} roles deleted.[/bold green]")
console.print("[bold green]Done.[/bold green]") console.print("[bold green]Done.[/bold green]")
except Exception as e: except Exception as e:
@ -1084,7 +1093,7 @@ class MigrationCLI:
progress.update(asset_task, total=total, completed=current, progress.update(asset_task, total=total, completed=current,
description=f"[red]Deleting {asset_type}: {name}") 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('emojis', 0)} emojis deleted.[/bold green]")
console.print(f"[bold green]{counts.get('stickers', 0)} stickers deleted.[/bold green]") console.print(f"[bold green]{counts.get('stickers', 0)} stickers deleted.[/bold green]")
console.print("[bold green]Done.[/bold green]") console.print("[bold green]Done.[/bold green]")