Write a Plugin

In this article, we will write a plugin. Specifically, we will write a plugin that lets users add a friend code1 for their nickname, and look up the friend codes of others. The friend codes will be persisted to disk, and we'll let users remove their friend code should they decide they do not want to be friendly.

For this article, we are going to be implementing both the persistence logic and IRC interface logic together. This is normally bad design, as the plugin should really only be plugging some service provided in IRC-agnostic code with IRC. We"ll look at refactoring this out in another article2.

For persistence, we'll be using the Dirty DB. Don't worry if you've never heard of it, it's a trivial to use database. It"s an extremely basic key-value database that you've probably never heard of, but is perfect for our use case. Just imagine it as a single persistent object.

Project Setup

Depending on whether you want to share your plugin with others or not, there are two ways to get started - locally or externally. You can always move from local to external.

External (Public) Plugin

If you want to contribute to the Tennu platform with a plugin for others to install, setting up an external plugin is the way to do so.

An external plugin is a plugin that is an npm package. So, to start out, create a new npm project. Create a directory for the project, create a code repo (probably git) both locally and wherever you store remote repoes (probably GitHub), and run npm init filling out the questions as appropriate. The package name should be the name of your plugin prefixed with "tennu-". For this plugin, it'd be tennu-friend-code. A good name for the main file is plugin.js.

The next step is to create plugin.js, and the rest of the article will discuss that. Live testing and plublishing your plugin are other things you'll want to do, explained in the following paragraphs.

When you want to live-test the plugin, you have a few options.

  • If your plugin is a single file, you can install the plugin locally into your bot and test there.
  • If the plugin is more than a single file, you can install with npm link.
  • If you want to test in a custom bot for your plugin, add tennu as a devDependency, and add a test-bot directory with the config.json file. Just make sure to add the directory to your .npmignore file so you don"t publish it.

When your plugin is well tested and ready to be published, change the version to 1.0.0 in the package.json, and npm publish. Then you can install it like any public plugin.

Local (Private) Plugin

If, for whatever reason, you do not want to publish your plugin for others to use, follow these instructions.

  1. Create a directory in your bot's project tennu_plugins if you have not already done so.
  2. Create a file that is the name of your plugin. In our case, friend-code.js.
  3. Write the plugin in the created file.
  4. Plugin Skeleton

    Now that we have a file to edit your plugin in, we should start with a skeleton. The one shown on the Getting Started tutorial is as good as any, so let us start with that.

    // Initialization of the node module.
    
    var TennuPluginName = {
        init: function (client, imports) {
            // Initialization of the plugin.
    
            return {
                exports: {
                    // Exported properties.
                },
    
                handlers: {
                    "!command": function (command) {
                        // Handle the command
                    }
                },
    
                help: {
                    "command": [
                        "!command <command>",
                        " ",
                        "Help info about command."
                    ]
                },
    
                commands: ["command"]
            }
        }
    };
    
    module.exports = TennuPluginName;

    First Edits

    We"ll want to do a few things.

    • Replace TennuPluginName with the name of our plugin. Let us call it FriendCode.
    • Determine if we want to export anything. Now, we could export a function to let other plugins look up friend codes, but YAGNI, so we will export nothing, and remove the property from our returned plugin instance.
    • What handlers and commands we need. In this case, we need three command handlers and a handler for the "error" event3 to cleanup.

      • !addfriendcode - A command to add a friend code.
      • !delfriendcode - A command to remove a friend code.
      • !friendcode - A command to look up a friend code.

      We need to add them to the commands array, and add a handler and help entry for each command.

    Putting that altogether, and we get a plugin structure that looks like this.

    // initialization of the node module
    
    var FriendCode = {
        init: function (client, imports) {
            // Initialization of the plugin.
    
            return {
                handlers: {
                    "!friendcode": function (command) {
                        // Handle the command
                    },
    
                    "!addfriendcode": function (command) {
                        // Handle the command
                    },
    
                    "!delfriendcode": function (command) {
                        // Handle the command
                    },
    
                    "error": function (error) {
                        // deinitialization code.
                    }
                },
    
                help: {
                    "friendcode": [
                        "friendcode <nickname>",
                        " ",
                        "Help info about command."
                    ],
    
                    "addfriendcode": [
                        "addfriendcode <friend-code>",
                        " ",
                        "Help info about command."
                    ],
    
                    "delfriendcode": [
                        "delfriendcode",
                        " ",
                        "Help info about command."
                    ]
                },
    
                commands: ["friendcode, addfriendcode, delfriendcode"]
            };
        }
    };
    
    module.exports = FriendCode;

    Initialization & Cleanup

    Next up, let"s set up the initialization code. There are two points of initialization: One more the module and one for the plugin. Anything that doesn"t change if you were running two bots simultaneously in the same program (which is possible) is placed before FriendCode is defined. Anything that is bot specific goes in the init function before you return the plugin.

    The only module level initialization we have to do is importing in the Dirty DB constructor. First make sure you've added it to your dependencies (npm install dirty --save4) and then add this line to the top of your plugin: var Dirty = require("dirty");.

    But there"s also a utility function format on the built-in util module. We are also going to require it to make our string handling code nicer.

    In the plugin initialization, we need to create an instance of the Dirty DB, and to do so, we need a location of the database. Since we don"t want to assume a location for the database, we'll ask users to add it to their config file. We can then read the value in the config file with client.config("config-key-name"). As per a convention in Tennu, we call this config value %plugin-name%-database or in our case, friend-code-database.

    If you haven"t read the documentation for the Dirty DB, you create one by calling Dirty(relativePath), and if the database exists, it'll use that database. Otherwise, it'll create that file and use it as the database. If for whatever reason the program doesn"t have access to the file it's trying to create, such as permissions errors or the directory doesn"t exist, it'll throw an error. We won"t worry about handling such errors here.

    Finally, we are supposed to close the database connection when we are done with it. This is done via the close method.

    Given all of this, let"s write it into code for our plugin.

    var Dirty = require("dirty");
    var format = require("util").format;
    
    var FriendCode = {
        init: function (client, imports) {
            var databaseLocation = client.config("friend-code-database");
            var db = Dirty(databaseLocation);
    
            return {
                handlers: {
                    "!friendcode": function (command) {
                        // Handle the command
                    },
    
                    "!addfriendcode": function (command) {
                        // Handle the command
                    },
    
                    "!delfriendcode": function (command) {
                        // Handle the command
                    },
    
                    error: function (_) {
                        db.close();
                    }
                },
    
                help: {
                    "friendcode": [
                        "friendcode <nickname>",
                        " ",
                        "Help info about command."
                    ],
    
                    "addfriendcode": [
                        "addfriendcode <friend-code>",
                        " ",
                        "Help info about command."
                    ],
    
                    "delfriendcode": [
                        "delfriendcode",
                        " ",
                        "Help info about command."
                    ]
                },
    
                commands: ["friendcode, addfriendcode, delfriendcode"]
            };
        }
    };
    
    module.exports = FriendCode;

    Handlers

    Alright, we have a database open, but our commands do not do anything. We need to implement them. But what information does each command need?

    The !friendcode command needs to know where to send the message and what argument is passed to it. You can either find out which channel (or nickname if private message) a command was sent using the "channel" property of the command object passed to the handler, or you could just return your response as one of the Response formats. We"ll be returning the String response type for this and the rest of our commands. For the argument, it'll be on the "args" property of the command object. Since a nickname is only ever one word, it'll be the first arg, or command.args[0].

    Both !addfriendcode and !delfriendcode need to know who is sending the command. This is easily found out with the "nickname" property on the command object. !addfriendcode also takes a friend code. While friend codes usually do not contain spaces, we'll be general and allow them5. Tennu doesn"t send the entire arguments as a simple string, so to get a multi-word argument, you slice the number of single-word arguments precede it and then join with a space. In our case, that would be client.args.slice(0).join(" "), but since we know that .slice(0) returns a copy of the array and .join(" ") doesn"t mutate the array, we can leave it out when there are no single-word arguments.

    Then, with this information, we need to set, get, or delete the proper information, and respond with the proper message. We should also make sure to check to make sure the user has a friend code before deleting it, and report that they already don"t have one if they do not.

    We also add the proper help for these commands.

    var Dirty = require("dirty");
    var format = require("util").format;
    
    var FriendCode = {
        init: function (client, imports) {
            var databaseLocation = client.config("friend-code-database");
            var db = Dirty(databaseLocation);
    
            return {
                handlers: {
                    "!friendcode": function (command) {
                        var friend = command.args[0];
    
                        var friendcode = db.get(friend);
    
                        if (friendcode) {
                            return format("Friendcode for %s: %s", friend, friendcode);
                        } else {
                            return format("Friendcode for %s not found.", friend);
                        }
                    },
    
                    "!addfriendcode": function (command) {
                        var code = command.args.join(" ");
                        var friend = command.nickname;
    
                        db.set(friend, code);
                        return format("%s: Friend code set!", friend);
                    },
    
                    "!delfriendcode": function (command) {
                        var friend = command.nickname;
    
                        if (db.get(friend)) {
                            db.rm(friend);
                            return format("%s: Friend Code deleted.", friend);
                        } else {
                            return format("%s: You already have no friend code.", friend);
                        }
                    },
    
                    error: function (_) {
                        db.close();
                    }
                },
    
                help: {
                    "friendcode": [
                        "friendcode <nickname>",
                        " ",
                        "Lookup the friendcode for that nickname."
                    ],
    
                    "addfriendcode": [
                        "addfriendcode <friend-code>",
                        " ",
                        "Add or change your friend code to the bot."
                    ],
    
                    "delfriendcode": [
                        "delfriendcode",
                        " ",
                        "Remove your friend code from the bot."
                    ]
                },
    
                commands: ["friendcode, addfriendcode, delfriendcode"]
            };
        }
    };
    
    module.exports = FriendCode;

    Final Notes

    At this point, your plugin is complete. Add "friend-code" to your plugins list, and add a new property to your config.json, "friend-code-database". If you aren't sure where to put your database, make a directory databases in your bot's directory and make it "databases/friend-codes.db". Then start your bot, and enjoy the new functionality.

    Footnotes

    1. This example was choosen for the Brave Frontier IRC channel, through other services also use friend codes, such as Nintendo.
    2. And an article after that may even show how to abstract the friend code logic into any per-user lookup command triple generating module.
    3. The "error" event in this case is what is sent by the IRC servers to tell you that you have ended your connection to it. It is not an actual error. Blame the original IRC specification author for this.
    4. If you don"t have a package.json, leave off the --save parameter.
    5. A good example of where allowing more than one word for the friend code is in Brave Frontier where some people have multiple accounts and thus multiple friend codes, so they could send !addfriendcode BF: 000000, JP: 111111.