Live video on the AT Protocol
at next 186 lines 5.2 kB view raw
1import { ChatMessageViewHydrated } from "streamplace"; 2 3export enum SystemMessageType { 4 stream_start = "stream_start", 5 stream_end = "stream_end", 6 notification = "notification", 7 command_error = "command_error", 8} 9 10export interface SystemMessageMetadata { 11 username?: string; 12 action?: string; 13 count?: number; 14 duration?: string; 15 reason?: string; 16 streamerName?: string; 17} 18 19/** 20 * Creates a system message with the proper structure 21 * @param type The type of system message 22 * @param text The message text 23 * @param metadata Optional metadata for the message 24 * @returns A properly formatted ChatMessageViewHydrated object 25 */ 26let systemMessageCounter = 0; 27 28export const createSystemMessage = ( 29 type: SystemMessageType, 30 text: string, 31 metadata?: SystemMessageMetadata, 32 date: Date = new Date(), 33): ChatMessageViewHydrated => { 34 const now = date; 35 const uniqueId = `${now.getTime()}-${systemMessageCounter++}`; 36 37 return { 38 uri: `at://did:sys:system/place.stream.chat.message/${uniqueId}`, 39 cid: `system-${uniqueId}`, 40 author: { 41 did: "did:sys:system", 42 handle: type, // Use handle to specify the type of system message 43 }, 44 record: { 45 text, 46 createdAt: now.toISOString(), 47 streamer: "system", 48 $type: "place.stream.chat.message", 49 }, 50 indexedAt: now.toISOString(), 51 chatProfile: { 52 color: { red: 128, green: 128, blue: 128 }, // Gray color for system messages 53 $type: "place.stream.chat.profile", 54 }, 55 }; 56}; 57 58/** 59 * System message factory functions for common scenarios 60 */ 61export const SystemMessages = { 62 streamStart: (streamerName: string): ChatMessageViewHydrated => 63 createSystemMessage( 64 SystemMessageType.stream_start, 65 `Now streaming - ${streamerName}`, 66 { 67 streamerName, 68 }, 69 ), 70 71 // technically, streams can't 'end' on Streamplace 72 // possibly we could use deleting or editing streams (`endedAt` param) for this? 73 streamEnd: (duration?: string): ChatMessageViewHydrated => 74 createSystemMessage( 75 SystemMessageType.stream_end, 76 duration ? `Stream has ended. Duration: ${duration}` : "Stream has ended", 77 { duration }, 78 ), 79 80 teleportArrival: ( 81 streamerName: string, 82 streamerDid: string, 83 count: number, 84 chatProfile?: any, 85 ): ChatMessageViewHydrated => { 86 const text = 87 count > 0 88 ? `${count} viewer${count !== 1 ? "s" : ""} teleported from ${streamerName}'s stream! Say hi!` 89 : `Someone teleported from ${streamerName}'s stream! Say hi!`; 90 const message = createSystemMessage(SystemMessageType.notification, text, { 91 streamerName, 92 count, 93 }); 94 95 // create a mention facet for the streamer name so it gets colored using existing mention rendering 96 if (chatProfile && streamerDid) { 97 const nameStart = text.indexOf(streamerName); 98 99 // encode byte positions 100 const encoder = new TextEncoder(); 101 const byteStart = encoder.encode(text.substring(0, nameStart)).length; 102 const byteEnd = byteStart + encoder.encode(streamerName).length; 103 104 message.record.facets = [ 105 { 106 index: { 107 byteStart, 108 byteEnd, 109 }, 110 features: [ 111 { 112 $type: "app.bsky.richtext.facet#mention", 113 did: streamerDid, 114 }, 115 ], 116 }, 117 ]; 118 } 119 120 return message; 121 }, 122 123 notification: (message: string): ChatMessageViewHydrated => 124 createSystemMessage(SystemMessageType.notification, message), 125 126 commandError: (message: string): ChatMessageViewHydrated => 127 createSystemMessage(SystemMessageType.command_error, message), 128}; 129 130/** 131 * Checks if a message is a system message 132 * @param message The message to check 133 * @returns True if the message is a system message 134 */ 135export const isSystemMessage = (message: ChatMessageViewHydrated): boolean => { 136 return message.author.did === "did:sys:system"; 137}; 138 139/** 140 * Gets the system message type from a message 141 * @param message The message to check 142 * @returns The system message type or null if not a system message 143 */ 144export const getSystemMessageType = ( 145 message: ChatMessageViewHydrated, 146): SystemMessageType | null => { 147 if (!isSystemMessage(message)) { 148 return null; 149 } 150 return message.author.handle as SystemMessageType; 151}; 152 153/** 154 * Parses metadata from a system message based on its type 155 * @param message The system message to parse 156 * @returns The parsed metadata 157 */ 158export const parseSystemMessageMetadata = ( 159 message: ChatMessageViewHydrated, 160): SystemMessageMetadata => { 161 const metadata: SystemMessageMetadata = {}; 162 const type = getSystemMessageType(message); 163 const text = message.record.text; 164 165 if (!type) return metadata; 166 167 switch (type) { 168 case "stream_end": { 169 const durationMatch = text.match(/Duration:\s*(\d+:\d+(?::\d+)?)/); 170 if (durationMatch) { 171 metadata.duration = durationMatch[1]; 172 } 173 break; 174 } 175 176 case "stream_start": { 177 const streamerMatch = text.match(/^(.+?)\s+is now live!/); 178 if (streamerMatch) { 179 metadata.streamerName = streamerMatch[1]; 180 } 181 break; 182 } 183 } 184 185 return metadata; 186};