Files
clawdbot/extensions/twitch/src/resolver.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

138 lines
3.9 KiB
TypeScript

/**
* Twitch resolver adapter for channel/user name resolution.
*
* This module implements the ChannelResolverAdapter interface to resolve
* Twitch usernames to user IDs via the Twitch Helix API.
*/
import { ApiClient } from "@twurple/api";
import { StaticAuthProvider } from "@twurple/auth";
import type { ChannelResolveKind, ChannelResolveResult } from "./types.js";
import type { ChannelLogSink, TwitchAccountConfig } from "./types.js";
import { normalizeToken } from "./utils/twitch.js";
/**
* Normalize a Twitch username - strip @ prefix and convert to lowercase
*/
function normalizeUsername(input: string): string {
const trimmed = input.trim();
if (trimmed.startsWith("@")) {
return trimmed.slice(1).toLowerCase();
}
return trimmed.toLowerCase();
}
/**
* Create a logger that includes the Twitch prefix
*/
function createLogger(logger?: ChannelLogSink): ChannelLogSink {
return {
info: (msg: string) => logger?.info(msg),
warn: (msg: string) => logger?.warn(msg),
error: (msg: string) => logger?.error(msg),
debug: (msg: string) => logger?.debug?.(msg) ?? (() => {}),
};
}
/**
* Resolve Twitch usernames to user IDs via the Helix API
*
* @param inputs - Array of usernames or user IDs to resolve
* @param account - Twitch account configuration with auth credentials
* @param kind - Type of target to resolve ("user" or "group")
* @param logger - Optional logger
* @returns Promise resolving to array of ChannelResolveResult
*/
export async function resolveTwitchTargets(
inputs: string[],
account: TwitchAccountConfig,
kind: ChannelResolveKind,
logger?: ChannelLogSink,
): Promise<ChannelResolveResult[]> {
const log = createLogger(logger);
if (!account.clientId || !account.token) {
log.error("Missing Twitch client ID or token");
return inputs.map((input) => ({
input,
resolved: false,
note: "missing Twitch credentials",
}));
}
const normalizedToken = normalizeToken(account.token);
const authProvider = new StaticAuthProvider(account.clientId, normalizedToken);
const apiClient = new ApiClient({ authProvider });
const results: ChannelResolveResult[] = [];
for (const input of inputs) {
const normalized = normalizeUsername(input);
if (!normalized) {
results.push({
input,
resolved: false,
note: "empty input",
});
continue;
}
const looksLikeUserId = /^\d+$/.test(normalized);
try {
if (looksLikeUserId) {
const user = await apiClient.users.getUserById(normalized);
if (user) {
results.push({
input,
resolved: true,
id: user.id,
name: user.name,
});
log.debug?.(`Resolved user ID ${normalized} -> ${user.name}`);
} else {
results.push({
input,
resolved: false,
note: "user ID not found",
});
log.warn(`User ID ${normalized} not found`);
}
} else {
const user = await apiClient.users.getUserByName(normalized);
if (user) {
results.push({
input,
resolved: true,
id: user.id,
name: user.name,
note: user.displayName !== user.name ? `display: ${user.displayName}` : undefined,
});
log.debug?.(`Resolved username ${normalized} -> ${user.id} (${user.name})`);
} else {
results.push({
input,
resolved: false,
note: "username not found",
});
log.warn(`Username ${normalized} not found`);
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
results.push({
input,
resolved: false,
note: `API error: ${errorMessage}`,
});
log.error(`Failed to resolve ${input}: ${errorMessage}`);
}
}
return results;
}