A work-in-progress chat bot for Streamplace with chat overlay functionality
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

streamplaceBot.ts: Added messy facet handling, shoutout command, JetstreamMessage as input

+61 -25
+61 -25
utils/streamplaceBot.ts
··· 1 - import { createStreamplaceMessage } from "./atcuteUtils.ts"; 1 + import RichtextBuilder from "@atcute/bluesky-richtext-builder"; 2 + import { createStreamplaceMessage, getShoutouts } from "./atcuteUtils.ts"; 3 + import { didResolver } from "./didResolver.ts"; 4 + import { ATPROTO_USERNAME } from "../env.ts"; 2 5 3 6 export interface CommandHandler { 4 - (message: ChatMessage, args: string[]): Promise<void> | void; 5 - } 6 - 7 - export interface ChatMessage { 8 - username: string; 9 - text: string; 10 - color?: string; 11 - timestamp: string; 12 - did?: string; 7 + (message: JetstreamMessage, args: string[]): Promise<void> | void; 13 8 } 14 9 15 - export class StreamplaceBot { 10 + class StreamplaceBot { 16 11 private streamerDid: string; 17 12 private commandPrefix: string; 18 13 private commands: Map<string, CommandHandler>; ··· 29 24 this.commandPrefix = commandPrefix; 30 25 this.commands = new Map(); 31 26 this.enabled = true; 27 + this.botDid = ATPROTO_USERNAME!; 32 28 33 29 // Register default commands 34 30 this.registerDefaultCommands(); ··· 38 34 * Process an incoming chat message and respond if it's a command 39 35 * @param message The chat message to process 40 36 */ 41 - async processMessage(message: ChatMessage): Promise<void> { 37 + async processMessage(message: JetstreamMessage): Promise<void> { 42 38 if (!this.enabled) return; 43 39 44 - const text = message.text.trim(); 40 + const record = message.commit.record!; 41 + const text = record.text.trim(); 45 42 46 43 // Check if message starts with command prefix 47 44 if (!text.startsWith(this.commandPrefix)) return; ··· 55 52 const handler = this.commands.get(commandName); 56 53 if (handler) { 57 54 console.log( 58 - `Executing command: ${commandName} from user: ${message.username}`, 55 + `Executing command: ${commandName} from user: ${message.did}`, 59 56 ); 60 57 try { 61 58 await handler(message, args); ··· 93 90 * Send a message to the chat 94 91 * @param text Message text 95 92 */ 96 - async sendMessage(text: string): Promise<void> { 93 + async sendMessage(text: string, facets?: any): Promise<void> { 97 94 try { 98 95 const result = await createStreamplaceMessage( 99 96 text, 100 97 this.streamerDid, 98 + facets, 101 99 ); 102 100 // Store bot's DID if not already known (from the first message) 103 101 if (!this.botDid && result?.uri) { ··· 113 111 } 114 112 } 115 113 116 - /** 117 - * Get the bot's DID (if known) 118 - */ 114 + // Get the bot's DID 119 115 getBotDid(): string | null { 120 116 return this.botDid; 121 117 } 122 118 123 - /** 124 - * Register the default set of commands 125 - */ 126 - private registerDefaultCommands(): void { 119 + // Get the streamer's DID 120 + getStreamerDid(): string | null { 121 + return this.streamerDid; 122 + } 123 + 124 + // Register the default set of commands 125 + private async registerDefaultCommands(): void { 126 + // Get shoutouts 127 + const shoutouts = await getShoutouts(this.streamerDid) as any; 128 + 127 129 // Help command 128 130 this.registerCommand("commands", async (_message, _args) => { 129 131 const commandsList = Array.from(this.commands.keys()) ··· 133 135 await this.sendMessage(`Available commands: ${commandsList}`); 134 136 }); 135 137 138 + this.registerCommand("shoutout", async (message, args) => { 139 + // TO-DO: caching 140 + const senderHandle = await didResolver.resolve(message.did); 141 + const shoutouteeDid = message.commit.record!.facets![0].features[0]! 142 + .did as Did; 143 + 144 + const foundRecord = shoutouts.records.find((record) => 145 + record.value.user === shoutouteeDid 146 + ); 147 + if (foundRecord) { 148 + await this.sendMessage(foundRecord.value.text, foundRecord.value.facets); 149 + } else { 150 + const { text, facets } = new RichtextBuilder() 151 + .addMention(`@${senderHandle.handle}`, message.did) 152 + .addText(" gives ") 153 + .addMention(`@${args[0].replace(/^@+/, "")}`, shoutouteeDid) 154 + .addText(" a shoutout!"); 155 + 156 + await this.sendMessage(text, facets); 157 + } 158 + }); 159 + 136 160 // Hug command 137 161 this.registerCommand("hug", async (message, args) => { 138 - const hugger = message.username; 139 - const huggee = args[0].replace(/^@+/, ""); 162 + // TO-DO: caching 163 + const senderHandle = await didResolver.resolve(message.did); 164 + const huggeeDid = message.commit.record!.facets![0].features[0]! 165 + .did as Did; 166 + 167 + const { text, facets } = new RichtextBuilder() 168 + .addMention(`@${senderHandle.handle}`, message.did) 169 + .addText(" gives ") 170 + .addMention(`@${args[0].replace(/^@+/, "")}`, huggeeDid) 171 + .addText(" a big hug!"); 140 172 141 - await this.sendMessage(`@${hugger} gives @${huggee} a big hug!`); 173 + await this.sendMessage(text, facets); 142 174 }); 143 175 } 144 176 } 177 + 178 + export const streamplaceBot = new StreamplaceBot( 179 + "did:plc:o6xucog6fghiyrvp7pyqxcs3", 180 + );