@elizaos/plugin-matrix 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/build.ts +16 -0
- package/dist/index.js +1045 -0
- package/package.json +34 -0
- package/src/__tests__/matrix.test.ts +550 -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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Room state provider for Matrix plugin.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Provider, ProviderResult, IAgentRuntime, Memory, State } from "@elizaos/core";
|
|
6
|
+
import { MatrixService } from "../service.js";
|
|
7
|
+
import { MATRIX_SERVICE_NAME, getMatrixLocalpart } from "../types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Provider that gives the agent information about the current Matrix room context.
|
|
11
|
+
*/
|
|
12
|
+
export const roomStateProvider: Provider = {
|
|
13
|
+
name: "matrixRoomState",
|
|
14
|
+
description: "Provides information about the current Matrix room context",
|
|
15
|
+
|
|
16
|
+
get: async (
|
|
17
|
+
runtime: IAgentRuntime,
|
|
18
|
+
message: Memory,
|
|
19
|
+
state: State
|
|
20
|
+
): Promise<ProviderResult> => {
|
|
21
|
+
// Only provide context for Matrix messages
|
|
22
|
+
if (message.content.source !== "matrix") {
|
|
23
|
+
return {
|
|
24
|
+
data: {},
|
|
25
|
+
values: {},
|
|
26
|
+
text: "",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const matrixService = runtime.getService(MATRIX_SERVICE_NAME) as MatrixService | undefined;
|
|
31
|
+
|
|
32
|
+
if (!matrixService || !matrixService.isConnected()) {
|
|
33
|
+
return {
|
|
34
|
+
data: { connected: false },
|
|
35
|
+
values: { connected: false },
|
|
36
|
+
text: "",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const agentName = state?.agentName || "The agent";
|
|
41
|
+
|
|
42
|
+
// Get room from state if available
|
|
43
|
+
const room = state?.data?.room as Record<string, unknown> | undefined;
|
|
44
|
+
const roomId = room?.roomId as string | undefined;
|
|
45
|
+
const roomName = room?.name as string | undefined;
|
|
46
|
+
const isEncrypted = room?.isEncrypted as boolean | undefined;
|
|
47
|
+
const isDirect = room?.isDirect as boolean | undefined;
|
|
48
|
+
const memberCount = room?.memberCount as number | undefined;
|
|
49
|
+
|
|
50
|
+
const userId = matrixService.getUserId();
|
|
51
|
+
const displayName = getMatrixLocalpart(userId);
|
|
52
|
+
|
|
53
|
+
let responseText = "";
|
|
54
|
+
|
|
55
|
+
if (isDirect) {
|
|
56
|
+
responseText = `${agentName} is in a direct message conversation on Matrix.`;
|
|
57
|
+
} else {
|
|
58
|
+
const roomLabel = roomName || roomId || "a Matrix room";
|
|
59
|
+
responseText = `${agentName} is currently in Matrix room "${roomLabel}".`;
|
|
60
|
+
|
|
61
|
+
if (memberCount) {
|
|
62
|
+
responseText += ` The room has ${memberCount} members.`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isEncrypted) {
|
|
67
|
+
responseText += " This room has end-to-end encryption enabled.";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
responseText += `\n\nMatrix is a decentralized communication protocol. ${agentName} is logged in as ${userId}.`;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
data: {
|
|
74
|
+
roomId,
|
|
75
|
+
roomName,
|
|
76
|
+
isEncrypted: isEncrypted || false,
|
|
77
|
+
isDirect: isDirect || false,
|
|
78
|
+
memberCount: memberCount || 0,
|
|
79
|
+
userId,
|
|
80
|
+
displayName,
|
|
81
|
+
homeserver: matrixService.getHomeserver(),
|
|
82
|
+
connected: true,
|
|
83
|
+
},
|
|
84
|
+
values: {
|
|
85
|
+
roomId,
|
|
86
|
+
roomName,
|
|
87
|
+
isEncrypted: isEncrypted || false,
|
|
88
|
+
isDirect: isDirect || false,
|
|
89
|
+
memberCount: memberCount || 0,
|
|
90
|
+
userId,
|
|
91
|
+
},
|
|
92
|
+
text: responseText,
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User context provider for Matrix plugin.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Provider, ProviderResult, IAgentRuntime, Memory, State } from "@elizaos/core";
|
|
6
|
+
import { MatrixService } from "../service.js";
|
|
7
|
+
import {
|
|
8
|
+
MATRIX_SERVICE_NAME,
|
|
9
|
+
getMatrixLocalpart,
|
|
10
|
+
getMatrixUserDisplayName,
|
|
11
|
+
type MatrixUserInfo,
|
|
12
|
+
} from "../types.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Provider that gives the agent information about the Matrix user context.
|
|
16
|
+
*/
|
|
17
|
+
export const userContextProvider: Provider = {
|
|
18
|
+
name: "matrixUserContext",
|
|
19
|
+
description: "Provides information about the Matrix user in the current conversation",
|
|
20
|
+
|
|
21
|
+
get: async (
|
|
22
|
+
runtime: IAgentRuntime,
|
|
23
|
+
message: Memory,
|
|
24
|
+
state: State
|
|
25
|
+
): Promise<ProviderResult> => {
|
|
26
|
+
// Only provide context for Matrix messages
|
|
27
|
+
if (message.content.source !== "matrix") {
|
|
28
|
+
return {
|
|
29
|
+
data: {},
|
|
30
|
+
values: {},
|
|
31
|
+
text: "",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const matrixService = runtime.getService(MATRIX_SERVICE_NAME) as MatrixService | undefined;
|
|
36
|
+
|
|
37
|
+
if (!matrixService || !matrixService.isConnected()) {
|
|
38
|
+
return {
|
|
39
|
+
data: {},
|
|
40
|
+
values: {},
|
|
41
|
+
text: "",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const agentName = state?.agentName || "The agent";
|
|
46
|
+
|
|
47
|
+
// Try to get sender info from message metadata
|
|
48
|
+
const metadata = message.content.metadata as Record<string, unknown> | undefined;
|
|
49
|
+
const senderInfo = metadata?.senderInfo as MatrixUserInfo | undefined;
|
|
50
|
+
|
|
51
|
+
if (!senderInfo) {
|
|
52
|
+
return {
|
|
53
|
+
data: {},
|
|
54
|
+
values: {},
|
|
55
|
+
text: "",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const displayName = getMatrixUserDisplayName(senderInfo);
|
|
60
|
+
const localpart = getMatrixLocalpart(senderInfo.userId);
|
|
61
|
+
|
|
62
|
+
const responseText = `${agentName} is talking to ${displayName} (${senderInfo.userId}) on Matrix.`;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
data: {
|
|
66
|
+
userId: senderInfo.userId,
|
|
67
|
+
displayName,
|
|
68
|
+
localpart,
|
|
69
|
+
avatarUrl: senderInfo.avatarUrl,
|
|
70
|
+
},
|
|
71
|
+
values: {
|
|
72
|
+
userId: senderInfo.userId,
|
|
73
|
+
displayName,
|
|
74
|
+
localpart,
|
|
75
|
+
},
|
|
76
|
+
text: responseText,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|
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
|
+
}
|