telegram.pl 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. #!/usr/bin/perl
  2. # Бот и рассыльщик сообщений для Телеграм
  3. # Ю. Жиловец, 6 ноября 2016 года
  4. # 311683401 - это я
  5. # -166458164 - чат MOL Prog
  6. use Modern::Perl;
  7. use utf8;
  8. use EV;
  9. use AnyEvent;
  10. use Mojolicious::Lite;
  11. use Mojo::UserAgent;
  12. use Data::Dumper;
  13. use Promises qw/deferred collect/;
  14. use Mojo::JSON qw/j/;
  15. use NetAddr::IP;
  16. use HTML::Restrict;
  17. my $NAME = "telegram";
  18. my $confdir = "config/".app->mode;
  19. use FindBin qw/$Bin/;
  20. use lib "$Bin/lib";
  21. use rabbit_async_rec;
  22. plugin yaml_config => {
  23. file => "$confdir/$NAME.cfg",
  24. stash_key => 'config',
  25. };
  26. our $config = app->config;
  27. $config->{_alert_allowed} = [ grep {$_} map {new NetAddr::IP($_)} @{$config->{alert_allowed}} ];
  28. app->secrets(["Marsz, Marsz, Dabrowski"]);
  29. my $log = new Mojo::Log;
  30. # https://core.telegram.org/bots/api#formatting-options
  31. my $html_strip = HTML::Restrict->new(rules => {
  32. b => [],
  33. strong => [],
  34. i => [],
  35. em => [],
  36. u => [],
  37. ins => [],
  38. s => [],
  39. strike => [],
  40. del => [],
  41. a => [qw/href/],
  42. code => [qw/class/],
  43. pre => [],
  44. });
  45. my $term;
  46. my $int;
  47. my $hup;
  48. my $rabbit;
  49. Mojo::IOLoop->next_tick(sub
  50. {
  51. $term = AnyEvent->signal(signal => "TERM", cb => \&terminate);
  52. $int = AnyEvent->signal(signal => "INT", cb => \&terminate);
  53. $hup = AnyEvent->signal(signal => "HUP", cb => \&terminate);
  54. $config->{rabbit}->{on_error} = sub { $log->error("[rabbit]".@_); terminate() };
  55. my $rabbit_args = $config->{rabbit};
  56. $rabbit_args->{product} = $NAME;
  57. $rabbit = new rabbit_async_rec($rabbit_args, sub
  58. {
  59. $rabbit->listen_queue($config->{queue}, $config->{bind}, \&incoming_message);
  60. });
  61. });
  62. ##########################
  63. my $ua = new Mojo::UserAgent;
  64. $ua->max_redirects(5);
  65. ##########################
  66. =cut
  67. hook before_dispatch => sub
  68. {
  69. my $c = shift;
  70. say $c->req->to_string;
  71. };
  72. hook after_dispatch => sub
  73. {
  74. my $c = shift;
  75. say $c->res->to_string;
  76. };
  77. =cut
  78. ##########################
  79. get "/health" => sub
  80. {
  81. shift->render(text => "Telegram OK");
  82. };
  83. sub check_ip
  84. {
  85. my $c = shift;
  86. my $remip = new NetAddr::IP($c->tx->remote_address);
  87. for (@{$config->{_alert_allowed}})
  88. {
  89. return 1 if $_->contains($remip);
  90. }
  91. $c->render(status => 403, text => "Неизвестный IP-адрес " . $c->tx->remote_address);
  92. return undef;
  93. }
  94. post "/alert" => sub
  95. {
  96. my $c = shift;
  97. return unless check_ip($c);
  98. my $channel = $c->param("to");
  99. my $params = j($c->req->body);
  100. for my $alert (@{$params->{alerts}})
  101. {
  102. my $icon = "\x{2753}";
  103. if ($alert->{status} eq "resolved")
  104. {
  105. $icon = "\x{1F197}";
  106. }
  107. elsif ($alert->{labels}->{severity} && $alert->{labels}->{severity} eq "critical")
  108. {
  109. $icon = "\x{1F534}";
  110. }
  111. elsif ($alert->{labels}->{severity} && $alert->{labels}->{severity} eq "warning")
  112. {
  113. $icon = "\x{26A0}";
  114. }
  115. my $message .= "$icon <code> [" . ($alert->{labels}->{server}||$alert->{labels}->{location}||"") . "] " . $alert->{labels}->{alertname} . " " .$alert->{labels}->{job}
  116. . " " . $alert->{labels}->{instance} . "</code>\n";
  117. $message .= $alert->{annotations}->{summary} . "\n" . "<i>" . $alert->{annotations}->{description} . "</i>";
  118. my $rest = {};
  119. my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  120. $rest->{silent} = 1 if $hour < 9 || $hour > 23;
  121. notify($channel, $message, $rest);
  122. }
  123. $c->render(text=>"ok");
  124. };
  125. post "/:token" => sub
  126. {
  127. my $c = shift;
  128. unless ($c->param("token") eq $config->{token})
  129. {
  130. return $c->render(status=>401, text=>"Request from unknown URL");
  131. }
  132. my $body = j($c->req->body);
  133. my $m = $body->{message};
  134. my $chatid = $m->{chat}->{id};
  135. my $from = ($m->{from}->{first_name}||"") . " " . ($m->{from}->{last_name}||"") . " (" . ($m->{from}->{username}||"") . ")";
  136. my $cmd = $m->{text};
  137. my $msgid = $m->{message_id};
  138. $c->render(text=>"ok");
  139. do_command($cmd, $chatid, {msgid=>$msgid, from=>$from});
  140. };
  141. sub incoming_message
  142. {
  143. my $m = shift;
  144. my $body = $m->{content};
  145. $log->debug($m->{routing_key}." ".Dumper($m->{content})) if $config->{debug};
  146. my $rk = $m->{routing_key};
  147. $rk =~ s/\./::/g;
  148. my $sub = reference($rk);
  149. unless ($sub)
  150. {
  151. $log->error("Unknown message: ".$rk);
  152. $rabbit->reply($m, "Unknown message") if $m->{header}->{reply_to};
  153. $rabbit->reject($m);
  154. return;
  155. }
  156. my $res;
  157. eval {
  158. $res = $sub->($body,$m);
  159. };
  160. if (ref $res eq "Promises::Promise")
  161. {
  162. $res->then(sub
  163. {
  164. my $result = shift;
  165. handle_reply($m, undef, $result);
  166. })
  167. ->catch(sub
  168. {
  169. my $err = shift;
  170. handle_reply($m, $err, undef);
  171. })
  172. }
  173. else
  174. {
  175. handle_reply($m, $@, $res);
  176. }
  177. }
  178. sub handle_reply
  179. {
  180. my $m = shift;
  181. my $err = shift;
  182. my $res = shift;
  183. if ($err)
  184. {
  185. $log->error(Dumper $@);
  186. $rabbit->reply($m, $@) if $m->{header}->{reply_to};
  187. $rabbit->reject($m);
  188. }
  189. else
  190. {
  191. $rabbit->ack($m);
  192. $rabbit->reply($m, $res) if $m->{header}->{reply_to};
  193. $log->debug("acknowledged") if $config->{debug};
  194. }
  195. }
  196. sub notify::telegram::send
  197. {
  198. my $body = shift;
  199. my $to = $body->{to};
  200. $to = [ $to ] unless ref($to) eq "ARRAY";
  201. $body->{disable_error_handler} = 1;
  202. my @results;
  203. my @promises = map {
  204. notify($_, $body->{message}, $body)->then(sub
  205. {
  206. my $reply = shift;
  207. push @results, { success=>1, msgid=>$reply->{result}->{message_id}, chat=>$reply->{result}->{chat}->{id} };
  208. })
  209. ->catch(sub
  210. {
  211. my $reply = shift;
  212. push @results, {success=>0, error => ref($reply) ? "$reply->{code}: $reply->{body}->{description}" : $reply};
  213. })
  214. } @$to;
  215. return collect(@promises)->then(sub
  216. {
  217. return \@results;
  218. })
  219. }
  220. ############################
  221. sub command::help
  222. {
  223. my $cmd = shift;
  224. my $args = shift;
  225. my $chatid = shift;
  226. my $rest = shift;
  227. notify($chatid,"*/my_id* Узнать свой идентификатор в Телеграме", $rest);
  228. }
  229. sub command::my_id
  230. {
  231. my $cmd = shift;
  232. my $args = shift;
  233. my $chatid = shift;
  234. my $rest = shift;
  235. notify($chatid,"Ваш идентификатор в Телеграме: *$chatid*", $rest);
  236. }
  237. sub command::start
  238. {
  239. my $cmd = shift;
  240. my $args = shift;
  241. my $chatid = shift;
  242. my $rest = shift;
  243. notify($chatid, "$config->{title}.\nДля получения списка команд наберите */help*");
  244. }
  245. ############################
  246. sub do_command
  247. {
  248. my $cmd = shift;
  249. my $chatid = shift;
  250. my $rest = shift;
  251. my ($c,@args) = split(/ /,$cmd);
  252. $c =~ s|^/||;
  253. my $sub = reference("command::$c");
  254. unless ($sub)
  255. {
  256. return notify($chatid, "Неизвестная команда. Введите */help*, чтобы увидеть список команд", $rest);
  257. }
  258. eval {
  259. $sub->($c, \@args, $chatid, $rest);
  260. };
  261. if ($@)
  262. {
  263. $log->error("$cmd from $chatid [$rest->{from}]: $@");
  264. notify($chatid, "Ошибка при выполнении команды", $rest);
  265. return;
  266. }
  267. }
  268. sub terminate
  269. {
  270. request("setWebhook",{url=>""})->then(sub
  271. {
  272. exit(0);
  273. })->catch(sub
  274. {
  275. $log->error(Dumper @_);
  276. });
  277. }
  278. sub request
  279. {
  280. my $action = shift;
  281. my $params = shift;
  282. my $deferred = deferred;
  283. $ua->post("https://api.telegram.org/bot$config->{token}/$action" => form => $params => sub
  284. {
  285. my ($ua, $tx) = @_;
  286. my $resp = $tx->result;
  287. if ($resp->is_error)
  288. {
  289. my $err = {};
  290. $err->{code} = $resp->code;
  291. $err->{url} = $tx->req->url->to_string;
  292. $err->{body} = $resp->body;
  293. $err->{body} = j($err->{body}) if $resp->headers->content_type eq "application/json";
  294. $deferred->reject($err);
  295. }
  296. else
  297. {
  298. my $body = $resp->body;
  299. $body = j($body) if $resp->headers->content_type eq "application/json";
  300. $deferred->resolve($body);
  301. }
  302. });
  303. return $deferred->promise;
  304. }
  305. sub notify
  306. {
  307. my $chatid = shift;
  308. my $message = shift;
  309. my $rest = shift || {};
  310. my $params = {
  311. chat_id => $chatid,
  312. text => $message,
  313. disable_web_page_preview => 1,
  314. };
  315. $params->{parse_mode} = "HTML" unless $rest->{parse_mode} && $rest->{parse_mode} eq "none";
  316. $params->{reply_to_message_id} = $rest->{msgid} if $rest->{msgid};
  317. $params->{disable_notification} = 1 if $rest->{silent};
  318. my $disable_error_handler = delete $params->{disable_error_handler};
  319. if ($params->{parse_mode} && $params->{parse_mode} eq "HTML")
  320. {
  321. $params->{text} = $html_strip->process($params->{text});
  322. }
  323. my $promise = request("sendMessage", $params);
  324. unless ($disable_error_handler)
  325. {
  326. $promise = $promise->catch(sub
  327. {
  328. $log->error(Dumper $params,@_);
  329. });
  330. }
  331. return $promise;
  332. }
  333. sub refpath
  334. {
  335. my $name = shift;
  336. $name =~ tr/.-/_/;
  337. $name =~ s|/|::|g;
  338. return reference($name);
  339. }
  340. sub reference
  341. {
  342. my $name = shift;
  343. return exists(&{$name}) ? \&{$name} : undef;
  344. }
  345. ##################################
  346. $log->info("Started (".app->mode.")");
  347. request("setWebhook",{url=>""})->then(sub
  348. {
  349. $log->info("Webhook to $config->{webhook}");
  350. return request("setWebhook",{url=>"$config->{webhook}/$config->{token}"});
  351. })->catch(sub
  352. {
  353. $log->error(Dumper @_);
  354. });
  355. app->start;