slacks 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/slacks/bot_user.rb +15 -0
- data/lib/slacks/channel.rb +79 -0
- data/lib/slacks/connection.rb +287 -0
- data/lib/slacks/conversation.rb +48 -0
- data/lib/slacks/core_ext/exception.rb +7 -0
- data/lib/slacks/driver.rb +91 -0
- data/lib/slacks/errors.rb +33 -0
- data/lib/slacks/event.rb +34 -0
- data/lib/slacks/guest_channel.rb +34 -0
- data/lib/slacks/listener.rb +44 -0
- data/lib/slacks/listener_collection.rb +31 -0
- data/lib/slacks/message.rb +71 -0
- data/lib/slacks/rtm_event.rb +30 -0
- data/lib/slacks/session.rb +147 -0
- data/lib/slacks/team.rb +12 -0
- data/lib/slacks/user.rb +29 -0
- data/lib/slacks/version.rb +3 -0
- data/lib/slacks.rb +3 -0
- data/slacks.gemspec +32 -0
- metadata +197 -0
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
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
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,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,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
|