The making of a Discord bot, from a joke project to (some) usefulness.
In early 2020, when the lockdowns were still going strong, I got possessed by this incredible need to create a bot. Bots have been a subject of fascination for me for a long time, out of various reasons, and now I wanted to get my hands dirty and build something for my small Discord community.
The bot has been through a number of revisions and refactoring as I have switched technologies around and the discord API changed and expanded, but I have stuck with javascript throughout its full duration and learned a lot of useful things!
To get the weird stuff out of the way, UMP9 is a character from the mobile strategy game Girls Frontline. She is a bit of an airhead so it was the perfect choice for a crappy bot.
The code is available at the bottom of this article.
Feature List
Reaction images
Conversational AI
Reminders
Weekly PSA
Reboot option
Delete messages
Random Discord interactions
Image tagging
Real time status adjustments
Soundboard
Linux fortune
Other misc discord features
Let’s go through them in order:
Reaction Images #
Arguably the simplest feature. The biggest leap Discord made from other chat applications was its ability to host and display pictures. This feature was quickly requested by the users for funposting and memes. After a few updates, the most recent Discord API makes adding this feature really easy:
client.on('ready', function(){
console.log("UMP9, ready for deployment!");
// ./Assets/Images/
} else if (mess.includes('!416dance')) {
message.channel.send({ files: [{ attachment: './Assets/Images/416dance.gif' }] });
} else if (mess.includes('!wrong')) {
message.channel.send({ files: [{ attachment: './Assets/Images/halp.png' }] });
} else if (mess.includes('!flash')) {
message.channel.send({ files: [{ attachment: './Assets/Images/flash.png' }] });
} else if (mess.includes('!show me ')) {
var str = mess,
delimiter = ' ',
start = 2,
tokens = str.split(delimiter).slice(start),
result = tokens.join(delimiter);
message.channel.send("I see " + result, {
files: [{ attachment: './Assets/Images/danger.png' }]
});
} else if (mess.includes('!danger')) {
message.channel.send({ files: [{ attachment: './Assets/Images/danger.png' }] });
} else if (mess.includes('!monday')) {
message.channel.send({ files: [{ attachment: './Assets/Images/mondays.png' }] });
} else if (mess.includes('!coffee')) {
message.channel.send({ files: [{ attachment: './Assets/Images/cofee.jpg' }] });
} else if (mess.includes('!wednesday')) {
message.channel.send({ files: [{ attachment: './Assets/Images/wednesday.jpg' }] });
} else if (mess.includes('!nogawed')) {
message.channel.send({ files: [{ attachment: './Assets/Images/nogawed.jpg' }] });
} else if (mess.includes('!smug')) {
message.channel.send ({ files: [{ attachment: './Assets/Images/smug.jpg' }] });
} else if (mess.includes('!hype')) {
message.channel.send({ files: [{ attachment: './Assets/Images/hype.gif' }] });
One of the functions that I did as an experiment was the !show me one. It simply returns the input with an image of UMP9 pointing which is occasionally funny.
Conversational AI #
The meat of the project and also the section on which I cannot go into too much detail. I will say what I tried:
Google DialogFlow
The very first attempts were via Google’s DialogFlow options. I picked this one because it was both free and easy to use and the input recognition was formidable even back in 2020. It also used to have a free tier with no limit back in 2020. Unfortunately, google deleted my project when they changed tiers and I no longer have any screenshots, but DialogFlow works based on a very simple prompt system.
The machine learning working in the background is smart enough to give the right response to queries even if they are not worded in the precises way defined in DialogFlow.
This was a fine system and I can absolutely see its use in business chatbots. Unfortunately, I wanted a bot that people could naturally talk to.
“If you wish to make an apple pie from scratch, you must first invent the universe”
Building and running my own chatbot from scratch on a laptop from 2013 would obviously be too tall of a task for what is still a joke project. I had signed up for GPT3 when it first came out, but I had yet to get access to the beta. Just as I was debating what to do next, I did get the email with a generous beta allowance. Overall I had a great time, and you can read more about it here:
https://robsware.github.io/2020/12/27/gpt3
Unfortunately, my trial access to GPT3 ended rather quickly and it became too costly to keep it running as a joke. It was time for a new solution.
Replika AI
Here is where we start breaking ToS. Replika is a chatbot app and website that earned some minor fame and notoriety on the internet. They use a mix between GPT3 and their in house AI development to create natural responses and interactions. It’s pretty decent, and it has a free version!
Unfortunately, it cannot be used outside the app and there is no API available. Fortunately, my main area of expertise when it comes to pentesting is web. I will not go into detail here, but with some fuzzing, rotating keys on timers and the web socket protocol, I was able to intercept and redirect all the messages to and from Replika to my Discord bot. The results are very good.
I realise this is a temporary solution and that at some point they will probably fix their web socket request and kill my access. If that happens, I will release on writeup on how I achieved it in the first place. I have been looking at Mycroft (https://mycroft.ai/) as a replacement once that happens and I do intend to give it a go at some point.
Reminders #
Since a lot of the users were using UMP9 as a notepad, I decided to add a reminder function for that purpose. It does not use a database and just relies on javascript variable storage.
}else if (mess.toLowerCase() === "!reminderbot") {
message.channel.send("Hello I can help you remember stuff:\n\n!reminderbot \t\tList of all Commands\n\n!remindme \t\t {time} {message}\n\t{time} Please have the amount of time be denoted by a time character.\n\t\tm - minutes, s - seconds, d - days.\n!remind {@User} {time} {message}\n\t{time} Please have the amount of time be denoted by a time character.\n\t\tm - minutes, s - seconds, d - days.\n\t");
// Reminds a specific user
} else if (mess.toLowerCase().startsWith('!remind')) {
try {
// Variables
var returntime;
var timemeasure;
var userid;
mess = mess.split(' ');
console.log('Message recieved from ' + message.author.id + ' at ' + Date.now().toString());
// Sets the userid for the recipiant
console.log(mess[1].replace('<@', '').slice(0, -1))
userid = client.users.cache.find(user => user.id === (mess[1].replace('<@', '').slice(0, -1)))
console.log(userid)
// Sets the return time
timemeasure = mess[2].substring((mess[2].length - 1), (mess[2].length))
returntime = mess[2].substring(0, (mess[2].length - 1))
// Based off the delimiter, sets the time
switch (timemeasure) {
case 's':
returntime = returntime * 1000;
break;
case 'm':
returntime = returntime * 1000 * 60;
break;
case 'h':
returntime = returntime * 1000 * 60 * 60;
break;
case 'd':
returntime = returntime * 1000 * 60 * 60 * 24;
break;
default:
returntime = returntime * 1000;
break;
}
// Returns the Message
setTimeout(function () {
// Removes the first 2 array items
mess.shift();
mess.shift();
mess.shift();
// Creates the message
var content = mess.join();
content = content.replaceAll(',', ' ');
message.reply("<@" + userid +"> " + content);
console.log('Message sent to ' + userid + ' at ' + Date.now().toString());
}, returntime)
} catch (e) {
message.reply("An error has occured, please make sure the command has a time delimiter and message");
console.error(e.toString());
}
It is able to set both reminders for self and for other users and can take input in seconds, minutes, hours and days. Whether the bot is up enough for the days to take effect is a different problem altogether.
Weekly PSA #
This one is a very short and simple function that sends 2 PSAs at the start of each week with some useful reminders for the game.
const job = cron.job('01 00 14 * * 1', function() {
client.channels.cache.get('653719279111372810').send("Commanders, don't forget to renew your exploration squad!");
job.stop();
});
job.start();
const job4 = cron.job('01 00 10 * * 1', function() {
client.channels.cache.get('653719279111372810').send("Commanders, it's a new week! Don't forget to share and let's do our best!");
job4.stop();
});
job4.start();
Reboot option #
Because this bot is written in JavaScript, it often hangs and crashes for no reason but not hard enough to show the bot as offline. To combat that, I added a function to reboot the bot from discord.
} else if (mess.includes('!whack')) {
var painResponses = ['Oww my head ಥ﹏ಥ', 'Why are you doing this to me ( ˃̣̣̥⌓˂̣̣̥ )', 'Not again (つ﹏<)', (" ", {files: ["./Assets/Images/feels9.png"]})];
const painReply = painResponses[getRandomInt(painResponses.length)];
message.channel.send(painReply).then(async function (message) {
await restartProcess();
});
Delete messages #
Another simple function, mostly for server management and deleting large amounts of text.
} else if (mess.includes('!purge')) {
if (message.member.permissions.has('MANAGE_MESSAGES')) {
var messNum = 1;
if (mess.length > 6) {
messNum = mess.split(' ')[1];
}
async function purge() {
var fetched = await message.channel.messages.fetch({limit: (messNum < 50 ? (Number(messNum) + 1) : 50)});
message.channel.bulkDelete(fetched);
console.log("Deleting the last " + fetched.size + " message(s).");
}
purge();
It takes an int input with the number of messages to delete and checks that there aren’t more than 50 messages in the request (fat finger protection).
Random Discord interactions #
This was another feature that got toned down over time. The bot will randomly send :3 messages in Discord and has a 0.01% chance to pat someone after they posted something. Upon a user sending a message, a dice is rolled to determine if the bot should send a :3 message. Then another dice is rolled to determine with how much delay should the bot send the message. This makes it appear somewhat more natural and can lead to some funny timing.
var rando = getRandomInt(90000) + 5000;
var chance = getRandomInt(1000) + 1;
var chance2 = getRandomInt(50) + 1
var randomHour = getRandomInt(23);
var randomMinute = getRandomInt(59);
client.user.setPresence({
status: 'online',
activity: {
name: 'you',
type: 'WATCHING'
}
});
if (chance == 1){
setTimeout(function() {
message.channel.send('*pats* ' + message.author.toString() + " :3");
}, rando);
}
if (chance2 == 1){
const job3 = cron.job('00 ' + randomMinute + ' ' + randomHour + ' * * *', function() {
message.channel.send(":3");
console.log(randomMinute)
console.log(randomHour)
var randomHour = getRandomInt(23);
var randomMinute = getRandomInt(59);
job3.stop()
console.log(randomMinute)
console.log(randomHour)
});
job3.start()
}
This feature has actually been a great exercise in observing how randomness can create emergent humour and how people will attribute higher intelligence to a glorified dice roller.
Image tagging #
A big feature, part of my experiments and getting more hands on with AI. Unfortunately, it has also been removed since I moved the bot to a rasPi that does not have the processing power to use this function.
The underlying detection engine is powered by DeepDanbooru: https://github.com/KichangKim/DeepDanbooru
While I did try to run my own training on it, some quick back of the envelope maths showed that I would need about 800 GBs of storage to store all the pictures from Danbooru, so I went with some of the pretrained options. I designed the function to take an image as an input, save it locally and timestamp it, and then run the DeepDanbooru tag recognition engine on it. This helped not cross the streams when users from different servers would submit images at the same time.
// DeepDanbooru
//https://blog.sukasuka.cn/?p=759
} else if (mess.includes('!tag')){
const fs = require(`fs`);
const async = require('async');
if (message.attachments.size > 0) {
async function start() {
console.log("This is an image")
//Save image
const imageIdentifer = Date.now()
await new Promise(resolve =>
request(message.attachments.first().url)
.pipe(fs.createWriteStream(`deepImages/Img-${imageIdentifer}.png`))
.on('finish', resolve));
var output = shell.exec(`deepdanbooru.exe evaluate --model-path deepdanbooru/model/model-resnet_custom_v4.h5 --tags-path deepdanbooru/model/tags.txt deepImages/Img-${imageIdentifer}.png`).stdout;
danbooruResponse = output.split('png:')[1];
message.channel.send(danbooruResponse)
}
start()
}
*/
It worked very well even on non anime images
While this feature certainly had the “wow factor” on point, it had very little use and it forced me to run the bot on a beefier machine. In the end, I ended up commenting it out as I moved the bot to a rasPi.
Real time status adjustments #
A funny gimmick that I used to play around with the discord API. The bot will change its status from away and “Watching terrible anime” to present and “Watching you”, and will revert back after interaction stops.
client.user.setPresence({
status: 'idle',
activity: {
name: 'terrible anime',
type: 'WATCHING'
}
});
client.on('message', function(message){
if (message.member.permissions.has("SEND_MESSAGES", "READ_MESSAGE_HISTORY")){
client.user.setPresence({
status: 'online',
activity: {
name: 'you',
type: 'WATCHING'
}
});
Soundboard #
Simple feature that essentially uploads .mp3 files to the chat that can be played on desktop. Most of them are memes.
else if (mess.includes('!emergency')) {
message.channel.send({ files: [{ attachment: './Assets/Audio/Energency.mp3' }] });
} else if (mess.includes('!presentday')) {
message.channel.send({ files: [{ attachment: './Assets/Audio/Present_Day_heh..._Present_Time.mp3' }] });
} else if (mess.includes('!ugly')) {
message.channel.send({ files: [{ attachment: './Assets/Audio/ugly.mp3' }] });
} else if (mess.includes('!whip')) {
message.channel.send({ files: [{ attachment: './Assets/Audio/whip.mp3' }] });
} else if (mess.includes('!gameover')) {
message.channel.send({ files: [{ attachment: './Assets/Audio/SOPMOD_Game_Over.mp3' }] });
} else if (mess.includes('!bossbattle')) {
message.channel.send({ files: [{ attachment: './Assets/Audio/whiteNyto.mp3' }] });
} else if (mess.includes('!triggered')) {
message.channel.send({ files: [{ attachment: './Assets/Audio/One_more_group_offended.mp3' }] });
} else if (mess.includes('!vice')) {
message.channel.send({ files: [{ attachment: './Assets/Audio/Vice_news_disclaimer.mp3' }] });
} else if (mess.includes('!soplaugh')) {
message.channel.send({ files: [{ attachment: './Assets/Audio/soplaugh.mp3' }] });
} else if (mess.includes('!DA NYA')) {
message.channel.send({ files: [{ attachment: './Assets/Audio/Nyaaaaa_IDW.mp3' }] });
} else if (mess.includes('!motivational')) {
message.channel.send({ files: [{ attachment: './Assets/Audio/Nothing_is_possible.mp3' }] });
A year or so ago we had a few attempts at a podcast and added a function to have the bot join the voice channel and play audio, feature which I assume is used by all those music bots. I haven’t updated it to the current discord API but here it is anyway:
/* Disabled until fixed. TODO fix
} else if (mess.includes('!play')) {
var voiceChannel = message.member.voice.channel;
voiceChannel.join().then(connection => {
const dispatcher = connection.play("./Assets/Audio/Vice_news_disclaimer.mp3");
}).catch(err => console.log(err));
*/
Linux fortune #
The classic Linux terminal fortune command. Nothing more, nothing less. I have simply used a list with all the fortune outputs and a dice roller to pick a random one.
} else if (mess.includes('!fortune')) {
var fs = require("fs");
var text = fs.readFileSync("./Assets/Text/fortune.txt") + '';
var textByLine = text.split("\n");
var result = textByLine[getRandomInt(textByLine.length)];
message.channel.send(result);
Other misc features #
A lot of these have been removed by the few left in:
The bot with react to being patted or being asked for patting:
} else if (mess.match('!p[a,e]t me')) {
message.channel.send('*pat pat* ' + message.author.username);
} else if (mess.match('!(p[a,e]t)+[s]?')) {
var patResponses = ["Thank you! I'm trying my best! ♫", ':3 ♫', '❤️'];
const patReply = patResponses[getRandomInt(patResponses.length)];
message.channel.send(patReply);
The Discord token is not stored in the program file but as a system variable:
const discord_token = process.env.DISCORD_TOKEN;
if (!discord_token) {
console.log("DISCORD_TOKEN is empty. Please run \"export DISCORD_TOKEN=<your token here>\", then try again.");
process.exit();
}
client.login(discord_token);
I have also added a very dirty and hacky error handler to deal with the idiosyncrasies of JavaScript.
process.on('uncaughtException', function (err) {
console.error(err);
console.log("Crashn't");
});
!flood will spam a lot of :3 to clear up the screen. Useful when you’re at work and someone posted something NSFW.
else if (mess.includes('!flood')) {
message.channel.send(":3\n:3\n:3\n:3\n:3\n:3\n...)
}
Conclussion #
I am still not sure if this is a serious project or still a joke. The code quality is certainly not up to par and the development schedule follows a very strict “when I feel like it” schedule. The bot is currently in use on about a dozen servers and I haven’t received any complaints for its current implementation. I have learned a lot from playing around with it and it gave me an excuse to avoid tinkering as I was working towards a real project, as loosely defined as that might be.
I think I am ready to shelve this project, which is why I am writing this blog post in the first place. It has become bloated enough and I have other things that demand more of my attention right now. The full code, sans the Replika part, is available here:
https://github.com/robsware/UMP9-public/blob/main/index.js
I have another bot that I have been brainstorming ideas about, mostly focused on assisting the workflow in my infosec job and hobbies. I can see myself using the lessons I learned from working on UMP9 on that, although I will be avoiding pure JavaScript and will either stick with TypeScript or Python.
I find bots fascinating and I love the idea of having those tiny helpers around. While the offerings of big companies (google, alexa, siri) are much better than anything I could write by myself, I think a touch of auteur makes it more endearing. It is a real, human touch instead of a corporate, (mostly) polished product. The knowledge gained in this experiment already came in useful plenty of times, so in the end, I’m glad I did it.