@elizaos/plugin-matrix-typescript 2.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/integration.test.ts +30 -0
- package/build.ts +16 -0
- package/dist/index.js +1045 -0
- package/package.json +31 -0
- package/src/actions/joinRoom.ts +147 -0
- package/src/actions/listRooms.ts +95 -0
- package/src/actions/sendMessage.ts +175 -0
- package/src/actions/sendReaction.ts +162 -0
- package/src/index.ts +105 -0
- package/src/providers/roomState.ts +95 -0
- package/src/providers/userContext.ts +79 -0
- package/src/service.ts +483 -0
- package/src/types.ts +334 -0
- package/tsconfig.json +21 -0
package/src/service.ts
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matrix service implementation for ElizaOS.
|
|
3
|
+
*
|
|
4
|
+
* This service provides Matrix messaging capabilities using matrix-js-sdk.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Service, type IAgentRuntime, type EventPayload, logger } from "@elizaos/core";
|
|
8
|
+
import * as sdk from "matrix-js-sdk";
|
|
9
|
+
import {
|
|
10
|
+
type IMatrixService,
|
|
11
|
+
type MatrixMessage,
|
|
12
|
+
type MatrixMessageSendOptions,
|
|
13
|
+
type MatrixRoom,
|
|
14
|
+
type MatrixSendResult,
|
|
15
|
+
type MatrixSettings,
|
|
16
|
+
type MatrixUserInfo,
|
|
17
|
+
MatrixConfigurationError,
|
|
18
|
+
MatrixEventTypes,
|
|
19
|
+
MatrixNotConnectedError,
|
|
20
|
+
MATRIX_SERVICE_NAME,
|
|
21
|
+
getMatrixLocalpart,
|
|
22
|
+
isValidMatrixRoomAlias,
|
|
23
|
+
isValidMatrixRoomId,
|
|
24
|
+
} from "./types.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Matrix messaging service for ElizaOS agents.
|
|
28
|
+
*/
|
|
29
|
+
export class MatrixService extends Service implements IMatrixService {
|
|
30
|
+
static serviceType: string = MATRIX_SERVICE_NAME;
|
|
31
|
+
|
|
32
|
+
capabilityDescription = "Matrix messaging service for chat communication";
|
|
33
|
+
|
|
34
|
+
declare protected runtime: IAgentRuntime;
|
|
35
|
+
private settings!: MatrixSettings;
|
|
36
|
+
private client!: sdk.MatrixClient;
|
|
37
|
+
private connected: boolean = false;
|
|
38
|
+
private syncing: boolean = false;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Start the Matrix service.
|
|
42
|
+
*/
|
|
43
|
+
static async start(runtime: IAgentRuntime): Promise<MatrixService> {
|
|
44
|
+
const service = new MatrixService();
|
|
45
|
+
await service.initialize(runtime);
|
|
46
|
+
return service;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Stop the Matrix service.
|
|
51
|
+
*/
|
|
52
|
+
static override async stopRuntime(runtime: IAgentRuntime): Promise<void> {
|
|
53
|
+
const service = runtime.getService(MATRIX_SERVICE_NAME) as MatrixService | undefined;
|
|
54
|
+
if (service) {
|
|
55
|
+
await service.stop();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Initialize the Matrix service.
|
|
61
|
+
*/
|
|
62
|
+
private async initialize(runtime: IAgentRuntime): Promise<void> {
|
|
63
|
+
this.runtime = runtime;
|
|
64
|
+
|
|
65
|
+
// Load configuration
|
|
66
|
+
this.settings = this.loadSettings();
|
|
67
|
+
|
|
68
|
+
// Validate configuration
|
|
69
|
+
this.validateSettings();
|
|
70
|
+
|
|
71
|
+
// Create Matrix client
|
|
72
|
+
this.client = sdk.createClient({
|
|
73
|
+
baseUrl: this.settings.homeserver,
|
|
74
|
+
userId: this.settings.userId,
|
|
75
|
+
accessToken: this.settings.accessToken,
|
|
76
|
+
deviceId: this.settings.deviceId,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Set up event handlers
|
|
80
|
+
this.setupEventHandlers();
|
|
81
|
+
|
|
82
|
+
// Start client
|
|
83
|
+
await this.connect();
|
|
84
|
+
|
|
85
|
+
logger.info(
|
|
86
|
+
`Matrix service initialized for ${this.settings.userId} on ${this.settings.homeserver}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Load settings from runtime.
|
|
92
|
+
*/
|
|
93
|
+
private loadSettings(): MatrixSettings {
|
|
94
|
+
// Helper to safely get string settings
|
|
95
|
+
const getStringSetting = (key: string): string | undefined => {
|
|
96
|
+
const value = this.runtime.getSetting(key);
|
|
97
|
+
return typeof value === "string" ? value : undefined;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const homeserver = getStringSetting("MATRIX_HOMESERVER");
|
|
101
|
+
const userId = getStringSetting("MATRIX_USER_ID");
|
|
102
|
+
const accessToken = getStringSetting("MATRIX_ACCESS_TOKEN");
|
|
103
|
+
const deviceId = getStringSetting("MATRIX_DEVICE_ID");
|
|
104
|
+
const roomsStr = getStringSetting("MATRIX_ROOMS");
|
|
105
|
+
const autoJoinStr = getStringSetting("MATRIX_AUTO_JOIN");
|
|
106
|
+
const encryptionStr = getStringSetting("MATRIX_ENCRYPTION");
|
|
107
|
+
const requireMentionStr = getStringSetting("MATRIX_REQUIRE_MENTION");
|
|
108
|
+
|
|
109
|
+
const rooms = roomsStr
|
|
110
|
+
? roomsStr.split(",").map((r: string) => r.trim()).filter(Boolean)
|
|
111
|
+
: [];
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
homeserver: homeserver || "",
|
|
115
|
+
userId: userId || "",
|
|
116
|
+
accessToken: accessToken || "",
|
|
117
|
+
deviceId,
|
|
118
|
+
rooms,
|
|
119
|
+
autoJoin: autoJoinStr === "true",
|
|
120
|
+
encryption: encryptionStr === "true",
|
|
121
|
+
requireMention: requireMentionStr === "true",
|
|
122
|
+
enabled: true,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validate the settings.
|
|
128
|
+
*/
|
|
129
|
+
private validateSettings(): void {
|
|
130
|
+
if (!this.settings.homeserver) {
|
|
131
|
+
throw new MatrixConfigurationError(
|
|
132
|
+
"MATRIX_HOMESERVER is required",
|
|
133
|
+
"MATRIX_HOMESERVER"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!this.settings.userId) {
|
|
138
|
+
throw new MatrixConfigurationError(
|
|
139
|
+
"MATRIX_USER_ID is required",
|
|
140
|
+
"MATRIX_USER_ID"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!this.settings.accessToken) {
|
|
145
|
+
throw new MatrixConfigurationError(
|
|
146
|
+
"MATRIX_ACCESS_TOKEN is required",
|
|
147
|
+
"MATRIX_ACCESS_TOKEN"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Set up event handlers for the Matrix client.
|
|
154
|
+
*/
|
|
155
|
+
private setupEventHandlers(): void {
|
|
156
|
+
// Sync events
|
|
157
|
+
this.client.on(sdk.ClientEvent.Sync, (state) => {
|
|
158
|
+
if (state === "PREPARED") {
|
|
159
|
+
this.syncing = true;
|
|
160
|
+
logger.info("Matrix sync complete");
|
|
161
|
+
this.runtime.emitEvent(MatrixEventTypes.SYNC_COMPLETE, {
|
|
162
|
+
runtime: this.runtime,
|
|
163
|
+
} as EventPayload);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Room timeline events (messages)
|
|
168
|
+
this.client.on(
|
|
169
|
+
sdk.RoomEvent.Timeline,
|
|
170
|
+
(event, room, toStartOfTimeline) => {
|
|
171
|
+
if (toStartOfTimeline) return;
|
|
172
|
+
if (event.getType() !== "m.room.message") return;
|
|
173
|
+
if (event.getSender() === this.settings.userId) return;
|
|
174
|
+
|
|
175
|
+
this.handleRoomMessage(event, room);
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Room membership events
|
|
180
|
+
this.client.on(sdk.RoomMemberEvent.Membership, (event, member) => {
|
|
181
|
+
if (member.userId !== this.settings.userId) return;
|
|
182
|
+
|
|
183
|
+
if (member.membership === "invite" && this.settings.autoJoin) {
|
|
184
|
+
const roomId = event.getRoomId();
|
|
185
|
+
if (roomId) {
|
|
186
|
+
logger.info(`Auto-joining room ${roomId}`);
|
|
187
|
+
this.client.joinRoom(roomId).catch((err) => {
|
|
188
|
+
logger.error(`Failed to auto-join room: ${err.message}`);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Handle an incoming room message.
|
|
197
|
+
*/
|
|
198
|
+
private handleRoomMessage(
|
|
199
|
+
event: sdk.MatrixEvent,
|
|
200
|
+
room: sdk.Room | undefined
|
|
201
|
+
): void {
|
|
202
|
+
const content = event.getContent();
|
|
203
|
+
const msgType = content.msgtype;
|
|
204
|
+
|
|
205
|
+
// Only handle text messages for now
|
|
206
|
+
if (msgType !== "m.text") return;
|
|
207
|
+
|
|
208
|
+
const roomId = event.getRoomId();
|
|
209
|
+
if (!roomId || !room) return;
|
|
210
|
+
|
|
211
|
+
// Check mention requirement
|
|
212
|
+
if (this.settings.requireMention) {
|
|
213
|
+
const body = content.body || "";
|
|
214
|
+
const localpart = getMatrixLocalpart(this.settings.userId);
|
|
215
|
+
const mentionPattern = new RegExp(`@?${localpart}`, "i");
|
|
216
|
+
if (!mentionPattern.test(body)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const sender = event.getSender();
|
|
222
|
+
const senderMember = room.getMember(sender || "");
|
|
223
|
+
|
|
224
|
+
const senderInfo: MatrixUserInfo = {
|
|
225
|
+
userId: sender || "",
|
|
226
|
+
displayName: senderMember?.name,
|
|
227
|
+
avatarUrl: senderMember?.getMxcAvatarUrl() || undefined,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Check for reply/thread
|
|
231
|
+
const relatesTo = content["m.relates_to"];
|
|
232
|
+
const isEdit = relatesTo?.rel_type === "m.replace";
|
|
233
|
+
const threadId = relatesTo?.rel_type === "m.thread" ? relatesTo.event_id : undefined;
|
|
234
|
+
const replyTo = relatesTo?.["m.in_reply_to"]?.event_id;
|
|
235
|
+
|
|
236
|
+
const message: MatrixMessage = {
|
|
237
|
+
eventId: event.getId() || "",
|
|
238
|
+
roomId,
|
|
239
|
+
sender: sender || "",
|
|
240
|
+
senderInfo,
|
|
241
|
+
content: content.body || "",
|
|
242
|
+
msgType,
|
|
243
|
+
formattedBody: content.formatted_body,
|
|
244
|
+
timestamp: event.getTs(),
|
|
245
|
+
threadId,
|
|
246
|
+
replyTo,
|
|
247
|
+
isEdit,
|
|
248
|
+
replacesEventId: isEdit ? relatesTo?.event_id : undefined,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const matrixRoom: MatrixRoom = {
|
|
252
|
+
roomId,
|
|
253
|
+
name: room.name,
|
|
254
|
+
topic: room.currentState.getStateEvents("m.room.topic", "")?.getContent()?.topic,
|
|
255
|
+
canonicalAlias: room.getCanonicalAlias() || undefined,
|
|
256
|
+
isEncrypted: room.hasEncryptionStateEvent(),
|
|
257
|
+
isDirect: this.client.getAccountData("m.direct")?.getContent()?.[sender || ""]?.includes(roomId) || false,
|
|
258
|
+
memberCount: room.getJoinedMemberCount(),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
logger.debug(
|
|
262
|
+
`Matrix message from ${senderInfo.displayName || sender} in ${room.name || roomId}: ${message.content.slice(0, 50)}...`
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
this.runtime.emitEvent(MatrixEventTypes.MESSAGE_RECEIVED, {
|
|
266
|
+
message,
|
|
267
|
+
room: matrixRoom,
|
|
268
|
+
runtime: this.runtime,
|
|
269
|
+
} as EventPayload);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Connect to Matrix.
|
|
274
|
+
*/
|
|
275
|
+
private async connect(): Promise<void> {
|
|
276
|
+
await this.client.startClient({ initialSyncLimit: 10 });
|
|
277
|
+
this.connected = true;
|
|
278
|
+
|
|
279
|
+
// Wait for initial sync
|
|
280
|
+
await new Promise<void>((resolve) => {
|
|
281
|
+
const listener = (state: string) => {
|
|
282
|
+
if (state === "PREPARED") {
|
|
283
|
+
this.client.removeListener(sdk.ClientEvent.Sync, listener);
|
|
284
|
+
resolve();
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
this.client.on(sdk.ClientEvent.Sync, listener);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Join configured rooms
|
|
291
|
+
for (const room of this.settings.rooms) {
|
|
292
|
+
try {
|
|
293
|
+
await this.joinRoom(room);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
logger.warn(`Failed to join room ${room}: ${err}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Shutdown the service.
|
|
302
|
+
*/
|
|
303
|
+
async stop(): Promise<void> {
|
|
304
|
+
if (this.client) {
|
|
305
|
+
this.client.stopClient();
|
|
306
|
+
}
|
|
307
|
+
this.connected = false;
|
|
308
|
+
logger.info("Matrix service stopped");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ============================================================================
|
|
312
|
+
// Public Interface
|
|
313
|
+
// ============================================================================
|
|
314
|
+
|
|
315
|
+
isConnected(): boolean {
|
|
316
|
+
return this.connected && this.syncing;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
getUserId(): string {
|
|
320
|
+
return this.settings.userId;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
getHomeserver(): string {
|
|
324
|
+
return this.settings.homeserver;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async getJoinedRooms(): Promise<MatrixRoom[]> {
|
|
328
|
+
const rooms = this.client.getRooms();
|
|
329
|
+
return rooms
|
|
330
|
+
.filter((room) => room.getMyMembership() === "join")
|
|
331
|
+
.map((room) => ({
|
|
332
|
+
roomId: room.roomId,
|
|
333
|
+
name: room.name,
|
|
334
|
+
topic: room.currentState.getStateEvents("m.room.topic", "")?.getContent()?.topic,
|
|
335
|
+
canonicalAlias: room.getCanonicalAlias() || undefined,
|
|
336
|
+
isEncrypted: room.hasEncryptionStateEvent(),
|
|
337
|
+
isDirect: false,
|
|
338
|
+
memberCount: room.getJoinedMemberCount(),
|
|
339
|
+
}));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async sendMessage(
|
|
343
|
+
text: string,
|
|
344
|
+
options?: MatrixMessageSendOptions
|
|
345
|
+
): Promise<MatrixSendResult> {
|
|
346
|
+
if (!this.isConnected()) {
|
|
347
|
+
throw new MatrixNotConnectedError();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const roomId = options?.roomId;
|
|
351
|
+
if (!roomId) {
|
|
352
|
+
return { success: false, error: "Room ID is required" };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Resolve room ID from alias if needed
|
|
356
|
+
let resolvedRoomId = roomId;
|
|
357
|
+
if (isValidMatrixRoomAlias(roomId)) {
|
|
358
|
+
const resolved = await this.client.getRoomIdForAlias(roomId);
|
|
359
|
+
resolvedRoomId = resolved.room_id;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Build content
|
|
363
|
+
const content: Record<string, unknown> = {
|
|
364
|
+
msgtype: "m.text",
|
|
365
|
+
body: text,
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
if (options?.formatted) {
|
|
369
|
+
content.format = "org.matrix.custom.html";
|
|
370
|
+
content.formatted_body = text;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Handle reply/thread
|
|
374
|
+
if (options?.threadId || options?.replyTo) {
|
|
375
|
+
content["m.relates_to"] = {};
|
|
376
|
+
|
|
377
|
+
if (options.threadId) {
|
|
378
|
+
(content["m.relates_to"] as Record<string, unknown>).rel_type = "m.thread";
|
|
379
|
+
(content["m.relates_to"] as Record<string, unknown>).event_id = options.threadId;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (options.replyTo) {
|
|
383
|
+
(content["m.relates_to"] as Record<string, unknown>)["m.in_reply_to"] = {
|
|
384
|
+
event_id: options.replyTo,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const response = await this.client.sendMessage(resolvedRoomId, content);
|
|
390
|
+
const eventId = response.event_id;
|
|
391
|
+
|
|
392
|
+
this.runtime.emitEvent(MatrixEventTypes.MESSAGE_SENT, {
|
|
393
|
+
roomId: resolvedRoomId,
|
|
394
|
+
eventId,
|
|
395
|
+
content: text,
|
|
396
|
+
runtime: this.runtime,
|
|
397
|
+
} as EventPayload);
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
success: true,
|
|
401
|
+
eventId,
|
|
402
|
+
roomId: resolvedRoomId,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async sendReaction(
|
|
407
|
+
roomId: string,
|
|
408
|
+
eventId: string,
|
|
409
|
+
emoji: string
|
|
410
|
+
): Promise<MatrixSendResult> {
|
|
411
|
+
if (!this.isConnected()) {
|
|
412
|
+
throw new MatrixNotConnectedError();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const content = {
|
|
416
|
+
"m.relates_to": {
|
|
417
|
+
rel_type: "m.annotation",
|
|
418
|
+
event_id: eventId,
|
|
419
|
+
key: emoji,
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const response = await this.client.sendEvent(roomId, "m.reaction", content);
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
success: true,
|
|
427
|
+
eventId: response.event_id,
|
|
428
|
+
roomId,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async joinRoom(roomIdOrAlias: string): Promise<string> {
|
|
433
|
+
if (!this.isConnected()) {
|
|
434
|
+
throw new MatrixNotConnectedError();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const response = await this.client.joinRoom(roomIdOrAlias);
|
|
438
|
+
const roomId = response.roomId;
|
|
439
|
+
|
|
440
|
+
logger.info(`Joined room ${roomId}`);
|
|
441
|
+
this.runtime.emitEvent(MatrixEventTypes.ROOM_JOINED, {
|
|
442
|
+
room: { roomId },
|
|
443
|
+
runtime: this.runtime,
|
|
444
|
+
} as EventPayload);
|
|
445
|
+
|
|
446
|
+
return roomId;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async leaveRoom(roomId: string): Promise<void> {
|
|
450
|
+
if (!this.isConnected()) {
|
|
451
|
+
throw new MatrixNotConnectedError();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
await this.client.leave(roomId);
|
|
455
|
+
logger.info(`Left room ${roomId}`);
|
|
456
|
+
this.runtime.emitEvent(MatrixEventTypes.ROOM_LEFT, {
|
|
457
|
+
roomId,
|
|
458
|
+
runtime: this.runtime,
|
|
459
|
+
} as EventPayload);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async sendTyping(
|
|
463
|
+
roomId: string,
|
|
464
|
+
typing: boolean,
|
|
465
|
+
timeout: number = 30000
|
|
466
|
+
): Promise<void> {
|
|
467
|
+
if (!this.isConnected()) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
await this.client.sendTyping(roomId, typing, timeout);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async sendReadReceipt(roomId: string, eventId: string): Promise<void> {
|
|
475
|
+
if (!this.isConnected()) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
await this.client.sendReadReceipt(
|
|
480
|
+
new sdk.MatrixEvent({ event_id: eventId, room_id: roomId })
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|