commands.pm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. use Modern::Perl;
  2. use utf8;
  3. use Mojo::Base -strict, -async_await, -signatures;
  4. use Data::Dumper;
  5. use experimental qw/switch/;
  6. our $client;
  7. our $abon_client;
  8. our $config;
  9. our $redis;
  10. sub __
  11. {
  12. @_;
  13. }
  14. ##############################################
  15. our $commands = [
  16. {command=>"help", name=>__("Помощь"), description=>__("Список доступных команд")},
  17. {command=>"info", name=>__("Информация"), description=>__("Информация о пользователе")},
  18. {command=>"balance", name=>__("Баланс"), description=>__("Проверка баланса"), main=>1, icon=>"\x{1F45B}"},
  19. {command=>"service", name=>__("Сервисы"), description=>__("Подключенные сервисы")},
  20. {command=>"credit", name=>__("Кредит"), description=>__("Установка кредита"), main=>1, icon=>"\x{1FAF0}"},
  21. {command=>"card", name=>__("Карта пополнения"), description=>__("Оплата карточкой пополнения"), main=>1, icon=>"\x{1F4B3}"},
  22. {command=>"payberry", name=>__("Payberry"), description=>__("Оплата через Payberry"), main=>1, icon=>"\x{1F17F}\x{FE0F}"},
  23. {command=>"transfer", name=>__("Перевод"), description=>__("Перевод денег")},
  24. {command=>"new_task", name=>__("Новая заявка"), description=>__("Новая заявка")},
  25. {command=>"tasks", name=>__("Заявки"), description=>__("Открытые заявки"), main=>1, icon=>"\x{2692}\x{FE0F}"},
  26. {command=>"support", name=>__("Техподдержка"), description=>__("Связь с техподдержкой")},
  27. {command=>"logout", name=>__("Выход"), description=>__("Выход")},
  28. ];
  29. #############################
  30. our $lex_actions = {
  31. xfer => {
  32. name => __("перевод денег"),
  33. canceled => __("отменён"),
  34. },
  35. card => {
  36. name => __("пополнение карточкой"),
  37. canceled => __("отмененo"),
  38. },
  39. };
  40. our $lex_vars = {
  41. xfer_to => __("номер личного счета"),
  42. xfer_amount => __("сумму"),
  43. card_code => __("код карточки"),
  44. card_serial => __("номер карточки"),
  45. };
  46. ##############################################
  47. sub command_help($fsa, $info)
  48. {
  49. my @list = map { "<b>/$_->{command}</b> " . _($_->{description}) } @$commands;
  50. reply($info, join("\n", @list));
  51. }
  52. async sub command_logout
  53. {
  54. my ($fsa, $info) = @_;
  55. my $uid = $fsa->note("[uid]");
  56. await $client->delete_p("client", "/client/$uid/telegram");
  57. reply($info, _("Благодарим за использование нашего бота"));
  58. $fsa->delete_note("[uid]");
  59. $fsa->state("logged_out");
  60. }
  61. async sub command_balance
  62. {
  63. my ($fsa, $info) = @_;
  64. my $uid = $fsa->note("[uid]");
  65. my $money = await $abon_client->get_p($info, "client", "/client/$uid/money?human=1");
  66. my $cur = $money->{human};
  67. my @lines = (
  68. sprintf("<u>%s:</u> <b>%.2f $cur</b> (%s %.2f $cur + %s %.2f $cur) ",
  69. _("Ваш баланс"), $money->{balance}, ("депозит"), $money->{deposit}, _("кредит"), $money->{credit}),
  70. );
  71. push @lines, sprintf("<u>%s:</u> %s", _("Оплачено до"), format_date($money->{last_day})) if $money->{last_day} ne "-";
  72. push @lines, sprintf("<u>%s:</u> %d%%", _("Скидка"), $money->{reduction}) if $money->{reduction};
  73. push @lines, sprintf("<u>%s:</u> <b>%.2f $cur</b> %s (%s)",
  74. _("Последнее снятие"), $money->{last_withdrawal}->{sum}, format_time($money->{last_withdrawal}->{date}), $money->{last_withdrawal}->{comment})
  75. if $money->{last_withdrawal}->{sum};
  76. push @lines, sprintf("<u>%s:</u> <b>%.2f $cur</b> %s", _("Последний платеж"), $money->{last_payment}->{sum}, format_time($money->{last_payment}->{date}))
  77. if $money->{last_payment}->{sum};
  78. for (keys %{ $money->{accounts} })
  79. {
  80. push @lines, sprintf("<u>%s:</u> <b>%.2f $cur</b>", $_, $money->{accounts}->{$_});
  81. }
  82. reply($info, @lines);
  83. }
  84. async sub command_info
  85. {
  86. my ($fsa, $info) = @_;
  87. my $uid = $fsa->note("[uid]");
  88. my $client = await $abon_client->get_p($info, "client", "/client/$uid");
  89. reply($info,
  90. sprintf("<u>%s</u>: %d", _("Номер учетной записи"), $client->{uid}),
  91. sprintf("<u>%s</u>: %s", _("Логин"), $client->{login}),
  92. sprintf("<u>%s</u>: %s", _("ФИО"), $client->{fio}),
  93. sprintf("<u>%s</u>: %s", _("Адрес"), $client->{address}),
  94. sprintf("<u>%s</u>: %s", _("Телефон"), $client->{phone}),
  95. );
  96. }
  97. async sub command_credit
  98. {
  99. my ($fsa, $info) = @_;
  100. my $uid = $fsa->note("[uid]");
  101. my $money = await $abon_client->get_p($info, "client", "/client/$uid/money?human=1");
  102. if ($money->{credit} > 0)
  103. {
  104. return reply($info, sprintf("%s <b>%.2f %s</b>", _("У вас уже установлен кредит"), $money->{credit}, $money->{human}));
  105. }
  106. reply_with($info, {
  107. inline_menu => [[
  108. { text=>_("Я согласен с условиями"), callback_data=>"\x00/set-credit" },
  109. ]],
  110. },
  111. _("Вы можете самостоятельно установить кредит на два дня"),
  112. _("<b>Ограничения</b>: только для физических лиц, продлевать кредит повторно до оплаты нельзя. При следующей оплате кредит будет погашен"),
  113. );
  114. }
  115. async sub callback_set_credit
  116. {
  117. my ($fsa, $info) = @_;
  118. my $uid = $fsa->note("[uid]");
  119. my $res = await $abon_client->post_p($info, "client", "/client/$uid/credit", {human=>1});
  120. return reply($info, _("Кредит не имеет смысла для бесплатных тарифных планов")) if $res->{credit} == 0;
  121. await reply_with($info, {button_menu=>1},
  122. sprintf("%s <b>%.2f %s</b>", _("Установлен кредит "), $res->{credit}, $res->{human}),
  123. "",
  124. );
  125. command_balance($fsa, $info);
  126. }
  127. async sub command_service
  128. {
  129. my ($fsa, $info) = @_;
  130. my $uid = $fsa->note("[uid]");
  131. my $res = await $abon_client->get_p($info, "client", "/client/$uid/service?human=1&as-array=1");
  132. my @list = map { sprintf("<u>%s:</u> %s (%s '%s')", $_->{name_ru}, format_wd($_->{tariff}, $_->{human}), _("тариф"), $_->{tariff}->{name_ru}) }
  133. grep { !$_->{disabled} } @$res;
  134. reply($info, @list);
  135. };
  136. ######################################
  137. async sub command_transfer
  138. {
  139. my ($fsa, $info) = @_;
  140. reply($info, _("Введите номер личного счета пользователя, которому вы хотите перевести деньги со своего собственного счета"));
  141. return needs_input($fsa, "xfer", "xfer_to");
  142. }
  143. async sub verify_xfer_to
  144. {
  145. my ($fsa, $target_uid, $info) = @_;
  146. my $uid = $fsa->note("[uid]");
  147. return _("Номер личного счета должен состоять из цифр") unless $target_uid =~ /^\d+$/;
  148. return _("Нельзя перевести деньги себе самому") if $uid==$target_uid;
  149. my $res = await $abon_client->get_p($info, "client", "/client/$target_uid");
  150. return _("Абонент") . " $target_uid " . _("отключен") if $res->{disabled};
  151. $fsa->note("xfer_fio" => $res->{fio});
  152. return undef;
  153. }
  154. async sub use_xfer_to
  155. {
  156. my ($fsa, $target_uid, $info) = @_;
  157. my $uid = $fsa->note("[uid]");
  158. my $nick = $fsa->note("xfer_fio");
  159. my @fio = split(/\s+/, $nick);
  160. if (@fio)
  161. {
  162. my $f = shift(@fio);
  163. $nick = join(" ", (substr($f, 0, 1) . ".", @fio));
  164. }
  165. $fsa->note(xfer_nick => $nick);
  166. reply(
  167. $info, _("Получатель денег: ") . $nick,
  168. _("Теперь введите сумму, которую хотите перевести"),
  169. );
  170. return needs_input($fsa, "xfer", "xfer_amount");
  171. }
  172. sub verify_xfer_amount
  173. {
  174. my ($fsa, $amount, $info) = @_;
  175. my $uid = $fsa->note("[uid]");
  176. $amount =~ s/,/./g;
  177. $amount =~ s/[^\d\.]//g;
  178. my $tmp = $amount;
  179. my $count = $amount =~ tr/.//;
  180. return _("Вы ввели неправильную сумму") if !$amount || $count>1;
  181. }
  182. async sub use_xfer_amount
  183. {
  184. my ($fsa, $amount, $info) = @_;
  185. my $uid = $fsa->note("[uid]");
  186. my $to_uid = $fsa->note("xfer_to");
  187. my $nick = $fsa->note("xfer_nick");
  188. reply_with($info, {
  189. inline_menu => [[
  190. { text=>_("Подтвердите перевод"), callback_data=>"\x00/transfer" },
  191. ]]
  192. },
  193. sprintf("%.2f %s %s %d (%s)", $amount, $config->{currency}->{human}, _(" на счет абонента"), $to_uid, $nick)
  194. );
  195. return "command";
  196. }
  197. async sub callback_transfer
  198. {
  199. my ($fsa, $info) = @_;
  200. my $uid = $fsa->note("[uid]");
  201. my $to_uid = $fsa->note("xfer_to");
  202. my $amount = $fsa->note("xfer_amount");
  203. return unless $to_uid && $amount;
  204. my $res = $abon_client->post_p($info, "client", "/client/$uid/money/to/$to_uid", {
  205. amount => $amount,
  206. ip => "0.0.0.0",
  207. via => "abonbot",
  208. currency => $config->{currency}->{name},
  209. });
  210. reply_with($info, {button_menu=>1}, _("Деньги успешно переведены"));
  211. command_balance($fsa, $info);
  212. }
  213. ##########################################
  214. async sub command_new_task
  215. {
  216. my ($fsa, $info) = @_;
  217. my $uid = $fsa->note("[uid]");
  218. my $res = await $abon_client->get_p($info, "task", "/task?created_by_client=$uid&list=new,work");
  219. if ($res->{total})
  220. {
  221. return reply($info, _("Создание новой заявки невозможно, пока не будут решены уже открытые"));
  222. }
  223. reply($info, _("Изложите вашу проблему"));
  224. $fsa->state("task_needs_descr");
  225. };
  226. async sub callback_task_post
  227. {
  228. my ($fsa, $info) = @_;
  229. my $uid = $fsa->note("[uid]");
  230. my $descr = $fsa->note("task_text");
  231. return unless $descr;
  232. my $params = {
  233. description => $descr,
  234. "for-client" => $uid,
  235. client => $uid,
  236. type => "client-issue",
  237. list => "new",
  238. task_attr => {
  239. source => "telegram"
  240. },
  241. };
  242. my $res = await $client->post_json_p("task", "/task", $params);
  243. reply_with($info, {button_menu=>1}, _("Ваша заявка размещена"));
  244. command_tasks($fsa, $info);
  245. }
  246. async sub callback_task_cancel
  247. {
  248. my ($fsa, $info) = @_;
  249. $fsa->delete_note("task_text");
  250. $fsa->delete_note("if_edited");
  251. reply_with($info, {button_menu=>1}, _("Заявка отменена"));
  252. }
  253. async sub command_tasks
  254. {
  255. my ($fsa, $info) = @_;
  256. my $uid = $fsa->note("[uid]");
  257. my $res = await $abon_client->get_p($info, "task", "/task?created_by_client=$uid&list=new,work&sort=id");
  258. return reply($info, _("Открытых заявок нет")) unless $res->{total};
  259. my @str = map {
  260. _("Заявка") . " <b>$_->{number}</b>\n$_->{description}\n<i>" . _("Комментариев") . " " . $_->{total_comments} . ", " . _(" не прочитано") . " " . $_->{unread_comments} . "</i>"
  261. } @{$res->{data}};
  262. my $menu = [[
  263. map {
  264. { text => _("Комментарии к заявке") . " " . $_->{number}, callback_data => "\x00/task $_->{entity}" }
  265. } @{$res->{data}}
  266. ]];
  267. reply_with($info, {inline_menu=>$menu}, @str);
  268. };
  269. async sub callback_task
  270. {
  271. my ($fsa, $info, $task_id) = @_;
  272. my $uid = $fsa->note("[uid]");
  273. my $res = await $client->get_p("task", "/task/$task_id?with_comments=1");
  274. my @str = map {
  275. format_timestamp($_->{created}) . " " . "<b>" . ($_->{created_by}->[0] eq "worker" ? _("Оператор") : _("Вы")) . ":</b>\n"
  276. . $_->{text}
  277. } grep { $_->{visibleToUser} } @{ $res->{comments} };
  278. my $menu = [[
  279. {text => _("Добавить комментарий"), callback_data => "\x00comment $task_id"},
  280. ]];
  281. reply_with($info, {inline_menu=>$menu}, @str);
  282. $client->post_p("task", "/task/$task_id/comment/read", {for_client=>$uid});
  283. }
  284. async sub callback_comment
  285. {
  286. my ($fsa, $info, $task_id) = @_;
  287. $fsa->note(task_id => $task_id);
  288. $fsa->state("task_needs_comment");
  289. reply($info, _("Введите текст вашего ответа"));
  290. }
  291. ##################################################
  292. async sub command_payberry
  293. {
  294. my ($fsa, $info) = @_;
  295. my $uid = $fsa->note("[uid]");
  296. my $menu = [[
  297. {text => _("Payberry"), url => $config->{pay}->{payberry_url} . "?acc=$uid"}
  298. ]];
  299. reply_with($info, {inline_menu=>$menu}, _("Для оплаты перейдите по ссылке"));
  300. }
  301. #####################################
  302. use constant FAIL_BLOCK => 10;
  303. use constant FAIL_ASK_SERIAL => 3;
  304. async sub command_card
  305. {
  306. my ($fsa, $info) = @_;
  307. my $uid = $fsa->note("[uid]");
  308. my $key = "card-guess-$uid";
  309. my $failed = $redis->get($key) || 0;
  310. if ($failed > FAIL_BLOCK)
  311. {
  312. return reply($info, _("Вы ввели неправильный код слишком много раз. Пополнение карточкой заблокировано на сутки"));
  313. }
  314. my $res = await $client->get_p("client", "/client/$uid");
  315. if ($res->{disabled})
  316. {
  317. return reply($info, _("Пополнение счета недоступно отключенным пользователям"));
  318. }
  319. reply($info, _("Введите код карточки пополнения (16 цифр, можно разделять их знаком '-')"));
  320. return needs_input($fsa, "card", "card_code");
  321. }
  322. sub verify_card_code($fsa, $code, $info)
  323. {
  324. $code =~ s/\D//g;
  325. return _("Код карточки должен состоять из 16 цифр") unless $code =~ /^\d{16}$/;
  326. return undef;
  327. }
  328. async sub use_card_code
  329. {
  330. my ($fsa, $code, $info) = @_;
  331. my $uid = $fsa->note("[uid]");
  332. my $key = "card-guess-$uid";
  333. my $fails = $redis->get($key) || 0;
  334. if ($fails > FAIL_ASK_SERIAL)
  335. {
  336. reply($info, _("Теперь введите номер карточки (7 цифр, можно разделять знаком'-')"));
  337. return needs_input($fsa, "card", "card_serial");
  338. }
  339. else
  340. {
  341. pay_from_card($info, $uid, $code, "");
  342. return "command";
  343. }
  344. }
  345. sub verify_card_serial($fsa, $serial, $info)
  346. {
  347. $serial =~ s/\D//g;
  348. return _("Номер карточки должен состоять из 7 цифр") unless $serial =~ /^\d{7}$/;
  349. return undef;
  350. }
  351. async sub use_card_serial
  352. {
  353. my ($fsa, $serial, $info) = @_;
  354. pay_from_card($info, $fsa->note("[uid]"), $fsa->note("card_code"), $serial);
  355. }
  356. async sub pay_from_card
  357. {
  358. my ($info, $uid, $code, $serial) = @_;
  359. my $args = {
  360. code => $code =~ s/\D//gr,
  361. uid => $uid,
  362. domain => $config->{domain},
  363. source => "abonbot",
  364. ip => "0.0.0.0",
  365. };
  366. $args->{serial} = $serial =~ s/\D//gr if $serial;
  367. my $res = eval { await $client->post_p("card", "/redemption", $args) };
  368. unless($@)
  369. {
  370. return reply_with($info, {button_menu=>1}, _("Ваш счёт пополнен на " . $res->{amount} . " " . $res->{currency_short_name_ru} . ". " . _("Оплачено до ") . $res->{paid_until}));
  371. };
  372. given($@->{code})
  373. {
  374. when(400)
  375. {
  376. reply_with($info, {button_menu=>1}, _("Вы ввели неправильный номер или код карточки"));
  377. my $key = "card-guess-$uid";
  378. $redis->incr($key);
  379. $redis->expire($key, 24 * 3600);
  380. }
  381. when(409)
  382. {
  383. reply_with($info, {button_menu=>1}, _("Карточка уже использована"));
  384. }
  385. when(406)
  386. {
  387. reply_with($info, {button_menu=>1}, _("Карточка не активирована. Обратитесь в службу поддержки"));
  388. }
  389. default
  390. {
  391. die $@;
  392. }
  393. }
  394. };
  395. #####################################################
  396. sub command_support
  397. {
  398. my ($fsa, $info) = @_;
  399. return reply($info, _("Телефоны техподдержки:"), @{$config->{support_phones}});
  400. }
  401. ##################################################
  402. sub needs_input($fsa, $action, $var)
  403. {
  404. $fsa->delete_note($var);
  405. $fsa->note(input_var => $var);
  406. $fsa->note(input_action => $action);
  407. $fsa->state("needs_input");
  408. return "needs_input";
  409. }
  410. ##################################################
  411. sub format_wd($rec, $cur)
  412. {
  413. return _("бесплатно") if $rec->{dayly} == 0 && $rec->{monthly} == 0;
  414. my $m = sprintf("<b>%.2f $cur</b> %s", $rec->{monthly}, _("в месяц")) if $rec->{monthly} != 0;
  415. my $d = sprintf("<b>%.2f $cur</b> %s", $rec->{dayly}, _("в день")) if $rec->{dayly} != 0;
  416. return ("$m + $d") if $m && $d;
  417. return $m if $m;
  418. return $d if $d;
  419. }
  420. sub parse_error
  421. {
  422. my $e = shift;
  423. return $e unless ref $e;
  424. return "$e->{code} $e->{message} $e->{body}";
  425. }
  426. 1;
  427. # локализация