diff --git a/extensions/twitch/src/actions.ts b/extensions/twitch/src/actions.ts index 727370b66..2a9756cd4 100644 --- a/extensions/twitch/src/actions.ts +++ b/extensions/twitch/src/actions.ts @@ -59,45 +59,23 @@ function readStringParam( return options.trim !== false ? str.trim() : str; } +/** Supported Twitch actions */ +const TWITCH_ACTIONS = new Set(["send" as const]); +type TwitchAction = typeof TWITCH_ACTIONS extends Set ? U : never; + /** * Twitch message actions adapter. - * - * Supports the "send" action for sending messages to Twitch channels. - * - * NOTE: Currently only the "send" action is supported. The action gate pattern - * (configurable actions per channel) will be added in a future update. For now, - * the available actions are hardcoded as Twitch chat only supports sending messages. */ export const twitchMessageActions: ChannelMessageActionAdapter = { /** * List available actions for this channel. - * - * Currently supports: - * - "send" - Send a message to a Twitch channel - * - * Future actions may include: - * - "ban"/"timeout" - Moderation actions - * - "announce" - Send announcements - * - * @param params - Parameters including config - * @returns Array of available action names */ - listActions: () => { - // Action gate pattern: In future, this will check channel config to determine - // which actions are enabled. For now, all Twitch channels support "send". - const actions = new Set(["send"]); - return Array.from(actions); - }, + listActions: () => [...TWITCH_ACTIONS], /** * Check if an action is supported. - * - * @param params - Action to check - * @returns true if the action is supported */ - supportsAction: ({ action }) => { - return action === "send"; - }, + supportsAction: ({ action }) => TWITCH_ACTIONS.has(action as TwitchAction), /** * Extract tool send parameters from action arguments. diff --git a/extensions/twitch/src/config-schema.ts b/extensions/twitch/src/config-schema.ts index 0f3323d1d..26c55b095 100644 --- a/extensions/twitch/src/config-schema.ts +++ b/extensions/twitch/src/config-schema.ts @@ -22,7 +22,7 @@ const TwitchAccountSchema = z.object({ enabled: z.boolean().optional(), /** Allowlist of Twitch user IDs who can interact with the bot (use IDs for safety, not usernames) */ allowFrom: z.array(z.string()).optional(), - /** Roles allowed to interact with the bot (e.g., ["mod", "vip", "sub"]) */ + /** Roles allowed to interact with the bot (e.g., ["moderator", "vip", "subscriber"]) */ allowedRoles: z.array(TwitchRoleSchema).optional(), /** Require @mention to trigger bot responses */ requireMention: z.boolean().optional(), diff --git a/extensions/twitch/src/probe.ts b/extensions/twitch/src/probe.ts index a2c1edcf0..cb7e3153e 100644 --- a/extensions/twitch/src/probe.ts +++ b/extensions/twitch/src/probe.ts @@ -40,14 +40,6 @@ export async function probeTwitch( let client: ChatClient | undefined; try { - // Create a timeout promise - const timeout = new Promise((_, reject) => { - setTimeout( - () => reject(new Error(`connection timeout after ${timeoutMs}ms`)), - timeoutMs, - ); - }); - // Create auth provider with the token const authProvider = new StaticAuthProvider( account.clientId ?? "", @@ -59,11 +51,53 @@ export async function probeTwitch( authProvider, }); - // Race between connection and timeout - await Promise.race([client.connect(), timeout]); + // Create a promise that resolves when connected + const connectionPromise = new Promise((resolve, reject) => { + let settled = false; + let connectListener: ReturnType | undefined; + let disconnectListener: ReturnType | undefined; + let authFailListener: ReturnType | undefined; - // Wait a moment for the connection to fully establish - await new Promise((resolve) => setTimeout(resolve, 500)); + const cleanup = () => { + if (settled) return; + settled = true; + // Remove all listeners + connectListener?.unbind(); + disconnectListener?.unbind(); + authFailListener?.unbind(); + }; + + // Success: connection established + connectListener = client?.onConnect(() => { + cleanup(); + resolve(); + }); + + // Failure: disconnected (e.g., auth failed) + disconnectListener = client?.onDisconnect((_manually, reason) => { + cleanup(); + reject(reason || new Error("Disconnected")); + }); + + // Failure: authentication failed + authFailListener = client?.onAuthenticationFailure(() => { + cleanup(); + reject(new Error("Authentication failed")); + }); + }); + + // Create timeout promise + const timeout = new Promise((_, reject) => { + setTimeout(() => reject(new Error(`timeout after ${timeoutMs}ms`)), timeoutMs); + }); + + // Set up listeners BEFORE connecting, then race against timeout + client.connect(); + await Promise.race([connectionPromise, timeout]); + + // Clean up connection before returning + client.quit(); + client = undefined; // If we got here, connection was successful return {