@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/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
+ }