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 $type: "place.stream.chat.profile",
50 },
51 };
52};
53
54/**
55 * System message factory functions for common scenarios
56 */
57export const SystemMessages = {
58 streamStart: (streamerName: string): ChatMessageViewHydrated =>
59 createSystemMessage(
60 SystemMessageType.stream_start,
61 `Now streaming - ${streamerName}`,
62 {
63 streamerName,
64 },
65 ),
66
67 // technically, streams can't 'end' on Streamplace
68 // possibly we could use deleting or editing streams (`endedAt` param) for this?
69 streamEnd: (duration?: string): ChatMessageViewHydrated =>
70 createSystemMessage(
71 SystemMessageType.stream_end,
72 duration ? `Stream has ended. Duration: ${duration}` : "Stream has ended",
73 { duration },
74 ),
75
76 notification: (message: string): ChatMessageViewHydrated =>
77 createSystemMessage(SystemMessageType.notification, message),
78};
79
80/**
81 * Checks if a message is a system message
82 * @param message The message to check
83 * @returns True if the message is a system message
84 */
85export const isSystemMessage = (message: ChatMessageViewHydrated): boolean => {
86 return message.author.did === "did:sys:system";
87};
88
89/**
90 * Gets the system message type from a message
91 * @param message The message to check
92 * @returns The system message type or null if not a system message
93 */
94export const getSystemMessageType = (
95 message: ChatMessageViewHydrated,
96): SystemMessageType | null => {
97 if (!isSystemMessage(message)) {
98 return null;
99 }
100 return message.author.handle as SystemMessageType;
101};
102
103/**
104 * Parses metadata from a system message based on its type
105 * @param message The system message to parse
106 * @returns The parsed metadata
107 */
108export const parseSystemMessageMetadata = (
109 message: ChatMessageViewHydrated,
110): SystemMessageMetadata => {
111 const metadata: SystemMessageMetadata = {};
112 const type = getSystemMessageType(message);
113 const text = message.record.text;
114
115 if (!type) return metadata;
116
117 switch (type) {
118 case "stream_end": {
119 const durationMatch = text.match(/Duration:\s*(\d+:\d+(?::\d+)?)/);
120 if (durationMatch) {
121 metadata.duration = durationMatch[1];
122 }
123 break;
124 }
125
126 case "stream_start": {
127 const streamerMatch = text.match(/^(.+?)\s+is now live!/);
128 if (streamerMatch) {
129 metadata.streamerName = streamerMatch[1];
130 }
131 break;
132 }
133 }
134
135 return metadata;
136};