Files
openclaw/extensions/twitch/src/probe.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

119 lines
3.1 KiB
TypeScript

import { StaticAuthProvider } from "@twurple/auth";
import { ChatClient } from "@twurple/chat";
import type { TwitchAccountConfig } from "./types.js";
import { normalizeToken } from "./utils/twitch.js";
/**
* Result of probing a Twitch account
*/
export type ProbeTwitchResult = {
ok: boolean;
error?: string;
username?: string;
elapsedMs: number;
connected?: boolean;
channel?: string;
};
/**
* Probe a Twitch account to verify the connection is working
*
* This tests the Twitch OAuth token by attempting to connect
* to the chat server and verify the bot's username.
*/
export async function probeTwitch(
account: TwitchAccountConfig,
timeoutMs: number,
): Promise<ProbeTwitchResult> {
const started = Date.now();
if (!account.token || !account.username) {
return {
ok: false,
error: "missing credentials (token, username)",
username: account.username,
elapsedMs: Date.now() - started,
};
}
const rawToken = normalizeToken(account.token.trim());
let client: ChatClient | undefined;
try {
const authProvider = new StaticAuthProvider(account.clientId ?? "", rawToken);
client = new ChatClient({
authProvider,
});
// Create a promise that resolves when connected
const connectionPromise = new Promise<void>((resolve, reject) => {
let settled = false;
let connectListener: ReturnType<ChatClient["onConnect"]> | undefined;
let disconnectListener: ReturnType<ChatClient["onDisconnect"]> | undefined;
let authFailListener: ReturnType<ChatClient["onAuthenticationFailure"]> | undefined;
const cleanup = () => {
if (settled) return;
settled = true;
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"));
});
});
const timeout = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(`timeout after ${timeoutMs}ms`)), timeoutMs);
});
client.connect();
await Promise.race([connectionPromise, timeout]);
client.quit();
client = undefined;
return {
ok: true,
connected: true,
username: account.username,
channel: account.channel,
elapsedMs: Date.now() - started,
};
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : String(error),
username: account.username,
channel: account.channel,
elapsedMs: Date.now() - started,
};
} finally {
if (client) {
try {
client.quit();
} catch {
// Ignore cleanup errors
}
}
}
}