Files
openclaw/extensions/twitch/src/send.ts
jaydenfyi f5c90f0e5c feat: Twitch Plugin (#1612)
* wip

* copy polugin files

* wip type changes

* refactor: improve Twitch plugin code quality and fix all tests

- Extract client manager registry for centralized lifecycle management
- Refactor to use early returns and reduce mutations
- Fix status check logic for clientId detection
- Add comprehensive test coverage for new modules
- Remove tests for unimplemented features (index.test.ts, resolver.test.ts)
- Fix mock setup issues in test suite (149 tests now passing)
- Improve error handling with errorResponse helper in actions.ts
- Normalize token handling to eliminate duplication

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* use accountId

* delete md file

* delte tsconfig

* adjust log level

* fix probe logic

* format

* fix monitor

* code review fixes

* format

* no mutation

* less mutation

* chain debug log

* await authProvider setup

* use uuid

* use spread

* fix tests

* update docs and remove bot channel fallback

* more readme fixes

* remove comments + fromat

* fix tests

* adjust access control logic

* format

* install

* simplify config object

* remove duplicate log tags + log received messages

* update docs

* update tests

* format

* strip markdown in monitor

* remove strip markdown config, enabled by default

* default requireMention to true

* fix store path arg

* fix multi account id + add unit test

* fix multi account id + add unit test

* make channel required and update docs

* remove whisper functionality

* remove duplicate connect log

* update docs with convert twitch link

* make twitch message processing non blocking

* schema consistent casing

* remove noisy ignore log

* use coreLogger

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-26 13:48:10 -06:00

137 lines
3.9 KiB
TypeScript

/**
* Twitch message sending functions with dependency injection support.
*
* These functions are the primary interface for sending messages to Twitch.
* They support dependency injection via the `deps` parameter for testability.
*/
import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js";
import { getClientManager as getRegistryClientManager } from "./client-manager-registry.js";
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
import { resolveTwitchToken } from "./token.js";
import { stripMarkdownForTwitch } from "./utils/markdown.js";
import { generateMessageId, isAccountConfigured, normalizeTwitchChannel } from "./utils/twitch.js";
/**
* Result from sending a message to Twitch.
*/
export interface SendMessageResult {
/** Whether the send was successful */
ok: boolean;
/** The message ID (generated for tracking) */
messageId: string;
/** Error message if the send failed */
error?: string;
}
/**
* Internal send function used by the outbound adapter.
*
* This function has access to the full Clawdbot config and handles
* account resolution, markdown stripping, and actual message sending.
*
* @param channel - The channel name
* @param text - The message text
* @param cfg - Full Clawdbot configuration
* @param accountId - Account ID to use
* @param stripMarkdown - Whether to strip markdown (default: true)
* @param logger - Logger instance
* @returns Result with message ID and status
*
* @example
* const result = await sendMessageTwitchInternal(
* "#mychannel",
* "Hello Twitch!",
* clawdbotConfig,
* "default",
* true,
* console,
* );
*/
export async function sendMessageTwitchInternal(
channel: string,
text: string,
cfg: ClawdbotConfig,
accountId: string = DEFAULT_ACCOUNT_ID,
stripMarkdown: boolean = true,
logger: Console = console,
): Promise<SendMessageResult> {
const account = getAccountConfig(cfg, accountId);
if (!account) {
const availableIds = Object.keys(cfg.channels?.twitch?.accounts ?? {});
return {
ok: false,
messageId: generateMessageId(),
error: `Account not found: ${accountId}. Available accounts: ${availableIds.join(", ") || "none"}`,
};
}
const tokenResolution = resolveTwitchToken(cfg, { accountId });
if (!isAccountConfigured(account, tokenResolution.token)) {
return {
ok: false,
messageId: generateMessageId(),
error:
`Account ${accountId} is not properly configured. ` +
"Required: username, clientId, and token (config or env for default account).",
};
}
const normalizedChannel = channel || account.channel;
if (!normalizedChannel) {
return {
ok: false,
messageId: generateMessageId(),
error: "No channel specified and no default channel in account config",
};
}
const cleanedText = stripMarkdown ? stripMarkdownForTwitch(text) : text;
if (!cleanedText) {
return {
ok: true,
messageId: "skipped",
};
}
const clientManager = getRegistryClientManager(accountId);
if (!clientManager) {
return {
ok: false,
messageId: generateMessageId(),
error: `Client manager not found for account: ${accountId}. Please start the Twitch gateway first.`,
};
}
try {
const result = await clientManager.sendMessage(
account,
normalizeTwitchChannel(normalizedChannel),
cleanedText,
cfg,
accountId,
);
if (!result.ok) {
return {
ok: false,
messageId: result.messageId ?? generateMessageId(),
error: result.error ?? "Send failed",
};
}
return {
ok: true,
messageId: result.messageId ?? generateMessageId(),
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
logger.error(`Failed to send message: ${errorMsg}`);
return {
ok: false,
messageId: generateMessageId(),
error: errorMsg,
};
}
}