update config to support multiple targets simultaneously

This commit is contained in:
rambros 2026-03-19 23:53:43 +05:30
parent 806966450b
commit 4b6e916425
4 changed files with 115 additions and 47 deletions

View file

@ -8,9 +8,12 @@ class AppConfig(BaseModel):
discord_server_id: Optional[str] = Field(default=None)
tool_mode: str = Field(default="direct_transfer") # direct_transfer | backup_transfer | backup_only
target_platform: str = Field(default="fluxer") # fluxer | stoat | none
target_bot_token: Optional[str] = Field(default=None)
target_server_id: Optional[str] = Field(default=None)
target_api_url: Optional[str] = Field(default=None)
fluxer_bot_token: Optional[str] = Field(default=None)
fluxer_server_id: Optional[str] = Field(default=None)
fluxer_api_url: Optional[str] = Field(default=None)
stoat_bot_token: Optional[str] = Field(default=None)
stoat_server_id: Optional[str] = Field(default=None)
stoat_api_url: Optional[str] = Field(default=None)
anonymize_users: bool = Field(default=False)
log_level: str = Field(default="DEBUG")
@ -27,28 +30,20 @@ class AppConfig(BaseModel):
return self.target_platform == "stoat"
@property
def fluxer_bot_token(self) -> Optional[str]:
return self.target_bot_token if self.target_platform == "fluxer" else None
def target_bot_token(self) -> Optional[str]:
return self.fluxer_bot_token if self.target_platform == "fluxer" else self.stoat_bot_token
@property
def target_server_id(self) -> Optional[str]:
return self.fluxer_server_id if self.target_platform == "fluxer" else self.stoat_server_id
@property
def target_api_url(self) -> Optional[str]:
return self.fluxer_api_url if self.target_platform == "fluxer" else self.stoat_api_url
@property
def fluxer_community_id(self) -> Optional[str]:
return self.target_server_id if self.target_platform == "fluxer" else None
@property
def fluxer_api_url(self) -> Optional[str]:
return self.target_api_url if self.target_platform == "fluxer" else None
@property
def stoat_bot_token(self) -> Optional[str]:
return self.target_bot_token if self.target_platform == "stoat" else None
@property
def stoat_server_id(self) -> Optional[str]:
return self.target_server_id if self.target_platform == "stoat" else None
@property
def stoat_api_url(self) -> Optional[str]:
return self.target_api_url if self.target_platform == "stoat" else None
return self.fluxer_server_id
def load_config(config_path: Union[str, Path] = "config.yaml", create_if_missing: bool = True) -> AppConfig:
path = Path(config_path)
@ -67,22 +62,22 @@ def load_config(config_path: Union[str, Path] = "config.yaml", create_if_missing
if not data:
raise ValueError("Configuration file is empty or invalid YAML.")
# ── migrate legacy configs that still have separate fluxer/stoat fields ──
if "fluxer_bot_token" in data or "stoat_bot_token" in data:
if data.get("fluxer_bot_token") and data["fluxer_bot_token"] not in ("FLUXER_BOT_TOKEN", None):
data.setdefault("target_platform", "fluxer")
data.setdefault("target_bot_token", data["fluxer_bot_token"])
data.setdefault("target_server_id", data.get("fluxer_community_id"))
data.setdefault("target_api_url", data.get("fluxer_api_url") or "default")
elif data.get("stoat_bot_token") and data["stoat_bot_token"] not in ("STOAT_BOT_TOKEN", None):
data.setdefault("target_platform", "stoat")
data.setdefault("target_bot_token", data["stoat_bot_token"])
data.setdefault("target_server_id", data.get("stoat_server_id"))
data.setdefault("target_api_url", data.get("stoat_api_url") or "default")
# Remove legacy keys so they don't conflict with the model
for key in ("fluxer_bot_token", "fluxer_community_id", "fluxer_api_url",
"stoat_bot_token", "stoat_server_id", "stoat_api_url",
"use_fluxer", "use_stoat"):
# ── migrate legacy configs that used single target fields ──
if "fluxer_community_id" in data:
data.setdefault("fluxer_server_id", data.pop("fluxer_community_id"))
if "target_bot_token" in data or "target_server_id" in data:
platform = data.get("target_platform", "fluxer")
if platform == "fluxer":
data.setdefault("fluxer_bot_token", data.get("target_bot_token"))
data.setdefault("fluxer_server_id", data.get("target_server_id"))
data.setdefault("fluxer_api_url", data.get("target_api_url"))
elif platform == "stoat":
data.setdefault("stoat_bot_token", data.get("target_bot_token"))
data.setdefault("stoat_server_id", data.get("target_server_id"))
data.setdefault("stoat_api_url", data.get("target_api_url"))
for key in ("target_bot_token", "target_server_id", "target_api_url", "use_fluxer", "use_stoat"):
data.pop(key, None)
return AppConfig(**data)

View file

@ -14,10 +14,9 @@ class MigrationDatabase:
Replaces the memory-bloated and O(N^2) JSON persistence for messages.
"""
_local = threading.local()
def __init__(self, db_path: Path):
self.db_path = db_path
self._local = threading.local()
self._init_db()
def _get_conn(self) -> sqlite3.Connection:

View file

@ -134,7 +134,7 @@ class ConfigSelectionScreen(Screen):
yield Header(show_clock=True)
with Center():
with Container(id="config_sel_container"):
yield Label(f"{get_app_version()} — Select Configuration", id="config_sel_title")
yield Label(f"Reaper Configs", id="config_sel_title")
with VerticalScroll(id="config_list_container"):
yield ListView(id="config_list")
with Horizontal(id="config_sel_actions"):
@ -449,15 +449,35 @@ class ConfigScreen(Screen):
coro = StoatWriter.fetch_guilds(token, api_url)
else: return
# Use the platform-specific saved ID so we don't cross-contaminate
if platform == "fluxer":
saved_id = self.config.fluxer_server_id
elif platform == "stoat":
saved_id = self.config.stoat_server_id
else:
saved_id = None
await self._fetch_and_populate(
coro,
"#btn_fetch_target_servers",
"#inp_target_server",
f"No {platform} servers found.",
self.config.target_server_id,
saved_id,
initial
)
# Guard: if the user switched platforms while the fetch was in-flight,
# discard the stale results so they don't contaminate the wrong platform.
try:
if self._get_selected_platform() != platform:
select = self.query_one("#inp_target_server", Select)
select.set_options([])
select.value = Select.BLANK
select.prompt = "Validate Bot Token"
self.query_one("#btn_fetch_target_servers", Button).variant = "primary"
except Exception:
pass
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "btn_fetch_guilds":
@ -498,6 +518,30 @@ class ConfigScreen(Screen):
def on_radio_set_changed(self, event: RadioSet.Changed) -> None:
if event.radio_set.id == "mode_radio":
self._toggle_target_section()
elif event.radio_set.id == "plat_radio":
plat = self._get_selected_platform()
try:
inp_token = self.query_one("#inp_target_token", Input)
inp_api = self.query_one("#inp_target_api", Input)
if plat == "fluxer":
inp_token.value = self.config.fluxer_bot_token or ""
api_val = self.config.fluxer_api_url
inp_api.value = api_val if (api_val and api_val != "default") else ""
elif plat == "stoat":
inp_token.value = self.config.stoat_bot_token or ""
api_val = self.config.stoat_api_url
inp_api.value = api_val if (api_val and api_val != "default") else ""
select = self.query_one("#inp_target_server", Select)
select.set_options([])
select.value = Select.BLANK
select.prompt = "Validate Bot Token"
self.query_one("#btn_fetch_target_servers", Button).variant = "primary"
if inp_token.value:
self.run_worker(self._do_fetch_target_servers(initial=True))
except Exception: pass
# ── save / start ─────────────────────────────────────────────────────
@ -514,15 +558,26 @@ class ConfigScreen(Screen):
# 3. Target Section
if self.config.tool_mode != "backup_only":
self.config.target_platform = self._get_selected_platform()
self.config.target_bot_token = self.query_one("#inp_target_token", Input).value.strip() or None
plat = self._get_selected_platform()
self.config.target_platform = plat
token_val = self.query_one("#inp_target_token", Input).value.strip() or None
t_select = self.query_one("#inp_target_server", Select)
if t_select.value not in (Select.BLANK, Select.NULL):
self.config.target_server_id = str(t_select.value)
target_api = self.query_one("#inp_target_api", Input).value.strip()
self.config.target_api_url = target_api or None
api_val = target_api or None
if plat == "fluxer":
self.config.fluxer_bot_token = token_val
# Only update server_id if user actually selected something
if t_select.value not in (Select.BLANK, Select.NULL):
self.config.fluxer_server_id = str(t_select.value)
self.config.fluxer_api_url = api_val
elif plat == "stoat":
self.config.stoat_bot_token = token_val
if t_select.value not in (Select.BLANK, Select.NULL):
self.config.stoat_server_id = str(t_select.value)
self.config.stoat_api_url = api_val
self.config.anonymize_users = self.query_one("#inp_anonymize_users", Switch).value
else:

View file

@ -339,6 +339,25 @@ class OperationPane(Container):
@work(exclusive=True)
async def run_validate(self) -> None:
try:
plat = "Fluxer" if self.target_platform == "fluxer" else "Stoat"
self.query_one("#op_lbl_t_header", Label).update(plat)
self.query_one("#op_lbl_d_server", Label).update("Server: [yellow]Validating...[/yellow]")
self.query_one("#op_lbl_d_bot", Label).update("Source: [yellow]Validating...[/yellow]" if self.view_mode == "backup" else "Bot: [yellow]Validating...[/yellow]")
self.query_one("#op_lbl_d_status", Label).update("Status: [yellow]Validating...[/yellow]")
self.query_one("#op_lbl_t_comm", Label).update("Community: [yellow]Validating...[/yellow]")
self.query_one("#op_lbl_t_bot", Label).update("Bot: [yellow]Validating...[/yellow]")
self.query_one("#op_lbl_t_status", Label).update("Status: [yellow]Validating...[/yellow]")
# Disable all operation buttons while validation is in progress
if self.view_mode == "shuttle":
for bid in ("#op_clone", "#op_sync", "#op_messages", "#op_danger"):
self.query_one(bid, Button).disabled = True
elif self.view_mode == "backup":
for bid in ("#op_backup_msgs", "#op_backup_sync"):
self.query_one(bid, Button).disabled = True
except Exception:
pass
self.validation_results = {
"discord_token": False, "discord_bot_name": None,
"discord_server": False, "discord_server_name": None,