slack-bot-manager 0.1.0pre1

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: 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: []