boty 0.0.17.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +559 -54
  4. data/bin/bot +10 -13
  5. data/docs/images/readme-01-screen-integration.png +0 -0
  6. data/docs/images/readme-02-screen-integration.png +0 -0
  7. data/docs/images/readme-03-screen-integration.png +0 -0
  8. data/lib/boty/action.rb +1 -1
  9. data/lib/boty/bot.rb +46 -63
  10. data/lib/boty/dsl.rb +52 -0
  11. data/lib/boty/eventable.rb +41 -0
  12. data/lib/boty/http.rb +33 -0
  13. data/lib/boty/locale.rb +17 -4
  14. data/lib/boty/logger.rb +21 -4
  15. data/lib/boty/rspec.rb +2 -3
  16. data/lib/boty/script_loader.rb +1 -1
  17. data/lib/boty/session.rb +21 -17
  18. data/lib/boty/slack/chat.rb +2 -2
  19. data/lib/boty/slack/message.rb +29 -0
  20. data/lib/boty/slack/users.rb +13 -0
  21. data/lib/boty/slack.rb +6 -0
  22. data/lib/boty/version.rb +1 -1
  23. data/lib/boty.rb +4 -4
  24. data/spec/boty/bot_spec.rb +105 -174
  25. data/spec/boty/dsl_spec.rb +125 -0
  26. data/spec/boty/http_spec.rb +5 -0
  27. data/spec/boty/logger_spec.rb +33 -0
  28. data/spec/boty/rspec_spec.rb +1 -1
  29. data/spec/boty/script_loader_spec.rb +27 -0
  30. data/spec/boty/session_spec.rb +9 -11
  31. data/spec/boty/slack/message_spec.rb +34 -0
  32. data/spec/boty/slack/users_spec.rb +41 -15
  33. data/spec/happy_path_spec.rb +22 -12
  34. data/spec/script/i18n_spec.rb +10 -4
  35. data/spec/script/pug_spec.rb +1 -1
  36. data/spec/spec_helper.rb +5 -2
  37. data/spec/support/logger_support.rb +20 -0
  38. data/spec/support/session_support.rb +2 -2
  39. data/template/project/bot.tt +4 -13
  40. data/template/project/script/ping.rb +3 -3
  41. metadata +14 -5
  42. data/lib/boty/message.rb +0 -27
  43. data/lib/boty/script_dsl.rb +0 -80
  44. data/spec/boty/message_spec.rb +0 -32
data/bin/bot CHANGED
@@ -1,22 +1,19 @@
1
1
  #!/usr/bin/env ruby
2
2
  require "./lib/boty"
3
3
 
4
- def set_locale(locale)
5
- begin
6
- Boty.locale = locale || :en
7
- rescue I18n::InvalidLocale
8
- Boty.locale = :en
9
- end
10
- Boty::Locale.reload
11
- end
4
+ Boty.locale = ARGV.pop || :en
12
5
 
13
- set_locale ARGV.pop
6
+ Boty::Logger.adapter = Boty::Logger::Multi.new([
7
+ Logger.new(STDOUT),
8
+ Logger.new("log/output.log", "daily")
9
+ ])
14
10
 
15
11
  session = Boty::Session.new
16
- session.start do |bot|
17
- bot.desc bot.name, I18n.t("template.presence", bot_name: bot.name)
18
- bot.hear(/#{bot.name}/i) do
12
+ session.start do
13
+ desc name, I18n.t("template.presence", bot_name: name)
14
+ hear(/#{name}/i) do
19
15
  next if message_from_me?
20
- say I18n.t "template.hello", user_name: message.user.name
16
+ logger.debug "saying hello"
17
+ say I18n.t "template.hello", user_name: user.name
21
18
  end
22
19
  end
data/lib/boty/action.rb CHANGED
@@ -39,7 +39,7 @@ module Boty
39
39
  end
40
40
 
41
41
  def execute(bot, message)
42
- dsl = ScriptDSL.new(bot)
42
+ dsl = DSL.new(bot)
43
43
  if match = message.match!(regex)
44
44
  matches = Array(match)
45
45
  matches.shift
data/lib/boty/bot.rb CHANGED
@@ -1,109 +1,88 @@
1
1
  module Boty
2
2
  class Bot
3
+ include Boty::Eventable
3
4
  include Boty::Logger
4
- attr_reader :id, :name, :trigger_message
5
+ include Slack
5
6
 
6
- def initialize(bot_info, session)
7
+ attr_reader :id, :name, :trigger_message, :brain
8
+
9
+ def initialize(bot_info)
7
10
  Locale.reload
8
11
  @raw_info, @id, @name = bot_info, bot_info["id"], bot_info["name"]
9
- @handlers ||= []
12
+ @listeners ||= []
10
13
  @commands ||= []
11
- @events = {}
12
- ScriptLoader.new(self).load
14
+ @brain ||= {}
13
15
  on :message, &method(:message_handler)
14
- end
15
-
16
- def event(data)
17
- return unless type = event_type(data)
18
-
19
- @events[type].each do |action|
20
- action.call data
21
- end
22
- end
23
-
24
- def on(event_type, &block)
25
- @events[event_type.to_sym] ||= []
26
- @events[event_type.to_sym] << block
27
- end
28
-
29
- def off(event_type, &block)
30
- if block_given?
31
- @events[event_type].delete_if { |b| b == block }
32
- else
33
- @events[event_type] = []
34
- end
16
+ ScriptLoader.new(self).load
35
17
  end
36
18
 
37
19
  def match(regex, &block)
38
- @handlers << create_action(regex, &block)
39
- end
40
-
41
- def no_match(regex, &block)
42
- @handlers.delete_if do |action|
43
- action.is_this? regex, block
44
- end
20
+ @listeners << create_action(regex, &block)
45
21
  end
46
22
 
47
23
  def respond(regex, &block)
48
24
  @commands << create_action(regex, &block)
49
25
  end
50
26
 
27
+ def no_match(regex, &block)
28
+ remove_action @listeners, regex, block
29
+ end
30
+
51
31
  def no_respond(regex, &block)
52
- @commands.delete_if do |action|
53
- action.is_this? regex, block
54
- end
32
+ remove_action @commands, regex, block
55
33
  end
56
34
 
57
35
  def no(command)
58
- @handlers.delete_if do |action|
59
- action.desc.command == command
60
- end
36
+ remove_action @listeners, command: command
37
+ remove_action @commands, command: command
38
+ end
61
39
 
62
- @commands.delete_if do |action|
63
- action.desc.command == command
64
- end
40
+ def desc(command, description = nil)
41
+ @current_desc = { command: command, description: description }
65
42
  end
66
43
 
67
44
  def say(message, api_parameters = {})
68
- channel = (@trigger_message && @trigger_message.channel) || "general"
45
+ channel = (@trigger_message && @trigger_message.channel) || "#general"
69
46
  options = { channel: channel }.merge api_parameters
70
47
  post_response = Slack.chat.post_message message, options
71
- logger.debug { "post response: #{post_response}" }
48
+ logger.debug { "Post response: #{post_response}." }
72
49
  end
73
50
 
74
- def desc(command, description = nil)
75
- @current_desc = { command: command, description: description }
51
+ def im(text, destiny: nil, to: nil, user_id: nil)
52
+ if destiny = User(user_id) || user_by_name(destiny, to)
53
+ logger.debug { "Sending #{text} to #{destiny.name}." }
54
+ Slack.chat.post_im destiny.id, text
55
+ else
56
+ logger.debug { "User not found, refusing to send im." }
57
+ end
76
58
  end
77
59
 
60
+ # TODO: return an Action object instead of a hash
78
61
  def know_how
79
- descriptions = (Array(@commands) + Array(@handlers)).compact.map(&:desc)
62
+ descriptions = (Array(@commands) + Array(@listeners)).compact.map(&:desc)
80
63
  descriptions.inject({}) { |hsh, desc|
81
64
  hsh.merge!(desc.command => desc.description)
82
65
  }
83
66
  end
84
67
 
85
- def im(message)
86
- logger.debug { "sending im messge to #{@trigger_message.user.name}" }
87
- Slack.chat.post_im @trigger_message.user.id, message
88
- end
68
+ private
89
69
 
90
- def brain
91
- @brain ||= {}
70
+ def remove_action(collection, regex = nil, block = nil, command: nil)
71
+ collection.delete_if do |action|
72
+ action.desc.command == command || action.is_this?(regex, block)
73
+ end
92
74
  end
93
75
 
94
- private
95
-
96
- def event_type(data)
97
- type = data["type"].to_sym
98
- logger.debug { "bot specifc event[#{type}] arrived: #{data}" }
99
- unless @events[type]
100
- logger.debug "no action binded to #{type}"
101
- return
76
+ def user_by_name(destiny, to)
77
+ if _to = to || destiny
78
+ Slack.users.by_name _to
79
+ else
80
+ @trigger_message.user
102
81
  end
103
- type
104
82
  end
105
83
 
106
84
  def create_action(regex, &block)
85
+ regex = Regexp.new(regex) if regex.is_a? String
107
86
  Boty::Action.new(regex, @current_desc, &block).tap {
108
87
  @current_desc = nil
109
88
  }
@@ -119,7 +98,11 @@ module Boty
119
98
  end
120
99
 
121
100
  def message_handler(data)
122
- actions = has_valid_mention?(data) ? @commands : @handlers
101
+ unless data["text"]
102
+ return logger.debug "Non text message, just ignoring."
103
+ end
104
+
105
+ actions = has_valid_mention?(data) ? @commands : @listeners
123
106
  @trigger_message = Message.new data
124
107
  begin
125
108
  Array(actions).each do |action|
data/lib/boty/dsl.rb ADDED
@@ -0,0 +1,52 @@
1
+ require "forwardable"
2
+
3
+ module Boty
4
+ class DSL
5
+ include Boty::Logger
6
+
7
+ INSTANCES = {}
8
+ private_constant :INSTANCES
9
+ class << self
10
+ alias original_constructor new
11
+ end
12
+
13
+ attr_accessor :bot
14
+
15
+ extend Forwardable
16
+ def_delegators :bot, :desc, :respond, :name, :brain, :know_how, :im, :say,
17
+ :match
18
+ def_delegators :message, :user, :channel
19
+
20
+ def self.new(bot)
21
+ INSTANCES[bot] ||= original_constructor bot
22
+ end
23
+
24
+ def initialize(bot)
25
+ @bot = bot
26
+ end
27
+
28
+ def message
29
+ @bot.trigger_message
30
+ end
31
+
32
+ def message_from_me?
33
+ message.from? @bot
34
+ end
35
+
36
+ def hear(*args, &block)
37
+ match(*args, &block)
38
+ end
39
+
40
+ def command(*args, &block)
41
+ respond(*args, &block)
42
+ end
43
+
44
+ def match_im(*args, &block)
45
+ command(*args, &block)
46
+ end
47
+
48
+ def http
49
+ @http ||= Boty::HTTP.new
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,41 @@
1
+ module Boty
2
+ module Eventable
3
+ include Boty::Logger
4
+ def events
5
+ @events ||= {}
6
+ end
7
+
8
+ def event(data)
9
+ return unless type = event_type(data)
10
+
11
+ events[type].each do |action|
12
+ action.call data
13
+ end
14
+ end
15
+
16
+ def on(event_type, &block)
17
+ events[event_type.to_sym] ||= []
18
+ events[event_type.to_sym] << block
19
+ end
20
+
21
+ def off(event_type, &block)
22
+ if block_given?
23
+ events[event_type].delete_if { |b| b == block }
24
+ else
25
+ events[event_type] = []
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def event_type(data)
32
+ type = data["type"].to_sym
33
+ logger.debug { "event[#{type}] arrived: #{data}" }
34
+ unless events[type]
35
+ logger.debug "no action binded to #{type}"
36
+ return
37
+ end
38
+ type
39
+ end
40
+ end
41
+ end
data/lib/boty/http.rb ADDED
@@ -0,0 +1,33 @@
1
+ module Boty
2
+ class HTTP
3
+ attr_reader :response # available in case of non 2XX responses...
4
+
5
+ [:get, :post, :put, :delete, :head, :patch, :options].each do |verb|
6
+ define_method verb do |url, params = {}|
7
+ @response = connection verb, url, params
8
+ handle_response
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def connection(verb, url, params)
15
+ uri = URI url
16
+ Faraday.new(url: "#{uri.scheme}://#{uri.host}") { |builder|
17
+ builder.adapter(*Faraday.default_adapter)
18
+ }.public_send(verb) { |req|
19
+ req.url uri.path, params
20
+ }
21
+ end
22
+
23
+ def handle_response
24
+ # TODO: use a response parser accordingly to the
25
+ body = response.body
26
+ if /application\/json/.match response.headers["Content-Type"]
27
+ JSON.parse body
28
+ else
29
+ body
30
+ end
31
+ end
32
+ end
33
+ end
data/lib/boty/locale.rb CHANGED
@@ -1,11 +1,24 @@
1
1
  module Boty
2
2
  class Locale
3
- def self.reload
3
+ def self.reload(locale = nil)
4
+ _locale = new
5
+ I18n.load_path = _locale.locales_paths.uniq
6
+ I18n.available_locales = I18n::Backend::Simple.new.available_locales
7
+ _locale.set_locale locale if locale
8
+ end
9
+
10
+ def locales_paths
4
11
  default_locales_path = File.expand_path("../../../locale/**/*.yml", __FILE__)
5
- locales = (Dir["locale/**/*.yml"] + Dir[default_locales_path]).
12
+ (Dir["locale/**/*.yml"] + Dir[default_locales_path]).
6
13
  map { |file| File.expand_path file }
7
- I18n.load_path = locales.uniq
8
- I18n.available_locales = I18n::Backend::Simple.new.available_locales
14
+ end
15
+
16
+ def set_locale(locale)
17
+ begin
18
+ I18n.locale = locale
19
+ rescue I18n::InvalidLocale
20
+ I18n.locale = :en
21
+ end
9
22
  end
10
23
  end
11
24
  end
data/lib/boty/logger.rb CHANGED
@@ -17,8 +17,22 @@ module Boty
17
17
  Logger.adapter
18
18
  end
19
19
 
20
- def log_level(level)
21
- logger.level = level
20
+ class Multi < ::Logger
21
+ def initialize(adapters)
22
+ @adapters = adapters
23
+ end
24
+
25
+ def level=(level)
26
+ @adapters.each do |adapter|
27
+ adapter.level = level
28
+ end
29
+ end
30
+
31
+ def add(*args, &block)
32
+ @adapters.each do |adapter|
33
+ adapter.add(*args, &block)
34
+ end
35
+ end
22
36
  end
23
37
 
24
38
  class Memory < ::Logger
@@ -29,8 +43,11 @@ module Boty
29
43
  end
30
44
 
31
45
  def add(*args, &block)
32
- _, _, message = args
33
- @logs << message
46
+ @logs << if block_given?
47
+ block.call
48
+ else
49
+ args[2]
50
+ end
34
51
  end
35
52
  end
36
53
 
data/lib/boty/rspec.rb CHANGED
@@ -9,8 +9,7 @@ module Boty
9
9
 
10
10
  before do
11
11
  @_bot = Boty::Bot.new(
12
- {"id" => "1234", "name" => "bot"},
13
- Boty::Session.new
12
+ {"id" => "1234", "name" => "bot"}
14
13
  )
15
14
 
16
15
  class << Boty::Slack.chat
@@ -24,7 +23,7 @@ module Boty
24
23
  end
25
24
 
26
25
  let(:bot) {
27
- Boty::ScriptDSL.new @_bot
26
+ Boty::DSL.new @_bot
28
27
  }
29
28
  end
30
29
  end
@@ -1,7 +1,7 @@
1
1
  module Boty
2
2
  class ScriptLoader
3
3
  def initialize(bot)
4
- @dsl = ScriptDSL.new bot
4
+ @dsl = DSL.new bot
5
5
  end
6
6
 
7
7
  def load
data/lib/boty/session.rb CHANGED
@@ -10,49 +10,53 @@ module Boty
10
10
  EM.run do
11
11
  login
12
12
  @bot = initialize_bot(&block)
13
- stablish_connection do |ws|
14
- ws.on :message do |event|
15
- begin
16
- on_message event, @bot
17
- rescue StandardError => e
18
- logger.error "Message #{event} could not be processed. #{e.message}"
19
- end
20
- end
21
- end
13
+ stablish_connection
22
14
  end
23
15
  self
24
16
  end
25
17
 
26
18
  private
27
19
 
20
+ def on_connect(ws)
21
+ ws.on :message do |event|
22
+ begin
23
+ on_message event, @bot
24
+ rescue StandardError => e
25
+ logger.error "Message #{event} could not be processed. #{e.message}"
26
+ logger.debug e.backtrace.reduce("") { |msg, line| msg << "#{line}\n" }
27
+ end
28
+ end
29
+ end
30
+
28
31
  def login
29
- logger.debug { "logging in against slack right now" }
32
+ logger.debug { "Logging in against slack right now" }
30
33
  @slack_info = Slack.rtm.start
31
34
  logger.debug { "yep! logged in!" }
32
35
  @session_url = @slack_info["url"]
33
36
  end
34
37
 
35
38
  def initialize_bot(&block)
36
- Bot.new(@slack_info["self"], self).tap { |bot|
37
- block.call ScriptDSL.new(bot) if block_given?
38
- logger.debug { "bot is configured and ready to go!" }
39
+ Bot.new(@slack_info["self"]).tap { |bot|
40
+ DSL.new(bot).instance_eval(&block) if block_given?
41
+ logger.debug { "Bot is configured and ready to go!" }
39
42
  }
40
43
  end
41
44
 
42
45
  def on_message(event, bot)
43
- logger.debug { "message arrived #{event.data}" }
46
+ logger.debug { "Message arrived. Creating bot event" }
44
47
  bot.event JSON.parse(event.data)
45
48
  end
46
49
 
47
50
  def on_close
48
- logger.debug { "bye byeb." }
51
+ logger.debug { "bye bye" }
52
+ #todo try to reconnect (stablish_connection) unless is an em interrupt
49
53
  end
50
54
 
51
55
  def stablish_connection
52
- logger.debug { "starting to listen on #{@session_url}" }
56
+ logger.debug { "Starting to listen on #{@session_url}" }
53
57
  ws = Faye::WebSocket::Client.new @session_url
54
58
  ws.on :close do on_close end
55
- yield ws if block_given?
59
+ on_connect ws
56
60
  end
57
61
  end
58
62
  end
@@ -15,8 +15,8 @@ module Boty
15
15
  URL.get parameterize(defaults.merge parameters)
16
16
  end
17
17
 
18
- def post_im(user, message)
19
- channel = im.open user
18
+ def post_im(user_id, message)
19
+ channel = im.open user_id
20
20
  post_message message, channel: channel.id
21
21
  end
22
22
  end
@@ -0,0 +1,29 @@
1
+ module Boty
2
+ module Slack
3
+ class Message
4
+ attr_accessor :text, :user, :channel, :ts, :team
5
+ attr_reader :match
6
+
7
+ def initialize(data, match: nil)
8
+ @text = data["text"]
9
+ @user = Slack.users.info data["user"]
10
+ @channel = data["channel"]
11
+ @ts = data["ts"]
12
+ @team = data["team"]
13
+ @match = match
14
+ end
15
+
16
+ def match!(regex)
17
+ @match = regex.match @text
18
+ end
19
+
20
+ def from?(author)
21
+ if author.respond_to? :id
22
+ @user.id == author.id
23
+ else
24
+ @user.id == author
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -10,6 +10,19 @@ module Boty
10
10
  info = URL.get url
11
11
  Slack::User.new info["user"]
12
12
  end
13
+
14
+ def list(parameters = {})
15
+ # TODO: this call should be cached.
16
+ url = parameterize parameters, path: ".list"
17
+ users = URL.get url
18
+ users["members"].map { |info|
19
+ Slack::User.new info
20
+ }
21
+ end
22
+
23
+ def by_name(name)
24
+ list.select { |user| user.name == name }.first
25
+ end
13
26
  end
14
27
  end
15
28
  end
data/lib/boty/slack.rb CHANGED
@@ -5,6 +5,7 @@ require "boty/slack/user"
5
5
  require "boty/slack/users"
6
6
  require "boty/slack/chat"
7
7
  require "boty/slack/rtm"
8
+ require "boty/slack/message"
8
9
 
9
10
  module Boty
10
11
  module Slack
@@ -13,5 +14,10 @@ module Boty
13
14
  def rtm; @rtm ||= RTM.new end
14
15
  def users; @users ||= Users.new end
15
16
  end
17
+
18
+ def User(user_id)
19
+ return unless user_id
20
+ Boty::Slack::User.new "id" => user_id
21
+ end
16
22
  end
17
23
  end
data/lib/boty/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Boty
2
- VERSION = "0.0.17.1"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/boty.rb CHANGED
@@ -17,8 +17,7 @@ $:.unshift File.expand_path("../../lib", __FILE__)
17
17
 
18
18
  module Boty
19
19
  def self.locale=(lang)
20
- Locale.reload
21
- I18n.locale = lang
20
+ Locale.reload lang
22
21
  end
23
22
 
24
23
  def self.locale
@@ -30,9 +29,10 @@ require "boty/version"
30
29
  require "boty/logger"
31
30
  require "boty/slack"
32
31
  require "boty/session"
33
- require "boty/message"
34
32
  require "boty/action"
35
33
  require "boty/script_loader"
36
- require "boty/script_dsl"
34
+ require "boty/dsl"
37
35
  require "boty/locale"
36
+ require "boty/eventable"
38
37
  require "boty/bot"
38
+ require "boty/http"