slack-bot-manager 0.1.0pre1
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 +6 -0
- data/.gitmodules +12 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE.md +22 -0
- data/README.md +159 -0
- data/UPGRADING.md +3 -0
- data/lib/slack-bot-manager/client/base.rb +89 -0
- data/lib/slack-bot-manager/client/commands.rb +26 -0
- data/lib/slack-bot-manager/config.rb +110 -0
- data/lib/slack-bot-manager/errors.rb +50 -0
- data/lib/slack-bot-manager/extend.rb +19 -0
- data/lib/slack-bot-manager/logger.rb +44 -0
- data/lib/slack-bot-manager/manager/base.rb +35 -0
- data/lib/slack-bot-manager/manager/connection.rb +167 -0
- data/lib/slack-bot-manager/manager/tokens.rb +87 -0
- data/lib/slack-bot-manager/version.rb +3 -0
- data/lib/slack-bot-manager.rb +20 -0
- data/slack-bot-manager.gemspec +29 -0
- metadata +106 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 211a6a7bd9fb7a2557d3c946bbf495ae562a186d
|
4
|
+
data.tar.gz: e2fc191df9ff401007c4f8e8e35bd2cf18302a8a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0349d9425e90b8b68ea9b890b2edcf255ef2896f1a7e5606c5d1fc9c8146111c0698b88ab7416ee132399b96dfbd02369e5f84bde88a181b1c69e932e08de48d
|
7
|
+
data.tar.gz: 128445cb7a39b80058bddd14fa89e0abda4db055fddb345ce330643a0c970c1fefbad3ec9978ebc6288226275bf1ed27625bf46b526618b5d0e7c66fbc92429f
|
data/.gitignore
ADDED
data/.gitmodules
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
[submodule "examples/emoji-art-bot"]
|
2
|
+
path = examples/emoji-art-bot
|
3
|
+
url = git@github.com:goosey/emoji-art-bot.git
|
4
|
+
[submodule "examples/downforeveryone-bot"]
|
5
|
+
path = examples/downforeveryone-bot
|
6
|
+
url = https://github.com/goosey/downforeveryone-bot
|
7
|
+
[submodule "examples/botspotting"]
|
8
|
+
path = examples/botspotting
|
9
|
+
url = git@github.com:goosey/botspotting.git
|
10
|
+
[submodule "examples/dm-bot"]
|
11
|
+
path = examples/dm-bot
|
12
|
+
url = git@github.com:gleuch/dm-bot.git
|
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
|
2
|
+
### Version 0.1.0 (20 Jan 2016)
|
3
|
+
|
4
|
+
__**NOT YET RELEASED**__
|
5
|
+
|
6
|
+
This is the first version of Slack Bot Manager, and includes
|
7
|
+
|
8
|
+
* Initial public release of Slack Bot Manager - [@gleuch](https://github.com/gleuch)
|
9
|
+
* Ability to register and remove tokens used for creating connections to Slack RTM. - [@gleuch](https://github.com/gleuch)
|
10
|
+
* Manager system to start, stop, and monitor multiple RTM connections. - [@gleuch](https://github.com/gleuch)
|
11
|
+
* Handle event and output logging. - [@gleuch](https://github.com/gleuch)
|
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2015-2016 Greg Leuch & Betaworks Studio
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
# Slack Bot Manager
|
2
|
+
|
3
|
+
Slack Bot Manager is a Ruby gem that allows for the management of multiple Slack RTM connections based on tokens. With only a few configuration changes, you can run a system for handling hundreds of simulatenous RTM connections for your Slack app.
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
While this has yet to be compiled to a Ruby gem, it can be installed from this repository within your Gemfile:
|
10
|
+
|
11
|
+
`gem 'slack-bot-manager', github: 'betaworks/slack-bot-manager', branch: 'master'`
|
12
|
+
|
13
|
+
__**This gem requires `redis` for tracking the status of tokens.**__
|
14
|
+
You will need to have `redis` running for this gem to work.
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
## Getting Started
|
19
|
+
|
20
|
+
(TODO)
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
## Running the Slack Bot Manager
|
25
|
+
|
26
|
+
Once you initialize a new `SlackBotManager::Manager`, you can use the following connection and token methods to run your cool new Slack bot app.
|
27
|
+
|
28
|
+
|
29
|
+
### Manager Connection Methods
|
30
|
+
|
31
|
+
You can run a manager supporting multiple RTM connections with just __**three**__ lines!
|
32
|
+
|
33
|
+
```
|
34
|
+
botmanager = SlackBotManager::Manager.new
|
35
|
+
botmanager.start
|
36
|
+
botmanager.monitor
|
37
|
+
```
|
38
|
+
|
39
|
+
These are the available connecton methods:
|
40
|
+
|
41
|
+
methods | description
|
42
|
+
------------|----------------------------------------------------------------------------------------------
|
43
|
+
`start` | Start connections by fetching known tokens and creating each connection
|
44
|
+
`stop` | Stop connections
|
45
|
+
`restart` | Restart connections
|
46
|
+
`status` | Get the status of the current manager (number of connections).
|
47
|
+
`monitor` | Run the manager in a continuous loop, checking for changes in connections and token statuses.
|
48
|
+
|
49
|
+
|
50
|
+
### Token Management Methods
|
51
|
+
|
52
|
+
Tokens are managed within Redis. SlackBotManager will manage and monitor these redis keys for additions, updates, and remvoals. New connections must be added into the redis `teams_key`, like so:
|
53
|
+
|
54
|
+
```
|
55
|
+
botmanager = SlackBotManager::Manager.new
|
56
|
+
botmanager.add_token('token1', 'token2', 'token3') # takes array
|
57
|
+
```
|
58
|
+
|
59
|
+
These are the available token methods:
|
60
|
+
|
61
|
+
methods | description
|
62
|
+
------------------------|----------------------------------------------------------------------------
|
63
|
+
`add_token(*tokens)` | Add new token(s), will connect within `monitor` loop. [array]
|
64
|
+
`remove_token(*tokens)` | Remove token(s), will disconnect within `monitor` loop. [array]
|
65
|
+
`update_token(*tokens)` | Update token(s), will trigger update methods within `monitor` loop. [array]
|
66
|
+
`check_token(*tokens)` | Check the status of token(s), output status(es). [array]
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
## Client Connections
|
71
|
+
|
72
|
+
Each RTM connection handled by `SlackBotManager::Manager` is generated by `SlackBotManager::Client`. This client class assists in checking RTM (websocket) connection status, storing various attributes, and includes event listener support.
|
73
|
+
|
74
|
+
The following instance variables are accessible by Client and the included Commands module:
|
75
|
+
|
76
|
+
variable | description
|
77
|
+
--------------|----------------------------------------------------------------------------------------
|
78
|
+
`connection` | `Slack::RealTime::Client` connection
|
79
|
+
`id` | Team's Slack ID (ex. `T123ABC`)
|
80
|
+
`token` | Team's Slack access token (ex. `xoxb-123abc456def`)
|
81
|
+
`status` | Known connection status. (`connected`, `disconnected`, `rate_limited`, `token_revoked`)
|
82
|
+
|
83
|
+
|
84
|
+
### Adding Event Listeners
|
85
|
+
|
86
|
+
You will want to handle your own RTM event listeners to perform specific functions. This is achieved by extending the `SlackBotManager:Commands` module, which is included within the `SlackBotManager::Client` class (and access to subsequent instance variables specific to that connection).
|
87
|
+
|
88
|
+
Each event must be prefixed with `on_`, e.g. `on_messsage` will handing incoming messages.
|
89
|
+
|
90
|
+
```
|
91
|
+
module SlackBotManager
|
92
|
+
module Commands
|
93
|
+
def on_hello(data)
|
94
|
+
puts "Connected to %s" % self.id
|
95
|
+
end
|
96
|
+
|
97
|
+
def on_team_join(data)
|
98
|
+
puts "New team member joined: %s" % data['user']['username']
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
(A full list of events is available from the [Slack API docs](https://api.slack.com/rtm#events).)
|
105
|
+
|
106
|
+
|
107
|
+
|
108
|
+
## Configuration
|
109
|
+
|
110
|
+
### Manager configuration options
|
111
|
+
|
112
|
+
setting | description
|
113
|
+
------------------|-----------------------------------------------------------------------------------
|
114
|
+
`tokens_key` | Redis key name for where tokens' status are stored. _(default: tokens:statuses)_
|
115
|
+
`teams_key` | Redis key name for where teams' tokens are stored. _(default: tokens:teams)_
|
116
|
+
`check_interval` | Interval (in seconds) for checking connections and tokens status. _(default: 5)_
|
117
|
+
`redis` | Define Redis connection. _(default: Redis.new)_
|
118
|
+
`logger` | Define the logger to use. _(default: Rails.logger or ::Logger.new(STDOUT))_
|
119
|
+
`log_level` | Explicity define the logger level. _(default: ::Logger::WARN)_
|
120
|
+
`verbose` | When true, set `log_level` to ::Logger::DEBUG. _(default: false)_
|
121
|
+
|
122
|
+
You can define these configuration options as:
|
123
|
+
|
124
|
+
```
|
125
|
+
SlackBotManager::Manager.configure do |config|
|
126
|
+
config.redis = Redis.new(host: '0.0.0.0', port: 6379)
|
127
|
+
config.check_interval = 10 # in seconds
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
For customization of Slack connections, including proxy, websocket ping, endpoint, user-agent, and more, check out the [slack-ruby-client README](https://github.com/dblock/slack-ruby-client/blob/master/README.md).
|
132
|
+
|
133
|
+
|
134
|
+
|
135
|
+
## Examples
|
136
|
+
|
137
|
+
(TODO)
|
138
|
+
|
139
|
+
|
140
|
+
|
141
|
+
## History
|
142
|
+
|
143
|
+
This gem will be released soon, and is based on earlier work created by [betaworks](https://betaworks.com) for [PlusPlus++](https://plusplus.chat) Slack app.
|
144
|
+
|
145
|
+
Also thanks to [slack-ruby-client](https://github.com/dblock/slack-ruby-client).
|
146
|
+
|
147
|
+
|
148
|
+
|
149
|
+
## Contributing
|
150
|
+
|
151
|
+
See [CONTRIBUTING](CONTRIBUTING.md).
|
152
|
+
|
153
|
+
|
154
|
+
|
155
|
+
## Copyright and License
|
156
|
+
|
157
|
+
Copyright (c) 2016 [Greg Leuch](https://gleu.ch) & [betaworks](https://betaworks.com).
|
158
|
+
|
159
|
+
Licensed under [MIT License](LICENSE.md).
|
data/UPGRADING.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
module SlackBotManager
|
2
|
+
class Client
|
3
|
+
|
4
|
+
include Commands
|
5
|
+
include Errors
|
6
|
+
include Logger
|
7
|
+
|
8
|
+
attr_accessor :commands, :connection, :id, :token, :status
|
9
|
+
attr_accessor(*Config::CLIENT_ATTRIBUTES)
|
10
|
+
|
11
|
+
def initialize(id, token, *args)
|
12
|
+
options = args.extract_options!
|
13
|
+
@id, @token, @events, @status = id, token, options[:events] || {}, :disconnected
|
14
|
+
|
15
|
+
# Setup client and assign commands
|
16
|
+
@connection = Slack::RealTime::Client.new(token: @token)
|
17
|
+
|
18
|
+
# Load config options
|
19
|
+
SlackBotManager::Config::CLIENT_ATTRIBUTES.each do |key|
|
20
|
+
send("#{key}=", options[key] || SlackBotManager.config.send(key))
|
21
|
+
end
|
22
|
+
|
23
|
+
# Assign commands
|
24
|
+
self.methods.each do |n|
|
25
|
+
# Require methods to include on_*
|
26
|
+
next unless n.match(/^on_/) && self.respond_to?(n)
|
27
|
+
assign_event(n.to_s.gsub(/^on_/, ''), n)
|
28
|
+
end
|
29
|
+
|
30
|
+
connect
|
31
|
+
end
|
32
|
+
|
33
|
+
def connect
|
34
|
+
connection.start_async
|
35
|
+
@status = :connected
|
36
|
+
rescue => err
|
37
|
+
handle_error(err)
|
38
|
+
end
|
39
|
+
|
40
|
+
def disconnect(reason=:disconnected)
|
41
|
+
connection && connection.stop!
|
42
|
+
rescue => err
|
43
|
+
handle_error(err)
|
44
|
+
ensure
|
45
|
+
@status = reason if @status == :connected
|
46
|
+
remove_instance_variable(:@connection) if @connection
|
47
|
+
end
|
48
|
+
|
49
|
+
def connected?
|
50
|
+
connection && connection.started?
|
51
|
+
end
|
52
|
+
|
53
|
+
def disconnected?
|
54
|
+
!connected?
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
def send_message(channel, text, *args)
|
61
|
+
options = args.extract_options!
|
62
|
+
# TODO : HANDLE CASES WHERE NEED TO POST ATTACHMENTS, SEND DMs, ETC
|
63
|
+
options[:channel] = channel
|
64
|
+
options[:text] = text
|
65
|
+
connection.message(options)
|
66
|
+
end
|
67
|
+
|
68
|
+
def assign_event(evt, evt_name)
|
69
|
+
connection.on(evt) do |data|
|
70
|
+
begin
|
71
|
+
send(evt_name, data) if respond_to?(evt_name)
|
72
|
+
rescue => err
|
73
|
+
handle_error(err)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Handle different error cases
|
79
|
+
def handle_error(err, data=nil)
|
80
|
+
case determine_error_type(err)
|
81
|
+
when :token_revoked; on_revoke(data)
|
82
|
+
when :rate_limited; on_rate_limit(data)
|
83
|
+
when :closed; on_close(data)
|
84
|
+
else; on_error(err, data)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module SlackBotManager
|
2
|
+
module Commands
|
3
|
+
|
4
|
+
# Handle when connection gets closed
|
5
|
+
def on_close(data, *args)
|
6
|
+
options = args.extract_options!
|
7
|
+
options[:code] ||= (data && data.code) || '1000'
|
8
|
+
|
9
|
+
disconnect
|
10
|
+
raise SlackBotManager::ConnectionRateLimited if ['1008','429'].include?(options[:code].to_s)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Handle rate limit errors coming from web API
|
14
|
+
def on_revoke(data)
|
15
|
+
disconnect(:token_revoked)
|
16
|
+
raise SlackBotManager::TokenRevoked
|
17
|
+
end
|
18
|
+
|
19
|
+
# Handle rate limit errors coming from web API
|
20
|
+
def on_rate_limit(data)
|
21
|
+
disconnect(:rate_limited)
|
22
|
+
raise SlackBotManager::ConnectionRateLimited
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module SlackBotManager
|
2
|
+
module Config
|
3
|
+
|
4
|
+
extend self
|
5
|
+
|
6
|
+
MANAGER_ATTRIBUTES = [
|
7
|
+
:tokens_key,
|
8
|
+
:teams_key,
|
9
|
+
:check_interval,
|
10
|
+
:redis,
|
11
|
+
:logger,
|
12
|
+
:log_level,
|
13
|
+
:verbose
|
14
|
+
]
|
15
|
+
|
16
|
+
CLIENT_ATTRIBUTES = [
|
17
|
+
:redis,
|
18
|
+
:logger,
|
19
|
+
:log_level,
|
20
|
+
:verbose
|
21
|
+
]
|
22
|
+
|
23
|
+
WEB_CLIENT_ATTRIBUTES = [
|
24
|
+
:user_agent,
|
25
|
+
:proxy,
|
26
|
+
:ca_path,
|
27
|
+
:ca_file,
|
28
|
+
:endpoint
|
29
|
+
]
|
30
|
+
|
31
|
+
RTM_CLIENT_ATTRIBUTES = [
|
32
|
+
:websocket_ping,
|
33
|
+
:websocket_proxy
|
34
|
+
]
|
35
|
+
|
36
|
+
attr_accessor *Config::MANAGER_ATTRIBUTES
|
37
|
+
attr_accessor *Config::CLIENT_ATTRIBUTES
|
38
|
+
attr_accessor *Config::WEB_CLIENT_ATTRIBUTES
|
39
|
+
attr_accessor *Config::RTM_CLIENT_ATTRIBUTES
|
40
|
+
|
41
|
+
def reset
|
42
|
+
# Slack web and realtime config options
|
43
|
+
Slack::Web::Config.reset
|
44
|
+
Slack::RealTime::Config.reset
|
45
|
+
|
46
|
+
self.tokens_key = 'tokens:statuses'
|
47
|
+
self.teams_key = 'tokens:teams'
|
48
|
+
self.check_interval = 5 # seconds
|
49
|
+
self.redis = Redis.new
|
50
|
+
self.logger = defined?(Rails) ? Rails.logger : ::Logger.new(STDOUT)
|
51
|
+
self.log_level = ::Logger::INFO
|
52
|
+
self.logger.formatter = SlackBotManager::Logger::Formatter.new
|
53
|
+
self.verbose = false
|
54
|
+
self.user_agent = "Slack Bot Manager/#{SlackBotManager::VERSION} <https://github.com/betaworks/slack-bot-manager>"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Slack Web Client config
|
58
|
+
Config::WEB_CLIENT_ATTRIBUTES.each do |name|
|
59
|
+
define_method "#{name}=" do |val|
|
60
|
+
Slack::Web.configure do |config|
|
61
|
+
config.send("#{name}=", val)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Slack RealTime Client config
|
67
|
+
Config::RTM_CLIENT_ATTRIBUTES.each do |name|
|
68
|
+
define_method "#{name}=" do |val|
|
69
|
+
Slack::Web.configure do |config|
|
70
|
+
config.send("#{name}=", val)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def verbose=(val)
|
76
|
+
@verbose = val
|
77
|
+
self.log_level = val ? ::Logger::DEBUG : ::Logger::INFO
|
78
|
+
end
|
79
|
+
|
80
|
+
def logger=(log)
|
81
|
+
@logger = log
|
82
|
+
|
83
|
+
# Also define Slack Web client logger
|
84
|
+
Slack::Web.configure do |config|
|
85
|
+
config.logger = @logger
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def log_level=(level)
|
90
|
+
self.logger.level = level
|
91
|
+
end
|
92
|
+
|
93
|
+
def log_formatter=(formatter)
|
94
|
+
self.logger.formatter = formatter
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
class << self
|
100
|
+
def configure
|
101
|
+
block_given? ? yield(Config) : Config
|
102
|
+
end
|
103
|
+
|
104
|
+
def config
|
105
|
+
Config
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
SlackBotManager::Config.reset
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module SlackBotManager
|
2
|
+
|
3
|
+
class ConnectionClosed < StandardError; end
|
4
|
+
class ConnectionRateLimited < StandardError; end
|
5
|
+
class InvalidToken < StandardError; end
|
6
|
+
class TokenAlreadyConnected < StandardError; end
|
7
|
+
class TokenNotConnected < StandardError; end
|
8
|
+
class TokenRevoked < StandardError; end
|
9
|
+
|
10
|
+
|
11
|
+
module Errors
|
12
|
+
|
13
|
+
# Mapping of error classes to type
|
14
|
+
CLASS_ERROR_TYPES = {
|
15
|
+
token_revoked: [
|
16
|
+
SlackBotManager::InvalidToken,
|
17
|
+
SlackBotManager::TokenRevoked
|
18
|
+
],
|
19
|
+
rate_limited: [
|
20
|
+
SlackBotManager::ConnectionRateLimited
|
21
|
+
],
|
22
|
+
closed: [
|
23
|
+
SlackBotManager::ConnectionClosed,
|
24
|
+
Slack::RealTime::Client::ClientNotStartedError
|
25
|
+
],
|
26
|
+
}
|
27
|
+
|
28
|
+
# Regexp mapping of error keywords to type
|
29
|
+
STRING_ERROR_TYPES = {
|
30
|
+
token_revoked: /token_revoked|account_inactive|invalid_auth/i,
|
31
|
+
rate_limited: /rate_limit|status 429/i,
|
32
|
+
closed: /closed/i,
|
33
|
+
}
|
34
|
+
|
35
|
+
def determine_error_type(err)
|
36
|
+
# Check known error types, unless string
|
37
|
+
CLASS_ERROR_TYPES.each{|k,v| return k if v.include?(err) } unless err.is_a?(String)
|
38
|
+
|
39
|
+
# Check string matches, as we might get code responses or capture something inside it
|
40
|
+
STRING_ERROR_TYPES.each{|k,v| return k if v.match(err.to_s) }
|
41
|
+
|
42
|
+
:error
|
43
|
+
end
|
44
|
+
|
45
|
+
def on_error(err,data=nil)
|
46
|
+
error(err)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# via https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/array/extract_options.rb
|
2
|
+
|
3
|
+
# Add extract_options! used in ActiveSupport
|
4
|
+
|
5
|
+
unless {}.respond_to?(:extractable_options?)
|
6
|
+
class Hash
|
7
|
+
def extractable_options?
|
8
|
+
instance_of?(Hash)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
unless [].respond_to?(:extract_options!)
|
14
|
+
class Array
|
15
|
+
def extract_options!
|
16
|
+
last.is_a?(Hash) && last.extractable_options? ? pop : {}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module SlackBotManager
|
2
|
+
module Logger
|
3
|
+
|
4
|
+
def info(msg)
|
5
|
+
logger.info(@id) { msg }
|
6
|
+
end
|
7
|
+
|
8
|
+
def debug(msg)
|
9
|
+
logger.debug(@id) { msg }
|
10
|
+
end
|
11
|
+
|
12
|
+
def warning(msg)
|
13
|
+
logger.warn(@id) { msg }
|
14
|
+
end
|
15
|
+
|
16
|
+
def error(msg)
|
17
|
+
logger.error(@id) { msg }
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
class Formatter
|
22
|
+
SEVERITY_TO_COLOR_MAP = {'DEBUG'=>'0;37', 'INFO'=>'32', 'WARN'=>'33', 'ERROR'=>'31', 'FATAL'=>'31', 'UNKNOWN'=>'37'}
|
23
|
+
|
24
|
+
def call(severity, timeat, progname, message)
|
25
|
+
formatted_severity = sprintf("%-5s",severity).strip
|
26
|
+
formatted_time = timeat.strftime("%Y-%m-%d %H:%M:%S.") << timeat.usec.to_s[0..2].rjust(3)
|
27
|
+
color = SEVERITY_TO_COLOR_MAP[severity]
|
28
|
+
|
29
|
+
# Handle backtrace, if any
|
30
|
+
msg = message.to_s
|
31
|
+
message.backtrace.each{|n| msg << "\n #{n}"} if message.respond_to?(:backtrace)
|
32
|
+
|
33
|
+
[
|
34
|
+
"\033[0;37m#{formatted_time}\033[0m", # Formatted time
|
35
|
+
"[\033[#{color}m#{formatted_severity}\033[0m]", # Level
|
36
|
+
"[PID:#{$$}]", # PID
|
37
|
+
progname && progname != '' && "(#{progname})", # Progname (team ID), if exists
|
38
|
+
msg.strip # Message
|
39
|
+
].compact.join(' ') + "\n"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SlackBotManager
|
2
|
+
class Manager
|
3
|
+
|
4
|
+
include Tokens
|
5
|
+
include Connection
|
6
|
+
include Errors
|
7
|
+
include Logger
|
8
|
+
|
9
|
+
attr_accessor :connections
|
10
|
+
attr_accessor(*Config::MANAGER_ATTRIBUTES)
|
11
|
+
|
12
|
+
def initialize(*args)
|
13
|
+
options = args.extract_options!
|
14
|
+
|
15
|
+
# Storage of connection keys
|
16
|
+
@connections = {}
|
17
|
+
|
18
|
+
# Load config options
|
19
|
+
SlackBotManager::Config::MANAGER_ATTRIBUTES.each do |key|
|
20
|
+
send("#{key}=", options[key] || SlackBotManager.config.send(key))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def configure
|
26
|
+
block_given? ? yield(config) : config
|
27
|
+
end
|
28
|
+
|
29
|
+
def config
|
30
|
+
Config
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
module SlackBotManager
|
2
|
+
module Connection
|
3
|
+
|
4
|
+
# Monitor our connections, while connections is {...}
|
5
|
+
def monitor
|
6
|
+
while connections do
|
7
|
+
sleep 1 # Pause momentarily
|
8
|
+
|
9
|
+
# On occasion, check our connection statuses
|
10
|
+
if Time.now.to_i % check_interval == 0
|
11
|
+
# Get tokens and connection statuses
|
12
|
+
tokens_status, rtm_status = self.redis.hgetall(tokens_key), self.redis.hgetall(teams_key)
|
13
|
+
|
14
|
+
# Manage connections
|
15
|
+
connections.each do |cid,conn|
|
16
|
+
id,_s = cid.split(':')
|
17
|
+
|
18
|
+
# Remove/continue if connection is/will close or no longer connected
|
19
|
+
if !conn
|
20
|
+
warning("Removing: #{id} (Reason: rtm_not_connection)")
|
21
|
+
destroy(cid: cid)
|
22
|
+
|
23
|
+
elsif !conn.connected?
|
24
|
+
warning("Removing: #{conn.id} (Reason: #{conn.status})")
|
25
|
+
to_remove = ['token_revoked'].include?((conn.status || '').to_s)
|
26
|
+
destroy(cid: cid, remove_token: to_remove)
|
27
|
+
|
28
|
+
# Team is no longer valid, remove
|
29
|
+
elsif !!tokens_status[conn.id].empty?
|
30
|
+
warning("Removing: #{conn.id} (Reason: token_missing)")
|
31
|
+
destroy(cid: cid, remove_token: true)
|
32
|
+
|
33
|
+
elsif rtm_status[conn.id] == 'destroy'
|
34
|
+
warning("Removing: #{conn.id} (Reason: token_destroy)")
|
35
|
+
destroy(cid: cid)
|
36
|
+
|
37
|
+
# Kill connection if token has changed, will re-create next block below
|
38
|
+
elsif tokens_status[conn.id] != conn.token
|
39
|
+
warning("Removing: #{conn.id} (Reason: new_token)")
|
40
|
+
destroy(cid: cid)
|
41
|
+
|
42
|
+
# Connection should be re-created unless it is active, will update next block below
|
43
|
+
elsif rtm_status[conn.id] != 'active'
|
44
|
+
warning("Restarting: #{conn.id} (Reason: #{rtm_status[conn.id]})")
|
45
|
+
destroy(cid: cid)
|
46
|
+
self.redis.hset(tokens_key, conn.id, tokens_status[conn.id])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Give pause before any reconnects, as destroy methods might still be processing in their threads
|
51
|
+
sleep 1
|
52
|
+
|
53
|
+
# Check for new tokens / reconnections (reload keys since we might modify if bad). Kill and recreate
|
54
|
+
tokens_status, rtm_status = self.redis.hgetall(tokens_key), self.redis.hgetall(teams_key)
|
55
|
+
tokens_diff = (tokens_status.keys - rtm_status.keys) + (rtm_status.keys - tokens_status.keys)
|
56
|
+
|
57
|
+
unless !!tokens_diff.empty?
|
58
|
+
tokens_diff.uniq.each do |id|
|
59
|
+
warning("Starting: #{id}")
|
60
|
+
destroy(id: id)
|
61
|
+
create(id, tokens_status[id])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
info("Active Connections: [#{connections.count}]")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Create websocket connections for active tokens
|
71
|
+
def start
|
72
|
+
# Clear RTM connections
|
73
|
+
self.redis.del(teams_key)
|
74
|
+
|
75
|
+
# Start a new connection for each team
|
76
|
+
self.redis.hgetall(tokens_key).each do |id,token|
|
77
|
+
create(id,token)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Remove all connections from app, will disconnect in monitor loop
|
82
|
+
def stop
|
83
|
+
# Thread wrapped to ensure no lock issues on shutdown
|
84
|
+
thr = Thread.new {
|
85
|
+
conns = self.redis.hgetall(teams_key)
|
86
|
+
self.redis.pipelined do
|
87
|
+
conns.each{|k,v| self.redis.hset(teams_key, k, 'destroy') }
|
88
|
+
end
|
89
|
+
info("Stopped.")
|
90
|
+
}
|
91
|
+
thr.join
|
92
|
+
end
|
93
|
+
|
94
|
+
# Issue restart status on all RTM connections, will re-connect in monitor loop
|
95
|
+
def restart
|
96
|
+
conns = self.redis.hgetall(teams_key)
|
97
|
+
self.redis.pipelined do
|
98
|
+
conns.each{|k,v| self.redis.hset(teams_key, k, 'restart') }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Get status of current connections
|
103
|
+
def status
|
104
|
+
info("Active connections: [#{self.redis.hgetall(teams_key).count}]")
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
protected
|
109
|
+
|
110
|
+
# Find the connection based on id and has active connection
|
111
|
+
def find_connection(id)
|
112
|
+
connections.each do |cid,conn|
|
113
|
+
return (conn.connected? ? conn : false) if conn && conn.id == id
|
114
|
+
end
|
115
|
+
false
|
116
|
+
end
|
117
|
+
|
118
|
+
# Create new connection if not exist
|
119
|
+
def create(id,token)
|
120
|
+
raise SlackBotManager::TokenAlreadyConnected if find_connection(id)
|
121
|
+
|
122
|
+
# Create connection
|
123
|
+
conn = SlackBotManager::Client.new(id, token)
|
124
|
+
|
125
|
+
# Add to connections using a uniq token, as we might have connection closing and opening with same id
|
126
|
+
if conn
|
127
|
+
cid = [id,Time.now.to_i].join(':')
|
128
|
+
connections[cid] = conn
|
129
|
+
info("Connected: #{id} (Connection: #{cid})")
|
130
|
+
self.redis.hset(teams_key, id, 'active')
|
131
|
+
end
|
132
|
+
rescue => err
|
133
|
+
on_error(err)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Disconnect from a RTM connection
|
137
|
+
def destroy(*args)
|
138
|
+
options = args.extract_options!
|
139
|
+
|
140
|
+
# Get connection or search for connection with cid
|
141
|
+
if options[:cid]
|
142
|
+
conn, cid = connections[options[:cid]], options[:cid]
|
143
|
+
elsif options[:id]
|
144
|
+
conn, cid = find_connection(options[:id])
|
145
|
+
end
|
146
|
+
return false unless conn && cid
|
147
|
+
|
148
|
+
# Kill connection, remove from connection keys, and delete from connections list
|
149
|
+
begin
|
150
|
+
thr = Thread.new {
|
151
|
+
self.redis.hdel(teams_key, conn.id) rescue nil
|
152
|
+
|
153
|
+
if options[:remove_token]
|
154
|
+
self.redis.hdel(tokens_key, conn.id) rescue nil
|
155
|
+
end
|
156
|
+
}
|
157
|
+
thr.join
|
158
|
+
connections.delete(cid)
|
159
|
+
rescue => err
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
rescue => err
|
163
|
+
on_error(err)
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module SlackBotManager
|
2
|
+
module Tokens
|
3
|
+
|
4
|
+
# Add token(s) to be connected
|
5
|
+
def add_token(*tokens)
|
6
|
+
tokens.each do |token|
|
7
|
+
begin
|
8
|
+
team_info = check_token_status(token)
|
9
|
+
|
10
|
+
# Add to token list
|
11
|
+
self.redis.hset(self.tokens_key, team_info['team_id'], token)
|
12
|
+
rescue => err
|
13
|
+
on_error(err)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Remove token(s) and connection(s)
|
19
|
+
def remove_token(*tokens)
|
20
|
+
tokens.each do |token|
|
21
|
+
begin
|
22
|
+
id = get_id_from_token(token) # As token should be in list
|
23
|
+
raise SlackBotManager::InvalidToken if !!id.empty?
|
24
|
+
|
25
|
+
# Delete from token and connections list
|
26
|
+
self.redis.hdel(self.tokens_key, id)
|
27
|
+
self.redis.hdel(self.teams_key, id)
|
28
|
+
rescue => err
|
29
|
+
on_error(err)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Remove all tokens
|
35
|
+
def clear_tokens
|
36
|
+
remove_token(*self.redis.hgetall(self.tokens_key).values)
|
37
|
+
rescue => err
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
# Restart token connection(s)
|
42
|
+
def update_token(*tokens)
|
43
|
+
tokens.each do |token|
|
44
|
+
begin
|
45
|
+
id = get_id_from_token(token) # As token should be in list
|
46
|
+
raise SlackBotManager::InvalidToken if !!id.empty?
|
47
|
+
|
48
|
+
# Issue reset command
|
49
|
+
self.redis.hset(self.teams_key, id, 'restart')
|
50
|
+
rescue => err
|
51
|
+
on_error(err)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Check token connection(s)
|
57
|
+
def check_token(*tokens)
|
58
|
+
rtm_keys = self.redis.hgetall(self.teams_key)
|
59
|
+
|
60
|
+
tokens.each do |token|
|
61
|
+
begin
|
62
|
+
team_info = check_token_status(token)
|
63
|
+
|
64
|
+
info("Team #{team_info['team_id']} :: #{rtm_keys[ team_info['team_id'] ] || 'not_connected'}")
|
65
|
+
rescue => err
|
66
|
+
on_error(err)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
# Get team id from Slack. (also test if token is valid)
|
74
|
+
def check_token_status(token)
|
75
|
+
info = Slack::Web::Client.new(token: token).auth_test
|
76
|
+
raise SlackBotManager::InvalidToken unless info && info['ok']
|
77
|
+
info
|
78
|
+
end
|
79
|
+
|
80
|
+
# Given a token, get id from tokens list
|
81
|
+
def get_id_from_token(token)
|
82
|
+
self.redis.hgetall(self.tokens_key).each{|id,t| return id if t == token }
|
83
|
+
false
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Core requirements
|
2
|
+
require 'logger'
|
3
|
+
require 'redis'
|
4
|
+
require 'slack-ruby-client'
|
5
|
+
|
6
|
+
# core components
|
7
|
+
require 'slack-bot-manager/version'
|
8
|
+
require 'slack-bot-manager/errors'
|
9
|
+
require 'slack-bot-manager/logger'
|
10
|
+
require 'slack-bot-manager/config'
|
11
|
+
require 'slack-bot-manager/extend.rb'
|
12
|
+
|
13
|
+
# bot client connection
|
14
|
+
require 'slack-bot-manager/client/commands'
|
15
|
+
require 'slack-bot-manager/client/base'
|
16
|
+
|
17
|
+
# connection manager
|
18
|
+
require 'slack-bot-manager/manager/connection'
|
19
|
+
require 'slack-bot-manager/manager/tokens'
|
20
|
+
require 'slack-bot-manager/manager/base'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
|
2
|
+
require 'slack-bot-manager/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'slack-bot-manager'
|
6
|
+
s.version = SlackBotManager::VERSION
|
7
|
+
s.authors = ['Greg Leuch']
|
8
|
+
s.email = ['greg@betaworks.com', 'contact@gleu.ch']
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.required_rubygems_version = '>= 1.3.6'
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
s.test_files = `git ls-files -- spec/*`.split("\n")
|
13
|
+
s.require_paths = ['lib']
|
14
|
+
s.homepage = 'http://github.com/betaworks/slack-bot-manager'
|
15
|
+
s.licenses = ['MIT']
|
16
|
+
s.summary = 'Slack RealTime API client connection manager.'
|
17
|
+
|
18
|
+
s.add_dependency 'slack-ruby-client', '>=0.5.1'
|
19
|
+
s.add_dependency 'faye-websocket', '>=0.10.0'
|
20
|
+
s.add_dependency 'redis', '>=3.2.2'
|
21
|
+
|
22
|
+
# s.add_development_dependency 'erubis'
|
23
|
+
# s.add_development_dependency 'json-schema'
|
24
|
+
# s.add_development_dependency 'rake'
|
25
|
+
# s.add_development_dependency 'rspec'
|
26
|
+
# s.add_development_dependency 'vcr'
|
27
|
+
# s.add_development_dependency 'webmock'
|
28
|
+
# s.add_development_dependency 'rubocop', '0.35.0'
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: slack-bot-manager
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0pre1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Greg Leuch
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-01-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: slack-ruby-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.5.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.5.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faye-websocket
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.10.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.10.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: redis
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.2.2
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.2.2
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- greg@betaworks.com
|
58
|
+
- contact@gleu.ch
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- ".gitignore"
|
64
|
+
- ".gitmodules"
|
65
|
+
- ".rspec"
|
66
|
+
- CHANGELOG.md
|
67
|
+
- LICENSE.md
|
68
|
+
- README.md
|
69
|
+
- UPGRADING.md
|
70
|
+
- lib/slack-bot-manager.rb
|
71
|
+
- lib/slack-bot-manager/client/base.rb
|
72
|
+
- lib/slack-bot-manager/client/commands.rb
|
73
|
+
- lib/slack-bot-manager/config.rb
|
74
|
+
- lib/slack-bot-manager/errors.rb
|
75
|
+
- lib/slack-bot-manager/extend.rb
|
76
|
+
- lib/slack-bot-manager/logger.rb
|
77
|
+
- lib/slack-bot-manager/manager/base.rb
|
78
|
+
- lib/slack-bot-manager/manager/connection.rb
|
79
|
+
- lib/slack-bot-manager/manager/tokens.rb
|
80
|
+
- lib/slack-bot-manager/version.rb
|
81
|
+
- slack-bot-manager.gemspec
|
82
|
+
homepage: http://github.com/betaworks/slack-bot-manager
|
83
|
+
licenses:
|
84
|
+
- MIT
|
85
|
+
metadata: {}
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
require_paths:
|
89
|
+
- lib
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: 1.3.6
|
100
|
+
requirements: []
|
101
|
+
rubyforge_project:
|
102
|
+
rubygems_version: 2.4.5.1
|
103
|
+
signing_key:
|
104
|
+
specification_version: 4
|
105
|
+
summary: Slack RealTime API client connection manager.
|
106
|
+
test_files: []
|