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

155 lines
3.9 KiB
TypeScript

import type { TwitchAccountConfig, TwitchChatMessage } from "./types.js";
/**
* Result of checking access control for a Twitch message
*/
export type TwitchAccessControlResult = {
allowed: boolean;
reason?: string;
matchKey?: string;
matchSource?: string;
};
/**
* Check if a Twitch message should be allowed based on account configuration
*
* This function implements the access control logic for incoming Twitch messages,
* checking allowlists, role-based restrictions, and mention requirements.
*
* Priority order:
* 1. If `requireMention` is true, message must mention the bot
* 2. If `allowFrom` is set, sender must be in the allowlist (by user ID)
* 3. If `allowedRoles` is set, sender must have at least one of the specified roles
*
* Note: You can combine `allowFrom` with `allowedRoles`. If a user is in `allowFrom`,
* they bypass role checks. This is useful for allowing specific users regardless of role.
*
* Available roles:
* - "moderator": Moderators
* - "owner": Channel owner/broadcaster
* - "vip": VIPs
* - "subscriber": Subscribers
* - "all": Anyone in the chat
*/
export function checkTwitchAccessControl(params: {
message: TwitchChatMessage;
account: TwitchAccountConfig;
botUsername: string;
}): TwitchAccessControlResult {
const { message, account, botUsername } = params;
if (account.requireMention ?? true) {
const mentions = extractMentions(message.message);
if (!mentions.includes(botUsername.toLowerCase())) {
return {
allowed: false,
reason: "message does not mention the bot (requireMention is enabled)",
};
}
}
if (account.allowFrom && account.allowFrom.length > 0) {
const allowFrom = account.allowFrom;
const senderId = message.userId;
if (!senderId) {
return {
allowed: false,
reason: "sender user ID not available for allowlist check",
};
}
if (allowFrom.includes(senderId)) {
return {
allowed: true,
matchKey: senderId,
matchSource: "allowlist",
};
}
}
if (account.allowedRoles && account.allowedRoles.length > 0) {
const allowedRoles = account.allowedRoles;
// "all" grants access to everyone
if (allowedRoles.includes("all")) {
return {
allowed: true,
matchKey: "all",
matchSource: "role",
};
}
const hasAllowedRole = checkSenderRoles({
message,
allowedRoles,
});
if (!hasAllowedRole) {
return {
allowed: false,
reason: `sender does not have any of the required roles: ${allowedRoles.join(", ")}`,
};
}
return {
allowed: true,
matchKey: allowedRoles.join(","),
matchSource: "role",
};
}
return {
allowed: true,
};
}
/**
* Check if the sender has any of the allowed roles
*/
function checkSenderRoles(params: { message: TwitchChatMessage; allowedRoles: string[] }): boolean {
const { message, allowedRoles } = params;
const { isMod, isOwner, isVip, isSub } = message;
for (const role of allowedRoles) {
switch (role) {
case "moderator":
if (isMod) return true;
break;
case "owner":
if (isOwner) return true;
break;
case "vip":
if (isVip) return true;
break;
case "subscriber":
if (isSub) return true;
break;
}
}
return false;
}
/**
* Extract @mentions from a Twitch chat message
*
* Returns a list of lowercase usernames that were mentioned in the message.
* Twitch mentions are in the format @username.
*/
export function extractMentions(message: string): string[] {
const mentionRegex = /@(\w+)/g;
const mentions: string[] = [];
let match: RegExpExecArray | null;
// biome-ignore lint/suspicious/noAssignInExpressions: Standard regex iteration pattern
while ((match = mentionRegex.exec(message)) !== null) {
const username = match[1];
if (username) {
mentions.push(username.toLowerCase());
}
}
return mentions;
}