|
|
|
@@ -0,0 +1,34 @@
|
|
|
|
|
import { z } from "zod";
|
|
|
|
|
|
|
|
|
|
export const PROTOCOL_VERSION = 1 as const;
|
|
|
|
|
export const viewerSchema = z.object({ uid: z.string().min(1), name: z.string().min(1).max(128), avatar: z.string().url().optional() });
|
|
|
|
|
export type Viewer = z.infer<typeof viewerSchema>;
|
|
|
|
|
|
|
|
|
|
const liveEvents = {
|
|
|
|
|
"live.danmaku": z.object({ viewer: viewerSchema, text: z.string().max(500) }),
|
|
|
|
|
"live.gift": z.object({ viewer: viewerSchema, giftName: z.string(), battery: z.number().int().nonnegative(), quantity: z.number().int().positive(), sourceEventId: z.string() }),
|
|
|
|
|
"live.gift.combo": z.object({ viewer: viewerSchema, giftName: z.string(), battery: z.number().int().nonnegative(), quantity: z.number().int().positive(), comboId: z.string() }),
|
|
|
|
|
"live.enter": z.object({ viewer: viewerSchema }),
|
|
|
|
|
"live.guard.buy": z.object({ viewer: viewerSchema, guardName: z.string(), quantity: z.number().int().positive(), price: z.number().int().nonnegative() }),
|
|
|
|
|
"live.superchat": z.object({ viewer: viewerSchema, message: z.string(), price: z.number().int().nonnegative(), sourceEventId: z.string() }),
|
|
|
|
|
"live.like": z.object({ viewer: viewerSchema }),
|
|
|
|
|
"live.share": z.object({ viewer: viewerSchema }),
|
|
|
|
|
"live.unknown": z.object({ cmd: z.string(), raw: z.unknown() }),
|
|
|
|
|
"system.status": z.object({ connected: z.boolean(), cookieCloud: z.boolean(), replyEnabled: z.boolean(), detail: z.string().optional() }),
|
|
|
|
|
"system.error": z.object({ code: z.string(), message: z.string() }),
|
|
|
|
|
"viewer.points.updated": z.object({ viewer: viewerSchema, delta: z.number().int(), balance: z.number().int(), reason: z.enum(["gift", "wheel"]) }),
|
|
|
|
|
"wheel.result": z.object({ viewer: viewerSchema, category: z.string(), fallback: z.boolean(), song: z.object({ id: z.number().int(), title: z.string(), tags: z.array(z.string()) }), cost: z.literal(150), balance: z.number().int() }),
|
|
|
|
|
"wheel.insufficient-balance": z.object({ viewer: viewerSchema, balance: z.number().int(), cost: z.literal(150) }),
|
|
|
|
|
"wheel.invalid-command": z.object({ viewer: viewerSchema, message: z.string() })
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
export type EventType = keyof typeof liveEvents;
|
|
|
|
|
export type EventPayload<T extends EventType> = z.infer<(typeof liveEvents)[T]>;
|
|
|
|
|
export type LiveEvent = { [T in EventType]: { version: 1; id: string; occurredAt: string; roomId: string; type: T; payload: EventPayload<T> } }[EventType];
|
|
|
|
|
|
|
|
|
|
const variants = Object.entries(liveEvents).map(([type, payload]) => z.object({ version: z.literal(1), id: z.string().uuid(), occurredAt: z.string().datetime(), roomId: z.string(), type: z.literal(type), payload }));
|
|
|
|
|
export const liveEventSchema = z.discriminatedUnion("type", variants as unknown as [z.ZodDiscriminatedUnionOption<"type">, ...z.ZodDiscriminatedUnionOption<"type">[]]) as unknown as z.ZodType<LiveEvent>;
|
|
|
|
|
|
|
|
|
|
export function makeEvent<T extends EventType>(roomId: string, type: T, payload: EventPayload<T>): Extract<LiveEvent, { type: T }> {
|
|
|
|
|
return { version: PROTOCOL_VERSION, id: crypto.randomUUID(), occurredAt: new Date().toISOString(), roomId, type, payload } as Extract<LiveEvent, { type: T }>;
|
|
|
|
|
}
|