kybus-bot 0.11.3 → 0.11.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5621cdb73308df7a7580e484959a825170aa1e9028c3ad55950c86a4ca6f5a2c
4
- data.tar.gz: caec550063dfc742d65687f8c8dda7de87cb91acb4409c9b81ac823e52843f14
3
+ metadata.gz: 688379f443d3679dba44474a96c6068755df45f81c8972cb3e715a0561722fd5
4
+ data.tar.gz: b5a21e91866dc4cd1dfd79f73b4556acf11a436d0ec4ae8127a67661b673bbc9
5
5
  SHA512:
6
- metadata.gz: 98558bf269ec8e876e76eeca9c0ecbc308424db2dd14697f1ef8813e42e917fb1603875a6c3cf541ba5707ec0c2c3e6d86fbd972c13c95e8a0b8f39d5e664a65
7
- data.tar.gz: bf47207fae0a45f1608470f480175a9ae4084a96080197bece81c2f793d87c4a4fc5b41bd743c838a6359e64fc743ef94d2014b81b9d4c766e0c29ecc7d73451
6
+ metadata.gz: 3b19d61d37bd6a50b214747c258880719e094ab538d77d86a6a1f7a85327c8d046826e7c2213857e270b93419f76806f00897fe428c5baf16b8d996fe28a66b7
7
+ data.tar.gz: '058300edd89b02221df1e293a320d5be930f2859420e16d7a36eef21a5fceda06f919a360594190c12660ea79aa837db214622a2b1a038e569c502c521c1eddf'
@@ -2,11 +2,11 @@
2
2
 
3
3
  module Kybus
4
4
  module Bot
5
- # Implements a factory singleton for building bot adapters
5
+ # Factory for building bot adapters from config.
6
6
  module Adapter
7
7
  extend Kybus::DRY::ResourceInjector
8
8
 
9
- # builds the abstract adapter
9
+ # Builds the adapter instance for the given config.
10
10
  def self.from_config(configs)
11
11
  require_relative configs['name']
12
12
  resource(configs['name']).new(configs)
@@ -8,7 +8,7 @@ module Kybus
8
8
  # :nodoc: #
9
9
  module Adapter
10
10
  # :nodoc: #
11
- # Wraps a debugging message inside a class.
11
+ # Debug message for test and development adapters.
12
12
  class DebugMessage < Kybus::Bot::Message
13
13
  # It receives a string with the raw text and the id of the channel
14
14
  attr_accessor :replied_message
@@ -80,7 +80,7 @@ module Kybus
80
80
  end
81
81
  end
82
82
 
83
- # This class simulates a message chat with a user.
83
+ # Simulates a message channel for the debug adapter.
84
84
  class Channel
85
85
  # It is build from
86
86
  # an array of raw messages, the name of the channel and the config
@@ -128,8 +128,9 @@ module Kybus
128
128
  end
129
129
 
130
130
  ##
131
- # This adapter is intended to be used on unit tests and development.
131
+ # Debug adapter for tests and local development.
132
132
  class Debug
133
+ include Kybus::Logger
133
134
  # Exception for stoping the loop of messages
134
135
  class NoMoreMessageException < Kybus::Exceptions::KybusError
135
136
  def initialize
@@ -171,26 +172,36 @@ module Kybus
171
172
  # interface for sending messages
172
173
  def send_message(contents, channel_name, attachment = nil)
173
174
  channel(channel_name).answer(contents, attachment)
175
+ rescue StandardError => e
176
+ log_error('Debug send_message failed', error: e.class, msg: e.message)
174
177
  end
175
178
 
176
179
  # interface for sending video
177
180
  def send_video(channel_name, video_url, _caption = nil)
178
181
  channel(channel_name).answer("VIDEO: #{video_url}")
182
+ rescue StandardError => e
183
+ log_error('Debug send_video failed', error: e.class, msg: e.message)
179
184
  end
180
185
 
181
186
  # interface for sending uadio
182
- def send_audio(channel_name, audio_url)
187
+ def send_audio(channel_name, audio_url, _caption = nil)
183
188
  channel(channel_name).answer("AUDIO: #{audio_url}")
189
+ rescue StandardError => e
190
+ log_error('Debug send_audio failed', error: e.class, msg: e.message)
184
191
  end
185
192
 
186
193
  # interface for sending image
187
194
  def send_image(channel_name, image_url, _caption = nil)
188
195
  channel(channel_name).answer("IMG: #{image_url}")
196
+ rescue StandardError => e
197
+ log_error('Debug send_image failed', error: e.class, msg: e.message)
189
198
  end
190
199
 
191
200
  # interface for sending image
192
- def send_document(channel_name, doc_url)
201
+ def send_document(channel_name, doc_url, _caption = nil)
193
202
  channel(channel_name).answer("DOC: #{doc_url}")
203
+ rescue StandardError => e
204
+ log_error('Debug send_document failed', error: e.class, msg: e.message)
194
205
  end
195
206
 
196
207
  def file_builder(data)
@@ -7,7 +7,7 @@ module Kybus
7
7
  # :nodoc: #
8
8
  module Adapter
9
9
  # :nodoc: #
10
- # Wraps a debugging message inside a class.
10
+ # Wraps a Discord message and exposes Kybus::Bot::Message API.
11
11
  class DiscordMessage < Kybus::Bot::Message
12
12
  # It receives a string with the raw text and the id of the channel
13
13
  def initialize(msg)
@@ -21,7 +21,7 @@ module Kybus
21
21
  end
22
22
 
23
23
  def message_id
24
- nil
24
+ @message.id if @message.respond_to?(:id)
25
25
  end
26
26
 
27
27
  def has_attachment?
@@ -46,16 +46,20 @@ module Kybus
46
46
  end
47
47
 
48
48
  def reply?
49
- @message.message.reply?
49
+ return false unless @message.respond_to?(:referenced_message)
50
+
51
+ !@message.referenced_message.nil?
50
52
  end
51
53
 
52
54
  def replied_message
53
- DiscordMessage.new(@message.message.referenced_message)
55
+ return unless reply?
56
+
57
+ DiscordMessage.new(@message.referenced_message)
54
58
  end
55
59
  end
56
60
 
57
61
  ##
58
- # This adapter is intended to be used on unit tests and development.
62
+ # Discord adapter for polling and sending messages.
59
63
  class Discord
60
64
  include ::Kybus::Logger
61
65
 
@@ -69,7 +73,7 @@ module Kybus
69
73
  def initialize(configs)
70
74
  @config = configs
71
75
  @client = Discordrb::Bot.new(token: @config['token'])
72
- @pool = []
76
+ @pool = Queue.new
73
77
  @client.message do |msg|
74
78
  @pool << msg
75
79
  end
@@ -82,13 +86,14 @@ module Kybus
82
86
 
83
87
  # Interface for receiving message
84
88
  def read_message
85
- # take the first message from the first open message,
86
89
  loop do
87
- break unless @pool.empty?
88
-
89
- sleep(0.1)
90
+ begin
91
+ msg = @pool.pop(true)
92
+ return @last_message = DiscordMessage.new(msg)
93
+ rescue ThreadError
94
+ sleep(0.1)
95
+ end
90
96
  end
91
- @last_message = DiscordMessage.new(@pool.shift)
92
97
  end
93
98
 
94
99
  # interface for sending messages
@@ -100,6 +105,8 @@ module Kybus
100
105
  else
101
106
  @client.user(channel_name).pm(contents)
102
107
  end
108
+ rescue StandardError => e
109
+ log_error('Discord send_message failed', error: e.class, msg: e.message)
103
110
  end
104
111
 
105
112
  def message_builder(raw_message)
@@ -108,20 +115,34 @@ module Kybus
108
115
 
109
116
  def send_file(channel_name, file, _caption = nil)
110
117
  @client.send_file(channel_name, File.open(file, 'r'))
118
+ rescue StandardError => e
119
+ log_error('Discord send_file failed', error: e.class, msg: e.message)
111
120
  end
112
121
 
113
122
  def send_video(channel_name, file, _caption = nil)
114
123
  @client.send_file(channel_name, File.open(file, 'r'))
124
+ rescue StandardError => e
125
+ log_error('Discord send_video failed', error: e.class, msg: e.message)
115
126
  end
116
127
 
117
128
  # interface for sending uadio
118
129
  def send_audio(channel_name, file, _caption = nil)
119
130
  @client.send_file(channel_name, File.open(file, 'r'))
131
+ rescue StandardError => e
132
+ log_error('Discord send_audio failed', error: e.class, msg: e.message)
120
133
  end
121
134
 
122
135
  # interface for sending image
123
136
  def send_image(channel_name, file, _caption = nil)
124
137
  @client.send_file(channel_name, File.open(file, 'r'))
138
+ rescue StandardError => e
139
+ log_error('Discord send_image failed', error: e.class, msg: e.message)
140
+ end
141
+
142
+ def send_document(channel_name, file, _caption = nil)
143
+ @client.send_file(channel_name, File.open(file, 'r'))
144
+ rescue StandardError => e
145
+ log_error('Discord send_document failed', error: e.class, msg: e.message)
125
146
  end
126
147
  end
127
148
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'telegram/bot'
4
+ require 'timeout'
4
5
  require 'faraday'
5
6
  require_relative 'telegram_file'
6
7
  require_relative 'telegram_message'
@@ -8,6 +9,7 @@ require_relative 'telegram_message'
8
9
  module Kybus
9
10
  module Bot
10
11
  module Adapter
12
+ # Telegram adapter for polling and sending messages.
11
13
  class Telegram
12
14
  include ::Kybus::Logger
13
15
 
@@ -19,18 +21,46 @@ module Kybus
19
21
  TelegramFile.register(:cli, @client)
20
22
  end
21
23
 
24
+ # Blocking read from Telegram long-polling.
22
25
  def read_message
23
26
  loop do
24
- @client.listen do |message|
25
- log_info('Received message', message: message.to_h, from: message.from.to_h)
26
- return @last_message = TelegramMessage.new(message)
27
+ Timeout.timeout(30) do
28
+ @client.listen do |message|
29
+ if message.respond_to?(:data) && message.respond_to?(:message)
30
+ return @last_message = serialize_callback_query(message)
31
+ end
32
+ message_hash = message.respond_to?(:to_h) ? message.to_h : {}
33
+ has_message_id = message.respond_to?(:message_id)
34
+ payload = message_hash.is_a?(Hash) ? (message_hash['result'] || message_hash[:result]) : nil
35
+ has_result_message_id = payload.is_a?(Hash) && payload['message_id']
36
+
37
+ unless has_message_id || has_result_message_id
38
+ log_info('Skipping non-message update', type: message.class.name)
39
+ next
40
+ end
41
+
42
+ from_hash = message.respond_to?(:from) && message.from ? message.from.to_h : nil
43
+ log_info('Received message', message: message_hash, from: from_hash)
44
+ return @last_message = TelegramMessage.new(message)
45
+ end
27
46
  end
28
47
  rescue ::Telegram::Bot::Exceptions::ResponseError => e
29
48
  log_error('An error occurred while calling to Telegram API', e)
49
+ sleep(5)
50
+ rescue Timeout::Error
51
+ log_error('Telegram read timeout, retrying')
52
+ sleep(2)
53
+ rescue StandardError => e
54
+ log_error('Error while reading Telegram message', error: e.class, msg: e.message)
55
+ sleep(5)
30
56
  end
31
57
  end
32
58
 
59
+ # Parse a webhook-style payload into a SerializedMessage.
33
60
  def handle_message(body)
61
+ if body['callback_query']
62
+ return serialize_callback_payload(body['callback_query'])
63
+ end
34
64
  chat_id = body.dig('message', 'chat', 'id')
35
65
  message_id = body.dig('message', 'message_id')
36
66
  user = extract_user(body.dig('message', 'from'))
@@ -48,6 +78,7 @@ module Kybus
48
78
  "[user](tg://user?id=#{id})"
49
79
  end
50
80
 
81
+ # Send a text message.
51
82
  def send_message(contents, channel_name)
52
83
  log_debug('Sending message', channel_name:, message: contents)
53
84
  @client.api.send_message(chat_id: channel_name.to_i, text: contents, parse_mode: @config['parse_mode'])
@@ -55,24 +86,49 @@ module Kybus
55
86
  nil if e.error_code == '403'
56
87
  end
57
88
 
89
+ # Send a text message with reply markup.
90
+ def send_message_with_markup(contents, channel_name, reply_markup: nil)
91
+ log_debug('Sending message', channel_name:, message: contents)
92
+ @client.api.send_message(chat_id: channel_name.to_i, text: contents, parse_mode: @config['parse_mode'],
93
+ reply_markup: reply_markup)
94
+ rescue ::Telegram::Bot::Exceptions::ResponseError => e
95
+ nil if e.error_code == '403'
96
+ end
97
+
98
+ # Edit an existing message.
99
+ def edit_message_text(channel_name, message_id, contents, reply_markup: nil)
100
+ @client.api.edit_message_text(chat_id: channel_name.to_i, message_id: message_id, text: contents,
101
+ parse_mode: @config['parse_mode'], reply_markup: reply_markup)
102
+ rescue ::Telegram::Bot::Exceptions::ResponseError => e
103
+ nil if e.error_code == '403'
104
+ end
105
+
58
106
  def send_video(channel_name, video_url, comment = nil)
59
107
  file = Faraday::FilePart.new(video_url, 'video/mp4')
60
- @client.api.send_video(chat_id: channel_name, video: file, caption: comment)
108
+ @client.api.send_video(chat_id: channel_name, video: file, caption: comment, parse_mode: @config['parse_mode'])
109
+ rescue ::Telegram::Bot::Exceptions::ResponseError => e
110
+ nil if e.error_code == '403'
61
111
  end
62
112
 
63
- def send_audio(channel_name, audio_url)
113
+ def send_audio(channel_name, audio_url, comment = nil)
64
114
  file = Faraday::FilePart.new(audio_url, 'audio/mp3')
65
- @client.api.send_audio(chat_id: channel_name, audio: file)
115
+ @client.api.send_audio(chat_id: channel_name, audio: file, caption: comment, parse_mode: @config['parse_mode'])
116
+ rescue ::Telegram::Bot::Exceptions::ResponseError => e
117
+ nil if e.error_code == '403'
66
118
  end
67
119
 
68
120
  def send_image(channel_name, image_url, comment = nil)
69
121
  file = Faraday::FilePart.new(image_url, 'image/jpeg')
70
- @client.api.send_photo(chat_id: channel_name, photo: file, caption: comment)
122
+ @client.api.send_photo(chat_id: channel_name, photo: file, caption: comment, parse_mode: @config['parse_mode'])
123
+ rescue ::Telegram::Bot::Exceptions::ResponseError => e
124
+ nil if e.error_code == '403'
71
125
  end
72
126
 
73
- def send_document(channel_name, image_url)
127
+ def send_document(channel_name, image_url, comment = nil)
74
128
  file = Faraday::FilePart.new(image_url, 'application/octet-stream')
75
- @client.api.send_document(chat_id: channel_name, document: file)
129
+ @client.api.send_document(chat_id: channel_name, document: file, caption: comment, parse_mode: @config['parse_mode'])
130
+ rescue ::Telegram::Bot::Exceptions::ResponseError => e
131
+ nil if e.error_code == '403'
76
132
  end
77
133
 
78
134
  def message_builder(raw_message)
@@ -86,7 +142,18 @@ module Kybus
86
142
  private
87
143
 
88
144
  def extract_user(from)
89
- from['username'] || from['first_name']
145
+ return if from.nil?
146
+
147
+ if from.respond_to?(:username) || from.respond_to?(:first_name)
148
+ username = from.respond_to?(:username) ? from.username : nil
149
+ return username if username && !username.to_s.empty?
150
+
151
+ return from.first_name if from.respond_to?(:first_name)
152
+ end
153
+
154
+ return from[:username] || from[:first_name] if from.respond_to?(:[])
155
+
156
+ nil
90
157
  end
91
158
 
92
159
  def extract_attachment(message)
@@ -111,6 +178,37 @@ module Kybus
111
178
  is_private?: replied_message.dig('chat', 'type') == 'private'
112
179
  ).serialize
113
180
  end
181
+
182
+ def serialize_callback_query(callback)
183
+ msg = callback.message
184
+ SerializedMessage.new(
185
+ provider: 'telegram',
186
+ channel_id: msg.chat.id,
187
+ message_id: msg.message_id,
188
+ user: extract_user(callback.from),
189
+ replied_message: nil,
190
+ raw_message: callback.data,
191
+ is_private?: msg.chat.type == 'private',
192
+ callback: true,
193
+ attachment: nil
194
+ )
195
+ end
196
+
197
+ def serialize_callback_payload(callback)
198
+ msg = callback['message'] || {}
199
+ chat = msg['chat'] || {}
200
+ SerializedMessage.new(
201
+ provider: 'telegram',
202
+ channel_id: chat['id'],
203
+ message_id: msg['message_id'],
204
+ user: extract_user(callback['from'] || {}),
205
+ replied_message: nil,
206
+ raw_message: callback['data'],
207
+ is_private?: chat['type'] == 'private',
208
+ callback: true,
209
+ attachment: nil
210
+ )
211
+ end
114
212
  end
115
213
 
116
214
  register('telegram', Telegram)
@@ -7,6 +7,7 @@ module Kybus
7
7
  module Bot
8
8
  # :nodoc: #
9
9
  module Adapter
10
+ # Telegram file wrapper with download helpers.
10
11
  class TelegramFile
11
12
  extend Kybus::DRY::ResourceInjector
12
13
  attr_reader :id
@@ -8,7 +8,7 @@ module Kybus
8
8
  # :nodoc: #
9
9
  module Adapter
10
10
  # :nodoc: #
11
- # Wraps a debugging message inside a class.
11
+ # Wraps a Telegram message and exposes Kybus::Bot::Message API.
12
12
  class TelegramMessage < Kybus::Bot::Message
13
13
  # It receives a string with the raw text and the id of the channel
14
14
  def initialize(message)
@@ -30,7 +30,15 @@ module Kybus
30
30
  end
31
31
 
32
32
  def message_id
33
- @message.respond_to?(:message_id) ? @message.message_id : @message['result']['message_id']
33
+ return @message.message_id if @message.respond_to?(:message_id)
34
+
35
+ if @message.respond_to?(:to_h)
36
+ result = @message.to_h
37
+ payload = result.is_a?(Hash) ? (result['result'] || result[:result]) : nil
38
+ return payload['message_id'] if payload.is_a?(Hash)
39
+ end
40
+
41
+ nil
34
42
  end
35
43
 
36
44
  # Returns the message contents
@@ -18,6 +18,7 @@ require 'forwardable'
18
18
 
19
19
  module Kybus
20
20
  module Bot
21
+ # Main bot runtime: command registry, provider IO, and state management.
21
22
  class Base # rubocop: disable Metrics/ClassLength
22
23
  extend Forwardable
23
24
  include Kybus::Logger
@@ -51,24 +52,33 @@ module Kybus
51
52
  build_pool
52
53
  end
53
54
 
55
+ # Extend DSL methods available inside command blocks.
54
56
  def self.helpers(mod = nil, &)
55
57
  DSLMethods.include(mod) if mod
56
58
  DSLMethods.class_eval(&) if block_given?
57
59
  end
58
60
 
61
+ # Enable automatic help and hints injection for commands.
62
+ def self.enable_command_help!
63
+ Kybus::Bot::CommandHelp.apply!(self)
64
+ end
65
+
59
66
  def extend(*)
60
67
  DSLMethods.include(*)
61
68
  end
62
69
 
70
+ # Returns the DSL context used to execute commands.
63
71
  def dsl
64
72
  @executor.dsl
65
73
  end
66
74
 
75
+ # Process an incoming provider message (webhook mode).
67
76
  def handle_message(msg)
68
77
  parsed = @provider.handle_message(msg)
69
78
  @executor.process_message(parsed)
70
79
  end
71
80
 
81
+ # Execute a background job (used by async forkers).
72
82
  def handle_job(job, args, channel_id)
73
83
  @executor.load_state!(channel_id)
74
84
  @forker.handle_job(job, args)
@@ -80,27 +90,67 @@ module Kybus
80
90
  pool.each(&:await)
81
91
  end
82
92
 
93
+ # Redirect execution to another command with params.
83
94
  def redirect(command, *params)
84
95
  @executor.redirect(command, params)
85
96
  end
86
97
 
98
+ # Send a message through the provider.
87
99
  def send_message(contents, channel)
88
100
  log_debug('Sending message', contents:, channel:)
89
101
  provider.message_builder(@provider.send_message(contents, channel))
90
102
  end
91
103
 
104
+ # Send a message with buttons using the active UX renderer.
105
+ def send_message_with_buttons(contents, buttons, channel)
106
+ ux.render_buttons(dsl, text: contents, buttons: buttons, channel: channel)
107
+ end
108
+
109
+ # Register a paginated query command with enhanced UX rendering.
110
+ def define_paginated_query(command, params: [], hint: nil, per_page: 10, &block)
111
+ register_command(command, params, hint: hint) do
112
+ @bot.run_paginated_query(self, command, params, per_page, &block)
113
+ end
114
+ end
115
+
116
+ # Render a paginated response using the active UX renderer.
117
+ def run_paginated_query(dsl, command, params, per_page, &block)
118
+ args = Array(params).map { |param| dsl.params[param] }
119
+ last_arg = args.last
120
+ value, page = split_value_page(last_arg || dsl.last_message.raw_message)
121
+ args[-1] = value if args.any?
122
+
123
+ result = block.call(dsl, *args, page, per_page)
124
+ total_pages = result[:total_pages] || 1
125
+ text = result[:text] || [result[:header], result[:body], result[:nav]].compact.join("\n")
126
+ key = result[:key] || "#{command}:#{args.compact.join(':')}"
127
+ prev_cmd = result[:prev_cmd] || (page > 1 ? build_paginated_command(command, args, page - 1) : nil)
128
+ next_cmd = result[:next_cmd] || (page < total_pages ? build_paginated_command(command, args, page + 1) : nil)
129
+
130
+ ux.render_paginated(dsl, key:, text:, prev_cmd:, next_cmd:)
131
+ end
132
+
133
+ # Returns the UX renderer for the current provider.
134
+ def ux
135
+ @ux ||= Kybus::Bot::UX.for(provider)
136
+ end
137
+
138
+ # Register a command and its params.
92
139
  def register_command(klass, params = [], &)
93
140
  definitions.register_command(klass, params, &)
94
141
  end
95
142
 
143
+ # Register a background job handler.
96
144
  def register_job(name, args = {}, &)
97
145
  @forker.register_command(name, args, &)
98
146
  end
99
147
 
148
+ # Enqueue a background job.
100
149
  def invoke_job(name, args)
101
150
  @forker.fork(name, args, dsl)
102
151
  end
103
152
 
153
+ # Enqueue a background job with delay.
104
154
  def invoke_job_with_delay(name, delay, args)
105
155
  @forker.fork(name, args, dsl, delay:)
106
156
  end
@@ -123,6 +173,20 @@ module Kybus
123
173
  Kybus::Storage::Repository.from_config(nil, repository_config, {})
124
174
  end
125
175
 
176
+ def split_value_page(raw)
177
+ token = raw.to_s
178
+ value, page_str = token.split(/__|\s+/, 2)
179
+ page = page_str.to_i
180
+ page = 1 if page <= 0
181
+ [value, page]
182
+ end
183
+
184
+ def build_paginated_command(command, args, page)
185
+ base = command.to_s
186
+ base += args.first.to_s if args.any? && !args.first.to_s.empty?
187
+ "#{base}__#{page}"
188
+ end
189
+
126
190
  def create_executor(configs, command_factory)
127
191
  @executor = if configs['sidekiq']
128
192
  require_relative 'sidekiq_command_executor'
@@ -5,6 +5,7 @@ module Kybus
5
5
  # Object that wraps a command, it is analogus to a route definition.
6
6
  # it currently only gets a param list, but it will be extended to a more
7
7
  # complex DSL.
8
+ # Command definition with params and executable block.
8
9
  class Command
9
10
  attr_accessor :name
10
11
  attr_reader :block, :params
@@ -23,12 +24,12 @@ module Kybus
23
24
  end
24
25
  end
25
26
 
26
- # Checks if the params object given contains all the needed values
27
+ # Checks if the params object given contains all the needed values.
27
28
  def ready?(current_params)
28
29
  params.all? { |key| current_params.key?(key) }
29
30
  end
30
31
 
31
- # Finds the first empty param from the given parameter
32
+ # Finds the first missing param from the given parameter set.
32
33
  def next_missing_param(current_params)
33
34
  params.find { |key| !current_params.key?(key) }.to_s
34
35
  end
@@ -4,12 +4,13 @@ require_relative 'command'
4
4
 
5
5
  module Kybus
6
6
  module Bot
7
+ # Registry for commands keyed by name/pattern.
7
8
  class CommandDefinition
8
9
  def initialize
9
10
  @commands = {}
10
11
  end
11
12
 
12
- # Stores an operation definition
13
+ # Stores an operation definition.
13
14
  def register_command(name, params, &)
14
15
  @commands[name] = Command.new(name, params, &)
15
16
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Kybus
4
4
  module Bot
5
+ # Runs commands when ready or requests missing params.
5
6
  class CommandHandler
6
7
  def initialize(executor)
7
8
  @executor = executor
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Kybus
4
4
  module Bot
5
+ # Persisted state for a channel (command, params, files, metadata).
5
6
  class CommandState
6
7
  attr_reader :command
7
8
 
@@ -35,6 +36,7 @@ module Kybus
35
36
  command.next_missing_param(params)
36
37
  end
37
38
 
39
+ # Store the last message for reply context.
38
40
  def last_message=(msg)
39
41
  @data[:last_message] = msg
40
42
  end
@@ -83,12 +85,14 @@ module Kybus
83
85
  @data[:params][param] = value
84
86
  end
85
87
 
88
+ # Metadata hash persisted with the state.
86
89
  def metadata
87
90
  @data[:metadata] = parse_json(@data[:metadata]) if @data[:metadata].is_a?(String)
88
- @data[:metadata] || {}
91
+ @data[:metadata] ||= {}
89
92
  end
90
93
 
91
94
  include Kybus::Logger
95
+ # Persist the current state into the repository.
92
96
  def save!
93
97
  backup = @data.clone
94
98
  serialize_data!
@@ -5,6 +5,7 @@ require_relative 'inline_command_matcher'
5
5
 
6
6
  module Kybus
7
7
  module Bot
8
+ # Factory for loading/saving command state from storage.
8
9
  class CommandStateFactory
9
10
  include Kybus::Storage::Datasource
10
11
  attr_reader :factory
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Kybus
4
4
  module Bot
5
+ # Execution wrapper for a command and its state.
5
6
  class ExecutionContest
6
7
  include Kybus::Logger
7
8
  extend Forwardable
@@ -15,6 +16,7 @@ module Kybus
15
16
  state.command.block
16
17
  end
17
18
 
19
+ # Execute the command block with a DSL context.
18
20
  def call!(context)
19
21
  context.state = state
20
22
  statement = context.instance_eval(&block)
@@ -28,6 +30,7 @@ module Kybus
28
30
  end
29
31
 
30
32
  # Stores a parameter into the status
33
+ # Adds a parameter value to the current state.
31
34
  def add_param(value)
32
35
  param = state.requested_param
33
36
  return unless param
@@ -40,6 +43,7 @@ module Kybus
40
43
  state.command.nil?
41
44
  end
42
45
 
46
+ # Adds an uploaded file to the current state.
43
47
  def add_file(file)
44
48
  param = state.requested_param
45
49
  return unless param