pechkin 1.2.0 → 1.2.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 +4 -4
- data/lib/pechkin.rb +4 -66
- data/lib/pechkin/auth.rb +25 -14
- data/lib/pechkin/cli.rb +6 -11
- data/lib/pechkin/command.rb +38 -0
- data/lib/pechkin/command/add_auth.rb +17 -0
- data/lib/pechkin/command/base.rb +40 -0
- data/lib/pechkin/command/check.rb +14 -0
- data/lib/pechkin/command/list.rb +42 -0
- data/lib/pechkin/command/run_server.rb +17 -0
- data/lib/pechkin/command/send_data.rb +46 -0
- data/lib/pechkin/configuration.rb +0 -19
- data/lib/pechkin/connector.rb +2 -2
- data/lib/pechkin/handler.rb +29 -10
- data/lib/pechkin/version.rb +1 -1
- metadata +23 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3447abc49727b844f0c86895242a10aa1c3cee00e1fbdff5deb915ba790ebe98
|
4
|
+
data.tar.gz: 3352b4e61c0a9885d8e0b14b497e41947952b996bd522e238ac519100965ff2d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 81fbdfd449664ed2715035649db5f3c9f31740ca9ade3295d5eab19415e6cc3ea9eae0652e99810c0ff3317b8815032a10e4e257246802efee3d31e247d4dbe9
|
7
|
+
data.tar.gz: 84ce0aa1dfb906e2bae2221e85fe77bdc48ec160ad6bf494c6ba1fcf64bb6a7c274a479a22825954cf66dad8470546cec9283a120434f85be0b7a17182eb4a22
|
data/lib/pechkin.rb
CHANGED
@@ -3,10 +3,12 @@ require 'rack'
|
|
3
3
|
require 'logger'
|
4
4
|
require 'prometheus/middleware/collector'
|
5
5
|
require 'prometheus/middleware/exporter'
|
6
|
+
require 'powerpack/string'
|
6
7
|
require 'htauth'
|
7
8
|
require 'base64'
|
8
9
|
|
9
10
|
require_relative 'pechkin/cli'
|
11
|
+
require_relative 'pechkin/command'
|
10
12
|
require_relative 'pechkin/exceptions'
|
11
13
|
require_relative 'pechkin/handler'
|
12
14
|
require_relative 'pechkin/message_template'
|
@@ -24,76 +26,12 @@ module Pechkin # :nodoc:
|
|
24
26
|
class << self
|
25
27
|
def run
|
26
28
|
options = CLI.parse(ARGV)
|
27
|
-
|
29
|
+
cmd = Command::Dispatcher.new(options).dispatch
|
30
|
+
cmd.execute
|
28
31
|
rescue StandardError => e
|
29
32
|
warn 'Error: ' + e.message
|
30
33
|
warn "\t" + e.backtrace.reverse.join("\n\t") if options.debug?
|
31
34
|
exit 2
|
32
35
|
end
|
33
36
|
end
|
34
|
-
|
35
|
-
class Main # :nodoc:
|
36
|
-
attr_reader :options, :configuration, :handler
|
37
|
-
|
38
|
-
def initialize(options)
|
39
|
-
@options = options
|
40
|
-
end
|
41
|
-
|
42
|
-
def run
|
43
|
-
if options.add_auth
|
44
|
-
add_auth
|
45
|
-
exit 0
|
46
|
-
end
|
47
|
-
|
48
|
-
@configuration = Configuration.load_from_directory(options.config_file)
|
49
|
-
@handler = Handler.new(@configuration.channels)
|
50
|
-
configuration.list if options.list?
|
51
|
-
return if options.check?
|
52
|
-
|
53
|
-
if options.send_data
|
54
|
-
send_data
|
55
|
-
else
|
56
|
-
run_server
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
def run_server
|
61
|
-
Rack::Server.start(app: AppBuilder.new.build(handler, options),
|
62
|
-
Port: options.port,
|
63
|
-
pid: options.pid_file,
|
64
|
-
Host: options.bind_address)
|
65
|
-
end
|
66
|
-
|
67
|
-
def send_data
|
68
|
-
ch, msg = parse_endpoint(options.send_data)
|
69
|
-
|
70
|
-
raise "#{ch}/#{msg} not found" unless handler.message?(ch, msg)
|
71
|
-
|
72
|
-
data = read_data(options.data)
|
73
|
-
|
74
|
-
handler.preview = options.preview
|
75
|
-
handler.handle(ch, msg, JSON.parse(data))
|
76
|
-
end
|
77
|
-
|
78
|
-
def read_data(data)
|
79
|
-
return data unless data.start_with?('@')
|
80
|
-
|
81
|
-
file = data[1..-1]
|
82
|
-
raise "File not found #{file}" unless File.exist?(file)
|
83
|
-
|
84
|
-
IO.read(file)
|
85
|
-
end
|
86
|
-
|
87
|
-
def parse_endpoint(endpoint)
|
88
|
-
endpoint.match(%r{^([^/]+)/(.+)}) do |m|
|
89
|
-
[m[1], m[2]]
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
def add_auth
|
94
|
-
user, password = options.add_auth.split(':')
|
95
|
-
Pechkin::Auth::Manager.new(options.htpasswd).add(user, password)
|
96
|
-
puts IO.read(options.htpasswd)
|
97
|
-
end
|
98
|
-
end
|
99
37
|
end
|
data/lib/pechkin/auth.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
module Pechkin
|
2
2
|
module Auth
|
3
|
+
class AuthError < StandardError; end
|
4
|
+
|
3
5
|
# Utility class for altering htpasswd files
|
4
6
|
class Manager
|
5
7
|
attr_reader :htpasswd
|
@@ -25,32 +27,41 @@ module Pechkin
|
|
25
27
|
end
|
26
28
|
|
27
29
|
def call(env)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
end
|
30
|
+
authorize(env)
|
31
|
+
@app.call(env)
|
32
|
+
rescue AuthError => e
|
33
|
+
body = { status: 'error', reason: e.message }.to_json
|
34
|
+
['401', { 'Content-Type' => 'application/json' }, [body]]
|
34
35
|
rescue StandardError => e
|
35
|
-
puts e.backtrace.reverse.join('\n\t')
|
36
36
|
body = { status: 'error', reason: e.message }.to_json
|
37
37
|
['503', { 'Content-Type' => 'application/json' }, [body]]
|
38
38
|
end
|
39
39
|
|
40
40
|
private
|
41
41
|
|
42
|
-
def
|
43
|
-
return
|
42
|
+
def authorize(env)
|
43
|
+
return unless htpasswd
|
44
44
|
|
45
|
-
auth = env['HTTP_AUTHORIZATION']
|
46
|
-
|
47
|
-
|
48
|
-
|
45
|
+
auth = env['HTTP_AUTHORIZATION']
|
46
|
+
raise AuthError, 'Auth header is missing' unless auth
|
47
|
+
|
48
|
+
match = auth.match(/^Basic (.*)$/)
|
49
|
+
raise AuthError, 'Auth is not basic' unless match
|
50
|
+
|
51
|
+
user, password = *Base64.decode64(match[1]).split(':')
|
52
|
+
check_auth(user, password)
|
49
53
|
end
|
50
54
|
|
51
55
|
def check_auth(user, password)
|
56
|
+
raise AuthError, 'User is missing' unless user
|
57
|
+
|
58
|
+
raise AuthError, 'Password is missing' unless password
|
59
|
+
|
52
60
|
e = htpasswd.fetch(user)
|
53
|
-
|
61
|
+
|
62
|
+
raise AuthError, "User '#{user}' not found" unless e
|
63
|
+
|
64
|
+
raise AuthError, "Can't authenticate" unless e.authenticated?(password)
|
54
65
|
end
|
55
66
|
end
|
56
67
|
end
|
data/lib/pechkin/cli.rb
CHANGED
@@ -38,13 +38,8 @@ module Pechkin
|
|
38
38
|
values = OpenStruct.new
|
39
39
|
parser = parser_create(values)
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
exit 2
|
44
|
-
else
|
45
|
-
parser.parse(args)
|
46
|
-
new.post_init(values)
|
47
|
-
end
|
41
|
+
parser.parse(args)
|
42
|
+
new.post_init(values)
|
48
43
|
end
|
49
44
|
|
50
45
|
def parser_create(values)
|
@@ -96,9 +91,9 @@ module Pechkin
|
|
96
91
|
|
97
92
|
separator 'Run options'
|
98
93
|
|
99
|
-
opt :
|
100
|
-
|
101
|
-
|
94
|
+
opt :config_dir, default: Dir.pwd,
|
95
|
+
names: ['-c', '--config-dir FILE'],
|
96
|
+
desc: 'Path to configuration file'
|
102
97
|
|
103
98
|
opt :port, names: ['--port PORT'], default: 9292, type: Integer
|
104
99
|
opt :bind_address, names: ['--address ADDRESS'], default: '127.0.0.1',
|
@@ -148,7 +143,7 @@ module Pechkin
|
|
148
143
|
desc: 'Print debug information and stack trace on errors'
|
149
144
|
|
150
145
|
def post_init(values)
|
151
|
-
default_htpasswd = File.join(values.
|
146
|
+
default_htpasswd = File.join(values.config_dir, PECHKIN_HTPASSWD_FILE)
|
152
147
|
values.htpasswd ||= default_htpasswd
|
153
148
|
|
154
149
|
values
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative 'command/base'
|
2
|
+
|
3
|
+
require_relative 'command/add_auth'
|
4
|
+
require_relative 'command/list'
|
5
|
+
require_relative 'command/check'
|
6
|
+
require_relative 'command/run_server'
|
7
|
+
require_relative 'command/send_data'
|
8
|
+
|
9
|
+
module Pechkin
|
10
|
+
# Contains general command processing.
|
11
|
+
module Command
|
12
|
+
# Dispatch command. Commands are placed in fixed order to allow matching
|
13
|
+
# rules be executed in right way. For example at first we check for
|
14
|
+
# --add-auth and than for --check. At the moment only RunServer should be
|
15
|
+
# last element of this sequence.
|
16
|
+
class Dispatcher
|
17
|
+
COMMANDS = [
|
18
|
+
AddAuth,
|
19
|
+
Check,
|
20
|
+
List,
|
21
|
+
SendData,
|
22
|
+
RunServer
|
23
|
+
].freeze
|
24
|
+
|
25
|
+
attr_reader :options
|
26
|
+
|
27
|
+
# @param cli_options [OpenStruct] command line options object
|
28
|
+
def initialize(cli_options)
|
29
|
+
@options = cli_options
|
30
|
+
end
|
31
|
+
|
32
|
+
# Dispatch command according to provided options
|
33
|
+
def dispatch
|
34
|
+
COMMANDS.map { |c| c.new(options) }.find(&:matches?)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Pechkin
|
2
|
+
module Command
|
3
|
+
# Read user:password combination and write it to htpasswd file. If file
|
4
|
+
# already contains user then record will be replaced
|
5
|
+
class AddAuth < Base
|
6
|
+
def matches?
|
7
|
+
options.add_auth
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute
|
11
|
+
user, password = options.add_auth.split(':')
|
12
|
+
Pechkin::Auth::Manager.new(options.htpasswd).add(user, password)
|
13
|
+
puts IO.read(options.htpasswd)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Pechkin
|
2
|
+
module Command
|
3
|
+
# Basic class for all commands
|
4
|
+
class Base
|
5
|
+
attr_reader :options
|
6
|
+
|
7
|
+
# Initializes command state
|
8
|
+
# @param options [OpenStruct] set of options which allows to configure
|
9
|
+
# command behaviour
|
10
|
+
# @opt stdout [IO] IO object which represents STDOUT
|
11
|
+
# @opt stderr [IO] IO object which represents STDERR
|
12
|
+
def initialize(options, stdout: STDOUT, stderr: STDERR)
|
13
|
+
@options = options
|
14
|
+
@stdout = stdout
|
15
|
+
@stderr = stderr
|
16
|
+
end
|
17
|
+
|
18
|
+
def configuration
|
19
|
+
config_dir = options.config_dir
|
20
|
+
@configuration ||= Configuration.load_from_directory(config_dir)
|
21
|
+
end
|
22
|
+
|
23
|
+
def handler
|
24
|
+
@handler ||= Handler.new(configuration.channels)
|
25
|
+
end
|
26
|
+
|
27
|
+
def matches?
|
28
|
+
raise 'Unimplemented'
|
29
|
+
end
|
30
|
+
|
31
|
+
def puts(*args)
|
32
|
+
@stdout.puts(*args)
|
33
|
+
end
|
34
|
+
|
35
|
+
def warn(*args)
|
36
|
+
@stderr.puts(*args)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Pechkin
|
2
|
+
module Command
|
3
|
+
BOT_ENTRY_FORMAT = ' %-25s %-10s %-60s '.freeze
|
4
|
+
CHAT_ENTRY_FORMAT = ' %-40s %-40s %-30s '.freeze
|
5
|
+
|
6
|
+
# List channels configuration
|
7
|
+
class List < Base
|
8
|
+
def matches?
|
9
|
+
options.list?
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute
|
13
|
+
cfg = configuration
|
14
|
+
|
15
|
+
puts "Working dir: #{cfg.working_dir}"
|
16
|
+
print_bots(cfg.bots)
|
17
|
+
print_channels(cfg.channels)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def print_bots(bots)
|
23
|
+
puts "\nBots:"
|
24
|
+
puts format(BOT_ENTRY_FORMAT, 'NAME', 'CONNECTOR', 'TOKEN')
|
25
|
+
bots.each do |name, bot|
|
26
|
+
puts format(BOT_ENTRY_FORMAT, name, bot.connector, bot.token)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def print_channels(channels)
|
31
|
+
puts "\nChannels:"
|
32
|
+
puts format(CHAT_ENTRY_FORMAT, 'CHANNEL', 'MESSAGE', 'BOT')
|
33
|
+
channels.each do |channel_name, channel|
|
34
|
+
channel.messages.each do |message_name, _message|
|
35
|
+
puts format(CHAT_ENTRY_FORMAT,
|
36
|
+
channel_name, message_name, channel.connector.name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Pechkin
|
2
|
+
module Command
|
3
|
+
# Start pechkin HTTP server
|
4
|
+
class RunServer < Base
|
5
|
+
def matches?
|
6
|
+
true # Always match
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute
|
10
|
+
Rack::Server.start(app: AppBuilder.new.build(handler, options),
|
11
|
+
Host: options.bind_adress,
|
12
|
+
Port: options.port,
|
13
|
+
pid: options.pid_file)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Pechkin
|
2
|
+
module Command
|
3
|
+
# Send data to channel and exit. Uses --preview flag to render message and
|
4
|
+
# flush it to STDOUT before sending
|
5
|
+
class SendData < Base
|
6
|
+
def matches?
|
7
|
+
options.send_data
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute
|
11
|
+
ch, msg = parse_endpoint(options.send_data)
|
12
|
+
|
13
|
+
raise "#{ch}/#{msg} not found" unless handler.message?(ch, msg)
|
14
|
+
|
15
|
+
data = read_data(options.data)
|
16
|
+
|
17
|
+
if options.preview
|
18
|
+
puts handler.preview(ch, msg, data)
|
19
|
+
else
|
20
|
+
handler.handle(ch, msg, data).each do |e|
|
21
|
+
puts "* #{e.inspect}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def read_data(data)
|
29
|
+
d = if data.start_with?('@')
|
30
|
+
file = data[1..-1]
|
31
|
+
raise "File not found #{file}" unless File.exist?(file)
|
32
|
+
IO.read(file)
|
33
|
+
else
|
34
|
+
data
|
35
|
+
end
|
36
|
+
JSON.parse(d)
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_endpoint(endpoint)
|
40
|
+
endpoint.match(%r{^([^/]+)/(.+)$}) do |m|
|
41
|
+
[m[1], m[2]]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -79,24 +79,5 @@ module Pechkin
|
|
79
79
|
@views = views
|
80
80
|
@channels = channels
|
81
81
|
end
|
82
|
-
|
83
|
-
def list
|
84
|
-
puts "Working dir: #{working_dir}\nBots:"
|
85
|
-
|
86
|
-
bots.each do |name, bot|
|
87
|
-
puts " #{name}(#{bot.connector}): #{bot.token}"
|
88
|
-
end
|
89
|
-
|
90
|
-
puts "\nChannels:"
|
91
|
-
channels.each do |channel_name, channel|
|
92
|
-
puts " - name #{channel_name}"
|
93
|
-
puts " bot: #{channel.connector.name}"
|
94
|
-
puts ' messages: '
|
95
|
-
channel.messages.each do |message_name, _message|
|
96
|
-
puts " - /#{channel_name}/#{message_name}"
|
97
|
-
end
|
98
|
-
puts
|
99
|
-
end
|
100
|
-
end
|
101
82
|
end
|
102
83
|
end
|
data/lib/pechkin/connector.rb
CHANGED
@@ -10,8 +10,8 @@ module Pechkin
|
|
10
10
|
def send_message(chat, message, message_desc); end
|
11
11
|
|
12
12
|
def preview(chats, message, _message_desc)
|
13
|
-
|
14
|
-
|
13
|
+
"Connector: #{self.class.name}; Chats: #{chats.join(', ')}\n" \
|
14
|
+
"Message:\n#{message}"
|
15
15
|
end
|
16
16
|
|
17
17
|
def post_data(url, data, headers: {})
|
data/lib/pechkin/handler.rb
CHANGED
@@ -3,7 +3,6 @@ module Pechkin
|
|
3
3
|
# services. Can skip some requests acording to filters.
|
4
4
|
class Handler
|
5
5
|
attr_reader :channels
|
6
|
-
attr_writer :preview
|
7
6
|
|
8
7
|
def initialize(channels)
|
9
8
|
@channels = channels
|
@@ -13,6 +12,7 @@ module Pechkin
|
|
13
12
|
# message id, and data object. By channel id we determine where to send
|
14
13
|
# data, by message id we determine how to transform this data to real
|
15
14
|
# message.
|
15
|
+
#
|
16
16
|
# @param channel_id [String] channel name from configuration. This name is
|
17
17
|
# obtained from directory structure we have in configuration directory.
|
18
18
|
# @param msg_id [String] message name from configuration. This name is
|
@@ -33,19 +33,38 @@ module Pechkin
|
|
33
33
|
|
34
34
|
chats = channel_config.chat_ids
|
35
35
|
connector = channel_config.connector
|
36
|
-
|
37
|
-
|
38
|
-
else
|
39
|
-
chats.map { |chat| connector.send_message(chat, text, message_config) }
|
40
|
-
end
|
36
|
+
|
37
|
+
chats.map { |chat| connector.send_message(chat, text, message_config) }
|
41
38
|
end
|
42
39
|
|
43
|
-
|
44
|
-
|
40
|
+
# Executes message handling and renders template using connector logic
|
41
|
+
#
|
42
|
+
# @param channel_id [String] channel name from configuration. This name is
|
43
|
+
# obtained from directory structure we have in configuration directory.
|
44
|
+
# @param msg_id [String] message name from configuration. This name is
|
45
|
+
# references yml file with message description
|
46
|
+
# @param data [Object] data object to render via template. This is usualy
|
47
|
+
# deserialized json.
|
48
|
+
# @see Configuration
|
49
|
+
def preview(channel_id, msg_id, data)
|
50
|
+
channel_config = fetch_channel(channel_id)
|
51
|
+
# Find message and try substitute values to message parameters.
|
52
|
+
message_config = substitute(data, fetch_message(channel_config, msg_id))
|
53
|
+
|
54
|
+
data = (message_config['variables'] || {}).merge(data)
|
55
|
+
template = message_config['template']
|
56
|
+
|
57
|
+
text = ''
|
58
|
+
text = template.render(data) unless template.nil?
|
59
|
+
|
60
|
+
chats = channel_config.chat_ids
|
61
|
+
connector = channel_config.connector
|
62
|
+
|
63
|
+
connector.preview(chats, text, message_config)
|
45
64
|
end
|
46
65
|
|
47
|
-
def
|
48
|
-
|
66
|
+
def message?(channel_id, msg_id)
|
67
|
+
channels.key?(channel_id) && channels[channel_id].messages.key?(msg_id)
|
49
68
|
end
|
50
69
|
|
51
70
|
private
|
data/lib/pechkin/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pechkin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.2.
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ilya Arkhanhelsky
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-02-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: grape
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - '='
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: 2.0.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: powerpack
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.1.2
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.1.2
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: prometheus-client
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -79,6 +93,13 @@ files:
|
|
79
93
|
- lib/pechkin/auth.rb
|
80
94
|
- lib/pechkin/channel.rb
|
81
95
|
- lib/pechkin/cli.rb
|
96
|
+
- lib/pechkin/command.rb
|
97
|
+
- lib/pechkin/command/add_auth.rb
|
98
|
+
- lib/pechkin/command/base.rb
|
99
|
+
- lib/pechkin/command/check.rb
|
100
|
+
- lib/pechkin/command/list.rb
|
101
|
+
- lib/pechkin/command/run_server.rb
|
102
|
+
- lib/pechkin/command/send_data.rb
|
82
103
|
- lib/pechkin/configuration.rb
|
83
104
|
- lib/pechkin/configuration/configuration_loader.rb
|
84
105
|
- lib/pechkin/configuration/configuration_loader_bots.rb
|