User Tools

Site Tools


discourse

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
discourse [2026/01/23 06:10] chelseadiscourse [2026/01/26 21:07] (current) 208.92.105.160
Line 415: Line 415:
   * Implement pagination if needed   * Implement pagination if needed
   * Consider rate limiting   * Consider rate limiting
-```+ 
 + 
 +===== Profile Image/Avatar Synchronization ===== 
 + 
 +==== How Avatar Syncing Works ==== 
 + 
 +The bridge synchronizes profile images/avatars bidirectionally between Discord and Discourse using webhook-based user impersonation. 
 + 
 +=== Discourse → Discord Avatar Flow === 
 + 
 +  - Bridge polls Discourse Chat API every 1.5 seconds for new messages 
 +  - Each message includes user info with an ''avatar_template'' field 
 +  - Avatar template format: ''/user_avatar/site/username/{size}/123_2.png'' 
 +  - Bridge calls ''build_avatar_url()'' to construct full URL: 
 +    * Replaces ''{size}'' placeholder with pixel size (default: 128) 
 +    * Prepends Discourse base URL if path is relative 
 +  - Full avatar URL passed to Discord webhook ''avatar_url'' parameter 
 +  - Discord displays the message with the Discourse user's actual avatar 
 + 
 +<code python> 
 +# Avatar URL construction (bridge.py lines 160-186) 
 +def build_avatar_url(self, avatar_template: str, size: int = 128) -> Optional[str]: 
 +    if not avatar_template: 
 +        return None 
 +    avatar_path = avatar_template.replace('{size}', str(size)) 
 +    if not avatar_path.startswith('/'): 
 +        avatar_path = '/' + avatar_path 
 +    return f"{self.base_url}{avatar_path}" 
 +</code> 
 + 
 +=== Discord → Discourse Avatar Flow === 
 + 
 +Discord user avatars are **not** synced to Discourse. Messages from Discord appear with the bot's configured username in Discourse, prefixed with ''[Discord] username:'' to identify the source user. 
 + 
 +=== User Impersonation === 
 + 
 +The bridge uses Discord webhooks to display messages from Discourse users with their actual: 
 +  * **Username** - Discourse display name shown in Discord 
 +  * **Avatar** - Discourse profile image displayed in Discord 
 + 
 +This creates a seamless experience where Discourse users appear as distinct users in Discord channels. 
 + 
 +---- 
 + 
 +===== Image Upload/Download Synchronization ===== 
 + 
 +==== Discord → Discourse Images ==== 
 + 
 +  - Discord attachments detected via ''message.attachments'' 
 +  - Images downloaded to temporary files using ''download_image()'' 
 +  - Uploaded to Discourse via ''/uploads.json?type=image'' endpoint 
 +  - Upload URL appended to message as markdown: ''![]({image_url})'' 
 +  - Temporary files cleaned up after processing 
 + 
 +==== Discourse → Discord Images ==== 
 + 
 +  - Images extracted from ''uploads'' field in Discourse messages 
 +  - Relative URLs converted to absolute URLs (prepends base_url) 
 +  - URLs appended to message content 
 +  - Discord auto-embeds images from URLs 
 + 
 +---- 
 + 
 +===== GIF and Video Handling ===== 
 + 
 +==== Giphy/Tenor URL Detection ==== 
 + 
 +The bridge detects and converts Giphy/Tenor share URLs to direct media URLs for proper embedding: 
 + 
 +  * **Giphy**: ''https://giphy.com/gifs/...'' → ''https://media.giphy.com/media/{ID}/giphy.gif'' 
 +  * **Tenor**: ''https://tenor.com/view/...'' → ''https://media.tenor.com/{ID}/tenor.gif'' 
 + 
 +==== MP4 to GIF Conversion ==== 
 + 
 +When Giphy/Tenor returns MP4 URLs (common for larger animations), the bridge converts them to GIF using FFmpeg: 
 + 
 +  - Downloads MP4 to temporary file 
 +  - Generates optimized color palette for compression 
 +  - Converts to GIF with configurable settings: 
 +    * Frame rate (default: 15 FPS) 
 +    * Width scaling (default: 480px) 
 +    * Duration limit (default: 10 seconds) 
 +    * CPU thread limit (default: 3 threads) 
 +  - Uploads converted GIF to Discourse 
 +  - Cleans up temporary files 
 + 
 +FFmpeg configuration options in ''bridge_config.json'': 
 +<code json> 
 +
 +  "bridge":
 +    "ffmpeg_threads": 3, 
 +    "ffmpeg_duration_limit": 10, 
 +    "ffmpeg_fps": 15, 
 +    "ffmpeg_scale_width": 480 
 +  } 
 +
 +</code> 
 + 
 +---- 
 + 
 +===== State Management ===== 
 + 
 +==== Message Tracking ==== 
 + 
 +The bridge tracks processed message IDs to prevent duplicates and enable resumption after restarts. 
 + 
 +State file structure (''bridge_state.json''): 
 +<code json> 
 +
 +  "discord_channels":
 +    "channel_id":
 +      "last_message_id": 1234567890, 
 +      "last_updated": "2024-01-15T12:34:56Z" 
 +    } 
 +  }, 
 +  "discourse_channels":
 +    "channel_id":
 +      "last_message_id": 456, 
 +      "last_updated": "2024-01-15T12:34:56Z" 
 +    } 
 +  } 
 +
 +</code> 
 + 
 +==== State Initialization ==== 
 + 
 +On first run, the bridge initializes state with the **latest** message IDs to prevent flooding channels with historical messages. 
 + 
 +---- 
 + 
 +===== Loop Prevention ===== 
 + 
 +The bridge implements multiple safeguards to prevent infinite message loops: 
 + 
 +==== Discord → Discourse ==== 
 +  * Messages prefixed with ''[Discord]'' tag 
 +  * Webhook messages (non-user) are detected and skipped 
 +  * Bot's own messages filtered by user ID 
 + 
 +==== Discourse → Discord ==== 
 +  * Messages containing ''[Discord]'' prefix are ignored 
 +  * Prevents echoing messages that originated from Discord 
 + 
 +---- 
 + 
 +===== Configuration Reference ===== 
 + 
 +==== Full Configuration Structure ==== 
 + 
 +<code json> 
 +
 +  "discord":
 +    "token": "bot_token_here", 
 +    "channels":
 +      { 
 +        "name": "channel_name", 
 +        "discord_channel_id": 123456789012345678, 
 +        "discourse_channel_id": 2, 
 +        "discord_webhook_url": null, 
 +        "discourse_webhook_url": "https://discourse.example.com/chat/hooks/UUID", 
 +        "enabled": true 
 +      } 
 +    ] 
 +  }, 
 +  "discourse":
 +    "base_url": "https://discourse.example.com", 
 +    "api_key": "api_key_here", 
 +    "username": "syncbot" 
 +  }, 
 +  "bridge":
 +    "polling_interval": 1.5, 
 +    "state_file": "bridge_state.json", 
 +    "log_level": "INFO", 
 +    "rate_limit_delay": 1.0, 
 +    "use_discord_webhooks": true, 
 +    "ffmpeg_threads": 3, 
 +    "ffmpeg_duration_limit": 10, 
 +    "ffmpeg_fps": 15, 
 +    "ffmpeg_scale_width": 480 
 +  } 
 +
 +</code> 
 + 
 +==== Configuration Options ==== 
 + 
 +^ Option ^ Default ^ Description ^ 
 +| ''polling_interval'' | 1.5 | Seconds between Discourse API polls | 
 +| ''state_file'' | bridge_state.json | Path to state persistence file | 
 +| ''log_level'' | INFO | Logging verbosity (DEBUG, INFO, WARNING, ERROR) | 
 +| ''rate_limit_delay'' | 1.0 | Minimum seconds between API calls | 
 +| ''use_discord_webhooks'' | true | Enable user impersonation via webhooks | 
 +| ''ffmpeg_threads'' | 3 | CPU threads for MP4 conversion | 
 +| ''ffmpeg_duration_limit'' | 10 | Max seconds of video to convert | 
 +| ''ffmpeg_fps'' | 15 | Frame rate for converted GIFs | 
 +| ''ffmpeg_scale_width'' | 480 | Width in pixels for converted GIFs | 
 + 
 +==== Environment Variable Overrides ==== 
 + 
 +  * ''DISCORD_TOKEN'' - Discord bot token 
 +  * ''DISCOURSE_API_KEY'' - Discourse API key 
 +  * ''DISCOURSE_USERNAME'' - Discourse bot username 
 +  * ''DISCOURSE_BASE_URL'' - Discourse instance URL 
 +  * ''LOG_LEVEL'' - Logging level 
 +  * ''CONFIG_FILE'' - Path to config file 
 +  * ''STATE_FILE'' - Path to state file 
 + 
 +---- 
 + 
 +===== Channel Mapping ===== 
 + 
 +==== Multi-Channel Support ==== 
 + 
 +The bridge supports multiple independent channel pairs. Each pair has: 
 +  * Discord channel ID 
 +  * Discourse channel ID 
 +  * Separate webhook URLs 
 +  * Independent enable/disable flag 
 +  * Separate state tracking 
 + 
 +==== Adding a New Channel Pair ==== 
 + 
 +  - Add entry to ''discord.channels'' array in config 
 +  - Set ''discord_channel_id'' and ''discourse_channel_id'' 
 +  - Create Discourse webhook and add URL to ''discourse_webhook_url'' 
 +  - Set ''enabled: true'' 
 +  - Restart bridge - Discord webhook created automatically 
 + 
 +---- 
 + 
 +===== Error Handling and Retry Logic ===== 
 + 
 +==== Automatic Retry ==== 
 + 
 +  * Failed Discord webhook sends **do not** update state 
 +  * Message remains in queue and retries on next poll cycle 
 +  * Prevents message loss on temporary failures 
 + 
 +==== Graceful Degradation ==== 
 + 
 +  * API errors caught and logged, processing continues 
 +  * Image download failures don't block message delivery 
 +  * FFmpeg conversion failures fall back to posting MP4 URL 
 + 
 +==== Timeouts ==== 
 + 
 +  * API requests: 30-60 second timeout 
 +  * FFmpeg conversion: 30-60 second timeout 
 +  * Prevents indefinite hangs 
 + 
 +---- 
 + 
 +===== Docker Deployment ===== 
 + 
 +==== Container Configuration ==== 
 + 
 +<code yaml> 
 +services: 
 +  syncbot: 
 +    build: . 
 +    restart: unless-stopped 
 +    deploy: 
 +      resources: 
 +        limits: 
 +          cpus: '1' 
 +          memory: 512M 
 +    logging: 
 +      driver: "json-file" 
 +      options: 
 +        max-size: "10m" 
 +        max-file: "3" 
 +    volumes: 
 +      - ./bridge-config.json:/app/bridge-config.json:ro 
 +      - ./bridge_state.json:/app/bridge_state.json 
 +</code> 
 + 
 +==== Security ==== 
 + 
 +  * Runs as non-root user inside container 
 +  * Config mounted read-only 
 +  * State file writable for persistence 
 +  * Resource limits prevent runaway processes 
 + 
 +---- 
 + 
 +===== Message Flow Diagrams ===== 
 + 
 +==== Discord → Discourse ==== 
 + 
 +<code> 
 +Discord User posts message 
 +         ↓ 
 +on_message() event triggered 
 +         ↓ 
 +Check: Is webhook message? → Skip 
 +Check: Is bot message? → Skip 
 +         ↓ 
 +Find channel config by Discord channel ID 
 +         ↓ 
 +Download image attachments (if any) 
 +         ↓ 
 +Upload images to Discourse 
 +         ↓ 
 +Extract/convert Giphy/Tenor URLs 
 +         ↓ 
 +Convert MP4 to GIF (if needed) 
 +         ↓ 
 +Format: "[Discord] username: message" 
 +         ↓ 
 +Append image URLs as markdown 
 +         ↓ 
 +Send via Discourse webhook 
 +         ↓ 
 +Update Discord channel state 
 +</code> 
 + 
 +==== Discourse → Discord ==== 
 + 
 +<code> 
 +poll_discourse() runs every 1.5 seconds 
 +         ↓ 
 +For each enabled channel: 
 +         ↓ 
 +GET messages after last_message_id 
 +         ↓ 
 +For each new message: 
 +         ↓ 
 +Check: Has [Discord] prefix? → Skip 
 +         ↓ 
 +Extract username from user info 
 +Extract avatar_template from user info 
 +         ↓ 
 +Build avatar URL (replace {size}, add base_url) 
 +         ↓ 
 +Extract image uploads, make URLs absolute 
 +         ↓ 
 +Truncate content to 2000 chars if needed 
 +         ↓ 
 +Send via Discord webhook: 
 +  - username: Discourse username 
 +  - avatar_url: Discourse avatar 
 +  - content: message text 
 +         ↓ 
 +Success? → Update Discourse channel state 
 +Failed? → Retry on next poll 
 +</code> 
 + 
 +---- 
 + 
 +===== Dependencies ===== 
 + 
 +==== Python Packages ==== 
 + 
 +  * ''discord.py>=2.0.0'' - Discord bot framework 
 +  * ''requests>=2.28.0'' - HTTP requests 
 +  * ''aiohttp>=3.8.0'' - Async HTTP 
 +  * ''python-dotenv>=0.19.0'' - Environment variables 
 + 
 +==== System Dependencies ==== 
 + 
 +  * Python 3.11+ 
 +  * FFmpeg (for MP4 to GIF conversion) 
 + 
 +---- 
 + 
 +===== Known Limitations ===== 
 + 
 +==== Not Currently Implemented ==== 
 + 
 +  * **Message Edit Sync** - Edits are not synchronized 
 +  * **Message Delete Sync** - Deletions are not synchronized 
 +  * **Reaction Sync** - Emoji reactions not bridged 
 +  * **PluralKit Support** - Removed in current version (was in earlier versions per git history) 
 +  * **Discord Avatar → Discourse** - Only Discourse avatars sync to Discord, not vice versa 
 + 
 +---- 
 + 
 +===== Troubleshooting ===== 
 + 
 +==== Messages Not Syncing ==== 
 + 
 +  * Check ''enabled: true'' in channel config 
 +  * Verify Discord bot has webhook permissions in channel 
 +  * Check Discourse webhook URL is correct 
 +  * Review logs for API errors 
 + 
 +==== Duplicate Messages ==== 
 + 
 +  * Check state file permissions (must be writable) 
 +  * Verify state file path matches config 
 + 
 +==== Images Not Appearing ==== 
 + 
 +  * Verify Discourse API key has upload permissions 
 +  * Check temporary directory is writable 
 +  * Review logs for upload errors 
 + 
 +==== GIFs Not Converting ==== 
 + 
 +  * Ensure FFmpeg is installed: ''ffmpeg -version'' 
 +  * Check FFmpeg is in PATH 
 +  * Review logs for conversion errors 
 +  * Try reducing ''ffmpeg_duration_limit'' or ''ffmpeg_scale_width'' 
discourse.1769148615.txt.gz · Last modified: by chelsea