Skip to content
This repository has been archived by the owner on Nov 4, 2019. It is now read-only.

Commit

Permalink
(re) added optional built-in help generator close #6
Browse files Browse the repository at this point in the history
  • Loading branch information
pveyes committed Jan 19, 2016
1 parent 23e7367 commit d37a8a0
Show file tree
Hide file tree
Showing 10 changed files with 363 additions and 15 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ ws.on('message', function (msg) {
```

**NOTE: .reaction() and .async() cannot be used here**

## Handling the unexpected

slack-robot will emit event if something happened. Below is the list of events
Expand All @@ -270,6 +271,11 @@ robot.on('error', function (err) {
});
```

## Help command generator

When you have many listener, you sometimes forget all your listeners. You can see it
by enabling help generator which will sent you all your listeners. Enable it using `robot.set('help_generator', true)` (it's disabled by default). It will add another listener that will listen to all text message containing "help". So if you send message
to the bot "show help" or "help", it will send you the command list.

## License

Expand Down
3 changes: 3 additions & 0 deletions example/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ var Robot = require('..');
var token = process.env.SLACK_TOKEN;
var robot = new Robot(token);

// enable help command
robot.set('help_generator', true);

// ignore all message in this channel
robot.ignore('#bot-playground');

Expand Down
38 changes: 32 additions & 6 deletions src/Listeners.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Listener from './Listener';
import { find } from 'lodash';

export default class Listeners {
constructor() {
Expand All @@ -11,7 +10,7 @@ export default class Listeners {
* @param {string} type
* @param {string|RegExp} value
* @param {function (req, res)} callback
* @return {Object} listener
* @return {Listener} listener
*/
add(type, value, callback) {
const entry = new Listener(type, value, callback);
Expand All @@ -21,17 +20,44 @@ export default class Listeners {

/**
* @public
* @param {string} id
* @return {?Object} listener
* @param {?string} id
* @return {Array.<Listener>|?Listener} listener
*/
get(id) {
return find(this._entries, 'id', id);
if (!id) {
return this._entries;
}

for (let i = 0; i < this._entries.length; i++) {
if (this._entries[i].id === id) {
return this._entries[i];
}
}

return null;
}

/**
*
* @public
* @param {string} id
* @return {boolean}
*/
remove(id) {
for (let i = 0; i < this._entries.length; i++) {
if (this._entries[i].id === id) {
this._entries.splice(i, 1);
return true;
}
}

return false;
}

/**
* @public
* @param {Message} message
* @return {?Object} listener
* @return {?Listener} listener
*/
find(message) {
let value = '';
Expand Down
74 changes: 67 additions & 7 deletions src/Robot.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Listeners from './Listeners';
import Message from './Message';
import Request from './Request';
import Response from './Response';
import plugins from './plugins';
import acls from './acls';

const logger = new Log('info');
Expand Down Expand Up @@ -38,19 +39,26 @@ export default class Robot extends EventEmitter {
/**
* Bot properties
*
* @var {Object}
* @private
*/
this._vars = {
concurrency: 1
};

/**
* List of message that is not processed yet by listener, mainly because
* missing information. Currently only stored reaction_added event message
*
* @var {Array.<MessageQueue}
* @private
*/
this._messageQueue = [];

/**
*
* @var {Array.<function(robot)>}
* @private
*/
this._plugins = [];

Expand Down Expand Up @@ -123,27 +131,32 @@ export default class Robot extends EventEmitter {
}

/**
* Add channel(s) to ignored list, so it won't be processed
* no matter what the listener is
*
* @public
* @param {...string} channels
*/
ignore(...channels) {
channels.forEach(channel => {
if (!Boolean(~this._ignoredChannels.indexOf(channel))) {
if (this._ignoredChannels.indexOf(channel) === -1) {
this._ignoredChannels.push(channel);
}
});
}

/**
* Inject plugin
*
* @public
* @param {function} plugin
*/
use(plugin) {
if (typeof plugin !== 'function') {
throw new Error('Invalid plugin type');
}

/* eslint no-implicit-coercion: 0 */
if (!~this._plugins.indexOf(plugin)) {
if (this._plugins.indexOf(plugin) === -1) {
plugin(this);
this._plugins.push(plugin);
}
Expand All @@ -158,14 +171,20 @@ export default class Robot extends EventEmitter {
*/
set(property, value) {
if (value !== null && value !== undefined) {
this._vars[property] = value;
// special property
if (property === 'help_generator') {
this.use(plugins.helpGenerator({ enable: Boolean(value) }));
} else {
this._vars[property] = value;
}
}
}

/**
* Send robot response by without listener
*
* Caveat: .reaction. .async is disabled
* @public
* @param {string} target
*/
to(target, callback) {
Expand All @@ -192,6 +211,37 @@ export default class Robot extends EventEmitter {
callback(res);
}

/**
* Get list of listeners
*
* @public
* @return {Array.<Listener>}
*/
getAllListeners() {
return this._listeners.get();
}

/**
* Get listener by id
*
* @public
* @param {string} listenerId
* @return {?Listener}
*/
getListener(listenerId) {
return this._listeners.get(listenerId);
}

/**
* Remove listener by id
*
* @public
* @param {string} listenerId
*/
removeListener(listenerId) {
return this._listeners.remove(listenerId);
}

/**
* Login and start actually listening to message
*
Expand Down Expand Up @@ -382,6 +432,8 @@ export default class Robot extends EventEmitter {
for (let i = 0; i < this._messageQueue.length; i++) {
const entry = this._messageQueue[i];

// Else statement doesn't do anything anyway so it's not
// worth covering
/* istanbul ignore else */
if (this._isReactionAddEvent(entry, message)) {
const msg = {
Expand All @@ -403,14 +455,22 @@ export default class Robot extends EventEmitter {
}
}

_isReactionAddEvent(entry, message) {
/**
* Check if new message is a complementary for reaction_added event
*
* @private
* @param {MessageQueue} queue
* @param {SlackMessage} message
* @return {boolean}
*/
_isReactionAddEvent(queue, message) {
/* istanbul ignore else */
if (message.message.file && message.message.file.id === entry.id) {
if (message.message.file && message.message.file.id === queue.id) {
return true;
}

// apparently istanbul doesn't recognize this else pattern
// so we should add another Ignore
// so we should add another ignore
/* istanbul ignore next */
return false;
}
Expand Down
37 changes: 37 additions & 0 deletions src/plugins/help-generator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const FILENAME = 'command-list.txt';
const COMMAND_DESCRIPTION = 'Show this message';

/**
* @param {Array<Listener>} listeners
* @return {string}
*/
function generateHelp(listeners) {
let helpText = '';

listeners.forEach(listener => {
const listenerType = listener.value instanceof RegExp ? `${listener.type} (regex)` : listener.type;
const listenerValue = listener.type === 'reaction_added' ? listener.value.toString().replace('\\', '') : listener.value.toString();
helpText += `type: ${listenerType}\n`;
helpText += `command: ${listenerValue}\n`;
helpText += `description: ${listener.description === '' ? '-' : listener.description}\n\n`;
});

return helpText.trim();
}

let helpListener;

export default function helpGenerator(opts) {
return function plugin(robot) {
if (opts.enable) {
helpListener = robot.listen(/help/, (req, res) => {
const helpText = generateHelp(robot.getAllListeners());
return res.upload(FILENAME, helpText).send();
})
.desc(COMMAND_DESCRIPTION)
.acl(robot.acls.dynamicMention);
} else if (helpListener) {
robot.removeListener(helpListener.id);
}
};
}
5 changes: 5 additions & 0 deletions src/plugins/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import helpGenerator from './help-generator';

export default {
helpGenerator
};
33 changes: 33 additions & 0 deletions test/Listeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,42 @@ describe('Listeners', () => {

it('should return null for invalid id', () => {
const l = new Listeners();
l.add('message', 'value1', () => {});
l.add('message', 'value2', () => {});
l.add('message', 'value3', () => {});
should.not.exist(l.get('random'));
});

it('should return all listeners if no id specified', () => {
const l = new Listeners();
l.add('message', 'value', () => {});
l.add('reaction_added', '+1', () => {});
const listeners = l.get();
listeners.length.should.be.equal(2);
listeners[0].type.should.be.equal('message');
listeners[0].value.should.be.equal('value');
listeners[1].type.should.be.equal('reaction_added');
listeners[1].value.should.be.equal('\\+1');
});

it('should be able to remove listener by id', () => {
const l = new Listeners();
l.add('message', 'value', () => {});
const id = l.add('reaction_added', '+1', () => {}).id;
l.remove(id).should.be.equal(true);
l.get().length.should.be.equal(1);
should.not.exist(l.get(id));
});

it('should not remove listener for invalid id', () => {
const l = new Listeners();
l.add('message', 'value', () => {});
const listener = l.add('reaction_added', '+1', () => {});
l.remove('random').should.be.equal(false);
l.get().length.should.be.equal(2);
l.get(listener.id).should.be.deep.equal(listener);
});

it('should find text message', () => {
const l = new Listeners();
const message = {
Expand Down
Loading

0 comments on commit d37a8a0

Please sign in to comment.