pechkin 1.2.2 → 1.3.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: c8edc5ca6908ec5bbe30dc46cac568f7461a7cd4a7b8f27edfec4bc9f03c406e
4
- data.tar.gz: 181cfaae800da3746168e124faeac2a794d8392c2291cff1ed44f1b2903ed35d
3
+ metadata.gz: 21153645eb45d32b113d2c95191e79ac5568a10829b3989ccc9f75da7b53bf9e
4
+ data.tar.gz: d80b4bbbeb7e4bdfeb251e358bb0a9c99d50eae96c5078b688bce5e12a3c73e3
5
5
  SHA512:
6
- metadata.gz: a779e1584430ceb11261abfa41c4a3f5a01e688867c43b87824a046c73e1398b1ae12a9aaaa2a565d864241d23effebb429dace47d41d77493e5ebd6f907fbd2
7
- data.tar.gz: 1533b9c729bc2b6c3b2498066bbbc027c44e03a106d4260538fb58399fa42565278661e32a426ffb07449ae33dfac6d89b43f96086983f534d33e7ce90240a03
6
+ metadata.gz: 30e9654efb5c1a11c4816feb51fc494be1251e106a2fac682be9cb68738faec6e8b45c747329f90cff226ef1fe9df2c097bb0083c79d57d55b020982773dd0b8
7
+ data.tar.gz: 62d6113f5ef1aedfe36c3eb86bc619d7cbac2cd9e54a37f64cbd293b1856efe47ba572bdf25efcc2a20f7b1affbbffe8cc2e74ad9784091cc69f7fa232ebe19b
@@ -11,6 +11,7 @@ require_relative 'pechkin/cli'
11
11
  require_relative 'pechkin/command'
12
12
  require_relative 'pechkin/exceptions'
13
13
  require_relative 'pechkin/handler'
14
+ require_relative 'pechkin/message_matcher'
14
15
  require_relative 'pechkin/message_template'
15
16
  require_relative 'pechkin/connector'
16
17
  require_relative 'pechkin/connector_slack'
@@ -1,114 +1,4 @@
1
- module Pechkin
2
- # Application configurator and builder. This creates all needed middleware
3
- # and stuff
4
- class AppBuilder
5
- def build(handler, options)
6
- app = App.new
7
- app.handler = handler
8
- prometheus = Pechkin::PrometheusUtils.registry
9
- logger = create_logger(options.log_dir)
10
-
11
- Rack::Builder.app do
12
- use Rack::CommonLogger, logger
13
- use Rack::Deflater
14
- use Prometheus::Middleware::Collector, registry: prometheus
15
- # Add Auth check if found htpasswd file or it was excplicitly provided
16
- # See CLI class for configuration details
17
- if options.htpasswd
18
- use Pechkin::Auth::Middleware, auth_file: options.htpasswd
19
- end
20
- use Prometheus::Middleware::Exporter, registry: prometheus
21
-
22
- run app
23
- end
24
- end
25
-
26
- private
27
-
28
- def create_logger(log_dir)
29
- if log_dir
30
- raise "Directory #{log_dir} does not exist" unless File.exist?(log_dir)
31
-
32
- log_file = File.join(log_dir, 'pechkin.log')
33
- file = File.open(log_file, File::WRONLY | File::APPEND)
34
- Logger.new(file)
35
- else
36
- Logger.new(STDOUT)
37
- end
38
- end
39
- end
40
-
41
- # Rack application to handle requests
42
- class App
43
- attr_accessor :handler
44
-
45
- def call(env)
46
- RequestHandler.new(handler, env).handle
47
- rescue StandardError => e
48
- body = { status: 'error', reason: e.message }.to_json
49
- ['503', { 'Content-Type' => 'application/json' }, [body]]
50
- end
51
- end
52
-
53
- # Http requests handler. We need fresh instance per each request. To keep
54
- # internal state isolated
55
- class RequestHandler
56
- REQ_PATH_PATTERN = %r{^/(.+)/([^/]+)/?$}
57
- DEFAULT_CONTENT_TYPE = { 'Content-Type' => 'application/json' }.freeze
58
- DEFAULT_HEADERS = {}.merge(DEFAULT_CONTENT_TYPE).freeze
59
-
60
- attr_reader :req, :env, :handler,
61
- :channel_id, :message_id
62
-
63
- def initialize(handler, env)
64
- @handler = handler
65
- @env = env
66
- @req = Rack::Request.new(env)
67
-
68
- @channel_id, @message_id = req.path_info.match(REQ_PATH_PATTERN) do |m|
69
- [m[1], m[2]]
70
- end
71
- end
72
-
73
- def handle
74
- return not_allowed unless post?
75
- return not_found unless message?
76
-
77
- begin
78
- data = JSON.parse(req.body.read)
79
- rescue JSON::JSONError => e
80
- return bad_request(e.message)
81
- end
82
-
83
- response(200, handler.handle(channel_id, message_id, data).to_json)
84
- end
85
-
86
- private
87
-
88
- def message?
89
- return false unless @channel_id && @message_id
90
-
91
- handler.message?(@channel_id, @message_id)
92
- end
93
-
94
- def not_allowed
95
- response(405, '{"status":"error", "reason":"method not allowed"}')
96
- end
97
-
98
- def not_found
99
- response(404, '{"status":"error", "reason":"message not found"}')
100
- end
101
-
102
- def bad_request(body)
103
- response(503, body)
104
- end
105
-
106
- def response(code, body)
107
- [code.to_s, DEFAULT_HEADERS, [body]]
108
- end
109
-
110
- def post?
111
- req.post?
112
- end
113
- end
114
- end
1
+ require_relative 'app/app_builder'
2
+ require_relative 'app/app_error'
3
+ require_relative 'app/app'
4
+ require_relative 'app/request_handler'
@@ -0,0 +1,43 @@
1
+ module Pechkin
2
+ # Rack application to handle requests
3
+ class App
4
+ DEFAULT_CONTENT_TYPE = { 'Content-Type' => 'application/json' }.freeze
5
+ DEFAULT_HEADERS = {}.merge(DEFAULT_CONTENT_TYPE).freeze
6
+
7
+ attr_accessor :handler, :logger
8
+
9
+ def initialize(logger)
10
+ @logger = logger
11
+ end
12
+
13
+ def call(env)
14
+ req = Rack::Request.new(env)
15
+ result = RequestHandler.new(handler, req, logger).handle
16
+ response(200, result)
17
+ rescue AppError => e
18
+ proces_app_error(req, e)
19
+ rescue StandardError => e
20
+ process_unhandled_error(req, e)
21
+ end
22
+
23
+ private
24
+
25
+ def response(code, body)
26
+ [code.to_s, DEFAULT_HEADERS, [body.to_json]]
27
+ end
28
+
29
+ def proces_app_error(req, err)
30
+ data = { status: 'error', message: err.message }
31
+ req.body.rewind
32
+ body = req.body.read
33
+ logger.error "Can't process message: #{err.message}. Body: '#{body}'"
34
+ response(err.code, data)
35
+ end
36
+
37
+ def process_unhandled_error(_req, err)
38
+ data = { status: 'error', message: err.message }
39
+ logger.error("#{err.message}\n\t" + err.backtrace.join("\n\t"))
40
+ response(503, data)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,41 @@
1
+ module Pechkin
2
+ # Application configurator and builder. This creates all needed middleware
3
+ # and stuff
4
+ class AppBuilder
5
+ def build(handler, options)
6
+ logger = create_logger(options.log_dir)
7
+ handler.logger = logger
8
+ app = App.new(logger)
9
+ app.handler = handler
10
+ prometheus = Pechkin::PrometheusUtils.registry
11
+
12
+ Rack::Builder.app do
13
+ use Rack::CommonLogger, logger
14
+ use Rack::Deflater
15
+ use Prometheus::Middleware::Collector, registry: prometheus
16
+ # Add Auth check if found htpasswd file or it was excplicitly provided
17
+ # See CLI class for configuration details
18
+ if options.htpasswd
19
+ use Pechkin::Auth::Middleware, auth_file: options.htpasswd
20
+ end
21
+ use Prometheus::Middleware::Exporter, registry: prometheus
22
+
23
+ run app
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def create_logger(log_dir)
30
+ if log_dir
31
+ raise "Directory #{log_dir} does not exist" unless File.exist?(log_dir)
32
+
33
+ log_file = File.join(log_dir, 'pechkin.log')
34
+ file = File.open(log_file, File::WRONLY | File::APPEND)
35
+ Logger.new(file)
36
+ else
37
+ Logger.new(STDOUT)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,27 @@
1
+ module Pechkin
2
+ # Generic application error class.
3
+ #
4
+ # Allows us return meaningful error messages
5
+ class AppError < StandardError
6
+ attr_reader :code
7
+
8
+ def initialize(code, msg)
9
+ super(msg)
10
+ @code = code
11
+ end
12
+
13
+ class << self
14
+ def bad_request(message)
15
+ AppError.new(503, message)
16
+ end
17
+
18
+ def message_not_found
19
+ AppError.new(404, 'message not found')
20
+ end
21
+
22
+ def http_method_not_allowed
23
+ AppError.new(405, 'method not allowed')
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ module Pechkin
2
+ # Http requests handler. We need fresh instance per each request. To keep
3
+ # internal state isolated
4
+ class RequestHandler
5
+ REQ_PATH_PATTERN = %r{^/(.+)/([^/]+)/?$}
6
+
7
+ attr_reader :req, :handler,
8
+ :channel_id, :message_id,
9
+ :logger
10
+
11
+ def initialize(handler, req, logger)
12
+ @handler = handler
13
+ @req = req
14
+ @logger = logger
15
+
16
+ @channel_id, @message_id = req.path_info.match(REQ_PATH_PATTERN) do |m|
17
+ [m[1], m[2]]
18
+ end
19
+ end
20
+
21
+ def handle
22
+ raise AppError.http_method_not_allowed unless post?
23
+ raise AppError.message_not_found unless message?
24
+
25
+ data = parse_data(req.body.read)
26
+ handler.handle(channel_id, message_id, data).each do |i|
27
+ logger.info "Sent #{channel_id}/#{message_id}: #{i.to_json}"
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def parse_data(data)
34
+ JSON.parse(data)
35
+ rescue JSON::JSONError => e
36
+ raise AppError.bad_request(e.message)
37
+ end
38
+
39
+ def message?
40
+ return false unless @channel_id && @message_id
41
+
42
+ handler.message?(@channel_id, @message_id)
43
+ end
44
+
45
+ def post?
46
+ req.post?
47
+ end
48
+ end
49
+ end
@@ -10,16 +10,16 @@ module Pechkin
10
10
  # Pechkin reads its configuration from provided directory structure. Basic
11
11
  # layout expected to be as follows:
12
12
  # .
13
- # | - bots/ <= Bots configuration
14
- # | | - marvin.yml <= Each bot described by yaml file
13
+ # | - bots/ <- Bots configuration
14
+ # | | - marvin.yml <- Each bot described by yaml file
15
15
  # | | - bender.yml
16
16
  # |
17
- # | - channels/ <= Channels description
17
+ # | - channels/ <- Channels description
18
18
  # | | - slack-repository-feed
19
19
  # | | - commit-hg.yml
20
20
  # | | - commit-svn.yml
21
21
  # |
22
- # | - views/ <= Template storage
22
+ # | - views/ <- Template storage
23
23
  # | - commit-hg.erb
24
24
  # | - commit-svn.erb
25
25
  #
@@ -13,7 +13,8 @@ module Pechkin # :nodoc:
13
13
  attachments = message_desc['slack_attachments'] || {}
14
14
 
15
15
  if text.strip.empty? && attachments.empty?
16
- return [channel, 400, 'Internal error: message is empty']
16
+ return { channel: channel, code: 400,
17
+ response: 'Internal error: message is empty' }
17
18
  end
18
19
 
19
20
  params = { channel: channel, text: text, attachments: attachments }
@@ -21,7 +22,7 @@ module Pechkin # :nodoc:
21
22
  url = 'https://slack.com/api/chat.postMessage'
22
23
  response = post_data(url, params, headers: @headers)
23
24
 
24
- [channel, response.code.to_i, response.body]
25
+ { channel: channel, code: response.code.to_i, response: response.body }
25
26
  end
26
27
  end
27
28
  end
@@ -12,7 +12,7 @@ module Pechkin
12
12
  params = options.update(chat_id: chat_id, text: message)
13
13
 
14
14
  response = post_data(method_url('sendMessage'), params)
15
- [chat_id, response.code.to_i, response.body]
15
+ { chat_id: chat_id, code: response.code.to_i, response: response.body }
16
16
  end
17
17
 
18
18
  private
@@ -2,10 +2,16 @@ module Pechkin
2
2
  # Processes feeded data chunks and sends them via connectors to needed IM
3
3
  # services. Can skip some requests acording to filters.
4
4
  class Handler
5
- attr_reader :channels
5
+ attr_reader :channels, :message_matcher
6
+ attr_accessor :logger
6
7
 
7
- def initialize(channels)
8
+ def initialize(channels, stdout = STDOUT, stderr = STDERR)
8
9
  @channels = channels
10
+ # Create empty logger by default
11
+ @logger = Logger.new(IO::NULL)
12
+ @stdout = stdout
13
+ @stderr = stderr
14
+ @message_matcher = MessageMatcher.new
9
15
  end
10
16
 
11
17
  # Handles message request. Each request has three parameters: channel id,
@@ -21,20 +27,18 @@ module Pechkin
21
27
  # deserialized json.
22
28
  # @see Configuration
23
29
  def handle(channel_id, msg_id, data)
24
- channel_config = fetch_channel(channel_id)
25
- # Find message and try substitute values to message parameters.
26
- message_config = substitute(data, fetch_message(channel_config, msg_id))
27
-
28
- data = (message_config['variables'] || {}).merge(data)
29
- template = message_config['template']
30
-
31
- text = ''
32
- text = template.render(data) unless template.nil?
33
-
30
+ channel_config, message_config, text =
31
+ prepare_message(channel_id, msg_id, data)
34
32
  chats = channel_config.chat_ids
35
33
  connector = channel_config.connector
36
34
 
37
- chats.map { |chat| connector.send_message(chat, text, message_config) }
35
+ if message_allowed?(message_config, data)
36
+ chats.map { |chat| connector.send_message(chat, text, message_config) }
37
+ else
38
+ logger.info "#{channel_id}/#{msg_id}: " \
39
+ "Skip sending message. Because it's not allowed"
40
+ []
41
+ end
38
42
  end
39
43
 
40
44
  # Executes message handling and renders template using connector logic
@@ -47,20 +51,16 @@ module Pechkin
47
51
  # deserialized json.
48
52
  # @see Configuration
49
53
  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
-
54
+ channel_config, message_config, text =
55
+ prepare_message(channel_id, msg_id, data)
60
56
  chats = channel_config.chat_ids
61
57
  connector = channel_config.connector
62
58
 
63
- connector.preview(chats, text, message_config)
59
+ if message_allowed?(message_config, data)
60
+ connector.preview(chats, text, message_config)
61
+ else
62
+ puts "No message sent beacuse it's not allowed"
63
+ end
64
64
  end
65
65
 
66
66
  def message?(channel_id, msg_id)
@@ -69,6 +69,10 @@ module Pechkin
69
69
 
70
70
  private
71
71
 
72
+ def puts(msg)
73
+ @stdout.puts(msg)
74
+ end
75
+
72
76
  # Find channel by it's id or trow ChannelNotFoundError
73
77
  def fetch_channel(channel_id)
74
78
  raise ChannelNotFoundError, channel_id unless channels.key?(channel_id)
@@ -84,6 +88,24 @@ module Pechkin
84
88
  message_list[msg_id]
85
89
  end
86
90
 
91
+ def message_allowed?(message_config, data)
92
+ message_matcher.matches?(message_config, data)
93
+ end
94
+
95
+ def prepare_message(channel_id, msg_id, data)
96
+ channel_config = fetch_channel(channel_id)
97
+ # Find message and try substitute values to message parameters.
98
+ message_config = substitute(data, fetch_message(channel_config, msg_id))
99
+
100
+ data = (message_config['variables'] || {}).merge(data)
101
+ template = message_config['template']
102
+
103
+ text = ''
104
+ text = template.render(data) unless template.nil?
105
+
106
+ [channel_config, message_config, text]
107
+ end
108
+
87
109
  def substitute(data, message_desc)
88
110
  substitute_recursive(Substitute.new(data), message_desc)
89
111
  end
@@ -0,0 +1,78 @@
1
+ module Pechkin
2
+ class MessageMatchError < StandardError; end
3
+ # Allows to match message configuration against received data.
4
+ #
5
+ # Data is checked againts either allow or forbid rules. But not both at the
6
+ # same time. Each field can contain list of rules to check. 'allow' list means
7
+ # we need at least one matching rule to allow data processing. And 'forbid'
8
+ # list respectievly means we need at least one matching rule to decline data
9
+ # processing.
10
+ class MessageMatcher
11
+ KEY_ALLOW = 'allow'.freeze
12
+ KEY_FORBID = 'forbid'.freeze
13
+ # Checks data object against allow / forbid rule sets in message
14
+ # configuration. If data object matches rules it means we can process this
15
+ # data and send message.
16
+ #
17
+ # @param message_config [Hash] message description.
18
+ # @param data [Hash] request object that need to be inspected whether we
19
+ # should process this data or not
20
+ # @return [Boolean] is data object matches message_config rules or not
21
+ def matches?(message_config, data)
22
+ check(message_config)
23
+
24
+ if message_config.key?(KEY_ALLOW)
25
+ rules = message_config[KEY_ALLOW]
26
+ rules.any? { |r| check_rule(r, data) }
27
+ elsif message_config.key?(KEY_FORBID)
28
+ rules = message_config[KEY_FORBID]
29
+ rules.all? { |r| !check_rule(r, data) }
30
+ else
31
+ true
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def check(msg)
38
+ return unless msg.key?(KEY_ALLOW) && msg.key?(KEY_FORBID)
39
+
40
+ raise MessageMatchError, 'Both allow and forbid present in message config'
41
+ end
42
+
43
+ # Check rule object against data. Rules are checked recursievly, i.e. we
44
+ # can take one field in rule and check it separately as new rule. If all
45
+ # fields are passed check then whole rule passed.
46
+ def check_rule(rule, data)
47
+ if rule.is_a?(Hash)
48
+ check_hash_rule(rule, data)
49
+ elsif rule.is_a?(Array)
50
+ check_array_rule(rule, data)
51
+ elsif rule.is_a?(String)
52
+ check_string_rule(rule, data)
53
+ else
54
+ rule.eql?(data)
55
+ end
56
+ end
57
+
58
+ def check_hash_rule(hash, data)
59
+ return false unless data.is_a?(Hash)
60
+
61
+ hash.all? do |key, value|
62
+ data.key?(key) && check_rule(value, data[key])
63
+ end
64
+ end
65
+
66
+ def check_array_rule(array, data)
67
+ return false unless data.is_a?(Array)
68
+
69
+ # Deep array check needs to be done against all elements so we zip arrays
70
+ # to pair each rule with data element
71
+ array.zip(data).all? { |r, d| check_rule(r, d) }
72
+ end
73
+
74
+ def check_string_rule(str, data)
75
+ str.eql? data
76
+ end
77
+ end
78
+ end
@@ -1,7 +1,7 @@
1
1
  module Pechkin
2
2
  # Keeps actual version
3
3
  module Version
4
- VERSION = [1, 2, 2].freeze
4
+ VERSION = [1, 3, 1].freeze
5
5
  class << self
6
6
  def version_string
7
7
  VERSION.join('.')
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pechkin
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.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-02-04 00:00:00.000000000 Z
11
+ date: 2020-02-07 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: grape
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - '='
18
- - !ruby/object:Gem::Version
19
- version: 1.1.0
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - '='
25
- - !ruby/object:Gem::Version
26
- version: 1.1.0
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: htauth
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -90,6 +76,10 @@ files:
90
76
  - bin/pechkin
91
77
  - lib/pechkin.rb
92
78
  - lib/pechkin/app.rb
79
+ - lib/pechkin/app/app.rb
80
+ - lib/pechkin/app/app_builder.rb
81
+ - lib/pechkin/app/app_error.rb
82
+ - lib/pechkin/app/request_handler.rb
93
83
  - lib/pechkin/auth.rb
94
84
  - lib/pechkin/channel.rb
95
85
  - lib/pechkin/cli.rb
@@ -111,6 +101,7 @@ files:
111
101
  - lib/pechkin/connector_telegram.rb
112
102
  - lib/pechkin/exceptions.rb
113
103
  - lib/pechkin/handler.rb
104
+ - lib/pechkin/message_matcher.rb
114
105
  - lib/pechkin/message_template.rb
115
106
  - lib/pechkin/prometheus_utils.rb
116
107
  - lib/pechkin/substitute.rb