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.
- 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"
|