use Modern::Perl; use utf8; use Mojo::Base -strict, -async_await, -signatures; use Data::Dumper; use experimental qw/switch/; our $client; our $abon_client; our $config; our $redis; sub __ { @_; } ############################################## our $commands = [ {command=>"help", name=>__("Помощь"), description=>__("Список доступных команд")}, {command=>"info", name=>__("Информация"), description=>__("Информация о пользователе")}, {command=>"balance", name=>__("Баланс"), description=>__("Проверка баланса"), main=>1, icon=>"\x{1F45B}"}, {command=>"service", name=>__("Сервисы"), description=>__("Подключенные сервисы")}, {command=>"credit", name=>__("Кредит"), description=>__("Установка кредита"), main=>1, icon=>"\x{1FAF0}"}, {command=>"card", name=>__("Карта пополнения"), description=>__("Оплата карточкой пополнения"), main=>1, icon=>"\x{1F4B3}"}, {command=>"payberry", name=>__("Payberry"), description=>__("Оплата через Payberry"), main=>1, icon=>"\x{1F17F}\x{FE0F}"}, {command=>"transfer", name=>__("Перевод"), description=>__("Перевод денег")}, {command=>"new_task", name=>__("Новая заявка"), description=>__("Новая заявка")}, {command=>"tasks", name=>__("Заявки"), description=>__("Открытые заявки"), main=>1, icon=>"\x{2692}\x{FE0F}"}, {command=>"support", name=>__("Техподдержка"), description=>__("Связь с техподдержкой")}, {command=>"logout", name=>__("Выход"), description=>__("Выход")}, ]; ############################# our $lex_actions = { xfer => { name => __("перевод денег"), canceled => __("отменён"), }, card => { name => __("пополнение карточкой"), canceled => __("отмененo"), }, }; our $lex_vars = { xfer_to => __("номер личного счета"), xfer_amount => __("сумму"), card_code => __("код карточки"), card_serial => __("номер карточки"), }; ############################################## sub command_help($fsa, $info) { my @list = map { "/$_->{command} " . _($_->{description}) } @$commands; reply($info, join("\n", @list)); } async sub command_logout { my ($fsa, $info) = @_; my $uid = $fsa->note("[uid]"); await $client->delete_p("client", "/client/$uid/telegram"); reply_with($info, {button_menu => 0}, _("Благодарим за использование нашего бота")); $fsa->delete_note("[uid]"); $fsa->state("logged_out"); } async sub command_balance { my ($fsa, $info) = @_; my $uid = $fsa->note("[uid]"); my $money = await $abon_client->get_p($info, "client", "/client/$uid/money?human=1"); my $cur = $money->{human}; my @lines = ( sprintf("%s: %.2f $cur (%s %.2f $cur + %s %.2f $cur) ", _("Ваш баланс"), $money->{balance}, ("депозит"), $money->{deposit}, _("кредит"), $money->{credit}), ); push @lines, sprintf("%s: %s", _("Оплачено включительно до"), format_date($money->{last_day})) if $money->{last_day} ne "-"; push @lines, sprintf("%s: %d%%", _("Скидка"), $money->{reduction}) if $money->{reduction}; push @lines, sprintf("%s: %.2f $cur %s (%s)", _("Последнее снятие"), $money->{last_withdrawal}->{sum}, format_time($money->{last_withdrawal}->{date}), $money->{last_withdrawal}->{comment}) if $money->{last_withdrawal}->{sum}; push @lines, sprintf("%s: %.2f $cur %s", _("Последний платеж"), $money->{last_payment}->{sum}, format_time($money->{last_payment}->{date})) if $money->{last_payment}->{sum}; for (keys %{ $money->{accounts} }) { push @lines, sprintf("%s: %.2f $cur", $_, $money->{accounts}->{$_}); } reply($info, @lines); } async sub command_info { my ($fsa, $info) = @_; my $uid = $fsa->note("[uid]"); my $client = await $abon_client->get_p($info, "client", "/client/$uid"); reply($info, sprintf("%s: %d", _("Номер учетной записи"), $client->{uid}), sprintf("%s: %s", _("Логин"), $client->{login}), sprintf("%s: %s", _("ФИО"), $client->{fio}), sprintf("%s: %s", _("Адрес"), $client->{address}), sprintf("%s: %s", _("Телефон"), $client->{phone}), ); } async sub command_credit { my ($fsa, $info) = @_; my $uid = $fsa->note("[uid]"); my $money = await $abon_client->get_p($info, "client", "/client/$uid/money?human=1"); if ($money->{credit} > 0) { return reply($info, sprintf("%s %.2f %s. %s", _("У вас уже установлен кредит"), $money->{credit}, $money->{human}, _("Продлевать кредит повторно до оплаты нельзя. При следующей оплате кредит будет погашен"))); } reply_with($info, { inline_menu => [[ { text=>_("Я согласен с условиями"), callback_data=>"\x00/set-credit" }, ]], }, _("Вы можете самостоятельно установить кредит на два дня"), _("Ограничения: только для физических лиц, продлевать кредит повторно до оплаты нельзя. При следующей оплате кредит будет погашен"), ); } async sub callback_set_credit { my ($fsa, $info) = @_; my $uid = $fsa->note("[uid]"); my $res = await $abon_client->post_p($info, "client", "/client/$uid/credit", {human=>1}); return reply($info, _("Кредит не имеет смысла для бесплатных тарифных планов")) if $res->{credit} == 0; await reply_with($info, {button_menu=>1}, sprintf("%s %.2f %s", _("Установлен кредит "), $res->{credit}, $res->{human}), "", ); command_balance($fsa, $info); } async sub command_service { my ($fsa, $info) = @_; my $uid = $fsa->note("[uid]"); my $res = await $abon_client->get_p($info, "client", "/client/$uid/service?human=1&as-array=1"); my @list = map { sprintf("%s: %s (%s '%s')", $_->{name_ru}, format_wd($_->{tariff}, $_->{human}), _("тариф"), $_->{tariff}->{name_ru}) } grep { !$_->{disabled} } @$res; reply($info, @list); }; ###################################### async sub command_transfer { my ($fsa, $info) = @_; reply($info, _("Введите номер личного счета пользователя, которому вы хотите перевести деньги со своего собственного счета")); return needs_input($fsa, "xfer", "xfer_to"); } async sub verify_xfer_to { my ($fsa, $target_uid, $info) = @_; my $uid = $fsa->note("[uid]"); return _("Номер личного счета должен состоять из цифр") unless $target_uid =~ /^\d+$/; return _("Нельзя перевести деньги себе самому") if $uid==$target_uid; my $res = eval { await $abon_client->get_p($info, "client", "/client/$target_uid") }; if ($@) { die $@ unless $@->{code} == 404; return _("Абонент") . " $target_uid " . _("не найден"); } return _("Абонент") . " $target_uid " . _("отключен") if $res->{disabled}; $fsa->note("xfer_fio" => $res->{fio}); return undef; } async sub use_xfer_to { my ($fsa, $target_uid, $info) = @_; my $uid = $fsa->note("[uid]"); my $nick = $fsa->note("xfer_fio"); my @fio = split(/\s+/, $nick); if (@fio) { my $f = shift(@fio); $nick = join(" ", (substr($f, 0, 1) . ".", @fio)); } $fsa->note(xfer_nick => $nick); reply( $info, _("Получатель денег: ") . $nick, _("Теперь введите сумму, которую хотите перевести"), ); return needs_input($fsa, "xfer", "xfer_amount"); } sub verify_xfer_amount { my ($fsa, $amount, $info) = @_; my $uid = $fsa->note("[uid]"); $amount =~ s/,/./g; $amount =~ s/[^\d\.]//g; my $tmp = $amount; my $count = $amount =~ tr/.//; return _("Вы ввели неправильную сумму") if !$amount || $count>1; } async sub use_xfer_amount { my ($fsa, $amount, $info) = @_; my $uid = $fsa->note("[uid]"); my $to_uid = $fsa->note("xfer_to"); my $nick = $fsa->note("xfer_nick"); reply_with($info, { inline_menu => [[ { text=>_("Подтвердите перевод"), callback_data=>"\x00/transfer" }, ]] }, sprintf("%.2f %s %s %d (%s)", $amount, $config->{currency}->{human}, _(" на счет абонента"), $to_uid, $nick) ); return "command"; } async sub callback_transfer { my ($fsa, $info) = @_; my $uid = $fsa->note("[uid]"); my $to_uid = $fsa->note("xfer_to"); my $amount = $fsa->note("xfer_amount"); return unless $to_uid && $amount; my $res = await $abon_client->post_p($info, "client", "/client/$uid/money/to/$to_uid", { amount => $amount, ip => "0.0.0.0", via => "abonbot", currency => $config->{currency}->{name}, }); reply_with($info, {button_menu=>1}, _("Деньги успешно переведены")); command_balance($fsa, $info); } ########################################## async sub command_new_task { my ($fsa, $info) = @_; my $uid = $fsa->note("[uid]"); my $res = await $abon_client->get_p($info, "task", "/task?created_by_client=$uid&list=new,work"); if ($res->{total}) { return reply($info, _("Создание новой заявки невозможно, пока не будут решены уже открытые")); } reply($info, _("Изложите вашу проблему")); $fsa->state("task_needs_descr"); }; async sub callback_task_post { my ($fsa, $info) = @_; my $uid = $fsa->note("[uid]"); my $descr = $fsa->note("task_text"); return unless $descr; my $params = { description => $descr, "for-client" => $uid, client => $uid, type => "client-issue", list => "new", task_attr => { source => "telegram" }, }; my $res = await $client->post_json_p("task", "/task", $params); reply_with($info, {button_menu=>1}, _("Ваша заявка размещена")); command_tasks($fsa, $info); } async sub callback_task_cancel { my ($fsa, $info) = @_; $fsa->delete_note("task_text"); $fsa->delete_note("if_edited"); reply_with($info, {button_menu=>1}, _("Заявка отменена")); } async sub command_tasks { my ($fsa, $info) = @_; my $uid = $fsa->note("[uid]"); my $res = await $abon_client->get_p($info, "task", "/task?created_by_client=$uid&list=new,work&sort=id&with_comments_for_client=1"); return reply($info, _("Открытых заявок нет")) unless $res->{total}; my @str = map { _("Заявка") . " $_->{number}\n$_->{description}\n" . _("Комментариев") . " " . $_->{total_comments} . ", " . _(" не прочитано") . " " . $_->{unread_comments} . "" } @{$res->{data}}; my $menu = [[ map { { text => _("Комментарии к заявке") . " " . $_->{number}, callback_data => "\x00/task $_->{entity}" } } @{$res->{data}} ]]; if (@{$res->{data}}) { reply_with($info, {inline_menu=>$menu}, @str); } else { reply($info, _("Комментариев пока нет")); } }; async sub callback_task { my ($fsa, $info, $task_id) = @_; my $uid = $fsa->note("[uid]"); my $res = await $client->get_p("task", "/task/$task_id?with_comments_for_client=1"); my @str = map { format_timestamp($_->{created}) . " " . "" . ($_->{created_by}->[0] eq "worker" ? _("Оператор") : _("Вы")) . ":\n" . $_->{text} } @{ $res->{comments} }; my $menu = [[ {text => _("Добавить комментарий"), callback_data => "\x00comment $task_id"}, ]]; unless (@str) { $str[0] = "" . _("Комментариев пока нет") . ""; } reply_with($info, {inline_menu=>$menu}, @str); $client->post_p("task", "/task/$task_id/comment/read", {for_client=>$uid}); } async sub callback_comment { my ($fsa, $info, $task_id) = @_; $fsa->note(task_id => $task_id); $fsa->state("task_needs_comment"); reply($info, _("Введите текст вашего ответа")); } ################################################## async sub command_payberry { my ($fsa, $info) = @_; my $uid = $fsa->note("[uid]"); my $menu = [[ {text => _("Payberry"), url => $config->{pay}->{payberry_url} . "?acc=$uid"} ]]; reply_with($info, {inline_menu=>$menu}, _("Для оплаты перейдите по ссылке")); } ##################################### use constant FAIL_BLOCK => 10; use constant FAIL_ASK_SERIAL => 3; async sub command_card { my ($fsa, $info) = @_; my $uid = $fsa->note("[uid]"); my $key = "card-guess-$uid"; my $failed = $redis->get($key) || 0; if ($failed > FAIL_BLOCK) { return reply($info, _("Вы ввели неправильный код слишком много раз. Пополнение карточкой заблокировано на сутки")); } my $res = await $client->get_p("client", "/client/$uid"); if ($res->{disabled}) { return reply($info, _("Пополнение счета недоступно отключенным пользователям")); } reply($info, _("Введите код карточки пополнения (16 цифр, можно разделять их знаком '-')")); return needs_input($fsa, "card", "card_code"); } sub verify_card_code($fsa, $code, $info) { $code =~ s/\D//g; return _("Код карточки должен состоять из 16 цифр") unless $code =~ /^\d{16}$/; return undef; } async sub use_card_code { my ($fsa, $code, $info) = @_; my $uid = $fsa->note("[uid]"); my $key = "card-guess-$uid"; my $fails = $redis->get($key) || 0; if ($fails > FAIL_ASK_SERIAL) { reply($info, _("Теперь введите номер карточки (7 цифр, можно разделять знаком'-')")); return needs_input($fsa, "card", "card_serial"); } else { pay_from_card($info, $uid, $code, ""); return "command"; } } sub verify_card_serial($fsa, $serial, $info) { $serial =~ s/\D//g; return _("Номер карточки должен состоять из 7 цифр") unless $serial =~ /^\d{7}$/; return undef; } async sub use_card_serial { my ($fsa, $serial, $info) = @_; pay_from_card($info, $fsa->note("[uid]"), $fsa->note("card_code"), $serial); } async sub pay_from_card { my ($info, $uid, $code, $serial) = @_; my $args = { code => $code =~ s/\D//gr, uid => $uid, domain => $config->{domain}, source => "abonbot", ip => "0.0.0.0", }; $args->{serial} = $serial =~ s/\D//gr if $serial; my $res = eval { await $client->post_p("card", "/redemption", $args) }; unless($@) { return reply_with($info, {button_menu=>1}, _("Ваш счёт пополнен на " . $res->{amount} . " " . $res->{currency_short_name_ru} . ". " . _("Оплачено включительно до ") . $res->{paid_until})); }; given($@->{code}) { when(400) { reply_with($info, {button_menu=>1}, _("Вы ввели неправильный номер или код карточки."), _("Пополнение прервано")); my $key = "card-guess-$uid"; $redis->incr($key); $redis->expire($key, 24 * 3600); } when(409) { reply_with($info, {button_menu=>1}, _("Карточка уже использована."), _("Пополнение прервано")); } when(406) { reply_with($info, {button_menu=>1}, _("Карточка не активирована. Обратитесь в службу поддержки."), _("Пополнение прервано")); } default { die $@; } } }; ##################################################### sub command_support { my ($fsa, $info) = @_; return reply($info, _("Телефоны техподдержки:"), @{$config->{support_phones}}); } ################################################## sub needs_input($fsa, $action, $var) { $fsa->delete_note($var); $fsa->note(input_var => $var); $fsa->note(input_action => $action); $fsa->state("needs_input"); return "needs_input"; } ################################################## sub format_wd($rec, $cur) { return _("бесплатно") if $rec->{dayly} == 0 && $rec->{monthly} == 0; my $m = sprintf("%.2f $cur %s", $rec->{monthly}, _("в месяц")) if $rec->{monthly} != 0; my $d = sprintf("%.2f $cur %s", $rec->{dayly}, _("в день")) if $rec->{dayly} != 0; return ("$m + $d") if $m && $d; return $m if $m; return $d if $d; } sub parse_error { my $e = shift; return $e unless ref $e; return "$e->{code} $e->{message} $e->{body}"; } 1; # локализация