command_bot 1.0.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 +7 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +36 -0
- data/.rubocop_todo.yml +7 -0
- data/.travis.yml +6 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +26 -0
- data/Rakefile +48 -0
- data/bin/console +12 -0
- data/command_bot.gemspec +27 -0
- data/examples/console_bot.rb +15 -0
- data/examples/helper.rb +49 -0
- data/examples/two_console_bots.rb +24 -0
- data/lib/command_bot.rb +10 -0
- data/lib/command_bot/bot.rb +117 -0
- data/lib/command_bot/command.rb +46 -0
- data/lib/command_bot/command_call.rb +57 -0
- data/lib/command_bot/command_identifier.rb +88 -0
- data/lib/command_bot/version.rb +6 -0
- metadata +64 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ce3490ea16754fc8c40d25e6a1c88ca15ecbb2457bb380b466c47797ad8d0af3
|
4
|
+
data.tar.gz: 7aff185e761c5770f853cb78053dc57b81c9ecc388ad61e93642ee98e8f2121d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b0ec894158f3d357ef9267c40549b5b8869184eda4b4beb7a30817ddd3440744108e0fa80d57a921ce493375dc050c40eed7f7e4916d808d8cdb5a77c2cc95a8
|
7
|
+
data.tar.gz: 831ea48cfaf594e2375a651798a1a0f775c09bbcbb6854a4a383674e87f2a0296ad75a22c190717f87bd17abfbd4210eb065da94cb839002b133ab35d80043e4
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
inherit_from: .rubocop_todo.yml
|
2
|
+
|
3
|
+
require:
|
4
|
+
- rubocop-performance
|
5
|
+
|
6
|
+
AllCops:
|
7
|
+
DisplayCopNames: true
|
8
|
+
DisplayStyleGuide: true
|
9
|
+
ExtraDetails: false
|
10
|
+
TargetRubyVersion: 2.6
|
11
|
+
Exclude:
|
12
|
+
- config/**/*
|
13
|
+
- tmp/**/*
|
14
|
+
- Capfile
|
15
|
+
- Gemfile
|
16
|
+
- Rakefile
|
17
|
+
|
18
|
+
# Follow RubyGuides on this one
|
19
|
+
Layout/EndOfLine:
|
20
|
+
EnforcedStyle: lf
|
21
|
+
|
22
|
+
# Just enough
|
23
|
+
Layout/LineLength:
|
24
|
+
Max: 100
|
25
|
+
|
26
|
+
# Follow RubyGuides on this one
|
27
|
+
Style/HashSyntax:
|
28
|
+
EnforcedStyle: ruby19_no_mixed_keys
|
29
|
+
|
30
|
+
# Prefer using Rubies methods
|
31
|
+
Style/HashEachMethods:
|
32
|
+
Enabled: true
|
33
|
+
Style/HashTransformKeys:
|
34
|
+
Enabled: true
|
35
|
+
Style/HashTransformValues:
|
36
|
+
Enabled: true
|
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2020-06-12 22:08:35 +0300 using RuboCop version 0.85.1.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Fizvlad
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# CommandBot
|
2
|
+
|
3
|
+
This gem can be used to create highly configurable text bots.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add gem to your Gemfile using
|
8
|
+
|
9
|
+
$ bundle add command_bot
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
See `examples` folder.
|
14
|
+
|
15
|
+
## Development
|
16
|
+
|
17
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
18
|
+
|
19
|
+
## Contributing
|
20
|
+
|
21
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/fizvlad/command_bot.
|
22
|
+
|
23
|
+
|
24
|
+
## License
|
25
|
+
|
26
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
|
3
|
+
require 'rubocop/rake_task'
|
4
|
+
RuboCop::RakeTask.new(:rubocop) {}
|
5
|
+
|
6
|
+
require 'rake/testtask'
|
7
|
+
Rake::TestTask.new do |t|
|
8
|
+
t.libs << 'test'
|
9
|
+
t.test_files = FileList['test/test*.rb']
|
10
|
+
t.verbose = true
|
11
|
+
end
|
12
|
+
|
13
|
+
namespace 'rubocop' do
|
14
|
+
desc 'Generate rubocop TODO file.'
|
15
|
+
task 'todo' do
|
16
|
+
puts `rubocop --auto-gen-config`
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
namespace 'yardoc' do
|
21
|
+
desc 'Generate documentation'
|
22
|
+
task 'generate' do
|
23
|
+
puts `yardoc lib/*`
|
24
|
+
end
|
25
|
+
|
26
|
+
desc 'List undocumented elements'
|
27
|
+
task 'undoc' do
|
28
|
+
puts `yardoc stats --list-undoc lib/*`
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
namespace 'eol' do
|
33
|
+
desc 'Replace CRLF with LF.'
|
34
|
+
task :dos2unix, [:pattern] do |_t, args|
|
35
|
+
path_list = Dir.glob(args.pattern || '**/*.{rb,rake}', File::FNM_EXTGLOB)
|
36
|
+
counter = 0
|
37
|
+
path_list.each do |path|
|
38
|
+
next unless File.file?(path)
|
39
|
+
|
40
|
+
counter += 1
|
41
|
+
puts "Handling `#{path}`..."
|
42
|
+
content = File.read(path, mode: 'rb')
|
43
|
+
content.gsub!(/\r\n/, "\n")
|
44
|
+
File.write(path, content, mode: 'wb')
|
45
|
+
end
|
46
|
+
puts "Handled #{counter} files."
|
47
|
+
end
|
48
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'command_bot'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
require 'pry'
|
12
|
+
Pry.start
|
data/command_bot.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/command_bot/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'command_bot'
|
7
|
+
spec.version = CommandBot::VERSION
|
8
|
+
spec.authors = ['Fizvlad']
|
9
|
+
spec.email = ['fizvlad@mail.ru']
|
10
|
+
|
11
|
+
spec.summary = 'General purpose text bots'
|
12
|
+
spec.homepage = 'https://github.com/fizvlad/command_bot'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.6.5')
|
15
|
+
|
16
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
17
|
+
spec.metadata['source_code_uri'] = 'https://github.com/fizvlad/command_bot'
|
18
|
+
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
21
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
22
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
23
|
+
end
|
24
|
+
spec.bindir = 'exe'
|
25
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ['lib']
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
bot = CommandBot::Bot.new(identifier: CommandBot::CommandIdentifier.primitive)
|
6
|
+
bot.add_commands(PingCommand, HelpCommand)
|
7
|
+
|
8
|
+
puts 'Awaiting input:'
|
9
|
+
loop do
|
10
|
+
text = gets.chomp
|
11
|
+
break if text.empty?
|
12
|
+
|
13
|
+
re = bot.handle(text)
|
14
|
+
puts re if re
|
15
|
+
end
|
data/examples/helper.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
4
|
+
require 'command_bot'
|
5
|
+
|
6
|
+
PingCommand = CommandBot::Command.new(
|
7
|
+
name: 'ping',
|
8
|
+
aliases: ['p'],
|
9
|
+
data: {
|
10
|
+
description: 'ping-pong',
|
11
|
+
description_long: 'Ping-pong.'
|
12
|
+
}
|
13
|
+
) do |_bot, _arguments, _options, *_other|
|
14
|
+
'pong'
|
15
|
+
end
|
16
|
+
|
17
|
+
HelpCommand = CommandBot::Command.new(
|
18
|
+
name: 'help',
|
19
|
+
aliases: ['h'],
|
20
|
+
data: {
|
21
|
+
description: 'show help message',
|
22
|
+
description_long: 'Show help message for specified command. Shows list of ' \
|
23
|
+
'command if atgument is not specified.',
|
24
|
+
another_info: 'you can pass any data here'
|
25
|
+
}
|
26
|
+
) do |bot, arguments, _options, *_other|
|
27
|
+
commands = if arguments.empty?
|
28
|
+
bot.commands
|
29
|
+
else
|
30
|
+
bot.commands.select { |e| arguments.include? e.name }
|
31
|
+
end
|
32
|
+
|
33
|
+
help_line = if arguments.empty?
|
34
|
+
proc do |e|
|
35
|
+
"#{e.name} (also known as #{e.aliases.join(', ')}) " \
|
36
|
+
"- #{e.data[:description]}"
|
37
|
+
end
|
38
|
+
else
|
39
|
+
proc do |e|
|
40
|
+
"#{e.name} (also known as #{e.aliases.join(', ')}) " \
|
41
|
+
"- #{e.data[:description]}\n#{e.data[:description_long]}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
<<~HELP
|
46
|
+
Commands list:
|
47
|
+
#{commands.map(&help_line).join("\n")}
|
48
|
+
HELP
|
49
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
command_identifier1 = CommandBot::CommandIdentifier.default(prefix: '!')
|
6
|
+
command_identifier2 = CommandBot::CommandIdentifier.default(prefix: '?')
|
7
|
+
|
8
|
+
bot1 = CommandBot::Bot.new(identifier: command_identifier1, log_level: Logger::DEBUG)
|
9
|
+
bot1.add_commands(PingCommand, HelpCommand)
|
10
|
+
|
11
|
+
bot2 = CommandBot::Bot.new(identifier: command_identifier2)
|
12
|
+
bot2.add_commands(HelpCommand, PingCommand)
|
13
|
+
|
14
|
+
puts 'Awaiting input:'
|
15
|
+
loop do
|
16
|
+
text = gets.chomp
|
17
|
+
break if text.empty?
|
18
|
+
|
19
|
+
re1 = bot1.handle(text)
|
20
|
+
puts re1 if re1
|
21
|
+
|
22
|
+
re2 = bot2.handle(text)
|
23
|
+
puts re2 if re2
|
24
|
+
end
|
data/lib/command_bot.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module CommandBot
|
6
|
+
# Main class.
|
7
|
+
class Bot
|
8
|
+
# Initialize new bot.
|
9
|
+
# @paran identifier [CommandIdentifier] defaults to {CommandIdentifier.default}.
|
10
|
+
# @param logger [Logger]
|
11
|
+
# @param log_level [Logger::Severity]
|
12
|
+
# @param command_not_found [Proc] process to execute when command can't be
|
13
|
+
# identified. Accepts single argument of {CommandCall}. Defaults to returning +nil+.
|
14
|
+
def initialize(identifier: nil, logger: nil, log_level: Logger::INFO, command_not_found: nil)
|
15
|
+
@identifier = identifier || CommandIdentifier.default
|
16
|
+
@command_not_found = command_not_found || proc { |_command_call| nil }
|
17
|
+
|
18
|
+
@commands = []
|
19
|
+
|
20
|
+
log_formatter = proc do |severity, datetime, progname, msg|
|
21
|
+
"[#{datetime}] #{severity} - #{progname || object_id}:\t #{msg}\n"
|
22
|
+
end
|
23
|
+
@logger = logger || Logger.new(STDOUT, formatter: log_formatter)
|
24
|
+
@logger.level = log_level
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Array<Command>]
|
28
|
+
attr_reader :commands
|
29
|
+
|
30
|
+
# @param name [String]
|
31
|
+
# @return [Command, nil]
|
32
|
+
def find_command(name)
|
33
|
+
logger.debug "Searching for command `#{name}`"
|
34
|
+
commands.find { |c| c.name_matches?(name) }
|
35
|
+
end
|
36
|
+
|
37
|
+
# Add new commands.
|
38
|
+
# @param commands [Command]
|
39
|
+
def add_commands(*commands)
|
40
|
+
logger.debug "Adding #{commands.size} new commands"
|
41
|
+
all_aliases_array = all_aliases
|
42
|
+
commands.each { |c| add_command_unless_alias_is_in_array(c, all_aliases_array) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Remove commands.
|
46
|
+
# @param commands [Command, String]
|
47
|
+
def remove_commands(*commands)
|
48
|
+
logger.debug "Removing #{commands.size} commands"
|
49
|
+
command_names = commands.map { |c| c.is_a?(String) ? c : c.name }
|
50
|
+
@commands.reject! { |ec| ec.name_matches?(*command_names) }
|
51
|
+
end
|
52
|
+
|
53
|
+
# Handle message.
|
54
|
+
# @param text [String] message text.
|
55
|
+
# @param data [Hash] additional data which should be passed to handler procedure.
|
56
|
+
# @return [void, nil] result depends on handler of command.
|
57
|
+
def handle(text, data = {})
|
58
|
+
logger.debug "Handling text: `#{text}` with additional data: #{data}"
|
59
|
+
command_call = identify_command_call(text, data)
|
60
|
+
return nil if command_call.nil? # Not a command call, so not handling.
|
61
|
+
|
62
|
+
handle_command_call(command_call)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Hadnle {CommandCall}.
|
66
|
+
# @param command_call [CommandCall]
|
67
|
+
# @return [void, nil] result depends on handler of command.
|
68
|
+
def handle_command_call(command_call)
|
69
|
+
logger.debug "Hadnling command call: #{command_call}"
|
70
|
+
if command_call.command
|
71
|
+
execute_command_call(command_call)
|
72
|
+
else
|
73
|
+
command_not_found(command_call)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# @return [Logger]
|
80
|
+
attr_reader :logger
|
81
|
+
|
82
|
+
# @return [Array<String>]
|
83
|
+
def all_aliases
|
84
|
+
re = @commands.map(&:all_aliases)
|
85
|
+
re.flatten!
|
86
|
+
re
|
87
|
+
end
|
88
|
+
|
89
|
+
# @note modifies +all_aliases_array+
|
90
|
+
def add_command_unless_alias_is_in_array(command, all_aliases_array)
|
91
|
+
command_aliases = command.all_aliases
|
92
|
+
intersection = all_aliases_array & command_aliases
|
93
|
+
if intersection.empty?
|
94
|
+
@commands << command
|
95
|
+
all_aliases_array.concat(command_aliases)
|
96
|
+
else
|
97
|
+
logger.warn "Command #{command.name} will not be added due to name intersection!"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# @return [CommandCall, nil]
|
102
|
+
def identify_command_call(text, data)
|
103
|
+
@identifier.call(self, text, data)
|
104
|
+
end
|
105
|
+
|
106
|
+
# @return [void]
|
107
|
+
def execute_command_call(command_call)
|
108
|
+
command_call.execute
|
109
|
+
# NOTE: it is possible to execute it from here, but IHMO command_call should
|
110
|
+
# be able to execute itself.
|
111
|
+
end
|
112
|
+
|
113
|
+
def command_not_found(command_call)
|
114
|
+
@command_not_found.call(command_call)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandBot
|
4
|
+
# Stores command handler.
|
5
|
+
class Command
|
6
|
+
# Initalize new command.
|
7
|
+
# @param name [String]
|
8
|
+
# @param aliases [Array<String>]
|
9
|
+
# @param data [Hash]
|
10
|
+
# @yieldparam bot [Bot]
|
11
|
+
# @yieldparam arguments [Array<String>]
|
12
|
+
# @yieldparam options [Hash<String, String>]
|
13
|
+
# @yieldparam other [Array<void>]
|
14
|
+
# @yieldreturn [void]
|
15
|
+
def initialize(name: '', aliases: [], data: {}, &handler)
|
16
|
+
@name = name
|
17
|
+
@aliases = aliases
|
18
|
+
@data = data
|
19
|
+
|
20
|
+
@handler = handler
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [String]
|
24
|
+
attr_reader :name
|
25
|
+
|
26
|
+
# @return [Array<String>]
|
27
|
+
attr_reader :aliases
|
28
|
+
|
29
|
+
# @return [Hash]
|
30
|
+
attr_reader :data
|
31
|
+
|
32
|
+
# @return [Proc]
|
33
|
+
attr_reader :handler
|
34
|
+
|
35
|
+
# @return [Array<String>]
|
36
|
+
def all_aliases
|
37
|
+
aliases + [name]
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param names [Array<String>]
|
41
|
+
# @return [Boolean]
|
42
|
+
def name_matches?(*names)
|
43
|
+
all_aliases.any? { |a| names.include?(a) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandBot
|
4
|
+
# Stores command call information.
|
5
|
+
class CommandCall
|
6
|
+
# New command call.
|
7
|
+
# @param bot [Bot]
|
8
|
+
# @param text [String]
|
9
|
+
# @param data [Hash]
|
10
|
+
def initialize(bot, text, data)
|
11
|
+
@bot = bot
|
12
|
+
@text = text
|
13
|
+
@data = data
|
14
|
+
|
15
|
+
@command = nil
|
16
|
+
@arguments = []
|
17
|
+
@options = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Bot]
|
21
|
+
attr_reader :bot
|
22
|
+
|
23
|
+
# @return [String]
|
24
|
+
attr_reader :text
|
25
|
+
|
26
|
+
# @return [Hash]
|
27
|
+
attr_reader :data
|
28
|
+
|
29
|
+
# @return [Command, nil] +nil+ if command can't be found.
|
30
|
+
attr_accessor :command
|
31
|
+
|
32
|
+
# @return [Array<String>]
|
33
|
+
attr_accessor :arguments
|
34
|
+
|
35
|
+
# @return [Hash<String, String>]
|
36
|
+
attr_accessor :options
|
37
|
+
|
38
|
+
# Executes command.
|
39
|
+
# @return [void]
|
40
|
+
def execute
|
41
|
+
raise 'Command not identified!' if command.nil?
|
42
|
+
|
43
|
+
command.handler.call(bot, arguments, options, data)
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [String]
|
47
|
+
def to_s
|
48
|
+
if command.nil?
|
49
|
+
"COMMAND_NOT_FOUND(`#{text}`)"
|
50
|
+
else
|
51
|
+
arg_str = arguments.join(', ')
|
52
|
+
opt_str = options.map { |k, v| "#{k}: #{v}" }.join(', ')
|
53
|
+
"#{command.name}[#{arg_str}]{#{opt_str}}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CommandBot
|
4
|
+
# Stores command call information.
|
5
|
+
class CommandIdentifier
|
6
|
+
# Create new identifier.
|
7
|
+
# @yieldparam bot [Bot]
|
8
|
+
# @yieldparam text [String]
|
9
|
+
# @yieldparam data [Hash]
|
10
|
+
# @yieldreturn [CommandCall, nil] +nil+ if not a command.
|
11
|
+
def initialize(&identifier)
|
12
|
+
@identifier = identifier
|
13
|
+
end
|
14
|
+
|
15
|
+
# Call identifier process.
|
16
|
+
# @param text [String]
|
17
|
+
# @param data [Hash]
|
18
|
+
# @return [CommandCall, nil] +nil+ if not a command.
|
19
|
+
def call(bot, text, data)
|
20
|
+
@identifier.call(bot, text, data)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param prefix [String] command prefix.
|
24
|
+
# @param disallow_whitespace [Boolean] whether to disallow whitespace
|
25
|
+
# between prefix and command name.
|
26
|
+
# @param o_prefix [String] prefix of option key.
|
27
|
+
# @param o_separator [String] separator between option and value.
|
28
|
+
# @return [CommandIdentifier]
|
29
|
+
def self.default(prefix: '', disallow_whitespace: false, o_prefix: '--', o_separator: '=') # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
30
|
+
new do |bot, text_original, data|
|
31
|
+
command_call = CommandCall.new(bot, text_original, data)
|
32
|
+
text = text_original.dup
|
33
|
+
|
34
|
+
# Not a command if got prefix and text do not start with it
|
35
|
+
next nil unless prefix.empty? || text.delete_prefix!(prefix)
|
36
|
+
# Check for whitespace
|
37
|
+
next nil if disallow_whitespace && text.lstrip!
|
38
|
+
|
39
|
+
words = text.split
|
40
|
+
next nil if words.empty? # Just a prefix
|
41
|
+
|
42
|
+
command_name = words.shift
|
43
|
+
command_name&.downcase! # Usually commands should not depend on case, so downcasing
|
44
|
+
|
45
|
+
options = _get_options(words, o_prefix, o_separator)
|
46
|
+
|
47
|
+
command_call.command = bot.find_command(command_name)
|
48
|
+
command_call.arguments = words # Everything left is considered to be arguments
|
49
|
+
command_call.options = options
|
50
|
+
command_call
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [CommandIdentifier] primitive optionless identifier.
|
55
|
+
def self.primitive
|
56
|
+
new do |bot, text, data|
|
57
|
+
command_call = CommandCall.new(bot, text, data)
|
58
|
+
words = text.split
|
59
|
+
command_call.command = bot.find_command(words.shift || '')
|
60
|
+
command_call.arguments = words
|
61
|
+
command_call.options = {}
|
62
|
+
command_call
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Utility method to get options hash from words array (it *will* be modified).
|
67
|
+
# @param words [Array<String>]
|
68
|
+
# @param prefix [String] prefix of option key.
|
69
|
+
# @param separator [String] separator between option and value.
|
70
|
+
def self._get_options(words, prefix, separator)
|
71
|
+
options = {}
|
72
|
+
while words.first&.start_with?(prefix)
|
73
|
+
str = words.shift
|
74
|
+
str.downcase!
|
75
|
+
str.delete_prefix! prefix
|
76
|
+
|
77
|
+
# Need to check for splitter in key
|
78
|
+
key, *, value = str.partition(separator)
|
79
|
+
|
80
|
+
# Following line allows writing '!command --key = val' with words=['--key', '=', 'val']
|
81
|
+
*, value = words.shift(2) if value.empty? && words.first == separator
|
82
|
+
|
83
|
+
options[key] = value || ''
|
84
|
+
end
|
85
|
+
options
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: command_bot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Fizvlad
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-06-14 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
- fizvlad@mail.ru
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".gitignore"
|
21
|
+
- ".rubocop.yml"
|
22
|
+
- ".rubocop_todo.yml"
|
23
|
+
- ".travis.yml"
|
24
|
+
- Gemfile
|
25
|
+
- LICENSE.txt
|
26
|
+
- README.md
|
27
|
+
- Rakefile
|
28
|
+
- bin/console
|
29
|
+
- command_bot.gemspec
|
30
|
+
- examples/console_bot.rb
|
31
|
+
- examples/helper.rb
|
32
|
+
- examples/two_console_bots.rb
|
33
|
+
- lib/command_bot.rb
|
34
|
+
- lib/command_bot/bot.rb
|
35
|
+
- lib/command_bot/command.rb
|
36
|
+
- lib/command_bot/command_call.rb
|
37
|
+
- lib/command_bot/command_identifier.rb
|
38
|
+
- lib/command_bot/version.rb
|
39
|
+
homepage: https://github.com/fizvlad/command_bot
|
40
|
+
licenses:
|
41
|
+
- MIT
|
42
|
+
metadata:
|
43
|
+
homepage_uri: https://github.com/fizvlad/command_bot
|
44
|
+
source_code_uri: https://github.com/fizvlad/command_bot
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.6.5
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubygems_version: 3.1.0.pre2
|
61
|
+
signing_key:
|
62
|
+
specification_version: 4
|
63
|
+
summary: General purpose text bots
|
64
|
+
test_files: []
|