mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 07:57:39 +00:00
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>
This commit is contained in:
154
extensions/twitch/src/access-control.ts
Normal file
154
extensions/twitch/src/access-control.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user