pechkin 1.2.1 → 1.4.0

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: 3447abc49727b844f0c86895242a10aa1c3cee00e1fbdff5deb915ba790ebe98
4
- data.tar.gz: 3352b4e61c0a9885d8e0b14b497e41947952b996bd522e238ac519100965ff2d
3
+ metadata.gz: 3fa8abd425ad6a776104aabeeb6916d4b8b4d77ae0f0d7e24b75f102bb70b016
4
+ data.tar.gz: 1b5b43910b029f8553f54ddfa5d70a7195b2db86d35a38ba1fcffc37521b7d4b
5
5
  SHA512:
6
- metadata.gz: 81fbdfd449664ed2715035649db5f3c9f31740ca9ade3295d5eab19415e6cc3ea9eae0652e99810c0ff3317b8815032a10e4e257246802efee3d31e247d4dbe9
7
- data.tar.gz: 84ce0aa1dfb906e2bae2221e85fe77bdc48ec160ad6bf494c6ba1fcf64bb6a7c274a479a22825954cf66dad8470546cec9283a120434f85be0b7a17182eb4a22
6
+ metadata.gz: d636b920cb207c03ce1f96ceeb47ec478214743e0ef4f49895c07982fcf68d2b724d4006058ef00de6903c986c6f40e8678ebbb7ccdaa681d6b552cfb70111a1
7
+ data.tar.gz: 5254cb1f323cb46af16b889d82ffd565a9a0f30e5665cc6c5487e59926f25e66b8a573e004ffcf4c2a34b8f17187b0e67443ef5bf5ffec3b8e2a68e079557def
@@ -0,0 +1,44 @@
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
+ logger.error(req.body.read)
41
+ response(503, data)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,39 @@
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
+ use Pechkin::Auth::Middleware, auth_file: options.htpasswd if options.htpasswd
19
+ use Prometheus::Middleware::Exporter, registry: prometheus
20
+
21
+ run app
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def create_logger(log_dir)
28
+ if log_dir
29
+ raise "Directory #{log_dir} does not exist" unless File.exist?(log_dir)
30
+
31
+ log_file = File.join(log_dir, 'pechkin.log')
32
+ file = File.open(log_file, File::WRONLY | File::APPEND)
33
+ Logger.new(file)
34
+ else
35
+ Logger.new($stdout)
36
+ end
37
+ end
38
+ end
39
+ 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{^/(.+)/([^/]+)/?$}.freeze
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
data/lib/pechkin/app.rb CHANGED
@@ -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'
data/lib/pechkin/auth.rb CHANGED
@@ -5,6 +5,7 @@ module Pechkin
5
5
  # Utility class for altering htpasswd files
6
6
  class Manager
7
7
  attr_reader :htpasswd
8
+
8
9
  def initialize(htpasswd)
9
10
  @htpasswd = htpasswd
10
11
  end
@@ -3,7 +3,7 @@ module Pechkin
3
3
  class Chanel
4
4
  attr_accessor :logger
5
5
 
6
- def initialize(connector, channel_list, logger = ::Logger.new(STDOUT))
6
+ def initialize(connector, channel_list, logger = ::Logger.new($stdout))
7
7
  @connector = connector
8
8
  @channel_list = channel_list
9
9
  @channel_list = [channel_list] unless channel_list.is_a?(Array)
data/lib/pechkin/cli.rb CHANGED
@@ -13,7 +13,7 @@ module Pechkin
13
13
  # @opt names [Array<String>] list of command line keys
14
14
  # @opt desc [String] option description
15
15
  # @opt type [Class] argument type to parse from command line, e.g. Integer
16
- def opt(name, default: nil, names:, desc: '', type: nil)
16
+ def opt(name, names:, default: nil, desc: '', type: nil)
17
17
  @cli_options ||= []
18
18
 
19
19
  # raise ':names is nil or empty' if names.nil? || names.empty?
@@ -9,7 +9,7 @@ module Pechkin
9
9
  # command behaviour
10
10
  # @opt stdout [IO] IO object which represents STDOUT
11
11
  # @opt stderr [IO] IO object which represents STDERR
12
- def initialize(options, stdout: STDOUT, stderr: STDERR)
12
+ def initialize(options, stdout: $stdout, stderr: $stderr)
13
13
  @options = options
14
14
  @stdout = stdout
15
15
  @stderr = stderr
@@ -8,7 +8,7 @@ module Pechkin
8
8
 
9
9
  def execute
10
10
  Rack::Server.start(app: AppBuilder.new.build(handler, options),
11
- Host: options.bind_adress,
11
+ Host: options.bind_address,
12
12
  Port: options.port,
13
13
  pid: options.pid_file)
14
14
  end
@@ -29,6 +29,7 @@ module Pechkin
29
29
  d = if data.start_with?('@')
30
30
  file = data[1..-1]
31
31
  raise "File not found #{file}" unless File.exist?(file)
32
+
32
33
  IO.read(file)
33
34
  else
34
35
  data
@@ -13,11 +13,11 @@ module Pechkin
13
13
  def create_connector(bot)
14
14
  case bot.connector
15
15
  when 'tg', 'telegram'
16
- TelegramConnector.new(bot.token, bot.name)
16
+ Connector::Telegram.new(bot.token, bot.name)
17
17
  when 'slack'
18
- SlackConnector.new(bot.token, bot.name)
18
+ Connector::Slack.new(bot.token, bot.name)
19
19
  else
20
- raise 'Unknown connector ' + bot.connector + ' for ' + bot.name
20
+ raise "Unknown connector #{bot.connector} for #{bot.name}"
21
21
  end
22
22
  end
23
23
 
@@ -15,9 +15,7 @@ module Pechkin
15
15
  def load_bots_configuration(working_dir, bots)
16
16
  bots_dir = File.join(working_dir, 'bots')
17
17
 
18
- unless File.directory?(bots_dir)
19
- raise ConfigurationError, "'#{bots_dir}' is not a directory"
20
- end
18
+ raise ConfigurationError, "'#{bots_dir}' is not a directory" unless File.directory?(bots_dir)
21
19
 
22
20
  Dir["#{bots_dir}/*.yml"].each do |bot_file|
23
21
  name = File.basename(bot_file, '.yml')
@@ -22,9 +22,7 @@ module Pechkin
22
22
  def load_channels_configuration(working_dir, channels)
23
23
  channels_dir = File.join(working_dir, 'channels')
24
24
 
25
- unless File.directory?(channels_dir)
26
- raise ConfigurationError, "'#{channels_dir}' is not a directory"
27
- end
25
+ raise ConfigurationError, "'#{channels_dir}' is not a directory" unless File.directory?(channels_dir)
28
26
 
29
27
  Dir["#{channels_dir}/*"].each do |channel_dir|
30
28
  next unless File.directory?(channel_dir)
@@ -63,9 +61,7 @@ module Pechkin
63
61
  message_config = YAML.safe_load(IO.read(file))
64
62
  name = File.basename(file, '.yml')
65
63
 
66
- if message_config.key?('template')
67
- message_config['template'] = get_template(message_config['template'])
68
- end
64
+ message_config['template'] = get_template(message_config['template']) if message_config.key?('template')
69
65
 
70
66
  messages[name] = message_config
71
67
  end
@@ -15,9 +15,7 @@ module Pechkin
15
15
  def load_views_configuration(working_dir, views)
16
16
  views_dir = File.join(working_dir, 'views')
17
17
 
18
- unless File.directory?(views_dir)
19
- raise ConfigurationError, "'#{views_dir}' is not a directory"
20
- end
18
+ raise ConfigurationError, "'#{views_dir}' is not a directory" unless File.directory?(views_dir)
21
19
 
22
20
  Dir["#{views_dir}/**/*.erb"].each do |f|
23
21
  relative_path = f["#{views_dir}/".length..-1]
@@ -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
  #
@@ -0,0 +1,29 @@
1
+ module Pechkin
2
+ module Connector
3
+ # Base connector
4
+ class Base
5
+ DEFAULT_HEADERS = {
6
+ 'Content-Type' => 'application/json; charset=UTF-8'
7
+ }.freeze
8
+
9
+ def send_message(chat, message, message_desc); end
10
+
11
+ def preview(chats, message, _message_desc)
12
+ "Connector: #{self.class.name}; Chats: #{chats.join(', ')}\n" \
13
+ "Message:\n#{message}"
14
+ end
15
+
16
+ def post_data(url, data, headers: {})
17
+ uri = URI.parse(url)
18
+ headers = DEFAULT_HEADERS.merge(headers)
19
+ http = Net::HTTP.new(uri.host, uri.port)
20
+ http.use_ssl = url.start_with?('https://')
21
+
22
+ request = Net::HTTP::Post.new(uri.request_uri, headers)
23
+ request.body = data.to_json
24
+
25
+ http.request(request)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ module Pechkin
2
+ module Connector
3
+ class Slack < Connector::Base # :nodoc:
4
+ attr_reader :name
5
+
6
+ def initialize(bot_token, name)
7
+ super()
8
+
9
+ @headers = { 'Authorization' => "Bearer #{bot_token}" }
10
+ @name = name
11
+ end
12
+
13
+ def send_message(channel, message, message_desc)
14
+ text = CGI.unescape_html(message)
15
+
16
+ attachments = message_desc['slack_attachments'] || {}
17
+
18
+ if text.strip.empty? && attachments.empty?
19
+ return { channel: channel, code: 400,
20
+ response: 'Internal error: message is empty' }
21
+ end
22
+
23
+ params = { channel: channel, text: text, attachments: attachments }
24
+
25
+ url = 'https://slack.com/api/chat.postMessage'
26
+ response = post_data(url, params, headers: @headers)
27
+
28
+ { channel: channel, code: response.code.to_i, response: response.body }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ module Pechkin
2
+ module Connector
3
+ class Telegram < Connector::Base # :nodoc:
4
+ attr_reader :name
5
+
6
+ def initialize(bot_token, name)
7
+ super()
8
+
9
+ @bot_token = bot_token
10
+ @name = name
11
+ end
12
+
13
+ def send_message(chat_id, message, message_desc)
14
+ options = { parse_mode: message_desc['telegram_parse_mode'] || 'HTML' }
15
+ params = options.update(chat_id: chat_id, text: message)
16
+
17
+ response = post_data(method_url('sendMessage'), params)
18
+ { chat_id: chat_id, code: response.code.to_i, response: response.body }
19
+ end
20
+
21
+ private
22
+
23
+ def method_url(method)
24
+ "https://api.telegram.org/bot#{@bot_token}/#{method}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -4,26 +4,6 @@ require 'uri'
4
4
  require 'json'
5
5
  require 'cgi'
6
6
 
7
- module Pechkin
8
- # Base connector
9
- class Connector
10
- def send_message(chat, message, message_desc); end
11
-
12
- def preview(chats, message, _message_desc)
13
- "Connector: #{self.class.name}; Chats: #{chats.join(', ')}\n" \
14
- "Message:\n#{message}"
15
- end
16
-
17
- def post_data(url, data, headers: {})
18
- uri = URI.parse(url)
19
- headers = { 'Content-Type' => 'application/json' }.merge(headers)
20
- http = Net::HTTP.new(uri.host, uri.port)
21
- http.use_ssl = url.start_with?('https://')
22
-
23
- request = Net::HTTP::Post.new(uri.request_uri, headers)
24
- request.body = data.to_json
25
-
26
- http.request(request)
27
- end
28
- end
29
- end
7
+ require_relative 'connector/base'
8
+ require_relative 'connector/slack'
9
+ require_relative 'connector/telegram'
@@ -4,7 +4,10 @@ module Pechkin
4
4
  super("No such channel #{channel_name}")
5
5
  end
6
6
  end
7
+
7
8
  class MessageNotFoundError < StandardError; end
9
+
8
10
  class MessageContentIsEmptyError < StandardError; end
11
+
9
12
  class ConfigurationError < StandardError; end
10
13
  end
@@ -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(@logger)
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.warn "#{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,94 @@
1
+ module Pechkin
2
+ class MessageMatchError < StandardError; end
3
+
4
+ # Allows to match message configuration against received data.
5
+ #
6
+ # Data is checked againts either allow or forbid rules. But not both at the
7
+ # same time. Each field can contain list of rules to check. 'allow' list means
8
+ # we need at least one matching rule to allow data processing. And 'forbid'
9
+ # list respectievly means we need at least one matching rule to decline data
10
+ # processing.
11
+ class MessageMatcher
12
+ KEY_ALLOW = 'allow'.freeze
13
+ KEY_FORBID = 'forbid'.freeze
14
+
15
+ attr_reader :logger
16
+
17
+ def initialize(logger)
18
+ @logger = logger
19
+ end
20
+
21
+ # Checks data object against allow / forbid rule sets in message
22
+ # configuration. If data object matches rules it means we can process this
23
+ # data and send message.
24
+ #
25
+ # @param message_config [Hash] message description.
26
+ # @param data [Hash] request object that need to be inspected whether we
27
+ # should process this data or not
28
+ # @return [Boolean] is data object matches message_config rules or not
29
+ def matches?(message_config, data)
30
+ check(message_config)
31
+
32
+ if message_config.key?(KEY_ALLOW)
33
+ rules = message_config[KEY_ALLOW]
34
+ rules.any? { |r| check_rule(r, r, data) }
35
+ elsif message_config.key?(KEY_FORBID)
36
+ rules = message_config[KEY_FORBID]
37
+ rules.all? { |r| !check_rule(r, r, data) }
38
+ else
39
+ true
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def check(msg)
46
+ return unless msg.key?(KEY_ALLOW) && msg.key?(KEY_FORBID)
47
+
48
+ raise MessageMatchError, 'Both allow and forbid present in message config'
49
+ end
50
+
51
+ # Check rule object against data. Rules are checked recursievly, i.e. we
52
+ # can take one field in rule and check it separately as new rule. If all
53
+ # fields are passed check then whole rule passed.
54
+ def check_rule(top_rule, sub_rule, data)
55
+ result = case sub_rule
56
+ when Hash
57
+ check_hash_rule(top_rule, sub_rule, data)
58
+ when Array
59
+ check_array_rule(top_rule, sub_rule, data)
60
+ when String
61
+ check_string_rule(top_rule, sub_rule, data)
62
+ else
63
+ sub_rule.eql?(data)
64
+ end
65
+
66
+ unless result
67
+ logger.info "Expected #{sub_rule.to_json} got #{data.to_json} when" \
68
+ " checking #{top_rule.to_json}"
69
+ end
70
+
71
+ result
72
+ end
73
+
74
+ def check_hash_rule(top_rule, hash, data)
75
+ return false unless data.is_a?(Hash)
76
+
77
+ hash.all? do |key, value|
78
+ data.key?(key) && check_rule(top_rule, value, data[key])
79
+ end
80
+ end
81
+
82
+ def check_array_rule(top_rule, array, data)
83
+ return false unless data.is_a?(Array)
84
+
85
+ # Deep array check needs to be done against all elements so we zip arrays
86
+ # to pair each rule with data element
87
+ array.zip(data).all? { |r, d| check_rule(top_rule, r, d) }
88
+ end
89
+
90
+ def check_string_rule(_top_rule, str, data)
91
+ str.eql? data
92
+ end
93
+ end
94
+ end
@@ -1,7 +1,7 @@
1
1
  module Pechkin
2
2
  # Keeps actual version
3
3
  module Version
4
- VERSION = [1, 2, 1].freeze
4
+ VERSION = [1, 4, 0].freeze
5
5
  class << self
6
6
  def version_string
7
7
  VERSION.join('.')
data/lib/pechkin.rb CHANGED
@@ -11,10 +11,9 @@ 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
- require_relative 'pechkin/connector_slack'
17
- require_relative 'pechkin/connector_telegram'
18
17
  require_relative 'pechkin/channel'
19
18
  require_relative 'pechkin/configuration'
20
19
  require_relative 'pechkin/substitute'
@@ -29,8 +28,8 @@ module Pechkin # :nodoc:
29
28
  cmd = Command::Dispatcher.new(options).dispatch
30
29
  cmd.execute
31
30
  rescue StandardError => e
32
- warn 'Error: ' + e.message
33
- warn "\t" + e.backtrace.reverse.join("\n\t") if options.debug?
31
+ warn "Error: #{e.message}"
32
+ warn "\t#{e.backtrace.reverse.join("\n\t")}" if options.debug?
34
33
  exit 2
35
34
  end
36
35
  end
metadata CHANGED
@@ -1,43 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pechkin
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.4.0
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: 2021-10-16 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
30
16
  requirements:
31
17
  - - '='
32
18
  - !ruby/object:Gem::Version
33
- version: 2.0.0
19
+ version: 2.1.1
34
20
  type: :runtime
35
21
  prerelease: false
36
22
  version_requirements: !ruby/object:Gem::Requirement
37
23
  requirements:
38
24
  - - '='
39
25
  - !ruby/object:Gem::Version
40
- version: 2.0.0
26
+ version: 2.1.1
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: powerpack
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -72,14 +58,14 @@ dependencies:
72
58
  requirements:
73
59
  - - '='
74
60
  - !ruby/object:Gem::Version
75
- version: 2.0.8
61
+ version: 2.2.3
76
62
  type: :runtime
77
63
  prerelease: false
78
64
  version_requirements: !ruby/object:Gem::Requirement
79
65
  requirements:
80
66
  - - '='
81
67
  - !ruby/object:Gem::Version
82
- version: 2.0.8
68
+ version: 2.2.3
83
69
  description:
84
70
  email: ilya.arkhanhelsky at gmail.com
85
71
  executables:
@@ -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
@@ -107,10 +97,12 @@ files:
107
97
  - lib/pechkin/configuration/configuration_loader_views.rb
108
98
  - lib/pechkin/configuration/model.rb
109
99
  - lib/pechkin/connector.rb
110
- - lib/pechkin/connector_slack.rb
111
- - lib/pechkin/connector_telegram.rb
100
+ - lib/pechkin/connector/base.rb
101
+ - lib/pechkin/connector/slack.rb
102
+ - lib/pechkin/connector/telegram.rb
112
103
  - lib/pechkin/exceptions.rb
113
104
  - lib/pechkin/handler.rb
105
+ - lib/pechkin/message_matcher.rb
114
106
  - lib/pechkin/message_template.rb
115
107
  - lib/pechkin/prometheus_utils.rb
116
108
  - lib/pechkin/substitute.rb
@@ -125,16 +117,16 @@ require_paths:
125
117
  - lib
126
118
  required_ruby_version: !ruby/object:Gem::Requirement
127
119
  requirements:
128
- - - ">="
120
+ - - ">"
129
121
  - !ruby/object:Gem::Version
130
- version: '0'
122
+ version: '2.5'
131
123
  required_rubygems_version: !ruby/object:Gem::Requirement
132
124
  requirements:
133
125
  - - ">="
134
126
  - !ruby/object:Gem::Version
135
127
  version: '0'
136
128
  requirements: []
137
- rubygems_version: 3.0.3
129
+ rubygems_version: 3.2.22
138
130
  signing_key:
139
131
  specification_version: 4
140
132
  summary: Web service to proxy webhooks to Telegram Bot API
@@ -1,27 +0,0 @@
1
- module Pechkin # :nodoc:
2
- class SlackConnector < Connector # :nodoc:
3
- attr_reader :name
4
-
5
- def initialize(bot_token, name)
6
- @headers = { 'Authorization' => "Bearer #{bot_token}" }
7
- @name = name
8
- end
9
-
10
- def send_message(channel, message, message_desc)
11
- text = CGI.unescape_html(message)
12
-
13
- attachments = message_desc['slack_attachments'] || {}
14
-
15
- if text.strip.empty? && attachments.empty?
16
- return [channel, 400, 'Internal error: message is empty']
17
- end
18
-
19
- params = { channel: channel, text: text, attachments: attachments }
20
-
21
- url = 'https://slack.com/api/chat.postMessage'
22
- response = post_data(url, params, headers: @headers)
23
-
24
- [channel, response.code.to_i, response.body]
25
- end
26
- end
27
- end
@@ -1,24 +0,0 @@
1
- module Pechkin
2
- class TelegramConnector < Connector #:nodoc:
3
- attr_reader :name
4
-
5
- def initialize(bot_token, name)
6
- @bot_token = bot_token
7
- @name = name
8
- end
9
-
10
- def send_message(chat_id, message, message_desc)
11
- options = { parse_mode: message_desc['telegram_parse_mode'] || 'HTML' }
12
- params = options.update(chat_id: chat_id, text: message)
13
-
14
- response = post_data(method_url('sendMessage'), params)
15
- [chat_id, response.code.to_i, response.body]
16
- end
17
-
18
- private
19
-
20
- def method_url(method)
21
- "https://api.telegram.org/bot#{@bot_token}/#{method}"
22
- end
23
- end
24
- end