Skip to content

Commit

Permalink
Adding Announce functionality (#14)
Browse files Browse the repository at this point in the history
* Adding announcement feature from #5
* Added announcement loop and tested it all out
* Added more details about announce feature in README
  • Loading branch information
shaunburdick authored Sep 15, 2016
1 parent 89adb97 commit 58b7b0b
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 3 deletions.
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ RUN apk add -U tzdata

ENV NODE_ENV=production \
SLACK_TOKEN=xoxb-foo \
SLACK_AUTO_RECONNECT=true
SLACK_AUTO_RECONNECT=true\
APP_ANNOUNCE_CHANNELS= \
APP_ANNOUNCE_TIMES=

ADD . /usr/src/myapp

Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ This bot can be told when you are out of the office. It will then listen to conv

**If you want a personal OoO bot that acts as you, check out [shaunburdick/slack-ooo-personal](https://github.com/shaunburdick/slack-ooo-personal)!**

##Usage
## Features
- Users set themselve out of the office by talking to the bot
- When a user that is set out of office is mentioned in a channel the bot is in, it will send a message to that channel that the user if out of the office and display their out of office message
- The bot can be configured to announce out of office users to specific channels on specific times (bot must be in that channel to make the announcement)

## Usage
To use this bot, you start a conversation with it:

```
Expand Down Expand Up @@ -53,6 +58,14 @@ OOO BotBOT [5:29 PM]
I will be out until next week
```

## Announcements
The bot can announce which users are out of office on a schedule. To enable this feature, you need to provide the bot with a list of channels to make the announcement and at what times (local to bot).
- **app.announce.channels**: this is an array of channel names to announce to
- The bot *must* be a member of the channel in order to make the announcements
- channels names *must* not contain the starting `#`
- **app.announce.times**: this is an array of times each day to make the announcement
- Each time will be considered as 24-hour time, (ie `13:00` is 1:00 pm)

## Install
1. Clone this [repository](https://github.com/shaunburdick/slack-ooo.git)
2. `npm install`
Expand All @@ -73,6 +86,8 @@ Official Image [shaunburdick/slack-ooo](https://registry.hub.docker.com/u/shaunb
You can set the configuration of the bot by using environment variables.
*ENVIRONMENT_VARIABLE*=Default Value

- *APP_ANNOUNCE_CHANNELS*=general,random, A list of channels to announce OoO on
- *APP_ANNOUNCE_TIMES*=08:00,16:00, A list of times to announce OoO users
- *SLACK_TOKEN*=xoxb-foo, Your Slack Token
- *SLACK_AUTO_RECONNECT*=true, Reconnect on disconnect

Expand Down
8 changes: 8 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
],
"repository": "https://github.com/shaunburdick/slack-ooo",
"env": {
"APP_ANNOUNCE_TIMES": {
"description": "A list of times to announce OoO users",
"value": "08:00, 16:00"
},
"APP_ANNOUNCE_CHANNELS": {
"description": "A list of channels to announce OoO users on",
"value": "general"
},
"SLACK_TOKEN": {
"description": "Your Slack token"
},
Expand Down
6 changes: 6 additions & 0 deletions config.default.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
var config = {
app: {
announce: {
channels: [], // no need to include #
times: [] // 24 hours
}
},
slack: {
token: 'xoxb-foo',
autoReconnect: true
Expand Down
2 changes: 2 additions & 0 deletions docker-cloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ bot:
environment:
SLACK_TOKEN: 'xoxb-foo'
SLACK_AUTO_RECONNECT: true
APP_ANNOUNCE_CHANNELS: ''
APP_ANNOUNCE_TIMES: ''
76 changes: 76 additions & 0 deletions lib/bot.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const Botkit = require('botkit');
const moment = require('moment');
const logger = require('./logger')();
const OOOUser = require('./ooouser');

Expand All @@ -21,6 +22,8 @@ class Bot {
this.lookup = new Map();

this.ooo_users = new Map();

this.nextAnnounce = null; // timeout pointer
}

/**
Expand Down Expand Up @@ -170,6 +173,74 @@ class Bot {
return this;
}

/**
* Start the loop to announce offline users
*
* @return {null}
*/
startAnnounceLoop () {
const self = this;

// Check to see if we have any channels and times
if (this.config.app.announce.channels.length && this.config.app.announce.times.length) {
const nextInterval = this.msTillNextTime(this.config.app.announce.times);
if (nextInterval > 0) {
logger.info(`Next announcement in: ${moment.duration(nextInterval).humanize()}`);
this.nextAnnounce = setTimeout(() => {
logger.info('Announcing on schedule to:', this.config.app.announce.channels);
// loop through channels and announce
self.config.app.announce.channels.forEach((channel) => {
// find the channel id
this.lookup.forEach((item) => {
// only announce on channel or group
if (
(item.is_channel || item.is_group) && // channel or group
self.config.app.announce.channels.indexOf(item.name) !== -1
) {
// send message to channel
logger.info(`Announcing offline on channel: ${item.name}(${item.id})`);
self.bot.say({
channel: item.id,
text: self.announceOffline()
});
}
});
});
self.startAnnounceLoop();
}, nextInterval);
}
} else {
logger.info('Not activating announce loop, No channels or times set');
}
}

/**
* Returns the number of milleseconds until the next time
*
* @param {string[]} times a list of times
* @return {integer} number of milleseconds until next time in list
*/
msTillNextTime (times) {
let retVal = -1;

if (times.length) {
const curTime = moment();
times.forEach((time) => {
const mTime = moment(time, 'HH:mm');
if (mTime.isBefore()) { // if we passed this time today
mTime.add(24, 'hours'); // move it to tomorrow
}

const diff = mTime.diff(curTime);
if (retVal === -1 || retVal > diff) {
retVal = diff; // we have a new winner
}
});
}

return retVal;
}

/**
* Start the bot
*
Expand Down Expand Up @@ -226,6 +297,7 @@ class Bot {
this.payload = payload;
this.populateLookup(payload);
this.slackOpen(payload);
this.startAnnounceLoop();
});

return this;
Expand All @@ -239,6 +311,10 @@ class Bot {
stop () {
this.bot.closeRTM();

if (this.nextInterval) {
clearTimeout(this.nextInterval); // stop announcement loop
}

return this;
}
}
Expand Down
20 changes: 19 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ function parseBool (string) {
return string;
}

/**
* Parse a comma separated list into an array of terms
*
* @param {string} list A comma separated list
* @return {string[]} an array of parsed terms
*/
function parseList (list) {
if (list) {
return list.replace(/^\s+|\s+$/, '').split(/\s*,\s*/);
}

return null;
}

/**
* Parses and enhances config object
*
Expand All @@ -31,6 +45,9 @@ function parse (cfg) {
/**
* Pull config from ENV if set
*/
config.app.announce.channels = parseList(process.env.APP_ANNOUNCE_CHANNELS) || config.app.announce.channels;
config.app.announce.times = parseList(process.env.APP_ANNOUNCE_TIMES) || config.app.announce.times;

config.slack.token = process.env.SLACK_TOKEN || config.slack.token;
config.slack.autoReconnect = parseBool(process.env.SLACK_AUTO_RECONNECT) ||
config.slack.autoReconnect;
Expand All @@ -40,5 +57,6 @@ function parse (cfg) {

module.exports = {
parse,
parseBool
parseBool,
parseList
};
36 changes: 36 additions & 0 deletions test/bot.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const test = require('tape');
const moment = require('moment');
const Bot = require('../lib/bot');
const config = require('../config.default');

Expand All @@ -11,3 +12,38 @@ test('Bot: Instantiation', (assert) => {

assert.end();
});

test('Bot: Find ms until next time', (assert) => {
const bot = new Bot(config);
const curDate = moment();

const times = [
moment(curDate).subtract(4, 'hours').format('HH:mm'),
moment(curDate).subtract(3, 'hours').format('HH:mm'),
moment(curDate).subtract(2, 'hours').format('HH:mm'),
moment(curDate).subtract(1, 'hours').format('HH:mm'),
moment(curDate).add(1, 'hours').format('HH:mm'),
moment(curDate).add(2, 'hours').format('HH:mm'),
moment(curDate).add(3, 'hours').format('HH:mm'),
moment(curDate).add(4, 'hours').format('HH:mm')
];

// rounding because we are rarely on the minute
assert.equal(
Math.round(bot.msTillNextTime(times) / 100000), 36,
'Next time should be about an hour away'
);

times.splice(4, 1); // remove one hour time
assert.equal(
Math.round(bot.msTillNextTime(times) / 100000), 72,
'Next time should be about two hours away'
);

assert.equal(
Math.round(bot.msTillNextTime(times.slice(0, 4)) / 1000000), 72,
'Next time should be about 20 hours away'
);

assert.end();
});
10 changes: 10 additions & 0 deletions test/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ test('Config: pass the original value if not a string', (assert) => {
assert.end();
});

test('Config: Parse a list from a comma separated list', (assert) => {
assert.deepEqual(
Config.parseList('foo, bar,fizz,buzz '),
['foo', 'bar', 'fizz', 'buzz'],
'Parse a list into an array'
);

assert.end();
});

test('Config: parse default config as is', (assert) => {
assert.equal(Config.parse(rawConfig), rawConfig);
assert.end();
Expand Down

0 comments on commit 58b7b0b

Please sign in to comment.