class Driftwood::Slack

Public Class Methods

new(config) click to toggle source
# File lib/driftwood/slack.rb, line 4
def initialize(config)
  @config   = config
  @teams    = {}
  @handlers = {}

  $logger.info "Started Slack bot"
end

Public Instance Methods

all_channels() click to toggle source
# File lib/driftwood/slack.rb, line 225
def all_channels
  @teams.keys.inject([]) do |channels, team_id|
    channels.concat(channels(team_id))
  end
end
all_messages(starting_from = 0) click to toggle source
# File lib/driftwood/slack.rb, line 237
def all_messages(starting_from = 0)
  @teams.keys.inject([]) do |messages, team_id|
    messages.concat(messages(team_id, starting_from))
  end
end
all_users() click to toggle source
# File lib/driftwood/slack.rb, line 231
def all_users
  @teams.keys.inject([]) do |users, team_id|
    users.concat(users(team_id))
  end
end
authorize(auth) click to toggle source
# File lib/driftwood/slack.rb, line 12
def authorize(auth)
  team_id = nil
  client  = Slack::Web::Client.new
  # OAuth Step 3: Success or failure
  # is this a cached authentication object?
  if auth.is_a? Array
    $logger.info "Using cached authentication for:"
    auth.each do |row|
      team_id         = row.delete(:team_id)
      @teams[team_id] = row
      $logger.info "  * #{row[:name] || team_id}"
    end

  # we have a token from a single oauth exchange
  elsif auth.is_a? String
    begin
      response = client.oauth_access(
        {
          client_id:     @config[:client_id],
          client_secret: @config[:client_secret],
          redirect_uri:  @config[:redirect_uri],
          code:          auth
        }
      )
      team_id = response['team_id']
      $logger.info "Authenticated new integration for team '#{team_id}'."
    rescue Slack::Web::Api::Error => e
      # Failure:
      # D'oh! Let the user know that something went wrong and output the error message returned by the Slack client.
      $logger.warn "Slack Authentication failed! Reason: #{e.message}"
      raise "Slack Authentication failed! Reason: #{e.message}"
    end

    # Yay! Auth succeeded! Let's store the tokens
    @teams[team_id] = {
      :user_access_token => response['access_token'],
      :bot_user_id       => response['bot']['bot_user_id'],
      :bot_access_token  => response['bot']['bot_access_token']
    }
  else
    raise "The authentication must either be a cached auth object or a token, not #{auth.class}"
  end

  # Create each Team's bot user and authorize the app to access the Team's Events.
  @teams.each do |team_id, team|
    begin
      team[:client] = create_slack_client(team[:bot_access_token])
      team[:user]   = create_slack_client(team[:user_access_token])
      team[:name]   = team_name(team_id)
      $logger.info "Authenticated tokens for team '#{team[:name]}'."
      $logger.debug JSON.pretty_generate(@teams[team_id])
    rescue Slack::Web::Api::Error => e
      $logger.error "Cached authentication failed for team '#{team[:name] || team_id}'."
    end
  end

  # return the hash structure w/o the client object for caching
  team(team_id)
end
channel_info(team_id, channel_id) click to toggle source
# File lib/driftwood/slack.rb, line 247
def channel_info(team_id, channel_id)
  @teams[team_id][:client].channels_info(channel: channel_id)['channel']
end
channel_name(team_id, channel_id) click to toggle source
# File lib/driftwood/slack.rb, line 260
def channel_name(team_id, channel_id)
  channel_info(team_id, channel_id)['name'] rescue channel_id
end
channels(team_id) click to toggle source
# File lib/driftwood/slack.rb, line 175
def channels(team_id)
  preserve = ['team_id', 'channel_id', 'name', 'created', 'is_private', 'topic', 'purpose', 'num_members']
  channels = @teams[team_id][:client].conversations_list(:types => 'public_channel, private_channel')
  channels = channels['channels'].reject{|i| i['is_archived']}
  channels.each do |row|
    row['team_id']    = team_id
    row['topic']      = row['topic']['value']
    row['purpose']    = row['purpose']['value']
    row['channel_id'] = row.delete('id')
    row.select! {|field| preserve.include? field}
  end
end
create_slack_client(slack_api_secret) click to toggle source
# File lib/driftwood/slack.rb, line 84
def create_slack_client(slack_api_secret)
  Slack.configure do |config|
    config.token = slack_api_secret
    fail 'Missing API token' unless config.token
  end
  Slack::Web::Client.new
end
event(request_data) click to toggle source
# File lib/driftwood/slack.rb, line 92
def event(request_data)
  # Check the verification token provided with the request to make sure it matches the verification token in
  # your app's setting to confirm that the request came from Slack.
  unless @config[:verification_token] == request_data['token']
    $logger.warn "Invalid Slack verification token received: #{request_data['token']}"
    raise "Invalid Slack verification token received: #{request_data['token']}"
  end

  case request_data['type']
  # When you enter your Events webhook URL into your app's Event Subscription settings, Slack verifies the
  # URL's authenticity by sending a challenge token to your endpoint, expecting your app to echo it back.
  # More info: https://api.slack.com/events/url_verification
  when 'url_verification'
    $logger.debug "Authorizing challenge: #{request_data['challenge']}"
    return request_data['challenge']

  when 'event_callback'
    # Get the Team ID and Event data from the request object
    team_id    = request_data['team_id']
    event_data = request_data['event']
    event_type = event_data['type']
    user_id    = event_data['user']

    # Don't process events from our bot user
    unless user_id == @teams[team_id][:bot_user_id]
      if @handlers.include? event_type
        begin
          @handlers[event_type].each do |handler|
            handler.call(team_id, event_data)
          end
        rescue => e
          $logger.error "Handler for #{event_type} failed: #{e.message}"
          $logger.debug e.backtrace.join("\n")
        end
      else
        $logger.info "Unhandled event:"
        $logger.debug JSON.pretty_generate(request_data)
      end
    end

  end
  'ok'
end
messages(team_id, starting_from = 0) click to toggle source
# File lib/driftwood/slack.rb, line 204
def messages(team_id, starting_from = 0)
  messages = []
  channels = channels(team_id).map{|c| c['channel_id']}

  channels.each do |channel_id|
    oldest = starting_from
    data   = { 'has_more' => true }

    while data['has_more'] do
      # note that we need the *user* scope here
      data   = @teams[team_id][:user].channels_history(:channel => channel_id,
                                                       :count   => 1000,
                                                       :oldest  => oldest)

      oldest = data['messages'].last['ts'] rescue oldest
      messages.concat normalize_messages(team_id, channel_id, data['messages'])
    end
  end
  messages
end
my_ims() click to toggle source
# File lib/driftwood/slack.rb, line 307
def my_ims
  @teams.map do |team_id, team|
    team[:client].im_list['ims'].map { |im| im['id'] }
  end.flatten
end
normalize_message(team_id, message) click to toggle source
# File lib/driftwood/slack.rb, line 291
def normalize_message(team_id, message)
  preserve_fields = [ 'team_id', 'user_id', 'text', 'channel_id','ts' ]

  message['team_id']    = team_id
  message['user_id']    = message.delete('user')
  message['channel_id'] = message.delete('channel')
  message.select! {|field| preserve_fields.include? field}
end
normalize_messages(team_id, channel_id, messages) click to toggle source
# File lib/driftwood/slack.rb, line 300
def normalize_messages(team_id, channel_id, messages)
  messages.map do |message|
    message['channel'] = channel_id
    normalize_message(team_id, message)
  end
end
normalize_user(user) click to toggle source
# File lib/driftwood/slack.rb, line 268
def normalize_user(user)
  # Why slack? Why?
  return if user['deleted']

  preserve_fields = [ 'team_id', 'user_id', 'name', 'real_name','display_name',
                      'updated', 'deleted', 'tz', 'tz_offset', 'is_owner', 'is_admin',
                      'status_text', 'status_emoji', 'image_72', 'image_192',
                      'title', 'phone', 'skype', 'email',
                    ]

  user['user_id']      = user.delete('id')
  user['title']        = user['profile']['title']
  user['phone']        = user['profile']['phone']
  user['skype']        = user['profile']['skype']
  user['email']        = user['profile']['email']
  user['display_name'] = user['profile']['display_name']
  user['status_text']  = user['profile']['status_text']
  user['status_emoji'] = user['profile']['status_emoji']
  user['image_72']     = user['profile']['image_72']
  user['image_192']    = user['profile']['image_192']
  user.select! {|field| preserve_fields.include? field}
end
real_name(team_id, user_id) click to toggle source
# File lib/driftwood/slack.rb, line 256
def real_name(team_id, user_id)
  user_info(team_id, user_id)['real_name'] rescue user_id
end
register_handler(event, &block) click to toggle source
# File lib/driftwood/slack.rb, line 72
def register_handler(event, &block)
  raise ArgumentError, "Handler (#{event}): no block passed" unless block_given?
  raise ArgumentError, "Handler (#{event}): incorrect parameters" unless block.parameters.length == 2

  @handlers[event] ||= Array.new
  @handlers[event]  << block

  $logger.info "Registered slack handler for #{event}"
  $logger.debug 'Current handlers:'
  $logger.debug @handlers.inspect
end
send_response(team_id, channel, text, unfurl_links = false, attachment = nil, ts = nil) click to toggle source

Send a response to an Event via the Web API.

# File lib/driftwood/slack.rb, line 137
def send_response(team_id, channel, text, unfurl_links = false, attachment = nil, ts = nil)
  # `ts` is optional, depending on whether we're sending the initial
  # welcome message or updating the existing welcome message tutorial items.
  # We open a new DM with `chat.postMessage` and update an existing DM with
  # `chat.update`.
  if ts
    @teams[team_id][:client].chat_update(
      as_user: 'true',
      channel: channel,
      ts: ts,
      text: text,
      attachments: attachment,
      unfurl_links: unfurl_links,
    )
  else
    @teams[team_id][:client].chat_postMessage(
      as_user: 'true',
      channel: channel,
      text: text,
      attachments: attachment,
      unfurl_links: unfurl_links,
    )
  end
end
shell() click to toggle source
# File lib/driftwood/slack.rb, line 318
def shell
  require 'pry'
  binding.pry
end
team(team_id) click to toggle source
# File lib/driftwood/slack.rb, line 162
def team(team_id)
  teams.select {|team| team[:team_id] == team_id }.first
end
team_info(team_id) click to toggle source
# File lib/driftwood/slack.rb, line 251
def team_info(team_id)
  @teams[team_id][:client].team_info()['team']
end
team_name(team_id) click to toggle source
# File lib/driftwood/slack.rb, line 264
def team_name(team_id)
  team_info(team_id)['name'] rescue team_id
end
teams() click to toggle source
# File lib/driftwood/slack.rb, line 166
def teams()
  # munge into an array of hashes, easier for external tools to use.
  # ensure that the client object is removed to prevent serialization issues
  # This weird copy-like thing is to prevent mutation of the original object
  @teams.map do |team_id, team|
    {:team_id => team_id}.merge(team.reject { |key,value| [:client, :user].include? key })
  end
end
to_me?(event_data) click to toggle source
# File lib/driftwood/slack.rb, line 313
def to_me?(event_data)
  return false unless event_data['channel_type'] == 'im'
  my_ims.include? event_data['channel']
end
user_info(team_id, user_id) click to toggle source
# File lib/driftwood/slack.rb, line 243
def user_info(team_id, user_id)
  @teams[team_id][:client].users_info(user: user_id)['user']
end
users(team_id) click to toggle source
# File lib/driftwood/slack.rb, line 188
def users(team_id)
  users  = []
  cursor = nil
  loop do
    data   = @teams[team_id][:client].users_list(limit: 500, cursor: cursor)
    cursor = data['response_metadata']['next_cursor']
    users.concat data['members']

    break if (cursor.nil? or cursor.empty?)
  end

  users.each do |row|
    normalize_user(row)
  end.compact
end