remove comments + fromat

This commit is contained in:
jaydenfyi 2026-01-25 15:46:25 +08:00
parent d8ca472e52
commit 75845606c4
11 changed files with 11 additions and 81 deletions

View File

@ -1,7 +1,8 @@
{
"permissions": {
"allow": [
"Bash(pnpm vitest:*)"
"Bash(pnpm vitest:*)",
"Bash(git diff:*)"
]
}
}

View File

@ -30,11 +30,11 @@ Minimal config:
username: "clawdbot",
token: "oauth:abc123...",
clientId: "your_client_id_here",
channel: "vevisk"
}
}
}
}
channel: "vevisk",
},
},
},
},
}
```

View File

@ -38,7 +38,6 @@ export function checkTwitchAccessControl(params: {
}): TwitchAccessControlResult {
const { message, account, botUsername } = params;
// Check mention requirement first
if (account.requireMention) {
const mentions = extractMentions(message.message);
if (!mentions.includes(botUsername.toLowerCase())) {
@ -49,7 +48,6 @@ export function checkTwitchAccessControl(params: {
}
}
// Check allowlist (by user ID)
if (account.allowFrom && account.allowFrom.length > 0) {
const allowFrom = account.allowFrom;
const senderId = message.userId;
@ -61,7 +59,6 @@ export function checkTwitchAccessControl(params: {
};
}
// Check if sender is in allowlist
if (!allowFrom.includes(senderId)) {
return {
allowed: false,
@ -69,7 +66,6 @@ export function checkTwitchAccessControl(params: {
};
}
// Sender is in allowlist, no need to check role restrictions
return {
allowed: true,
matchKey: senderId,
@ -77,7 +73,6 @@ export function checkTwitchAccessControl(params: {
};
}
// Check role-based restrictions
if (account.allowedRoles && account.allowedRoles.length > 0) {
const allowedRoles = account.allowedRoles;
@ -90,7 +85,6 @@ export function checkTwitchAccessControl(params: {
};
}
// Check if sender has any of the allowed roles
const hasAllowedRole = checkSenderRoles({
message,
allowedRoles,
@ -110,7 +104,6 @@ export function checkTwitchAccessControl(params: {
};
}
// No restrictions configured - allow everyone
return {
allowed: true,
};

View File

@ -45,7 +45,6 @@ async function processTwitchMessage(params: {
}): Promise<void> {
const { message, account, accountId, config, runtime, core, statusSink } = params;
// Resolve route for this message
const route = core.channel.routing.resolveAgentRoute({
cfg: config as Parameters<typeof core.channel.routing.resolveAgentRoute>[0]["cfg"],
channel: "twitch",
@ -56,7 +55,6 @@ async function processTwitchMessage(params: {
},
});
// Build message body
const rawBody = message.message;
const body = core.channel.reply.formatAgentEnvelope({
channel: "Twitch",
@ -68,7 +66,6 @@ async function processTwitchMessage(params: {
body: rawBody,
});
// Build context payload
const ctxPayload = core.channel.reply.finalizeInboundContext({
Body: body,
RawBody: rawBody,
@ -89,7 +86,6 @@ async function processTwitchMessage(params: {
OriginatingTo: `twitch:channel:${message.channel}`,
});
// Record session
const storePath = core.channel.session.resolveStorePath(
(config as Parameters<typeof core.channel.session.resolveStorePath>[0]["cfg"])?.session?.store,
{ agentId: route.agentId },
@ -103,14 +99,12 @@ async function processTwitchMessage(params: {
},
});
// Resolve markdown table mode
const tableMode = core.channel.text.resolveMarkdownTableMode({
cfg: config as Parameters<typeof core.channel.text.resolveMarkdownTableMode>[0]["cfg"],
channel: "twitch",
accountId,
});
// Dispatch reply
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
ctx: ctxPayload,
cfg: config as Parameters<
@ -191,7 +185,6 @@ export async function monitorTwitchProvider(
const core = getTwitchRuntime();
let stopped = false;
// Create logger for client manager
const logger = {
info: (msg: string) => runtime.log?.(`[twitch] ${msg}`),
warn: (msg: string) => runtime.log?.(`[twitch] ${msg}`),
@ -199,10 +192,8 @@ export async function monitorTwitchProvider(
debug: (msg: string) => runtime.log?.(`[twitch] ${msg}`),
};
// Get or create client manager
const clientManager = getOrCreateClientManager(accountId, logger);
// Establish connection
try {
await clientManager.getClient(
account,
@ -216,7 +207,6 @@ export async function monitorTwitchProvider(
throw error;
}
// Register message handler
const unregisterHandler = clientManager.onMessage(account, async (message) => {
if (stopped) return;
@ -256,13 +246,11 @@ export async function monitorTwitchProvider(
}
});
// Stop function
const stop = () => {
stopped = true;
unregisterHandler();
};
// Handle abort signal
abortSignal.addEventListener("abort", stop, { once: true });
return { stop };

View File

@ -108,12 +108,10 @@ export const twitchOutbound: ChannelOutboundAdapter = {
sendText: async (params: ChannelOutboundContext): Promise<OutboundDeliveryResult> => {
const { cfg, to, text, accountId, signal } = params;
// Check for abort signal
if (signal?.aborted) {
throw new Error("Outbound delivery aborted");
}
// Resolve account
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
const account = getAccountConfig(cfg, resolvedAccountId);
if (!account) {
@ -124,19 +122,16 @@ export const twitchOutbound: ChannelOutboundAdapter = {
);
}
// Get channel (support target parameter override)
const channel = to || account.channel;
if (!channel) {
throw new Error("No channel specified and no default channel in account config");
}
// Get plugin config for markdown stripping
const pluginCfg = parsePluginConfig(
// biome-ignore lint/suspicious/noExplicitAny: pluginConfig is not part of CoreConfig
(cfg as any).pluginConfig ?? {},
);
// Send message
const result = await sendMessageTwitchInternal(
normalizeTwitchChannel(channel),
text,
@ -179,15 +174,12 @@ export const twitchOutbound: ChannelOutboundAdapter = {
sendMedia: async (params: ChannelOutboundContext): Promise<OutboundDeliveryResult> => {
const { text, mediaUrl, signal } = params;
// Check for abort signal
if (signal?.aborted) {
throw new Error("Outbound delivery aborted");
}
// Combine text and media URL
const message = mediaUrl ? `${text || ""} ${mediaUrl}`.trim() : text;
// Delegate to sendText
if (!twitchOutbound.sendText) {
throw new Error("sendText not implemented");
}

View File

@ -41,10 +41,8 @@ export async function probeTwitch(
let client: ChatClient | undefined;
try {
// Create auth provider with the token
const authProvider = new StaticAuthProvider(account.clientId ?? "", rawToken);
// Create chat client
client = new ChatClient({
authProvider,
});
@ -59,7 +57,6 @@ export async function probeTwitch(
const cleanup = () => {
if (settled) return;
settled = true;
// Remove all listeners
connectListener?.unbind();
disconnectListener?.unbind();
authFailListener?.unbind();
@ -84,20 +81,16 @@ export async function probeTwitch(
});
});
// Create timeout promise
const timeout = new Promise<never>((_, 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 {
ok: true,
connected: true,
@ -114,7 +107,6 @@ export async function probeTwitch(
elapsedMs: Date.now() - started,
};
} finally {
// Always clean up the client
if (client) {
try {
client.quit();

View File

@ -51,7 +51,6 @@ export async function resolveTwitchTargets(
): Promise<ChannelResolveResult[]> {
const log = createLogger(logger);
// Validate credentials
if (!account.clientId || !account.token) {
log.error("Missing Twitch client ID or token");
return inputs.map((input) => ({
@ -61,20 +60,16 @@ export async function resolveTwitchTargets(
}));
}
// Normalize token - strip oauth: prefix if present
const normalizedToken = normalizeToken(account.token);
// Create auth provider and API client
const authProvider = new StaticAuthProvider(account.clientId, normalizedToken);
const apiClient = new ApiClient({ authProvider });
const results: ChannelResolveResult[] = [];
// Process each input
for (const input of inputs) {
const normalized = normalizeUsername(input);
// Skip empty inputs
if (!normalized) {
results.push({
input,
@ -84,12 +79,10 @@ export async function resolveTwitchTargets(
continue;
}
// If it looks like a user ID (numeric), validate it exists
const looksLikeUserId = /^\d+$/.test(normalized);
try {
if (looksLikeUserId) {
// Validate user ID by fetching the user
const user = await apiClient.users.getUserById(normalized);
if (user) {
@ -109,7 +102,6 @@ export async function resolveTwitchTargets(
log.warn(`User ID ${normalized} not found`);
}
} else {
// Resolve username to user ID
const user = await apiClient.users.getUserByName(normalized);
if (user) {

View File

@ -56,7 +56,6 @@ export async function sendMessageTwitchInternal(
stripMarkdown: boolean = true,
logger: Console = console,
): Promise<SendMessageResult> {
// Resolve account
const account = getAccountConfig(cfg, accountId);
if (!account) {
const availableIds = Object.keys(cfg.channels?.twitch?.accounts ?? {});
@ -78,7 +77,6 @@ export async function sendMessageTwitchInternal(
};
}
// Normalize channel (parameter override, then account channel)
const normalizedChannel = channel || account.channel;
if (!normalizedChannel) {
return {
@ -88,7 +86,6 @@ export async function sendMessageTwitchInternal(
};
}
// Strip markdown if enabled
const cleanedText = stripMarkdown ? stripMarkdownForTwitch(text) : text;
if (!cleanedText) {
return {
@ -97,7 +94,6 @@ export async function sendMessageTwitchInternal(
};
}
// Get client manager from registry
const clientManager = getRegistryClientManager(accountId);
if (!clientManager) {
return {

View File

@ -35,10 +35,8 @@ export function collectTwitchStatusIssues(
for (const entry of accounts) {
const accountId = entry.accountId;
// Skip if not a Twitch account
if (!accountId) continue;
// Get full account config if available
let account: ReturnType<typeof getAccountConfig> | null = null;
let cfg: Parameters<typeof resolveTwitchToken>[0] | undefined;
if (getCfg) {
@ -52,7 +50,6 @@ export function collectTwitchStatusIssues(
}
}
// Check 1: Account not configured
if (!entry.configured) {
issues.push({
channel: "twitch",
@ -61,10 +58,9 @@ export function collectTwitchStatusIssues(
message: "Twitch account is not properly configured",
fix: "Add required fields: username, token, and clientId to your account configuration",
});
continue; // Skip further checks if not configured
continue;
}
// Check 2: Account disabled
if (entry.enabled === false) {
issues.push({
channel: "twitch",
@ -73,10 +69,9 @@ export function collectTwitchStatusIssues(
message: "Twitch account is disabled",
fix: "Set enabled: true in your account configuration to enable this account",
});
continue; // Skip further checks if disabled
continue;
}
// Check 3: Missing clientId (check if username/token present but no clientId)
if (account && account.username && account.token && !account.clientId) {
issues.push({
channel: "twitch",
@ -87,12 +82,10 @@ export function collectTwitchStatusIssues(
});
}
// Checks that require account config
const tokenResolution = cfg
? resolveTwitchToken(cfg as Parameters<typeof resolveTwitchToken>[0], { accountId })
: { token: "", source: "none" };
if (account && isAccountConfigured(account, tokenResolution.token)) {
// Check 4: Token format warning (normalized, but may indicate config issue)
if (account.token?.startsWith("oauth:")) {
issues.push({
channel: "twitch",
@ -103,7 +96,6 @@ export function collectTwitchStatusIssues(
});
}
// Check 5: clientSecret provided without refreshToken
if (account.clientSecret && !account.refreshToken) {
issues.push({
channel: "twitch",
@ -114,7 +106,6 @@ export function collectTwitchStatusIssues(
});
}
// Check 6: Access control warnings
if (account.allowFrom && account.allowFrom.length === 0) {
issues.push({
channel: "twitch",
@ -125,7 +116,6 @@ export function collectTwitchStatusIssues(
});
}
// Check 7: Invalid role combinations
if (
account.allowedRoles?.includes("all") &&
account.allowFrom &&
@ -141,7 +131,6 @@ export function collectTwitchStatusIssues(
}
}
// Check 8: Runtime errors
if (entry.lastError) {
issues.push({
channel: "twitch",
@ -152,7 +141,6 @@ export function collectTwitchStatusIssues(
});
}
// Check 9: Account never connected successfully
if (
entry.configured &&
!entry.running &&
@ -169,7 +157,6 @@ export function collectTwitchStatusIssues(
});
}
// Check 10: Long-running connection may need reconnection
if (entry.running && entry.lastStartAt) {
const uptime = Date.now() - entry.lastStartAt;
const daysSinceStart = uptime / (1000 * 60 * 60 * 24);

View File

@ -21,7 +21,8 @@ const mockQuit = vi.fn();
const mockUnbind = vi.fn();
// Event handler storage for testing
const messageHandlers: Array<(channel: string, user: string, message: string, msg: any) => void> = [];
const messageHandlers: Array<(channel: string, user: string, message: string, msg: any) => void> =
[];
const whisperHandlers: Array<(user: string, message: string, msg: any) => void> = [];
// Mock functions that track handlers and return unbind objects

View File

@ -26,14 +26,11 @@ export class TwitchClientManager {
}
if (account.clientSecret) {
// Use RefreshingAuthProvider - can handle tokens with or without refresh tokens
const authProvider = new RefreshingAuthProvider({
clientId: account.clientId,
clientSecret: account.clientSecret,
});
// Use addUserForToken to figure out the user ID from the token
// This works whether we have a refresh token or not
await authProvider
.addUserForToken({
accessToken: normalizedToken,
@ -52,14 +49,12 @@ export class TwitchClientManager {
);
});
// Set up token refresh event listener (only fires if refreshToken is provided)
authProvider.onRefresh((userId, token) => {
this.logger.info(
`[twitch] Access token refreshed for user ${userId} (expires in ${token.expiresIn ? `${token.expiresIn}s` : "unknown"})`,
);
});
// Set up token refresh failure listener
authProvider.onRefreshFailure((userId, error) => {
this.logger.error(
`[twitch] Failed to refresh access token for user ${userId}: ${error.message}`,
@ -76,7 +71,6 @@ export class TwitchClientManager {
return authProvider;
}
// Fall back to StaticAuthProvider for backward compatibility (no clientSecret)
this.logger.info(
`[twitch] Using StaticAuthProvider for ${account.username} (no clientSecret provided)`,
);
@ -98,7 +92,6 @@ export class TwitchClientManager {
return existing;
}
// Resolve token from config or environment
const tokenResolution = resolveTwitchToken(cfg, {
accountId,
});
@ -119,13 +112,10 @@ export class TwitchClientManager {
throw new Error("Missing Twitch client ID");
}
// Normalize token - strip oauth: prefix if present (Twurple doesn't need it)
const normalizedToken = normalizeToken(tokenResolution.token);
// Create auth provider
const authProvider = await this.createAuthProvider(account, normalizedToken);
// Create chat client
const client = new ChatClient({
authProvider,
channels: [account.channel],
@ -160,10 +150,8 @@ export class TwitchClientManager {
},
});
// Set up event handlers
this.setupClientHandlers(client, account);
// Connect
client.connect();
this.clients.set(key, client);