diff --git a/bot_scripts/bounty-awaiting-approval-slack-ping.js b/bot_scripts/bounty-awaiting-approval-slack-ping.js index f156427..50c9e48 100644 --- a/bot_scripts/bounty-awaiting-approval-slack-ping.js +++ b/bot_scripts/bounty-awaiting-approval-slack-ping.js @@ -18,20 +18,20 @@ const slackHelper = require('../lib/slack') const botName = 'bounty-awaiting-approval-slack-ping' -module.exports = (robot, getSlackMentionFromGitHubId) => { - registerForNewBounties(robot, getSlackMentionFromGitHubId) +module.exports = (robot) => { + registerForNewBounties(robot) } -function registerForNewBounties (robot, getSlackMentionFromGitHubId) { +function registerForNewBounties (robot) { robot.on('issues.labeled', async context => { // Make sure we don't listen to our own messages if (context.isBot) return null - await notifyCollaborators(context, robot, getSlackMentionFromGitHubId) + await notifyCollaborators(context, robot) }) } -async function notifyCollaborators (context, robot, getSlackMentionFromGitHubId) { +async function notifyCollaborators (context, robot) { const { github, payload } = context const ownerName = payload.repository.owner.login const repoName = payload.repository.name @@ -57,12 +57,12 @@ async function notifyCollaborators (context, robot, getSlackMentionFromGitHubId) robot.log(`${botName} - issue #${payload.issue.number} on ${ownerName}/${repoName} was labeled as a bounty awaiting approval. Pinging slack...`) - const slackCollaborators = await getSlackCollaborators(ownerName, repoName, github, robot, gitHubTeamConfig, getSlackMentionFromGitHubId) + const slackCollaborators = await getSlackCollaborators(ownerName, repoName, github, robot, gitHubTeamConfig) // Mention the project board owner as well, if configured const bountyProjectBoardOwner = bountyProjectBoardConfig['owner'] if (bountyProjectBoardOwner) { - const slackUserMention = getSlackMentionFromGitHubId(bountyProjectBoardOwner) + const slackUserMention = robot.gitHubIdMapper.getSlackMentionFromGitHubId(bountyProjectBoardOwner) if (slackUserMention) { slackCollaborators.push(slackUserMention) } @@ -82,7 +82,7 @@ function randomInt (low, high) { } // Get the Slack IDs of the collaborators of this repo. -async function getSlackCollaborators (ownerName, repoName, github, robot, gitHubTeamConfig, getSlackMentionFromGitHubId) { +async function getSlackCollaborators (ownerName, repoName, github, robot, gitHubTeamConfig) { const teamSlug = gitHubTeamConfig['slug'] if (!teamSlug) { robot.log.debug(`${botName} - GitHub team slug not configured in repo ${ownerName}/${repoName}, ignoring`) @@ -100,12 +100,21 @@ async function getSlackCollaborators (ownerName, repoName, github, robot, gitHub const teamMembers = await github.paginate(github.orgs.getTeamMembers({id: team.id, per_page: 100}), res => res.data) // Create an array of Slack usernames from GitHub usernames - const slackUsers = teamMembers.map(u => u.login).map(getSlackMentionFromGitHubId).filter(id => id) + const gitHubUsers = teamMembers.map(u => u.login) + const slackUsers = new HashSet() + for (const gitHubUser of gitHubUsers) { + const id = await robot.gitHubIdMapper.getSlackMentionFromGitHubId(gitHubUser) + if (id) { + slackUsers.add(id) + } + } + + // Select 2 random Slack team members const randomTeamMemberLimit = 2 const selectedSlackUsers = new HashSet() - while (selectedSlackUsers.length < randomTeamMemberLimit || selectedSlackUsers.length < slackUsers.length) { - const slackUser = slackUsers[randomInt(0, slackUsers.length)] + while (selectedSlackUsers.size() < randomTeamMemberLimit || selectedSlackUsers.size() < slackUsers.size()) { + const slackUser = slackUsers[randomInt(0, slackUsers.size())] selectedSlackUsers.add(slackUser) } diff --git a/bot_scripts/notify-reviewers-via-slack.js b/bot_scripts/notify-reviewers-via-slack.js index 3e969d0..a4c38f4 100644 --- a/bot_scripts/notify-reviewers-via-slack.js +++ b/bot_scripts/notify-reviewers-via-slack.js @@ -11,24 +11,24 @@ const slackHelper = require('../lib/slack') const botName = 'notify-reviewers-via-slack' -module.exports = (robot, getSlackIdFromGitHubId) => { +module.exports = (robot) => { robot.log(`${botName} - Starting...`) - registerForNewReviewRequests(robot, getSlackIdFromGitHubId) + registerForNewReviewRequests(robot) } -function registerForNewReviewRequests (robot, getSlackIdFromGitHubId) { +function registerForNewReviewRequests (robot) { robot.on('pull_request.review_requested', async context => { // Make sure we don't listen to our own messages if (context.isBot) return null - await notifyReviewer(context, robot, getSlackIdFromGitHubId) + await notifyReviewer(context, robot) }) } -async function notifyReviewer (context, robot, getSlackIdFromGitHubId) { +async function notifyReviewer (context, robot) { const { payload } = context const reviewer = payload.requested_reviewer - const userID = getSlackIdFromGitHubId(reviewer.login) + const userID = await robot.gitHubIdMapper.getSlackIdFromGitHubId(reviewer.login) if (!userID) { robot.log.warn('Could not find Slack ID for GitHub user', reviewer.login) diff --git a/index.js b/index.js index 3b865ba..a97e81c 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,3 @@ -const MemCache = require('mem-cache') -const slackGitHubCache = new MemCache({ timeoutDisabled: true }) -const SlackGitHubCacheBuilder = require('./lib/retrieve-slack-github-users') const Slack = require('./lib/slack') module.exports = async (robot) => { @@ -8,7 +5,7 @@ module.exports = async (robot) => { Slack(robot, slack => {}) - await new Promise((resolve, reject) => { + await new Promise(resolve => { robot.on('slack.connected', event => { robot.log.info(`Connected to Slack`) @@ -19,20 +16,15 @@ module.exports = async (robot) => { }) }) - const slackCachePromise = SlackGitHubCacheBuilder.build(robot, slackGitHubCache) + robot['gitHubIdMapper'] = require('./lib/github-id-mapper')(robot) require('./bot_scripts/assign-new-pr-to-review')(robot) require('./bot_scripts/assign-approved-pr-to-test')(robot) require('./bot_scripts/assign-to-bounty-awaiting-for-approval')(robot) require('./bot_scripts/greet-new-contributor')(robot) require('./bot_scripts/trigger-automation-test-build')(robot) - - await slackCachePromise - robot.log.info('Slack user ID cache populated, loading remainder of scripts') - - // Add scripts which require using the Slack/GitHub cache after this comment - require('./bot_scripts/bounty-awaiting-approval-slack-ping')(robot, getSlackMentionFromGitHubId) - require('./bot_scripts/notify-reviewers-via-slack')(robot, getSlackIdFromGitHubId) + require('./bot_scripts/bounty-awaiting-approval-slack-ping')(robot) + require('./bot_scripts/notify-reviewers-via-slack')(robot) // For more information on building apps: // https://probot.github.io/docs/ @@ -40,15 +32,3 @@ module.exports = async (robot) => { // To get your app running against GitHub, see: // https://probot.github.io/docs/development/ } - -function getSlackMentionFromGitHubId (gitHubId) { - const id = SlackGitHubCacheBuilder.getSlackIdFromGitHubId(gitHubId, slackGitHubCache) - if (!id) { - return null - } - return `<@${id}>` -} - -function getSlackIdFromGitHubId (gitHubId) { - return SlackGitHubCacheBuilder.getSlackIdFromGitHubId(gitHubId, slackGitHubCache) -} diff --git a/lib/retrieve-slack-github-users.js b/lib/github-id-mapper.js similarity index 70% rename from lib/retrieve-slack-github-users.js rename to lib/github-id-mapper.js index 75dede3..04ddd77 100644 --- a/lib/retrieve-slack-github-users.js +++ b/lib/github-id-mapper.js @@ -1,124 +1,143 @@ -// Description: -// Configuration-related functionality -// -// Dependencies: -// "mem-cache": "0.0.5" -// -// Author: -// PombeirP - -const { WebClient } = require('@slack/client') -const token = process.env.SLACK_USER_TOKEN || '' - -module.exports.build = async (robot, cache) => { - const web = new WebClient(token) - await populateCache(robot, web, cache) -} - -module.exports.getGitHubIdFromSlackId = (slackUserId, cache) => { - return cache.get(getSlackId2GitHubIdCacheKeyName(slackUserId)) -} - -module.exports.getSlackIdFromGitHubId = (gitHubId, cache) => { - return cache.get(getGitHub2SlackIdCacheKeyName(gitHubId)) -} - -async function populateCache (robot, web, cache) { - robot.log.info('Populating Slack user ID cache...') - - try { - const usersList = await web.users.list() // TODO: This call should be paginated to avoid hitting limits (memory, API): https://api.slack.com/docs/pagination#cursors - const activeUsersList = usersList.members.filter(u => !u.deleted && !u.is_bot && u.id !== 'USLACKBOT') - - let gitHubFieldId = null - let usersMissingGitHubInfo = [] - let usersContainingGitHubInfo = [] - let rateLimitWait = 10000 - let profileFetchPreviousBatchCount = 3 - let profileFetchBatchCount = 0 - for (let i = 0; i < activeUsersList.length;) { - const user = activeUsersList[i] - - try { - ++profileFetchBatchCount - const { profile } = await web.users.profile.get({ user: user.id, include_labels: !gitHubFieldId }) - const username = profile.display_name_normalized || profile.real_name_normalized - - if (!gitHubFieldId) { - // Find the field ID for the field with the 'Github ID' label - gitHubFieldId = findGitHubLabelId(profile) - } - - if (!gitHubFieldId) { - robot.log.warn(`No GitHub ID field found in @${username} (${user.id}) profile!`) - ++i - continue - } - - if (profile.fields && profile.fields[gitHubFieldId]) { - const gitHubUsername = profile.fields[gitHubFieldId].value - robot.log.debug(`@${username} (${user.id}) -> ${gitHubUsername}`) - - cache.set(getSlackId2GitHubIdCacheKeyName(user.id), gitHubUsername) - cache.set(getGitHub2SlackIdCacheKeyName(gitHubUsername), user.id) - usersContainingGitHubInfo = usersContainingGitHubInfo.concat(username) - } else { - robot.log.warn(`@${username} (${user.id}) has no GitHub ID set`) - usersMissingGitHubInfo = usersMissingGitHubInfo.concat(username) - } - - ++i - await sleep(1500) - } catch (e) { - if (e.name === 'Error' && e.message === 'ratelimited') { - robot.log.trace(`Rate-limited, waiting ${rateLimitWait / 1000}s...`) - await sleep(rateLimitWait) - if (profileFetchBatchCount < profileFetchPreviousBatchCount) { - // If we managed to fetch fewer profiles than the last time we got rate-limited, then try increasing the wait period - rateLimitWait += 5000 - } - profileFetchPreviousBatchCount = profileFetchBatchCount - profileFetchBatchCount = 0 - continue - } - - throw e - } - } - robot.log.info(`Populated Slack user ID cache with ${usersContainingGitHubInfo.length} users: ${usersContainingGitHubInfo.map(s => '@' + s).join(', ')}`) - if (usersMissingGitHubInfo) { - robot.log.warn(`The following ${usersMissingGitHubInfo.length} Slack users have no GitHub info in their profiles: ${usersMissingGitHubInfo.map(s => '@' + s).join(', ')}`) - } - } catch (e) { - robot.log.error(`Error while populating Slack user ID cache: ${e}`) - } -} - -function findGitHubLabelId (profile) { - if (profile.fields) { - for (const fieldId in profile.fields) { - const field = profile.fields[fieldId] - if (field.label === 'Github ID') { - return fieldId - } - } - } - - return null -} - -function getSlackId2GitHubIdCacheKeyName (slackUserId) { - return `Slack-${slackUserId}` -} - -function getGitHub2SlackIdCacheKeyName (gitHubUsername) { - return `GitHub-${gitHubUsername}` -} - -function timeout (ms) { - return new Promise(resolve => setTimeout(resolve, ms)) -} - -async function sleep (timeoutMs) { - await timeout(timeoutMs) -} +// Description: +// GitHub ID mapping to other connected systems (e.g. Slack) +// +// Dependencies: +// mem-cache: "0.0.5" +// @slack/client: "^3.16.0" +// +// Author: +// PombeirP + +const MemCache = require('mem-cache') +const { WebClient } = require('@slack/client') + +const token = process.env.SLACK_USER_TOKEN || '' + +module.exports = (robot) => new GitHubSlackIdMapper(robot) + +class GitHubSlackIdMapper { + constructor (robot) { + this.robot = robot + this.cache = new MemCache({ timeoutDisabled: true }) + this.buildPromise = new Promise((resolve, reject) => internalBuild(this.robot, this.cache).then(resolve).catch(reject)) + } + + async getGitHubIdFromSlackId (slackUserId, cache) { + await this.buildPromise + return cache.get(getSlackId2GitHubIdCacheKeyName(slackUserId)) + } + + async getSlackIdFromGitHubId (gitHubId) { + await this.buildPromise + return this.cache.get(getGitHub2SlackIdCacheKeyName(gitHubId)) + } + + async getSlackMentionFromGitHubId (gitHubId) { + const id = await this.getSlackIdFromGitHubId(gitHubId) + if (!id) { + return null + } + return `<@${id}>` + } +} + +async function internalBuild (robot, cache) { + robot.log.info('Populating Slack user ID cache...') + + try { + const slackWeb = new WebClient(token) // We need to use a different token because users.profile API is not available to bot users + const usersList = await slackWeb.users.list() // TODO: This call should be paginated to avoid hitting limits (memory, API): https://api.slack.com/docs/pagination#cursors + const activeUsersList = usersList.members.filter(u => !u.deleted && !u.is_bot && u.id !== 'USLACKBOT') + + let gitHubFieldId = null + let usersMissingGitHubInfo = [] + let usersContainingGitHubInfo = [] + let rateLimitWait = 10000 + let profileFetchPreviousBatchCount = 3 + let profileFetchBatchCount = 0 + for (let i = 0; i < activeUsersList.length;) { + const user = activeUsersList[i] + + try { + ++profileFetchBatchCount + const { profile } = await slackWeb.users.profile.get({ user: user.id, include_labels: !gitHubFieldId }) + const username = profile.display_name_normalized || profile.real_name_normalized + + if (!gitHubFieldId) { + // Find the field ID for the field with the 'Github ID' label + gitHubFieldId = findGitHubLabelId(profile) + } + + if (!gitHubFieldId) { + robot.log.warn(`No GitHub ID field found in @${username} (${user.id}) profile!`) + ++i + continue + } + + if (profile.fields && profile.fields[gitHubFieldId]) { + const gitHubUsername = profile.fields[gitHubFieldId].value + robot.log.debug(`@${username} (${user.id}) -> ${gitHubUsername}`) + + cache.set(getSlackId2GitHubIdCacheKeyName(user.id), gitHubUsername) + cache.set(getGitHub2SlackIdCacheKeyName(gitHubUsername), user.id) + usersContainingGitHubInfo = usersContainingGitHubInfo.concat(username) + } else { + robot.log.warn(`@${username} (${user.id}) has no GitHub ID set`) + usersMissingGitHubInfo = usersMissingGitHubInfo.concat(username) + } + + ++i + await sleep(1500) + } catch (e) { + if (e.name === 'Error' && e.message === 'ratelimited') { + robot.log.trace(`Rate-limited, waiting ${rateLimitWait / 1000}s...`) + await sleep(rateLimitWait) + if (profileFetchBatchCount < profileFetchPreviousBatchCount) { + // If we managed to fetch fewer profiles than the last time we got rate-limited, then try increasing the wait period + rateLimitWait += 5000 + } + profileFetchPreviousBatchCount = profileFetchBatchCount + profileFetchBatchCount = 0 + continue + } + + throw e + } + } + robot.log.info(`Populated Slack user ID cache with ${usersContainingGitHubInfo.length} users: ${usersContainingGitHubInfo.map(s => '@' + s).join(', ')}`) + if (usersMissingGitHubInfo) { + robot.log.warn(`The following ${usersMissingGitHubInfo.length} Slack users have no GitHub info in their profiles: ${usersMissingGitHubInfo.map(s => '@' + s).join(', ')}`) + } + } catch (e) { + robot.log.error(`Error while populating Slack user ID cache: ${e}`) + } +} + +function findGitHubLabelId (profile) { + if (profile.fields) { + for (const fieldId in profile.fields) { + const field = profile.fields[fieldId] + if (field.label === 'Github ID') { + return fieldId + } + } + } + + return null +} + +function getSlackId2GitHubIdCacheKeyName (slackUserId) { + return `Slack-${slackUserId}` +} + +function getGitHub2SlackIdCacheKeyName (gitHubUsername) { + return `GitHub-${gitHubUsername}` +} + +function timeout (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +async function sleep (timeoutMs) { + await timeout(timeoutMs) +} diff --git a/lib/slack-github-id-map.js b/lib/slack-github-id-map.js new file mode 100644 index 0000000..e69de29