🎯Project Goals
In my undergraduate, I participated in my university’s debate society. The two main formats of debate in Canada are British Parliamentary and Canadian Parliamentary. British Parliamentary consist of 8 speakers delivering 7-minute speeches with 1 minute of “protected” time at the beginning and end of their speech where the opposition speakers are not allowed to offer a question. At the high school level speeches are 5 minutes long with 30 seconds of protected time. The second format is Canadian Parliamentary which consists of 4 speakers but speakers now have the choice to deliver a 7-minute speech, a 6-minute speech, a 10-minute speech, a 3-minute speech, or a 4-minute speech depending on their speaker position in the debate with varying “protected” time.
This can get confusing, but conventionally the adjudicators of each debate round would keep track of time and deliver “time signals” where an adjudicator would bang the table to indicate that the speech has entered/exited protected time.
This system works for the most part when debates occur in person, but over the COVID-19 pandemic debates were forced to be held online; mainly on Zoom or Discord.
A clear problem of debating online was: “How does an adjudicator efficiently indicate time signals now?” Typing in chat wouldn’t be so easy: debaters would have to keep their eye on the chat while delivering a speech, and some adjudicators felt that typing “protected time” would break their note-taking flow. Interrupting a speech by unmuting your microphone also has its challenge: if the speaker wasn’t paying attention they may mistake an interruption as a signal that the opponent was offering a question.
In March 2020, a tournament called Lord Discord Cup was held. A member of the community named Keeghan Lucas made a game-changing Discord bot named the TDBot
(referencing the Tournament Directory role) that would automate time signals for debaters.
I then realized a massive opportunity: why should this be restricted to a single tournament? Expanding this technology could truly help organize tournaments by managing participants’ roles, creating timers that could pause, resume, and stop, and adding functionality to accommodate a variety of speech lengths and time signals would help universities all over the country, and eventually the globe. So that’s what I did. Except, I wanted to distinguish my product, so I named it NotTDBot
for clarity.
⚙️How it works
When NotTDBot
is added to the Discord server it subscribes to each Discord text channel and listens for entries that start with the +
symbol. If the +
symbol is detected, NotTDBot
offers a couple of commands:
Timing
NotTDBot
’s bread and butter is to time a speech.
+start
creates a 7-minute speech timer+pause
pauses the current timer+resume
resumes the current timer+end
ends the current timer
+start
comes with a few presets:
+start {10}
creates a 10-minute speech timer+start {8}
creates an 8-minute speech timer+start {6}
creates a 6-minute speech timer+start {7}
creates a 7-minute speech timer+start {4}
creates a 4-minute speech timer+start {3}
creates a 3-minute speech timer
Why +resume
and not +start
again? Initially, I foresaw an issue where there would be some debate rounds where no one understood how to use the NotTDBot
. I envisioned that an adjudicator or tournament organizer would be able to start a timer for another room, thereby becoming the owner of multiple concurrent timers.
I developed an indexing system for managing timers that would follow the following use case:
- Bob starts a timer with
+start
- Bob needs to start a concurrent timer, and uses
+start
again - Bob needs to pause his first timer and uses
+pause {0}
to indicate which timer to pause - Bob needs to restart his first timer and uses
+resume {0}
- Bob needs to end his second timer early, and uses
+end {1}
- Bob wants to see which timers are still active, so he uses
+timers
and sees his first timer running
This indexing system become a legacy feature because the main use case became users owning at most one timer.
You can also create custom timers:
+start CUSTOM {7} {0.5} {6.5}
In the above example, the total length of the timer is 7 minutes, the first signal that “protected” time has ended will be at 0.5 minutes (30 seconds) and the last signal that “protected” time has started again will be at 6.5 minutes (6 min & 30 sec).
Polls
Debaters love democracy, so it became common to vote on things.
You could use the +poll
command to make a poll:
+poll {Poll Title} [Option 1] [Option 2] [Option 3]
The most common use case was voting on how “you wanted to participate in a practice” so I made a preset:
+poll DEBATE
Which would generate the following poll:
Developer Tools
I also made some tools for myself:
+dev terminate
a kill switch to shut off the bot+dev servers
return a list of all servers the bot is on+dev wake
returns a message if the bot is active+dev commands
returns the list of commands available+dev announce {some announcement}
send a direct message to all administrators of the servers the bot is added to+dev admins
return a list of all administrators of the servers the bot is added to
🔍Taking a look under the hood
There were two main languages dominating the online documentation for creating Discord bots: JavaScript, and Python.
You may or may not know, I’m a JavaScript defender so the choice was obvious. (Actually, there was more support for JavaScript development at the time)
Initiating the bot
This process may have changed as the Discord API
has evolved, but when I was originally developing this program it was necessary to instantiate a Discord.Client
with a Partial
for the bot to handle events correctly for uncached messages (that is, messages that were made before the bot was turned on). (source)
Instantiating the bot looked something like:
const bot = new Discord.Client({ partials: ['MESSAGE', 'CHANNEL', 'REACTION'] });
The further setup would be to bind a commands
key of the bot
to a Discord.Collection
, which is essentially a utility class for the JavaScript Map
structure with additional functions. (source)
1bot.commands = new Discord.Collection()
Processing Commands
As I mentioned in how it works, the bot basically monitors channel activity for messages that begin with the +
character. Initially, the prefix was !
but there were many jukebox bots and admin bots that used !
so I changed it to the uncommon +
to avoid conflicts. These days Discord requires commands to start with /
, but I don’t intend to update NotTDBot
to handle this change.
The entry point JavaScript file bot.js
had the following logic:
- load bot commands from modules
- monitor for chat messages
- check if incoming message has a
+
prefix and matches a loaded command- if yes, execute command module
- check if incoming message has a
- login bot with a provided Discord application token
Load Commands
The project directory is organized similarly to:
[project]├── bot.js└── assets └── scripts └── commands ├── Command.js ├── Timer.js └── Poll.js
This file structure is convenient for the loading commands step. If each module in /.../commands
exports a single class that has an execute
function, then we can load the classes with the node:fs
library similar to:
1const files = fs2 .readdirSync("./assets/scripts/commands/")3 .filter((file) => file.endsWith(".js"))4for (const file of files) {5 const command = require(`./assets/scripts/commands/${file}`)6
7 bot.commands.set(command.name.toLowerCase(), new command(bot))8}
Command Class
There is a single Command
class template that other commands extended from which enables the command loading seen above:
1class Command {2 constructor(bot, options = {}) {3 this.bot = bot4 this.name = options.name || "No name provided."5 this.aliases = options.aliases || []6 this.description = options.description || "No description provided."7 this.category = options.category || "Misc."8 this.usage = options.usage || "No usage provided."9 this.presets = options.presets || "No presets assigned."10 }11
12 async execute(message, args) {13 throw new Error(`Command ${this.name} doesn't have execute function.`)14 }15}
As you can see, details about the specific command can be passed from the options
parameter, where the bot itself is passed from the bot
parameter so that we can access the bot
Managers like guilds
(servers), users
, and messages
for servers the bot is on.
New commands can extend Command
:
1class Poll extends Command {2 constructor(...args) {3 super(...args, {4 name: "poll",5 description:6 'Creates a poll with custom questions and answers.\n Your question and options can be as long as you want. Maximum poll of 20 options\nIf you have no option, a "yes" and "no" poll will be generated',7 category: "Poll",8 usage: "!poll {question} [option1] [option2]",9 presets:10 "DEBATE - Creates a poll with the question: What would you like to do for today's meeting?. with options: Anything, Debate, Judge, Vibe.",11 })12 }13 async execute(message, args) {14 // do some command15 }16}17
18const command = new Poll(bot)
Timer
JavaScript has a built-in setTimeout
method, but unfortunately, it’s not so easy to pause a timeout. You can clearTimeout
in JavaScript but you have to do some type of wrapper for keeping track of the elapsed and remaining time:
1class Timer {2 constructor(message, poiOpen, poiClose, speechLength) {3 this.message = message4 this.poiOpen = poiOpen5 this.poiClose = poiClose6 this.speechLength = speechLength7 this.running = false8 this.count = 09
10 // this is relating to the indexing system11 if (!timers[this.message.author.id]) {12 this.id = 013 } else {14 this.id = timers[this.message.author.id].length15 }16 }17 setId(id) {18 this.id = id19 }20 start() {21 this.running = true22 this.tick()23 }24 pause() {25 this.running = false26 }27 stop() {28 this.running = false29 removeTimer(this.message, this.id)30 }31 tick() {32 if (this.running) {33 if (this.count == this.poiOpen * 60) {34 signal(this.message, "OPEN")35 } else if (this.count == this.poiClose * 60) {36 signal(this.message, "CLOSED")37 } else if (this.count == this.speechLength * 60) {38 start_grace(this.message)39 this.stop()40 }41 this.count++42 setTimeout(() => this.tick(), 1000, this)43 }44 }45}
Most of these class functions are self-explanatory, but I want to touch on a big design choice:
“What is even going on with the tick function??”
The tick function follows the following logic:
- execute
tick
whenstart
is executed - check if the timer is still running
- check if any protected time signals need to be sent
- check if the speech is over
- increase the
count
(the elapsed seconds) - start a
setTimeout
that executes a recursivetick
call after 1 second
All timer signals are sent to the user through the Discord reply function of the Message class:
1message.reply("Timer started!: 7 Minute Speech")
The indexing system worked by holding an object of user id
keys that would be bound to an array of their current timers:
type Timers = { [user_id]: Timer[]}
Poll
The logic when receiving a +poll
command goes as follows:
- check whether there are more than 1 arguments
- if yes, check if the command is calling for a preset poll template (i.e.,
+poll DEBATE
) - check whether the second argument is correctly wrapped in a curly bracket - if yes, use the contents of the bracket as the
question
- if there is a valid question, parse the command arguments for answers options wrapped in square brackets
- if yes, check if the command is calling for a preset poll template (i.e.,
NotTDBot
utilized an embedded message to send a poll:
1function poll_response(message, question, answer_options) {2 let final_options = "\n"3 let index = 974 let emoji_prefix = ":regional_indicator_"5 for (var i = 0; i < answer_options.length; i++) {6 let temp =7 answer_options[i].charAt(0).toUpperCase() +8 answer_options[i].substring(1)9 final_options += "\n"10 final_options +=11 emoji_prefix + String.fromCharCode(index) + ":" + ": " + temp + "\n"12 index++13 }14 message.channel15 .send({16 embed: {17 title: "Poll: " + final_options,18 color: 3447003,19 description: final_options,20 },21 })22 .then((sent) => {23 // Responding to poll logic24 })25}
Where letters
is an array of alphabet chars ['A', 'B', ..., 'Z']
A user is expected to “react” to the poll question with an emoji to indicate their answer. To make it easier for users, I implemented some logic to have NotTDBot
react to its own message with the available options:
1// Responding to poll logic2
3for (var i = 0; i < answer_options.length; i++) {4 sent.react(letters[i])5}
Monitor Messages
Discord.Client
has a message listener which can be used to check for the prefix I talked about in processing commands. We can then split the command from the arguments and do a simple if ... else
statement to execute the correct command:
1bot.on("message", async (message) => {2 if (message.content.charAt(0) != prefix) return3 let args = message.content.substring(prefix.length).split(" ")4 let command = args[0]5
6 if (7 command == "start" ||8 command == "pause" ||9 command == "resume" ||10 command == "timers" ||11 command == "end"12 ) {13 bot.commands.get("timer").execute(message, args)14 }15})
Deployment
The deployment was simple:
- make an application on https://discord.com/developers/applications
- retrieve your application token
-
use
Discord.Client
login1bot.login(SECRET_APP_TOKEN) -
retrieve your client id
-
generate an invitation link for your bot using https://discordapi.com/permissions.html and your client id
-
deploy the bot on a server. I used Linode and Docker to containerize the bot with a simple Dockerfile:
Terminal window FROM node:16.16.0WORKDIR /appCOPY package*.json ./RUN npm installCOPY . .CMD ["node", "bot.js"]
Start using NotTDBot and my takeaways
Well, you can’t, really. Discord has a policy that bots cannot be added to more than 100 servers unless the bot is verified.
When NotTDBot
approached 100 servers I went to do the verification process, when I was notified my bot was flagged for inorganic growth:
At the beginning of online debate, there weren’t too many tech-savvy debate members, so only a few members in the community would be responsible for creating servers for their club and debate events. I.e., my club would ask me to create the club’s Discord server and also create the club’s debate event Discord server.
So, because there were too many common moderators in multiple servers, my bot was flagged as inorganic growth.
When I tried to appeal this or find some workaround, Discord support suggested I remove my bot from a significant amount of servers until the warning went away:
// Feb, 2021Hey Eyas,
Thanks for reaching out!
Regarding your query with the verification of your server, it would be best to reach out to our Community team by heading over to[https://dis.gd/cprog](https://dis.gd/cprog).
About the verification of your bot, though, it appears that a significant number of the servers this bot is in are owned by the same user(s).This is considered inorganic and disqualifies a bot for verification.We would encourage you to review the servers your bot is in to determine how this happened and to apply in the future once this is no longer the case.
Note that this status cannot be contested.We can't lift this error for you, nor can we share the specifics on how many of your bot's servers can be owned by the same user or set of users.If you're unsure how to proceed, I'd recommend running the Get Current User Guilds command for your bot, then using Get Guild to determine owner_ids and search for commonalities.[https://discord.com/developers/docs/resources/user#get-current-user-guilds](https://discord.com/developers/docs/resources/user#get-current-user-guilds) [https://discord.com/developers/docs/resources/guild#get-guild](https://discord.com/developers/docs/resources/guild#get-guild)
You can either choose to leave some of the servers leading to this error or continue letting your bot grow to minimize the portion of your bot's server ownership represented by non-unique users.The commands to act on the above endpoints and to make your bot leave a given server to vary by library, so if you're unsure, we'd encourage you to review the documentation for your codebase.You can also consider joining the Discord Developers server at [https://discord.gg/discord-developers](https://discord.gg/discord-developers) to seek further advice from our community.
If you have any other questions or concerns, please don't hesitate to let me know and I'd be happy to help further.
Best,[Discord Support Member]
While I was dealing with this roadblock, NotTDBot
couldn’t be added to any new servers. It soon became more convenient to use an alternative technology that was rapidly developing like Tabtastic or Hear! Hear!, and soon NotTDBot
became obsolete.
While NotTDBot
didn’t achieve world dominance, I did learn a great deal about product development, and a surprising amount of business (supply and demand babyy!!)