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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6a85ac81ee32154705d92319c32e7706eb4af5d7a741bebcc4fee1ce64004b3
4
- data.tar.gz: 4f36119f5bca18893aed4cdfa69ee4e1426291f2ac585b8b970d12ee604a62b4
3
+ metadata.gz: 3447abc49727b844f0c86895242a10aa1c3cee00e1fbdff5deb915ba790ebe98
4
+ data.tar.gz: 3352b4e61c0a9885d8e0b14b497e41947952b996bd522e238ac519100965ff2d
5
5
  SHA512:
6
- metadata.gz: 85742668000030821d0bc05ac5531144d2a34ff890c788ff568c309e4dc9020b808a64ffdb9f84b3346247af8de14bad70f3a6a915d1df3cc5aaf6e682f5628d
7
- data.tar.gz: e5a3ab2a6fb85241223d589aed03603bf2e1295825200cfc2f4eb8adac89eb9780d40f578e84ab1657ff45e521738c83f15ca9ef7df867964a2e30cac547cc77
6
+ metadata.gz: 81fbdfd449664ed2715035649db5f3c9f31740ca9ade3295d5eab19415e6cc3ea9eae0652e99810c0ff3317b8815032a10e4e257246802efee3d31e247d4dbe9
7
+ data.tar.gz: 84ce0aa1dfb906e2bae2221e85fe77bdc48ec160ad6bf494c6ba1fcf64bb6a7c274a479a22825954cf66dad8470546cec9283a120434f85be0b7a17182eb4a22
@@ -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
- Main.new(options).run
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
@@ -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
- if authorized?(env)
29
- @app.call(env)
30
- else
31
- body = { status: 'error', reason: 'unathorized' }.to_json
32
- ['401', { 'Content-Type' => 'application/json' }, [body]]
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 authorized?(env)
43
- return true unless htpasswd
42
+ def authorize(env)
43
+ return unless htpasswd
44
44
 
45
- auth = env['HTTP_AUTHORIZATION'] || ''
46
- auth.match(/^Basic (.+)$/) do |m|
47
- check_auth(*Base64.decode64(m[1]).split(':'))
48
- end
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
- e && e.authenticated?(password)
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
@@ -38,13 +38,8 @@ module Pechkin
38
38
  values = OpenStruct.new
39
39
  parser = parser_create(values)
40
40
 
41
- if args.empty?
42
- puts parser.help
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 :config_file, default: Dir.pwd,
100
- names: ['-c', '--config-dir FILE'],
101
- desc: 'Path to configuration file'
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.config_file, PECHKIN_HTPASSWD_FILE)
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,14 @@
1
+ module Pechkin
2
+ module Command
3
+ # Check configuration consistency and exit.
4
+ class Check < Base
5
+ def matches?
6
+ options.check?
7
+ end
8
+
9
+ def execute
10
+ configuration # load configuration from disk and do nothing more
11
+ end
12
+ end
13
+ end
14
+ 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
@@ -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
- puts "Connector: #{self.class.name}; Chats: #{chats.join(', ')}\n"
14
- puts "Message:\n#{message}"
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: {})
@@ -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
- if preview?
37
- connector.preview(chats, text, message_config)
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
- def message?(channel_id, msg_id)
44
- channels.key?(channel_id) && channels[channel_id].messages.key?(msg_id)
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 preview?
48
- @preview
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
@@ -1,7 +1,7 @@
1
1
  module Pechkin
2
2
  # Keeps actual version
3
3
  module Version
4
- VERSION = [1, 2, 0].freeze
4
+ VERSION = [1, 2, 1].freeze
5
5
  class << self
6
6
  def version_string
7
7
  VERSION.join('.')
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.0
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-01-17 00:00:00.000000000 Z
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