3 İşlemeler 9d1a52d21d ... d0f4e482c6

Yazar SHA1 Mesaj Tarih
  Yuriy Zhilovets d0f4e482c6 BETA 5 ay önce
  Yuriy Zhilovets 9b4b3ab382 + needs_input (не отлажено) 5 ay önce
  Yuriy Zhilovets f33bc2cff3 + меню из кнопок 5 ay önce
4 değiştirilmiş dosya ile 362 ekleme ve 273 silme
  1. 73 29
      abonbot.pl
  2. 241 51
      modules/commands.pm
  3. 11 6
      modules/fsa.pm
  4. 37 187
      modules/rules.pm

+ 73 - 29
abonbot.pl

@@ -16,7 +16,6 @@ use Mojo::JSON qw/j/;
 use NetAddr::IP;
 use HTML::Restrict;
 use Redis;
-use Mojo::Promise;
 use Time::timegm qw/timegm/;
 use POSIX::strptime;
 
@@ -46,6 +45,7 @@ plugin yaml_config => {
 };
 
 our $config = app->config;
+our $commands;
 
 app->secrets(["Marsz, Marsz, Dabrowski"]);
 
@@ -104,15 +104,15 @@ my $ua = new Mojo::UserAgent;
 $ua->max_redirects(5);
 
 our $fsa;
-our $commands;
 
 my $locale_dir = "$Bin/locale";
 my $locales = localization::available_locales($locale_dir);
 my $locale_handles = localization->add_mo($locale_dir, $locales);
 
-our $kb_menu = [[
-  map { _($_->{name}) } grep {!$_->{hide_in_kb}} @$commands,
-]];
+my @list = ();
+my @commlist = map { {text=>"$_->{icon}\x{2003}$_->{name}"} } grep {$_->{main}} @$commands;
+push @list, [ splice @commlist, 0, 2 ] while @commlist;
+our $button_menu = \@list;
 
 ##########################
 
@@ -176,13 +176,21 @@ post "/:token" => async sub
   
   my $fsa = make_fsa($chatid, $from);
   say "*** restore fsa: ", $fsa->state;
- 
-  eval {
-    say "*** current state = ", $fsa->state, Dumper $fsa->notes;;
-    my $new_state = await $fsa->switch($line, $from);
-    say "*** switched to ", $new_state, Dumper $fsa->notes;
-  };
-  report($from, $@) if $@;
+
+  if ($body->{edited_message} && (my $if_edited = $fsa->note("if_edited")))
+  {
+    my ($target_msg, $var) = split("/", $if_edited);
+    $fsa->note($var => $line) if $from->{msgid} == $target_msg;
+  }
+  else
+  {
+     eval {
+      say "*** current state = ", $fsa->state, Dumper $fsa->notes;;
+      my $new_state = await $fsa->switch($line, $from);
+      say "*** switched to ", $new_state, Dumper $fsa->notes;
+    };
+    report($from, $@) if $@;
+  }
 
   save_fsa($fsa, $chatid);
 };
@@ -305,7 +313,7 @@ sub request
   my $action = shift;
   my $params = shift;
 
-   $ua->post_p("https://api.telegram.org/bot$config->{token}/$action" => json => $params)
+  $ua->post_p("https://api.telegram.org/bot$config->{token}/$action" => json => $params)
   ->then(sub
   {
     my $tx = shift;
@@ -352,11 +360,18 @@ sub notify($info, $message, $args={})
   $params->{parse_mode} ||= "HTML";
   $params->{reply_to} = $args->{reply_to} if $args->{reply_to};
   $params->{disable_notification} = 1 if $args->{silent};
+  $params->{reply_to} = $args->{msgid};
 
   if ($args->{menu})
   {
     $params->{reply_markup} = { keyboard => $args->{menu} };
     $params->{resize_keyboard} = Mojo::JSON->true;
+    $params->{is_persistent} = Mojo::JSON->true;
+  }
+  
+  if ($args->{remove_menu})
+  {
+    $params->{reply_markup} = {remove_keyboard => Mojo::JSON->true };
   }
 
   if ($args->{inline_menu})
@@ -385,12 +400,24 @@ sub notify($info, $message, $args={})
 
 sub reply($info, @lines)
 {
-  return notify($info, join("\n", @lines), {reply_to=>$info->{msgid}});
+  return notify($info, join("\n", @lines), {});
 }
 
 sub reply_with($info, $params, @lines)
 {
-  $params->{reply_to} = $info->{msgid};
+  if (exists $params->{button_menu})
+  {
+    if (delete $params->{button_menu})
+    {
+      $params->{menu} = $button_menu;
+    }
+    else
+    {
+      delete $params->{menu};
+      $params->{remove_menu} = 1;
+    }
+  }
+  
   return notify($info, join("\n", @lines), $params);
 }
 
@@ -400,26 +427,43 @@ async sub do_command
 {
   my ($fsa, $cmd, $info) = @_;
 
-  if ($info->{id}<0)
-  {
-    return reply($info, _("Этот бот не работает в чатах"));
-  }
-
-  my ($c,@args) = split(/\s+/,$cmd);
-  $c =~ s/\@MolAbonbotBot$//;
+  return reply($info, _("Этот бот не работает в чатах")) if $info->{id} < 0;
+  
+  my ($sub, $args, $to_clean) = find_command($cmd);
+  return reply($info, _("Неизвестная команда")) unless $sub;
 
-  my $prefix = substr($c, 0, 1) eq "\x00" ? "callback" : "command";
-  $c =~ s|^\x00||;
-  $c =~ s|^/||;
+  $fsa->delete_temp if $to_clean;
+  await $sub->($fsa, $info, @$args);
+}
 
-  my $sub = refpath("${prefix}_$c");
+sub find_command
+{
+  my $cmd = shift;
+  my ($c, @args) = split(/\x20+/,$cmd);
+  $c =~ s/\@MolAbonbotBot$//;
+  
+  my $prefix = "command";
+  my $to_clean = 1;
 
-  unless ($sub)
+  if (substr($c, 0, 1) eq "/")
+  {
+  }
+  elsif (substr($c, 0, 1) eq "\x00")
+  {
+    $prefix = "callback";
+    undef $to_clean;
+  }
+  else
   {
-    return reply($info, _("Неизвестная команда"));
+    my @found = grep { $_->{main} && index($cmd, "$_->{icon}\x{2003}$_->{name}")==0 } @$commands;
+    return undef unless @found;
+    $c = $found[0]->{command};
   }
 
-  await $sub->($fsa, $info, @args);
+  $c =~ s|^\x00||;
+  $c =~ s|^/||;
+  
+  return refpath("${prefix}_$c"), \@args, $to_clean;
 }
 
 sub refpath

+ 241 - 51
modules/commands.pm

@@ -3,6 +3,7 @@ use utf8;
 
 use Mojo::Base -strict, -async_await, -signatures;
 use Data::Dumper;
+use experimental qw/switch/;
 
 our $client;
 our $abon_client;
@@ -17,20 +18,40 @@ sub __
 ##############################################
 
 our $commands = [
-  {command=>"help", name=>__("Помощь"), description=>__("Список доступных команд"), hide_in_kb=>1},
+  {command=>"help", name=>__("Помощь"), description=>__("Список доступных команд")},
   {command=>"info", name=>__("Информация"), description=>__("Информация о пользователе")},
-  {command=>"balance", name=>__("Баланс"), description=>__("Проверка баланса")},
+  {command=>"balance", name=>__("Баланс"), description=>__("Проверка баланса"), main=>1, icon=>"\x{1F45B}"},
   {command=>"service", name=>__("Сервисы"), description=>__("Подключенные сервисы")},
-  {command=>"credit", name=>__("Кредит"), description=>__("Установка кредита")},
-  {command=>"card", name=>__("Карта пополнения"), description=>__("Оплата карточкой пополнения")},
-  {command=>"payberry", name=>__("Payberry"), description=>__("Оплата через Payberry")},
+  {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=>__("Открытые заявки")},
+  {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)
@@ -43,19 +64,19 @@ async sub command_logout
 {
   my ($fsa, $info) = @_;
   
-  my $uid = $fsa->note("uid");
+  my $uid = $fsa->note("[uid]");
 
   await $client->delete_p("client", "/client/$uid/telegram");
   reply($info, _("Благодарим за использование нашего бота"));
 
-  $fsa->delete_note("uid");
+  $fsa->delete_note("[uid]");
   $fsa->state("logged_out");
 }
 
 async sub command_balance
 {
   my ($fsa, $info) = @_;
-  my $uid = $fsa->note("uid");
+  my $uid = $fsa->note("[uid]");
 
   my $money = await $abon_client->get_p($info, "client", "/client/$uid/money?human=1");
   my $cur = $money->{human};
@@ -86,10 +107,10 @@ async sub command_info
 {
   my ($fsa, $info) = @_;
 
-  my $uid = $fsa->note("uid");
+  my $uid = $fsa->note("[uid]");
   my $client = await $abon_client->get_p($info, "client", "/client/$uid");
   reply($info, 
-    sprintf("<u>%s</u>: %d", _("Лицевой счет"), $client->{uid}),
+    sprintf("<u>%s</u>: %d", _("Номер учетной записи"), $client->{uid}),
     sprintf("<u>%s</u>: %s", _("Логин"), $client->{login}),
     sprintf("<u>%s</u>: %s", _("ФИО"), $client->{fio}),
     sprintf("<u>%s</u>: %s", _("Адрес"), $client->{address}),
@@ -100,7 +121,7 @@ async sub command_info
 async sub command_credit
 {
   my ($fsa, $info) = @_;
-  my $uid = $fsa->note("uid");
+  my $uid = $fsa->note("[uid]");
 
   my $money = await $abon_client->get_p($info, "client", "/client/$uid/money?human=1");
 
@@ -122,12 +143,12 @@ async sub command_credit
 async sub callback_set_credit
 {
   my ($fsa, $info) = @_;
-  my $uid = $fsa->note("uid");
+  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($info, 
+  await reply_with($info, {button_menu=>1},
     sprintf("%s <b>%.2f %s</b>", _("Установлен кредит "), $res->{credit}, $res->{human}), 
     "",
   );
@@ -138,7 +159,7 @@ async sub callback_set_credit
 async sub command_service
 {
   my ($fsa, $info) = @_;
-  my $uid = $fsa->note("uid");
+  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("<u>%s:</u> %s (%s '%s')", $_->{name_ru},  format_wd($_->{tariff}, $_->{human}), _("тариф"), $_->{tariff}->{name_ru}) } 
@@ -147,28 +168,94 @@ async sub command_service
   reply($info, @list);
 };
 
+######################################
+
 async sub command_transfer
 {
   my ($fsa, $info) = @_;
-  reply($info, _("Введите лицевой счет пользователя, которому вы хотите перевести деньги со своего счета"));
+  reply($info, _("Введите номер личного счета пользователя, которому вы хотите перевести деньги со своего собственного счета"));
   
-  $fsa->delete_note("xfer_to");
-  $fsa->delete_note("xfer_amount");
-  $fsa->state("xfer_needs_uid");
+  return needs_input($fsa, "xfer", "xfer_to");
 }
 
-async sub callback_transfer
+async sub verify_xfer_to
 {
-  my ($fsa, $info) = @_;
-  my $uid = $fsa->note("uid");
-  
-  my $to_uid = $fsa->delete_note("xfer_to");
-  my $amount = $fsa->delete_note("xfer_amount");
+  my ($fsa, $target_uid, $info) = @_;
+  my $uid = $fsa->note("[uid]");
+     
+  return _("Номер личного счета должен состоять из цифр") unless $target_uid =~ /^\d+$/;
+  return _("Нельзя перевести деньги себе самому") if $uid==$target_uid;
+     
+  my $res = await $abon_client->get_p($info, "client", "/client/$target_uid");
+  return _("Абонент") . " $target_uid " . _("отключен") if $res->{disabled};
   
-  unless ($to_uid && $amount)
+  $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)
   {
-    return reply($info, _("Произошла внутренняя ошибка"));
+    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 = $abon_client->post_p($info, "client", "/client/$uid/money/to/$to_uid", {
      amount => $amount,
@@ -177,7 +264,7 @@ async sub callback_transfer
      currency => $config->{currency}->{name},
   });
 
-  reply($info, _("Деньги успешно переведены"));
+  reply_with($info, {button_menu=>1}, _("Деньги успешно переведены"));
   command_balance($fsa, $info);
 }
 
@@ -186,7 +273,7 @@ async sub callback_transfer
 async sub command_new_task
 {
   my ($fsa, $info) = @_;
-  my $uid = $fsa->note("uid");
+  my $uid = $fsa->note("[uid]");
   
   my $res = await $abon_client->get_p($info, "task", "/task?created_by_client=$uid&list=new,work");
   
@@ -202,10 +289,13 @@ async sub command_new_task
 async sub callback_task_post
 {
   my ($fsa, $info) = @_;
-  my $uid = $fsa->note("uid");
+  my $uid = $fsa->note("[uid]");
+  
+  my $descr = $fsa->note("task_text");
+  return unless $descr;
 
   my $params = {
-     description => $fsa->note("task_text"),
+     description => $descr,
      "for-client" => $uid,
      client => $uid,
      type => "client-issue",
@@ -217,25 +307,24 @@ async sub callback_task_post
 
    my $res = await $client->post_json_p("task", "/task", $params);
 
-   reply($info, "Заявка размещена");
-
-   $fsa->delete_note("task_text");  
+   reply_with($info, {button_menu=>1}, _("Ваша заявка размещена"));
    command_tasks($fsa, $info);
 }
 
 async sub callback_task_cancel
 {
-say 666;
   my ($fsa, $info) = @_;
-  my $uid = $fsa->note("uid");
-say "aaa", Dumper $fsa;
-  reply($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 $uid = $fsa->note("[uid]");
   
   my $res = await $abon_client->get_p($info, "task", "/task?created_by_client=$uid&list=new,work&sort=id");
   
@@ -257,7 +346,7 @@ async sub command_tasks
 async sub callback_task
 {
   my ($fsa, $info, $task_id) = @_;
-  my $uid = $fsa->note("uid");
+  my $uid = $fsa->note("[uid]");
   
   my $res = await $client->get_p("task", "/task/$task_id?with_comments=1");
   
@@ -286,12 +375,10 @@ async sub callback_comment
 
 ##################################################
 
-use constant FAIL_BLOCK => 10;
-
 async sub command_payberry
 {
   my ($fsa, $info) = @_;
-  my $uid = $fsa->note("uid");
+  my $uid = $fsa->note("[uid]");
 
   my $menu = [[
     {text => _("Payberry"), url => $config->{pay}->{payberry_url} . "?acc=$uid"}
@@ -300,10 +387,15 @@ async sub command_payberry
   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 $uid = $fsa->note("[uid]");
   
   my $key    = "card-guess-$uid";
   my $failed = $redis->get($key) || 0;
@@ -320,17 +412,114 @@ async sub command_card
   }
 
   reply($info, _("Введите код карточки пополнения (16 цифр, можно разделять их знаком '-')"));
+  return needs_input($fsa, "card", "card_code");
+}
 
-  $fsa->delete_note("card_code");
-  $fsa->delete_note("card_serial");
-  
-  $fsa->state("card_needs_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}});
+  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";
 }
 
 ##################################################
@@ -340,7 +529,7 @@ sub format_wd($rec, $cur)
   return _("бесплатно") if $rec->{dayly} == 0 && $rec->{monthly} == 0;
 
   my $m = sprintf("<b>%.2f $cur</b> %s", $rec->{monthly}, _("в месяц")) if $rec->{monthly} != 0;
-  my $d = sprintf("<b>%.2f $cur</b> %s", $rec->{dayly}, _("в месяц")) if $rec->{dayly} != 0;
+  my $d = sprintf("<b>%.2f $cur</b> %s", $rec->{dayly}, _("в день")) if $rec->{dayly} != 0;
 
   return ("$m + $d") if $m && $d;
   return $m if $m;
@@ -355,6 +544,7 @@ sub parse_error
   return "$e->{code} $e->{message} $e->{body}";
 }
 
+
 1;
 
 # локализация

+ 11 - 6
modules/fsa.pm

@@ -11,7 +11,7 @@ package fsa;
 
 sub new($class, $rules, $state, $notes={})
 {
-  $notes->{_state} = $state;
+  $notes->{"[state]"} = $state;
   bless {rules=>$rules, notes=>$notes}, $class;
 }
 
@@ -20,10 +20,10 @@ sub state($self, $new_state=undef)
   if (defined($new_state))
   {
     die "Unknown state '$new_state" unless exists $self->{rules}->{$new_state};
-    $self->note(_state => $new_state);
+    $self->note("[state]" => $new_state);
   }
 
-  $self->{notes}->{_state};
+  $self->{notes}->{"[state]"};
 }
 
 sub note($self, $key, $val=undef)
@@ -46,22 +46,27 @@ sub delete_note($self, $key)
   delete $self->{notes}->{$key};
 }
 
+sub delete_temp($self)
+{
+  delete @{$self->{notes}}{grep {m|^[^[]|} keys %{$self->{notes}}};
+}
+
 async sub switch
 {
   my ($self, $line, @rest) = @_;
-  my $state = $self->{notes}->{_state};
+  my $state = $self->{notes}->{"[state]"};
   
   my $rule = $self->{rules}->{$state};
   die "fsa::switch: no rule for '$state'" unless $rule;
   
   my $new_state = $rule->($self, $line, @rest);
   $new_state = await $new_state if ref $new_state && $new_state->can("then");
-  $new_state ||= $self->{notes}->{_state};
+  $new_state ||= $self->{notes}->{"[state]"};
   use Data::Dumper;
   say Dumper $new_state;
   die "fsa::switch: unknown new state '$new_state'" unless exists $self->{rules}->{$new_state};
 
-  $self->note("_state", $new_state);
+  $self->note("[state]" => $new_state);
   $new_state;
 }
 

+ 37 - 187
modules/rules.pm

@@ -12,10 +12,9 @@ our $redis;
 our $config;
 our $client;
 our $commands;
-our $kb_menu;
 our $abon_client;
-
-use constant FAIL_ASK_SERIAL => 3;
+our $lex_vars;
+our $lex_actions;
 
 ##################################################
 
@@ -34,9 +33,9 @@ my $rules = {
      # Поступила команда /start
      
      # У нас есть uid в автомате? Это значит, что мы прочитали состояние из внешней базы
-     if ($fsa->note("uid"))
+     if ($fsa->note("[uid]"))
      {
-       reply($info, _("Для получения списка доступных команд введите /help"));
+       reply_with($info, { button_menu => 1 }, _("Для получения списка доступных команд введите /help"));
        return "command";
      }
      
@@ -48,7 +47,7 @@ my $rules = {
        {
          reply($info, 
            _("Вас приветствует провайдер") . " " . $config->{provider},
-           _("Введите номер лицевого счёта или логин")
+           _("Введите номер учетной записи или логин")
          );
          
          return "needs_login";
@@ -59,8 +58,8 @@ my $rules = {
        }
      }
 
-     reply($info, greet($res->{fio}), _("Для получения списка доступных команд введите /help"));
-     $fsa->note(uid => $res->{uid});
+     reply_with($info, { button_menu => 1 },  greet($res->{fio}), _("Для получения списка доступных команд введите /help"));
+     $fsa->note("[uid]" => $res->{uid});
      return "command";
    },
    
@@ -85,8 +84,9 @@ my $rules = {
         die $@;
       }
 
-      reply($info, greet($res->{fio}), _("Для получения списка доступных команд введите /help"));         
-      $fsa->note(uid => $res->{uid});
+      reply_with($info, { button_menu => 1 },  greet($res->{fio}), _("Для получения списка доступных команд введите /help"));
+
+      $fsa->note("[uid]" => $res->{uid});
       $fsa->delete_note("login");
       
       "command";
@@ -100,99 +100,15 @@ my $rules = {
      $fsa->state; # или все тот же command, или команда установила уже свое состояние
    },
    
-   #### Перевод денег
-   
-   xfer_needs_uid => async sub
-   {
-     my ($fsa, $target_uid, $info) = @_;
-     my $uid = $fsa->note("uid");
-     
-     $fsa->state("command"); # на случай die
-     
-     unless($target_uid)
-     {
-       reply($info, _("Перевод денег прерван"));
-     }
-
-     unless ($target_uid =~ /^\d+$/)
-     {
-       reply($info, _("Неправильный номер личного счета"), _("Введите его заново или пустую строку, если передумали пополнять"));
-       return "xfer_needs_uid";
-     }
-     
-     if ($uid==$target_uid)
-     {
-       reply($info, _("Нельзя перевести деньги себе самому"));
-       return "command";
-     }
-     
-     my $res = await $abon_client->get_p($info, "client", "/client/$target_uid");
-     
-     if ($res->{disabled})
-     {
-       reply($info, _("Абонент $target_uid отключен"));
-       return "command";
-     }
-     
-     my $nick = $res->{fio};
-     my @fio = split(/\s+/, $res->{fio});
-     if (@fio)
-     {
-       my $f = shift(@fio);
-       $nick = join(" ", (substr($f, 0, 1) . ".", @fio));
-     }
-     
-     reply(
-       $info, _("Получатель денег: ") .  $nick,
-       "Теперь введите сумму, которую хотите перевести",
-     );
-     
-     $fsa->note(xfer_to => $target_uid);
-     return "xfer_needs_amount";
-   }, 
-   
-   xfer_needs_amount => async sub
-   {
-     my ($fsa, $amount, $info) = @_;
-     my $uid = $fsa->note("uid");
-     my $to_uid = $fsa->note("xfer_to");
-     
-     unless($amount)
-     {
-       reply($info, _("Перевод денег прерван"));
-     }
-     
-     $amount =~ s/,/./g;
-     $amount =~ s/[^\d\.]//g;
-     my $tmp = $amount;
-     my $count = $amount =~ tr/.//;
-     
-     if (!$amount || $count>1)
-     {
-       reply($info, _("Вы ввели неправильную сумму"), _("Введите ее заново или пустую строку, если передумали пополнять"));
-       return "xfer_needs_amount";
-     }
-     
-     $fsa->note(xfer_amount => $amount);
-     reply_with($info, {
-         inline_menu => [[
-           { text=>_("Подтвердите перевод"), callback_data=>"\x00/transfer" },
-         ]]
-       },
-       sprintf("%.2f %s %s %d", $amount, $config->{currency}->{human}, _(" на счет абонента"), $to_uid),
-     );
-     
-     return "command";
-   },
-   
    #### Заявки
    
    task_needs_descr => async sub
    {
      my ($fsa, $descr, $info) = @_;
-     my $uid = $fsa->note("uid");
+     my $uid = $fsa->note("[uid]");
      
      $fsa->note(task_text => $descr);
+     $fsa->note(if_edited => "$info->{msgid}/task_text");
 
      reply_with($info, {
          inline_menu => [[
@@ -209,7 +125,7 @@ my $rules = {
    task_needs_comment => async sub
    {
      my ($fsa, $comment, $info) = @_;
-     my $uid = $fsa->note("uid");
+     my $uid = $fsa->note("[uid]");
      my $task_id = $fsa->note("task_id");
      
      my $params = {
@@ -221,69 +137,48 @@ my $rules = {
      my $res = await $client->post_p("task", "/task/$task_id/comment", $params);
 
      reply($info, "Ваш ответ добавлен");
-     $fsa->delete_note("task_id");
      return "command";
    },
    
-   ##### Карточка пополнения
+   ###################################
    
-   card_needs_code => sub
+   needs_input => async sub
    {
-     my ($fsa, $code, $info) = @_;
-     my $uid = $fsa->note("uid");
+     my ($fsa, $input, $info) = @_;
+     my $uid = $fsa->note("[uid]");
 
      $fsa->state("command"); # на случай die
 
-     $code =~ s/\D//g;
+     my $action = $fsa->note("input_action");
      
-     unless($code)
-     {
-       reply($info, _("Пополнение карточкой прервано"));
-     }
-
-     unless ($code =~ /^\d{16}$/)
+     if ($input eq "\x00/cancel_input")
      {
-       reply($info, _("Код карточки должен состоять из 16 цифр"), _("Введите его заново или пустую строку, если передумали пополнять"));
-       return "card_needs_code";
+       reply_with($info, {button_menu=>1}, ucfirst($lex_actions->{$action}->{name}) . " " . $lex_actions->{$action}->{canceled});
+       return "command";
      }
      
-     my $key    = "card-guess-$uid";
-     my $fails = $redis->get($key) || 0;
+     my $var = $fsa->note("input_var");
+     my $sub = refpath("verify_$var") || sub { undef };
      
-     if ($fails > FAIL_ASK_SERIAL)
+     my $error = $sub->($fsa, $input, $info);
+     $error = await $error if ref $error && $error->can("then");
+     if ($error)
      {
-       $fsa->note(card_code => $code);
-       reply($info, _("Теперь введите номер карточки (7 цифр, можно разделять знаком'-')"));
-       return "card_needs_serial";
+       reply_with($info, {
+         inline_menu => [[
+           { text=>_("Отмена"), callback_data=>"\x00/cancel_input" },
+         ]]
+       }, $error, "", _("Введите заново ") . $lex_vars->{$var} . _(" или нажмите Отмена, чтобы прервать ") . $lex_actions->{$action}->{name});
+       return "needs_input";
      }
      else
      {
-       pay_from_card($info, $uid, $code, "");
-       return "command";
+       $fsa->note($var => $input);
+       $sub = refpath("use_$var") || sub { "command" };
+       my $state = await $sub->($fsa, $input, $info);
+       return $state;
      }
    },
-   
-   card_needs_serial => sub
-   {
-     my ($fsa, $serial, $info) = @_;
-
-     $fsa->state("command"); # на случай die
-
-     unless($serial)
-     {
-       reply($info, _("Пополнение карточкой прервано"));
-     }
-
-     unless ($serial =~ /^\d{7}$/)
-     {
-       reply($info, _("Номер карточки должен состоять из 7 цифр"), _("Введите его заново или пустую строку, если передумали пополнять"));
-       return "card_needs_serial";
-     }
-
-     pay_from_card($info, $fsa->note("uid"), $fsa->note("card_code"), $serial);
-     
-     return "command";
-   },
 
 };
 
@@ -295,51 +190,6 @@ sub greet($whom)
   "$greet, $whom";
 }
 
-async sub pay_from_card
-{
-     my ($info, $uid, $code, $serial) = @_;
-
-     my $args    = { 
-       code => $code,
-       uid  => $uid,
-       domain => $config->{domain}, 
-       source => "abonbot", 
-       ip     => "0.0.0.0",
-     };
-     
-     $args->{serial} = $serial if $serial;
-     
-     my $res = eval { await $client->post_p("card", "/redemption", $args) };
-
-     unless($@)
-     {     
-       return reply($info, _("Ваш счёт пополнен на " . $res->{amount} . " " . $res->{currency_short_name_ru} . ". " . _("Оплачено до ") . $res->{paid_until}));
-     };
-
-     given($@->{code})
-     {
-       when(400)
-       {
-         reply($info, _("Вы ввели неправильный номер или код карточки"));
-         my $key    = "card-guess-$uid";
-         $redis->incr($key);
-         $redis->expire($key, 24 * 3600);
-       }
-       when(409) 
-       {
-          reply($info, _("Карточка уже использована"));
-       }
-       when(406) 
-       {
-          reply($info, _("Карточка не активирована. Обратитесь в службу поддержки"));
-       }
-       default 
-       {
-         die $@;
-       }
-     }
-};
-
 ######################
 
 sub make_key($id)
@@ -368,7 +218,7 @@ sub make_fsa($chatid, $from)
   if (keys %$notes)
   {
     utf8::decode($notes->{$_}) for keys %$notes;
-    return fsa->new($rules, $notes->{_state}, $notes); 
+    return fsa->new($rules, $notes->{"[state]"}, $notes); 
   }
   else
   {