#!/usr/bin/perl
# Бот и рассыльщик сообщений для Телеграм
# Ю. Жиловец, 6 ноября 2016 года
# 311683401 - это я
# -166458164 - чат MOL Prog
use Modern::Perl;
use utf8;
use EV;
use AnyEvent;
use Mojolicious::Lite;
use Mojo::UserAgent;
use Data::Dumper;
use Promises qw/deferred collect/;
use Mojo::JSON qw/j/;
use NetAddr::IP;
use HTML::Restrict;
my $NAME = "telegram";
my $confdir = "config/".app->mode;
use FindBin qw/$Bin/;
use lib "$Bin/lib";
use rabbit_async_rec;
plugin yaml_config => {
file => "$confdir/$NAME.cfg",
stash_key => 'config',
};
our $config = app->config;
$config->{_alert_allowed} = [ grep {$_} map {new NetAddr::IP($_)} @{$config->{alert_allowed}} ];
app->secrets(["Marsz, Marsz, Dabrowski"]);
my $log = new Mojo::Log;
# https://core.telegram.org/bots/api#formatting-options
my $html_strip = HTML::Restrict->new(rules => {
b => [],
strong => [],
i => [],
em => [],
u => [],
ins => [],
s => [],
strike => [],
del => [],
a => [qw/href/],
code => [qw/class/],
pre => [],
});
my $term;
my $int;
my $hup;
my $rabbit;
Mojo::IOLoop->next_tick(sub
{
$term = AnyEvent->signal(signal => "TERM", cb => \&terminate);
$int = AnyEvent->signal(signal => "INT", cb => \&terminate);
$hup = AnyEvent->signal(signal => "HUP", cb => \&terminate);
$config->{rabbit}->{on_error} = sub { $log->error("[rabbit]".@_); terminate() };
my $rabbit_args = $config->{rabbit};
$rabbit_args->{product} = $NAME;
$rabbit = new rabbit_async_rec($rabbit_args, sub
{
$rabbit->listen_queue($config->{queue}, $config->{bind}, \&incoming_message);
});
});
##########################
my $ua = new Mojo::UserAgent;
$ua->max_redirects(5);
##########################
=cut
hook before_dispatch => sub
{
my $c = shift;
say $c->req->to_string;
};
hook after_dispatch => sub
{
my $c = shift;
say $c->res->to_string;
};
=cut
##########################
get "/health" => sub
{
shift->render(text => "Telegram OK");
};
sub check_ip
{
my $c = shift;
my $remip = new NetAddr::IP($c->tx->remote_address);
for (@{$config->{_alert_allowed}})
{
return 1 if $_->contains($remip);
}
$c->render(status => 403, text => "Неизвестный IP-адрес " . $c->tx->remote_address);
return undef;
}
post "/alert" => sub
{
my $c = shift;
return unless check_ip($c);
my $channel = $c->param("to");
my $params = j($c->req->body);
for my $alert (@{$params->{alerts}})
{
my $icon = "\x{2753}";
if ($alert->{status} eq "resolved")
{
$icon = "\x{1F197}";
}
elsif ($alert->{labels}->{severity} && $alert->{labels}->{severity} eq "critical")
{
$icon = "\x{1F534}";
}
elsif ($alert->{labels}->{severity} && $alert->{labels}->{severity} eq "warning")
{
$icon = "\x{26A0}";
}
my $message .= "$icon [" . ($alert->{labels}->{server}||$alert->{labels}->{location}||"") . "] " . $alert->{labels}->{alertname} . " " .$alert->{labels}->{job}
. " " . $alert->{labels}->{instance} . "\n";
$message .= $alert->{annotations}->{summary} . "\n" . "" . $alert->{annotations}->{description} . "";
my $rest = {};
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
$rest->{silent} = 1 if $hour < 9 || $hour > 23;
notify($channel, $message, $rest);
}
$c->render(text=>"ok");
};
post "/:token" => sub
{
my $c = shift;
unless ($c->param("token") eq $config->{token})
{
return $c->render(status=>401, text=>"Request from unknown URL");
}
my $body = j($c->req->body);
my $m = $body->{message};
my $chatid = $m->{chat}->{id};
my $from = ($m->{from}->{first_name}||"") . " " . ($m->{from}->{last_name}||"") . " (" . ($m->{from}->{username}||"") . ")";
my $cmd = $m->{text};
my $msgid = $m->{message_id};
$c->render(text=>"ok");
do_command($cmd, $chatid, {msgid=>$msgid, from=>$from});
};
sub incoming_message
{
my $m = shift;
my $body = $m->{content};
$log->debug($m->{routing_key}." ".Dumper($m->{content})) if $config->{debug};
my $rk = $m->{routing_key};
$rk =~ s/\./::/g;
my $sub = reference($rk);
unless ($sub)
{
$log->error("Unknown message: ".$rk);
$rabbit->reply($m, "Unknown message") if $m->{header}->{reply_to};
$rabbit->reject($m);
return;
}
my $res;
eval {
$res = $sub->($body,$m);
};
if (ref $res eq "Promises::Promise")
{
$res->then(sub
{
my $result = shift;
handle_reply($m, undef, $result);
})
->catch(sub
{
my $err = shift;
handle_reply($m, $err, undef);
})
}
else
{
handle_reply($m, $@, $res);
}
}
sub handle_reply
{
my $m = shift;
my $err = shift;
my $res = shift;
if ($err)
{
$log->error(Dumper $@);
$rabbit->reply($m, $@) if $m->{header}->{reply_to};
$rabbit->reject($m);
}
else
{
$rabbit->ack($m);
$rabbit->reply($m, $res) if $m->{header}->{reply_to};
$log->debug("acknowledged") if $config->{debug};
}
}
sub notify::telegram::send
{
my $body = shift;
my $to = $body->{to};
$to = [ $to ] unless ref($to) eq "ARRAY";
$body->{disable_error_handler} = 1;
my @results;
my @promises = map {
notify($_, $body->{message}, $body)->then(sub
{
my $reply = shift;
push @results, { success=>1, msgid=>$reply->{result}->{message_id}, chat=>$reply->{result}->{chat}->{id} };
})
->catch(sub
{
my $reply = shift;
push @results, {success=>0, error => ref($reply) ? "$reply->{code}: $reply->{body}->{description}" : $reply};
})
} @$to;
return collect(@promises)->then(sub
{
return \@results;
})
}
############################
sub command::help
{
my $cmd = shift;
my $args = shift;
my $chatid = shift;
my $rest = shift;
notify($chatid,"*/my_id* Узнать свой идентификатор в Телеграме", $rest);
}
sub command::my_id
{
my $cmd = shift;
my $args = shift;
my $chatid = shift;
my $rest = shift;
notify($chatid,"Ваш идентификатор в Телеграме: *$chatid*", $rest);
}
sub command::start
{
my $cmd = shift;
my $args = shift;
my $chatid = shift;
my $rest = shift;
notify($chatid, "$config->{title}.\nДля получения списка команд наберите */help*");
}
############################
sub do_command
{
my $cmd = shift;
my $chatid = shift;
my $rest = shift;
my ($c,@args) = split(/ /,$cmd);
$c =~ s|^/||;
my $sub = reference("command::$c");
unless ($sub)
{
return notify($chatid, "Неизвестная команда. Введите */help*, чтобы увидеть список команд", $rest);
}
eval {
$sub->($c, \@args, $chatid, $rest);
};
if ($@)
{
$log->error("$cmd from $chatid [$rest->{from}]: $@");
notify($chatid, "Ошибка при выполнении команды", $rest);
return;
}
}
sub terminate
{
request("setWebhook",{url=>""})->then(sub
{
exit(0);
})->catch(sub
{
$log->error(Dumper @_);
});
}
sub request
{
my $action = shift;
my $params = shift;
my $deferred = deferred;
$ua->post("https://api.telegram.org/bot$config->{token}/$action" => form => $params => sub
{
my ($ua, $tx) = @_;
my $resp = $tx->result;
if ($resp->is_error)
{
my $err = {};
$err->{code} = $resp->code;
$err->{url} = $tx->req->url->to_string;
$err->{body} = $resp->body;
$err->{body} = j($err->{body}) if $resp->headers->content_type eq "application/json";
$deferred->reject($err);
}
else
{
my $body = $resp->body;
$body = j($body) if $resp->headers->content_type eq "application/json";
$deferred->resolve($body);
}
});
return $deferred->promise;
}
sub notify
{
my $chatid = shift;
my $message = shift;
my $rest = shift || {};
my $params = {
chat_id => $chatid,
text => $message,
disable_web_page_preview => 1,
};
$params->{parse_mode} = "HTML" unless $rest->{parse_mode} && $rest->{parse_mode} eq "none";
$params->{reply_to_message_id} = $rest->{msgid} if $rest->{msgid};
$params->{disable_notification} = 1 if $rest->{silent};
my $disable_error_handler = delete $params->{disable_error_handler};
if ($params->{parse_mode} && $params->{parse_mode} eq "HTML")
{
$params->{text} = $html_strip->process($params->{text});
}
my $promise = request("sendMessage", $params);
unless ($disable_error_handler)
{
$promise = $promise->catch(sub
{
$log->error(Dumper $params,@_);
});
}
return $promise;
}
sub refpath
{
my $name = shift;
$name =~ tr/.-/_/;
$name =~ s|/|::|g;
return reference($name);
}
sub reference
{
my $name = shift;
return exists(&{$name}) ? \&{$name} : undef;
}
##################################
$log->info("Started (".app->mode.")");
request("setWebhook",{url=>""})->then(sub
{
$log->info("Webhook to $config->{webhook}");
return request("setWebhook",{url=>"$config->{webhook}/$config->{token}"});
})->catch(sub
{
$log->error(Dumper @_);
});
app->start;