2018-02-14 23:57:46 +00:00
// Description:
// GitHub ID mapping to other connected systems (e.g. Slack)
//
// Dependencies:
// mem-cache: "0.0.5"
2018-03-27 18:40:14 +00:00
// memjs: "^1.2.0"
2018-02-14 23:57:46 +00:00
// @slack/client: "^3.16.0"
//
// Author:
// PombeirP
const MemCache = require ( 'mem-cache' )
const { WebClient } = require ( '@slack/client' )
const token = process . env . SLACK _USER _TOKEN || ''
2018-03-29 16:28:04 +00:00
const cacheMemcachedKey = 'slack-profile-cache-json'
2018-04-23 16:33:52 +00:00
const slackIdCacheKeyPrefix = 'Slack-'
const slackUsernameCacheKeyPrefix = 'SlackUN-'
const gitHubIdCacheKeyPrefix = 'GitHub-'
2018-03-28 16:08:52 +00:00
var allowLoadFromCache = true
2018-02-14 23:57:46 +00:00
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 ) )
2018-03-27 19:24:51 +00:00
// Refresh cache every day
2018-03-28 16:08:00 +00:00
setInterval ( ( ) => internalBuild ( this . robot , this . cache ) , 24 * 60 * 60 * 1000 )
2018-02-14 23:57:46 +00:00
}
2018-03-29 16:28:04 +00:00
async getSlackUsernameFromSlackId ( slackUserId ) {
await this . buildPromise
const profile = this . cache . get ( getSlackId2ProfileCacheKeyName ( slackUserId ) )
if ( profile ) {
return profile . name
}
return null
}
2018-04-23 16:33:52 +00:00
async getSlackIdFromSlackUsername ( slackUsername ) {
await this . buildPromise
const id = this . cache . get ( getSlackUsername2IdCacheKeyName ( slackUsername ) )
return id
}
2018-03-28 16:02:12 +00:00
async getGitHubHandleFromSlackId ( slackUserId ) {
2018-02-14 23:57:46 +00:00
await this . buildPromise
2018-03-28 16:02:12 +00:00
const profile = this . cache . get ( getSlackId2ProfileCacheKeyName ( slackUserId ) )
if ( profile ) {
return profile . github _handle
}
return null
2018-02-14 23:57:46 +00:00
}
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 } > `
}
2018-03-27 18:40:14 +00:00
async getMainnetPubKeyFromSlackId ( slackUserId ) {
await this . buildPromise
const profile = this . cache . get ( getSlackId2ProfileCacheKeyName ( slackUserId ) )
if ( profile ) {
return profile . pubkey
}
return null
}
2018-02-14 23:57:46 +00:00
}
async function internalBuild ( robot , cache ) {
2018-03-27 18:40:14 +00:00
const mc = robot [ 'memcache' ]
if ( allowLoadFromCache && mc ) {
2018-03-28 16:08:52 +00:00
try {
2018-03-29 16:28:04 +00:00
const json = await mc . get ( cacheMemcachedKey )
if ( json . value ) {
const cacheFromFile = JSON . parse ( json . value )
2018-03-28 16:08:52 +00:00
for ( const kvp of cacheFromFile ) {
2018-04-23 16:33:52 +00:00
if ( kvp . k . startsWith ( slackIdCacheKeyPrefix ) && ! kvp . v . hasOwnProperty ( 'pubkey' ) ) {
2018-03-27 18:40:14 +00:00
cache . clean ( )
break
}
2018-03-28 16:08:52 +00:00
cache . set ( kvp . k , kvp . v )
2018-04-23 16:33:52 +00:00
if ( kvp . k . startsWith ( slackIdCacheKeyPrefix ) ) {
const profile = kvp . v
cache . set ( getSlackUsername2IdCacheKeyName ( profile . name ) , kvp . k . substring ( slackIdCacheKeyPrefix . length ) )
}
2018-03-28 16:08:52 +00:00
}
allowLoadFromCache = false
2018-03-27 18:40:14 +00:00
if ( cache . length > 0 ) {
robot . log . info ( ` Read Slack user cache from Memcached ( ${ cache . length } entries) ` )
return
}
2018-03-28 16:08:52 +00:00
}
} catch ( error ) {
// Ignore
robot . log . info ( 'Could not find Slack user cache' )
}
}
2018-02-14 23:57:46 +00:00
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
2018-03-27 18:40:14 +00:00
let pubKeyFieldId = null
2018-02-14 23:57:46 +00:00
let usersMissingGitHubInfo = [ ]
2018-03-27 18:40:14 +00:00
let usersMissingMainnetAddress = [ ]
2018-02-14 23:57:46 +00:00
let usersContainingGitHubInfo = [ ]
2018-03-27 18:40:14 +00:00
let usersWithMainnetPubkey = 0
2018-02-14 23:57:46 +00:00
let rateLimitWait = 10000
let profileFetchPreviousBatchCount = 3
let profileFetchBatchCount = 0
for ( let i = 0 ; i < activeUsersList . length ; ) {
const user = activeUsersList [ i ]
try {
++ profileFetchBatchCount
2018-03-27 18:40:14 +00:00
const { profile } = await slackWeb . users . profile . get ( { user : user . id , include _labels : ! gitHubFieldId || ! pubKeyFieldId } )
2018-02-14 23:57:46 +00:00
const username = profile . display _name _normalized || profile . real _name _normalized
if ( ! gitHubFieldId ) {
// Find the field ID for the field with the 'Github ID' label
2018-03-28 16:02:12 +00:00
gitHubFieldId = findProfileLabelId ( profile , 'Github ID' )
2018-02-14 23:57:46 +00:00
}
2018-03-27 18:40:14 +00:00
if ( ! pubKeyFieldId ) {
// Find the field ID for the field with the 'Mainnet Address' label
pubKeyFieldId = findProfileLabelId ( profile , 'Mainnet Address' )
}
2018-02-14 23:57:46 +00:00
if ( ! gitHubFieldId ) {
robot . log . warn ( ` No GitHub ID field found in @ ${ username } ( ${ user . id } ) profile! ` )
}
2018-03-27 18:40:14 +00:00
if ( ! pubKeyFieldId ) {
robot . log . warn ( ` No Mainnet Address field found in @ ${ username } ( ${ user . id } ) profile! ` )
}
2018-02-14 23:57:46 +00:00
2018-03-29 16:28:04 +00:00
const gitHubUsername = gitHubFieldId && profile . fields && profile . fields [ gitHubFieldId ] ? profile . fields [ gitHubFieldId ] . value . replace ( 'https://github.com/' , '' ) : null
2018-03-28 16:02:12 +00:00
if ( gitHubUsername ) {
2018-02-14 23:57:46 +00:00
usersContainingGitHubInfo = usersContainingGitHubInfo . concat ( username )
} else {
usersMissingGitHubInfo = usersMissingGitHubInfo . concat ( username )
}
2018-03-27 18:40:14 +00:00
const pubkey = profile . fields && profile . fields [ pubKeyFieldId ] ? profile . fields [ pubKeyFieldId ] . value : null
if ( pubkey ) {
++ usersWithMainnetPubkey
} else {
usersMissingMainnetAddress = usersMissingMainnetAddress . concat ( username )
}
const data = { name : username , github _handle : gitHubUsername , pubkey : pubkey }
2018-03-28 16:02:12 +00:00
robot . log . debug ( ` @ ${ username } ( ${ user . id } ) -> ${ JSON . stringify ( data ) } ` )
cache . set ( getSlackId2ProfileCacheKeyName ( user . id ) , data )
2018-04-23 16:33:52 +00:00
cache . set ( getSlackUsername2IdCacheKeyName ( username ) , user . id )
2018-03-28 16:02:12 +00:00
if ( gitHubUsername ) {
cache . set ( getGitHub2SlackIdCacheKeyName ( gitHubUsername ) , user . id )
}
2018-02-14 23:57:46 +00:00
++ 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 ( ', ' ) } ` )
}
2018-03-27 18:40:14 +00:00
if ( usersMissingMainnetAddress ) {
robot . log . warn ( ` The following ${ usersMissingMainnetAddress . length } Slack users have no Mainnet address in their profiles: ${ usersMissingMainnetAddress . map ( s => '@' + s ) . join ( ', ' ) } ` )
}
robot . log . info ( ` ${ usersWithMainnetPubkey } users in ${ activeUsersList . length } have a mainnet public key address configured ` )
2018-03-28 16:08:52 +00:00
// Write cache out to JSON file for faster startup next time
const c = [ ]
for ( const key of cache . keys ) {
c . push ( { k : key , v : cache . get ( key ) } )
}
2018-03-29 16:28:04 +00:00
if ( mc ) {
try {
2018-03-27 18:40:14 +00:00
await mc . set ( cacheMemcachedKey , JSON . stringify ( c , { } , 2 ) , { } )
2018-03-29 16:28:04 +00:00
robot . log . info ( ` Saved cache to Memcached ` )
} catch ( error ) {
robot . log . warn ( ` Error while saving cache to Memcached: ${ error } ` )
}
}
2018-02-14 23:57:46 +00:00
} catch ( e ) {
robot . log . error ( ` Error while populating Slack user ID cache: ${ e } ` )
}
}
2018-03-28 16:02:12 +00:00
function findProfileLabelId ( profile , labelName ) {
2018-02-14 23:57:46 +00:00
if ( profile . fields ) {
for ( const fieldId in profile . fields ) {
const field = profile . fields [ fieldId ]
2018-03-28 16:02:12 +00:00
if ( field . label === labelName ) {
2018-02-14 23:57:46 +00:00
return fieldId
}
}
}
return null
}
2018-03-28 16:02:12 +00:00
function getSlackId2ProfileCacheKeyName ( slackUserId ) {
2018-04-23 16:33:52 +00:00
return slackIdCacheKeyPrefix . concat ( slackUserId )
}
function getSlackUsername2IdCacheKeyName ( slackUsername ) {
return slackUsernameCacheKeyPrefix . concat ( slackUsername )
2018-02-14 23:57:46 +00:00
}
function getGitHub2SlackIdCacheKeyName ( gitHubUsername ) {
2018-04-23 16:33:52 +00:00
return gitHubIdCacheKeyPrefix . concat ( gitHubUsername )
2018-02-14 23:57:46 +00:00
}
function timeout ( ms ) {
return new Promise ( resolve => setTimeout ( resolve , ms ) )
}
async function sleep ( timeoutMs ) {
await timeout ( timeoutMs )
}