kybus-bot 0.10.0 → 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kybus
4
+ module Bot
5
+ class ParameterSaver
6
+ def initialize(executor)
7
+ @executor = executor
8
+ end
9
+
10
+ def save_token!(message)
11
+ @executor.execution_context.last_message = message.serialize
12
+ if @executor.execution_context.expecting_command?
13
+ command = @executor.channel_factory.command(message.command)
14
+ set_command(command, message)
15
+ else
16
+ save_param!(message)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def save_param!(message)
23
+ @executor.execution_context.add_param(message.raw_message)
24
+ save_attachment!(message) if message.has_attachment?
25
+ end
26
+
27
+ def save_attachment!(message)
28
+ file = @executor.bot.provider.file_builder(message.attachment)
29
+ @executor.execution_context.add_file(file)
30
+ end
31
+
32
+ def set_command(command, message)
33
+ if @executor.inline_args && !command
34
+ search_command_with_inline_arg(message)
35
+ elsif !@executor.inline_args && !command
36
+ set_default_command
37
+ else
38
+ @executor.execution_context.command = command
39
+ end
40
+ end
41
+
42
+ def search_command_with_inline_arg(message)
43
+ command, values = @executor.channel_factory.command_with_inline_arg(message.raw_message || '')
44
+ if command
45
+ set_command_with_values(command, values)
46
+ else
47
+ set_default_command
48
+ end
49
+ end
50
+
51
+ def set_command_with_values(command, values)
52
+ @executor.execution_context.command = command
53
+ values.each do |value|
54
+ @executor.execution_context.next_param = @executor.execution_context.next_missing_param
55
+ @executor.execution_context.add_param(value)
56
+ end
57
+ end
58
+
59
+ def set_default_command
60
+ @executor.execution_context.command = @executor.channel_factory.default_command
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kybus
4
+ module Bot
5
+ class RegularCommandMatcher
6
+ def initialize(definitions)
7
+ @definitions = definitions
8
+ @matchers = build_matchers
9
+ end
10
+
11
+ def find_command(search)
12
+ @definitions.each do |name, command|
13
+ matcher = @matchers[name.class]
14
+ result = matcher&.call(name, command, search)
15
+ return result if result
16
+ end
17
+ nil
18
+ end
19
+
20
+ private
21
+
22
+ def build_matchers
23
+ {
24
+ String => method(:match_string),
25
+ Class => method(:match_class),
26
+ Regexp => method(:match_regexp)
27
+ }
28
+ end
29
+
30
+ def match_string(name, command, search)
31
+ command if name == search
32
+ end
33
+
34
+ def match_class(name, command, search)
35
+ command if search.is_a?(name)
36
+ end
37
+
38
+ def match_regexp(name, command, search)
39
+ return unless search.is_a?(String) && name.match?(search)
40
+
41
+ storable_command = command.clone
42
+ storable_command.name = search
43
+ storable_command
44
+ end
45
+ end
46
+ end
47
+ end
@@ -4,6 +4,9 @@ require 'forwardable'
4
4
  require_relative 'command/command'
5
5
  require_relative 'command/command_definition'
6
6
  require_relative 'command/execution_context'
7
+ require_relative 'dsl_methods'
8
+ require_relative 'command/command_handler'
9
+ require_relative 'command/parameter_saver'
7
10
 
8
11
  module Kybus
9
12
  module Bot
@@ -11,7 +14,7 @@ module Kybus
11
14
  extend Forwardable
12
15
 
13
16
  include Kybus::Logger
14
- attr_reader :dsl, :bot, :execution_context
17
+ attr_reader :dsl, :bot, :execution_context, :channel_factory, :inline_args, :error
15
18
 
16
19
  def_delegator :execution_context, :save!, :save_execution_context!
17
20
 
@@ -29,79 +32,32 @@ module Kybus
29
32
  @dsl = DSLMethods.new(bot.provider, state, bot)
30
33
  @inline_args = inline_args
31
34
  @precommand_hook = proc {}
35
+ @parameter_saver = ParameterSaver.new(self)
36
+ @command_handler = CommandHandler.new(self)
32
37
  end
33
38
 
34
- # Process a single message, this method can be overwriten to enable
35
- # more complex implementations of commands. It receives a message object.
36
- def process_message(message)
37
- @execution_context = ExecutionContest.new(message.channel_id, @channel_factory)
38
- save_token!(message)
39
- msg = run_command_or_prepare!
40
- save_execution_context!
41
- msg
42
- end
43
-
44
- def save_param!(message)
45
- execution_context.add_param(message.raw_message)
46
- return unless message.has_attachment?
47
-
48
- file = bot.provider.file_builder(message.attachment)
49
- execution_context.add_file(file)
50
- end
51
-
52
- def search_command_with_inline_arg(message)
53
- command, values = @channel_factory.command_with_inline_arg(message.raw_message || '')
54
- if command
55
- execution_context.command = command
56
- values.each do |value|
57
- execution_context.next_param = execution_context.next_missing_param
58
- execution_context.add_param(value)
59
- end
60
- else
61
- execution_context.command = @channel_factory.default_command
62
- end
63
- end
64
-
65
- def save_token!(message)
66
- execution_context.set_last_message(message.serialize)
67
- if execution_context.expecting_command?
68
- command = @channel_factory.command(message.command)
69
- if @inline_args && !command
70
- search_command_with_inline_arg(message)
71
- elsif !@inline_args && !command
72
- execution_context.command = @channel_factory.default_command
73
- else
74
- execution_context.command = command
75
- end
76
- else
77
- save_param!(message)
78
- end
79
- end
80
-
81
- def run_command_or_prepare!
82
- if execution_context.ready?
83
- @dsl.state = execution_context.state
84
- @dsl.instance_eval(&@precommand_hook)
85
- msg = run_command!
86
- execution_context.clear_command
87
- msg
39
+ def precommand_hook(&)
40
+ if block_given?
41
+ @precommand_hook = proc(&)
88
42
  else
89
- param = execution_context.next_missing_param
90
- ask_param(param, execution_context.state.command.params_ask_label(param))
43
+ @precommand_hook
91
44
  end
92
45
  end
93
46
 
94
- def precommand_hook(&)
95
- @precommand_hook = proc(&)
47
+ def process_message(message)
48
+ setup_execution_context(message)
49
+ @parameter_saver.save_token!(message)
50
+ msg = @command_handler.run_command_or_prepare!
51
+ save_execution_context!
52
+ msg
96
53
  end
97
54
 
98
55
  def fallback(error)
99
- catch = @channel_factory.command(error)
56
+ catch_command = @channel_factory.command(error)
100
57
  log_error('Unexpected error', error)
101
- execution_context.command = catch if catch
58
+ execution_context.command = catch_command if catch_command
102
59
  end
103
60
 
104
- # Method for triggering command
105
61
  def run_command!
106
62
  execution_context.call!(@dsl)
107
63
  rescue StandardError => e
@@ -112,28 +68,40 @@ module Kybus
112
68
  end
113
69
 
114
70
  def invoke(command, args)
115
- state.command = command
116
- command.params.zip(args).each do |param, value|
117
- state.store_param(param, value)
118
- end
119
- run_command_or_prepare!
71
+ set_state_command(command, args)
72
+ @command_handler.run_command_or_prepare!
120
73
  end
121
74
 
122
75
  def redirect(command_name, args)
123
76
  command = @channel_factory.command(command_name)
124
- if command.nil? || command.params_size != args.size
125
- raise "Wrong redirect #{command_name}, #{bot.registered_commands}"
126
- end
127
-
77
+ validate_redirect(command, command_name, args)
128
78
  invoke(command, args)
129
79
  end
130
80
 
131
- # Sends a message to get the next parameter from the user
132
81
  def ask_param(param, label = nil)
133
82
  msg = label || "I need you to tell me #{param}"
134
- bot.send_message(msg, last_message.channel_id)
83
+ bot.dsl.send_message(msg, last_message.channel_id)
135
84
  execution_context.next_param = param
136
85
  end
86
+
87
+ private
88
+
89
+ def setup_execution_context(message)
90
+ @execution_context = ExecutionContest.new(message.channel_id, @channel_factory)
91
+ end
92
+
93
+ def set_state_command(command, args)
94
+ state.command = command
95
+ command.params.zip(args).each do |param, value|
96
+ state.store_param(param, value)
97
+ end
98
+ end
99
+
100
+ def validate_redirect(command, command_name, args)
101
+ return unless command.nil? || command.params_size != args.size
102
+
103
+ raise ::Kybus::Bot::Base::BotError, "Wrong redirect #{command_name}, #{bot.registered_commands}"
104
+ end
137
105
  end
138
106
  end
139
107
  end
@@ -6,7 +6,7 @@ module Kybus
6
6
  include Kybus::Logger
7
7
 
8
8
  attr_accessor :state
9
- attr_reader :provider
9
+ attr_reader :provider, :args
10
10
 
11
11
  def initialize(provider, state, bot)
12
12
  @provider = provider
@@ -77,13 +77,17 @@ module Kybus
77
77
  state&.command&.name
78
78
  end
79
79
 
80
- def redirect(*args)
81
- @bot.redirect(*args)
80
+ def redirect(*)
81
+ @bot.redirect(*)
82
82
  end
83
83
 
84
84
  def abort(msg = nil)
85
85
  raise ::Kybus::Bot::Base::AbortError, msg
86
86
  end
87
+
88
+ def fork(command, arguments = {})
89
+ @bot.invoke_job(command, arguments)
90
+ end
87
91
  end
88
92
  end
89
93
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kybus
4
+ module Bot
5
+ class Base
6
+ class BotError < StandardError; end
7
+ class AbortError < BotError; end
8
+
9
+ class EmptyMessageError < BotError
10
+ def initialize
11
+ super('Message is empty')
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kybus
4
+ module Bot
5
+ module Forkers
6
+ class JobNotFound < ::Kybus::Bot::Base::BotError; end
7
+ class JobNotReady < ::Kybus::Bot::Base::BotError; end
8
+
9
+ extend Kybus::DRY::ResourceInjector
10
+ register(:forkers, {})
11
+
12
+ def self.register_forker(name, provider)
13
+ forkers = resource(:forkers)
14
+ forkers[name] = provider
15
+ end
16
+
17
+ def self.forker(name)
18
+ forkers = resource(:forkers)
19
+ forkers[name]
20
+ end
21
+
22
+ def self.from_config(bot, configs)
23
+ provider_name = configs&.dig('provider') || 'thread'
24
+ provider = forker(provider_name)
25
+ provider.new(bot, configs)
26
+ end
27
+
28
+ class Base
29
+ include Kybus::Logger
30
+
31
+ def initialize(bot, configs)
32
+ @configs = configs
33
+ @bot = bot
34
+ @command_definition = CommandDefinition.new
35
+ end
36
+
37
+ def register_command(command, arguments, &)
38
+ @command_definition.register_command(command, arguments, &)
39
+ end
40
+
41
+ def fork(command, arguments, dsl)
42
+ job_definition = @command_definition[command]
43
+ raise JobNotFound if job_definition.nil?
44
+
45
+ raise JobNotReady unless job_definition.ready?(arguments)
46
+
47
+ invoke(command, arguments, job_definition, dsl)
48
+ end
49
+
50
+ def handle_job(command, args)
51
+ job_definition = @command_definition[command]
52
+ @bot.dsl.instance_eval do
53
+ @args = args
54
+ instance_eval(&job_definition.block)
55
+ end
56
+ log_info('Job Executed', command:, args:)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kybus
4
+ module Bot
5
+ module Forkers
6
+ class LambdaSQSForker < Base
7
+ extend Kybus::DRY::ResourceInjector
8
+
9
+ def self.register_queue_client(client)
10
+ register(:sqs, client)
11
+ end
12
+
13
+ def initialize(bot, configs)
14
+ super
15
+ @client = LambdaSQSForker.resource(:sqs)
16
+ @queue = configs['queue']
17
+ @queue_url = @client.get_queue_url(queue_name: @queue).queue_url
18
+ end
19
+
20
+ def invoke(command, args, _job_definition, dsl)
21
+ @client.send_message(queue_url: @queue_url, message_body: make_message(command, args, dsl).to_json)
22
+ end
23
+
24
+ def make_message(command, args, dsl)
25
+ {
26
+ job: command,
27
+ args: args.to_h,
28
+ state: dsl.state.to_h
29
+ }
30
+ end
31
+
32
+ def handle_job(command, args)
33
+ log_info('Got job from SQS', command:)
34
+ super
35
+ end
36
+ end
37
+
38
+ register_forker('sqs', LambdaSQSForker)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kybus
4
+ module Bot
5
+ module Forkers
6
+ class ThreadForker < Base
7
+ def invoke(command, args, _job_definition, _dsl)
8
+ Thread.new do
9
+ log_info('Forking job', command:)
10
+ handle_job(command, args)
11
+ end
12
+ end
13
+ end
14
+
15
+ register_forker('thread', ThreadForker)
16
+ end
17
+ end
18
+ end
@@ -5,55 +5,73 @@ module Kybus
5
5
  module Migrator
6
6
  class << self
7
7
  def run_migrations!(config)
8
- case config['name']
8
+ migrator = migrator_for(config['name'])
9
+ migrator.run_migrations!(config)
10
+ end
11
+
12
+ private
13
+
14
+ def migrator_for(name)
15
+ case name
9
16
  when 'sequel'
10
- run_sequel_migrations(config)
17
+ SequelMigrator
11
18
  when 'dynamoid'
12
- run_dynamoid_migrations(config)
19
+ DynamoidMigrator
13
20
  else
14
- raise "Provider not supported #{config['name']}"
21
+ raise "Provider not supported #{name}"
15
22
  end
16
23
  end
24
+ end
17
25
 
18
- private
26
+ class SequelMigrator
27
+ class << self
28
+ def run_migrations!(config)
29
+ require 'sequel'
30
+ require 'sequel/extensions/migration'
19
31
 
20
- def run_sequel_migrations(config)
21
- require 'sequel'
22
- require 'sequel/extensions/migration'
23
-
24
- conn = Sequel.connect(config['endpoint'])
25
- conn.create_table?(:bot_sessions) do
26
- String :channel_id
27
- String :user
28
- String :params, text: true
29
- String :metadata, text: true
30
- String :files, text: true
31
- String :cmd
32
- String :requested_param
33
- String :last_message, text: true
32
+ conn = Sequel.connect(config['endpoint'])
33
+ conn.create_table?(:bot_sessions) do
34
+ String :channel_id
35
+ String :user
36
+ String :params, text: true
37
+ String :metadata, text: true
38
+ String :files, text: true
39
+ String :cmd
40
+ String :requested_param
41
+ String :last_message, text: true
42
+ end
34
43
  end
35
44
  end
45
+ end
36
46
 
37
- def run_dynamoid_migrations(config)
38
- repository = Kybus::Storage::Datasource::DynamoidRepository.from_config(
39
- 'name' => 'dynamoid',
40
- 'dynamoid_config' => true,
41
- 'access_key' => config['access_key'],
42
- 'secret_key' => config['secret_key'],
43
- 'region' => config['region'],
44
- 'endpoint' => config['endpoint'],
45
- 'namespace' => config['namespace'],
46
- 'table' => 'bot_sessions',
47
- 'primary_key' => 'channel_id',
48
- 'fields' => Base::DYNAMOID_FIELDS,
49
- 'read_capacity' => config['read_capacity'] || 1,
50
- 'write_capacity' => config['write_capacity'] || 1
51
- )
52
-
53
- # Ensure the table is created
54
- return if Dynamoid.adapter.list_tables.include?('bot_sessions')
55
-
56
- repository.model_class.create_table(sync: true)
47
+ class DynamoidMigrator
48
+ class << self
49
+ def run_migrations!(config)
50
+ repository = Kybus::Storage::Datasource::DynamoidRepository.from_config(
51
+ 'name' => 'dynamoid',
52
+ 'dynamoid_config' => true,
53
+ 'access_key' => config['access_key'],
54
+ 'secret_key' => config['secret_key'],
55
+ 'region' => config['region'],
56
+ 'endpoint' => config['endpoint'],
57
+ 'namespace' => config['namespace'],
58
+ 'table' => 'bot_sessions',
59
+ 'primary_key' => 'channel_id',
60
+ 'fields' => Base::DYNAMOID_FIELDS,
61
+ 'read_capacity' => config['read_capacity'] || 1,
62
+ 'write_capacity' => config['write_capacity'] || 1
63
+ )
64
+
65
+ create_table_if_not_exists(repository)
66
+ end
67
+
68
+ private
69
+
70
+ def create_table_if_not_exists(repository)
71
+ return if Dynamoid.adapter.list_tables.include?('bot_sessions')
72
+
73
+ repository.model_class.create_table(sync: true)
74
+ end
57
75
  end
58
76
  end
59
77
  end
@@ -2,21 +2,15 @@
2
2
 
3
3
  module Kybus
4
4
  module Bot
5
- # Base iplementation for messages from distinct providers
5
+ # Base implementation for messages from distinct providers
6
6
  class SerializedMessage < Message
7
7
  MANDATORY_FIELDS = %i[channel_id provider message_id user raw_message].freeze
8
8
 
9
9
  def initialize(data)
10
- data = data.to_h if data.is_a?(SerializedMessage)
11
- raise 'BadSerializedMessage: nil message' if data.nil?
12
-
13
- data = data[:data] if data.is_a?(Hash) && data.key?(:data)
14
-
15
- missing_keys = MANDATORY_FIELDS.reject { |k| data.keys.include?(k) }
16
- raise "BadSerializedMessage: Missing keys `#{missing_keys}', got: #{data}" unless missing_keys.empty?
17
-
18
- @data = data.is_a?(String) ? JSON.parse(data, symbolize_names: true) : data
19
- @data[:replied_message] = SerializedMessage.new(@data[:replied_message]) if @data[:replied_message]
10
+ super()
11
+ @data = parse_data(data)
12
+ validate_data!
13
+ parse_replied_message
20
14
  end
21
15
 
22
16
  def self.from_json(json)
@@ -32,7 +26,7 @@ module Kybus
32
26
  !@data[attachment].nil?
33
27
  end
34
28
 
35
- def method_missing(method, *_args)
29
+ def method_missing(method, *_args) # rubocop:disable Style/MissingRespondToMissing
36
30
  @data[method]
37
31
  end
38
32
 
@@ -40,8 +34,32 @@ module Kybus
40
34
  @data.dup
41
35
  end
42
36
 
43
- def to_json(*args)
44
- @data.to_json(*args)
37
+ def to_json(*)
38
+ @data.to_json(*)
39
+ end
40
+
41
+ private
42
+
43
+ def parse_data(data)
44
+ data = data.to_h if data.is_a?(SerializedMessage)
45
+ raise Base::BotError, 'BadSerializedMessage: nil message' if data.nil?
46
+
47
+ data = data[:data] if data.is_a?(Hash) && data.key?(:data)
48
+ data.is_a?(String) ? JSON.parse(data, symbolize_names: true) : data
49
+ end
50
+
51
+ def validate_data!
52
+ missing_keys = MANDATORY_FIELDS.reject { |k| @data.key?(k) }
53
+ return if missing_keys.empty?
54
+
55
+ raise Base::BotError,
56
+ "BadSerializedMessage: Missing keys `#{missing_keys}', got: #{@data}"
57
+ end
58
+
59
+ def parse_replied_message
60
+ return unless @data[:replied_message]
61
+
62
+ @data[:replied_message] = SerializedMessage.new(@data[:replied_message])
45
63
  end
46
64
  end
47
65
  end
@@ -32,7 +32,9 @@ module Kybus
32
32
  dsl, state = build_context(details_json)
33
33
  dsl.instance_eval(&state.command.block)
34
34
  rescue StandardError => e
35
+ # :nocov:
35
36
  log_error('Error in worker', error: e.class, msg: e.message, trace: e.backtrace)
37
+ # :nocov:
36
38
  end
37
39
  end
38
40
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kybus
4
4
  module Bot
5
- VERSION = '0.10.0'
5
+ VERSION = '0.11.1'
6
6
  end
7
7
  end