@openclaw/twitch 2026.2.21 → 2026.5.1-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/README.md +2 -2
- package/api.ts +21 -0
- package/channel-plugin-api.ts +1 -0
- package/index.test.ts +13 -0
- package/index.ts +12 -16
- package/openclaw.plugin.json +6 -0
- package/package.json +29 -7
- package/runtime-api.ts +22 -0
- package/setup-entry.ts +9 -0
- package/setup-plugin-api.ts +3 -0
- package/src/access-control.test.ts +136 -239
- package/src/access-control.ts +11 -4
- package/src/actions.test.ts +74 -0
- package/src/actions.ts +7 -6
- package/src/client-manager-registry.ts +0 -28
- package/src/config-schema.ts +2 -2
- package/src/config.test.ts +147 -1
- package/src/config.ts +76 -15
- package/src/monitor.ts +126 -85
- package/src/outbound.test.ts +140 -75
- package/src/outbound.ts +4 -5
- package/src/plugin.test.ts +39 -1
- package/src/plugin.ts +176 -242
- package/src/probe.test.ts +1 -1
- package/src/probe.ts +16 -5
- package/src/resolver.ts +5 -3
- package/src/runtime.ts +8 -13
- package/src/send.test.ts +92 -59
- package/src/send.ts +18 -15
- package/src/setup-surface.test.ts +511 -0
- package/src/setup-surface.ts +520 -0
- package/src/status.test.ts +120 -153
- package/src/status.ts +1 -1
- package/src/test-fixtures.ts +1 -1
- package/src/token.test.ts +22 -1
- package/src/token.ts +8 -6
- package/src/twitch-client.test.ts +18 -25
- package/src/twitch-client.ts +5 -6
- package/src/types.ts +7 -46
- package/src/utils/twitch.ts +8 -2
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -21
- package/src/onboarding.test.ts +0 -316
- package/src/onboarding.ts +0 -417
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Twitch channel plugin for OpenClaw.
|
|
|
5
5
|
## Install (local checkout)
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
openclaw plugins install ./
|
|
8
|
+
openclaw plugins install ./path/to/local/twitch-plugin
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Install (npm)
|
|
@@ -31,7 +31,7 @@ Minimal config (simplified single-account):
|
|
|
31
31
|
accessToken: "oauth:abc123...", // OAuth Access Token (add oauth: prefix)
|
|
32
32
|
clientId: "xyz789...", // Client ID from Token Generator
|
|
33
33
|
channel: "vevisk", // Channel to join (required)
|
|
34
|
-
allowFrom: ["123456789"], // (recommended) Your Twitch user ID only (Convert your twitch username to ID at https://www.streamweasels.com/tools/convert-twitch-username
|
|
34
|
+
allowFrom: ["123456789"], // (recommended) Your Twitch user ID only (Convert your twitch username to ID at https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/)
|
|
35
35
|
},
|
|
36
36
|
},
|
|
37
37
|
}
|
package/api.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type ChannelAccountSnapshot,
|
|
3
|
+
type ChannelCapabilities,
|
|
4
|
+
type ChannelGatewayContext,
|
|
5
|
+
type ChannelLogSink,
|
|
6
|
+
type ChannelMessageActionAdapter,
|
|
7
|
+
type ChannelMessageActionContext,
|
|
8
|
+
type ChannelMeta,
|
|
9
|
+
type ChannelOutboundAdapter,
|
|
10
|
+
type ChannelOutboundContext,
|
|
11
|
+
type ChannelPlugin,
|
|
12
|
+
type ChannelResolveKind,
|
|
13
|
+
type ChannelResolveResult,
|
|
14
|
+
type ChannelStatusAdapter,
|
|
15
|
+
type OpenClawConfig,
|
|
16
|
+
type OutboundDeliveryResult,
|
|
17
|
+
type RuntimeEnv,
|
|
18
|
+
type WizardPrompter,
|
|
19
|
+
} from "./runtime-api.js";
|
|
20
|
+
export { twitchPlugin } from "./src/plugin.js";
|
|
21
|
+
export { setTwitchRuntime } from "./src/runtime.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { twitchPlugin } from "./src/plugin.js";
|
package/index.test.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { assertBundledChannelEntries } from "openclaw/plugin-sdk/channel-test-helpers";
|
|
2
|
+
import { describe } from "vitest";
|
|
3
|
+
import entry from "./index.js";
|
|
4
|
+
import setupEntry from "./setup-entry.js";
|
|
5
|
+
|
|
6
|
+
describe("twitch bundled entries", () => {
|
|
7
|
+
assertBundledChannelEntries({
|
|
8
|
+
entry,
|
|
9
|
+
expectedId: "twitch",
|
|
10
|
+
expectedName: "Twitch",
|
|
11
|
+
setupEntry,
|
|
12
|
+
});
|
|
13
|
+
});
|
package/index.ts
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
-
import { twitchPlugin } from "./src/plugin.js";
|
|
4
|
-
import { setTwitchRuntime } from "./src/runtime.js";
|
|
1
|
+
import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
|
|
5
2
|
|
|
6
|
-
export {
|
|
7
|
-
|
|
8
|
-
const plugin = {
|
|
3
|
+
export default defineBundledChannelEntry({
|
|
9
4
|
id: "twitch",
|
|
10
5
|
name: "Twitch",
|
|
11
|
-
description: "Twitch channel plugin",
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
api.registerChannel({ plugin: twitchPlugin as any });
|
|
6
|
+
description: "Twitch IRC chat channel plugin",
|
|
7
|
+
importMetaUrl: import.meta.url,
|
|
8
|
+
plugin: {
|
|
9
|
+
specifier: "./channel-plugin-api.js",
|
|
10
|
+
exportName: "twitchPlugin",
|
|
17
11
|
},
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
runtime: {
|
|
13
|
+
specifier: "./api.js",
|
|
14
|
+
exportName: "setTwitchRuntime",
|
|
15
|
+
},
|
|
16
|
+
});
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,20 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/twitch",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.5.1-beta.1",
|
|
4
4
|
"description": "OpenClaw Twitch channel plugin",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/openclaw/openclaw"
|
|
8
|
+
},
|
|
5
9
|
"type": "module",
|
|
6
10
|
"dependencies": {
|
|
7
|
-
"@twurple/api": "^8.
|
|
8
|
-
"@twurple/auth": "^8.
|
|
9
|
-
"@twurple/chat": "^8.
|
|
10
|
-
"zod": "^4.3.6"
|
|
11
|
+
"@twurple/api": "^8.1.3",
|
|
12
|
+
"@twurple/auth": "^8.1.3",
|
|
13
|
+
"@twurple/chat": "^8.1.3"
|
|
11
14
|
},
|
|
12
15
|
"devDependencies": {
|
|
13
|
-
"openclaw": "workspace:*"
|
|
16
|
+
"@openclaw/plugin-sdk": "workspace:*"
|
|
14
17
|
},
|
|
15
18
|
"openclaw": {
|
|
16
19
|
"extensions": [
|
|
17
20
|
"./index.ts"
|
|
18
|
-
]
|
|
21
|
+
],
|
|
22
|
+
"setupEntry": "./setup-entry.ts",
|
|
23
|
+
"install": {
|
|
24
|
+
"npmSpec": "@openclaw/twitch",
|
|
25
|
+
"defaultChoice": "npm",
|
|
26
|
+
"minHostVersion": ">=2026.4.10"
|
|
27
|
+
},
|
|
28
|
+
"channel": {
|
|
29
|
+
"id": "twitch",
|
|
30
|
+
"label": "Twitch",
|
|
31
|
+
"selectionLabel": "Twitch (Chat)",
|
|
32
|
+
"docsPath": "/channels/twitch",
|
|
33
|
+
"blurb": "Twitch chat integration",
|
|
34
|
+
"aliases": [
|
|
35
|
+
"twitch-chat"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"release": {
|
|
39
|
+
"publishToNpm": true
|
|
40
|
+
}
|
|
19
41
|
}
|
|
20
42
|
}
|
package/runtime-api.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Private runtime barrel for the bundled Twitch extension.
|
|
2
|
+
// Keep this barrel thin and aligned with the local extension surface.
|
|
3
|
+
|
|
4
|
+
export type {
|
|
5
|
+
ChannelAccountSnapshot,
|
|
6
|
+
ChannelCapabilities,
|
|
7
|
+
ChannelGatewayContext,
|
|
8
|
+
ChannelLogSink,
|
|
9
|
+
ChannelMessageActionAdapter,
|
|
10
|
+
ChannelMessageActionContext,
|
|
11
|
+
ChannelMeta,
|
|
12
|
+
ChannelOutboundAdapter,
|
|
13
|
+
ChannelOutboundContext,
|
|
14
|
+
ChannelResolveKind,
|
|
15
|
+
ChannelResolveResult,
|
|
16
|
+
ChannelStatusAdapter,
|
|
17
|
+
} from "openclaw/plugin-sdk/channel-contract";
|
|
18
|
+
export type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
19
|
+
export type { OutboundDeliveryResult } from "openclaw/plugin-sdk/channel-send-result";
|
|
20
|
+
export type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
|
21
|
+
export type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
|
|
22
|
+
export type { WizardPrompter } from "openclaw/plugin-sdk/setup";
|
package/setup-entry.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
});
|
|
@@ -17,16 +17,79 @@ describe("checkTwitchAccessControl", () => {
|
|
|
17
17
|
channel: "testchannel",
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
function runAccessCheck(params: {
|
|
21
|
+
account?: Partial<TwitchAccountConfig>;
|
|
22
|
+
message?: Partial<TwitchChatMessage>;
|
|
23
|
+
}) {
|
|
24
|
+
return checkTwitchAccessControl({
|
|
25
|
+
message: {
|
|
23
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: {
|
|
24
44
|
message: "@testbot hello",
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
},
|
|
30
93
|
});
|
|
31
94
|
expect(result.allowed).toBe(true);
|
|
32
95
|
});
|
|
@@ -34,30 +97,20 @@ describe("checkTwitchAccessControl", () => {
|
|
|
34
97
|
|
|
35
98
|
describe("requireMention default", () => {
|
|
36
99
|
it("defaults to true when undefined", () => {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const result = checkTwitchAccessControl({
|
|
43
|
-
message,
|
|
44
|
-
account: mockAccount,
|
|
45
|
-
botUsername: "testbot",
|
|
100
|
+
const result = runAccessCheck({
|
|
101
|
+
message: {
|
|
102
|
+
message: "hello bot",
|
|
103
|
+
},
|
|
46
104
|
});
|
|
47
105
|
expect(result.allowed).toBe(false);
|
|
48
106
|
expect(result.reason).toContain("does not mention the bot");
|
|
49
107
|
});
|
|
50
108
|
|
|
51
109
|
it("allows mention when requireMention is undefined", () => {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const result = checkTwitchAccessControl({
|
|
58
|
-
message,
|
|
59
|
-
account: mockAccount,
|
|
60
|
-
botUsername: "testbot",
|
|
110
|
+
const result = runAccessCheck({
|
|
111
|
+
message: {
|
|
112
|
+
message: "@testbot hello",
|
|
113
|
+
},
|
|
61
114
|
});
|
|
62
115
|
expect(result.allowed).toBe(true);
|
|
63
116
|
});
|
|
@@ -65,52 +118,25 @@ describe("checkTwitchAccessControl", () => {
|
|
|
65
118
|
|
|
66
119
|
describe("requireMention", () => {
|
|
67
120
|
it("allows messages that mention the bot", () => {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
};
|
|
72
|
-
const message: TwitchChatMessage = {
|
|
73
|
-
...mockMessage,
|
|
74
|
-
message: "@testbot hello",
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const result = checkTwitchAccessControl({
|
|
78
|
-
message,
|
|
79
|
-
account,
|
|
80
|
-
botUsername: "testbot",
|
|
121
|
+
const result = runAccessCheck({
|
|
122
|
+
account: { requireMention: true },
|
|
123
|
+
message: { message: "@testbot hello" },
|
|
81
124
|
});
|
|
82
125
|
expect(result.allowed).toBe(true);
|
|
83
126
|
});
|
|
84
127
|
|
|
85
128
|
it("blocks messages that don't mention the bot", () => {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
requireMention: true,
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const result = checkTwitchAccessControl({
|
|
92
|
-
message: mockMessage,
|
|
93
|
-
account,
|
|
94
|
-
botUsername: "testbot",
|
|
129
|
+
const result = runAccessCheck({
|
|
130
|
+
account: { requireMention: true },
|
|
95
131
|
});
|
|
96
132
|
expect(result.allowed).toBe(false);
|
|
97
133
|
expect(result.reason).toContain("does not mention the bot");
|
|
98
134
|
});
|
|
99
135
|
|
|
100
136
|
it("is case-insensitive for bot username", () => {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
};
|
|
105
|
-
const message: TwitchChatMessage = {
|
|
106
|
-
...mockMessage,
|
|
107
|
-
message: "@TestBot hello",
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const result = checkTwitchAccessControl({
|
|
111
|
-
message,
|
|
112
|
-
account,
|
|
113
|
-
botUsername: "testbot",
|
|
137
|
+
const result = runAccessCheck({
|
|
138
|
+
account: { requireMention: true },
|
|
139
|
+
message: { message: "@TestBot hello" },
|
|
114
140
|
});
|
|
115
141
|
expect(result.allowed).toBe(true);
|
|
116
142
|
});
|
|
@@ -118,62 +144,35 @@ describe("checkTwitchAccessControl", () => {
|
|
|
118
144
|
|
|
119
145
|
describe("allowFrom allowlist", () => {
|
|
120
146
|
it("allows users in the allowlist", () => {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const message: TwitchChatMessage = {
|
|
126
|
-
...mockMessage,
|
|
127
|
-
message: "@testbot hello",
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const result = checkTwitchAccessControl({
|
|
131
|
-
message,
|
|
132
|
-
account,
|
|
133
|
-
botUsername: "testbot",
|
|
147
|
+
const result = expectAllowedAccessCheck({
|
|
148
|
+
account: {
|
|
149
|
+
allowFrom: ["123456", "789012"],
|
|
150
|
+
},
|
|
134
151
|
});
|
|
135
|
-
expect(result.allowed).toBe(true);
|
|
136
152
|
expect(result.matchKey).toBe("123456");
|
|
137
153
|
expect(result.matchSource).toBe("allowlist");
|
|
138
154
|
});
|
|
139
155
|
|
|
140
156
|
it("blocks users not in allowlist when allowFrom is set", () => {
|
|
141
|
-
|
|
142
|
-
...mockAccount,
|
|
157
|
+
expectAllowFromBlocked({
|
|
143
158
|
allowFrom: ["789012"],
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
message: "@testbot hello",
|
|
148
|
-
};
|
|
159
|
+
reason: "allowFrom",
|
|
160
|
+
});
|
|
161
|
+
});
|
|
149
162
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
163
|
+
it("blocks everyone when allowFrom is explicitly empty", () => {
|
|
164
|
+
expectAllowFromBlocked({
|
|
165
|
+
allowFrom: [],
|
|
166
|
+
reason: "allowFrom",
|
|
154
167
|
});
|
|
155
|
-
expect(result.allowed).toBe(false);
|
|
156
|
-
expect(result.reason).toContain("allowFrom");
|
|
157
168
|
});
|
|
158
169
|
|
|
159
170
|
it("blocks messages without userId", () => {
|
|
160
|
-
|
|
161
|
-
...mockAccount,
|
|
171
|
+
expectAllowFromBlocked({
|
|
162
172
|
allowFrom: ["123456"],
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
...mockMessage,
|
|
166
|
-
message: "@testbot hello",
|
|
167
|
-
userId: undefined,
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const result = checkTwitchAccessControl({
|
|
171
|
-
message,
|
|
172
|
-
account,
|
|
173
|
-
botUsername: "testbot",
|
|
173
|
+
message: { userId: undefined },
|
|
174
|
+
reason: "user ID not available",
|
|
174
175
|
});
|
|
175
|
-
expect(result.allowed).toBe(false);
|
|
176
|
-
expect(result.reason).toContain("user ID not available");
|
|
177
176
|
});
|
|
178
177
|
|
|
179
178
|
it("bypasses role checks when user is in allowlist", () => {
|
|
@@ -197,68 +196,30 @@ describe("checkTwitchAccessControl", () => {
|
|
|
197
196
|
});
|
|
198
197
|
|
|
199
198
|
it("blocks user with role when not in allowlist", () => {
|
|
200
|
-
|
|
201
|
-
...mockAccount,
|
|
199
|
+
expectAllowFromBlocked({
|
|
202
200
|
allowFrom: ["789012"],
|
|
203
201
|
allowedRoles: ["moderator"],
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
...mockMessage,
|
|
207
|
-
message: "@testbot hello",
|
|
208
|
-
userId: "123456",
|
|
209
|
-
isMod: true,
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const result = checkTwitchAccessControl({
|
|
213
|
-
message,
|
|
214
|
-
account,
|
|
215
|
-
botUsername: "testbot",
|
|
202
|
+
message: { userId: "123456", isMod: true },
|
|
203
|
+
reason: "allowFrom",
|
|
216
204
|
});
|
|
217
|
-
expect(result.allowed).toBe(false);
|
|
218
|
-
expect(result.reason).toContain("allowFrom");
|
|
219
205
|
});
|
|
220
206
|
|
|
221
207
|
it("blocks user not in allowlist even when roles configured", () => {
|
|
222
|
-
|
|
223
|
-
...mockAccount,
|
|
208
|
+
expectAllowFromBlocked({
|
|
224
209
|
allowFrom: ["789012"],
|
|
225
210
|
allowedRoles: ["moderator"],
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
...mockMessage,
|
|
229
|
-
message: "@testbot hello",
|
|
230
|
-
userId: "123456",
|
|
231
|
-
isMod: false,
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
const result = checkTwitchAccessControl({
|
|
235
|
-
message,
|
|
236
|
-
account,
|
|
237
|
-
botUsername: "testbot",
|
|
211
|
+
message: { userId: "123456", isMod: false },
|
|
212
|
+
reason: "allowFrom",
|
|
238
213
|
});
|
|
239
|
-
expect(result.allowed).toBe(false);
|
|
240
|
-
expect(result.reason).toContain("allowFrom");
|
|
241
214
|
});
|
|
242
215
|
});
|
|
243
216
|
|
|
244
217
|
describe("allowedRoles", () => {
|
|
245
218
|
it("allows users with matching role", () => {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
};
|
|
250
|
-
const message: TwitchChatMessage = {
|
|
251
|
-
...mockMessage,
|
|
252
|
-
message: "@testbot hello",
|
|
253
|
-
isMod: true,
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const result = checkTwitchAccessControl({
|
|
257
|
-
message,
|
|
258
|
-
account,
|
|
259
|
-
botUsername: "testbot",
|
|
219
|
+
const result = expectSingleRoleAllowed({
|
|
220
|
+
role: "moderator",
|
|
221
|
+
message: { isMod: true },
|
|
260
222
|
});
|
|
261
|
-
expect(result.allowed).toBe(true);
|
|
262
223
|
expect(result.matchSource).toBe("role");
|
|
263
224
|
});
|
|
264
225
|
|
|
@@ -304,98 +265,40 @@ describe("checkTwitchAccessControl", () => {
|
|
|
304
265
|
});
|
|
305
266
|
|
|
306
267
|
it("allows all users when role is 'all'", () => {
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const message: TwitchChatMessage = {
|
|
312
|
-
...mockMessage,
|
|
313
|
-
message: "@testbot hello",
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
const result = checkTwitchAccessControl({
|
|
317
|
-
message,
|
|
318
|
-
account,
|
|
319
|
-
botUsername: "testbot",
|
|
268
|
+
const result = expectAllowedAccessCheck({
|
|
269
|
+
account: {
|
|
270
|
+
allowedRoles: ["all"],
|
|
271
|
+
},
|
|
320
272
|
});
|
|
321
|
-
expect(result.allowed).toBe(true);
|
|
322
273
|
expect(result.matchKey).toBe("all");
|
|
323
274
|
});
|
|
324
275
|
|
|
325
276
|
it("handles moderator role", () => {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
};
|
|
330
|
-
const message: TwitchChatMessage = {
|
|
331
|
-
...mockMessage,
|
|
332
|
-
message: "@testbot hello",
|
|
333
|
-
isMod: true,
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
const result = checkTwitchAccessControl({
|
|
337
|
-
message,
|
|
338
|
-
account,
|
|
339
|
-
botUsername: "testbot",
|
|
277
|
+
expectSingleRoleAllowed({
|
|
278
|
+
role: "moderator",
|
|
279
|
+
message: { isMod: true },
|
|
340
280
|
});
|
|
341
|
-
expect(result.allowed).toBe(true);
|
|
342
281
|
});
|
|
343
282
|
|
|
344
283
|
it("handles subscriber role", () => {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
};
|
|
349
|
-
const message: TwitchChatMessage = {
|
|
350
|
-
...mockMessage,
|
|
351
|
-
message: "@testbot hello",
|
|
352
|
-
isSub: true,
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
const result = checkTwitchAccessControl({
|
|
356
|
-
message,
|
|
357
|
-
account,
|
|
358
|
-
botUsername: "testbot",
|
|
284
|
+
expectSingleRoleAllowed({
|
|
285
|
+
role: "subscriber",
|
|
286
|
+
message: { isSub: true },
|
|
359
287
|
});
|
|
360
|
-
expect(result.allowed).toBe(true);
|
|
361
288
|
});
|
|
362
289
|
|
|
363
290
|
it("handles owner role", () => {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
};
|
|
368
|
-
const message: TwitchChatMessage = {
|
|
369
|
-
...mockMessage,
|
|
370
|
-
message: "@testbot hello",
|
|
371
|
-
isOwner: true,
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
const result = checkTwitchAccessControl({
|
|
375
|
-
message,
|
|
376
|
-
account,
|
|
377
|
-
botUsername: "testbot",
|
|
291
|
+
expectSingleRoleAllowed({
|
|
292
|
+
role: "owner",
|
|
293
|
+
message: { isOwner: true },
|
|
378
294
|
});
|
|
379
|
-
expect(result.allowed).toBe(true);
|
|
380
295
|
});
|
|
381
296
|
|
|
382
297
|
it("handles vip role", () => {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
};
|
|
387
|
-
const message: TwitchChatMessage = {
|
|
388
|
-
...mockMessage,
|
|
389
|
-
message: "@testbot hello",
|
|
390
|
-
isVip: true,
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
const result = checkTwitchAccessControl({
|
|
394
|
-
message,
|
|
395
|
-
account,
|
|
396
|
-
botUsername: "testbot",
|
|
298
|
+
expectSingleRoleAllowed({
|
|
299
|
+
role: "vip",
|
|
300
|
+
message: { isVip: true },
|
|
397
301
|
});
|
|
398
|
-
expect(result.allowed).toBe(true);
|
|
399
302
|
});
|
|
400
303
|
});
|
|
401
304
|
|
|
@@ -421,21 +324,15 @@ describe("checkTwitchAccessControl", () => {
|
|
|
421
324
|
});
|
|
422
325
|
|
|
423
326
|
it("checks allowlist before allowedRoles", () => {
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
const result = checkTwitchAccessControl({
|
|
436
|
-
message,
|
|
437
|
-
account,
|
|
438
|
-
botUsername: "testbot",
|
|
327
|
+
const result = runAccessCheck({
|
|
328
|
+
account: {
|
|
329
|
+
allowFrom: ["123456"],
|
|
330
|
+
allowedRoles: ["owner"],
|
|
331
|
+
},
|
|
332
|
+
message: {
|
|
333
|
+
message: "@testbot hello",
|
|
334
|
+
isOwner: false,
|
|
335
|
+
},
|
|
439
336
|
});
|
|
440
337
|
expect(result.allowed).toBe(true);
|
|
441
338
|
expect(result.matchSource).toBe("allowlist");
|