@openora/core 0.4.1-canary.60 → 0.4.1-canary.63
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 +2 -0
- package/dist/admin-console/plugin.d.ts.map +1 -1
- package/dist/analytics/plugin.d.ts +2 -0
- package/dist/analytics/plugin.d.ts.map +1 -1
- package/dist/audit/plugin.d.ts +2 -0
- package/dist/audit/plugin.d.ts.map +1 -1
- package/dist/audit/plugin.js +13 -0
- package/dist/audit/plugin.js.map +1 -1
- package/dist/casino/gaming/plugin.d.ts +2 -0
- package/dist/casino/gaming/plugin.d.ts.map +1 -1
- package/dist/casino/lobby/plugin.d.ts +2 -0
- package/dist/casino/lobby/plugin.d.ts.map +1 -1
- package/dist/cms/plugin.d.ts +2 -0
- package/dist/cms/plugin.d.ts.map +1 -1
- package/dist/compliance/plugin.d.ts +2 -0
- package/dist/compliance/plugin.d.ts.map +1 -1
- package/dist/contracts/adapters/audit.d.ts +1 -1
- package/dist/contracts/adapters/audit.d.ts.map +1 -1
- package/dist/contracts/adapters/audit.js.map +1 -1
- package/dist/contracts/adapters/chat-system-writer.d.ts +7 -1
- package/dist/contracts/adapters/chat-system-writer.d.ts.map +1 -1
- package/dist/contracts/adapters/chat-system-writer.js.map +1 -1
- package/dist/contracts/adapters/gift-commands.d.ts +58 -0
- package/dist/contracts/adapters/gift-commands.d.ts.map +1 -0
- package/dist/contracts/adapters/gift-commands.js +11 -0
- package/dist/contracts/adapters/gift-commands.js.map +1 -0
- package/dist/contracts/adapters/index.d.ts +5 -1
- package/dist/contracts/adapters/index.d.ts.map +1 -1
- package/dist/contracts/adapters/index.js +2 -0
- package/dist/contracts/adapters/index.js.map +1 -1
- package/dist/contracts/adapters/rain-commands.d.ts +27 -0
- package/dist/contracts/adapters/rain-commands.d.ts.map +1 -0
- package/dist/contracts/adapters/rain-commands.js +12 -0
- package/dist/contracts/adapters/rain-commands.js.map +1 -0
- package/dist/contracts/schemas/chat-command-metadata.d.ts +26 -0
- package/dist/contracts/schemas/chat-command-metadata.d.ts.map +1 -1
- package/dist/contracts/schemas/chat-command-metadata.js +25 -0
- package/dist/contracts/schemas/chat-command-metadata.js.map +1 -1
- package/dist/contracts/schemas/chat-command.d.ts +126 -0
- package/dist/contracts/schemas/chat-command.d.ts.map +1 -1
- package/dist/contracts/schemas/chat-command.js +19 -1
- package/dist/contracts/schemas/chat-command.js.map +1 -1
- package/dist/contracts/schemas/events.d.ts +15 -3
- package/dist/contracts/schemas/events.d.ts.map +1 -1
- package/dist/contracts/schemas/events.js +17 -6
- package/dist/contracts/schemas/events.js.map +1 -1
- package/dist/contracts/schemas/identity.d.ts +2 -1
- package/dist/contracts/schemas/identity.d.ts.map +1 -1
- package/dist/contracts/schemas/identity.js +5 -1
- package/dist/contracts/schemas/identity.js.map +1 -1
- package/dist/engagement/chat/__tests__/chat.service.int.test.js +211 -8
- package/dist/engagement/chat/__tests__/chat.service.int.test.js.map +1 -1
- package/dist/engagement/chat/contract/index.d.ts +719 -40
- package/dist/engagement/chat/contract/index.d.ts.map +1 -1
- package/dist/engagement/chat/contract/index.js +51 -5
- package/dist/engagement/chat/contract/index.js.map +1 -1
- package/dist/engagement/chat/drizzle/migrations/0004_panoramic_robin_chapel.sql +6 -0
- package/dist/engagement/chat/drizzle/migrations/meta/0004_snapshot.json +666 -0
- package/dist/engagement/chat/drizzle/migrations/meta/_journal.json +7 -0
- package/dist/engagement/chat/plugin.d.ts +2 -0
- package/dist/engagement/chat/plugin.d.ts.map +1 -1
- package/dist/engagement/chat/router/index.d.ts +608 -34
- package/dist/engagement/chat/router/index.d.ts.map +1 -1
- package/dist/engagement/chat/router/index.js +10 -2
- package/dist/engagement/chat/router/index.js.map +1 -1
- package/dist/engagement/chat/schema/index.d.ts +72 -0
- package/dist/engagement/chat/schema/index.d.ts.map +1 -1
- package/dist/engagement/chat/schema/index.js +15 -3
- package/dist/engagement/chat/schema/index.js.map +1 -1
- package/dist/engagement/chat/service/chat.service.d.ts +421 -34
- package/dist/engagement/chat/service/chat.service.d.ts.map +1 -1
- package/dist/engagement/chat/service/chat.service.js +155 -40
- package/dist/engagement/chat/service/chat.service.js.map +1 -1
- package/dist/engagement/chat-commands/contract/index.d.ts +181 -73
- package/dist/engagement/chat-commands/contract/index.d.ts.map +1 -1
- package/dist/engagement/chat-commands/contract/index.js +50 -72
- package/dist/engagement/chat-commands/contract/index.js.map +1 -1
- package/dist/engagement/chat-commands/plugin.d.ts +3 -1
- package/dist/engagement/chat-commands/plugin.d.ts.map +1 -1
- package/dist/engagement/chat-commands/plugin.js +4 -4
- package/dist/engagement/chat-commands/plugin.js.map +1 -1
- package/dist/engagement/chat-commands/router/index.d.ts +185 -83
- package/dist/engagement/chat-commands/router/index.d.ts.map +1 -1
- package/dist/engagement/chat-commands/router/index.js +35 -25
- package/dist/engagement/chat-commands/router/index.js.map +1 -1
- package/dist/engagement/chat-commands/seed/index.d.ts.map +1 -1
- package/dist/engagement/chat-commands/seed/index.js +12 -0
- package/dist/engagement/chat-commands/seed/index.js.map +1 -1
- package/dist/engagement/chat-commands/service/__tests__/chat-commands.service.test.js +205 -733
- 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 +44 -68
- package/dist/engagement/chat-commands/service/chat-commands.service.d.ts.map +1 -1
- package/dist/engagement/chat-commands/service/chat-commands.service.js +143 -571
- package/dist/engagement/chat-commands/service/chat-commands.service.js.map +1 -1
- package/dist/engagement/contracts.d.ts +1 -0
- package/dist/engagement/contracts.d.ts.map +1 -1
- package/dist/engagement/contracts.js +1 -0
- package/dist/engagement/contracts.js.map +1 -1
- package/dist/engagement/index.d.ts +1 -0
- package/dist/engagement/index.d.ts.map +1 -1
- package/dist/engagement/index.js +1 -0
- package/dist/engagement/index.js.map +1 -1
- package/dist/engagement/notifications/plugin.d.ts +2 -0
- package/dist/engagement/notifications/plugin.d.ts.map +1 -1
- package/dist/engagement/server.d.ts +1 -0
- package/dist/engagement/server.d.ts.map +1 -1
- package/dist/engagement/server.js +1 -0
- package/dist/engagement/server.js.map +1 -1
- package/dist/engagement/social-transfers/contract/index.d.ts +85 -0
- package/dist/engagement/social-transfers/contract/index.d.ts.map +1 -0
- package/dist/engagement/social-transfers/contract/index.js +21 -0
- package/dist/engagement/social-transfers/contract/index.js.map +1 -0
- package/dist/engagement/social-transfers/drizzle/migrations/0000_clammy_rattler.sql +46 -0
- package/dist/engagement/social-transfers/drizzle/migrations/meta/0000_snapshot.json +287 -0
- package/dist/engagement/social-transfers/drizzle/migrations/meta/_journal.json +13 -0
- package/dist/engagement/social-transfers/drizzle.config.d.ts +4 -0
- package/dist/engagement/social-transfers/drizzle.config.d.ts.map +1 -0
- package/dist/engagement/social-transfers/drizzle.config.js +21 -0
- package/dist/engagement/social-transfers/drizzle.config.js.map +1 -0
- package/dist/engagement/social-transfers/index.d.ts +3 -0
- package/dist/engagement/social-transfers/index.d.ts.map +1 -0
- package/dist/engagement/social-transfers/index.js +6 -0
- package/dist/engagement/social-transfers/index.js.map +1 -0
- package/dist/engagement/social-transfers/migrate.d.ts +5 -0
- package/dist/engagement/social-transfers/migrate.d.ts.map +1 -0
- package/dist/engagement/social-transfers/migrate.js +17 -0
- package/dist/engagement/social-transfers/migrate.js.map +1 -0
- package/dist/engagement/social-transfers/plugin.d.ts +111 -0
- package/dist/engagement/social-transfers/plugin.d.ts.map +1 -0
- package/dist/engagement/social-transfers/plugin.js +24 -0
- package/dist/engagement/social-transfers/plugin.js.map +1 -0
- package/dist/engagement/social-transfers/router/index.d.ts +77 -0
- package/dist/engagement/social-transfers/router/index.d.ts.map +1 -0
- package/dist/engagement/social-transfers/router/index.js +25 -0
- package/dist/engagement/social-transfers/router/index.js.map +1 -0
- package/dist/engagement/social-transfers/schema/index.d.ts +609 -0
- package/dist/engagement/social-transfers/schema/index.d.ts.map +1 -0
- package/dist/engagement/social-transfers/schema/index.js +61 -0
- package/dist/engagement/social-transfers/schema/index.js.map +1 -0
- package/dist/engagement/social-transfers/service/__tests__/social-transfers.service.test.d.ts +2 -0
- package/dist/engagement/social-transfers/service/__tests__/social-transfers.service.test.d.ts.map +1 -0
- package/dist/engagement/social-transfers/service/__tests__/social-transfers.service.test.js +757 -0
- package/dist/engagement/social-transfers/service/__tests__/social-transfers.service.test.js.map +1 -0
- package/dist/engagement/social-transfers/service/social-transfers.service.d.ts +75 -0
- package/dist/engagement/social-transfers/service/social-transfers.service.d.ts.map +1 -0
- package/dist/engagement/social-transfers/service/social-transfers.service.js +789 -0
- package/dist/engagement/social-transfers/service/social-transfers.service.js.map +1 -0
- package/dist/iam/plugin.d.ts +2 -0
- package/dist/iam/plugin.d.ts.map +1 -1
- package/dist/iam/service/iam.service.d.ts +1 -1
- package/dist/pam/identity/__tests__/identity.service.int.test.js +47 -2
- package/dist/pam/identity/__tests__/identity.service.int.test.js.map +1 -1
- package/dist/pam/identity/__tests__/phone-login.service.int.test.js +23 -2
- package/dist/pam/identity/__tests__/phone-login.service.int.test.js.map +1 -1
- package/dist/pam/identity/plugin.d.ts +2 -0
- package/dist/pam/identity/plugin.d.ts.map +1 -1
- package/dist/pam/identity/service/identity.service.d.ts.map +1 -1
- package/dist/pam/identity/service/identity.service.js +29 -1
- package/dist/pam/identity/service/identity.service.js.map +1 -1
- package/dist/pam/identity/service/phone-login.service.d.ts.map +1 -1
- package/dist/pam/identity/service/phone-login.service.js +23 -0
- package/dist/pam/identity/service/phone-login.service.js.map +1 -1
- package/dist/pam/player-management/__tests__/player.router.int.test.js +27 -4
- package/dist/pam/player-management/__tests__/player.router.int.test.js.map +1 -1
- package/dist/pam/player-management/__tests__/player.service.int.test.js +159 -6
- package/dist/pam/player-management/__tests__/player.service.int.test.js.map +1 -1
- package/dist/pam/player-management/contract/index.d.ts +39 -0
- package/dist/pam/player-management/contract/index.d.ts.map +1 -1
- package/dist/pam/player-management/contract/index.js +33 -1
- package/dist/pam/player-management/contract/index.js.map +1 -1
- package/dist/pam/player-management/plugin.d.ts +3 -1
- package/dist/pam/player-management/plugin.d.ts.map +1 -1
- package/dist/pam/player-management/plugin.js +3 -3
- package/dist/pam/player-management/plugin.js.map +1 -1
- package/dist/pam/player-management/router/index.d.ts +21 -0
- package/dist/pam/player-management/router/index.d.ts.map +1 -1
- package/dist/pam/player-management/router/index.js +12 -2
- package/dist/pam/player-management/router/index.js.map +1 -1
- package/dist/pam/player-management/service/player.service.d.ts +10 -3
- package/dist/pam/player-management/service/player.service.d.ts.map +1 -1
- package/dist/pam/player-management/service/player.service.js +66 -3
- package/dist/pam/player-management/service/player.service.js.map +1 -1
- package/dist/pam/player-note/__tests__/player-note.router.int.test.d.ts +2 -0
- package/dist/pam/player-note/__tests__/player-note.router.int.test.d.ts.map +1 -0
- package/dist/pam/player-note/__tests__/player-note.router.int.test.js +52 -0
- package/dist/pam/player-note/__tests__/player-note.router.int.test.js.map +1 -0
- package/dist/pam/player-note/plugin.d.ts +49 -46
- package/dist/pam/player-note/plugin.d.ts.map +1 -1
- package/dist/pam/player-note/plugin.js +3 -1
- package/dist/pam/player-note/plugin.js.map +1 -1
- package/dist/pam/player-note/router/index.d.ts +2 -1
- package/dist/pam/player-note/router/index.d.ts.map +1 -1
- package/dist/pam/player-note/router/index.js +13 -2
- package/dist/pam/player-note/router/index.js.map +1 -1
- package/dist/pam/profile/plugin.d.ts +2 -0
- package/dist/pam/profile/plugin.d.ts.map +1 -1
- package/dist/pam/tag/plugin.d.ts +2 -0
- package/dist/pam/tag/plugin.d.ts.map +1 -1
- package/dist/server/auth/permissions.d.ts +24 -0
- package/dist/server/auth/permissions.d.ts.map +1 -1
- package/dist/server/auth/permissions.js +4 -0
- package/dist/server/auth/permissions.js.map +1 -1
- package/dist/server/runtime/core-token-catalog.d.ts +2 -0
- package/dist/server/runtime/core-token-catalog.d.ts.map +1 -1
- package/dist/server/runtime/core-token-catalog.js +3 -1
- package/dist/server/runtime/core-token-catalog.js.map +1 -1
- package/dist/wallet/__tests__/wallet.service.int.test.js +7 -0
- package/dist/wallet/__tests__/wallet.service.int.test.js.map +1 -1
- package/dist/wallet/plugin.d.ts +2 -0
- package/dist/wallet/plugin.d.ts.map +1 -1
- package/dist/wallet/router/index.js +1 -1
- package/dist/wallet/router/index.js.map +1 -1
- package/dist/wallet/service/wallet.service.d.ts.map +1 -1
- package/dist/wallet/service/wallet.service.js +16 -0
- package/dist/wallet/service/wallet.service.js.map +1 -1
- package/package.json +21 -1
|
@@ -1,61 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { makeNotFoundError, makeConflictError, mapConcurrent, moneyToNumber, findOneOrThrow, serializeRow, createDomainError, } from '@openora/core/server';
|
|
1
|
+
import { eq, asc, desc, count } from 'drizzle-orm';
|
|
2
|
+
import { makeNotFoundError, makeConflictError, createDomainError, findOneOrThrow, pageToOffset, } from '@openora/core/server';
|
|
4
3
|
import { chatChannel } from '@openora/core/contracts';
|
|
5
4
|
import { ChatCommandTypeSchema } from '../contract/index.js';
|
|
6
|
-
import { chatCommandConfig
|
|
5
|
+
import { chatCommandConfig } from '../schema/index.js';
|
|
7
6
|
export const CommandDisabledError = makeNotFoundError('ChatCommand');
|
|
8
|
-
export const
|
|
7
|
+
export const ChatPlayerNotFoundError = makeNotFoundError('ChatPlayer');
|
|
9
8
|
export const InsufficientBalanceError = makeConflictError('InsufficientBalance', 'Not enough balance');
|
|
10
9
|
export const ExceedsLimitError = makeConflictError('ExceedsLimit', 'Amount exceeds the command limit');
|
|
11
10
|
export const BelowMinimumError = makeConflictError('BelowMinimum', 'Amount is below the minimum for this command');
|
|
11
|
+
export const NoOnlineUsersError = makeConflictError('NoOnlineUsers', 'No other users are online in this room');
|
|
12
|
+
export const TooManyRecipientsError = makeConflictError('TooManyRecipients', 'Amount too small: you need at least $1 per recipient');
|
|
12
13
|
export const RainCreditError = makeConflictError('RainCreditError', 'A recipient wallet is unavailable; rain aborted');
|
|
13
|
-
export const ChatPlayerNotFoundError = makeNotFoundError('ChatPlayer');
|
|
14
14
|
export const GiftNotFoundError = makeNotFoundError('ChatGift');
|
|
15
15
|
export const GiftAlreadyClaimedError = makeConflictError('GiftAlreadyClaimed', 'This gift has already been claimed');
|
|
16
16
|
export const GiftSelfClaimError = makeConflictError('GiftSelfClaim', 'You cannot claim your own gift');
|
|
17
|
-
export const
|
|
18
|
-
export const SelfModerationActionError = makeConflictError('SelfModerationAction', 'You cannot block or ignore yourself');
|
|
19
|
-
export const TooManyRecipientsError = makeConflictError('TooManyRecipients', 'Amount too small: you need at least $1 per recipient');
|
|
17
|
+
export const GiftCreditError = makeConflictError('GiftCreditError', 'Recipient wallet is unavailable; gift claim aborted');
|
|
20
18
|
export const ChatCommandIdempotencyKeyReuseError = makeConflictError('ChatCommandIdempotencyKeyReuse', 'This idempotency key was already used with different request parameters');
|
|
21
19
|
export const ConcurrentCommandReplayError = makeConflictError('ConcurrentCommandReplay', 'This request is already being processed - please retry');
|
|
22
|
-
export const ChatRoomNotMemberError = createDomainError('ChatRoomNotMemberError', (roomId) => `You are not a member of room: ${roomId}`);
|
|
23
|
-
const COMMAND_IDEMPOTENCY_TTL_MS = 5 * 60 * 1000;
|
|
24
|
-
// The replay guard must match on the COMPLETE request, not just the amount - a reused
|
|
25
|
-
// key with a different room, recipient count, or donate target is a distinct request,
|
|
26
|
-
// not a replay of the original. `idempotencyKey` itself is excluded so the fingerprint
|
|
27
|
-
// is stable for the row it guards.
|
|
28
|
-
export function fingerprintCommand(input) {
|
|
29
|
-
const canonical = input.type === 'gift'
|
|
30
|
-
? { type: input.type, amount: input.amount, roomId: input.roomId }
|
|
31
|
-
: input.type === 'rain'
|
|
32
|
-
? {
|
|
33
|
-
type: input.type,
|
|
34
|
-
amount: input.amount,
|
|
35
|
-
recipientCount: input.recipientCount,
|
|
36
|
-
roomId: input.roomId,
|
|
37
|
-
}
|
|
38
|
-
: {
|
|
39
|
-
type: input.type,
|
|
40
|
-
amount: input.amount,
|
|
41
|
-
targetUsername: input.targetUsername,
|
|
42
|
-
roomId: input.roomId,
|
|
43
|
-
};
|
|
44
|
-
return createHash('sha256').update(JSON.stringify(canonical)).digest('hex');
|
|
45
|
-
}
|
|
46
|
-
function shuffleArray(arr) {
|
|
47
|
-
const result = [...arr];
|
|
48
|
-
for (let i = result.length - 1; i > 0; i--) {
|
|
49
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
50
|
-
const a = result.at(i);
|
|
51
|
-
const b = result.at(j);
|
|
52
|
-
if (a !== undefined && b !== undefined) {
|
|
53
|
-
result[i] = b;
|
|
54
|
-
result[j] = a;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return result;
|
|
58
|
-
}
|
|
20
|
+
export const ChatRoomNotMemberError = createDomainError('ChatRoomNotMemberError', (roomId) => roomId ? `You are not a member of room: ${roomId}` : 'You are not a member of global chat');
|
|
59
21
|
function toDescriptor(row) {
|
|
60
22
|
return {
|
|
61
23
|
key: ChatCommandTypeSchema.parse(row.key),
|
|
@@ -63,34 +25,29 @@ function toDescriptor(row) {
|
|
|
63
25
|
label: row.label,
|
|
64
26
|
description: row.description ?? null,
|
|
65
27
|
config: row.config ?? null,
|
|
28
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
66
29
|
};
|
|
67
30
|
}
|
|
31
|
+
const ADMIN_COMMAND_SORT_COLUMNS = {
|
|
32
|
+
key: chatCommandConfig.key,
|
|
33
|
+
updatedAt: chatCommandConfig.updatedAt,
|
|
34
|
+
};
|
|
68
35
|
export class ChatCommandsService {
|
|
69
36
|
drizzle;
|
|
70
|
-
systemWriter;
|
|
71
|
-
wallet;
|
|
72
37
|
directory;
|
|
73
|
-
audit;
|
|
74
|
-
transport;
|
|
75
|
-
events;
|
|
76
38
|
blockWriter;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
constructor(drizzle,
|
|
39
|
+
giftCommands;
|
|
40
|
+
rainCommands;
|
|
41
|
+
transport;
|
|
42
|
+
audit;
|
|
43
|
+
constructor(drizzle, directory, blockWriter, giftCommands, rainCommands, transport, audit) {
|
|
82
44
|
this.drizzle = drizzle;
|
|
83
|
-
this.systemWriter = systemWriter;
|
|
84
|
-
this.wallet = wallet;
|
|
85
45
|
this.directory = directory;
|
|
86
|
-
this.audit = audit;
|
|
87
|
-
this.transport = transport;
|
|
88
|
-
this.events = events;
|
|
89
46
|
this.blockWriter = blockWriter;
|
|
90
|
-
this.
|
|
91
|
-
this.
|
|
92
|
-
this.
|
|
93
|
-
this.
|
|
47
|
+
this.giftCommands = giftCommands;
|
|
48
|
+
this.rainCommands = rainCommands;
|
|
49
|
+
this.transport = transport;
|
|
50
|
+
this.audit = audit;
|
|
94
51
|
}
|
|
95
52
|
async listCommands(includeDisabled = false) {
|
|
96
53
|
const rows = await this.drizzle.db
|
|
@@ -99,512 +56,19 @@ export class ChatCommandsService {
|
|
|
99
56
|
.where(includeDisabled ? undefined : eq(chatCommandConfig.enabled, true));
|
|
100
57
|
return rows.map(toDescriptor);
|
|
101
58
|
}
|
|
102
|
-
async
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
async searchPlayers(q, limit, viewerId) {
|
|
116
|
-
const ids = await this.directory.findPlayerIds(q, limit);
|
|
117
|
-
if (ids.length === 0) {
|
|
118
|
-
return [];
|
|
119
|
-
}
|
|
120
|
-
const excluded = new Set(await this.blockWriter.getExcludedUserIds(viewerId));
|
|
121
|
-
const filteredIds = ids.filter((id) => !excluded.has(id));
|
|
122
|
-
if (filteredIds.length === 0) {
|
|
123
|
-
return [];
|
|
124
|
-
}
|
|
125
|
-
const summaries = await this.directory.lookupPlayers(filteredIds);
|
|
126
|
-
return summaries.map((s) => ({
|
|
127
|
-
userId: s.userId,
|
|
128
|
-
username: s.username,
|
|
129
|
-
avatarUrl: s.avatarUrl,
|
|
130
|
-
level: s.level,
|
|
131
|
-
}));
|
|
132
|
-
}
|
|
133
|
-
async getPlayerProfile(userId, viewerId) {
|
|
134
|
-
const summaries = await this.directory.lookupPlayers([userId]);
|
|
135
|
-
const summary = summaries.find((s) => s.userId === userId);
|
|
136
|
-
if (!summary) {
|
|
137
|
-
throw new ChatPlayerNotFoundError(userId);
|
|
138
|
-
}
|
|
139
|
-
const isSelf = userId === viewerId;
|
|
140
|
-
const stats = isSelf ? await this.gameReporting.getPlayerStats(userId) : null;
|
|
141
|
-
return {
|
|
142
|
-
userId: summary.userId,
|
|
143
|
-
username: summary.username,
|
|
144
|
-
avatarUrl: summary.avatarUrl,
|
|
145
|
-
level: summary.level,
|
|
146
|
-
joinedAt: isSelf ? summary.createdAt.toISOString() : null,
|
|
147
|
-
totalWagered: stats?.totalWagered ?? null,
|
|
148
|
-
totalBets: stats?.totalBets ?? null,
|
|
149
|
-
currency: isSelf ? summary.currency : null,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
async executeCommand(input, actorId) {
|
|
153
|
-
if (input.roomId) {
|
|
154
|
-
try {
|
|
155
|
-
await this.roomAccess.verifyRoomAccess(input.roomId, actorId);
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
if (error instanceof Error && error.name === 'ChatRoomNotMemberError') {
|
|
159
|
-
throw new ChatRoomNotMemberError(input.roomId);
|
|
160
|
-
}
|
|
161
|
-
throw error;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
const [row] = await this.drizzle.db
|
|
165
|
-
.select()
|
|
166
|
-
.from(chatCommandConfig)
|
|
167
|
-
.where(eq(chatCommandConfig.key, input.type))
|
|
168
|
-
.limit(1);
|
|
169
|
-
if (!row || !row.enabled) {
|
|
170
|
-
throw new CommandDisabledError(input.type);
|
|
171
|
-
}
|
|
172
|
-
if (input.type === 'gift') {
|
|
173
|
-
return this.handleGift(input, actorId, row.config ?? null);
|
|
174
|
-
}
|
|
175
|
-
if (input.type === 'donate') {
|
|
176
|
-
return this.handleDonate(input, actorId, row.config ?? null);
|
|
177
|
-
}
|
|
178
|
-
if (input.type === 'block' || input.type === 'ignore') {
|
|
179
|
-
return this.handleBlockAction(input, actorId);
|
|
180
|
-
}
|
|
181
|
-
if (input.type === 'rain') {
|
|
182
|
-
return this.handleRain(input, actorId, row.config ?? null);
|
|
183
|
-
}
|
|
184
|
-
// exhaustive — TypeScript cannot narrow a union-literal discriminant ('block'|'ignore') away
|
|
185
|
-
throw new CommandDisabledError(input.type);
|
|
186
|
-
}
|
|
187
|
-
async findCommandReplay(commandType, actorId, idempotencyKey, fingerprint) {
|
|
188
|
-
const record = await this.cache.get(this.idempotencyCacheKey(commandType, actorId, idempotencyKey));
|
|
189
|
-
if (!record) {
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
if (record.fingerprint !== fingerprint) {
|
|
193
|
-
throw new ChatCommandIdempotencyKeyReuseError();
|
|
194
|
-
}
|
|
195
|
-
if (!record.result) {
|
|
196
|
-
throw new ConcurrentCommandReplayError();
|
|
197
|
-
}
|
|
198
|
-
return record.result;
|
|
199
|
-
}
|
|
200
|
-
idempotencyCacheKey(commandType, actorId, idempotencyKey) {
|
|
201
|
-
return `chat-command:idempotency:${actorId}:${commandType}:${idempotencyKey}`;
|
|
202
|
-
}
|
|
203
|
-
async reserveCommandIdempotency(commandType, actorId, idempotencyKey, fingerprint) {
|
|
204
|
-
const reserved = await this.cache.setIfAbsent(this.idempotencyCacheKey(commandType, actorId, idempotencyKey), { fingerprint, result: null }, { ttlMs: this.idempotencyTtlMs });
|
|
205
|
-
if (!reserved) {
|
|
206
|
-
throw new ConcurrentCommandReplayError();
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
async completeCommandIdempotency(commandType, actorId, idempotencyKey, fingerprint, result) {
|
|
210
|
-
await this.cache.set(this.idempotencyCacheKey(commandType, actorId, idempotencyKey), { fingerprint, result }, { ttlMs: this.idempotencyTtlMs });
|
|
211
|
-
}
|
|
212
|
-
async runWithCommandReservation(commandType, actorId, idempotencyKey, work) {
|
|
213
|
-
try {
|
|
214
|
-
return await work();
|
|
215
|
-
}
|
|
216
|
-
catch (error) {
|
|
217
|
-
await this.cache.delete(this.idempotencyCacheKey(commandType, actorId, idempotencyKey));
|
|
218
|
-
throw error;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
// Exact, case-insensitive username resolution for callers holding a complete, already-known
|
|
222
|
-
// username (never a partial search term) - `/donate`, `/block`, `/ignore`. Distinct from
|
|
223
|
-
// findPlayerIds' capped fuzzy substring search, which can silently drop the real match once
|
|
224
|
-
// more than 20 unrelated accounts substring-collide with a short/common username.
|
|
225
|
-
async resolveExactPlayer(username) {
|
|
226
|
-
const summary = await this.directory.getPlayerByUsername(username);
|
|
227
|
-
if (!summary) {
|
|
228
|
-
throw new ChatPlayerNotFoundError(username);
|
|
229
|
-
}
|
|
230
|
-
return summary;
|
|
231
|
-
}
|
|
232
|
-
async handleGift(input, actorId, config) {
|
|
233
|
-
if (config?.maxAmount !== undefined &&
|
|
234
|
-
moneyToNumber(input.amount) > moneyToNumber(config.maxAmount)) {
|
|
235
|
-
throw new ExceedsLimitError();
|
|
236
|
-
}
|
|
237
|
-
if (config?.minAmount !== undefined &&
|
|
238
|
-
moneyToNumber(input.amount) < moneyToNumber(config.minAmount)) {
|
|
239
|
-
throw new BelowMinimumError();
|
|
240
|
-
}
|
|
241
|
-
const fingerprint = fingerprintCommand(input);
|
|
242
|
-
const replay = await this.findCommandReplay('gift', actorId, input.idempotencyKey, fingerprint);
|
|
243
|
-
if (replay) {
|
|
244
|
-
return replay;
|
|
245
|
-
}
|
|
246
|
-
await this.reserveCommandIdempotency('gift', actorId, input.idempotencyKey, fingerprint);
|
|
247
|
-
// Resolve sender's username for the gift card metadata.
|
|
248
|
-
const senderSummaries = await this.directory.lookupPlayers([actorId]);
|
|
249
|
-
const senderSummary = senderSummaries.find((s) => s.userId === actorId);
|
|
250
|
-
if (!senderSummary) {
|
|
251
|
-
throw new ChatPlayerNotFoundError(actorId);
|
|
252
|
-
}
|
|
253
|
-
const senderUsername = senderSummary.username;
|
|
254
|
-
const { msg, giftId, currency } = await this.runWithCommandReservation('gift', actorId, input.idempotencyKey, () => this.drizzle.db.transaction(async (tx) => {
|
|
255
|
-
const debit = await this.wallet.debit(tx, {
|
|
256
|
-
userId: actorId,
|
|
257
|
-
amount: input.amount,
|
|
258
|
-
type: 'gift',
|
|
259
|
-
});
|
|
260
|
-
if (!debit.ok) {
|
|
261
|
-
throw new InsufficientBalanceError();
|
|
262
|
-
}
|
|
263
|
-
const [giftRow] = await tx
|
|
264
|
-
.insert(chatGift)
|
|
265
|
-
.values({
|
|
266
|
-
senderId: actorId,
|
|
267
|
-
senderUsername,
|
|
268
|
-
amount: input.amount,
|
|
269
|
-
currency: debit.currency,
|
|
270
|
-
roomId: input.roomId,
|
|
271
|
-
// messageId will be updated after postSystemMessage; we use a placeholder UUID
|
|
272
|
-
// that is immediately overwritten by the UPDATE below in the same transaction.
|
|
273
|
-
// Pattern: insert with a temporary value, then set the real foreign reference.
|
|
274
|
-
messageId: '00000000-0000-0000-0000-000000000000',
|
|
275
|
-
})
|
|
276
|
-
.returning();
|
|
277
|
-
if (!giftRow) {
|
|
278
|
-
throw new InsufficientBalanceError();
|
|
279
|
-
}
|
|
280
|
-
const systemMsg = await this.systemWriter.postSystemMessage({
|
|
281
|
-
roomId: input.roomId,
|
|
282
|
-
actorId,
|
|
283
|
-
tx,
|
|
284
|
-
metadata: {
|
|
285
|
-
command: 'gift',
|
|
286
|
-
giftId: giftRow.id,
|
|
287
|
-
senderId: actorId,
|
|
288
|
-
senderUsername,
|
|
289
|
-
amount: input.amount,
|
|
290
|
-
currency: debit.currency,
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
// Back-fill the real message id now that we have it.
|
|
294
|
-
await tx
|
|
295
|
-
.update(chatGift)
|
|
296
|
-
.set({ messageId: systemMsg.id })
|
|
297
|
-
.where(eq(chatGift.id, giftRow.id));
|
|
298
|
-
await this.audit.recordInTransaction(tx, {
|
|
299
|
-
actorId,
|
|
300
|
-
actorType: 'player',
|
|
301
|
-
action: 'chat.gift',
|
|
302
|
-
resourceType: 'chat_gift',
|
|
303
|
-
resourceId: giftRow.id,
|
|
304
|
-
before: null,
|
|
305
|
-
after: { amount: input.amount, roomId: input.roomId },
|
|
306
|
-
});
|
|
307
|
-
return { msg: systemMsg, giftId: giftRow.id, currency: debit.currency };
|
|
308
|
-
}));
|
|
309
|
-
await this.completeCommandIdempotency('gift', actorId, input.idempotencyKey, fingerprint, msg);
|
|
310
|
-
// The caller now owns the commit boundary: postSystemMessage was passed `tx` above so
|
|
311
|
-
// it did not auto-publish - publish only now that this transaction has committed.
|
|
312
|
-
void this.transport.publish(chatChannel(input.roomId), msg);
|
|
313
|
-
void this.events.emit('chat.gift.sent', {
|
|
314
|
-
giftId,
|
|
315
|
-
senderId: actorId,
|
|
316
|
-
senderUsername,
|
|
317
|
-
amount: input.amount,
|
|
318
|
-
currency,
|
|
319
|
-
roomId: input.roomId,
|
|
320
|
-
messageId: msg.id,
|
|
321
|
-
});
|
|
322
|
-
return msg;
|
|
323
|
-
}
|
|
324
|
-
async getGift(giftId, viewerId) {
|
|
325
|
-
const rows = await this.drizzle.db
|
|
326
|
-
.select()
|
|
327
|
-
.from(chatGift)
|
|
328
|
-
.where(eq(chatGift.id, giftId))
|
|
329
|
-
.limit(1);
|
|
330
|
-
const row = findOneOrThrow(rows, new GiftNotFoundError(giftId));
|
|
331
|
-
await this.roomAccess.verifyRoomAccess(row.roomId, viewerId);
|
|
332
|
-
const serialized = serializeRow(row, {
|
|
333
|
-
dateFields: ['claimedAt', 'createdAt'],
|
|
334
|
-
decimalFields: ['amount'],
|
|
335
|
-
});
|
|
336
|
-
return {
|
|
337
|
-
id: serialized.id,
|
|
338
|
-
senderId: serialized.senderId,
|
|
339
|
-
senderUsername: serialized.senderUsername,
|
|
340
|
-
amount: serialized.amount,
|
|
341
|
-
currency: serialized.currency,
|
|
342
|
-
claimedBy: serialized.claimedBy ?? null,
|
|
343
|
-
claimedByUsername: serialized.claimedByUsername ?? null,
|
|
344
|
-
claimedAt: serialized.claimedAt ?? null,
|
|
345
|
-
createdAt: serialized.createdAt,
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
async claimGift(giftId, claimerId) {
|
|
349
|
-
// Resolve claimer's username upfront — if the claimer is somehow not in the
|
|
350
|
-
// directory that is a hard server error (defensive guard).
|
|
351
|
-
const claimerSummaries = await this.directory.lookupPlayers([claimerId]);
|
|
352
|
-
const claimerSummary = claimerSummaries.find((s) => s.userId === claimerId);
|
|
353
|
-
if (!claimerSummary) {
|
|
354
|
-
throw new GiftNotFoundError(claimerId);
|
|
355
|
-
}
|
|
356
|
-
const claimerUsername = claimerSummary.username;
|
|
357
|
-
// Fetch gift to check self-claim BEFORE attempting the atomic update.
|
|
358
|
-
const existing = await this.drizzle.db
|
|
359
|
-
.select()
|
|
360
|
-
.from(chatGift)
|
|
361
|
-
.where(eq(chatGift.id, giftId))
|
|
362
|
-
.limit(1);
|
|
363
|
-
const giftRow = findOneOrThrow(existing, new GiftNotFoundError(giftId));
|
|
364
|
-
await this.roomAccess.verifyRoomAccess(giftRow.roomId, claimerId);
|
|
365
|
-
if (giftRow.senderId === claimerId) {
|
|
366
|
-
throw new GiftSelfClaimError();
|
|
367
|
-
}
|
|
368
|
-
const { claimed, currency, roomId } = await this.drizzle.db.transaction(async (tx) => {
|
|
369
|
-
const claimedAt = new Date();
|
|
370
|
-
const results = await tx
|
|
371
|
-
.update(chatGift)
|
|
372
|
-
.set({ claimedBy: claimerId, claimedByUsername: claimerUsername, claimedAt })
|
|
373
|
-
.where(and(eq(chatGift.id, giftId), isNull(chatGift.claimedBy)))
|
|
374
|
-
.returning();
|
|
375
|
-
if (results.length === 0) {
|
|
376
|
-
// Another claimer won the race.
|
|
377
|
-
throw new GiftAlreadyClaimedError();
|
|
378
|
-
}
|
|
379
|
-
const updated = findOneOrThrow(results, new GiftAlreadyClaimedError());
|
|
380
|
-
const credit = await this.wallet.credit(tx, {
|
|
381
|
-
userId: claimerId,
|
|
382
|
-
amount: updated.amount,
|
|
383
|
-
currency: updated.currency,
|
|
384
|
-
type: 'gift',
|
|
385
|
-
});
|
|
386
|
-
if (!credit.ok) {
|
|
387
|
-
throw new GiftNotFoundError(claimerId);
|
|
388
|
-
}
|
|
389
|
-
await this.audit.recordInTransaction(tx, {
|
|
390
|
-
actorId: claimerId,
|
|
391
|
-
actorType: 'player',
|
|
392
|
-
action: 'chat.gift.claimed',
|
|
393
|
-
resourceType: 'chat_gift',
|
|
394
|
-
resourceId: giftId,
|
|
395
|
-
before: null,
|
|
396
|
-
after: { claimedBy: claimerId, amount: updated.amount },
|
|
397
|
-
});
|
|
398
|
-
return { claimed: updated, currency: updated.currency, roomId: updated.roomId };
|
|
399
|
-
});
|
|
400
|
-
void this.transport.publish(chatChannel(roomId), {
|
|
401
|
-
event: 'gift.claimed',
|
|
402
|
-
giftId,
|
|
403
|
-
claimedBy: claimerId,
|
|
404
|
-
claimedByUsername: claimerUsername,
|
|
405
|
-
claimedAt: claimed.claimedAt?.toISOString() ?? new Date().toISOString(),
|
|
406
|
-
});
|
|
407
|
-
void this.events.emit('chat.gift.claimed', {
|
|
408
|
-
giftId,
|
|
409
|
-
claimerId,
|
|
410
|
-
claimerUsername,
|
|
411
|
-
senderId: giftRow.senderId,
|
|
412
|
-
amount: claimed.amount,
|
|
413
|
-
currency,
|
|
414
|
-
roomId,
|
|
415
|
-
});
|
|
416
|
-
return {
|
|
417
|
-
claimedBy: claimerId,
|
|
418
|
-
claimedByUsername: claimerUsername,
|
|
419
|
-
claimedAt: claimed.claimedAt?.toISOString() ?? new Date().toISOString(),
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
async handleRain(input, actorId, config) {
|
|
423
|
-
if (config?.maxAmount !== undefined &&
|
|
424
|
-
moneyToNumber(input.amount) > moneyToNumber(config.maxAmount)) {
|
|
425
|
-
throw new ExceedsLimitError();
|
|
426
|
-
}
|
|
427
|
-
if (config?.minAmount !== undefined &&
|
|
428
|
-
moneyToNumber(input.amount) < moneyToNumber(config.minAmount)) {
|
|
429
|
-
throw new BelowMinimumError();
|
|
430
|
-
}
|
|
431
|
-
const configMax = config?.maxRecipients ?? 50;
|
|
432
|
-
if (input.recipientCount > configMax) {
|
|
433
|
-
throw new ExceedsLimitError();
|
|
434
|
-
}
|
|
435
|
-
const amountUnits = Math.floor(moneyToNumber(input.amount));
|
|
436
|
-
if (input.recipientCount > amountUnits) {
|
|
437
|
-
throw new TooManyRecipientsError();
|
|
438
|
-
}
|
|
439
|
-
const fingerprint = fingerprintCommand(input);
|
|
440
|
-
const replay = await this.findCommandReplay('rain', actorId, input.idempotencyKey, fingerprint);
|
|
441
|
-
if (replay) {
|
|
442
|
-
return replay;
|
|
443
|
-
}
|
|
444
|
-
await this.reserveCommandIdempotency('rain', actorId, input.idempotencyKey, fingerprint);
|
|
445
|
-
const allOnline = await this.transport.getOnlineUserIds(chatChannel(input.roomId));
|
|
446
|
-
const recipients = shuffleArray(allOnline.filter((id) => id !== actorId)).slice(0, input.recipientCount);
|
|
447
|
-
if (recipients.length === 0) {
|
|
448
|
-
throw new NoOnlineUsersError();
|
|
449
|
-
}
|
|
450
|
-
const { msg, currency, totalDistributed } = await this.runWithCommandReservation('rain', actorId, input.idempotencyKey, () => this.drizzle.db.transaction(async (tx) => {
|
|
451
|
-
const splitResult = await tx.execute(sql `SELECT
|
|
452
|
-
(floor(floor(${input.amount}::numeric) / ${recipients.length}))::text AS per_recipient,
|
|
453
|
-
(floor(floor(${input.amount}::numeric) / ${recipients.length}) * ${recipients.length})::text AS total_distributed`);
|
|
454
|
-
const { per_recipient: perRecipient, total_distributed: totalDistributed } = splitResult
|
|
455
|
-
.rows[0];
|
|
456
|
-
const debit = await this.wallet.debit(tx, {
|
|
457
|
-
userId: actorId,
|
|
458
|
-
amount: totalDistributed,
|
|
459
|
-
type: 'rain',
|
|
460
|
-
});
|
|
461
|
-
if (!debit.ok) {
|
|
462
|
-
throw new InsufficientBalanceError();
|
|
463
|
-
}
|
|
464
|
-
const credits = await mapConcurrent(recipients, 10, (userId) => this.wallet.credit(tx, {
|
|
465
|
-
userId,
|
|
466
|
-
amount: perRecipient,
|
|
467
|
-
currency: debit.currency,
|
|
468
|
-
type: 'rain',
|
|
469
|
-
}));
|
|
470
|
-
if (credits.some((c) => !c.ok)) {
|
|
471
|
-
throw new RainCreditError();
|
|
472
|
-
}
|
|
473
|
-
const systemMsg = await this.systemWriter.postSystemMessage({
|
|
474
|
-
roomId: input.roomId,
|
|
475
|
-
actorId,
|
|
476
|
-
tx,
|
|
477
|
-
metadata: {
|
|
478
|
-
command: 'rain',
|
|
479
|
-
fromUserId: actorId,
|
|
480
|
-
amount: totalDistributed,
|
|
481
|
-
currency: debit.currency,
|
|
482
|
-
recipientCount: recipients.length,
|
|
483
|
-
perRecipient,
|
|
484
|
-
},
|
|
485
|
-
});
|
|
486
|
-
await this.audit.recordInTransaction(tx, {
|
|
487
|
-
actorId,
|
|
488
|
-
actorType: 'player',
|
|
489
|
-
action: 'chat.rain',
|
|
490
|
-
resourceType: 'chat_room',
|
|
491
|
-
resourceId: input.roomId,
|
|
492
|
-
before: null,
|
|
493
|
-
after: { amount: totalDistributed, recipientCount: recipients.length },
|
|
494
|
-
});
|
|
495
|
-
return { msg: systemMsg, currency: debit.currency, totalDistributed };
|
|
496
|
-
}));
|
|
497
|
-
await this.completeCommandIdempotency('rain', actorId, input.idempotencyKey, fingerprint, msg);
|
|
498
|
-
void this.transport.publish(chatChannel(input.roomId), msg);
|
|
499
|
-
void this.events.emit('chat.rain.distributed', {
|
|
500
|
-
fromUserId: actorId,
|
|
501
|
-
recipients,
|
|
502
|
-
recipientCount: recipients.length,
|
|
503
|
-
totalAmount: totalDistributed,
|
|
504
|
-
currency,
|
|
505
|
-
roomId: input.roomId,
|
|
506
|
-
});
|
|
507
|
-
return msg;
|
|
508
|
-
}
|
|
509
|
-
async handleDonate(input, actorId, config) {
|
|
510
|
-
if (config?.maxAmount !== undefined &&
|
|
511
|
-
moneyToNumber(input.amount) > moneyToNumber(config.maxAmount)) {
|
|
512
|
-
throw new ExceedsLimitError();
|
|
513
|
-
}
|
|
514
|
-
if (config?.minAmount !== undefined &&
|
|
515
|
-
moneyToNumber(input.amount) < moneyToNumber(config.minAmount)) {
|
|
516
|
-
throw new BelowMinimumError();
|
|
517
|
-
}
|
|
518
|
-
const target = await this.resolveExactPlayer(input.targetUsername);
|
|
519
|
-
if (target.userId === actorId) {
|
|
520
|
-
throw new DonateSelfError();
|
|
521
|
-
}
|
|
522
|
-
const senderSummaries = await this.directory.lookupPlayers([actorId]);
|
|
523
|
-
const sender = senderSummaries.find((s) => s.userId === actorId);
|
|
524
|
-
if (!sender) {
|
|
525
|
-
throw new ChatPlayerNotFoundError(actorId);
|
|
526
|
-
}
|
|
527
|
-
const fingerprint = fingerprintCommand(input);
|
|
528
|
-
const replay = await this.findCommandReplay('donate', actorId, input.idempotencyKey, fingerprint);
|
|
529
|
-
if (replay) {
|
|
530
|
-
return replay;
|
|
531
|
-
}
|
|
532
|
-
await this.reserveCommandIdempotency('donate', actorId, input.idempotencyKey, fingerprint);
|
|
533
|
-
const { msg, currency } = await this.runWithCommandReservation('donate', actorId, input.idempotencyKey, () => this.drizzle.db.transaction(async (tx) => {
|
|
534
|
-
const debit = await this.wallet.debit(tx, {
|
|
535
|
-
userId: actorId,
|
|
536
|
-
amount: input.amount,
|
|
537
|
-
type: 'tip',
|
|
538
|
-
});
|
|
539
|
-
if (!debit.ok) {
|
|
540
|
-
throw new InsufficientBalanceError();
|
|
541
|
-
}
|
|
542
|
-
const credit = await this.wallet.credit(tx, {
|
|
543
|
-
userId: target.userId,
|
|
544
|
-
amount: input.amount,
|
|
545
|
-
currency: debit.currency,
|
|
546
|
-
type: 'tip',
|
|
547
|
-
});
|
|
548
|
-
if (!credit.ok) {
|
|
549
|
-
throw new ChatPlayerNotFoundError(target.userId);
|
|
550
|
-
}
|
|
551
|
-
const systemMsg = await this.systemWriter.postSystemMessage({
|
|
552
|
-
roomId: input.roomId,
|
|
553
|
-
actorId,
|
|
554
|
-
tx,
|
|
555
|
-
metadata: {
|
|
556
|
-
command: 'donate',
|
|
557
|
-
recipientId: target.userId,
|
|
558
|
-
recipientUsername: target.username,
|
|
559
|
-
amount: input.amount,
|
|
560
|
-
currency: debit.currency,
|
|
561
|
-
},
|
|
562
|
-
});
|
|
563
|
-
await this.audit.recordInTransaction(tx, {
|
|
564
|
-
actorId,
|
|
565
|
-
actorType: 'player',
|
|
566
|
-
action: 'chat.donate',
|
|
567
|
-
resourceType: 'chat_donation',
|
|
568
|
-
resourceId: systemMsg.id,
|
|
569
|
-
before: null,
|
|
570
|
-
after: { recipientId: target.userId, amount: input.amount, currency: debit.currency },
|
|
571
|
-
});
|
|
572
|
-
return { msg: systemMsg, currency: debit.currency };
|
|
573
|
-
}));
|
|
574
|
-
await this.completeCommandIdempotency('donate', actorId, input.idempotencyKey, fingerprint, msg);
|
|
575
|
-
void this.transport.publish(chatChannel(input.roomId), msg);
|
|
576
|
-
void this.events.emit('chat.donate.sent', {
|
|
577
|
-
senderId: actorId,
|
|
578
|
-
senderUsername: sender.username,
|
|
579
|
-
recipientId: target.userId,
|
|
580
|
-
recipientUsername: target.username,
|
|
581
|
-
amount: input.amount,
|
|
582
|
-
currency,
|
|
583
|
-
roomId: input.roomId,
|
|
584
|
-
});
|
|
585
|
-
return msg;
|
|
586
|
-
}
|
|
587
|
-
/** Command must be used to update users that chat commands setup has been invalidated and needs to be refetched */
|
|
588
|
-
async handleBlockAction(input, actorId) {
|
|
589
|
-
const summary = await this.resolveExactPlayer(input.targetUsername);
|
|
590
|
-
if (summary.userId === actorId) {
|
|
591
|
-
throw new SelfModerationActionError();
|
|
592
|
-
}
|
|
593
|
-
if (input.type === 'block') {
|
|
594
|
-
await this.blockWriter.blockUser(actorId, summary.userId);
|
|
595
|
-
}
|
|
596
|
-
else {
|
|
597
|
-
await this.blockWriter.ignoreUser(actorId, summary.userId);
|
|
598
|
-
}
|
|
599
|
-
return this.systemWriter.postSystemMessage({
|
|
600
|
-
roomId: input.roomId,
|
|
601
|
-
actorId,
|
|
602
|
-
metadata: {
|
|
603
|
-
command: input.type,
|
|
604
|
-
targetUserId: summary.userId,
|
|
605
|
-
displayName: summary.username,
|
|
606
|
-
},
|
|
607
|
-
});
|
|
59
|
+
async adminListCommands({ page, limit, sortBy, sortOrder, }) {
|
|
60
|
+
const dir = sortOrder === 'asc' ? asc : desc;
|
|
61
|
+
const col = ADMIN_COMMAND_SORT_COLUMNS[sortBy];
|
|
62
|
+
const [rows, [{ n }]] = await Promise.all([
|
|
63
|
+
this.drizzle.db
|
|
64
|
+
.select()
|
|
65
|
+
.from(chatCommandConfig)
|
|
66
|
+
.orderBy(dir(col))
|
|
67
|
+
.limit(limit)
|
|
68
|
+
.offset(pageToOffset(page, limit)),
|
|
69
|
+
this.drizzle.db.select({ n: count() }).from(chatCommandConfig),
|
|
70
|
+
]);
|
|
71
|
+
return { items: rows.map(toDescriptor), total: Number(n), page, limit };
|
|
608
72
|
}
|
|
609
73
|
async adminUpdateCommand(input, actorId) {
|
|
610
74
|
const rows = await this.drizzle.db
|
|
@@ -637,5 +101,113 @@ export class ChatCommandsService {
|
|
|
637
101
|
});
|
|
638
102
|
return toDescriptor(row);
|
|
639
103
|
}
|
|
104
|
+
async searchMentions(q, limit, viewerId) {
|
|
105
|
+
const ids = await this.directory.findPlayerIds(q, limit);
|
|
106
|
+
if (ids.length === 0) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
const excluded = new Set(await this.blockWriter.getExcludedUserIds(viewerId));
|
|
110
|
+
const filteredIds = ids.filter((id) => !excluded.has(id));
|
|
111
|
+
if (filteredIds.length === 0) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
const summaries = await this.directory.lookupPlayers(filteredIds);
|
|
115
|
+
return summaries.map((s) => ({ userId: s.userId, username: s.username }));
|
|
116
|
+
}
|
|
117
|
+
async postGift(input, actorId) {
|
|
118
|
+
const result = await this.giftCommands.sendGift(input, actorId);
|
|
119
|
+
if (result.ok) {
|
|
120
|
+
return result.message;
|
|
121
|
+
}
|
|
122
|
+
switch (result.reason) {
|
|
123
|
+
case 'disabled':
|
|
124
|
+
throw new CommandDisabledError('gift');
|
|
125
|
+
case 'player_not_found':
|
|
126
|
+
throw new ChatPlayerNotFoundError(result.playerId);
|
|
127
|
+
case 'insufficient_balance':
|
|
128
|
+
throw new InsufficientBalanceError();
|
|
129
|
+
case 'exceeds_limit':
|
|
130
|
+
throw new ExceedsLimitError();
|
|
131
|
+
case 'below_minimum':
|
|
132
|
+
throw new BelowMinimumError();
|
|
133
|
+
case 'idempotency_key_reuse':
|
|
134
|
+
throw new ChatCommandIdempotencyKeyReuseError();
|
|
135
|
+
case 'concurrent_replay':
|
|
136
|
+
throw new ConcurrentCommandReplayError();
|
|
137
|
+
case 'room_not_member':
|
|
138
|
+
throw new ChatRoomNotMemberError(input.roomId);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async claimGift(giftId, claimerId) {
|
|
142
|
+
const result = await this.giftCommands.claimGift(giftId, claimerId);
|
|
143
|
+
if (result.ok) {
|
|
144
|
+
return {
|
|
145
|
+
claimedBy: result.claimedBy,
|
|
146
|
+
claimedByUsername: result.claimedByUsername,
|
|
147
|
+
claimedAt: result.claimedAt,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
switch (result.reason) {
|
|
151
|
+
case 'gift_not_found':
|
|
152
|
+
throw new GiftNotFoundError(giftId);
|
|
153
|
+
case 'already_claimed':
|
|
154
|
+
throw new GiftAlreadyClaimedError();
|
|
155
|
+
case 'self_claim':
|
|
156
|
+
throw new GiftSelfClaimError();
|
|
157
|
+
case 'room_not_member':
|
|
158
|
+
throw new ChatRoomNotMemberError(result.roomId ?? null);
|
|
159
|
+
case 'gift_credit_failed':
|
|
160
|
+
throw new GiftCreditError();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async getGift(giftId, viewerId) {
|
|
164
|
+
const result = await this.giftCommands.getGift(giftId, viewerId);
|
|
165
|
+
if (result.ok) {
|
|
166
|
+
return result.gift;
|
|
167
|
+
}
|
|
168
|
+
switch (result.reason) {
|
|
169
|
+
case 'gift_not_found':
|
|
170
|
+
throw new GiftNotFoundError(giftId);
|
|
171
|
+
case 'room_not_member':
|
|
172
|
+
throw new ChatRoomNotMemberError(result.roomId ?? null);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Zero rain business logic here - the RAIN_COMMANDS port (bound by
|
|
176
|
+
// social-transfers) owns money movement, limit checks, idempotency, and
|
|
177
|
+
// posting/publishing the resulting chat message. This module only resolves
|
|
178
|
+
// who is online (it owns presence for the whole chat-command surface via
|
|
179
|
+
// its own dependency on `chat`) and translates the port's discriminated
|
|
180
|
+
// result into the typed errors this module's router maps to transport codes.
|
|
181
|
+
async postRain(input, actorId) {
|
|
182
|
+
const onlineUserIds = await this.transport.getOnlineUserIds(chatChannel(input.roomId));
|
|
183
|
+
const result = await this.rainCommands.sendRain({ ...input, onlineUserIds }, actorId);
|
|
184
|
+
if (result.ok) {
|
|
185
|
+
return result.message;
|
|
186
|
+
}
|
|
187
|
+
switch (result.reason) {
|
|
188
|
+
case 'disabled':
|
|
189
|
+
throw new CommandDisabledError('rain');
|
|
190
|
+
case 'player_not_found':
|
|
191
|
+
throw new ChatPlayerNotFoundError(result.playerId);
|
|
192
|
+
case 'insufficient_balance':
|
|
193
|
+
throw new InsufficientBalanceError();
|
|
194
|
+
case 'exceeds_limit':
|
|
195
|
+
throw new ExceedsLimitError();
|
|
196
|
+
case 'below_minimum':
|
|
197
|
+
throw new BelowMinimumError();
|
|
198
|
+
case 'no_online_users':
|
|
199
|
+
throw new NoOnlineUsersError();
|
|
200
|
+
case 'too_many_recipients':
|
|
201
|
+
throw new TooManyRecipientsError();
|
|
202
|
+
case 'rain_credit_failed':
|
|
203
|
+
throw new RainCreditError();
|
|
204
|
+
case 'idempotency_key_reuse':
|
|
205
|
+
throw new ChatCommandIdempotencyKeyReuseError();
|
|
206
|
+
case 'concurrent_replay':
|
|
207
|
+
throw new ConcurrentCommandReplayError();
|
|
208
|
+
case 'room_not_member':
|
|
209
|
+
throw new ChatRoomNotMemberError(input.roomId);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
640
212
|
}
|
|
641
213
|
//# sourceMappingURL=chat-commands.service.js.map
|