slack-bot-server 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 61c3888618c930da32157133af1fd57ac3a40123
4
+ data.tar.gz: 4f7c8adff1540e9162cef41f0ade4a2ee06aeb39
5
+ SHA512:
6
+ metadata.gz: 051cc3a2f27714cf76559aab701dbdb2194eaba113376eb832d886b61116b3c5240c8bcf9beb3cac01c23ee7bc247c61838f234774f8ff9a64ac0c98fe76568f
7
+ data.tar.gz: 76c4c6ee283a948aa13330b0e237bbbbc1562013b0077903cb0d45fb44d5cb5c3c6219d1188361ef0104adc31a1613dfa9951b570cf2e3e52071fd6840e0e506
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.3
4
+ before_install: gem install bundler -v 1.10.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in slack_bot_server.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 James Adam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,41 @@
1
+ # SlackBotServer
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/slack_bot_server`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'slack_bot_server'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install slack_bot_server
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. Run `bundle exec slack_bot_server` to use the gem in this directory, ignoring other installed copies of this gem.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/slack_bot_server.
36
+
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
41
+
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,11 @@
1
+ require 'slack_bot_server/version'
2
+ require 'slack_bot_server/server'
3
+ require 'logger'
4
+
5
+ module SlackBotServer
6
+ def self.logger
7
+ @logger ||= Logger.new(STDOUT)
8
+ end
9
+ end
10
+
11
+ SlackBotServer.logger.level = Logger::INFO
@@ -0,0 +1,189 @@
1
+ require 'slack'
2
+ require 'securerandom'
3
+
4
+ class SlackBotServer::Bot
5
+ attr_reader :key
6
+
7
+ class InvalidToken < RuntimeError; end
8
+
9
+ def initialize(token:, key: SecureRandom.uuid)
10
+ @token = token
11
+ @key = key
12
+ @api = ::Slack::Client.new(token: @token)
13
+ @im_channel_ids = []
14
+
15
+ raise InvalidToken unless auth_test['ok']
16
+ end
17
+
18
+ def say(options)
19
+ @api.chat_postMessage(default_message_options.merge(options))
20
+ end
21
+
22
+ def reply(options)
23
+ channel = @last_received_data['channel']
24
+ @api.chat_postMessage(default_message_options.merge(options.merge(channel: channel)))
25
+ end
26
+
27
+ def say_to(user_id, options)
28
+ result = @api.im_open(user: user_id)
29
+ channel_id = result['channel']['id']
30
+ say(options.merge(channel: channel_id))
31
+ end
32
+
33
+ def call(method, args)
34
+ args.symbolize_keys!
35
+ @api.send(method, args)
36
+ end
37
+
38
+ def start
39
+ @ws = Faye::WebSocket::Client.new(websocket_url, nil, ping: 60)
40
+
41
+ @ws.on :open do |event|
42
+ log "connected to '#{team}'"
43
+ load_im_channels
44
+ end
45
+
46
+ @ws.on :message do |event|
47
+ begin
48
+ debug event
49
+ handle_message(event)
50
+ rescue => e
51
+ log error: e
52
+ end
53
+ end
54
+
55
+ @ws.on :close do |event|
56
+ log "disconnected"
57
+ end
58
+ end
59
+
60
+ def stop
61
+ log "closing connection"
62
+ @ws.close
63
+ log "closed"
64
+ end
65
+
66
+ class << self
67
+ def username(name)
68
+ default_message_options[:username] = name
69
+ end
70
+
71
+ def icon_url(url)
72
+ default_message_options[:icon_url] = url
73
+ end
74
+
75
+ def default_message_options
76
+ @default_message_options ||= {channel: '#general'}
77
+ end
78
+
79
+ def callbacks_for(type)
80
+ callbacks = @callbacks[type.to_sym] || []
81
+ if superclass.respond_to?(:callbacks_for)
82
+ callbacks += superclass.callbacks_for(type)
83
+ end
84
+ callbacks
85
+ end
86
+
87
+ def on(type, &block)
88
+ @callbacks ||= {}
89
+ @callbacks[type.to_sym] ||= []
90
+ @callbacks[type.to_sym] << block
91
+ end
92
+
93
+ def on_mention(&block)
94
+ on(:message) do |data|
95
+ if !bot_message?(data) &&
96
+ (data['text'] =~ /\A#{user}[\s\:](.*)/ ||
97
+ data['text'] =~ /\A<@#{user_id}>[\s\:](.*)/)
98
+ message = $1.strip
99
+ @last_received_data = data.merge('message' => message)
100
+ instance_exec(@last_received_data, &block)
101
+ end
102
+ end
103
+ end
104
+
105
+ def on_im(&block)
106
+ on(:message) do |data|
107
+ if is_im_channel?(data['channel']) && !bot_message?(data)
108
+ @last_received_data = data.merge('message' => data['text'])
109
+ instance_exec(@last_received_data, &block)
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ on :im_created do |data|
116
+ channel_id = data['channel']['id']
117
+ log "Adding new IM channel: #{channel_id}"
118
+ @im_channel_ids << channel_id
119
+ end
120
+
121
+ def to_s
122
+ "<#{self.class.name} key:#{key}>"
123
+ end
124
+
125
+ private
126
+
127
+ def handle_message(event)
128
+ data = MultiJson.load(event.data)
129
+ if data["type"]
130
+ relevant_callbacks = self.class.callbacks_for(data["type"])
131
+ if relevant_callbacks && relevant_callbacks.any?
132
+ relevant_callbacks.each do |c|
133
+ instance_exec(data, &c)
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ def log(message)
140
+ text = message.is_a?(String) ? message : message.inspect
141
+ text = "[BOT/#{user}] #{text}"
142
+ SlackBotServer.logger.info(message)
143
+ end
144
+
145
+ def debug(message)
146
+ text = message.is_a?(String) ? message : message.inspect
147
+ text = "[BOT/#{user}] #{text}"
148
+ SlackBotServer.logger.debug(message)
149
+ end
150
+
151
+ def user
152
+ auth_test['user']
153
+ end
154
+
155
+ def user_id
156
+ auth_test['user_id']
157
+ end
158
+
159
+ def team
160
+ auth_test['team']
161
+ end
162
+
163
+ def auth_test
164
+ @auth_test ||= @api.auth_test
165
+ end
166
+
167
+ def load_im_channels
168
+ log "Loading IM channels"
169
+ result = @api.im_list
170
+ @im_channel_ids = result['ims'].map { |d| d['id'] }
171
+ log im_channels: @im_channel_ids
172
+ end
173
+
174
+ def is_im_channel?(id)
175
+ @im_channel_ids.include?(id)
176
+ end
177
+
178
+ def bot_message?(data)
179
+ data['subtype'] == 'bot_message'
180
+ end
181
+
182
+ def websocket_url
183
+ @api.post('rtm.start')['url']
184
+ end
185
+
186
+ def default_message_options
187
+ self.class.default_message_options
188
+ end
189
+ end
@@ -0,0 +1,14 @@
1
+ class SlackBotServer::LocalQueue
2
+ def initialize
3
+ @queue = Queue.new
4
+ end
5
+
6
+ def push(value)
7
+ @queue << value
8
+ end
9
+
10
+ def pop
11
+ value = @queue.pop(true) rescue ThreadError
12
+ value == ThreadError ? nil : value
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ require 'multi_json'
2
+
3
+ class SlackBotServer::RedisQueue
4
+ def initialize(redis)
5
+ @key = 'slack_bot_server:queue'
6
+ @redis = redis
7
+ end
8
+
9
+ def push(value)
10
+ @redis.rpush @key, MultiJson.dump(value)
11
+ end
12
+
13
+ def pop
14
+ json_value = @redis.lpop @key
15
+ if json_value
16
+ MultiJson.load(json_value)
17
+ else
18
+ nil
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,23 @@
1
+ # Send commands to a running SlackBotServer::Server instance
2
+ #
3
+ # This should be initialized with a queue that is shared with the
4
+ # targetted server (e.g. the same local queue instance, or a
5
+ # redis queue instance that points at the same redis server).
6
+
7
+ class SlackBotServer::RemoteControl
8
+ def initialize(queue)
9
+ @queue = queue
10
+ end
11
+
12
+ def add_token(token)
13
+ @queue.push([:add_token, token])
14
+ end
15
+
16
+ def remove_bot(key)
17
+ @queue.push([:remove_bot, key])
18
+ end
19
+
20
+ def call(key, method, args)
21
+ @queue.push([:call, [key, method, args]])
22
+ end
23
+ end
@@ -0,0 +1,98 @@
1
+ require 'slack_bot_server/bot'
2
+ require 'slack_bot_server/simple_bot'
3
+ require 'slack_bot_server/redis_queue'
4
+
5
+ class SlackBotServer::Server
6
+ attr_reader :queue
7
+
8
+ def initialize(queue: SlackBotServer::LocalQueue.new)
9
+ @queue = queue
10
+ @bots = {}
11
+ @new_token_proc = -> (token) { SlackBotServer::SimpleBot.new(token: token) }
12
+ @running = false
13
+ end
14
+
15
+ def on_new_token(&block)
16
+ @new_token_proc = block
17
+ end
18
+
19
+ def start
20
+ EM.run do
21
+ @running = true
22
+ @bots.each { |key, bot| bot.start }
23
+ add_timers
24
+ end
25
+ end
26
+
27
+ def add_timers
28
+ EM.add_periodic_timer(1) do
29
+ begin
30
+ next_message = queue.pop
31
+ process_instruction(next_message) if next_message
32
+ rescue => e
33
+ log_error(e)
34
+ end
35
+ end
36
+ end
37
+
38
+ def start_in_background
39
+ Thread.start { start }
40
+ end
41
+
42
+ def bot(key)
43
+ @bots[key.to_sym]
44
+ end
45
+
46
+ def add_bot(bot)
47
+ log "adding bot #{bot}"
48
+ @bots[bot.key.to_sym] = bot
49
+ bot.start if @running
50
+ end
51
+
52
+ def add_token(token)
53
+ bot = @new_token_proc.call(token)
54
+ add_bot(bot) if bot
55
+ rescue => e
56
+ log_error(e)
57
+ end
58
+
59
+ def remove_bot(key)
60
+ if (bot = bot(key))
61
+ bot.stop
62
+ @bots.delete(key.to_sym)
63
+ end
64
+ rescue => e
65
+ log_error(e)
66
+ end
67
+
68
+ private
69
+
70
+ def process_instruction(instruction)
71
+ type, *args = instruction
72
+ case type.to_sym
73
+ when :add_token
74
+ token = args.first
75
+ log "got new token: '#{token}'"
76
+ add_token(token)
77
+ when :remove_bot
78
+ key = args.first
79
+ remove_bot(key)
80
+ when :call
81
+ key, method, method_args = args
82
+ bot = bot(key)
83
+ bot.call(method, method_args)
84
+ else
85
+ log unknown_command: instruction
86
+ end
87
+ end
88
+
89
+ def log(message)
90
+ text = message.is_a?(String) ? message : message.inspect
91
+ SlackBotServer.logger.info(text)
92
+ end
93
+
94
+ def log_error(e)
95
+ SlackBotServer.logger.warn("Error in server: #{e} - #{e.message}")
96
+ SlackBotServer.logger.warn(e.backtrace.join("\n"))
97
+ end
98
+ end
@@ -0,0 +1,13 @@
1
+ require 'slack_bot_server/bot'
2
+
3
+ class SlackBotServer::SimpleBot < SlackBotServer::Bot
4
+ username 'SimpleBot'
5
+
6
+ on_mention do |data|
7
+ reply text: "You said '#{data['message']}', and I'm frankly fascinated."
8
+ end
9
+
10
+ on_im do
11
+ reply text: "Hmm, OK, let me get back to you about that."
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module SlackBotServer
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'slack_bot_server/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "slack-bot-server"
8
+ spec.version = SlackBotServer::VERSION
9
+ spec.authors = ["James Adam"]
10
+ spec.email = ["james@lazyatom.com"]
11
+
12
+ spec.summary = %q{A server for hosting slack bots.}
13
+ spec.description = %q{This software lets you write and host multiple slack bots, potentially for multiple different teams or even services.}
14
+ spec.homepage = "https://github.com/lazyatom/slack-bot-server"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "slack-api", "~> 1.1"
23
+ spec.add_dependency "multi_json"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.10"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec"
28
+ spec.add_development_dependency "rspec-eventmachine"
29
+ end
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: slack-bot-server
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - James Adam
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-06-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: slack-api
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: multi_json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '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'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.10'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-eventmachine
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: This software lets you write and host multiple slack bots, potentially
98
+ for multiple different teams or even services.
99
+ email:
100
+ - james@lazyatom.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - ".rspec"
107
+ - ".travis.yml"
108
+ - Gemfile
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - lib/slack_bot_server.rb
113
+ - lib/slack_bot_server/bot.rb
114
+ - lib/slack_bot_server/local_queue.rb
115
+ - lib/slack_bot_server/redis_queue.rb
116
+ - lib/slack_bot_server/remote_control.rb
117
+ - lib/slack_bot_server/server.rb
118
+ - lib/slack_bot_server/simple_bot.rb
119
+ - lib/slack_bot_server/version.rb
120
+ - slack_bot_server.gemspec
121
+ homepage: https://github.com/lazyatom/slack-bot-server
122
+ licenses:
123
+ - MIT
124
+ metadata: {}
125
+ post_install_message:
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubyforge_project:
141
+ rubygems_version: 2.4.5
142
+ signing_key:
143
+ specification_version: 4
144
+ summary: A server for hosting slack bots.
145
+ test_files: []