@openora/core 0.4.1-canary.71 → 0.4.1-canary.73
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/__tests__/backoffice.service.int.test.js +30 -12
- package/dist/admin-console/__tests__/backoffice.service.int.test.js.map +1 -1
- package/dist/admin-console/contract/index.d.ts +4 -0
- package/dist/admin-console/contract/index.d.ts.map +1 -1
- package/dist/admin-console/contract/index.js +3 -2
- package/dist/admin-console/contract/index.js.map +1 -1
- package/dist/admin-console/plugin.d.ts +1 -0
- package/dist/admin-console/plugin.d.ts.map +1 -1
- package/dist/admin-console/router/index.d.ts +2 -0
- package/dist/admin-console/router/index.d.ts.map +1 -1
- package/dist/admin-console/service/backoffice.service.d.ts +2 -0
- package/dist/admin-console/service/backoffice.service.d.ts.map +1 -1
- package/dist/admin-console/service/backoffice.service.js +2 -0
- package/dist/admin-console/service/backoffice.service.js.map +1 -1
- package/dist/analytics/plugin.d.ts +1 -0
- package/dist/analytics/plugin.d.ts.map +1 -1
- package/dist/audit/plugin.d.ts +1 -0
- package/dist/audit/plugin.d.ts.map +1 -1
- package/dist/audit/plugin.js +6 -0
- package/dist/audit/plugin.js.map +1 -1
- package/dist/casino/gaming/plugin.d.ts +1 -0
- package/dist/casino/gaming/plugin.d.ts.map +1 -1
- package/dist/casino/lobby/plugin.d.ts +1 -0
- package/dist/casino/lobby/plugin.d.ts.map +1 -1
- package/dist/cms/plugin.d.ts +1 -0
- package/dist/cms/plugin.d.ts.map +1 -1
- package/dist/compliance/plugin.d.ts +1 -0
- package/dist/compliance/plugin.d.ts.map +1 -1
- package/dist/contracts/adapters/admin-user-directory.d.ts +4 -2
- package/dist/contracts/adapters/admin-user-directory.d.ts.map +1 -1
- package/dist/contracts/adapters/admin-user-directory.js.map +1 -1
- package/dist/contracts/adapters/chat-moderation.d.ts +75 -0
- package/dist/contracts/adapters/chat-moderation.d.ts.map +1 -0
- package/dist/contracts/adapters/chat-moderation.js +3 -0
- package/dist/contracts/adapters/chat-moderation.js.map +1 -0
- package/dist/contracts/adapters/index.d.ts +2 -0
- package/dist/contracts/adapters/index.d.ts.map +1 -1
- package/dist/contracts/adapters/index.js +1 -0
- package/dist/contracts/adapters/index.js.map +1 -1
- package/dist/contracts/adapters/realtime.d.ts +9 -1
- package/dist/contracts/adapters/realtime.d.ts.map +1 -1
- package/dist/contracts/adapters/realtime.js.map +1 -1
- package/dist/contracts/schemas/events.d.ts +19 -4
- package/dist/contracts/schemas/events.d.ts.map +1 -1
- package/dist/contracts/schemas/events.js +12 -5
- package/dist/contracts/schemas/events.js.map +1 -1
- package/dist/engagement/chat/__tests__/chat.service.int.test.js +315 -19
- package/dist/engagement/chat/__tests__/chat.service.int.test.js.map +1 -1
- package/dist/engagement/chat/contract/constants.d.ts +1 -1
- package/dist/engagement/chat/contract/constants.d.ts.map +1 -1
- package/dist/engagement/chat/contract/constants.js +1 -1
- package/dist/engagement/chat/contract/constants.js.map +1 -1
- package/dist/engagement/chat/contract/index.d.ts +764 -18
- package/dist/engagement/chat/contract/index.d.ts.map +1 -1
- package/dist/engagement/chat/contract/index.js +236 -5
- package/dist/engagement/chat/contract/index.js.map +1 -1
- package/dist/engagement/chat/drizzle/migrations/0005_tranquil_gwen_stacy.sql +100 -0
- package/dist/engagement/chat/drizzle/migrations/0006_tiny_ultron.sql +3 -0
- package/dist/engagement/chat/drizzle/migrations/meta/0005_snapshot.json +1422 -0
- package/dist/engagement/chat/drizzle/migrations/meta/0006_snapshot.json +1456 -0
- package/dist/engagement/chat/drizzle/migrations/meta/_journal.json +14 -0
- package/dist/engagement/chat/plugin.d.ts +2 -1
- package/dist/engagement/chat/plugin.d.ts.map +1 -1
- package/dist/engagement/chat/plugin.js +25 -13
- package/dist/engagement/chat/plugin.js.map +1 -1
- package/dist/engagement/chat/router/index.d.ts +518 -18
- package/dist/engagement/chat/router/index.d.ts.map +1 -1
- package/dist/engagement/chat/router/index.js +140 -20
- package/dist/engagement/chat/router/index.js.map +1 -1
- package/dist/engagement/chat/schema/index.d.ts +1017 -16
- package/dist/engagement/chat/schema/index.d.ts.map +1 -1
- package/dist/engagement/chat/schema/index.js +110 -4
- package/dist/engagement/chat/schema/index.js.map +1 -1
- package/dist/engagement/chat/service/chat-ban.service.d.ts +26 -0
- package/dist/engagement/chat/service/chat-ban.service.d.ts.map +1 -0
- package/dist/engagement/chat/service/chat-ban.service.js +139 -0
- package/dist/engagement/chat/service/chat-ban.service.js.map +1 -0
- package/dist/engagement/chat/service/chat-message-moderation.service.d.ts +14 -0
- package/dist/engagement/chat/service/chat-message-moderation.service.d.ts.map +1 -0
- package/dist/engagement/chat/service/chat-message-moderation.service.js +48 -0
- package/dist/engagement/chat/service/chat-message-moderation.service.js.map +1 -0
- package/dist/engagement/chat/service/chat-moderation.service.d.ts +30 -0
- package/dist/engagement/chat/service/chat-moderation.service.d.ts.map +1 -0
- package/dist/engagement/chat/service/chat-moderation.service.js +40 -0
- package/dist/engagement/chat/service/chat-moderation.service.js.map +1 -0
- package/dist/engagement/chat/service/chat-mute.service.d.ts +26 -0
- package/dist/engagement/chat/service/chat-mute.service.d.ts.map +1 -0
- package/dist/engagement/chat/service/chat-mute.service.js +143 -0
- package/dist/engagement/chat/service/chat-mute.service.js.map +1 -0
- package/dist/engagement/chat/service/chat-room-ban.service.d.ts +29 -0
- package/dist/engagement/chat/service/chat-room-ban.service.d.ts.map +1 -0
- package/dist/engagement/chat/service/chat-room-ban.service.js +105 -0
- package/dist/engagement/chat/service/chat-room-ban.service.js.map +1 -0
- package/dist/engagement/chat/service/chat-room-membership.service.d.ts +72 -0
- package/dist/engagement/chat/service/chat-room-membership.service.d.ts.map +1 -0
- package/dist/engagement/chat/service/chat-room-membership.service.js +176 -0
- package/dist/engagement/chat/service/chat-room-membership.service.js.map +1 -0
- package/dist/engagement/chat/service/chat-room-mute.service.d.ts +25 -0
- package/dist/engagement/chat/service/chat-room-mute.service.d.ts.map +1 -0
- package/dist/engagement/chat/service/chat-room-mute.service.js +89 -0
- package/dist/engagement/chat/service/chat-room-mute.service.js.map +1 -0
- package/dist/engagement/chat/service/chat.service.d.ts +348 -61
- package/dist/engagement/chat/service/chat.service.d.ts.map +1 -1
- package/dist/engagement/chat/service/chat.service.js +460 -194
- package/dist/engagement/chat/service/chat.service.js.map +1 -1
- package/dist/engagement/chat/service/errors/chat-moderation.errors.d.ts +21 -0
- package/dist/engagement/chat/service/errors/chat-moderation.errors.d.ts.map +1 -0
- package/dist/engagement/chat/service/errors/chat-moderation.errors.js +31 -0
- package/dist/engagement/chat/service/errors/chat-moderation.errors.js.map +1 -0
- package/dist/engagement/chat-commands/contract/index.js +1 -1
- package/dist/engagement/chat-commands/contract/index.js.map +1 -1
- package/dist/engagement/chat-commands/drizzle/migrations/0001_sturdy_barracuda.sql +1 -0
- package/dist/engagement/chat-commands/drizzle/migrations/meta/0001_snapshot.json +83 -0
- package/dist/engagement/chat-commands/drizzle/migrations/meta/_journal.json +7 -0
- package/dist/engagement/chat-commands/plugin.d.ts +1 -0
- package/dist/engagement/chat-commands/plugin.d.ts.map +1 -1
- package/dist/engagement/chat-commands/schema/index.d.ts +0 -199
- package/dist/engagement/chat-commands/schema/index.d.ts.map +1 -1
- package/dist/engagement/chat-commands/schema/index.js +1 -19
- package/dist/engagement/chat-commands/schema/index.js.map +1 -1
- package/dist/engagement/chat-commands/service/__tests__/chat-commands.service.test.js +15 -1
- package/dist/engagement/chat-commands/service/__tests__/chat-commands.service.test.js.map +1 -1
- package/dist/engagement/chat-commands/service/chat-commands.service.d.ts +1 -0
- package/dist/engagement/chat-commands/service/chat-commands.service.d.ts.map +1 -1
- package/dist/engagement/chat-commands/service/chat-commands.service.js +38 -3
- package/dist/engagement/chat-commands/service/chat-commands.service.js.map +1 -1
- package/dist/engagement/notifications/plugin.d.ts +1 -0
- package/dist/engagement/notifications/plugin.d.ts.map +1 -1
- package/dist/engagement/social/plugin.d.ts +1 -0
- package/dist/engagement/social/plugin.d.ts.map +1 -1
- package/dist/engagement/social-transfers/plugin.d.ts +1 -0
- package/dist/engagement/social-transfers/plugin.d.ts.map +1 -1
- package/dist/engagement/social-transfers/schema/index.d.ts +1 -2
- package/dist/engagement/social-transfers/schema/index.d.ts.map +1 -1
- package/dist/engagement/social-transfers/schema/index.js +1 -2
- package/dist/engagement/social-transfers/schema/index.js.map +1 -1
- package/dist/engagement/social-transfers/service/__tests__/social-transfers.service.test.js +19 -0
- package/dist/engagement/social-transfers/service/__tests__/social-transfers.service.test.js.map +1 -1
- package/dist/iam/plugin.d.ts +1 -0
- package/dist/iam/plugin.d.ts.map +1 -1
- package/dist/iam/seed/data/default-admin-roles.d.ts.map +1 -1
- package/dist/iam/seed/data/default-admin-roles.js +3 -0
- package/dist/iam/seed/data/default-admin-roles.js.map +1 -1
- package/dist/pam/identity/__tests__/admin-user-directory.int.test.js +33 -3
- package/dist/pam/identity/__tests__/admin-user-directory.int.test.js.map +1 -1
- package/dist/pam/identity/admin-user-directory.d.ts +12 -0
- package/dist/pam/identity/admin-user-directory.d.ts.map +1 -1
- package/dist/pam/identity/admin-user-directory.js +25 -25
- package/dist/pam/identity/admin-user-directory.js.map +1 -1
- package/dist/pam/identity/plugin.d.ts +1 -0
- package/dist/pam/identity/plugin.d.ts.map +1 -1
- package/dist/pam/player-management/__tests__/player.service.int.test.js +4 -0
- package/dist/pam/player-management/__tests__/player.service.int.test.js.map +1 -1
- package/dist/pam/player-management/plugin.d.ts +1 -0
- package/dist/pam/player-management/plugin.d.ts.map +1 -1
- package/dist/pam/player-note/plugin.d.ts +1 -0
- package/dist/pam/player-note/plugin.d.ts.map +1 -1
- package/dist/pam/profile/plugin.d.ts +1 -0
- package/dist/pam/profile/plugin.d.ts.map +1 -1
- package/dist/pam/tag/plugin.d.ts +1 -0
- package/dist/pam/tag/plugin.d.ts.map +1 -1
- package/dist/server/auth/permissions.d.ts +12 -12
- package/dist/server/auth/permissions.js +2 -2
- package/dist/server/auth/permissions.js.map +1 -1
- package/dist/server/kernel/__tests__/realtime-transport.test.js +7 -0
- package/dist/server/kernel/__tests__/realtime-transport.test.js.map +1 -1
- package/dist/server/kernel/orpc-error-map.d.ts.map +1 -1
- package/dist/server/kernel/orpc-error-map.js +4 -1
- package/dist/server/kernel/orpc-error-map.js.map +1 -1
- package/dist/server/kernel/realtime-transport.d.ts +3 -1
- package/dist/server/kernel/realtime-transport.d.ts.map +1 -1
- package/dist/server/kernel/realtime-transport.js +22 -4
- package/dist/server/kernel/realtime-transport.js.map +1 -1
- package/dist/server/runtime/core-token-catalog.d.ts +1 -0
- package/dist/server/runtime/core-token-catalog.d.ts.map +1 -1
- package/dist/server/runtime/core-token-catalog.js +2 -1
- package/dist/server/runtime/core-token-catalog.js.map +1 -1
- package/dist/testing/realtime-transport-conformance.d.ts.map +1 -1
- package/dist/testing/realtime-transport-conformance.js +4 -0
- package/dist/testing/realtime-transport-conformance.js.map +1 -1
- package/dist/wallet/__tests__/wallet.service.int.test.js +19 -2
- package/dist/wallet/__tests__/wallet.service.int.test.js.map +1 -1
- package/dist/wallet/contract/index.d.ts +2 -0
- package/dist/wallet/contract/index.d.ts.map +1 -1
- package/dist/wallet/contract/index.js +1 -0
- package/dist/wallet/contract/index.js.map +1 -1
- package/dist/wallet/plugin.d.ts +1 -0
- package/dist/wallet/plugin.d.ts.map +1 -1
- package/dist/wallet/router/index.d.ts +1 -0
- package/dist/wallet/router/index.d.ts.map +1 -1
- package/dist/wallet/service/wallet.service.d.ts +1 -0
- package/dist/wallet/service/wallet.service.d.ts.map +1 -1
- package/dist/wallet/service/wallet.service.js +1 -0
- package/dist/wallet/service/wallet.service.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { randomInt, randomUUID } from 'node:crypto';
|
|
2
|
-
import { createLogger,
|
|
3
|
-
import { chatChannel } from '@openora/core/contracts';
|
|
4
|
-
import { eq, and, isNull, lt, desc, asc, notInArray, inArray, count, ne } from 'drizzle-orm';
|
|
2
|
+
import { createLogger, makeOwnershipError, makeConflictError, createDomainError, assertOwnership, findOneOrThrow, serializeRow, pageToOffset, withAdvisoryXactLock, } from '@openora/core/server';
|
|
3
|
+
import { chatChannel, GLOBAL_CHAT_ROOM_ID } from '@openora/core/contracts';
|
|
4
|
+
import { eq, and, isNull, or, gt, lt, desc, asc, notInArray, inArray, count, ne, ilike, } from 'drizzle-orm';
|
|
5
5
|
import { user } from '@openora/core/pam/schema/identity';
|
|
6
|
-
import {
|
|
6
|
+
import { player } from '@openora/core/pam/schema/profile';
|
|
7
|
+
import { chatRoom, chatMessage, chatUserBlock, chatUserIgnore, chatRoomMember, chatRoomBan, chatRoomRule, chatRoomConfiguration, chatPlatformBan, } from '../schema/index.js';
|
|
8
|
+
import { ChatMessageNotFoundError, ChatRoomNotFoundError } from './chat-moderation.service.js';
|
|
9
|
+
export { ChatRoomNotMemberError, ChatRoomNotModeratorError, ChatRoomSelfModerationError, ChatRoomLastModeratorError, ChatRoomJoinCodeNotFoundError, ChatRoomBannedError, ChatPlayerBannedError, } from './errors/chat-moderation.errors.js';
|
|
10
|
+
import { ChatRoomNotMemberError, ChatRoomNotModeratorError, ChatPlayerBannedError, } from './errors/chat-moderation.errors.js';
|
|
7
11
|
import { DEFAULT_MESSAGE_LIMIT, JOIN_CODE_ALPHABET, JOIN_CODE_LENGTH, MAX_PRIVATE_ROOMS_PER_PLAYER, PRIVATE_ROOM_SLUG_PREFIX, } from '../contract/constants.js';
|
|
8
12
|
import { moderateContent } from '../moderation/index.js';
|
|
9
13
|
const logger = createLogger('chat');
|
|
@@ -14,21 +18,15 @@ function publishChatEvent(transport, roomId, event) {
|
|
|
14
18
|
logger.error({ err, roomId }, 'chat realtime publish failed');
|
|
15
19
|
});
|
|
16
20
|
}
|
|
17
|
-
export const ChatRoomNotFoundError = makeNotFoundError('ChatRoom');
|
|
18
21
|
export const ChatRoomOwnershipError = makeOwnershipError('ChatRoom');
|
|
19
|
-
export const ChatMessageNotFoundError = makeNotFoundError('ChatMessage');
|
|
20
|
-
export const ChatMessageOwnershipError = makeOwnershipError('ChatMessage');
|
|
21
22
|
export const ChatMessageBlockedError = createDomainError('ChatMessageBlockedError', () => 'Message blocked: it contains prohibited language');
|
|
22
23
|
export const ChatSelfBlockError = createDomainError('ChatSelfBlockError', () => 'You cannot block yourself');
|
|
23
24
|
export const ChatSelfIgnoreError = createDomainError('ChatSelfIgnoreError', () => 'You cannot ignore yourself');
|
|
24
25
|
export const ChatRoomSlugConflictError = makeConflictError('ChatRoomSlugConflictError', 'A room with this slug already exists');
|
|
25
26
|
export const ChatRoomLimitReachedError = makeConflictError('ChatRoomLimitReachedError', 'Private room limit reached');
|
|
26
|
-
export const
|
|
27
|
-
export const
|
|
28
|
-
|
|
29
|
-
export const ChatRoomNotMemberError = createDomainError('ChatRoomNotMemberError', (roomId) => `You are not a member of room: ${roomId}`);
|
|
30
|
-
export const ChatRoomNotModeratorError = createDomainError('ChatRoomNotModeratorError', (roomId) => `You are not a moderator of room: ${roomId}`);
|
|
31
|
-
export const ChatRoomSelfModerationError = createDomainError('ChatRoomSelfModerationError', () => 'You cannot kick or ban yourself');
|
|
27
|
+
export const ChatRoomRuleNotFoundError = createDomainError('ChatRoomRuleNotFoundError', (id) => `Chat room rule not found: ${id}`);
|
|
28
|
+
export const ChatRoomConfigurationNotFoundError = createDomainError('ChatRoomConfigurationNotFoundError', (id) => `Chat room configuration not found: ${id}`);
|
|
29
|
+
const CHAT_MODERATOR_ROLES = ['moderator', 'owner'];
|
|
32
30
|
function gateContent(content) {
|
|
33
31
|
const result = moderateContent(content);
|
|
34
32
|
if (!result.ok) {
|
|
@@ -38,11 +36,17 @@ function gateContent(content) {
|
|
|
38
36
|
}
|
|
39
37
|
function toRoom(record) {
|
|
40
38
|
const { deletedAt: _deletedAt, ...room } = record;
|
|
41
|
-
return serializeRow(room, { dateFields: ['createdAt'] });
|
|
39
|
+
return serializeRow({ ...room, isBanned: false, bannedUntil: null }, { dateFields: ['createdAt', 'bannedUntil'] });
|
|
42
40
|
}
|
|
43
41
|
function toMessage(record) {
|
|
44
42
|
return serializeRow(record, { dateFields: ['createdAt'] });
|
|
45
43
|
}
|
|
44
|
+
function toRule(record) {
|
|
45
|
+
return serializeRow(record, { dateFields: ['createdAt', 'updatedAt'] });
|
|
46
|
+
}
|
|
47
|
+
function toConfiguration(record) {
|
|
48
|
+
return serializeRow(record, { dateFields: ['createdAt', 'updatedAt'] });
|
|
49
|
+
}
|
|
46
50
|
function toSystemMessage(record) {
|
|
47
51
|
const message = toMessage(record);
|
|
48
52
|
return { ...message, actorId: message.userId };
|
|
@@ -82,12 +86,16 @@ export class ChatService {
|
|
|
82
86
|
events;
|
|
83
87
|
transport;
|
|
84
88
|
directory;
|
|
89
|
+
audit;
|
|
90
|
+
moderation;
|
|
85
91
|
identityReader;
|
|
86
|
-
constructor(drizzle, events, transport, directory, identityReader) {
|
|
92
|
+
constructor(drizzle, events, transport, directory, audit, moderation, identityReader) {
|
|
87
93
|
this.drizzle = drizzle;
|
|
88
94
|
this.events = events;
|
|
89
95
|
this.transport = transport;
|
|
90
96
|
this.directory = directory;
|
|
97
|
+
this.audit = audit;
|
|
98
|
+
this.moderation = moderation;
|
|
91
99
|
this.identityReader = identityReader;
|
|
92
100
|
}
|
|
93
101
|
subscribeMessages(roomId, listener, viewerId, connectionId = randomUUID()) {
|
|
@@ -120,7 +128,7 @@ export class ChatService {
|
|
|
120
128
|
else {
|
|
121
129
|
deliver(message);
|
|
122
130
|
}
|
|
123
|
-
});
|
|
131
|
+
}, viewerId);
|
|
124
132
|
return () => {
|
|
125
133
|
unsubscribe();
|
|
126
134
|
presence?.leave(channel, presenceMemberId, connectionId);
|
|
@@ -169,6 +177,27 @@ export class ChatService {
|
|
|
169
177
|
if (!room) {
|
|
170
178
|
throw new ChatRoomNotFoundError(roomId);
|
|
171
179
|
}
|
|
180
|
+
if (viewerId) {
|
|
181
|
+
const now = new Date();
|
|
182
|
+
const [roomBan] = await this.drizzle.db
|
|
183
|
+
.select({ expiresAt: chatRoomBan.expiresAt })
|
|
184
|
+
.from(chatRoomBan)
|
|
185
|
+
.where(and(eq(chatRoomBan.roomId, roomId), eq(chatRoomBan.userId, viewerId), isNull(chatRoomBan.liftedAt), or(isNull(chatRoomBan.expiresAt), gt(chatRoomBan.expiresAt, now))))
|
|
186
|
+
.limit(1);
|
|
187
|
+
if (roomBan) {
|
|
188
|
+
throw new ChatPlayerBannedError(roomBan.expiresAt);
|
|
189
|
+
}
|
|
190
|
+
const [platformBan] = await this.drizzle.db
|
|
191
|
+
.select({ expiresAt: chatPlatformBan.expiresAt })
|
|
192
|
+
.from(chatPlatformBan)
|
|
193
|
+
.where(and(eq(chatPlatformBan.userId, viewerId), isNull(chatPlatformBan.liftedAt), or(isNull(chatPlatformBan.expiresAt), gt(chatPlatformBan.expiresAt, now)), room.isPublic
|
|
194
|
+
? or(eq(chatPlatformBan.scope, '__all'), eq(chatPlatformBan.scope, '__all_public'), and(eq(chatPlatformBan.scope, 'room'), eq(chatPlatformBan.roomId, roomId)))
|
|
195
|
+
: or(eq(chatPlatformBan.scope, '__all'), and(eq(chatPlatformBan.scope, 'room'), eq(chatPlatformBan.roomId, roomId)))))
|
|
196
|
+
.limit(1);
|
|
197
|
+
if (platformBan) {
|
|
198
|
+
throw new ChatPlayerBannedError(platformBan.expiresAt);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
172
201
|
if (!room.isPublic) {
|
|
173
202
|
if (!viewerId) {
|
|
174
203
|
throw new ChatRoomNotMemberError(roomId);
|
|
@@ -184,6 +213,19 @@ export class ChatService {
|
|
|
184
213
|
}
|
|
185
214
|
return room;
|
|
186
215
|
}
|
|
216
|
+
async verifyGlobalAccess(viewerId) {
|
|
217
|
+
if (!viewerId) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const [platformBan] = await this.drizzle.db
|
|
221
|
+
.select({ expiresAt: chatPlatformBan.expiresAt })
|
|
222
|
+
.from(chatPlatformBan)
|
|
223
|
+
.where(and(eq(chatPlatformBan.userId, viewerId), isNull(chatPlatformBan.liftedAt), or(isNull(chatPlatformBan.expiresAt), gt(chatPlatformBan.expiresAt, new Date())), or(eq(chatPlatformBan.scope, '__global'), eq(chatPlatformBan.scope, '__all_public'), eq(chatPlatformBan.scope, '__all'))))
|
|
224
|
+
.limit(1);
|
|
225
|
+
if (platformBan) {
|
|
226
|
+
throw new ChatPlayerBannedError(platformBan.expiresAt);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
187
229
|
async listRooms(viewerId) {
|
|
188
230
|
const publicRooms = await this.drizzle.db
|
|
189
231
|
.select()
|
|
@@ -197,20 +239,59 @@ export class ChatService {
|
|
|
197
239
|
.select({ roomId: chatRoomMember.roomId })
|
|
198
240
|
.from(chatRoomMember)
|
|
199
241
|
.where(eq(chatRoomMember.userId, viewerId));
|
|
200
|
-
|
|
201
|
-
|
|
242
|
+
const now = new Date();
|
|
243
|
+
const bans = await this.drizzle.db
|
|
244
|
+
.select({ roomId: chatRoomBan.roomId, expiresAt: chatRoomBan.expiresAt })
|
|
245
|
+
.from(chatRoomBan)
|
|
246
|
+
.where(and(eq(chatRoomBan.userId, viewerId), isNull(chatRoomBan.liftedAt), or(isNull(chatRoomBan.expiresAt), gt(chatRoomBan.expiresAt, now))));
|
|
247
|
+
const roomBanById = new Map(bans.map((ban) => [ban.roomId, ban.expiresAt]));
|
|
248
|
+
const bannedPrivateIds = bans.map((ban) => ban.roomId);
|
|
249
|
+
const platformBans = await this.drizzle.db
|
|
250
|
+
.select({
|
|
251
|
+
scope: chatPlatformBan.scope,
|
|
252
|
+
roomId: chatPlatformBan.roomId,
|
|
253
|
+
expiresAt: chatPlatformBan.expiresAt,
|
|
254
|
+
})
|
|
255
|
+
.from(chatPlatformBan)
|
|
256
|
+
.where(and(eq(chatPlatformBan.userId, viewerId), isNull(chatPlatformBan.liftedAt), or(isNull(chatPlatformBan.expiresAt), gt(chatPlatformBan.expiresAt, now))));
|
|
257
|
+
const allPublicBan = platformBans.find((ban) => ban.scope === '__all_public' || ban.scope === '__all');
|
|
258
|
+
const allPrivateBan = platformBans.find((ban) => ban.scope === '__all');
|
|
259
|
+
const globalBan = platformBans.find((ban) => ban.scope === '__global');
|
|
260
|
+
for (const ban of platformBans) {
|
|
261
|
+
if (ban.scope === 'room' && ban.roomId) {
|
|
262
|
+
roomBanById.set(ban.roomId, ban.expiresAt);
|
|
263
|
+
}
|
|
202
264
|
}
|
|
203
265
|
const privateRooms = await this.drizzle.db
|
|
204
266
|
.select()
|
|
205
267
|
.from(chatRoom)
|
|
206
268
|
.where(and(eq(chatRoom.isPublic, false), isNull(chatRoom.deletedAt), inArray(chatRoom.id, memberIds.map((r) => r.roomId))))
|
|
207
269
|
.orderBy(asc(chatRoom.createdAt));
|
|
208
|
-
|
|
270
|
+
const privateBannedRooms = bannedPrivateIds.length
|
|
271
|
+
? await this.drizzle.db
|
|
272
|
+
.select()
|
|
273
|
+
.from(chatRoom)
|
|
274
|
+
.where(and(eq(chatRoom.isPublic, false), isNull(chatRoom.deletedAt), inArray(chatRoom.id, bannedPrivateIds)))
|
|
275
|
+
: [];
|
|
276
|
+
const uniqueRooms = [...publicRooms, ...privateRooms, ...privateBannedRooms].filter((room, index, rows) => rows.findIndex((candidate) => candidate.id === room.id) === index);
|
|
277
|
+
return uniqueRooms.map((room) => {
|
|
278
|
+
const roomBanUntil = roomBanById.get(room.id) ?? null;
|
|
279
|
+
const platformBanUntil = room.isPublic
|
|
280
|
+
? (allPublicBan?.expiresAt ?? null)
|
|
281
|
+
: (allPrivateBan?.expiresAt ?? null);
|
|
282
|
+
return serializeRow({
|
|
283
|
+
...room,
|
|
284
|
+
isBanned: room.slug === GLOBAL_CHAT_ROOM_ID
|
|
285
|
+
? Boolean(globalBan || allPublicBan)
|
|
286
|
+
: roomBanById.has(room.id) || Boolean(room.isPublic ? allPublicBan : allPrivateBan),
|
|
287
|
+
bannedUntil: roomBanById.has(room.id) ? roomBanUntil : platformBanUntil,
|
|
288
|
+
}, { dateFields: ['createdAt', 'bannedUntil'] });
|
|
289
|
+
});
|
|
209
290
|
}
|
|
210
|
-
async listAdminRooms({ page, limit, sortBy, sortOrder, }) {
|
|
291
|
+
async listAdminRooms({ page, limit, name, sortBy, sortOrder, }) {
|
|
211
292
|
const dir = sortOrder === 'asc' ? asc : desc;
|
|
212
293
|
const col = sortBy === 'name' ? chatRoom.name : chatRoom.createdAt;
|
|
213
|
-
const where = and(eq(chatRoom.isPublic, true), isNull(chatRoom.deletedAt));
|
|
294
|
+
const where = and(eq(chatRoom.isPublic, true), isNull(chatRoom.deletedAt), name ? ilike(chatRoom.name, `%${name}%`) : undefined);
|
|
214
295
|
const [rows, [{ n }]] = await Promise.all([
|
|
215
296
|
this.drizzle.db
|
|
216
297
|
.select()
|
|
@@ -223,13 +304,205 @@ export class ChatService {
|
|
|
223
304
|
]);
|
|
224
305
|
return { items: rows.map(toRoom), total: Number(n), page, limit };
|
|
225
306
|
}
|
|
307
|
+
async listModeratedRooms({ viewerId, page, limit, name, sortBy, sortOrder, }) {
|
|
308
|
+
const dir = sortOrder === 'asc' ? asc : desc;
|
|
309
|
+
const col = sortBy === 'name' ? chatRoom.name : chatRoom.createdAt;
|
|
310
|
+
const where = and(eq(chatRoomMember.userId, viewerId), inArray(chatRoomMember.role, ['owner', 'moderator']), isNull(chatRoom.deletedAt), name ? ilike(chatRoom.name, `%${name}%`) : undefined);
|
|
311
|
+
const [rows, [{ n }]] = await Promise.all([
|
|
312
|
+
this.drizzle.db
|
|
313
|
+
.select({ room: chatRoom })
|
|
314
|
+
.from(chatRoomMember)
|
|
315
|
+
.innerJoin(chatRoom, eq(chatRoom.id, chatRoomMember.roomId))
|
|
316
|
+
.where(where)
|
|
317
|
+
.orderBy(dir(col))
|
|
318
|
+
.limit(limit)
|
|
319
|
+
.offset(pageToOffset(page, limit)),
|
|
320
|
+
this.drizzle.db
|
|
321
|
+
.select({ n: count() })
|
|
322
|
+
.from(chatRoomMember)
|
|
323
|
+
.innerJoin(chatRoom, eq(chatRoom.id, chatRoomMember.roomId))
|
|
324
|
+
.where(where),
|
|
325
|
+
]);
|
|
326
|
+
return { items: rows.map(({ room }) => toRoom(room)), total: Number(n), page, limit };
|
|
327
|
+
}
|
|
226
328
|
async getRoom({ roomId, viewerId }) {
|
|
227
329
|
const room = await this.verifyRoomAccess(roomId, viewerId);
|
|
228
330
|
return toRoom(room);
|
|
229
331
|
}
|
|
332
|
+
async assertRoomModerator(roomId, actorId, targetId) {
|
|
333
|
+
const [actor] = await this.drizzle.db
|
|
334
|
+
.select({ role: chatRoomMember.role })
|
|
335
|
+
.from(chatRoomMember)
|
|
336
|
+
.where(and(eq(chatRoomMember.roomId, roomId), eq(chatRoomMember.userId, actorId)))
|
|
337
|
+
.limit(1);
|
|
338
|
+
if (!actor || !CHAT_MODERATOR_ROLES.includes(actor.role)) {
|
|
339
|
+
throw new ChatRoomNotModeratorError(roomId);
|
|
340
|
+
}
|
|
341
|
+
if (targetId) {
|
|
342
|
+
const [target] = await this.drizzle.db
|
|
343
|
+
.select({ role: chatRoomMember.role })
|
|
344
|
+
.from(chatRoomMember)
|
|
345
|
+
.where(and(eq(chatRoomMember.roomId, roomId), eq(chatRoomMember.userId, targetId)))
|
|
346
|
+
.limit(1);
|
|
347
|
+
if (actor.role !== 'owner' && target && target.role !== 'member') {
|
|
348
|
+
throw new ChatRoomNotModeratorError(roomId);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return actor.role;
|
|
352
|
+
}
|
|
353
|
+
async listRoomRules(roomReference, viewerId) {
|
|
354
|
+
const isGlobalRoom = roomReference === GLOBAL_CHAT_ROOM_ID;
|
|
355
|
+
const [room] = await this.drizzle.db
|
|
356
|
+
.select({ id: chatRoom.id, slug: chatRoom.slug, isPublic: chatRoom.isPublic })
|
|
357
|
+
.from(chatRoom)
|
|
358
|
+
.where(and(isGlobalRoom ? eq(chatRoom.slug, GLOBAL_CHAT_ROOM_ID) : eq(chatRoom.id, roomReference), isNull(chatRoom.deletedAt)))
|
|
359
|
+
.limit(1);
|
|
360
|
+
if (!room) {
|
|
361
|
+
throw new ChatRoomNotFoundError(roomReference);
|
|
362
|
+
}
|
|
363
|
+
if (!room.isPublic) {
|
|
364
|
+
if (!viewerId) {
|
|
365
|
+
throw new ChatRoomNotMemberError(room.id);
|
|
366
|
+
}
|
|
367
|
+
const [member] = await this.drizzle.db
|
|
368
|
+
.select({ id: chatRoomMember.id })
|
|
369
|
+
.from(chatRoomMember)
|
|
370
|
+
.where(and(eq(chatRoomMember.roomId, room.id), eq(chatRoomMember.userId, viewerId)))
|
|
371
|
+
.limit(1);
|
|
372
|
+
if (!member) {
|
|
373
|
+
throw new ChatRoomNotMemberError(room.id);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const rows = await this.drizzle.db
|
|
377
|
+
.select()
|
|
378
|
+
.from(chatRoomRule)
|
|
379
|
+
.where(eq(chatRoomRule.roomId, room.id))
|
|
380
|
+
.orderBy(asc(chatRoomRule.orderNum), asc(chatRoomRule.createdAt));
|
|
381
|
+
return rows.map(toRule);
|
|
382
|
+
}
|
|
383
|
+
async createRoomRule({ roomId, actorId, orderNum, content, }) {
|
|
384
|
+
await this.assertRoomModerator(roomId, actorId);
|
|
385
|
+
let nextOrder = orderNum;
|
|
386
|
+
if (nextOrder === undefined) {
|
|
387
|
+
const [last] = await this.drizzle.db
|
|
388
|
+
.select({ orderNum: chatRoomRule.orderNum })
|
|
389
|
+
.from(chatRoomRule)
|
|
390
|
+
.where(eq(chatRoomRule.roomId, roomId))
|
|
391
|
+
.orderBy(desc(chatRoomRule.orderNum))
|
|
392
|
+
.limit(1);
|
|
393
|
+
nextOrder = (last?.orderNum ?? 0) + 1;
|
|
394
|
+
}
|
|
395
|
+
const [created] = await this.drizzle.db
|
|
396
|
+
.insert(chatRoomRule)
|
|
397
|
+
.values({ roomId, createdBy: actorId, orderNum: nextOrder, content })
|
|
398
|
+
.returning();
|
|
399
|
+
return toRule(created);
|
|
400
|
+
}
|
|
401
|
+
async updateRoomRule({ roomId, id, actorId, orderNum, content, }) {
|
|
402
|
+
await this.assertRoomModerator(roomId, actorId);
|
|
403
|
+
const patch = { updatedAt: new Date() };
|
|
404
|
+
if (orderNum !== undefined) {
|
|
405
|
+
patch.orderNum = orderNum;
|
|
406
|
+
}
|
|
407
|
+
if (content !== undefined) {
|
|
408
|
+
patch.content = content;
|
|
409
|
+
}
|
|
410
|
+
const updated = findOneOrThrow(await this.drizzle.db
|
|
411
|
+
.update(chatRoomRule)
|
|
412
|
+
.set(patch)
|
|
413
|
+
.where(and(eq(chatRoomRule.id, id), eq(chatRoomRule.roomId, roomId)))
|
|
414
|
+
.returning(), new ChatRoomRuleNotFoundError(id));
|
|
415
|
+
return toRule(updated);
|
|
416
|
+
}
|
|
417
|
+
async deleteRoomRule({ roomId, id, actorId, }) {
|
|
418
|
+
await this.assertRoomModerator(roomId, actorId);
|
|
419
|
+
findOneOrThrow(await this.drizzle.db
|
|
420
|
+
.delete(chatRoomRule)
|
|
421
|
+
.where(and(eq(chatRoomRule.id, id), eq(chatRoomRule.roomId, roomId)))
|
|
422
|
+
.returning({ id: chatRoomRule.id }), new ChatRoomRuleNotFoundError(id));
|
|
423
|
+
return { success: true };
|
|
424
|
+
}
|
|
425
|
+
async getRoomConfiguration(roomId, viewerId) {
|
|
426
|
+
await this.verifyRoomAccess(roomId, viewerId);
|
|
427
|
+
let [config] = await this.drizzle.db
|
|
428
|
+
.select()
|
|
429
|
+
.from(chatRoomConfiguration)
|
|
430
|
+
.where(eq(chatRoomConfiguration.roomId, roomId))
|
|
431
|
+
.limit(1);
|
|
432
|
+
if (!config) {
|
|
433
|
+
[config] = await this.drizzle.db.insert(chatRoomConfiguration).values({ roomId }).returning();
|
|
434
|
+
}
|
|
435
|
+
return toConfiguration(config);
|
|
436
|
+
}
|
|
437
|
+
async updateRoomConfiguration({ roomId, actorId, ...patch }) {
|
|
438
|
+
await this.assertRoomModerator(roomId, actorId);
|
|
439
|
+
const [config] = await this.drizzle.db
|
|
440
|
+
.insert(chatRoomConfiguration)
|
|
441
|
+
.values({ roomId, ...patch, updatedAt: new Date() })
|
|
442
|
+
.onConflictDoUpdate({
|
|
443
|
+
target: chatRoomConfiguration.roomId,
|
|
444
|
+
set: { ...patch, updatedAt: new Date() },
|
|
445
|
+
})
|
|
446
|
+
.returning();
|
|
447
|
+
return toConfiguration(config);
|
|
448
|
+
}
|
|
449
|
+
async listRoomUsers({ roomId, actorId, status, }) {
|
|
450
|
+
await this.assertRoomModerator(roomId, actorId);
|
|
451
|
+
const role = status === 'all' ? undefined : status;
|
|
452
|
+
const canSeeAdminUsers = await this.canSeeAdminUsers(actorId);
|
|
453
|
+
const members = await this.drizzle.db
|
|
454
|
+
.select({ member: chatRoomMember, username: user.name })
|
|
455
|
+
.from(chatRoomMember)
|
|
456
|
+
.leftJoin(user, eq(user.id, chatRoomMember.userId))
|
|
457
|
+
.where(and(eq(chatRoomMember.roomId, roomId), role ? eq(chatRoomMember.role, role) : undefined, canSeeAdminUsers
|
|
458
|
+
? undefined
|
|
459
|
+
: or(isNull(user.id), notInArray(user.role, ['admin', 'super-admin']))))
|
|
460
|
+
.orderBy(asc(chatRoomMember.joinedAt));
|
|
461
|
+
const bans = await this.drizzle.db
|
|
462
|
+
.select()
|
|
463
|
+
.from(chatRoomBan)
|
|
464
|
+
.where(and(eq(chatRoomBan.roomId, roomId), isNull(chatRoomBan.liftedAt)));
|
|
465
|
+
const now = Date.now();
|
|
466
|
+
const activeBans = new Map(bans
|
|
467
|
+
.filter((ban) => !ban.expiresAt || ban.expiresAt.getTime() > now)
|
|
468
|
+
.map((ban) => [ban.userId, ban]));
|
|
469
|
+
const names = await this.directory.lookupPlayers(members.map(({ member }) => member.userId));
|
|
470
|
+
const nameById = new Map(names.map((name) => [name.userId, name.username]));
|
|
471
|
+
return members.map(({ member, username }) => {
|
|
472
|
+
const ban = activeBans.get(member.userId);
|
|
473
|
+
return serializeRow({
|
|
474
|
+
userId: member.userId,
|
|
475
|
+
username: nameById.get(member.userId) ?? username ?? null,
|
|
476
|
+
role: member.role,
|
|
477
|
+
joinedAt: member.joinedAt,
|
|
478
|
+
blocked: Boolean(ban),
|
|
479
|
+
banId: ban?.id ?? null,
|
|
480
|
+
banExpiresAt: ban?.expiresAt ?? null,
|
|
481
|
+
}, { dateFields: ['joinedAt', 'banExpiresAt'] });
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
async listRoomBlockedUsers({ roomId, actorId }) {
|
|
485
|
+
await this.assertRoomModerator(roomId, actorId);
|
|
486
|
+
const rows = await this.drizzle.db
|
|
487
|
+
.select()
|
|
488
|
+
.from(chatRoomBan)
|
|
489
|
+
.where(and(eq(chatRoomBan.roomId, roomId), isNull(chatRoomBan.liftedAt), or(isNull(chatRoomBan.expiresAt), gt(chatRoomBan.expiresAt, new Date()))))
|
|
490
|
+
.orderBy(desc(chatRoomBan.createdAt));
|
|
491
|
+
const latest = new Map();
|
|
492
|
+
for (const row of rows) {
|
|
493
|
+
if (!latest.has(row.userId)) {
|
|
494
|
+
latest.set(row.userId, row);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return [...latest.values()].map((row) => serializeRow(row, { dateFields: ['createdAt', 'expiresAt', 'liftedAt'] }));
|
|
498
|
+
}
|
|
230
499
|
async getRoomMessages({ roomId, limit = DEFAULT_MESSAGE_LIMIT, before, viewerId, }) {
|
|
231
500
|
await this.verifyRoomAccess(roomId, viewerId);
|
|
232
|
-
const conditions = [
|
|
501
|
+
const conditions = [
|
|
502
|
+
eq(chatMessage.roomId, roomId),
|
|
503
|
+
eq(chatMessage.isDeleted, false),
|
|
504
|
+
isNull(chatMessage.deletedAt),
|
|
505
|
+
];
|
|
233
506
|
if (before) {
|
|
234
507
|
conditions.push(lt(chatMessage.createdAt, new Date(before)));
|
|
235
508
|
}
|
|
@@ -242,6 +515,93 @@ export class ChatService {
|
|
|
242
515
|
.limit(limit);
|
|
243
516
|
return messages.map(toPublicMessage);
|
|
244
517
|
}
|
|
518
|
+
async listAdminRoomMessages({ roomId, page, limit, senderId, playerId, includeDeleted, }) {
|
|
519
|
+
const roomWhere = roomId === GLOBAL_CHAT_ROOM_ID
|
|
520
|
+
? and(eq(chatRoom.slug, GLOBAL_CHAT_ROOM_ID), isNull(chatRoom.deletedAt))
|
|
521
|
+
: and(eq(chatRoom.id, roomId), isNull(chatRoom.deletedAt));
|
|
522
|
+
const [room] = await this.drizzle.db
|
|
523
|
+
.select({ id: chatRoom.id })
|
|
524
|
+
.from(chatRoom)
|
|
525
|
+
.where(roomWhere)
|
|
526
|
+
.limit(1);
|
|
527
|
+
if (!room) {
|
|
528
|
+
throw new ChatRoomNotFoundError(roomId);
|
|
529
|
+
}
|
|
530
|
+
const where = and(roomId === GLOBAL_CHAT_ROOM_ID ? isNull(chatMessage.roomId) : eq(chatMessage.roomId, roomId), includeDeleted ? undefined : eq(chatMessage.isDeleted, false), senderId ? eq(chatMessage.userId, senderId) : undefined, playerId ? eq(player.id, playerId) : undefined);
|
|
531
|
+
const [rows, [{ n }]] = await Promise.all([
|
|
532
|
+
this.drizzle.db
|
|
533
|
+
.select({ message: chatMessage, playerId: player.id })
|
|
534
|
+
.from(chatMessage)
|
|
535
|
+
.leftJoin(player, eq(player.userId, chatMessage.userId))
|
|
536
|
+
.where(where)
|
|
537
|
+
.orderBy(desc(chatMessage.createdAt))
|
|
538
|
+
.limit(limit)
|
|
539
|
+
.offset(pageToOffset(page, limit)),
|
|
540
|
+
this.drizzle.db
|
|
541
|
+
.select({ n: count() })
|
|
542
|
+
.from(chatMessage)
|
|
543
|
+
.leftJoin(player, eq(player.userId, chatMessage.userId))
|
|
544
|
+
.where(where),
|
|
545
|
+
]);
|
|
546
|
+
return {
|
|
547
|
+
items: rows.map(({ message, playerId }) => ({ ...toPublicMessage(message), playerId })),
|
|
548
|
+
total: Number(n),
|
|
549
|
+
page,
|
|
550
|
+
limit,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
async listAdminMessages({ page, limit, roomId, senderId, playerId, search, includeDeleted, sortOrder, }) {
|
|
554
|
+
const roomCondition = roomId === GLOBAL_CHAT_ROOM_ID
|
|
555
|
+
? isNull(chatMessage.roomId)
|
|
556
|
+
: roomId
|
|
557
|
+
? eq(chatMessage.roomId, roomId)
|
|
558
|
+
: undefined;
|
|
559
|
+
const where = and(eq(chatMessage.type, 'user'), roomCondition, senderId ? eq(chatMessage.userId, senderId) : undefined, playerId ? eq(player.id, playerId) : undefined, search ? ilike(chatMessage.content, `%${search}%`) : undefined, includeDeleted
|
|
560
|
+
? undefined
|
|
561
|
+
: and(eq(chatMessage.isDeleted, false), isNull(chatMessage.deletedAt)));
|
|
562
|
+
const direction = sortOrder === 'asc' ? asc : desc;
|
|
563
|
+
const [rows, [{ n }]] = await Promise.all([
|
|
564
|
+
this.drizzle.db
|
|
565
|
+
.select({
|
|
566
|
+
id: chatMessage.id,
|
|
567
|
+
createdAt: chatMessage.createdAt,
|
|
568
|
+
roomId: chatMessage.roomId,
|
|
569
|
+
playerId: player.id,
|
|
570
|
+
roomName: chatRoom.name,
|
|
571
|
+
content: chatMessage.content,
|
|
572
|
+
})
|
|
573
|
+
.from(chatMessage)
|
|
574
|
+
.leftJoin(player, eq(player.userId, chatMessage.userId))
|
|
575
|
+
.leftJoin(chatRoom, eq(chatRoom.id, chatMessage.roomId))
|
|
576
|
+
.where(where)
|
|
577
|
+
.orderBy(direction(chatMessage.createdAt))
|
|
578
|
+
.limit(limit)
|
|
579
|
+
.offset(pageToOffset(page, limit)),
|
|
580
|
+
this.drizzle.db
|
|
581
|
+
.select({ n: count() })
|
|
582
|
+
.from(chatMessage)
|
|
583
|
+
.leftJoin(player, eq(player.userId, chatMessage.userId))
|
|
584
|
+
.leftJoin(chatRoom, eq(chatRoom.id, chatMessage.roomId))
|
|
585
|
+
.where(where),
|
|
586
|
+
]);
|
|
587
|
+
return {
|
|
588
|
+
items: rows.map((row) => {
|
|
589
|
+
const date = row.createdAt.toISOString();
|
|
590
|
+
return {
|
|
591
|
+
id: row.id,
|
|
592
|
+
date,
|
|
593
|
+
roomId: row.roomId ?? GLOBAL_CHAT_ROOM_ID,
|
|
594
|
+
playerId: row.playerId,
|
|
595
|
+
roomName: row.roomName ?? 'Global',
|
|
596
|
+
content: row.content,
|
|
597
|
+
time: date.slice(11, 19),
|
|
598
|
+
};
|
|
599
|
+
}),
|
|
600
|
+
total: Number(n),
|
|
601
|
+
page,
|
|
602
|
+
limit,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
245
605
|
async appendExcludedSendersFilter(conditions, viewerId) {
|
|
246
606
|
if (!viewerId) {
|
|
247
607
|
return;
|
|
@@ -253,7 +613,8 @@ export class ChatService {
|
|
|
253
613
|
}
|
|
254
614
|
async sendRoomMessage({ userId, username, roomId, content, }) {
|
|
255
615
|
// TODO: check RG_SELF_EXCLUSION_SERVICE before send (sealed token not yet implemented)
|
|
256
|
-
await this.verifyRoomAccess(roomId, userId);
|
|
616
|
+
const room = await this.verifyRoomAccess(roomId, userId);
|
|
617
|
+
await this.moderation.assertCanSend(userId, roomId, room.isPublic);
|
|
257
618
|
const safeContent = gateContent(content);
|
|
258
619
|
const displayName = await this.resolveDisplayName(userId, username);
|
|
259
620
|
const [record] = await this.drizzle.db
|
|
@@ -274,17 +635,30 @@ export class ChatService {
|
|
|
274
635
|
publishChatEvent(this.transport, roomId, message);
|
|
275
636
|
return message;
|
|
276
637
|
}
|
|
277
|
-
async deleteMessage(id, userId) {
|
|
638
|
+
async deleteMessage(id, userId, meta) {
|
|
278
639
|
const message = findOneOrThrow(await this.drizzle.db.select().from(chatMessage).where(eq(chatMessage.id, id)), new ChatMessageNotFoundError(id));
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
640
|
+
let roomId = message.roomId;
|
|
641
|
+
if (roomId === null) {
|
|
642
|
+
const [globalRoom] = await this.drizzle.db
|
|
643
|
+
.select({ id: chatRoom.id })
|
|
644
|
+
.from(chatRoom)
|
|
645
|
+
.where(and(eq(chatRoom.slug, '__global'), isNull(chatRoom.deletedAt)))
|
|
646
|
+
.limit(1);
|
|
647
|
+
if (!globalRoom) {
|
|
648
|
+
throw new ChatRoomNotFoundError('__global');
|
|
649
|
+
}
|
|
650
|
+
roomId = globalRoom.id;
|
|
651
|
+
}
|
|
652
|
+
await this.assertRoomModerator(roomId, userId);
|
|
653
|
+
return this.moderation.deleteMessage(id, userId, meta, 'player');
|
|
285
654
|
}
|
|
286
655
|
async getGlobalMessages(limit = DEFAULT_MESSAGE_LIMIT, viewerId) {
|
|
287
|
-
|
|
656
|
+
await this.verifyGlobalAccess(viewerId);
|
|
657
|
+
const conditions = [
|
|
658
|
+
isNull(chatMessage.roomId),
|
|
659
|
+
eq(chatMessage.isDeleted, false),
|
|
660
|
+
isNull(chatMessage.deletedAt),
|
|
661
|
+
];
|
|
288
662
|
await this.appendExcludedSendersFilter(conditions, viewerId);
|
|
289
663
|
const messages = await this.drizzle.db
|
|
290
664
|
.select()
|
|
@@ -296,6 +670,7 @@ export class ChatService {
|
|
|
296
670
|
}
|
|
297
671
|
async sendGlobalMessage(userId, username, content) {
|
|
298
672
|
// TODO: check RG_SELF_EXCLUSION_SERVICE before send (sealed token not yet implemented)
|
|
673
|
+
await this.moderation.assertCanSend(userId, null);
|
|
299
674
|
const safeContent = gateContent(content);
|
|
300
675
|
const displayName = await this.resolveDisplayName(userId, username);
|
|
301
676
|
const [record] = await this.drizzle.db
|
|
@@ -501,20 +876,29 @@ export class ChatService {
|
|
|
501
876
|
return [...(await this.excludedSenderIdsFor(viewerId))];
|
|
502
877
|
}
|
|
503
878
|
async createRoom({ name, slug, category, actorId, ip, userAgent, }) {
|
|
504
|
-
const [existing] = await this.drizzle.db
|
|
505
|
-
.select({ id: chatRoom.id })
|
|
506
|
-
.from(chatRoom)
|
|
507
|
-
.where(eq(chatRoom.slug, slug))
|
|
508
|
-
.limit(1);
|
|
509
|
-
if (existing) {
|
|
510
|
-
throw new ChatRoomSlugConflictError();
|
|
511
|
-
}
|
|
512
879
|
let record;
|
|
513
880
|
try {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
881
|
+
record = await this.drizzle.db.transaction(async (tx) => {
|
|
882
|
+
const [existing] = await tx
|
|
883
|
+
.select({ id: chatRoom.id })
|
|
884
|
+
.from(chatRoom)
|
|
885
|
+
.where(eq(chatRoom.slug, slug))
|
|
886
|
+
.limit(1);
|
|
887
|
+
if (existing) {
|
|
888
|
+
throw new ChatRoomSlugConflictError();
|
|
889
|
+
}
|
|
890
|
+
const [created] = await tx
|
|
891
|
+
.insert(chatRoom)
|
|
892
|
+
.values({ name, slug, category, joinCode: generateJoinCode() })
|
|
893
|
+
.returning();
|
|
894
|
+
await tx.insert(chatRoomConfiguration).values({ roomId: created.id });
|
|
895
|
+
if (actorId) {
|
|
896
|
+
await tx
|
|
897
|
+
.insert(chatRoomMember)
|
|
898
|
+
.values({ roomId: created.id, userId: actorId, role: 'owner' });
|
|
899
|
+
}
|
|
900
|
+
return created;
|
|
901
|
+
});
|
|
518
902
|
}
|
|
519
903
|
catch (error) {
|
|
520
904
|
if (isUniqueConstraintViolation(error)) {
|
|
@@ -583,6 +967,18 @@ export class ChatService {
|
|
|
583
967
|
});
|
|
584
968
|
return toRoom(updated);
|
|
585
969
|
}
|
|
970
|
+
async updatePrivateRoom({ id, name, actorId, ip, userAgent, }) {
|
|
971
|
+
const room = findOneOrThrow(await this.drizzle.db
|
|
972
|
+
.select()
|
|
973
|
+
.from(chatRoom)
|
|
974
|
+
.where(and(eq(chatRoom.id, id), eq(chatRoom.isPublic, false), isNull(chatRoom.deletedAt)))
|
|
975
|
+
.limit(1), new ChatRoomNotFoundError(id));
|
|
976
|
+
if (!room.creatorId) {
|
|
977
|
+
throw new ChatRoomOwnershipError();
|
|
978
|
+
}
|
|
979
|
+
assertOwnership(room.creatorId, actorId, new ChatRoomOwnershipError());
|
|
980
|
+
return this.updateRoom({ id, name, actorId, ip, userAgent });
|
|
981
|
+
}
|
|
586
982
|
async deletePrivateRoom({ roomId, userId, ip, userAgent, }) {
|
|
587
983
|
const room = findOneOrThrow(await this.drizzle.db
|
|
588
984
|
.select()
|
|
@@ -593,14 +989,14 @@ export class ChatService {
|
|
|
593
989
|
throw new ChatRoomOwnershipError();
|
|
594
990
|
}
|
|
595
991
|
assertOwnership(room.creatorId, userId, new ChatRoomOwnershipError());
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
.set({ deletedAt: new Date() })
|
|
599
|
-
.where(eq(chatRoom.id, roomId));
|
|
992
|
+
const deletedAt = new Date();
|
|
993
|
+
await this.drizzle.db.update(chatRoom).set({ deletedAt }).where(eq(chatRoom.id, roomId));
|
|
600
994
|
this.events.emit('chat.private_room.deleted', {
|
|
601
995
|
roomId,
|
|
602
996
|
creatorId: userId,
|
|
603
997
|
playerId: await this.identityReader.getPlayerIdByUserIdSafe(userId),
|
|
998
|
+
before: { name: room.name, slug: room.slug, category: room.category },
|
|
999
|
+
after: { deletedAt: deletedAt.toISOString() },
|
|
604
1000
|
ip: ip ?? null,
|
|
605
1001
|
userAgent: userAgent ?? null,
|
|
606
1002
|
});
|
|
@@ -651,9 +1047,10 @@ export class ChatService {
|
|
|
651
1047
|
creatorId: userId,
|
|
652
1048
|
})
|
|
653
1049
|
.returning();
|
|
1050
|
+
await t.insert(chatRoomConfiguration).values({ roomId: room.id });
|
|
654
1051
|
await t
|
|
655
1052
|
.insert(chatRoomMember)
|
|
656
|
-
.values({ roomId: room.id, userId, role: '
|
|
1053
|
+
.values({ roomId: room.id, userId, role: 'owner' })
|
|
657
1054
|
.onConflictDoNothing();
|
|
658
1055
|
return room;
|
|
659
1056
|
}));
|
|
@@ -676,150 +1073,6 @@ export class ChatService {
|
|
|
676
1073
|
// unreachable: loop returns or rethrows on every iteration
|
|
677
1074
|
throw new ChatRoomSlugConflictError();
|
|
678
1075
|
}
|
|
679
|
-
async joinRoom({ userId, joinCode, ip, userAgent, }) {
|
|
680
|
-
const [candidate] = await this.drizzle.db
|
|
681
|
-
.select({ id: chatRoom.id })
|
|
682
|
-
.from(chatRoom)
|
|
683
|
-
.where(and(eq(chatRoom.joinCode, joinCode), isNull(chatRoom.deletedAt)))
|
|
684
|
-
.limit(1);
|
|
685
|
-
if (!candidate) {
|
|
686
|
-
throw new ChatRoomJoinCodeNotFoundError(joinCode);
|
|
687
|
-
}
|
|
688
|
-
const { room, inserted } = await this.drizzle.db.transaction((t) => withAdvisoryXactLock(t, `chat-room:${candidate.id}`, async () => {
|
|
689
|
-
const [room] = await t
|
|
690
|
-
.select()
|
|
691
|
-
.from(chatRoom)
|
|
692
|
-
.where(and(eq(chatRoom.id, candidate.id), eq(chatRoom.joinCode, joinCode), isNull(chatRoom.deletedAt)))
|
|
693
|
-
.limit(1);
|
|
694
|
-
if (!room) {
|
|
695
|
-
throw new ChatRoomJoinCodeNotFoundError(joinCode);
|
|
696
|
-
}
|
|
697
|
-
const [ban] = await t
|
|
698
|
-
.select({ id: chatRoomBan.id })
|
|
699
|
-
.from(chatRoomBan)
|
|
700
|
-
.where(and(eq(chatRoomBan.roomId, room.id), eq(chatRoomBan.userId, userId)))
|
|
701
|
-
.limit(1);
|
|
702
|
-
if (ban) {
|
|
703
|
-
throw new ChatRoomBannedError(room.id);
|
|
704
|
-
}
|
|
705
|
-
// Idempotent: re-joining is a no-op (preserves existing role).
|
|
706
|
-
const inserted = await t
|
|
707
|
-
.insert(chatRoomMember)
|
|
708
|
-
.values({ roomId: room.id, userId })
|
|
709
|
-
.onConflictDoNothing()
|
|
710
|
-
.returning();
|
|
711
|
-
return { room, inserted };
|
|
712
|
-
}));
|
|
713
|
-
if (inserted.length > 0) {
|
|
714
|
-
this.events.emit('chat.room.member.joined', {
|
|
715
|
-
roomId: room.id,
|
|
716
|
-
userId,
|
|
717
|
-
playerId: await this.identityReader.getPlayerIdByUserIdSafe(userId),
|
|
718
|
-
ip: ip ?? null,
|
|
719
|
-
userAgent: userAgent ?? null,
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
return toRoom(room);
|
|
723
|
-
}
|
|
724
|
-
async leaveRoom({ userId, roomId, ip, userAgent, }) {
|
|
725
|
-
await this.verifyRoomAccess(roomId, userId);
|
|
726
|
-
const removed = await this.drizzle.db.transaction((t) => withAdvisoryXactLock(t, `chat-room:${roomId}`, async () => {
|
|
727
|
-
const [memberRow] = await t
|
|
728
|
-
.select({ role: chatRoomMember.role })
|
|
729
|
-
.from(chatRoomMember)
|
|
730
|
-
.where(and(eq(chatRoomMember.roomId, roomId), eq(chatRoomMember.userId, userId)))
|
|
731
|
-
.limit(1);
|
|
732
|
-
if (memberRow?.role === 'moderator') {
|
|
733
|
-
const [{ modCount }] = await t
|
|
734
|
-
.select({ modCount: count() })
|
|
735
|
-
.from(chatRoomMember)
|
|
736
|
-
.where(and(eq(chatRoomMember.roomId, roomId), eq(chatRoomMember.role, 'moderator')));
|
|
737
|
-
if (Number(modCount) <= 1) {
|
|
738
|
-
throw new ChatRoomLastModeratorError();
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
return t
|
|
742
|
-
.delete(chatRoomMember)
|
|
743
|
-
.where(and(eq(chatRoomMember.roomId, roomId), eq(chatRoomMember.userId, userId)))
|
|
744
|
-
.returning();
|
|
745
|
-
}));
|
|
746
|
-
if (removed.length > 0) {
|
|
747
|
-
this.events.emit('chat.room.member.left', {
|
|
748
|
-
roomId,
|
|
749
|
-
userId,
|
|
750
|
-
playerId: await this.identityReader.getPlayerIdByUserIdSafe(userId),
|
|
751
|
-
ip: ip ?? null,
|
|
752
|
-
userAgent: userAgent ?? null,
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
return { success: true };
|
|
756
|
-
}
|
|
757
|
-
async kickMember({ moderatorId, roomId, userId, ip, userAgent, }) {
|
|
758
|
-
if (moderatorId === userId) {
|
|
759
|
-
throw new ChatRoomSelfModerationError();
|
|
760
|
-
}
|
|
761
|
-
await this.verifyRoomAccess(roomId, moderatorId);
|
|
762
|
-
const removed = await this.drizzle.db.transaction(async (t) => {
|
|
763
|
-
const [mod] = await t
|
|
764
|
-
.select({ role: chatRoomMember.role })
|
|
765
|
-
.from(chatRoomMember)
|
|
766
|
-
.where(and(eq(chatRoomMember.roomId, roomId), eq(chatRoomMember.userId, moderatorId)))
|
|
767
|
-
.limit(1);
|
|
768
|
-
if (!mod || mod.role !== 'moderator') {
|
|
769
|
-
throw new ChatRoomNotModeratorError(roomId);
|
|
770
|
-
}
|
|
771
|
-
return t
|
|
772
|
-
.delete(chatRoomMember)
|
|
773
|
-
.where(and(eq(chatRoomMember.roomId, roomId), eq(chatRoomMember.userId, userId)))
|
|
774
|
-
.returning();
|
|
775
|
-
});
|
|
776
|
-
if (removed.length > 0) {
|
|
777
|
-
this.events.emit('chat.room.member.kicked', {
|
|
778
|
-
roomId,
|
|
779
|
-
userId,
|
|
780
|
-
kickedBy: moderatorId,
|
|
781
|
-
playerId: await this.identityReader.getPlayerIdByUserIdSafe(moderatorId),
|
|
782
|
-
ip: ip ?? null,
|
|
783
|
-
userAgent: userAgent ?? null,
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
await this.transport.revokeClient?.(userId);
|
|
787
|
-
return { success: true };
|
|
788
|
-
}
|
|
789
|
-
async banMember({ moderatorId, roomId, userId, ip, userAgent, }) {
|
|
790
|
-
if (moderatorId === userId) {
|
|
791
|
-
throw new ChatRoomSelfModerationError();
|
|
792
|
-
}
|
|
793
|
-
await this.verifyRoomAccess(roomId, moderatorId);
|
|
794
|
-
// Idempotent: banning an already-banned user is a no-op.
|
|
795
|
-
await this.drizzle.db.transaction((t) => withAdvisoryXactLock(t, `chat-room:${roomId}`, async () => {
|
|
796
|
-
const [mod] = await t
|
|
797
|
-
.select({ role: chatRoomMember.role })
|
|
798
|
-
.from(chatRoomMember)
|
|
799
|
-
.where(and(eq(chatRoomMember.roomId, roomId), eq(chatRoomMember.userId, moderatorId)))
|
|
800
|
-
.limit(1);
|
|
801
|
-
if (!mod || mod.role !== 'moderator') {
|
|
802
|
-
throw new ChatRoomNotModeratorError(roomId);
|
|
803
|
-
}
|
|
804
|
-
await t
|
|
805
|
-
.insert(chatRoomBan)
|
|
806
|
-
.values({ roomId, userId, bannedBy: moderatorId })
|
|
807
|
-
.onConflictDoNothing();
|
|
808
|
-
await t
|
|
809
|
-
.delete(chatRoomMember)
|
|
810
|
-
.where(and(eq(chatRoomMember.roomId, roomId), eq(chatRoomMember.userId, userId)));
|
|
811
|
-
}));
|
|
812
|
-
this.events.emit('chat.room.member.banned', {
|
|
813
|
-
roomId,
|
|
814
|
-
userId,
|
|
815
|
-
bannedBy: moderatorId,
|
|
816
|
-
playerId: await this.identityReader.getPlayerIdByUserIdSafe(moderatorId),
|
|
817
|
-
ip: ip ?? null,
|
|
818
|
-
userAgent: userAgent ?? null,
|
|
819
|
-
});
|
|
820
|
-
await this.transport.revokeClient?.(userId);
|
|
821
|
-
return { success: true };
|
|
822
|
-
}
|
|
823
1076
|
async postSystemMessage(args) {
|
|
824
1077
|
const db = args.tx ?? this.drizzle.db;
|
|
825
1078
|
const [record] = await db
|
|
@@ -858,18 +1111,31 @@ export class ChatService {
|
|
|
858
1111
|
}
|
|
859
1112
|
async listRoomMembers({ roomId, viewerId }) {
|
|
860
1113
|
await this.verifyRoomAccess(roomId, viewerId);
|
|
1114
|
+
const canSeeAdminUsers = viewerId ? await this.canSeeAdminUsers(viewerId) : false;
|
|
861
1115
|
const members = await this.drizzle.db
|
|
862
1116
|
.select({
|
|
863
1117
|
userId: chatRoomMember.userId,
|
|
864
1118
|
role: chatRoomMember.role,
|
|
865
1119
|
joinedAt: chatRoomMember.joinedAt,
|
|
1120
|
+
username: user.name,
|
|
866
1121
|
})
|
|
867
1122
|
.from(chatRoomMember)
|
|
868
|
-
.
|
|
1123
|
+
.leftJoin(user, eq(user.id, chatRoomMember.userId))
|
|
1124
|
+
.where(and(eq(chatRoomMember.roomId, roomId), canSeeAdminUsers
|
|
1125
|
+
? undefined
|
|
1126
|
+
: or(isNull(user.id), notInArray(user.role, ['admin', 'super-admin']))))
|
|
869
1127
|
.orderBy(asc(chatRoomMember.joinedAt));
|
|
870
1128
|
const summaries = await this.directory.lookupPlayers(members.map((m) => m.userId));
|
|
871
1129
|
const usernameByUserId = new Map(summaries.map((s) => [s.userId, s.username]));
|
|
872
|
-
return members.map((m) => serializeRow({ ...m, username: usernameByUserId.get(m.userId) ?? null }, { dateFields: ['joinedAt'] }));
|
|
1130
|
+
return members.map((m) => serializeRow({ ...m, username: usernameByUserId.get(m.userId) ?? m.username ?? null }, { dateFields: ['joinedAt'] }));
|
|
1131
|
+
}
|
|
1132
|
+
async canSeeAdminUsers(viewerId) {
|
|
1133
|
+
const [viewer] = await this.drizzle.db
|
|
1134
|
+
.select({ role: user.role })
|
|
1135
|
+
.from(user)
|
|
1136
|
+
.where(eq(user.id, viewerId))
|
|
1137
|
+
.limit(1);
|
|
1138
|
+
return viewer?.role === 'admin' || viewer?.role === 'super-admin';
|
|
873
1139
|
}
|
|
874
1140
|
}
|
|
875
1141
|
//# sourceMappingURL=chat.service.js.map
|