boty 0.0.17.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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"