Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,10 @@
"name": "ClientMetaSchema",
"file": "packages/core/src/contracts/schemas/common.ts"
},
{
"name": "CommandChatMessageSchema",
"file": "packages/core/src/contracts/schemas/chat-command.ts"
},
{
"name": "CommandConfigSchema",
"file": "packages/core/src/engagement/chat-commands/contract/index.ts"
Expand Down
19 changes: 18 additions & 1 deletion packages/core/src/contracts/adapters/chat-system-writer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { createToken } from './token.js';
import type { SystemChatMessage, CommandMetadata } from '../schemas/chat-command.js';
import type {
SystemChatMessage,
CommandChatMessage,
CommandMetadata,
} from '../schemas/chat-command.js';

export type { SystemChatMessage as ChatSystemMessage };
export type { CommandChatMessage };

export type ChatSystemWriter = {
postSystemMessage(args: {
Expand All @@ -10,6 +15,18 @@ export type ChatSystemWriter = {
metadata: CommandMetadata;
tx?: unknown;
}): Promise<SystemChatMessage>;
postCommandMessage(args: {
roomId: string | null;
userId: string;
username: string;
metadata: CommandMetadata;
tx?: unknown;
}): Promise<CommandChatMessage>;
updateCommandMessage(args: {
messageId: string;
metadata: CommandMetadata;
tx?: unknown;
}): Promise<CommandChatMessage>;
};

export const CHAT_SYSTEM_WRITER = createToken<ChatSystemWriter>('ChatSystemWriter');
4 changes: 2 additions & 2 deletions packages/core/src/contracts/adapters/gift-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// AGENTS.md).
import { createToken, type Token } from './token.js';
import type { Uuid } from '../schemas/common.js';
import type { ChatSystemMessage as SystemChatMessage } from './chat-system-writer.js';
import type { CommandChatMessage } from './chat-system-writer.js';

export type SendGiftArgs = {
amount: string;
Expand All @@ -27,7 +27,7 @@ export type SendGiftFailureReason =
| 'room_not_member';

export type SendGiftResult =
| { ok: true; message: SystemChatMessage }
| { ok: true; message: CommandChatMessage }
| { ok: false; reason: SendGiftFailureReason };

export type ClaimGiftFailureReason =
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/contracts/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,11 @@ export { LOGIN_ENFORCEMENT } from './login-enforcement.js';
export type { PlayEligibilityPort } from './play-eligibility.js';
export { PLAY_ELIGIBILITY } from './play-eligibility.js';

export type { ChatSystemMessage, ChatSystemWriter } from './chat-system-writer.js';
export type {
ChatSystemMessage,
CommandChatMessage,
ChatSystemWriter,
} from './chat-system-writer.js';
export { CHAT_SYSTEM_WRITER } from './chat-system-writer.js';

export type { ChatBlockWriter } from './chat-block-writer.js';
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/contracts/adapters/rain-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// import. ADR-0017.
import { createToken, type Token } from './token.js';
import type { Uuid } from '../schemas/common.js';
import type { ChatSystemMessage } from './chat-system-writer.js';
import type { CommandChatMessage } from './chat-system-writer.js';

export type SendRainArgs = {
amount: string;
Expand All @@ -36,7 +36,7 @@ export type SendRainFailureReason =
| 'room_not_member';

export type SendRainResult =
| { ok: true; message: ChatSystemMessage }
| { ok: true; message: CommandChatMessage }
| { ok: false; reason: SendRainFailureReason };

export type RainCommands = {
Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/contracts/schemas/chat-command-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,31 @@ export const GiftCommandMetadataSchema = z.object({
senderUsername: z.string(),
amount: MoneyAmountSchema,
currency: z.string(),
// Optional for compatibility with gift messages persisted before claim state
// was included in command metadata. New gift messages always populate these.
status: z.enum(['available', 'claimed']).optional(),
claimedBy: UuidSchema.nullable().optional(),
claimedByUsername: z.string().nullable().optional(),
claimedAt: z.string().nullable().optional(),
});
export type GiftCommandMetadata = z.infer<typeof GiftCommandMetadataSchema>;

export const RainCommandMetadataSchema = z.object({
command: z.literal('rain'),
fromUserId: UuidSchema,
fromUsername: z.string().optional(),
amount: MoneyAmountSchema,
currency: z.string(),
recipientCount: z.number().int(),
perRecipient: MoneyAmountSchema,
recipients: z
.array(
z.object({
userId: UuidSchema,
username: z.string(),
}),
)
.optional(),
});
export type RainCommandMetadata = z.infer<typeof RainCommandMetadataSchema>;

Expand Down Expand Up @@ -59,6 +74,8 @@ export type UnignoreCommandMetadata = z.infer<typeof UnignoreCommandMetadataSche

export const DonateCommandMetadataSchema = z.object({
command: z.literal('donate'),
senderId: UuidSchema.optional(),
senderUsername: z.string().optional(),
recipientId: UuidSchema,
recipientUsername: z.string(),
amount: MoneyAmountSchema,
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/contracts/schemas/chat-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ export const SystemChatMessageSchema = z.object({
});
export type SystemChatMessage = z.infer<typeof SystemChatMessageSchema>;

/** A user-authored chat message carrying structured command metadata. */
export const CommandChatMessageSchema = z.object({
id: UuidSchema,
roomId: UuidSchema.nullable(),
userId: UuidSchema,
username: z.string(),
content: z.string(),
type: z.literal('user'),
metadata: CommandMetadataSchema,
isDeleted: z.boolean(),
createdAt: z.string(),
});
export type CommandChatMessage = z.infer<typeof CommandChatMessageSchema>;

/**
* Client-facing sentinel for global chat wherever a chat command's `roomId`
* field is otherwise a real room UUID (`/gift`, `/rain`, `/donate`) - the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
UuidSchema,
MoneyAmountSchema,
SystemChatMessageSchema,
CommandChatMessageSchema,
ChatRoomIdSchema,
TimestampSchema,
} from '@openora/core/contracts';
Expand Down Expand Up @@ -50,7 +51,7 @@ export const MentionResultSchema = z.object({
});
export type MentionResult = z.infer<typeof MentionResultSchema>;

export { SystemChatMessageSchema };
export { SystemChatMessageSchema, CommandChatMessageSchema };

export const PostGiftInputSchema = z.object({
amount: MoneyAmountSchema,
Expand Down Expand Up @@ -99,7 +100,7 @@ export const chatCommandsContract = {
postGift: oc
.route({ method: 'POST', path: '/chat-command/gift' })
.input(PostGiftInputSchema)
.output(SystemChatMessageSchema),
.output(CommandChatMessageSchema),

claimGift: oc
.route({ method: 'POST', path: '/chat-command/gift/{id}/claim' })
Expand All @@ -118,7 +119,7 @@ export const chatCommandsContract = {
postRain: oc
.route({ method: 'POST', path: '/chat-command/rain' })
.input(PostRainInputSchema)
.output(SystemChatMessageSchema),
.output(CommandChatMessageSchema),

mentionSearch: oc
.route({ method: 'GET', path: '/chat-command/mention-search' })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect, vi } from 'vitest';
import { mock, makeDrizzle } from '../../../../testing/mock.js';
import type {
ChatSystemMessage,
CommandChatMessage,
ChatBlockWriter,
AdminUserDirectory,
GiftCommands,
Expand Down Expand Up @@ -41,18 +41,25 @@ const ENABLED_ROW = {

const DISABLED_ROW = { ...ENABLED_ROW, enabled: false };

const SYSTEM_MSG: ChatSystemMessage = {
const SYSTEM_MSG: CommandChatMessage = {
id: MSG_ID,
roomId: ROOM_ID,
actorId: ACTOR_ID,
userId: ACTOR_ID,
username: 'bob',
content: '',
type: 'user',
isDeleted: false,
metadata: {
command: 'gift',
giftId: GIFT_ID,
senderId: ACTOR_ID,
senderUsername: 'bob',
amount: '10.00000000',
currency: 'USD',
status: 'available',
claimedBy: null,
claimedByUsername: null,
claimedAt: null,
},
createdAt: new Date().toISOString(),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '@openora/core/server';
import type {
Uuid,
ChatSystemMessage,
CommandChatMessage,
ChatBlockWriter,
AdminUserDirectory,
GiftCommands,
Expand Down Expand Up @@ -193,7 +193,7 @@ export class ChatCommandsService {
return summaries.map((s) => ({ userId: s.userId, username: s.username }));
}

async postGift(input: PostGiftInput, actorId: Uuid): Promise<ChatSystemMessage> {
async postGift(input: PostGiftInput, actorId: Uuid): Promise<CommandChatMessage> {
const result = await this.giftCommands.sendGift(input, actorId);
if (result.ok) {
return result.message;
Expand Down Expand Up @@ -258,7 +258,7 @@ export class ChatCommandsService {
// who is online (it owns presence for the whole chat-command surface via
// its own dependency on `chat`) and translates the port's discriminated
// result into the typed errors this module's router maps to transport codes.
async postRain(input: PostRainInput, actorId: Uuid): Promise<ChatSystemMessage> {
async postRain(input: PostRainInput, actorId: Uuid): Promise<CommandChatMessage> {
const onlineUserIds = await this.transport.getOnlineUserIds(chatChannel(input.roomId));
const result = await this.rainCommands.sendRain({ ...input, onlineUserIds }, actorId);
if (result.ok) {
Expand Down
45 changes: 45 additions & 0 deletions packages/core/src/engagement/chat/service/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
RealtimeTransport,
CommandMetadata,
ChatSystemMessage,
CommandChatMessage,
AdminUserDirectory,
} from '@openora/core/contracts';
import { chatChannel } from '@openora/core/contracts';
Expand Down Expand Up @@ -1185,6 +1186,50 @@ export class ChatService {
return msg;
}

async postCommandMessage(args: {
roomId: ChatRoom['id'] | null;
userId: User['id'];
username: string;
metadata: CommandMetadata;
tx?: unknown;
}): Promise<CommandChatMessage> {
const db = (args.tx as DrizzleDb | undefined) ?? this.drizzle.db;
const [record] = await db
.insert(chatMessage)
.values({
roomId: args.roomId,
userId: args.userId,
username: args.username,
content: '',
type: 'user',
metadata: args.metadata,
})
.returning();
const message = toMessage(record);
if (!args.tx) {
void this.transport.publish(chatChannel(args.roomId), message);
}
return message as CommandChatMessage;
}

async updateCommandMessage(args: {
messageId: ChatMessage['id'];
metadata: CommandMetadata;
tx?: unknown;
}): Promise<CommandChatMessage> {
const db = (args.tx as DrizzleDb | undefined) ?? this.drizzle.db;
const [record] = await db
.update(chatMessage)
.set({ metadata: args.metadata })
.where(eq(chatMessage.id, args.messageId))
.returning();
const message = toMessage(record);
if (!args.tx) {
void this.transport.publish(chatChannel(message.roomId), message);
}
return message as CommandChatMessage;
}

async listRoomMembers({ roomId, viewerId }: { roomId: ChatRoom['id']; viewerId?: User['id'] }) {
await this.verifyRoomAccess(roomId, viewerId);
const members = await this.drizzle.db
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import {
UuidSchema,
MoneyAmountSchema,
SystemChatMessageSchema,
CommandChatMessageSchema,
ChatRoomIdSchema,
} from '@openora/core/contracts';

export { SystemChatMessageSchema };
export { SystemChatMessageSchema, CommandChatMessageSchema };

export const SendDonateInputSchema = z.object({
targetUsername: z.string().min(1),
Expand All @@ -25,5 +26,5 @@ export const socialTransfersContract = {
sendDonate: oc
.route({ method: 'POST', path: '/social-transfers/donate' })
.input(SendDonateInputSchema)
.output(SystemChatMessageSchema),
.output(CommandChatMessageSchema),
};
Loading
Loading