{"version":3,"file":"channel-sender.mjs","names":[],"sources":["../../../src/services/channel-sender.ts"],"sourcesContent":["/**\n * Channel-Agnostic Message Sender\n *\n * Abstracts OpenClaw's per-channel sendMessage* functions into a single\n * interface so the rest of the crypto extension never hard-codes a channel.\n *\n * Dynamically discovers available channels from `api.runtime.channel` at\n * runtime, so new channels added to OpenClaw (e.g. Matrix, Teams, Nostr)\n * are picked up automatically.\n *\n * Usage:\n *   const sender = createChannelSender(api);\n *   await sender.send('telegram', chatId, 'Hello!');\n *\n * Or with auto-detection from session key / hook context:\n *   await sender.sendToSession(sessionKey, chatId, 'Hello!');\n */\n\n// ── Types ───────────────────────────────────────────────────────────────────\n\n/**\n * Channel identifier. This is a string (not a union) so new channels added\n * upstream are supported without code changes here.\n */\nexport type ChannelId = string;\n\n/**\n * Well-known channels for static references. This list does NOT limit which\n * channels can be used — it's only for code that needs a specific channel name.\n */\nconst WELL_KNOWN_CHANNELS = new Set([\n  'telegram', 'discord', 'slack', 'signal', 'imessage', 'bluebubbles',\n  'whatsapp', 'line', 'matrix', 'msteams', 'googlechat', 'feishu',\n  'irc', 'mattermost', 'nextcloud-talk', 'nostr', 'synology-chat',\n  'tlon', 'twitch', 'zalo', 'zalouser', 'webchat',\n]);\n\n/**\n * Known sendMessage function naming convention per channel.\n * OpenClaw uses: runtime.channel.<name>.sendMessage<PascalCase>\n * We map from the channel name to the expected function name.\n */\nconst SEND_FN_OVERRIDES: Record<string, string> = {\n  imessage: 'sendMessageIMessage',\n  whatsapp: 'sendMessageWhatsApp',\n  msteams: 'sendMessageMSTeams',\n  googlechat: 'sendMessageGoogleChat',\n  bluebubbles: 'sendMessageBlueBubbles',\n  'nextcloud-talk': 'sendMessageNextcloudTalk',\n  'synology-chat': 'sendMessageSynologyChat',\n  webchat: 'sendMessageWebChat',\n  zalouser: 'sendMessageZaloUser',\n};\n\nexport interface SendOptions {\n  /** Account ID for multi-account setups. Defaults to 'default'. */\n  accountId?: string;\n}\n\nexport interface ChannelSender {\n  /**\n   * Send a message to a specific channel + recipient.\n   * Returns true on success, false if the channel's send function isn't available.\n   */\n  send(channel: ChannelId, recipientId: string, text: string, opts?: SendOptions): Promise<boolean>;\n\n  /**\n   * Auto-detect channel from a session key (e.g. \"telegram-123456\", \"discord:789\")\n   * and send to the extracted recipient.\n   */\n  sendToSession(sessionKey: string, text: string, opts?: SendOptions): Promise<boolean>;\n\n  /**\n   * Check if a channel's send function is available on the current runtime.\n   */\n  isAvailable(channel: ChannelId): boolean;\n\n  /**\n   * List all channels that have a send function available on the current runtime.\n   */\n  availableChannels(): string[];\n}\n\n// ── Session Key Parsing ─────────────────────────────────────────────────────\n\n/**\n * Check if a string looks like a known channel name.\n * Accepts well-known channels plus any key present on `api.runtime.channel`.\n */\nfunction isChannelName(candidate: string, runtimeChannels?: Set<string>): boolean {\n  if (WELL_KNOWN_CHANNELS.has(candidate)) return true;\n  if (runtimeChannels?.has(candidate)) return true;\n  return false;\n}\n\n/** Lazily built set of runtime channel names for parseSessionKey. */\nlet _runtimeChannelNames: Set<string> | undefined;\n\n/**\n * Provide runtime channel names so parseSessionKey can recognize dynamically\n * registered channels. Called once when createChannelSender initializes.\n */\nexport function setRuntimeChannelNames(names: Set<string>): void {\n  _runtimeChannelNames = names;\n}\n\n/**\n * Parse a session key to extract the channel and user/chat ID.\n *\n * OpenClaw session keys follow these patterns:\n *   telegram-123456789        (Telegram user ID)\n *   telegram:123456789        (alternate separator)\n *   discord-987654321         (Discord user/channel ID)\n *   discord:987654321\n *   slack-U1234567            (Slack user ID)\n *   signal-+15551234567       (Signal phone number)\n *   whatsapp-15551234567      (WhatsApp)\n *   imessage-user@icloud.com  (iMessage)\n *   matrix-@user:server.com   (Matrix)\n *   line-U1234567             (LINE user ID)\n *\n * Also handles compound keys like \"telegram-123456789-agent-default\".\n */\nexport function parseSessionKey(sessionKey: string): { channel: ChannelId; userId: string } | null {\n  if (!sessionKey) return null;\n\n  // Try pattern: <channel><separator><userId>[<separator>rest...]\n  // Separator is either '-' or ':'\n  for (const sep of [':', '-']) {\n    const idx = sessionKey.indexOf(sep);\n    if (idx === -1) continue;\n\n    const candidate = sessionKey.slice(0, idx).toLowerCase();\n    if (isChannelName(candidate, _runtimeChannelNames)) {\n      // userId is everything between the first and second separator (or end of string)\n      const rest = sessionKey.slice(idx + 1);\n      // For compound keys like \"telegram-123456-agent-default\", extract just the ID\n      // We look for a numeric-ish or identifier portion\n      const idMatch = rest.match(/^([^-:]+)/);\n      const userId = idMatch?.[1] ?? rest;\n      if (userId) {\n        return { channel: candidate, userId };\n      }\n    }\n  }\n\n  return null;\n}\n\n/**\n * Extract a user/sender ID from hook context — channel-agnostic.\n *\n * Tries multiple sources in priority order:\n * 1. ctx.requesterSenderId (OpenClaw tool context)\n * 2. ctx.senderId\n * 3. event.from / event.metadata.senderId\n * 4. parseSessionKey(ctx.sessionKey).userId\n */\nexport function extractSenderId(event: any, ctx: any): string | null {\n  // Direct from context (most reliable)\n  if (ctx?.requesterSenderId) return String(ctx.requesterSenderId);\n  if (ctx?.senderId) return String(ctx.senderId);\n\n  // From event payload\n  if (event?.from) return String(event.from);\n  if (event?.metadata?.senderId) return String(event.metadata.senderId);\n\n  // Fall back to session key parsing\n  if (ctx?.sessionKey) {\n    const parsed = parseSessionKey(ctx.sessionKey);\n    if (parsed) return parsed.userId;\n  }\n\n  return null;\n}\n\n/**\n * Extract the channel ID from hook context — channel-agnostic.\n *\n * Tries:\n * 1. ctx.channelId (directly from OpenClaw hook context)\n * 2. ctx.messageChannel\n * 3. parseSessionKey(ctx.sessionKey).channel\n */\nexport function extractChannelId(ctx: any): ChannelId | null {\n  if (ctx?.channelId && typeof ctx.channelId === 'string') {\n    return ctx.channelId;\n  }\n  if (ctx?.messageChannel && typeof ctx.messageChannel === 'string') {\n    return ctx.messageChannel;\n  }\n  if (ctx?.sessionKey) {\n    const parsed = parseSessionKey(ctx.sessionKey);\n    if (parsed) return parsed.channel;\n  }\n  return null;\n}\n\n// ── Channel Sender Factory ──────────────────────────────────────────────────\n\n/**\n * Derive the expected sendMessage function name for a channel.\n *\n * Convention: runtime.channel.<name>.sendMessage<PascalCase>\n * e.g., telegram → sendMessageTelegram, discord → sendMessageDiscord\n *\n * Some channels have non-standard casing — handled via SEND_FN_OVERRIDES.\n */\nfunction deriveSendFnName(channel: string): string {\n  const override = SEND_FN_OVERRIDES[channel];\n  if (override) return override;\n  // Default: sendMessage + capitalize first letter\n  return 'sendMessage' + channel.charAt(0).toUpperCase() + channel.slice(1);\n}\n\n/**\n * Create a channel-agnostic sender backed by the plugin API's runtime.\n *\n * Dynamically discovers channels from `api.runtime.channel` so new upstream\n * channels are automatically supported.\n */\nexport function createChannelSender(api: any): ChannelSender {\n  // Build runtime channel name set for parseSessionKey to use\n  const runtime = api.runtime?.channel;\n  if (runtime && typeof runtime === 'object') {\n    const names = new Set(Object.keys(runtime));\n    setRuntimeChannelNames(names);\n  }\n\n  /** Get the send function for a given channel, or null if unavailable. */\n  function getSendFn(channel: string): ((recipientId: string, text: string, opts: any) => Promise<any>) | null {\n    if (!runtime) return null;\n\n    const channelRuntime = runtime[channel];\n    if (!channelRuntime) return null;\n\n    // Try the derived function name first\n    const fnName = deriveSendFnName(channel);\n    if (typeof channelRuntime[fnName] === 'function') {\n      return channelRuntime[fnName];\n    }\n\n    // Fallback: look for any function matching sendMessage*\n    for (const key of Object.keys(channelRuntime)) {\n      if (key.startsWith('sendMessage') && typeof channelRuntime[key] === 'function') {\n        return channelRuntime[key];\n      }\n    }\n\n    return null;\n  }\n\n  return {\n    async send(channel, recipientId, text, opts) {\n      const sendFn = getSendFn(channel);\n      if (!sendFn) {\n        api.logger?.warn?.(\n          `[crypto] Channel sender: ${channel} sendMessage not available`\n        );\n        return false;\n      }\n\n      try {\n        await sendFn(recipientId, text, { accountId: opts?.accountId ?? 'default' });\n        return true;\n      } catch (err) {\n        api.logger?.warn?.(\n          `[crypto] Channel sender: failed to send via ${channel}: ${err instanceof Error ? err.message : String(err)}`\n        );\n        return false;\n      }\n    },\n\n    async sendToSession(sessionKey, text, opts) {\n      const parsed = parseSessionKey(sessionKey);\n      if (!parsed) {\n        api.logger?.warn?.(\n          `[crypto] Channel sender: could not parse session key \"${sessionKey}\"`\n        );\n        return false;\n      }\n      return this.send(parsed.channel, parsed.userId, text, opts);\n    },\n\n    isAvailable(channel) {\n      return getSendFn(channel) !== null;\n    },\n\n    availableChannels() {\n      if (!runtime || typeof runtime !== 'object') return [];\n      return Object.keys(runtime).filter(ch => getSendFn(ch) !== null);\n    },\n  };\n}\n"],"mappings":"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"}