kybus-bot 0.5.1 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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