UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

185 lines (163 loc) 5.57 kB
/** * Twitch outbound adapter for sending messages. * * Implements the ChannelOutboundAdapter interface for Twitch chat. * Supports text and media (URL) sending with markdown stripping and chunking. */ import type { ChannelOutboundAdapter, ChannelOutboundContext, OutboundDeliveryResult, } from "./types.js"; import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js"; import { sendMessageTwitchInternal } from "./send.js"; import { chunkTextForTwitch } from "./utils/markdown.js"; import { missingTargetError, normalizeTwitchChannel } from "./utils/twitch.js"; /** * Twitch outbound adapter. * * Handles sending text and media to Twitch channels with automatic * markdown stripping and message chunking. */ export const twitchOutbound: ChannelOutboundAdapter = { /** Direct delivery mode - messages are sent immediately */ deliveryMode: "direct", /** Twitch chat message limit is 500 characters */ textChunkLimit: 500, /** Word-boundary chunker with markdown stripping */ chunker: chunkTextForTwitch, /** * Resolve target from context. * * Handles target resolution with allowlist support for implicit/heartbeat modes. * For explicit mode, accepts any valid channel name. * * @param params - Resolution parameters * @returns Resolved target or error */ resolveTarget: ({ to, allowFrom, mode }) => { const trimmed = to?.trim() ?? ""; const allowListRaw = (allowFrom ?? []) .map((entry: unknown) => String(entry).trim()) .filter(Boolean); const hasWildcard = allowListRaw.includes("*"); const allowList = allowListRaw .filter((entry: string) => entry !== "*") .map((entry: string) => normalizeTwitchChannel(entry)) .filter((entry): entry is string => entry.length > 0); // If target is provided, normalize and validate it if (trimmed) { const normalizedTo = normalizeTwitchChannel(trimmed); // For implicit/heartbeat modes with allowList, check against allowlist if (mode === "implicit" || mode === "heartbeat") { if (hasWildcard || allowList.length === 0) { return { ok: true, to: normalizedTo }; } if (allowList.includes(normalizedTo)) { return { ok: true, to: normalizedTo }; } // Fallback to first allowFrom entry return { ok: true, to: allowList[0] }; } // For explicit mode, accept any valid channel name return { ok: true, to: normalizedTo }; } // No target provided, use allowFrom fallback if (allowList.length > 0) { return { ok: true, to: allowList[0] }; } // No target and no allowFrom - error return { ok: false, error: missingTargetError( "Twitch", "<channel-name> or channels.twitch.accounts.<account>.allowFrom[0]", ), }; }, /** * Send a text message to a Twitch channel. * * Strips markdown if enabled, validates account configuration, * and sends the message via the Twitch client. * * @param params - Send parameters including target, text, and config * @returns Delivery result with message ID and status * * @example * const result = await twitchOutbound.sendText({ * cfg: openclawConfig, * to: "#mychannel", * text: "Hello Twitch!", * accountId: "default", * }); */ sendText: async (params: ChannelOutboundContext): Promise<OutboundDeliveryResult> => { const { cfg, to, text, accountId, signal } = params; if (signal?.aborted) { throw new Error("Outbound delivery aborted"); } const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID; const account = getAccountConfig(cfg, resolvedAccountId); if (!account) { const availableIds = Object.keys(cfg.channels?.twitch?.accounts ?? {}); throw new Error( `Twitch account not found: ${resolvedAccountId}. ` + `Available accounts: ${availableIds.join(", ") || "none"}`, ); } const channel = to || account.channel; if (!channel) { throw new Error("No channel specified and no default channel in account config"); } const result = await sendMessageTwitchInternal( normalizeTwitchChannel(channel), text, cfg, resolvedAccountId, true, // stripMarkdown console, ); if (!result.ok) { throw new Error(result.error ?? "Send failed"); } return { channel: "twitch", messageId: result.messageId, timestamp: Date.now(), to: normalizeTwitchChannel(channel), }; }, /** * Send media to a Twitch channel. * * Note: Twitch chat doesn't support direct media uploads. * This sends the media URL as text instead. * * @param params - Send parameters including media URL * @returns Delivery result with message ID and status * * @example * const result = await twitchOutbound.sendMedia({ * cfg: openclawConfig, * to: "#mychannel", * text: "Check this out!", * mediaUrl: "https://example.com/image.png", * accountId: "default", * }); */ sendMedia: async (params: ChannelOutboundContext): Promise<OutboundDeliveryResult> => { const { text, mediaUrl, signal } = params; if (signal?.aborted) { throw new Error("Outbound delivery aborted"); } const message = mediaUrl ? `${text || ""} ${mediaUrl}`.trim() : text; if (!twitchOutbound.sendText) { throw new Error("sendText not implemented"); } return twitchOutbound.sendText({ ...params, text: message, }); }, };