implement "start from message id" in tui
This commit is contained in:
parent
5ac3f75f91
commit
17d20df80a
4 changed files with 155 additions and 13 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
105
src/ui/modals.py
105
src/ui/modals.py
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue