{"version":3,"file":"discord.d.ts","sourceRoot":"","sources":["../../../src/gateway/channels/discord.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,oCAAoC;AACpC,MAAM,WAAW,aAAa;IAC7B,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,8BAA8B;AAC9B,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;CAiBjB,CAAC;AAEX,sBAAsB;AACtB,qBAAa,cAAe,SAAQ,kBAAkB;IACrD,QAAQ,CAAC,WAAW,aAAa;IAEjC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,EAAE,CAAC,CAAY;IACvB,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,cAAc,CAAC,CAAS;IAChC,OAAO,CAAC,iBAAiB,CAAC,CAAiB;IAC3C,OAAO,CAAC,OAAO,CAAC,CAAc;IAC9B,OAAO,CAAC,gBAAgB,CAAC,CAAS;IAElC,YAAY,MAAM,EAAE,aAAa,EAOhC;IAED,6BAA6B;IACvB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAQhC;IAED,2BAA2B;IACrB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAM9B;IAED,gCAAgC;IAC1B,IAAI,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA2ClD;IAED,0BAA0B;IACpB,UAAU,IAAI,OAAO,CAAC,cAAc,CAAC,CAsB1C;YAGa,cAAc;IAqC5B,6BAA6B;IAC7B,OAAO,CAAC,oBAAoB;IAsC5B,0BAA0B;IAC1B,OAAO,CAAC,WAAW;IAenB,qBAAqB;IACrB,OAAO,CAAC,aAAa;IASrB,4BAA4B;IAC5B,OAAO,CAAC,QAAQ;IAgBhB,0BAA0B;IAC1B,OAAO,CAAC,MAAM;IAiBd,4BAA4B;IAC5B,OAAO,CAAC,cAAc;IAqBtB,yBAAyB;IACzB,OAAO,CAAC,WAAW;IAOnB,kCAAkC;IAClC,OAAO,CAAC,mBAAmB;IAmE3B,gCAAgC;IAChC,OAAO,CAAC,WAAW;IAInB,+BAA+B;IAC/B,OAAO,CAAC,aAAa;YAMP,OAAO;CA2BrB","sourcesContent":["/**\n * Discord Channel Adapter\n *\n * Uses Discord Gateway WebSocket and REST API\n */\n\nimport { 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/** Discord channel configuration */\nexport interface DiscordConfig {\n\t/** Bot token */\n\tbotToken: string;\n\t/** Application ID */\n\tapplicationId: string;\n\t/** Intents (default: basic + messages + guilds) */\n\tintents?: number;\n}\n\n/** Discord Gateway intents */\nexport const DiscordIntents = {\n\tGUILDS: 1 << 0,\n\tGUILD_MEMBERS: 1 << 1,\n\tGUILD_MODERATION: 1 << 2,\n\tGUILD_EMOJIS_AND_STICKERS: 1 << 3,\n\tGUILD_INTEGRATIONS: 1 << 4,\n\tGUILD_WEBHOOKS: 1 << 5,\n\tGUILD_INVITES: 1 << 6,\n\tGUILD_VOICE_STATES: 1 << 7,\n\tGUILD_PRESENCES: 1 << 8,\n\tGUILD_MESSAGES: 1 << 9,\n\tGUILD_MESSAGE_REACTIONS: 1 << 10,\n\tGUILD_MESSAGE_TYPING: 1 << 11,\n\tDIRECT_MESSAGES: 1 << 12,\n\tDIRECT_MESSAGE_REACTIONS: 1 << 13,\n\tDIRECT_MESSAGE_TYPING: 1 << 14,\n\tMESSAGE_CONTENT: 1 << 15,\n} as const;\n\n/** Discord adapter */\nexport class DiscordAdapter extends BaseChannelAdapter {\n\treadonly channelType = \"discord\";\n\n\tprivate botToken: string;\n\tprivate applicationId: string;\n\tprivate intents: number;\n\tprivate ws?: WebSocket;\n\tprivate sessionId?: string;\n\tprivate sequenceNumber?: number;\n\tprivate heartbeatInterval?: NodeJS.Timeout;\n\tprivate botInfo?: DiscordUser;\n\tprivate resumeGatewayUrl?: string;\n\n\tconstructor(config: ChannelConfig) {\n\t\tsuper(config);\n\t\tconst discordConfig = config.config as unknown as DiscordConfig;\n\t\tthis.botToken = discordConfig.botToken;\n\t\tthis.applicationId = discordConfig.applicationId;\n\t\t// Default intents: GUILDS + GUILD_MESSAGES + DIRECT_MESSAGES + MESSAGE_CONTENT\n\t\tthis.intents = discordConfig.intents ?? 0b11111111110111;\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<DiscordUser>(\"users/@me\");\n\t\tthis.botInfo = response;\n\t\tconsole.log(`Discord bot initialized: ${this.botInfo?.username}#${this.botInfo?.discriminator}`);\n\n\t\t// Connect to Gateway\n\t\tawait this.connectGateway();\n\t}\n\n\t/** Shutdown the adapter */\n\tasync shutdown(): Promise<void> {\n\t\tthis.connected = false;\n\t\tif (this.heartbeatInterval) {\n\t\t\tclearInterval(this.heartbeatInterval);\n\t\t}\n\t\tthis.ws?.close();\n\t}\n\n\t/** Send a message to Discord */\n\tasync send(message: OutboundMessage): Promise<void> {\n\t\tconst channelId = 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// Discord max message length is 2000\n\t\t\t\t\tconst chunks = this.chunkText(part.text, 2000);\n\t\t\t\t\tfor (const chunk of chunks) {\n\t\t\t\t\t\tawait this.apiCall(`channels/${channelId}/messages`, {\n\t\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\t\tcontent: chunk,\n\t\t\t\t\t\t\t\tmessage_reference: message.replyTo\n\t\t\t\t\t\t\t\t\t? { message_id: message.replyTo }\n\t\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\t},\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// Send typing indicator\n\t\t\t\t\tawait this.apiCall(`channels/${channelId}/typing`, { method: \"POST\" });\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\t// URL encode emoji for API\n\t\t\t\t\t\tconst emoji = encodeURIComponent(part.emoji);\n\t\t\t\t\t\tawait this.apiCall(\n\t\t\t\t\t\t\t`channels/${channelId}/messages/${part.messageId}/reactions/${emoji}/@me`,\n\t\t\t\t\t\t\t{ method: \"PUT\" },\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 multipart/form-data, skip for now\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?.username ?? \"Discord Bot\",\n\t\t\tusername: this.botInfo?.username,\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: true,\n\t\t\t\tcards: true, // Via embeds\n\t\t\t\tvoice: false, // Bot can join but not implemented here\n\t\t\t\tmaxMessageLength: 2000,\n\t\t\t\tmaxFileSize: 25 * 1024 * 1024, // 25MB\n\t\t\t},\n\t\t};\n\t}\n\n\t/** Connect to Discord Gateway */\n\tprivate async connectGateway(): Promise<void> {\n\t\t// Get gateway URL\n\t\tconst gatewayInfo = await this.apiCall<DiscordGatewayInfo>(\"gateway/bot\");\n\t\tconst gatewayUrl = this.resumeGatewayUrl ?? `${gatewayInfo?.url ?? \"wss://gateway.discord.gg\"}?v=10&encoding=json`;\n\n\t\tconsole.log(`Connecting to Discord Gateway: ${gatewayUrl}`);\n\n\t\tthis.ws = new WebSocket(gatewayUrl);\n\n\t\tthis.ws.on(\"open\", () => {\n\t\t\tconsole.log(\"Discord Gateway connected\");\n\t\t});\n\n\t\tthis.ws.on(\"message\", (data: Buffer) => {\n\t\t\ttry {\n\t\t\t\tconst payload = JSON.parse(data.toString());\n\t\t\t\tthis.handleGatewayPayload(payload);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"Failed to parse Discord payload:\", error);\n\t\t\t}\n\t\t});\n\n\t\tthis.ws.on(\"close\", (code: number) => {\n\t\t\tconsole.log(`Discord Gateway closed: ${code}`);\n\t\t\tthis.connected = false;\n\t\t\tif (code !== 1000) {\n\t\t\t\t// Attempt to reconnect\n\t\t\t\tsetTimeout(() => this.connectGateway(), 5000);\n\t\t\t}\n\t\t});\n\n\t\tthis.ws.on(\"error\", (error: Error) => {\n\t\t\tconsole.error(\"Discord Gateway error:\", error);\n\t\t\tthis.emitError(error);\n\t\t});\n\t}\n\n\t/** Handle Gateway payload */\n\tprivate handleGatewayPayload(payload: DiscordPayload): void {\n\t\t// Update sequence number\n\t\tif (payload.s !== undefined) {\n\t\t\tthis.sequenceNumber = payload.s;\n\t\t}\n\n\t\tswitch (payload.op) {\n\t\t\tcase 10: // Hello\n\t\t\t\tthis.handleHello(payload.d);\n\t\t\t\tbreak;\n\n\t\t\tcase 11: // Heartbeat ACK\n\t\t\t\t// Heartbeat acknowledged\n\t\t\t\tbreak;\n\n\t\t\tcase 0: // Dispatch\n\t\t\t\tif (payload.t) {\n\t\t\t\t\tthis.handleDispatch(payload.t, payload.d);\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 1: // Heartbeat\n\t\t\t\tthis.sendHeartbeat();\n\t\t\t\tbreak;\n\n\t\t\tcase 7: // Reconnect\n\t\t\t\tconsole.log(\"Discord requested reconnect\");\n\t\t\t\tthis.ws?.close(1000);\n\t\t\t\tbreak;\n\n\t\t\tcase 9: // Invalid Session\n\t\t\t\tconsole.log(\"Discord session invalid, re-identifying\");\n\t\t\t\tthis.sessionId = undefined;\n\t\t\t\tsetTimeout(() => this.identify(), 5000);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/** Handle Hello opcode */\n\tprivate handleHello(data: unknown): void {\n\t\tconst helloData = data as { heartbeat_interval: number };\n\t\t// Start heartbeat\n\t\tthis.heartbeatInterval = setInterval(() => {\n\t\t\tthis.sendHeartbeat();\n\t\t}, helloData.heartbeat_interval);\n\n\t\t// Identify or resume\n\t\tif (this.sessionId) {\n\t\t\tthis.resume();\n\t\t} else {\n\t\t\tthis.identify();\n\t\t}\n\t}\n\n\t/** Send heartbeat */\n\tprivate sendHeartbeat(): void {\n\t\tif (this.ws?.readyState === WebSocket.OPEN) {\n\t\t\tthis.ws.send(JSON.stringify({\n\t\t\t\top: 1,\n\t\t\t\td: this.sequenceNumber ?? null,\n\t\t\t}));\n\t\t}\n\t}\n\n\t/** Send identify payload */\n\tprivate identify(): void {\n\t\tconsole.log(\"Identifying with Discord Gateway\");\n\t\tthis.ws?.send(JSON.stringify({\n\t\t\top: 2,\n\t\t\td: {\n\t\t\t\ttoken: this.botToken,\n\t\t\t\tintents: this.intents,\n\t\t\t\tproperties: {\n\t\t\t\t\tos: process.platform,\n\t\t\t\t\tbrowser: \"Piagnet\",\n\t\t\t\t\tdevice: \"Piagnet\",\n\t\t\t\t},\n\t\t\t},\n\t\t}));\n\t}\n\n\t/** Send resume payload */\n\tprivate resume(): void {\n\t\tif (!this.sessionId) {\n\t\t\tconsole.log(\"No session to resume, identifying instead\");\n\t\t\tthis.identify();\n\t\t\treturn;\n\t\t}\n\t\tconsole.log(\"Resuming Discord session\");\n\t\tthis.ws?.send(JSON.stringify({\n\t\t\top: 6,\n\t\t\td: {\n\t\t\t\ttoken: this.botToken,\n\t\t\t\tsession_id: this.sessionId,\n\t\t\t\tseq: this.sequenceNumber,\n\t\t\t},\n\t\t}));\n\t}\n\n\t/** Handle dispatch event */\n\tprivate handleDispatch(eventType: string, data: unknown): void {\n\t\tswitch (eventType) {\n\t\t\tcase \"READY\":\n\t\t\t\tthis.handleReady(data as DiscordReady);\n\t\t\t\tbreak;\n\n\t\t\tcase \"RESUMED\":\n\t\t\t\tconsole.log(\"Discord session resumed\");\n\t\t\t\tthis.connected = true;\n\t\t\t\tbreak;\n\n\t\t\tcase \"MESSAGE_CREATE\":\n\t\t\t\tthis.handleMessageCreate(data as DiscordMessage);\n\t\t\t\tbreak;\n\n\t\t\tcase \"INTERACTION_CREATE\":\n\t\t\t\t// Handle slash commands\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/** Handle Ready event */\n\tprivate handleReady(data: DiscordReady): void {\n\t\tconsole.log(`Discord ready: ${data.user.username}#${data.user.discriminator}`);\n\t\tthis.sessionId = data.session_id;\n\t\tthis.resumeGatewayUrl = data.resume_gateway_url;\n\t\tthis.connected = true;\n\t}\n\n\t/** Handle message create event */\n\tprivate handleMessageCreate(message: DiscordMessage): void {\n\t\t// Ignore own messages\n\t\tif (message.author.id === this.botInfo?.id) return;\n\n\t\t// Ignore bots\n\t\tif (message.author.bot) return;\n\n\t\t// Determine scope\n\t\tconst scope = message.guild_id ? \"group\" : \"dm\";\n\n\t\t// Check access control\n\t\tif (scope === \"group\" && this.config.access.groupPolicy === \"mention\") {\n\t\t\tconst isMentioned = this.isMentioned(message);\n\t\t\tif (!isMentioned) return;\n\t\t}\n\n\t\t// Build sender info\n\t\tconst sender: SenderInfo = {\n\t\t\tid: message.author.id,\n\t\t\tname: message.author.global_name ?? message.author.username,\n\t\t\tusername: message.author.username,\n\t\t\tavatar: message.author.avatar\n\t\t\t\t? `https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png`\n\t\t\t\t: undefined,\n\t\t\tisAdmin: false, // Would need guild member fetch\n\t\t\tguildId: message.guild_id,\n\t\t};\n\n\t\t// Build content\n\t\tlet content: InboundMessage[\"content\"];\n\n\t\tif (message.content) {\n\t\t\t// Check for commands\n\t\t\tif (message.content.startsWith(\"/\")) {\n\t\t\t\tconst [command, ...args] = message.content.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(message.content),\n\t\t\t\t};\n\t\t\t}\n\t\t} else {\n\t\t\treturn; // Ignore empty messages for now\n\t\t}\n\n\t\t// Get agent ID from binding\n\t\tconst agentId = this.config.name;\n\n\t\t// Create inbound message\n\t\tconst inboundMessage = this.createInboundMessage({\n\t\t\tid: message.id,\n\t\t\tagentId,\n\t\t\tscope,\n\t\t\tidentifier: message.channel_id,\n\t\t\tsender,\n\t\t\tcontent,\n\t\t\treplyTo: message.referenced_message?.id,\n\t\t});\n\n\t\tthis.emitMessage(inboundMessage);\n\t}\n\n\t/** Check if bot is mentioned */\n\tprivate isMentioned(message: DiscordMessage): boolean {\n\t\treturn message.mentions?.some((m) => m.id === this.botInfo?.id) ?? false;\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(/<@!?\\d+>/g, \"\").trim();\n\t}\n\n\t/** Make API call to Discord */\n\tprivate async apiCall<T = unknown>(\n\t\tendpoint: string,\n\t\toptions: { method?: string; body?: unknown } = {},\n\t): Promise<T> {\n\t\tconst url = `https://discord.com/api/v10/${endpoint}`;\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: options.method ?? \"GET\",\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bot ${this.botToken}`,\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tbody: options.body ? JSON.stringify(options.body) : undefined,\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst error = await response.text();\n\t\t\tthrow new Error(`Discord API error: ${response.status} ${error}`);\n\t\t}\n\n\t\t// Handle 204 No Content\n\t\tif (response.status === 204) {\n\t\t\treturn undefined as T;\n\t\t}\n\n\t\treturn response.json() as T;\n\t}\n}\n\n/** Register with global registry */\nglobalChannelRegistry.register(\"discord\", DiscordAdapter);\n\n/** Discord API types */\ninterface DiscordPayload {\n\top: number;\n\td?: unknown;\n\ts?: number;\n\tt?: string;\n}\n\ninterface DiscordUser {\n\tid: string;\n\tusername: string;\n\tdiscriminator: string;\n\tglobal_name?: string;\n\tavatar?: string;\n\tbot?: boolean;\n}\n\ninterface DiscordMessage {\n\tid: string;\n\tchannel_id: string;\n\tguild_id?: string;\n\tauthor: DiscordUser;\n\tcontent: string;\n\ttimestamp: string;\n\tmentions: DiscordUser[];\n\treferenced_message?: DiscordMessage;\n}\n\ninterface DiscordReady {\n\tv: number;\n\tuser: DiscordUser;\n\tguilds: unknown[];\n\tsession_id: string;\n\tresume_gateway_url: string;\n}\n\ninterface DiscordGatewayInfo {\n\turl: string;\n}\n"]}