remove comments + fromat
This commit is contained in:
parent
d8ca472e52
commit
75845606c4
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(pnpm vitest:*)"
|
"Bash(pnpm vitest:*)",
|
||||||
|
"Bash(git diff:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,11 +30,11 @@ Minimal config:
|
|||||||
username: "clawdbot",
|
username: "clawdbot",
|
||||||
token: "oauth:abc123...",
|
token: "oauth:abc123...",
|
||||||
clientId: "your_client_id_here",
|
clientId: "your_client_id_here",
|
||||||
channel: "vevisk"
|
channel: "vevisk",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,6 @@ export function checkTwitchAccessControl(params: {
|
|||||||
}): TwitchAccessControlResult {
|
}): TwitchAccessControlResult {
|
||||||
const { message, account, botUsername } = params;
|
const { message, account, botUsername } = params;
|
||||||
|
|
||||||
// Check mention requirement first
|
|
||||||
if (account.requireMention) {
|
if (account.requireMention) {
|
||||||
const mentions = extractMentions(message.message);
|
const mentions = extractMentions(message.message);
|
||||||
if (!mentions.includes(botUsername.toLowerCase())) {
|
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) {
|
if (account.allowFrom && account.allowFrom.length > 0) {
|
||||||
const allowFrom = account.allowFrom;
|
const allowFrom = account.allowFrom;
|
||||||
const senderId = message.userId;
|
const senderId = message.userId;
|
||||||
@ -61,7 +59,6 @@ export function checkTwitchAccessControl(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if sender is in allowlist
|
|
||||||
if (!allowFrom.includes(senderId)) {
|
if (!allowFrom.includes(senderId)) {
|
||||||
return {
|
return {
|
||||||
allowed: false,
|
allowed: false,
|
||||||
@ -69,7 +66,6 @@ export function checkTwitchAccessControl(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sender is in allowlist, no need to check role restrictions
|
|
||||||
return {
|
return {
|
||||||
allowed: true,
|
allowed: true,
|
||||||
matchKey: senderId,
|
matchKey: senderId,
|
||||||
@ -77,7 +73,6 @@ export function checkTwitchAccessControl(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check role-based restrictions
|
|
||||||
if (account.allowedRoles && account.allowedRoles.length > 0) {
|
if (account.allowedRoles && account.allowedRoles.length > 0) {
|
||||||
const allowedRoles = account.allowedRoles;
|
const allowedRoles = account.allowedRoles;
|
||||||
|
|
||||||
@ -90,7 +85,6 @@ export function checkTwitchAccessControl(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if sender has any of the allowed roles
|
|
||||||
const hasAllowedRole = checkSenderRoles({
|
const hasAllowedRole = checkSenderRoles({
|
||||||
message,
|
message,
|
||||||
allowedRoles,
|
allowedRoles,
|
||||||
@ -110,7 +104,6 @@ export function checkTwitchAccessControl(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// No restrictions configured - allow everyone
|
|
||||||
return {
|
return {
|
||||||
allowed: true,
|
allowed: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -45,7 +45,6 @@ async function processTwitchMessage(params: {
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { message, account, accountId, config, runtime, core, statusSink } = params;
|
const { message, account, accountId, config, runtime, core, statusSink } = params;
|
||||||
|
|
||||||
// Resolve route for this message
|
|
||||||
const route = core.channel.routing.resolveAgentRoute({
|
const route = core.channel.routing.resolveAgentRoute({
|
||||||
cfg: config as Parameters<typeof core.channel.routing.resolveAgentRoute>[0]["cfg"],
|
cfg: config as Parameters<typeof core.channel.routing.resolveAgentRoute>[0]["cfg"],
|
||||||
channel: "twitch",
|
channel: "twitch",
|
||||||
@ -56,7 +55,6 @@ async function processTwitchMessage(params: {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build message body
|
|
||||||
const rawBody = message.message;
|
const rawBody = message.message;
|
||||||
const body = core.channel.reply.formatAgentEnvelope({
|
const body = core.channel.reply.formatAgentEnvelope({
|
||||||
channel: "Twitch",
|
channel: "Twitch",
|
||||||
@ -68,7 +66,6 @@ async function processTwitchMessage(params: {
|
|||||||
body: rawBody,
|
body: rawBody,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build context payload
|
|
||||||
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
||||||
Body: body,
|
Body: body,
|
||||||
RawBody: rawBody,
|
RawBody: rawBody,
|
||||||
@ -89,7 +86,6 @@ async function processTwitchMessage(params: {
|
|||||||
OriginatingTo: `twitch:channel:${message.channel}`,
|
OriginatingTo: `twitch:channel:${message.channel}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Record session
|
|
||||||
const storePath = core.channel.session.resolveStorePath(
|
const storePath = core.channel.session.resolveStorePath(
|
||||||
(config as Parameters<typeof core.channel.session.resolveStorePath>[0]["cfg"])?.session?.store,
|
(config as Parameters<typeof core.channel.session.resolveStorePath>[0]["cfg"])?.session?.store,
|
||||||
{ agentId: route.agentId },
|
{ agentId: route.agentId },
|
||||||
@ -103,14 +99,12 @@ async function processTwitchMessage(params: {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Resolve markdown table mode
|
|
||||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||||
cfg: config as Parameters<typeof core.channel.text.resolveMarkdownTableMode>[0]["cfg"],
|
cfg: config as Parameters<typeof core.channel.text.resolveMarkdownTableMode>[0]["cfg"],
|
||||||
channel: "twitch",
|
channel: "twitch",
|
||||||
accountId,
|
accountId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dispatch reply
|
|
||||||
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
||||||
ctx: ctxPayload,
|
ctx: ctxPayload,
|
||||||
cfg: config as Parameters<
|
cfg: config as Parameters<
|
||||||
@ -191,7 +185,6 @@ export async function monitorTwitchProvider(
|
|||||||
const core = getTwitchRuntime();
|
const core = getTwitchRuntime();
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
|
|
||||||
// Create logger for client manager
|
|
||||||
const logger = {
|
const logger = {
|
||||||
info: (msg: string) => runtime.log?.(`[twitch] ${msg}`),
|
info: (msg: string) => runtime.log?.(`[twitch] ${msg}`),
|
||||||
warn: (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}`),
|
debug: (msg: string) => runtime.log?.(`[twitch] ${msg}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get or create client manager
|
|
||||||
const clientManager = getOrCreateClientManager(accountId, logger);
|
const clientManager = getOrCreateClientManager(accountId, logger);
|
||||||
|
|
||||||
// Establish connection
|
|
||||||
try {
|
try {
|
||||||
await clientManager.getClient(
|
await clientManager.getClient(
|
||||||
account,
|
account,
|
||||||
@ -216,7 +207,6 @@ export async function monitorTwitchProvider(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register message handler
|
|
||||||
const unregisterHandler = clientManager.onMessage(account, async (message) => {
|
const unregisterHandler = clientManager.onMessage(account, async (message) => {
|
||||||
if (stopped) return;
|
if (stopped) return;
|
||||||
|
|
||||||
@ -256,13 +246,11 @@ export async function monitorTwitchProvider(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stop function
|
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
stopped = true;
|
stopped = true;
|
||||||
unregisterHandler();
|
unregisterHandler();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle abort signal
|
|
||||||
abortSignal.addEventListener("abort", stop, { once: true });
|
abortSignal.addEventListener("abort", stop, { once: true });
|
||||||
|
|
||||||
return { stop };
|
return { stop };
|
||||||
|
|||||||
@ -108,12 +108,10 @@ export const twitchOutbound: ChannelOutboundAdapter = {
|
|||||||
sendText: async (params: ChannelOutboundContext): Promise<OutboundDeliveryResult> => {
|
sendText: async (params: ChannelOutboundContext): Promise<OutboundDeliveryResult> => {
|
||||||
const { cfg, to, text, accountId, signal } = params;
|
const { cfg, to, text, accountId, signal } = params;
|
||||||
|
|
||||||
// Check for abort signal
|
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
throw new Error("Outbound delivery aborted");
|
throw new Error("Outbound delivery aborted");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve account
|
|
||||||
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
||||||
const account = getAccountConfig(cfg, resolvedAccountId);
|
const account = getAccountConfig(cfg, resolvedAccountId);
|
||||||
if (!account) {
|
if (!account) {
|
||||||
@ -124,19 +122,16 @@ export const twitchOutbound: ChannelOutboundAdapter = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get channel (support target parameter override)
|
|
||||||
const channel = to || account.channel;
|
const channel = to || account.channel;
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
throw new Error("No channel specified and no default channel in account config");
|
throw new Error("No channel specified and no default channel in account config");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get plugin config for markdown stripping
|
|
||||||
const pluginCfg = parsePluginConfig(
|
const pluginCfg = parsePluginConfig(
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: pluginConfig is not part of CoreConfig
|
// biome-ignore lint/suspicious/noExplicitAny: pluginConfig is not part of CoreConfig
|
||||||
(cfg as any).pluginConfig ?? {},
|
(cfg as any).pluginConfig ?? {},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Send message
|
|
||||||
const result = await sendMessageTwitchInternal(
|
const result = await sendMessageTwitchInternal(
|
||||||
normalizeTwitchChannel(channel),
|
normalizeTwitchChannel(channel),
|
||||||
text,
|
text,
|
||||||
@ -179,15 +174,12 @@ export const twitchOutbound: ChannelOutboundAdapter = {
|
|||||||
sendMedia: async (params: ChannelOutboundContext): Promise<OutboundDeliveryResult> => {
|
sendMedia: async (params: ChannelOutboundContext): Promise<OutboundDeliveryResult> => {
|
||||||
const { text, mediaUrl, signal } = params;
|
const { text, mediaUrl, signal } = params;
|
||||||
|
|
||||||
// Check for abort signal
|
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
throw new Error("Outbound delivery aborted");
|
throw new Error("Outbound delivery aborted");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine text and media URL
|
|
||||||
const message = mediaUrl ? `${text || ""} ${mediaUrl}`.trim() : text;
|
const message = mediaUrl ? `${text || ""} ${mediaUrl}`.trim() : text;
|
||||||
|
|
||||||
// Delegate to sendText
|
|
||||||
if (!twitchOutbound.sendText) {
|
if (!twitchOutbound.sendText) {
|
||||||
throw new Error("sendText not implemented");
|
throw new Error("sendText not implemented");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,10 +41,8 @@ export async function probeTwitch(
|
|||||||
let client: ChatClient | undefined;
|
let client: ChatClient | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create auth provider with the token
|
|
||||||
const authProvider = new StaticAuthProvider(account.clientId ?? "", rawToken);
|
const authProvider = new StaticAuthProvider(account.clientId ?? "", rawToken);
|
||||||
|
|
||||||
// Create chat client
|
|
||||||
client = new ChatClient({
|
client = new ChatClient({
|
||||||
authProvider,
|
authProvider,
|
||||||
});
|
});
|
||||||
@ -59,7 +57,6 @@ export async function probeTwitch(
|
|||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
settled = true;
|
settled = true;
|
||||||
// Remove all listeners
|
|
||||||
connectListener?.unbind();
|
connectListener?.unbind();
|
||||||
disconnectListener?.unbind();
|
disconnectListener?.unbind();
|
||||||
authFailListener?.unbind();
|
authFailListener?.unbind();
|
||||||
@ -84,20 +81,16 @@ export async function probeTwitch(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create timeout promise
|
|
||||||
const timeout = new Promise<never>((_, reject) => {
|
const timeout = new Promise<never>((_, reject) => {
|
||||||
setTimeout(() => reject(new Error(`timeout after ${timeoutMs}ms`)), timeoutMs);
|
setTimeout(() => reject(new Error(`timeout after ${timeoutMs}ms`)), timeoutMs);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up listeners BEFORE connecting, then race against timeout
|
|
||||||
client.connect();
|
client.connect();
|
||||||
await Promise.race([connectionPromise, timeout]);
|
await Promise.race([connectionPromise, timeout]);
|
||||||
|
|
||||||
// Clean up connection before returning
|
|
||||||
client.quit();
|
client.quit();
|
||||||
client = undefined;
|
client = undefined;
|
||||||
|
|
||||||
// If we got here, connection was successful
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
connected: true,
|
connected: true,
|
||||||
@ -114,7 +107,6 @@ export async function probeTwitch(
|
|||||||
elapsedMs: Date.now() - started,
|
elapsedMs: Date.now() - started,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
// Always clean up the client
|
|
||||||
if (client) {
|
if (client) {
|
||||||
try {
|
try {
|
||||||
client.quit();
|
client.quit();
|
||||||
|
|||||||
@ -51,7 +51,6 @@ export async function resolveTwitchTargets(
|
|||||||
): Promise<ChannelResolveResult[]> {
|
): Promise<ChannelResolveResult[]> {
|
||||||
const log = createLogger(logger);
|
const log = createLogger(logger);
|
||||||
|
|
||||||
// Validate credentials
|
|
||||||
if (!account.clientId || !account.token) {
|
if (!account.clientId || !account.token) {
|
||||||
log.error("Missing Twitch client ID or token");
|
log.error("Missing Twitch client ID or token");
|
||||||
return inputs.map((input) => ({
|
return inputs.map((input) => ({
|
||||||
@ -61,20 +60,16 @@ export async function resolveTwitchTargets(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize token - strip oauth: prefix if present
|
|
||||||
const normalizedToken = normalizeToken(account.token);
|
const normalizedToken = normalizeToken(account.token);
|
||||||
|
|
||||||
// Create auth provider and API client
|
|
||||||
const authProvider = new StaticAuthProvider(account.clientId, normalizedToken);
|
const authProvider = new StaticAuthProvider(account.clientId, normalizedToken);
|
||||||
const apiClient = new ApiClient({ authProvider });
|
const apiClient = new ApiClient({ authProvider });
|
||||||
|
|
||||||
const results: ChannelResolveResult[] = [];
|
const results: ChannelResolveResult[] = [];
|
||||||
|
|
||||||
// Process each input
|
|
||||||
for (const input of inputs) {
|
for (const input of inputs) {
|
||||||
const normalized = normalizeUsername(input);
|
const normalized = normalizeUsername(input);
|
||||||
|
|
||||||
// Skip empty inputs
|
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
results.push({
|
results.push({
|
||||||
input,
|
input,
|
||||||
@ -84,12 +79,10 @@ export async function resolveTwitchTargets(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it looks like a user ID (numeric), validate it exists
|
|
||||||
const looksLikeUserId = /^\d+$/.test(normalized);
|
const looksLikeUserId = /^\d+$/.test(normalized);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (looksLikeUserId) {
|
if (looksLikeUserId) {
|
||||||
// Validate user ID by fetching the user
|
|
||||||
const user = await apiClient.users.getUserById(normalized);
|
const user = await apiClient.users.getUserById(normalized);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
@ -109,7 +102,6 @@ export async function resolveTwitchTargets(
|
|||||||
log.warn(`User ID ${normalized} not found`);
|
log.warn(`User ID ${normalized} not found`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Resolve username to user ID
|
|
||||||
const user = await apiClient.users.getUserByName(normalized);
|
const user = await apiClient.users.getUserByName(normalized);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
|||||||
@ -56,7 +56,6 @@ export async function sendMessageTwitchInternal(
|
|||||||
stripMarkdown: boolean = true,
|
stripMarkdown: boolean = true,
|
||||||
logger: Console = console,
|
logger: Console = console,
|
||||||
): Promise<SendMessageResult> {
|
): Promise<SendMessageResult> {
|
||||||
// Resolve account
|
|
||||||
const account = getAccountConfig(cfg, accountId);
|
const account = getAccountConfig(cfg, accountId);
|
||||||
if (!account) {
|
if (!account) {
|
||||||
const availableIds = Object.keys(cfg.channels?.twitch?.accounts ?? {});
|
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;
|
const normalizedChannel = channel || account.channel;
|
||||||
if (!normalizedChannel) {
|
if (!normalizedChannel) {
|
||||||
return {
|
return {
|
||||||
@ -88,7 +86,6 @@ export async function sendMessageTwitchInternal(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip markdown if enabled
|
|
||||||
const cleanedText = stripMarkdown ? stripMarkdownForTwitch(text) : text;
|
const cleanedText = stripMarkdown ? stripMarkdownForTwitch(text) : text;
|
||||||
if (!cleanedText) {
|
if (!cleanedText) {
|
||||||
return {
|
return {
|
||||||
@ -97,7 +94,6 @@ export async function sendMessageTwitchInternal(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get client manager from registry
|
|
||||||
const clientManager = getRegistryClientManager(accountId);
|
const clientManager = getRegistryClientManager(accountId);
|
||||||
if (!clientManager) {
|
if (!clientManager) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -35,10 +35,8 @@ export function collectTwitchStatusIssues(
|
|||||||
for (const entry of accounts) {
|
for (const entry of accounts) {
|
||||||
const accountId = entry.accountId;
|
const accountId = entry.accountId;
|
||||||
|
|
||||||
// Skip if not a Twitch account
|
|
||||||
if (!accountId) continue;
|
if (!accountId) continue;
|
||||||
|
|
||||||
// Get full account config if available
|
|
||||||
let account: ReturnType<typeof getAccountConfig> | null = null;
|
let account: ReturnType<typeof getAccountConfig> | null = null;
|
||||||
let cfg: Parameters<typeof resolveTwitchToken>[0] | undefined;
|
let cfg: Parameters<typeof resolveTwitchToken>[0] | undefined;
|
||||||
if (getCfg) {
|
if (getCfg) {
|
||||||
@ -52,7 +50,6 @@ export function collectTwitchStatusIssues(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check 1: Account not configured
|
|
||||||
if (!entry.configured) {
|
if (!entry.configured) {
|
||||||
issues.push({
|
issues.push({
|
||||||
channel: "twitch",
|
channel: "twitch",
|
||||||
@ -61,10 +58,9 @@ export function collectTwitchStatusIssues(
|
|||||||
message: "Twitch account is not properly configured",
|
message: "Twitch account is not properly configured",
|
||||||
fix: "Add required fields: username, token, and clientId to your account configuration",
|
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) {
|
if (entry.enabled === false) {
|
||||||
issues.push({
|
issues.push({
|
||||||
channel: "twitch",
|
channel: "twitch",
|
||||||
@ -73,10 +69,9 @@ export function collectTwitchStatusIssues(
|
|||||||
message: "Twitch account is disabled",
|
message: "Twitch account is disabled",
|
||||||
fix: "Set enabled: true in your account configuration to enable this account",
|
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) {
|
if (account && account.username && account.token && !account.clientId) {
|
||||||
issues.push({
|
issues.push({
|
||||||
channel: "twitch",
|
channel: "twitch",
|
||||||
@ -87,12 +82,10 @@ export function collectTwitchStatusIssues(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks that require account config
|
|
||||||
const tokenResolution = cfg
|
const tokenResolution = cfg
|
||||||
? resolveTwitchToken(cfg as Parameters<typeof resolveTwitchToken>[0], { accountId })
|
? resolveTwitchToken(cfg as Parameters<typeof resolveTwitchToken>[0], { accountId })
|
||||||
: { token: "", source: "none" };
|
: { token: "", source: "none" };
|
||||||
if (account && isAccountConfigured(account, tokenResolution.token)) {
|
if (account && isAccountConfigured(account, tokenResolution.token)) {
|
||||||
// Check 4: Token format warning (normalized, but may indicate config issue)
|
|
||||||
if (account.token?.startsWith("oauth:")) {
|
if (account.token?.startsWith("oauth:")) {
|
||||||
issues.push({
|
issues.push({
|
||||||
channel: "twitch",
|
channel: "twitch",
|
||||||
@ -103,7 +96,6 @@ export function collectTwitchStatusIssues(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check 5: clientSecret provided without refreshToken
|
|
||||||
if (account.clientSecret && !account.refreshToken) {
|
if (account.clientSecret && !account.refreshToken) {
|
||||||
issues.push({
|
issues.push({
|
||||||
channel: "twitch",
|
channel: "twitch",
|
||||||
@ -114,7 +106,6 @@ export function collectTwitchStatusIssues(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check 6: Access control warnings
|
|
||||||
if (account.allowFrom && account.allowFrom.length === 0) {
|
if (account.allowFrom && account.allowFrom.length === 0) {
|
||||||
issues.push({
|
issues.push({
|
||||||
channel: "twitch",
|
channel: "twitch",
|
||||||
@ -125,7 +116,6 @@ export function collectTwitchStatusIssues(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check 7: Invalid role combinations
|
|
||||||
if (
|
if (
|
||||||
account.allowedRoles?.includes("all") &&
|
account.allowedRoles?.includes("all") &&
|
||||||
account.allowFrom &&
|
account.allowFrom &&
|
||||||
@ -141,7 +131,6 @@ export function collectTwitchStatusIssues(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check 8: Runtime errors
|
|
||||||
if (entry.lastError) {
|
if (entry.lastError) {
|
||||||
issues.push({
|
issues.push({
|
||||||
channel: "twitch",
|
channel: "twitch",
|
||||||
@ -152,7 +141,6 @@ export function collectTwitchStatusIssues(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check 9: Account never connected successfully
|
|
||||||
if (
|
if (
|
||||||
entry.configured &&
|
entry.configured &&
|
||||||
!entry.running &&
|
!entry.running &&
|
||||||
@ -169,7 +157,6 @@ export function collectTwitchStatusIssues(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check 10: Long-running connection may need reconnection
|
|
||||||
if (entry.running && entry.lastStartAt) {
|
if (entry.running && entry.lastStartAt) {
|
||||||
const uptime = Date.now() - entry.lastStartAt;
|
const uptime = Date.now() - entry.lastStartAt;
|
||||||
const daysSinceStart = uptime / (1000 * 60 * 60 * 24);
|
const daysSinceStart = uptime / (1000 * 60 * 60 * 24);
|
||||||
|
|||||||
@ -21,7 +21,8 @@ const mockQuit = vi.fn();
|
|||||||
const mockUnbind = vi.fn();
|
const mockUnbind = vi.fn();
|
||||||
|
|
||||||
// Event handler storage for testing
|
// 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> = [];
|
const whisperHandlers: Array<(user: string, message: string, msg: any) => void> = [];
|
||||||
|
|
||||||
// Mock functions that track handlers and return unbind objects
|
// Mock functions that track handlers and return unbind objects
|
||||||
|
|||||||
@ -26,14 +26,11 @@ export class TwitchClientManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (account.clientSecret) {
|
if (account.clientSecret) {
|
||||||
// Use RefreshingAuthProvider - can handle tokens with or without refresh tokens
|
|
||||||
const authProvider = new RefreshingAuthProvider({
|
const authProvider = new RefreshingAuthProvider({
|
||||||
clientId: account.clientId,
|
clientId: account.clientId,
|
||||||
clientSecret: account.clientSecret,
|
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
|
await authProvider
|
||||||
.addUserForToken({
|
.addUserForToken({
|
||||||
accessToken: normalizedToken,
|
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) => {
|
authProvider.onRefresh((userId, token) => {
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`[twitch] Access token refreshed for user ${userId} (expires in ${token.expiresIn ? `${token.expiresIn}s` : "unknown"})`,
|
`[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) => {
|
authProvider.onRefreshFailure((userId, error) => {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`[twitch] Failed to refresh access token for user ${userId}: ${error.message}`,
|
`[twitch] Failed to refresh access token for user ${userId}: ${error.message}`,
|
||||||
@ -76,7 +71,6 @@ export class TwitchClientManager {
|
|||||||
return authProvider;
|
return authProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to StaticAuthProvider for backward compatibility (no clientSecret)
|
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`[twitch] Using StaticAuthProvider for ${account.username} (no clientSecret provided)`,
|
`[twitch] Using StaticAuthProvider for ${account.username} (no clientSecret provided)`,
|
||||||
);
|
);
|
||||||
@ -98,7 +92,6 @@ export class TwitchClientManager {
|
|||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve token from config or environment
|
|
||||||
const tokenResolution = resolveTwitchToken(cfg, {
|
const tokenResolution = resolveTwitchToken(cfg, {
|
||||||
accountId,
|
accountId,
|
||||||
});
|
});
|
||||||
@ -119,13 +112,10 @@ export class TwitchClientManager {
|
|||||||
throw new Error("Missing Twitch client ID");
|
throw new Error("Missing Twitch client ID");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize token - strip oauth: prefix if present (Twurple doesn't need it)
|
|
||||||
const normalizedToken = normalizeToken(tokenResolution.token);
|
const normalizedToken = normalizeToken(tokenResolution.token);
|
||||||
|
|
||||||
// Create auth provider
|
|
||||||
const authProvider = await this.createAuthProvider(account, normalizedToken);
|
const authProvider = await this.createAuthProvider(account, normalizedToken);
|
||||||
|
|
||||||
// Create chat client
|
|
||||||
const client = new ChatClient({
|
const client = new ChatClient({
|
||||||
authProvider,
|
authProvider,
|
||||||
channels: [account.channel],
|
channels: [account.channel],
|
||||||
@ -160,10 +150,8 @@ export class TwitchClientManager {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up event handlers
|
|
||||||
this.setupClientHandlers(client, account);
|
this.setupClientHandlers(client, account);
|
||||||
|
|
||||||
// Connect
|
|
||||||
client.connect();
|
client.connect();
|
||||||
|
|
||||||
this.clients.set(key, client);
|
this.clients.set(key, client);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user