arrow-left

All pages
gitbookPowered by GitBook
1 of 5

Loading...

Loading...

Loading...

Loading...

Loading...

Koa Authentication

In this example we'll be using Enmap to store user data in order to authenticate users on a simple Koa application. In order to make this secure, we'll be using bcrypt to encrypt the passwords, so of course they will not be plain text in the database.

hashtag
Requirements

We'll be using koa as a web server module, along with koa-session in order to store the session information. Additionally, for ease of use, koa-router is used to simplify the route code.

This tutorial uses koa-session which, by default, is insecure since it stores the entire session data in a browser cookie. This means the password, though encrypted, would be availble in the cookie, and easy to spoof. There are many session stores available for different storage system, but using them is beyond the scope of this example.

To install those requirements, run the following in a new, empty folder for your project:

Once all of those are installed, we're ready to start! We're going to create an index page, login page, and one "protected" page that requires login. Let's start with the top of the file, which is all the required modules:

So now we have all the basics necessary.

hashtag
Account Creation Function

Let's create a few functions specifically for the login features, related to enmap. First, a function to create a new user:

This function takes in the following arguments:

  • username which obviously is self-explanatory: it's the username entered during login.

  • name is the "full name" or "display name", for a friendly display on the page or an email.

  • plainpw

The function first checks if the username exists and returns an error if it does. It then generates a salted, hashed version of the password which it stores in the database. Don't let the name fool you, the password is not "encrypted", which implies that it can be decrypted. Instead, it's a "cryptographic hash functions", a unidirectional function that cannot be undone. The only way to verify that a password is correct is to re-hash it again and compared the hashes.

Once the hashed password is obtained, the user itself is stored in the database with all 4 incoming arguments except the password which is the hashed version.

hashtag
Login Function

The login function takes in the username and the incoming plain password and verifies that the hashed version corresponds with the one stored in the database.

An important point here is that this function returns a promise in all cases. If the username doesn't exist or the password is blank, a false response is returned in a promise. Otherwise, the response of bcrypt's compare function is returned. This function returns true if the passwords match, false if they do not.

hashtag
Defining some app settings

There's a few configuration items we need to take care of. First off, the session settings:

Then we need to setup how Koa will handle rendering EJS pages. This is one pretty awesome thing about Koa, that this can be setup automatically and globally, but don't let me gush all over this!

hashtag
Basic Routes

So let's establish our "routes", which is the pages that can be accessed by the browser. With the help of the Router, this can be really straightforward.

Then we have the login route, which does a lot of the bulk of our work. It checks for login, and adds everything it needs to the session in Koa if the authentication is successful:

Let's also create a logout function, that simply destroys the current session and returns the user to the index:

This one is pretty straightforward, so I don't think I need to get into the details, right? ;)

Lastly, we have the route for our "private" page. the one that only works if you're logged in. Now, there are "better" ways to establish protected routes, but let's go with the simplest one for now. We're just going to check for the logged property of the session to determine if the user is logged in.

hashtag
The End of the File

At the very end of our file we still have a bit of stuff to add. Mainly starting the server, but also telling the parser and routers to initialize. This would be how it's done:

hashtag
Creating Templates

While templating is slightly beyond the scope of an authentication tutorial, I would be remiss to ignore the fact that logging in without a page would be... let's say a little hard.

Koa's EJS templating configuration, that we did above, means that templates need to appear in the views folder. There will be a few template files:

  • template.html will be the "main" template. It will have the header, footer, and whatever else we want to appear on every page.

  • index.html will be the main page everyone can access.

  • login.html

Let's start with the template.html file.

The <%- body %> tag is where the contents of the other pages appear.

The index.html is just some welcome thingy that we aren't concerned about:

Then we have the login.html page which has our form. The form simply posts back to itself, so it'll trigger the .post endpoint:

And, finally, the secret.html page we've all been waiting for. Nothing that you haven't heard before, though:

With all said and done, . Want a more "advanced" version of this project? Check out .

is the password desired for this account. It has to come in as plain text, obviously, in order to be properly saved in the database.
  • admin is a boolean value representing whether the user should be administrator. If creating something like a blog, only an administrator could create other administrators. It's false by default.

  • contains the login page and form
  • secret.html has a little secret about something.

  • This should be your projectarrow-up-right
    My full blogging platform, Koarrotsarrow-up-right
    npm i enmap better-sqlite-pool koa koa-session koa-ejs koa-bodyparser koa-router bcrypt
    // Native Imports
    const { sep, resolve, join } = require("path");
    
    // Enmap Imports
    const Enmap = require("enmap");
    const users = new Enmap({ name: "users" });
    
    // Bcrypt's hashing system
    const bcrypt = require("bcrypt");
    
    // Koa Imports
    const Koa = require("koa");
    // Koa's EJS renderer for HTML views.
    const render = require("koa-ejs");
    // The Body Parser to accept incoming form data and file uploads.
    const parser = require("koa-bodyparser");
    // The default "sessions" support.
    const session = require("koa-session");
    // Koa's get/post/etc router to simplify routes.
    const Router = require("koa-router");
    // Initializing the main components.
    const router = new Router();
    const app = new Koa();
    
    // Define the data directory for templates (views).
    const dataDir = resolve(`${process.cwd()}${sep}`);
    const newuser = (username, name, plainpw, admin = false) => {
      if (users.has(username)) throw Error(`User ${username} already exists!`);
      bcrypt.hash(plainpw, 10, (err, password) => {
        if (err) throw err;
        users.set(username, {
          username, name, password, admin, created: Date.now()
        });
      });
    };
    const login = (username, password) => {
      const user = this.users.get(username);
      if (!user) return new Promise(resp => resp(false));
      if (!password) return new Promise(resp => resp(false));
      return bcrypt.compare(password, user.password);
    };
    app.keys = ['some secret hurr'];
    app.use(session(app));
    render(app, {
      root: join(__dirname, 'views'),
      layout: 'template',
      viewExt: 'html',
      cache: false,
      debug: true
    });
    router.get("/", async (ctx, next) => {
      // This is our static index page. It will render the views/index.js file
      await ctx.render("index");
    });
    // we obviously need a route to show the login page itself, too!
    router.get("/login", async (ctx) => {
      await ctx.render("login");  
    });
    
    router.post("/login", async (ctx) => {
      // Fail if there is no username and password. 
      // This relies on koa-bodyparser
      if (!ctx.request.body.username || !ctx.request.body.password) {
        ctx.throw(400, "Missing Username or Password");
      }
      // Use our login function to verify the username/password is correct
      const success = await login(ctx.request.body.username, ctx.request.body.password);
      if (success) {
        // get the user's information
        const user = users.get(ctx.request.body.username);
        // Set all our session parameters:
        ctx.session.logged = true;
        ctx.session.username = ctx.request.body.username;
        ctx.session.admin = user.admin;
        ctx.session.name = user.name;
        // Save the session itself. This sets the cookie in the browser, 
        // as well as save into the sessions in memory.
        ctx.session.save();
        console.log(`User authenticated: ${user.username}`);
        // Once logged in, redirect to the secret page.
        ctx.redirect("/secret");
      } else {
        console.log("Authentication Failed");
        // Throw if the above login returns false.
        ctx.throw(403, "Nope. Not allowed, mate.");
      }
    });
    router.get("/logout", async (ctx) => {
      ctx.session = null;
      ctx.redirect("/");
    });
    router.get("/secret", async (ctx) => {
      if(!ctx.session.logged) ctx.throw(403, "Unauthorized to view this page");
      await ctx.render("secret");
    });
    app
      .use(parser())
      .use(router.routes())
      .use(router.allowedMethods());
    
    app.on('error', (err, ctx) => {
      console.error('server error', err, ctx)
    });
    
    app.listen(3000);
    views/template.html
    <!DOCTYPE html>
    <html lang="en">
    	<head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    		<title>A Page</title>
    	</head>
      <body>
        <nav>
         <ul>
          <li><a href="/">Home</a></li>
          <li><a href="/login">Login</a></li>
          <li><a href="/secret">Secret</a></li>
         </ul>
        </nav>
      <%- body %>
      </body>
    </html>
    views/index.html
    <h1>Index</h1>
    <p>Lorem Ipsum Carrots</p>
    views/login.html
    <h1>Login</h1>
    <form method="POST">
     <p>Username: <input type="text" name="username" id="username"></p>
     <p>Password: <input type="password" name="password" id="password"></p>
     <p><button type="submit">Login</button></p>
    </form>
    views/secret.html
    <h1>Some Secret</h1>
    <p>Han Shot First.</p>
    

    D.js Mod Logs

    This page describes using 2 different enmaps in tandem using autonum and a reference array.

    An interesting problem in javascript is that having an array of objects can be quite the ordeal. A lot of things you want to do require functions and loops, bleh. So, where Enmap is meant to be easier to use, this is an area where it's still a bit hard to handle things.

    But there's a solution. If Enmap isn't enough, how about **TWO ** Enmap???? So yeah, we're going to be using one enmap to store user data, and another to store "warnings", that is to say, moderation actions stored as objects.

    When we add a new element to the actions enmap, we'll be adding a reference to that new entry in the user enmap, via the autonum feature.

    If you've read about databases a bit, you might have heard about "autonum" and "automatic indexes" before, and this is exactly it. Alright let's get down to brass tax!

    hashtag
    The Actions Enmap

    The actions enmap will be using autonum to generate new unique numbers that we'll be able to reference later. The setup is very typical, to begin. We'll attach these things to the discord.js client to keep things simple.

    When we want to create a new action, it's a simple act of using autonum to get a key automatically. Let's do a simple warning:

    So what this does is twofold: it gives us an ID, as well as save the data for this new warning in the Enmap.

    hashtag
    The User Enmap

    You might already have one of those enmaps lying around, but if you don't, the deal's pretty much the same (because enmap is simple!):

    We of course need to have some properties in there, and this will be done using ensure(). This is very similar to our Points system, and it can be done on user join (guildMemberAdd) and/or in the message event. Both would be fine:

    hashtag
    The Commands

    So now we have everything ready to create a simple warn command that will use the above setup to create what we need:

    So how does this help us in the end? If you look at the warnings, you only get a bunch of IDs, right? Well, we can most definitely do some array magic in order to get these proper values... Yeah let's do that. Abracadabra!

    Now go have fun an explore the endless possibilities of this system :D

    client.modActions = new Enmap({
        name: 'actions'
    });
    const newActionId = client.modActions.autonum;
    client.modActions.set(newActionId, {
        user: target.id,
        guild: message.guild.id,
        type: 'warning',
        moderator: message.author.id,
        reason: "Don't do it again!",
        when: Date.now()
    });
    client.userProfiles = new Enmap({
        name: 'userProfiles'
    });
    client.userProfiles.ensure(message.author.id, {
        id: message.author.id,
        guild: message.guild.id,
        totalActions: 0,
        warnings: [],
        kicks: []
    });
    if (command === 'warn') {
        // get a target user
        const target = message.mentions.users.first();
        // remove the mention from the message, join for a reason
        const reason = args.slice(1).join(" ");
        const newActionId = client.modActions.autonum;
        client.modActions.set(newActionId, {
            user: target.id,
            guild: message.guild.id,
            type: 'warning',
            moderator: message.author.id,
            reason: reason,
            when: Date.now()
        });
        // Push the action to the user's warnings
        client.userProfiles.push(target.id, newActionId, 'warnings');
        client.userProfiles.inc(target.id, 'totalActions');
        // then send some message or embed or whatever
        message.channel.send(`${target} was warned for '${reason}'`);
    }
    if (command === 'mywarns') {
        const warnIDs = client.userProfiles.get(message.author.id, 'warnings');
        const warnData = warnIDs.map(id => client.modActions.get(id));
        // have fun displaying this wooh!
        message.reply(`You have ${warnIDs.length} warns, buddy!`);
    }

    D.js Per-Server Settings

    This example uses a very, very simple bot made in discord.js to demonstrate how easily Enmaparrow-up-right can be used to create a per-server configuration system.

    Remember to follow the Installation Instructions before running any of this!

    hashtag
    Initializing

    Our first task is of course to initialize the enmap correctly. In this case, we're attaching the settings to our client object so we can use it in different commands.

    hashtag
    Events Setup

    hashtag
    Message Event

    The main event for our bot, where messages are received. Any error here will probably crash the bot on every message received, so be careful!

    hashtag
    Command to set configurations

    hashtag
    Command to show the configuration

    // start discord.js init
    // config with token and prefix.
    const config = require("./config.json"); 
    // Code below supports and is tested under "stable" 11.3.x
    const Discord = require("discord.js");
    const client = new Discord.Client();
    // end discord.js init
    
    // Initialize the server configurations
    const Enmap = require('enmap');
    
    // I attach settings to client to allow for modular bot setups
    // In this example we'll leverage fetchAll:false and autoFetch:true for
    // best efficiency in memory usage. We also have to use cloneLevel:'deep'
    // to avoid our values to be "reference" to the default settings.
    // The explanation for why is complex - just go with it.
    client.settings = new Enmap({
      name: "settings",
      fetchAll: false,
      autoFetch: true,
      cloneLevel: 'deep',
      autoEnsure: {
        prefix: "!",
        modLogChannel: "mod-log",
        modRole: "Moderator",
        adminRole: "Administrator",
        welcomeChannel: "welcome",
        welcomeMessage: "Say hello to {{user}}, everyone!"
      }
    });
    
    client.on("guildDelete", guild => {
      // When the bot leaves or is kicked, delete settings to prevent stale entries.
      client.settings.delete(guild.id);
    });
    
    client.on("guildMemberAdd", member => {
      // This executes when a member joins, so let's welcome them!
    
      // First, ensure the settings exist
      client.settings.ensure(member.guild.id, defaultSettings);
    
      // First, get the welcome message using get: 
      let welcomeMessage = client.settings.get(member.guild.id, "welcomeMessage");
    
      // Our welcome message has a bit of a placeholder, let's fix that:
      welcomeMessage = welcomeMessage.replace("{{user}}", member.user.tag)
    
      // we'll send to the welcome channel.
      member.guild.channels.cache
        .find(channel => channel.name === client.settings.get(member.guild.id, "welcomeChannel"))
        .send(welcomeMessage)
        .catch(console.error);
    });
    client.on("message", async (message) => {
      // This stops if it's not a guild (obviously), and we ignore all bots.
      // Pretty standard for any bot.
      if(!message.guild || message.author.bot) return;
    
      // We get the value, and autoEnsure guarantees we have a value already.
      const guildConf = client.settings.get(message.guild.id);
    
      // Now we can use the values! We stop processing if the message does not
      // start with our prefix for this guild.
      if(message.content.indexOf(guildConf.prefix) !== 0) return;
    
      //Then we use the config prefix to get our arguments and command:
      const args = message.content.split(/\s+/g);
      const command = args.shift().slice(guildConf.prefix.length).toLowerCase();
    
      // Commands Go Here
    });
      // Alright. Let's make a command! This one changes the value of any key
      // in the configuration.
      if(command === "setconf") {
        // Command is admin only, let's grab the admin value: 
        const adminRole = message.guild.roles.cache.find(role => role.name === guildConf.adminRole);
        if(!adminRole) return message.reply("Administrator Role Not Found");
    
        // Then we'll exit if the user is not admin
        if(!message.member.roles.cache.has(adminRole.id)) {
          return message.reply("You're not an admin, sorry!");
        }
    
        // Let's get our key and value from the arguments. 
        // This is array destructuring, by the way. 
        const [prop, ...value] = args;
        // Example: 
        // prop: "prefix"
        // value: ["+"]
        // (yes it's an array, we join it further down!)
    
        // We can check that the key exists to avoid having multiple useless, 
        // unused keys in the config:
        if(!client.settings.has(message.guild.id, prop)) {
          return message.reply("This key is not in the configuration.");
        }
    
        // Now we can finally change the value. Here we only have strings for values 
        // so we won't bother trying to make sure it's the right type and such. 
        client.settings.set(message.guild.id, value.join(" "), prop);
    
        // We can confirm everything's done to the client.
        message.channel.send(`Guild configuration item ${prop} has been changed to:\n\`${value.join(" ")}\``);
      }
      if(command === "showconf") {
        let configProps = Object.keys(guildConf).map(prop => {
          return `${prop}  :  ${guildConf[prop]}`;
        });
        message.channel.send(`The following are the server's current configuration:
        \`\`\`${configProps.join("\n")}\`\`\``);
      }

    Examples

    D.js Points/Currency

    This points bot is simple, but functional. Make sure you've followed the Installation Instructions before doing the following.

    First, you need to create a new persistent Enmap. Here's how it goes:

    That will create a new Enmap under the name of points, and attaches it to the client object so it can be used where ever you have access to the client object.

    hashtag
    Accumulating Points

    The obvious goal of a points system is to accumulate fake internet points and gloat about it. So, of course, that's going to be our first focus. In this example implementation, we will make the points guild-specific, and unusable in DMs. Points will still accumulate even if the user does a command, which simplifies our code a bit.

    Our starting point is a very basic message handler with pre-existing commands - such as what we see in the page on An Idiot's Guide. The code is as such:

    We do have a small caveat - we really don't want to react on Direct Messages, so our whole code will be in a block that checks for that:

    Our very first step is going to be to initialize a new entry in the enmap for any new user - one we haven't received a message from before. This is done using the enmap.ensure(key, defaultvalue) method, which can check if a specific key exists in the Enmap, write it if it doesn't (and return the defaultvalue in this case). Note that our keys take the format of guildid-userid so they're unique to the guild and the user. Also, our data in this case is a complete object, which we'll take advantage of fully.

    There's obviously a few ways we could have done this, including some fancy ternary condition or whatever. I will, however, keep this code as simple to read as possible.

    The following bit is super simple - Enmap has a method to directly increment a value in Enmap even if it's in an object. Pretty clever if I do say so myself!

    circle-info

    Have your own way of incrementing points? No problem! Enmap.math() gives you the ability to add, multiply, and act upon any numerical value or property. To add 10 points, for instance, client.points.math(key, "+", 10, "points") would be used.

    hashtag
    Ding!

    Time to level up! If a user has enough points, they will go up a level. Now we have to do some math here, but don't run off in fear, this one's pretty easy. This is how we calculate the levels:

    This line will calculate the square root of currentPoints then multiplies that result by 0.1 then floors that result for a round number.

    Now we should work out if you've amassed enough points to actually level up, by grabbing the current user's level and comparing them. If the new calculated level is higher, it means the user leveled up and we can deal with that, first by sending them a very annoying mee6-inspired message!

    Lastly, we want to update the score.level value with the new level so throw this under the message.reply.

    So here's the whole thing from top to bottom, with bonus comments!

    hashtag
    Points & Level Commands

    Alright, that's the bulk of the code, you could throw this into your bot and it would work like a charm, however your users wouldn't know how many points, or even their levels, so let's fix that, make a new command called points, which will also show them their level.

    circle-info

    Obviously there's no way for us to know how you're making commands, so again we'll assume you're doing a bot in a single js file. You may need to adjust the code, of course!

    So let's re-iterate our current starting position.

    The points command would look like this:

    hashtag
    We are the champions, my friend!

    Let's finish this off with a very simple leaderboard command that will show the top 10 users in the current guild. For this we'll need to filter the Enmap to only get the users for the current guild, then we'll convert the results to an array, sort that, and keep the first 10 results only.

    circle-info

    We convert to an array because an Enmap, just like its underlying Map structure, is not ordered and thus cannot be sorted. It may seem ordered because it stores by keys, but that's actually a quirk, not a feature.

    So here's our leaderboard command:

    hashtag
    ADDENDUM: Extra Commands!

    const Enmap = require("enmap");
    client.points = new Enmap("points");
    Command with Argumentsarrow-up-right
    client.on("message", message => {
      if (message.author.bot) return;
      // This is where we'll put our code.
      if (message.content.indexOf(config.prefix) !== 0) return;
    
      const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
      const command = args.shift().toLowerCase();
    
      // Command-specific code here!
    });
    client.on("message", message => {
      if (message.author.bot) return;
      if (message.guild) {
        // This is where we'll put our code.
      }
      // Rest of message handler
    });
    client.on("message", message => {
      if (message.author.bot) return;
      if (message.guild) {
        client.points.ensure(`${message.guild.id}-${message.author.id}`, {
          user: message.author.id,
          guild: message.guild.id,
          points: 0,
          level: 1
        });
        // Code continued...
      }
      // Rest of message handler
    });
    client.on("message", message => {
      if (message.author.bot) return;
      if (message.guild) {
        // Let's simplify the `key` part of this.
        const key = `${message.guild.id}-${message.author.id}`;
        client.points.ensure(key, {
          user: message.author.id,
          guild: message.guild.id,
          points: 0,
          level: 1
        });
        client.points.inc(key, "points");
      }
      // Rest of message handler
    });
    const curLevel = Math.floor(0.1 * Math.sqrt(client.points.get(key, "points")));
    if (client.points.get(key, "level") < curLevel) {
      message.reply(`You've leveled up to level **${curLevel}**! Ain't that dandy?`);
    }
    client.points.set(key, curLevel, "level");
    client.on("message", message => {
      // As usual, ignore all bots.
      if (message.author.bot) return;
    
      // If this is not in a DM, execute the points code.
      if (message.guild) {
        // We'll use the key often enough that simplifying it is worth the trouble.
        const key = `${message.guild.id}-${message.author.id}`;
    
        // Triggers on new users we haven't seen before.
        client.points.ensure(`${message.guild.id}-${message.author.id}`, {
          user: message.author.id,
          guild: message.guild.id,
          points: 0,
          level: 1
        });
    
        client.points.inc(key, "points");
    
        // Calculate the user's current level
        const curLevel = Math.floor(0.1 * Math.sqrt(client.points.get(key, "points")));
    
        // Act upon level up by sending a message and updating the user's level in enmap.
        if (client.points.get(key, "level") < curLevel) {
          message.reply(`You've leveled up to level **${curLevel}**! Ain't that dandy?`);
          client.points.set(key, curLevel, "level");
        }
      }
      // Rest of message handler
    });
    client.on("message", message => {
      if (message.author.bot) return;
      if (message.guild) { /* Points Code Here */ }
      if (message.content.indexOf(config.prefix) !== 0) return;
    
      const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
      const command = args.shift().toLowerCase();
    
      // Command-specific code here!
    });
      if (command === "points") {
        const key = `${message.guild.id}-${message.author.id}`;
        return message.channel.send(`You currently have ${client.points.get(key, "points")} points, and are level ${client.points.get(key, "level")}!`);
      }
    if(command === "leaderboard") {
      // Get a filtered list (for this guild only), and convert to an array while we're at it.
      const filtered = client.points.filter( p => p.guild === message.guild.id ).array();
    
      // Sort it to get the top results... well... at the top. Y'know.
      const sorted = filtered.sort((a, b) => b.points - a.points);
    
      // Slice it, dice it, get the top 10 of it!
      const top10 = sorted.splice(0, 10);
    
      // Now shake it and show it! (as a nice embed, too!)
      const embed = new Discord.MessageEmbed()
        .setTitle("Leaderboard")
        .setAuthor(client.user.username, message.guild.iconURL())
        .setDescription("Our top 10 points leaders!")
        .setColor(0x00AE86);
      for(const data of top10) {
        try {
          embed.addField(client.users.cache.get(data.user).tag, `${data.points} points (level ${data.level})`);
        } catch {
          embed.addField(`<@${data.user}>`, `${data.points} points (level ${data.level})`);
        }
      }
      return message.channel.send({embed});
    }
      if(command === "give") {
        // Limited to guild owner - adjust to your own preference!
        if(message.author.id !== message.guild.ownerID) 
          return message.reply("You're not the boss of me, you can't do that!");
    
        const user = message.mentions.users.first() || client.users.get(args[0]);
        if(!user) return message.reply("You must mention someone or give their ID!");
    
        const pointsToAdd = parseInt(args[1], 10);
        if(!pointsToAdd) 
          return message.reply("You didn't tell me how many points to give...")
    
        // Ensure there is a points entry for this user.
        client.points.ensure(`${message.guild.id}-${user.id}`, {
          user: message.author.id,
          guild: message.guild.id,
          points: 0,
          level: 1
        });
    
        // Get their current points.
        let userPoints = client.points.get(`${message.guild.id}-${user.id}`, "points");
        userPoints += pointsToAdd;
    
    
        // And we save it!
        client.points.set(`${message.guild.id}-${user.id}`, userPoints, "points")
    
        message.channel.send(`${user.tag} has received **${pointsToAdd}** points and now stands at **${userPoints}** points.`);
      }
    
      if(command === "cleanup") {
        // Let's clean up the database of all "old" users, 
        // and those who haven't been around for... say a month.
    
        // Get a filtered list (for this guild only).
        const filtered = client.points.filter( p => p.guild === message.guild.id );
    
        // We then filter it again (ok we could just do this one, but for clarity's sake...)
        // So we get only users that haven't been online for a month, or are no longer in the guild.
        const rightNow = new Date();
        const toRemove = filtered.filter(data => {
          return !message.guild.members.cache.has(data.user) || rightNow - 2592000000 > data.lastSeen;
        });
    
        toRemove.forEach(data => {
          client.points.delete(`${message.guild.id}-${data.user}`);
        });
    
        message.channel.send(`I've cleaned up ${toRemove.size} old farts.`);
      }