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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +559 -54
- data/bin/bot +10 -13
- data/docs/images/readme-01-screen-integration.png +0 -0
- data/docs/images/readme-02-screen-integration.png +0 -0
- data/docs/images/readme-03-screen-integration.png +0 -0
- data/lib/boty/action.rb +1 -1
- data/lib/boty/bot.rb +46 -63
- data/lib/boty/dsl.rb +52 -0
- data/lib/boty/eventable.rb +41 -0
- data/lib/boty/http.rb +33 -0
- data/lib/boty/locale.rb +17 -4
- data/lib/boty/logger.rb +21 -4
- data/lib/boty/rspec.rb +2 -3
- data/lib/boty/script_loader.rb +1 -1
- data/lib/boty/session.rb +21 -17
- data/lib/boty/slack/chat.rb +2 -2
- data/lib/boty/slack/message.rb +29 -0
- data/lib/boty/slack/users.rb +13 -0
- data/lib/boty/slack.rb +6 -0
- data/lib/boty/version.rb +1 -1
- data/lib/boty.rb +4 -4
- data/spec/boty/bot_spec.rb +105 -174
- data/spec/boty/dsl_spec.rb +125 -0
- data/spec/boty/http_spec.rb +5 -0
- data/spec/boty/logger_spec.rb +33 -0
- data/spec/boty/rspec_spec.rb +1 -1
- data/spec/boty/script_loader_spec.rb +27 -0
- data/spec/boty/session_spec.rb +9 -11
- data/spec/boty/slack/message_spec.rb +34 -0
- data/spec/boty/slack/users_spec.rb +41 -15
- data/spec/happy_path_spec.rb +22 -12
- data/spec/script/i18n_spec.rb +10 -4
- data/spec/script/pug_spec.rb +1 -1
- data/spec/spec_helper.rb +5 -2
- data/spec/support/logger_support.rb +20 -0
- data/spec/support/session_support.rb +2 -2
- data/template/project/bot.tt +4 -13
- data/template/project/script/ping.rb +3 -3
- metadata +14 -5
- data/lib/boty/message.rb +0 -27
- data/lib/boty/script_dsl.rb +0 -80
- 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
|
-
|
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
|
-
|
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
|
17
|
-
|
18
|
-
|
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
|
-
|
16
|
+
logger.debug "saying hello"
|
17
|
+
say I18n.t "template.hello", user_name: user.name
|
21
18
|
end
|
22
19
|
end
|
Binary file
|
Binary file
|
Binary file
|
data/lib/boty/action.rb
CHANGED
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
|
-
|
5
|
+
include Slack
|
5
6
|
|
6
|
-
|
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
|
-
@
|
12
|
+
@listeners ||= []
|
10
13
|
@commands ||= []
|
11
|
-
@
|
12
|
-
ScriptLoader.new(self).load
|
14
|
+
@brain ||= {}
|
13
15
|
on :message, &method(:message_handler)
|
14
|
-
|
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
|
-
@
|
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
|
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
|
-
@
|
59
|
-
|
60
|
-
|
36
|
+
remove_action @listeners, command: command
|
37
|
+
remove_action @commands, command: command
|
38
|
+
end
|
61
39
|
|
62
|
-
|
63
|
-
|
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 { "
|
48
|
+
logger.debug { "Post response: #{post_response}." }
|
72
49
|
end
|
73
50
|
|
74
|
-
def
|
75
|
-
|
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(@
|
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
|
-
|
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
|
91
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
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
|
-
|
12
|
+
(Dir["locale/**/*.yml"] + Dir[default_locales_path]).
|
6
13
|
map { |file| File.expand_path file }
|
7
|
-
|
8
|
-
|
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
|
-
|
21
|
-
|
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
|
-
|
33
|
-
|
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::
|
26
|
+
Boty::DSL.new @_bot
|
28
27
|
}
|
29
28
|
end
|
30
29
|
end
|
data/lib/boty/script_loader.rb
CHANGED
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
|
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 { "
|
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"]
|
37
|
-
|
38
|
-
logger.debug { "
|
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 { "
|
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
|
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 { "
|
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
|
-
|
59
|
+
on_connect ws
|
56
60
|
end
|
57
61
|
end
|
58
62
|
end
|
data/lib/boty/slack/chat.rb
CHANGED
@@ -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
|
data/lib/boty/slack/users.rb
CHANGED
@@ -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
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/
|
34
|
+
require "boty/dsl"
|
37
35
|
require "boty/locale"
|
36
|
+
require "boty/eventable"
|
38
37
|
require "boty/bot"
|
38
|
+
require "boty/http"
|