kybus-bot 0.5.1 → 0.8.1
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/lib/kybus/bot/adapters/debug.rb +68 -10
- data/lib/kybus/bot/adapters/discord.rb +9 -9
- data/lib/kybus/bot/adapters/telegram.rb +25 -98
- data/lib/kybus/bot/adapters/telegram_file.rb +59 -0
- data/lib/kybus/bot/adapters/telegram_message.rb +61 -0
- data/lib/kybus/bot/base.rb +39 -210
- data/lib/kybus/bot/{command_definition.rb → command/command.rb} +18 -22
- data/lib/kybus/bot/command/command_definition.rb +29 -0
- data/lib/kybus/bot/command/command_state.rb +76 -0
- data/lib/kybus/bot/command/command_state_factory.rb +69 -0
- data/lib/kybus/bot/command/execution_context.rb +63 -0
- data/lib/kybus/bot/command_executor.rb +131 -0
- data/lib/kybus/bot/dsl_methods.rb +79 -0
- data/lib/kybus/bot/message.rb +5 -8
- data/lib/kybus/bot/test.rb +18 -6
- data/lib/kybus/bot/version.rb +1 -1
- metadata +14 -5
data/lib/kybus/bot/base.rb
CHANGED
@@ -3,19 +3,19 @@
|
|
3
3
|
require 'kybus/dry/daemon'
|
4
4
|
require 'kybus/bot/adapters/base'
|
5
5
|
require 'kybus/storage'
|
6
|
-
require_relative '
|
6
|
+
require_relative 'command/command_state'
|
7
|
+
require_relative 'dsl_methods'
|
8
|
+
require_relative 'command_executor'
|
9
|
+
require_relative 'command/command_state_factory'
|
7
10
|
|
8
11
|
require 'kybus/logger'
|
12
|
+
require 'forwardable'
|
9
13
|
|
10
14
|
module Kybus
|
11
15
|
module Bot
|
12
16
|
# Base class for bot implementation. It wraps the threads execution, the
|
13
17
|
# provider and the state storage inside an object.
|
14
18
|
class Base
|
15
|
-
include Kybus::Storage::Datasource
|
16
|
-
include Kybus::Logger
|
17
|
-
attr_reader :provider
|
18
|
-
|
19
19
|
class BotError < StandardError; end
|
20
20
|
|
21
21
|
class EmptyMessageError < BotError
|
@@ -24,26 +24,13 @@ module Kybus
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
provider.send_message(channel || current_channel, content)
|
30
|
-
end
|
31
|
-
|
32
|
-
def rescue_from(klass, &block)
|
33
|
-
@commands.register_command(klass, [], block)
|
34
|
-
end
|
27
|
+
extend Forwardable
|
28
|
+
include Kybus::Logger
|
35
29
|
|
36
|
-
|
37
|
-
provider.send_image(channel || current_channel, content)
|
38
|
-
end
|
30
|
+
attr_reader :provider, :executor, :pool_size, :pool, :definitions
|
39
31
|
|
40
|
-
|
41
|
-
|
42
|
-
end
|
43
|
-
|
44
|
-
def send_document(content, channel = nil)
|
45
|
-
provider.send_document(channel || current_channel, content)
|
46
|
-
end
|
32
|
+
def_delegators :executor, :state, :precommand_hook
|
33
|
+
def_delegators :definitions, :registered_commands
|
47
34
|
|
48
35
|
# Configurations needed:
|
49
36
|
# - pool_size: number of threads created in execution
|
@@ -52,216 +39,58 @@ module Kybus
|
|
52
39
|
# - name: The bot name
|
53
40
|
# - repository: Configurations about the state storage
|
54
41
|
def initialize(configs)
|
55
|
-
|
42
|
+
build_pool(configs['pool_size'])
|
56
43
|
@provider = Kybus::Bot::Adapter.from_config(configs['provider'])
|
57
|
-
@commands = Kybus::Bot::CommandDefinition.new
|
58
|
-
register_command('default') do; end
|
59
|
-
|
60
44
|
# TODO: move this to config
|
61
|
-
|
45
|
+
repository = Kybus::Storage::Repository.from_config(
|
62
46
|
nil,
|
63
|
-
configs['state_repository']
|
64
|
-
.merge('primary_key' => 'channel_id',
|
65
|
-
'table' => 'bot_sessions'),
|
47
|
+
configs['state_repository'].merge('primary_key' => 'channel_id', 'table' => 'bot_sessions'),
|
66
48
|
{}
|
67
49
|
)
|
68
|
-
@
|
69
|
-
|
70
|
-
@
|
50
|
+
@definitions = Kybus::Bot::CommandDefinition.new
|
51
|
+
command_factory = CommandStateFactory.new(repository, @definitions)
|
52
|
+
@executor = Kybus::Bot::CommandExecutor.new(self, command_factory, configs['inline_args'])
|
53
|
+
register_command('default') { nil }
|
71
54
|
end
|
72
55
|
|
73
|
-
|
74
|
-
|
75
|
-
@pool = Array.new(@pool_size) do
|
56
|
+
def build_pool(pool_size)
|
57
|
+
@pool = Array.new(pool_size) do
|
76
58
|
# TODO: Create a subclass with the context execution
|
77
|
-
Kybus::DRY::Daemon.new(
|
59
|
+
Kybus::DRY::Daemon.new(pool_size, true) do
|
78
60
|
message = provider.read_message
|
79
|
-
|
80
|
-
process_message(message)
|
61
|
+
executor.process_message(message)
|
81
62
|
end
|
82
63
|
end
|
83
|
-
# TODO: Implement an interface for killing the process
|
84
|
-
@pool.each(&:run)
|
85
|
-
# :nocov: #
|
86
|
-
@pool.each(&:await)
|
87
|
-
# :nocov: #
|
88
|
-
end
|
89
|
-
|
90
|
-
# Process a single message, this method can be overwriten to enable
|
91
|
-
# more complex implementations of commands. It receives a message object.
|
92
|
-
def process_message(message)
|
93
|
-
run_simple_command!(message)
|
94
|
-
end
|
95
|
-
|
96
|
-
# Executes a command with the easiest definition. It runs a state machine:
|
97
|
-
# - If the message is a command, set the status to asking params
|
98
|
-
# - If the message is a param, stores it
|
99
|
-
# - If the command is ready to be executed, trigger it.
|
100
|
-
def run_simple_command!(message)
|
101
|
-
load_state!(message.channel_id)
|
102
|
-
log_debug('loaded state', message: message.to_h, state: @state.to_h)
|
103
|
-
if message.command?
|
104
|
-
self.command = message.raw_message
|
105
|
-
else
|
106
|
-
add_param(message.raw_message)
|
107
|
-
add_file(message.attachment) if message.has_attachment?
|
108
|
-
end
|
109
|
-
if command_ready?
|
110
|
-
run_command!
|
111
|
-
else
|
112
|
-
ask_param(next_missing_param)
|
113
|
-
end
|
114
|
-
save_state!
|
115
|
-
rescue StandardError => e
|
116
|
-
catch = @commands[e.class]
|
117
|
-
raise if catch.nil?
|
118
|
-
|
119
|
-
instance_eval(&catch.block)
|
120
|
-
clear_command
|
121
|
-
end
|
122
|
-
|
123
|
-
# DSL method for adding simple commands
|
124
|
-
def register_command(name, params = [], &block)
|
125
|
-
@commands.register_command(name, params, block)
|
126
|
-
end
|
127
|
-
|
128
|
-
# Method for triggering command
|
129
|
-
def run_command!
|
130
|
-
instance_eval(¤t_command_object.block)
|
131
|
-
clear_command
|
132
|
-
end
|
133
|
-
|
134
|
-
def clear_command
|
135
|
-
@state[:cmd] = nil
|
136
|
-
end
|
137
|
-
|
138
|
-
# Checks if the command is ready to be executed
|
139
|
-
def command_ready?
|
140
|
-
cmd = current_command_object
|
141
|
-
cmd.ready?(current_params)
|
142
|
-
end
|
143
|
-
|
144
|
-
# loads parameters from state
|
145
|
-
def current_params
|
146
|
-
@state[:params] || {}
|
147
|
-
end
|
148
|
-
|
149
|
-
def params
|
150
|
-
current_params
|
151
|
-
end
|
152
|
-
|
153
|
-
def add_file(file)
|
154
|
-
return if @state[:requested_param].nil?
|
155
|
-
|
156
|
-
log_debug('Received new file',
|
157
|
-
param: @state[:requested_param].to_sym,
|
158
|
-
file: file.to_h)
|
159
|
-
|
160
|
-
files[@state[:requested_param].to_sym] = provider.file_builder(file)
|
161
|
-
@state[:params][("_#{@state[:requested_param]}_filename").to_sym] = file.file_name
|
162
|
-
end
|
163
|
-
|
164
|
-
def files
|
165
|
-
@state[:files] ||= {}
|
166
64
|
end
|
167
65
|
|
168
|
-
def
|
169
|
-
|
66
|
+
def dsl
|
67
|
+
@executor.dsl
|
170
68
|
end
|
171
69
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
# Loads command from state
|
181
|
-
def current_command_object
|
182
|
-
command = @state[:cmd]
|
183
|
-
@commands[command] || @commands['default']
|
184
|
-
end
|
185
|
-
|
186
|
-
# returns the current_channel from where the message was sent
|
187
|
-
def current_channel
|
188
|
-
@state[:channel_id]
|
189
|
-
end
|
190
|
-
|
191
|
-
def current_user
|
192
|
-
@last_message.user
|
193
|
-
end
|
194
|
-
|
195
|
-
def is_private?
|
196
|
-
@last_message.is_private?
|
197
|
-
end
|
198
|
-
|
199
|
-
# stores the command into state
|
200
|
-
def command=(cmd)
|
201
|
-
log_debug('Message set as command', command: cmd)
|
202
|
-
|
203
|
-
@state[:cmd] = cmd.split(' ').first
|
204
|
-
@state[:params] = { }
|
205
|
-
@state[:files] = { }
|
206
|
-
end
|
207
|
-
|
208
|
-
# validates which is the following parameter required
|
209
|
-
def next_missing_param
|
210
|
-
current_command_object.next_missing_param(current_params)
|
211
|
-
end
|
212
|
-
|
213
|
-
# Sends a message to get the next parameter from the user
|
214
|
-
def ask_param(param)
|
215
|
-
log_debug('I\'m going to ask the next param', param: param)
|
216
|
-
provider.send_message(current_channel,
|
217
|
-
"I need you to tell me #{param}")
|
218
|
-
@state[:requested_param] = param.to_s
|
219
|
-
end
|
220
|
-
|
221
|
-
# Stores a parameter into the status
|
222
|
-
def add_param(value)
|
223
|
-
return if @state[:requested_param].nil?
|
224
|
-
|
225
|
-
log_debug('Received new param',
|
226
|
-
param: @state[:requested_param].to_sym,
|
227
|
-
value: value)
|
228
|
-
|
229
|
-
@state[:params][@state[:requested_param].to_sym] = value
|
230
|
-
end
|
231
|
-
|
232
|
-
# Loads the state from storage
|
233
|
-
def load_state!(channel)
|
234
|
-
@state = load_state(channel)
|
70
|
+
# Starts the bot execution, this is a blocking call.
|
71
|
+
def run
|
72
|
+
# TODO: Implement an interface for killing the process
|
73
|
+
pool.each(&:run)
|
74
|
+
# :nocov: #
|
75
|
+
pool.each(&:await)
|
76
|
+
# :nocov: #
|
235
77
|
end
|
236
78
|
|
237
|
-
|
238
|
-
|
239
|
-
data = @factory.get(channel)
|
240
|
-
data[:params] = JSON.parse(data[:params] || '{}', symbolize_names: true)
|
241
|
-
data[:files] = JSON.parse(data[:files] || '{}', symbolize_names: true)
|
242
|
-
data
|
243
|
-
rescue Kybus::Storage::Exceptions::ObjectNotFound
|
244
|
-
@factory.create(channel_id: channel, params: {}.to_json)
|
79
|
+
def redirect(command, *params)
|
80
|
+
@executor.invoke(command, params)
|
245
81
|
end
|
246
82
|
|
247
|
-
def
|
83
|
+
def send_message(contents, channel)
|
84
|
+
log_debug('Sending message', contents:, channel:)
|
85
|
+
provider.message_builder(@provider.send_message(contents, channel))
|
248
86
|
end
|
249
87
|
|
250
|
-
|
251
|
-
|
252
|
-
backup = @state.clone
|
253
|
-
%i[params files].each do |param|
|
254
|
-
@state[param] = @state[param].to_json
|
255
|
-
end
|
256
|
-
|
257
|
-
@state.store
|
258
|
-
%i[params files].each do |param|
|
259
|
-
@state[param] = backup[param]
|
260
|
-
end
|
88
|
+
def register_command(klass, params = [], &block)
|
89
|
+
definitions.register_command(klass, params, &block)
|
261
90
|
end
|
262
91
|
|
263
|
-
def
|
264
|
-
|
92
|
+
def rescue_from(klass, &block)
|
93
|
+
definitions.register_command(klass, [], &block)
|
265
94
|
end
|
266
95
|
end
|
267
96
|
end
|
@@ -6,43 +6,39 @@ module Kybus
|
|
6
6
|
# it currently only gets a param list, but it will be extended to a more
|
7
7
|
# complex DSL.
|
8
8
|
class Command
|
9
|
-
|
9
|
+
attr_accessor :name
|
10
|
+
attr_reader :block, :params
|
10
11
|
|
11
12
|
# Receives a list of params as symbols and the lambda with the block.
|
12
|
-
def initialize(
|
13
|
-
@
|
13
|
+
def initialize(name, params_config, &block)
|
14
|
+
@name = name
|
14
15
|
@block = block
|
16
|
+
case params_config
|
17
|
+
when Array
|
18
|
+
@params = params_config
|
19
|
+
@params_config = {}
|
20
|
+
when Hash
|
21
|
+
@params = params_config.keys
|
22
|
+
@params_config = params_config
|
23
|
+
end
|
15
24
|
end
|
16
25
|
|
17
26
|
# Checks if the params object given contains all the needed values
|
18
27
|
def ready?(current_params)
|
19
|
-
|
28
|
+
params.all? { |key| current_params.key?(key) }
|
20
29
|
end
|
21
30
|
|
22
31
|
# Finds the first empty param from the given parameter
|
23
32
|
def next_missing_param(current_params)
|
24
|
-
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
# Wraps a collection of commands.
|
29
|
-
class CommandDefinition
|
30
|
-
def initialize
|
31
|
-
@commands = {}
|
32
|
-
end
|
33
|
-
|
34
|
-
# Stores an operation definition
|
35
|
-
def register_command(name, params, block)
|
36
|
-
@commands[name] = Command.new(params, block)
|
33
|
+
params.find { |key| !current_params.key?(key) }.to_s
|
37
34
|
end
|
38
35
|
|
39
|
-
def
|
40
|
-
@
|
36
|
+
def params_ask_label(param)
|
37
|
+
@params_config[param.to_sym]
|
41
38
|
end
|
42
39
|
|
43
|
-
|
44
|
-
|
45
|
-
@commands[name]
|
40
|
+
def params_size
|
41
|
+
@params.size
|
46
42
|
end
|
47
43
|
end
|
48
44
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kybus
|
4
|
+
module Bot
|
5
|
+
class CommandDefinition
|
6
|
+
def initialize
|
7
|
+
@commands = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
# Stores an operation definition
|
11
|
+
def register_command(name, params, &block)
|
12
|
+
@commands[name] = Command.new(name, params, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def registered_commands
|
16
|
+
@commands.keys
|
17
|
+
end
|
18
|
+
|
19
|
+
def each(&block)
|
20
|
+
@commands.each(&block)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns a command with the name
|
24
|
+
def [](name)
|
25
|
+
@commands[name]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kybus
|
4
|
+
module Bot
|
5
|
+
class CommandState
|
6
|
+
attr_reader :command
|
7
|
+
|
8
|
+
def initialize(data, command)
|
9
|
+
@command = command
|
10
|
+
data[:params] = JSON.parse(data[:params] || '{}', symbolize_names: true)
|
11
|
+
data[:files] = JSON.parse(data[:files] || '{}', symbolize_names: true)
|
12
|
+
@data = data
|
13
|
+
end
|
14
|
+
|
15
|
+
def clear_command
|
16
|
+
@data[:cmd] = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def ready?
|
20
|
+
command&.ready?(params)
|
21
|
+
end
|
22
|
+
|
23
|
+
# validates which is the following parameter required
|
24
|
+
def next_missing_param
|
25
|
+
command.next_missing_param(params)
|
26
|
+
end
|
27
|
+
|
28
|
+
def command=(cmd)
|
29
|
+
@command = cmd
|
30
|
+
@data[:cmd] = cmd.name
|
31
|
+
@data[:params] = {}
|
32
|
+
@data[:files] = {}
|
33
|
+
end
|
34
|
+
|
35
|
+
def params
|
36
|
+
@data[:params] || {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def channel_id
|
40
|
+
@data[:channel_id]
|
41
|
+
end
|
42
|
+
|
43
|
+
def files
|
44
|
+
@data[:files] || {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def save_file(identifier, file)
|
48
|
+
files[identifier] = file
|
49
|
+
store_param("_#{@data[:requested_param]}_filename".to_sym, file.file_name)
|
50
|
+
end
|
51
|
+
|
52
|
+
def requested_param=(param)
|
53
|
+
@data[:requested_param] = param
|
54
|
+
end
|
55
|
+
|
56
|
+
def requested_param
|
57
|
+
@data[:requested_param]
|
58
|
+
end
|
59
|
+
|
60
|
+
def store_param(param, value)
|
61
|
+
@data[:params][param] = value
|
62
|
+
end
|
63
|
+
|
64
|
+
def save!
|
65
|
+
backup = @data.clone
|
66
|
+
%i[params files].each do |param|
|
67
|
+
@data[param] = @data[param].to_json
|
68
|
+
end
|
69
|
+
@data.store
|
70
|
+
%i[params files].each do |param|
|
71
|
+
@data[param] = backup[param]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kybus
|
4
|
+
module Bot
|
5
|
+
class CommandStateFactory
|
6
|
+
include Kybus::Storage::Datasource
|
7
|
+
attr_reader :factory
|
8
|
+
|
9
|
+
def initialize(repository, definitions)
|
10
|
+
factory = Kybus::Storage::Factory.new(EmptyModel)
|
11
|
+
factory.register(:default, :json)
|
12
|
+
factory.register(:json, repository)
|
13
|
+
@factory = factory
|
14
|
+
@definitions = definitions
|
15
|
+
end
|
16
|
+
|
17
|
+
def command(search)
|
18
|
+
@definitions.each do |name, command|
|
19
|
+
case name
|
20
|
+
when String
|
21
|
+
return command if name == search
|
22
|
+
when Class
|
23
|
+
return command if search.is_a?(name)
|
24
|
+
when Regexp
|
25
|
+
if search.is_a?(String) && name.match?(search)
|
26
|
+
storable_command = command.clone
|
27
|
+
storable_command.name = search
|
28
|
+
return storable_command
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def default_command
|
36
|
+
@definitions['default']
|
37
|
+
end
|
38
|
+
|
39
|
+
def command_or_default(name)
|
40
|
+
command(name) || default_command
|
41
|
+
end
|
42
|
+
|
43
|
+
def command_with_inline_arg(name_with_arg)
|
44
|
+
@definitions.each do |name, command|
|
45
|
+
case name
|
46
|
+
when Class
|
47
|
+
return [command, []] if name_with_arg.is_a?(name)
|
48
|
+
when String
|
49
|
+
return [command, name_with_arg.gsub(name, '').split('__')] if name_with_arg.start_with?(name)
|
50
|
+
when Regexp
|
51
|
+
next unless name_with_arg.match?(name)
|
52
|
+
|
53
|
+
storable_command = command.dup
|
54
|
+
storable_command.name = name_with_arg
|
55
|
+
return [storable_command, [name_with_arg]]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def load_state(channel)
|
62
|
+
data = factory.get(channel.to_s)
|
63
|
+
CommandState.new(data, command(data[:cmd]))
|
64
|
+
rescue Kybus::Storage::Exceptions::ObjectNotFound
|
65
|
+
CommandState.new(factory.create(channel_id: channel.to_s, params: '{}'), nil)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kybus
|
4
|
+
module Bot
|
5
|
+
class ExecutionContest
|
6
|
+
include Kybus::Logger
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
attr_reader :state
|
10
|
+
|
11
|
+
def_delegator :state, :requested_param=, :next_param=
|
12
|
+
def_delegators :state, :clear_command, :save!, :ready?, :next_missing_param
|
13
|
+
|
14
|
+
def block
|
15
|
+
state.command.block
|
16
|
+
end
|
17
|
+
|
18
|
+
def call!(context)
|
19
|
+
context.state = state
|
20
|
+
statement = context.instance_eval(&block)
|
21
|
+
clear_command
|
22
|
+
statement
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(channel_id, channel_factory)
|
26
|
+
@channel_factory = channel_factory
|
27
|
+
load_state!(channel_id)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Stores a parameter into the status
|
31
|
+
def add_param(value)
|
32
|
+
param = state.requested_param
|
33
|
+
return unless param
|
34
|
+
|
35
|
+
log_debug('Received new param', param:, value:)
|
36
|
+
state.store_param(param.to_sym, value)
|
37
|
+
end
|
38
|
+
|
39
|
+
def expecting_command?
|
40
|
+
state.command.nil?
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_file(file)
|
44
|
+
param = state.requested_param
|
45
|
+
return unless param
|
46
|
+
|
47
|
+
log_debug('Received new file', param:, file: file.to_h)
|
48
|
+
state.save_file(param.to_sym, file)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Loads the state from storage
|
52
|
+
def load_state!(channel)
|
53
|
+
@state = @channel_factory.load_state(channel)
|
54
|
+
end
|
55
|
+
|
56
|
+
# stores the command into state
|
57
|
+
def command=(cmd)
|
58
|
+
log_debug('Message set as command', command: cmd)
|
59
|
+
state.command = cmd
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|