Class: Driftwood::Slack

Inherits:
Object
  • Object
show all
Defined in:
lib/driftwood/slack.rb

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ Slack

Returns a new instance of Slack.



4
5
6
7
8
9
10
# File 'lib/driftwood/slack.rb', line 4

def initialize(config)
  @config   = config
  @teams    = {}
  @handlers = {}

  $logger.info "Started Slack bot"
end

Instance Method Details

#all_channelsObject



225
226
227
228
229
# 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) ⇒ Object



237
238
239
240
241
# 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_usersObject



231
232
233
234
235
# 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) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# 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) ⇒ Object



247
248
249
# 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) ⇒ Object



260
261
262
# 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) ⇒ Object



175
176
177
178
179
180
181
182
183
184
185
186
# 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) ⇒ Object



84
85
86
87
88
89
90
# 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) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# 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) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# 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_imsObject



307
308
309
310
311
# 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) ⇒ Object



291
292
293
294
295
296
297
298
# 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) ⇒ Object



300
301
302
303
304
305
# 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) ⇒ Object



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# 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) ⇒ Object



256
257
258
# File 'lib/driftwood/slack.rb', line 256

def real_name(team_id, user_id)
  (team_id, user_id)['real_name'] rescue user_id
end

#register_handler(event, &block) ⇒ Object

Raises:

  • (ArgumentError)


72
73
74
75
76
77
78
79
80
81
82
# 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) ⇒ Object

Send a response to an Event via the Web API.



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# 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

#shellObject



318
319
320
321
# File 'lib/driftwood/slack.rb', line 318

def shell
  require 'pry'
  binding.pry
end

#team(team_id) ⇒ Object



162
163
164
# 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) ⇒ Object



251
252
253
# File 'lib/driftwood/slack.rb', line 251

def team_info(team_id)
  @teams[team_id][:client].team_info()['team']
end

#team_name(team_id) ⇒ Object



264
265
266
# File 'lib/driftwood/slack.rb', line 264

def team_name(team_id)
  team_info(team_id)['name'] rescue team_id
end

#teamsObject



166
167
168
169
170
171
172
173
# 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) ⇒ Boolean

Returns:

  • (Boolean)


313
314
315
316
# 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) ⇒ Object



243
244
245
# File 'lib/driftwood/slack.rb', line 243

def (team_id, user_id)
  @teams[team_id][:client].users_info(user: user_id)['user']
end

#users(team_id) ⇒ Object



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# 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