rebot 0.0.3

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: 9383bbb0fcee4b0c1a3a2f1fee70503e64331680
4
+ data.tar.gz: 9ae4bf2210a7d052b17ed1ecff6b90f00da6f479
5
+ SHA512:
6
+ metadata.gz: 11c8cc7d702e01f75ecfe41747ff648161841b7589dafbde346040c2a0409986628f7018b5f8fd4639a592e4a10994c3187d326f0e49196f4212f88d0d0cb213
7
+ data.tar.gz: f1f004e708d9c695dc50d2ea2770b6fce27cad13917efac01b4024383f4a25f48a1cda1bf369e7863419e9b6bda73e6bd1fef9ce8457f21c21354347f51baaba
@@ -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.2.3
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rebot.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Artyom Keydunov
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
+ # Rebot
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/rebot`. 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 'rebot'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install rebot
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 spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rebot. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
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,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rebot"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require "rebot"
3
+ require "rebot/cli"
4
+
5
+ Rebot::CLI.start ARGV
@@ -0,0 +1,30 @@
1
+ require 'slack'
2
+ require 'logger'
3
+ require 'faye/websocket'
4
+
5
+ require "rebot/version"
6
+ require "rebot/message"
7
+ require "rebot/bot"
8
+ require "rebot/server"
9
+ require "rebot/conversation"
10
+ require "rebot/redis_queue"
11
+ require "rebot/remote_control"
12
+
13
+ module Rebot
14
+ def self.logger
15
+ @logger ||= Logger.new(STDOUT)
16
+ end
17
+
18
+ def self.conversation(name, &block)
19
+ @convos ||= {}
20
+ @convos[name.to_sym] = block
21
+ end
22
+
23
+ def self.find_conversation(name)
24
+ if convo = @convos[name.to_sym]
25
+ convo
26
+ else
27
+ raise "Cannot find conversation: #{name}"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,320 @@
1
+ module Rebot
2
+ class Bot
3
+ attr_reader :token, :identity
4
+
5
+ class InvalidToken < RuntimeError; end
6
+
7
+ class Identity < Struct.new(:name, :id); end
8
+
9
+ def initialize(token:)
10
+ @token = token
11
+ @identity = nil
12
+ @api = ::Slack::Client.new(token: @token)
13
+
14
+ @im_channel_ids = []
15
+ @channel_ids = []
16
+ @group_ids = []
17
+
18
+ @message_count = 0
19
+
20
+ @ims = []
21
+
22
+ @convos = []
23
+
24
+ @connected = false
25
+ @running = false
26
+ end
27
+
28
+ def say(message)
29
+ @message_count += 1
30
+ slack_message = {
31
+ :id => @message_count,
32
+ :type => "message",
33
+
34
+ :channel => message[:channel],
35
+ :text => message[:text] || "", # slack-web-api gem does not allow nil for text,
36
+ :username => message[:username],
37
+ :parse => message[:parse],
38
+ :link_names => message[:link_names],
39
+ :attachments => (message[:attachments] ? JSON.dump(message[:attachments]) : nil),
40
+ :unfurl_links => message[:unfurl_links],
41
+ :unfurl_media => message[:unfurl_media],
42
+ :icon_url => message[:icon_url],
43
+ :icon_emoji => message[:icon_emoji],
44
+ :as_user => message[:as_user] || true
45
+ }
46
+
47
+ #if (message[:icon_url] || message[:icon_emoji] || message[:username] )
48
+ # slack_message[:as_user] = false
49
+ #else
50
+ # slack_message[:as_user] = message[:as_user] || true
51
+ #end
52
+
53
+ # These options are not supported by the RTM
54
+ # so if they are specified, we use the web API to send messages.
55
+ if slack_message[:attachments] || slack_message[:icon_emoji] || slack_message[:username] || slack_message[:icon_url]
56
+ @api.chat_postMessage(slack_message)
57
+ else
58
+ @ws.send(JSON.dump(slack_message))
59
+ end
60
+ end
61
+
62
+ def typing
63
+ @message_count += 1
64
+ @ws.send(JSON.dump(channel: @last_received_message.channel, id: @message_count, type: "typing"))
65
+ end
66
+
67
+ def reply(text_or_options)
68
+ channel = @last_received_message.channel
69
+
70
+ if text_or_options.is_a?(String)
71
+ options = { channel: channel, text: text_or_options }
72
+ elsif text_or_options.is_a?(Hash)
73
+ options = text_or_options.merge(channel: channel)
74
+ else
75
+ raise "unreachable"
76
+ end
77
+
78
+ say(options)
79
+ end
80
+
81
+ def call(method, args)
82
+ args.symbolize_keys!
83
+ @api.send(method, args)
84
+ end
85
+
86
+ def start
87
+ unless auth_test['ok']
88
+ log "Error connecting bot (token: #{token}) to Slack: #{auth_test}"
89
+ return
90
+ end
91
+
92
+ # TODO: it should be async
93
+ rtm_start = @api.post('rtm.start')
94
+ @identity = Identity.new(rtm_start['self']['name'], rtm_start['self']['id'])
95
+ @ws = Faye::WebSocket::Client.new(rtm_start['url'], nil, ping: 60)
96
+
97
+ @running = true
98
+ @ws.on :open do |event|
99
+ @connected = true
100
+ log "connected to '#{team}'"
101
+ load_im_channels
102
+ load_channels
103
+ end
104
+
105
+ @ws.on :message do |event|
106
+ begin
107
+ debug event.data
108
+ handle_event(event)
109
+ rescue => e
110
+ log error: e
111
+ log backtrace: e.backtrace
112
+ Rollbar.error(e)
113
+ end
114
+ end
115
+
116
+ @ws.on :close do |event|
117
+ log "disconnected"
118
+ @connected = false
119
+ @auth_test = nil
120
+ if @running
121
+ start
122
+ end
123
+ end
124
+
125
+ EM.add_periodic_timer(1) do
126
+ @convos.each { |convo| convo.tick }
127
+ end
128
+ end
129
+
130
+ def stop
131
+ log "closing connection"
132
+ @running = false
133
+ @ws.close
134
+ log "closed"
135
+ end
136
+
137
+ def connected?
138
+ @connected
139
+ end
140
+
141
+ def start_conversation(name = nil, *args, &block)
142
+ if name
143
+ convo_block = Rebot.find_conversation(name)
144
+ elsif block_given?
145
+ convo_block = block
146
+ else
147
+ raise "Pass registered conversation name or block"
148
+ end
149
+
150
+ convo = Conversation.new(self, @last_received_message)
151
+ @convos.push(convo)
152
+ convo.start(convo_block, *args)
153
+ end
154
+
155
+ def conversation_ended(convo)
156
+ @convos.delete(convo)
157
+ end
158
+
159
+ class << self
160
+ def callbacks_for(type)
161
+ callbacks = @callbacks[type.to_sym] || []
162
+ if superclass.respond_to?(:callbacks_for)
163
+ callbacks += superclass.callbacks_for(type)
164
+ end
165
+ callbacks
166
+ end
167
+
168
+ def on(type, &block)
169
+ @callbacks ||= {}
170
+ @callbacks[type.to_sym] ||= []
171
+ @callbacks[type.to_sym] << block
172
+ end
173
+
174
+ def hears(pattern, &block)
175
+ callback = Proc.new do |message|
176
+ pattern = pattern.is_a?(String) ? Regexp.new(pattern, true) : pattern
177
+ if match_data = pattern.match(message.text)
178
+ debug "I heard #{pattern}"
179
+ instance_exec(message, *match_data.captures, &block)
180
+ false
181
+ end
182
+ end
183
+
184
+ on(:dm, &callback)
185
+ on(:mention, &callback)
186
+ end
187
+
188
+ def afters
189
+ @afters ||= []
190
+ end
191
+
192
+ def befores
193
+ @befores ||= []
194
+ end
195
+
196
+ def after(&block)
197
+ afters.push(block)
198
+ end
199
+
200
+ def before(&block)
201
+ befores.push(block)
202
+ end
203
+ end
204
+
205
+ on :im_created do |data|
206
+ channel_id = data['channel']['id']
207
+ log "Adding new IM channel: #{channel_id}"
208
+ @im_channel_ids << channel_id
209
+ end
210
+
211
+ on :channel_joined do |data|
212
+ channel_id = data['channel']['id']
213
+ log "Adding new channel: #{channel_id}"
214
+ @channel_ids << channel_id
215
+ end
216
+
217
+ on :channel_left do |data|
218
+ channel_id = data['channel']
219
+ log "Removing channel: #{channel_id}"
220
+ @channel_ids.delete(channel_id)
221
+ end
222
+
223
+ def to_s
224
+ "<#{self.class.name} token:#{token}>"
225
+ end
226
+
227
+ private
228
+
229
+ def handle_event(event)
230
+ data = JSON.parse(event.data)
231
+
232
+ # this is a confirmation of something we sent.
233
+ return unless data['ok'].nil?
234
+
235
+ if data['type'] == 'message'
236
+ return if data['user'] == auth_test['user_id']
237
+
238
+ # Ignore messages from slackbot
239
+ return if data['user'] == "USLACKBOT" || data['username'] == 'slackbot'
240
+ # message without text is probably an edit
241
+ return if data['text'].nil?
242
+
243
+ message = Message.new(data, self)
244
+ @last_received_message = message
245
+
246
+ self.class.befores.each { |c| instance_exec(message, &c) }
247
+
248
+ if convo = find_conversation(message)
249
+ convo.handle(message)
250
+ else
251
+ trigger(message.event, message)
252
+ end
253
+
254
+ self.class.afters.each { |c| instance_exec(message, &c) }
255
+ else
256
+ trigger(data['type'], data)
257
+ end
258
+ end
259
+
260
+ def trigger(event, message)
261
+ relevant_callbacks = self.class.callbacks_for(event)
262
+ if relevant_callbacks && relevant_callbacks.any?
263
+ relevant_callbacks.each do |c|
264
+ resp = instance_exec(message, &c)
265
+ break if resp == false
266
+ end
267
+ end
268
+ end
269
+
270
+ def find_conversation(message)
271
+ @convos.detect do |convo|
272
+ convo.active? &&
273
+ convo.source_message.channel == message.channel &&
274
+ convo.source_message.user == message.user
275
+ end
276
+ end
277
+
278
+ def log(message)
279
+ text = message.is_a?(String) ? message : message.inspect
280
+ text = "[BOT/#{user}] #{text}"
281
+ Rebot.logger.info(message)
282
+ end
283
+
284
+ def debug(message)
285
+ text = message.is_a?(String) ? message : message.inspect
286
+ text = "[BOT/#{user}] #{text}"
287
+ Rebot.logger.debug(message)
288
+ end
289
+
290
+ def user
291
+ auth_test['user']
292
+ end
293
+
294
+ def user_id
295
+ auth_test['user_id']
296
+ end
297
+
298
+ def team
299
+ auth_test['team']
300
+ end
301
+
302
+ def auth_test
303
+ @auth_test ||= @api.auth_test
304
+ end
305
+
306
+ def load_im_channels
307
+ debug "Loading IM channels"
308
+ result = @api.im_list
309
+ @im_channel_ids = result['ims'].map { |d| d['id'] }
310
+ debug im_channels: @im_channel_ids
311
+ end
312
+
313
+ def load_channels
314
+ debug "Loading Channels"
315
+ result = @api.channels_list(exclude_archived: 1)
316
+ @channel_ids = result['channels'].select { |d| d['is_member'] == true }.map { |d| d['id'] }
317
+ debug channels: @channel_ids
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,43 @@
1
+ require "thor"
2
+
3
+ module Rebot
4
+ class CLI < Thor
5
+ include Thor::Actions
6
+ class_option :verbose, type: :boolean, default: true, aliases: "-v"
7
+
8
+ attr_reader :bot_name
9
+
10
+ source_root File.expand_path("../../../template", __FILE__)
11
+
12
+ desc "new [BOT_NAME]", "Creates a BOT_NAME dir with your new bot structure."
13
+ def new(bot_name)
14
+ @bot_name = bot_name
15
+ @bot_class = camel_case(bot_name)
16
+ directory "project", bot_name, verbose: verbose
17
+ after_create
18
+ end
19
+
20
+ private
21
+
22
+ def camel_case(string)
23
+ return string.gsub(/^./) { |l| l.capitalize } if !string.match(/[_-]/)
24
+ altered_string = string.downcase.capitalize
25
+ altered_string.scan(/[_-][a-zA-Z]/).each do |match|
26
+ altered_string.gsub!(match, match[1].upcase)
27
+ end
28
+
29
+ altered_string
30
+ end
31
+
32
+ def verbose
33
+ options[:verbose]
34
+ end
35
+
36
+ def after_create
37
+ inside @bot_name do
38
+ run "chmod +x server"
39
+ run "bundle install"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,141 @@
1
+ module Rebot
2
+ class Conversation
3
+ DEFAULT_TIMEOUT = 180 # set default timeout to 180 seconds (3 minutes)
4
+ DEFAULT_TIMEOUT_MESSAGE = "We can pick this up later."
5
+
6
+ attr_reader :data, :source_message
7
+
8
+ def initialize(bot, source_message)
9
+ @bot = bot
10
+ @source_message = source_message
11
+ @data = {}
12
+ @handlers_stack = [[]]
13
+ @messages = []
14
+ @sent = []
15
+ @last_active_at = Time.now
16
+
17
+ @timeout = DEFAULT_TIMEOUT
18
+ @timeout_message = DEFAULT_TIMEOUT_MESSAGE
19
+ end
20
+
21
+ def timeout(value = nil)
22
+ if value
23
+ @timeout = value
24
+ else
25
+ @timeout
26
+ end
27
+ end
28
+
29
+ def timeout_message(value = nil)
30
+ if value
31
+ @timeout_message = value
32
+ else
33
+ @timeout_message
34
+ end
35
+ end
36
+
37
+ def start(block, *args)
38
+ @status = :active
39
+ instance_exec(*args, &block)
40
+ end
41
+
42
+ def matched_option(text)
43
+ stack = @handlers_stack.last
44
+ handler = stack.find do |o|
45
+ if o[:pattern]
46
+ @last_matched_data = o[:pattern].match(text)
47
+ end
48
+ end
49
+
50
+ unless handler
51
+ handler = stack.find { |o| o[:default] }
52
+ end
53
+
54
+ handler
55
+ end
56
+
57
+ def active?
58
+ @status == :active
59
+ end
60
+
61
+ def tick
62
+ return unless active?
63
+
64
+ if @messages.any?
65
+ message = @messages.shift
66
+ @bot.say(message)
67
+ @sent.push(message)
68
+ @last_active_at = Time.now
69
+ end
70
+
71
+ if @messages.empty? && @handlers_stack.last.empty?
72
+ @status = :completed
73
+ @bot.conversation_ended(self)
74
+ end
75
+
76
+ if Time.now - @last_active_at > timeout
77
+ # TODO: keep it simple for now
78
+ @status = :timeout
79
+ @bot.say(text: timeout_message, channel: @source_message.channel)
80
+ @bot.conversation_ended(self)
81
+ end
82
+ end
83
+
84
+ def handle(message)
85
+ @last_active_at = Time.now
86
+ Rebot.logger.debug "Handling message in conversation: #{message.text}"
87
+ if option = matched_option(message.text)
88
+ @handlers_stack.push([])
89
+ if option[:default]
90
+ instance_exec(message.text, &option[:callback])
91
+ else
92
+ instance_exec(*@last_matched_data.captures, &option[:callback])
93
+ end
94
+ else
95
+ # FIXME
96
+ say(@sent.last) if @messages.empty?
97
+ end
98
+ end
99
+
100
+ def say(message)
101
+ if message.is_a?(String)
102
+ message = { text: message, channel: @source_message.channel }
103
+ else
104
+ message[:channel] = @source_message.channel
105
+ end
106
+ @messages.push(message)
107
+ end
108
+
109
+ def hears(pattern, meta = {}, &callback)
110
+ pattern = Regexp.new(pattern) if pattern.is_a?(String)
111
+ @handlers_stack.last.push(
112
+ { pattern: pattern, callback: callback }.merge(meta)
113
+ )
114
+ end
115
+
116
+ def repeat
117
+ @handlers_stack.pop
118
+ end
119
+
120
+ def default( &callback)
121
+ hears(nil, { default: true }, &callback)
122
+ end
123
+
124
+ # extract into Conversation::Helpers
125
+ def time
126
+ /\d{1,2}:\d{1,2}\s*(am|pm)/i
127
+ end
128
+
129
+ def anything
130
+ /(.*)/i
131
+ end
132
+
133
+ def yes
134
+ /^(yes|yea|yup|yep|ya|sure|ok|y|yeah|yah)/i
135
+ end
136
+
137
+ def no
138
+ /^(no|nah|nope|n)/i
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,48 @@
1
+ module Rebot
2
+ class Message
3
+ attr_reader :text, :user, :channel, :event
4
+
5
+ def initialize(data, bot)
6
+ @bot = bot
7
+ @data = data
8
+
9
+ # FIXME: calling private method
10
+ @mention_regex = /\A(<@#{@bot.send(:user_id)}>)[\s\:](.*)/
11
+
12
+ @event = resolve_event(data['type'])
13
+ @text = cleanup_text(data['text'])
14
+ @user = data['user']
15
+ @channel = data['channel']
16
+ end
17
+
18
+ def []=(key, value)
19
+ @data[key.to_s] = value
20
+ end
21
+
22
+ def [](key)
23
+ @data[key.to_s]
24
+ end
25
+
26
+ private
27
+
28
+ def resolve_event(type)
29
+ if @data['text'] =~ @mention_regex
30
+ type = 'mention'
31
+ elsif @data['channel'].match(/^D/)
32
+ type = 'dm'
33
+ end
34
+ type
35
+ end
36
+
37
+ def cleanup_text(text)
38
+ text = text.strip
39
+ if md = text.match(@mention_regex)
40
+ text = md[2].strip
41
+ end
42
+ text = text.gsub(/&lt;/, '<');
43
+ text = text.gsub(/&gt;/, '>');
44
+ text = text.gsub(/&amp;/, '&');
45
+ text
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,26 @@
1
+ module Rebot
2
+ class RedisQueue
3
+ def initialize(redis=nil)
4
+ @key = 'rebot:queue'
5
+ @redis = if redis
6
+ redis
7
+ else
8
+ require 'redis'
9
+ Redis.new
10
+ end
11
+ end
12
+
13
+ def push(value)
14
+ @redis.rpush @key, JSON.dump(value)
15
+ end
16
+
17
+ def pop
18
+ json_value = @redis.lpop @key
19
+ if json_value
20
+ MultiJson.load(json_value, symbolize_keys: true)
21
+ else
22
+ nil
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ module Rebot
2
+ class RemoteControl
3
+ def self.add_token(token)
4
+ queue.push([:add_token, token])
5
+ end
6
+
7
+ def self.remove_bot(key)
8
+ @queue.push([:remove_bot, key])
9
+ end
10
+
11
+ def self.say(key, message_data)
12
+ queue.push([:say, key, message_data])
13
+ end
14
+
15
+ def self.call(key, method, args)
16
+ queue.push([:call, [key, method, args]])
17
+ end
18
+
19
+ def self.queue
20
+ @queue ||= RedisQueue.new
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,96 @@
1
+ module Rebot
2
+ class Server
3
+ attr_reader :queue
4
+
5
+ def initialize(queue: nil)
6
+ @queue = queue
7
+ @bots = {}
8
+ @new_token_proc = -> (token) { Rebot::BaseBot.new(token: token) }
9
+ @running = false
10
+ end
11
+
12
+ def on_new_token(&block)
13
+ @new_token_proc = block
14
+ end
15
+
16
+ def start
17
+ EM.run do
18
+ begin
19
+ @running = true
20
+ @bots.each { |key, bot| bot.start }
21
+ listen_for_instructions if @queue
22
+ rescue => e
23
+ log_error(e)
24
+ end
25
+ end
26
+ end
27
+
28
+ def listen_for_instructions
29
+ EM.add_periodic_timer(1) do
30
+ next_message = queue.pop
31
+ process_instruction(next_message) if next_message
32
+ end
33
+ end
34
+
35
+ def add_bot(bot)
36
+ # Do not add bot same bot twice
37
+ return if @bots[bot.token]
38
+ log "adding bot #{bot}"
39
+ @bots[bot.token] = bot
40
+ bot.start if @running
41
+ end
42
+
43
+ def add_token(token)
44
+ bot = @new_token_proc.call(token)
45
+ add_bot(bot) if bot
46
+ rescue => e
47
+ log_error(e)
48
+ end
49
+
50
+ def remove_bot(token)
51
+ if (bot = @bots[token])
52
+ bot.stop
53
+ @bots.delete(token)
54
+ end
55
+ rescue => e
56
+ log_error(e)
57
+ end
58
+
59
+ private
60
+
61
+ def process_instruction(instruction)
62
+ type, *args = instruction
63
+ case type.to_sym
64
+ when :add_token
65
+ token = args.first
66
+ log "got new token: '#{token}'"
67
+ add_token(token)
68
+ when :remove_bot
69
+ token = args.first
70
+ remove_bot(token)
71
+ when :say
72
+ token, message_data = args
73
+ if bot = @bots[token]
74
+ bot.say(message_data)
75
+ end
76
+ when :call
77
+ token, method, method_args = args
78
+ if bot = @bots[token]
79
+ bot.call(method, method_args)
80
+ end
81
+ else
82
+ log unknown_command: instruction
83
+ end
84
+ end
85
+
86
+ def log(message)
87
+ text = message.is_a?(String) ? message : message.inspect
88
+ Rebot.logger.info(text)
89
+ end
90
+
91
+ def log_error(e)
92
+ Rebot.logger.warn("Error in server: #{e} - #{e.message}")
93
+ Rebot.logger.warn(e.backtrace.join("\n"))
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,3 @@
1
+ module Rebot
2
+ VERSION = "0.0.3"
3
+ end
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rebot/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rebot"
8
+ spec.version = Rebot::VERSION
9
+ spec.authors = ["Artyom Keydunov"]
10
+ spec.email = ["artyom.keydunov@gmail.com"]
11
+
12
+ spec.summary = %q{Framework for building bot applications}
13
+ spec.description = %q{Framework for building bot applications}
14
+ spec.homepage = "https://github.com/keydunov/rebot"
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.required_ruby_version = "~>2.0"
23
+
24
+ spec.add_runtime_dependency "eventmachine"
25
+ spec.add_runtime_dependency "faye-websocket"
26
+ spec.add_runtime_dependency "thor"
27
+ spec.add_runtime_dependency "faraday"
28
+ spec.add_runtime_dependency "slack-web-api"
29
+ spec.add_runtime_dependency "redis"
30
+
31
+ spec.add_development_dependency "bundler", "~> 1.10"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_development_dependency "rspec"
34
+ end
@@ -0,0 +1,9 @@
1
+ class <%= @bot_class %> < Rebot::Bot
2
+ hears "who are you?" do
3
+ reply "My name is `#{identity.name}` and I am here to help you, boss."
4
+ end
5
+
6
+ hears /^(hi|hello)/i do
7
+ start_conversation :hello
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "rebot"
@@ -0,0 +1,14 @@
1
+ Rebot.conversation :hello do
2
+ say "Hi!"
3
+ say "How are you doing?"
4
+ say "Do you have any plans for a weekend?"
5
+
6
+ hears yes do
7
+ say "Nice! :+1:. Hope you have a great weekend!"
8
+ end
9
+
10
+ hears no do
11
+ say "Oh! Do you want to play with rebot framework?"
12
+ say "Check it out at https://github.com/keydunov/rebot"
13
+ end
14
+ end
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ require 'bundler'
3
+ Bundler.setup
4
+
5
+ require 'rebot'
6
+
7
+ require "./<%= @bot_name %>"
8
+ Dir[File.join(__dir__, 'conversations/*.rb')].each { |f| require f }
9
+
10
+ Rebot.logger.level = Logger::INFO unless ENV['VERBOSE']
11
+
12
+ #Create a new server
13
+ server = Rebot::Server.new
14
+
15
+ server.on_new_token do |token|
16
+ <%= @bot_class %>.new(token: token)
17
+ end
18
+
19
+ tokens = (ENV['TOKENS'] || "").split(",")
20
+ if tokens.empty?
21
+ puts "Please create new bot at https://slack.com/apps/new/A0F7YS25R-bots"
22
+ puts "Add run TOKENS=first-token,second-token ./server"
23
+ end
24
+ tokens.each do |token|
25
+ server.add_token(token)
26
+ end
27
+
28
+ server.start
metadata ADDED
@@ -0,0 +1,195 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rebot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Artyom Keydunov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-01-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: eventmachine
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
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'
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: thor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: faraday
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: slack-web-api
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
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: redis
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: bundler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.10'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.10'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '10.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '10.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: Framework for building bot applications
140
+ email:
141
+ - artyom.keydunov@gmail.com
142
+ executables:
143
+ - rebot
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - ".gitignore"
148
+ - ".rspec"
149
+ - ".travis.yml"
150
+ - Gemfile
151
+ - LICENSE.txt
152
+ - README.md
153
+ - Rakefile
154
+ - bin/console
155
+ - bin/setup
156
+ - exe/rebot
157
+ - lib/rebot.rb
158
+ - lib/rebot/bot.rb
159
+ - lib/rebot/cli.rb
160
+ - lib/rebot/conversation.rb
161
+ - lib/rebot/message.rb
162
+ - lib/rebot/redis_queue.rb
163
+ - lib/rebot/remote_control.rb
164
+ - lib/rebot/server.rb
165
+ - lib/rebot/version.rb
166
+ - rebot.gemspec
167
+ - template/project/%bot_name%.rb.tt
168
+ - template/project/Gemfile
169
+ - template/project/conversations/hello.rb
170
+ - template/project/server.tt
171
+ homepage: https://github.com/keydunov/rebot
172
+ licenses:
173
+ - MIT
174
+ metadata: {}
175
+ post_install_message:
176
+ rdoc_options: []
177
+ require_paths:
178
+ - lib
179
+ required_ruby_version: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - "~>"
182
+ - !ruby/object:Gem::Version
183
+ version: '2.0'
184
+ required_rubygems_version: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ requirements: []
190
+ rubyforge_project:
191
+ rubygems_version: 2.4.5
192
+ signing_key:
193
+ specification_version: 4
194
+ summary: Framework for building bot applications
195
+ test_files: []