@openora/core 0.4.1-canary.111 → 0.4.1-canary.113
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/dist/.tsbuildinfo +1 -1
- package/dist/admin-console/plugin.d.ts +9 -0
- package/dist/admin-console/plugin.d.ts.map +1 -1
- package/dist/analytics/plugin.d.ts +9 -0
- package/dist/analytics/plugin.d.ts.map +1 -1
- package/dist/audit/plugin.d.ts +9 -0
- package/dist/audit/plugin.d.ts.map +1 -1
- package/dist/casino/gaming/plugin.d.ts +9 -0
- package/dist/casino/gaming/plugin.d.ts.map +1 -1
- package/dist/casino/lobby/plugin.d.ts +9 -0
- package/dist/casino/lobby/plugin.d.ts.map +1 -1
- package/dist/cms/plugin.d.ts +9 -0
- package/dist/cms/plugin.d.ts.map +1 -1
- package/dist/compliance/plugin.d.ts +9 -0
- package/dist/compliance/plugin.d.ts.map +1 -1
- package/dist/contracts/schemas/__tests__/platform-config.test.d.ts +2 -0
- package/dist/contracts/schemas/__tests__/platform-config.test.d.ts.map +1 -0
- package/dist/contracts/schemas/__tests__/platform-config.test.js +16 -0
- package/dist/contracts/schemas/__tests__/platform-config.test.js.map +1 -0
- package/dist/contracts/schemas/chat-attachment.d.ts +20 -0
- package/dist/contracts/schemas/chat-attachment.d.ts.map +1 -0
- package/dist/contracts/schemas/chat-attachment.js +14 -0
- package/dist/contracts/schemas/chat-attachment.js.map +1 -0
- package/dist/contracts/schemas/index.d.ts +1 -0
- package/dist/contracts/schemas/index.d.ts.map +1 -1
- package/dist/contracts/schemas/index.js +1 -0
- package/dist/contracts/schemas/index.js.map +1 -1
- package/dist/contracts/schemas/platform-config.d.ts +29 -0
- package/dist/contracts/schemas/platform-config.d.ts.map +1 -1
- package/dist/contracts/schemas/platform-config.js +45 -0
- package/dist/contracts/schemas/platform-config.js.map +1 -1
- package/dist/engagement/chat/__tests__/chat.service.int.test.js +255 -14
- package/dist/engagement/chat/__tests__/chat.service.int.test.js.map +1 -1
- package/dist/engagement/chat/__tests__/contract.test.d.ts +2 -0
- package/dist/engagement/chat/__tests__/contract.test.d.ts.map +1 -0
- package/dist/engagement/chat/__tests__/contract.test.js +44 -0
- package/dist/engagement/chat/__tests__/contract.test.js.map +1 -0
- package/dist/engagement/chat/__tests__/global-chat-room.int.test.js +10 -1
- package/dist/engagement/chat/__tests__/global-chat-room.int.test.js.map +1 -1
- package/dist/engagement/chat/__tests__/validate-attachment.test.d.ts +2 -0
- package/dist/engagement/chat/__tests__/validate-attachment.test.d.ts.map +1 -0
- package/dist/engagement/chat/__tests__/validate-attachment.test.js +38 -0
- package/dist/engagement/chat/__tests__/validate-attachment.test.js.map +1 -0
- package/dist/engagement/chat/contract/index.d.ts +185 -2
- package/dist/engagement/chat/contract/index.d.ts.map +1 -1
- package/dist/engagement/chat/contract/index.js +21 -3
- package/dist/engagement/chat/contract/index.js.map +1 -1
- package/dist/engagement/chat/drizzle/migrations/0008_robust_korvac.sql +1 -0
- package/dist/engagement/chat/drizzle/migrations/meta/0008_snapshot.json +1462 -0
- package/dist/engagement/chat/drizzle/migrations/meta/_journal.json +7 -0
- package/dist/engagement/chat/moderation/index.d.ts +1 -0
- package/dist/engagement/chat/moderation/index.d.ts.map +1 -1
- package/dist/engagement/chat/moderation/index.js +1 -0
- package/dist/engagement/chat/moderation/index.js.map +1 -1
- package/dist/engagement/chat/moderation/validate-attachment.d.ts +9 -0
- package/dist/engagement/chat/moderation/validate-attachment.d.ts.map +1 -0
- package/dist/engagement/chat/moderation/validate-attachment.js +27 -0
- package/dist/engagement/chat/moderation/validate-attachment.js.map +1 -0
- package/dist/engagement/chat/plugin.d.ts +9 -0
- package/dist/engagement/chat/plugin.d.ts.map +1 -1
- package/dist/engagement/chat/plugin.js +14 -2
- package/dist/engagement/chat/plugin.js.map +1 -1
- package/dist/engagement/chat/router/index.d.ts +118 -2
- package/dist/engagement/chat/router/index.d.ts.map +1 -1
- package/dist/engagement/chat/router/index.js +10 -4
- package/dist/engagement/chat/router/index.js.map +1 -1
- package/dist/engagement/chat/schema/index.d.ts +37 -0
- package/dist/engagement/chat/schema/index.d.ts.map +1 -1
- package/dist/engagement/chat/schema/index.js +1 -0
- package/dist/engagement/chat/schema/index.js.map +1 -1
- package/dist/engagement/chat/service/chat.service.d.ts +85 -6
- package/dist/engagement/chat/service/chat.service.d.ts.map +1 -1
- package/dist/engagement/chat/service/chat.service.js +83 -5
- package/dist/engagement/chat/service/chat.service.js.map +1 -1
- package/dist/engagement/chat-commands/plugin.d.ts +9 -0
- package/dist/engagement/chat-commands/plugin.d.ts.map +1 -1
- package/dist/engagement/notifications/__tests__/notification-event-map.test.d.ts +2 -0
- package/dist/engagement/notifications/__tests__/notification-event-map.test.d.ts.map +1 -0
- package/dist/engagement/notifications/__tests__/notification-event-map.test.js +235 -0
- package/dist/engagement/notifications/__tests__/notification-event-map.test.js.map +1 -0
- package/dist/engagement/notifications/__tests__/notifications.router.int.test.d.ts +2 -0
- package/dist/engagement/notifications/__tests__/notifications.router.int.test.d.ts.map +1 -0
- package/dist/engagement/notifications/__tests__/notifications.router.int.test.js +142 -0
- package/dist/engagement/notifications/__tests__/notifications.router.int.test.js.map +1 -0
- package/dist/engagement/notifications/__tests__/notifications.service.int.test.js +113 -7
- package/dist/engagement/notifications/__tests__/notifications.service.int.test.js.map +1 -1
- package/dist/engagement/notifications/contract/index.d.ts +98 -17
- package/dist/engagement/notifications/contract/index.d.ts.map +1 -1
- package/dist/engagement/notifications/contract/index.js +34 -4
- package/dist/engagement/notifications/contract/index.js.map +1 -1
- package/dist/engagement/notifications/drizzle/migrations/0001_classy_pixie.sql +1 -0
- package/dist/engagement/notifications/drizzle/migrations/0002_open_madame_web.sql +2 -0
- package/dist/engagement/notifications/drizzle/migrations/meta/0001_snapshot.json +98 -0
- package/dist/engagement/notifications/drizzle/migrations/meta/0002_snapshot.json +119 -0
- package/dist/engagement/notifications/drizzle/migrations/meta/_journal.json +14 -0
- package/dist/engagement/notifications/plugin.d.ts +23 -3
- package/dist/engagement/notifications/plugin.d.ts.map +1 -1
- package/dist/engagement/notifications/plugin.js +219 -75
- package/dist/engagement/notifications/plugin.js.map +1 -1
- package/dist/engagement/notifications/react/notifications.d.ts +20 -0
- package/dist/engagement/notifications/react/notifications.d.ts.map +1 -0
- package/dist/engagement/notifications/react/notifications.js +39 -0
- package/dist/engagement/notifications/react/notifications.js.map +1 -0
- package/dist/engagement/notifications/router/index.d.ts +70 -17
- package/dist/engagement/notifications/router/index.d.ts.map +1 -1
- package/dist/engagement/notifications/router/index.js +37 -16
- package/dist/engagement/notifications/router/index.js.map +1 -1
- package/dist/engagement/notifications/schema/index.d.ts +36 -0
- package/dist/engagement/notifications/schema/index.d.ts.map +1 -1
- package/dist/engagement/notifications/schema/index.js +7 -2
- package/dist/engagement/notifications/schema/index.js.map +1 -1
- package/dist/engagement/notifications/service/notifications.service.d.ts +27 -19
- package/dist/engagement/notifications/service/notifications.service.d.ts.map +1 -1
- package/dist/engagement/notifications/service/notifications.service.js +41 -11
- package/dist/engagement/notifications/service/notifications.service.js.map +1 -1
- package/dist/engagement/react.d.ts +2 -0
- package/dist/engagement/react.d.ts.map +1 -0
- package/dist/engagement/react.js +3 -0
- package/dist/engagement/react.js.map +1 -0
- package/dist/engagement/social/plugin.d.ts +9 -0
- package/dist/engagement/social/plugin.d.ts.map +1 -1
- package/dist/iam/plugin.d.ts +9 -0
- package/dist/iam/plugin.d.ts.map +1 -1
- package/dist/pam/identity/plugin.d.ts +9 -0
- package/dist/pam/identity/plugin.d.ts.map +1 -1
- package/dist/pam/player-management/plugin.d.ts +9 -0
- package/dist/pam/player-management/plugin.d.ts.map +1 -1
- package/dist/pam/player-note/plugin.d.ts +9 -0
- package/dist/pam/player-note/plugin.d.ts.map +1 -1
- package/dist/pam/profile/plugin.d.ts +9 -0
- package/dist/pam/profile/plugin.d.ts.map +1 -1
- package/dist/pam/tag/plugin.d.ts +9 -0
- package/dist/pam/tag/plugin.d.ts.map +1 -1
- package/dist/server/runtime/core-token-catalog.d.ts +9 -0
- package/dist/server/runtime/core-token-catalog.d.ts.map +1 -1
- package/dist/wallet/plugin.d.ts +9 -0
- package/dist/wallet/plugin.d.ts.map +1 -1
- package/package.json +6 -1
|
@@ -18,7 +18,7 @@ import { migrate } from '../migrate.js';
|
|
|
18
18
|
// NO_CLIENT_META,
|
|
19
19
|
// } from '../../../testing/mock.js';
|
|
20
20
|
import { chatChannel } from '@openora/core/contracts';
|
|
21
|
-
import { ChatService, ChatMessageBlockedError, ChatSelfBlockError, ChatSelfIgnoreError, ChatRoomSlugConflictError, ChatRoomJoinCodeNotFoundError, ChatRoomBannedError, ChatRoomLimitReachedError, ChatRoomLastModeratorError, ChatRoomNotMemberError, ChatRoomNotModeratorError, ChatRoomSelfModerationError, ChatRoomOwnershipError, } from '../service/chat.service.js';
|
|
21
|
+
import { ChatService, ChatMessageBlockedError, ChatAttachmentRejectedError, ChatSelfBlockError, ChatSelfIgnoreError, ChatRoomSlugConflictError, ChatRoomJoinCodeNotFoundError, ChatRoomBannedError, ChatRoomLimitReachedError, ChatRoomLastModeratorError, ChatRoomNotMemberError, ChatRoomNotModeratorError, ChatRoomSelfModerationError, ChatRoomOwnershipError, } from '../service/chat.service.js';
|
|
22
22
|
import { ChatModerationService, ChatRoomNotFoundError, ChatMessageNotFoundError, ChatPlayerMutedError, ChatPlayerBannedError, ChatAdminPrivateRoomModerationError, } from '../service/chat-moderation.service.js';
|
|
23
23
|
import { chatMessage, chatRoom, chatRoomMember, chatRoomBan, chatRoomRule, chatRoomConfiguration, chatRoomMute, chatRoomRemove, chatUserBlock, chatUserIgnore, chatMute, chatPlatformBan, } from '../schema/index.js';
|
|
24
24
|
import { ChatRoomMembershipService } from '../service/chat-room-membership.service.js';
|
|
@@ -35,7 +35,7 @@ function makeService(directory = mock({
|
|
|
35
35
|
isActive: true,
|
|
36
36
|
role: 'player',
|
|
37
37
|
})),
|
|
38
|
-
}), socialCommands) {
|
|
38
|
+
}), socialCommands, allowedAttachmentHosts = []) {
|
|
39
39
|
const transport = new InProcessRealtimeTransport();
|
|
40
40
|
const events = makeEventBus();
|
|
41
41
|
const audit = mock({
|
|
@@ -44,7 +44,17 @@ function makeService(directory = mock({
|
|
|
44
44
|
});
|
|
45
45
|
const moderation = new ChatModerationService(db.drizzle, transport, audit);
|
|
46
46
|
const identityReader = makeIdentityReader();
|
|
47
|
-
const chatService = new ChatService(
|
|
47
|
+
const chatService = new ChatService({
|
|
48
|
+
drizzle: db.drizzle,
|
|
49
|
+
events,
|
|
50
|
+
transport,
|
|
51
|
+
directory,
|
|
52
|
+
audit,
|
|
53
|
+
moderation,
|
|
54
|
+
identityReader,
|
|
55
|
+
allowedAttachmentHosts,
|
|
56
|
+
socialCommands,
|
|
57
|
+
});
|
|
48
58
|
const membership = new ChatRoomMembershipService(db.drizzle, events, audit, transport, identityReader);
|
|
49
59
|
const roomBan = new ChatRoomBanService(db.drizzle, events, audit, transport, identityReader);
|
|
50
60
|
const roomMute = new ChatRoomMuteService(db.drizzle, audit);
|
|
@@ -128,6 +138,7 @@ describe('ChatService realtime wiring', () => {
|
|
|
128
138
|
content: 'hi',
|
|
129
139
|
type: 'user',
|
|
130
140
|
metadata: null,
|
|
141
|
+
attachment: null,
|
|
131
142
|
isDeleted: false,
|
|
132
143
|
createdAt: '2026-05-29T00:00:00.000Z',
|
|
133
144
|
};
|
|
@@ -188,6 +199,7 @@ describe('ChatService.subscribeMessages per-viewer block filtering (real PG)', (
|
|
|
188
199
|
content: 'hi',
|
|
189
200
|
type: 'user',
|
|
190
201
|
metadata: null,
|
|
202
|
+
attachment: null,
|
|
191
203
|
isDeleted: false,
|
|
192
204
|
createdAt: '2026-05-29T00:00:00.000Z',
|
|
193
205
|
};
|
|
@@ -240,7 +252,11 @@ describe('ChatService.sendGlobalMessage (real PG)', () => {
|
|
|
240
252
|
it('stores the username from the verified user, ignoring the header fallback', async () => {
|
|
241
253
|
const { svc, events } = makeService();
|
|
242
254
|
const account = await seedUser(db, { name: 'Platform Admin', username: 'platform_admin' });
|
|
243
|
-
const msg = await svc.sendGlobalMessage(
|
|
255
|
+
const msg = await svc.sendGlobalMessage({
|
|
256
|
+
userId: account.id,
|
|
257
|
+
username: 'spoofed-header-name',
|
|
258
|
+
content: 'hi',
|
|
259
|
+
});
|
|
244
260
|
expect(msg.username).toBe('platform_admin');
|
|
245
261
|
const [stored] = await db.drizzle.db.select().from(chatMessage);
|
|
246
262
|
expect(stored).toMatchObject({ roomId: null, username: 'platform_admin' });
|
|
@@ -253,20 +269,24 @@ describe('ChatService.sendGlobalMessage (real PG)', () => {
|
|
|
253
269
|
it('falls back to the passed name, then anonymous, when no user row exists', async () => {
|
|
254
270
|
const { svc } = makeService();
|
|
255
271
|
const userId = randomUUID();
|
|
256
|
-
expect((await svc.sendGlobalMessage(userId, 'fallback', 'hi')).username).toBe('fallback');
|
|
257
|
-
expect((await svc.sendGlobalMessage(userId, '', 'hi')).username).toBe('anonymous');
|
|
272
|
+
expect((await svc.sendGlobalMessage({ userId, username: 'fallback', content: 'hi' })).username).toBe('fallback');
|
|
273
|
+
expect((await svc.sendGlobalMessage({ userId, username: '', content: 'hi' })).username).toBe('anonymous');
|
|
258
274
|
});
|
|
259
275
|
it('blocks profane messages before they are stored or published', async () => {
|
|
260
276
|
const { svc, transport } = makeService();
|
|
261
277
|
const delivered = [];
|
|
262
278
|
transport.subscribe('chat:global', (m) => delivered.push(m));
|
|
263
|
-
await expect(svc.sendGlobalMessage(randomUUID(), 'Alice', 'this is shit')).rejects.toThrow(ChatMessageBlockedError);
|
|
279
|
+
await expect(svc.sendGlobalMessage({ userId: randomUUID(), username: 'Alice', content: 'this is shit' })).rejects.toThrow(ChatMessageBlockedError);
|
|
264
280
|
expect(delivered).toHaveLength(0);
|
|
265
281
|
expect(await db.drizzle.db.select().from(chatMessage)).toHaveLength(0);
|
|
266
282
|
});
|
|
267
283
|
it('persists URL-sanitized content', async () => {
|
|
268
284
|
const { svc } = makeService();
|
|
269
|
-
const msg = await svc.sendGlobalMessage(
|
|
285
|
+
const msg = await svc.sendGlobalMessage({
|
|
286
|
+
userId: randomUUID(),
|
|
287
|
+
username: 'Alice',
|
|
288
|
+
content: 'click javascript:alert(1)',
|
|
289
|
+
});
|
|
270
290
|
expect(msg.content).toBe('click javascript alert(1)');
|
|
271
291
|
const [stored] = await db.drizzle.db.select().from(chatMessage);
|
|
272
292
|
expect(stored?.content).toBe('click javascript alert(1)');
|
|
@@ -275,9 +295,60 @@ describe('ChatService.sendGlobalMessage (real PG)', () => {
|
|
|
275
295
|
const { svc, transport } = makeService();
|
|
276
296
|
const delivered = [];
|
|
277
297
|
transport.subscribe('chat:global', (m) => delivered.push(m));
|
|
278
|
-
const msg = await svc.sendGlobalMessage(
|
|
298
|
+
const msg = await svc.sendGlobalMessage({
|
|
299
|
+
userId: randomUUID(),
|
|
300
|
+
username: 'Alice',
|
|
301
|
+
content: 'hello',
|
|
302
|
+
});
|
|
279
303
|
expect(delivered.map((m) => m.id)).toEqual([msg.id]);
|
|
280
304
|
});
|
|
305
|
+
it('persists an attachment whose host is on the allow-list and publishes it', async () => {
|
|
306
|
+
const { svc, transport } = makeService(undefined, undefined, ['media.example.com']);
|
|
307
|
+
const delivered = [];
|
|
308
|
+
transport.subscribe('chat:global', (m) => delivered.push(m));
|
|
309
|
+
const attachment = {
|
|
310
|
+
kind: 'gif',
|
|
311
|
+
provider: 'example',
|
|
312
|
+
externalId: 'abc123',
|
|
313
|
+
url: 'https://media.example.com/abc123.gif',
|
|
314
|
+
previewUrl: 'https://media.example.com/abc123-preview.gif',
|
|
315
|
+
width: 320,
|
|
316
|
+
height: 240,
|
|
317
|
+
title: 'a gif',
|
|
318
|
+
};
|
|
319
|
+
const msg = await svc.sendGlobalMessage({
|
|
320
|
+
userId: randomUUID(),
|
|
321
|
+
username: 'Alice',
|
|
322
|
+
content: '',
|
|
323
|
+
attachment,
|
|
324
|
+
});
|
|
325
|
+
expect(msg.type).toBe('user');
|
|
326
|
+
if (msg.type === 'user') {
|
|
327
|
+
expect(msg.attachment).toEqual(attachment);
|
|
328
|
+
}
|
|
329
|
+
const [stored] = await db.drizzle.db.select().from(chatMessage);
|
|
330
|
+
expect(stored?.attachment).toEqual(attachment);
|
|
331
|
+
expect(delivered.map((m) => m.id)).toEqual([msg.id]);
|
|
332
|
+
});
|
|
333
|
+
it('rejects an attachment whose host is not on the allow-list', async () => {
|
|
334
|
+
const { svc } = makeService(undefined, undefined, ['media.example.com']);
|
|
335
|
+
await expect(svc.sendGlobalMessage({
|
|
336
|
+
userId: randomUUID(),
|
|
337
|
+
username: 'Alice',
|
|
338
|
+
content: '',
|
|
339
|
+
attachment: {
|
|
340
|
+
kind: 'gif',
|
|
341
|
+
provider: 'example',
|
|
342
|
+
externalId: 'abc123',
|
|
343
|
+
url: 'https://evil.example.net/abc123.gif',
|
|
344
|
+
previewUrl: 'https://evil.example.net/abc123-preview.gif',
|
|
345
|
+
width: 320,
|
|
346
|
+
height: 240,
|
|
347
|
+
title: 'a gif',
|
|
348
|
+
},
|
|
349
|
+
})).rejects.toThrow(ChatAttachmentRejectedError);
|
|
350
|
+
expect(await db.drizzle.db.select().from(chatMessage)).toHaveLength(0);
|
|
351
|
+
});
|
|
281
352
|
});
|
|
282
353
|
describe('ChatService message reads (real PG)', () => {
|
|
283
354
|
it('returns global messages newest-first, excluding soft-deleted ones', async () => {
|
|
@@ -362,6 +433,176 @@ describe('ChatService.sendRoomMessage (real PG)', () => {
|
|
|
362
433
|
})).rejects.toBeInstanceOf(ChatRoomNotMemberError);
|
|
363
434
|
expect(await db.drizzle.db.select().from(chatMessage)).toHaveLength(0);
|
|
364
435
|
});
|
|
436
|
+
it('persists an attachment whose host is on the allow-list and publishes it', async () => {
|
|
437
|
+
const { svc, transport } = makeService(undefined, undefined, ['media.example.com']);
|
|
438
|
+
const room = await seedRoom();
|
|
439
|
+
const account = await seedUser(db, { name: 'Alice', username: 'alice' });
|
|
440
|
+
const delivered = [];
|
|
441
|
+
transport.subscribe(chatChannel(room.id), (m) => delivered.push(m));
|
|
442
|
+
const attachment = {
|
|
443
|
+
kind: 'gif',
|
|
444
|
+
provider: 'example',
|
|
445
|
+
externalId: 'abc123',
|
|
446
|
+
url: 'https://media.example.com/abc123.gif',
|
|
447
|
+
previewUrl: 'https://media.example.com/abc123-preview.gif',
|
|
448
|
+
width: 320,
|
|
449
|
+
height: 240,
|
|
450
|
+
title: 'a gif',
|
|
451
|
+
};
|
|
452
|
+
const msg = await svc.sendRoomMessage({
|
|
453
|
+
userId: account.id,
|
|
454
|
+
username: 'ignored',
|
|
455
|
+
roomId: room.id,
|
|
456
|
+
content: '',
|
|
457
|
+
attachment,
|
|
458
|
+
});
|
|
459
|
+
expect(msg.type).toBe('user');
|
|
460
|
+
if (msg.type === 'user') {
|
|
461
|
+
expect(msg.attachment).toEqual(attachment);
|
|
462
|
+
}
|
|
463
|
+
const [stored] = await db.drizzle.db.select().from(chatMessage);
|
|
464
|
+
expect(stored?.attachment).toEqual(attachment);
|
|
465
|
+
expect(delivered.map((m) => m.id)).toEqual([msg.id]);
|
|
466
|
+
});
|
|
467
|
+
it('rejects an attachment whose host is not on the allow-list', async () => {
|
|
468
|
+
const { svc } = makeService(undefined, undefined, ['media.example.com']);
|
|
469
|
+
const room = await seedRoom();
|
|
470
|
+
const account = await seedUser(db, { name: 'Alice', username: 'alice' });
|
|
471
|
+
await expect(svc.sendRoomMessage({
|
|
472
|
+
userId: account.id,
|
|
473
|
+
username: 'ignored',
|
|
474
|
+
roomId: room.id,
|
|
475
|
+
content: '',
|
|
476
|
+
attachment: {
|
|
477
|
+
kind: 'gif',
|
|
478
|
+
provider: 'example',
|
|
479
|
+
externalId: 'abc123',
|
|
480
|
+
url: 'https://evil.example.net/abc123.gif',
|
|
481
|
+
previewUrl: 'https://evil.example.net/abc123-preview.gif',
|
|
482
|
+
width: 320,
|
|
483
|
+
height: 240,
|
|
484
|
+
title: 'a gif',
|
|
485
|
+
},
|
|
486
|
+
})).rejects.toThrow(ChatAttachmentRejectedError);
|
|
487
|
+
expect(await db.drizzle.db.select().from(chatMessage)).toHaveLength(0);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
describe('ChatService @mention detection (real PG)', () => {
|
|
491
|
+
function mentionDirectory(summaries) {
|
|
492
|
+
return mock({
|
|
493
|
+
lookupPlayers: async () => [],
|
|
494
|
+
getPlayerByUsername: vi
|
|
495
|
+
.fn()
|
|
496
|
+
.mockImplementation((username) => Promise.resolve(summaries.find((s) => s.username.toLowerCase() === username.toLowerCase()) ?? null)),
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
function summaryFor(userId, username) {
|
|
500
|
+
return {
|
|
501
|
+
playerId: randomUUID(),
|
|
502
|
+
userId,
|
|
503
|
+
username,
|
|
504
|
+
email: `${username}@example.com`,
|
|
505
|
+
kycStatus: null,
|
|
506
|
+
language: null,
|
|
507
|
+
avatarUrl: null,
|
|
508
|
+
createdAt: new Date(),
|
|
509
|
+
level: 1,
|
|
510
|
+
currency: 'USD',
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
it('resolves an @username in a room message and emits chat.user.mentioned', async () => {
|
|
514
|
+
const bobId = randomUUID();
|
|
515
|
+
const { svc, events } = makeService(mentionDirectory([summaryFor(bobId, 'bob')]));
|
|
516
|
+
const room = await seedRoom();
|
|
517
|
+
const msg = await svc.sendRoomMessage({
|
|
518
|
+
userId: randomUUID(),
|
|
519
|
+
username: 'alice',
|
|
520
|
+
roomId: room.id,
|
|
521
|
+
content: 'hello @bob, welcome',
|
|
522
|
+
});
|
|
523
|
+
await waitFor(() => vi.mocked(events.emit).mock.calls.some(([topic]) => topic === 'chat.user.mentioned'));
|
|
524
|
+
expect(events.emit).toHaveBeenCalledWith('chat.user.mentioned', {
|
|
525
|
+
mentionedUserId: bobId,
|
|
526
|
+
byUserId: msg.userId,
|
|
527
|
+
roomId: room.id,
|
|
528
|
+
messageId: msg.id,
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
it('resolves an @username in a global message with a null roomId', async () => {
|
|
532
|
+
const bobId = randomUUID();
|
|
533
|
+
const { svc, events } = makeService(mentionDirectory([summaryFor(bobId, 'bob')]));
|
|
534
|
+
const authorId = randomUUID();
|
|
535
|
+
const msg = await svc.sendGlobalMessage({
|
|
536
|
+
userId: authorId,
|
|
537
|
+
username: 'alice',
|
|
538
|
+
content: 'hey @bob',
|
|
539
|
+
});
|
|
540
|
+
await waitFor(() => vi.mocked(events.emit).mock.calls.some(([topic]) => topic === 'chat.user.mentioned'));
|
|
541
|
+
expect(events.emit).toHaveBeenCalledWith('chat.user.mentioned', {
|
|
542
|
+
mentionedUserId: bobId,
|
|
543
|
+
byUserId: authorId,
|
|
544
|
+
roomId: null,
|
|
545
|
+
messageId: msg.id,
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
it('excludes a self-mention', async () => {
|
|
549
|
+
const authorId = randomUUID();
|
|
550
|
+
const { svc, events } = makeService(mentionDirectory([summaryFor(authorId, 'alice')]));
|
|
551
|
+
await svc.sendGlobalMessage({
|
|
552
|
+
userId: authorId,
|
|
553
|
+
username: 'alice',
|
|
554
|
+
content: 'talking to myself @alice',
|
|
555
|
+
});
|
|
556
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
557
|
+
expect(events.emit).not.toHaveBeenCalledWith('chat.user.mentioned', expect.anything());
|
|
558
|
+
});
|
|
559
|
+
it('emits once per uniquely mentioned user even when repeated in the message', async () => {
|
|
560
|
+
const bobId = randomUUID();
|
|
561
|
+
const { svc, events } = makeService(mentionDirectory([summaryFor(bobId, 'bob')]));
|
|
562
|
+
await svc.sendGlobalMessage({
|
|
563
|
+
userId: randomUUID(),
|
|
564
|
+
username: 'alice',
|
|
565
|
+
content: '@bob @bob are you there @Bob?',
|
|
566
|
+
});
|
|
567
|
+
await waitFor(() => vi.mocked(events.emit).mock.calls.some(([topic]) => topic === 'chat.user.mentioned'));
|
|
568
|
+
const mentionCalls = vi
|
|
569
|
+
.mocked(events.emit)
|
|
570
|
+
.mock.calls.filter(([topic]) => topic === 'chat.user.mentioned');
|
|
571
|
+
expect(mentionCalls).toHaveLength(1);
|
|
572
|
+
});
|
|
573
|
+
it('silently drops an @token that does not resolve to a known username', async () => {
|
|
574
|
+
const { svc, events } = makeService(mentionDirectory([]));
|
|
575
|
+
await expect(svc.sendGlobalMessage({ userId: randomUUID(), username: 'alice', content: 'hello @nobody' })).resolves.toBeDefined();
|
|
576
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
577
|
+
expect(events.emit).not.toHaveBeenCalledWith('chat.user.mentioned', expect.anything());
|
|
578
|
+
});
|
|
579
|
+
it('does not treat the domain part of an email address as a mention', async () => {
|
|
580
|
+
const acmeId = randomUUID();
|
|
581
|
+
const { svc, events } = makeService(mentionDirectory([summaryFor(acmeId, 'acme')]));
|
|
582
|
+
await expect(svc.sendGlobalMessage({
|
|
583
|
+
userId: randomUUID(),
|
|
584
|
+
username: 'alice',
|
|
585
|
+
content: 'contact me at info@acme.com',
|
|
586
|
+
})).resolves.toBeDefined();
|
|
587
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
588
|
+
expect(events.emit).not.toHaveBeenCalledWith('chat.user.mentioned', expect.anything());
|
|
589
|
+
});
|
|
590
|
+
it('does not emit chat.user.mentioned when the mentioned user has blocked the sender', async () => {
|
|
591
|
+
const bobId = randomUUID();
|
|
592
|
+
const authorId = randomUUID();
|
|
593
|
+
const { svc, events } = makeService(mentionDirectory([summaryFor(bobId, 'bob')]));
|
|
594
|
+
await svc.blockUser(bobId, authorId);
|
|
595
|
+
await svc.sendGlobalMessage({ userId: authorId, username: 'alice', content: 'hello @bob' });
|
|
596
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
597
|
+
expect(events.emit).not.toHaveBeenCalledWith('chat.user.mentioned', expect.anything());
|
|
598
|
+
});
|
|
599
|
+
it('fails closed (does not emit) when the block check throws', async () => {
|
|
600
|
+
const authorId = randomUUID();
|
|
601
|
+
const { svc, events } = makeService(mentionDirectory([summaryFor('not-a-valid-uuid', 'bob')]));
|
|
602
|
+
await expect(svc.sendGlobalMessage({ userId: authorId, username: 'alice', content: 'hello @bob' })).resolves.toBeDefined();
|
|
603
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
604
|
+
expect(events.emit).not.toHaveBeenCalledWith('chat.user.mentioned', expect.anything());
|
|
605
|
+
});
|
|
365
606
|
});
|
|
366
607
|
describe('ChatService.deleteMessage (real PG)', () => {
|
|
367
608
|
it('soft-deletes a message for a room owner and publishes a tombstone', async () => {
|
|
@@ -1200,7 +1441,7 @@ describe('ChatService moderation (real PG)', () => {
|
|
|
1200
1441
|
roomId: globalRoom.id,
|
|
1201
1442
|
readOnlyMode: true,
|
|
1202
1443
|
});
|
|
1203
|
-
await expect(svc.sendGlobalMessage(randomUUID(), 'ReadOnly', 'hello')).rejects.toBeInstanceOf(ChatPlayerMutedError);
|
|
1444
|
+
await expect(svc.sendGlobalMessage({ userId: randomUUID(), username: 'ReadOnly', content: 'hello' })).rejects.toBeInstanceOf(ChatPlayerMutedError);
|
|
1204
1445
|
});
|
|
1205
1446
|
it.each(['__all_public', '__all'])('enforces %s mutes on global sends', async (scope) => {
|
|
1206
1447
|
const { svc, moderation } = makeService();
|
|
@@ -1213,7 +1454,7 @@ describe('ChatService moderation (real PG)', () => {
|
|
|
1213
1454
|
...NO_CLIENT_META,
|
|
1214
1455
|
});
|
|
1215
1456
|
const [mute] = await db.drizzle.db.select().from(chatMute).limit(1);
|
|
1216
|
-
await expect(svc.sendGlobalMessage(mute.userId, 'Muted', 'hello')).rejects.toBeInstanceOf(ChatPlayerMutedError);
|
|
1457
|
+
await expect(svc.sendGlobalMessage({ userId: mute.userId, username: 'Muted', content: 'hello' })).rejects.toBeInstanceOf(ChatPlayerMutedError);
|
|
1217
1458
|
});
|
|
1218
1459
|
it('supports a global mute when the virtual global room has no row', async () => {
|
|
1219
1460
|
const { svc, moderation } = makeService();
|
|
@@ -1226,7 +1467,7 @@ describe('ChatService moderation (real PG)', () => {
|
|
|
1226
1467
|
actorId: randomUUID(),
|
|
1227
1468
|
...NO_CLIENT_META,
|
|
1228
1469
|
})).resolves.toEqual({ success: true });
|
|
1229
|
-
await expect(svc.sendGlobalMessage(userId, 'Muted', 'hello')).rejects.toBeInstanceOf(ChatPlayerMutedError);
|
|
1470
|
+
await expect(svc.sendGlobalMessage({ userId, username: 'Muted', content: 'hello' })).rejects.toBeInstanceOf(ChatPlayerMutedError);
|
|
1230
1471
|
});
|
|
1231
1472
|
it('enforces an all-chat mute in private rooms', async () => {
|
|
1232
1473
|
const { svc, moderation } = makeService();
|
|
@@ -1278,7 +1519,7 @@ describe('ChatService moderation (real PG)', () => {
|
|
|
1278
1519
|
actorId: randomUUID(),
|
|
1279
1520
|
...NO_CLIENT_META,
|
|
1280
1521
|
});
|
|
1281
|
-
await expect(svc.sendGlobalMessage(userId, 'Banned', 'hello')).rejects.toBeInstanceOf(ChatPlayerBannedError);
|
|
1522
|
+
await expect(svc.sendGlobalMessage({ userId, username: 'Banned', content: 'hello' })).rejects.toBeInstanceOf(ChatPlayerBannedError);
|
|
1282
1523
|
await expect(svc.sendRoomMessage({ userId, username: 'Banned', roomId: room.id, content: 'hello' })).resolves.toMatchObject({ userId });
|
|
1283
1524
|
await moderation.unban({
|
|
1284
1525
|
userId,
|
|
@@ -1286,7 +1527,7 @@ describe('ChatService moderation (real PG)', () => {
|
|
|
1286
1527
|
actorId: randomUUID(),
|
|
1287
1528
|
...NO_CLIENT_META,
|
|
1288
1529
|
});
|
|
1289
|
-
await expect(svc.sendGlobalMessage(userId, 'Banned', 'hello')).resolves.toMatchObject({
|
|
1530
|
+
await expect(svc.sendGlobalMessage({ userId, username: 'Banned', content: 'hello' })).resolves.toMatchObject({
|
|
1290
1531
|
userId,
|
|
1291
1532
|
});
|
|
1292
1533
|
});
|