kybus-bot 0.5.1 → 0.7.0

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: fcb4039ffd0fa38cadb57ba4a6a0813d5c105590677f1e6f6a7ef85fa6ee2365
4
- data.tar.gz: abf1c8941afd74565c25e012b3eec8e94e17d1e0a42a83b2bc20be1eceac0192
3
+ metadata.gz: f5c13b85c7bdfcde5cf2aabfa9c07e78a59da781c06d86ca91842605e3467b61
4
+ data.tar.gz: ef7f22cea28720b25bbbee6930b397b28eb5890e1e470bb66155c691050ed101
5
5
  SHA512:
6
- metadata.gz: f1e09cd5437c55dbfc6a38df6c3f8ff1383f6f2974538e3ec2ebd380ea38db0e12cd9ce8ba82b87de15bd1200fef0d610ab1e81ee34c644e5fd064f564a31bd7
7
- data.tar.gz: 159c13cb384b3168647e572544cdb96098a4d11b8ae4ed7c1143ba562fd0c52eccdf9295729f97e1614ef44d020681bd9615dc064522664114a3c211d9319566
6
+ metadata.gz: c01b8aadc1b567f9181ad731baa7861882ec535482c79c51eb45fee0257ab443ee9fc8857e133eab1b4dd814b725b4170fff2513484a1759f08ed1add74741ad
7
+ data.tar.gz: e80319c534274c58df1cf58e8794a56c44100ac46c0483e0d7b9a193e612944efbf610dc3b6da24f91648ad764fa45fb8e7503f4a0774c5b1c541e8c41be3b6b
@@ -13,7 +13,30 @@ module Kybus
13
13
  # It receives a string with the raw text and the id of the channel
14
14
  attr_reader :attachment
15
15
 
16
+ class DebugFile
17
+ def initialize(path)
18
+ @path = path
19
+ end
20
+
21
+ def to_json(obj)
22
+ to_h.to_json(obj)
23
+ end
24
+
25
+ def file_name
26
+ @path
27
+ end
28
+
29
+ def download
30
+ File.read(@path)
31
+ end
32
+
33
+ def to_h
34
+ { path: @path }
35
+ end
36
+ end
37
+
16
38
  def initialize(text, channel, attachment = nil)
39
+ super()
17
40
  @text = text
18
41
  @channel = channel
19
42
  @attachment = attachment
@@ -36,6 +59,14 @@ module Kybus
36
59
  def has_attachment?
37
60
  !!attachment
38
61
  end
62
+
63
+ def reply?
64
+ @reply
65
+ end
66
+
67
+ def is_private?
68
+ true
69
+ end
39
70
  end
40
71
 
41
72
  # This class simulates a message chat with a user.
@@ -66,7 +97,7 @@ module Kybus
66
97
  DebugMessage.new(@pending_messages.shift, @name)
67
98
  end
68
99
 
69
- def send_data(message, attachment)
100
+ def send_data(message, _attachment)
70
101
  return unless @echo
71
102
 
72
103
  puts "Sending message to channel: #{@name}"
@@ -92,6 +123,8 @@ module Kybus
92
123
  end
93
124
  end
94
125
 
126
+ attr_accessor :last_message
127
+
95
128
  # It receives a hash with the configurations:
96
129
  # - name: the name of the channel
97
130
  # - channels a key value, where the key is a name and the value the
@@ -112,11 +145,7 @@ module Kybus
112
145
  raise NoMoreMessageException if @channels.values.all?(&:empty?)
113
146
 
114
147
  msg = @channels.values.find(&:open?)
115
- return msg.read_message if msg
116
-
117
- # :nocov: #
118
- sleep(1)
119
- # :nocov: #
148
+ return @last_message = msg.read_message if msg
120
149
  end
121
150
  end
122
151
 
@@ -131,7 +160,7 @@ module Kybus
131
160
  end
132
161
 
133
162
  # interface for sending video
134
- def send_video(channel_name, video_url)
163
+ def send_video(channel_name, video_url, caption = nil)
135
164
  channel(channel_name).answer("VIDEO: #{video_url}")
136
165
  end
137
166
 
@@ -141,10 +170,28 @@ module Kybus
141
170
  end
142
171
 
143
172
  # interface for sending image
144
- def send_image(channel_name, image_url)
173
+ def send_image(channel_name, image_url, caption = nil)
145
174
  channel(channel_name).answer("IMG: #{image_url}")
146
175
  end
147
176
 
177
+ # interface for sending image
178
+ def send_document(channel_name, doc_url)
179
+ channel(channel_name).answer("DOC: #{doc_url}")
180
+ end
181
+
182
+ def file_builder(data)
183
+ case data
184
+ when DebugMessage::DebugFile
185
+ data
186
+ when Hash
187
+ DebugMessage::DebugFile.new(data[:path])
188
+ end
189
+ end
190
+
191
+ def mention(user)
192
+ "@#{user}"
193
+ end
194
+
148
195
  # changes echo config
149
196
  def echo=(toogle)
150
197
  @channels.each { |_, channel| channel.echo = toogle }
@@ -11,6 +11,7 @@ module Kybus
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)
14
+ super
14
15
  @message = msg
15
16
  end
16
17
 
@@ -45,6 +46,9 @@ module Kybus
45
46
  # This adapter is intended to be used on unit tests and development.
46
47
  class Discord
47
48
  include ::Kybus::Logger
49
+
50
+ attr_reader :last_message, :client
51
+
48
52
  # It receives a hash with the configurations:
49
53
  # - name: the name of the channel
50
54
  # - channels a key value, where the key is a name and the value the
@@ -60,8 +64,6 @@ module Kybus
60
64
  @client.run(:async)
61
65
  end
62
66
 
63
- attr_reader :client
64
-
65
67
  def mention(id)
66
68
  "<@!#{id}>"
67
69
  end
@@ -70,17 +72,15 @@ module Kybus
70
72
  def read_message
71
73
  # take the first message from the first open message,
72
74
  loop do
73
- if @pool.empty?
74
- sleep(0.1)
75
- else
76
- break
77
- end
75
+ break unless @pool.empty?
76
+
77
+ sleep(0.1)
78
78
  end
79
- DiscordMessage.new(@pool.shift)
79
+ @last_message = DiscordMessage.new(@pool.shift)
80
80
  end
81
81
 
82
82
  # interface for sending messages
83
- def send_message(channel_name, contents)
83
+ def send_message(channel_name, contents, caption = nil)
84
84
  puts "#{channel_name} => #{contents}" if @config['debug']
85
85
  channel = @client.channel(channel_name)
86
86
  if channel
@@ -1,100 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'telegram/bot'
4
+ require 'faraday'
5
+ require_relative 'telegram_file'
6
+ require_relative 'telegram_message'
4
7
 
5
8
  module Kybus
6
9
  module Bot
7
- # :nodoc: #
8
10
  module Adapter
9
- # :nodoc: #
10
- # Wraps a debugging message inside a class.
11
- class TelegramMessage < Kybus::Bot::Message
12
- # It receives a string with the raw text and the id of the channel
13
- def initialize(message)
14
- @message = message
15
- end
16
-
17
- def reply?
18
- !!@message.reply_to_message
19
- end
20
-
21
- def replied_message
22
- TelegramMessage.new(@message.reply_to_message)
23
- end
24
-
25
- # Returns the channel id
26
- def channel_id
27
- @message.chat.id
28
- end
29
-
30
- # Returns the message contents
31
- def raw_message
32
- @message.to_s
33
- end
34
-
35
- def is_private?
36
- @message.chat.type == 'private'
37
- end
38
-
39
- def has_attachment?
40
- !!@message.document
41
- end
42
-
43
- def attachment
44
- @message.document
45
- end
46
-
47
- def user
48
- @message.from.id
49
- end
50
- end
51
-
52
- class TelegramFile
53
- extend Kybus::DRY::ResourceInjector
54
- attr_reader :id
55
- def initialize(message)
56
- if message.is_a?(String)
57
- @id = message
58
- elsif message.is_a?(Hash)
59
- @id = message['id'] || message[:id]
60
- elsif message.is_a?(TelegramFile)
61
- @id = message.id
62
- else
63
- @id = message.file_id
64
- end
65
- end
66
-
67
- def to_h
68
- {
69
- provide: 'telegram',
70
- id: @id
71
- }
72
- end
73
-
74
- def cli
75
- @cli ||= TelegramFile.resource(:cli)
76
- end
77
-
78
- def meta
79
- @meta ||= cli.api.get_file(file_id: @id)
80
- end
81
-
82
- def original_name
83
- meta.dig('result', 'file_name')
84
- end
85
-
86
- def download
87
- token = cli.api.token
88
- file_path = meta.dig('result', 'file_path')
89
- path = "https://api.telegram.org/file/bot#{token}/#{file_path}"
90
- Faraday.get(path).body
91
- end
92
- end
93
-
94
11
  ##
95
12
  # This adapter is intended to be used on unit tests and development.
96
13
  class Telegram
97
14
  include ::Kybus::Logger
15
+
16
+ attr_reader :last_message
17
+
98
18
  # It receives a hash with the configurations:
99
19
  # - name: the name of the channel
100
20
  # - channels a key value, where the key is a name and the value the
@@ -113,10 +33,12 @@ module Kybus
113
33
  @client.listen do |message|
114
34
  log_info('Received message', message: message.to_h,
115
35
  from: message.from.to_h)
116
- return TelegramMessage.new(message)
36
+ return @last_message = TelegramMessage.new(message)
117
37
  end
118
38
  rescue ::Telegram::Bot::Exceptions::ResponseError => e
39
+ # :nocov:
119
40
  log_error('An error ocurred while calling to Telegram API', e)
41
+ # :nocov:
120
42
  end
121
43
  end
122
44
 
@@ -124,36 +46,37 @@ module Kybus
124
46
  "[user](tg://user?id=#{id})"
125
47
  end
126
48
 
127
-
128
49
  # interface for sending messages
129
50
  def send_message(channel_name, contents)
130
51
  puts "#{channel_name} => #{contents}" if @config['debug']
131
52
  @client.api.send_message(chat_id: channel_name, text: contents)
132
- rescue ::Telegram::Bot::Exceptions::ResponseError => err
133
- return if err[:error_code] == '403'
53
+ # :nocov:
54
+ rescue ::Telegram::Bot::Exceptions::ResponseError => e
55
+ return if e[:error_code] == '403'
134
56
  end
57
+ # :nocov:
135
58
 
136
59
  # interface for sending video
137
- def send_video(channel_name, video_url)
138
- file = Faraday::UploadIO.new(video_url, 'video/mp4')
139
- @client.api.send_video(chat_id: channel_name, audio: file)
60
+ def send_video(channel_name, video_url, comment = nil)
61
+ file = Faraday::FilePart.new(video_url, 'video/mp4')
62
+ @client.api.send_video(chat_id: channel_name, video: file, caption: comment)
140
63
  end
141
64
 
142
65
  # interface for sending uadio
143
66
  def send_audio(channel_name, audio_url)
144
- file = Faraday::UploadIO.new(audio_url, 'audio/mp3')
67
+ file = Faraday::FilePart.new(audio_url, 'audio/mp3')
145
68
  @client.api.send_audio(chat_id: channel_name, audio: file)
146
69
  end
147
70
 
148
71
  # interface for sending image
149
- def send_image(channel_name, image_url)
150
- file = Faraday::UploadIO.new(image_url, 'image/jpeg')
151
- @client.api.send_photo(chat_id: channel_name, photo: file)
72
+ def send_image(channel_name, image_url, comment = nil)
73
+ file = Faraday::FilePart.new(image_url, 'image/jpeg')
74
+ @client.api.send_photo(chat_id: channel_name, photo: file, caption: comment)
152
75
  end
153
76
 
154
77
  # interface for sending document
155
78
  def send_document(channel_name, image_url)
156
- file = Faraday::UploadIO.new(image_url, 'application/octect-stream')
79
+ file = Faraday::FilePart.new(image_url, 'application/octect-stream')
157
80
  @client.api.send_document(chat_id: channel_name, document: file)
158
81
  end
159
82
 
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'telegram/bot'
4
+ require 'faraday'
5
+
6
+ module Kybus
7
+ module Bot
8
+ # :nodoc: #
9
+ module Adapter
10
+ class TelegramFile
11
+ extend Kybus::DRY::ResourceInjector
12
+ attr_reader :id
13
+
14
+ def initialize(message)
15
+ @id = case message
16
+ when String
17
+ message
18
+ when Hash
19
+ message['id'] || message[:id]
20
+ when TelegramFile
21
+ message.id
22
+ else
23
+ message.file_id
24
+ end
25
+ end
26
+
27
+ def to_h
28
+ {
29
+ provide: 'telegram',
30
+ id: @id
31
+ }
32
+ end
33
+
34
+ def cli
35
+ @cli ||= TelegramFile.resource(:cli)
36
+ end
37
+
38
+ def meta
39
+ @meta ||= cli.api.get_file(file_id: @id)
40
+ end
41
+
42
+ def original_name
43
+ meta.dig('result', 'file_name')
44
+ end
45
+
46
+ def download
47
+ token = cli.api.token
48
+ file_path = meta.dig('result', 'file_path')
49
+ path = "https://api.telegram.org/file/bot#{token}/#{file_path}"
50
+ Faraday.get(path).body
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'telegram/bot'
4
+ require 'faraday'
5
+
6
+ module Kybus
7
+ module Bot
8
+ # :nodoc: #
9
+ module Adapter
10
+ # :nodoc: #
11
+ # Wraps a debugging message inside a class.
12
+ class TelegramMessage < Kybus::Bot::Message
13
+ # It receives a string with the raw text and the id of the channel
14
+ def initialize(message)
15
+ super()
16
+ @message = message
17
+ end
18
+
19
+ def reply?
20
+ !!@message.reply_to_message
21
+ end
22
+
23
+ def replied_message
24
+ TelegramMessage.new(@message.reply_to_message)
25
+ end
26
+
27
+ # Returns the channel id
28
+ def channel_id
29
+ @message.chat.id
30
+ end
31
+
32
+ # Returns the message contents
33
+ def raw_message
34
+ @message.to_s
35
+ end
36
+
37
+ def is_private?
38
+ @message.chat.type == 'private'
39
+ end
40
+
41
+ def has_attachment?
42
+ !!attachment
43
+ end
44
+
45
+ def attachment
46
+ @message.document || @message.photo&.last || @message.audio
47
+ end
48
+
49
+ def user
50
+ @message.from.id
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -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
35
-
36
- def send_image(content, channel = nil)
37
- provider.send_image(channel || current_channel, content)
38
- end
27
+ extend Forwardable
28
+ include Kybus::Logger
39
29
 
40
- def send_audio(content, channel = nil)
41
- provider.send_audio(channel || current_channel, content)
42
- end
30
+ attr_reader :provider, :executor, :pool_size, :pool, :definitions
43
31
 
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,45 @@ 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
64
+ end
65
+
66
+ # Starts the bot execution, this is a blocking call.
67
+ def run
83
68
  # TODO: Implement an interface for killing the process
84
- @pool.each(&:run)
69
+ pool.each(&:run)
85
70
  # :nocov: #
86
- @pool.each(&:await)
71
+ pool.each(&:await)
87
72
  # :nocov: #
88
73
  end
89
74
 
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
- end
167
-
168
- def file(name)
169
- (file = files[name]) && provider.file_builder(file)
170
- end
171
-
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']
75
+ def register_command(klass, params = [], &block)
76
+ definitions.register_command(klass, params, &block)
184
77
  end
185
78
 
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)
235
- end
236
-
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)
245
- end
246
-
247
- def parse_state!
248
- end
249
-
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
261
- end
262
-
263
- def session
264
- @repository
79
+ def rescue_from(klass, &block)
80
+ definitions.register_command(klass, [], &block)
265
81
  end
266
82
  end
267
83
  end
@@ -6,43 +6,23 @@ 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_reader :block, :params, :name
10
10
 
11
11
  # Receives a list of params as symbols and the lambda with the block.
12
- def initialize(params, block)
12
+ def initialize(name, params, &block)
13
+ @name = name
13
14
  @params = params
14
15
  @block = block
15
16
  end
16
17
 
17
18
  # Checks if the params object given contains all the needed values
18
19
  def ready?(current_params)
19
- @params.all? { |key| current_params.key?(key) }
20
+ params.all? { |key| current_params.key?(key) }
20
21
  end
21
22
 
22
23
  # Finds the first empty param from the given parameter
23
24
  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)
37
- end
38
-
39
- def registered_commands
40
- @commands.keys
41
- end
42
-
43
- # Returns a command with the name
44
- def [](name)
45
- @commands[name]
25
+ params.find { |key| !current_params.key?(key) }.to_s
46
26
  end
47
27
  end
48
28
  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,77 @@
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
+
70
+ @data.store
71
+ %i[params files].each do |param|
72
+ @data[param] = backup[param]
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,45 @@
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(name)
18
+ @definitions[name]
19
+ end
20
+
21
+ def default_command
22
+ @definitions['default']
23
+ end
24
+
25
+ def command_or_default(name)
26
+ command(name) || default_command
27
+ end
28
+
29
+ def command_with_inline_arg(name_with_arg)
30
+ @definitions.each do |name, command|
31
+ next unless name.is_a?(String)
32
+ return [command, name_with_arg.gsub(name, '')] if name_with_arg.start_with?(name)
33
+ end
34
+ nil
35
+ end
36
+
37
+ def load_state(channel)
38
+ data = factory.get(channel)
39
+ CommandState.new(data, command_or_default(data[:cmd]))
40
+ rescue Kybus::Storage::Exceptions::ObjectNotFound
41
+ CommandState.new(factory.create(channel_id: channel, params: '{}'), nil)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,58 @@
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
+ context.instance_eval(&block)
21
+ clear_command
22
+ end
23
+
24
+ def initialize(channel_id, channel_factory)
25
+ @channel_factory = channel_factory
26
+ load_state!(channel_id)
27
+ end
28
+
29
+ # Stores a parameter into the status
30
+ def add_param(value)
31
+ param = state.requested_param
32
+ return unless param
33
+
34
+ log_debug('Received new param', param:, value:)
35
+ state.store_param(param.to_sym, value)
36
+ end
37
+
38
+ def add_file(file)
39
+ param = state.requested_param
40
+ return unless param
41
+
42
+ log_debug('Received new file', param:, file: file.to_h)
43
+ state.save_file(param.to_sym, file)
44
+ end
45
+
46
+ # Loads the state from storage
47
+ def load_state!(channel)
48
+ @state = @channel_factory.load_state(channel)
49
+ end
50
+
51
+ # stores the command into state
52
+ def command=(cmd)
53
+ log_debug('Message set as command', command: cmd)
54
+ state.command = cmd
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require_relative 'command/command'
5
+ require_relative 'command/command_definition'
6
+ require_relative 'command/execution_context'
7
+
8
+ module Kybus
9
+ module Bot
10
+ class CommandExecutor
11
+ extend Forwardable
12
+
13
+ include Kybus::Logger
14
+ attr_reader :dsl, :bot, :execution_context
15
+
16
+ def_delegator :execution_context, :save!, :save_execution_context!
17
+
18
+ def state
19
+ execution_context&.state
20
+ end
21
+
22
+ def initialize(bot, channel_factory, inline_args)
23
+ @bot = bot
24
+ @channel_factory = channel_factory
25
+ @dsl = DSLMethods.new(bot.provider, state)
26
+ @inline_args = inline_args
27
+ @precommand_hook = proc {}
28
+ end
29
+
30
+ # Process a single message, this method can be overwriten to enable
31
+ # more complex implementations of commands. It receives a message object.
32
+ def process_message(message)
33
+ @execution_context = ExecutionContest.new(message.channel_id, @channel_factory)
34
+ save_token!(message)
35
+ run_command_or_prepare!
36
+ save_execution_context!
37
+ end
38
+
39
+ def save_param!(message)
40
+ execution_context.add_param(message.raw_message)
41
+ return unless message.has_attachment?
42
+
43
+ file = bot.provider.file_builder(message.attachment)
44
+ execution_context.add_file(file)
45
+ end
46
+
47
+ def search_command_with_inline_arg(message)
48
+ command, value = @channel_factory.command_with_inline_arg(message.raw_message)
49
+ if command
50
+ execution_context.command = command
51
+ execution_context.next_param = execution_context.next_missing_param
52
+ execution_context.add_param(value)
53
+ else
54
+ execution_context.command = @channel_factory.default_command
55
+ end
56
+ end
57
+
58
+ def save_token!(message)
59
+ if message.command?
60
+ command = @channel_factory.command(message.command)
61
+ if @inline_args && !command
62
+ search_command_with_inline_arg(message)
63
+ elsif !@inline_args && !command
64
+ execution_context.command = @channel_factory.default_command
65
+ else
66
+ execution_context.command = command
67
+ end
68
+ else
69
+ save_param!(message)
70
+ end
71
+ end
72
+
73
+ def run_command_or_prepare!
74
+ if execution_context.ready?
75
+ @dsl.state = execution_context.state
76
+ @dsl.instance_eval(&@precommand_hook)
77
+ run_command!
78
+ else
79
+ ask_param(execution_context.next_missing_param)
80
+ end
81
+ end
82
+
83
+ def precommand_hook(&block)
84
+ @precommand_hook = proc(&block)
85
+ end
86
+
87
+ def fallback(error)
88
+ catch = @channel_factory.command(error.class)
89
+ log_error('Unexpected error', error)
90
+ execution_context.command = catch if catch
91
+ end
92
+
93
+ # Method for triggering command
94
+ def run_command!
95
+ execution_context.call!(@dsl)
96
+ rescue StandardError => e
97
+ raise unless fallback(e)
98
+
99
+ retry
100
+ end
101
+
102
+ # Sends a message to get the next parameter from the user
103
+ def ask_param(param)
104
+ provider = bot.provider
105
+ msg = "I need you to tell me #{param}"
106
+ log_debug(msg)
107
+ provider.send_message(provider.last_message.channel_id, msg)
108
+ execution_context.next_param = param
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kybus
4
+ module Bot
5
+ class DSLMethods
6
+ attr_accessor :state
7
+ attr_reader :provider
8
+
9
+ def initialize(provider, state)
10
+ @provider = provider
11
+ @state = state
12
+ end
13
+
14
+ def send_message(content, channel = nil)
15
+ raise(Base::EmptyMessageError) unless content
16
+
17
+ provider.send_message(channel || current_channel, content)
18
+ end
19
+
20
+ def send_image(content, channel = nil, caption: nil)
21
+ provider.send_image(channel || current_channel, content, caption)
22
+ end
23
+
24
+ def send_video(content, channel = nil, caption: nil)
25
+ provider.send_video(channel || current_channel, content, caption)
26
+ end
27
+
28
+ def send_audio(content, channel = nil)
29
+ provider.send_audio(channel || current_channel, content)
30
+ end
31
+
32
+ def send_document(content, channel = nil)
33
+ provider.send_document(channel || current_channel, content)
34
+ end
35
+
36
+ def params
37
+ state.params
38
+ end
39
+
40
+ def files
41
+ state.files
42
+ end
43
+
44
+ def file(name)
45
+ (file = files[name]) && provider.file_builder(file)
46
+ end
47
+
48
+ def mention(name)
49
+ provider.mention(name)
50
+ end
51
+
52
+ def current_user
53
+ provider.last_message.user
54
+ end
55
+
56
+ def is_private?
57
+ provider.last_message.is_private?
58
+ end
59
+
60
+ def last_message
61
+ provider.last_message
62
+ end
63
+
64
+ # returns the current_channel from where the message was sent
65
+ def current_channel
66
+ state.channel_id
67
+ end
68
+ end
69
+ end
70
+ end
@@ -8,17 +8,14 @@ module Kybus
8
8
  # Base implementation for messages from distinct providers
9
9
  class Message
10
10
  # Converts the messages into a hash
11
- def to_h
12
- {
13
- text: raw_message,
14
- channel: channel_id
15
- }
16
- end
17
-
18
11
  # Returns true when the received message is a command. Convention states
19
12
  # that messages should start with '/' to be considered commands
20
13
  def command?
21
- raw_message&.split(' ')&.first&.start_with?('/')
14
+ command&.start_with?('/')
15
+ end
16
+
17
+ def command
18
+ raw_message&.split(' ')&.first
22
19
  end
23
20
  end
24
21
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'base'
2
4
  require_relative 'adapters/debug'
3
5
 
@@ -13,19 +15,24 @@ module Kybus
13
15
  'pool_size' => 1,
14
16
  'provider' => {
15
17
  'name' => 'debug',
16
- 'echo' => true,
18
+ 'echo' => false,
17
19
  'channels' => { 'testing' => [] }
18
20
  }
19
21
  }.freeze
20
22
 
21
- def self.make_test_bot
22
- new(CONFIG)
23
+ def self.make_test_bot(extra_configs = {})
24
+ new(CONFIG.merge(extra_configs))
23
25
  end
24
26
 
25
27
  def receives(msg, attachments = nil)
26
- msg = ::Kybus::Bot::Adapter::DebugMessage.new(msg, 'testing', attachments)
27
- @last_message = msg
28
- process_message(msg)
28
+ attachments = Adapter::DebugMessage::DebugFile.new(attachments) if attachments
29
+ msg = Adapter::DebugMessage.new(msg, 'testing', attachments)
30
+ provider.last_message = msg
31
+ executor.process_message(msg)
32
+ end
33
+
34
+ def expects(method)
35
+ executor.dsl.expects(method)
29
36
  end
30
37
  end
31
38
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kybus
4
4
  module Bot
5
- VERSION = '0.5.1'
5
+ VERSION = '0.7.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kybus-bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gilberto Vargas
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-16 00:00:00.000000000 Z
11
+ date: 2022-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: kybus-core
@@ -218,8 +218,16 @@ files:
218
218
  - lib/kybus/bot/adapters/debug.rb
219
219
  - lib/kybus/bot/adapters/discord.rb
220
220
  - lib/kybus/bot/adapters/telegram.rb
221
+ - lib/kybus/bot/adapters/telegram_file.rb
222
+ - lib/kybus/bot/adapters/telegram_message.rb
221
223
  - lib/kybus/bot/base.rb
222
- - lib/kybus/bot/command_definition.rb
224
+ - lib/kybus/bot/command/command.rb
225
+ - lib/kybus/bot/command/command_definition.rb
226
+ - lib/kybus/bot/command/command_state.rb
227
+ - lib/kybus/bot/command/command_state_factory.rb
228
+ - lib/kybus/bot/command/execution_context.rb
229
+ - lib/kybus/bot/command_executor.rb
230
+ - lib/kybus/bot/dsl_methods.rb
223
231
  - lib/kybus/bot/message.rb
224
232
  - lib/kybus/bot/migrator.rb
225
233
  - lib/kybus/bot/test.rb
@@ -228,7 +236,8 @@ files:
228
236
  homepage: https://github.com/tachomex/kybus
229
237
  licenses:
230
238
  - MIT
231
- metadata: {}
239
+ metadata:
240
+ rubygems_mfa_required: 'true'
232
241
  post_install_message:
233
242
  rdoc_options: []
234
243
  require_paths:
@@ -244,7 +253,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
244
253
  - !ruby/object:Gem::Version
245
254
  version: '0'
246
255
  requirements: []
247
- rubygems_version: 3.2.32
256
+ rubygems_version: 3.3.7
248
257
  signing_key:
249
258
  specification_version: 4
250
259
  summary: Provides a framework for building bots with ruby