hubot-issues
Version:
hubot issue tracker
1,099 lines (968 loc) • 31.3 kB
JavaScript
// Description:
// hubot issue tracking
//
// Commands:
// hubot how do you track issues? - Reply with instructions
//
// Author:
// Benjamin Eidelman <beneidel@gmail.com>
//
module.exports = function(robot) {
var _ = require('lodash');
var BrainRepo = require('../brain-repo');
var repo = new BrainRepo(robot, { key: 'hubotissues' });
var settingsRepo = new BrainRepo(robot, { key: 'hubotissuessettings' });
var Chatter = require('../chatter');
var chatter = new Chatter(robot);
var descriptionAnalyzer = require('../description-analyzer');
var EmailSender = require('../email-sender');
var emailSender = new EmailSender();
if (robot.constructor.name === 'MockRobot') {
emailSender.enableTestMode();
robot.emailSender = emailSender;
}
/* introduction */
chatter.hear('that might be a bug', function(res, thing) {
if (chatter.matches('I found a bug', res.match[0])) {
// a bug that will be tracked
return;
}
if (thing) {
this.send(res, 'maybe you found a bug on', { thing: thing });
} else {
this.send(res, 'maybe you found a bug');
}
this.setReplyContext(res, 'maybefoundabug', true);
});
chatter.hear('yes, please, how?', function(res) {
if (this.getReplyContext(res, 'maybefoundabug')) {
this.deleteReplyContext(res, 'maybefoundabug');
this.send(res, 'say I found a bug');
}
});
chatter.hear('how do I track bugs?', function(res) {
this.send(res, 'say I found a bug');
});
function getRoomSettings(room) {
var rooms = settingsRepo.get('rooms');
if (!rooms) {
return {};
}
return rooms.list[room] || {};
}
function saveRoomSettings(room, settings) {
var rooms = settingsRepo.get('rooms');
if (!rooms) {
rooms = settingsRepo.create({
id: 'rooms',
list: []
});
}
var update = false;
if (!rooms.list[room]) {
rooms.list[room] = settings;
update = true;
} else {
var roomSettings = rooms.list[room];
Object.keys(settings).forEach(function(key) {
if (roomSettings[key] !== settings[key]) {
roomSettings[key] = settings[key];
update = true;
}
});
}
if (update) {
settingsRepo.update(rooms);
}
return rooms.list[room];
}
function extractTagsFromDescription(description, tags) {
description = description.replace(/\#([\w\d\-\_\.\:]+)/gim, function(match, tag) {
if (tags.indexOf(tag) < 0) {
tags.push(tag);
}
return '';
});
return description.replace(/[,; ]*$/, '');
}
/* creating */
chatter.hear('I found a bug', function(res, description) {
var analysis = descriptionAnalyzer.analyze(description);
if (analysis.message) {
res.send(analysis.message);
return;
}
var tags = [];
description = extractTagsFromDescription(description, tags);
var issueData = {
description: description,
author: res.message.user.name,
createdAt: currentTime(),
lastMentionAt: currentTime(),
state: 'pending'
};
if (tags && tags.length) {
issueData.tags = tags;
}
var issue = repo.create(issueData);
this.setRoomContext(res, 'issueid', issue.id);
saveRoomSettings(res.message.room, { notifications: true });
this.send(res, 'issue created', { issue: issue });
});
robot.router.post('/hubot/issues', function(req, res) {
var data = req.body.payload ? JSON.parse(req.body.payload) : req.body;
var token = data.token || req.query.token;
if (process.env.HUBOT_ISSUES_HTTP_TOKEN && process.env.HUBOT_ISSUES_HTTP_TOKEN !== token) {
res.send(403, 'Not Authorized');
return;
}
if (!data.description) {
res.send(400, 'description is required');
}
var issue = repo.create({
description: data.description,
author: data.author || 'anonymous',
createdAt: currentTime(),
lastMentionAt: currentTime(),
state: 'pending'
});
if (data.room) {
chatter.setRoomContext({ room: data.room }, 'issueid', issue.id);
saveRoomSettings(data.room, { notifications: true });
robot.messageRoom(data.room, chatter.renderMessage(null, 'issue created from http' +
(data.author ? '' : ' anonymous'), { issue: issue }));
}
res.send(201, 'Created');
});
/* change ownership */
chatter.hear('that\'s from someone else', function(res, id, author){
if (!id) {
id = 'that';
}
if (['that', 'it'].indexOf(id.toLowerCase()) >= 0) {
var context = this.getRoomContext(res, 'issueid');
if (context && context.value) {
id = context.value;
} else {
this.send(res, 'issue not found in context', { ref: id });
return;
}
}
if (/^(me|myself)$/i.test(author)) {
author = res.message.user.name;
}
var issue = repo.get(+id);
if (!issue) {
this.send(res, 'issue not found', { id: id });
return;
}
if (issue.author === author) {
this.send(res, 'issue already authored by that person', { issue: issue });
return;
}
issue.author = author;
issue.lastMentionAt = currentTime();
repo.update(issue);
this.setRoomContext(res, 'issueid', issue.id);
if (issue.state === 'fixed') {
var pendingIssues = repo.filter(function(issue) {
return issue.state === 'pending';
});
this.send(res, 'issue fixed', { issue: issue, pendingIssues: pendingIssues });
} else {
this.send(res, 'issue author changed', { issue: issue });
}
});
/* listing */
function formatList(res, issues) {
var issuesByState = _.groupBy(issues, 'state');
var list = [];
var states = Object.keys(issuesByState);
_.sortBy(states, function(state) {
switch(state) {
case 'pending':
return 0;
case 'fixed':
return 1;
case 'verified':
return 9;
default:
return 8;
}
});
states.forEach(function(state){
/* jshint loopfunc: true */
var groupIssues = issuesByState[state];
var group = {
name: state,
count: groupIssues.length
};
list.push(chatter.renderMessage(res, 'list group', {
group: group
}));
if (state === 'pending') {
_.sortBy(groupIssues, 'assignee');
}
groupIssues.forEach(function(issue) {
list.push(chatter.renderMessage(res, 'list issue', {
group: group,
issue: issue,
tags: stringifyTags(issue.tags)
}));
});
});
return list.join('\n');
}
chatter.hear('what\'s broken?', function(res, type) {
type = (type || '').trim().toLowerCase();
if (type === 'all') {
type = '';
}
if (type === 'my' || / my$/i.test(type)) {
return;
}
if (/delete/.test(type)) {
// this is a delete command
return;
}
var issues = repo.filter(function(issue){
if (type) {
if (issue.state === type) {
return true;
}
if (issue.tags && issue.tags.indexOf(type) >= 0) {
return true;
}
} else {
return issue.state !== 'verified';
}
});
if (issues.length < 1) {
if (!type || /^(pending|all)$/i.test(type)) {
this.send(res, 'no more issues');
return;
}
this.send(res, 'list empty', { type: type });
return;
}
res.send(formatList(res, issues));
});
chatter.hear('what can I do next?', function(res) {
var user = res.message.user.name;
var issues = repo.filter(function(issue){
if (issue.author === user && issue.state !== 'fixed') {
return true;
}
if (issue.assignee === user && issue.state === 'pending') {
return true;
}
return false;
});
if (issues.length < 1) {
issues = repo.filter(function(issue) {
return issue.state === 'pending' && !issue.assignee;
});
}
if (issues.length < 1) {
this.send(res, 'your list empty');
return;
}
res.send(formatList(res, issues));
});
/* assign */
chatter.hear('I\'m fixing that', function(res, id) {
if (!id) {
id = 'that';
}
if (['that', 'it'].indexOf(id.toLowerCase()) >= 0) {
var context = this.getRoomContext(res, 'issueid');
if (context && context.value) {
id = context.value;
} else {
this.send(res, 'issue not found in context', { ref: id });
return;
}
}
var issue = repo.get(+id);
if (!issue) {
this.send(res, 'issue not found', { id: id });
return;
}
if (issue.state !== 'pending') {
this.send(res, 'cannot assign, issue is not pending', { issue: issue });
return;
}
if (issue.assignee === res.message.user.name) {
this.send(res, 'issue already assigned to you', { issue: issue });
return;
}
if (issue.assignee) {
this.send(res, 'issue already assigned to someone else', { issue: issue });
return;
}
issue.assignee = res.message.user.name;
issue.lastMentionAt = currentTime();
repo.update(issue);
this.setRoomContext(res, 'issueid', issue.id);
this.send(res, 'issue assigned', { issue: issue });
});
chatter.hear('I\'m not fixing that', function(res, id) {
if (['that', 'it'].indexOf(id.toLowerCase()) >= 0) {
var context = this.getRoomContext(res, 'issueid');
if (context && context.value) {
id = context.value;
} else {
this.send(res, 'issue not found in context', { ref: id });
return;
}
}
var issue = repo.get(+id);
if (!issue) {
this.send(res, 'issue not found', { id: id });
return;
}
if (issue.assignee !== res.message.user.name) {
this.send(res, 'issue already not assigned to you', { issue: issue });
return;
}
delete issue.assignee;
repo.update(issue);
this.setRoomContext(res, 'issueid', issue.id);
this.send(res, 'issue unassigned', { issue: issue });
});
/* fixed */
chatter.hear('I fixed that', function(res, id) {
if (['that', 'it'].indexOf(id.toLowerCase()) >= 0) {
var context = this.getRoomContext(res, 'issueid');
if (context && context.value) {
id = context.value;
} else {
this.send(res, 'issue not found in context', { ref: id });
return;
}
}
var issue = repo.get(+id);
if (!issue) {
this.send(res, 'issue not found', { id: id });
return;
}
if (issue.state !== 'pending') {
this.send(res, 'cannot fix, issue is not pending', { issue: issue });
return;
}
if (issue.assignee && issue.assignee !== res.message.user.name) {
this.send(res, 'issue was being fixed by someone else', { issue: issue });
}
this.setRoomContext(res, 'issueid', issue.id);
issue.assignee = res.message.user.name;
issue.lastMentionAt = currentTime();
repo.update(issue);
if (issue.assignee === issue.author) {
// autofix, consider it verified
issue.state = 'verified';
issue.stateChangedAt = currentTime();
repo.update(issue);
this.send(res, 'issue fixed by author', { issue: issue });
return;
}
issue.state = 'fixed';
issue.stateChangedAt = currentTime();
repo.update(issue);
var pendingIssues = repo.filter(function(issue) {
return issue.state === 'pending';
});
this.send(res, 'issue fixed', { issue: issue, pendingIssues: pendingIssues });
});
/* clarification */
chatter.hear('needs clarification', function(res, id, question) {
if (!id) {
id = 'that';
}
var context;
if (['that', 'it'].indexOf(id.toLowerCase()) >= 0) {
context = this.getRoomContext(res, 'issueid');
if (context && context.value) {
id = context.value;
} else {
this.send(res, 'issue not found in context', { ref: id });
return;
}
}
var issue = repo.get(+id);
if (!issue) {
this.send(res, 'issue not found', { id: id });
return;
}
this.setRoomContext(res, 'issueid', issue.id);
if (question) {
issue.log = issue.log || [];
issue.log.push(res.message.user.name + '> ' + question);
repo.update(issue);
this.send(res, 'issue needs clarification, can author answer?', { issue: issue });
} else {
this.setReplyContext(res, 'needsClarificationId', issue.id);
this.send(res, 'issue needs clarification, what\'s your question?', { issue: issue });
}
});
chatter.hear([
/^([\s\S]+)\?$/i,
], function(res, question) {
var context;
var issue;
context = this.getReplyContext(res, 'needsClarificationId');
if (context && context.value) {
issue = repo.get(context.value);
if (issue) {
issue.log = issue.log || [];
issue.log.push(res.message.user.name + '> ' + question);
issue.lastMentionAt = currentTime();
repo.update(issue);
this.send(res, 'issue needs clarification, can author answer?', { issue: issue });
}
this.setReplyContext(res, 'needsClarificationId', null);
return;
}
});
/* commenting */
chatter.hear('about that', function(res, id, comment) {
if (!id) {
id = 'that';
}
var context;
if (['that', 'it'].indexOf(id.toLowerCase()) >= 0) {
context = this.getRoomContext(res, 'issueid');
if (context && context.value) {
id = context.value;
} else {
// issue not found, might be just an unrelated comment
return;
}
}
var issue = repo.get(+id);
if (!issue) {
this.send(res, 'issue not found', { id: id });
return;
}
this.setRoomContext(res, 'issueid', issue.id);
if (context && !res.match[1] && issue.author !== res.message.user.name) {
// only author can comment using "also|and|but"
return;
}
if (comment && comment.trim()) {
issue.log = issue.log || [];
issue.log.push(res.message.user.name + '> ' + comment);
issue.lastMentionAt = currentTime();
repo.update(issue);
this.send(res, 'comment added', { issue: issue, comment: comment });
} else {
this.setReplyContext(res, 'addingCommentId', issue.id);
this.send(res, 'comment, what?', { issue: issue });
}
});
chatter.hear([
/^([\s\S]+)$/i,
], function(res, comment) {
if (chatter.matches('about that', res.match[0])) {
return;
}
var context;
var issue;
context = this.getReplyContext(res, 'rejectReasonForId');
if (context && context.value) {
issue = repo.get(context.value);
if (issue) {
issue.log = issue.log || [];
issue.log.push(res.message.user.name + '> ' + comment);
issue.lastMentionAt = currentTime();
repo.update(issue);
this.send(res, 'rejected fix, got reason', { issue: issue });
}
this.setReplyContext(res, 'needsClarificationId', null);
return;
}
context = this.getReplyContext(res, 'addingCommentId');
if (!context || !context.value) {
return;
}
if (chatter.matches('nevermind', comment.trim())) {
this.setReplyContext(res, 'addingCommentId', null);
return;
}
issue = repo.get(context.value);
if (issue) {
issue.log = issue.log || [];
issue.log.push(res.message.user.name + '> ' + comment);
issue.lastMentionAt = currentTime();
repo.update(issue);
this.send(res, 'comment added', { issue: issue, comment: comment });
}
this.setReplyContext(res, 'addingCommentId', null);
});
/* tags */
function parseTags(tagString) {
if (!tagString) {
return [];
}
return tagString.split(/(?:,|and)/gim).map(function(tag){
return tag.toLowerCase().trim().replace(/^#/, '');
}).filter(function(tag){
return /\w/i.test(tag) &&
!/^((?:a )?dup(?:licate)? of|same as)/.test(tag) &&
[
'pending', 'fixing', 'fixed', 'done', 'solved', 'ok now', 'gone', 'rejected', 'verified', 'dup', 'duplicate'
].indexOf(tag.replace(/^not /, '')) < 0;
});
}
function stringifyTags(tags, prefix) {
if (!tags) {
return '';
}
if (typeof prefix === 'undefined') {
prefix = '#';
}
if (tags.length < 2) {
return prefix + tags[0];
}
return '#' + tags.slice(0, -1).join(', #') +
' and #' + tags[tags.length - 1];
}
chatter.hear('that is tag', function(res, id, tagString) {
var tags = parseTags(tagString);
if (!tags || !tags.length) {
return;
}
id = id.toLowerCase().trim();
var ids;
if (/[ ,]/.test(id)) {
// multiple ids
ids = id.split(/[ ,]+/g).map(function(anId) {
return anId.replace(/[ ,\#]+/g, '');
}).filter(function(anId) {
return anId !== 'and';
});
} else {
ids = [id];
}
function applyTags(tags, issue) {
var tagsChanged = false;
tags.forEach(function(tag) {
if (/^not /.test(tag)) {
for (var i = 0; i < issue.tags.length; i++) {
if ('not ' + issue.tags[i] === tag) {
issue.tags.splice(i, 1);
i--;
tagsChanged = true;
}
}
if (issue.tags.length < 1) {
delete issue.tags;
}
} else {
issue.tags = issue.tags || [];
if (issue.tags.indexOf(tag) < 0) {
issue.tags.push(tag);
tagsChanged = true;
}
}
});
return tagsChanged;
}
var context;
for (var i = 0; i < ids.length; i++) {
if (['that', 'it'].indexOf(ids[i].toLowerCase()) >= 0) {
context = this.getRoomContext(res, 'issueid');
if (context && context.value) {
ids[i] = context.value;
} else {
this.send(res, 'issue not found in context', { ref: ids[i] });
return;
}
}
var issue = repo.get(+ids[i]);
if (!issue) {
this.send(res, 'issue not found', { id: ids[i] });
return;
}
if (ids.length < 2) {
this.setRoomContext(res, 'issueid', issue.id);
}
var tagsChanged = applyTags(tags, issue);
if (tagsChanged) {
issue.lastMentionAt = currentTime();
repo.update(issue);
}
if (ids.length < 2) {
if (issue.tags && issue.tags.length) {
this.send(res, 'issue tagged', { issue: issue, tags: stringifyTags(issue.tags) });
} else {
this.send(res, 'issue has no tags', { issue: issue });
}
}
}
if (ids.length > 1) {
this.send(res, 'issues tagged', { issues: stringifyTags(ids), tags: stringifyTags(tags) });
}
});
/* duplicates */
chatter.hear('duplicate', function(res, duplicateId) {
if (!duplicateId) {
duplicateId = 'that';
}
var context;
if (['that', 'it'].indexOf(duplicateId.toLowerCase()) >= 0) {
context = this.getRoomContext(res, 'issueid');
if (context && context.value) {
duplicateId = context.value;
} else {
this.send(res, 'issue not found in context', { ref: duplicateId });
return;
}
}
this.setRoomContext(res, 'issueid', duplicateId);
this.setReplyContext(res, 'duplicateId', duplicateId);
this.send(res, 'duplicate of which?', { id: duplicateId });
});
chatter.hear('duplicate of', function(res, duplicateId, id) {
var context;
if (duplicateId.toLowerCase() === 'of') {
context = this.getReplyContext(res, 'duplicateId');
if (!context) {
return;
}
duplicateId = context.value.toString();
}
if (['that', 'it'].indexOf(id.toLowerCase()) >= 0) {
if (['that', 'it'].indexOf(duplicateId.toLowerCase()) >= 0) {
this.send(res, 'that duplicate of that?', { duplicateRef: duplicateId, ref: id });
return;
}
context = this.getRoomContext(res, 'issueid');
if (context && context.value) {
id = context.value;
} else {
this.send(res, 'issue not found in context', { ref: id });
return;
}
}
if (['that', 'it'].indexOf(duplicateId.toLowerCase()) >= 0) {
context = this.getRoomContext(res, 'issueid');
if (context && context.value) {
duplicateId = context.value;
} else {
this.send(res, 'issue not found in context', { ref: duplicateId });
return;
}
}
if (+id === +duplicateId) {
this.send(res, 'issue duplicate of itself?', { ref: id });
}
var issue = repo.get(+id);
if (!issue) {
this.send(res, 'issue not found', { id: id });
return;
}
var duplicate = repo.get(+duplicateId);
if (!duplicate) {
this.send(res, 'issue not found', { id: duplicateId });
return;
}
issue.log = issue.log || [];
issue.log.push(duplicate.author + '> ' + duplicate.description + ' (ex #' + duplicate.id + ')');
issue.lastMentionAt = currentTime();
repo.update(issue);
repo.delete(duplicate);
this.setRoomContext(res, 'issueid', issue.id);
this.send(res, 'issue duplicate deleted', { duplicate: duplicate, issue: issue });
});
/* issue details */
chatter.hear('tell me about that', function(res, id) {
if (/^(\d+|that|it)$/.test(res.match[0])) {
// just a number or word, it's probably not a question for me
return;
}
var context;
if (['that', 'it'].indexOf(id.toLowerCase()) >= 0) {
context = this.getRoomContext(res, 'issueid');
if (context && context.value) {
id = context.value;
} else {
this.send(res, 'issue not found in context', { ref: id });
return;
}
}
var issue = repo.get(+id);
if (!issue) {
this.send(res, 'issue not found', { id: id });
return;
}
this.setRoomContext(res, 'issueid', issue.id);
var output = [this.renderMessage(res, 'issue details', { issue: issue, tags: stringifyTags(issue.tags) })];
if (issue.log) {
issue.log.forEach(function(text){
output.push(chatter.renderMessage(res, 'issue details log entry', { issue: issue, text: text }));
});
}
issue.lastMentionAt = currentTime();
repo.update(issue);
res.send(output.join('\n'));
});
/* reject */
chatter.hear('reject that', function(res, id, reason) {
if (['that', 'it'].indexOf(id.toLowerCase()) >= 0) {
var context = this.getRoomContext(res, 'issueid');
if (context && context.value) {
id = context.value;
} else {
this.send(res, 'issue not found in context', { ref: id });
return;
}
}
var issue = repo.get(+id);
if (!issue) {
this.send(res, 'issue not found', { id: id });
return;
}
this.setRoomContext(res, 'issueid', issue.id);
if (issue.state !== 'fixed') {
this.send(res, 'cannot reject, issue is not fixed', { issue: issue });
return;
}
issue.state = 'pending';
issue.stateChangedAt = currentTime();
issue.lastMentionAt = currentTime();
if (reason) {
issue.log = issue.log || [];
issue.log.push(res.message.user.name + '> ' + reason);
}
repo.update(issue);
if (issue.assignee && issue.assignee === res.message.user.name) {
this.send(res, 'rejected your fix', { issue: issue, reason: reason });
} else {
this.send(res, reason ? 'rejected fix' : 'rejected fix, no reason',
{ issue: issue, reason: reason });
if (!reason) {
this.setReplyContext(res, 'rejectReasonForId', issue.id);
}
}
});
/* verify */
chatter.hear('verified that', function(res, id) {
if (['that', 'it'].indexOf(id.toLowerCase()) >= 0) {
var context = this.getRoomContext(res, 'issueid');
if (context && context.value) {
id = context.value;
} else {
this.send(res, 'issue not found in context', { ref: id });
return;
}
}
var issue = repo.get(+id);
if (!issue) {
this.send(res, 'issue not found', { id: id });
return;
}
this.setRoomContext(res, 'issueid', issue.id);
if (issue.author !== 'anonymous' && issue.author !== res.message.user.name) {
this.send(res, 'only author can verify', { issue: issue });
}
if (issue.state !== 'fixed') {
this.send(res, 'cannot verify, issue is not fixed', { issue: issue });
return;
}
issue.state = 'verified';
issue.stateChangedAt = currentTime();
issue.lastMentionAt = currentTime();
repo.update(issue);
this.send(res, 'verified fix', { issue: issue });
if (!this.getContext({ name: 'garbageCollected'})) {
this.setContext({ name: 'garbageCollected'}, true, 12 * 60 * 60000);
repo.deleteAll(function(issue) {
// delete verified issues after 24hs
return issue.state === 'verified' && hoursSince(issue.lastMentionAt) > 24;
});
}
});
/* delete */
chatter.hear('delete that', function(res, id) {
if (['that', 'it'].indexOf(id.toLowerCase()) >= 0) {
var context = this.getRoomContext(res, 'issueid');
if (context && context.value) {
id = context.value;
} else {
this.send(res, 'issue not found in context', { ref: id });
return;
}
}
var issue = repo.get(+id);
if (!issue) {
this.send(res, 'issue not found', { id: id });
return;
}
this.setRoomContext(res, 'issueid', issue.id);
repo.delete(issue);
this.send(res, issue.author === res.message.user.name ?
'issue deleted by author' : 'issue deleted', { issue: issue });
});
chatter.hear('delete verified', function(res) {
var count = repo.deleteAll({ state: 'verified' }).count;
this.send(res, 'issues deleted', { count: count });
});
/* send */
chatter.hear('send that', function(res, id, to, toAlias) {
var self = this;
if (['that', 'it'].indexOf(id.toLowerCase()) >= 0) {
var context = this.getRoomContext(res, 'issueid');
if (context && context.value) {
id = context.value;
} else {
this.send(res, 'issue not found in context', { ref: id });
return;
}
}
var issue = repo.get(+id);
if (!issue) {
this.send(res, 'issue not found', { id: id });
return;
}
this.setRoomContext(res, 'issueid', issue.id);
var body = [this.renderMessage(res, 'issue details', { issue: issue, tags: stringifyTags(issue.tags) })];
if (issue.log) {
issue.log.forEach(function(text){
body.push(chatter.renderMessage(res, 'issue details log entry', { issue: issue, text: text }));
});
}
to = to.toLowerCase().trim();
var aliases = settingsRepo.get('aliases');
if (aliases && aliases.entries) {
to = aliases.entries[to] || to;
}
var message = {
text: body.join('\n') + '\n',
to: to,
subject: '[issue] ' + issue.description + ((issue.tags && issue.tags.length) ?
(' #' + issue.tags.join(', #')) : '')
};
emailSender.send(message, function(err) {
if (err) {
console.log('[email send error]', err.message);
console.log(err.stack);
self.send(res, 'issue send failed', { issue: issue, message: message, errorMessage: err.message });
return;
}
self.send(res, 'issue sent', { issue: issue, message: message });
repo.delete(issue);
if (toAlias) {
var aliases = settingsRepo.get('aliases') || { id: 'aliases' };
if (!aliases.entries) {
aliases.entries = {};
}
aliases.entries[toAlias.toLowerCase().trim()] = to;
settingsRepo.upsert(aliases);
}
});
});
/* notifications */
function currentTime() {
// time shift used when unit testing
var timeShift = robot.timeHasShifted && robot.brain.get('timeShift') || 0;
return new Date().getTime() + timeShift;
}
function timeSince(date) {
var time = typeof date === 'number' ? date : date.getTime();
return currentTime() - time;
}
function hoursSince(date) {
return timeSince(date) / 1000 / 3600;
}
chatter.hear([
/^.*$/i,
], function(res) {
if (this.getContext({ name: 'noNotifications'})) {
return;
}
var roomSettings = getRoomSettings(res.message.room);
if (!roomSettings.notifications) {
return;
}
var issues = repo.filter(function(issue){
return issue.state !== 'verified' && hoursSince(issue.lastMentionAt) > 4;
});
if (!issues.length) {
// no issues to notify about, don't look again for 30min
this.setContext({ name: 'noNotifications'}, true, 30 * 1000);
return;
}
var user = res.message.user.name;
issues = issues.filter(function(issue){
var hoursSinceLastMention = hoursSince(issue.lastMentionAt);
if (issue.state === 'fixed' && issue.author === user && hoursSinceLastMention > 4) {
return true;
}
if (issue.state === 'pending' && issue.assignee === user && hoursSinceLastMention > 8) {
return true;
}
});
var personalIssues = issues && issues.length;
if (!personalIssues) {
issues = repo.filter(function(issue){
// issues that have been pending for a long time,
var hoursSinceLastMention = hoursSince(issue.lastMentionAt);
return issue.state === 'pending' && !issue.assignee && hoursSinceLastMention > 22;
});
}
if (!issues.length) {
return;
}
if (issues.length === 1) {
this.setRoomContext(res, 'issueid', issues[0].id);
}
issues.forEach(function(issue) {
issue.lastMentionAt = currentTime();
repo.update(issue);
});
if (personalIssues) {
var issuesList = stringifyTags(issues.map(function(issue) {
return issue.id.toString();
}));
this.setRoomContext(res, 'issueid', issuesList[0].id);
this.send(res, issues.length === 1 ?
'issue waiting for your ' + (issues[0].state === 'fixed' ? 'verification' : 'fix') :
'issues waiting for you', {
issue: issues[0],
issues: issues,
issuesList: issuesList,
pending: issues.filter(function(issue) {
return issue.state === 'pending';
}),
fixed: issues.filter(function(issue) {
return issue.state === 'fixed';
})
}
);
} else {
this.send(res, issues.length === 1 ? 'pending issue waiting' : 'pending issues waiting',
{ issues: issues });
}
});
/* settings */
chatter.hear('no notifications here', function(res) {
saveRoomSettings(res.message.room, {
notifications: false
});
this.send(res, 'notifications off');
});
chatter.hear('do notifications here', function(res) {
saveRoomSettings(res.message.room, {
notifications: true
});
this.send(res, 'notifications on');
});
chatter.hear('hubot speak in language', function(res, lang) {
try {
chatter.loadLanguage(lang);
this.send(res, 'language loaded');
} catch (err) {
// language not found for this script
}
});
};