Building Moderation Discord Bots with Appwrite Cloud Functions

Building Moderation Discord Bots with Appwrite Cloud Functions

End User Meme

At Appwrite, we try to make our software flexible and agnostic to any tech stack or use case. While Appwrite’s primary users are building backends for web and mobile apps, it is also used for all types of weird use cases (like the Appwrite Minecraft SDK). In the same spirit, we often abuse our own code and tinker with Appwrite for unconventional purposes. This week, I experimented with building Discord Bots and deploying them using Appwrite’s Functions service.

Wait a minute, what’s an Appwrite?

Appwrite is a “Backend-as-a-Service,” which in laymen's terms just means you can use it as a backend to your mobile or web apps. After deployment, Appwrite provides APIs to handle user authentication, data and file storage, cloud functions, and other features that can be used alongside backend services, or replace them entirely. This is a convenient package of services that handles most things needed by a Discord bot.

What we’ll do in this post is abuse Appwrite 0.13’s new Functions service to host persistent Discord bots. The sharp-witted reader will have noticed the anomaly here. Generally, cloud functions are meant to be short, headless tasks executed on a remote server to handle business logic. This is distinctly different from a Discord bot, which requires a persistent process. This is why I called this use case “abusive.” Under the hood of Appwrite 0.13’s new Functions service, there is a persistent executor that can be reused. This persistent runtime is what makes the new cloud functions runtime so responsive, but also the mechanism we’ll take advantage of.

Learn more about our Functions service’s architecture.

Talk is cheap, where’s the code?

If you’d like to follow along, I’d recommend first following Appwrite’s “Getting Started”) guide, and make sure you know the basics of writing a bot with Discord.js). We’re diving straight into the deep end to use Appwrite and Discord.js to build bots in unconventional ways... for science!

Mad Science

Let’s start by creating an Appwrite function. We can create a function using Appwrite’s CLI.

In your working directory, run the following commands:

# Initialize the client
appwrite client --endpoint http://<API endpoint>/v1 

# Login, this command is interactive
appwrite login

# Init Project in your directory, this command is interactive
appwrite init project

# Init Function, select Node.js as the runtime
appwrite init function

After running these commands, you should see an appwrite.json config file generated in your directory, pointing to the Appwrite instance and project you specified. You can find the source code for the automatically generated function in functions/<function name>.

If you navigate to your project’s Functions page on the Appwrite console, you will see the newly created function.

Appwrite Functions Console

You can create a deployment for this function using the following CLI command:

appwrite deploy function

You can view your deployment and test it using the Execute Now button.

Deployed Function

Turning Function into Discord Bot

We’ll be creating a Discord bot using Node.js and Discord.js. To add the required dependencies, add the following lines to your function’s package.json:

{
    ...
    "dependencies": {
            "discord.js": "^13.6.0",
            "node-appwrite": "^5.0.0"
    }
}

Then, we will edit src/index.js to add a simple Discord command:

const sdk = require("node-appwrite");
const { Client, Intents, MessageEmbed } = require('discord.js');

let client = null; 

// This is the entry point for our cloud function 
module.exports = async function (req, res) {
  if (client) {
    res.send("Already initialized");
    return
  }
  initClient(req);
  res.send("Initialized");
};

// This is run once to init the Discord.js client.
function initClient(req) {
  client = new Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES] });

  client.once('ready', () => {
    console.log('Ready!');
  });

  client.on('messageCreate', async (message) => {
    // Ignore bots
    if (message.author.bot) return;

    let command = message.content.split(' ')[0];
    let params = message.content.split(' ').slice(1);

    // Our first command
    switch (command) {
      case '!hello':
        message.channel.send('world!');
        break;
      }
            // you can add more commands in switch cases.
  });

  client.login(req.env['DISCORD_TOKEN']);
}

When the function is first invoked, the init function is executed, registering a Discord bot in the background process of our Appwrite Function. Further invocations of this function will return if the Discord client has already been initialized.

Notice how the client requires an environment variable that provides the Discord API Token? We can add this environment variable in the Appwrite console. Navigate to the setting section of your Function to add the DISCORD_TOKEN environment variable:

Env. Vars

Execute the function, the Discord bot should start and respond to your commands.

Basic command

Adding Commands and Appwrite Integration

Let’s first add a simple moderation command, !warn. We want to be able to warn a user if they break rules, and track how many times they’ve been warned.

First we’ll need to initialize our Appwrite SDK:


let client = null;
let appwrite = null;
let database = null;
...
function initClient(req) {
  ...
  appwrite = new sdk.Client();
  appwrite = appwrite
    .setEndpoint(req.env['APPWRITE_FUNCTION_ENDPOINT'])
    .setProject(req.env['APPWRITE_FUNCTION_PROJECT_ID'])
    .setKey(req.env['APPWRITE_FUNCTION_API_KEY'])
    .setSelfSigned(true); 

  database = new sdk.Database(client);
  ...
}

Note that you’ll need to create an API key with DB access and add new environment variables APPWRITE_FUNCTION_ENDPOINT and APPWRITE_FUNCTION_API_KEY in your Appwrite Function. APPWRITE_FUNCTION_ENDPOINT can be either your appwrite domain (if you are hosting it on a server or VPC) with /v1 appended to the end or it can be http://127.0.0.1/v1 if you are working on the same machine which you host Appwrite.

Then, we need to create a collection to track the number of warnings. You can do this in the Appwrite console UI, but we’re going to take advantage of Appwrite CLI’s ability to deploy collections programmatically. You can define your collection in your appwrite.json, and example can be found in this Gist.

Then, deploy the collection with:

appwrite deploy collection

You can confirm that the deployment worked by checking the Appwrite console. This collection will have three attributes:

  • member: string - Used to store the Discord user’s ID. Note in the Indexes tab, there is an index on this tab that allows IDs to be queried.
  • warnings: integer - Used to track the number of warnings issued toward a certain user.
  • reasons: string[] - Array of strings that track why a user was warned.

We will query this collection when a user is warned.

Warning Collectiom

To register a command in our Discord bot, add the following case to the switch statement in src/index.js:

case '!warn':
  if (message.member.permissions.has('MANAGE_MESSAGES')) {
    let member = message.mentions.members.first().user;
    let reason = params.slice(1).join(' ').trim();
    let warnings = 0;
    if (!member || !reason) {
      message.channel.send("The command should be formatted as: `!warn <@member> <reason>`");
      return
    }

    try {
      let { documents, total } = await database.listDocuments(req.env['COLLECTION_ID'], [sdk.Query.equal("member", member.id)], 1);
      if (total < 1) {
        await database.createDocument(req.env['COLLECTION_ID'], 'unique()',
          {
            "member": member.id,
            "warnings": 1,
            "reasons": [reason]
          });
        warnings = 1;
      }
      else {
        id = documents[0]["$id"]
        warnings = documents[0]["warnings"] + 1;
        const reasons = documents[0]["reasons"].concat([reason]);
        await database.updateDocument(req.env['COLLECTION_ID'], id, {
          "warnings": warnings,
          "reasons": reasons
        });
      }
    }
    catch (e) {
      message.channel.send("Something broke when logging warning to Appwrite!");
      console.log(e);
      return;
    }

    let warnEmbed = new MessageEmbed()
      .setColor('#ff0000')
      .setTitle('Warn')
      .setDescription(`${member.username} has been warned for ${reason}. ${member.username} has been warned ${warnings} times!`);
      message.channel.send({ embeds: [warnEmbed] });

  }
  else {
    message.channel.send("You don't have permission to use !warn");
  }

With this addition, you can warn a user with a command! Note how it displays the number of times the user has been warned.

Warn Command

Let’s modify the !warn command further by adding a check to ban a user after 3 warnings with the following conditional statement:

if (warnings >= 3) {
    message.mentions.members.first().ban();
    message.channel.send(`${member.username} has been banned for 3 warnings!`);
}

See the ban hammer in action:

Tripple Warning Ban

Lastly, let's add a command to view past warnings called !record. Like before, we will register a new command that fetches the number of past warnings they have received:

case '!record':
  if (message.member.permissions.has('MANAGE_MESSAGES')) {
    let member = message.mentions.members.first().user;
    let warnings = 0;
    if (!member) {
      message.channel.send("The command should be formatted as: `!record <@member>`");
      return
    }

    try {
      let { documents, total } = await database.listDocuments(req.env['COLLECTION_ID'], [sdk.Query.equal("member", member.id)], 1);
      if (total < 1) {
        warnings = 0
      }
      else {
        id = documents[0]["$id"]
        warnings = documents[0]["warnings"];
      }
    }
    catch (e) {
      message.channel.send("Something broke while trying to fetch past warnings from Appwrite!");
      return;
    }

    let recordEmbed = new MessageEmbed()
      .setColor('#00ff00')
      .setTitle('Warning Record')
      .setDescription(`${member.username} has been warned ${warnings} times!`);
    message.channel.send({ embeds: [recordEmbed] });
  }
  else {
    message.channel.send("You don't have permission to use !record");
  }

When you type !record followed by a mention, it will display the number of past warnings received by a particular user.

View Warnings

Debugging Tips

There’s a few debugging tips I’d like to mention if you’re having trouble following this post:

  • Try running a Discord bot outside Appwrite first. This serves as a sanity check to isolate the source of error.
  • Check to make sure your Appwrite Function has access to all intended environment variables by returning them in a JSON object using res.json().
  • Write some test Node.js scripts to try the Appwrite SDK functions that you’re unfamiliar with before putting them into a Discord bot.

Final Remarks

The Discord bot shown today is a small proof of concept to get you started with Appwrite and Discord.js. Appwrite’s many services and server-side SDKs can gracefully cover many of the common needs of a Discord bot:

  • Store and fetch user files and images with Appwrite’s scalable Storage service.
  • Custom emote commands using Appwrite’s image manipulation APIs.
  • Manage user information using Appwrite’s Database service.
  • Write web and mobile apps for users to managed their saved files and emotes.

Since Appwrite is self-hosted and open-source, you’ll also know your user data is in safe hands. If you’re thinking about writing a Discord bot, I highly recommend trying Appwrite.

Appwrite is designed with flexibility in mind. While Appwrite Functions is really designed to run headless cloud functions that are not persistent like Firebase Cloud Functions or AWS Lambda, the functions executor can be used to do so much more — like running a persistent Discord bot in the underlying executor. To learn more about how the Appwrite Function runtimes work under the hood and see performance benchmarks, you can take a peek at our blog post.

📚 Learn more You can use the following resources to learn more and get help: