Skip to content

Commit

Permalink
[WIP] FTO behavior (publiclab#51)
Browse files Browse the repository at this point in the history
* Start working towards the FTO behavior

* Fix linting issue

* Make FTO behavior work with promises

* Refine FTO behavior to accept names of multiple repositories

* Use the newer shinier model

* Add basic tests for FTO behavior

* Add more advanced tests

* Add mockGithub to utils.js to allow usage in multiple GitHub behaviors

* Add documentation for FTO Behavior

* Fix markdown issues

* Finalize changes to FTO Behavior
  • Loading branch information
ryzokuken authored and ananyo2012 committed Jul 4, 2017
1 parent 73f5853 commit 039b73b
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 7 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ help

1. `trigger: String` => The event on which the behavior is supposed to be triggered. Currently, two events are supported - the `join` event which is triggered whenever a new user joins a particular channel, and the `message` event whenever a particular user sends a message on a channel.
2. `action: Function` => This is the action of the behavior which is called by the bot at the appropriate moment. The bot provides the function with different arguments depending on the corresponding behavior's `trigger` value. For behaviors with `trigger` equal to `join`, the bot passes three arguments to the action function upon trigger. These are:

1. `channel: String` => This is the name of the channel on which the user joined.
2. `username: String` => This is the username of the new user.
3. `botNick: String` => This is the username of the bot.
Expand All @@ -56,12 +57,17 @@ help

1. `botNick: String` => This is the username of the bot.
2. `options: Array` => This is an array containing additional options provided by the user.

This function may return a result string which will be sent back to the user in a message, or a promise that will eventually yield such a string.

3. `keyword: String` **(Only required for message triggered behaviors)** => This is the keyword that must be present in the message in order for the bot to call the behavior's action function.

## Behaviors

Behaviors are clearly defined tasks performed by the bot. A behavior may use any third-party functions to perform this action, but it needs to expose a few mandatory fields that are used by the chatbot to trigger the behavior at the appropriate time.

A behavior may export an object of type `Behavior` or a factory function that returns such an object. These factory functions come into play when you want a shared state between multiple behaviors. For example: If you have multiple behaviors that make use of a database, there is no need to get a database connection individually for each such behavior.

### Greet
`trigger` **join**

Expand All @@ -73,6 +79,12 @@ The bot greets users when they join the channel.

Prints out the help messages of the modules whose names have been specified. If no modules were mentioned, all modules are explained.

### First Timers Only
`trigger` **message**
`keyword` **fto**

Prints out all the issues in the specified publiclab repositories labelled 'first-timers-only'. If no repositories are mentioned, the user is asked to do so.

## Dependencies

### 1. NVM
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"author": "Ujjwal Sharma",
"license": "GPL-3.0",
"dependencies": {
"github": "^9.2.0",
"irc": "^0.5.2",
"readline": "^1.3.0"
},
Expand Down
45 changes: 45 additions & 0 deletions spec/fto-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const Behaviors = require('../src/behaviors');
const mockGithub = require('./test-utils').mockGithub;

const emptyResponse = 'You need to mention the name of a repository.';
const existingResponse = 'publiclab/existing\n1 => My first fake issue\n2 => My second fake issue';
const nonexistingReponse = 'publiclab/nonexisting is not a valid repository.';

describe('FTO Behavior', () => {
const botNick = 'testbot';
const ftoBehavior = require('../src/behaviors/fto')({ github: mockGithub });
const behaviors = new Behaviors(botNick, undefined, [], [ftoBehavior]);

it('should ask user to mention a repository', (done) => {
behaviors.getResponse(botNick, 'fto').then(response => {
expect(response).toBe(emptyResponse);
done();
});
});

it('should get existing repository', (done) => {
behaviors.getResponse(botNick, 'fto existing').then(response => {
expect(response).toBe(existingResponse);
done();
});
});

it('should get nonexisting repository', (done) => {
behaviors.getResponse(botNick, 'fto nonexisting').then(response => {
expect(response).toBe(nonexistingReponse);
done();
});
});

it('should get different combinations of repositories', (done) => {
behaviors.getResponse(botNick, 'fto nonexisting existing nonexisting').then(response => {
expect(response).toBe(`${nonexistingReponse}\n\n${existingResponse}\n\n${nonexistingReponse}`);
done();
});

behaviors.getResponse(botNick, 'fto existing nonexisting existing').then(response => {
expect(response).toBe(`${existingResponse}\n\n${nonexistingReponse}\n\n${existingResponse}`);
done();
});
});
});
27 changes: 27 additions & 0 deletions spec/test-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const mockGithub = {
issues: {
getForRepo: ({ repo }) => {
return new Promise((resolve, reject) => {
if (repo == 'existing') {
resolve({
data: [{
number: 1,
title: 'My first fake issue'
}, {
number: 2,
title: 'My second fake issue'
}]
});
} else {
let err = new Error();
err.status = 'Not Found';
reject(err);
}
});
}
}
};

module.exports = {
mockGithub
};
31 changes: 31 additions & 0 deletions src/behaviors/fto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const Behavior = require('../models/behavior');

module.exports = ({ github }) => {
function ftoRepo(repo) {
return github.issues.getForRepo({
owner: 'publiclab',
repo,
labels: 'first-timers-only'
}).then(data => {
return data.data.reduce((acc, issue) => {
return acc + `\n${issue.number} => ${issue.title}`;
}, `publiclab/${repo}`);
}).catch(err => {
if (err.status === 'Not Found') {
return `publiclab/${repo} is not a valid repository.`;
} else {
throw err;
}
});
}

const ftoAction = (botNick, options) => {
if (options.length > 0) {
return Promise.all(options.map(ftoRepo)).then(repos => repos.join('\n\n'));
} else {
return 'You need to mention the name of a repository.';
}
};

return new Behavior('message', ftoAction, 'fto');
};
4 changes: 2 additions & 2 deletions src/behaviors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,6 @@ class Behaviors {
this.client.sendMessage(to, response);
}
}
}).catch(err => {
console.error(err);
});
});
}
Expand All @@ -78,6 +76,8 @@ class Behaviors {
// If the message was not meant for the bot
return undefined;
}
}).catch(err => {
console.error(err);
});
}

Expand Down
10 changes: 8 additions & 2 deletions src/bot.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
const fs = require('fs');
const Github = require('github');

const Behaviors = require('./behaviors');

const state = {
github: new Github()
};

const greetBehavior = require('./behaviors/greet');
const helpBehavior = require('./behaviors/help').helpBehavior;
const ftoBehavior = require('./behaviors/fto')(state);

// Read file synchronously because we'd need this object in later steps anyway.
const config = JSON.parse(fs.readFileSync('config.json', 'utf8'));

let client;

if (process.env.TEST) {
const CliClient = require('./interfaces/cli');
client = new CliClient(config.name);
Expand All @@ -25,7 +30,8 @@ const joinBehaviors = [
];

const messageBehaviors = [
helpBehavior
helpBehavior,
ftoBehavior
];

const behaviors = new Behaviors(config.name, client, joinBehaviors, messageBehaviors);
Expand Down
3 changes: 1 addition & 2 deletions src/interfaces/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ class CliClient {
}

sendMessage(channel, message) {
console.log(`[${this.nick} => ${channel}]\n${message}`);
console.log(`[${channel} => ${this.nick}]`);
console.log(message);
}

addMessageHandler(actions) {
Expand Down
53 changes: 52 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ acorn@^5.0.1:
version "5.0.3"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.0.3.tgz#c460df08491463f028ccb82eab3730bf01087b3d"

agent-base@2:
version "2.1.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-2.1.1.tgz#d6de10d5af6132d5bd692427d46fc538539094c7"
dependencies:
extend "~3.0.0"
semver "~5.0.1"

ajv-keywords@^1.0.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
Expand Down Expand Up @@ -142,7 +149,7 @@ d@1:
dependencies:
es5-ext "^0.10.9"

debug@^2.1.1:
debug@2, debug@^2.1.1, debug@^2.2.0:
version "2.6.8"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
dependencies:
Expand Down Expand Up @@ -327,6 +334,10 @@ exit@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"

extend@3, extend@~3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"

fast-levenshtein@~2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
Expand Down Expand Up @@ -354,6 +365,13 @@ flat-cache@^1.2.1:
graceful-fs "^4.1.2"
write "^0.2.1"

[email protected]:
version "0.0.7"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-0.0.7.tgz#34b90bab2a911aa347571da90f22bd36ecd8a919"
dependencies:
debug "^2.2.0"
stream-consume "^0.1.0"

fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
Expand All @@ -368,6 +386,15 @@ generate-object-property@^1.1.0:
dependencies:
is-property "^1.0.0"

github@^9.2.0:
version "9.2.0"
resolved "https://registry.yarnpkg.com/github/-/github-9.2.0.tgz#8a886dc40dd63636707dcaf99df3df26c59f16fc"
dependencies:
follow-redirects "0.0.7"
https-proxy-agent "^1.0.0"
mime "^1.2.11"
netrc "^0.1.4"

glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
Expand Down Expand Up @@ -404,6 +431,14 @@ has-ansi@^2.0.0:
dependencies:
ansi-regex "^2.0.0"

https-proxy-agent@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz#35f7da6c48ce4ddbfa264891ac593ee5ff8671e6"
dependencies:
agent-base "2"
debug "2"
extend "3"

iconv@~2.2.1:
version "2.2.3"
resolved "https://registry.yarnpkg.com/iconv/-/iconv-2.2.3.tgz#e084d60eeb7d73da7f0a9c096e4c8abe090bfaed"
Expand Down Expand Up @@ -561,6 +596,10 @@ lodash@^4.0.0, lodash@^4.3.0:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"

mime@^1.2.11:
version "1.3.6"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0"

minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
Expand Down Expand Up @@ -593,6 +632,10 @@ natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"

netrc@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/netrc/-/netrc-0.1.4.tgz#6be94fcaca8d77ade0a9670dc460914c94472444"

node-icu-charset-detector@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/node-icu-charset-detector/-/node-icu-charset-detector-0.2.0.tgz#c2320da374ddcb671fc54cb4a0e041e156ffd639"
Expand Down Expand Up @@ -748,6 +791,10 @@ safe-buffer@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"

semver@~5.0.1:
version "5.0.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a"

shelljs@^0.7.5:
version "0.7.8"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3"
Expand All @@ -764,6 +811,10 @@ sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"

stream-consume@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/stream-consume/-/stream-consume-0.1.0.tgz#a41ead1a6d6081ceb79f65b061901b6d8f3d1d0f"

string-width@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
Expand Down

0 comments on commit 039b73b

Please sign in to comment.