ShushBot for Discord
I love discord.
Not the kind that emerges through civil unrest or anything but the one for dating egirls (and playing games with your friends i guess).
Online collaboration or voice based platforms have spread like a virus (dont cancel me pls) through our workflows and social lives.
AOL Instant Messenger!
The great-grandfather of modern VoIP and social messaging apps is likely IRC (Internet Relay Chat), which emerged in the late '80s for text-based real-time communication. Following closely behind, AOL instant messenger (aim) and ICQ arrived in the late '90s, introducing more user-friendly interfaces.
Skype was the big daddy of the internet though. When it came onto the scene in the early 2000s, it made the idea of voice and video over the internet mainstream.
the skype call screen
Then Enterprise-focused apps like Slack appeared, blending traditional messaging with modern workplace tools.
Finally, Discord emerged in 2015 as a hybrid, offering text, voice, and video chat primarily aimed at gamers. Discord implements the concept of each community having their own server, complete with their own moderators, who have a reeeally interesting reputation, to say the least.
discord mods be like
Yes, discord mods. Usually stereotyped as overweight, power-hungry weebs with yellow fever; it's not exactly the ideal candidate for curating content on your server.
For such a mature platform, it's surprising that Discord doesn't have a more comprehensive moderation system besides muting, deafening, or banning/kicking users from servers using their icky mods.
Meet "ShushBot"
Let's be honest: group voice chats can get chaotic. You log into Discord, join a server, and suddenly, you're hit by a wave of people talking over each other. What if you could bring some order? Enter ShushBot!
Not only does ShushBot get rid of creepy human mods, it also offers the best alternative to user moderation.
The Hack
So how does this shit work? Well, basically, the bot listens for audio in a voice channel, and when the intended target's starts yapping—SSSSHHHHHHH!—it sends earrapingly loud shushing noises everytime they speak.
shhhhh
Setup
0. Make sure you have a Discord account and set yourself up to be a developer.
1. Go to Discord Developer Portal. Click "New Application" and give it a name.
2. Go to the "OAuth2" tab. Under "OAuth2 URL Generator", select the permissions your bot needs. Copy the generated URL on the bottom and visit it to invite the bot to your server.
permissions should look like this
3. The generated bot token you find the the "Bot" tab should be placed in a config.json in the root of the project.
config.json
{
"token": "token_value_here"
}
The Code
The whole repository can be found at this link: ShushBot!
The repository is coded in Typescript and leverages the discord.js library for listening in voice channels and playing audio. Here is some brief explanation of the key parts of the files:
registerCommands.ts
import type { Client } from 'discord.js';
import { SlashCommandBuilder } from '@discordjs/builders';
export async function registerCommands(client: Client) {
const guild = client.guilds.cache.get('your server id here');
if (!guild) return;
const commands = [
new SlashCommandBuilder()
.setName('join')
.setDescription('Joins the voice channel that you are in')
.toJSON(),
new SlashCommandBuilder()
.setName('shush')
.setDescription('Enables recording for a user')
.addUserOption(option =>
option.setName('speaker')
.setDescription('The user to shush')
.setRequired(true)
)
.toJSON(),
new SlashCommandBuilder()
.setName('leave')
.setDescription('Leave the voice channel')
.toJSON(),
];
await guild.commands.set(commands);
}
This file registers the commands to the server you are inviting the bot to, this allows you to use the commands /join
, /shush
, and /leave
.
Where it says 'your server id here'
, in the Discord app, make sure to set your profile settings to "Developer Mode", then just hover over and right-click the server icon you want to import the bot into and copy the server ID
interactions.ts
import { entersState, joinVoiceChannel, VoiceConnection, VoiceConnectionStatus } from '@discordjs/voice';
import { Client, CommandInteraction, GuildMember, Snowflake } from 'discord.js';
import { createListeningStream } from './createListeningStream';
/*...*/
async function shush(
interaction: CommandInteraction,
recordable: Set<Snowflake>,
client: Client,
connection?: VoiceConnection,
) {
if (connection) {
const userId = interaction.options.get('speaker')!.value! as Snowflake;
recordable.add(userId);
const receiver = connection.receiver;
if (connection.receiver.speaking.users.has(userId)) {
createListeningStream(receiver, userId, client.users.cache.get(userId), connection);
}
await interaction.reply({ ephemeral: true, content: 'Listening!' });
} else {
await interaction.reply({ ephemeral: true, content: 'Join a voice channel and then try that again!' });
}
}
/*...*/
export const interactionHandlers = new Map<
string,
(
interaction: CommandInteraction,
recordable: Set<Snowflake>,
client: Client,
connection?: VoiceConnection,
) => Promise<void>
>();
interactionHandlers.set('join', join);
interactionHandlers.set('shush', shush);
interactionHandlers.set('leave', leave);
This file sets up the main logic for each command setup in registerCommands.ts. The core of this file obviously being the shushing logic.
In the shush
function, if the bot is connected to a voice channel, it fetches the user's unique ID (Snowflake) and starts listening. Otherwise, it prompts the user to join a channel. The function replies to indicate its listening status. (replying in this context, refers to the bot sending a message back to the user who invoked the shush command.)
createLIsteningStream.js
import { createWriteStream, createReadStream } from 'fs';
import { pipeline, Transform } from 'stream';
import { EndBehaviorType, VoiceReceiver, createAudioPlayer, createAudioResource, VoiceConnection, StreamType } from '@discordjs/voice';
import type { User } from 'discord.js';
import * as prism from 'prism-media';
class AudioLevelMonitor extends Transform {
private player = createAudioPlayer();
//Loop method
private resource = createAudioResource(createReadStream('long-shhhh.ogg'), {
inputType: StreamType.OggOpus,
});
//private isCurrentlyTalking = false;
constructor(private connection: VoiceConnection) {
super();
this.player.play(this.resource);
this.connection.subscribe(this.player);
this.player.pause();
this.player.on('error', error => {
console.error(`AudioPlayer Error: ${error}`);
});
}
override _transform(chunk: Buffer, _encoding: string, callback: (error?: Error | null, data?: any) => void) {
let isTalking = false;
// Analyze the audio chunk and set isTalking accordingly
for (let i = 0; i <= chunk.length - 2; i += 2) {
const amplitude = Math.abs(chunk.readInt16LE(i));
if (amplitude > 500) {
isTalking = true;
break;
}
}
//Memory leak method
/*if (isTalking && !this.isCurrentlyTalking) {
this.isCurrentlyTalking = true;
if (this.player.state.status !== 'playing') {
console.log("Talking");
let newResource = createAudioResource(createReadStream('shhhh.ogg'), {
inputType: StreamType.OggOpus,
});
this.player.play(newResource);
this.connection.subscribe(this.player);
}
} else if (!isTalking && this.isCurrentlyTalking) {
console.log('Silence');
this.isCurrentlyTalking = false;
//if (this.player.state.status !== AudioPlayerStatus.Idle) {
this.player.stop();
//}
}*/
//Loop method
if (isTalking) {
console.log('Talking');
//this.isCurrentlyTalking = true;
this.player.unpause();
} else {
this.player.pause();
console.log('Silence');
//this.isCurrentlyTalking = false;
}
callback(null, chunk);
}
}
export function createListeningStream(receiver: VoiceReceiver, userId: string, _user?: User, connection?: VoiceConnection) {
if (!connection) return;
const opusStream = receiver.subscribe(userId, {
end: {
behavior: EndBehaviorType.AfterSilence,
duration: 1000,
},
});
const audioMonitor = new AudioLevelMonitor(connection);
// Error Handling
opusStream.on('error', (err) => {
console.error(`OpusStream Error: ${err}`);
});
opusStream.on('close', () => {
console.log('OpusStream closed');
});
audioMonitor.on('error', (err) => {
console.error(`AudioMonitor Error: ${err}`);
});
// Pipe the opus stream into the audio monitor
opusStream.pipe(audioMonitor);
}
This file is responsible for setting up the listening stream which allows the bot to figure out if the person attached to the /shush
command is talking or not by measuring the audio stream's amplitude:
// Analyze the audio chunk and set isTalking accordingly
for (let i = 0; i <= chunk.length - 2; i += 2) {
const amplitude = Math.abs(chunk.readInt16LE(i));
if (amplitude > 500) {
isTalking = true;
break;
}
}
It then pauses or unpauses long-shhhh.ogg depending on the isTalking
state.
What is .ogg?
The File type .ogg (specifically encoded in Opus as opposed to Vorbis) is the audio file used by Discord.
Originally, I tried to create a shhhh.ogg file that would play everytime the shushed user began talking but it started to slow down drastically and eventually stop working fairly quickly. This lead me to believe there was a memory leak so instead, I created long-shhhh.ogg, which would just pause and unpause depending on the isTalking
state.
This didn't fix shit tho, although it probably made it more memory efficient.
Running the bot
Once you download the project files and invited the bot to your server and changed the config.json to use your generated bot token from the Discord developer portal, and made sure to change the registerCommands.ts to contain your server's ID. Just cd to the directory that contains the project and run the following:
npm start
Now, in your server, you can run /join
to have the bot join a channel you're in, and /shush some_user
to shush someone!
Demo
There you have it guys. ShushBot—the bot that literally tells you to shut it. If you're still reading this, then maybe, just maybe, ShushBot has charmed you enough to warrant seven or eight minutes of your precious time.
Now go ahead, add ShushBot to your server. Then let me know how it goes—unless, of course, you've been shushed.
Code for this project can be found here
Do you like unhinged content? Follow me on X!