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
|
return results
|
||||||
|
|
||||||
profile = bp / "server_profile" / "profile.json"
|
profile = bp / "server_profile" / "profile.json"
|
||||||
if profile.exists():
|
structure = bp / "server_profile" / "structure.json"
|
||||||
|
if profile.exists() and structure.exists():
|
||||||
try:
|
try:
|
||||||
data = json.loads(profile.read_text(encoding="utf-8"))
|
data = json.loads(profile.read_text(encoding="utf-8"))
|
||||||
results["token"] = True
|
results["token"] = True
|
||||||
results["server"] = True
|
results["server"] = True
|
||||||
results["bot_name"] = "BackupReader"
|
results["bot_name"] = "LOCAL BACKUP"
|
||||||
results["server_name"] = data.get("name", "Unknown")
|
results["server_name"] = data.get("name", "Unknown")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ from textual import work
|
||||||
from src.core.configuration import load_config
|
from src.core.configuration import load_config
|
||||||
from src.core.base import MigrationContext
|
from src.core.base import MigrationContext
|
||||||
from src.disco_reaper.exporter import DiscordExporter
|
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):
|
class BackupPane(Container):
|
||||||
|
|
@ -306,12 +306,30 @@ class BackupPane(Container):
|
||||||
if choice == "btn_back":
|
if choice == "btn_back":
|
||||||
modal_prog.dismiss()
|
modal_prog.dismiss()
|
||||||
continue
|
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":
|
elif choice == "btn_main_menu":
|
||||||
modal_prog.dismiss()
|
modal_prog.dismiss()
|
||||||
self.app.switch_screen("config_selection")
|
self.app.switch_screen("config_selection")
|
||||||
return
|
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
|
break
|
||||||
|
|
||||||
modal_prog.phase_progress()
|
modal_prog.phase_progress()
|
||||||
|
|
@ -349,7 +367,7 @@ class BackupPane(Container):
|
||||||
|
|
||||||
accumulated_msgs = await self.exporter.export_channel_messages(
|
accumulated_msgs = await self.exporter.export_channel_messages(
|
||||||
chan.id, progress_callback=update_msg_count, force=force_overwrite,
|
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(
|
accumulated_msgs = await self.exporter.export_threads(
|
||||||
chan.id, progress_callback=update_msg_count, force=force_overwrite,
|
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})
|
self.dismiss({"channels": selected, "force": force})
|
||||||
elif event.button.id == "btn_cancel_chan":
|
elif event.button.id == "btn_cancel_chan":
|
||||||
self.dismiss(None)
|
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.base import MigrationContext
|
||||||
from src.core.audit import log_audit_event
|
from src.core.audit import log_audit_event
|
||||||
from src.ui.modals import (
|
from src.ui.modals import (
|
||||||
ProgressScreen, SubMenuModal, ChannelPickerScreen, OptionSelectModal,
|
ProgressScreen, SubMenuModal, ChannelPickerScreen, OptionSelectModal, MessageIDInputModal,
|
||||||
)
|
)
|
||||||
|
|
||||||
import src.fluxer.roles_permissions as fluxer_roles
|
import src.fluxer.roles_permissions as fluxer_roles
|
||||||
|
|
@ -161,13 +161,15 @@ class ShuttlePane(Container):
|
||||||
s_disp = f'[green]"{d_name}"[/green]'
|
s_disp = f'[green]"{d_name}"[/green]'
|
||||||
b_disp = f'[green]{d_bot}[/green]'
|
b_disp = f'[green]{d_bot}[/green]'
|
||||||
elif v.get("discord_token") is False:
|
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]"
|
s_disp, b_disp = "[red]INVALID TOKEN[/red]", "[red]INVALID TOKEN[/red]"
|
||||||
else:
|
else:
|
||||||
s_disp, b_disp = "[red]NOT SET UP[/red]", "[red]NOT SET UP[/red]"
|
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}")
|
self.query_one("#sp_lbl_d_server", Label).update(f"Server: {s_disp}")
|
||||||
if self.config.tool_mode == "backup_transfer":
|
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}")
|
self.query_one("#sp_lbl_d_bot", Label).update(f"Source: {b_disp}")
|
||||||
else:
|
else:
|
||||||
self.query_one("#sp_lbl_d_bot", Label).update(f"Bot: {b_disp}")
|
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",
|
"000000000000000000", "DISCORD_SERVER_ID", "FLUXER_COMMUNITY_ID",
|
||||||
"STOAT_SERVER_ID", "TARGET_SERVER_ID", "", None,
|
"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
|
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
|
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).")
|
logger.info("Proceeding with 'Continue Migration' (incremental sink).")
|
||||||
after_id = int(last_migrated)
|
after_id = int(last_migrated)
|
||||||
elif choice == "btn_start_id":
|
elif choice == "btn_start_id":
|
||||||
# Fallback to full for now since we don't have an ID input dialog yet
|
loop = asyncio.get_running_loop()
|
||||||
modal.write("[yellow]Custom Message ID start not fully implemented, starting from beginning.[/yellow]")
|
future = loop.create_future()
|
||||||
logger.info("Proceeding with 'Start from ID' (fallback to begin).")
|
def id_callback(res: int | None) -> None:
|
||||||
after_id = 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:
|
else:
|
||||||
logger.info("Proceeding with 'Start from First' (clean sink).")
|
logger.info("Proceeding with 'Start from First' (clean sink).")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue