From 518438cb0907f8a91a301d9284a0e825bc3d17c2 Mon Sep 17 00:00:00 2001 From: rambros Date: Thu, 19 Mar 2026 22:21:25 +0530 Subject: [PATCH] add ability to anonymize users --- src/core/configuration.py | 1 + src/fluxer/migrate_message.py | 28 +++++++++++++++++++--------- src/fluxer/writer.py | 4 ++-- src/stoat/migrate_message.py | 28 +++++++++++++++++++--------- src/stoat/writer.py | 2 +- src/ui/main_app.py | 13 ++++++++++++- 6 files changed, 54 insertions(+), 22 deletions(-) diff --git a/src/core/configuration.py b/src/core/configuration.py index 727e1e8..11c38ab 100644 --- a/src/core/configuration.py +++ b/src/core/configuration.py @@ -11,6 +11,7 @@ class AppConfig(BaseModel): target_bot_token: Optional[str] = Field(default=None) target_server_id: Optional[str] = Field(default=None) target_api_url: Optional[str] = Field(default=None) + anonymize_users: bool = Field(default=False) log_level: str = Field(default="DEBUG") # ── backward‑compat shims (read‑only) ──────────────────────────────── diff --git a/src/fluxer/migrate_message.py b/src/fluxer/migrate_message.py index d4ad07f..f0f3aba 100644 --- a/src/fluxer/migrate_message.py +++ b/src/fluxer/migrate_message.py @@ -17,7 +17,7 @@ from src.core.utils import resolve_discord_links logger = logging.getLogger(__name__) -def clean_mentions(content: str, guild, user_mentions=None, role_mentions=None, channel_mentions=None, emoji_map=None, channel_map=None, state=None, target_server_id=None, channel_names=None) -> str: +def clean_mentions(content: str, guild, user_mentions=None, role_mentions=None, channel_mentions=None, emoji_map=None, channel_map=None, state=None, target_server_id=None, channel_names=None, anonymize_users=False) -> str: if content is None: return "" if not content or not guild: @@ -25,6 +25,9 @@ def clean_mentions(content: str, guild, user_mentions=None, role_mentions=None, def replace_user(match): uid = int(match.group(1)) + if anonymize_users and state: + alias = state.get_user_alias(str(uid)) + return f"`@{alias}`" # 1. Try provided guild member = guild.get_member(uid) if member: @@ -395,7 +398,8 @@ async def migrate_messages( context.state.channel_map, state=context.state, target_server_id=context.fluxer_writer.community_id, - channel_names=context.channel_names if hasattr(context, 'channel_names') else None + channel_names=context.channel_names if hasattr(context, 'channel_names') else None, + anonymize_users=context.config.anonymize_users ) logger.debug(f"Message {msg.id} cleaned content length: {len(content) if content else 0}") @@ -429,7 +433,8 @@ async def migrate_messages( context.state.channel_map, state=context.state, target_server_id=context.fluxer_writer.community_id, - channel_names=context.channel_names if hasattr(context, 'channel_names') else None + channel_names=context.channel_names if hasattr(context, 'channel_names') else None, + anonymize_users=context.config.anonymize_users ) # Add snapshot attachments to the list to process attachments_to_process.extend(snapshot.attachments) @@ -549,17 +554,22 @@ async def migrate_messages( if thread_name and stats["messages"] == 0: content = f"> <<< THREAD: **{thread_name}** >>>\n{content}" - avatar_url = str(msg.author.display_avatar.url) if msg.author.display_avatar.url else None - if avatar_url and not avatar_url.startswith("http"): - avatar_url = None + # Get or generate alias + alias = context.state.get_user_alias(str(msg.author.id)) - # Trigger alias generation/storage in DB without replacing the display name yet - context.state.get_user_alias(str(msg.author.id)) + if context.config.anonymize_users: + author_name = alias + avatar_url = f"https://api.dicebear.com/9.x/fun-emoji/jpg?seed={alias}" + else: + author_name = msg.author.display_name + avatar_url = str(msg.author.display_avatar.url) if msg.author.display_avatar.url else None + if avatar_url and not avatar_url.startswith("http"): + avatar_url = None logger.debug(f"Fluxer: Calling send_message for Discord ID {msg.id}") fluxer_msg_id = await context.fluxer_writer.send_message( channel_id=target_channel_id, - author_name=msg.author.display_name, + author_name=author_name, author_avatar_url=avatar_url, content=content, timestamp=int(msg.created_at.timestamp()), diff --git a/src/fluxer/writer.py b/src/fluxer/writer.py index 58603f4..d1984c3 100644 --- a/src/fluxer/writer.py +++ b/src/fluxer/writer.py @@ -285,7 +285,7 @@ class FluxerWriter: # -# is Fluxer/Discord's subtext markdown: small, muted grey text prefix = f"-# \n" if is_forwarded: - prefix += "-# -->*forwarded*\n" + prefix += "-# ⤷*forwarded*\n" display_content = content if is_forwarded and content: @@ -345,7 +345,7 @@ class FluxerWriter: # We add the author name to the prefix since bot name won't match bot_prefix = f"-# \n" if is_forwarded: - bot_prefix += "-# -->*forwarded*\n" + bot_prefix += "-# ⤷*forwarded*\n" bot_prefix += f"-# · {author_name}\n" final_bot_content = bot_prefix + display_content if display_content else bot_prefix diff --git a/src/stoat/migrate_message.py b/src/stoat/migrate_message.py index dddfdb2..f20a6db 100644 --- a/src/stoat/migrate_message.py +++ b/src/stoat/migrate_message.py @@ -17,7 +17,7 @@ from src.core.utils import resolve_discord_links logger = logging.getLogger(__name__) -def clean_mentions(content: str, guild, user_mentions=None, role_mentions=None, channel_mentions=None, emoji_map=None, channel_map=None, state=None, target_server_id=None, channel_names=None) -> str: +def clean_mentions(content: str, guild, user_mentions=None, role_mentions=None, channel_mentions=None, emoji_map=None, channel_map=None, state=None, target_server_id=None, channel_names=None, anonymize_users=False) -> str: if content is None: return "" if not content or not guild: @@ -25,6 +25,9 @@ def clean_mentions(content: str, guild, user_mentions=None, role_mentions=None, def replace_user(match): uid = int(match.group(1)) + if anonymize_users and state: + alias = state.get_user_alias(str(uid)) + return f"`@{alias}`" # 1. Try provided guild member = guild.get_member(uid) if member: @@ -398,7 +401,8 @@ async def migrate_messages( context.state.channel_map, state=context.state, target_server_id=context.stoat_writer.community_id, - channel_names=context.channel_names if hasattr(context, 'channel_names') else None + channel_names=context.channel_names if hasattr(context, 'channel_names') else None, + anonymize_users=context.config.anonymize_users ) logger.debug(f"Message {msg.id} cleaned content length: {len(content) if content else 0}") @@ -428,7 +432,8 @@ async def migrate_messages( context.state.channel_map, state=context.state, target_server_id=context.stoat_writer.community_id, - channel_names=context.channel_names if hasattr(context, 'channel_names') else None + channel_names=context.channel_names if hasattr(context, 'channel_names') else None, + anonymize_users=context.config.anonymize_users ) attachments_to_process.extend(snapshot.attachments) logger.debug(f"Found forwarded snapshot content: {content[:50]}... and {len(snapshot.attachments)} attachments") @@ -546,16 +551,21 @@ async def migrate_messages( if thread_name and stats["messages"] == 0: content = f"> <<< THREAD: **{thread_name}** >>>\n{content}" - avatar_url = str(msg.author.display_avatar.url) if msg.author.display_avatar.url else None - if avatar_url and not avatar_url.startswith("http"): - avatar_url = None + # Get or generate alias + alias = context.state.get_user_alias(str(msg.author.id)) - # Trigger alias generation/storage in DB without replacing the display name yet - context.state.get_user_alias(str(msg.author.id)) + if context.config.anonymize_users: + author_name = alias + avatar_url = f"https://api.dicebear.com/9.x/fun-emoji/jpg?seed={alias}" + else: + author_name = msg.author.display_name + avatar_url = str(msg.author.display_avatar.url) if msg.author.display_avatar.url else None + if avatar_url and not avatar_url.startswith("http"): + avatar_url = None stoat_msg_id = await context.stoat_writer.send_message( channel_id=target_channel_id, - author_name=msg.author.display_name, + author_name=author_name, author_avatar_url=avatar_url, content=content, timestamp=int(msg.created_at.timestamp()), diff --git a/src/stoat/writer.py b/src/stoat/writer.py index f8d7602..8a0bf76 100644 --- a/src/stoat/writer.py +++ b/src/stoat/writer.py @@ -316,7 +316,7 @@ class StoatWriter: # Build content with timestamp prefix prefix = f"###### \n" if is_forwarded: - prefix += "##### -->*forwarded*\n" + prefix += "##### ⤷*forwarded*\n" display_content = content if is_forwarded and content: diff --git a/src/ui/main_app.py b/src/ui/main_app.py index c03affe..5d78384 100644 --- a/src/ui/main_app.py +++ b/src/ui/main_app.py @@ -9,7 +9,7 @@ from textual.app import App, ComposeResult from textual.containers import Container, Horizontal, Vertical, VerticalScroll, Center from textual.widgets import ( Header, Footer, Button, Label, Input, ListItem, - ListView, Rule, RadioButton, RadioSet, Select, Static, Markdown + ListView, Rule, RadioButton, RadioSet, Select, Static, Markdown, Switch ) from textual.screen import Screen, ModalScreen @@ -252,6 +252,8 @@ class ConfigScreen(Screen): .fetch_row Input { width: 1fr; } .fetch_row Button { width: auto; margin-left: 1; } #inp_discord_server { margin-bottom: 1; } + .switch_row { height: auto; align: left middle; margin-top: 1; margin-bottom: 1; } + #lbl_anonymize { margin-right: 2; margin-top: 0; } """ BINDINGS = [("escape", "go_back", "Back")] @@ -348,6 +350,13 @@ class ConfigScreen(Screen): placeholder="Leave this Empty for official instance", tooltip="Enter the custom API url\nfor self hosted instances" ) + + yield Horizontal( + Label("Anonymize Users:", id="lbl_anonymize"), + Switch(value=self.config.anonymize_users, id="inp_anonymize_users", tooltip="Anonymize user Names and Avatars during migration"), + id="anonymize_row", + classes="switch_row" + ) yield Rule(id="footer_rule") with Horizontal(id="cfg_actions"): @@ -514,6 +523,8 @@ class ConfigScreen(Screen): target_api = self.query_one("#inp_target_api", Input).value.strip() self.config.target_api_url = target_api or None + + self.config.anonymize_users = self.query_one("#inp_anonymize_users", Switch).value else: self.config.target_platform = "none"