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.
@@ -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 'command_definition'
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
- def send_message(content, channel = nil)
28
- raise(EmptyMessageError) unless content
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
- def send_image(content, channel = nil)
37
- provider.send_image(channel || current_channel, content)
38
- end
30
+ attr_reader :provider, :executor, :pool_size, :pool, :definitions
39
31
 
40
- def send_audio(content, channel = nil)
41
- provider.send_audio(channel || current_channel, content)
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
- @pool_size = configs['pool_size']
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
- @repository = Kybus::Storage::Repository.from_config(
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
- @factory = Kybus::Storage::Factory.new(EmptyModel)
69
- @factory.register(:default, :json)
70
- @factory.register(:json, @repository)
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
- # Starts the bot execution, this is a blocking call.
74
- def run
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(@pool_size, true) do
59
+ Kybus::DRY::Daemon.new(pool_size, true) do
78
60
  message = provider.read_message
79
- @last_message = message
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(&current_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 file(name)
169
- (file = files[name]) && provider.file_builder(file)
66
+ def dsl
67
+ @executor.dsl
170
68
  end
171
69
 
172
- def mention(name)
173
- provider.mention(name)
174
- end
175
-
176
- def registered_commands
177
- @commands.registered_commands
178
- end
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
- # Private implementation for load message
238
- def load_state(channel)
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 parse_state!
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
- # Saves the state into storage
251
- def save_state!
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 session
264
- @repository
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
- attr_reader :block
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(params, block)
13
- @params = params
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
- @params.all? { |key| current_params.key?(key) }
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
- @params.find { |key| !current_params.key?(key) }
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 registered_commands
40
- @commands.keys
36
+ def params_ask_label(param)
37
+ @params_config[param.to_sym]
41
38
  end
42
39
 
43
- # Returns a command with the name
44
- def [](name)
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