* feat: add Slack channel skill (/add-slack) Slack Bot integration via @slack/bolt with Socket Mode. Can replace WhatsApp entirely (SLACK_ONLY=true) or run alongside it. - SlackChannel implementing Channel interface (46 unit tests) - Socket Mode connection (no public URL needed) - @mention translation (Slack <@UBOTID> → TRIGGER_PATTERN) - Message splitting at 4000-char Slack API limit - Thread flattening (threaded replies delivered as channel messages) - User name resolution with caching - Outgoing message queue with flush-on-reconnect - Channel metadata sync with pagination - Proper Bolt types (GenericMessageEvent | BotMessageEvent) - Multi-channel orchestrator changes (conditional channel creation) - Setup guide (SLACK_SETUP.md) and known limitations documented Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * local settings * adjusted when installing --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2.8 KiB
2.8 KiB
Intent: src/index.ts modifications
What changed
Refactored from single WhatsApp channel to multi-channel architecture supporting Slack alongside WhatsApp.
Key sections
Imports (top of file)
- Added:
SlackChannelfrom./channels/slack.js - Added:
SLACK_ONLYfrom./config.js - Added:
readEnvFilefrom./env.js - Existing:
findChannelfrom./router.jsandChanneltype from./types.jsare already present
Module-level state
- Kept:
let whatsapp: WhatsAppChannel— still needed forsyncGroupMetadatareference - Added:
let slack: SlackChannel | undefined— direct reference forsyncChannelMetadata - Kept:
const channels: Channel[] = []— array of all active channels
processGroupMessages()
- Uses
findChannel(channels, chatJid)lookup (already exists in base) - Uses
channel.setTyping?.()andchannel.sendMessage()(already exists in base)
startMessageLoop()
- Uses
findChannel(channels, chatJid)per group (already exists in base) - Uses
channel.setTyping?.()for typing indicators (already exists in base)
main()
- Added: Reads Slack tokens via
readEnvFile()to check if Slack is configured - Added: conditional WhatsApp creation (
if (!SLACK_ONLY)) - Added: conditional Slack creation (
if (hasSlackTokens)) - Changed: scheduler
sendMessageusesfindChannel()→channel.sendMessage() - Changed: IPC
syncGroupMetadatasyncs both WhatsApp and Slack metadata - Changed: IPC
sendMessageusesfindChannel()→channel.sendMessage()
Shutdown handler
- Changed from
await whatsapp.disconnect()tofor (const ch of channels) await ch.disconnect() - Disconnects all active channels (WhatsApp, Slack, or any future channels) on SIGTERM/SIGINT
Invariants
- All existing message processing logic (triggers, cursors, idle timers) is preserved
- The
runAgentfunction is completely unchanged - State management (loadState/saveState) is unchanged
- Recovery logic is unchanged
- Container runtime check is unchanged (ensureContainerSystemRunning)
Design decisions
Double readEnvFile for Slack tokens
main() in index.ts reads SLACK_BOT_TOKEN/SLACK_APP_TOKEN via readEnvFile() to check
whether Slack is configured (controls whether to instantiate SlackChannel). The SlackChannel
constructor reads them again independently. This is intentional — index.ts needs to decide
whether to create the channel, while SlackChannel needs the actual token values. Keeping
both reads follows the security pattern of not passing secrets through intermediate variables.
Must-keep
- The
escapeXmlandformatMessagesre-exports - The
_setRegisteredGroupstest helper - The
isDirectRunguard at bottom - All error handling and cursor rollback logic in processGroupMessages
- The outgoing queue flush and reconnection logic (in each channel, not here)