@aigne/afs-matrix 1.11.0-beta.12
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/LICENSE.md +26 -0
- package/dist/index.cjs +457 -0
- package/dist/index.d.cts +156 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +156 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +458 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { RouteContext } from "@aigne/afs/provider";
|
|
2
|
+
import { BaseMessageProvider, BotConfig, BufferedMessage, MessageCapabilities, MessageSender, SendOptions } from "@aigne/afs-messaging";
|
|
3
|
+
import { AFSExecResult, AFSListResult, ProviderTreeSchema } from "@aigne/afs";
|
|
4
|
+
|
|
5
|
+
//#region src/client.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* MatrixClient — pure Matrix Client-Server API wrapper.
|
|
8
|
+
*
|
|
9
|
+
* Zero external dependencies (uses native fetch).
|
|
10
|
+
* No AFS or application-specific concepts.
|
|
11
|
+
*/
|
|
12
|
+
interface MatrixClientOptions {
|
|
13
|
+
accessToken: string;
|
|
14
|
+
homeserver: string;
|
|
15
|
+
timeout?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Matrix room message event (subset). */
|
|
18
|
+
interface MatrixEvent {
|
|
19
|
+
event_id: string;
|
|
20
|
+
type: string;
|
|
21
|
+
sender: string;
|
|
22
|
+
content: {
|
|
23
|
+
msgtype?: string;
|
|
24
|
+
body?: string;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
};
|
|
27
|
+
origin_server_ts: number;
|
|
28
|
+
room_id?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Matrix sync response (subset). */
|
|
31
|
+
interface MatrixSyncResponse {
|
|
32
|
+
next_batch: string;
|
|
33
|
+
rooms?: {
|
|
34
|
+
join?: Record<string, {
|
|
35
|
+
timeline?: {
|
|
36
|
+
events?: MatrixEvent[];
|
|
37
|
+
};
|
|
38
|
+
}>;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Matrix whoami response. */
|
|
42
|
+
interface MatrixWhoamiResponse {
|
|
43
|
+
user_id: string;
|
|
44
|
+
device_id?: string;
|
|
45
|
+
}
|
|
46
|
+
declare class MatrixClient {
|
|
47
|
+
private readonly _accessToken;
|
|
48
|
+
private readonly _homeserver;
|
|
49
|
+
private readonly _timeout;
|
|
50
|
+
constructor(options: MatrixClientOptions);
|
|
51
|
+
sendMessage(roomId: string, text: string): Promise<{
|
|
52
|
+
event_id: string;
|
|
53
|
+
}>;
|
|
54
|
+
sendTyping(roomId: string, userId: string, timeout?: number): Promise<void>;
|
|
55
|
+
sync(since?: string, timeout?: number): Promise<MatrixSyncResponse>;
|
|
56
|
+
whoami(): Promise<MatrixWhoamiResponse>;
|
|
57
|
+
private _call;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/index.d.ts
|
|
61
|
+
interface AFSMatrixOptions {
|
|
62
|
+
/** Multi-bot config */
|
|
63
|
+
bots?: Array<{
|
|
64
|
+
name: string;
|
|
65
|
+
token: string;
|
|
66
|
+
homeserver: string;
|
|
67
|
+
conversations?: string[];
|
|
68
|
+
}>;
|
|
69
|
+
/** Single-bot shorthand: access token */
|
|
70
|
+
accessToken?: string;
|
|
71
|
+
/** Single-bot shorthand: homeserver URL */
|
|
72
|
+
homeserver?: string;
|
|
73
|
+
/** Single-bot shorthand: room IDs to monitor */
|
|
74
|
+
rooms?: string[];
|
|
75
|
+
bufferSize?: number;
|
|
76
|
+
}
|
|
77
|
+
declare class AFSMatrix extends BaseMessageProvider {
|
|
78
|
+
static manifest(): {
|
|
79
|
+
name: string;
|
|
80
|
+
description: string;
|
|
81
|
+
uriTemplate: string;
|
|
82
|
+
category: string;
|
|
83
|
+
schema: {
|
|
84
|
+
type: string;
|
|
85
|
+
properties: {
|
|
86
|
+
homeserver: {
|
|
87
|
+
type: string;
|
|
88
|
+
description: string;
|
|
89
|
+
};
|
|
90
|
+
accessToken: {
|
|
91
|
+
type: string;
|
|
92
|
+
description: string;
|
|
93
|
+
sensitive: boolean;
|
|
94
|
+
};
|
|
95
|
+
rooms: {
|
|
96
|
+
type: string;
|
|
97
|
+
items: {
|
|
98
|
+
type: string;
|
|
99
|
+
};
|
|
100
|
+
description: string;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
required: string[];
|
|
104
|
+
};
|
|
105
|
+
tags: string[];
|
|
106
|
+
capabilityTags: string[];
|
|
107
|
+
security: {
|
|
108
|
+
riskLevel: string;
|
|
109
|
+
resourceAccess: string[];
|
|
110
|
+
notes: string[];
|
|
111
|
+
};
|
|
112
|
+
capabilities: {
|
|
113
|
+
network: {
|
|
114
|
+
egress: boolean;
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
static treeSchema(): ProviderTreeSchema;
|
|
119
|
+
readonly providerName = "matrix";
|
|
120
|
+
readonly eventPrefix = "matrix";
|
|
121
|
+
private readonly _syncStates;
|
|
122
|
+
private readonly _userIds;
|
|
123
|
+
constructor(options: AFSMatrixOptions);
|
|
124
|
+
getMessageCapabilities(): MessageCapabilities;
|
|
125
|
+
createBotClient(config: BotConfig): MatrixClient;
|
|
126
|
+
sendMessage(client: unknown, convId: string, text: string, _opts?: SendOptions): Promise<{
|
|
127
|
+
messageId: string;
|
|
128
|
+
}>;
|
|
129
|
+
sendTypingIndicator(client: unknown, convId: string): Promise<void>;
|
|
130
|
+
normalizeMessage(raw: Record<string, unknown>): BufferedMessage;
|
|
131
|
+
normalizeSender(raw: Record<string, unknown>): MessageSender;
|
|
132
|
+
start(botName?: string): void;
|
|
133
|
+
stop(botName?: string): void;
|
|
134
|
+
dispose(): void;
|
|
135
|
+
listRootActions(_ctx: RouteContext): Promise<AFSListResult>;
|
|
136
|
+
execStart(_ctx: RouteContext): Promise<AFSExecResult>;
|
|
137
|
+
execStop(_ctx: RouteContext): Promise<AFSExecResult>;
|
|
138
|
+
execProcessEvent(_ctx: RouteContext, args: Record<string, unknown>): Promise<AFSExecResult>;
|
|
139
|
+
private _syncLoop;
|
|
140
|
+
/** Process an externally-provided Matrix event. Public for testing. */
|
|
141
|
+
_processEvent(payload: Record<string, unknown>): void;
|
|
142
|
+
/** Add a room to a bot. Defaults to first bot. */
|
|
143
|
+
addRoom(roomId: string, botName?: string): void;
|
|
144
|
+
/** Remove a room from a bot. */
|
|
145
|
+
removeRoom(roomId: string, botName?: string): void;
|
|
146
|
+
/** Add a message to the ring buffer directly. Public for testing/conformance. */
|
|
147
|
+
_addToBuffer(roomId: string, msg: {
|
|
148
|
+
event_id: string;
|
|
149
|
+
sender: string;
|
|
150
|
+
body: string;
|
|
151
|
+
origin_server_ts?: number;
|
|
152
|
+
}): void;
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
export { AFSMatrix, AFSMatrixOptions, type MatrixClient, type MatrixEvent, type MatrixSyncResponse, type MatrixWhoamiResponse };
|
|
156
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/client.ts","../src/index.ts"],"mappings":";;;;;;;;;;AASA;UAAiB,mBAAA;EACf,WAAA;EACA,UAAA;EACA,OAAA;AAAA;;UAIe,WAAA;EACf,QAAA;EACA,IAAA;EACA,MAAA;EACA,OAAA;IACE,OAAA;IACA,IAAA;IAAA,CACC,GAAA;EAAA;EAEH,gBAAA;EACA,OAAA;AAAA;;UAIe,kBAAA;EACf,UAAA;EACA,KAAA;IACE,IAAA,GAAO,MAAA;MAGH,QAAA;QACE,MAAA,GAAS,WAAA;MAAA;IAAA;EAAA;AAAA;;UAQF,oBAAA;EACf,OAAA;EACA,SAAA;AAAA;AAAA,cAKW,YAAA;EAAA,iBACM,YAAA;EAAA,iBACA,WAAA;EAAA,iBACA,QAAA;cAEL,OAAA,EAAS,mBAAA;EAUf,WAAA,CAAY,MAAA,UAAgB,IAAA,WAAe,OAAA;IAAU,QAAA;EAAA;EASrD,UAAA,CAAW,MAAA,UAAgB,MAAA,UAAgB,OAAA,YAAkB,OAAA;EAQ7D,IAAA,CAAK,KAAA,WAAgB,OAAA,YAAmB,OAAA,CAAQ,kBAAA;EAOhD,MAAA,CAAA,GAAU,OAAA,CAAQ,oBAAA;EAAA,QAMV,KAAA;AAAA;;;UC1DC,gBAAA;EDbf;ECeA,IAAA,GAAO,KAAA;IACL,IAAA;IACA,KAAA;IACA,UAAA;IACA,aAAA;EAAA;EDdF;ECiBA,WAAA;EDfE;ECiBF,UAAA;EDdM;ECgBN,KAAA;EACA,UAAA;AAAA;AAAA,cAWW,SAAA,SAAkB,mBAAA;EAAA,OACtB,QAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SA6BA,UAAA,CAAA,GAAc,kBAAA;EAAA,SA6BZ,YAAA;EAAA,SACA,WAAA;EAAA,iBAEQ,WAAA;EAAA,iBACA,QAAA;cAEL,OAAA,EAAS,gBAAA;EAyCrB,sBAAA,CAAA,GAA0B,mBAAA;EAqB1B,eAAA,CAAgB,MAAA,EAAQ,SAAA,GAAY,YAAA;EAO9B,WAAA,CACJ,MAAA,WACA,MAAA,UACA,IAAA,UACA,KAAA,GAAQ,WAAA,GACP,OAAA;IAAU,SAAA;EAAA;EAMP,mBAAA,CAAoB,MAAA,WAAiB,MAAA,WAAiB,OAAA;EAa5D,gBAAA,CAAiB,GAAA,EAAK,MAAA,oBAA0B,eAAA;EAchD,eAAA,CAAgB,GAAA,EAAK,MAAA,oBAA0B,aAAA;EAa/C,KAAA,CAAM,OAAA;EAUN,IAAA,CAAK,OAAA;EAaL,OAAA,CAAA;EAcM,eAAA,CAAgB,IAAA,EAAM,YAAA,GAAe,OAAA,CAAQ,aAAA;EAuB7C,SAAA,CAAU,IAAA,EAAM,YAAA,GAAe,OAAA,CAAQ,aAAA;EAMvC,QAAA,CAAS,IAAA,EAAM,YAAA,GAAe,OAAA,CAAQ,aAAA;EAMtC,gBAAA,CACJ,IAAA,EAAM,YAAA,EACN,IAAA,EAAM,MAAA,oBACL,OAAA,CAAQ,aAAA;EAAA,QAOG,SAAA;EAtRJ;EA+UV,aAAA,CAAc,OAAA,EAAS,MAAA;EApUF;EAgWrB,OAAA,CAAQ,MAAA,UAAgB,OAAA;EAlUH;EA+UrB,UAAA,CAAW,MAAA,UAAgB,OAAA;EAnQD;EA6Q1B,YAAA,CACE,MAAA,UACA,GAAA;IACE,QAAA;IACA,MAAA;IACA,IAAA;IACA,gBAAA;EAAA;AAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { Actions } from "@aigne/afs/provider";
|
|
2
|
+
import { BaseMessageProvider } from "@aigne/afs-messaging";
|
|
3
|
+
|
|
4
|
+
//#region src/client.ts
|
|
5
|
+
/**
|
|
6
|
+
* MatrixClient — pure Matrix Client-Server API wrapper.
|
|
7
|
+
*
|
|
8
|
+
* Zero external dependencies (uses native fetch).
|
|
9
|
+
* No AFS or application-specific concepts.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_TIMEOUT = 3e4;
|
|
12
|
+
let _txnCounter = 0;
|
|
13
|
+
var MatrixClient = class {
|
|
14
|
+
_accessToken;
|
|
15
|
+
_homeserver;
|
|
16
|
+
_timeout;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
if (!options.accessToken) throw new Error("MatrixClient requires an accessToken");
|
|
19
|
+
if (!options.homeserver) throw new Error("MatrixClient requires a homeserver");
|
|
20
|
+
this._accessToken = options.accessToken;
|
|
21
|
+
this._homeserver = options.homeserver.replace(/\/+$/, "");
|
|
22
|
+
this._timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
23
|
+
}
|
|
24
|
+
async sendMessage(roomId, text) {
|
|
25
|
+
const txnId = `m${Date.now()}.${_txnCounter++}`;
|
|
26
|
+
return this._call("PUT", `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`, {
|
|
27
|
+
msgtype: "m.text",
|
|
28
|
+
body: text
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async sendTyping(roomId, userId, timeout = 1e4) {
|
|
32
|
+
await this._call("PUT", `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/typing/${encodeURIComponent(userId)}`, {
|
|
33
|
+
typing: true,
|
|
34
|
+
timeout
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async sync(since, timeout) {
|
|
38
|
+
const params = new URLSearchParams();
|
|
39
|
+
if (since) params.set("since", since);
|
|
40
|
+
if (timeout !== void 0) params.set("timeout", String(timeout));
|
|
41
|
+
return this._call("GET", `/_matrix/client/v3/sync?${params.toString()}`);
|
|
42
|
+
}
|
|
43
|
+
async whoami() {
|
|
44
|
+
return this._call("GET", "/_matrix/client/v3/account/whoami");
|
|
45
|
+
}
|
|
46
|
+
async _call(method, path, body) {
|
|
47
|
+
const headers = { Authorization: `Bearer ${this._accessToken}` };
|
|
48
|
+
const init = {
|
|
49
|
+
method,
|
|
50
|
+
headers,
|
|
51
|
+
signal: AbortSignal.timeout(this._timeout)
|
|
52
|
+
};
|
|
53
|
+
if (body) {
|
|
54
|
+
headers["Content-Type"] = "application/json";
|
|
55
|
+
init.body = JSON.stringify(body);
|
|
56
|
+
}
|
|
57
|
+
const response = await fetch(`${this._homeserver}${path}`, init);
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const desc = (await response.text().catch(() => "")).replace(this._accessToken, "***");
|
|
60
|
+
throw new Error(`Matrix API error ${response.status}: ${desc}`);
|
|
61
|
+
}
|
|
62
|
+
return response.json();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region \0@oxc-project+runtime@0.108.0/helpers/decorate.js
|
|
68
|
+
function __decorate(decorators, target, key, desc) {
|
|
69
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
70
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
71
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
72
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/index.ts
|
|
77
|
+
var AFSMatrix = class extends BaseMessageProvider {
|
|
78
|
+
static manifest() {
|
|
79
|
+
return {
|
|
80
|
+
name: "matrix",
|
|
81
|
+
description: "Matrix messaging — decentralized, federated chat protocol.\n- Sync long-poll for real-time message reception\n- Multi-bot support with per-bot room monitoring\n- Path: /:bot/conversations/:roomId/messages/:eventId",
|
|
82
|
+
uriTemplate: "matrix://{homeserver}",
|
|
83
|
+
category: "messaging",
|
|
84
|
+
schema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
homeserver: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "Matrix homeserver URL"
|
|
90
|
+
},
|
|
91
|
+
accessToken: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "Matrix access token",
|
|
94
|
+
sensitive: true
|
|
95
|
+
},
|
|
96
|
+
rooms: {
|
|
97
|
+
type: "array",
|
|
98
|
+
items: { type: "string" },
|
|
99
|
+
description: "Room IDs to monitor"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
required: ["homeserver", "accessToken"]
|
|
103
|
+
},
|
|
104
|
+
tags: [
|
|
105
|
+
"matrix",
|
|
106
|
+
"messaging",
|
|
107
|
+
"chat",
|
|
108
|
+
"decentralized",
|
|
109
|
+
"federated"
|
|
110
|
+
],
|
|
111
|
+
capabilityTags: [
|
|
112
|
+
"read-write",
|
|
113
|
+
"crud",
|
|
114
|
+
"search",
|
|
115
|
+
"auth:token",
|
|
116
|
+
"remote",
|
|
117
|
+
"http",
|
|
118
|
+
"real-time"
|
|
119
|
+
],
|
|
120
|
+
security: {
|
|
121
|
+
riskLevel: "external",
|
|
122
|
+
resourceAccess: ["internet"],
|
|
123
|
+
notes: ["Connects to Matrix homeserver — requires access token"]
|
|
124
|
+
},
|
|
125
|
+
capabilities: { network: { egress: true } }
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
static treeSchema() {
|
|
129
|
+
return {
|
|
130
|
+
operations: [
|
|
131
|
+
"list",
|
|
132
|
+
"read",
|
|
133
|
+
"exec",
|
|
134
|
+
"stat",
|
|
135
|
+
"explain"
|
|
136
|
+
],
|
|
137
|
+
tree: {
|
|
138
|
+
"/": {
|
|
139
|
+
kind: "messaging:root",
|
|
140
|
+
operations: ["list", "exec"],
|
|
141
|
+
actions: [
|
|
142
|
+
"add-bot",
|
|
143
|
+
"remove-bot",
|
|
144
|
+
"start",
|
|
145
|
+
"stop",
|
|
146
|
+
"process-event"
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
"/{bot}": {
|
|
150
|
+
kind: "messaging:bot",
|
|
151
|
+
operations: ["list", "read"]
|
|
152
|
+
},
|
|
153
|
+
"/{bot}/ctl": {
|
|
154
|
+
kind: "messaging:status",
|
|
155
|
+
operations: ["read"]
|
|
156
|
+
},
|
|
157
|
+
"/{bot}/conversations": {
|
|
158
|
+
kind: "messaging:conversations",
|
|
159
|
+
operations: ["list"]
|
|
160
|
+
},
|
|
161
|
+
"/{bot}/conversations/{convId}": {
|
|
162
|
+
kind: "messaging:conversation",
|
|
163
|
+
operations: ["list"]
|
|
164
|
+
},
|
|
165
|
+
"/{bot}/conversations/{convId}/messages": {
|
|
166
|
+
kind: "messaging:messages",
|
|
167
|
+
operations: ["list", "exec"],
|
|
168
|
+
actions: ["send"]
|
|
169
|
+
},
|
|
170
|
+
"/{bot}/conversations/{convId}/messages/{msgId}": {
|
|
171
|
+
kind: "messaging:message",
|
|
172
|
+
operations: ["read"]
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
auth: {
|
|
176
|
+
type: "token",
|
|
177
|
+
env: ["MATRIX_ACCESS_TOKEN"]
|
|
178
|
+
},
|
|
179
|
+
bestFor: [
|
|
180
|
+
"decentralized chat",
|
|
181
|
+
"federated messaging",
|
|
182
|
+
"privacy-focused comms"
|
|
183
|
+
],
|
|
184
|
+
notFor: ["file storage", "database queries"]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
providerName = "matrix";
|
|
188
|
+
eventPrefix = "matrix";
|
|
189
|
+
_syncStates = /* @__PURE__ */ new Map();
|
|
190
|
+
_userIds = /* @__PURE__ */ new Map();
|
|
191
|
+
constructor(options) {
|
|
192
|
+
let bots;
|
|
193
|
+
if (options.bots) {
|
|
194
|
+
bots = options.bots.map((b) => ({
|
|
195
|
+
name: b.name,
|
|
196
|
+
accessToken: b.token,
|
|
197
|
+
homeserver: b.homeserver,
|
|
198
|
+
conversations: b.conversations
|
|
199
|
+
}));
|
|
200
|
+
for (const bot of options.bots) {
|
|
201
|
+
if (!bot.token) throw new Error(`AFSMatrix bot "${bot.name}" requires a token`);
|
|
202
|
+
if (!bot.homeserver) throw new Error(`AFSMatrix bot "${bot.name}" requires a homeserver`);
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
if (!options.accessToken) throw new Error("AFSMatrix requires an accessToken");
|
|
206
|
+
if (!options.homeserver) throw new Error("AFSMatrix requires a homeserver");
|
|
207
|
+
bots = [{
|
|
208
|
+
name: "default",
|
|
209
|
+
accessToken: options.accessToken,
|
|
210
|
+
homeserver: options.homeserver,
|
|
211
|
+
conversations: options.rooms ?? []
|
|
212
|
+
}];
|
|
213
|
+
}
|
|
214
|
+
super({
|
|
215
|
+
bots,
|
|
216
|
+
bufferSize: options.bufferSize
|
|
217
|
+
});
|
|
218
|
+
for (const bot of bots) this._syncStates.set(bot.name, {
|
|
219
|
+
syncing: false,
|
|
220
|
+
disposed: false,
|
|
221
|
+
abort: null,
|
|
222
|
+
backoff: 1e3,
|
|
223
|
+
nextBatch: null
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
getMessageCapabilities() {
|
|
227
|
+
return {
|
|
228
|
+
formats: {
|
|
229
|
+
send: ["text", "markdown"],
|
|
230
|
+
receive: ["text"]
|
|
231
|
+
},
|
|
232
|
+
maxMessageLength: 65536,
|
|
233
|
+
features: {
|
|
234
|
+
edit: true,
|
|
235
|
+
delete: false,
|
|
236
|
+
reply: true,
|
|
237
|
+
thread: true,
|
|
238
|
+
reaction: true,
|
|
239
|
+
inlineKeyboard: false
|
|
240
|
+
},
|
|
241
|
+
limits: { messagesPerSecond: 10 }
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
createBotClient(config) {
|
|
245
|
+
return new MatrixClient({
|
|
246
|
+
accessToken: config.accessToken,
|
|
247
|
+
homeserver: config.homeserver
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
async sendMessage(client, convId, text, _opts) {
|
|
251
|
+
return { messageId: (await client.sendMessage(convId, text)).event_id };
|
|
252
|
+
}
|
|
253
|
+
async sendTypingIndicator(client, convId) {
|
|
254
|
+
const mc = client;
|
|
255
|
+
const botName = [...this._botClients.entries()].find(([, c]) => c === client)?.[0] ?? "";
|
|
256
|
+
let userId = this._userIds.get(botName);
|
|
257
|
+
if (!userId) {
|
|
258
|
+
userId = (await mc.whoami()).user_id;
|
|
259
|
+
this._userIds.set(botName, userId);
|
|
260
|
+
}
|
|
261
|
+
await mc.sendTyping(convId, userId);
|
|
262
|
+
}
|
|
263
|
+
normalizeMessage(raw) {
|
|
264
|
+
const evt = raw;
|
|
265
|
+
return {
|
|
266
|
+
id: String(evt.event_id ?? "0"),
|
|
267
|
+
text: String(evt.content?.body ?? ""),
|
|
268
|
+
from: this.normalizeSender({ sender: evt.sender }),
|
|
269
|
+
timestamp: evt.origin_server_ts ? Math.floor(evt.origin_server_ts / 1e3) : Math.floor(Date.now() / 1e3),
|
|
270
|
+
conversationId: String(evt.room_id ?? ""),
|
|
271
|
+
platform: { event_id: evt.event_id }
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
normalizeSender(raw) {
|
|
275
|
+
const sender = String(raw.sender ?? raw.id ?? "");
|
|
276
|
+
return {
|
|
277
|
+
id: sender,
|
|
278
|
+
name: sender.match(/^@([^:]+):/)?.[1] ?? sender
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
start(botName) {
|
|
282
|
+
const names = botName ? [botName] : [...this._syncStates.keys()];
|
|
283
|
+
for (const name of names) {
|
|
284
|
+
const state = this._syncStates.get(name);
|
|
285
|
+
if (!state || state.syncing || state.disposed) continue;
|
|
286
|
+
state.syncing = true;
|
|
287
|
+
this._syncLoop(name);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
stop(botName) {
|
|
291
|
+
const names = botName ? [botName] : [...this._syncStates.keys()];
|
|
292
|
+
for (const name of names) {
|
|
293
|
+
const state = this._syncStates.get(name);
|
|
294
|
+
if (!state) continue;
|
|
295
|
+
state.syncing = false;
|
|
296
|
+
if (state.abort) {
|
|
297
|
+
state.abort.abort();
|
|
298
|
+
state.abort = null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
dispose() {
|
|
303
|
+
for (const [, state] of this._syncStates) {
|
|
304
|
+
state.syncing = false;
|
|
305
|
+
state.disposed = true;
|
|
306
|
+
if (state.abort) {
|
|
307
|
+
state.abort.abort();
|
|
308
|
+
state.abort = null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async listRootActions(_ctx) {
|
|
313
|
+
return { data: [
|
|
314
|
+
this.buildEntry("/.actions/add-bot", { meta: { description: "Add a bot instance at runtime" } }),
|
|
315
|
+
this.buildEntry("/.actions/remove-bot", { meta: { description: "Remove a bot instance" } }),
|
|
316
|
+
this.buildEntry("/.actions/start", { meta: { description: "Start sync loop for updates" } }),
|
|
317
|
+
this.buildEntry("/.actions/stop", { meta: { description: "Stop sync loop" } }),
|
|
318
|
+
this.buildEntry("/.actions/process-event", { meta: { description: "Inject a Matrix event externally" } })
|
|
319
|
+
] };
|
|
320
|
+
}
|
|
321
|
+
async execStart(_ctx) {
|
|
322
|
+
this.start();
|
|
323
|
+
return {
|
|
324
|
+
success: true,
|
|
325
|
+
data: {
|
|
326
|
+
ok: true,
|
|
327
|
+
syncing: true
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
async execStop(_ctx) {
|
|
332
|
+
this.stop();
|
|
333
|
+
return {
|
|
334
|
+
success: true,
|
|
335
|
+
data: {
|
|
336
|
+
ok: true,
|
|
337
|
+
syncing: false
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
async execProcessEvent(_ctx, args) {
|
|
342
|
+
this._processEvent(args);
|
|
343
|
+
return {
|
|
344
|
+
success: true,
|
|
345
|
+
data: { ok: true }
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
async _syncLoop(botName) {
|
|
349
|
+
const state = this._syncStates.get(botName);
|
|
350
|
+
const client = this._getClient(botName);
|
|
351
|
+
if (!state || !client) return;
|
|
352
|
+
while (state.syncing && !state.disposed) try {
|
|
353
|
+
state.abort = new AbortController();
|
|
354
|
+
const response = await client.sync(state.nextBatch ?? void 0, 3e4);
|
|
355
|
+
state.nextBatch = response.next_batch;
|
|
356
|
+
if (response.rooms?.join) for (const [roomId, room] of Object.entries(response.rooms.join)) {
|
|
357
|
+
const events = room.timeline?.events ?? [];
|
|
358
|
+
for (const evt of events) if (evt.type === "m.room.message" && evt.content?.msgtype === "m.text") {
|
|
359
|
+
const text = String(evt.content.body ?? "").trim();
|
|
360
|
+
if (!text) continue;
|
|
361
|
+
this.emitMessageReceived(botName, {
|
|
362
|
+
id: String(evt.event_id),
|
|
363
|
+
text,
|
|
364
|
+
from: this.normalizeSender({ sender: evt.sender }),
|
|
365
|
+
timestamp: evt.origin_server_ts ? Math.floor(evt.origin_server_ts / 1e3) : Math.floor(Date.now() / 1e3),
|
|
366
|
+
conversationId: roomId,
|
|
367
|
+
platform: { event_id: evt.event_id }
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
state.backoff = 1e3;
|
|
372
|
+
} catch (err) {
|
|
373
|
+
if (state.disposed) break;
|
|
374
|
+
const msg = err instanceof Error ? err.message : "";
|
|
375
|
+
if (/API error (401|403)/.test(msg)) {
|
|
376
|
+
state.syncing = false;
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
await new Promise((r) => setTimeout(r, state.backoff));
|
|
380
|
+
state.backoff = Math.min(state.backoff * 2, 6e4);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/** Process an externally-provided Matrix event. Public for testing. */
|
|
384
|
+
_processEvent(payload) {
|
|
385
|
+
const botName = payload.botName ?? this._botOrder[0] ?? "default";
|
|
386
|
+
const evt = payload;
|
|
387
|
+
const eventId = String(evt.event_id ?? evt.id ?? Date.now());
|
|
388
|
+
const sender = String(evt.sender ?? "");
|
|
389
|
+
const body = String(evt.content?.body ?? evt.body ?? "").trim();
|
|
390
|
+
const roomId = String(evt.room_id ?? evt.roomId ?? "");
|
|
391
|
+
const originServerTs = evt.origin_server_ts ?? evt.timestamp;
|
|
392
|
+
if (!body) return;
|
|
393
|
+
this.emitMessageReceived(botName, {
|
|
394
|
+
id: eventId,
|
|
395
|
+
text: body,
|
|
396
|
+
from: this.normalizeSender({ sender }),
|
|
397
|
+
timestamp: originServerTs ? Math.floor(Number(originServerTs) / 1e3) : Math.floor(Date.now() / 1e3),
|
|
398
|
+
conversationId: roomId,
|
|
399
|
+
platform: { event_id: eventId }
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
/** Add a room to a bot. Defaults to first bot. */
|
|
403
|
+
addRoom(roomId, botName) {
|
|
404
|
+
const name = botName ?? this._botOrder[0];
|
|
405
|
+
if (!name) return;
|
|
406
|
+
const convs = this._botConversations.get(name);
|
|
407
|
+
if (!convs || convs.has(roomId)) return;
|
|
408
|
+
convs.add(roomId);
|
|
409
|
+
const buffers = this._botBuffers.get(name);
|
|
410
|
+
if (buffers && !buffers.has(roomId)) buffers.set(roomId, []);
|
|
411
|
+
}
|
|
412
|
+
/** Remove a room from a bot. */
|
|
413
|
+
removeRoom(roomId, botName) {
|
|
414
|
+
const name = botName ?? this._botOrder[0];
|
|
415
|
+
if (!name) return;
|
|
416
|
+
const convs = this._botConversations.get(name);
|
|
417
|
+
if (convs) convs.delete(roomId);
|
|
418
|
+
const buffers = this._botBuffers.get(name);
|
|
419
|
+
if (buffers) buffers.delete(roomId);
|
|
420
|
+
}
|
|
421
|
+
/** Add a message to the ring buffer directly. Public for testing/conformance. */
|
|
422
|
+
_addToBuffer(roomId, msg) {
|
|
423
|
+
const botName = this._botOrder[0] ?? "default";
|
|
424
|
+
const convs = this._botConversations.get(botName);
|
|
425
|
+
if (convs && !convs.has(roomId)) convs.add(roomId);
|
|
426
|
+
let botBuffers = this._botBuffers.get(botName);
|
|
427
|
+
if (!botBuffers) {
|
|
428
|
+
botBuffers = /* @__PURE__ */ new Map();
|
|
429
|
+
this._botBuffers.set(botName, botBuffers);
|
|
430
|
+
}
|
|
431
|
+
let buffer = botBuffers.get(roomId);
|
|
432
|
+
if (!buffer) {
|
|
433
|
+
buffer = [];
|
|
434
|
+
botBuffers.set(roomId, buffer);
|
|
435
|
+
}
|
|
436
|
+
const senderName = msg.sender.match(/^@([^:]+):/)?.[1] ?? msg.sender;
|
|
437
|
+
buffer.push({
|
|
438
|
+
id: msg.event_id,
|
|
439
|
+
text: msg.body,
|
|
440
|
+
from: {
|
|
441
|
+
id: msg.sender,
|
|
442
|
+
name: senderName
|
|
443
|
+
},
|
|
444
|
+
timestamp: msg.origin_server_ts ? Math.floor(msg.origin_server_ts / 1e3) : Math.floor(Date.now() / 1e3),
|
|
445
|
+
conversationId: roomId,
|
|
446
|
+
platform: { event_id: msg.event_id }
|
|
447
|
+
});
|
|
448
|
+
while (buffer.length > this._bufferSize) buffer.shift();
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
__decorate([Actions("/")], AFSMatrix.prototype, "listRootActions", null);
|
|
452
|
+
__decorate([Actions.Exec("/", "start")], AFSMatrix.prototype, "execStart", null);
|
|
453
|
+
__decorate([Actions.Exec("/", "stop")], AFSMatrix.prototype, "execStop", null);
|
|
454
|
+
__decorate([Actions.Exec("/", "process-event")], AFSMatrix.prototype, "execProcessEvent", null);
|
|
455
|
+
|
|
456
|
+
//#endregion
|
|
457
|
+
export { AFSMatrix };
|
|
458
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/client.ts","../src/index.ts"],"sourcesContent":["/**\n * MatrixClient — pure Matrix Client-Server API wrapper.\n *\n * Zero external dependencies (uses native fetch).\n * No AFS or application-specific concepts.\n */\n\nconst DEFAULT_TIMEOUT = 30_000;\n\nexport interface MatrixClientOptions {\n accessToken: string;\n homeserver: string;\n timeout?: number;\n}\n\n/** Matrix room message event (subset). */\nexport interface MatrixEvent {\n event_id: string;\n type: string;\n sender: string;\n content: {\n msgtype?: string;\n body?: string;\n [key: string]: unknown;\n };\n origin_server_ts: number;\n room_id?: string;\n}\n\n/** Matrix sync response (subset). */\nexport interface MatrixSyncResponse {\n next_batch: string;\n rooms?: {\n join?: Record<\n string,\n {\n timeline?: {\n events?: MatrixEvent[];\n };\n }\n >;\n };\n}\n\n/** Matrix whoami response. */\nexport interface MatrixWhoamiResponse {\n user_id: string;\n device_id?: string;\n}\n\nlet _txnCounter = 0;\n\nexport class MatrixClient {\n private readonly _accessToken: string;\n private readonly _homeserver: string;\n private readonly _timeout: number;\n\n constructor(options: MatrixClientOptions) {\n if (!options.accessToken) throw new Error(\"MatrixClient requires an accessToken\");\n if (!options.homeserver) throw new Error(\"MatrixClient requires a homeserver\");\n this._accessToken = options.accessToken;\n this._homeserver = options.homeserver.replace(/\\/+$/, \"\");\n this._timeout = options.timeout ?? DEFAULT_TIMEOUT;\n }\n\n // ─── API Methods ─────────────────────────────────────────\n\n async sendMessage(roomId: string, text: string): Promise<{ event_id: string }> {\n const txnId = `m${Date.now()}.${_txnCounter++}`;\n return this._call(\n \"PUT\",\n `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,\n { msgtype: \"m.text\", body: text },\n );\n }\n\n async sendTyping(roomId: string, userId: string, timeout = 10000): Promise<void> {\n await this._call(\n \"PUT\",\n `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/typing/${encodeURIComponent(userId)}`,\n { typing: true, timeout },\n );\n }\n\n async sync(since?: string, timeout?: number): Promise<MatrixSyncResponse> {\n const params = new URLSearchParams();\n if (since) params.set(\"since\", since);\n if (timeout !== undefined) params.set(\"timeout\", String(timeout));\n return this._call(\"GET\", `/_matrix/client/v3/sync?${params.toString()}`);\n }\n\n async whoami(): Promise<MatrixWhoamiResponse> {\n return this._call(\"GET\", \"/_matrix/client/v3/account/whoami\");\n }\n\n // ─── Internal ────────────────────────────────────────────\n\n private async _call(method: string, path: string, body?: Record<string, unknown>): Promise<any> {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this._accessToken}`,\n };\n const init: RequestInit = {\n method,\n headers,\n signal: AbortSignal.timeout(this._timeout),\n };\n\n if (body) {\n headers[\"Content-Type\"] = \"application/json\";\n init.body = JSON.stringify(body);\n }\n\n const response = await fetch(`${this._homeserver}${path}`, init);\n\n if (!response.ok) {\n const text = await response.text().catch(() => \"\");\n const desc = text.replace(this._accessToken, \"***\");\n throw new Error(`Matrix API error ${response.status}: ${desc}`);\n }\n\n return response.json();\n }\n}\n","/**\n * AFSMatrix — AFS provider for Matrix (Element).\n *\n * Extends BaseMessageProvider (Plan 9 model).\n * Path structure (from base):\n * / → List bots\n * /:botName/ctl → Bot status\n * /:botName/conversations/:roomId/messages → List messages\n * /:botName/conversations/:roomId/messages/:evId → Read message\n *\n * Additional Matrix actions:\n * /.actions/start → Start sync loop\n * /.actions/stop → Stop sync loop\n * /.actions/process-event → Inject event externally\n *\n * Standard events (from base):\n * messaging:message → Inbound text message\n * messaging:command → Inbound /command\n */\n\nimport type { AFSExecResult, AFSListResult, ProviderTreeSchema } from \"@aigne/afs\";\nimport { Actions, type RouteContext } from \"@aigne/afs/provider\";\nimport type {\n BotConfig,\n BufferedMessage,\n MessageCapabilities,\n MessageSender,\n SendOptions,\n} from \"@aigne/afs-messaging\";\nimport { BaseMessageProvider } from \"@aigne/afs-messaging\";\nimport { MatrixClient, type MatrixEvent } from \"./client.js\";\n\nexport type {\n MatrixClient,\n MatrixEvent,\n MatrixSyncResponse,\n MatrixWhoamiResponse,\n} from \"./client.js\";\n\nexport interface AFSMatrixOptions {\n /** Multi-bot config */\n bots?: Array<{\n name: string;\n token: string;\n homeserver: string;\n conversations?: string[];\n }>;\n /** Single-bot shorthand: access token */\n accessToken?: string;\n /** Single-bot shorthand: homeserver URL */\n homeserver?: string;\n /** Single-bot shorthand: room IDs to monitor */\n rooms?: string[];\n bufferSize?: number;\n}\n\ninterface SyncState {\n syncing: boolean;\n disposed: boolean;\n abort: AbortController | null;\n backoff: number;\n nextBatch: string | null;\n}\n\nexport class AFSMatrix extends BaseMessageProvider {\n static manifest() {\n return {\n name: \"matrix\",\n description:\n \"Matrix messaging — decentralized, federated chat protocol.\\n- Sync long-poll for real-time message reception\\n- Multi-bot support with per-bot room monitoring\\n- Path: /:bot/conversations/:roomId/messages/:eventId\",\n uriTemplate: \"matrix://{homeserver}\",\n category: \"messaging\",\n schema: {\n type: \"object\",\n properties: {\n homeserver: { type: \"string\", description: \"Matrix homeserver URL\" },\n accessToken: { type: \"string\", description: \"Matrix access token\", sensitive: true },\n rooms: { type: \"array\", items: { type: \"string\" }, description: \"Room IDs to monitor\" },\n },\n required: [\"homeserver\", \"accessToken\"],\n },\n tags: [\"matrix\", \"messaging\", \"chat\", \"decentralized\", \"federated\"],\n capabilityTags: [\"read-write\", \"crud\", \"search\", \"auth:token\", \"remote\", \"http\", \"real-time\"],\n security: {\n riskLevel: \"external\",\n resourceAccess: [\"internet\"],\n notes: [\"Connects to Matrix homeserver — requires access token\"],\n },\n capabilities: {\n network: { egress: true },\n },\n };\n }\n\n static treeSchema(): ProviderTreeSchema {\n return {\n operations: [\"list\", \"read\", \"exec\", \"stat\", \"explain\"],\n tree: {\n \"/\": {\n kind: \"messaging:root\",\n operations: [\"list\", \"exec\"],\n actions: [\"add-bot\", \"remove-bot\", \"start\", \"stop\", \"process-event\"],\n },\n \"/{bot}\": { kind: \"messaging:bot\", operations: [\"list\", \"read\"] },\n \"/{bot}/ctl\": { kind: \"messaging:status\", operations: [\"read\"] },\n \"/{bot}/conversations\": { kind: \"messaging:conversations\", operations: [\"list\"] },\n \"/{bot}/conversations/{convId}\": { kind: \"messaging:conversation\", operations: [\"list\"] },\n \"/{bot}/conversations/{convId}/messages\": {\n kind: \"messaging:messages\",\n operations: [\"list\", \"exec\"],\n actions: [\"send\"],\n },\n \"/{bot}/conversations/{convId}/messages/{msgId}\": {\n kind: \"messaging:message\",\n operations: [\"read\"],\n },\n },\n auth: { type: \"token\", env: [\"MATRIX_ACCESS_TOKEN\"] },\n bestFor: [\"decentralized chat\", \"federated messaging\", \"privacy-focused comms\"],\n notFor: [\"file storage\", \"database queries\"],\n };\n }\n\n readonly providerName = \"matrix\";\n readonly eventPrefix = \"matrix\";\n\n private readonly _syncStates = new Map<string, SyncState>();\n private readonly _userIds = new Map<string, string>();\n\n constructor(options: AFSMatrixOptions) {\n let bots: BotConfig[];\n if (options.bots) {\n bots = options.bots.map((b) => ({\n name: b.name,\n accessToken: b.token,\n homeserver: b.homeserver,\n conversations: b.conversations,\n }));\n for (const bot of options.bots) {\n if (!bot.token) throw new Error(`AFSMatrix bot \"${bot.name}\" requires a token`);\n if (!bot.homeserver) throw new Error(`AFSMatrix bot \"${bot.name}\" requires a homeserver`);\n }\n } else {\n if (!options.accessToken) throw new Error(\"AFSMatrix requires an accessToken\");\n if (!options.homeserver) throw new Error(\"AFSMatrix requires a homeserver\");\n bots = [\n {\n name: \"default\",\n accessToken: options.accessToken,\n homeserver: options.homeserver,\n conversations: options.rooms ?? [],\n },\n ];\n }\n\n super({ bots, bufferSize: options.bufferSize });\n\n for (const bot of bots) {\n this._syncStates.set(bot.name, {\n syncing: false,\n disposed: false,\n abort: null,\n backoff: 1000,\n nextBatch: null,\n });\n }\n }\n\n // ─── Abstract Implementation ─────────────────────────\n\n getMessageCapabilities(): MessageCapabilities {\n return {\n formats: {\n send: [\"text\", \"markdown\"],\n receive: [\"text\"],\n },\n maxMessageLength: 65536,\n features: {\n edit: true,\n delete: false,\n reply: true,\n thread: true,\n reaction: true,\n inlineKeyboard: false,\n },\n limits: {\n messagesPerSecond: 10,\n },\n };\n }\n\n createBotClient(config: BotConfig): MatrixClient {\n return new MatrixClient({\n accessToken: config.accessToken as string,\n homeserver: config.homeserver as string,\n });\n }\n\n async sendMessage(\n client: unknown,\n convId: string,\n text: string,\n _opts?: SendOptions,\n ): Promise<{ messageId: string }> {\n const mc = client as MatrixClient;\n const result = await mc.sendMessage(convId, text);\n return { messageId: result.event_id };\n }\n\n async sendTypingIndicator(client: unknown, convId: string): Promise<void> {\n const mc = client as MatrixClient;\n // Resolve user ID (cached after first call)\n const botName = [...this._botClients.entries()].find(([, c]) => c === client)?.[0] ?? \"\";\n let userId = this._userIds.get(botName);\n if (!userId) {\n const whoami = await mc.whoami();\n userId = whoami.user_id;\n this._userIds.set(botName, userId);\n }\n await mc.sendTyping(convId, userId);\n }\n\n normalizeMessage(raw: Record<string, unknown>): BufferedMessage {\n const evt = raw as unknown as MatrixEvent;\n return {\n id: String(evt.event_id ?? \"0\"),\n text: String(evt.content?.body ?? \"\"),\n from: this.normalizeSender({ sender: evt.sender } as unknown as Record<string, unknown>),\n timestamp: evt.origin_server_ts\n ? Math.floor(evt.origin_server_ts / 1000)\n : Math.floor(Date.now() / 1000),\n conversationId: String(evt.room_id ?? \"\"),\n platform: { event_id: evt.event_id },\n };\n }\n\n normalizeSender(raw: Record<string, unknown>): MessageSender {\n const sender = String(raw.sender ?? raw.id ?? \"\");\n // Extract localpart from @user:server format\n const match = sender.match(/^@([^:]+):/);\n const name = match?.[1] ?? sender;\n return {\n id: sender,\n name,\n };\n }\n\n // ─── Lifecycle ─────────────────────────────────────────\n\n start(botName?: string): void {\n const names = botName ? [botName] : [...this._syncStates.keys()];\n for (const name of names) {\n const state = this._syncStates.get(name);\n if (!state || state.syncing || state.disposed) continue;\n state.syncing = true;\n this._syncLoop(name);\n }\n }\n\n stop(botName?: string): void {\n const names = botName ? [botName] : [...this._syncStates.keys()];\n for (const name of names) {\n const state = this._syncStates.get(name);\n if (!state) continue;\n state.syncing = false;\n if (state.abort) {\n state.abort.abort();\n state.abort = null;\n }\n }\n }\n\n dispose(): void {\n for (const [, state] of this._syncStates) {\n state.syncing = false;\n state.disposed = true;\n if (state.abort) {\n state.abort.abort();\n state.abort = null;\n }\n }\n }\n\n // ─── Matrix-specific Actions ─────────────────────────\n\n @Actions(\"/\")\n async listRootActions(_ctx: RouteContext): Promise<AFSListResult> {\n return {\n data: [\n this.buildEntry(\"/.actions/add-bot\", {\n meta: { description: \"Add a bot instance at runtime\" },\n }),\n this.buildEntry(\"/.actions/remove-bot\", {\n meta: { description: \"Remove a bot instance\" },\n }),\n this.buildEntry(\"/.actions/start\", {\n meta: { description: \"Start sync loop for updates\" },\n }),\n this.buildEntry(\"/.actions/stop\", {\n meta: { description: \"Stop sync loop\" },\n }),\n this.buildEntry(\"/.actions/process-event\", {\n meta: { description: \"Inject a Matrix event externally\" },\n }),\n ],\n };\n }\n\n @Actions.Exec(\"/\", \"start\")\n async execStart(_ctx: RouteContext): Promise<AFSExecResult> {\n this.start();\n return { success: true, data: { ok: true, syncing: true } };\n }\n\n @Actions.Exec(\"/\", \"stop\")\n async execStop(_ctx: RouteContext): Promise<AFSExecResult> {\n this.stop();\n return { success: true, data: { ok: true, syncing: false } };\n }\n\n @Actions.Exec(\"/\", \"process-event\")\n async execProcessEvent(\n _ctx: RouteContext,\n args: Record<string, unknown>,\n ): Promise<AFSExecResult> {\n this._processEvent(args);\n return { success: true, data: { ok: true } };\n }\n\n // ─── Sync Loop ──────────────────────────────────────\n\n private async _syncLoop(botName: string): Promise<void> {\n const state = this._syncStates.get(botName);\n const client = this._getClient(botName) as MatrixClient | undefined;\n if (!state || !client) return;\n\n while (state.syncing && !state.disposed) {\n try {\n state.abort = new AbortController();\n const response = await client.sync(state.nextBatch ?? undefined, 30000);\n\n state.nextBatch = response.next_batch;\n\n // Process joined room events\n if (response.rooms?.join) {\n for (const [roomId, room] of Object.entries(response.rooms.join)) {\n const events = room.timeline?.events ?? [];\n for (const evt of events) {\n if (evt.type === \"m.room.message\" && evt.content?.msgtype === \"m.text\") {\n const text = String(evt.content.body ?? \"\").trim();\n if (!text) continue;\n\n this.emitMessageReceived(botName, {\n id: String(evt.event_id),\n text,\n from: this.normalizeSender({ sender: evt.sender } as unknown as Record<\n string,\n unknown\n >),\n timestamp: evt.origin_server_ts\n ? Math.floor(evt.origin_server_ts / 1000)\n : Math.floor(Date.now() / 1000),\n conversationId: roomId,\n platform: { event_id: evt.event_id },\n });\n }\n }\n }\n }\n\n state.backoff = 1000;\n } catch (err) {\n if (state.disposed) break;\n // Fail fast on auth errors — retrying won't help and may cause lockout\n const msg = err instanceof Error ? err.message : \"\";\n if (/API error (401|403)/.test(msg)) {\n state.syncing = false;\n break;\n }\n await new Promise((r) => setTimeout(r, state.backoff));\n state.backoff = Math.min(state.backoff * 2, 60_000);\n }\n }\n }\n\n // ─── External Event Injection ──────────────────────────\n\n /** Process an externally-provided Matrix event. Public for testing. */\n _processEvent(payload: Record<string, unknown>): void {\n const botName = (payload.botName as string) ?? this._botOrder[0] ?? \"default\";\n const evt = payload as any;\n\n // Support both wrapped and direct event formats\n const eventId = String(evt.event_id ?? evt.id ?? Date.now());\n const sender = String(evt.sender ?? \"\");\n const body = String(evt.content?.body ?? evt.body ?? \"\").trim();\n const roomId = String(evt.room_id ?? evt.roomId ?? \"\");\n const originServerTs = evt.origin_server_ts ?? evt.timestamp;\n\n if (!body) return;\n\n this.emitMessageReceived(botName, {\n id: eventId,\n text: body,\n from: this.normalizeSender({ sender } as unknown as Record<string, unknown>),\n timestamp: originServerTs\n ? Math.floor(Number(originServerTs) / 1000)\n : Math.floor(Date.now() / 1000),\n conversationId: roomId,\n platform: { event_id: eventId },\n });\n }\n\n // ─── Convenience Methods (test helpers) ─────────────\n\n /** Add a room to a bot. Defaults to first bot. */\n addRoom(roomId: string, botName?: string): void {\n const name = botName ?? this._botOrder[0];\n if (!name) return;\n const convs = this._botConversations.get(name);\n if (!convs || convs.has(roomId)) return;\n convs.add(roomId);\n const buffers = this._botBuffers.get(name);\n if (buffers && !buffers.has(roomId)) {\n buffers.set(roomId, []);\n }\n }\n\n /** Remove a room from a bot. */\n removeRoom(roomId: string, botName?: string): void {\n const name = botName ?? this._botOrder[0];\n if (!name) return;\n const convs = this._botConversations.get(name);\n if (convs) convs.delete(roomId);\n const buffers = this._botBuffers.get(name);\n if (buffers) buffers.delete(roomId);\n }\n\n /** Add a message to the ring buffer directly. Public for testing/conformance. */\n _addToBuffer(\n roomId: string,\n msg: {\n event_id: string;\n sender: string;\n body: string;\n origin_server_ts?: number;\n },\n ): void {\n const botName = this._botOrder[0] ?? \"default\";\n\n // Auto-add conversation\n const convs = this._botConversations.get(botName);\n if (convs && !convs.has(roomId)) {\n convs.add(roomId);\n }\n\n // Add to buffer directly (no event emission — for test setup)\n let botBuffers = this._botBuffers.get(botName);\n if (!botBuffers) {\n botBuffers = new Map();\n this._botBuffers.set(botName, botBuffers);\n }\n let buffer = botBuffers.get(roomId);\n if (!buffer) {\n buffer = [];\n botBuffers.set(roomId, buffer);\n }\n\n // Extract localpart from @user:server\n const match = msg.sender.match(/^@([^:]+):/);\n const senderName = match?.[1] ?? msg.sender;\n\n buffer.push({\n id: msg.event_id,\n text: msg.body,\n from: { id: msg.sender, name: senderName },\n timestamp: msg.origin_server_ts\n ? Math.floor(msg.origin_server_ts / 1000)\n : Math.floor(Date.now() / 1000),\n conversationId: roomId,\n platform: { event_id: msg.event_id },\n });\n while (buffer.length > this._bufferSize) {\n buffer.shift();\n }\n }\n}\n"],"mappings":"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"}
|