{"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../../src/gateway/channels/slack.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACN,kBAAkB,EAElB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAkB,eAAe,EAAc,MAAM,mBAAmB,CAAC;AAErF,kCAAkC;AAClC,MAAM,WAAW,WAAW;IAC3B,2BAA2B;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sCAAsC;IACtC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,yBAAyB;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,qCAAqC;AACrC,cAAM,YAAa,SAAQ,kBAAkB;IAC5C,QAAQ,CAAC,WAAW,WAAW;IAE/B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAC,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAU;IAC5B,OAAO,CAAC,EAAE,CAAC,CAAY;IACvB,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,oBAAoB,CAAK;IACjC,OAAO,CAAC,OAAO,CAAC,CAAY;IAC5B,OAAO,CAAC,SAAS,CAAC,CAAS;IAE3B,YAAY,MAAM,EAAE,aAAa,EAMhC;IAED,6BAA6B;IACvB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAmBhC;IAED,2BAA2B;IACrB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAG9B;IAED,8BAA8B;IACxB,IAAI,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqClD;IAED,0BAA0B;IACpB,UAAU,IAAI,OAAO,CAAC,cAAc,CAAC,CAsB1C;YAGa,iBAAiB;IAwC/B,iCAAiC;IACjC,OAAO,CAAC,mBAAmB;IAY3B,yBAAyB;IACzB,OAAO,CAAC,WAAW;IAmBnB,4BAA4B;IAC5B,OAAO,CAAC,mBAAmB;IAc3B,2BAA2B;IAC3B,OAAO,CAAC,kBAAkB;IAoE1B,+BAA+B;IAC/B,OAAO,CAAC,gBAAgB;IA8BxB,2BAA2B;IAC3B,OAAO,CAAC,gBAAgB;IAYxB,+BAA+B;IAC/B,OAAO,CAAC,aAAa;IAKrB,+BAA+B;IAC/B,OAAO,CAAC,aAAa;YAMP,OAAO;CAuBrB;AAoDD,OAAO,EAAE,YAAY,EAAE,CAAC","sourcesContent":["/**\n * Slack Channel Adapter\n *\n * Uses Slack Bolt framework or Events API via Socket Mode\n */\n\nimport type { WebSocket } from \"ws\";\nimport {\n\tBaseChannelAdapter,\n\tglobalChannelRegistry,\n\ttype ChannelConfig,\n\ttype ChannelProfile,\n} from \"./base.js\";\nimport type { InboundMessage, OutboundMessage, SenderInfo } from \"../types/index.js\";\n\n/** Slack channel configuration */\nexport interface SlackConfig {\n\t/** Bot token (xoxb-...) */\n\tbotToken: string;\n\t/** App token for Socket Mode (xapp-...) */\n\tappToken?: string;\n\t/** Signing secret for HTTP mode */\n\tsigningSecret?: string;\n\t/** Use Socket Mode (default: true) */\n\tsocketMode?: boolean;\n\t/** Port for HTTP mode */\n\tport?: number;\n}\n\n/** Slack adapter using Events API */\nclass SlackAdapter extends BaseChannelAdapter {\n\treadonly channelType = \"slack\";\n\n\tprivate botToken: string;\n\tprivate appToken?: string;\n\tprivate socketMode: boolean;\n\tprivate ws?: WebSocket;\n\tprivate reconnectAttempts = 0;\n\tprivate maxReconnectAttempts = 5;\n\tprivate botInfo?: SlackUser;\n\tprivate socketUrl?: string;\n\n\tconstructor(config: ChannelConfig) {\n\t\tsuper(config);\n\t\tconst slackConfig = config.config as unknown as SlackConfig;\n\t\tthis.botToken = slackConfig.botToken;\n\t\tthis.appToken = slackConfig.appToken;\n\t\tthis.socketMode = slackConfig.socketMode ?? true;\n\t}\n\n\t/** Initialize the adapter */\n\tasync initialize(): Promise<void> {\n\t\t// Get bot info\n\t\tconst response = await this.apiCall<{ ok: boolean; error?: string; user_id: string; user: string }>(\"auth.test\");\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Failed to initialize Slack bot: ${response.error}`);\n\t\t}\n\n\t\tthis.botInfo = {\n\t\t\tid: response.user_id,\n\t\t\tname: response.user,\n\t\t};\n\t\tconsole.log(`Slack bot initialized: @${this.botInfo.name}`);\n\n\t\tif (this.socketMode && this.appToken) {\n\t\t\tawait this.connectSocketMode();\n\t\t} else {\n\t\t\t// HTTP mode would require an Express server\n\t\t\tthrow new Error(\"HTTP mode not implemented. Please use Socket Mode with appToken.\");\n\t\t}\n\t}\n\n\t/** Shutdown the adapter */\n\tasync shutdown(): Promise<void> {\n\t\tthis.connected = false;\n\t\tthis.ws?.close();\n\t}\n\n\t/** Send a message to Slack */\n\tasync send(message: OutboundMessage): Promise<void> {\n\t\tconst channel = message.target;\n\n\t\tfor (const part of message.parts) {\n\t\t\tswitch (part.type) {\n\t\t\t\tcase \"text\": {\n\t\t\t\t\t// Slack max message length is 4000 for blocks, but using text is safer\n\t\t\t\t\tconst chunks = this.chunkText(part.text, 4000);\n\t\t\t\t\tfor (const chunk of chunks) {\n\t\t\t\t\t\tawait this.apiCall(\"chat.postMessage\", {\n\t\t\t\t\t\t\tchannel,\n\t\t\t\t\t\t\ttext: chunk,\n\t\t\t\t\t\t\tthread_ts: message.threadId,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"typing\": {\n\t\t\t\t\t// Slack doesn't have a persistent typing indicator\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"reaction\": {\n\t\t\t\t\tif (part.messageId) {\n\t\t\t\t\t\tawait this.apiCall(\"reactions.add\", {\n\t\t\t\t\t\t\tchannel,\n\t\t\t\t\t\t\ttimestamp: part.messageId,\n\t\t\t\t\t\t\tname: this.sanitizeEmoji(part.emoji),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\t// Images and files require files.upload or blocks\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Get channel profile */\n\tasync getProfile(): Promise<ChannelProfile> {\n\t\treturn {\n\t\t\ttype: this.channelType,\n\t\t\tid: this.botInfo?.id ?? \"unknown\",\n\t\t\tname: this.botInfo?.name ?? \"Slack Bot\",\n\t\t\tusername: this.botInfo?.name,\n\t\t\tconnected: this.connected,\n\t\t\tcapabilities: {\n\t\t\t\ttext: true,\n\t\t\t\timages: true,\n\t\t\t\tfiles: true,\n\t\t\t\treactions: true,\n\t\t\t\tthreads: true,\n\t\t\t\teditMessages: true,\n\t\t\t\tdeleteMessages: true,\n\t\t\t\ttyping: false,\n\t\t\t\tcards: true, // Via Block Kit\n\t\t\t\tvoice: false,\n\t\t\t\tmaxMessageLength: 4000,\n\t\t\t\tmaxFileSize: 1 * 1024 * 1024 * 1024, // 1GB for paid workspaces\n\t\t\t},\n\t\t};\n\t}\n\n\t/** Connect via Socket Mode */\n\tprivate async connectSocketMode(): Promise<void> {\n\t\t// Get WebSocket URL\n\t\tconst response = await this.apiCall<{ ok: boolean; error?: string; url: string }>(\"apps.connections.open\", {}, this.appToken!);\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Failed to open Slack connection: ${response.error}`);\n\t\t}\n\n\t\tthis.socketUrl = response.url;\n\t\tconsole.log(\"Connecting to Slack Socket Mode...\");\n\n\t\tconst WebSocket = (await import(\"ws\")).default;\n\t\tconst ws = new WebSocket(this.socketUrl!);\n\t\tthis.ws = ws;\n\n\t\tws.on(\"open\", () => {\n\t\t\tconsole.log(\"Slack Socket Mode connected\");\n\t\t\tthis.reconnectAttempts = 0;\n\t\t});\n\n\t\tws.on(\"message\", (data: Buffer) => {\n\t\t\ttry {\n\t\t\t\tconst envelope = JSON.parse(data.toString());\n\t\t\t\tthis.handleSocketMessage(envelope);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"Failed to parse Slack message:\", error);\n\t\t\t}\n\t\t});\n\n\t\tws.on(\"close\", () => {\n\t\t\tconsole.log(\"Slack Socket Mode disconnected\");\n\t\t\tthis.connected = false;\n\t\t\tthis.attemptReconnect();\n\t\t});\n\n\t\tws.on(\"error\", (error: Error) => {\n\t\t\tconsole.error(\"Slack Socket Mode error:\", error);\n\t\t\tthis.emitError(error);\n\t\t});\n\t}\n\n\t/** Handle Socket Mode message */\n\tprivate handleSocketMessage(envelope: SlackEnvelope): void {\n\t\t// Acknowledge the message\n\t\tif (envelope.envelope_id) {\n\t\t\tthis.ws?.send(JSON.stringify({ envelope_id: envelope.envelope_id }));\n\t\t}\n\n\t\t// Process the payload\n\t\tif (envelope.payload) {\n\t\t\tthis.handleEvent(envelope.payload);\n\t\t}\n\t}\n\n\t/** Handle Slack event */\n\tprivate handleEvent(payload: SlackEventPayload): void {\n\t\tswitch (payload.type) {\n\t\t\tcase \"event_callback\":\n\t\t\t\tif (payload.event) {\n\t\t\t\t\tthis.handleEventCallback(payload.event);\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"app_rate_limited\":\n\t\t\t\tconsole.warn(\"Slack rate limited:\", payload);\n\t\t\t\tbreak;\n\n\t\t\tcase \"hello\":\n\t\t\t\tconsole.log(\"Slack Socket Mode ready\");\n\t\t\t\tthis.connected = true;\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/** Handle event callback */\n\tprivate handleEventCallback(event: SlackEvent): void {\n\t\tswitch (event.type) {\n\t\t\tcase \"message\":\n\t\t\t\tthis.handleMessageEvent(event as SlackMessageEvent);\n\t\t\t\tbreak;\n\n\t\t\tcase \"app_mention\":\n\t\t\t\tthis.handleAppMention(event as SlackAppMentionEvent);\n\t\t\t\tbreak;\n\n\t\t\t// Handle other events as needed\n\t\t}\n\t}\n\n\t/** Handle message event */\n\tprivate handleMessageEvent(event: SlackMessageEvent): void {\n\t\t// Ignore messages from self\n\t\tif (event.user === this.botInfo?.id) return;\n\n\t\t// Ignore bot messages\n\t\tif (event.bot_id) return;\n\n\t\t// Ignore message edits and deletions for now\n\t\tif (event.subtype) return;\n\n\t\t// Determine scope\n\t\tconst scope = event.channel_type === \"im\" ? \"dm\" : \"group\";\n\n\t\t// Check access control\n\t\tif (scope === \"group\" && this.config.access.groupPolicy === \"mention\") {\n\t\t\t// For Slack, we'd need to check if the bot was mentioned\n\t\t\t// This is handled by the app_mention event, so we can ignore here\n\t\t\treturn;\n\t\t}\n\n\t\t// Build sender info\n\t\tconst sender: SenderInfo = {\n\t\t\tid: event.user,\n\t\t\tname: event.user_profile?.real_name ?? event.user,\n\t\t\tusername: event.user_profile?.display_name ?? event.user,\n\t\t\tavatar: event.user_profile?.image_72,\n\t\t\tteamId: event.team,\n\t\t};\n\n\t\t// Build content\n\t\tlet content: InboundMessage[\"content\"];\n\n\t\tif (event.text) {\n\t\t\t// Check for commands\n\t\t\tif (event.text.startsWith(\"/\")) {\n\t\t\t\tconst [command, ...args] = event.text.slice(1).split(\" \");\n\t\t\t\tcontent = {\n\t\t\t\t\ttype: \"command\",\n\t\t\t\t\tcommand,\n\t\t\t\t\targs,\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\tcontent = {\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: this.cleanMentions(event.text),\n\t\t\t\t};\n\t\t\t}\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\n\t\t// Get agent ID\n\t\tconst agentId = this.config.name;\n\n\t\t// Create inbound message\n\t\tconst inboundMessage = this.createInboundMessage({\n\t\t\tid: event.ts,\n\t\t\tagentId,\n\t\t\tscope,\n\t\t\tidentifier: event.channel,\n\t\t\tsender,\n\t\t\tcontent,\n\t\t\tthreadId: event.thread_ts,\n\t\t});\n\n\t\tthis.emitMessage(inboundMessage);\n\t}\n\n\t/** Handle app mention event */\n\tprivate handleAppMention(event: SlackAppMentionEvent): void {\n\t\t// Similar to message but always a mention\n\t\tif (event.user === this.botInfo?.id) return;\n\n\t\tconst sender: SenderInfo = {\n\t\t\tid: event.user,\n\t\t\tname: event.user,\n\t\t\tteamId: event.team,\n\t\t};\n\n\t\tconst content: InboundMessage[\"content\"] = {\n\t\t\ttype: \"text\",\n\t\t\ttext: this.cleanMentions(event.text),\n\t\t};\n\n\t\tconst agentId = this.config.name;\n\n\t\tconst inboundMessage = this.createInboundMessage({\n\t\t\tid: event.ts,\n\t\t\tagentId,\n\t\t\tscope: \"group\",\n\t\t\tidentifier: event.channel,\n\t\t\tsender,\n\t\t\tcontent,\n\t\t\tthreadId: event.thread_ts,\n\t\t});\n\n\t\tthis.emitMessage(inboundMessage);\n\t}\n\n\t/** Attempt to reconnect */\n\tprivate attemptReconnect(): void {\n\t\tif (this.reconnectAttempts < this.maxReconnectAttempts) {\n\t\t\tthis.reconnectAttempts++;\n\t\t\tconst delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);\n\t\t\tconsole.log(`Attempting to reconnect to Slack in ${delay}ms (attempt ${this.reconnectAttempts})`);\n\t\t\tsetTimeout(() => this.connectSocketMode(), delay);\n\t\t} else {\n\t\t\tconsole.error(\"Max reconnection attempts reached\");\n\t\t\tthis.emitError(new Error(\"Failed to reconnect to Slack after maximum attempts\"));\n\t\t}\n\t}\n\n\t/** Clean mentions from text */\n\tprivate cleanMentions(text: string): string {\n\t\t// Replace <@USER_ID> with @username or remove\n\t\treturn text.replace(/<@([A-Z0-9]+)>/g, \"\").trim();\n\t}\n\n\t/** Sanitize emoji for Slack */\n\tprivate sanitizeEmoji(emoji: string): string {\n\t\t// Remove colons if present\n\t\treturn emoji.replace(/:/g, \"\");\n\t}\n\n\t/** Make API call to Slack */\n\tprivate async apiCall<T = unknown>(\n\t\tmethod: string,\n\t\tparams: Record<string, unknown> = {},\n\t\ttoken?: string,\n\t): Promise<T> {\n\t\tconst url = `https://slack.com/api/${method}`;\n\t\tconst authToken = token ?? this.botToken;\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${authToken}`,\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tbody: JSON.stringify(params),\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Slack API error: ${response.status} ${response.statusText}`);\n\t\t}\n\n\t\treturn response.json() as T;\n\t}\n}\n\n/** Register with global registry */\nglobalChannelRegistry.register(\"slack\", SlackAdapter);\n\n/** Slack API types */\ninterface SlackUser {\n\tid: string;\n\tname: string;\n}\n\ninterface SlackEnvelope {\n\tenvelope_id?: string;\n\tpayload?: SlackEventPayload;\n}\n\ninterface SlackEventPayload {\n\ttype: string;\n\tevent?: SlackMessageEvent | SlackAppMentionEvent;\n\tretry_attempt?: number;\n}\n\ninterface SlackEvent {\n\ttype: string;\n}\n\ninterface SlackMessageEvent extends SlackEvent {\n\tuser: string;\n\tchannel: string;\n\tts: string;\n\ttext: string;\n\tteam?: string;\n\tchannel_type: string;\n\tthread_ts?: string;\n\tbot_id?: string;\n\tsubtype?: string;\n\tuser_profile?: {\n\t\treal_name?: string;\n\t\tdisplay_name?: string;\n\t\timage_72?: string;\n\t};\n}\n\ninterface SlackAppMentionEvent extends SlackEvent {\n\tuser: string;\n\tchannel: string;\n\tts: string;\n\ttext: string;\n\tteam: string;\n\tthread_ts?: string;\n}\n\nexport { SlackAdapter };\n"]}