discourse
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| discourse [2026/01/23 06:05] – 208.92.105.181 | discourse [2026/01/26 21:07] (current) – 208.92.105.160 | ||
|---|---|---|---|
| Line 90: | Line 90: | ||
| ==== 7. Install and Configure Nginx ==== | ==== 7. Install and Configure Nginx ==== | ||
| + | |||
| + | #This nginx setup in this guide is but one valid configuration. | ||
| <code bash> | <code bash> | ||
| apt-get install nginx | apt-get install nginx | ||
| Line 216: | Line 218: | ||
| * Syntax: '' | * Syntax: '' | ||
| * Example: '' | * Example: '' | ||
| + | |||
| + | ``` | ||
| + | ===== Discourse API Configuration ===== | ||
| + | |||
| + | ==== Webhook Configuration ==== | ||
| + | |||
| + | === Available Events === | ||
| + | |||
| + | **Topic Events** | ||
| + | * Topic is created | ||
| + | * Topic is revised | ||
| + | * Topic is updated | ||
| + | * Topic is deleted | ||
| + | * Topic is recovered | ||
| + | |||
| + | **Post Events** | ||
| + | * Post is created | ||
| + | * Post is updated | ||
| + | * Post is deleted | ||
| + | * Post is recovered | ||
| + | |||
| + | **User Events** | ||
| + | * User logged in | ||
| + | * User logged out | ||
| + | * User confirmed e-mail | ||
| + | * User is created | ||
| + | * User is approved | ||
| + | * User is updated | ||
| + | * User is deleted | ||
| + | * User is suspended | ||
| + | * User is unsuspended | ||
| + | * User is anonymized | ||
| + | |||
| + | **Group Events** | ||
| + | * Group is created | ||
| + | * Group is updated | ||
| + | * Group is deleted | ||
| + | |||
| + | **Category Events** | ||
| + | * Category is created | ||
| + | * Category is updated | ||
| + | * Category is deleted | ||
| + | |||
| + | **Tag Events** | ||
| + | * Tag is created | ||
| + | * Tag is updated | ||
| + | * Tag is deleted | ||
| + | |||
| + | **Chat Events** (Critical for sync) | ||
| + | * Message is created | ||
| + | * Message is edited | ||
| + | * Message is trashed | ||
| + | * Message is restored | ||
| + | |||
| + | **Other Events** | ||
| + | * Reviewable Events | ||
| + | * Notification Events | ||
| + | * Solved Events | ||
| + | * Badge Events | ||
| + | * Group User Events | ||
| + | * Like Events | ||
| + | * User Promoted Events | ||
| + | * Topic Voting Events | ||
| + | |||
| + | === Webhook Settings === | ||
| + | |||
| + | **Content Type:** '' | ||
| + | |||
| + | **Secret:** Optional string for generating HMAC signatures (implement signature verification to prevent spoofed webhooks) | ||
| + | |||
| + | **Filtering Options:** | ||
| + | * Triggered Categories - only fire webhooks for specific categories | ||
| + | * Triggered Tags - only fire webhooks for specific tags | ||
| + | * Triggered Groups - only fire webhooks for specific groups | ||
| + | * Leave blank to trigger for all | ||
| + | |||
| + | **TLS Certificate Check:** Enable unless using self-signed certs in development | ||
| + | |||
| + | === Configured Webhook Endpoints === | ||
| + | |||
| + | **hobbiesync webhook:** | ||
| + | * URL: '' | ||
| + | |||
| + | **general webhook:** | ||
| + | * URL: '' | ||
| + | |||
| + | Path structure: ''/ | ||
| + | |||
| + | ==== Chat API Endpoints ==== | ||
| + | |||
| + | Base path: ''/ | ||
| + | |||
| + | === Channel Operations === | ||
| + | |||
| + | **List channels** | ||
| + | < | ||
| + | GET / | ||
| + | </ | ||
| + | |||
| + | **Get specific channel** | ||
| + | < | ||
| + | GET / | ||
| + | </ | ||
| + | |||
| + | **List messages** | ||
| + | < | ||
| + | GET / | ||
| + | </ | ||
| + | |||
| + | **Send message** | ||
| + | < | ||
| + | POST / | ||
| + | </ | ||
| + | |||
| + | === Authentication === | ||
| + | |||
| + | Discourse API requires headers: | ||
| + | < | ||
| + | Api-Key: {your_api_key} | ||
| + | Api-Username: | ||
| + | </ | ||
| + | |||
| + | **API Credentials: | ||
| + | * API Key: '' | ||
| + | * Username: '' | ||
| + | |||
| + | **Security Notes:** | ||
| + | * Store credentials in environment variables or secrets management (Vault) | ||
| + | * API key has full access as the bot user | ||
| + | * Revoke/ | ||
| + | * Ensure bot user has appropriate permissions (chat access, posting rights to target channels) | ||
| + | |||
| + | ==== Architecture Considerations ==== | ||
| + | |||
| + | === Critical Events for Chat Sync === | ||
| + | |||
| + | **Primary focus:** | ||
| + | * Chat events (message created/ | ||
| + | * Post events - if bridging forum posts to Discord channels | ||
| + | * User events (login/ | ||
| + | |||
| + | === Bidirectional Sync Challenges === | ||
| + | |||
| + | **Discourse → Discord:** | ||
| + | * Webhooks handle this direction | ||
| + | * Parse incoming webhook payloads | ||
| + | * Map to Discord API calls | ||
| + | |||
| + | **Discord → Discourse: | ||
| + | * Discord bot with message event listeners | ||
| + | * POST to Discourse API ''/ | ||
| + | * Requires Discord bot token and event subscriptions | ||
| + | |||
| + | **Loop Prevention: | ||
| + | * Track message IDs/sources to avoid infinite echo | ||
| + | * Store mapping of Discourse message ID ↔ Discord message ID | ||
| + | * Ignore messages from own bot user | ||
| + | |||
| + | === State Management === | ||
| + | |||
| + | Required mappings to store: | ||
| + | * Message ID mapping (Discourse chat message ID ↔ Discord message ID) | ||
| + | * User mapping (Discourse username ↔ Discord user/ | ||
| + | * Channel mapping (Discourse channel ID ↔ Discord channel ID) | ||
| + | * Edit/delete operations require retrieving these mappings | ||
| + | |||
| + | === Event Filtering Strategy === | ||
| + | |||
| + | **Options: | ||
| + | * "Send me everything" | ||
| + | * Selective events - choose specific event types | ||
| + | * Category/ | ||
| + | |||
| + | **Recommendation: | ||
| + | |||
| + | === Implementation Flow === | ||
| + | |||
| + | **Webhook Receiver (Discourse → Discord):** | ||
| + | - Receive webhook POST request | ||
| + | - Verify HMAC signature if secret configured | ||
| + | - Parse JSON payload | ||
| + | - Extract message/ | ||
| + | - Check for existing mapping (to detect edits/ | ||
| + | - Call Discord API | ||
| + | - Store message ID mapping | ||
| + | |||
| + | **Discord Bot (Discord → Discourse): | ||
| + | - Listen for Discord message events | ||
| + | - Check if message is from bot (skip to prevent loop) | ||
| + | - Format message for Discourse | ||
| + | - POST to Discourse API with credentials | ||
| + | - Store message ID mapping | ||
| + | |||
| + | **Message History/ | ||
| + | * Use '' | ||
| + | * Implement pagination if needed | ||
| + | * 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.1769148337.txt.gz · Last modified: by 208.92.105.181
