slacks 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 789c9ac13d517eb62b249b38f9bdd781295e14e4
4
+ data.tar.gz: d05efc4b0f64cdc399324b52257f3fd88884f19d
5
+ SHA512:
6
+ metadata.gz: df416c6fdb36216f70a5d76537aef833ea11e1996f186aa9caaad4d0bf55e30ef7629a0e528fab52845e8b933e6ea500f2635dbc0ec6176f6fb8f710f5af409d
7
+ data.tar.gz: 02e5185cc2714d40259aedd3b4211d4b764ac5c6ca4d2bc7892f5cb20e3989fbe8c79b5d729ab01e5ceb88b390850083405ec5f49b7d7b4f2cf9944fb9d67f9d
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /.ruby-version
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ before_install: gem install bundler -v 1.11.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in slacks.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Bob Lail
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # Slacks
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/slacks`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'slacks'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install slacks
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/slacks.
36
+
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
41
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "slacks"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,15 @@
1
+ module Slacks
2
+ class BotUser
3
+ attr_reader :id, :name
4
+
5
+ def initialize(data)
6
+ @id = data.fetch("id")
7
+ @name = data.fetch("name")
8
+ end
9
+
10
+ def to_s
11
+ "@#{name}"
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,79 @@
1
+ module Slacks
2
+ class Channel
3
+ attr_reader :id, :name, :type
4
+
5
+ def initialize(session, attributes={})
6
+ @session = session
7
+ @id = attributes["id"]
8
+ @name = attributes["name"]
9
+ @type = :channel
10
+ @type = :group if attributes["is_group"]
11
+ @type = :direct_message if attributes["is_im"]
12
+ end
13
+
14
+ def reply(*messages)
15
+ messages.flatten!
16
+ return unless messages.any?
17
+
18
+ first_message = messages.shift
19
+ message_options = {}
20
+ message_options = messages.shift if messages.length == 1 && messages[0].is_a?(Hash)
21
+ session.slack.send_message(first_message, message_options.merge(channel: id))
22
+
23
+ messages.each do |message|
24
+ sleep message.length / session.typing_speed
25
+ session.slack.send_message(message, channel: id)
26
+ end
27
+ end
28
+ alias :say :reply
29
+
30
+ def random_reply(replies)
31
+ if replies.is_a?(Hash)
32
+ weights = replies.values
33
+ unless weights.reduce(&:+) == 1.0
34
+ raise ArgumentError, "Reply weights don't add up to 1.0"
35
+ end
36
+
37
+ draw = rand
38
+ sum = 0
39
+ pick = nil
40
+ replies.each do |reply, weight|
41
+ pick = reply unless sum > draw
42
+ sum += weight
43
+ end
44
+ reply pick
45
+ else
46
+ reply replies.sample
47
+ end
48
+ end
49
+
50
+ def direct_message?
51
+ type == :direct_message
52
+ end
53
+ alias :dm? :direct_message?
54
+ alias :im? :direct_message?
55
+
56
+ def private_group?
57
+ type == :group
58
+ end
59
+ alias :group? :private_group?
60
+ alias :private? :private_group?
61
+
62
+ def guest?
63
+ false
64
+ end
65
+
66
+ def inspect
67
+ "<Slacks::Channel id=\"#{id}\" name=\"#{name}\">"
68
+ end
69
+
70
+ def to_s
71
+ return name if private?
72
+ return "@#{name}" if direct_message?
73
+ "##{name}"
74
+ end
75
+
76
+ private
77
+ attr_reader :session
78
+ end
79
+ end
@@ -0,0 +1,287 @@
1
+ require "slacks/bot_user"
2
+ require "slacks/team"
3
+ require "slacks/channel"
4
+ require "slacks/guest_channel"
5
+ require "slacks/conversation"
6
+ require "slacks/driver"
7
+ require "slacks/rtm_event"
8
+ require "slacks/listener"
9
+ require "slacks/message"
10
+ require "slacks/user"
11
+ require "slacks/errors"
12
+ require "faraday"
13
+ require "faraday/raise_errors"
14
+
15
+ module Slacks
16
+ class Connection
17
+ attr_reader :team, :bot, :token
18
+
19
+ EVENT_MESSAGE = "message".freeze
20
+ EVENT_GROUP_JOINED = "group_joined".freeze
21
+ EVENT_USER_JOINED = "team_join".freeze
22
+
23
+ def initialize(session, token)
24
+ @user_ids_dm_ids = {}
25
+ @users_by_id = {}
26
+ @user_id_by_name = {}
27
+ @groups_by_id = {}
28
+ @group_id_by_name = {}
29
+ @channels_by_id = {}
30
+ @channel_id_by_name = {}
31
+
32
+ @session = session
33
+ @token = token
34
+ end
35
+
36
+
37
+
38
+ def send_message(message, options={})
39
+ channel = options.fetch(:channel) { raise ArgumentError, "Missing parameter :channel" }
40
+ attachments = Array(options[:attachments])
41
+ params = {
42
+ channel: to_channel_id(channel),
43
+ text: message,
44
+ as_user: true, # post as the authenticated user (rather than as slackbot)
45
+ link_names: 1} # find and link channel names and user names
46
+ params.merge!(attachments: MultiJson.dump(attachments)) if attachments.any?
47
+ params.merge!(options.select { |key, _| [:username, :as_user, :parse, :link_names,
48
+ :unfurl_links, :unfurl_media, :icon_url, :icon_emoji].member?(key) })
49
+ api("chat.postMessage", params)
50
+ end
51
+
52
+ def add_reaction(emojis, message)
53
+ Array(emojis).each do |emoji|
54
+ api("reactions.add", {
55
+ name: emoji.gsub(/^:|:$/, ""),
56
+ channel: message.channel.id,
57
+ timestamp: message.timestamp })
58
+ end
59
+ end
60
+
61
+
62
+
63
+ def listen!
64
+ response = api("rtm.start")
65
+
66
+ unless response["ok"]
67
+ raise MigrationInProgress if response["error"] == "migration_in_progress"
68
+ raise ResponseError.new(response, response["error"])
69
+ end
70
+
71
+ store_context!(response)
72
+
73
+ client = Slacks::Driver.new
74
+ client.connect_to websocket_url
75
+
76
+ client.on(:error) do |event|
77
+ raise ConnectionError.new(event)
78
+ end
79
+
80
+ client.on(:message) do |data|
81
+ case data["type"]
82
+ when EVENT_GROUP_JOINED
83
+ group = data["channel"]
84
+ @groups_by_id[group["id"]] = group
85
+ @group_id_by_name[group["name"]] = group["id"]
86
+
87
+ when EVENT_USER_JOINED
88
+ user = data["user"]
89
+ @users_by_id[user["id"]] = user
90
+ @user_id_by_name[user["name"]] = user["id"]
91
+
92
+ when EVENT_MESSAGE
93
+ # Don't respond to things that this bot said
94
+ next if data["user"] == bot.id
95
+ # ...or to messages with no text
96
+ next if data["text"].nil? || data["text"].empty?
97
+ yield data
98
+ end
99
+ end
100
+
101
+ client.main_loop
102
+
103
+ rescue EOFError
104
+ # Slack hung up on us, we'll ask for a new WebSocket URL and reconnect.
105
+ session.error "Websocket Driver received EOF; reconnecting"
106
+ retry
107
+ end
108
+
109
+
110
+
111
+ def channels
112
+ user_id_by_name.keys + group_id_by_name.keys + channel_id_by_name.keys
113
+ end
114
+
115
+
116
+
117
+ def find_channel(id)
118
+ case id
119
+ when /^U/ then find_user(id)
120
+ when /^D/
121
+ user = find_user(get_user_id_for_dm(id))
122
+ Slacks::Channel.new session, {
123
+ "id" => id,
124
+ "is_im" => true,
125
+ "name" => user.username }
126
+ when /^G/
127
+ Slacks::Channel.new session, groups_by_id.fetch(id) do
128
+ raise ArgumentError, "Unable to find a group with the ID #{id.inspect}"
129
+ end
130
+ else
131
+ Slacks::Channel.new session, channels_by_id.fetch(id) do
132
+ raise ArgumentError, "Unable to find a channel with the ID #{id.inspect}"
133
+ end
134
+ end
135
+ end
136
+
137
+ def find_user(id)
138
+ Slacks::User.new session, users_by_id.fetch(id) do
139
+ raise ArgumentError, "Unable to find a user with the ID #{id.inspect}"
140
+ end
141
+ end
142
+
143
+
144
+
145
+ def user_exists?(username)
146
+ return false if username.nil?
147
+ to_user_id(username).present?
148
+ rescue ArgumentError
149
+ false
150
+ end
151
+
152
+ def users
153
+ fetch_users! if @users_by_id.empty?
154
+ @users_by_id.values
155
+ end
156
+
157
+
158
+
159
+ private
160
+ attr_reader :session,
161
+ :user_ids_dm_ids,
162
+ :users_by_id,
163
+ :user_id_by_name,
164
+ :groups_by_id,
165
+ :group_id_by_name,
166
+ :channels_by_id,
167
+ :channel_id_by_name,
168
+ :websocket_url
169
+
170
+
171
+
172
+ def store_context!(response)
173
+ @websocket_url = response.fetch("url")
174
+ @bot = BotUser.new(response.fetch("self"))
175
+ @team = Team.new(response.fetch("team"))
176
+
177
+ @channels_by_id = Hash[response.fetch("channels").map { |attrs| [attrs.fetch("id"), attrs] }]
178
+ @channel_id_by_name = Hash[response.fetch("channels").map { |attrs| ["##{attrs.fetch("name")}", attrs.fetch("id")] }]
179
+
180
+ @users_by_id = Hash[response.fetch("users").map { |attrs| [attrs.fetch("id"), attrs] }]
181
+ @user_id_by_name = Hash[response.fetch("users").map { |attrs| ["@#{attrs.fetch("name")}", attrs.fetch("id")] }]
182
+
183
+ @groups_by_id = Hash[response.fetch("groups").map { |attrs| [attrs.fetch("id"), attrs] }]
184
+ @group_id_by_name = Hash[response.fetch("groups").map { |attrs| [attrs.fetch("name"), attrs.fetch("id")] }]
185
+ rescue KeyError
186
+ raise ResponseError.new(response, $!.message)
187
+ end
188
+
189
+
190
+
191
+ def to_channel_id(name)
192
+ return name if name =~ /^[DGC]/ # this already looks like a channel id
193
+ return get_dm_for_username(name) if name.start_with?("@")
194
+ return to_group_id(name) unless name.start_with?("#")
195
+
196
+ channel_id_by_name[name] || fetch_channels![name] || missing_channel!(name)
197
+ end
198
+
199
+ def to_group_id(name)
200
+ group_id_by_name[name] || fetch_groups![name] || missing_group!(name)
201
+ end
202
+
203
+ def to_user_id(name)
204
+ user_id_by_name[name] || fetch_users![name] || missing_user!(name)
205
+ end
206
+
207
+ def get_dm_for_username(name)
208
+ get_dm_for_user_id to_user_id(name)
209
+ end
210
+
211
+ def get_dm_for_user_id(user_id)
212
+ channel_id = user_ids_dm_ids[user_id] ||= begin
213
+ response = api("im.open", user: user_id)
214
+ raise ArgumentError, "Unable to direct message the user #{user_id.inspect}: #{response["error"]}" unless response["ok"]
215
+ response["channel"]["id"]
216
+ end
217
+ raise ArgumentError, "Unable to direct message the user #{user_id.inspect}" unless channel_id
218
+ channel_id
219
+ end
220
+
221
+
222
+
223
+ def fetch_channels!
224
+ response = api("channels.list")
225
+ @channels_by_id = response["channels"].index_by { |attrs| attrs["id"] }
226
+ @channel_id_by_name = Hash[response["channels"].map { |attrs| ["##{attrs["name"]}", attrs["id"]] }]
227
+ end
228
+
229
+ def fetch_groups!
230
+ response = api("groups.list")
231
+ @groups_by_id = response["groups"].index_by { |attrs| attrs["id"] }
232
+ @group_id_by_name = Hash[response["groups"].map { |attrs| [attrs["name"], attrs["id"]] }]
233
+ end
234
+
235
+ def fetch_users!
236
+ response = api("users.list")
237
+ @users_by_id = response["members"].index_by { |attrs| attrs["id"] }
238
+ @user_id_by_name = Hash[response["members"].map { |attrs| ["@#{attrs["name"]}", attrs["id"]] }]
239
+ end
240
+
241
+
242
+
243
+ def missing_channel!(name)
244
+ raise ArgumentError, "Couldn't find a channel named #{name}"
245
+ end
246
+
247
+ def missing_group!(name)
248
+ raise ArgumentError, "Couldn't find a private group named #{name}"
249
+ end
250
+
251
+ def missing_user!(name)
252
+ raise ArgumentError, "Couldn't find a user named #{name}"
253
+ end
254
+
255
+
256
+
257
+ def get_user_id_for_dm(dm)
258
+ user_id = user_ids_dm_ids.key(dm)
259
+ unless user_id
260
+ response = api("im.list")
261
+ user_ids_dm_ids.merge! Hash[response["ims"].map { |attrs| attrs.values_at("user", "id") }]
262
+ user_id = user_ids_dm_ids.key(dm)
263
+ end
264
+ raise ArgumentError, "Unable to find a user for the direct message ID #{dm.inspect}" unless user_id
265
+ user_id
266
+ end
267
+
268
+
269
+
270
+ def api(command, options={})
271
+ response = http.post(command, options.merge(token: token))
272
+ MultiJson.load(response.body)
273
+
274
+ rescue MultiJson::ParseError
275
+ $!.additional_information[:response_body] = response.body
276
+ $!.additional_information[:response_status] = response.status
277
+ raise
278
+ end
279
+
280
+ def http
281
+ @http ||= Faraday.new(url: "https://slack.com/api").tap do |connection|
282
+ connection.use Faraday::RaiseErrors
283
+ end
284
+ end
285
+
286
+ end
287
+ end
@@ -0,0 +1,48 @@
1
+ require "thread_safe"
2
+
3
+ module Slacks
4
+ class Conversation
5
+
6
+ def initialize(session, channel, sender)
7
+ @session = session
8
+ raise NotInChannelError, channel if channel.guest?
9
+
10
+ @channel = channel
11
+ @sender = sender
12
+ @listeners = ThreadSafe::Array.new
13
+ end
14
+
15
+ def listen_for(matcher, flags=[], &block)
16
+ session.listen_for(matcher, flags, &block).tap do |listener|
17
+ listener.conversation = self
18
+ listeners.push listener
19
+ end
20
+ end
21
+
22
+ def includes?(e)
23
+ e.channel.id == channel.id && e.sender.id == sender.id
24
+ end
25
+
26
+ def reply(*messages)
27
+ channel.reply(*messages)
28
+ end
29
+ alias :say :reply
30
+
31
+ def ask(question, expect: nil)
32
+ listen_for(expect) do |e|
33
+ e.stop_listening!
34
+ yield e
35
+ end
36
+
37
+ reply question
38
+ end
39
+
40
+ def end!
41
+ listeners.each(&:stop_listening!)
42
+ end
43
+
44
+ private
45
+ attr_reader :session, :channel, :sender, :listeners
46
+
47
+ end
48
+ end
@@ -0,0 +1,7 @@
1
+ # Allow exception handlers to add information
2
+ # to an exception for upstream
3
+ class Exception
4
+ def additional_information
5
+ @additional_information ||= {}
6
+ end
7
+ end
@@ -0,0 +1,91 @@
1
+ require "multi_json"
2
+ require "socket"
3
+ require "websocket/driver"
4
+
5
+ # Adapted from https://github.com/mackwic/slack-rtmapi
6
+
7
+ module Slacks
8
+ class Driver
9
+ attr_accessor :stop
10
+
11
+ def initialize
12
+ @has_been_init = false
13
+ @stop = false
14
+ @callbacks = {}
15
+ end
16
+
17
+ VALID = [:open, :message, :error].freeze
18
+ def on(type, &block)
19
+ unless VALID.include? type
20
+ raise ArgumentError.new "Client#on accept one of #{VALID.inspect}"
21
+ end
22
+
23
+ callbacks[type] = block
24
+ end
25
+
26
+ # This init has been delayed because the SSL handshake is a blocking and
27
+ # expensive call
28
+ def connect_to(url)
29
+ raise "Already been init" if @has_been_init
30
+ url = URI(url)
31
+ raise ArgumentError.new ":url must be a valid websocket secure url!" unless url.scheme == "wss"
32
+
33
+ @socket = OpenSSL::SSL::SSLSocket.new(TCPSocket.new url.host, 443)
34
+ socket.connect # costly and blocking !
35
+
36
+ internalWrapper = (Struct.new :url, :socket do
37
+ def write(*args)
38
+ self.socket.write(*args)
39
+ end
40
+ end).new url.to_s, socket
41
+
42
+ # this, also, is costly and blocking
43
+ @driver = WebSocket::Driver.client internalWrapper
44
+ driver.on :open do
45
+ @connected = true
46
+ unless callbacks[:open].nil?
47
+ callbacks[:open].call
48
+ end
49
+ end
50
+
51
+ driver.on :error do |event|
52
+ @connected = false
53
+ unless callbacks[:error].nil?
54
+ callbacks[:error].call event
55
+ end
56
+ end
57
+
58
+ driver.on :message do |event|
59
+ data = MultiJson.load event.data
60
+ unless callbacks[:message].nil?
61
+ callbacks[:message].call data
62
+ end
63
+ end
64
+
65
+ driver.start
66
+ @has_been_init = true
67
+ end
68
+
69
+ def connected?
70
+ @connected || false
71
+ end
72
+
73
+ # All the polling work is done here
74
+ def inner_loop
75
+ return if @stop
76
+
77
+ data = @socket.readpartial 4096
78
+ driver.parse data unless data.nil? or data.empty?
79
+ end
80
+
81
+ def main_loop
82
+ loop do
83
+ inner_loop
84
+ end
85
+ end
86
+
87
+ private
88
+ attr_reader :url, :socket, :driver, :callbacks
89
+
90
+ end
91
+ end
@@ -0,0 +1,33 @@
1
+ module Slacks
2
+ class MigrationInProgress < RuntimeError
3
+ def initialize
4
+ super "Team is being migrated between servers. Try the request again in a few seconds."
5
+ end
6
+ end
7
+
8
+ class ResponseError < RuntimeError
9
+ def initialize(response, message)
10
+ super message
11
+ additional_information[:response] = response
12
+ end
13
+ end
14
+
15
+ class ConnectionError < RuntimeError
16
+ def initialize(event)
17
+ super "There was a connection error in the WebSocket"
18
+ additional_information[:event] = event
19
+ end
20
+ end
21
+
22
+ class AlreadyRespondedError < RuntimeError
23
+ def initialize(message=nil)
24
+ super message || "You have already replied to this Slash Command; you can only reply once"
25
+ end
26
+ end
27
+
28
+ class NotInChannelError < RuntimeError
29
+ def initialize(channel)
30
+ super "The bot is not in the channel #{channel} and cannot reply"
31
+ end
32
+ end
33
+ end