Skip to content

Commit

Permalink
Merge pull request publiclab#42 from publiclab/modularity
Browse files Browse the repository at this point in the history
Shift to behavior based architecture
  • Loading branch information
ryzokuken authored Jun 20, 2017
2 parents d001bff + 1f3d955 commit 6fb31e6
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 123 deletions.
7 changes: 7 additions & 0 deletions behaviors/greet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const Behavior = require('../models/behavior');

const greetAction = (channel, username, botNick) => {
return `Welcome to Publiclab, ${username}! For a quick walkthrough, send the message: \`${botNick} help\``;
};

module.exports = new Behavior('join', greetAction);
36 changes: 36 additions & 0 deletions behaviors/help.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const Behavior = require('../models/behavior');

function helpMessage (name, service) {
let out = `# ${service}\n`;

switch (service) {
case 'chatbot':
out += `\`${name} help [<module>...]\`: Prints out this descriptive help message for each mentioned module. If no modules are specified, print the help message for ALL modules.`;
break;
default:
out += `\`${service}\` is not the name of a valid module. Try looking up the \`chatbot\` module instead.`;
}

return out;
}

function printGeneralHelp (botNick) {
return helpMessage(botNick, 'chatbot');
}

function printSpecificHelp (botNick, options) {
return options.map((service) => helpMessage(botNick, service)).join('\n\n');
}

let helpAction = (botNick, options) => {
if(options.length == 0) {
return printGeneralHelp(botNick);
} else {
return printSpecificHelp(botNick, options);
}
};

module.exports = {
helpBehavior: new Behavior('message', helpAction, 'help'),
helpMessage
};
86 changes: 86 additions & 0 deletions behaviors/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const utils = require('../utils');

const greetBehavior = require('./greet');
const helpBehavior = require('./help').helpBehavior;

function parseMessage (message) {
return message.split(/[\s,.;:!?]/g).filter(String);
}

function messageResponse(botNick, parsed, behaviors) {
// This function takes the parsed version of the message and the array of
// behaviors with `trigger` equal to "message" (i.e. the behaviors supposed to
// be triggered on message), executes the action of a behavior if it was
// mentioned by `keyword` and returns the result

// This snippet looks if the parsed message contains the keyword of any of the
// behavior inside the behaviors array.
const behavior = behaviors.find(behavior =>
utils.contains(parsed, behavior.keyword)
);

// If there was a match, remove the behavior's keyword from the parsed message
// call the behavior's action with the remaining message and the bot's nick
if (behavior) {
utils.remove(parsed, behavior.keyword);
return behavior.action(botNick, parsed);
}
}

class Behaviors {
constructor(botNick, client) {
this.botNick = botNick;
this.client = client;
this.joinBehaviors = [ greetBehavior ];
this.messageBehaviors = [ helpBehavior ];
}

addJoinHandler() {
this.client.addJoinHandler((channel, username) => {
this.joinBehaviors.forEach(behavior => {
this.client.sendMessage(channel, behavior.action(channel, username, this.botNick));
});
});
}

addMessageHandler() {
this.client.addMessageHandler((from, to, message) => {
const response = this.getResponse(to, message);
if(response) {
if(to === this.botNick) {
// Message was recieved in a DM
this.client.sendMessage(from, response);
} else {
// Message was recieved in a normal channel
this.client.sendMessage(to, response);
}
}
});
}

getResponse(to, message) {
let parsed = parseMessage(message);
if(to === this.botNick) {
// If the message was sent directly to the bot (eg: in a DM)
return messageResponse(this.botNick, parsed, this.messageBehaviors);
} else if (utils.contains(parsed, this.botNick)) {
// If bot was mentioned
utils.remove(parsed, this.botNick);
return messageResponse(this.botNick, parsed, this.messageBehaviors);
} else if (utils.contains(parsed, '@' + this.botNick)) {
// If bot was mentioned, Gitter style
utils.remove(parsed, '@' + this.botNick);
return messageResponse(this.botNick, parsed, this.messageBehaviors);
} else {
// If the message was not meant for the bot
return undefined;
}
}

bootstrap() {
this.addJoinHandler();
this.addMessageHandler();
}
}

module.exports = Behaviors;
7 changes: 3 additions & 4 deletions bot.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const config = require('./config');

const Chatbot = require('./services/chatbot');
const Behaviors = require('./behaviors');

let client;

Expand All @@ -14,6 +14,5 @@ if (process.env.TEST) {
client = new IrcClient(config.server, config.name, config.channels);
}

const chatbot = new Chatbot(config.name, client);

chatbot.addListeners();
const behaviors = new Behaviors(config.name, client);
behaviors.bootstrap();
9 changes: 9 additions & 0 deletions models/behavior.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class Behavior {
constructor(trigger, action, keyword) {
this.trigger = trigger;
this.action = action;
this.keyword = keyword;
}
}

module.exports = Behavior;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "plotsbot",
"version": "0.3.11",
"version": "1.0.0",
"description": "A bot for Public Lab",
"main": "bot.js",
"repository": "[email protected]:publiclab/plotsbot.git",
Expand Down
53 changes: 0 additions & 53 deletions services/chatbot.js

This file was deleted.

25 changes: 0 additions & 25 deletions spec/chatbot-spec.js

This file was deleted.

26 changes: 26 additions & 0 deletions spec/help-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const Behaviors = require('../behaviors');
const helpMessage = require('../behaviors/help').helpMessage;

describe('Help Behavior', () => {
const botNick = 'testbot';
const behaviors = new Behaviors(botNick, undefined);

const chatbotHelp = helpMessage(botNick, 'chatbot');
const invalidHelp = helpMessage(botNick, 'invalid');

it('should print general help', () => {
expect(behaviors.getResponse(botNick, 'help')).toBe(chatbotHelp);
});

it('should print specific help for existing modules', () => {
expect(behaviors.getResponse(botNick, 'help chatbot')).toBe(chatbotHelp);
});

it('should print specific help for nonexisting modules', () => {
expect(behaviors.getResponse(botNick, 'help invalid')).toBe(invalidHelp);
});

it('should print specific help for existing and nonexisting modules combined', () => {
expect(behaviors.getResponse(botNick, 'help chatbot invalid')).toBe(chatbotHelp + '\n\n' + invalidHelp);
});
});
46 changes: 6 additions & 40 deletions utils.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,12 @@
function helpMessage (name, service) {
let out = `# ${service}\n`;

switch (service) {
case 'chatbot':
out += `\`${name} help [<module>...]\`: Prints out this descriptive help \
message for each mentioned module. If no modules are specified, print the help \
message for ALL modules.`;
break;
default:
out += `\`${service}\` is not the name of a valid module. Try looking up \
the \`chatbot\` module instead.`;
}

return out;
function contains(array, element) {
return array.indexOf(element) == -1 ? false : true;
}

function printGeneralHelp (name) {
return helpMessage(name, 'chatbot');
}

function printSpecificHelp (name, message) {
return message.map((service) => helpMessage(name, service)).join('\n\n');
}

function parseMessage (message) {
return message.split(/[\s,.;:!?]/g).filter(String);
}

function messageResponse (name, message) {
switch (message[0]) {
case 'help':
message.splice(0, 1);

if (message.length === 0) {
return printGeneralHelp(name);
} else {
return printSpecificHelp(name, message);
}
}
function remove(array, element) {
array.splice(array.indexOf(element), 1);
}

module.exports = {
parseMessage,
messageResponse
contains,
remove
};

0 comments on commit 6fb31e6

Please sign in to comment.