- Published on
ClearBot (aka "Burn Bot")
- Authors
-
-
- Name
- David Mohundro
- Bluesky
- @david.mohundro.com
-
Almost 9 years ago, an open source project was begun. It had one singular purpose… to make office life a bit more fun. Here’s the initial commit for “clearbot” (which lovingly was renamed to Burn Bot)!
What is “Burn Bot”?
So, Burn Bot is a very simple Slack bot that listens to a handful of events and mostly uses them to queue it to play sounds on the Clear Function office Sonos speaker.
But… why?
I know, I know, that sounds… weird. But recall, the singular purpose for Burn Bot was to introduce a bit more fun in our office environment. Keep in mind, this was pre-COVID… so, remote work (or even hybrid work for that matter) was not the default like it is today. There was plenty of remote work back then, but nothing like now.
So, Clear Function was office first. And we had a lot of fun with our small team. But MORE FUN is even better.
The initial premise was that we could post something like burn
in any channel and then the office speaker would play a funny sound letting everyone know that someone made a really funny joke to tease someone.
But… how?
The high level architecture for the system has remained largely the same from even almost 10 years ago.
Here’s a simple diagram:
sequenceDiagram Slack-->>ClearBot: POST /slack/events { some message } ClearBot-->>Sonos Proxy: websocket play_url { some message } Sonos Proxy-->>node-sonos-http-api: GET http://localhost:5001/Office/clip/burn.mp3
So, here’s the general flow in words:
- Someone posts a message to a channel that Clear Bot is configured to respond to, like
burn
orsay hello world
. - This results in Slack forwarding that event to Clear Bot
- Clear Bot has a websocket connection to Sonos Proxy (I’ll explain this more shortly)
- Finally, Sonos Proxy then talks to
node-sonos-http-api
(I’ll explain this one, too)
So… there is a bit of duct tape to connect everything here. But for a hobbyist project for fun, it isn’t too bad.
ClearBot (the Slack Bot)
The actual clearbot (Burn Bot’s actual source) is all open source and hosted at https://github.com/clearfunction/clearbot. The initial commit referenced above shows that the first version was based on Hubot. Hubot is still an active bot-based library, but Slack ended up deprecating “custom integrations” so I ported the entire library over to Slack’s Bolt API (see the PR up at https://github.com/clearfunction/clearbot/pull/13).
The code is super simple.
In src/app.ts
, there is really one function that kicks things off… attachResponses
. It looks like this:
export function attachResponses(app: App, sonos: Sonos): void { burnResponses.forEach((resp) => { app.message(resp.listen, async ({ say }) => { if (resp.message) { say(resp.message); }
if (typeof resp.play === 'function') { sonos.playOnSonos(resp.play(), say); } else if (typeof resp.play === 'string') { sonos.playOnSonos(resp.play, say); } }); });}
The burnResponses
is an array of type Response
that match this interface:
export interface Response { keyword: string; description: string; listen: string | RegExp; play: string | RandomPlay; message?: string;}
So, this means that we have an array of configurations (all hard-coded actually) that look like this:
{ keyword: 'sickburn | fire', description: 'A random sick burn!', listen: /!sickburn|:fire:/i, play: () => randomResponse(burns), }, { keyword: 'yakety', description: 'Yakety sax!', listen: /yakety/i, play: 'yakkety.mp3', },
The listen
prop defines what we’re looking for in a message… if someone sends sickburn
it will call randomResponse
… if someone instead sends yakety
it will play the Yakety Sax clip. Pretty simple.
There is also a say
command hooked up that will just ask Sonos to do a voice to text for whatever comes in.
All of the “sound playing” goes to a Sonos implementation that calls emits a websocket message, meaning we have to have a Sonos Proxy connected to our clearbot. That’s the next piece.
Sonos Proxy
The Sonos Proxy is also open source and lives at https://github.com/clearfunction/sonos-proxy-nodejs. It is even simpler than Burn Bot’s source… in fact, this is the main bit of logic (with some logging and comments removed):
socket.on('play_url', data => { enumeratePlayers(roomName => { playClip(roomName, { file: url, volume: 20, }); }); });
socket.on('play_text', data => { enumeratePlayers(roomName => { sayClip(roomName, data); }); });
// ...
function playClip(roomName: string, data: PlayClip): void { const file = encodeURIComponent(data.file); const volume = encodeURIComponent(data.volume); fetch(`${process.env.SONOS_BRIDGE_URL}/${encodeURIComponent(roomName)}/clip/${file}/${volume}`);}
It just waits for websocket messages and forwards them on to this SONOS_BRIDGE_URL
. Which leads to…
Sonos HTTP API
That last bit of functionality is all defined at https://github.com/jishi/node-sonos-http-api. It is where the magic actually happens. Sonos speakers (at the time of this writing and as far as I know) don’t have any defined public API, but this open source project built one.
Meaning, via the magic of open source, we can programmatically play funny sound clips on Sonos speakers on the same network that our Sonos Proxy lives on.
Wrapping Up
And that’s it! Over the last few years, we’ve added a multitude of other funny sound clips to it… then we’ll go months without playing a single thing.
One actual useful thing from it is that we have a slackbot
reminder in a channel that says Reminder: @MrBurns say standup in 1 minute.
every morning at 8:29am… which means we get an audible cue to join our standups. That is super useful and, without fail, we’ll miss it if we have a power outage or similar that takes the machine that our proxy runs on down.
It can be a lot of fun just being silly with technology - try it out sometime!