@openclaw/twitch 2026.5.2 → 2026.5.3-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 +3 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/index.js +18 -0
- package/dist/markdown-MRdI1sR7.js +306 -0
- package/dist/monitor-DS0YTAPB.js +333 -0
- package/dist/plugin-BQX9GiIn.js +878 -0
- package/dist/runtime-QZ5I3GlI.js +8 -0
- package/dist/runtime-api.js +1 -0
- package/dist/setup-entry.js +11 -0
- package/dist/setup-plugin-api.js +2 -0
- package/dist/setup-surface-yArVgckI.js +400 -0
- package/dist/twitch-CklAMZL5.js +131 -0
- package/package.json +20 -3
- package/api.ts +0 -21
- package/channel-plugin-api.ts +0 -1
- package/index.test.ts +0 -13
- package/index.ts +0 -16
- package/runtime-api.ts +0 -22
- package/setup-entry.ts +0 -9
- package/setup-plugin-api.ts +0 -3
- package/src/access-control.test.ts +0 -388
- package/src/access-control.ts +0 -173
- package/src/actions.test.ts +0 -74
- package/src/actions.ts +0 -175
- package/src/client-manager-registry.ts +0 -87
- package/src/config-schema.test.ts +0 -46
- package/src/config-schema.ts +0 -88
- package/src/config.test.ts +0 -233
- package/src/config.ts +0 -177
- package/src/monitor.ts +0 -314
- package/src/outbound.test.ts +0 -468
- package/src/outbound.ts +0 -186
- package/src/plugin.test.ts +0 -77
- package/src/plugin.ts +0 -208
- package/src/probe.test.ts +0 -196
- package/src/probe.ts +0 -130
- package/src/resolver.ts +0 -139
- package/src/runtime.ts +0 -9
- package/src/send.test.ts +0 -309
- package/src/send.ts +0 -139
- package/src/setup-surface.test.ts +0 -511
- package/src/setup-surface.ts +0 -520
- package/src/status.test.ts +0 -237
- package/src/status.ts +0 -179
- package/src/test-fixtures.ts +0 -30
- package/src/token.test.ts +0 -192
- package/src/token.ts +0 -93
- package/src/twitch-client.test.ts +0 -582
- package/src/twitch-client.ts +0 -276
- package/src/types.ts +0 -104
- package/src/utils/markdown.ts +0 -98
- package/src/utils/twitch.ts +0 -84
- package/test/setup.ts +0 -7
- package/tsconfig.json +0 -16
package/setup-entry.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entry-contract";
|
|
2
|
-
|
|
3
|
-
export default defineBundledChannelSetupEntry({
|
|
4
|
-
importMetaUrl: import.meta.url,
|
|
5
|
-
plugin: {
|
|
6
|
-
specifier: "./setup-plugin-api.js",
|
|
7
|
-
exportName: "twitchSetupPlugin",
|
|
8
|
-
},
|
|
9
|
-
});
|
package/setup-plugin-api.ts
DELETED
|
@@ -1,388 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { checkTwitchAccessControl, extractMentions } from "./access-control.js";
|
|
3
|
-
import type { TwitchAccountConfig, TwitchChatMessage } from "./types.js";
|
|
4
|
-
|
|
5
|
-
describe("checkTwitchAccessControl", () => {
|
|
6
|
-
const mockAccount: TwitchAccountConfig = {
|
|
7
|
-
username: "testbot",
|
|
8
|
-
accessToken: "test",
|
|
9
|
-
clientId: "test-client-id",
|
|
10
|
-
channel: "testchannel",
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const mockMessage: TwitchChatMessage = {
|
|
14
|
-
username: "testuser",
|
|
15
|
-
userId: "123456",
|
|
16
|
-
message: "hello bot",
|
|
17
|
-
channel: "testchannel",
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
function runAccessCheck(params: {
|
|
21
|
-
account?: Partial<TwitchAccountConfig>;
|
|
22
|
-
message?: Partial<TwitchChatMessage>;
|
|
23
|
-
}) {
|
|
24
|
-
return checkTwitchAccessControl({
|
|
25
|
-
message: {
|
|
26
|
-
...mockMessage,
|
|
27
|
-
...params.message,
|
|
28
|
-
},
|
|
29
|
-
account: {
|
|
30
|
-
...mockAccount,
|
|
31
|
-
...params.account,
|
|
32
|
-
},
|
|
33
|
-
botUsername: "testbot",
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function expectSingleRoleAllowed(params: {
|
|
38
|
-
role: NonNullable<TwitchAccountConfig["allowedRoles"]>[number];
|
|
39
|
-
message: Partial<TwitchChatMessage>;
|
|
40
|
-
}) {
|
|
41
|
-
const result = runAccessCheck({
|
|
42
|
-
account: { allowedRoles: [params.role] },
|
|
43
|
-
message: {
|
|
44
|
-
message: "@testbot hello",
|
|
45
|
-
...params.message,
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
expect(result.allowed).toBe(true);
|
|
49
|
-
return result;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function expectAllowedAccessCheck(params: {
|
|
53
|
-
account?: Partial<TwitchAccountConfig>;
|
|
54
|
-
message?: Partial<TwitchChatMessage>;
|
|
55
|
-
}) {
|
|
56
|
-
const result = runAccessCheck({
|
|
57
|
-
account: params.account,
|
|
58
|
-
message: {
|
|
59
|
-
message: "@testbot hello",
|
|
60
|
-
...params.message,
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
expect(result.allowed).toBe(true);
|
|
64
|
-
return result;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function expectAllowFromBlocked(params: {
|
|
68
|
-
allowFrom: string[];
|
|
69
|
-
allowedRoles?: NonNullable<TwitchAccountConfig["allowedRoles"]>;
|
|
70
|
-
message?: Partial<TwitchChatMessage>;
|
|
71
|
-
reason: string;
|
|
72
|
-
}) {
|
|
73
|
-
const result = runAccessCheck({
|
|
74
|
-
account: {
|
|
75
|
-
allowFrom: params.allowFrom,
|
|
76
|
-
allowedRoles: params.allowedRoles,
|
|
77
|
-
},
|
|
78
|
-
message: {
|
|
79
|
-
message: "@testbot hello",
|
|
80
|
-
...params.message,
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
expect(result.allowed).toBe(false);
|
|
84
|
-
expect(result.reason).toContain(params.reason);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
describe("when no restrictions are configured", () => {
|
|
88
|
-
it("allows messages that mention the bot (default requireMention)", () => {
|
|
89
|
-
const result = runAccessCheck({
|
|
90
|
-
message: {
|
|
91
|
-
message: "@testbot hello",
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
expect(result.allowed).toBe(true);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe("requireMention default", () => {
|
|
99
|
-
it("defaults to true when undefined", () => {
|
|
100
|
-
const result = runAccessCheck({
|
|
101
|
-
message: {
|
|
102
|
-
message: "hello bot",
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
expect(result.allowed).toBe(false);
|
|
106
|
-
expect(result.reason).toContain("does not mention the bot");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("allows mention when requireMention is undefined", () => {
|
|
110
|
-
const result = runAccessCheck({
|
|
111
|
-
message: {
|
|
112
|
-
message: "@testbot hello",
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
expect(result.allowed).toBe(true);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe("requireMention", () => {
|
|
120
|
-
it("allows messages that mention the bot", () => {
|
|
121
|
-
const result = runAccessCheck({
|
|
122
|
-
account: { requireMention: true },
|
|
123
|
-
message: { message: "@testbot hello" },
|
|
124
|
-
});
|
|
125
|
-
expect(result.allowed).toBe(true);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("blocks messages that don't mention the bot", () => {
|
|
129
|
-
const result = runAccessCheck({
|
|
130
|
-
account: { requireMention: true },
|
|
131
|
-
});
|
|
132
|
-
expect(result.allowed).toBe(false);
|
|
133
|
-
expect(result.reason).toContain("does not mention the bot");
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("is case-insensitive for bot username", () => {
|
|
137
|
-
const result = runAccessCheck({
|
|
138
|
-
account: { requireMention: true },
|
|
139
|
-
message: { message: "@TestBot hello" },
|
|
140
|
-
});
|
|
141
|
-
expect(result.allowed).toBe(true);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe("allowFrom allowlist", () => {
|
|
146
|
-
it("allows users in the allowlist", () => {
|
|
147
|
-
const result = expectAllowedAccessCheck({
|
|
148
|
-
account: {
|
|
149
|
-
allowFrom: ["123456", "789012"],
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
expect(result.matchKey).toBe("123456");
|
|
153
|
-
expect(result.matchSource).toBe("allowlist");
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("blocks users not in allowlist when allowFrom is set", () => {
|
|
157
|
-
expectAllowFromBlocked({
|
|
158
|
-
allowFrom: ["789012"],
|
|
159
|
-
reason: "allowFrom",
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("blocks everyone when allowFrom is explicitly empty", () => {
|
|
164
|
-
expectAllowFromBlocked({
|
|
165
|
-
allowFrom: [],
|
|
166
|
-
reason: "allowFrom",
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("blocks messages without userId", () => {
|
|
171
|
-
expectAllowFromBlocked({
|
|
172
|
-
allowFrom: ["123456"],
|
|
173
|
-
message: { userId: undefined },
|
|
174
|
-
reason: "user ID not available",
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("bypasses role checks when user is in allowlist", () => {
|
|
179
|
-
const account: TwitchAccountConfig = {
|
|
180
|
-
...mockAccount,
|
|
181
|
-
allowFrom: ["123456"],
|
|
182
|
-
allowedRoles: ["owner"],
|
|
183
|
-
};
|
|
184
|
-
const message: TwitchChatMessage = {
|
|
185
|
-
...mockMessage,
|
|
186
|
-
message: "@testbot hello",
|
|
187
|
-
isOwner: false,
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
const result = checkTwitchAccessControl({
|
|
191
|
-
message,
|
|
192
|
-
account,
|
|
193
|
-
botUsername: "testbot",
|
|
194
|
-
});
|
|
195
|
-
expect(result.allowed).toBe(true);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("blocks user with role when not in allowlist", () => {
|
|
199
|
-
expectAllowFromBlocked({
|
|
200
|
-
allowFrom: ["789012"],
|
|
201
|
-
allowedRoles: ["moderator"],
|
|
202
|
-
message: { userId: "123456", isMod: true },
|
|
203
|
-
reason: "allowFrom",
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it("blocks user not in allowlist even when roles configured", () => {
|
|
208
|
-
expectAllowFromBlocked({
|
|
209
|
-
allowFrom: ["789012"],
|
|
210
|
-
allowedRoles: ["moderator"],
|
|
211
|
-
message: { userId: "123456", isMod: false },
|
|
212
|
-
reason: "allowFrom",
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
describe("allowedRoles", () => {
|
|
218
|
-
it("allows users with matching role", () => {
|
|
219
|
-
const result = expectSingleRoleAllowed({
|
|
220
|
-
role: "moderator",
|
|
221
|
-
message: { isMod: true },
|
|
222
|
-
});
|
|
223
|
-
expect(result.matchSource).toBe("role");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("allows users with any of multiple roles", () => {
|
|
227
|
-
const account: TwitchAccountConfig = {
|
|
228
|
-
...mockAccount,
|
|
229
|
-
allowedRoles: ["moderator", "vip", "subscriber"],
|
|
230
|
-
};
|
|
231
|
-
const message: TwitchChatMessage = {
|
|
232
|
-
...mockMessage,
|
|
233
|
-
message: "@testbot hello",
|
|
234
|
-
isVip: true,
|
|
235
|
-
isMod: false,
|
|
236
|
-
isSub: false,
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
const result = checkTwitchAccessControl({
|
|
240
|
-
message,
|
|
241
|
-
account,
|
|
242
|
-
botUsername: "testbot",
|
|
243
|
-
});
|
|
244
|
-
expect(result.allowed).toBe(true);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("blocks users without matching role", () => {
|
|
248
|
-
const account: TwitchAccountConfig = {
|
|
249
|
-
...mockAccount,
|
|
250
|
-
allowedRoles: ["moderator"],
|
|
251
|
-
};
|
|
252
|
-
const message: TwitchChatMessage = {
|
|
253
|
-
...mockMessage,
|
|
254
|
-
message: "@testbot hello",
|
|
255
|
-
isMod: false,
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const result = checkTwitchAccessControl({
|
|
259
|
-
message,
|
|
260
|
-
account,
|
|
261
|
-
botUsername: "testbot",
|
|
262
|
-
});
|
|
263
|
-
expect(result.allowed).toBe(false);
|
|
264
|
-
expect(result.reason).toContain("does not have any of the required roles");
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("allows all users when role is 'all'", () => {
|
|
268
|
-
const result = expectAllowedAccessCheck({
|
|
269
|
-
account: {
|
|
270
|
-
allowedRoles: ["all"],
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
expect(result.matchKey).toBe("all");
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("handles moderator role", () => {
|
|
277
|
-
expectSingleRoleAllowed({
|
|
278
|
-
role: "moderator",
|
|
279
|
-
message: { isMod: true },
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it("handles subscriber role", () => {
|
|
284
|
-
expectSingleRoleAllowed({
|
|
285
|
-
role: "subscriber",
|
|
286
|
-
message: { isSub: true },
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it("handles owner role", () => {
|
|
291
|
-
expectSingleRoleAllowed({
|
|
292
|
-
role: "owner",
|
|
293
|
-
message: { isOwner: true },
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it("handles vip role", () => {
|
|
298
|
-
expectSingleRoleAllowed({
|
|
299
|
-
role: "vip",
|
|
300
|
-
message: { isVip: true },
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
describe("combined restrictions", () => {
|
|
306
|
-
it("checks requireMention before allowlist", () => {
|
|
307
|
-
const account: TwitchAccountConfig = {
|
|
308
|
-
...mockAccount,
|
|
309
|
-
requireMention: true,
|
|
310
|
-
allowFrom: ["123456"],
|
|
311
|
-
};
|
|
312
|
-
const message: TwitchChatMessage = {
|
|
313
|
-
...mockMessage,
|
|
314
|
-
message: "hello", // No mention
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const result = checkTwitchAccessControl({
|
|
318
|
-
message,
|
|
319
|
-
account,
|
|
320
|
-
botUsername: "testbot",
|
|
321
|
-
});
|
|
322
|
-
expect(result.allowed).toBe(false);
|
|
323
|
-
expect(result.reason).toContain("does not mention the bot");
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it("checks allowlist before allowedRoles", () => {
|
|
327
|
-
const result = runAccessCheck({
|
|
328
|
-
account: {
|
|
329
|
-
allowFrom: ["123456"],
|
|
330
|
-
allowedRoles: ["owner"],
|
|
331
|
-
},
|
|
332
|
-
message: {
|
|
333
|
-
message: "@testbot hello",
|
|
334
|
-
isOwner: false,
|
|
335
|
-
},
|
|
336
|
-
});
|
|
337
|
-
expect(result.allowed).toBe(true);
|
|
338
|
-
expect(result.matchSource).toBe("allowlist");
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
describe("extractMentions", () => {
|
|
344
|
-
it("extracts single mention", () => {
|
|
345
|
-
const mentions = extractMentions("hello @testbot");
|
|
346
|
-
expect(mentions).toEqual(["testbot"]);
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it("extracts multiple mentions", () => {
|
|
350
|
-
const mentions = extractMentions("hello @testbot and @otheruser");
|
|
351
|
-
expect(mentions).toEqual(["testbot", "otheruser"]);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it("returns empty array when no mentions", () => {
|
|
355
|
-
const mentions = extractMentions("hello everyone");
|
|
356
|
-
expect(mentions).toEqual([]);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
it("handles mentions at start of message", () => {
|
|
360
|
-
const mentions = extractMentions("@testbot hello");
|
|
361
|
-
expect(mentions).toEqual(["testbot"]);
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it("handles mentions at end of message", () => {
|
|
365
|
-
const mentions = extractMentions("hello @testbot");
|
|
366
|
-
expect(mentions).toEqual(["testbot"]);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it("converts mentions to lowercase", () => {
|
|
370
|
-
const mentions = extractMentions("hello @TestBot");
|
|
371
|
-
expect(mentions).toEqual(["testbot"]);
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
it("extracts alphanumeric usernames", () => {
|
|
375
|
-
const mentions = extractMentions("hello @user123");
|
|
376
|
-
expect(mentions).toEqual(["user123"]);
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
it("handles underscores in usernames", () => {
|
|
380
|
-
const mentions = extractMentions("hello @test_user");
|
|
381
|
-
expect(mentions).toEqual(["test_user"]);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it("handles empty string", () => {
|
|
385
|
-
const mentions = extractMentions("");
|
|
386
|
-
expect(mentions).toEqual([]);
|
|
387
|
-
});
|
|
388
|
-
});
|
package/src/access-control.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
2
|
-
import type { TwitchAccountConfig, TwitchChatMessage } from "./types.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Result of checking access control for a Twitch message
|
|
6
|
-
*/
|
|
7
|
-
type TwitchAccessControlResult = {
|
|
8
|
-
allowed: boolean;
|
|
9
|
-
reason?: string;
|
|
10
|
-
matchKey?: string;
|
|
11
|
-
matchSource?: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Check if a Twitch message should be allowed based on account configuration
|
|
16
|
-
*
|
|
17
|
-
* This function implements the access control logic for incoming Twitch messages,
|
|
18
|
-
* checking allowlists, role-based restrictions, and mention requirements.
|
|
19
|
-
*
|
|
20
|
-
* Priority order:
|
|
21
|
-
* 1. If `requireMention` is true, message must mention the bot
|
|
22
|
-
* 2. If `allowFrom` is set, sender must be in the allowlist (by user ID)
|
|
23
|
-
* 3. If `allowedRoles` is set (and `allowFrom` is not), sender must have at least one role
|
|
24
|
-
*
|
|
25
|
-
* Note: `allowFrom` is a hard allowlist. When set, only those user IDs are allowed.
|
|
26
|
-
* Use `allowedRoles` as an alternative when you don't want to maintain an allowlist.
|
|
27
|
-
*
|
|
28
|
-
* Available roles:
|
|
29
|
-
* - "moderator": Moderators
|
|
30
|
-
* - "owner": Channel owner/broadcaster
|
|
31
|
-
* - "vip": VIPs
|
|
32
|
-
* - "subscriber": Subscribers
|
|
33
|
-
* - "all": Anyone in the chat
|
|
34
|
-
*/
|
|
35
|
-
export function checkTwitchAccessControl(params: {
|
|
36
|
-
message: TwitchChatMessage;
|
|
37
|
-
account: TwitchAccountConfig;
|
|
38
|
-
botUsername: string;
|
|
39
|
-
}): TwitchAccessControlResult {
|
|
40
|
-
const { message, account, botUsername } = params;
|
|
41
|
-
|
|
42
|
-
if (account.requireMention ?? true) {
|
|
43
|
-
const mentions = extractMentions(message.message);
|
|
44
|
-
if (!mentions.includes(normalizeLowercaseStringOrEmpty(botUsername))) {
|
|
45
|
-
return {
|
|
46
|
-
allowed: false,
|
|
47
|
-
reason: "message does not mention the bot (requireMention is enabled)",
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (account.allowFrom !== undefined) {
|
|
53
|
-
const allowFrom = account.allowFrom;
|
|
54
|
-
if (allowFrom.length === 0) {
|
|
55
|
-
return {
|
|
56
|
-
allowed: false,
|
|
57
|
-
reason: "sender is not in allowFrom allowlist",
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
const senderId = message.userId;
|
|
61
|
-
|
|
62
|
-
if (!senderId) {
|
|
63
|
-
return {
|
|
64
|
-
allowed: false,
|
|
65
|
-
reason: "sender user ID not available for allowlist check",
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (allowFrom.includes(senderId)) {
|
|
70
|
-
return {
|
|
71
|
-
allowed: true,
|
|
72
|
-
matchKey: senderId,
|
|
73
|
-
matchSource: "allowlist",
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
allowed: false,
|
|
79
|
-
reason: "sender is not in allowFrom allowlist",
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (account.allowedRoles && account.allowedRoles.length > 0) {
|
|
84
|
-
const allowedRoles = account.allowedRoles;
|
|
85
|
-
|
|
86
|
-
// "all" grants access to everyone
|
|
87
|
-
if (allowedRoles.includes("all")) {
|
|
88
|
-
return {
|
|
89
|
-
allowed: true,
|
|
90
|
-
matchKey: "all",
|
|
91
|
-
matchSource: "role",
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const hasAllowedRole = checkSenderRoles({
|
|
96
|
-
message,
|
|
97
|
-
allowedRoles,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
if (!hasAllowedRole) {
|
|
101
|
-
return {
|
|
102
|
-
allowed: false,
|
|
103
|
-
reason: `sender does not have any of the required roles: ${allowedRoles.join(", ")}`,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
allowed: true,
|
|
109
|
-
matchKey: allowedRoles.join(","),
|
|
110
|
-
matchSource: "role",
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
allowed: true,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Check if the sender has any of the allowed roles
|
|
121
|
-
*/
|
|
122
|
-
function checkSenderRoles(params: { message: TwitchChatMessage; allowedRoles: string[] }): boolean {
|
|
123
|
-
const { message, allowedRoles } = params;
|
|
124
|
-
const { isMod, isOwner, isVip, isSub } = message;
|
|
125
|
-
|
|
126
|
-
for (const role of allowedRoles) {
|
|
127
|
-
switch (role) {
|
|
128
|
-
case "moderator":
|
|
129
|
-
if (isMod) {
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
break;
|
|
133
|
-
case "owner":
|
|
134
|
-
if (isOwner) {
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
break;
|
|
138
|
-
case "vip":
|
|
139
|
-
if (isVip) {
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
break;
|
|
143
|
-
case "subscriber":
|
|
144
|
-
if (isSub) {
|
|
145
|
-
return true;
|
|
146
|
-
}
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Extract @mentions from a Twitch chat message
|
|
156
|
-
*
|
|
157
|
-
* Returns a list of lowercase usernames that were mentioned in the message.
|
|
158
|
-
* Twitch mentions are in the format @username.
|
|
159
|
-
*/
|
|
160
|
-
export function extractMentions(message: string): string[] {
|
|
161
|
-
const mentionRegex = /@(\w+)/g;
|
|
162
|
-
const mentions: string[] = [];
|
|
163
|
-
let match: RegExpExecArray | null;
|
|
164
|
-
|
|
165
|
-
while ((match = mentionRegex.exec(message)) !== null) {
|
|
166
|
-
const username = match[1];
|
|
167
|
-
if (username) {
|
|
168
|
-
mentions.push(normalizeLowercaseStringOrEmpty(username));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return mentions;
|
|
173
|
-
}
|
package/src/actions.test.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
2
|
-
import { twitchMessageActions } from "./actions.js";
|
|
3
|
-
import type { ResolvedTwitchAccountContext } from "./config.js";
|
|
4
|
-
import { resolveTwitchAccountContext } from "./config.js";
|
|
5
|
-
import { twitchOutbound } from "./outbound.js";
|
|
6
|
-
|
|
7
|
-
vi.mock("./config.js", () => ({
|
|
8
|
-
DEFAULT_ACCOUNT_ID: "default",
|
|
9
|
-
resolveTwitchAccountContext: vi.fn(),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
vi.mock("./outbound.js", () => ({
|
|
13
|
-
twitchOutbound: {
|
|
14
|
-
sendText: vi.fn(),
|
|
15
|
-
},
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
function createSecondaryAccountContext(accountId = "secondary"): ResolvedTwitchAccountContext {
|
|
19
|
-
return {
|
|
20
|
-
accountId,
|
|
21
|
-
account: {
|
|
22
|
-
channel: "secondary-channel",
|
|
23
|
-
username: "secondary",
|
|
24
|
-
accessToken: "oauth:secondary-token",
|
|
25
|
-
clientId: "secondary-client",
|
|
26
|
-
enabled: true,
|
|
27
|
-
},
|
|
28
|
-
tokenResolution: { source: "config", token: "oauth:secondary-token" },
|
|
29
|
-
configured: true,
|
|
30
|
-
availableAccountIds: ["default", "secondary"],
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe("twitchMessageActions", () => {
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
vi.clearAllMocks();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("uses configured defaultAccount when action accountId is omitted", async () => {
|
|
40
|
-
vi.mocked(resolveTwitchAccountContext)
|
|
41
|
-
.mockImplementationOnce(() => createSecondaryAccountContext())
|
|
42
|
-
.mockImplementation((_cfg, accountId) =>
|
|
43
|
-
createSecondaryAccountContext(accountId?.trim() || "secondary"),
|
|
44
|
-
);
|
|
45
|
-
const sendText = twitchOutbound.sendText;
|
|
46
|
-
if (!sendText) {
|
|
47
|
-
throw new Error("twitchOutbound.sendText is unavailable");
|
|
48
|
-
}
|
|
49
|
-
vi.mocked(sendText).mockResolvedValue({
|
|
50
|
-
channel: "twitch",
|
|
51
|
-
messageId: "msg-1",
|
|
52
|
-
timestamp: 1,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
await twitchMessageActions.handleAction!({
|
|
56
|
-
action: "send",
|
|
57
|
-
params: { message: "Hello!" },
|
|
58
|
-
cfg: {
|
|
59
|
-
channels: {
|
|
60
|
-
twitch: {
|
|
61
|
-
defaultAccount: "secondary",
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
} as never);
|
|
66
|
-
|
|
67
|
-
expect(twitchOutbound.sendText).toHaveBeenCalledWith(
|
|
68
|
-
expect.objectContaining({
|
|
69
|
-
accountId: "secondary",
|
|
70
|
-
to: "secondary-channel",
|
|
71
|
-
}),
|
|
72
|
-
);
|
|
73
|
-
});
|
|
74
|
-
});
|