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.

New !bot default command, type-safe default commands

+86 -40
+14 -3
utils/commandHandler.ts
··· 1 - import { CommitEvent } from "@atcute/jetstream"; 1 + import { CommitEvent, CreateCommit } from "@atcute/jetstream"; 2 2 import { is } from "@atcute/lexicons"; 3 3 import { listRecords } from "./atcuteUtils.ts"; 4 - import { OnlineTimtinkersBotCommand } from "./lexicons/index.ts"; 4 + import { 5 + OnlineTimtinkersBotCommand, 6 + PlaceStreamChatMessage, 7 + } from "./lexicons/index.ts"; 5 8 import { defaultCommands } from "./commands/defaultCommands.ts"; 6 9 import StreamplaceBot from "./streamplaceBot.ts"; 7 10 11 + export interface MessageCreateEvent extends CommitEvent { 12 + commit: MessageCreateCommit; 13 + } 14 + 15 + interface MessageCreateCommit extends CreateCommit { 16 + record: PlaceStreamChatMessage.Main; 17 + } 18 + 8 19 export interface CommandWrapper { 9 20 ( 10 - event: CommitEvent, 21 + event: MessageCreateEvent, 11 22 params: string[], 12 23 bot: StreamplaceBot, 13 24 ): Promise<void> | void;
+47 -24
utils/commands/defaultCommands.ts
··· 2 2 import { getRecord } from "../atcuteUtils.ts"; 3 3 import { didResolver } from "../didResolver.ts"; 4 4 import type { CommandWrapper } from "../commandHandler.ts"; 5 + import { FOLLOWER_MODE } from "../../env.ts"; 5 6 import { AppBskyRichtextFacet } from "@atcute/bluesky"; 6 7 import { is } from "@atcute/lexicons"; 7 8 ··· 13 14 await bot.sendMessage(`Available commands: ${commandsList}`); 14 15 }; 15 16 16 - const shoutoutCommand: CommandWrapper = async (message, params, bot) => { 17 - const senderProfile = await didResolver.resolve(message.did); 17 + const botCommand: CommandWrapper = async (_event, _params, bot) => { 18 + const richtextBuilder = new RichtextBuilder() 19 + .addText("I am a Streamplace chat bot created by ") 20 + .addMention("@timtinkers.online", "did:plc:o6xucog6fghiyrvp7pyqxcs3") 21 + .addText(". "); 22 + if (FOLLOWER_MODE) { 23 + richtextBuilder.addText( 24 + "If you want to use me in your chat, just follow me on Bluesky. ", 25 + ); 26 + } 27 + richtextBuilder.addText("My source code is available on ") 28 + .addLink( 29 + "Tangled", 30 + "https://tangled.org/did:plc:xnpibbj2hcqlsodutcnirmxp/streamplace-bot", 31 + ) 32 + .addText(" under a permissive MIT license."); 33 + 34 + const { text, facets } = richtextBuilder; 35 + await bot.sendMessage(text, facets); 36 + }; 37 + 38 + const shoutoutCommand: CommandWrapper = async (event, params, bot) => { 39 + const senderProfile = await didResolver.resolve(event.did); 18 40 let mention: AppBskyRichtextFacet.Mention | string; 19 41 let receiverDid, receiverProfile, customShoutout; 20 42 21 43 if ( 22 44 is( 23 45 AppBskyRichtextFacet.mentionSchema, 24 - message.commit.record?.facets?.[0]?.features?.[0], 46 + event.commit.record?.facets?.[0]?.features?.[0], 25 47 ) 26 48 ) { 27 - mention = message.commit.record?.facets?.[0]?.features?.[0]; 49 + mention = event.commit.record?.facets?.[0]?.features?.[0]; 28 50 receiverDid = mention.did as Did; 29 51 receiverProfile = await didResolver.resolve(receiverDid); 30 52 customShoutout = bot.getShoutout(receiverDid); ··· 38 60 await bot.sendMessage(customShoutout.text, customShoutout.facets); 39 61 } else if (receiverDid && receiverProfile) { 40 62 const { text, facets } = new RichtextBuilder() 41 - .addMention(senderProfile.handle, message.did) 63 + .addMention(senderProfile.handle, event.did) 42 64 .addText(" gives ") 43 65 .addMention(receiverProfile.handle, receiverDid) 44 66 .addText(" a shoutout!"); ··· 46 68 await bot.sendMessage(text, facets); 47 69 } else { 48 70 const { text, facets } = new RichtextBuilder() 49 - .addMention(senderProfile.handle, message.did) 71 + .addMention(senderProfile.handle, event.did) 50 72 .addText(` gives ${mention} a shoutout!`); 51 73 52 74 await bot.sendMessage(text, facets); ··· 54 76 }; 55 77 56 78 // Hug command 57 - const hugCommand: CommandWrapper = async (message, params, bot) => { 58 - const senderProfile = await didResolver.resolve(message.did); 79 + const hugCommand: CommandWrapper = async (event, params, bot) => { 80 + const senderProfile = await didResolver.resolve(event.did); 59 81 let mention: AppBskyRichtextFacet.Mention | string; 60 82 let receiverDid, receiverProfile; 61 83 62 84 if ( 63 85 is( 64 86 AppBskyRichtextFacet.mentionSchema, 65 - message.commit.record?.facets?.[0]?.features?.[0], 87 + event.commit.record?.facets?.[0]?.features?.[0], 66 88 ) 67 89 ) { 68 - mention = message.commit.record?.facets?.[0]?.features?.[0]; 90 + mention = event.commit.record?.facets?.[0]?.features?.[0]; 69 91 receiverDid = mention.did as Did; 70 92 receiverProfile = await didResolver.resolve(receiverDid); 71 93 } else if (params.length > 0) { ··· 76 98 77 99 if (receiverDid && receiverProfile) { 78 100 const { text, facets } = new RichtextBuilder() 79 - .addMention(senderProfile.handle, message.did) 101 + .addMention(senderProfile.handle, event.did) 80 102 .addText(" gives ") 81 103 .addMention(receiverProfile.handle, receiverDid) 82 104 .addText(" a big hug!"); ··· 84 106 await bot.sendMessage(text, facets); 85 107 } else { 86 108 const { text, facets } = new RichtextBuilder() 87 - .addMention(senderProfile.handle, message.did) 109 + .addMention(senderProfile.handle, event.did) 88 110 .addText(` gives ${mention} a big hug!`); 89 111 90 112 await bot.sendMessage(text, facets); 91 113 } 92 114 }; 93 115 94 - const linkatCommand: CommandWrapper = async (_message, _params, bot) => { 116 + const linkatCommand: CommandWrapper = async (_event, _params, bot) => { 95 117 const streamerDid = bot.getStreamerDid(); 96 118 const streamer = await bot.getUserProfile(streamerDid); 97 119 const linkat = await getRecord(streamer.pdsEndpoint, { ··· 126 148 await bot.sendMessage(text, facets); 127 149 }; 128 150 129 - const pronounsCommand: CommandWrapper = async (message, _params, bot) => { 151 + const pronounsCommand: CommandWrapper = async (event, _params, bot) => { 130 152 const streamer = await bot.getUserProfile(bot.getStreamerDid()); 131 153 const actorProfileURL: `${string}:${string}` = 132 - `https://pdsls.dev/at://${message.did}/app.bsky.actor.profile/self`; 154 + `https://pdsls.dev/at://${event.did}/app.bsky.actor.profile/self`; 133 155 134 156 const richtextBuilder = new RichtextBuilder(); 135 - if (streamer.pronouns) { 157 + if (streamer.actorProfile?.pronouns) { 136 158 richtextBuilder 137 159 .addMention(streamer.handle, bot.getStreamerDid()) 138 160 .addText( 139 - `'s pronouns are ${streamer.pronouns}. `, 161 + `'s pronouns are ${streamer.actorProfile.pronouns}. `, 140 162 ); 141 163 } 142 164 richtextBuilder.addText("You can set your pronouns and website in your ") ··· 157 179 await bot.sendMessage(text, facets); 158 180 }; 159 181 160 - const lurkCommand: CommandWrapper = async (message, _params, bot) => { 161 - const senderChatter = await bot.getUserProfile(message.did); 182 + const lurkCommand: CommandWrapper = async (event, _params, bot) => { 183 + const senderChatter = await bot.getUserProfile(event.did); 162 184 163 185 const { text, facets } = new RichtextBuilder() 164 186 .addText("We hope you enjoy your lurk, ") 165 - .addMention(senderChatter.handle, message.did) 187 + .addMention(senderChatter.handle, event.did) 166 188 .addText("! Thank you for being here!"); 167 189 168 190 await bot.sendMessage(text, facets); 169 191 }; 170 192 171 - const unlurkCommand: CommandWrapper = async (message, _params, bot) => { 172 - const senderChatter = await bot.getUserProfile(message.did); 193 + const unlurkCommand: CommandWrapper = async (event, _params, bot) => { 194 + const senderChatter = await bot.getUserProfile(event.did); 173 195 174 196 const { text, facets } = new RichtextBuilder() 175 197 .addText("Welcome back from your lurk, ") 176 - .addMention(senderChatter.handle, message.did) 198 + .addMention(senderChatter.handle, event.did) 177 199 .addText("!"); 178 200 179 201 await bot.sendMessage(text, facets); 180 202 }; 181 203 182 - const songCommand: CommandWrapper = async (_message, _params, bot) => { 204 + const songCommand: CommandWrapper = async (_event, _params, bot) => { 183 205 const streamerDid = bot.getStreamerDid(); 184 206 const streamer = await bot.getUserProfile(streamerDid); 185 207 const tealStatus = await getRecord(streamer.pdsEndpoint, { ··· 235 257 236 258 export const defaultCommands: Map<string, CommandWrapper> = new Map([ 237 259 ["commands", commandsCommand], 260 + ["bot", botCommand], 238 261 ["shoutout", shoutoutCommand], 239 262 ["so", shoutoutCommand], 240 263 ["hug", hugCommand],
+25 -13
utils/streamplaceBot.ts
··· 1 + import { AppBskyRichtextFacet } from "@atcute/bluesky"; 1 2 import { CommitEvent } from "@atcute/jetstream"; 2 3 import { atprotoClient } from "../main.ts"; 3 4 import { AtprotoClient, listRecords } from "./atcuteUtils.ts"; 4 - import { CommandHandler, type CommandWrapper } from "./commandHandler.ts"; 5 + import { 6 + CommandHandler, 7 + type CommandWrapper, 8 + MessageCreateEvent, 9 + } from "./commandHandler.ts"; 5 10 import { didResolver } from "./didResolver.ts"; 6 - import { PlaceStreamChatMessage } from "./lexicons/index.ts"; 11 + import { 12 + PlaceStreamChatMessage, 13 + PlaceStreamRichtextFacet, 14 + } from "./lexicons/index.ts"; 7 15 8 16 // Shoutout record from the repository 9 17 interface ShoutoutRecord { ··· 128 136 // Process an incoming chat message and respond if it's a command 129 137 async processMessage(event: CommitEvent): Promise<void> { 130 138 if (!this.enabled) return; 131 - if (event.commit.operation === "delete") { 132 - throw new Error( 133 - "CommandHandler did not receive CreateCommit, nothing to handle.", 134 - ); 139 + if (event.commit.operation !== "create") { 140 + return; 135 141 } 136 142 137 143 const streamer = await this.getUserProfile(this.streamerDid); 144 + // TODO: process follows, teleports as well 138 145 const record = event.commit.record as PlaceStreamChatMessage.Main; 139 146 const text = record.text.trim(); 140 147 ··· 168 175 `Executing command: ${commandName} in: ${streamer.handle} from user: ${chatter.handle}`, 169 176 ); 170 177 try { 171 - await commandWrapper(event, params, this); 178 + await commandWrapper(event as MessageCreateEvent, params, this); 172 179 } catch (error) { 173 180 console.error(`Error executing command ${commandName}:`, error); 174 181 } ··· 176 183 } 177 184 178 185 // Send a message to the chat 179 - async sendMessage(text: string, facets?: Facet[]): Promise<void> { 186 + async sendMessage( 187 + text: string, 188 + facets?: PlaceStreamRichtextFacet.Main[] | AppBskyRichtextFacet.Main[], 189 + ): Promise<void> { 180 190 try { 181 - await this.client.createMessage( 182 - text, 183 - this.streamerDid, 184 - facets, 185 - ); 191 + { 192 + await this.client.createMessage( 193 + text, 194 + this.streamerDid, 195 + facets as PlaceStreamRichtextFacet.Main[], 196 + ); 197 + } 186 198 } catch (error) { 187 199 console.error("Error sending bot message:", error); 188 200 }