pechkin 1.2.1 → 1.4.0

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