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 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
@@ -0,0 +1,6 @@
1
+ ._*
2
+ .DS_Store
3
+ .env
4
+ pkg
5
+ Gemfile.lock
6
+ tokens.yml
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
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
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,3 @@
1
+ # Upgrading Slack Bot Manager
2
+
3
+ Since this is still in version 0.1.0, there are no upgrade notes.
@@ -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,3 @@
1
+ module SlackBotManager
2
+ VERSION = '0.1.0pre1'
3
+ 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: []