DEV Community 👩‍💻👨‍💻

Ray
Ray

Posted on • Updated on

Release 0.4, the Halfway Point

Phew! It's been kind of a wild weekend, and a wild previous week. Unfortunately, this blog post was meant to be written at the end of last week, not the beginning of this week. However, life and work have their ways of messing with our plans.

I decided to take on a feature request on the Mousehunt Timer Bot and have been plugging away at it.

The process is similar to the code in the other commands, it will include a way to search the agiletravels database for the correct item, then print a list of items that can be found in it for the user to peruse. Seems pretty easy right?

The first step will be to take a look at other commands' code and think about how I can write a command that looks and acts like them. Then I need to look into how the bot displays the information (which I have done when I fixed a bug previously) and set up the correct message to display.

This will be another living document that I'll update as I work on this issue. Please stay tuned!

Session One

In my first work session, I started by laying out the process for creating this command.

  1. Research the other commands and figure out how the internals of the discord bot really work - get to know the code.

  2. Set up my command with copy-pasted code from a different command to show that I can at least get the bot to spit out information from my new file.

  3. Write the actual command, taking code structure and design cues from the code of the other commands.

  4. Bug test.

  5. Write tests for my command and perform pull request.

  6. Review critique on the PR and get the issue closed.

Next, I worked on points 1 and 2. the goal of this session was to simply get the bot to spit out information that I coded exactly as I said above.

I decided to copy over the code from the next command because it seemed like the most straightforward - it required no arguments from the user so I could quickly get to know how a discord bot truly works.

R4_Session_1_1

So how does a discord bot's commands work? Basically, in your index.js file (here called MHTimer.js), you create a collection and then just populate it with the contents of the commands folder using the following code. It's actually quite simple.

const Discord = require('discord.js');
const client = new Client({ disabledEvents: ['TYPING_START'] });
client.commands = new Collection();
 const commandFiles = fs.readdirSync('src/commands').filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
    try {
        const command = require(`./commands/${file}`);
        if (command.name) {
            if (typeof(command.canDM) === 'undefined') {
                command.canDM = true;
                Logger.log(`Set canDM to true for ${command.name}`);
            }
            if (command.initialize) {
                command.initialize().catch((err) => {
                    Logger.error(`Error initializing ${command.name}: ${err}`);
                    throw err;
                });
            }
            client.commands.set(command.name, command);
        } else {
            Logger.error(`Error in ${file}: Command name property is missing`);
        }
    } catch (e) {
        Logger.error(`Could not load ${file}:`, e);
    }
}
Logger.log(`Commands: Loaded ${client.commands.size} commands: ${oxfordStringifyValues(client.commands.map(command => command.name))}`);
Enter fullscreen mode Exit fullscreen mode

Please be aware that this code is from MHTimerBot and is not at all universally applicable. See the Discord docs for more information.

This session was pretty short so I also decided to figure out what the author of the issue had in mind when they specified "Can use the loot nicknames used by ifind."

const all_loot = getLoot(searchString, message.client.nicknames.get('loot'));
        if (all_loot && all_loot.length) {
            // We have multiple options, show the interactive menu
            urlInfo.qsParams = opts;
            sendInteractiveSearchResult(all_loot, message.channel, formatLoot,
                ['dm', 'group'].includes(message.channel.type), urlInfo, searchString);
            theResult.replied = true;
            theResult.success = true;
            theResult.sentDM = ['dm', 'group'].includes(message.channel.type);
Enter fullscreen mode Exit fullscreen mode

I believe this is the code block they're referring to when they said that so I'll try to incorporate that when I'm writing the command in my second session.

Session Two

I started off the second session with a bit of a roadblock.

Ending the last session on what was basically a baseline command may not have been a mistake but it did mean I had a lot of work to get done. The roadblock came from not really feeling like I knew how the discord bot operated. After fumbling about for half an hour trying to figure out how to write my command by just staring at the others, I decided to map out exactly what my command needed to do, and where, in the code, it needed to go.

message -> 

getConvertibles() [match search term with database items] -> 

formatConvertibles() [get possible items and quantity, display in columns]
Enter fullscreen mode Exit fullscreen mode

This is a rough line I want my command to go through. I also mirrors the line the find command for mice and loot takes.

After figuring this out, I could start on my coding.

Main code from whatsin.js

const theResult = new CommandResult({ message, success: false, sentDM: false });
    let reply = '';
    const opts = {};
    const urlInfo = {
        qsParams: {},
        uri: 'https://agiletravels.com/converter.php',
        type: 'convertible',
    };
    if (!tokens)
        reply = 'I just cannot find what you\'re looking for (since you didn\'t tell me what it was).';
    else {
        const searchString = tokens.join(' ').toLowerCase();
        const all_convertibles = getConvertibles(searchString, message.client.nicknames.get('mice'));
        if (all_convertibles && all_convertibles.length) {
            // We have multiple options, show the interactive menu
            urlInfo.qsParams = opts;
            sendInteractiveSearchResult(all_convertibles, message.channel, formatConvertibles,
                ['dm', 'group'].includes(message.channel.type), urlInfo, searchString);
            theResult.replied = true;
            theResult.success = true;
            theResult.sentDM = ['dm', 'group'].includes(message.channel.type);
        }
Enter fullscreen mode Exit fullscreen mode

This is pretty close to what the find.js or ifind.js looks like. Since what I'm trying to do is work with the same api that command is in order to do almost the exact same thing, I believed I could reuse a lot of that code.

I had a few setback this session, but most were easy to deal with. The only one that gave me trouble was resolving in issue with the .map() function. I was accidentally using a filter when I didn't need to.

Excerpt from formatConvertibles()

const converter = results
        .map(convertible => {
            return {
                item: convertible.item.substring(0, 20),
                average_qty: convertible.total_items / convertible.total,
            };
        });
    const order = ['item', 'average_qty'];
    const labels = { item: 'Item', average_qty: 'Average Qty' };
Enter fullscreen mode Exit fullscreen mode

At the end of the second session, I feel very confident and am ready to begin work on the tests next session.

Session 3

This is the last session, or at least, that last planned session.

All that this session consisted of was me cleaning up the code a bit and sending the pull request off to the main developers.

That said, if there are any changes they need me to make, or if they ask me to add tests, I will document the process here.

r4_Session_3_01

So what we need to do following this is 2 things: follow-up on creating the tests and fix our line endings. Two fairly simple things.

The code for the tests is written with sinon.js, a framework that I'm not terribly familiar with but shouldn't pose too much of a problem.

That said, I had a little trouble figuring out how I should go about doing my tests. Should I add tests in the mhct-lookup file to cover the new functions I built in the sister file? Or should I create my own test file for my new command? I figured I should do both.

Then, after working with the test files for hours I figured out that when I mock a function in one file, and it's mocked in another, it completely demolishes the second file and breaks the test. I needed to either scale down my testing or find some kind of workaround.

Top comments (0)

Want to rep DEV and be comfy at the same time?

Check out our classic DEV shirt — available in multiple colors.