commands.pm 14 KB

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