@openclaw/twitch 2026.5.14-beta.1 → 2026.5.14-beta.2
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 +2 -2
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{monitor-BQ5mdzvw.js → monitor-DAM8hrYU.js} +2 -2
- package/dist/{plugin-CxTwdtw4.js → plugin-6H4HbNEm.js} +306 -7
- package/dist/setup-plugin-api.js +1 -1
- package/dist/{setup-surface-C1-HFfeJ.js → setup-surface-B7FlQUTK.js} +129 -3
- package/package.json +4 -4
- package/dist/markdown-BbzAc3z0.js +0 -306
- package/dist/twitch-BsSKTyqW.js +0 -129
- /package/dist/{runtime-QZ5I3GlI.js → runtime-C5O5cqdz.js} +0 -0
package/dist/api.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as twitchPlugin } from "./plugin-
|
|
2
|
-
import { n as setTwitchRuntime } from "./runtime-
|
|
1
|
+
import { t as twitchPlugin } from "./plugin-6H4HbNEm.js";
|
|
2
|
+
import { n as setTwitchRuntime } from "./runtime-C5O5cqdz.js";
|
|
3
3
|
export { setTwitchRuntime, twitchPlugin };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as twitchPlugin } from "./plugin-
|
|
1
|
+
import { t as twitchPlugin } from "./plugin-6H4HbNEm.js";
|
|
2
2
|
export { twitchPlugin };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as getTwitchRuntime } from "./runtime-
|
|
1
|
+
import { n as stripMarkdownForTwitch, r as getOrCreateClientManager } from "./plugin-6H4HbNEm.js";
|
|
2
|
+
import { t as getTwitchRuntime } from "./runtime-C5O5cqdz.js";
|
|
3
3
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
4
4
|
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
5
5
|
import { createChannelIngressResolver, defineStableChannelIngressIdentity } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { a as getAccountConfig, c as resolveTwitchAccountContext, i as DEFAULT_ACCOUNT_ID, l as resolveTwitchSnapshotAccountId, o as listAccountIds, r as twitchSetupWizard, s as resolveDefaultTwitchAccountId, t as twitchSetupAdapter } from "./setup-surface-C1-HFfeJ.js";
|
|
3
|
-
import { a as removeClientManager, n as stripMarkdownForTwitch, r as getClientManager, t as chunkTextForTwitch } from "./markdown-BbzAc3z0.js";
|
|
1
|
+
import { a as getAccountConfig, c as resolveTwitchAccountContext, d as isAccountConfigured, f as missingTargetError, h as resolveTwitchToken, i as DEFAULT_ACCOUNT_ID, l as resolveTwitchSnapshotAccountId, m as normalizeTwitchChannel, o as listAccountIds, p as normalizeToken, r as twitchSetupWizard, s as resolveDefaultTwitchAccountId, t as twitchSetupAdapter, u as generateMessageId } from "./setup-surface-B7FlQUTK.js";
|
|
4
2
|
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
|
5
3
|
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
|
|
6
4
|
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
@@ -10,11 +8,312 @@ import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState }
|
|
|
10
8
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
11
9
|
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
12
10
|
import { createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-message";
|
|
13
|
-
import { StaticAuthProvider } from "@twurple/auth";
|
|
14
|
-
import { ChatClient } from "@twurple/chat";
|
|
11
|
+
import { RefreshingAuthProvider, StaticAuthProvider } from "@twurple/auth";
|
|
12
|
+
import { ChatClient, LogLevel } from "@twurple/chat";
|
|
15
13
|
import { MarkdownConfigSchema } from "openclaw/plugin-sdk/channel-config-primitives";
|
|
16
14
|
import { z } from "zod";
|
|
17
15
|
import { ApiClient } from "@twurple/api";
|
|
16
|
+
//#region extensions/twitch/src/twitch-client.ts
|
|
17
|
+
/**
|
|
18
|
+
* Manages Twitch chat client connections
|
|
19
|
+
*/
|
|
20
|
+
var TwitchClientManager = class {
|
|
21
|
+
constructor(logger) {
|
|
22
|
+
this.logger = logger;
|
|
23
|
+
this.clients = /* @__PURE__ */ new Map();
|
|
24
|
+
this.messageHandlers = /* @__PURE__ */ new Map();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create an auth provider for the account.
|
|
28
|
+
*/
|
|
29
|
+
async createAuthProvider(account, normalizedToken) {
|
|
30
|
+
if (!account.clientId) throw new Error("Missing Twitch client ID");
|
|
31
|
+
if (account.clientSecret) {
|
|
32
|
+
const authProvider = new RefreshingAuthProvider({
|
|
33
|
+
clientId: account.clientId,
|
|
34
|
+
clientSecret: account.clientSecret
|
|
35
|
+
});
|
|
36
|
+
await authProvider.addUserForToken({
|
|
37
|
+
accessToken: normalizedToken,
|
|
38
|
+
refreshToken: account.refreshToken ?? null,
|
|
39
|
+
expiresIn: account.expiresIn ?? null,
|
|
40
|
+
obtainmentTimestamp: account.obtainmentTimestamp ?? Date.now()
|
|
41
|
+
}).then((userId) => {
|
|
42
|
+
this.logger.info(`Added user ${userId} to RefreshingAuthProvider for ${account.username}`);
|
|
43
|
+
}).catch((err) => {
|
|
44
|
+
this.logger.error(`Failed to add user to RefreshingAuthProvider: ${formatErrorMessage(err)}`);
|
|
45
|
+
});
|
|
46
|
+
authProvider.onRefresh((userId, token) => {
|
|
47
|
+
this.logger.info(`Access token refreshed for user ${userId} (expires in ${token.expiresIn ? `${token.expiresIn}s` : "unknown"})`);
|
|
48
|
+
});
|
|
49
|
+
authProvider.onRefreshFailure((userId, error) => {
|
|
50
|
+
this.logger.error(`Failed to refresh access token for user ${userId}: ${error.message}`);
|
|
51
|
+
});
|
|
52
|
+
const refreshStatus = account.refreshToken ? "automatic token refresh enabled" : "token refresh disabled (no refresh token)";
|
|
53
|
+
this.logger.info(`Using RefreshingAuthProvider for ${account.username} (${refreshStatus})`);
|
|
54
|
+
return authProvider;
|
|
55
|
+
}
|
|
56
|
+
this.logger.info(`Using StaticAuthProvider for ${account.username} (no clientSecret provided)`);
|
|
57
|
+
return new StaticAuthProvider(account.clientId, normalizedToken);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get or create a chat client for an account
|
|
61
|
+
*/
|
|
62
|
+
async getClient(account, cfg, accountId) {
|
|
63
|
+
const key = this.getAccountKey(account);
|
|
64
|
+
const existing = this.clients.get(key);
|
|
65
|
+
if (existing) return existing;
|
|
66
|
+
const tokenResolution = resolveTwitchToken(cfg, { accountId });
|
|
67
|
+
if (!tokenResolution.token) {
|
|
68
|
+
this.logger.error(`Missing Twitch token for account ${account.username} (set channels.twitch.accounts.${account.username}.token or OPENCLAW_TWITCH_ACCESS_TOKEN for default)`);
|
|
69
|
+
throw new Error("Missing Twitch token");
|
|
70
|
+
}
|
|
71
|
+
this.logger.debug?.(`Using ${tokenResolution.source} token source for ${account.username}`);
|
|
72
|
+
if (!account.clientId) {
|
|
73
|
+
this.logger.error(`Missing Twitch client ID for account ${account.username}`);
|
|
74
|
+
throw new Error("Missing Twitch client ID");
|
|
75
|
+
}
|
|
76
|
+
const normalizedToken = normalizeToken(tokenResolution.token);
|
|
77
|
+
const client = new ChatClient({
|
|
78
|
+
authProvider: await this.createAuthProvider(account, normalizedToken),
|
|
79
|
+
channels: [account.channel],
|
|
80
|
+
rejoinChannelsOnReconnect: true,
|
|
81
|
+
requestMembershipEvents: true,
|
|
82
|
+
logger: {
|
|
83
|
+
minLevel: LogLevel.WARNING,
|
|
84
|
+
custom: { log: (level, message) => {
|
|
85
|
+
switch (level) {
|
|
86
|
+
case LogLevel.CRITICAL:
|
|
87
|
+
this.logger.error(message);
|
|
88
|
+
break;
|
|
89
|
+
case LogLevel.ERROR:
|
|
90
|
+
this.logger.error(message);
|
|
91
|
+
break;
|
|
92
|
+
case LogLevel.WARNING:
|
|
93
|
+
this.logger.warn(message);
|
|
94
|
+
break;
|
|
95
|
+
case LogLevel.INFO:
|
|
96
|
+
this.logger.info(message);
|
|
97
|
+
break;
|
|
98
|
+
case LogLevel.DEBUG:
|
|
99
|
+
this.logger.debug?.(message);
|
|
100
|
+
break;
|
|
101
|
+
case LogLevel.TRACE:
|
|
102
|
+
this.logger.debug?.(message);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
} }
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
this.setupClientHandlers(client, account);
|
|
109
|
+
client.connect();
|
|
110
|
+
this.clients.set(key, client);
|
|
111
|
+
this.logger.info(`Connected to Twitch as ${account.username}`);
|
|
112
|
+
return client;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Set up message and event handlers for a client
|
|
116
|
+
*/
|
|
117
|
+
setupClientHandlers(client, account) {
|
|
118
|
+
const key = this.getAccountKey(account);
|
|
119
|
+
client.onMessage((channelName, _user, messageText, msg) => {
|
|
120
|
+
const handler = this.messageHandlers.get(key);
|
|
121
|
+
if (handler) {
|
|
122
|
+
const normalizedChannel = channelName.startsWith("#") ? channelName.slice(1) : channelName;
|
|
123
|
+
const from = `twitch:${msg.userInfo.userName}`;
|
|
124
|
+
const preview = messageText.slice(0, 100).replace(/\n/g, "\\n");
|
|
125
|
+
this.logger.debug?.(`twitch inbound: channel=${normalizedChannel} from=${from} len=${messageText.length} preview="${preview}"`);
|
|
126
|
+
handler({
|
|
127
|
+
username: msg.userInfo.userName,
|
|
128
|
+
displayName: msg.userInfo.displayName,
|
|
129
|
+
userId: msg.userInfo.userId,
|
|
130
|
+
message: messageText,
|
|
131
|
+
channel: normalizedChannel,
|
|
132
|
+
id: msg.id,
|
|
133
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
134
|
+
isMod: msg.userInfo.isMod,
|
|
135
|
+
isOwner: msg.userInfo.isBroadcaster,
|
|
136
|
+
isVip: msg.userInfo.isVip,
|
|
137
|
+
isSub: msg.userInfo.isSubscriber,
|
|
138
|
+
chatType: "group"
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
this.logger.info(`Set up handlers for ${key}`);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Set a message handler for an account
|
|
146
|
+
* @returns A function that removes the handler when called
|
|
147
|
+
*/
|
|
148
|
+
onMessage(account, handler) {
|
|
149
|
+
const key = this.getAccountKey(account);
|
|
150
|
+
this.messageHandlers.set(key, handler);
|
|
151
|
+
return () => {
|
|
152
|
+
this.messageHandlers.delete(key);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Disconnect a client
|
|
157
|
+
*/
|
|
158
|
+
async disconnect(account) {
|
|
159
|
+
const key = this.getAccountKey(account);
|
|
160
|
+
const client = this.clients.get(key);
|
|
161
|
+
if (client) {
|
|
162
|
+
client.quit();
|
|
163
|
+
this.clients.delete(key);
|
|
164
|
+
this.messageHandlers.delete(key);
|
|
165
|
+
this.logger.info(`Disconnected ${key}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Disconnect all clients
|
|
170
|
+
*/
|
|
171
|
+
async disconnectAll() {
|
|
172
|
+
this.clients.forEach((client) => client.quit());
|
|
173
|
+
this.clients.clear();
|
|
174
|
+
this.messageHandlers.clear();
|
|
175
|
+
this.logger.info(" Disconnected all clients");
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Send a message to a channel
|
|
179
|
+
*/
|
|
180
|
+
async sendMessage(account, channel, message, cfg, accountId) {
|
|
181
|
+
try {
|
|
182
|
+
const client = await this.getClient(account, cfg, accountId);
|
|
183
|
+
const messageId = crypto.randomUUID();
|
|
184
|
+
await client.say(channel, message);
|
|
185
|
+
return {
|
|
186
|
+
ok: true,
|
|
187
|
+
messageId
|
|
188
|
+
};
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.logger.error(`Failed to send message: ${formatErrorMessage(error)}`);
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
error: formatErrorMessage(error)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Generate a unique key for an account
|
|
199
|
+
*/
|
|
200
|
+
getAccountKey(account) {
|
|
201
|
+
return `${account.username}:${account.channel}`;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Clear all clients and handlers (for testing)
|
|
205
|
+
*/
|
|
206
|
+
_clearForTest() {
|
|
207
|
+
this.clients.clear();
|
|
208
|
+
this.messageHandlers.clear();
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region extensions/twitch/src/client-manager-registry.ts
|
|
213
|
+
/**
|
|
214
|
+
* Client manager registry for Twitch plugin.
|
|
215
|
+
*
|
|
216
|
+
* Manages the lifecycle of TwitchClientManager instances across the plugin,
|
|
217
|
+
* ensuring proper cleanup when accounts are stopped or reconfigured.
|
|
218
|
+
*/
|
|
219
|
+
/**
|
|
220
|
+
* Global registry of client managers.
|
|
221
|
+
* Keyed by account ID.
|
|
222
|
+
*/
|
|
223
|
+
const registry = /* @__PURE__ */ new Map();
|
|
224
|
+
/**
|
|
225
|
+
* Get or create a client manager for an account.
|
|
226
|
+
*
|
|
227
|
+
* @param accountId - The account ID
|
|
228
|
+
* @param logger - Logger instance
|
|
229
|
+
* @returns The client manager
|
|
230
|
+
*/
|
|
231
|
+
function getOrCreateClientManager(accountId, logger) {
|
|
232
|
+
const existing = registry.get(accountId);
|
|
233
|
+
if (existing) return existing.manager;
|
|
234
|
+
const manager = new TwitchClientManager(logger);
|
|
235
|
+
registry.set(accountId, {
|
|
236
|
+
manager,
|
|
237
|
+
accountId,
|
|
238
|
+
logger,
|
|
239
|
+
createdAt: Date.now()
|
|
240
|
+
});
|
|
241
|
+
logger.info(`Registered client manager for account: ${accountId}`);
|
|
242
|
+
return manager;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Get an existing client manager for an account.
|
|
246
|
+
*
|
|
247
|
+
* @param accountId - The account ID
|
|
248
|
+
* @returns The client manager, or undefined if not registered
|
|
249
|
+
*/
|
|
250
|
+
function getClientManager(accountId) {
|
|
251
|
+
return registry.get(accountId)?.manager;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Disconnect and remove a client manager from the registry.
|
|
255
|
+
*
|
|
256
|
+
* @param accountId - The account ID
|
|
257
|
+
* @returns Promise that resolves when cleanup is complete
|
|
258
|
+
*/
|
|
259
|
+
async function removeClientManager(accountId) {
|
|
260
|
+
const entry = registry.get(accountId);
|
|
261
|
+
if (!entry) return;
|
|
262
|
+
await entry.manager.disconnectAll();
|
|
263
|
+
registry.delete(accountId);
|
|
264
|
+
entry.logger.info(`Unregistered client manager for account: ${accountId}`);
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region extensions/twitch/src/utils/markdown.ts
|
|
268
|
+
/**
|
|
269
|
+
* Markdown utilities for Twitch chat
|
|
270
|
+
*
|
|
271
|
+
* Twitch chat doesn't support markdown formatting, so we strip it before sending.
|
|
272
|
+
* Based on OpenClaw's markdownToText in src/agents/tools/web-fetch-utils.ts.
|
|
273
|
+
*/
|
|
274
|
+
/**
|
|
275
|
+
* Strip markdown formatting from text for Twitch compatibility.
|
|
276
|
+
*
|
|
277
|
+
* Removes images, links, bold, italic, strikethrough, code blocks, inline code,
|
|
278
|
+
* headers, and list formatting. Replaces newlines with spaces since Twitch
|
|
279
|
+
* is a single-line chat medium.
|
|
280
|
+
*
|
|
281
|
+
* @param markdown - The markdown text to strip
|
|
282
|
+
* @returns Plain text with markdown removed
|
|
283
|
+
*/
|
|
284
|
+
function stripMarkdownForTwitch(markdown) {
|
|
285
|
+
return markdown.replace(/!\[[^\]]*]\([^)]+\)/g, "").replace(/\[([^\]]+)]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/__([^_]+)__/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/_([^_]+)_/g, "$1").replace(/~~([^~]+)~~/g, "$1").replace(/```[\s\S]*?```/g, (block) => block.replace(/```[^\n]*\n?/g, "").replace(/```/g, "")).replace(/`([^`]+)`/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/^\s*[-*+]\s+/gm, "").replace(/^\s*\d+\.\s+/gm, "").replace(/\r/g, "").replace(/[ \t]+\n/g, "\n").replace(/\n/g, " ").replace(/[ \t]{2,}/g, " ").trim();
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Simple word-boundary chunker for Twitch (500 char limit).
|
|
289
|
+
* Strips markdown before chunking to avoid breaking markdown patterns.
|
|
290
|
+
*
|
|
291
|
+
* @param text - The text to chunk
|
|
292
|
+
* @param limit - Maximum characters per chunk (Twitch limit is 500)
|
|
293
|
+
* @returns Array of text chunks
|
|
294
|
+
*/
|
|
295
|
+
function chunkTextForTwitch(text, limit) {
|
|
296
|
+
const cleaned = stripMarkdownForTwitch(text);
|
|
297
|
+
if (!cleaned) return [];
|
|
298
|
+
if (limit <= 0) return [cleaned];
|
|
299
|
+
if (cleaned.length <= limit) return [cleaned];
|
|
300
|
+
const chunks = [];
|
|
301
|
+
let remaining = cleaned;
|
|
302
|
+
while (remaining.length > limit) {
|
|
303
|
+
const window = remaining.slice(0, limit);
|
|
304
|
+
const lastSpaceIndex = window.lastIndexOf(" ");
|
|
305
|
+
if (lastSpaceIndex === -1) {
|
|
306
|
+
chunks.push(window);
|
|
307
|
+
remaining = remaining.slice(limit);
|
|
308
|
+
} else {
|
|
309
|
+
chunks.push(window.slice(0, lastSpaceIndex));
|
|
310
|
+
remaining = remaining.slice(lastSpaceIndex + 1);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (remaining) chunks.push(remaining);
|
|
314
|
+
return chunks;
|
|
315
|
+
}
|
|
316
|
+
//#endregion
|
|
18
317
|
//#region extensions/twitch/src/send.ts
|
|
19
318
|
/**
|
|
20
319
|
* Twitch message sending functions with dependency injection support.
|
|
@@ -954,7 +1253,7 @@ const twitchPlugin = createChatChannelPlugin({
|
|
|
954
1253
|
lastError: null
|
|
955
1254
|
});
|
|
956
1255
|
ctx.log?.info(`Starting Twitch connection for ${account.username}`);
|
|
957
|
-
const { monitorTwitchProvider } = await import("./monitor-
|
|
1256
|
+
const { monitorTwitchProvider } = await import("./monitor-DAM8hrYU.js");
|
|
958
1257
|
await monitorTwitchProvider({
|
|
959
1258
|
account,
|
|
960
1259
|
accountId,
|
|
@@ -978,4 +1277,4 @@ const twitchPlugin = createChatChannelPlugin({
|
|
|
978
1277
|
}
|
|
979
1278
|
});
|
|
980
1279
|
//#endregion
|
|
981
|
-
export { twitchPlugin as t };
|
|
1280
|
+
export { stripMarkdownForTwitch as n, getOrCreateClientManager as r, twitchPlugin as t };
|
package/dist/setup-plugin-api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as twitchSetupPlugin } from "./setup-surface-
|
|
1
|
+
import { n as twitchSetupPlugin } from "./setup-surface-B7FlQUTK.js";
|
|
2
2
|
export { twitchSetupPlugin };
|
|
@@ -1,8 +1,134 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, listCombinedAccountIds, normalizeAccountId, resolveNormalizedAccountEntry } from "openclaw/plugin-sdk/account-resolution";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
4
|
import { normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
|
|
4
5
|
import { getChatChannelMeta } from "openclaw/plugin-sdk/core";
|
|
5
6
|
import { formatDocsLink, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/setup";
|
|
7
|
+
//#region extensions/twitch/src/token.ts
|
|
8
|
+
/**
|
|
9
|
+
* Twitch access token resolution with environment variable support.
|
|
10
|
+
*
|
|
11
|
+
* Supports reading Twitch OAuth access tokens from config or environment variable.
|
|
12
|
+
* The OPENCLAW_TWITCH_ACCESS_TOKEN env var is only used for the default account.
|
|
13
|
+
*
|
|
14
|
+
* Token resolution priority:
|
|
15
|
+
* 1. Account access token from merged config (accounts.{id} or base-level for default)
|
|
16
|
+
* 2. Environment variable: OPENCLAW_TWITCH_ACCESS_TOKEN (default account only)
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Normalize a Twitch OAuth token - ensure it has the oauth: prefix
|
|
20
|
+
*/
|
|
21
|
+
function normalizeTwitchToken(raw) {
|
|
22
|
+
if (!raw) return;
|
|
23
|
+
const trimmed = raw.trim();
|
|
24
|
+
if (!trimmed) return;
|
|
25
|
+
return trimmed.startsWith("oauth:") ? trimmed : `oauth:${trimmed}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve Twitch access token from config or environment variable.
|
|
29
|
+
*
|
|
30
|
+
* Priority:
|
|
31
|
+
* 1. Account access token (from merged config - base-level for default, or accounts.{accountId})
|
|
32
|
+
* 2. Environment variable: OPENCLAW_TWITCH_ACCESS_TOKEN (default account only)
|
|
33
|
+
*
|
|
34
|
+
* The getAccountConfig function handles merging base-level config with accounts.default,
|
|
35
|
+
* so this logic works for both simplified and multi-account patterns.
|
|
36
|
+
*
|
|
37
|
+
* @param cfg - OpenClaw config
|
|
38
|
+
* @param opts - Options including accountId and optional envToken override
|
|
39
|
+
* @returns Token resolution with source
|
|
40
|
+
*/
|
|
41
|
+
function resolveTwitchToken(cfg, opts = {}) {
|
|
42
|
+
const accountId = normalizeAccountId(opts.accountId);
|
|
43
|
+
const twitchCfg = cfg?.channels?.twitch;
|
|
44
|
+
const accounts = twitchCfg?.accounts;
|
|
45
|
+
const accountCfg = resolveNormalizedAccountEntry(accounts, accountId, normalizeAccountId);
|
|
46
|
+
let token;
|
|
47
|
+
if (accountId === DEFAULT_ACCOUNT_ID) token = normalizeTwitchToken((typeof twitchCfg?.accessToken === "string" ? twitchCfg.accessToken : void 0) || accountCfg?.accessToken);
|
|
48
|
+
else token = normalizeTwitchToken(accountCfg?.accessToken);
|
|
49
|
+
if (token) return {
|
|
50
|
+
token,
|
|
51
|
+
source: "config"
|
|
52
|
+
};
|
|
53
|
+
const envToken = accountId === DEFAULT_ACCOUNT_ID ? normalizeTwitchToken(opts.envToken ?? process.env.OPENCLAW_TWITCH_ACCESS_TOKEN) : void 0;
|
|
54
|
+
if (envToken) return {
|
|
55
|
+
token: envToken,
|
|
56
|
+
source: "env"
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
token: "",
|
|
60
|
+
source: "none"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region extensions/twitch/src/utils/twitch.ts
|
|
65
|
+
/**
|
|
66
|
+
* Twitch-specific utility functions
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Normalize Twitch channel names.
|
|
70
|
+
*
|
|
71
|
+
* Removes the '#' prefix if present, converts to lowercase, and trims whitespace.
|
|
72
|
+
* Twitch channel names are case-insensitive and don't use the '#' prefix in the API.
|
|
73
|
+
*
|
|
74
|
+
* @param channel - The channel name to normalize
|
|
75
|
+
* @returns Normalized channel name
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* normalizeTwitchChannel("#TwitchChannel") // "twitchchannel"
|
|
79
|
+
* normalizeTwitchChannel("MyChannel") // "mychannel"
|
|
80
|
+
*/
|
|
81
|
+
function normalizeTwitchChannel(channel) {
|
|
82
|
+
const trimmed = normalizeLowercaseStringOrEmpty(channel);
|
|
83
|
+
return trimmed.startsWith("#") ? trimmed.slice(1) : trimmed;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Create a standardized error message for missing target.
|
|
87
|
+
*
|
|
88
|
+
* @param provider - The provider name (e.g., "Twitch")
|
|
89
|
+
* @param hint - Optional hint for how to fix the issue
|
|
90
|
+
* @returns Error object with descriptive message
|
|
91
|
+
*/
|
|
92
|
+
function missingTargetError(provider, hint) {
|
|
93
|
+
return /* @__PURE__ */ new Error(`Delivering to ${provider} requires target${hint ? ` ${hint}` : ""}`);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Generate a unique message ID for Twitch messages.
|
|
97
|
+
*
|
|
98
|
+
* Twurple's say() doesn't return the message ID, so we generate one
|
|
99
|
+
* for tracking purposes.
|
|
100
|
+
*
|
|
101
|
+
* @returns A unique message ID
|
|
102
|
+
*/
|
|
103
|
+
function generateMessageId() {
|
|
104
|
+
return `${Date.now()}-${randomUUID()}`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Normalize OAuth token by removing the "oauth:" prefix if present.
|
|
108
|
+
*
|
|
109
|
+
* Twurple doesn't require the "oauth:" prefix, so we strip it for consistency.
|
|
110
|
+
*
|
|
111
|
+
* @param token - The OAuth token to normalize
|
|
112
|
+
* @returns Normalized token without "oauth:" prefix
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* normalizeToken("oauth:abc123") // "abc123"
|
|
116
|
+
* normalizeToken("abc123") // "abc123"
|
|
117
|
+
*/
|
|
118
|
+
function normalizeToken(token) {
|
|
119
|
+
return token.startsWith("oauth:") ? token.slice(6) : token;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Check if an account is properly configured with required credentials.
|
|
123
|
+
*
|
|
124
|
+
* @param account - The Twitch account config to check
|
|
125
|
+
* @returns true if the account has required credentials
|
|
126
|
+
*/
|
|
127
|
+
function isAccountConfigured(account, resolvedToken) {
|
|
128
|
+
const token = resolvedToken ?? account?.accessToken;
|
|
129
|
+
return Boolean(account?.username && token && account?.clientId);
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
6
132
|
//#region extensions/twitch/src/config.ts
|
|
7
133
|
/**
|
|
8
134
|
* Default account ID for Twitch
|
|
@@ -397,4 +523,4 @@ const twitchSetupPlugin = {
|
|
|
397
523
|
setupWizard: twitchSetupWizard
|
|
398
524
|
};
|
|
399
525
|
//#endregion
|
|
400
|
-
export { getAccountConfig as a, resolveTwitchAccountContext as c, DEFAULT_ACCOUNT_ID$1 as i, resolveTwitchSnapshotAccountId as l, twitchSetupPlugin as n, listAccountIds as o, twitchSetupWizard as r, resolveDefaultTwitchAccountId as s, twitchSetupAdapter as t };
|
|
526
|
+
export { getAccountConfig as a, resolveTwitchAccountContext as c, isAccountConfigured as d, missingTargetError as f, resolveTwitchToken as h, DEFAULT_ACCOUNT_ID$1 as i, resolveTwitchSnapshotAccountId as l, normalizeTwitchChannel as m, twitchSetupPlugin as n, listAccountIds as o, normalizeToken as p, twitchSetupWizard as r, resolveDefaultTwitchAccountId as s, twitchSetupAdapter as t, generateMessageId as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/twitch",
|
|
3
|
-
"version": "2026.5.14-beta.
|
|
3
|
+
"version": "2026.5.14-beta.2",
|
|
4
4
|
"description": "OpenClaw Twitch channel plugin",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"minHostVersion": ">=2026.4.10"
|
|
28
28
|
},
|
|
29
29
|
"compat": {
|
|
30
|
-
"pluginApi": ">=2026.5.14-beta.
|
|
30
|
+
"pluginApi": ">=2026.5.14-beta.2"
|
|
31
31
|
},
|
|
32
32
|
"build": {
|
|
33
|
-
"openclawVersion": "2026.5.14-beta.
|
|
33
|
+
"openclawVersion": "2026.5.14-beta.2"
|
|
34
34
|
},
|
|
35
35
|
"channel": {
|
|
36
36
|
"id": "twitch",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"README.md"
|
|
58
58
|
],
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"openclaw": ">=2026.5.14-beta.
|
|
60
|
+
"openclaw": ">=2026.5.14-beta.2"
|
|
61
61
|
},
|
|
62
62
|
"peerDependenciesMeta": {
|
|
63
63
|
"openclaw": {
|
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
import { i as normalizeToken, o as resolveTwitchToken } from "./twitch-BsSKTyqW.js";
|
|
2
|
-
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
3
|
-
import { RefreshingAuthProvider, StaticAuthProvider } from "@twurple/auth";
|
|
4
|
-
import { ChatClient, LogLevel } from "@twurple/chat";
|
|
5
|
-
//#region extensions/twitch/src/twitch-client.ts
|
|
6
|
-
/**
|
|
7
|
-
* Manages Twitch chat client connections
|
|
8
|
-
*/
|
|
9
|
-
var TwitchClientManager = class {
|
|
10
|
-
constructor(logger) {
|
|
11
|
-
this.logger = logger;
|
|
12
|
-
this.clients = /* @__PURE__ */ new Map();
|
|
13
|
-
this.messageHandlers = /* @__PURE__ */ new Map();
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Create an auth provider for the account.
|
|
17
|
-
*/
|
|
18
|
-
async createAuthProvider(account, normalizedToken) {
|
|
19
|
-
if (!account.clientId) throw new Error("Missing Twitch client ID");
|
|
20
|
-
if (account.clientSecret) {
|
|
21
|
-
const authProvider = new RefreshingAuthProvider({
|
|
22
|
-
clientId: account.clientId,
|
|
23
|
-
clientSecret: account.clientSecret
|
|
24
|
-
});
|
|
25
|
-
await authProvider.addUserForToken({
|
|
26
|
-
accessToken: normalizedToken,
|
|
27
|
-
refreshToken: account.refreshToken ?? null,
|
|
28
|
-
expiresIn: account.expiresIn ?? null,
|
|
29
|
-
obtainmentTimestamp: account.obtainmentTimestamp ?? Date.now()
|
|
30
|
-
}).then((userId) => {
|
|
31
|
-
this.logger.info(`Added user ${userId} to RefreshingAuthProvider for ${account.username}`);
|
|
32
|
-
}).catch((err) => {
|
|
33
|
-
this.logger.error(`Failed to add user to RefreshingAuthProvider: ${formatErrorMessage(err)}`);
|
|
34
|
-
});
|
|
35
|
-
authProvider.onRefresh((userId, token) => {
|
|
36
|
-
this.logger.info(`Access token refreshed for user ${userId} (expires in ${token.expiresIn ? `${token.expiresIn}s` : "unknown"})`);
|
|
37
|
-
});
|
|
38
|
-
authProvider.onRefreshFailure((userId, error) => {
|
|
39
|
-
this.logger.error(`Failed to refresh access token for user ${userId}: ${error.message}`);
|
|
40
|
-
});
|
|
41
|
-
const refreshStatus = account.refreshToken ? "automatic token refresh enabled" : "token refresh disabled (no refresh token)";
|
|
42
|
-
this.logger.info(`Using RefreshingAuthProvider for ${account.username} (${refreshStatus})`);
|
|
43
|
-
return authProvider;
|
|
44
|
-
}
|
|
45
|
-
this.logger.info(`Using StaticAuthProvider for ${account.username} (no clientSecret provided)`);
|
|
46
|
-
return new StaticAuthProvider(account.clientId, normalizedToken);
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Get or create a chat client for an account
|
|
50
|
-
*/
|
|
51
|
-
async getClient(account, cfg, accountId) {
|
|
52
|
-
const key = this.getAccountKey(account);
|
|
53
|
-
const existing = this.clients.get(key);
|
|
54
|
-
if (existing) return existing;
|
|
55
|
-
const tokenResolution = resolveTwitchToken(cfg, { accountId });
|
|
56
|
-
if (!tokenResolution.token) {
|
|
57
|
-
this.logger.error(`Missing Twitch token for account ${account.username} (set channels.twitch.accounts.${account.username}.token or OPENCLAW_TWITCH_ACCESS_TOKEN for default)`);
|
|
58
|
-
throw new Error("Missing Twitch token");
|
|
59
|
-
}
|
|
60
|
-
this.logger.debug?.(`Using ${tokenResolution.source} token source for ${account.username}`);
|
|
61
|
-
if (!account.clientId) {
|
|
62
|
-
this.logger.error(`Missing Twitch client ID for account ${account.username}`);
|
|
63
|
-
throw new Error("Missing Twitch client ID");
|
|
64
|
-
}
|
|
65
|
-
const normalizedToken = normalizeToken(tokenResolution.token);
|
|
66
|
-
const client = new ChatClient({
|
|
67
|
-
authProvider: await this.createAuthProvider(account, normalizedToken),
|
|
68
|
-
channels: [account.channel],
|
|
69
|
-
rejoinChannelsOnReconnect: true,
|
|
70
|
-
requestMembershipEvents: true,
|
|
71
|
-
logger: {
|
|
72
|
-
minLevel: LogLevel.WARNING,
|
|
73
|
-
custom: { log: (level, message) => {
|
|
74
|
-
switch (level) {
|
|
75
|
-
case LogLevel.CRITICAL:
|
|
76
|
-
this.logger.error(message);
|
|
77
|
-
break;
|
|
78
|
-
case LogLevel.ERROR:
|
|
79
|
-
this.logger.error(message);
|
|
80
|
-
break;
|
|
81
|
-
case LogLevel.WARNING:
|
|
82
|
-
this.logger.warn(message);
|
|
83
|
-
break;
|
|
84
|
-
case LogLevel.INFO:
|
|
85
|
-
this.logger.info(message);
|
|
86
|
-
break;
|
|
87
|
-
case LogLevel.DEBUG:
|
|
88
|
-
this.logger.debug?.(message);
|
|
89
|
-
break;
|
|
90
|
-
case LogLevel.TRACE:
|
|
91
|
-
this.logger.debug?.(message);
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
} }
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
this.setupClientHandlers(client, account);
|
|
98
|
-
client.connect();
|
|
99
|
-
this.clients.set(key, client);
|
|
100
|
-
this.logger.info(`Connected to Twitch as ${account.username}`);
|
|
101
|
-
return client;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Set up message and event handlers for a client
|
|
105
|
-
*/
|
|
106
|
-
setupClientHandlers(client, account) {
|
|
107
|
-
const key = this.getAccountKey(account);
|
|
108
|
-
client.onMessage((channelName, _user, messageText, msg) => {
|
|
109
|
-
const handler = this.messageHandlers.get(key);
|
|
110
|
-
if (handler) {
|
|
111
|
-
const normalizedChannel = channelName.startsWith("#") ? channelName.slice(1) : channelName;
|
|
112
|
-
const from = `twitch:${msg.userInfo.userName}`;
|
|
113
|
-
const preview = messageText.slice(0, 100).replace(/\n/g, "\\n");
|
|
114
|
-
this.logger.debug?.(`twitch inbound: channel=${normalizedChannel} from=${from} len=${messageText.length} preview="${preview}"`);
|
|
115
|
-
handler({
|
|
116
|
-
username: msg.userInfo.userName,
|
|
117
|
-
displayName: msg.userInfo.displayName,
|
|
118
|
-
userId: msg.userInfo.userId,
|
|
119
|
-
message: messageText,
|
|
120
|
-
channel: normalizedChannel,
|
|
121
|
-
id: msg.id,
|
|
122
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
123
|
-
isMod: msg.userInfo.isMod,
|
|
124
|
-
isOwner: msg.userInfo.isBroadcaster,
|
|
125
|
-
isVip: msg.userInfo.isVip,
|
|
126
|
-
isSub: msg.userInfo.isSubscriber,
|
|
127
|
-
chatType: "group"
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
this.logger.info(`Set up handlers for ${key}`);
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Set a message handler for an account
|
|
135
|
-
* @returns A function that removes the handler when called
|
|
136
|
-
*/
|
|
137
|
-
onMessage(account, handler) {
|
|
138
|
-
const key = this.getAccountKey(account);
|
|
139
|
-
this.messageHandlers.set(key, handler);
|
|
140
|
-
return () => {
|
|
141
|
-
this.messageHandlers.delete(key);
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Disconnect a client
|
|
146
|
-
*/
|
|
147
|
-
async disconnect(account) {
|
|
148
|
-
const key = this.getAccountKey(account);
|
|
149
|
-
const client = this.clients.get(key);
|
|
150
|
-
if (client) {
|
|
151
|
-
client.quit();
|
|
152
|
-
this.clients.delete(key);
|
|
153
|
-
this.messageHandlers.delete(key);
|
|
154
|
-
this.logger.info(`Disconnected ${key}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Disconnect all clients
|
|
159
|
-
*/
|
|
160
|
-
async disconnectAll() {
|
|
161
|
-
this.clients.forEach((client) => client.quit());
|
|
162
|
-
this.clients.clear();
|
|
163
|
-
this.messageHandlers.clear();
|
|
164
|
-
this.logger.info(" Disconnected all clients");
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Send a message to a channel
|
|
168
|
-
*/
|
|
169
|
-
async sendMessage(account, channel, message, cfg, accountId) {
|
|
170
|
-
try {
|
|
171
|
-
const client = await this.getClient(account, cfg, accountId);
|
|
172
|
-
const messageId = crypto.randomUUID();
|
|
173
|
-
await client.say(channel, message);
|
|
174
|
-
return {
|
|
175
|
-
ok: true,
|
|
176
|
-
messageId
|
|
177
|
-
};
|
|
178
|
-
} catch (error) {
|
|
179
|
-
this.logger.error(`Failed to send message: ${formatErrorMessage(error)}`);
|
|
180
|
-
return {
|
|
181
|
-
ok: false,
|
|
182
|
-
error: formatErrorMessage(error)
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Generate a unique key for an account
|
|
188
|
-
*/
|
|
189
|
-
getAccountKey(account) {
|
|
190
|
-
return `${account.username}:${account.channel}`;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Clear all clients and handlers (for testing)
|
|
194
|
-
*/
|
|
195
|
-
_clearForTest() {
|
|
196
|
-
this.clients.clear();
|
|
197
|
-
this.messageHandlers.clear();
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
//#endregion
|
|
201
|
-
//#region extensions/twitch/src/client-manager-registry.ts
|
|
202
|
-
/**
|
|
203
|
-
* Client manager registry for Twitch plugin.
|
|
204
|
-
*
|
|
205
|
-
* Manages the lifecycle of TwitchClientManager instances across the plugin,
|
|
206
|
-
* ensuring proper cleanup when accounts are stopped or reconfigured.
|
|
207
|
-
*/
|
|
208
|
-
/**
|
|
209
|
-
* Global registry of client managers.
|
|
210
|
-
* Keyed by account ID.
|
|
211
|
-
*/
|
|
212
|
-
const registry = /* @__PURE__ */ new Map();
|
|
213
|
-
/**
|
|
214
|
-
* Get or create a client manager for an account.
|
|
215
|
-
*
|
|
216
|
-
* @param accountId - The account ID
|
|
217
|
-
* @param logger - Logger instance
|
|
218
|
-
* @returns The client manager
|
|
219
|
-
*/
|
|
220
|
-
function getOrCreateClientManager(accountId, logger) {
|
|
221
|
-
const existing = registry.get(accountId);
|
|
222
|
-
if (existing) return existing.manager;
|
|
223
|
-
const manager = new TwitchClientManager(logger);
|
|
224
|
-
registry.set(accountId, {
|
|
225
|
-
manager,
|
|
226
|
-
accountId,
|
|
227
|
-
logger,
|
|
228
|
-
createdAt: Date.now()
|
|
229
|
-
});
|
|
230
|
-
logger.info(`Registered client manager for account: ${accountId}`);
|
|
231
|
-
return manager;
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Get an existing client manager for an account.
|
|
235
|
-
*
|
|
236
|
-
* @param accountId - The account ID
|
|
237
|
-
* @returns The client manager, or undefined if not registered
|
|
238
|
-
*/
|
|
239
|
-
function getClientManager(accountId) {
|
|
240
|
-
return registry.get(accountId)?.manager;
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Disconnect and remove a client manager from the registry.
|
|
244
|
-
*
|
|
245
|
-
* @param accountId - The account ID
|
|
246
|
-
* @returns Promise that resolves when cleanup is complete
|
|
247
|
-
*/
|
|
248
|
-
async function removeClientManager(accountId) {
|
|
249
|
-
const entry = registry.get(accountId);
|
|
250
|
-
if (!entry) return;
|
|
251
|
-
await entry.manager.disconnectAll();
|
|
252
|
-
registry.delete(accountId);
|
|
253
|
-
entry.logger.info(`Unregistered client manager for account: ${accountId}`);
|
|
254
|
-
}
|
|
255
|
-
//#endregion
|
|
256
|
-
//#region extensions/twitch/src/utils/markdown.ts
|
|
257
|
-
/**
|
|
258
|
-
* Markdown utilities for Twitch chat
|
|
259
|
-
*
|
|
260
|
-
* Twitch chat doesn't support markdown formatting, so we strip it before sending.
|
|
261
|
-
* Based on OpenClaw's markdownToText in src/agents/tools/web-fetch-utils.ts.
|
|
262
|
-
*/
|
|
263
|
-
/**
|
|
264
|
-
* Strip markdown formatting from text for Twitch compatibility.
|
|
265
|
-
*
|
|
266
|
-
* Removes images, links, bold, italic, strikethrough, code blocks, inline code,
|
|
267
|
-
* headers, and list formatting. Replaces newlines with spaces since Twitch
|
|
268
|
-
* is a single-line chat medium.
|
|
269
|
-
*
|
|
270
|
-
* @param markdown - The markdown text to strip
|
|
271
|
-
* @returns Plain text with markdown removed
|
|
272
|
-
*/
|
|
273
|
-
function stripMarkdownForTwitch(markdown) {
|
|
274
|
-
return markdown.replace(/!\[[^\]]*]\([^)]+\)/g, "").replace(/\[([^\]]+)]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/__([^_]+)__/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/_([^_]+)_/g, "$1").replace(/~~([^~]+)~~/g, "$1").replace(/```[\s\S]*?```/g, (block) => block.replace(/```[^\n]*\n?/g, "").replace(/```/g, "")).replace(/`([^`]+)`/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/^\s*[-*+]\s+/gm, "").replace(/^\s*\d+\.\s+/gm, "").replace(/\r/g, "").replace(/[ \t]+\n/g, "\n").replace(/\n/g, " ").replace(/[ \t]{2,}/g, " ").trim();
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Simple word-boundary chunker for Twitch (500 char limit).
|
|
278
|
-
* Strips markdown before chunking to avoid breaking markdown patterns.
|
|
279
|
-
*
|
|
280
|
-
* @param text - The text to chunk
|
|
281
|
-
* @param limit - Maximum characters per chunk (Twitch limit is 500)
|
|
282
|
-
* @returns Array of text chunks
|
|
283
|
-
*/
|
|
284
|
-
function chunkTextForTwitch(text, limit) {
|
|
285
|
-
const cleaned = stripMarkdownForTwitch(text);
|
|
286
|
-
if (!cleaned) return [];
|
|
287
|
-
if (limit <= 0) return [cleaned];
|
|
288
|
-
if (cleaned.length <= limit) return [cleaned];
|
|
289
|
-
const chunks = [];
|
|
290
|
-
let remaining = cleaned;
|
|
291
|
-
while (remaining.length > limit) {
|
|
292
|
-
const window = remaining.slice(0, limit);
|
|
293
|
-
const lastSpaceIndex = window.lastIndexOf(" ");
|
|
294
|
-
if (lastSpaceIndex === -1) {
|
|
295
|
-
chunks.push(window);
|
|
296
|
-
remaining = remaining.slice(limit);
|
|
297
|
-
} else {
|
|
298
|
-
chunks.push(window.slice(0, lastSpaceIndex));
|
|
299
|
-
remaining = remaining.slice(lastSpaceIndex + 1);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
if (remaining) chunks.push(remaining);
|
|
303
|
-
return chunks;
|
|
304
|
-
}
|
|
305
|
-
//#endregion
|
|
306
|
-
export { removeClientManager as a, getOrCreateClientManager as i, stripMarkdownForTwitch as n, getClientManager as r, chunkTextForTwitch as t };
|
package/dist/twitch-BsSKTyqW.js
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_ACCOUNT_ID, normalizeAccountId, resolveNormalizedAccountEntry } from "openclaw/plugin-sdk/account-resolution";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
|
-
//#region extensions/twitch/src/token.ts
|
|
5
|
-
/**
|
|
6
|
-
* Twitch access token resolution with environment variable support.
|
|
7
|
-
*
|
|
8
|
-
* Supports reading Twitch OAuth access tokens from config or environment variable.
|
|
9
|
-
* The OPENCLAW_TWITCH_ACCESS_TOKEN env var is only used for the default account.
|
|
10
|
-
*
|
|
11
|
-
* Token resolution priority:
|
|
12
|
-
* 1. Account access token from merged config (accounts.{id} or base-level for default)
|
|
13
|
-
* 2. Environment variable: OPENCLAW_TWITCH_ACCESS_TOKEN (default account only)
|
|
14
|
-
*/
|
|
15
|
-
/**
|
|
16
|
-
* Normalize a Twitch OAuth token - ensure it has the oauth: prefix
|
|
17
|
-
*/
|
|
18
|
-
function normalizeTwitchToken(raw) {
|
|
19
|
-
if (!raw) return;
|
|
20
|
-
const trimmed = raw.trim();
|
|
21
|
-
if (!trimmed) return;
|
|
22
|
-
return trimmed.startsWith("oauth:") ? trimmed : `oauth:${trimmed}`;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Resolve Twitch access token from config or environment variable.
|
|
26
|
-
*
|
|
27
|
-
* Priority:
|
|
28
|
-
* 1. Account access token (from merged config - base-level for default, or accounts.{accountId})
|
|
29
|
-
* 2. Environment variable: OPENCLAW_TWITCH_ACCESS_TOKEN (default account only)
|
|
30
|
-
*
|
|
31
|
-
* The getAccountConfig function handles merging base-level config with accounts.default,
|
|
32
|
-
* so this logic works for both simplified and multi-account patterns.
|
|
33
|
-
*
|
|
34
|
-
* @param cfg - OpenClaw config
|
|
35
|
-
* @param opts - Options including accountId and optional envToken override
|
|
36
|
-
* @returns Token resolution with source
|
|
37
|
-
*/
|
|
38
|
-
function resolveTwitchToken(cfg, opts = {}) {
|
|
39
|
-
const accountId = normalizeAccountId(opts.accountId);
|
|
40
|
-
const twitchCfg = cfg?.channels?.twitch;
|
|
41
|
-
const accounts = twitchCfg?.accounts;
|
|
42
|
-
const accountCfg = resolveNormalizedAccountEntry(accounts, accountId, normalizeAccountId);
|
|
43
|
-
let token;
|
|
44
|
-
if (accountId === DEFAULT_ACCOUNT_ID) token = normalizeTwitchToken((typeof twitchCfg?.accessToken === "string" ? twitchCfg.accessToken : void 0) || accountCfg?.accessToken);
|
|
45
|
-
else token = normalizeTwitchToken(accountCfg?.accessToken);
|
|
46
|
-
if (token) return {
|
|
47
|
-
token,
|
|
48
|
-
source: "config"
|
|
49
|
-
};
|
|
50
|
-
const envToken = accountId === DEFAULT_ACCOUNT_ID ? normalizeTwitchToken(opts.envToken ?? process.env.OPENCLAW_TWITCH_ACCESS_TOKEN) : void 0;
|
|
51
|
-
if (envToken) return {
|
|
52
|
-
token: envToken,
|
|
53
|
-
source: "env"
|
|
54
|
-
};
|
|
55
|
-
return {
|
|
56
|
-
token: "",
|
|
57
|
-
source: "none"
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
//#endregion
|
|
61
|
-
//#region extensions/twitch/src/utils/twitch.ts
|
|
62
|
-
/**
|
|
63
|
-
* Twitch-specific utility functions
|
|
64
|
-
*/
|
|
65
|
-
/**
|
|
66
|
-
* Normalize Twitch channel names.
|
|
67
|
-
*
|
|
68
|
-
* Removes the '#' prefix if present, converts to lowercase, and trims whitespace.
|
|
69
|
-
* Twitch channel names are case-insensitive and don't use the '#' prefix in the API.
|
|
70
|
-
*
|
|
71
|
-
* @param channel - The channel name to normalize
|
|
72
|
-
* @returns Normalized channel name
|
|
73
|
-
*
|
|
74
|
-
* @example
|
|
75
|
-
* normalizeTwitchChannel("#TwitchChannel") // "twitchchannel"
|
|
76
|
-
* normalizeTwitchChannel("MyChannel") // "mychannel"
|
|
77
|
-
*/
|
|
78
|
-
function normalizeTwitchChannel(channel) {
|
|
79
|
-
const trimmed = normalizeLowercaseStringOrEmpty(channel);
|
|
80
|
-
return trimmed.startsWith("#") ? trimmed.slice(1) : trimmed;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Create a standardized error message for missing target.
|
|
84
|
-
*
|
|
85
|
-
* @param provider - The provider name (e.g., "Twitch")
|
|
86
|
-
* @param hint - Optional hint for how to fix the issue
|
|
87
|
-
* @returns Error object with descriptive message
|
|
88
|
-
*/
|
|
89
|
-
function missingTargetError(provider, hint) {
|
|
90
|
-
return /* @__PURE__ */ new Error(`Delivering to ${provider} requires target${hint ? ` ${hint}` : ""}`);
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Generate a unique message ID for Twitch messages.
|
|
94
|
-
*
|
|
95
|
-
* Twurple's say() doesn't return the message ID, so we generate one
|
|
96
|
-
* for tracking purposes.
|
|
97
|
-
*
|
|
98
|
-
* @returns A unique message ID
|
|
99
|
-
*/
|
|
100
|
-
function generateMessageId() {
|
|
101
|
-
return `${Date.now()}-${randomUUID()}`;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Normalize OAuth token by removing the "oauth:" prefix if present.
|
|
105
|
-
*
|
|
106
|
-
* Twurple doesn't require the "oauth:" prefix, so we strip it for consistency.
|
|
107
|
-
*
|
|
108
|
-
* @param token - The OAuth token to normalize
|
|
109
|
-
* @returns Normalized token without "oauth:" prefix
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* normalizeToken("oauth:abc123") // "abc123"
|
|
113
|
-
* normalizeToken("abc123") // "abc123"
|
|
114
|
-
*/
|
|
115
|
-
function normalizeToken(token) {
|
|
116
|
-
return token.startsWith("oauth:") ? token.slice(6) : token;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Check if an account is properly configured with required credentials.
|
|
120
|
-
*
|
|
121
|
-
* @param account - The Twitch account config to check
|
|
122
|
-
* @returns true if the account has required credentials
|
|
123
|
-
*/
|
|
124
|
-
function isAccountConfigured(account, resolvedToken) {
|
|
125
|
-
const token = resolvedToken ?? account?.accessToken;
|
|
126
|
-
return Boolean(account?.username && token && account?.clientId);
|
|
127
|
-
}
|
|
128
|
-
//#endregion
|
|
129
|
-
export { normalizeTwitchChannel as a, normalizeToken as i, isAccountConfigured as n, resolveTwitchToken as o, missingTargetError as r, generateMessageId as t };
|
|
File without changes
|