diff --git a/.env.example b/.env.example index bfce2ea..7d665fc 100644 --- a/.env.example +++ b/.env.example @@ -9,4 +9,7 @@ WEBHOOK_SECRET=development WEBHOOK_PROXY_URL=https://smee.io/ZyQCjZTDPT3pd4SD # The "Bot User OAuth Access Token" of your Slack App -SLACK_BOT_TOKEN= \ No newline at end of file +SLACK_BOT_TOKEN= + +# A "User Legacy Token" of your Slack App, used to access the "users.profile.get" API +SLACK_USER_TOKEN= \ No newline at end of file diff --git a/index.js b/index.js index 03cd663..4497a8e 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,21 @@ -module.exports = (robot) => { +var MemCache = require('mem-cache') +var SlackGitHubCacheBuilder = require('./lib/retrieve-slack-github-users') + +module.exports = async (robot) => { console.log('Yay, the app was loaded!') + var slackGitHubCache = new MemCache({ timeoutDisabled: true }) + var slackCachePromise = SlackGitHubCacheBuilder.build(robot, slackGitHubCache) + require('./scripts/assign-new-pr-to-review')(robot) require('./scripts/assign-approved-pr-to-test')(robot) require('./scripts/assign-to-bounty-awaiting-for-approval')(robot) require('./scripts/greet-new-contributor')(robot) + await slackCachePromise + + // Add scripts which require using the Slack/GitHub cache after this comment + // For more information on building apps: // https://probot.github.io/docs/ diff --git a/lib/retrieve-slack-github-users.js b/lib/retrieve-slack-github-users.js new file mode 100644 index 0000000..d13761d --- /dev/null +++ b/lib/retrieve-slack-github-users.js @@ -0,0 +1,124 @@ +// 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.getGitHubIdFromSlackUsername = (slackUsername, cache) => { + return cache.get(getSlackCacheKeyName(slackUsername)) +} + +module.exports.getSlackUsernameFromGitHubId = (gitHubId, cache) => { + return cache.get(getGitHubCacheKeyName(gitHubId)) +} + +async function populateCache (robot, web, cache) { + robot.log.info('Populating Slack username 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 profile (@${username})!`) + ++i + continue + } + + if (profile.fields && profile.fields[gitHubFieldId]) { + const gitHubUsername = profile.fields[gitHubFieldId].value + robot.log.debug(`@${username} -> ${gitHubUsername}`) + + cache.set(getSlackCacheKeyName(username), gitHubUsername) + cache.set(getGitHubCacheKeyName(gitHubUsername), username) + 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 username 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 username 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 getSlackCacheKeyName (slackUsername) { + return `Slack-${slackUsername}` +} + +function getGitHubCacheKeyName (gitHubUsername) { + return `GitHub-${gitHubUsername}` +} + +function timeout (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +async function sleep (timeoutMs) { + await timeout(timeoutMs) +} diff --git a/package-lock.json b/package-lock.json index 61a0078..a726e7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4542,6 +4542,11 @@ "mimic-fn": "1.1.0" } }, + "mem-cache": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mem-cache/-/mem-cache-0.0.5.tgz", + "integrity": "sha1-EUCWcbMQEXPY3PQgcK5nv1Dzd/8=" + }, "merge": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", diff --git a/package.json b/package.json index 23fbf0a..d1225f1 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "eslint": "^4.16.0", + "mem-cache": "0.0.5", "probot": "^5.0.0", "probot-config": "^0.1.0", "probot-gpg-status": "^0.5.4",