import * as express from "express" ; import * as _ from "underscore" ; import config from "../config" ; import { Listener } from "../listeners/listener" ; import { SlackActionListener } from "../listeners/slack_action_listener" ; import { SlackEventListener } from "../listeners/slack_event_listener" ; import { Looker } from "../looker" ; import { Message , SentMessage } from "../message" ; import { ReplyContext } from "../reply_context" ; import { SlackUtils } from "../slack_utils" ; import { IChannel , Service } from "./service" ; const { WebClient } = require ( "@slack/web-api" ) ; const botkit = require ( "botkit" ) ; const getUrls = require ( "get-urls" ) ; export class SlackService extends Service { private controller : any ; private defaultBot : any ; private webClient : any ; public async usableChannels ( ) { let channels = await this . usablePublicChannels ( ) ; channels = channels . concat ( await this . usableDMs ( ) ) ; return channels ; } public replyContextForChannelId ( id ) { return new ReplyContext ( this . defaultBot , this . defaultBot , { channel : id } as SentMessage ) ; } protected start ( ) { this . controller = botkit . slackbot ( { debug : config . debugMode , } ) ; this . defaultBot = this . controller . spawn ( { retry : 10 , token : config . slackApiKey , } ) . startRTM ( ) ; this . webClient = this . getSlackWebClient ( config . slackApiKey ) ; this . defaultBot . api . team . info ( { } , ( err , response ) => { if ( response != null ? response . ok : undefined ) { response . team . bot = { id : "lookerbot" , name : "lookerbot" , } ; this . controller . saveTeam ( response . team , ( ) => console . log ( "Saved the team information..." ) ) ; } else { throw new Error ( `Could not connect to the Slack API. Ensure your Slack API key is correct. (${ err })` ) ; } } ) ; this . controller . setupWebserver ( process . env . PORT || 3333 , ( err , expressWebserver ) => { this . controller . createWebhookEndpoints ( expressWebserver ) ; super . setWebserver ( expressWebserver ) ; if ( SlackUtils . slackButtonsEnabled ) { const actionListener = new SlackActionListener ( expressWebserver , this , Looker . all ) ; actionListener . bot = this . defaultBot ; this . startListener ( actionListener ) ; } else { console . log ( "Slack buttons are disabled or not configured." ) ; } const eventListener = new SlackEventListener ( expressWebserver , this , Looker . all ) ; this . startListener ( eventListener ) ; } ) ; const processCommand = ( bot , message , isDM = false ) => { const context = new ReplyContext ( this . defaultBot , bot , message ) ; context . isDM = isDM ; this . ensureUserAuthorized ( context , ( ) => { this . messageHandler ( context ) ; } ) ; } ; this . controller . on ( "rtm_reconnect_failed" , ( ) => { throw new Error ( "Failed to reconnect to the Slack RTM API." ) ; } ) ; this . controller . on ( "slash_command" , ( bot , message ) => { if ( ! SlackUtils . checkToken ( bot , message ) ) { return ; } processCommand ( bot , message ) ; } ) ; this . controller . on ( "direct_mention" , ( bot , message ) => { message . text = SlackUtils . stripMessageText ( message . text ) ; processCommand ( bot , message ) ; } ) ; this . controller . on ( "direct_message" , ( bot , message ) => { if ( message . text . indexOf ( "/" ) !== 0 ) { message . text = SlackUtils . stripMessageText ( message . text ) ; processCommand ( bot , message , true ) ; } } ) ; this . controller . on ( "ambient" , ( bot , message ) => { if ( ! message . text || ( message . subtype === "bot_message" ) ) { return ; } if ( process . env . LOOKER_SLACKBOT_EXPAND_URLS !== "true" ) { return ; } const context = new ReplyContext ( this . defaultBot , bot , message ) ; this . ensureUserAuthorized ( context , ( ) => { getUrls ( message . text ) . forEach ( ( url ) => { this . urlHandler ( context , url . replace ( "%3E" , "" ) ) ; } ) ; } , { silent : true } ) ; } ) ; } private getSlackWebClient ( token ) { return new WebClient ( token ) ; } private async usablePublicChannels ( ) { const result = await this . webClient . conversations . list ( { exclude_archived : true } ) ; let channels = result . channels . filter ( ( c ) => c . is_member && ! c . is_archived ) ; channels = _ . sortBy ( channels , "name" ) ; const reformatted = channels . map ( ( channel ) => ( { id : channel . id , label : `#${ channel . name }` } ) ) ; return reformatted ; } private usableDMs ( ) { return new Promise < IChannel [ ] > ( ( resolve , reject ) => { this . defaultBot . api . users . list ( { } , ( err , response ) => { if ( err || ! response . ok ) { reject ( err ) ; } else { let users = response . members . filter ( ( u ) => { return ! u . is_restricted && ! u . is_ultra_restricted && ! u . is_bot && ! u . is_app_user && ! u . deleted ; } ) ; users = _ . sortBy ( users , "name" ) ; const reformatted = users . map ( ( user ) => ( { id : user . id , label : `@${ user . name }` } ) ) ; resolve ( reformatted ) ; } } ) ; } ) ; } private ensureUserAuthorized ( context , callback , options = { silent : false } ) { const reply = ( text ) => { if ( ! options . silent ) { context . replyPrivate ( { text } ) ; } } ; this . defaultBot . api . users . info ( { user : context . sourceMessage . user } , ( error , response ) => { if ( error || ! response . user ) { reply ( `Could not fetch your user info from Slack. ${ error || "" }` ) ; } else { const user = response . user ; if ( ! config . enableGuestUsers && ( user . is_restricted || user . is_ultra_restricted ) ) { reply ( `Sorry @${ user . name }, as a guest user you're not able to use this command.` ) ; } else if ( ! config . enableSharedWorkspaces && user . team_id !== this . defaultBot . team_info . id && ! ( user . teams && user . teams . includes ( this . defaultBot . team_info . id ) ) ) { reply ( `Sorry @${ user . name }, as a user from another workspace you're not able to use this command.` ) ; } else if ( user . is_stranger ) { reply ( `Sorry @${ user . name }, as a user from another workspace you're not able to use this command.` ) ; } else if ( user . is_bot || user . is_app_user ) { reply ( `Sorry @${ user . name }, as a bot you're not able to use this command.` ) ; } else { callback ( ) ; } } } ) ; } }