@openclaw/twitch 2026.5.9-beta.1 → 2026.5.10-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -1,3 +1,3 @@
1
- import { t as twitchPlugin } from "./plugin-CoIt3s7P.js";
1
+ import { t as twitchPlugin } from "./plugin-BLMSYnzs.js";
2
2
  import { n as setTwitchRuntime } from "./runtime-QZ5I3GlI.js";
3
3
  export { setTwitchRuntime, twitchPlugin };
@@ -1,2 +1,2 @@
1
- import { t as twitchPlugin } from "./plugin-CoIt3s7P.js";
1
+ import { t as twitchPlugin } from "./plugin-BLMSYnzs.js";
2
2
  export { twitchPlugin };
@@ -2,116 +2,127 @@ import { i as getOrCreateClientManager, n as stripMarkdownForTwitch } from "./ma
2
2
  import { t as getTwitchRuntime } from "./runtime-QZ5I3GlI.js";
3
3
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
4
4
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
5
+ import { createChannelIngressResolver, defineStableChannelIngressIdentity } from "openclaw/plugin-sdk/channel-ingress-runtime";
5
6
  //#region extensions/twitch/src/access-control.ts
6
- /**
7
- * Check if a Twitch message should be allowed based on account configuration
8
- *
9
- * This function implements the access control logic for incoming Twitch messages,
10
- * checking allowlists, role-based restrictions, and mention requirements.
11
- *
12
- * Priority order:
13
- * 1. If `requireMention` is true, message must mention the bot
14
- * 2. If `allowFrom` is set, sender must be in the allowlist (by user ID)
15
- * 3. If `allowedRoles` is set (and `allowFrom` is not), sender must have at least one role
16
- *
17
- * Note: `allowFrom` is a hard allowlist. When set, only those user IDs are allowed.
18
- * Use `allowedRoles` as an alternative when you don't want to maintain an allowlist.
19
- *
20
- * Available roles:
21
- * - "moderator": Moderators
22
- * - "owner": Channel owner/broadcaster
23
- * - "vip": VIPs
24
- * - "subscriber": Subscribers
25
- * - "all": Anyone in the chat
26
- */
27
- function checkTwitchAccessControl(params) {
7
+ const twitchUserIdentity = defineStableChannelIngressIdentity({
8
+ key: "sender-id",
9
+ entryIdPrefix: "twitch-user-entry"
10
+ });
11
+ const twitchRoleIdentity = defineStableChannelIngressIdentity({
12
+ key: "role-moderator",
13
+ kind: "role",
14
+ normalizeEntry: normalizeTwitchRole,
15
+ normalizeSubject: normalizeTwitchRole,
16
+ aliases: [
17
+ "owner",
18
+ "vip",
19
+ "subscriber"
20
+ ].map((role) => ({
21
+ key: `role-${role}`,
22
+ kind: "role",
23
+ normalizeEntry: () => null,
24
+ normalizeSubject: normalizeTwitchRole
25
+ })),
26
+ isWildcardEntry: (entry) => normalizeTwitchRole(entry) === "all",
27
+ resolveEntryId: ({ entryIndex }) => `twitch-role-entry-${entryIndex + 1}`
28
+ });
29
+ async function checkTwitchAccessControl(params) {
28
30
  const { message, account, botUsername } = params;
29
- if (account.requireMention ?? true) {
30
- if (!extractMentions(message.message).includes(normalizeLowercaseStringOrEmpty(botUsername))) return {
31
- allowed: false,
32
- reason: "message does not mention the bot (requireMention is enabled)"
33
- };
34
- }
35
- if (account.allowFrom !== void 0) {
36
- const allowFrom = account.allowFrom;
37
- if (allowFrom.length === 0) return {
38
- allowed: false,
39
- reason: "sender is not in allowFrom allowlist"
40
- };
41
- const senderId = message.userId;
42
- if (!senderId) return {
43
- allowed: false,
44
- reason: "sender user ID not available for allowlist check"
45
- };
46
- if (allowFrom.includes(senderId)) return {
31
+ const policyKind = resolveTwitchPolicyKind(account);
32
+ const decision = (await createChannelIngressResolver({
33
+ channelId: "twitch",
34
+ accountId: "default",
35
+ identity: policyKind === "role" ? twitchRoleIdentity : twitchUserIdentity
36
+ }).message({
37
+ subject: policyKind === "role" ? twitchRoleSubject(message) : { stableId: message.userId },
38
+ conversation: {
39
+ kind: "group",
40
+ id: message.channel
41
+ },
42
+ event: { mayPair: false },
43
+ mentionFacts: {
44
+ canDetectMention: true,
45
+ wasMentioned: mentionsBot(message.message, botUsername)
46
+ },
47
+ dmPolicy: "open",
48
+ groupPolicy: policyKind === "open" ? "open" : "allowlist",
49
+ policy: { activation: {
50
+ requireMention: account.requireMention ?? true,
51
+ allowTextCommands: false,
52
+ order: "before-sender"
53
+ } },
54
+ groupAllowFrom: policyKind === "allowFrom" ? account.allowFrom : policyKind === "role" ? account.allowedRoles : void 0
55
+ })).ingress;
56
+ if (decision.decisiveGateId === "activation" && decision.admission !== "dispatch") return {
57
+ allowed: false,
58
+ reason: "message does not mention the bot (requireMention is enabled)"
59
+ };
60
+ if (decision.admission === "dispatch") {
61
+ if (policyKind === "allowFrom") return {
47
62
  allowed: true,
48
- matchKey: senderId,
63
+ matchKey: params.message.userId,
49
64
  matchSource: "allowlist"
50
65
  };
51
- return {
52
- allowed: false,
53
- reason: "sender is not in allowFrom allowlist"
54
- };
55
- }
56
- if (account.allowedRoles && account.allowedRoles.length > 0) {
57
- const allowedRoles = account.allowedRoles;
58
- if (allowedRoles.includes("all")) return {
66
+ if (policyKind === "role") return {
59
67
  allowed: true,
60
- matchKey: "all",
68
+ matchKey: params.account.allowedRoles?.join(","),
61
69
  matchSource: "role"
62
70
  };
63
- if (!checkSenderRoles({
64
- message,
65
- allowedRoles
66
- })) return {
71
+ return { allowed: true };
72
+ }
73
+ if (policyKind === "allowFrom") {
74
+ if (!params.message.userId) return {
67
75
  allowed: false,
68
- reason: `sender does not have any of the required roles: ${allowedRoles.join(", ")}`
76
+ reason: "sender user ID not available for allowlist check"
69
77
  };
70
78
  return {
71
- allowed: true,
72
- matchKey: allowedRoles.join(","),
73
- matchSource: "role"
79
+ allowed: false,
80
+ reason: "sender is not in allowFrom allowlist"
74
81
  };
75
82
  }
76
- return { allowed: true };
83
+ if (policyKind === "role") return {
84
+ allowed: false,
85
+ reason: `sender does not have any of the required roles: ${params.account.allowedRoles?.join(", ") ?? ""}`
86
+ };
87
+ return {
88
+ allowed: false,
89
+ reason: reasonForTwitchIngressDecision(decision)
90
+ };
77
91
  }
78
- /**
79
- * Check if the sender has any of the allowed roles
80
- */
81
- function checkSenderRoles(params) {
82
- const { message, allowedRoles } = params;
83
- const { isMod, isOwner, isVip, isSub } = message;
84
- for (const role of allowedRoles) switch (role) {
85
- case "moderator":
86
- if (isMod) return true;
87
- break;
88
- case "owner":
89
- if (isOwner) return true;
90
- break;
91
- case "vip":
92
- if (isVip) return true;
93
- break;
94
- case "subscriber":
95
- if (isSub) return true;
96
- break;
92
+ function resolveTwitchPolicyKind(account) {
93
+ if (account.allowFrom !== void 0) return "allowFrom";
94
+ if (account.allowedRoles && account.allowedRoles.length > 0) return "role";
95
+ return "open";
96
+ }
97
+ function twitchRoleSubject(message) {
98
+ return {
99
+ stableId: message.isMod ? "moderator" : void 0,
100
+ aliases: {
101
+ "role-owner": message.isOwner ? "owner" : void 0,
102
+ "role-vip": message.isVip ? "vip" : void 0,
103
+ "role-subscriber": message.isSub ? "subscriber" : void 0
104
+ }
105
+ };
106
+ }
107
+ function normalizeTwitchRole(value) {
108
+ const role = normalizeLowercaseStringOrEmpty(value);
109
+ if (role === "*") return "all";
110
+ return role === "moderator" || role === "owner" || role === "vip" || role === "subscriber" || role === "all" ? role : null;
111
+ }
112
+ function reasonForTwitchIngressDecision(decision) {
113
+ switch (decision.reasonCode) {
114
+ case "activation_skipped": return "message does not mention the bot (requireMention is enabled)";
115
+ case "group_policy_empty_allowlist":
116
+ case "group_policy_not_allowlisted": return "sender is not in allowFrom allowlist";
117
+ default: return decision.reasonCode;
97
118
  }
98
- return false;
99
119
  }
100
- /**
101
- * Extract @mentions from a Twitch chat message
102
- *
103
- * Returns a list of lowercase usernames that were mentioned in the message.
104
- * Twitch mentions are in the format @username.
105
- */
106
- function extractMentions(message) {
120
+ function mentionsBot(message, botUsername) {
121
+ const expected = normalizeLowercaseStringOrEmpty(botUsername);
107
122
  const mentionRegex = /@(\w+)/g;
108
- const mentions = [];
109
123
  let match;
110
- while ((match = mentionRegex.exec(message)) !== null) {
111
- const username = match[1];
112
- if (username) mentions.push(normalizeLowercaseStringOrEmpty(username));
113
- }
114
- return mentions;
124
+ while ((match = mentionRegex.exec(message)) !== null) if ((match[1] ? normalizeLowercaseStringOrEmpty(match[1]) : "") === expected) return true;
125
+ return false;
115
126
  }
116
127
  //#endregion
117
128
  //#region extensions/twitch/src/monitor.ts
@@ -292,23 +303,26 @@ async function monitorTwitchProvider(options) {
292
303
  }
293
304
  const unregisterHandler = clientManager.onMessage(account, (message) => {
294
305
  if (stopped) return;
295
- const botUsername = normalizeLowercaseStringOrEmpty(account.username);
296
- if (normalizeLowercaseStringOrEmpty(message.username) === botUsername) return;
297
- if (!checkTwitchAccessControl({
298
- message,
299
- account,
300
- botUsername
301
- }).allowed) return;
302
- statusSink?.({ lastInboundAt: Date.now() });
303
- processTwitchMessage({
304
- message,
305
- account,
306
- accountId,
307
- config,
308
- runtime,
309
- core,
310
- statusSink
311
- }).catch((err) => {
306
+ (async () => {
307
+ const botUsername = normalizeLowercaseStringOrEmpty(account.username);
308
+ if (normalizeLowercaseStringOrEmpty(message.username) === botUsername) return;
309
+ const access = await checkTwitchAccessControl({
310
+ message,
311
+ account,
312
+ botUsername
313
+ });
314
+ if (stopped || !access.allowed) return;
315
+ statusSink?.({ lastInboundAt: Date.now() });
316
+ await processTwitchMessage({
317
+ message,
318
+ account,
319
+ accountId,
320
+ config,
321
+ runtime,
322
+ core,
323
+ statusSink
324
+ });
325
+ })().catch((err) => {
312
326
  runtime.error?.(`Message processing failed: ${String(err)}`);
313
327
  });
314
328
  });
@@ -954,7 +954,7 @@ const twitchPlugin = createChatChannelPlugin({
954
954
  lastError: null
955
955
  });
956
956
  ctx.log?.info(`Starting Twitch connection for ${account.username}`);
957
- const { monitorTwitchProvider } = await import("./monitor-DmMYPZvJ.js");
957
+ const { monitorTwitchProvider } = await import("./monitor-DvbgvvL4.js");
958
958
  await monitorTwitchProvider({
959
959
  account,
960
960
  accountId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/twitch",
3
- "version": "2026.5.9-beta.1",
3
+ "version": "2026.5.10-beta.1",
4
4
  "description": "OpenClaw Twitch channel plugin",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,10 +26,10 @@
26
26
  "minHostVersion": ">=2026.4.10"
27
27
  },
28
28
  "compat": {
29
- "pluginApi": ">=2026.5.9-beta.1"
29
+ "pluginApi": ">=2026.5.10-beta.1"
30
30
  },
31
31
  "build": {
32
- "openclawVersion": "2026.5.9-beta.1"
32
+ "openclawVersion": "2026.5.10-beta.1"
33
33
  },
34
34
  "channel": {
35
35
  "id": "twitch",
@@ -56,7 +56,7 @@
56
56
  "README.md"
57
57
  ],
58
58
  "peerDependencies": {
59
- "openclaw": ">=2026.5.9-beta.1"
59
+ "openclaw": ">=2026.5.10-beta.1"
60
60
  },
61
61
  "peerDependenciesMeta": {
62
62
  "openclaw": {