Live video on the AT Protocol
1import { ChatMessageViewHydrated } from "streamplace";
2
3export enum SystemMessageType {
4 stream_start = "stream_start",
5 stream_end = "stream_end",
6 notification = "notification",
7}
8
9export interface SystemMessageMetadata {
10 username?: string;
11 action?: string;
12 count?: number;
13 duration?: string;
14 reason?: string;
15 streamerName?: string;
16}
17
18/**
19 * Creates a system message with the proper structure
20 * @param type The type of system message
21 * @param text The message text
22 * @param metadata Optional metadata for the message
23 * @returns A properly formatted ChatMessageViewHydrated object
24 */
25export const createSystemMessage = (
26 type: SystemMessageType,
27 text: string,
28 metadata?: SystemMessageMetadata,
29 date: Date = new Date(),
30): ChatMessageViewHydrated => {
31 const now = date;
32
33 return {
34 uri: `at://did:sys:system/place.stream.chat.message/${now.getTime()}`,
35 cid: `system-${now.getTime()}`,
36 author: {
37 did: "did:sys:system",
38 handle: type, // Use handle to specify the type of system message
39 },
40 record: {
41 text,
42 createdAt: now.toISOString(),
43 streamer: "system",
44 $type: "place.stream.chat.message",
45 },
46 indexedAt: now.toISOString(),
47 chatProfile: {
48 color: { red: 128, green: 128, blue: 128 }, // Gray color for system messages
49 },
50 };
51};
52
53/**
54 * System message factory functions for common scenarios
55 */
56export const SystemMessages = {
57 streamStart: (streamerName: string): ChatMessageViewHydrated =>
58 createSystemMessage(
59 SystemMessageType.stream_start,
60 `Now streaming - ${streamerName}`,
61 {
62 streamerName,
63 },
64 ),
65
66 // technically, streams can't 'end' on Streamplace
67 // possibly we could use deleting or editing streams (`endedAt` param) for this?
68 streamEnd: (duration?: string): ChatMessageViewHydrated =>
69 createSystemMessage(
70 SystemMessageType.stream_end,
71 duration ? `Stream has ended. Duration: ${duration}` : "Stream has ended",
72 { duration },
73 ),
74
75 notification: (message: string): ChatMessageViewHydrated =>
76 createSystemMessage(SystemMessageType.notification, message),
77};
78
79/**
80 * Checks if a message is a system message
81 * @param message The message to check
82 * @returns True if the message is a system message
83 */
84export const isSystemMessage = (message: ChatMessageViewHydrated): boolean => {
85 return message.author.did === "did:sys:system";
86};
87
88/**
89 * Gets the system message type from a message
90 * @param message The message to check
91 * @returns The system message type or null if not a system message
92 */
93export const getSystemMessageType = (
94 message: ChatMessageViewHydrated,
95): SystemMessageType | null => {
96 if (!isSystemMessage(message)) {
97 return null;
98 }
99 return message.author.handle as SystemMessageType;
100};
101
102/**
103 * Parses metadata from a system message based on its type
104 * @param message The system message to parse
105 * @returns The parsed metadata
106 */
107export const parseSystemMessageMetadata = (
108 message: ChatMessageViewHydrated,
109): SystemMessageMetadata => {
110 const metadata: SystemMessageMetadata = {};
111 const type = getSystemMessageType(message);
112 const text = message.record.text;
113
114 if (!type) return metadata;
115
116 switch (type) {
117 case "stream_end": {
118 const durationMatch = text.match(/Duration:\s*(\d+:\d+(?::\d+)?)/);
119 if (durationMatch) {
120 metadata.duration = durationMatch[1];
121 }
122 break;
123 }
124
125 case "stream_start": {
126 const streamerMatch = text.match(/^(.+?)\s+is now live!/);
127 if (streamerMatch) {
128 metadata.streamerName = streamerMatch[1];
129 }
130 break;
131 }
132 }
133
134 return metadata;
135};