Skip to main content

Discord Bot: Camera's On

info

This is a quick project to have a way to enforce "cameras on" policy on my Discord server using a bot that will "police" this. I used Claude 3 Opus to help me develop this. These are my notes.

AI Assisted

Please note, I use a combination of Claude 3 Opus (paid) and some ChatGPT-4 (paid) to guide me and generate code, I then run and tweak the steps and code until it works for my use case.

Overview

Setting up a Discord bot consists of three things

  1. Discord Bot Application at discord.com
  2. A server to run your code on
  3. A Discord server to invite your bot to.

Discord Bot Application

Reference

Get notes from here: "Get Started" guide.

  • Go to the Discord Developer Portal (https://discord.com/developers/applications)
  • Create a new application, give app (bot) a name.
  • Go to side menu Bot
  • reset Token and copy for later
  • disable Public Bot
  • Got to side menu OAuth2, look for the OAuth2 URL Generator
  • select scope bot
  • then select permissions Send Messages, Manage Roles, Move Members and Read Message History
  • Go down to the generated URL and copy - this is how you add your bot to your server.
note

Permissions-wise, "kicking" someone from a voice channel is actually to "move" them out, so you need Move Members and not Kick Members which will axe them from the disord server entirely.

Heroku Bot App

I'm going to run my bot code on Heroku. I need to create a repo with my code, and link it up with Heroku.

Create my GH repo

My initial repo consists of the following files:

  • .gitignore
  • .env
  • Procfile
  • bot.js
  • package.json
.gitignore
node_modules/
.env
.env
# local dev environment
BOT_TOKEN=YOUR_BOT_TOKEN
CAMERA_ON_CHANNELS=CHANNEL_ID_1,CHANNEL_ID_2
WARNING_TIMEOUT=30000
  • Replace YOUR_BOT_TOKEN with your actual bot token
  • Replace CHANNEL_ID_1,CHANNEL_ID_2 with the comma-separated list of video channel IDs
  • Adjust the WARNING_TIMEOUT value as needed
Profile
worker: node bot.js
note

This is the final working bot code with a few bells & whistles. I started generating code with Claude 3, but after a few snags went back to GPT-4o and my honest opinion, is that GPT gave me better quality code that solved the complex use case quicker than Claude.

bot.js
require('dotenv').config();
const Discord = require('discord.js');
const client = new Discord.Client();

const token = process.env.BOT_TOKEN;
const cameraOnChannels = process.env.CAMERA_ON_CHANNELS.split(',');
const warningTimeout = parseInt(process.env.WARNING_TIMEOUT);
const warnedUsers = new Map();

client.on('ready', () => {
console.log(`Logged in as ${client.user.tag}!`);
});

client.on('error', (error) => {
console.error('The bot encountered an error:', error);
});

client.on('voiceStateUpdate', async (oldState, newState) => {
console.log(`Voice state update detected for user ${newState.member.user.tag}. Old Channel: ${oldState.channelID}, New Channel: ${newState.channelID}, Camera On: ${newState.selfVideo}`);

if (!cameraOnChannels.includes(newState.channelID)) return;

const member = newState.member;
const channel = newState.channel;

if (newState.channelID !== oldState.channelID && !newState.selfVideo) {
// User joined the voice channel with camera disabled
console.log(`User ${member.user.tag} joined the monitored channel "${channel.name}" without camera enabled.`);
handleCameraOff(member, channel);
} else if (newState.channelID === oldState.channelID && !newState.selfVideo && !warnedUsers.has(member.id)) {
// User disabled their camera while in the voice channel
console.log(`User ${member.user.tag} disabled their camera in the monitored channel "${channel.name}".`);
handleCameraOff(member, channel);
} else if (newState.selfVideo && warnedUsers.has(member.id)) {
// User enabled their camera
console.log(`User ${member.user.tag} enabled their camera in the monitored channel "${channel.name}".`);
clearWarning(member.id);
}
});

async function handleCameraOff(member, channel) {
try {
const warningMessage = await member.send(`📷 Attention! Please enable your camera in the channel "**${channel.name}**" within the next ${warningTimeout / 1000} seconds, or you will be removed from the channel. 🚨`);
console.log(`Sent warning message to user ${member.user.tag}.`);

const timeoutId = setTimeout(async () => {
if (!member.voice.selfVideo) {
await member.voice.setChannel(null);
await member.send(`❌ You have been removed from the channel "**${channel.name}**" due to not enabling your camera. Please rejoin the channel and enable your camera to participate. 🙏`);
console.log(`User ${member.user.tag} was removed from the channel "${channel.name}" for not enabling their camera.`);
}
}, warningTimeout);

warnedUsers.set(member.id, { timeoutId, warningMessage });
console.log(`Set timeout for user ${member.user.tag}.`);
} catch (error) {
console.error('Error handling camera off:', error);
}
}

async function clearWarning(memberId) {
const userInfo = warnedUsers.get(memberId);
if (userInfo) {
clearTimeout(userInfo.timeoutId);
warnedUsers.delete(memberId);
console.log(`Cleared warning for user with ID ${memberId}.`);

try {
await userInfo.warningMessage.edit(`✨ Thank you for enabling your camera! Your cooperation is appreciated. 😊👍`);
console.log(`Edited warning message for user with ID ${memberId}.`);
} catch (editError) {
console.error(`Failed to edit warning message for user with ID ${memberId}:`, editError);
}
}
}

// Log in to Discord
client.login(token);
package.json
{
"name": "rxbot",
"version": "1.0.0",
"description": "A Swiss Army Knife Utility Bot",
"main": "bot.js",
"scripts": {
"start": "node bot.js"
},
"dependencies": {
"discord.js": "^12.5.3"
}

Do the usual things to the code, git add . && git commit -m "initial commit", add remote branch as needed and push git push origin main.

Configure Heroku App

As per bot.js you need to create the following three config vars in your Heroku App.

  • BOT_TOKEN
  • CAMERA_ON_CHANNELS
  • WARNING_TIMEOUT

And add your bot token from discord portal, the channel IDs from your discord server and an arbitrary amount of time for the warning.

It should look like this:

Config Vars

Connect GH to Heroku (CLI)

Install Heroku cli, login.

Check remotes on current repo.

Add heroku remote with heroku git:remote -a <app-name>

~/Repos/rxbot on CameraEnabled !1git remote -v   
origin [email protected]:ronamosa/rxbot.git (fetch)
origin [email protected]:ronamosa/rxbot.git (push)
~/Repos/rxbot on CameraEnabled ❯ heroku git:remote -a rxbot
set git remote heroku to https://git.heroku.com/rxbot.git

Connect GH to Heroku (Manual)

login to Heroku, create your app, go into your app, go to Deploy tab and then look for Deployment method and see this:

Connect GitHub

Choose github

Choose the rxbot repo and connect it

Choose repo

Successfully connected.

Connect Success

Discord Server Setup

There's not much to this, the OAuth2 URL you generated in the Discord portal at the beginning.

Go to that URL and you should see this

OAuth URL

Accept Permissions:

Accept Perms

Success!

Added to Discord

note

Only realised I had already doxxed myself in the last screenshot after creating my redacted shots 1 and 2 last.

Bot in Action

When a user joins the hard-coded channels that are on the "cameras on" list, they get a warning if their camera is off

Warning

if they comply, the warning disappears (edited) and a thank you is in it's place

Comply

any time a user disables their camera in these rooms, they will get a warning

ReWarned

after 30s of no compliance, the user is re-"moved" from the voice channel

Removed

Troubleshooting

Permissions

First time running the basic bot code, permissions tweak needed:

2024-05-17T22:44:01.638622+00:00 app[worker.1]: /app/node_modules/discord.js/src/rest/RequestHandler.js:154
2024-05-17T22:44:01.638692+00:00 app[worker.1]: throw new DiscordAPIError(request.path, data, request.method, res.status);
2024-05-17T22:44:01.638693+00:00 app[worker.1]: ^
2024-05-17T22:44:01.638693+00:00 app[worker.1]:
2024-05-17T22:44:01.638694+00:00 app[worker.1]: DiscordAPIError: Missing Permissions
2024-05-17T22:44:01.638694+00:00 app[worker.1]: at RequestHandler.execute (/app/node_modules/discord.js/src/rest/RequestHandler.js:154:13)
2024-05-17T22:44:01.638694+00:00 app[worker.1]: at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
2024-05-17T22:44:01.638696+00:00 app[worker.1]: at async RequestHandler.push (/app/node_modules/discord.js/src/rest/RequestHandler.js:39:14)
2024-05-17T22:44:01.638696+00:00 app[worker.1]: at async GuildMember.edit (/app/node_modules/discord.js/src/structures/GuildMember.js:312:5) {
2024-05-17T22:44:01.638697+00:00 app[worker.1]: method: 'patch',
2024-05-17T22:44:01.638698+00:00 app[worker.1]: path: '/guilds/1086150687269847080/members/894143297050787850',
2024-05-17T22:44:01.638698+00:00 app[worker.1]: code: 50013,
2024-05-17T22:44:01.638698+00:00 app[worker.1]: httpStatus: 403
2024-05-17T22:44:01.638698+00:00 app[worker.1]: }

Crashed on Heroku

2024-05-16T04:45:28.045293+00:00 heroku[web.1]: Starting process with command `npm start`
2024-05-16T04:45:29.006096+00:00 app[web.1]:
2024-05-16T04:45:29.006148+00:00 app[web.1]: > [email protected] start /app
2024-05-16T04:45:29.006148+00:00 app[web.1]: > node bot.js
2024-05-16T04:45:29.006149+00:00 app[web.1]:
2024-05-16T04:45:29.563611+00:00 app[web.1]: Logged in as RXBOT#4425
2024-05-16T04:46:28.333745+00:00 heroku[web.1]: Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch
2024-05-16T04:46:28.345690+00:00 heroku[web.1]: Stopping process with SIGKILL
2024-05-16T04:46:28.449933+00:00 heroku[web.1]: Process exited with status 137
2024-05-16T04:46:28.474043+00:00 heroku[web.1]: State changed from starting to crashed

solution: change dyno from web dyno to worker dyno

alt text

restart with heroku cli: heroku ps:scale worker=1 -a rxbot

2024-05-16T10:29:29.364100+00:00 heroku[web.1]: Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch
2024-05-16T10:29:29.376816+00:00 heroku[web.1]: Stopping process with SIGKILL
2024-05-16T10:29:29.470658+00:00 heroku[web.1]: Process exited with status 137
2024-05-16T10:29:29.499871+00:00 heroku[web.1]: State changed from starting to crashed
2024-05-16T12:14:14.832513+00:00 app[api]: Scaled to web@0:Basic worker@0:Basic by user [email protected]
2024-05-16T12:14:14.933958+00:00 heroku[web.1]: State changed from crashed to down
2024-05-16T12:14:21.653969+00:00 app[api]: Scaled to web@0:Basic worker@1:Basic by user [email protected]
2024-05-16T12:14:26.105195+00:00 heroku[worker.1]: Starting process with command `node bot.js`
2024-05-16T12:14:27.035181+00:00 heroku[worker.1]: State changed from starting to up
2024-05-16T12:14:28.600243+00:00 app[worker.1]: Logged in as RXBOT#4425