improve message state handling
This commit is contained in:
parent
6600be74c3
commit
17d9094084
3 changed files with 140 additions and 88 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -20,7 +20,7 @@ wheels/
|
|||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
temp.txt
|
||||
*.txt
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
|
|
@ -32,8 +32,8 @@ config.yaml
|
|||
!config.example.yaml
|
||||
|
||||
# Logs and State
|
||||
migration.log
|
||||
state.json
|
||||
*.log
|
||||
*.json
|
||||
|
||||
# Temporary Test Scripts
|
||||
test_*.py
|
||||
|
|
|
|||
|
|
@ -5,22 +5,28 @@ from typing import Dict, Any
|
|||
class MigrationState:
|
||||
"""Manages persistence of the migration state to allow resumability."""
|
||||
|
||||
def __init__(self, state_file: str | Path = "state.json"):
|
||||
def __init__(self, state_file: str | Path = "state.json", messages_file: str | Path = "messages.json"):
|
||||
self.state_file = Path(state_file)
|
||||
self.messages_file = Path(messages_file)
|
||||
|
||||
# mappings: discord_id -> fluxer_id
|
||||
self.channel_map: Dict[str, str] = {}
|
||||
self.category_map: Dict[str, str] = {}
|
||||
self.role_map: Dict[str, str] = {}
|
||||
self.emoji_map: Dict[str, str] = {}
|
||||
self.sticker_map: Dict[str, str] = {}
|
||||
self.message_map: Dict[str, str] = {}
|
||||
|
||||
# tracking last message timestamp per channel to resume
|
||||
# message tracking
|
||||
self.message_map: Dict[str, str] = {}
|
||||
self.last_message_timestamps: Dict[str, str] = {}
|
||||
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
migrated_state = False
|
||||
migrated_messages = False
|
||||
|
||||
# 1. Load primary state.json
|
||||
if self.state_file.exists():
|
||||
with open(self.state_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
|
@ -29,109 +35,133 @@ class MigrationState:
|
|||
self.role_map = data.get("roles", {})
|
||||
self.emoji_map = data.get("emojis", {})
|
||||
self.sticker_map = data.get("stickers", {})
|
||||
|
||||
# Check for legacy messages in state.json
|
||||
if "messages" in data or "last_message_timestamps" in data:
|
||||
self.message_map = data.get("messages", {})
|
||||
self.last_message_timestamps = data.get("last_message_timestamps", {})
|
||||
migrated_messages = True # We found legacy data, we should write it out to messages.json later
|
||||
|
||||
# Legacy Migration: Move role_, emoji_, sticker_ from channel_map to dedicated maps
|
||||
migrated = False
|
||||
legacy_keys = list(self.channel_map.keys())
|
||||
for k in legacy_keys:
|
||||
if k.startswith("role_"):
|
||||
discord_id = k.replace("role_", "")
|
||||
self.role_map[discord_id] = self.channel_map.pop(k)
|
||||
migrated = True
|
||||
migrated_state = True
|
||||
elif k.startswith("emoji_"):
|
||||
discord_id = k.replace("emoji_", "")
|
||||
self.emoji_map[discord_id] = self.channel_map.pop(k)
|
||||
migrated = True
|
||||
migrated_state = True
|
||||
elif k.startswith("sticker_"):
|
||||
discord_id = k.replace("sticker_", "")
|
||||
self.sticker_map[discord_id] = self.channel_map.pop(k)
|
||||
migrated = True
|
||||
migrated_state = True
|
||||
|
||||
if migrated:
|
||||
self.save()
|
||||
# 2. Load separate messages.json (overrides legacy state.json)
|
||||
if self.messages_file.exists():
|
||||
with open(self.messages_file, "r", encoding="utf-8") as f:
|
||||
msg_data = json.load(f)
|
||||
self.message_map = msg_data.get("messages", {})
|
||||
self.last_message_timestamps = msg_data.get("last_message_timestamps", {})
|
||||
migrated_messages = False # No need to force a migrating save since it already exists
|
||||
|
||||
def save(self):
|
||||
# 3. Save if we migrated any legacy data to separate maps/files
|
||||
if migrated_state or migrated_messages:
|
||||
self.save_state()
|
||||
if migrated_messages:
|
||||
self.save_messages()
|
||||
|
||||
def save_state(self):
|
||||
"""Saves only the core server configuration (channels, roles, emojis)."""
|
||||
data = {
|
||||
"channels": self.channel_map,
|
||||
"categories": self.category_map,
|
||||
"roles": self.role_map,
|
||||
"emojis": self.emoji_map,
|
||||
"stickers": self.sticker_map,
|
||||
"last_message_timestamps": self.last_message_timestamps,
|
||||
"messages": self.message_map
|
||||
"stickers": self.sticker_map
|
||||
}
|
||||
with open(self.state_file, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=4)
|
||||
|
||||
def save_messages(self):
|
||||
"""Saves only the message tracking data."""
|
||||
data = {
|
||||
"last_message_timestamps": self.last_message_timestamps,
|
||||
"messages": self.message_map
|
||||
}
|
||||
with open(self.messages_file, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=4)
|
||||
|
||||
# --- Type Specific Getters/Setters ---
|
||||
|
||||
def set_channel_mapping(self, discord_id: str, fluxer_id: str):
|
||||
self.channel_map[str(discord_id)] = str(fluxer_id)
|
||||
self.save()
|
||||
self.save_state()
|
||||
|
||||
def get_fluxer_channel_id(self, discord_id: str) -> str | None:
|
||||
return self.channel_map.get(str(discord_id))
|
||||
|
||||
def remove_channel_mapping(self, discord_id: str):
|
||||
self.channel_map.pop(str(discord_id), None)
|
||||
self.save()
|
||||
self.save_state()
|
||||
|
||||
def set_category_mapping(self, discord_id: str, fluxer_id: str):
|
||||
self.category_map[str(discord_id)] = str(fluxer_id)
|
||||
self.save()
|
||||
self.save_state()
|
||||
|
||||
def get_fluxer_category_id(self, discord_id: str) -> str | None:
|
||||
return self.category_map.get(str(discord_id))
|
||||
|
||||
def remove_category_mapping(self, discord_id: str):
|
||||
self.category_map.pop(str(discord_id), None)
|
||||
self.save()
|
||||
|
||||
def set_message_mapping(self, discord_id: str, fluxer_id: str):
|
||||
self.message_map[str(discord_id)] = str(fluxer_id)
|
||||
self.save()
|
||||
|
||||
def get_fluxer_message_id(self, discord_id: str) -> str | None:
|
||||
return self.message_map.get(str(discord_id))
|
||||
|
||||
def update_last_message_timestamp(self, channel_id: str, timestamp: str):
|
||||
self.last_message_timestamps[str(channel_id)] = timestamp
|
||||
self.save()
|
||||
|
||||
# --- Type Specific Getters/Setters ---
|
||||
self.save_state()
|
||||
|
||||
def set_role_mapping(self, discord_id: str, fluxer_id: str):
|
||||
self.role_map[str(discord_id)] = str(fluxer_id)
|
||||
self.save()
|
||||
self.save_state()
|
||||
|
||||
def get_fluxer_role_id(self, discord_id: str) -> str | None:
|
||||
return self.role_map.get(str(discord_id))
|
||||
|
||||
def remove_role_mapping(self, discord_id: str):
|
||||
self.role_map.pop(str(discord_id), None)
|
||||
self.save()
|
||||
self.save_state()
|
||||
|
||||
def set_emoji_mapping(self, discord_id: str, fluxer_id: str):
|
||||
self.emoji_map[str(discord_id)] = str(fluxer_id)
|
||||
self.save()
|
||||
self.save_state()
|
||||
|
||||
def get_fluxer_emoji_id(self, discord_id: str) -> str | None:
|
||||
return self.emoji_map.get(str(discord_id))
|
||||
|
||||
def remove_emoji_mapping(self, discord_id: str):
|
||||
self.emoji_map.pop(str(discord_id), None)
|
||||
self.save()
|
||||
self.save_state()
|
||||
|
||||
def set_sticker_mapping(self, discord_id: str, fluxer_id: str):
|
||||
self.sticker_map[str(discord_id)] = str(fluxer_id)
|
||||
self.save()
|
||||
self.save_state()
|
||||
|
||||
def get_fluxer_sticker_id(self, discord_id: str) -> str | None:
|
||||
return self.sticker_map.get(str(discord_id))
|
||||
|
||||
def remove_sticker_mapping(self, discord_id: str):
|
||||
self.sticker_map.pop(str(discord_id), None)
|
||||
self.save()
|
||||
self.save_state()
|
||||
|
||||
# --- Message Management ---
|
||||
|
||||
def set_message_mapping(self, discord_id: str, fluxer_id: str):
|
||||
self.message_map[str(discord_id)] = str(fluxer_id)
|
||||
self.save_messages()
|
||||
|
||||
def get_fluxer_message_id(self, discord_id: str) -> str | None:
|
||||
return self.message_map.get(str(discord_id))
|
||||
|
||||
def update_last_message_timestamp(self, channel_id: str, timestamp: str):
|
||||
self.last_message_timestamps[str(channel_id)] = timestamp
|
||||
self.save_messages()
|
||||
|
||||
# --- Danger Zone Clearing ---
|
||||
|
||||
|
|
@ -139,21 +169,21 @@ class MigrationState:
|
|||
"""Clears all channel and category mappings."""
|
||||
self.channel_map.clear()
|
||||
self.category_map.clear()
|
||||
self.save()
|
||||
self.save_state()
|
||||
|
||||
def clear_role_mappings(self):
|
||||
"""Clears all role mappings."""
|
||||
self.role_map.clear()
|
||||
self.save()
|
||||
self.save_state()
|
||||
|
||||
def clear_asset_mappings(self):
|
||||
"""Clears all emoji and sticker mappings."""
|
||||
self.emoji_map.clear()
|
||||
self.sticker_map.clear()
|
||||
self.save()
|
||||
self.save_state()
|
||||
|
||||
def clear_message_history(self):
|
||||
"""Clears all message mappings and timestamps."""
|
||||
self.message_map.clear()
|
||||
self.last_message_timestamps.clear()
|
||||
self.save()
|
||||
self.save_messages()
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ class MigrationCLI:
|
|||
|
||||
console.print("(Q) Exit")
|
||||
|
||||
choice = Prompt.ask("\nSelect an option", choices=["1", "2", "3", "4", "5", "6", "7", "Q", "q"], default="Q").upper()
|
||||
choice = Prompt.ask("\nSelect an option [1/2/3/4/5/6/7/Q]", choices=["1", "2", "3", "4", "5", "6", "7", "Q", "q"], default="Q", show_choices=False).upper()
|
||||
|
||||
if choice == "1":
|
||||
await self.clone_server_template()
|
||||
|
|
@ -381,7 +381,7 @@ class MigrationCLI:
|
|||
console.print("[bold red](F) Force re-clone, creates duplicate channels![/bold red]")
|
||||
console.print("[bold yellow](B) Back[/bold yellow]")
|
||||
|
||||
choice = Prompt.ask("Select an option", choices=["Y", "F", "B"], default="Y").upper()
|
||||
choice = Prompt.ask("Select an option [Y/F/B]", choices=["Y", "y", "F", "f", "B", "b"], default="Y", show_choices=False).upper()
|
||||
|
||||
if choice == "B":
|
||||
await self.engine.close_connections()
|
||||
|
|
@ -430,7 +430,7 @@ class MigrationCLI:
|
|||
console.print("(2) Sync Category Permissions & Channel Permissions")
|
||||
console.print("(B) Back")
|
||||
|
||||
choice = Prompt.ask("Select an option", choices=["1", "2", "B", "b"], default="B").upper()
|
||||
choice = Prompt.ask("Select an option [1/2/B]", choices=["1", "2", "B", "b"], default="B", show_choices=False).upper()
|
||||
|
||||
if choice == "B":
|
||||
return
|
||||
|
|
@ -469,7 +469,7 @@ class MigrationCLI:
|
|||
console.print("[bold red](F) Force Overwrite[/bold red]")
|
||||
console.print("[bold yellow](B) Back[/bold yellow]")
|
||||
|
||||
sub_choice = Prompt.ask("Select an option", choices=["Y", "F", "B"], default="Y").upper()
|
||||
sub_choice = Prompt.ask("Select an option [Y/F/B]", choices=["Y", "y", "F", "f", "B", "b"], default="Y", show_choices=False).upper()
|
||||
|
||||
if sub_choice == "B":
|
||||
return
|
||||
|
|
@ -524,7 +524,7 @@ class MigrationCLI:
|
|||
console.print("[bold green](Y) Proceed with Permission synchronization[/bold green]")
|
||||
console.print("[bold yellow](B) Back[/bold yellow]")
|
||||
|
||||
sub_choice = Prompt.ask("Select an option", choices=["Y", "B"], default="Y").upper()
|
||||
sub_choice = Prompt.ask("Select an option [Y/B]", choices=["Y", "y", "B", "b"], default="Y", show_choices=False).upper()
|
||||
|
||||
if sub_choice == "B":
|
||||
return
|
||||
|
|
@ -598,7 +598,7 @@ class MigrationCLI:
|
|||
console.print("(3) Sync Emojis and Stickers")
|
||||
console.print("(B) Back")
|
||||
|
||||
choice = Prompt.ask("Select an option", choices=["1", "2", "3", "B", "b"], default="B").upper()
|
||||
choice = Prompt.ask("Select an option [1/2/3/B]", choices=["1", "2", "3", "B", "b"], default="B", show_choices=False).upper()
|
||||
|
||||
if choice == "B":
|
||||
return
|
||||
|
|
@ -625,7 +625,7 @@ class MigrationCLI:
|
|||
console.print("[bold red](F) Force Overwrite[/bold red]")
|
||||
console.print("[bold yellow](B) Back[/bold yellow]")
|
||||
|
||||
choice = Prompt.ask("Select an option", choices=["Y", "F", "B"], default="Y").upper()
|
||||
choice = Prompt.ask("Select an option [Y/F/B]", choices=["Y", "y", "F", "f", "B", "b"], default="Y", show_choices=False).upper()
|
||||
|
||||
if choice == "B":
|
||||
return
|
||||
|
|
@ -692,7 +692,7 @@ class MigrationCLI:
|
|||
console.print("(4) Sync Everything")
|
||||
console.print("(B) Back")
|
||||
|
||||
choice = Prompt.ask("Select an option", choices=["1", "2", "3", "4", "B", "b"], default="B").upper()
|
||||
choice = Prompt.ask("Select an option [1/2/3/4/B]", choices=["1", "2", "3", "4", "B", "b"], default="B", show_choices=False).upper()
|
||||
|
||||
if choice == "B":
|
||||
return
|
||||
|
|
@ -721,7 +721,7 @@ class MigrationCLI:
|
|||
await self.engine.close_connections()
|
||||
|
||||
async def migrate_message_history(self):
|
||||
console.print("\n[bold]Message History Migration (Manual Selection)[/bold]")
|
||||
console.print("\n[bold]Message History Migration[/bold]")
|
||||
|
||||
if not self.tokens_valid:
|
||||
console.print("[bold red]Error: You must have a valid configuration (Option 6) to migrate messages.[/bold red]")
|
||||
|
|
@ -837,6 +837,9 @@ class MigrationCLI:
|
|||
if first_msg:
|
||||
# Link format: https://discord.com/channels/GUILD_ID/CHANNEL_ID/MESSAGE_ID
|
||||
first_msg_link = f"https://discord.com/channels/{self.config.discord_server_id}/{source_channel.id}/{first_msg.id}"
|
||||
server_name = self.validation_results.get("discord_server_name", "server")
|
||||
|
||||
while True:
|
||||
console.print(f"\n[bold green]Channel Found![/bold green]")
|
||||
console.print(f"Oldest Message Link: {first_msg_link}")
|
||||
console.print(f"Author: [blue]{first_msg.author.name}[/blue]")
|
||||
|
|
@ -846,25 +849,39 @@ class MigrationCLI:
|
|||
console.print("(M) Provide Specific message link or ID")
|
||||
console.print("(B) Back")
|
||||
|
||||
start_mode = Prompt.ask("Start migration", choices=["Y", "M", "B"], default="Y").upper()
|
||||
start_mode = Prompt.ask("Start migration [Y/M/B]", choices=["Y", "y", "M", "m", "B", "b"], default="Y", show_choices=False).upper()
|
||||
|
||||
if start_mode == "B":
|
||||
return
|
||||
elif start_mode == "Y":
|
||||
break
|
||||
elif start_mode == "M":
|
||||
custom_link = Prompt.ask("Enter Discord Message Link")
|
||||
prompt_msg = "Enter Discord Message Link"
|
||||
valid_message_found = False
|
||||
while True:
|
||||
custom_link = Prompt.ask(prompt_msg)
|
||||
if str(custom_link).upper() in ["B", "BACK"]:
|
||||
break
|
||||
try:
|
||||
# Extract message ID from end of link
|
||||
custom_id = int(custom_link.split("/")[-1])
|
||||
custom_id = int(str(custom_link).strip().split("/")[-1])
|
||||
# Check if message exists to give feedback
|
||||
msg = await self.engine.discord_reader.get_message(source_channel.id, custom_id)
|
||||
if msg:
|
||||
# To include this message, we start 'after' the ID before it
|
||||
after_id = custom_id - 1
|
||||
console.print(f"[green]Confirmed: Starting from {msg.author.name}'s message.[/green]")
|
||||
console.print(f"\n[green]Confirmed: Starting from {msg.author.name}'s message.[/green]")
|
||||
valid_message_found = True
|
||||
break
|
||||
else:
|
||||
console.print("[red]Warning: Message ID not found in this channel. Starting from beginning.[/red]")
|
||||
console.print("\n[red]Error parsing link: 404 Not Found (error code: 10008): Unknown Message.[/red]")
|
||||
prompt_msg = f"[yellow]Provide a valid Message link or Message ID from \"{server_name}\" (or 'B' to go back)[/yellow]"
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error parsing link: {e}. Starting from beginning.[/red]")
|
||||
console.print(f"\n[red]Error parsing link: {e}[/red]")
|
||||
prompt_msg = f"[yellow]Provide a valid Message link or Message ID from \"{server_name}\" (or 'B' to go back)[/yellow]"
|
||||
|
||||
if valid_message_found:
|
||||
break
|
||||
else:
|
||||
console.print("[yellow]Source channel appears to be empty. Nothing to migrate.[/yellow]")
|
||||
return
|
||||
|
|
@ -968,9 +985,10 @@ class MigrationCLI:
|
|||
console.print("(B) Back")
|
||||
|
||||
choice = Prompt.ask(
|
||||
"Select a Danger Zone option",
|
||||
"Select a Danger Zone option [1/2/3/4/B]",
|
||||
choices=["1", "2", "3", "4", "B", "b"],
|
||||
default="B"
|
||||
default="B",
|
||||
show_choices=False
|
||||
).upper()
|
||||
|
||||
if choice == "B":
|
||||
|
|
@ -979,7 +997,8 @@ class MigrationCLI:
|
|||
# ---- (1) Delete all Channels & Categories ----
|
||||
if choice == "1":
|
||||
console.print("")
|
||||
console.print("[bold red]This will DELETE every channel and category in the Fluxer community.[/bold red]")
|
||||
community_name = self.validation_results.get("fluxer_community_name", "Unknown")
|
||||
console.print(f"[bold red]This will DELETE every channel and category in the Fluxer community: \"{community_name}\"[/bold red]")
|
||||
if not Confirm.ask("[bold red]Are you absolutely sure?[/bold red]"):
|
||||
return
|
||||
if not Confirm.ask("[bold red]Last chance \u2013 this cannot be undone. Continue?[/bold red]"):
|
||||
|
|
@ -1010,7 +1029,8 @@ class MigrationCLI:
|
|||
# ---- (2) Reset Channel & Category Permissions ----
|
||||
elif choice == "2":
|
||||
console.print("")
|
||||
console.print("[bold red]This will RESET all permission overwrites on every channel and category.[/bold red]")
|
||||
community_name = self.validation_results.get("fluxer_community_name", "Unknown")
|
||||
console.print(f"[bold red]This will RESET all permission overwrites on every channel and category in the Fluxer community: \"{community_name}\"[/bold red]")
|
||||
if not Confirm.ask("[bold red]Are you absolutely sure?[/bold red]"):
|
||||
return
|
||||
if not Confirm.ask("[bold red]Last chance \u2013 all custom permission overrides will be wiped. Continue?[/bold red]"):
|
||||
|
|
@ -1041,7 +1061,8 @@ class MigrationCLI:
|
|||
# ---- (3) Delete all Roles ----
|
||||
elif choice == "3":
|
||||
console.print("")
|
||||
console.print("[bold red]This will DELETE all roles in the Fluxer community.[/bold red]")
|
||||
community_name = self.validation_results.get("fluxer_community_name", "Unknown")
|
||||
console.print(f"[bold red]This will DELETE all roles in the Fluxer community: \"{community_name}\"[/bold red]")
|
||||
console.print("[dim]Managed roles (including the bot's own role) and @everyone are automatically protected.[/dim]")
|
||||
if not Confirm.ask("[bold red]Are you absolutely sure?[/bold red]"):
|
||||
return
|
||||
|
|
@ -1073,7 +1094,8 @@ class MigrationCLI:
|
|||
# ---- (4) Delete all Emojis & Stickers ----
|
||||
elif choice == "4":
|
||||
console.print("")
|
||||
console.print("[bold red]This will DELETE all custom emojis and stickers in the Fluxer community.[/bold red]")
|
||||
community_name = self.validation_results.get("fluxer_community_name", "Unknown")
|
||||
console.print(f"[bold red]This will DELETE all custom emojis and stickers in the Fluxer community: \"{community_name}\"[/bold red]")
|
||||
if not Confirm.ask("[bold red]Are you absolutely sure?[/bold red]"):
|
||||
return
|
||||
if not Confirm.ask("[bold red]Last chance \u2013 this cannot be undone. Continue?[/bold red]"):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue