From 4b6e9164252b9767587e79d27b89ab6fa1e03a0b Mon Sep 17 00:00:00 2001 From: rambros Date: Thu, 19 Mar 2026 23:53:43 +0530 Subject: [PATCH] update config to support multiple targets simultaneously --- src/core/configuration.py | 71 ++++++++++++++++++--------------------- src/core/database.py | 3 +- src/ui/main_app.py | 69 +++++++++++++++++++++++++++++++++---- src/ui/shuttle_ops.py | 19 +++++++++++ 4 files changed, 115 insertions(+), 47 deletions(-) diff --git a/src/core/configuration.py b/src/core/configuration.py index 11c38ab..ad71ed3 100644 --- a/src/core/configuration.py +++ b/src/core/configuration.py @@ -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) diff --git a/src/core/database.py b/src/core/database.py index 5b82791..b1ffd4f 100644 --- a/src/core/database.py +++ b/src/core/database.py @@ -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: diff --git a/src/ui/main_app.py b/src/ui/main_app.py index 5d78384..d13e0a6 100644 --- a/src/ui/main_app.py +++ b/src/ui/main_app.py @@ -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: diff --git a/src/ui/shuttle_ops.py b/src/ui/shuttle_ops.py index d21ec7f..1cd690e 100644 --- a/src/ui/shuttle_ops.py +++ b/src/ui/shuttle_ops.py @@ -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,