@elizaos/plugin-twitch 2.0.0-alpha.3
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/__tests__/integration.test.ts +889 -0
- package/build.ts +17 -0
- package/dist/index.js +1021 -0
- package/package.json +35 -0
- package/src/actions/joinChannel.ts +147 -0
- package/src/actions/leaveChannel.ts +172 -0
- package/src/actions/listChannels.ts +100 -0
- package/src/actions/sendMessage.ts +177 -0
- package/src/index.ts +117 -0
- package/src/providers/channelState.ts +98 -0
- package/src/providers/userContext.ts +117 -0
- package/src/service.ts +488 -0
- package/src/types.ts +340 -0
- package/tsconfig.json +21 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { logger as logger2 } from "@elizaos/core";
|
|
3
|
+
|
|
4
|
+
// src/service.ts
|
|
5
|
+
import {
|
|
6
|
+
logger,
|
|
7
|
+
Service
|
|
8
|
+
} from "@elizaos/core";
|
|
9
|
+
import { RefreshingAuthProvider, StaticAuthProvider } from "@twurple/auth";
|
|
10
|
+
import { ChatClient } from "@twurple/chat";
|
|
11
|
+
|
|
12
|
+
// src/types.ts
|
|
13
|
+
var MAX_TWITCH_MESSAGE_LENGTH = 500;
|
|
14
|
+
var TWITCH_SERVICE_NAME = "twitch";
|
|
15
|
+
var TwitchEventTypes;
|
|
16
|
+
((TwitchEventTypes2) => {
|
|
17
|
+
TwitchEventTypes2["MESSAGE_RECEIVED"] = "TWITCH_MESSAGE_RECEIVED";
|
|
18
|
+
TwitchEventTypes2["MESSAGE_SENT"] = "TWITCH_MESSAGE_SENT";
|
|
19
|
+
TwitchEventTypes2["JOIN_CHANNEL"] = "TWITCH_JOIN_CHANNEL";
|
|
20
|
+
TwitchEventTypes2["LEAVE_CHANNEL"] = "TWITCH_LEAVE_CHANNEL";
|
|
21
|
+
TwitchEventTypes2["CONNECTION_READY"] = "TWITCH_CONNECTION_READY";
|
|
22
|
+
TwitchEventTypes2["CONNECTION_LOST"] = "TWITCH_CONNECTION_LOST";
|
|
23
|
+
})(TwitchEventTypes ||= {});
|
|
24
|
+
function normalizeChannel(channel) {
|
|
25
|
+
return channel.startsWith("#") ? channel.slice(1) : channel;
|
|
26
|
+
}
|
|
27
|
+
function formatChannelForDisplay(channel) {
|
|
28
|
+
const normalized = normalizeChannel(channel);
|
|
29
|
+
return `#${normalized}`;
|
|
30
|
+
}
|
|
31
|
+
function getTwitchUserDisplayName(user) {
|
|
32
|
+
return user.displayName || user.username;
|
|
33
|
+
}
|
|
34
|
+
function stripMarkdownForTwitch(text) {
|
|
35
|
+
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, `
|
|
36
|
+
|
|
37
|
+
`).trim();
|
|
38
|
+
}
|
|
39
|
+
function splitMessageForTwitch(text, maxLength = MAX_TWITCH_MESSAGE_LENGTH) {
|
|
40
|
+
if (text.length <= maxLength) {
|
|
41
|
+
return [text];
|
|
42
|
+
}
|
|
43
|
+
const chunks = [];
|
|
44
|
+
let remaining = text;
|
|
45
|
+
while (remaining.length > 0) {
|
|
46
|
+
if (remaining.length <= maxLength) {
|
|
47
|
+
chunks.push(remaining);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
let splitIndex = remaining.lastIndexOf(". ", maxLength);
|
|
51
|
+
if (splitIndex === -1 || splitIndex < maxLength / 2) {
|
|
52
|
+
splitIndex = remaining.lastIndexOf(" ", maxLength);
|
|
53
|
+
}
|
|
54
|
+
if (splitIndex === -1 || splitIndex < maxLength / 2) {
|
|
55
|
+
splitIndex = maxLength;
|
|
56
|
+
}
|
|
57
|
+
chunks.push(remaining.slice(0, splitIndex).trim());
|
|
58
|
+
remaining = remaining.slice(splitIndex).trim();
|
|
59
|
+
}
|
|
60
|
+
return chunks;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class TwitchPluginError extends Error {
|
|
64
|
+
constructor(message) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = "TwitchPluginError";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class TwitchServiceNotInitializedError extends TwitchPluginError {
|
|
71
|
+
constructor(message = "Twitch service is not initialized") {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = "TwitchServiceNotInitializedError";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class TwitchNotConnectedError extends TwitchPluginError {
|
|
78
|
+
constructor(message = "Twitch client is not connected") {
|
|
79
|
+
super(message);
|
|
80
|
+
this.name = "TwitchNotConnectedError";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class TwitchConfigurationError extends TwitchPluginError {
|
|
85
|
+
settingName;
|
|
86
|
+
constructor(message, settingName) {
|
|
87
|
+
super(message);
|
|
88
|
+
this.name = "TwitchConfigurationError";
|
|
89
|
+
this.settingName = settingName;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class TwitchApiError extends TwitchPluginError {
|
|
94
|
+
statusCode;
|
|
95
|
+
constructor(message, statusCode) {
|
|
96
|
+
super(message);
|
|
97
|
+
this.name = "TwitchApiError";
|
|
98
|
+
this.statusCode = statusCode;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/service.ts
|
|
103
|
+
class TwitchService extends Service {
|
|
104
|
+
static serviceType = TWITCH_SERVICE_NAME;
|
|
105
|
+
capabilityDescription = "Provides Twitch chat integration for sending and receiving messages";
|
|
106
|
+
settings;
|
|
107
|
+
client;
|
|
108
|
+
connected = false;
|
|
109
|
+
joinedChannels = new Set;
|
|
110
|
+
static async start(runtime) {
|
|
111
|
+
const service = new TwitchService;
|
|
112
|
+
await service.initialize(runtime);
|
|
113
|
+
return service;
|
|
114
|
+
}
|
|
115
|
+
static async stopRuntime(runtime) {
|
|
116
|
+
const service = runtime.getService(TWITCH_SERVICE_NAME);
|
|
117
|
+
if (service) {
|
|
118
|
+
await service.stop();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async initialize(runtime) {
|
|
122
|
+
this.runtime = runtime;
|
|
123
|
+
this.settings = this.loadSettings();
|
|
124
|
+
this.validateSettings();
|
|
125
|
+
const authProvider = await this.createAuthProvider();
|
|
126
|
+
const allChannels = [
|
|
127
|
+
this.settings.channel,
|
|
128
|
+
...this.settings.additionalChannels
|
|
129
|
+
].map(normalizeChannel);
|
|
130
|
+
this.client = new ChatClient({
|
|
131
|
+
authProvider,
|
|
132
|
+
channels: allChannels,
|
|
133
|
+
rejoinChannelsOnReconnect: true
|
|
134
|
+
});
|
|
135
|
+
this.setupEventHandlers();
|
|
136
|
+
await this.connect();
|
|
137
|
+
logger.info(`Twitch service initialized for ${this.settings.username}, joined channels: ${allChannels.join(", ")}`);
|
|
138
|
+
}
|
|
139
|
+
loadSettings() {
|
|
140
|
+
const username = this.runtime.getSetting("TWITCH_USERNAME");
|
|
141
|
+
const clientId = this.runtime.getSetting("TWITCH_CLIENT_ID");
|
|
142
|
+
const accessToken = this.runtime.getSetting("TWITCH_ACCESS_TOKEN");
|
|
143
|
+
const clientSecret = this.runtime.getSetting("TWITCH_CLIENT_SECRET");
|
|
144
|
+
const refreshToken = this.runtime.getSetting("TWITCH_REFRESH_TOKEN");
|
|
145
|
+
const channel = this.runtime.getSetting("TWITCH_CHANNEL");
|
|
146
|
+
const additionalChannelsStr = this.runtime.getSetting("TWITCH_CHANNELS");
|
|
147
|
+
const requireMentionStr = this.runtime.getSetting("TWITCH_REQUIRE_MENTION");
|
|
148
|
+
const allowedRolesStr = this.runtime.getSetting("TWITCH_ALLOWED_ROLES");
|
|
149
|
+
const additionalChannels = typeof additionalChannelsStr === "string" && additionalChannelsStr ? additionalChannelsStr.split(",").map((c) => c.trim()).filter(Boolean) : [];
|
|
150
|
+
const allowedRoles = typeof allowedRolesStr === "string" && allowedRolesStr ? allowedRolesStr.split(",").map((r) => r.trim().toLowerCase()) : ["all"];
|
|
151
|
+
return {
|
|
152
|
+
username: typeof username === "string" ? username : "",
|
|
153
|
+
clientId: typeof clientId === "string" ? clientId : "",
|
|
154
|
+
accessToken: typeof accessToken === "string" ? accessToken : "",
|
|
155
|
+
clientSecret: typeof clientSecret === "string" ? clientSecret : undefined,
|
|
156
|
+
refreshToken: typeof refreshToken === "string" ? refreshToken : undefined,
|
|
157
|
+
channel: typeof channel === "string" ? channel : "",
|
|
158
|
+
additionalChannels,
|
|
159
|
+
requireMention: requireMentionStr === "true",
|
|
160
|
+
allowedRoles,
|
|
161
|
+
allowedUserIds: [],
|
|
162
|
+
enabled: true
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
validateSettings() {
|
|
166
|
+
if (!this.settings.username) {
|
|
167
|
+
throw new TwitchConfigurationError("TWITCH_USERNAME is required", "TWITCH_USERNAME");
|
|
168
|
+
}
|
|
169
|
+
if (!this.settings.clientId) {
|
|
170
|
+
throw new TwitchConfigurationError("TWITCH_CLIENT_ID is required", "TWITCH_CLIENT_ID");
|
|
171
|
+
}
|
|
172
|
+
if (!this.settings.accessToken) {
|
|
173
|
+
throw new TwitchConfigurationError("TWITCH_ACCESS_TOKEN is required", "TWITCH_ACCESS_TOKEN");
|
|
174
|
+
}
|
|
175
|
+
if (!this.settings.channel) {
|
|
176
|
+
throw new TwitchConfigurationError("TWITCH_CHANNEL is required", "TWITCH_CHANNEL");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async createAuthProvider() {
|
|
180
|
+
const token = this.normalizeToken(this.settings.accessToken);
|
|
181
|
+
if (this.settings.clientSecret) {
|
|
182
|
+
const authProvider = new RefreshingAuthProvider({
|
|
183
|
+
clientId: this.settings.clientId,
|
|
184
|
+
clientSecret: this.settings.clientSecret
|
|
185
|
+
});
|
|
186
|
+
await authProvider.addUserForToken({
|
|
187
|
+
accessToken: token,
|
|
188
|
+
refreshToken: this.settings.refreshToken || null,
|
|
189
|
+
expiresIn: null,
|
|
190
|
+
obtainmentTimestamp: Date.now()
|
|
191
|
+
});
|
|
192
|
+
authProvider.onRefresh((userId, newToken) => {
|
|
193
|
+
logger.info(`Twitch token refreshed for user ${userId}, expires in ${newToken.expiresIn}s`);
|
|
194
|
+
});
|
|
195
|
+
authProvider.onRefreshFailure((userId, error) => {
|
|
196
|
+
logger.error(`Twitch token refresh failed for user ${userId}: ${error.message}`);
|
|
197
|
+
});
|
|
198
|
+
logger.info(`Using RefreshingAuthProvider for ${this.settings.username}`);
|
|
199
|
+
return authProvider;
|
|
200
|
+
}
|
|
201
|
+
logger.info(`Using StaticAuthProvider for ${this.settings.username}`);
|
|
202
|
+
return new StaticAuthProvider(this.settings.clientId, token);
|
|
203
|
+
}
|
|
204
|
+
normalizeToken(token) {
|
|
205
|
+
return token.startsWith("oauth:") ? token.slice(6) : token;
|
|
206
|
+
}
|
|
207
|
+
setupEventHandlers() {
|
|
208
|
+
this.client.onConnect(() => {
|
|
209
|
+
this.connected = true;
|
|
210
|
+
logger.info("Twitch chat connected");
|
|
211
|
+
this.runtime.emitEvent("TWITCH_CONNECTION_READY" /* CONNECTION_READY */, {
|
|
212
|
+
runtime: this.runtime
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
this.client.onDisconnect((_manually, reason) => {
|
|
216
|
+
this.connected = false;
|
|
217
|
+
logger.warn(`Twitch chat disconnected: ${reason || "unknown reason"}`);
|
|
218
|
+
this.runtime.emitEvent("TWITCH_CONNECTION_LOST" /* CONNECTION_LOST */, {
|
|
219
|
+
runtime: this.runtime,
|
|
220
|
+
reason
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
this.client.onJoin((channel, user) => {
|
|
224
|
+
const normalized = normalizeChannel(channel);
|
|
225
|
+
if (user.toLowerCase() === this.settings.username.toLowerCase()) {
|
|
226
|
+
this.joinedChannels.add(normalized);
|
|
227
|
+
logger.info(`Joined Twitch channel: ${normalized}`);
|
|
228
|
+
this.runtime.emitEvent("TWITCH_JOIN_CHANNEL" /* JOIN_CHANNEL */, {
|
|
229
|
+
runtime: this.runtime,
|
|
230
|
+
channel: normalized
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
this.client.onPart((channel, user) => {
|
|
235
|
+
const normalized = normalizeChannel(channel);
|
|
236
|
+
if (user.toLowerCase() === this.settings.username.toLowerCase()) {
|
|
237
|
+
this.joinedChannels.delete(normalized);
|
|
238
|
+
logger.info(`Left Twitch channel: ${normalized}`);
|
|
239
|
+
this.runtime.emitEvent("TWITCH_LEAVE_CHANNEL" /* LEAVE_CHANNEL */, {
|
|
240
|
+
runtime: this.runtime,
|
|
241
|
+
channel: normalized
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
this.client.onMessage((channel, user, text, msg) => {
|
|
246
|
+
this.handleMessage(channel, user, text, msg);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
handleMessage(channel, _user, text, msg) {
|
|
250
|
+
const normalizedChannel = normalizeChannel(channel);
|
|
251
|
+
if (msg.userInfo.userName.toLowerCase() === this.settings.username.toLowerCase()) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const userInfo = {
|
|
255
|
+
userId: msg.userInfo.userId,
|
|
256
|
+
username: msg.userInfo.userName,
|
|
257
|
+
displayName: msg.userInfo.displayName,
|
|
258
|
+
isModerator: msg.userInfo.isMod,
|
|
259
|
+
isBroadcaster: msg.userInfo.isBroadcaster,
|
|
260
|
+
isVip: msg.userInfo.isVip,
|
|
261
|
+
isSubscriber: msg.userInfo.isSubscriber,
|
|
262
|
+
color: msg.userInfo.color,
|
|
263
|
+
badges: msg.userInfo.badges
|
|
264
|
+
};
|
|
265
|
+
if (!this.isUserAllowed(userInfo)) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (this.settings.requireMention) {
|
|
269
|
+
const mentionPattern = new RegExp(`@${this.settings.username}\\b`, "i");
|
|
270
|
+
if (!mentionPattern.test(text)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const message = {
|
|
275
|
+
id: msg.id,
|
|
276
|
+
channel: normalizedChannel,
|
|
277
|
+
text,
|
|
278
|
+
user: userInfo,
|
|
279
|
+
timestamp: new Date,
|
|
280
|
+
isAction: msg.isCheer,
|
|
281
|
+
isHighlighted: msg.isHighlight,
|
|
282
|
+
replyTo: msg.parentMessageId ? {
|
|
283
|
+
messageId: msg.parentMessageId,
|
|
284
|
+
userId: msg.parentMessageUserId || "",
|
|
285
|
+
username: msg.parentMessageUserName || "",
|
|
286
|
+
text: msg.parentMessageText || ""
|
|
287
|
+
} : undefined
|
|
288
|
+
};
|
|
289
|
+
logger.debug(`Twitch message from ${userInfo.displayName} in #${normalizedChannel}: ${text.slice(0, 50)}...`);
|
|
290
|
+
this.runtime.emitEvent("TWITCH_MESSAGE_RECEIVED" /* MESSAGE_RECEIVED */, {
|
|
291
|
+
runtime: this.runtime,
|
|
292
|
+
message
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async connect() {
|
|
296
|
+
await this.client.connect();
|
|
297
|
+
this.connected = true;
|
|
298
|
+
}
|
|
299
|
+
async stop() {
|
|
300
|
+
if (this.client) {
|
|
301
|
+
this.client.quit();
|
|
302
|
+
}
|
|
303
|
+
this.connected = false;
|
|
304
|
+
this.joinedChannels.clear();
|
|
305
|
+
logger.info("Twitch service stopped");
|
|
306
|
+
}
|
|
307
|
+
isConnected() {
|
|
308
|
+
return this.connected;
|
|
309
|
+
}
|
|
310
|
+
getBotUsername() {
|
|
311
|
+
return this.settings.username;
|
|
312
|
+
}
|
|
313
|
+
getPrimaryChannel() {
|
|
314
|
+
return this.settings.channel;
|
|
315
|
+
}
|
|
316
|
+
getJoinedChannels() {
|
|
317
|
+
return Array.from(this.joinedChannels);
|
|
318
|
+
}
|
|
319
|
+
isUserAllowed(user) {
|
|
320
|
+
if (this.settings.allowedUserIds.length > 0 && !this.settings.allowedUserIds.includes(user.userId)) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
if (this.settings.allowedRoles.includes("all")) {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
if (this.settings.allowedRoles.includes("owner") && user.isBroadcaster) {
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
if (this.settings.allowedRoles.includes("moderator") && user.isModerator) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
if (this.settings.allowedRoles.includes("vip") && user.isVip) {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
if (this.settings.allowedRoles.includes("subscriber") && user.isSubscriber) {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
async sendMessage(text, options) {
|
|
341
|
+
if (!this.connected) {
|
|
342
|
+
throw new TwitchNotConnectedError;
|
|
343
|
+
}
|
|
344
|
+
const channel = normalizeChannel(options?.channel || this.settings.channel);
|
|
345
|
+
const cleanedText = stripMarkdownForTwitch(text);
|
|
346
|
+
if (!cleanedText) {
|
|
347
|
+
return { success: true, messageId: "skipped-empty" };
|
|
348
|
+
}
|
|
349
|
+
const chunks = splitMessageForTwitch(cleanedText);
|
|
350
|
+
let lastMessageId;
|
|
351
|
+
for (const chunk of chunks) {
|
|
352
|
+
if (options?.replyTo) {
|
|
353
|
+
await this.client.say(channel, chunk, { replyTo: options.replyTo });
|
|
354
|
+
} else {
|
|
355
|
+
await this.client.say(channel, chunk);
|
|
356
|
+
}
|
|
357
|
+
lastMessageId = crypto.randomUUID();
|
|
358
|
+
if (chunks.length > 1) {
|
|
359
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
this.runtime.emitEvent("TWITCH_MESSAGE_SENT" /* MESSAGE_SENT */, {
|
|
363
|
+
runtime: this.runtime,
|
|
364
|
+
channel,
|
|
365
|
+
text: cleanedText,
|
|
366
|
+
messageId: lastMessageId
|
|
367
|
+
});
|
|
368
|
+
return { success: true, messageId: lastMessageId };
|
|
369
|
+
}
|
|
370
|
+
async joinChannel(channel) {
|
|
371
|
+
const normalized = normalizeChannel(channel);
|
|
372
|
+
await this.client.join(normalized);
|
|
373
|
+
this.joinedChannels.add(normalized);
|
|
374
|
+
}
|
|
375
|
+
async leaveChannel(channel) {
|
|
376
|
+
const normalized = normalizeChannel(channel);
|
|
377
|
+
await this.client.part(normalized);
|
|
378
|
+
this.joinedChannels.delete(normalized);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// src/actions/joinChannel.ts
|
|
382
|
+
import {
|
|
383
|
+
composePromptFromState,
|
|
384
|
+
ModelType,
|
|
385
|
+
parseJSONObjectFromText
|
|
386
|
+
} from "@elizaos/core";
|
|
387
|
+
var JOIN_CHANNEL_TEMPLATE = `You are helping to extract a Twitch channel name.
|
|
388
|
+
|
|
389
|
+
The user wants to join a Twitch channel.
|
|
390
|
+
|
|
391
|
+
Recent conversation:
|
|
392
|
+
{{recentMessages}}
|
|
393
|
+
|
|
394
|
+
Extract the channel name to join (without the # prefix).
|
|
395
|
+
|
|
396
|
+
Respond with a JSON object like:
|
|
397
|
+
{
|
|
398
|
+
"channel": "channelname"
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
Only respond with the JSON object, no other text.`;
|
|
402
|
+
var joinChannel = {
|
|
403
|
+
name: "TWITCH_JOIN_CHANNEL",
|
|
404
|
+
similes: ["JOIN_TWITCH_CHANNEL", "ENTER_CHANNEL", "CONNECT_CHANNEL"],
|
|
405
|
+
description: "Join a Twitch channel to listen and send messages",
|
|
406
|
+
validate: async (_runtime, message, _state) => {
|
|
407
|
+
return message.content.source === "twitch";
|
|
408
|
+
},
|
|
409
|
+
handler: async (runtime, message, state, _options, callback) => {
|
|
410
|
+
const twitchService = runtime.getService(TWITCH_SERVICE_NAME);
|
|
411
|
+
if (!twitchService || !twitchService.isConnected()) {
|
|
412
|
+
if (callback) {
|
|
413
|
+
callback({
|
|
414
|
+
text: "Twitch service is not available.",
|
|
415
|
+
source: "twitch"
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
return { success: false, error: "Twitch service not available" };
|
|
419
|
+
}
|
|
420
|
+
const currentState = state ?? await runtime.composeState(message);
|
|
421
|
+
const prompt = await composePromptFromState({
|
|
422
|
+
template: JOIN_CHANNEL_TEMPLATE,
|
|
423
|
+
state: currentState
|
|
424
|
+
});
|
|
425
|
+
let channelName = null;
|
|
426
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
427
|
+
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
428
|
+
prompt
|
|
429
|
+
});
|
|
430
|
+
const parsed = parseJSONObjectFromText(String(response));
|
|
431
|
+
if (parsed?.channel) {
|
|
432
|
+
channelName = normalizeChannel(String(parsed.channel));
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (!channelName) {
|
|
437
|
+
if (callback) {
|
|
438
|
+
callback({
|
|
439
|
+
text: "I couldn't understand which channel you want me to join. Please specify the channel name.",
|
|
440
|
+
source: "twitch"
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
return { success: false, error: "Could not extract channel name" };
|
|
444
|
+
}
|
|
445
|
+
if (twitchService.getJoinedChannels().includes(channelName)) {
|
|
446
|
+
if (callback) {
|
|
447
|
+
callback({
|
|
448
|
+
text: `Already in channel #${channelName}.`,
|
|
449
|
+
source: "twitch"
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
success: true,
|
|
454
|
+
data: { channel: channelName, alreadyJoined: true }
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
await twitchService.joinChannel(channelName);
|
|
458
|
+
if (callback) {
|
|
459
|
+
callback({
|
|
460
|
+
text: `Joined channel #${channelName}.`,
|
|
461
|
+
source: message.content.source
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
success: true,
|
|
466
|
+
data: {
|
|
467
|
+
channel: channelName
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
},
|
|
471
|
+
examples: [
|
|
472
|
+
[
|
|
473
|
+
{
|
|
474
|
+
name: "{{user1}}",
|
|
475
|
+
content: { text: "Join the channel shroud" }
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
name: "{{agent}}",
|
|
479
|
+
content: {
|
|
480
|
+
text: "I'll join that channel.",
|
|
481
|
+
actions: ["TWITCH_JOIN_CHANNEL"]
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
]
|
|
485
|
+
]
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// src/actions/leaveChannel.ts
|
|
489
|
+
import {
|
|
490
|
+
composePromptFromState as composePromptFromState2,
|
|
491
|
+
ModelType as ModelType2,
|
|
492
|
+
parseJSONObjectFromText as parseJSONObjectFromText2
|
|
493
|
+
} from "@elizaos/core";
|
|
494
|
+
var LEAVE_CHANNEL_TEMPLATE = `You are helping to extract a Twitch channel name.
|
|
495
|
+
|
|
496
|
+
The user wants to leave a Twitch channel.
|
|
497
|
+
|
|
498
|
+
Recent conversation:
|
|
499
|
+
{{recentMessages}}
|
|
500
|
+
|
|
501
|
+
Currently joined channels: {{joinedChannels}}
|
|
502
|
+
|
|
503
|
+
Extract the channel name to leave (without the # prefix).
|
|
504
|
+
|
|
505
|
+
Respond with a JSON object like:
|
|
506
|
+
{
|
|
507
|
+
"channel": "channelname"
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
Only respond with the JSON object, no other text.`;
|
|
511
|
+
var leaveChannel = {
|
|
512
|
+
name: "TWITCH_LEAVE_CHANNEL",
|
|
513
|
+
similes: [
|
|
514
|
+
"LEAVE_TWITCH_CHANNEL",
|
|
515
|
+
"EXIT_CHANNEL",
|
|
516
|
+
"PART_CHANNEL",
|
|
517
|
+
"DISCONNECT_CHANNEL"
|
|
518
|
+
],
|
|
519
|
+
description: "Leave a Twitch channel",
|
|
520
|
+
validate: async (_runtime, message, _state) => {
|
|
521
|
+
return message.content.source === "twitch";
|
|
522
|
+
},
|
|
523
|
+
handler: async (runtime, message, state, _options, callback) => {
|
|
524
|
+
const twitchService = runtime.getService(TWITCH_SERVICE_NAME);
|
|
525
|
+
if (!twitchService || !twitchService.isConnected()) {
|
|
526
|
+
if (callback) {
|
|
527
|
+
callback({
|
|
528
|
+
text: "Twitch service is not available.",
|
|
529
|
+
source: "twitch"
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
return { success: false, error: "Twitch service not available" };
|
|
533
|
+
}
|
|
534
|
+
const joinedChannels = twitchService.getJoinedChannels();
|
|
535
|
+
const currentState = state ?? await runtime.composeState(message);
|
|
536
|
+
const enrichedState = {
|
|
537
|
+
...currentState,
|
|
538
|
+
joinedChannels: joinedChannels.join(", ")
|
|
539
|
+
};
|
|
540
|
+
const prompt = await composePromptFromState2({
|
|
541
|
+
template: LEAVE_CHANNEL_TEMPLATE,
|
|
542
|
+
state: enrichedState
|
|
543
|
+
});
|
|
544
|
+
let channelName = null;
|
|
545
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
546
|
+
const response = await runtime.useModel(ModelType2.TEXT_SMALL, {
|
|
547
|
+
prompt
|
|
548
|
+
});
|
|
549
|
+
const parsed = parseJSONObjectFromText2(String(response));
|
|
550
|
+
if (parsed?.channel) {
|
|
551
|
+
channelName = normalizeChannel(String(parsed.channel));
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (!channelName) {
|
|
556
|
+
if (callback) {
|
|
557
|
+
callback({
|
|
558
|
+
text: "I couldn't understand which channel you want me to leave. Please specify the channel name.",
|
|
559
|
+
source: "twitch"
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
return { success: false, error: "Could not extract channel name" };
|
|
563
|
+
}
|
|
564
|
+
if (!joinedChannels.includes(channelName)) {
|
|
565
|
+
if (callback) {
|
|
566
|
+
callback({
|
|
567
|
+
text: `Not currently in channel #${channelName}.`,
|
|
568
|
+
source: "twitch"
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return { success: false, error: "Not in that channel" };
|
|
572
|
+
}
|
|
573
|
+
if (channelName === twitchService.getPrimaryChannel()) {
|
|
574
|
+
if (callback) {
|
|
575
|
+
callback({
|
|
576
|
+
text: `Cannot leave the primary channel #${channelName}.`,
|
|
577
|
+
source: "twitch"
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
return { success: false, error: "Cannot leave primary channel" };
|
|
581
|
+
}
|
|
582
|
+
await twitchService.leaveChannel(channelName);
|
|
583
|
+
if (callback) {
|
|
584
|
+
callback({
|
|
585
|
+
text: `Left channel #${channelName}.`,
|
|
586
|
+
source: message.content.source
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
success: true,
|
|
591
|
+
data: {
|
|
592
|
+
channel: channelName
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
},
|
|
596
|
+
examples: [
|
|
597
|
+
[
|
|
598
|
+
{
|
|
599
|
+
name: "{{user1}}",
|
|
600
|
+
content: { text: "Leave the channel shroud" }
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
name: "{{agent}}",
|
|
604
|
+
content: {
|
|
605
|
+
text: "I'll leave that channel.",
|
|
606
|
+
actions: ["TWITCH_LEAVE_CHANNEL"]
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
]
|
|
610
|
+
]
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// src/actions/listChannels.ts
|
|
614
|
+
var listChannels = {
|
|
615
|
+
name: "TWITCH_LIST_CHANNELS",
|
|
616
|
+
similes: [
|
|
617
|
+
"LIST_TWITCH_CHANNELS",
|
|
618
|
+
"SHOW_CHANNELS",
|
|
619
|
+
"GET_CHANNELS",
|
|
620
|
+
"CURRENT_CHANNELS"
|
|
621
|
+
],
|
|
622
|
+
description: "List all Twitch channels the bot is currently in",
|
|
623
|
+
validate: async (_runtime, message, _state) => {
|
|
624
|
+
return message.content.source === "twitch";
|
|
625
|
+
},
|
|
626
|
+
handler: async (runtime, message, _state, _options, callback) => {
|
|
627
|
+
const twitchService = runtime.getService(TWITCH_SERVICE_NAME);
|
|
628
|
+
if (!twitchService || !twitchService.isConnected()) {
|
|
629
|
+
if (callback) {
|
|
630
|
+
callback({
|
|
631
|
+
text: "Twitch service is not available.",
|
|
632
|
+
source: "twitch"
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
return { success: false, error: "Twitch service not available" };
|
|
636
|
+
}
|
|
637
|
+
const joinedChannels = twitchService.getJoinedChannels();
|
|
638
|
+
const primaryChannel = twitchService.getPrimaryChannel();
|
|
639
|
+
const channelList = joinedChannels.map((channel) => {
|
|
640
|
+
const displayName = formatChannelForDisplay(channel);
|
|
641
|
+
const isPrimary = channel === primaryChannel;
|
|
642
|
+
return isPrimary ? `${displayName} (primary)` : displayName;
|
|
643
|
+
});
|
|
644
|
+
const responseText = joinedChannels.length > 0 ? `Currently in ${joinedChannels.length} channel(s):
|
|
645
|
+
${channelList.map((c) => `• ${c}`).join(`
|
|
646
|
+
`)}` : "Not currently in any channels.";
|
|
647
|
+
if (callback) {
|
|
648
|
+
callback({
|
|
649
|
+
text: responseText,
|
|
650
|
+
source: message.content.source
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
success: true,
|
|
655
|
+
data: {
|
|
656
|
+
channelCount: joinedChannels.length,
|
|
657
|
+
channels: joinedChannels,
|
|
658
|
+
primaryChannel
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
},
|
|
662
|
+
examples: [
|
|
663
|
+
[
|
|
664
|
+
{
|
|
665
|
+
name: "{{user1}}",
|
|
666
|
+
content: { text: "What channels are you in?" }
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
name: "{{agent}}",
|
|
670
|
+
content: {
|
|
671
|
+
text: "I'll list the channels I'm currently in.",
|
|
672
|
+
actions: ["TWITCH_LIST_CHANNELS"]
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
]
|
|
676
|
+
]
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// src/actions/sendMessage.ts
|
|
680
|
+
import {
|
|
681
|
+
composePromptFromState as composePromptFromState3,
|
|
682
|
+
ModelType as ModelType3,
|
|
683
|
+
parseJSONObjectFromText as parseJSONObjectFromText3
|
|
684
|
+
} from "@elizaos/core";
|
|
685
|
+
var SEND_MESSAGE_TEMPLATE = `You are helping to extract send message parameters for Twitch chat.
|
|
686
|
+
|
|
687
|
+
The user wants to send a message to a Twitch channel.
|
|
688
|
+
|
|
689
|
+
Recent conversation:
|
|
690
|
+
{{recentMessages}}
|
|
691
|
+
|
|
692
|
+
Extract the following:
|
|
693
|
+
1. text: The message text to send
|
|
694
|
+
2. channel: The channel name to send to (without # prefix), or "current" for the current channel
|
|
695
|
+
|
|
696
|
+
Respond with a JSON object like:
|
|
697
|
+
{
|
|
698
|
+
"text": "The message to send",
|
|
699
|
+
"channel": "current"
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
Only respond with the JSON object, no other text.`;
|
|
703
|
+
var sendMessage = {
|
|
704
|
+
name: "TWITCH_SEND_MESSAGE",
|
|
705
|
+
similes: [
|
|
706
|
+
"SEND_TWITCH_MESSAGE",
|
|
707
|
+
"TWITCH_CHAT",
|
|
708
|
+
"CHAT_TWITCH",
|
|
709
|
+
"SAY_IN_TWITCH"
|
|
710
|
+
],
|
|
711
|
+
description: "Send a message to a Twitch channel",
|
|
712
|
+
validate: async (_runtime, message, _state) => {
|
|
713
|
+
return message.content.source === "twitch";
|
|
714
|
+
},
|
|
715
|
+
handler: async (runtime, message, state, _options, callback) => {
|
|
716
|
+
const twitchService = runtime.getService(TWITCH_SERVICE_NAME);
|
|
717
|
+
if (!twitchService || !twitchService.isConnected()) {
|
|
718
|
+
if (callback) {
|
|
719
|
+
callback({
|
|
720
|
+
text: "Twitch service is not available.",
|
|
721
|
+
source: "twitch"
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
return { success: false, error: "Twitch service not available" };
|
|
725
|
+
}
|
|
726
|
+
const currentState = state ?? await runtime.composeState(message);
|
|
727
|
+
const prompt = await composePromptFromState3({
|
|
728
|
+
template: SEND_MESSAGE_TEMPLATE,
|
|
729
|
+
state: currentState
|
|
730
|
+
});
|
|
731
|
+
let messageInfo = null;
|
|
732
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
733
|
+
const response = await runtime.useModel(ModelType3.TEXT_SMALL, {
|
|
734
|
+
prompt
|
|
735
|
+
});
|
|
736
|
+
const parsed = parseJSONObjectFromText3(String(response));
|
|
737
|
+
if (parsed?.text) {
|
|
738
|
+
messageInfo = {
|
|
739
|
+
text: String(parsed.text),
|
|
740
|
+
channel: String(parsed.channel || "current")
|
|
741
|
+
};
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (!messageInfo || !messageInfo.text) {
|
|
746
|
+
if (callback) {
|
|
747
|
+
callback({
|
|
748
|
+
text: "I couldn't understand what message you want me to send. Please try again.",
|
|
749
|
+
source: "twitch"
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return { success: false, error: "Could not extract message parameters" };
|
|
753
|
+
}
|
|
754
|
+
let targetChannel = twitchService.getPrimaryChannel();
|
|
755
|
+
if (messageInfo.channel && messageInfo.channel !== "current") {
|
|
756
|
+
targetChannel = normalizeChannel(messageInfo.channel);
|
|
757
|
+
}
|
|
758
|
+
if (currentState?.data?.room?.channelId) {
|
|
759
|
+
targetChannel = normalizeChannel(currentState.data.room.channelId);
|
|
760
|
+
}
|
|
761
|
+
const result = await twitchService.sendMessage(messageInfo.text, {
|
|
762
|
+
channel: targetChannel
|
|
763
|
+
});
|
|
764
|
+
if (!result.success) {
|
|
765
|
+
if (callback) {
|
|
766
|
+
callback({
|
|
767
|
+
text: `Failed to send message: ${result.error}`,
|
|
768
|
+
source: "twitch"
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
return { success: false, error: result.error };
|
|
772
|
+
}
|
|
773
|
+
if (callback) {
|
|
774
|
+
callback({
|
|
775
|
+
text: "Message sent successfully.",
|
|
776
|
+
source: message.content.source
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
return {
|
|
780
|
+
success: true,
|
|
781
|
+
data: {
|
|
782
|
+
channel: targetChannel,
|
|
783
|
+
messageId: result.messageId
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
},
|
|
787
|
+
examples: [
|
|
788
|
+
[
|
|
789
|
+
{
|
|
790
|
+
name: "{{user1}}",
|
|
791
|
+
content: { text: "Send a message to chat saying 'Hello everyone!'" }
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
name: "{{agent}}",
|
|
795
|
+
content: {
|
|
796
|
+
text: "I'll send that message to the chat.",
|
|
797
|
+
actions: ["TWITCH_SEND_MESSAGE"]
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
]
|
|
801
|
+
]
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
// src/providers/channelState.ts
|
|
805
|
+
var channelStateProvider = {
|
|
806
|
+
name: "twitchChannelState",
|
|
807
|
+
description: "Provides information about the current Twitch channel context",
|
|
808
|
+
get: async (runtime, message, state) => {
|
|
809
|
+
if (message.content.source !== "twitch") {
|
|
810
|
+
return {
|
|
811
|
+
data: {},
|
|
812
|
+
values: {},
|
|
813
|
+
text: ""
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
const twitchService = runtime.getService(TWITCH_SERVICE_NAME);
|
|
817
|
+
if (!twitchService || !twitchService.isConnected()) {
|
|
818
|
+
return {
|
|
819
|
+
data: {
|
|
820
|
+
connected: false
|
|
821
|
+
},
|
|
822
|
+
values: {
|
|
823
|
+
connected: false
|
|
824
|
+
},
|
|
825
|
+
text: ""
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
const agentName = state?.agentName || "The agent";
|
|
829
|
+
const room = state?.data?.room;
|
|
830
|
+
const channelId = room?.channelId;
|
|
831
|
+
const channel = channelId ? normalizeChannel(channelId) : twitchService.getPrimaryChannel();
|
|
832
|
+
const joinedChannels = twitchService.getJoinedChannels();
|
|
833
|
+
const isPrimaryChannel = channel === twitchService.getPrimaryChannel();
|
|
834
|
+
const botUsername = twitchService.getBotUsername();
|
|
835
|
+
let responseText = `${agentName} is currently in Twitch channel ${formatChannelForDisplay(channel)}.`;
|
|
836
|
+
if (isPrimaryChannel) {
|
|
837
|
+
responseText += " This is the primary channel.";
|
|
838
|
+
}
|
|
839
|
+
responseText += `
|
|
840
|
+
|
|
841
|
+
Twitch is a live streaming platform. Chat messages are public and visible to all viewers.`;
|
|
842
|
+
responseText += ` ${agentName} is logged in as @${botUsername}.`;
|
|
843
|
+
responseText += ` Currently connected to ${joinedChannels.length} channel(s).`;
|
|
844
|
+
return {
|
|
845
|
+
data: {
|
|
846
|
+
channel,
|
|
847
|
+
displayChannel: formatChannelForDisplay(channel),
|
|
848
|
+
isPrimaryChannel,
|
|
849
|
+
botUsername,
|
|
850
|
+
joinedChannels,
|
|
851
|
+
channelCount: joinedChannels.length,
|
|
852
|
+
connected: true
|
|
853
|
+
},
|
|
854
|
+
values: {
|
|
855
|
+
channel,
|
|
856
|
+
displayChannel: formatChannelForDisplay(channel),
|
|
857
|
+
isPrimaryChannel,
|
|
858
|
+
botUsername,
|
|
859
|
+
channelCount: joinedChannels.length
|
|
860
|
+
},
|
|
861
|
+
text: responseText
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
// src/providers/userContext.ts
|
|
867
|
+
var userContextProvider = {
|
|
868
|
+
name: "twitchUserContext",
|
|
869
|
+
description: "Provides information about the Twitch user in the current conversation",
|
|
870
|
+
get: async (runtime, message, state) => {
|
|
871
|
+
if (message.content.source !== "twitch") {
|
|
872
|
+
return {
|
|
873
|
+
data: {},
|
|
874
|
+
values: {},
|
|
875
|
+
text: ""
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
const twitchService = runtime.getService(TWITCH_SERVICE_NAME);
|
|
879
|
+
if (!twitchService || !twitchService.isConnected()) {
|
|
880
|
+
return {
|
|
881
|
+
data: {},
|
|
882
|
+
values: {},
|
|
883
|
+
text: ""
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
const agentName = state?.agentName || "The agent";
|
|
887
|
+
const metadata = message.content.metadata;
|
|
888
|
+
const userInfo = metadata?.user;
|
|
889
|
+
if (!userInfo) {
|
|
890
|
+
return {
|
|
891
|
+
data: {},
|
|
892
|
+
values: {},
|
|
893
|
+
text: ""
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
const displayName = getTwitchUserDisplayName(userInfo);
|
|
897
|
+
const roles = [];
|
|
898
|
+
if (userInfo.isBroadcaster) {
|
|
899
|
+
roles.push("broadcaster");
|
|
900
|
+
}
|
|
901
|
+
if (userInfo.isModerator) {
|
|
902
|
+
roles.push("moderator");
|
|
903
|
+
}
|
|
904
|
+
if (userInfo.isVip) {
|
|
905
|
+
roles.push("VIP");
|
|
906
|
+
}
|
|
907
|
+
if (userInfo.isSubscriber) {
|
|
908
|
+
roles.push("subscriber");
|
|
909
|
+
}
|
|
910
|
+
const roleText = roles.length > 0 ? roles.join(", ") : "viewer";
|
|
911
|
+
let responseText = `${agentName} is talking to ${displayName} (${roleText}) in Twitch chat.`;
|
|
912
|
+
if (userInfo.isBroadcaster) {
|
|
913
|
+
responseText += ` ${displayName} is the channel owner/broadcaster.`;
|
|
914
|
+
} else if (userInfo.isModerator) {
|
|
915
|
+
responseText += ` ${displayName} is a channel moderator.`;
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
data: {
|
|
919
|
+
userId: userInfo.userId,
|
|
920
|
+
username: userInfo.username,
|
|
921
|
+
displayName,
|
|
922
|
+
isBroadcaster: userInfo.isBroadcaster,
|
|
923
|
+
isModerator: userInfo.isModerator,
|
|
924
|
+
isVip: userInfo.isVip,
|
|
925
|
+
isSubscriber: userInfo.isSubscriber,
|
|
926
|
+
roles,
|
|
927
|
+
color: userInfo.color
|
|
928
|
+
},
|
|
929
|
+
values: {
|
|
930
|
+
userId: userInfo.userId,
|
|
931
|
+
username: userInfo.username,
|
|
932
|
+
displayName,
|
|
933
|
+
roleText,
|
|
934
|
+
isBroadcaster: userInfo.isBroadcaster,
|
|
935
|
+
isModerator: userInfo.isModerator
|
|
936
|
+
},
|
|
937
|
+
text: responseText
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
// src/index.ts
|
|
943
|
+
var twitchPlugin = {
|
|
944
|
+
name: "twitch",
|
|
945
|
+
description: "Twitch chat integration plugin for ElizaOS with real-time messaging",
|
|
946
|
+
services: [TwitchService],
|
|
947
|
+
actions: [sendMessage, joinChannel, leaveChannel, listChannels],
|
|
948
|
+
providers: [channelStateProvider, userContextProvider],
|
|
949
|
+
tests: [],
|
|
950
|
+
init: async (_config, runtime) => {
|
|
951
|
+
const username = runtime.getSetting("TWITCH_USERNAME");
|
|
952
|
+
const clientId = runtime.getSetting("TWITCH_CLIENT_ID");
|
|
953
|
+
const accessToken = runtime.getSetting("TWITCH_ACCESS_TOKEN");
|
|
954
|
+
const channel = runtime.getSetting("TWITCH_CHANNEL");
|
|
955
|
+
logger2.info("=".repeat(60));
|
|
956
|
+
logger2.info("Twitch Plugin Configuration");
|
|
957
|
+
logger2.info("=".repeat(60));
|
|
958
|
+
logger2.info(` Username: ${username ? "✓ Set" : "✗ Missing (required)"}`);
|
|
959
|
+
logger2.info(` Client ID: ${clientId ? "✓ Set" : "✗ Missing (required)"}`);
|
|
960
|
+
logger2.info(` Access Token: ${accessToken ? "✓ Set" : "✗ Missing (required)"}`);
|
|
961
|
+
logger2.info(` Channel: ${channel ? `✓ ${channel}` : "✗ Missing (required)"}`);
|
|
962
|
+
logger2.info("=".repeat(60));
|
|
963
|
+
const missing = [];
|
|
964
|
+
if (!username)
|
|
965
|
+
missing.push("TWITCH_USERNAME");
|
|
966
|
+
if (!clientId)
|
|
967
|
+
missing.push("TWITCH_CLIENT_ID");
|
|
968
|
+
if (!accessToken)
|
|
969
|
+
missing.push("TWITCH_ACCESS_TOKEN");
|
|
970
|
+
if (!channel)
|
|
971
|
+
missing.push("TWITCH_CHANNEL");
|
|
972
|
+
if (missing.length > 0) {
|
|
973
|
+
logger2.warn(`Twitch plugin: Missing required configuration: ${missing.join(", ")}`);
|
|
974
|
+
}
|
|
975
|
+
const clientSecret = runtime.getSetting("TWITCH_CLIENT_SECRET");
|
|
976
|
+
const refreshToken = runtime.getSetting("TWITCH_REFRESH_TOKEN");
|
|
977
|
+
const additionalChannels = runtime.getSetting("TWITCH_CHANNELS");
|
|
978
|
+
const requireMention = runtime.getSetting("TWITCH_REQUIRE_MENTION");
|
|
979
|
+
const allowedRoles = runtime.getSetting("TWITCH_ALLOWED_ROLES");
|
|
980
|
+
if (clientSecret && refreshToken) {
|
|
981
|
+
logger2.info(" Token Refresh: ✓ Enabled (client secret and refresh token set)");
|
|
982
|
+
} else if (clientSecret || refreshToken) {
|
|
983
|
+
logger2.warn(" Token Refresh: ⚠ Partial (need both TWITCH_CLIENT_SECRET and TWITCH_REFRESH_TOKEN)");
|
|
984
|
+
}
|
|
985
|
+
if (additionalChannels) {
|
|
986
|
+
logger2.info(` Additional Channels: ${additionalChannels}`);
|
|
987
|
+
}
|
|
988
|
+
if (requireMention === "true") {
|
|
989
|
+
logger2.info(" Require Mention: ✓ Enabled (will only respond to @mentions)");
|
|
990
|
+
}
|
|
991
|
+
if (allowedRoles) {
|
|
992
|
+
logger2.info(` Allowed Roles: ${allowedRoles}`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
var src_default = twitchPlugin;
|
|
997
|
+
export {
|
|
998
|
+
userContextProvider,
|
|
999
|
+
stripMarkdownForTwitch,
|
|
1000
|
+
splitMessageForTwitch,
|
|
1001
|
+
sendMessage,
|
|
1002
|
+
normalizeChannel,
|
|
1003
|
+
listChannels,
|
|
1004
|
+
leaveChannel,
|
|
1005
|
+
joinChannel,
|
|
1006
|
+
getTwitchUserDisplayName,
|
|
1007
|
+
formatChannelForDisplay,
|
|
1008
|
+
src_default as default,
|
|
1009
|
+
channelStateProvider,
|
|
1010
|
+
TwitchServiceNotInitializedError,
|
|
1011
|
+
TwitchService,
|
|
1012
|
+
TwitchPluginError,
|
|
1013
|
+
TwitchNotConnectedError,
|
|
1014
|
+
TwitchEventTypes,
|
|
1015
|
+
TwitchConfigurationError,
|
|
1016
|
+
TwitchApiError,
|
|
1017
|
+
TWITCH_SERVICE_NAME,
|
|
1018
|
+
MAX_TWITCH_MESSAGE_LENGTH
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
//# debugId=A95FC42F6866C30064756E2164756E21
|