@elizaos/plugin-twitch 2.0.0-beta.1 → 2.0.3-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/index.js DELETED
@@ -1,972 +0,0 @@
1
- // src/index.ts
2
- import { getConnectorAccountManager, logger as logger2 } from "@elizaos/core";
3
-
4
- // src/accounts.ts
5
- var DEFAULT_TWITCH_ACCOUNT_ID = "default";
6
- function stringSetting(runtime, key) {
7
- const value = runtime.getSetting(key);
8
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
9
- }
10
- function characterConfig(runtime) {
11
- const settings = runtime.character?.settings;
12
- const raw = settings?.twitch;
13
- return raw && typeof raw === "object" ? raw : {};
14
- }
15
- function parseAccountsJson(runtime) {
16
- const raw = stringSetting(runtime, "TWITCH_ACCOUNTS");
17
- if (!raw)
18
- return {};
19
- try {
20
- const parsed = JSON.parse(raw);
21
- if (Array.isArray(parsed)) {
22
- return Object.fromEntries(parsed.filter((item) => Boolean(item) && typeof item === "object").map((item) => [
23
- normalizeTwitchAccountId(item.accountId ?? item.id),
24
- item
25
- ]));
26
- }
27
- return parsed && typeof parsed === "object" ? parsed : {};
28
- } catch {
29
- return {};
30
- }
31
- }
32
- function allAccountConfigs(runtime) {
33
- return {
34
- ...characterConfig(runtime).accounts ?? {},
35
- ...parseAccountsJson(runtime)
36
- };
37
- }
38
- function accountConfig(runtime, accountId) {
39
- const accounts = allAccountConfigs(runtime);
40
- return accounts[accountId] ?? accounts[normalizeTwitchAccountId(accountId)] ?? {};
41
- }
42
- function boolValue(value, fallback = false) {
43
- if (typeof value === "boolean")
44
- return value;
45
- if (typeof value === "string")
46
- return value.trim().toLowerCase() === "true";
47
- return fallback;
48
- }
49
- function stringList(value) {
50
- if (Array.isArray(value))
51
- return value.map((item) => String(item).trim()).filter(Boolean);
52
- if (typeof value === "string") {
53
- return value.split(",").map((item) => item.trim()).filter(Boolean);
54
- }
55
- return [];
56
- }
57
- function roleList(value) {
58
- const values = stringList(value).map((role) => role.toLowerCase());
59
- return values.length ? values : ["all"];
60
- }
61
- function normalizeTwitchAccountId(accountId) {
62
- if (typeof accountId !== "string")
63
- return DEFAULT_TWITCH_ACCOUNT_ID;
64
- const trimmed = accountId.trim();
65
- return trimmed || DEFAULT_TWITCH_ACCOUNT_ID;
66
- }
67
- function listTwitchAccountIds(runtime) {
68
- const ids = new Set;
69
- const config = characterConfig(runtime);
70
- if (stringSetting(runtime, "TWITCH_ACCESS_TOKEN") || config.accessToken) {
71
- ids.add(DEFAULT_TWITCH_ACCOUNT_ID);
72
- }
73
- for (const id of Object.keys(allAccountConfigs(runtime))) {
74
- ids.add(normalizeTwitchAccountId(id));
75
- }
76
- return Array.from(ids.size ? ids : new Set([DEFAULT_TWITCH_ACCOUNT_ID])).sort((a, b) => a.localeCompare(b));
77
- }
78
- function resolveDefaultTwitchAccountId(runtime) {
79
- const requested = stringSetting(runtime, "TWITCH_DEFAULT_ACCOUNT_ID") ?? stringSetting(runtime, "TWITCH_ACCOUNT_ID");
80
- if (requested)
81
- return normalizeTwitchAccountId(requested);
82
- const ids = listTwitchAccountIds(runtime);
83
- return ids.includes(DEFAULT_TWITCH_ACCOUNT_ID) ? DEFAULT_TWITCH_ACCOUNT_ID : ids[0];
84
- }
85
- function readTwitchAccountId(...sources) {
86
- for (const source of sources) {
87
- if (!source || typeof source !== "object")
88
- continue;
89
- const record = source;
90
- const parameters = record.parameters && typeof record.parameters === "object" ? record.parameters : {};
91
- const data = record.data && typeof record.data === "object" ? record.data : {};
92
- const metadata = record.metadata && typeof record.metadata === "object" ? record.metadata : {};
93
- const twitch = data.twitch && typeof data.twitch === "object" ? data.twitch : {};
94
- const value = record.accountId ?? parameters.accountId ?? data.accountId ?? twitch.accountId ?? metadata.accountId;
95
- if (typeof value === "string" && value.trim())
96
- return normalizeTwitchAccountId(value);
97
- }
98
- return;
99
- }
100
- function resolveTwitchAccountSettings(runtime, requestedAccountId) {
101
- const accountId = normalizeTwitchAccountId(requestedAccountId ?? resolveDefaultTwitchAccountId(runtime));
102
- const base = characterConfig(runtime);
103
- const account = accountConfig(runtime, accountId);
104
- const allowEnv = accountId === DEFAULT_TWITCH_ACCOUNT_ID;
105
- return {
106
- accountId,
107
- username: account.username ?? base.username ?? (allowEnv ? stringSetting(runtime, "TWITCH_USERNAME") : undefined) ?? "",
108
- clientId: account.clientId ?? base.clientId ?? (allowEnv ? stringSetting(runtime, "TWITCH_CLIENT_ID") : undefined) ?? "",
109
- accessToken: account.accessToken ?? base.accessToken ?? (allowEnv ? stringSetting(runtime, "TWITCH_ACCESS_TOKEN") : undefined) ?? "",
110
- clientSecret: account.clientSecret ?? base.clientSecret ?? (allowEnv ? stringSetting(runtime, "TWITCH_CLIENT_SECRET") : undefined),
111
- refreshToken: account.refreshToken ?? base.refreshToken ?? (allowEnv ? stringSetting(runtime, "TWITCH_REFRESH_TOKEN") : undefined),
112
- channel: account.channel ?? base.channel ?? (allowEnv ? stringSetting(runtime, "TWITCH_CHANNEL") : undefined) ?? "",
113
- additionalChannels: stringList(account.additionalChannels ?? account.channels ?? base.additionalChannels ?? base.channels ?? (allowEnv ? stringSetting(runtime, "TWITCH_CHANNELS") : undefined)),
114
- requireMention: boolValue(account.requireMention ?? base.requireMention ?? (allowEnv ? stringSetting(runtime, "TWITCH_REQUIRE_MENTION") : undefined)),
115
- allowedRoles: roleList(account.allowedRoles ?? base.allowedRoles ?? (allowEnv ? stringSetting(runtime, "TWITCH_ALLOWED_ROLES") : undefined)),
116
- allowedUserIds: stringList(account.allowedUserIds ?? base.allowedUserIds),
117
- enabled: boolValue(account.enabled ?? base.enabled, true)
118
- };
119
- }
120
- // src/service.ts
121
- import {
122
- logger,
123
- Service
124
- } from "@elizaos/core";
125
- import { RefreshingAuthProvider, StaticAuthProvider } from "@twurple/auth";
126
- import { ChatClient } from "@twurple/chat";
127
-
128
- // src/types.ts
129
- var MAX_TWITCH_MESSAGE_LENGTH = 500;
130
- var TWITCH_SERVICE_NAME = "twitch";
131
- var TwitchEventTypes;
132
- ((TwitchEventTypes2) => {
133
- TwitchEventTypes2["MESSAGE_RECEIVED"] = "TWITCH_MESSAGE_RECEIVED";
134
- TwitchEventTypes2["MESSAGE_SENT"] = "TWITCH_MESSAGE_SENT";
135
- TwitchEventTypes2["JOIN_CHANNEL"] = "TWITCH_JOIN_CHANNEL";
136
- TwitchEventTypes2["LEAVE_CHANNEL"] = "TWITCH_LEAVE_CHANNEL";
137
- TwitchEventTypes2["CONNECTION_READY"] = "TWITCH_CONNECTION_READY";
138
- TwitchEventTypes2["CONNECTION_LOST"] = "TWITCH_CONNECTION_LOST";
139
- })(TwitchEventTypes ||= {});
140
- function normalizeChannel(channel) {
141
- return channel.startsWith("#") ? channel.slice(1) : channel;
142
- }
143
- function formatChannelForDisplay(channel) {
144
- const normalized = normalizeChannel(channel);
145
- return `#${normalized}`;
146
- }
147
- function getTwitchUserDisplayName(user) {
148
- return user.displayName || user.username;
149
- }
150
- function stripMarkdownForTwitch(text) {
151
- return text.replace(/\*\*([^*]+)\*\*/g, "$1").replace(/__([^_]+)__/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/_([^_]+)_/g, "$1").replace(/~~([^~]+)~~/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/```[\s\S]*?```/g, "[code block]").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/^>\s+/gm, "").replace(/^[-*+]\s+/gm, "• ").replace(/^\d+\.\s+/gm, "• ").replace(/\n{3,}/g, `
152
-
153
- `).trim();
154
- }
155
- function splitMessageForTwitch(text, maxLength = MAX_TWITCH_MESSAGE_LENGTH) {
156
- if (text.length <= maxLength) {
157
- return [text];
158
- }
159
- const chunks = [];
160
- let remaining = text;
161
- while (remaining.length > 0) {
162
- if (remaining.length <= maxLength) {
163
- chunks.push(remaining);
164
- break;
165
- }
166
- let splitIndex = remaining.lastIndexOf(". ", maxLength);
167
- if (splitIndex === -1 || splitIndex < maxLength / 2) {
168
- splitIndex = remaining.lastIndexOf(" ", maxLength);
169
- }
170
- if (splitIndex === -1 || splitIndex < maxLength / 2) {
171
- splitIndex = maxLength;
172
- }
173
- chunks.push(remaining.slice(0, splitIndex).trim());
174
- remaining = remaining.slice(splitIndex).trim();
175
- }
176
- return chunks;
177
- }
178
-
179
- class TwitchPluginError extends Error {
180
- constructor(message) {
181
- super(message);
182
- this.name = "TwitchPluginError";
183
- }
184
- }
185
-
186
- class TwitchServiceNotInitializedError extends TwitchPluginError {
187
- constructor(message = "Twitch service is not initialized") {
188
- super(message);
189
- this.name = "TwitchServiceNotInitializedError";
190
- }
191
- }
192
-
193
- class TwitchNotConnectedError extends TwitchPluginError {
194
- constructor(message = "Twitch client is not connected") {
195
- super(message);
196
- this.name = "TwitchNotConnectedError";
197
- }
198
- }
199
-
200
- class TwitchConfigurationError extends TwitchPluginError {
201
- settingName;
202
- constructor(message, settingName) {
203
- super(message);
204
- this.name = "TwitchConfigurationError";
205
- this.settingName = settingName;
206
- }
207
- }
208
-
209
- class TwitchApiError extends TwitchPluginError {
210
- statusCode;
211
- constructor(message, statusCode) {
212
- super(message);
213
- this.name = "TwitchApiError";
214
- this.statusCode = statusCode;
215
- }
216
- }
217
-
218
- // src/service.ts
219
- var TWITCH_CONNECTOR_CONTEXTS = ["social", "connectors"];
220
- var TWITCH_CONNECTOR_CAPABILITIES = [
221
- "send_message",
222
- "resolve_targets",
223
- "list_rooms",
224
- "join",
225
- "leave",
226
- "chat_context"
227
- ];
228
- function normalizeTwitchConnectorQuery(value) {
229
- return normalizeChannel(value.trim().replace(/^@/, "")).toLowerCase();
230
- }
231
- function scoreTwitchChannelMatch(query, channel) {
232
- const normalized = normalizeTwitchConnectorQuery(channel);
233
- if (!query) {
234
- return 0.45;
235
- }
236
- if (normalized === query) {
237
- return 1;
238
- }
239
- if (normalized.startsWith(query)) {
240
- return 0.85;
241
- }
242
- if (normalized.includes(query)) {
243
- return 0.7;
244
- }
245
- return 0;
246
- }
247
- async function logTwurpleCall(op, context, fn) {
248
- const startedAt = Date.now();
249
- logger.debug({ sdk: "twurple", op, ...context }, `[TwitchService] ${op} started`);
250
- try {
251
- const result = await fn();
252
- logger.info({
253
- sdk: "twurple",
254
- op,
255
- ...context,
256
- durationMs: Date.now() - startedAt
257
- }, `[TwitchService] ${op} ok`);
258
- return result;
259
- } catch (error) {
260
- const message = error instanceof Error ? error.message : String(error);
261
- logger.warn({
262
- sdk: "twurple",
263
- op,
264
- ...context,
265
- durationMs: Date.now() - startedAt,
266
- error: message
267
- }, `[TwitchService] ${op} failed`);
268
- throw error;
269
- }
270
- }
271
-
272
- class TwitchService extends Service {
273
- static serviceType = TWITCH_SERVICE_NAME;
274
- capabilityDescription = "Provides Twitch chat integration for sending and receiving messages";
275
- settings;
276
- client;
277
- connected = false;
278
- joinedChannels = new Set;
279
- accountServices = new Map;
280
- static async start(runtime) {
281
- const service = new TwitchService;
282
- await service.initialize(runtime);
283
- return service;
284
- }
285
- static registerSendHandlers(runtime, serviceInstance) {
286
- if (!serviceInstance) {
287
- return;
288
- }
289
- for (const accountService of serviceInstance.getAccountServiceList()) {
290
- const accountId = accountService.getAccountId(runtime);
291
- const sendHandler = accountService.handleSendMessage.bind(accountService);
292
- if (typeof runtime.registerMessageConnector === "function") {
293
- runtime.registerMessageConnector({
294
- source: "twitch",
295
- accountId,
296
- label: "Twitch",
297
- description: "Twitch public chat connector for sending messages to joined channels.",
298
- capabilities: [...TWITCH_CONNECTOR_CAPABILITIES],
299
- supportedTargetKinds: ["channel"],
300
- contexts: [...TWITCH_CONNECTOR_CONTEXTS],
301
- metadata: {
302
- accountId,
303
- service: TWITCH_SERVICE_NAME,
304
- maxMessageLength: MAX_TWITCH_MESSAGE_LENGTH
305
- },
306
- resolveTargets: accountService.resolveConnectorTargets.bind(accountService),
307
- listRecentTargets: accountService.listRecentConnectorTargets.bind(accountService),
308
- listRooms: accountService.listConnectorRooms.bind(accountService),
309
- joinHandler: accountService.handleJoinChannel.bind(accountService),
310
- leaveHandler: accountService.handleLeaveChannel.bind(accountService),
311
- getChatContext: accountService.getConnectorChatContext.bind(accountService),
312
- sendHandler
313
- });
314
- runtime.logger.info({ src: "plugin:twitch", agentId: runtime.agentId }, "Registered Twitch chat connector");
315
- continue;
316
- }
317
- }
318
- }
319
- static async stopRuntime(runtime) {
320
- const service = runtime.getService(TWITCH_SERVICE_NAME);
321
- if (service) {
322
- await service.stop();
323
- }
324
- }
325
- async initialize(runtime) {
326
- this.runtime = runtime;
327
- const startedAccounts = [];
328
- for (const accountId of listTwitchAccountIds(runtime)) {
329
- const settings = resolveTwitchAccountSettings(runtime, accountId);
330
- if (settings.enabled === false) {
331
- continue;
332
- }
333
- const accountService = new TwitchService;
334
- await accountService.initializeAccount(runtime, accountId);
335
- this.accountServices.set(accountService.getAccountId(), accountService);
336
- startedAccounts.push(accountService.getAccountId());
337
- }
338
- if (startedAccounts.length === 0) {
339
- logger.warn("No enabled Twitch accounts configured");
340
- return;
341
- }
342
- logger.info(`Twitch service started ${startedAccounts.length} account(s): ${startedAccounts.join(", ")}`);
343
- }
344
- async initializeAccount(runtime, accountId) {
345
- this.runtime = runtime;
346
- this.settings = this.loadSettings(accountId);
347
- this.validateSettings();
348
- const authProvider = await this.createAuthProvider();
349
- const allChannels = [
350
- this.settings.channel,
351
- ...this.settings.additionalChannels
352
- ].map(normalizeChannel);
353
- this.client = new ChatClient({
354
- authProvider,
355
- channels: allChannels,
356
- rejoinChannelsOnReconnect: true
357
- });
358
- this.setupEventHandlers();
359
- await this.connect();
360
- logger.info(`Twitch service initialized for ${this.settings.username}, joined channels: ${allChannels.join(", ")}`);
361
- }
362
- loadSettings(accountId) {
363
- return resolveTwitchAccountSettings(this.runtime, accountId);
364
- }
365
- validateSettings() {
366
- if (!this.settings.username) {
367
- throw new TwitchConfigurationError("TWITCH_USERNAME is required", "TWITCH_USERNAME");
368
- }
369
- if (!this.settings.clientId) {
370
- throw new TwitchConfigurationError("TWITCH_CLIENT_ID is required", "TWITCH_CLIENT_ID");
371
- }
372
- if (!this.settings.accessToken) {
373
- throw new TwitchConfigurationError("TWITCH_ACCESS_TOKEN is required", "TWITCH_ACCESS_TOKEN");
374
- }
375
- if (!this.settings.channel) {
376
- throw new TwitchConfigurationError("TWITCH_CHANNEL is required", "TWITCH_CHANNEL");
377
- }
378
- }
379
- async createAuthProvider() {
380
- const token = this.normalizeToken(this.settings.accessToken);
381
- if (this.settings.clientSecret) {
382
- const authProvider = new RefreshingAuthProvider({
383
- clientId: this.settings.clientId,
384
- clientSecret: this.settings.clientSecret
385
- });
386
- await authProvider.addUserForToken({
387
- accessToken: token,
388
- refreshToken: this.settings.refreshToken || null,
389
- expiresIn: null,
390
- obtainmentTimestamp: Date.now()
391
- });
392
- authProvider.onRefresh((userId, newToken) => {
393
- logger.info(`Twitch token refreshed for user ${userId}, expires in ${newToken.expiresIn}s`);
394
- });
395
- authProvider.onRefreshFailure((userId, error) => {
396
- logger.error(`Twitch token refresh failed for user ${userId}: ${error.message}`);
397
- });
398
- logger.info(`Using RefreshingAuthProvider for ${this.settings.username}`);
399
- return authProvider;
400
- }
401
- logger.info(`Using StaticAuthProvider for ${this.settings.username}`);
402
- return new StaticAuthProvider(this.settings.clientId, token);
403
- }
404
- normalizeToken(token) {
405
- return token.startsWith("oauth:") ? token.slice(6) : token;
406
- }
407
- setupEventHandlers() {
408
- this.client.onConnect(() => {
409
- this.connected = true;
410
- logger.info("Twitch chat connected");
411
- this.runtime.emitEvent("TWITCH_CONNECTION_READY" /* CONNECTION_READY */, {
412
- runtime: this.runtime,
413
- accountId: this.getAccountId()
414
- });
415
- });
416
- this.client.onDisconnect((_manually, reason) => {
417
- this.connected = false;
418
- logger.warn(`Twitch chat disconnected: ${reason || "unknown reason"}`);
419
- this.runtime.emitEvent("TWITCH_CONNECTION_LOST" /* CONNECTION_LOST */, {
420
- runtime: this.runtime,
421
- accountId: this.getAccountId(),
422
- reason
423
- });
424
- });
425
- this.client.onJoin((channel, user) => {
426
- const normalized = normalizeChannel(channel);
427
- if (user.toLowerCase() === this.settings.username.toLowerCase()) {
428
- this.joinedChannels.add(normalized);
429
- logger.info(`Joined Twitch channel: ${normalized}`);
430
- this.runtime.emitEvent("TWITCH_JOIN_CHANNEL" /* JOIN_CHANNEL */, {
431
- runtime: this.runtime,
432
- accountId: this.getAccountId(),
433
- channel: normalized
434
- });
435
- }
436
- });
437
- this.client.onPart((channel, user) => {
438
- const normalized = normalizeChannel(channel);
439
- if (user.toLowerCase() === this.settings.username.toLowerCase()) {
440
- this.joinedChannels.delete(normalized);
441
- logger.info(`Left Twitch channel: ${normalized}`);
442
- this.runtime.emitEvent("TWITCH_LEAVE_CHANNEL" /* LEAVE_CHANNEL */, {
443
- runtime: this.runtime,
444
- accountId: this.getAccountId(),
445
- channel: normalized
446
- });
447
- }
448
- });
449
- this.client.onMessage((channel, user, text, msg) => {
450
- this.handleMessage(channel, user, text, msg);
451
- });
452
- }
453
- handleMessage(channel, _user, text, msg) {
454
- const normalizedChannel = normalizeChannel(channel);
455
- if (msg.userInfo.userName.toLowerCase() === this.settings.username.toLowerCase()) {
456
- return;
457
- }
458
- const userInfo = {
459
- userId: msg.userInfo.userId,
460
- username: msg.userInfo.userName,
461
- displayName: msg.userInfo.displayName,
462
- isModerator: msg.userInfo.isMod,
463
- isBroadcaster: msg.userInfo.isBroadcaster,
464
- isVip: msg.userInfo.isVip,
465
- isSubscriber: msg.userInfo.isSubscriber,
466
- color: msg.userInfo.color,
467
- badges: msg.userInfo.badges
468
- };
469
- if (!this.isUserAllowed(userInfo)) {
470
- return;
471
- }
472
- if (this.settings.requireMention) {
473
- const mentionPattern = new RegExp(`@${this.settings.username}\\b`, "i");
474
- if (!mentionPattern.test(text)) {
475
- return;
476
- }
477
- }
478
- const message = {
479
- id: msg.id,
480
- channel: normalizedChannel,
481
- text,
482
- user: userInfo,
483
- timestamp: new Date,
484
- isAction: msg.isCheer,
485
- isHighlighted: msg.isHighlight,
486
- replyTo: msg.parentMessageId ? {
487
- messageId: msg.parentMessageId,
488
- userId: msg.parentMessageUserId || "",
489
- username: msg.parentMessageUserName || "",
490
- text: msg.parentMessageText || ""
491
- } : undefined
492
- };
493
- logger.debug(`Twitch message from ${userInfo.displayName} in #${normalizedChannel}: ${text.slice(0, 50)}...`);
494
- this.runtime.emitEvent("TWITCH_MESSAGE_RECEIVED" /* MESSAGE_RECEIVED */, {
495
- runtime: this.runtime,
496
- accountId: this.getAccountId(),
497
- message
498
- });
499
- }
500
- async connect() {
501
- await this.client.connect();
502
- this.connected = true;
503
- }
504
- async stop() {
505
- if (this.accountServices?.size > 0) {
506
- await Promise.all(Array.from(this.accountServices.values()).map((service) => service.stop()));
507
- this.accountServices.clear();
508
- logger.info("Twitch service stopped");
509
- return;
510
- }
511
- if (this.client) {
512
- this.client.quit();
513
- }
514
- this.connected = false;
515
- this.joinedChannels.clear();
516
- logger.info("Twitch service stopped");
517
- }
518
- getAccountServiceList() {
519
- return this.accountServices?.size > 0 ? Array.from(this.accountServices.values()) : [this];
520
- }
521
- getDefaultAccountService() {
522
- if (!this.accountServices || this.accountServices.size === 0) {
523
- return this;
524
- }
525
- const defaultAccountId = normalizeTwitchAccountId(resolveDefaultTwitchAccountId(this.runtime));
526
- return this.accountServices.get(defaultAccountId) ?? Array.from(this.accountServices.values())[0];
527
- }
528
- getAccountService(accountId) {
529
- if (!this.accountServices || this.accountServices.size === 0) {
530
- const ownAccountId = this.getAccountId();
531
- if (normalizeTwitchAccountId(accountId) !== ownAccountId) {
532
- throw new Error(`Twitch account '${accountId}' is not available in this service instance`);
533
- }
534
- return this;
535
- }
536
- const normalized = normalizeTwitchAccountId(accountId);
537
- const service = this.accountServices.get(normalized);
538
- if (!service) {
539
- throw new Error(`Twitch account '${normalized}' is not available`);
540
- }
541
- return service;
542
- }
543
- isConnected() {
544
- if (this.accountServices?.size > 0) {
545
- return Array.from(this.accountServices.values()).some((service) => service.isConnected());
546
- }
547
- return this.connected;
548
- }
549
- getBotUsername() {
550
- if (this.accountServices?.size > 0) {
551
- return this.getDefaultAccountService().getBotUsername();
552
- }
553
- return this.settings.username;
554
- }
555
- getAccountId(runtime) {
556
- if (this.accountServices?.size > 0) {
557
- return this.getDefaultAccountService().getAccountId(runtime);
558
- }
559
- return normalizeTwitchAccountId(this.settings?.accountId ?? (runtime ? resolveDefaultTwitchAccountId(runtime) : undefined));
560
- }
561
- getPrimaryChannel() {
562
- if (this.accountServices?.size > 0) {
563
- return this.getDefaultAccountService().getPrimaryChannel();
564
- }
565
- return this.settings.channel;
566
- }
567
- getJoinedChannels() {
568
- if (this.accountServices?.size > 0) {
569
- return this.getDefaultAccountService().getJoinedChannels();
570
- }
571
- return Array.from(this.joinedChannels);
572
- }
573
- async handleSendMessage(runtime, target, content) {
574
- const requestedAccountId = normalizeTwitchAccountId(target.accountId ?? readTwitchAccountId(content, target) ?? this.getAccountId());
575
- if (this.accountServices?.size > 0) {
576
- await this.getAccountService(requestedAccountId).handleSendMessage(runtime, target, content);
577
- return;
578
- }
579
- if (requestedAccountId !== this.getAccountId()) {
580
- throw new Error(`Twitch account '${requestedAccountId}' is not available in this service instance`);
581
- }
582
- const text = typeof content.text === "string" ? content.text.trim() : "";
583
- if (!text) {
584
- throw new Error("Twitch connector requires non-empty text content.");
585
- }
586
- let channel = target.channelId;
587
- let replyTo = target.threadId;
588
- if (target.roomId && !channel) {
589
- const room = await runtime.getRoom(target.roomId);
590
- channel = room?.channelId;
591
- const metadata = room?.metadata;
592
- replyTo = replyTo ?? (typeof metadata?.twitchReplyTo === "string" ? metadata.twitchReplyTo : undefined);
593
- }
594
- await this.sendMessage(text, {
595
- channel: channel ? normalizeChannel(channel) : this.getPrimaryChannel(),
596
- replyTo
597
- });
598
- }
599
- async resolveConnectorTargets(query, _context) {
600
- const normalizedQuery = normalizeTwitchConnectorQuery(query);
601
- return this.getConnectorChannels().map((channel) => {
602
- const score = scoreTwitchChannelMatch(normalizedQuery, channel);
603
- return score > 0 ? this.buildChannelTarget(channel, score) : null;
604
- }).filter((target) => Boolean(target)).slice(0, 25);
605
- }
606
- async listConnectorRooms(_context) {
607
- return this.getConnectorChannels().map((channel) => this.buildChannelTarget(channel, 0.5)).slice(0, 50);
608
- }
609
- async listRecentConnectorTargets(context) {
610
- const targets = [];
611
- const room = context.roomId && typeof context.runtime.getRoom === "function" ? await context.runtime.getRoom(context.roomId) : null;
612
- const channel = context.target?.channelId ?? room?.channelId;
613
- if (channel) {
614
- targets.push(this.buildChannelTarget(channel, 0.95));
615
- }
616
- targets.push(...await this.listConnectorRooms(context));
617
- const seen = new Set;
618
- return targets.filter((target) => {
619
- const channelId = target.target.channelId;
620
- if (!channelId || seen.has(channelId)) {
621
- return false;
622
- }
623
- seen.add(channelId);
624
- return true;
625
- }).slice(0, 25);
626
- }
627
- async handleJoinChannel(_runtime, params) {
628
- const channel = this.resolveChannelOpTarget(params);
629
- if (!channel) {
630
- throw new Error("Twitch MESSAGE operation=join requires channelId, alias, or target channel.");
631
- }
632
- if (this.joinedChannels.has(channel)) {
633
- return;
634
- }
635
- await this.joinChannel(channel);
636
- }
637
- async handleLeaveChannel(_runtime, params) {
638
- const channel = this.resolveChannelOpTarget(params);
639
- if (!channel) {
640
- throw new Error("Twitch MESSAGE operation=leave requires channelId, alias, or target channel.");
641
- }
642
- if (channel === normalizeChannel(this.getPrimaryChannel())) {
643
- throw new Error(`Cannot leave the primary Twitch channel #${channel}.`);
644
- }
645
- if (!this.joinedChannels.has(channel)) {
646
- throw new Error(`Not currently in Twitch channel #${channel}.`);
647
- }
648
- await this.leaveChannel(channel);
649
- }
650
- async getConnectorChatContext(target, context) {
651
- let channel = target.channelId;
652
- if (!channel && target.roomId) {
653
- const room = await context.runtime.getRoom(target.roomId);
654
- channel = room?.channelId;
655
- }
656
- channel = channel ? normalizeChannel(channel) : this.getPrimaryChannel();
657
- return {
658
- target: {
659
- source: "twitch",
660
- accountId: this.getAccountId(),
661
- channelId: channel
662
- },
663
- label: formatChannelForDisplay(channel),
664
- summary: "Twitch chat messages are public and visible to viewers in the channel.",
665
- metadata: {
666
- accountId: this.getAccountId(),
667
- twitchChannel: channel,
668
- botUsername: this.getBotUsername(),
669
- joined: this.joinedChannels.has(channel)
670
- }
671
- };
672
- }
673
- getConnectorChannels() {
674
- const channels = new Set;
675
- if (this.settings?.channel) {
676
- channels.add(normalizeChannel(this.settings.channel));
677
- }
678
- for (const channel of this.settings?.additionalChannels ?? []) {
679
- channels.add(normalizeChannel(channel));
680
- }
681
- for (const channel of this.joinedChannels) {
682
- channels.add(normalizeChannel(channel));
683
- }
684
- return Array.from(channels);
685
- }
686
- resolveChannelOpTarget(params) {
687
- const targetRecord = params.target;
688
- const raw = params.target?.channelId ?? params.channelId ?? params.alias ?? targetRecord?.alias ?? targetRecord?.name;
689
- return typeof raw === "string" && raw.trim() ? normalizeChannel(raw.trim()) : null;
690
- }
691
- buildChannelTarget(channel, score) {
692
- const normalized = normalizeChannel(channel);
693
- return {
694
- target: {
695
- source: "twitch",
696
- accountId: this.getAccountId(),
697
- channelId: normalized
698
- },
699
- label: formatChannelForDisplay(normalized),
700
- kind: "channel",
701
- description: "Twitch public chat channel",
702
- score,
703
- contexts: [...TWITCH_CONNECTOR_CONTEXTS],
704
- metadata: {
705
- accountId: this.getAccountId(),
706
- twitchChannel: normalized,
707
- joined: this.joinedChannels.has(normalized),
708
- primary: normalized === this.getPrimaryChannel()
709
- }
710
- };
711
- }
712
- isUserAllowed(user) {
713
- if (this.settings.allowedUserIds.length > 0 && !this.settings.allowedUserIds.includes(user.userId)) {
714
- return false;
715
- }
716
- if (this.settings.allowedRoles.includes("all")) {
717
- return true;
718
- }
719
- if (this.settings.allowedRoles.includes("owner") && user.isBroadcaster) {
720
- return true;
721
- }
722
- if (this.settings.allowedRoles.includes("moderator") && user.isModerator) {
723
- return true;
724
- }
725
- if (this.settings.allowedRoles.includes("vip") && user.isVip) {
726
- return true;
727
- }
728
- if (this.settings.allowedRoles.includes("subscriber") && user.isSubscriber) {
729
- return true;
730
- }
731
- return false;
732
- }
733
- async sendMessage(text, options) {
734
- if (this.accountServices?.size > 0) {
735
- const accountId = normalizeTwitchAccountId(options?.accountId ?? this.getAccountId());
736
- return this.getAccountService(accountId).sendMessage(text, options);
737
- }
738
- if (!this.connected) {
739
- throw new TwitchNotConnectedError;
740
- }
741
- const channel = normalizeChannel(options?.channel || this.settings.channel);
742
- const cleanedText = stripMarkdownForTwitch(text);
743
- if (!cleanedText) {
744
- return { success: true, messageId: "skipped-empty" };
745
- }
746
- const chunks = splitMessageForTwitch(cleanedText);
747
- let lastMessageId;
748
- for (const chunk of chunks) {
749
- await logTwurpleCall("say", { channel, chunkLen: chunk.length, replyTo: options?.replyTo }, async () => {
750
- if (options?.replyTo) {
751
- await this.client.say(channel, chunk, {
752
- replyTo: options.replyTo
753
- });
754
- } else {
755
- await this.client.say(channel, chunk);
756
- }
757
- });
758
- lastMessageId = crypto.randomUUID();
759
- if (chunks.length > 1) {
760
- await new Promise((resolve) => setTimeout(resolve, 300));
761
- }
762
- }
763
- this.runtime.emitEvent("TWITCH_MESSAGE_SENT" /* MESSAGE_SENT */, {
764
- runtime: this.runtime,
765
- accountId: this.getAccountId(),
766
- channel,
767
- text: cleanedText,
768
- messageId: lastMessageId
769
- });
770
- return { success: true, messageId: lastMessageId };
771
- }
772
- async joinChannel(channel) {
773
- if (this.accountServices?.size > 0) {
774
- await this.getDefaultAccountService().joinChannel(channel);
775
- return;
776
- }
777
- const normalized = normalizeChannel(channel);
778
- await logTwurpleCall("join", { channel: normalized }, async () => {
779
- await this.client.join(normalized);
780
- });
781
- this.joinedChannels.add(normalized);
782
- }
783
- async leaveChannel(channel) {
784
- if (this.accountServices?.size > 0) {
785
- await this.getDefaultAccountService().leaveChannel(channel);
786
- return;
787
- }
788
- const normalized = normalizeChannel(channel);
789
- await logTwurpleCall("part", { channel: normalized }, async () => {
790
- await this.client.part(normalized);
791
- });
792
- this.joinedChannels.delete(normalized);
793
- }
794
- }
795
- // src/connector-account-provider.ts
796
- var TWITCH_PROVIDER_ID = "twitch";
797
- function toConnectorAccount(settings) {
798
- const now = Date.now();
799
- const configured = Boolean(settings.username && settings.clientId && settings.accessToken);
800
- return {
801
- id: normalizeTwitchAccountId(settings.accountId),
802
- provider: TWITCH_PROVIDER_ID,
803
- label: settings.username || settings.channel || settings.accountId,
804
- role: "OWNER",
805
- purpose: ["messaging"],
806
- accessGate: "open",
807
- status: settings.enabled !== false && configured ? "connected" : "disabled",
808
- externalId: settings.username || undefined,
809
- displayHandle: settings.username || undefined,
810
- createdAt: now,
811
- updatedAt: now,
812
- metadata: {
813
- channel: settings.channel ?? "",
814
- additionalChannels: settings.additionalChannels ?? [],
815
- requireMention: settings.requireMention ?? false,
816
- hasRefreshToken: Boolean(settings.refreshToken)
817
- }
818
- };
819
- }
820
- function createTwitchConnectorAccountProvider(runtime) {
821
- return {
822
- provider: TWITCH_PROVIDER_ID,
823
- label: "Twitch",
824
- listAccounts: async (_manager) => {
825
- const ids = listTwitchAccountIds(runtime);
826
- if (ids.length === 0) {
827
- return [
828
- toConnectorAccount(resolveTwitchAccountSettings(runtime, DEFAULT_TWITCH_ACCOUNT_ID))
829
- ];
830
- }
831
- return ids.map((id) => toConnectorAccount(resolveTwitchAccountSettings(runtime, id)));
832
- },
833
- createAccount: async (input, _manager) => {
834
- return {
835
- ...input,
836
- provider: TWITCH_PROVIDER_ID,
837
- role: input.role ?? "OWNER",
838
- purpose: input.purpose ?? ["messaging"],
839
- accessGate: input.accessGate ?? "open",
840
- status: input.status ?? "pending"
841
- };
842
- },
843
- patchAccount: async (_accountId, patch, _manager) => {
844
- return { ...patch, provider: TWITCH_PROVIDER_ID };
845
- },
846
- deleteAccount: async (_accountId, _manager) => {}
847
- };
848
- }
849
-
850
- // src/workflow-credential-provider.ts
851
- import { Service as Service2 } from "@elizaos/core";
852
- var WORKFLOW_CREDENTIAL_PROVIDER_TYPE = "workflow_credential_provider";
853
- var SUPPORTED = ["httpHeaderAuth"];
854
-
855
- class TwitchWorkflowCredentialProvider extends Service2 {
856
- static serviceType = WORKFLOW_CREDENTIAL_PROVIDER_TYPE;
857
- capabilityDescription = "Supplies Twitch credentials to the workflow plugin.";
858
- static async start(runtime) {
859
- return new TwitchWorkflowCredentialProvider(runtime);
860
- }
861
- async stop() {}
862
- async resolve(_userId, credType) {
863
- if (credType !== "httpHeaderAuth")
864
- return null;
865
- const accessToken = this.runtime.getSetting("TWITCH_ACCESS_TOKEN");
866
- if (!accessToken?.trim())
867
- return null;
868
- return {
869
- status: "credential_data",
870
- data: { name: "Authorization", value: `Bearer ${accessToken.trim()}` }
871
- };
872
- }
873
- checkCredentialTypes(credTypes) {
874
- return {
875
- supported: credTypes.filter((t) => SUPPORTED.includes(t)),
876
- unsupported: credTypes.filter((t) => !SUPPORTED.includes(t))
877
- };
878
- }
879
- }
880
-
881
- // src/index.ts
882
- var twitchPlugin = {
883
- name: "twitch",
884
- description: "Twitch chat integration plugin for ElizaOS with real-time messaging",
885
- services: [TwitchService, TwitchWorkflowCredentialProvider],
886
- actions: [],
887
- providers: [],
888
- tests: [],
889
- autoEnable: {
890
- connectorKeys: ["twitch"]
891
- },
892
- init: async (_config, runtime) => {
893
- try {
894
- const manager = getConnectorAccountManager(runtime);
895
- manager.registerProvider(createTwitchConnectorAccountProvider(runtime));
896
- } catch (err) {
897
- logger2.warn({
898
- src: "plugin:twitch",
899
- err: err instanceof Error ? err.message : String(err)
900
- }, "Failed to register Twitch provider with ConnectorAccountManager");
901
- }
902
- const username = runtime.getSetting("TWITCH_USERNAME");
903
- const clientId = runtime.getSetting("TWITCH_CLIENT_ID");
904
- const accessToken = runtime.getSetting("TWITCH_ACCESS_TOKEN");
905
- const channel = runtime.getSetting("TWITCH_CHANNEL");
906
- logger2.info("=".repeat(60));
907
- logger2.info("Twitch Plugin Configuration");
908
- logger2.info("=".repeat(60));
909
- logger2.info(` Username: ${username ? "✓ Set" : "✗ Missing (required)"}`);
910
- logger2.info(` Client ID: ${clientId ? "✓ Set" : "✗ Missing (required)"}`);
911
- logger2.info(` Access Token: ${accessToken ? "✓ Set" : "✗ Missing (required)"}`);
912
- logger2.info(` Channel: ${channel ? `✓ ${channel}` : "✗ Missing (required)"}`);
913
- logger2.info("=".repeat(60));
914
- const missing = [];
915
- if (!username)
916
- missing.push("TWITCH_USERNAME");
917
- if (!clientId)
918
- missing.push("TWITCH_CLIENT_ID");
919
- if (!accessToken)
920
- missing.push("TWITCH_ACCESS_TOKEN");
921
- if (!channel)
922
- missing.push("TWITCH_CHANNEL");
923
- if (missing.length > 0) {
924
- logger2.warn(`Twitch plugin: Missing required configuration: ${missing.join(", ")}`);
925
- }
926
- const clientSecret = runtime.getSetting("TWITCH_CLIENT_SECRET");
927
- const refreshToken = runtime.getSetting("TWITCH_REFRESH_TOKEN");
928
- const additionalChannels = runtime.getSetting("TWITCH_CHANNELS");
929
- const requireMention = runtime.getSetting("TWITCH_REQUIRE_MENTION");
930
- const allowedRoles = runtime.getSetting("TWITCH_ALLOWED_ROLES");
931
- if (clientSecret && refreshToken) {
932
- logger2.info(" Token Refresh: ✓ Enabled (client secret and refresh token set)");
933
- } else if (clientSecret || refreshToken) {
934
- logger2.warn(" Token Refresh: ⚠ Partial (need both TWITCH_CLIENT_SECRET and TWITCH_REFRESH_TOKEN)");
935
- }
936
- if (additionalChannels) {
937
- logger2.info(` Additional Channels: ${additionalChannels}`);
938
- }
939
- if (requireMention === "true") {
940
- logger2.info(" Require Mention: ✓ Enabled (will only respond to @mentions)");
941
- }
942
- if (allowedRoles) {
943
- logger2.info(` Allowed Roles: ${allowedRoles}`);
944
- }
945
- }
946
- };
947
- var src_default = twitchPlugin;
948
- export {
949
- stripMarkdownForTwitch,
950
- splitMessageForTwitch,
951
- resolveTwitchAccountSettings,
952
- resolveDefaultTwitchAccountId,
953
- readTwitchAccountId,
954
- normalizeTwitchAccountId,
955
- normalizeChannel,
956
- listTwitchAccountIds,
957
- getTwitchUserDisplayName,
958
- formatChannelForDisplay,
959
- src_default as default,
960
- TwitchServiceNotInitializedError,
961
- TwitchService,
962
- TwitchPluginError,
963
- TwitchNotConnectedError,
964
- TwitchEventTypes,
965
- TwitchConfigurationError,
966
- TwitchApiError,
967
- TWITCH_SERVICE_NAME,
968
- MAX_TWITCH_MESSAGE_LENGTH,
969
- DEFAULT_TWITCH_ACCOUNT_ID
970
- };
971
-
972
- //# debugId=E272CF35229333E264756E2164756E21