@auxiora/channels 1.0.0
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 +191 -0
- package/dist/adapters/bluebubbles.d.ts +63 -0
- package/dist/adapters/bluebubbles.d.ts.map +1 -0
- package/dist/adapters/bluebubbles.js +197 -0
- package/dist/adapters/bluebubbles.js.map +1 -0
- package/dist/adapters/discord.d.ts +27 -0
- package/dist/adapters/discord.d.ts.map +1 -0
- package/dist/adapters/discord.js +202 -0
- package/dist/adapters/discord.js.map +1 -0
- package/dist/adapters/email.d.ts +39 -0
- package/dist/adapters/email.d.ts.map +1 -0
- package/dist/adapters/email.js +359 -0
- package/dist/adapters/email.js.map +1 -0
- package/dist/adapters/googlechat.d.ts +77 -0
- package/dist/adapters/googlechat.d.ts.map +1 -0
- package/dist/adapters/googlechat.js +232 -0
- package/dist/adapters/googlechat.js.map +1 -0
- package/dist/adapters/matrix.d.ts +37 -0
- package/dist/adapters/matrix.d.ts.map +1 -0
- package/dist/adapters/matrix.js +262 -0
- package/dist/adapters/matrix.js.map +1 -0
- package/dist/adapters/signal.d.ts +32 -0
- package/dist/adapters/signal.d.ts.map +1 -0
- package/dist/adapters/signal.js +216 -0
- package/dist/adapters/signal.js.map +1 -0
- package/dist/adapters/slack.d.ts +29 -0
- package/dist/adapters/slack.d.ts.map +1 -0
- package/dist/adapters/slack.js +202 -0
- package/dist/adapters/slack.js.map +1 -0
- package/dist/adapters/teams.d.ts +66 -0
- package/dist/adapters/teams.d.ts.map +1 -0
- package/dist/adapters/teams.js +227 -0
- package/dist/adapters/teams.js.map +1 -0
- package/dist/adapters/telegram.d.ts +28 -0
- package/dist/adapters/telegram.d.ts.map +1 -0
- package/dist/adapters/telegram.js +170 -0
- package/dist/adapters/telegram.js.map +1 -0
- package/dist/adapters/twilio.d.ts +63 -0
- package/dist/adapters/twilio.d.ts.map +1 -0
- package/dist/adapters/twilio.js +193 -0
- package/dist/adapters/twilio.js.map +1 -0
- package/dist/adapters/whatsapp.d.ts +99 -0
- package/dist/adapters/whatsapp.d.ts.map +1 -0
- package/dist/adapters/whatsapp.js +218 -0
- package/dist/adapters/whatsapp.js.map +1 -0
- package/dist/adapters/zalo.d.ts +64 -0
- package/dist/adapters/zalo.d.ts.map +1 -0
- package/dist/adapters/zalo.js +216 -0
- package/dist/adapters/zalo.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/manager.d.ts +35 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +127 -0
- package/dist/manager.js.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +32 -0
- package/src/adapters/bluebubbles.ts +294 -0
- package/src/adapters/discord.ts +253 -0
- package/src/adapters/email.ts +457 -0
- package/src/adapters/googlechat.ts +364 -0
- package/src/adapters/matrix.ts +376 -0
- package/src/adapters/signal.ts +313 -0
- package/src/adapters/slack.ts +252 -0
- package/src/adapters/teams.ts +320 -0
- package/src/adapters/telegram.ts +208 -0
- package/src/adapters/twilio.ts +256 -0
- package/src/adapters/whatsapp.ts +342 -0
- package/src/adapters/zalo.ts +319 -0
- package/src/index.ts +78 -0
- package/src/manager.ts +180 -0
- package/src/types.ts +84 -0
- package/tests/bluebubbles.test.ts +438 -0
- package/tests/email.test.ts +136 -0
- package/tests/googlechat.test.ts +439 -0
- package/tests/matrix.test.ts +564 -0
- package/tests/signal.test.ts +404 -0
- package/tests/slack.test.ts +343 -0
- package/tests/teams.test.ts +429 -0
- package/tests/twilio.test.ts +269 -0
- package/tests/whatsapp.test.ts +530 -0
- package/tests/zalo.test.ts +499 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { MatrixAdapter } from '../src/adapters/matrix.js';
|
|
3
|
+
|
|
4
|
+
// Mock audit
|
|
5
|
+
vi.mock('@auxiora/audit', () => ({
|
|
6
|
+
audit: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe('MatrixAdapter', () => {
|
|
10
|
+
let adapter: MatrixAdapter;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
adapter = new MatrixAdapter({
|
|
14
|
+
homeserverUrl: 'https://matrix.example.com',
|
|
15
|
+
userId: '@bot:example.com',
|
|
16
|
+
accessToken: 'test-access-token',
|
|
17
|
+
autoJoinRooms: true,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should have correct metadata', () => {
|
|
26
|
+
expect(adapter.type).toBe('matrix');
|
|
27
|
+
expect(adapter.name).toBe('Matrix');
|
|
28
|
+
expect(adapter.isConnected()).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should connect successfully', async () => {
|
|
32
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
33
|
+
ok: true,
|
|
34
|
+
json: async () => ({ user_id: '@bot:example.com' }),
|
|
35
|
+
} as Response);
|
|
36
|
+
|
|
37
|
+
// Mock the sync call to avoid infinite loop
|
|
38
|
+
const syncResponse = {
|
|
39
|
+
ok: true,
|
|
40
|
+
json: async () => ({ next_batch: 'batch1', rooms: {} }),
|
|
41
|
+
} as Response;
|
|
42
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(syncResponse);
|
|
43
|
+
|
|
44
|
+
await adapter.connect();
|
|
45
|
+
expect(adapter.isConnected()).toBe(true);
|
|
46
|
+
|
|
47
|
+
await adapter.disconnect();
|
|
48
|
+
expect(adapter.isConnected()).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should fail to connect with invalid credentials', async () => {
|
|
52
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
53
|
+
ok: false,
|
|
54
|
+
status: 401,
|
|
55
|
+
statusText: 'Unauthorized',
|
|
56
|
+
text: async () => 'Unauthorized',
|
|
57
|
+
} as unknown as Response);
|
|
58
|
+
|
|
59
|
+
await expect(adapter.connect()).rejects.toThrow('Matrix API error 401');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should send a message successfully', async () => {
|
|
63
|
+
// Connect first
|
|
64
|
+
vi.spyOn(globalThis, 'fetch')
|
|
65
|
+
.mockResolvedValueOnce({
|
|
66
|
+
ok: true,
|
|
67
|
+
json: async () => ({ user_id: '@bot:example.com' }),
|
|
68
|
+
} as Response)
|
|
69
|
+
.mockResolvedValueOnce({
|
|
70
|
+
ok: true,
|
|
71
|
+
json: async () => ({ next_batch: 'batch1', rooms: {} }),
|
|
72
|
+
} as Response);
|
|
73
|
+
|
|
74
|
+
await adapter.connect();
|
|
75
|
+
|
|
76
|
+
// Mock send
|
|
77
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
78
|
+
ok: true,
|
|
79
|
+
json: async () => ({ event_id: '$event123' }),
|
|
80
|
+
} as Response);
|
|
81
|
+
|
|
82
|
+
const result = await adapter.send('!room:example.com', {
|
|
83
|
+
content: 'Hello, Matrix!',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(result.success).toBe(true);
|
|
87
|
+
expect(result.messageId).toBe('$event123');
|
|
88
|
+
|
|
89
|
+
await adapter.disconnect();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle send errors', async () => {
|
|
93
|
+
// Connect first
|
|
94
|
+
vi.spyOn(globalThis, 'fetch')
|
|
95
|
+
.mockResolvedValueOnce({
|
|
96
|
+
ok: true,
|
|
97
|
+
json: async () => ({ user_id: '@bot:example.com' }),
|
|
98
|
+
} as Response)
|
|
99
|
+
.mockResolvedValueOnce({
|
|
100
|
+
ok: true,
|
|
101
|
+
json: async () => ({ next_batch: 'batch1', rooms: {} }),
|
|
102
|
+
} as Response);
|
|
103
|
+
|
|
104
|
+
await adapter.connect();
|
|
105
|
+
|
|
106
|
+
// Mock failed send
|
|
107
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
|
|
108
|
+
ok: false,
|
|
109
|
+
status: 403,
|
|
110
|
+
statusText: 'Forbidden',
|
|
111
|
+
text: async () => 'Not a member of the room',
|
|
112
|
+
} as unknown as Response);
|
|
113
|
+
|
|
114
|
+
const result = await adapter.send('!room:example.com', {
|
|
115
|
+
content: 'Should fail',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(result.success).toBe(false);
|
|
119
|
+
expect(result.error).toContain('403');
|
|
120
|
+
|
|
121
|
+
await adapter.disconnect();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle incoming messages via sync', async () => {
|
|
125
|
+
const receivedMessages: unknown[] = [];
|
|
126
|
+
adapter.onMessage(async (msg) => {
|
|
127
|
+
receivedMessages.push(msg);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const syncResponse = {
|
|
131
|
+
next_batch: 'batch2',
|
|
132
|
+
rooms: {
|
|
133
|
+
join: {
|
|
134
|
+
'!room:example.com': {
|
|
135
|
+
timeline: {
|
|
136
|
+
events: [
|
|
137
|
+
{
|
|
138
|
+
event_id: '$msg1',
|
|
139
|
+
type: 'm.room.message',
|
|
140
|
+
sender: '@alice:example.com',
|
|
141
|
+
origin_server_ts: 1700000000000,
|
|
142
|
+
content: {
|
|
143
|
+
msgtype: 'm.text',
|
|
144
|
+
body: 'Hello bot!',
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Connect: whoami + first sync (with messages) + second sync (abort)
|
|
155
|
+
vi.spyOn(globalThis, 'fetch')
|
|
156
|
+
.mockResolvedValueOnce({
|
|
157
|
+
ok: true,
|
|
158
|
+
json: async () => ({ user_id: '@bot:example.com' }),
|
|
159
|
+
} as Response)
|
|
160
|
+
.mockResolvedValueOnce({
|
|
161
|
+
ok: true,
|
|
162
|
+
json: async () => syncResponse,
|
|
163
|
+
} as Response)
|
|
164
|
+
.mockImplementation(() => new Promise(() => {})); // Hang on subsequent syncs
|
|
165
|
+
|
|
166
|
+
await adapter.connect();
|
|
167
|
+
|
|
168
|
+
// Wait for the sync loop to process
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
170
|
+
|
|
171
|
+
expect(receivedMessages).toHaveLength(1);
|
|
172
|
+
const msg = receivedMessages[0] as { content: string; senderId: string; channelId: string };
|
|
173
|
+
expect(msg.content).toBe('Hello bot!');
|
|
174
|
+
expect(msg.senderId).toBe('@alice:example.com');
|
|
175
|
+
expect(msg.channelId).toBe('!room:example.com');
|
|
176
|
+
|
|
177
|
+
await adapter.disconnect();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should ignore own messages', async () => {
|
|
181
|
+
const receivedMessages: unknown[] = [];
|
|
182
|
+
adapter.onMessage(async (msg) => {
|
|
183
|
+
receivedMessages.push(msg);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const syncResponse = {
|
|
187
|
+
next_batch: 'batch2',
|
|
188
|
+
rooms: {
|
|
189
|
+
join: {
|
|
190
|
+
'!room:example.com': {
|
|
191
|
+
timeline: {
|
|
192
|
+
events: [
|
|
193
|
+
{
|
|
194
|
+
event_id: '$msg1',
|
|
195
|
+
type: 'm.room.message',
|
|
196
|
+
sender: '@bot:example.com', // Own message
|
|
197
|
+
origin_server_ts: 1700000000000,
|
|
198
|
+
content: {
|
|
199
|
+
msgtype: 'm.text',
|
|
200
|
+
body: 'My own message',
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
vi.spyOn(globalThis, 'fetch')
|
|
211
|
+
.mockResolvedValueOnce({
|
|
212
|
+
ok: true,
|
|
213
|
+
json: async () => ({ user_id: '@bot:example.com' }),
|
|
214
|
+
} as Response)
|
|
215
|
+
.mockResolvedValueOnce({
|
|
216
|
+
ok: true,
|
|
217
|
+
json: async () => syncResponse,
|
|
218
|
+
} as Response)
|
|
219
|
+
.mockImplementation(() => new Promise(() => {}));
|
|
220
|
+
|
|
221
|
+
await adapter.connect();
|
|
222
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
223
|
+
|
|
224
|
+
expect(receivedMessages).toHaveLength(0);
|
|
225
|
+
|
|
226
|
+
await adapter.disconnect();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should auto-join rooms when configured', async () => {
|
|
230
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
|
231
|
+
|
|
232
|
+
const syncResponse = {
|
|
233
|
+
next_batch: 'batch2',
|
|
234
|
+
rooms: {
|
|
235
|
+
invite: {
|
|
236
|
+
'!newroom:example.com': {
|
|
237
|
+
invite_state: {
|
|
238
|
+
events: [],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
fetchSpy
|
|
246
|
+
.mockResolvedValueOnce({
|
|
247
|
+
ok: true,
|
|
248
|
+
json: async () => ({ user_id: '@bot:example.com' }),
|
|
249
|
+
} as Response)
|
|
250
|
+
.mockResolvedValueOnce({
|
|
251
|
+
ok: true,
|
|
252
|
+
json: async () => syncResponse,
|
|
253
|
+
} as Response)
|
|
254
|
+
.mockResolvedValueOnce({
|
|
255
|
+
ok: true,
|
|
256
|
+
json: async () => ({ room_id: '!newroom:example.com' }),
|
|
257
|
+
} as Response)
|
|
258
|
+
.mockImplementation(() => new Promise(() => {}));
|
|
259
|
+
|
|
260
|
+
await adapter.connect();
|
|
261
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
262
|
+
|
|
263
|
+
// Verify that a join request was made
|
|
264
|
+
const joinCall = fetchSpy.mock.calls.find(
|
|
265
|
+
(call) => typeof call[0] === 'string' && call[0].includes('/join')
|
|
266
|
+
);
|
|
267
|
+
expect(joinCall).toBeDefined();
|
|
268
|
+
|
|
269
|
+
await adapter.disconnect();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should send reply with relation', async () => {
|
|
273
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
|
274
|
+
|
|
275
|
+
fetchSpy
|
|
276
|
+
.mockResolvedValueOnce({
|
|
277
|
+
ok: true,
|
|
278
|
+
json: async () => ({ user_id: '@bot:example.com' }),
|
|
279
|
+
} as Response)
|
|
280
|
+
.mockResolvedValueOnce({
|
|
281
|
+
ok: true,
|
|
282
|
+
json: async () => ({ next_batch: 'batch1', rooms: {} }),
|
|
283
|
+
} as Response);
|
|
284
|
+
|
|
285
|
+
await adapter.connect();
|
|
286
|
+
|
|
287
|
+
fetchSpy.mockResolvedValueOnce({
|
|
288
|
+
ok: true,
|
|
289
|
+
json: async () => ({ event_id: '$reply123' }),
|
|
290
|
+
} as Response);
|
|
291
|
+
|
|
292
|
+
const result = await adapter.send('!room:example.com', {
|
|
293
|
+
content: 'Reply message',
|
|
294
|
+
replyToId: '$original123',
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
expect(result.success).toBe(true);
|
|
298
|
+
|
|
299
|
+
// Verify the body includes the reply relation
|
|
300
|
+
const sendCall = fetchSpy.mock.calls.find(
|
|
301
|
+
(call) =>
|
|
302
|
+
typeof call[0] === 'string' &&
|
|
303
|
+
call[0].includes('/send/m.room.message/')
|
|
304
|
+
);
|
|
305
|
+
expect(sendCall).toBeDefined();
|
|
306
|
+
const body = JSON.parse(sendCall![1]?.body as string);
|
|
307
|
+
expect(body['m.relates_to']['m.in_reply_to'].event_id).toBe('$original123');
|
|
308
|
+
|
|
309
|
+
await adapter.disconnect();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should register error handler', () => {
|
|
313
|
+
const handler = vi.fn();
|
|
314
|
+
adapter.onError(handler);
|
|
315
|
+
// No assertion needed - just verify no error during registration
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('sender filtering', () => {
|
|
319
|
+
it('should allow all messages when allowlists are not set', async () => {
|
|
320
|
+
const receivedMessages: unknown[] = [];
|
|
321
|
+
adapter.onMessage(async (msg) => {
|
|
322
|
+
receivedMessages.push(msg);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const syncResponse = {
|
|
326
|
+
next_batch: 'batch2',
|
|
327
|
+
rooms: {
|
|
328
|
+
join: {
|
|
329
|
+
'!room:example.com': {
|
|
330
|
+
timeline: {
|
|
331
|
+
events: [
|
|
332
|
+
{
|
|
333
|
+
event_id: '$msg1',
|
|
334
|
+
type: 'm.room.message',
|
|
335
|
+
sender: '@anyone:example.com',
|
|
336
|
+
origin_server_ts: 1700000000000,
|
|
337
|
+
content: { msgtype: 'm.text', body: 'Hello!' },
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
vi.spyOn(globalThis, 'fetch')
|
|
347
|
+
.mockResolvedValueOnce({ ok: true, json: async () => ({ user_id: '@bot:example.com' }) } as Response)
|
|
348
|
+
.mockResolvedValueOnce({ ok: true, json: async () => syncResponse } as Response)
|
|
349
|
+
.mockImplementation(() => new Promise(() => {}));
|
|
350
|
+
|
|
351
|
+
await adapter.connect();
|
|
352
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
353
|
+
expect(receivedMessages).toHaveLength(1);
|
|
354
|
+
await adapter.disconnect();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should allow messages from allowed users', async () => {
|
|
358
|
+
const { audit } = await import('@auxiora/audit');
|
|
359
|
+
const filteredAdapter = new MatrixAdapter({
|
|
360
|
+
homeserverUrl: 'https://matrix.example.com',
|
|
361
|
+
userId: '@bot:example.com',
|
|
362
|
+
accessToken: 'test-access-token',
|
|
363
|
+
allowedUsers: ['@alice:example.com'],
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const receivedMessages: unknown[] = [];
|
|
367
|
+
filteredAdapter.onMessage(async (msg) => {
|
|
368
|
+
receivedMessages.push(msg);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const syncResponse = {
|
|
372
|
+
next_batch: 'batch2',
|
|
373
|
+
rooms: {
|
|
374
|
+
join: {
|
|
375
|
+
'!room:example.com': {
|
|
376
|
+
timeline: {
|
|
377
|
+
events: [
|
|
378
|
+
{
|
|
379
|
+
event_id: '$msg1',
|
|
380
|
+
type: 'm.room.message',
|
|
381
|
+
sender: '@alice:example.com',
|
|
382
|
+
origin_server_ts: 1700000000000,
|
|
383
|
+
content: { msgtype: 'm.text', body: 'Allowed!' },
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
vi.spyOn(globalThis, 'fetch')
|
|
393
|
+
.mockResolvedValueOnce({ ok: true, json: async () => ({ user_id: '@bot:example.com' }) } as Response)
|
|
394
|
+
.mockResolvedValueOnce({ ok: true, json: async () => syncResponse } as Response)
|
|
395
|
+
.mockImplementation(() => new Promise(() => {}));
|
|
396
|
+
|
|
397
|
+
await filteredAdapter.connect();
|
|
398
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
399
|
+
expect(receivedMessages).toHaveLength(1);
|
|
400
|
+
expect((receivedMessages[0] as { content: string }).content).toBe('Allowed!');
|
|
401
|
+
expect(audit).not.toHaveBeenCalledWith('message.filtered', expect.anything());
|
|
402
|
+
await filteredAdapter.disconnect();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should block messages from non-allowed users', async () => {
|
|
406
|
+
const { audit } = await import('@auxiora/audit');
|
|
407
|
+
const filteredAdapter = new MatrixAdapter({
|
|
408
|
+
homeserverUrl: 'https://matrix.example.com',
|
|
409
|
+
userId: '@bot:example.com',
|
|
410
|
+
accessToken: 'test-access-token',
|
|
411
|
+
allowedUsers: ['@alice:example.com'],
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const receivedMessages: unknown[] = [];
|
|
415
|
+
filteredAdapter.onMessage(async (msg) => {
|
|
416
|
+
receivedMessages.push(msg);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const syncResponse = {
|
|
420
|
+
next_batch: 'batch2',
|
|
421
|
+
rooms: {
|
|
422
|
+
join: {
|
|
423
|
+
'!room:example.com': {
|
|
424
|
+
timeline: {
|
|
425
|
+
events: [
|
|
426
|
+
{
|
|
427
|
+
event_id: '$msg1',
|
|
428
|
+
type: 'm.room.message',
|
|
429
|
+
sender: '@eve:example.com',
|
|
430
|
+
origin_server_ts: 1700000000000,
|
|
431
|
+
content: { msgtype: 'm.text', body: 'Blocked!' },
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
vi.spyOn(globalThis, 'fetch')
|
|
441
|
+
.mockResolvedValueOnce({ ok: true, json: async () => ({ user_id: '@bot:example.com' }) } as Response)
|
|
442
|
+
.mockResolvedValueOnce({ ok: true, json: async () => syncResponse } as Response)
|
|
443
|
+
.mockImplementation(() => new Promise(() => {}));
|
|
444
|
+
|
|
445
|
+
await filteredAdapter.connect();
|
|
446
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
447
|
+
expect(receivedMessages).toHaveLength(0);
|
|
448
|
+
expect(audit).toHaveBeenCalledWith('message.filtered', expect.objectContaining({
|
|
449
|
+
channelType: 'matrix',
|
|
450
|
+
senderId: '@eve:example.com',
|
|
451
|
+
reason: 'user_not_allowed',
|
|
452
|
+
}));
|
|
453
|
+
await filteredAdapter.disconnect();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should block messages from non-allowed rooms', async () => {
|
|
457
|
+
const { audit } = await import('@auxiora/audit');
|
|
458
|
+
const filteredAdapter = new MatrixAdapter({
|
|
459
|
+
homeserverUrl: 'https://matrix.example.com',
|
|
460
|
+
userId: '@bot:example.com',
|
|
461
|
+
accessToken: 'test-access-token',
|
|
462
|
+
allowedRooms: ['!allowed:example.com'],
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const receivedMessages: unknown[] = [];
|
|
466
|
+
filteredAdapter.onMessage(async (msg) => {
|
|
467
|
+
receivedMessages.push(msg);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const syncResponse = {
|
|
471
|
+
next_batch: 'batch2',
|
|
472
|
+
rooms: {
|
|
473
|
+
join: {
|
|
474
|
+
'!blocked:example.com': {
|
|
475
|
+
timeline: {
|
|
476
|
+
events: [
|
|
477
|
+
{
|
|
478
|
+
event_id: '$msg1',
|
|
479
|
+
type: 'm.room.message',
|
|
480
|
+
sender: '@alice:example.com',
|
|
481
|
+
origin_server_ts: 1700000000000,
|
|
482
|
+
content: { msgtype: 'm.text', body: 'Wrong room!' },
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
vi.spyOn(globalThis, 'fetch')
|
|
492
|
+
.mockResolvedValueOnce({ ok: true, json: async () => ({ user_id: '@bot:example.com' }) } as Response)
|
|
493
|
+
.mockResolvedValueOnce({ ok: true, json: async () => syncResponse } as Response)
|
|
494
|
+
.mockImplementation(() => new Promise(() => {}));
|
|
495
|
+
|
|
496
|
+
await filteredAdapter.connect();
|
|
497
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
498
|
+
expect(receivedMessages).toHaveLength(0);
|
|
499
|
+
expect(audit).toHaveBeenCalledWith('message.filtered', expect.objectContaining({
|
|
500
|
+
channelType: 'matrix',
|
|
501
|
+
roomId: '!blocked:example.com',
|
|
502
|
+
reason: 'room_not_allowed',
|
|
503
|
+
}));
|
|
504
|
+
await filteredAdapter.disconnect();
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('should strip reply fallback from content', async () => {
|
|
509
|
+
const receivedMessages: unknown[] = [];
|
|
510
|
+
adapter.onMessage(async (msg) => {
|
|
511
|
+
receivedMessages.push(msg);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
const syncResponse = {
|
|
515
|
+
next_batch: 'batch2',
|
|
516
|
+
rooms: {
|
|
517
|
+
join: {
|
|
518
|
+
'!room:example.com': {
|
|
519
|
+
timeline: {
|
|
520
|
+
events: [
|
|
521
|
+
{
|
|
522
|
+
event_id: '$msg1',
|
|
523
|
+
type: 'm.room.message',
|
|
524
|
+
sender: '@alice:example.com',
|
|
525
|
+
origin_server_ts: 1700000000000,
|
|
526
|
+
content: {
|
|
527
|
+
msgtype: 'm.text',
|
|
528
|
+
body: '> <@bob:example.com> Original message\n\nActual reply',
|
|
529
|
+
'm.relates_to': {
|
|
530
|
+
'm.in_reply_to': {
|
|
531
|
+
event_id: '$original',
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
vi.spyOn(globalThis, 'fetch')
|
|
544
|
+
.mockResolvedValueOnce({
|
|
545
|
+
ok: true,
|
|
546
|
+
json: async () => ({ user_id: '@bot:example.com' }),
|
|
547
|
+
} as Response)
|
|
548
|
+
.mockResolvedValueOnce({
|
|
549
|
+
ok: true,
|
|
550
|
+
json: async () => syncResponse,
|
|
551
|
+
} as Response)
|
|
552
|
+
.mockImplementation(() => new Promise(() => {}));
|
|
553
|
+
|
|
554
|
+
await adapter.connect();
|
|
555
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
556
|
+
|
|
557
|
+
expect(receivedMessages).toHaveLength(1);
|
|
558
|
+
const msg = receivedMessages[0] as { content: string; replyToId: string };
|
|
559
|
+
expect(msg.content).toBe('Actual reply');
|
|
560
|
+
expect(msg.replyToId).toBe('$original');
|
|
561
|
+
|
|
562
|
+
await adapter.disconnect();
|
|
563
|
+
});
|
|
564
|
+
});
|