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 +4 -4
- data/lib/pechkin/app/app.rb +44 -0
- data/lib/pechkin/app/app_builder.rb +39 -0
- data/lib/pechkin/app/app_error.rb +27 -0
- data/lib/pechkin/app/request_handler.rb +49 -0
- data/lib/pechkin/app.rb +4 -114
- data/lib/pechkin/auth.rb +1 -0
- data/lib/pechkin/channel.rb +1 -1
- data/lib/pechkin/cli.rb +1 -1
- data/lib/pechkin/command/base.rb +1 -1
- data/lib/pechkin/command/run_server.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 +2 -6
- data/lib/pechkin/configuration/configuration_loader_views.rb +1 -3
- data/lib/pechkin/configuration.rb +4 -4
- 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 +46 -24
- data/lib/pechkin/message_matcher.rb +94 -0
- data/lib/pechkin/version.rb +1 -1
- data/lib/pechkin.rb +3 -4
- metadata +17 -25
- data/lib/pechkin/connector_slack.rb +0 -27
- 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: 3fa8abd425ad6a776104aabeeb6916d4b8b4d77ae0f0d7e24b75f102bb70b016
|
4
|
+
data.tar.gz: 1b5b43910b029f8553f54ddfa5d70a7195b2db86d35a38ba1fcffc37521b7d4b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
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
data/lib/pechkin/channel.rb
CHANGED
@@ -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(
|
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,
|
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,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/
|
14
|
-
# | | - marvin.yml
|
13
|
+
# | - bots/ <- Bots configuration
|
14
|
+
# | | - marvin.yml <- Each bot described by yaml file
|
15
15
|
# | | - bender.yml
|
16
16
|
# |
|
17
|
-
# | - channels/
|
17
|
+
# | - channels/ <- Channels description
|
18
18
|
# | | - slack-repository-feed
|
19
19
|
# | | - commit-hg.yml
|
20
20
|
# | | - commit-svn.yml
|
21
21
|
# |
|
22
|
-
# | - views/
|
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
|
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
@@ -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 =
|
25
|
-
|
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
|
-
|
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 =
|
51
|
-
|
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
|
-
|
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
|
data/lib/pechkin/version.rb
CHANGED
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
|
33
|
-
warn "\t
|
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.
|
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:
|
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.
|
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.
|
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.
|
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.
|
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/
|
111
|
-
- lib/pechkin/
|
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: '
|
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.
|
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
|