kybus-bot 0.5.1 → 0.7.0

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