telegram.pl 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  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. sub notify::telegram::delete
  221. {
  222. my $body = shift;
  223. my $chat = $body->{chat};
  224. my $msg = $body->{msgid};
  225. return request("deleteMessage", {chat_id => $chat, message_id => $msg});
  226. }
  227. sub notify::telegram::update
  228. {
  229. my $body = shift;
  230. my $chat = $body->{chat};
  231. my $msg = $body->{msgid};
  232. my $message = $body->{message};
  233. my $params = {
  234. chat_id => $chat,
  235. message_id => $msg,
  236. text => $message,
  237. disable_web_page_preview => 1,
  238. };
  239. $params->{parse_mode} ||= "HTML";
  240. if ($params->{parse_mode} eq "HTML")
  241. {
  242. $params->{text} = $html_strip->process($params->{text});
  243. }
  244. return request("editMessageText", $params);
  245. }
  246. ############################
  247. sub command::help
  248. {
  249. my $cmd = shift;
  250. my $args = shift;
  251. my $chatid = shift;
  252. my $rest = shift;
  253. notify($chatid,"*/my_id* Узнать свой идентификатор в Телеграме", $rest);
  254. }
  255. sub command::my_id
  256. {
  257. my $cmd = shift;
  258. my $args = shift;
  259. my $chatid = shift;
  260. my $rest = shift;
  261. notify($chatid,"Ваш идентификатор в Телеграме: *$chatid*", $rest);
  262. }
  263. sub command::start
  264. {
  265. my $cmd = shift;
  266. my $args = shift;
  267. my $chatid = shift;
  268. my $rest = shift;
  269. notify($chatid, "$config->{title}.\nДля получения списка команд наберите */help*");
  270. }
  271. ############################
  272. sub do_command
  273. {
  274. my $cmd = shift;
  275. my $chatid = shift;
  276. my $rest = shift;
  277. my ($c,@args) = split(/ /,$cmd);
  278. $c =~ s|^/||;
  279. my $sub = reference("command::$c");
  280. unless ($sub)
  281. {
  282. return notify($chatid, "Неизвестная команда. Введите */help*, чтобы увидеть список команд", $rest);
  283. }
  284. eval {
  285. $sub->($c, \@args, $chatid, $rest);
  286. };
  287. if ($@)
  288. {
  289. $log->error("$cmd from $chatid [$rest->{from}]: $@");
  290. notify($chatid, "Ошибка при выполнении команды", $rest);
  291. return;
  292. }
  293. }
  294. sub terminate
  295. {
  296. request("setWebhook",{url=>""})->then(sub
  297. {
  298. exit(0);
  299. })->catch(sub
  300. {
  301. $log->error(Dumper @_);
  302. });
  303. }
  304. sub request
  305. {
  306. my $action = shift;
  307. my $params = shift;
  308. my $deferred = deferred;
  309. $ua->post("https://api.telegram.org/bot$config->{token}/$action" => form => $params => sub
  310. {
  311. my ($ua, $tx) = @_;
  312. my $resp = $tx->result;
  313. if ($resp->is_error)
  314. {
  315. my $err = {};
  316. $err->{code} = $resp->code;
  317. $err->{url} = $tx->req->url->to_string;
  318. $err->{body} = $resp->body;
  319. $err->{body} = j($err->{body}) if $resp->headers->content_type eq "application/json";
  320. $deferred->reject($err);
  321. }
  322. else
  323. {
  324. my $body = $resp->body;
  325. $body = j($body) if $resp->headers->content_type eq "application/json";
  326. $deferred->resolve($body);
  327. }
  328. });
  329. return $deferred->promise;
  330. }
  331. sub notify
  332. {
  333. my $chatid = shift;
  334. my $message = shift;
  335. my $rest = shift || {};
  336. my $params = {
  337. chat_id => $chatid,
  338. text => $message,
  339. disable_web_page_preview => 1,
  340. };
  341. $params->{parse_mode} ||= "HTML";
  342. $params->{reply_to_message_id} = $rest->{reply_to} if $rest->{reply_to};
  343. $params->{disable_notification} = 1 if $rest->{silent};
  344. my $disable_error_handler = delete $params->{disable_error_handler};
  345. if ($params->{parse_mode} eq "HTML")
  346. {
  347. $params->{text} = $html_strip->process($params->{text});
  348. }
  349. my $promise = request("sendMessage", $params);
  350. unless ($disable_error_handler)
  351. {
  352. $promise = $promise->catch(sub
  353. {
  354. $log->error(Dumper $params,@_);
  355. });
  356. }
  357. return $promise;
  358. }
  359. sub refpath
  360. {
  361. my $name = shift;
  362. $name =~ tr/.-/_/;
  363. $name =~ s|/|::|g;
  364. return reference($name);
  365. }
  366. sub reference
  367. {
  368. my $name = shift;
  369. return exists(&{$name}) ? \&{$name} : undef;
  370. }
  371. ##################################
  372. $log->info("Started (".app->mode.")");
  373. request("setWebhook",{url=>""})->then(sub
  374. {
  375. $log->info("Webhook to $config->{webhook}");
  376. return request("setWebhook",{url=>"$config->{webhook}/$config->{token}"});
  377. })->catch(sub
  378. {
  379. $log->error(Dumper @_);
  380. });
  381. app->start;