implement "start from message id" in tui

This commit is contained in:
rambros 2026-03-06 10:04:38 +05:30
parent 5ac3f75f91
commit 17d20df80a
4 changed files with 155 additions and 13 deletions

View file

@ -787,12 +787,13 @@ class BackupReader:
return results
profile = bp / "server_profile" / "profile.json"
if profile.exists():
structure = bp / "server_profile" / "structure.json"
if profile.exists() and structure.exists():
try:
data = json.loads(profile.read_text(encoding="utf-8"))
results["token"] = True
results["server"] = True
results["bot_name"] = "BackupReader"
results["bot_name"] = "LOCAL BACKUP"
results["server_name"] = data.get("name", "Unknown")
except Exception:
pass

View file

@ -21,7 +21,7 @@ from textual import work
from src.core.configuration import load_config
from src.core.base import MigrationContext
from src.disco_reaper.exporter import DiscordExporter
from src.ui.modals import ProgressScreen, ChannelSelectScreen
from src.ui.modals import ProgressScreen, ChannelSelectScreen, MessageIDInputModal
class BackupPane(Container):
@ -306,12 +306,30 @@ class BackupPane(Container):
if choice == "btn_back":
modal_prog.dismiss()
continue
elif choice == "btn_start_id":
loop = asyncio.get_running_loop()
future = loop.create_future()
def id_callback(res: int | None) -> None:
if not future.done():
future.set_result(res)
id_modal = MessageIDInputModal(self.engine.discord_reader, selected_channels[0].id)
self.app.push_screen(id_modal, id_callback)
verified_id = await future
if verified_id is None:
# User cancelled the ID input
continue
after_id = verified_id
elif choice == "btn_main_menu":
modal_prog.dismiss()
self.app.switch_screen("config_selection")
return
# Proceed to progress
# If we are here, proceeding either via Start First or Start from ID (after_id)
if choice == "btn_start_first":
after_id = None
break
modal_prog.phase_progress()
@ -349,7 +367,7 @@ class BackupPane(Container):
accumulated_msgs = await self.exporter.export_channel_messages(
chan.id, progress_callback=update_msg_count, force=force_overwrite,
accumulated_count=accumulated_msgs
accumulated_count=accumulated_msgs, after_id=after_id
)
accumulated_msgs = await self.exporter.export_threads(
chan.id, progress_callback=update_msg_count, force=force_overwrite,

View file

@ -699,3 +699,108 @@ class ChannelSelectScreen(Screen[dict]):
self.dismiss({"channels": selected, "force": force})
elif event.button.id == "btn_cancel_chan":
self.dismiss(None)
# ---------------------------------------------------------------------------
# MessageIDInputModal Input a Discord Message ID and verify it
# ---------------------------------------------------------------------------
class MessageIDInputModal(ModalScreen[int | None]):
"""Modal to input a message ID, verify it exists, and then confirm start."""
DEFAULT_CSS = """
MessageIDInputModal { align: center middle; }
#msg_id_dialog {
width: 80%;
height: auto;
border: solid yellow;
padding: 1 2;
background: $surface;
}
#msg_preview_container {
border: solid $primary;
padding: 1 2;
margin: 1 0;
height: auto;
min-height: 5;
}
#msg_id_buttons {
height: auto;
dock: bottom;
margin-top: 1;
}
#msg_id_buttons Button { width: 1fr; margin: 0 1; }
"""
def __init__(self, reader, channel_id: int):
super().__init__()
self.reader = reader
self.channel_id = channel_id
self.verified_id: int | None = None
def compose(self) -> ComposeResult:
with Container(id="msg_id_dialog"):
yield Label("[bold yellow]Start from specific Message ID[/bold yellow]")
yield Input(placeholder="Enter Discord Message ID (e.g., 123456789012345678)", id="input_msg_id", type="number")
with Container(id="msg_preview_container"):
yield Label("Enter an ID and click Verify to preview.", id="lbl_msg_preview")
with Horizontal(id="msg_id_buttons"):
yield Button("Verify", variant="primary", id="btn_verify_start", disabled=True)
yield Button("Back", variant="warning", id="btn_cancel_msg_id")
def on_input_changed(self, event: Input.Changed) -> None:
if event.input.id == "input_msg_id":
btn = self.query_one("#btn_verify_start", Button)
self.verified_id = None
btn.label = "Verify"
btn.variant = "primary"
val = event.input.value.strip()
if val and val.isdigit():
btn.disabled = False
else:
btn.disabled = True
preview = self.query_one("#lbl_msg_preview", Label)
preview.update("Click Verify to fetch message.")
async def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "btn_cancel_msg_id":
self.dismiss(None)
elif event.button.id == "btn_verify_start":
# If already verified, we are starting
if self.verified_id is not None:
self.dismiss(self.verified_id)
return
# Otherwise we verify
btn = event.button
inp = self.query_one("#input_msg_id", Input)
preview = self.query_one("#lbl_msg_preview", Label)
msg_id_str = inp.value.strip()
try:
msg_id = int(msg_id_str)
except ValueError:
preview.update("[bold red]Invalid ID format. Must be numeric.[/bold red]")
return
btn.disabled = True
preview.update("[cyan]Fetching message...[/cyan]")
try:
msg = await self.reader.get_message(self.channel_id, msg_id)
if not msg:
preview.update("[bold red]Message not found in this channel.[/bold red]")
else:
self.verified_id = msg_id
content = msg.content or (f"[dim]({len(msg.attachments)} attachments)[/dim]" if msg.attachments else "[dim](no content)[/dim]")
preview.update(f"[bold green]Message Found![/bold green]\n\n[bold]{msg.author.display_name}:[/bold] {content[:300]}")
btn.label = "Start Migration"
btn.variant = "success"
except Exception as e:
preview.update(f"[bold red]Error fetching message:[/bold red] {e}")
finally:
btn.disabled = False

View file

@ -20,7 +20,7 @@ from src.core.configuration import load_config
from src.core.base import MigrationContext
from src.core.audit import log_audit_event
from src.ui.modals import (
ProgressScreen, SubMenuModal, ChannelPickerScreen, OptionSelectModal,
ProgressScreen, SubMenuModal, ChannelPickerScreen, OptionSelectModal, MessageIDInputModal,
)
import src.fluxer.roles_permissions as fluxer_roles
@ -161,13 +161,15 @@ class ShuttlePane(Container):
s_disp = f'[green]"{d_name}"[/green]'
b_disp = f'[green]{d_bot}[/green]'
elif v.get("discord_token") is False:
if self.config.tool_mode == "backup_transfer":
s_disp, b_disp = "[red]NOT FOUND[/red]", "[red]NOT FOUND[/red]"
else:
s_disp, b_disp = "[red]INVALID TOKEN[/red]", "[red]INVALID TOKEN[/red]"
else:
s_disp, b_disp = "[red]NOT SET UP[/red]", "[red]NOT SET UP[/red]"
self.query_one("#sp_lbl_d_server", Label).update(f"Server: {s_disp}")
if self.config.tool_mode == "backup_transfer":
b_disp = "[green]LOCAL BACKUP[/green]" if v.get("discord_server") else "[red]NOT FOUND[/red]"
self.query_one("#sp_lbl_d_bot", Label).update(f"Source: {b_disp}")
else:
self.query_one("#sp_lbl_d_bot", Label).update(f"Bot: {b_disp}")
@ -227,6 +229,9 @@ class ShuttlePane(Container):
"000000000000000000", "DISCORD_SERVER_ID", "FLUXER_COMMUNITY_ID",
"STOAT_SERVER_ID", "TARGET_SERVER_ID", "", None,
]
if self.config.tool_mode == "backup_transfer":
d_dummy = self.config.discord_server_id in fillers
else:
d_dummy = self.config.discord_bot_token in fillers or self.config.discord_server_id in fillers
t_dummy = (self.config.target_bot_token or "") in fillers or (self.config.target_server_id or "") in fillers
@ -894,10 +899,23 @@ class ShuttlePane(Container):
logger.info("Proceeding with 'Continue Migration' (incremental sink).")
after_id = int(last_migrated)
elif choice == "btn_start_id":
# Fallback to full for now since we don't have an ID input dialog yet
modal.write("[yellow]Custom Message ID start not fully implemented, starting from beginning.[/yellow]")
logger.info("Proceeding with 'Start from ID' (fallback to begin).")
after_id = None
loop = asyncio.get_running_loop()
future = loop.create_future()
def id_callback(res: int | None) -> None:
if not future.done():
future.set_result(res)
id_modal = MessageIDInputModal(self.engine.discord_reader, source_channel.id)
self.app.push_screen(id_modal, id_callback)
verified_id = await future
if verified_id is None:
# User cancelled the ID input, stay on the progress modal
logger.info("User cancelled 'Start from ID' input.")
continue
logger.info(f"Proceeding with 'Start from ID': {verified_id}")
after_id = verified_id
else:
logger.info("Proceeding with 'Start from First' (clean sink).")