@openclaw/twitch 2026.2.21 → 2026.5.2-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 +224 -1
- package/package.json +36 -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.test.ts +46 -0
- package/src/config-schema.ts +25 -21
- 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/src/plugin.ts
CHANGED
|
@@ -5,23 +5,37 @@
|
|
|
5
5
|
* This is the primary entry point for the Twitch channel integration.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
import { buildChannelConfigSchema } from "openclaw/plugin-sdk";
|
|
8
|
+
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
|
9
|
+
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
|
|
10
|
+
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
11
|
+
import {
|
|
12
|
+
createLoggedPairingApprovalNotifier,
|
|
13
|
+
createPairingPrefixStripper,
|
|
14
|
+
} from "openclaw/plugin-sdk/channel-pairing";
|
|
15
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
|
16
|
+
import { buildPassiveProbedChannelStatusSummary } from "openclaw/plugin-sdk/extension-shared";
|
|
17
|
+
import {
|
|
18
|
+
createComputedAccountStatusAdapter,
|
|
19
|
+
createDefaultChannelRuntimeState,
|
|
20
|
+
} from "openclaw/plugin-sdk/status-helpers";
|
|
10
21
|
import { twitchMessageActions } from "./actions.js";
|
|
11
22
|
import { removeClientManager } from "./client-manager-registry.js";
|
|
12
23
|
import { TwitchConfigSchema } from "./config-schema.js";
|
|
13
|
-
import {
|
|
14
|
-
|
|
24
|
+
import {
|
|
25
|
+
DEFAULT_ACCOUNT_ID,
|
|
26
|
+
getAccountConfig,
|
|
27
|
+
listAccountIds,
|
|
28
|
+
resolveDefaultTwitchAccountId,
|
|
29
|
+
resolveTwitchAccountContext,
|
|
30
|
+
resolveTwitchSnapshotAccountId,
|
|
31
|
+
} from "./config.js";
|
|
15
32
|
import { twitchOutbound } from "./outbound.js";
|
|
16
33
|
import { probeTwitch } from "./probe.js";
|
|
17
34
|
import { resolveTwitchTargets } from "./resolver.js";
|
|
35
|
+
import { twitchSetupAdapter, twitchSetupWizard } from "./setup-surface.js";
|
|
18
36
|
import { collectTwitchStatusIssues } from "./status.js";
|
|
19
|
-
import { resolveTwitchToken } from "./token.js";
|
|
20
37
|
import type {
|
|
21
|
-
ChannelAccountSnapshot,
|
|
22
|
-
ChannelCapabilities,
|
|
23
38
|
ChannelLogSink,
|
|
24
|
-
ChannelMeta,
|
|
25
39
|
ChannelPlugin,
|
|
26
40
|
ChannelResolveKind,
|
|
27
41
|
ChannelResolveResult,
|
|
@@ -29,6 +43,8 @@ import type {
|
|
|
29
43
|
} from "./types.js";
|
|
30
44
|
import { isAccountConfigured } from "./utils/twitch.js";
|
|
31
45
|
|
|
46
|
+
type ResolvedTwitchAccount = TwitchAccountConfig & { accountId?: string | null };
|
|
47
|
+
|
|
32
48
|
/**
|
|
33
49
|
* Twitch channel plugin.
|
|
34
50
|
*
|
|
@@ -36,239 +52,157 @@ import { isAccountConfigured } from "./utils/twitch.js";
|
|
|
36
52
|
* for OpenClaw. Supports message sending, receiving, access control, and
|
|
37
53
|
* status monitoring.
|
|
38
54
|
*/
|
|
39
|
-
export const twitchPlugin: ChannelPlugin<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
docsPath: "/channels/twitch",
|
|
49
|
-
blurb: "Twitch chat integration",
|
|
50
|
-
aliases: ["twitch-chat"],
|
|
51
|
-
} satisfies ChannelMeta,
|
|
52
|
-
|
|
53
|
-
/** Onboarding adapter */
|
|
54
|
-
onboarding: twitchOnboardingAdapter,
|
|
55
|
-
|
|
56
|
-
/** Pairing configuration */
|
|
57
|
-
pairing: {
|
|
58
|
-
idLabel: "twitchUserId",
|
|
59
|
-
normalizeAllowEntry: (entry) => entry.replace(/^(twitch:)?user:?/i, ""),
|
|
60
|
-
notifyApproval: async ({ id }) => {
|
|
61
|
-
// Note: Twitch doesn't support DMs from bots, so pairing approval is limited
|
|
62
|
-
// We'll log the approval instead
|
|
63
|
-
console.warn(`Pairing approved for user ${id} (notification sent via chat if possible)`);
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
/** Supported chat capabilities */
|
|
68
|
-
capabilities: {
|
|
69
|
-
chatTypes: ["group"],
|
|
70
|
-
} satisfies ChannelCapabilities,
|
|
71
|
-
|
|
72
|
-
/** Configuration schema for Twitch channel */
|
|
73
|
-
configSchema: buildChannelConfigSchema(TwitchConfigSchema),
|
|
74
|
-
|
|
75
|
-
/** Account configuration management */
|
|
76
|
-
config: {
|
|
77
|
-
/** List all configured account IDs */
|
|
78
|
-
listAccountIds: (cfg: OpenClawConfig): string[] => listAccountIds(cfg),
|
|
79
|
-
|
|
80
|
-
/** Resolve an account config by ID */
|
|
81
|
-
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null): TwitchAccountConfig => {
|
|
82
|
-
const account = getAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
|
|
83
|
-
if (!account) {
|
|
84
|
-
// Return a default/empty account if not configured
|
|
85
|
-
return {
|
|
86
|
-
username: "",
|
|
87
|
-
accessToken: "",
|
|
88
|
-
clientId: "",
|
|
89
|
-
enabled: false,
|
|
90
|
-
} as TwitchAccountConfig;
|
|
91
|
-
}
|
|
92
|
-
return account;
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
/** Get the default account ID */
|
|
96
|
-
defaultAccountId: (): string => DEFAULT_ACCOUNT_ID,
|
|
97
|
-
|
|
98
|
-
/** Check if an account is configured */
|
|
99
|
-
isConfigured: (_account: unknown, cfg: OpenClawConfig): boolean => {
|
|
100
|
-
const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID);
|
|
101
|
-
const tokenResolution = resolveTwitchToken(cfg, { accountId: DEFAULT_ACCOUNT_ID });
|
|
102
|
-
return account ? isAccountConfigured(account, tokenResolution.token) : false;
|
|
103
|
-
},
|
|
104
|
-
|
|
105
|
-
/** Check if an account is enabled */
|
|
106
|
-
isEnabled: (account: TwitchAccountConfig | undefined): boolean => account?.enabled !== false,
|
|
107
|
-
|
|
108
|
-
/** Describe account status */
|
|
109
|
-
describeAccount: (account: TwitchAccountConfig | undefined) => {
|
|
110
|
-
return {
|
|
111
|
-
accountId: DEFAULT_ACCOUNT_ID,
|
|
112
|
-
enabled: account?.enabled !== false,
|
|
113
|
-
configured: account ? isAccountConfigured(account, account?.accessToken) : false,
|
|
114
|
-
};
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
/** Outbound message adapter */
|
|
119
|
-
outbound: twitchOutbound,
|
|
120
|
-
|
|
121
|
-
/** Message actions adapter */
|
|
122
|
-
actions: twitchMessageActions,
|
|
123
|
-
|
|
124
|
-
/** Resolver adapter for username -> user ID resolution */
|
|
125
|
-
resolver: {
|
|
126
|
-
resolveTargets: async ({
|
|
127
|
-
cfg,
|
|
128
|
-
accountId,
|
|
129
|
-
inputs,
|
|
130
|
-
kind,
|
|
131
|
-
runtime,
|
|
132
|
-
}: {
|
|
133
|
-
cfg: OpenClawConfig;
|
|
134
|
-
accountId?: string | null;
|
|
135
|
-
inputs: string[];
|
|
136
|
-
kind: ChannelResolveKind;
|
|
137
|
-
runtime: import("../../../src/runtime.js").RuntimeEnv;
|
|
138
|
-
}): Promise<ChannelResolveResult[]> => {
|
|
139
|
-
const account = getAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
|
|
140
|
-
|
|
141
|
-
if (!account) {
|
|
142
|
-
return inputs.map((input) => ({
|
|
143
|
-
input,
|
|
144
|
-
resolved: false,
|
|
145
|
-
note: "account not configured",
|
|
146
|
-
}));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Adapt RuntimeEnv.log to ChannelLogSink
|
|
150
|
-
const log: ChannelLogSink = {
|
|
151
|
-
info: (msg) => runtime.log(msg),
|
|
152
|
-
warn: (msg) => runtime.log(msg),
|
|
153
|
-
error: (msg) => runtime.error(msg),
|
|
154
|
-
debug: (msg) => runtime.log(msg),
|
|
155
|
-
};
|
|
156
|
-
return await resolveTwitchTargets(inputs, account, kind, log);
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
|
|
160
|
-
/** Status monitoring adapter */
|
|
161
|
-
status: {
|
|
162
|
-
/** Default runtime state */
|
|
163
|
-
defaultRuntime: {
|
|
164
|
-
accountId: DEFAULT_ACCOUNT_ID,
|
|
165
|
-
running: false,
|
|
166
|
-
lastStartAt: null,
|
|
167
|
-
lastStopAt: null,
|
|
168
|
-
lastError: null,
|
|
169
|
-
},
|
|
170
|
-
|
|
171
|
-
/** Build channel summary from snapshot */
|
|
172
|
-
buildChannelSummary: ({ snapshot }: { snapshot: ChannelAccountSnapshot }) => ({
|
|
173
|
-
configured: snapshot.configured ?? false,
|
|
174
|
-
running: snapshot.running ?? false,
|
|
175
|
-
lastStartAt: snapshot.lastStartAt ?? null,
|
|
176
|
-
lastStopAt: snapshot.lastStopAt ?? null,
|
|
177
|
-
lastError: snapshot.lastError ?? null,
|
|
178
|
-
probe: snapshot.probe,
|
|
179
|
-
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
180
|
-
}),
|
|
181
|
-
|
|
182
|
-
/** Probe account connection */
|
|
183
|
-
probeAccount: async ({
|
|
184
|
-
account,
|
|
185
|
-
timeoutMs,
|
|
186
|
-
}: {
|
|
187
|
-
account: TwitchAccountConfig;
|
|
188
|
-
timeoutMs: number;
|
|
189
|
-
}): Promise<unknown> => {
|
|
190
|
-
return await probeTwitch(account, timeoutMs);
|
|
55
|
+
export const twitchPlugin: ChannelPlugin<ResolvedTwitchAccount> =
|
|
56
|
+
createChatChannelPlugin<ResolvedTwitchAccount>({
|
|
57
|
+
pairing: {
|
|
58
|
+
idLabel: "twitchUserId",
|
|
59
|
+
normalizeAllowEntry: createPairingPrefixStripper(/^(twitch:)?user:?/i),
|
|
60
|
+
notifyApproval: createLoggedPairingApprovalNotifier(
|
|
61
|
+
({ id }) => `Pairing approved for user ${id} (notification sent via chat if possible)`,
|
|
62
|
+
console.warn,
|
|
63
|
+
),
|
|
191
64
|
},
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
65
|
+
outbound: twitchOutbound,
|
|
66
|
+
base: {
|
|
67
|
+
id: "twitch",
|
|
68
|
+
meta: {
|
|
69
|
+
id: "twitch",
|
|
70
|
+
label: "Twitch",
|
|
71
|
+
selectionLabel: "Twitch (Chat)",
|
|
72
|
+
docsPath: "/channels/twitch",
|
|
73
|
+
blurb: "Twitch chat integration",
|
|
74
|
+
aliases: ["twitch-chat"],
|
|
75
|
+
},
|
|
76
|
+
setup: twitchSetupAdapter,
|
|
77
|
+
setupWizard: twitchSetupWizard,
|
|
78
|
+
capabilities: {
|
|
79
|
+
chatTypes: ["group"],
|
|
80
|
+
},
|
|
81
|
+
configSchema: buildChannelConfigSchema(TwitchConfigSchema),
|
|
82
|
+
config: {
|
|
83
|
+
listAccountIds: (cfg: OpenClawConfig): string[] => listAccountIds(cfg),
|
|
84
|
+
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null): ResolvedTwitchAccount => {
|
|
85
|
+
const resolvedAccountId = accountId ?? resolveDefaultTwitchAccountId(cfg);
|
|
86
|
+
const account = getAccountConfig(cfg, resolvedAccountId);
|
|
87
|
+
if (!account) {
|
|
88
|
+
return {
|
|
89
|
+
accountId: resolvedAccountId,
|
|
90
|
+
channel: "",
|
|
91
|
+
username: "",
|
|
92
|
+
accessToken: "",
|
|
93
|
+
clientId: "",
|
|
94
|
+
enabled: false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
accountId: resolvedAccountId,
|
|
99
|
+
...account,
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
defaultAccountId: (cfg: OpenClawConfig): string => resolveDefaultTwitchAccountId(cfg),
|
|
103
|
+
isConfigured: (_account: unknown, cfg: OpenClawConfig): boolean =>
|
|
104
|
+
resolveTwitchAccountContext(cfg).configured,
|
|
105
|
+
isEnabled: (account: ResolvedTwitchAccount | undefined): boolean =>
|
|
106
|
+
account?.enabled !== false,
|
|
107
|
+
describeAccount: (account: TwitchAccountConfig | undefined) =>
|
|
108
|
+
account
|
|
109
|
+
? describeAccountSnapshot({
|
|
110
|
+
account,
|
|
111
|
+
configured: isAccountConfigured(account, account.accessToken),
|
|
112
|
+
})
|
|
113
|
+
: {
|
|
114
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
115
|
+
enabled: false,
|
|
116
|
+
configured: false,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
actions: twitchMessageActions,
|
|
120
|
+
resolver: {
|
|
121
|
+
resolveTargets: async ({
|
|
122
|
+
cfg,
|
|
123
|
+
accountId,
|
|
124
|
+
inputs,
|
|
125
|
+
kind,
|
|
126
|
+
runtime,
|
|
127
|
+
}: {
|
|
128
|
+
cfg: OpenClawConfig;
|
|
129
|
+
accountId?: string | null;
|
|
130
|
+
inputs: string[];
|
|
131
|
+
kind: ChannelResolveKind;
|
|
132
|
+
runtime: import("openclaw/plugin-sdk/runtime-env").RuntimeEnv;
|
|
133
|
+
}): Promise<ChannelResolveResult[]> => {
|
|
134
|
+
const account = getAccountConfig(cfg, accountId ?? resolveDefaultTwitchAccountId(cfg));
|
|
135
|
+
if (!account) {
|
|
136
|
+
return inputs.map((input) => ({
|
|
137
|
+
input,
|
|
138
|
+
resolved: false,
|
|
139
|
+
note: "account not configured",
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const log: ChannelLogSink = {
|
|
144
|
+
info: (msg) => runtime.log(msg),
|
|
145
|
+
warn: (msg) => runtime.log(msg),
|
|
146
|
+
error: (msg) => runtime.error(msg),
|
|
147
|
+
debug: (msg) => runtime.log(msg),
|
|
148
|
+
};
|
|
149
|
+
return await resolveTwitchTargets(inputs, account, kind, log);
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
status: createComputedAccountStatusAdapter<ResolvedTwitchAccount>({
|
|
153
|
+
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
|
|
154
|
+
buildChannelSummary: ({ snapshot }) => buildPassiveProbedChannelStatusSummary(snapshot),
|
|
155
|
+
probeAccount: async ({ account, timeoutMs }) => await probeTwitch(account, timeoutMs),
|
|
156
|
+
collectStatusIssues: collectTwitchStatusIssues,
|
|
157
|
+
resolveAccountSnapshot: ({ account, cfg }) => {
|
|
158
|
+
const resolvedAccountId =
|
|
159
|
+
account.accountId || resolveTwitchSnapshotAccountId(cfg, account);
|
|
160
|
+
const { configured } = resolveTwitchAccountContext(cfg, resolvedAccountId);
|
|
161
|
+
return {
|
|
162
|
+
accountId: resolvedAccountId,
|
|
163
|
+
enabled: account.enabled !== false,
|
|
164
|
+
configured,
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
gateway: {
|
|
169
|
+
startAccount: async (ctx): Promise<void> => {
|
|
170
|
+
const account = ctx.account;
|
|
171
|
+
const accountId = ctx.accountId;
|
|
172
|
+
|
|
173
|
+
ctx.setStatus?.({
|
|
174
|
+
accountId,
|
|
175
|
+
running: true,
|
|
176
|
+
lastStartAt: Date.now(),
|
|
177
|
+
lastError: null,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
ctx.log?.info(`Starting Twitch connection for ${account.username}`);
|
|
181
|
+
|
|
182
|
+
// Lazy import: the monitor pulls the reply pipeline; avoid ESM init cycles.
|
|
183
|
+
const { monitorTwitchProvider } = await import("./monitor.js");
|
|
184
|
+
await monitorTwitchProvider({
|
|
185
|
+
account,
|
|
186
|
+
accountId,
|
|
187
|
+
config: ctx.cfg,
|
|
188
|
+
runtime: ctx.runtime,
|
|
189
|
+
abortSignal: ctx.abortSignal,
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
stopAccount: async (ctx): Promise<void> => {
|
|
193
|
+
const account = ctx.account;
|
|
194
|
+
const accountId = ctx.accountId;
|
|
195
|
+
|
|
196
|
+
await removeClientManager(accountId);
|
|
197
|
+
|
|
198
|
+
ctx.setStatus?.({
|
|
199
|
+
accountId,
|
|
200
|
+
running: false,
|
|
201
|
+
lastStopAt: Date.now(),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
ctx.log?.info(`Stopped Twitch connection for ${account.username}`);
|
|
205
|
+
},
|
|
206
|
+
},
|
|
272
207
|
},
|
|
273
|
-
}
|
|
274
|
-
};
|
|
208
|
+
});
|
package/src/probe.test.ts
CHANGED
package/src/probe.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { StaticAuthProvider } from "@twurple/auth";
|
|
2
2
|
import { ChatClient } from "@twurple/chat";
|
|
3
|
-
import type { BaseProbeResult } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { BaseProbeResult } from "openclaw/plugin-sdk/channel-contract";
|
|
4
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
4
5
|
import type { TwitchAccountConfig } from "./types.js";
|
|
5
6
|
import { normalizeToken } from "./utils/twitch.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Result of probing a Twitch account
|
|
9
10
|
*/
|
|
10
|
-
|
|
11
|
+
type ProbeTwitchResult = BaseProbeResult<string> & {
|
|
11
12
|
username?: string;
|
|
12
13
|
elapsedMs: number;
|
|
13
14
|
connected?: boolean;
|
|
@@ -82,12 +83,22 @@ export async function probeTwitch(
|
|
|
82
83
|
});
|
|
83
84
|
});
|
|
84
85
|
|
|
86
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
85
87
|
const timeout = new Promise<never>((_, reject) => {
|
|
86
|
-
|
|
88
|
+
timeoutHandle = setTimeout(
|
|
89
|
+
() => reject(new Error(`timeout after ${timeoutMs}ms`)),
|
|
90
|
+
timeoutMs,
|
|
91
|
+
);
|
|
87
92
|
});
|
|
88
93
|
|
|
89
94
|
client.connect();
|
|
90
|
-
|
|
95
|
+
try {
|
|
96
|
+
await Promise.race([connectionPromise, timeout]);
|
|
97
|
+
} finally {
|
|
98
|
+
if (timeoutHandle) {
|
|
99
|
+
clearTimeout(timeoutHandle);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
91
102
|
|
|
92
103
|
client.quit();
|
|
93
104
|
client = undefined;
|
|
@@ -102,7 +113,7 @@ export async function probeTwitch(
|
|
|
102
113
|
} catch (error) {
|
|
103
114
|
return {
|
|
104
115
|
ok: false,
|
|
105
|
-
error:
|
|
116
|
+
error: formatErrorMessage(error),
|
|
106
117
|
username: account.username,
|
|
107
118
|
channel: account.channel,
|
|
108
119
|
elapsedMs: Date.now() - started,
|
package/src/resolver.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { ApiClient } from "@twurple/api";
|
|
9
9
|
import { StaticAuthProvider } from "@twurple/auth";
|
|
10
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
11
|
+
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
10
12
|
import type { ChannelResolveKind, ChannelResolveResult } from "./types.js";
|
|
11
13
|
import type { ChannelLogSink, TwitchAccountConfig } from "./types.js";
|
|
12
14
|
import { normalizeToken } from "./utils/twitch.js";
|
|
@@ -17,9 +19,9 @@ import { normalizeToken } from "./utils/twitch.js";
|
|
|
17
19
|
function normalizeUsername(input: string): string {
|
|
18
20
|
const trimmed = input.trim();
|
|
19
21
|
if (trimmed.startsWith("@")) {
|
|
20
|
-
return trimmed.slice(1)
|
|
22
|
+
return normalizeLowercaseStringOrEmpty(trimmed.slice(1));
|
|
21
23
|
}
|
|
22
|
-
return trimmed
|
|
24
|
+
return normalizeLowercaseStringOrEmpty(trimmed);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -123,7 +125,7 @@ export async function resolveTwitchTargets(
|
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
} catch (error) {
|
|
126
|
-
const errorMessage =
|
|
128
|
+
const errorMessage = formatErrorMessage(error);
|
|
127
129
|
results.push({
|
|
128
130
|
input,
|
|
129
131
|
resolved: false,
|
package/src/runtime.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getTwitchRuntime(): PluginRuntime {
|
|
10
|
-
if (!runtime) {
|
|
11
|
-
throw new Error("Twitch runtime not initialized");
|
|
12
|
-
}
|
|
13
|
-
return runtime;
|
|
14
|
-
}
|
|
4
|
+
const { setRuntime: setTwitchRuntime, getRuntime: getTwitchRuntime } =
|
|
5
|
+
createPluginRuntimeStore<PluginRuntime>({
|
|
6
|
+
pluginId: "twitch",
|
|
7
|
+
errorMessage: "Twitch runtime not initialized",
|
|
8
|
+
});
|
|
9
|
+
export { getTwitchRuntime, setTwitchRuntime };
|