Yuriy Zhilovets 5 mesiacov pred
rodič
commit
d0f4e482c6
4 zmenil súbory, kde vykonal 219 pridanie a 249 odobranie
  1. 20 9
      abonbot.pl
  2. 165 81
      modules/commands.pm
  3. 11 6
      modules/fsa.pm
  4. 23 153
      modules/rules.pm

+ 20 - 9
abonbot.pl

@@ -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);
 };
@@ -421,9 +429,10 @@ async sub do_command
 
   return reply($info, _("Этот бот не работает в чатах")) if $info->{id} < 0;
   
-  my ($sub, $args) = find_command($cmd);
+  my ($sub, $args, $to_clean) = find_command($cmd);
   return reply($info, _("Неизвестная команда")) unless $sub;
 
+  $fsa->delete_temp if $to_clean;
   await $sub->($fsa, $info, @$args);
 }
 
@@ -434,6 +443,7 @@ sub find_command
   $c =~ s/\@MolAbonbotBot$//;
   
   my $prefix = "command";
+  my $to_clean = 1;
 
   if (substr($c, 0, 1) eq "/")
   {
@@ -441,6 +451,7 @@ sub find_command
   elsif (substr($c, 0, 1) eq "\x00")
   {
     $prefix = "callback";
+    undef $to_clean;
   }
   else
   {
@@ -452,7 +463,7 @@ sub find_command
   $c =~ s|^\x00||;
   $c =~ s|^/||;
   
-  return refpath("${prefix}_$c"), \@args;
+  return refpath("${prefix}_$c"), \@args, $to_clean;
 }
 
 sub refpath

+ 165 - 81
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;
@@ -19,18 +20,38 @@ sub __
 our $commands = [
   {command=>"help", name=>__("Помощь"), description=>__("Список доступных команд")},
   {command=>"info", name=>__("Информация"), description=>__("Информация о пользователе")},
-  {command=>"balance", name=>__("Баланс"), description=>__("Проверка баланса"), main=>1, icon=>"\x{1FA99}"},
+  {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{2764}\x{FE0F}\x{200D}\x{1FA79}"},
+  {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,7 +107,7 @@ 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}),
@@ -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}) } 
@@ -154,22 +175,13 @@ async sub command_transfer
   my ($fsa, $info) = @_;
   reply($info, _("Введите номер личного счета пользователя, которому вы хотите перевести деньги со своего собственного счета"));
   
-  $fsa->delete_note("xfer_to");
-  $fsa->delete_note("xfer_amount");
-  $fsa->delete_note("xfer_fio");
-  
-  return needs_input(
-    fsa => $fsa,
-    name => "xfer_to",
-    item => _("номер личного счета"),
-    process => _("перевод денег"),
-  );
+  return needs_input($fsa, "xfer", "xfer_to");
 }
 
 async sub verify_xfer_to
 {
   my ($fsa, $target_uid, $info) = @_;
-  my $uid = $fsa->note("uid");
+  my $uid = $fsa->note("[uid]");
      
   return _("Номер личного счета должен состоять из цифр") unless $target_uid =~ /^\d+$/;
   return _("Нельзя перевести деньги себе самому") if $uid==$target_uid;
@@ -177,14 +189,14 @@ async sub verify_xfer_to
   my $res = await $abon_client->get_p($info, "client", "/client/$target_uid");
   return _("Абонент") . " $target_uid " . _("отключен") if $res->{disabled};
   
-  $fsa->note(xfer_fio => $res->{fio});
+  $fsa->note("xfer_fio" => $res->{fio});
   return undef;
 }
 
 async sub use_xfer_to
 {
   my ($fsa, $target_uid, $info) = @_;
-  my $uid = $fsa->note("uid");
+  my $uid = $fsa->note("[uid]");
      
   my $nick = $fsa->note("xfer_fio");
   my @fio = split(/\s+/, $nick);
@@ -194,23 +206,20 @@ async sub use_xfer_to
     $nick = join(" ", (substr($f, 0, 1) . ".", @fio));
   }
      
+  $fsa->note(xfer_nick => $nick);
+     
   reply(
     $info, _("Получатель денег: ") .  $nick,
     _("Теперь введите сумму, которую хотите перевести"),
   );
 
-  return needs_input(
-    fsa => $fsa,
-    name => "xfer_amount",
-    item => _("сумму"),
-    process => _("перевод денег"),
-  );     
+  return needs_input($fsa, "xfer", "xfer_amount");
 } 
 
 sub verify_xfer_amount
 {
   my ($fsa, $amount, $info) = @_;
-  my $uid = $fsa->note("uid");
+  my $uid = $fsa->note("[uid]");
      
   $amount =~ s/,/./g;
   $amount =~ s/[^\d\.]//g;
@@ -223,15 +232,17 @@ sub verify_xfer_amount
 async sub use_xfer_amount
 {
   my ($fsa, $amount, $info) = @_;
-  my $uid = $fsa->note("uid");
+  
+  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", $amount, $config->{currency}->{human}, _(" на счет абонента"), $to_uid),
+    sprintf("%.2f %s %s %d (%s)", $amount, $config->{currency}->{human}, _(" на счет абонента"), $to_uid, $nick)
   );
      
   return "command";
@@ -240,16 +251,11 @@ async sub use_xfer_amount
 async sub callback_transfer
 {
   my ($fsa, $info) = @_;
-  my $uid = $fsa->note("uid");
+  my $uid = $fsa->note("[uid]");
   
-  my $to_uid = $fsa->delete_note("xfer_to");
-  my $amount = $fsa->delete_note("xfer_amount");
-  $fsa->delete_note("xfer_fio");
-  
-  unless ($to_uid && $amount)
-  {
-    return reply($info, _("Произошла внутренняя ошибка"));
-  }
+  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,
@@ -258,7 +264,7 @@ async sub callback_transfer
      currency => $config->{currency}->{name},
   });
 
-  reply($info, _("Деньги успешно переведены"));
+  reply_with($info, {button_menu=>1}, _("Деньги успешно переведены"));
   command_balance($fsa, $info);
 }
 
@@ -267,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");
   
@@ -283,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",
@@ -298,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");
   
@@ -338,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");
   
@@ -367,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"}
@@ -381,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;
@@ -401,41 +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(%args)
+sub needs_input($fsa, $action, $var)
 {
-  my $name = $args{name};
-  my $fsa = $args{fsa};
+  $fsa->delete_note($var);
+  $fsa->note(input_var => $var);
+  $fsa->note(input_action => $action);
   
-  $fsa->delete_note($name);
-  $fsa->note(input_name => $name);
-  $fsa->note(input_again => _("Введите заново ") . $args{item} . _(" или нажмите Отмена, чтобы прервать ") . $args{process});
-  $fsa->note(input_process => $args{process});
   $fsa->state("needs_input");
-  return "needs_input";
-}
 
-sub cancel_input
-{
-  my ($fsa, $info) = @_;
-  $fsa->delete_note($fsa->delete_note("input_name"));
-  $fsa->delete_note("input_again");
-  reply($info, ucfirst($fsa->delete_note("input_process")) . " " . _("отменён"));
-  $fsa->state("command");
+  return "needs_input";
 }
 
 ##################################################
@@ -445,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;

+ 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;
 }
 

+ 23 - 153
modules/rules.pm

@@ -13,8 +13,8 @@ our $config;
 our $client;
 our $commands;
 our $abon_client;
-
-use constant FAIL_ASK_SERIAL => 3;
+our $lex_vars;
+our $lex_actions;
 
 ##################################################
 
@@ -33,7 +33,7 @@ my $rules = {
      # Поступила команда /start
      
      # У нас есть uid в автомате? Это значит, что мы прочитали состояние из внешней базы
-     if ($fsa->note("uid"))
+     if ($fsa->note("[uid]"))
      {
        reply_with($info, { button_menu => 1 }, _("Для получения списка доступных команд введите /help"));
        return "command";
@@ -59,7 +59,7 @@ my $rules = {
      }
 
      reply_with($info, { button_menu => 1 },  greet($res->{fio}), _("Для получения списка доступных команд введите /help"));
-     $fsa->note(uid => $res->{uid});
+     $fsa->note("[uid]" => $res->{uid});
      return "command";
    },
    
@@ -86,7 +86,7 @@ my $rules = {
 
       reply_with($info, { button_menu => 1 },  greet($res->{fio}), _("Для получения списка доступных команд введите /help"));
 
-      $fsa->note(uid => $res->{uid});
+      $fsa->note("[uid]" => $res->{uid});
       $fsa->delete_note("login");
       
       "command";
@@ -100,50 +100,15 @@ my $rules = {
      $fsa->state; # или все тот же command, или команда установила уже свое состояние
    },
    
-   #### Перевод денег
-   
-   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 => [[
@@ -160,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 = {
@@ -172,94 +137,44 @@ 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;
-     
-     unless($code)
-     {
-       reply($info, _("Пополнение карточкой прервано"));
-     }
-
-     unless ($code =~ /^\d{16}$/)
-     {
-       reply($info, _("Код карточки должен состоять из 16 цифр"), _("Введите его заново или пустую строку, если передумали пополнять"));
-       return "card_needs_code";
-     }
-     
-     my $key    = "card-guess-$uid";
-     my $fails = $redis->get($key) || 0;
+     my $action = $fsa->note("input_action");
      
-     if ($fails > FAIL_ASK_SERIAL)
-     {
-       $fsa->note(card_code => $code);
-       reply($info, _("Теперь введите номер карточки (7 цифр, можно разделять знаком'-')"));
-       return "card_needs_serial";
-     }
-     else
+     if ($input eq "\x00/cancel_input")
      {
-       pay_from_card($info, $uid, $code, "");
+       reply_with($info, {button_menu=>1}, ucfirst($lex_actions->{$action}->{name}) . " " . $lex_actions->{$action}->{canceled});
        return "command";
      }
-   },
-   
-   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";
-   },
-   
-   needs_input => async sub
-   {
-     my ($fsa, $input, $info) = @_;
-     my $uid = $fsa->note("uid");
-     
-     $fsa->state("command"); # на случай die
      
-     my $name = $fsa->note("input_name");
-     my $sub = refpath("verify_$name") || sub { undef };
+     my $var = $fsa->note("input_var");
+     my $sub = refpath("verify_$var") || sub { undef };
      
-     my $error = await $sub->($fsa, $input, $info);
+     my $error = $sub->($fsa, $input, $info);
+     $error = await $error if ref $error && $error->can("then");
      if ($error)
      {
        reply_with($info, {
          inline_menu => [[
            { text=>_("Отмена"), callback_data=>"\x00/cancel_input" },
          ]]
-       }, $error, $fsa->note("input_again"));
+       }, $error, "", _("Введите заново ") . $lex_vars->{$var} . _(" или нажмите Отмена, чтобы прервать ") . $lex_actions->{$action}->{name});
        return "needs_input";
      }
      else
      {
-       $fsa->note($name => $input);
-       $sub = refpath("use_$name") || sub { "command" };
+       $fsa->note($var => $input);
+       $sub = refpath("use_$var") || sub { "command" };
        my $state = await $sub->($fsa, $input, $info);
        return $state;
      }
@@ -275,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)
@@ -348,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
   {