pechkin 1.3.1 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/pechkin/app/app.rb +2 -1
- data/lib/pechkin/app/app_builder.rb +2 -4
- data/lib/pechkin/app/request_handler.rb +1 -1
- data/lib/pechkin/auth.rb +1 -0
- data/lib/pechkin/{configuration/model.rb → bot.rb} +0 -1
- data/lib/pechkin/channel.rb +1 -57
- data/lib/pechkin/cli.rb +1 -1
- data/lib/pechkin/command/base.rb +1 -1
- data/lib/pechkin/command/send_data.rb +1 -0
- data/lib/pechkin/configuration/configuration_loader.rb +3 -3
- data/lib/pechkin/configuration/configuration_loader_bots.rb +1 -3
- data/lib/pechkin/configuration/configuration_loader_channels.rb +40 -5
- data/lib/pechkin/configuration/configuration_loader_views.rb +1 -3
- data/lib/pechkin/configuration.rb +0 -1
- data/lib/pechkin/connector/base.rb +29 -0
- data/lib/pechkin/connector/slack.rb +32 -0
- data/lib/pechkin/connector/telegram.rb +28 -0
- data/lib/pechkin/connector.rb +3 -23
- data/lib/pechkin/exceptions.rb +3 -0
- data/lib/pechkin/handler.rb +5 -10
- data/lib/pechkin/message.rb +64 -0
- data/lib/pechkin/message_matcher.rb +32 -16
- data/lib/pechkin/prometheus_utils.rb +6 -0
- data/lib/pechkin/version.rb +1 -1
- data/lib/pechkin.rb +5 -5
- metadata +14 -12
- data/lib/pechkin/connector_slack.rb +0 -28
- data/lib/pechkin/connector_telegram.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fdd7554b43eb0ac8fa2fdb0fc9d02d31b4acf859c1b66a9470c1fdbd524d0eef
|
4
|
+
data.tar.gz: 224c8857ae9b72a48b91c8342040f9866d30a285a7f6e8595bf27e577f44bdd2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 623290e1546eb766cb844203af31e8f3708016199f4b1771e55c468cb252d21ecd874452795dc8417ba06d3be3e096526b3ad6c96826c84fa8189cd522e61ecb
|
7
|
+
data.tar.gz: 61f56c2dc2b400cced01460d977ee216d2c546be2935b3ce968f08c0266ad29d5915b14ea6601ca31f1497f984aba8eb9a1b4cfbbb27f5d358d44f49bd81157a
|
data/lib/pechkin/app/app.rb
CHANGED
@@ -34,9 +34,10 @@ module Pechkin
|
|
34
34
|
response(err.code, data)
|
35
35
|
end
|
36
36
|
|
37
|
-
def process_unhandled_error(
|
37
|
+
def process_unhandled_error(req, err)
|
38
38
|
data = { status: 'error', message: err.message }
|
39
39
|
logger.error("#{err.message}\n\t" + err.backtrace.join("\n\t"))
|
40
|
+
logger.error(req.body.read)
|
40
41
|
response(503, data)
|
41
42
|
end
|
42
43
|
end
|
@@ -15,9 +15,7 @@ module Pechkin
|
|
15
15
|
use Prometheus::Middleware::Collector, registry: prometheus
|
16
16
|
# Add Auth check if found htpasswd file or it was excplicitly provided
|
17
17
|
# See CLI class for configuration details
|
18
|
-
if options.htpasswd
|
19
|
-
use Pechkin::Auth::Middleware, auth_file: options.htpasswd
|
20
|
-
end
|
18
|
+
use Pechkin::Auth::Middleware, auth_file: options.htpasswd if options.htpasswd
|
21
19
|
use Prometheus::Middleware::Exporter, registry: prometheus
|
22
20
|
|
23
21
|
run app
|
@@ -34,7 +32,7 @@ module Pechkin
|
|
34
32
|
file = File.open(log_file, File::WRONLY | File::APPEND)
|
35
33
|
Logger.new(file)
|
36
34
|
else
|
37
|
-
Logger.new(
|
35
|
+
Logger.new($stdout)
|
38
36
|
end
|
39
37
|
end
|
40
38
|
end
|
@@ -2,7 +2,7 @@ module Pechkin
|
|
2
2
|
# Http requests handler. We need fresh instance per each request. To keep
|
3
3
|
# internal state isolated
|
4
4
|
class RequestHandler
|
5
|
-
REQ_PATH_PATTERN = %r{^/(.+)/([^/]+)/?$}
|
5
|
+
REQ_PATH_PATTERN = %r{^/(.+)/([^/]+)/?$}.freeze
|
6
6
|
|
7
7
|
attr_reader :req, :handler,
|
8
8
|
:channel_id, :message_id,
|
data/lib/pechkin/auth.rb
CHANGED
data/lib/pechkin/channel.rb
CHANGED
@@ -1,59 +1,3 @@
|
|
1
1
|
module Pechkin
|
2
|
-
|
3
|
-
class Chanel
|
4
|
-
attr_accessor :logger
|
5
|
-
|
6
|
-
def initialize(connector, channel_list, logger = ::Logger.new(STDOUT))
|
7
|
-
@connector = connector
|
8
|
-
@channel_list = channel_list
|
9
|
-
@channel_list = [channel_list] unless channel_list.is_a?(Array)
|
10
|
-
@logger = logger
|
11
|
-
end
|
12
|
-
|
13
|
-
def send_message(message, data, message_desc)
|
14
|
-
text = message.nil? ? '' : Message.new(data).render(message)
|
15
|
-
|
16
|
-
message_desc = substitute(data, message_desc)
|
17
|
-
|
18
|
-
logger.warn 'Resulting text is empty' if text.empty?
|
19
|
-
results = @channel_list.map do |id|
|
20
|
-
@connector.send_message(id, text, message_desc)
|
21
|
-
end
|
22
|
-
|
23
|
-
process_results(message, results)
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def substitute(data, message_desc)
|
29
|
-
substitute_recursive(Substitute.new(data), message_desc)
|
30
|
-
end
|
31
|
-
|
32
|
-
def substitute_recursive(substitutions, object)
|
33
|
-
case object
|
34
|
-
when String
|
35
|
-
substitutions.process(object)
|
36
|
-
when Array
|
37
|
-
object.map { |o| substitute_recursive(substitutions, o) }
|
38
|
-
when Hash
|
39
|
-
r = {}
|
40
|
-
object.each { |k, v| r[k] = substitute_recursive(substitutions, v) }
|
41
|
-
r
|
42
|
-
else
|
43
|
-
object
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
def process_results(message, results)
|
48
|
-
success, error = results.partition { |_chat, code, _body| code < 400 }
|
49
|
-
error.each do |chat, code, body|
|
50
|
-
logger.error "#{message} => #{chat}[HTTP #{code}]: #{body}"
|
51
|
-
end
|
52
|
-
|
53
|
-
{
|
54
|
-
successful: success.map(&:first),
|
55
|
-
errors: error
|
56
|
-
}
|
57
|
-
end
|
58
|
-
end
|
2
|
+
Channel = Struct.new(:chat_ids, :connector, :messages, keyword_init: true)
|
59
3
|
end
|
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,
|
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?
|
data/lib/pechkin/command/base.rb
CHANGED
@@ -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:
|
12
|
+
def initialize(options, stdout: $stdout, stderr: $stderr)
|
13
13
|
@options = options
|
14
14
|
@stdout = stdout
|
15
15
|
@stderr = stderr
|
@@ -13,11 +13,11 @@ module Pechkin
|
|
13
13
|
def create_connector(bot)
|
14
14
|
case bot.connector
|
15
15
|
when 'tg', 'telegram'
|
16
|
-
|
16
|
+
Connector::Telegram.new(bot.token, bot.name)
|
17
17
|
when 'slack'
|
18
|
-
|
18
|
+
Connector::Slack.new(bot.token, bot.name)
|
19
19
|
else
|
20
|
-
raise
|
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,16 +61,53 @@ module Pechkin
|
|
63
61
|
message_config = YAML.safe_load(IO.read(file))
|
64
62
|
name = File.basename(file, '.yml')
|
65
63
|
|
64
|
+
# Dirty workaround. I need to recursively load templates. When doing it
|
65
|
+
# we looking for {'template': '...path to template..' } objects. But we
|
66
|
+
# don't want to force user write something like:
|
67
|
+
# text:
|
68
|
+
# template: '... path to main template...'
|
69
|
+
# because it's too mouthful for such common case.
|
70
|
+
#
|
71
|
+
# So now we pull main template out, then load everyting else. Then put
|
72
|
+
# it back.
|
73
|
+
template = nil
|
66
74
|
if message_config.key?('template')
|
67
|
-
|
75
|
+
template = get_template(message_config['template'])
|
76
|
+
message_config.delete('template')
|
68
77
|
end
|
69
78
|
|
70
|
-
|
79
|
+
message_config = load_templates(message_config)
|
80
|
+
message_config['template'] = template unless template.nil?
|
81
|
+
|
82
|
+
messages[name] = Message.new(message_config)
|
71
83
|
end
|
72
84
|
|
73
85
|
messages
|
74
86
|
end
|
75
87
|
|
88
|
+
def load_templates(object)
|
89
|
+
case object
|
90
|
+
when String
|
91
|
+
object
|
92
|
+
when Array
|
93
|
+
object.map { |o| load_templates(o) }
|
94
|
+
when Hash
|
95
|
+
if object.key?('template')
|
96
|
+
msg = 'When using template only 1 KV pair allowed'
|
97
|
+
raise ConfigurationError, msg unless object.size == 1
|
98
|
+
|
99
|
+
# Replace whole object with created template.
|
100
|
+
get_template(object['template'])
|
101
|
+
else
|
102
|
+
r = {}
|
103
|
+
object.each { |k, v| r[k] = load_templates(v) }
|
104
|
+
r
|
105
|
+
end
|
106
|
+
else
|
107
|
+
object
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
76
111
|
def get_template(path)
|
77
112
|
msg = "Can't find template: #{path}"
|
78
113
|
raise ConfigurationError, msg unless @views.key?(path)
|
@@ -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]
|
@@ -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
|
data/lib/pechkin/connector.rb
CHANGED
@@ -4,26 +4,6 @@ require 'uri'
|
|
4
4
|
require 'json'
|
5
5
|
require 'cgi'
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
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'
|
data/lib/pechkin/exceptions.rb
CHANGED
data/lib/pechkin/handler.rb
CHANGED
@@ -5,13 +5,13 @@ module Pechkin
|
|
5
5
|
attr_reader :channels, :message_matcher
|
6
6
|
attr_accessor :logger
|
7
7
|
|
8
|
-
def initialize(channels, stdout =
|
8
|
+
def initialize(channels, stdout = $stdout, stderr = $stderr)
|
9
9
|
@channels = channels
|
10
10
|
# Create empty logger by default
|
11
11
|
@logger = Logger.new(IO::NULL)
|
12
12
|
@stdout = stdout
|
13
13
|
@stderr = stderr
|
14
|
-
@message_matcher = MessageMatcher.new
|
14
|
+
@message_matcher = MessageMatcher.new(@logger)
|
15
15
|
end
|
16
16
|
|
17
17
|
# Handles message request. Each request has three parameters: channel id,
|
@@ -35,7 +35,7 @@ module Pechkin
|
|
35
35
|
if message_allowed?(message_config, data)
|
36
36
|
chats.map { |chat| connector.send_message(chat, text, message_config) }
|
37
37
|
else
|
38
|
-
logger.
|
38
|
+
logger.warn "#{channel_id}/#{msg_id}: " \
|
39
39
|
"Skip sending message. Because it's not allowed"
|
40
40
|
[]
|
41
41
|
end
|
@@ -95,13 +95,8 @@ module Pechkin
|
|
95
95
|
def prepare_message(channel_id, msg_id, data)
|
96
96
|
channel_config = fetch_channel(channel_id)
|
97
97
|
# Find message and try substitute values to message parameters.
|
98
|
-
|
99
|
-
|
100
|
-
data = (message_config['variables'] || {}).merge(data)
|
101
|
-
template = message_config['template']
|
102
|
-
|
103
|
-
text = ''
|
104
|
-
text = template.render(data) unless template.nil?
|
98
|
+
message = fetch_message(channel_config, msg_id)
|
99
|
+
message_config, text = message.prepare(data)
|
105
100
|
|
106
101
|
[channel_config, message_config, text]
|
107
102
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Pechkin
|
2
|
+
# Message object
|
3
|
+
#
|
4
|
+
# TBD
|
5
|
+
class Message
|
6
|
+
def initialize(message)
|
7
|
+
@message = message
|
8
|
+
end
|
9
|
+
|
10
|
+
def prepare(data)
|
11
|
+
data = (@message['variables'] || {}).merge(data)
|
12
|
+
# Find message and try substitute values to message parameters.
|
13
|
+
message_config = render(data, substitute(data, @message))
|
14
|
+
text = ''
|
15
|
+
text = message_config.delete('template') if message_config.key?('template')
|
16
|
+
|
17
|
+
[message_config, text]
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_h
|
21
|
+
Marshal.load(Marshal.dump(@message))
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def substitute(data, message_desc)
|
27
|
+
substitute_recursive(Substitute.new(data), message_desc)
|
28
|
+
end
|
29
|
+
|
30
|
+
def substitute_recursive(substitutions, object)
|
31
|
+
case object
|
32
|
+
when String
|
33
|
+
substitutions.process(object)
|
34
|
+
when Array
|
35
|
+
object.map { |o| substitute_recursive(substitutions, o) }
|
36
|
+
when Hash
|
37
|
+
r = {}
|
38
|
+
object.each { |k, v| r[k] = substitute_recursive(substitutions, v) }
|
39
|
+
r
|
40
|
+
else
|
41
|
+
object
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def render(data, message_desc)
|
46
|
+
render_recursive(data, message_desc)
|
47
|
+
end
|
48
|
+
|
49
|
+
def render_recursive(data, object)
|
50
|
+
case object
|
51
|
+
when MessageTemplate
|
52
|
+
object.render(data)
|
53
|
+
when Array
|
54
|
+
object.map { |o| render_recursive(data, o) }
|
55
|
+
when Hash
|
56
|
+
r = {}
|
57
|
+
object.each { |k, v| r[k] = render_recursive(data, v) }
|
58
|
+
r
|
59
|
+
else
|
60
|
+
object
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
module Pechkin
|
2
2
|
class MessageMatchError < StandardError; end
|
3
|
+
|
3
4
|
# Allows to match message configuration against received data.
|
4
5
|
#
|
5
6
|
# Data is checked againts either allow or forbid rules. But not both at the
|
@@ -10,6 +11,13 @@ module Pechkin
|
|
10
11
|
class MessageMatcher
|
11
12
|
KEY_ALLOW = 'allow'.freeze
|
12
13
|
KEY_FORBID = 'forbid'.freeze
|
14
|
+
|
15
|
+
attr_reader :logger
|
16
|
+
|
17
|
+
def initialize(logger)
|
18
|
+
@logger = logger
|
19
|
+
end
|
20
|
+
|
13
21
|
# Checks data object against allow / forbid rule sets in message
|
14
22
|
# configuration. If data object matches rules it means we can process this
|
15
23
|
# data and send message.
|
@@ -23,10 +31,10 @@ module Pechkin
|
|
23
31
|
|
24
32
|
if message_config.key?(KEY_ALLOW)
|
25
33
|
rules = message_config[KEY_ALLOW]
|
26
|
-
rules.any? { |r| check_rule(r, data) }
|
34
|
+
rules.any? { |r| check_rule(r, r, data) }
|
27
35
|
elsif message_config.key?(KEY_FORBID)
|
28
36
|
rules = message_config[KEY_FORBID]
|
29
|
-
rules.all? { |r| !check_rule(r, data) }
|
37
|
+
rules.all? { |r| !check_rule(r, r, data) }
|
30
38
|
else
|
31
39
|
true
|
32
40
|
end
|
@@ -43,35 +51,43 @@ module Pechkin
|
|
43
51
|
# Check rule object against data. Rules are checked recursievly, i.e. we
|
44
52
|
# can take one field in rule and check it separately as new rule. If all
|
45
53
|
# fields are passed check then whole rule passed.
|
46
|
-
def check_rule(
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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}"
|
55
69
|
end
|
70
|
+
|
71
|
+
result
|
56
72
|
end
|
57
73
|
|
58
|
-
def check_hash_rule(hash, data)
|
74
|
+
def check_hash_rule(top_rule, hash, data)
|
59
75
|
return false unless data.is_a?(Hash)
|
60
76
|
|
61
77
|
hash.all? do |key, value|
|
62
|
-
data.key?(key) && check_rule(value, data[key])
|
78
|
+
data.key?(key) && check_rule(top_rule, value, data[key])
|
63
79
|
end
|
64
80
|
end
|
65
81
|
|
66
|
-
def check_array_rule(array, data)
|
82
|
+
def check_array_rule(top_rule, array, data)
|
67
83
|
return false unless data.is_a?(Array)
|
68
84
|
|
69
85
|
# Deep array check needs to be done against all elements so we zip arrays
|
70
86
|
# to pair each rule with data element
|
71
|
-
array.zip(data).all? { |r, d| check_rule(r, d) }
|
87
|
+
array.zip(data).all? { |r, d| check_rule(top_rule, r, d) }
|
72
88
|
end
|
73
89
|
|
74
|
-
def check_string_rule(str, data)
|
90
|
+
def check_string_rule(_top_rule, str, data)
|
75
91
|
str.eql? data
|
76
92
|
end
|
77
93
|
end
|
@@ -5,6 +5,12 @@ module Pechkin
|
|
5
5
|
registry = ::Prometheus::Client.registry
|
6
6
|
registry.gauge(:pechkin_start_time_seconds,
|
7
7
|
docstring: 'Startup timestamp').set(Time.now.to_i)
|
8
|
+
|
9
|
+
version_labels = { version: Pechkin::Version.version_string }
|
10
|
+
registry.gauge(:pechkin_version,
|
11
|
+
docstring: 'Pechkin version', labels: [:version])
|
12
|
+
.set(1, labels: version_labels)
|
13
|
+
|
8
14
|
registry
|
9
15
|
end
|
10
16
|
end
|
data/lib/pechkin/version.rb
CHANGED
data/lib/pechkin.rb
CHANGED
@@ -8,15 +8,15 @@ require 'htauth'
|
|
8
8
|
require 'base64'
|
9
9
|
|
10
10
|
require_relative 'pechkin/cli'
|
11
|
+
require_relative 'pechkin/bot'
|
12
|
+
require_relative 'pechkin/channel'
|
13
|
+
require_relative 'pechkin/message'
|
11
14
|
require_relative 'pechkin/command'
|
12
15
|
require_relative 'pechkin/exceptions'
|
13
16
|
require_relative 'pechkin/handler'
|
14
17
|
require_relative 'pechkin/message_matcher'
|
15
18
|
require_relative 'pechkin/message_template'
|
16
19
|
require_relative 'pechkin/connector'
|
17
|
-
require_relative 'pechkin/connector_slack'
|
18
|
-
require_relative 'pechkin/connector_telegram'
|
19
|
-
require_relative 'pechkin/channel'
|
20
20
|
require_relative 'pechkin/configuration'
|
21
21
|
require_relative 'pechkin/substitute'
|
22
22
|
require_relative 'pechkin/prometheus_utils'
|
@@ -30,8 +30,8 @@ module Pechkin # :nodoc:
|
|
30
30
|
cmd = Command::Dispatcher.new(options).dispatch
|
31
31
|
cmd.execute
|
32
32
|
rescue StandardError => e
|
33
|
-
warn
|
34
|
-
warn "\t
|
33
|
+
warn "Error: #{e.message}"
|
34
|
+
warn "\t#{e.backtrace.reverse.join("\n\t")}" if options.debug?
|
35
35
|
exit 2
|
36
36
|
end
|
37
37
|
end
|
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.
|
4
|
+
version: 1.6.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:
|
11
|
+
date: 2021-10-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: htauth
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 2.
|
19
|
+
version: 2.1.1
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 2.
|
26
|
+
version: 2.1.1
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: powerpack
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -58,14 +58,14 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - '='
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 2.
|
61
|
+
version: 2.2.3
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - '='
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: 2.
|
68
|
+
version: 2.2.3
|
69
69
|
description:
|
70
70
|
email: ilya.arkhanhelsky at gmail.com
|
71
71
|
executables:
|
@@ -81,6 +81,7 @@ files:
|
|
81
81
|
- lib/pechkin/app/app_error.rb
|
82
82
|
- lib/pechkin/app/request_handler.rb
|
83
83
|
- lib/pechkin/auth.rb
|
84
|
+
- lib/pechkin/bot.rb
|
84
85
|
- lib/pechkin/channel.rb
|
85
86
|
- lib/pechkin/cli.rb
|
86
87
|
- lib/pechkin/command.rb
|
@@ -95,12 +96,13 @@ files:
|
|
95
96
|
- lib/pechkin/configuration/configuration_loader_bots.rb
|
96
97
|
- lib/pechkin/configuration/configuration_loader_channels.rb
|
97
98
|
- lib/pechkin/configuration/configuration_loader_views.rb
|
98
|
-
- lib/pechkin/configuration/model.rb
|
99
99
|
- lib/pechkin/connector.rb
|
100
|
-
- lib/pechkin/
|
101
|
-
- lib/pechkin/
|
100
|
+
- lib/pechkin/connector/base.rb
|
101
|
+
- lib/pechkin/connector/slack.rb
|
102
|
+
- lib/pechkin/connector/telegram.rb
|
102
103
|
- lib/pechkin/exceptions.rb
|
103
104
|
- lib/pechkin/handler.rb
|
105
|
+
- lib/pechkin/message.rb
|
104
106
|
- lib/pechkin/message_matcher.rb
|
105
107
|
- lib/pechkin/message_template.rb
|
106
108
|
- lib/pechkin/prometheus_utils.rb
|
@@ -116,16 +118,16 @@ require_paths:
|
|
116
118
|
- lib
|
117
119
|
required_ruby_version: !ruby/object:Gem::Requirement
|
118
120
|
requirements:
|
119
|
-
- - "
|
121
|
+
- - ">"
|
120
122
|
- !ruby/object:Gem::Version
|
121
|
-
version: '
|
123
|
+
version: '2.5'
|
122
124
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
125
|
requirements:
|
124
126
|
- - ">="
|
125
127
|
- !ruby/object:Gem::Version
|
126
128
|
version: '0'
|
127
129
|
requirements: []
|
128
|
-
rubygems_version: 3.
|
130
|
+
rubygems_version: 3.2.22
|
129
131
|
signing_key:
|
130
132
|
specification_version: 4
|
131
133
|
summary: Web service to proxy webhooks to Telegram Bot API
|
@@ -1,28 +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: channel, code: 400,
|
17
|
-
response: 'Internal error: message is empty' }
|
18
|
-
end
|
19
|
-
|
20
|
-
params = { channel: channel, text: text, attachments: attachments }
|
21
|
-
|
22
|
-
url = 'https://slack.com/api/chat.postMessage'
|
23
|
-
response = post_data(url, params, headers: @headers)
|
24
|
-
|
25
|
-
{ channel: channel, code: response.code.to_i, response: response.body }
|
26
|
-
end
|
27
|
-
end
|
28
|
-
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: chat_id, code: response.code.to_i, response: 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
|