discourse
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revision | |||
| discourse [2026/01/23 06:10] – [Architecture Considerations] chelsea | discourse [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/ | ||
| + | |||
| + | ==== How Avatar Syncing Works ==== | ||
| + | |||
| + | The bridge synchronizes profile images/ | ||
| + | |||
| + | === 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 format: ''/ | ||
| + | - Bridge calls '' | ||
| + | * Replaces '' | ||
| + | * Prepends Discourse base URL if path is relative | ||
| + | - Full avatar URL passed to Discord webhook '' | ||
| + | - 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, | ||
| + | if not avatar_template: | ||
| + | return None | ||
| + | avatar_path = avatar_template.replace(' | ||
| + | if not avatar_path.startswith('/' | ||
| + | avatar_path = '/' | ||
| + | return f" | ||
| + | </ | ||
| + | |||
| + | === 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 '' | ||
| + | |||
| + | === 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/ | ||
| + | |||
| + | ==== Discord → Discourse Images ==== | ||
| + | |||
| + | - Discord attachments detected via '' | ||
| + | - Images downloaded to temporary files using '' | ||
| + | - Uploaded to Discourse via ''/ | ||
| + | - Upload URL appended to message as markdown: '' | ||
| + | - Temporary files cleaned up after processing | ||
| + | |||
| + | ==== Discourse → Discord Images ==== | ||
| + | |||
| + | - Images extracted from '' | ||
| + | - 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**: '' | ||
| + | * **Tenor**: '' | ||
| + | |||
| + | ==== MP4 to GIF Conversion ==== | ||
| + | |||
| + | When Giphy/Tenor returns MP4 URLs (common for larger animations), | ||
| + | |||
| + | - 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 '' | ||
| + | <code json> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== State Management ===== | ||
| + | |||
| + | ==== Message Tracking ==== | ||
| + | |||
| + | The bridge tracks processed message IDs to prevent duplicates and enable resumption after restarts. | ||
| + | |||
| + | State file structure ('' | ||
| + | <code json> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }, | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ==== 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 '' | ||
| + | * Webhook messages (non-user) are detected and skipped | ||
| + | * Bot's own messages filtered by user ID | ||
| + | |||
| + | ==== Discourse → Discord ==== | ||
| + | * Messages containing '' | ||
| + | * Prevents echoing messages that originated from Discord | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Configuration Reference ===== | ||
| + | |||
| + | ==== Full Configuration Structure ==== | ||
| + | |||
| + | <code json> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | ] | ||
| + | }, | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ==== Configuration Options ==== | ||
| + | |||
| + | ^ Option ^ Default ^ Description ^ | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | |||
| + | ==== Environment Variable Overrides ==== | ||
| + | |||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== 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/ | ||
| + | * Separate state tracking | ||
| + | |||
| + | ==== Adding a New Channel Pair ==== | ||
| + | |||
| + | - Add entry to '' | ||
| + | - Set '' | ||
| + | - Create Discourse webhook and add URL to '' | ||
| + | - Set '' | ||
| + | - 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: ' | ||
| + | memory: 512M | ||
| + | logging: | ||
| + | driver: " | ||
| + | options: | ||
| + | max-size: " | ||
| + | max-file: " | ||
| + | volumes: | ||
| + | - ./ | ||
| + | - ./ | ||
| + | </ | ||
| + | |||
| + | ==== 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 ==== | ||
| + | |||
| + | < | ||
| + | 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 MP4 to GIF (if needed) | ||
| + | ↓ | ||
| + | Format: " | ||
| + | ↓ | ||
| + | Append image URLs as markdown | ||
| + | ↓ | ||
| + | Send via Discourse webhook | ||
| + | ↓ | ||
| + | Update Discord channel state | ||
| + | </ | ||
| + | |||
| + | ==== Discourse → Discord ==== | ||
| + | |||
| + | < | ||
| + | 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 | ||
| + | </ | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Dependencies ===== | ||
| + | |||
| + | ==== Python Packages ==== | ||
| + | |||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | |||
| + | ==== 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 '' | ||
| + | * 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: '' | ||
| + | * Check FFmpeg is in PATH | ||
| + | * Review logs for conversion errors | ||
| + | * Try reducing '' | ||
| + | |||
discourse.txt · Last modified: by 208.92.105.160
