pechkin 1.2.0 → 1.2.1

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: 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