kybus-bot 0.10.0 → 0.11.1

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: 5133b5a89dc9947680ecdbd8ec25befed895f57e017d19121278617b6bc0ff4d
4
- data.tar.gz: '08e309bdf344e7066a5354c516487b12764b85cca7ea38165054471e5280cba7'
3
+ metadata.gz: 6027eb3dbf33a839ee3b209eda5c8175a75372a3b03021cf171d07794f9ba1b2
4
+ data.tar.gz: 1271955a903acbbebecec80eca93302f144bd97ec70b93ad6c2d6171381942a7
5
5
  SHA512:
6
- metadata.gz: da4828437e972ecd01dab962800ae67f011a7ed8ceb88b9f1484d7c4caef740811dfd816f578620a5679a86768b21a00a976750d206c25cdcd7df66e4e3f4a6c
7
- data.tar.gz: c7bc12f54fb1352f774cbf607c12bbcae4802edc0d0058e99a38a7bb1554a8083b721a4e715e2185fd92fab668ece278e83ce4a2a15d43f28ddd23f5175cccfa
6
+ metadata.gz: bee0655c5cc486692a84b0e63cace81b3c0028cd10c88699d486d0e05714acb866f9afc68708b66e1bc3a5c5d94aebc43a04ceb875c99331d7d600faea02d39c
7
+ data.tar.gz: db7c59187ff02ca0931382d4ad49a9483047dff4ac6c55217aa68ffbcc6c6a9caa5ef4d5416b773e0a51c280e019d6865f359cb70f5689b9b127c6401190e0e7
@@ -35,14 +35,17 @@ module Kybus
35
35
  end
36
36
  end
37
37
 
38
+ def self.make_message_id
39
+ @message_id ||= 0
40
+ @message_id += 1
41
+ end
42
+
38
43
  def initialize(text, channel, attachment = nil)
39
44
  super()
40
45
  @text = text
41
46
  @channel = channel
42
47
  @attachment = attachment
43
- @@message_id ||= 0
44
- @@message_id += 1
45
- @message_id = @@message_id + 1
48
+ @message_id = DebugMessage.make_message_id
46
49
  end
47
50
 
48
51
  # Returns the channel id
@@ -125,7 +128,7 @@ module Kybus
125
128
  # This adapter is intended to be used on unit tests and development.
126
129
  class Debug
127
130
  # Exception for stoping the loop of messages
128
- class NoMoreMessageException < Kybus::Exceptions::AntError
131
+ class NoMoreMessageException < Kybus::Exceptions::KybusError
129
132
  def initialize
130
133
  super('There are no messages left')
131
134
  end
@@ -8,118 +8,70 @@ require_relative 'telegram_message'
8
8
  module Kybus
9
9
  module Bot
10
10
  module Adapter
11
- ##
12
- # This adapter is intended to be used on unit tests and development.
13
11
  class Telegram
14
12
  include ::Kybus::Logger
15
13
 
16
14
  attr_reader :last_message
17
15
 
18
- # It receives a hash with the configurations:
19
- # - name: the name of the channel
20
- # - channels a key value, where the key is a name and the value the
21
- # list of the messages in the channel.
22
- # - echo: a flag to enable debug messages.
23
16
  def initialize(configs)
24
17
  @config = configs
25
18
  @client = ::Telegram::Bot::Client.new(@config['token'])
26
19
  TelegramFile.register(:cli, @client)
27
20
  end
28
21
 
29
- # Interface for receiving message
30
22
  def read_message
31
- # take the first message from the first open message,
32
23
  loop do
33
24
  @client.listen do |message|
34
- log_info('Received message', message: message.to_h,
35
- from: message.from.to_h)
25
+ log_info('Received message', message: message.to_h, from: message.from.to_h)
36
26
  return @last_message = TelegramMessage.new(message)
37
27
  end
38
28
  rescue ::Telegram::Bot::Exceptions::ResponseError => e
39
- # :nocov:
40
- log_error('An error ocurred while calling to Telegram API', e)
41
- # :nocov:
29
+ log_error('An error occurred while calling to Telegram API', e)
42
30
  end
43
31
  end
44
32
 
45
33
  def handle_message(body)
46
34
  chat_id = body.dig('message', 'chat', 'id')
47
35
  message_id = body.dig('message', 'message_id')
48
- user = body.dig('message', 'from', 'username') || body.dig('message', 'from', 'first_name')
36
+ user = extract_user(body.dig('message', 'from'))
49
37
  raw_message = body.dig('message', 'text')
50
-
51
- replied_message = body.dig('message', 'reply_to_message')
52
38
  is_private = body.dig('message', 'chat', 'type') == 'private'
39
+ attachment = extract_attachment(body['message'])
40
+ serialized_replied_message = serialize_replied_message(body.dig('message', 'reply_to_message'))
53
41
 
54
- # Check if the message has an attachment
55
- has_attachment = body.dig('message',
56
- 'photo') || body.dig('message', 'document') || body.dig('message', 'video')
57
- attachment = if has_attachment
58
- body.dig('message',
59
- 'photo')&.last || body.dig('message', 'document') || body.dig('message', 'video')
60
- end
61
-
62
- # Serialize replied_message if it exists
63
- serialized_replied_message = if replied_message
64
- SerializedMessage.new(
65
- provider: 'telegram',
66
- channel_id: replied_message.dig('chat', 'id'),
67
- message_id: replied_message['message_id'],
68
- user: replied_message.dig('from',
69
- 'username') || replied_message.dig('from',
70
- 'first_name'),
71
- raw_message: replied_message['text'],
72
- is_private?: replied_message.dig('chat', 'type') == 'private'
73
- ).serialize
74
- end
75
-
76
- SerializedMessage.new(
77
- provider: 'telegram',
78
- channel_id: chat_id,
79
- message_id:,
80
- user:,
81
- replied_message: serialized_replied_message,
82
- raw_message:,
83
- is_private?: is_private,
84
- attachment:
85
- )
42
+ SerializedMessage.new(provider: 'telegram', channel_id: chat_id, message_id:, user:,
43
+ replied_message: serialized_replied_message, raw_message:, is_private?: is_private,
44
+ attachment:)
86
45
  end
87
46
 
88
47
  def mention(id)
89
48
  "[user](tg://user?id=#{id})"
90
49
  end
91
50
 
92
- # interface for sending messages
93
51
  def send_message(contents, channel_name)
94
52
  log_debug('Sending message', channel_name:, message: contents)
95
53
  @client.api.send_message(chat_id: channel_name.to_i, text: contents, parse_mode: @config['parse_mode'])
96
- # :nocov:
97
54
  rescue ::Telegram::Bot::Exceptions::ResponseError => e
98
55
  nil if e.error_code == '403'
99
56
  end
100
- # :nocov:
101
57
 
102
- # interface for sending video
103
58
  def send_video(channel_name, video_url, comment = nil)
104
59
  file = Faraday::FilePart.new(video_url, 'video/mp4')
105
60
  @client.api.send_video(chat_id: channel_name, video: file, caption: comment)
106
61
  end
107
62
 
108
- # interface for sending uadio
109
63
  def send_audio(channel_name, audio_url)
110
64
  file = Faraday::FilePart.new(audio_url, 'audio/mp3')
111
65
  @client.api.send_audio(chat_id: channel_name, audio: file)
112
66
  end
113
67
 
114
- # interface for sending image
115
68
  def send_image(channel_name, image_url, comment = nil)
116
69
  file = Faraday::FilePart.new(image_url, 'image/jpeg')
117
70
  @client.api.send_photo(chat_id: channel_name, photo: file, caption: comment)
118
71
  end
119
72
 
120
- # interface for sending document
121
73
  def send_document(channel_name, image_url)
122
- file = Faraday::FilePart.new(image_url, 'application/octect-stream')
74
+ file = Faraday::FilePart.new(image_url, 'application/octet-stream')
123
75
  @client.api.send_document(chat_id: channel_name, document: file)
124
76
  end
125
77
 
@@ -130,6 +82,35 @@ module Kybus
130
82
  def file_builder(file)
131
83
  TelegramFile.new(file)
132
84
  end
85
+
86
+ private
87
+
88
+ def extract_user(from)
89
+ from['username'] || from['first_name']
90
+ end
91
+
92
+ def extract_attachment(message)
93
+ return unless message
94
+
95
+ %w[photo document video].each do |type|
96
+ attachment = message[type]
97
+ return type == 'photo' ? attachment.last : attachment if attachment
98
+ end
99
+ nil
100
+ end
101
+
102
+ def serialize_replied_message(replied_message)
103
+ return unless replied_message
104
+
105
+ SerializedMessage.new(
106
+ provider: 'telegram',
107
+ channel_id: replied_message.dig('chat', 'id'),
108
+ message_id: replied_message['message_id'],
109
+ user: extract_user(replied_message['from']),
110
+ raw_message: replied_message['text'],
111
+ is_private?: replied_message.dig('chat', 'type') == 'private'
112
+ ).serialize
113
+ end
133
114
  end
134
115
 
135
116
  register('telegram', Telegram)
@@ -40,7 +40,7 @@ module Kybus
40
40
  end
41
41
 
42
42
  def original_name
43
- meta.dig('result', 'file_name')
43
+ meta.file_path
44
44
  end
45
45
 
46
46
  def file_name
@@ -49,7 +49,7 @@ module Kybus
49
49
 
50
50
  def download
51
51
  token = cli.api.token
52
- file_path = meta.dig('result', 'file_path')
52
+ file_path = meta.file_path
53
53
  path = "https://api.telegram.org/file/bot#{token}/#{file_path}"
54
54
  Faraday.get(path).body
55
55
  end
@@ -47,9 +47,13 @@ module Kybus
47
47
  end
48
48
 
49
49
  def attachment
50
- (@message.respond_to?(:document) && @message&.document) ||
51
- (@message.respond_to?(:photo) && @message.photo&.last) ||
52
- (@message.respond_to?(:audio) && @message&.audio)
50
+ %i[document photo audio].each do |method|
51
+ next unless @message.respond_to?(method)
52
+
53
+ attachment = @message.public_send(method)
54
+ return method == :photo ? attachment.last : attachment if attachment
55
+ end
56
+ nil
53
57
  end
54
58
 
55
59
  def user
@@ -6,25 +6,19 @@ require 'kybus/storage'
6
6
  require_relative 'command/command_state'
7
7
  require_relative 'dsl_methods'
8
8
  require_relative 'command_executor'
9
+ require_relative 'command/command_definition'
10
+ require_relative 'command/execution_context'
9
11
  require_relative 'command/command_state_factory'
10
-
12
+ require_relative 'exceptions'
13
+ require_relative 'forkers/base'
14
+ require_relative 'forkers/thread_forker'
15
+ require_relative 'forkers/lambda_sqs_forker'
11
16
  require 'kybus/logger'
12
17
  require 'forwardable'
13
18
 
14
19
  module Kybus
15
20
  module Bot
16
- # Base class for bot implementation. It wraps the threads execution, the
17
- # provider and the state storage inside an object.
18
- class Base
19
- class BotError < StandardError; end
20
- class AbortError < BotError; end
21
-
22
- class EmptyMessageError < BotError
23
- def initialize
24
- super('Message is empty')
25
- end
26
- end
27
-
21
+ class Base # rubocop: disable Metrics/ClassLength
28
22
  extend Forwardable
29
23
  include Kybus::Logger
30
24
 
@@ -44,41 +38,17 @@ module Kybus
44
38
  def_delegators :executor, :state, :precommand_hook
45
39
  def_delegators :definitions, :registered_commands
46
40
 
47
- # Configurations needed:
48
- # - pool_size: number of threads created in execution
49
- # - provider: a configuration for a thread provider.
50
- # See supported adapters
51
- # - name: The bot name
52
- # - repository: Configurations about the state storage
53
-
54
41
  def initialize(configs)
55
- build_pool(configs['pool_size'])
42
+ @pool_size = configs['pool_size']
56
43
  @provider = Kybus::Bot::Adapter.from_config(configs['provider'])
57
- # TODO: move this to config
58
- repository = make_repository(configs)
59
44
  @definitions = Kybus::Bot::CommandDefinition.new
45
+ repository = create_repository(configs)
60
46
  command_factory = CommandStateFactory.new(repository, @definitions)
61
- @executor = if configs['sidekiq']
62
- require_relative 'sidekiq_command_executor'
63
- Kybus::Bot::SidekiqCommandExecutor.new(self, command_factory, configs)
64
- else
65
- Kybus::Bot::CommandExecutor.new(self, command_factory, configs['inline_args'])
66
- end
67
- register_command('default') { nil }
68
- rescue_from(::Kybus::Bot::Base::AbortError) do
69
- msg = params[:_last_exception]&.message
70
- send_message(msg) if msg && msg != 'Kybus::Bot::Base::AbortError'
71
- end
72
- end
73
-
74
- def make_repository(configs)
75
- repository_config = configs['state_repository'].merge('primary_key' => 'channel_id', 'table' => 'bot_sessions')
76
- repository_config.merge!('fields' => DYNAMOID_FIELDS) if repository_config['name'] == 'dynamoid'
77
- Kybus::Storage::Repository.from_config(nil, repository_config, {})
78
- end
79
-
80
- def extend(*args)
81
- DSLMethods.include(*args)
47
+ @executor = create_executor(configs, command_factory)
48
+ register_default_command
49
+ register_abort_handler
50
+ build_forker(configs)
51
+ build_pool
82
52
  end
83
53
 
84
54
  def self.helpers(mod = nil, &)
@@ -86,14 +56,8 @@ module Kybus
86
56
  DSLMethods.class_eval(&) if block_given?
87
57
  end
88
58
 
89
- def build_pool(pool_size)
90
- @pool = Array.new(pool_size) do
91
- # TODO: Create a subclass with the context execution
92
- Kybus::DRY::Daemon.new(pool_size, true) do
93
- message = provider.read_message
94
- executor.process_message(message)
95
- end
96
- end
59
+ def extend(*)
60
+ DSLMethods.include(*)
97
61
  end
98
62
 
99
63
  def dsl
@@ -105,13 +69,14 @@ module Kybus
105
69
  @executor.process_message(parsed)
106
70
  end
107
71
 
108
- # Starts the bot execution, this is a blocking call.
72
+ def handle_job(job, args, channel_id)
73
+ @executor.execution_context.load_state!(channel_id)
74
+ @forker.handle_job(job, args)
75
+ end
76
+
109
77
  def run
110
- # TODO: Implement an interface for killing the process
111
78
  pool.each(&:run)
112
- # :nocov: #
113
79
  pool.each(&:await)
114
- # :nocov: #
115
80
  end
116
81
 
117
82
  def redirect(command, *params)
@@ -127,14 +92,63 @@ module Kybus
127
92
  definitions.register_command(klass, params, &)
128
93
  end
129
94
 
95
+ def register_job(name, args, &)
96
+ @forker.register_command(name, args, &)
97
+ end
98
+
99
+ def invoke_job(name, args)
100
+ @forker.fork(name, args, dsl)
101
+ end
102
+
130
103
  def rescue_from(klass, &)
131
104
  definitions.register_command(klass, [], &)
132
105
  end
133
106
 
134
- def method_missing(method, ...)
135
- raise unless dsl.respond_to?(method)
107
+ def method_missing(method, ...) # rubocop: disable Style/MissingRespondToMissing
108
+ return dsl.send(method, ...) if dsl.respond_to?(method)
136
109
 
137
- dsl.send(method, ...)
110
+ super
111
+ end
112
+
113
+ private
114
+
115
+ def create_repository(configs)
116
+ repository_config = configs['state_repository'].merge('primary_key' => 'channel_id', 'table' => 'bot_sessions')
117
+ repository_config.merge!('fields' => DYNAMOID_FIELDS) if repository_config['name'] == 'dynamoid'
118
+ Kybus::Storage::Repository.from_config(nil, repository_config, {})
119
+ end
120
+
121
+ def create_executor(configs, command_factory)
122
+ if configs['sidekiq']
123
+ require_relative 'sidekiq_command_executor'
124
+ Kybus::Bot::SidekiqCommandExecutor.new(self, command_factory, configs)
125
+ else
126
+ Kybus::Bot::CommandExecutor.new(self, command_factory, configs['inline_args'])
127
+ end
128
+ end
129
+
130
+ def register_default_command
131
+ register_command('default') { nil }
132
+ end
133
+
134
+ def register_abort_handler
135
+ rescue_from(Kybus::Bot::Base::AbortError) do
136
+ msg = params[:_last_exception]&.message
137
+ send_message(msg) if msg && msg != 'Kybus::Bot::Base::AbortError'
138
+ end
139
+ end
140
+
141
+ def build_forker(configs)
142
+ @forker = Forkers.from_config(self, configs['forker'])
143
+ end
144
+
145
+ def build_pool
146
+ @pool = Array.new(pool_size) do
147
+ Kybus::DRY::Daemon.new(pool_size, true) do
148
+ message = provider.read_message
149
+ executor.process_message(message)
150
+ end
151
+ end
138
152
  end
139
153
  end
140
154
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'command'
4
+
3
5
  module Kybus
4
6
  module Bot
5
7
  class CommandDefinition
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kybus
4
+ module Bot
5
+ class CommandHandler
6
+ def initialize(executor)
7
+ @executor = executor
8
+ end
9
+
10
+ def run_command_or_prepare!
11
+ if @executor.execution_context.ready?
12
+ run_ready_command
13
+ else
14
+ ask_for_next_param
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def run_ready_command
21
+ @executor.dsl.state = @executor.execution_context.state
22
+ @executor.dsl.instance_eval(&@executor.precommand_hook)
23
+ msg = @executor.run_command!
24
+ @executor.execution_context.clear_command
25
+ msg
26
+ end
27
+
28
+ def ask_for_next_param
29
+ param = @executor.execution_context.next_missing_param
30
+ @executor.ask_param(param, @executor.execution_context.state.command.params_ask_label(param))
31
+ end
32
+ end
33
+ end
34
+ end
@@ -7,12 +7,7 @@ module Kybus
7
7
 
8
8
  def initialize(data, command)
9
9
  @command = command
10
- data = JSON.parse(data, symbolize_names: true) if data.is_a?(String)
11
- (data[:params] = JSON.parse(data[:params] || '{}', symbolize_names: true)) if data[:params].is_a?(String)
12
- (data[:metadata] = JSON.parse(data[:metadata] || '{}', symbolize_names: true)) if data[:metadata].is_a?(String)
13
- (data[:files] = JSON.parse(data[:files] || '{}', symbolize_names: true)) if data[:files].is_a?(String)
14
- (data[:last_message] = data[:last_message] && SerializedMessage.from_json(data[:last_message]))
15
- @data = data
10
+ @data = parse_data(data)
16
11
  end
17
12
 
18
13
  def self.from_json(str, commands_provider)
@@ -36,12 +31,11 @@ module Kybus
36
31
  command&.ready?(params)
37
32
  end
38
33
 
39
- # validates which is the following parameter required
40
34
  def next_missing_param
41
35
  command.next_missing_param(params)
42
36
  end
43
37
 
44
- def set_last_message(msg)
38
+ def last_message=(msg)
45
39
  @data[:last_message] = msg
46
40
  end
47
41
 
@@ -91,12 +85,29 @@ module Kybus
91
85
 
92
86
  def save!
93
87
  backup = @data.clone
94
- %i[params files last_message metadata].each do |param|
95
- @data[param] = @data[param].to_json
96
- end
88
+ serialize_data!
97
89
  @data.store
90
+ @data = backup
91
+ end
92
+
93
+ private
94
+
95
+ def parse_data(data)
96
+ data = JSON.parse(data, symbolize_names: true) if data.is_a?(String)
97
+ %i[params metadata files].each do |key|
98
+ data[key] = parse_json(data[key]) if data[key].is_a?(String)
99
+ end
100
+ data[:last_message] = SerializedMessage.from_json(data[:last_message]) if data[:last_message]
101
+ data
102
+ end
103
+
104
+ def parse_json(value)
105
+ JSON.parse(value || '{}', symbolize_names: true)
106
+ end
107
+
108
+ def serialize_data!
98
109
  %i[params files last_message metadata].each do |param|
99
- @data[param] = backup[param]
110
+ @data[param] = @data[param].to_json
100
111
  end
101
112
  end
102
113
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'regular_command_matcher'
4
+ require_relative 'inline_command_matcher'
5
+
3
6
  module Kybus
4
7
  module Bot
5
8
  class CommandStateFactory
@@ -7,29 +10,14 @@ module Kybus
7
10
  attr_reader :factory
8
11
 
9
12
  def initialize(repository, definitions)
10
- factory = Kybus::Storage::Factory.new(EmptyModel)
11
- factory.register(:default, :json)
12
- factory.register(:json, repository)
13
- @factory = factory
13
+ @factory = build_factory(repository)
14
14
  @definitions = definitions
15
+ @regular_command_matcher = RegularCommandMatcher.new(definitions)
16
+ @inline_command_matcher = InlineCommandMatcher.new(definitions)
15
17
  end
16
18
 
17
19
  def command(search)
18
- @definitions.each do |name, command|
19
- case name
20
- when String
21
- return command if name == search
22
- when Class
23
- return command if search.is_a?(name)
24
- when Regexp
25
- if search.is_a?(String) && name.match?(search)
26
- storable_command = command.clone
27
- storable_command.name = search
28
- return storable_command
29
- end
30
- end
31
- end
32
- nil
20
+ @regular_command_matcher.find_command(search)
33
21
  end
34
22
 
35
23
  def default_command
@@ -41,21 +29,7 @@ module Kybus
41
29
  end
42
30
 
43
31
  def command_with_inline_arg(name_with_arg)
44
- @definitions.each do |name, command|
45
- case name
46
- when Class
47
- return [command, []] if name_with_arg.is_a?(name)
48
- when String
49
- return [command, name_with_arg.gsub(name, '').split('__')] if name_with_arg.start_with?(name)
50
- when Regexp
51
- next unless name_with_arg.match?(name)
52
-
53
- storable_command = command.dup
54
- storable_command.name = name_with_arg
55
- return [storable_command, [name_with_arg]]
56
- end
57
- end
58
- nil
32
+ @inline_command_matcher.find_command_with_inline_arg(name_with_arg)
59
33
  end
60
34
 
61
35
  def load_state(channel)
@@ -64,6 +38,15 @@ module Kybus
64
38
  rescue Kybus::Storage::Exceptions::ObjectNotFound
65
39
  CommandState.new(factory.create(channel_id: channel.to_s, params: '{}', metadata: '{}', last_message: nil), nil)
66
40
  end
41
+
42
+ private
43
+
44
+ def build_factory(repository)
45
+ factory = Kybus::Storage::Factory.new(EmptyModel)
46
+ factory.register(:default, :json)
47
+ factory.register(:json, repository)
48
+ factory
49
+ end
67
50
  end
68
51
  end
69
52
  end
@@ -9,7 +9,7 @@ module Kybus
9
9
  attr_reader :state
10
10
 
11
11
  def_delegator :state, :requested_param=, :next_param=
12
- def_delegators :state, :clear_command, :save!, :ready?, :next_missing_param, :set_last_message
12
+ def_delegators :state, :clear_command, :save!, :ready?, :next_missing_param, :last_message=
13
13
 
14
14
  def block
15
15
  state.command.block
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kybus
4
+ module Bot
5
+ class InlineCommandMatcher
6
+ def initialize(definitions)
7
+ @definitions = definitions
8
+ @matchers = build_matchers
9
+ end
10
+
11
+ def find_command_with_inline_arg(name_with_arg)
12
+ @definitions.each do |name, command|
13
+ matcher = @matchers[name.class]
14
+ result = matcher&.call(name, command, name_with_arg)
15
+ return result if result
16
+ end
17
+ nil
18
+ end
19
+
20
+ private
21
+
22
+ def build_matchers
23
+ {
24
+ Class => method(:match_inline_class),
25
+ String => method(:match_inline_string),
26
+ Regexp => method(:match_inline_regexp)
27
+ }
28
+ end
29
+
30
+ def match_inline_class(name, command, name_with_arg)
31
+ [command, []] if name_with_arg.is_a?(name)
32
+ end
33
+
34
+ def match_inline_string(name, command, name_with_arg)
35
+ [command, name_with_arg.gsub(name, '').split('__')] if name_with_arg.start_with?(name)
36
+ end
37
+
38
+ def match_inline_regexp(name, command, name_with_arg)
39
+ return unless name_with_arg.match?(name)
40
+
41
+ storable_command = command.dup
42
+ storable_command.name = name_with_arg
43
+ [storable_command, [name_with_arg]]
44
+ end
45
+ end
46
+ end
47
+ end