#!/usr/bin/env python3 """ Maintenance script to convert Markdown bold (**text**) to Unicode bold. This fixes posts that contain Markdown formatting which doesn't render on LinkedIn. Unicode bold characters are used instead, which display correctly on LinkedIn. Usage: python maintenance_fix_markdown_bold.py # Dry run (preview changes) python maintenance_fix_markdown_bold.py --apply # Apply changes to database """ import asyncio import re import sys from uuid import UUID from loguru import logger from src.database import db # Unicode Bold character mappings (Mathematical Sans-Serif Bold) BOLD_MAP = { # Uppercase A-Z 'A': '๐—”', 'B': '๐—•', 'C': '๐—–', 'D': '๐——', 'E': '๐—˜', 'F': '๐—™', 'G': '๐—š', 'H': '๐—›', 'I': '๐—œ', 'J': '๐—', 'K': '๐—ž', 'L': '๐—Ÿ', 'M': '๐— ', 'N': '๐—ก', 'O': '๐—ข', 'P': '๐—ฃ', 'Q': '๐—ค', 'R': '๐—ฅ', 'S': '๐—ฆ', 'T': '๐—ง', 'U': '๐—จ', 'V': '๐—ฉ', 'W': '๐—ช', 'X': '๐—ซ', 'Y': '๐—ฌ', 'Z': '๐—ญ', # Lowercase a-z 'a': '๐—ฎ', 'b': '๐—ฏ', 'c': '๐—ฐ', 'd': '๐—ฑ', 'e': '๐—ฒ', 'f': '๐—ณ', 'g': '๐—ด', 'h': '๐—ต', 'i': '๐—ถ', 'j': '๐—ท', 'k': '๐—ธ', 'l': '๐—น', 'm': '๐—บ', 'n': '๐—ป', 'o': '๐—ผ', 'p': '๐—ฝ', 'q': '๐—พ', 'r': '๐—ฟ', 's': '๐˜€', 't': '๐˜', 'u': '๐˜‚', 'v': '๐˜ƒ', 'w': '๐˜„', 'x': '๐˜…', 'y': '๐˜†', 'z': '๐˜‡', # Numbers 0-9 '0': '๐Ÿฌ', '1': '๐Ÿญ', '2': '๐Ÿฎ', '3': '๐Ÿฏ', '4': '๐Ÿฐ', '5': '๐Ÿฑ', '6': '๐Ÿฒ', '7': '๐Ÿณ', '8': '๐Ÿด', '9': '๐Ÿต', # German umlauts 'ร„': '๐—”ฬˆ', 'ร–': '๐—ขฬˆ', 'รœ': '๐—จฬˆ', 'รค': '๐—ฎฬˆ', 'รถ': '๐—ผฬˆ', 'รผ': '๐˜‚ฬˆ', 'รŸ': 'รŸ', # No bold variant, keep as is } def to_unicode_bold(text: str) -> str: """Convert plain text to Unicode bold characters.""" result = [] for char in text: result.append(BOLD_MAP.get(char, char)) return ''.join(result) def convert_markdown_bold(content: str) -> str: """ Convert Markdown bold (**text**) to Unicode bold. Also handles: - __text__ (alternative markdown bold) - Nested or multiple occurrences """ # Pattern for **text** (non-greedy, handles multiple) pattern_asterisk = r'\*\*(.+?)\*\*' # Pattern for __text__ pattern_underscore = r'__(.+?)__' def replace_with_bold(match): inner_text = match.group(1) return to_unicode_bold(inner_text) # Apply conversions result = re.sub(pattern_asterisk, replace_with_bold, content) result = re.sub(pattern_underscore, replace_with_bold, result) return result def has_markdown_bold(content: str) -> bool: """Check if content contains Markdown bold syntax.""" return bool(re.search(r'\*\*.+?\*\*|__.+?__', content)) async def fix_all_posts(apply: bool = False): """ Find and fix all posts with Markdown bold formatting. Args: apply: If True, apply changes to database. If False, just preview. """ logger.info("Loading all users...") users = await db.list_users() total_posts = 0 posts_with_markdown = 0 fixed_posts = [] for user in users: posts = await db.get_generated_posts(user.id) for post in posts: total_posts += 1 if not post.post_content: continue if has_markdown_bold(post.post_content): posts_with_markdown += 1 original = post.post_content converted = convert_markdown_bold(original) # Get user display name (email or linkedin name) user_name = user.email or user.linkedin_name or str(user.id) fixed_posts.append({ 'id': post.id, 'user': user_name, 'topic': post.topic_title, 'original': original, 'converted': converted, }) # Show preview print(f"\n{'='*60}") print(f"Post: {post.topic_title}") print(f"User: {user_name}") print(f"ID: {post.id}") print(f"{'-'*60}") # Find and highlight the changes bold_matches = re.findall(r'\*\*(.+?)\*\*|__(.+?)__', original) for match in bold_matches: text = match[0] or match[1] print(f" **{text}** โ†’ {to_unicode_bold(text)}") print(f"\n{'='*60}") print(f"SUMMARY") print(f"{'='*60}") print(f"Total posts scanned: {total_posts}") print(f"Posts with Markdown bold: {posts_with_markdown}") if not fixed_posts: print("\nNo posts need fixing!") return if apply: print(f"\nApplying changes to {len(fixed_posts)} posts...") for post_data in fixed_posts: try: # Update the post in database await asyncio.to_thread( lambda pid=post_data['id'], content=post_data['converted']: db.client.table("generated_posts").update({ "post_content": content }).eq("id", str(pid)).execute() ) logger.info(f"Fixed post: {post_data['topic']}") except Exception as e: logger.error(f"Failed to update post {post_data['id']}: {e}") print(f"\nDone! Fixed {len(fixed_posts)} posts.") else: print(f"\nDRY RUN - No changes applied.") print(f"Run with --apply to fix these {len(fixed_posts)} posts.") async def main(): apply = '--apply' in sys.argv if apply: print("MODE: APPLY CHANGES") print("This will modify posts in the database.") response = input("Are you sure? (yes/no): ") if response.lower() != 'yes': print("Aborted.") return else: print("MODE: DRY RUN (preview only)") print("Add --apply flag to actually modify posts.\n") await fix_all_posts(apply=apply) if __name__ == "__main__": asyncio.run(main())