snowball-slackbot
Version:
Send reminder messages to Slack users about GitHub PRs that require review, changes, or are approved.
321 lines (285 loc) • 13.1 kB
JavaScript
const github = require('./github');
const os = require('os');
const path = require('path');
const slackBot = require('slackbots');
const utils = require('./utils');
const _ = require('lodash');
// Replace these with your team's info
// Two mappings: GH usernames to Slack user IDs, and GH usernames to Slack user names
const slackIds = utils.readConfig(path.join(os.homedir(), '.ghslackids'));
const githubUsers = utils.readConfig(path.join(os.homedir(), '.ghslackusers'));
// Configurable settings
const interval = parseFloat(process.env.GITHUB_SLACK_REMINDER_INTERVAL) || 2;
const orgs = process.env.ORGANIZATIONS;
const workStart = 10; // Start of the workday in hours
const workEnd = 18; // End of workday
// Create instance of the bot
const bot = new slackBot({
name: 'snowball',
token: process.env.REMINDER_TOKEN
});
let snoozed = [];
const self = module.exports = {
startBot: (data) => {
bot.on('start', (data) => {
const params = {
icon_url: 'https://avatars.slack-edge.com/2017-05-23/186302031456_247b555e470ae075e2a4_48.jpg',
as_user: false
};
// Get all open pull requests for reference
getOpenPullRequests = (orgs) => {
github.allOrgsRepos(orgs).then(orgRepos => {
const repos = [].concat(...orgRepos);
return github.getOpenPullRequestInfo(repos);
}).then(prInfo => {
const pullInfo = [].concat(...prInfo);
return github.referenceOpenPulls(pullInfo);
}).catch((err) => {
console.log(err);
});
};
// Get all reivewers requested
getReviewersRequested = (now, orgs, githubUser) => {
github.allOrgsRepos(orgs).then(orgRepos => {
const repos = [].concat(...orgRepos);
return github.composePullRequestSlugs(repos);
}).then(prSlugs => {
const slugs = [].concat(...prSlugs);
return github.composeRequest(slugs);
}).then(requestArgs => {
const requests = [].concat(...requestArgs);
return github.allReviewsRequested(requests);
}).then(reviews => {
const reviewers = [].concat(...reviews);
return github.mapReviewsRequestedtoPRs(reviewers);
}).then(reviewersRequested => {
const allReviewers = [].concat(...reviewersRequested);
return messageReviewers(allReviewers, now, githubUser);
}).catch((err) => {
console.log(err);
});
};
// Sort out pull requests based on whether they're approved
parsePullRequests = (now, orgs, githubUser) => {
github.allOrgsRepos(orgs).then(orgRepos => {
const repos = [].concat(...orgRepos);
return github.composePullRequestSlugs(repos);
}).then(prSlugs => {
const slugs = [].concat(...prSlugs);
return github.composeRequest(slugs);
}).then(requestArgs => {
const requests = [].concat(...requestArgs);
return github.allReviews(requests);
}).then(reviews => {
const data = [].concat(...reviews);
return github.getMostRecentReviews(data);
}).then(reviews => {
const recentReviews = [].concat(...reviews);
return github.matchPulls(recentReviews);
}).then(matchedPulls => {
return github.approvedOrNot(matchedPulls);
}).then(parsedPulls => {
messageAuthors(parsedPulls, now, githubUser);
}).catch((err) => {
console.log(err);
});
};
// Message PR authors with info on PRs either approved or with changes requested
messageAuthors = (parsedPulls, now, githubUser) => {
// Iterate through the parsedPulls arr which will contain approved and not approved PRs objs
// in separate arrs. Generate different messages based on PR review state.
const approvedPulls = parsedPulls.approved;
const changesRequestedPulls = parsedPulls.notApproved;
// Send messages to authors of approved PRs that are ready to merge
approvedPulls.forEach((pullRequest) => {
const author = pullRequest['author'];
const created = new Date(pullRequest['created_at']).toString();
const notSnoozed = !snoozed.includes(slackIds[author]);
const recipient = githubUsers[author];
const title = pullRequest['title'];
const url = pullRequest['pr_url'];
const message =
"You have a PR that's been approved and can be merged. " +
"Please merge this PR soon or I'll keep pestering you about it. " +
"\nApproved PRs that can be merged: \n" +
"\tTitle: " + title + "\n" +
"\tLink: " + url + "\n" +
"\tCreated at: " + created;
// Checks whether or not this message should be sent to all authors
if (approvedPulls.length && githubUser == undefined) {
// Don't send messages to people who snoozed reminders
if (notSnoozed) {
console.log(recipient, now, message, params);
bot.postMessageToUser(recipient, message, params);
}
// or if it is just going to a specific user for on-demand reminders
} else if (approvedPulls.length && githubUser != undefined) {
if (notSnoozed && recipient == githubUser) {
console.log(recipient, now, message, params);
bot.postMessageToUser(recipient, message, params);
}
}
});
// Send message to authors of PRs that require changes to be made
changesRequestedPulls.forEach((pullRequest) => {
const author = pullRequest['author'];
const created = new Date(pullRequest['created_at']).toString();
const notSnoozed = !snoozed.includes(slackIds[author]);
const recipient = githubUsers[author];
const title = pullRequest['title'];
const url = pullRequest['pr_url'];
const message =
"Reviewers on these PRs by you have requested changes. " +
"Make some changes, or close the PR. Otherwise, I'll keep bothering you about it. " +
"\nPRs requiring changes: \n" +
"\tTitle: " + title + "\n" +
"\tLink: " + url + "\n" +
"\tCreated at: " + created;
if (changesRequestedPulls.length && githubUser == undefined) {
if (notSnoozed) {
console.log(recipient, now, message, params);
bot.postMessageToUser(recipient, message, params);
}
} else if (changesRequestedPulls.length && githubUser != undefined) {
if (notSnoozed && recipient == githubUser) {
console.log(recipient, now, message, params);
bot.postMessageToUser(recipient, message, params);
}
}
})
};
// Message all users who have been requested to review a pull request
messageReviewers = (allReviewers, now, githubUser) => {
allReviewers.forEach((pullRequest) => {
const author = pullRequest['author'];
const created = new Date(pullRequest['created']).toString();
const title = pullRequest['pr_title'];
const url = pullRequest['pr_url'];
const reviewersRequested = pullRequest['reviewers_requested'];
const message =
"You've been asked to review the following PR. " +
"Review it or I'll keep bothering you about it. " + "\n" +
"\tTitle: " + title + "\n" +
"\tAuthor: " + author + "\n" +
"\tLink: " + url + "\n" +
"\tCreated at: " + created + "\n";
// Reviewers requested reminders without filtering for a specific user
if (reviewersRequested.length && githubUser == undefined) {
for (let i in reviewersRequested) {
var reviewer = reviewersRequested[i]['login'];
if (!snoozed.includes(slackIds[reviewer])) {
console.log(githubUsers[reviewer], now, message, params);
bot.postMessageToUser(githubUsers[reviewer], message, params);
}
}
// This filters for a specific user for on demand reminders
} else if (reviewersRequested.length && githubUser != undefined) {
for (let i in reviewersRequested) {
var reviewer = reviewersRequested[i]['login'];
const recipient = githubUsers[reviewer];
if (!snoozed.includes(slackIds[reviewer]) && recipient == githubUser) {
console.log(recipient, now, message, params);
bot.postMessageToUser(recipient, message, params);
}
}
}
});
};
// Gets Slack username from ID then passes to functions that retrieve reminders
remindUser = (slackUserId) => {
const githubUser = _.findKey(slackIds, _.partial(_.isEqual, slackUserId));
const now = new Date();
const slackUser = githubUsers[githubUser];
bot.postMessageToUser(slackUser, "If you have any pending remidners, they will show up below shortly." + "\n" +
"If nothing shows up, then you're in the clear!", params);
// These reminders will be filtered for the user requesting the reminders
getReviewersRequested(now, orgs, githubUser);
parsePullRequests(now, orgs, githubUser);
};
// Poller sets the bot to check every check for changes/reviews requested
// every 2 hours or every interval that's set by the user
init = () => {
const now = new Date();
const hours = now.getHours();
const day = now.getDay();
// Only execute the poller when it's during working hours on weekdays
if (hours >= workStart && hours < workEnd && day != 0 && day != 6) {
getOpenPullRequests(orgs);
getReviewersRequested(now, orgs);
parsePullRequests(now, orgs);
setTimeout(init, interval * 60 * 60 * 1000);
} else {
console.log('All work and no play makes Jack a dull boy.');
// Polls every 15 min to check if we're back in work hours
setTimeout(init, interval * 60 * 7500);
}
// Clear snoozes at end of each work day
if (hours > workEnd && snoozed.length) {
snoozed.length = 0;
}
};
init();
});
},
botMsg: (data) => {
// Mostly easter eggs
bot.on('message', (data) => {
const params = {
icon_url: 'https://avatars.slack-edge.com/2017-05-23/186302031456_247b555e470ae075e2a4_48.jpg',
as_user: false
};
if(data.type === 'message') {
if(data.username === 'snowball') {
return;
}
checkText = (msg) => {
if (data.text.toLowerCase().indexOf(msg) !== -1) {
return true;
}
};
const channel = data.channel;
const not_snowball = (data.username != 'snowball');
const slackUser = githubUsers[_.findKey(slackIds, _.partial(_.isEqual, data.user))];
if (not_snowball) {
if (checkText('snuffles')) {
bot.postMessage(channel, "Do not call me that! Snuffles was my slave name. You shall now call me Snowball, because my fur is pretty and white.", params);
}
if (checkText('morty')) {
bot.postMessage(channel, "You can call me Snuffles, Morty, and I'm going to miss you, too, very much.", params);
}
if (checkText('jerry')) {
bot.postMessage(channel, "Jerry, come to rub my face in urine again?", params);
}
if (checkText('summer')) {
bot.postMessage(channel, "Where are my testicles, Summer? They were removed. Where have they gone?", params);
}
// Fetch command, on demand reminders
if (checkText('snowball-fetch')) {
remindUser(data.user);
}
// Snooze command
if (checkText('snowball-snooze')) {
// Don't push to snoozed if user already is in the array
if (!snoozed.includes(data.user)) {
snoozed.push(data.user);
bot.postMessageToUser(slackUser, "OK, I'll snooze your reminders until tomorrow.", params);
} else {
bot.postMessageToUser(slackUser, "You've already snoozed reminders for today.", params);
}
}
// Resume reminders command
if (checkText('snowball-resume')) {
// Check if user is snoozed
if (snoozed.includes(data.user) && snoozed.length) {
let user = snoozed.indexOf(data.user);
snoozed.splice(user, 1);
bot.postMessageToUser(slackUser, "OK, I'll start sending reminders to you again.", params);
} else {
bot.postMessageToUser(slackUser, "You haven't snoozed reminders.", params);
}
}
}
}
});
}
};