Przeglądaj źródła

+ command_transfer (не тестировано)

Yuriy Zhilovets 1 rok temu
rodzic
commit
94480d4750
4 zmienionych plików z 188 dodań i 363 usunięć
  1. 69 119
      abonbot.pl
  2. 2 2
      modules/abon_client.pm
  3. 77 71
      modules/commands.pm
  4. 40 171
      modules/fsa.pm

+ 69 - 119
abonbot.pl

@@ -7,7 +7,7 @@ use Modern::Perl;
 use utf8;
 
 use EV;
-use Mojo::Base -strict, -signatures;
+use Mojo::Base -strict, -signatures, -async_await;
 use Mojolicious::Lite;
 use Mojo::UserAgent;
 use Data::Dumper;
@@ -30,7 +30,7 @@ use darsan_auth;
 use darsan_client;
 use rabbit_async_rec;
 use commands;
-use fsa;
+use rules;
 use abon_client;
 use localization;
 
@@ -122,7 +122,7 @@ get "/health" => sub
   shift->render(text => "Abonbot OK");
 };
 
-post "/:token" => sub
+post "/:token" => async sub
 {
   my $c = shift;
 
@@ -136,20 +136,20 @@ post "/:token" => sub
   my $body = j($c->req->body);
 #say Dumper $body;
 
-  my ($from, $chatid, $cmd);
+  my ($chatid, $line, $from);
   
   if (my $m = $body->{message} || $body->{edited_message})
   {
     $from = $m->{from};
     $chatid = $from->{id};
-    $cmd = $m->{text};
+    $line = $m->{text};
     $from->{msgid} = $m->{message_id};
   }
   elsif ($m = $body->{callback_query})
   {
     $from = $m->{from};
     $chatid = $from->{id};
-    $cmd = $m->{data};
+    $line = $m->{data};
     $from->{msgid} = $m->{message}->{message_id};
   }
   else
@@ -157,68 +157,45 @@ post "/:token" => sub
     return $log->error("Unknown message type");
   }
   
-say ">>> $cmd";
-  restore_fsa_state($chatid, $from);
-  process_input($cmd, $chatid, $from);
-  save_fsa_state($chatid);
-};
+  say ">>> $line";
+  
+  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 $@;
 
-=cut
-$VAR1 = {
-          'callback_query' => {
-                              'data' => '32320/set-credit',
-                              'from' => {
-                                        'id' => 311683401,
-                                        'username' => 'AntonMetelkin2',
-                                        'first_name' => "\x{410}\x{43d}\x{442}\x{43e}\x{43d}",
-                                        'language_code' => 'ru',
-                                        'last_name' => "\x{41c}\x{435}\x{442}\x{451}\x{43b}\x{43a}\x{438}\x{43d}",
-                                        'is_bot' => bless( do{\(my $o = 0)}, 'JSON::PP::Boolean' )
-                                      },
-                              'chat_instance' => '4165635351277858977',
-                              'id' => '1338670018114168009',
-                              'message' => {
-                                           'reply_markup' => {
-                                                             'inline_keyboard' => [
-                                                                                  [
-                                                                                    {
-                                                                                      'callback_data' => '32320/set-credit',
-                                                                                      'text' => "\x{42f} \x{441}\x{43e}\x{433}\x{43b}\x{430}\x{441}\x{435}\x{43d} \x{441} \x{443}\x{441}\x{43b}\x{43e}\x{432}\x{438}\x{44f}\x{43c}\x{438}"
-                                                                                    }
-                                                                                  ]
-                                                                                ]
-                                                           },
-                                           'text' => "\x{412}\x{44b} \x{43c}\x{43e}\x{436}\x{435}\x{442}\x{435} \x{441}\x{430}\x{43c}\x{43e}\x{441}\x{442}\x{43e}\x{44f}\x{442}\x{435}\x{43b}\x{44c}\x{43d}\x{43e} \x{443}\x{441}\x{442}\x{430}\x{43d}\x{43e}\x{432}\x{438}\x{442}\x{44c} \x{43a}\x{440}\x{435}\x{434}\x{438}\x{442} \x{43d}\x{430} \x{434}\x{432}\x{430} \x{434}\x{43d}\x{44f}
-\x{41e}\x{433}\x{440}\x{430}\x{43d}\x{438}\x{447}\x{435}\x{43d}\x{438}\x{44f}: \x{442}\x{43e}\x{43b}\x{44c}\x{43a}\x{43e} \x{434}\x{43b}\x{44f} \x{444}\x{438}\x{437}\x{438}\x{447}\x{435}\x{441}\x{43a}\x{438}\x{445} \x{43b}\x{438}\x{446}, \x{43f}\x{440}\x{43e}\x{434}\x{43b}\x{435}\x{432}\x{430}\x{442}\x{44c} \x{43a}\x{440}\x{435}\x{434}\x{438}\x{442} \x{43f}\x{43e}\x{432}\x{442}\x{43e}\x{440}\x{43d}\x{43e} \x{434}\x{43e} \x{43e}\x{43f}\x{43b}\x{430}\x{442}\x{44b} \x{43d}\x{435}\x{43b}\x{44c}\x{437}\x{44f}. \x{41f}\x{440}\x{438} \x{441}\x{43b}\x{435}\x{434}\x{443}\x{44e}\x{449}\x{435}\x{439} \x{43e}\x{43f}\x{43b}\x{430}\x{442}\x{435} \x{43a}\x{440}\x{435}\x{434}\x{438}\x{442} \x{431}\x{443}\x{434}\x{435}\x{442} \x{43f}\x{43e}\x{433}\x{430}\x{448}\x{435}\x{43d}",
-                                           'entities' => [
-                                                         {
-                                                           'type' => 'bold',
-                                                           'offset' => 54,
-                                                           'length' => 11
-                                                         }
-                                                       ],
-                                           'message_id' => 995,
-                                           'chat' => {
-                                                     'last_name' => "\x{41c}\x{435}\x{442}\x{451}\x{43b}\x{43a}\x{438}\x{43d}",
-                                                     'id' => 311683401,
-                                                     'type' => 'private',
-                                                     'username' => 'AntonMetelkin2',
-                                                     'first_name' => "\x{410}\x{43d}\x{442}\x{43e}\x{43d}"
-                                                   },
-                                           'from' => {
-                                                     'is_bot' => bless( do{\(my $o = 1)}, 'JSON::PP::Boolean' ),
-                                                     'first_name' => "\x{41c}\x{430}\x{43a}\x{435}\x{435}\x{432}\x{43a}\x{430}-\x{41e}\x{43d}\x{43b}\x{430}\x{439}\x{43d}",
-                                                     'id' => 7443432620,
-                                                     'username' => 'MolAbonBot'
-                                                   },
-                                           'date' => 1723730668
-                                         }
-                            },
-          'update_id' => 292858034
-        };
+  save_fsa($fsa, $chatid);
+};
 
-=cut
+sub report($info, $err)
+{
+  if (ref $err eq "Mojo::Exception")
+  {
+    return secret_error($info, $err->message . Dumper $err->line);
+  }
+  
+  if (ref $err eq "HASH" && exists $err->{code} && $err->{code}>=400 && $err->{code}<500 && ref $err->{body} eq "HASH")
+  {
+    reply($info, $err->{body}->{text_ru} || $err->{body}->{text});
+  }
+  else
+  {
+    secret_error($info, Dumper $err);
+  }
+}
 
+sub secret_error($info, $str)
+{
+  my $code = int(rand(10000));
+  $log->error("====== $code");
+  $log->error($str);
+  reply($info, _("Произошла ошибка. Сообщите в службу технической поддержки код") . " $code");
+}
 
 ##################################
 
@@ -274,31 +251,27 @@ sub request
    });
 }
 
-sub notify
+sub notify($info, $message, $args={})
 {
-  my $chatid = shift;
-  my $message = shift;
-  my $rest = shift || {};
-
   my $params = {
-    chat_id => $chatid,
+    chat_id => $info->{id},
     text => $message,
     disable_web_page_preview => 1,
   };
 
   $params->{parse_mode} ||= "HTML";
-  $params->{reply_to} = $rest->{reply_to} if $rest->{reply_to};
-  $params->{disable_notification} = 1 if $rest->{silent};
+  $params->{reply_to} = $args->{reply_to} if $args->{reply_to};
+  $params->{disable_notification} = 1 if $args->{silent};
 
-  if ($rest->{menu})
+  if ($args->{menu})
   {
-    $params->{reply_markup} = { keyboard => $rest->{menu} };
+    $params->{reply_markup} = { keyboard => $args->{menu} };
     $params->{resize_keyboard} = Mojo::JSON->true;
   }
 
-  if ($rest->{inline_menu})
+  if ($args->{inline_menu})
   {
-    $params->{reply_markup} = { inline_keyboard => $rest->{inline_menu} };
+    $params->{reply_markup} = { inline_keyboard => $args->{inline_menu} };
   }
 
   my $disable_error_handler = delete $params->{disable_error_handler};
@@ -320,62 +293,43 @@ sub notify
   return $promise;
 }
 
-sub reply($rec, @lines)
+sub reply($info, @lines)
 {
-  return notify($rec->{id}, join("\n", @lines), {reply_to=>$rec->{msgid}});
+  return notify($info, join("\n", @lines), {reply_to=>$info->{msgid}});
 }
 
-sub reply_with($rec, $params, @lines)
+sub reply_with($info, $params, @lines)
 {
-  $params->{reply_to} = $rec->{msgid};
-  return notify($rec->{id}, join("\n", @lines), $params);
+  $params->{reply_to} = $info->{msgid};
+  return notify($info, join("\n", @lines), $params);
 }
 
 #################################
 
-sub do_command
+async sub do_command
 {
-  my $cmd = shift;
-  my $chatid = shift;
-  my $rest = shift;
+  my ($fsa, $cmd, $info) = @_;
 
-  local($Data::Dumper::Terse) = 1;
+  if ($info->{id}<0)
+  {
+    return reply($info, _("Этот бот не работает в чатах"));
+  }
 
   my ($c,@args) = split(/\s+/,$cmd);
-  $c =~ s|^/||;
   $c =~ s/\@MolAbonbotBot$//;
-  
-  if ($chatid<0)
-  {
-    return notify($chatid, _("Этот бот не работает в чатах"), $rest);
-  }
 
-  my $sub = refpath("command_$c");
+  my $prefix = substr($c, 0, 1) eq "\x00" ? "callback" : "command";
+  $c =~ s|^\x00||;
+  $c =~ s|^/||;
+
+  my $sub = refpath("${prefix}_$c");
 
   unless ($sub)
   {
-    return notify($chatid, _("Неизвестная команда"), $rest);
+    return reply($info, _("Неизвестная команда"));
   }
 
-  my $state;
-  eval {
-    my $res = $sub->($chatid, $fsa->notes("uid"), $rest);
-    if (ref $res eq "Mojo::Promise")
-    {
-      $res->catch(sub($err)
-      {
-        $log->error(Dumper $err);
-      });
-    }
-  };
-
-  if ($@)
-  {
-    my $msg = ref $@ eq "HASH" ? Dumper($@) : $@;
-    $log->error("$cmd from $chatid: $msg");
-    notify($chatid, "Ошибка при выполнении команды $cmd: $msg");
-    return;
-  }
+  await $sub->($fsa, $info);
 }
 
 sub refpath
@@ -392,11 +346,6 @@ sub reference
   return exists(&{$name}) ? \&{$name} : undef;
 }
 
-sub make_key($id, $type)
-{
-  return "ab-$id-$type";
-}
-
 sub _loc($lang, $str)
 {
   return $str unless exists $locale_handles->{$lang};
@@ -405,6 +354,7 @@ sub _loc($lang, $str)
 
 sub _($str)
 {
+  return $str; #!!!
   return _loc($fsa->notes("lang") || "ru", $str);
 }
 

+ 2 - 2
modules/abon_client.pm

@@ -21,12 +21,12 @@ sub new($class)
 }
 
 our $AUTOLOAD;
-sub AUTOLOAD($self, $tel_id, @rest)
+sub AUTOLOAD($self, $info, @rest)
 {
   # Remove qualifier from original method name...
   my $called =  $AUTOLOAD =~ s/.*:://r;
     
-  return $self->{client}->auth($auth->telegram($tel_id))->$called(@rest);
+  return $self->{client}->auth($auth->telegram($info->{id}))->$called(@rest);
 }
 
 package abon_auth;

+ 77 - 71
modules/commands.pm

@@ -5,8 +5,8 @@ use Mojo::Base -strict, -async_await, -signatures;
 use Data::Dumper;
 
 our $client;
-our $fsa;
 our $abon_client;
+our $config;
 
 sub __
 {
@@ -21,40 +21,37 @@ our $commands = [
   {command=>"balance", name=>__("Баланс"), description=>__("Проверка баланса")},
   {command=>"service", name=>__("Сервисы"), description=>__("Подключенные сервисы")},
   {command=>"credit", name=>__("Кредит"), description=>__("Установка кредита")},
+  {command=>"transfer", name=>__("Перевод"), description=>__("Перевод денег")},
   {command=>"logout", name=>__("Выход"), description=>__("Выход")},
 ];
 
 ##############################################
 
-sub command_help($chatid, $uid, $rest)
+sub command_help($fsa, $info)
 {
   my @list = map { "<b>/$_->{command}</b> " . _($_->{description}) } @$commands;
-  notify($chatid, join("\n", @list), $rest);
+  reply($info, join("\n", @list));
 }
 
-sub command_start($chatid, $uid, $from)
+async sub command_logout
 {
-  set_new_state("");
-}
-
-sub command_logout($chatid, $uid, $rest)
-{
-  notify($chatid, _("Благодарим за использование нашего бота"));
+  my ($fsa, $info) = @_;
+  
+  my $uid = $fsa->note("uid");
 
-  $client->del("client", "/client/$uid/telegram");
-  if (my $err = $client->error)
-  {
-    report($chatid, $err);
-  }
+  await $client->delete_p("client", "/client/$uid/telegram");
+  reply($info, _("Благодарим за использование нашего бота"));
 
-  $fsa->notes(uid => undef);
-  set_new_state("logged_out");
+  $fsa->delete_note("uid");
+  $fsa->state("logged_out");
 }
 
 async sub command_balance
 {
-  my ($chatid, $uid, $rest) = @_;
-  my $money = await $abon_client->get_p($chatid, "client", "/client/$uid/money?human=1");
+  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 = (
@@ -76,14 +73,17 @@ async sub command_balance
     push @lines, sprintf("<u>%s:</u> <b>%.2f $cur</b>", $_, $money->{accounts}->{$_});
   }
     
-  reply($rest, @lines);    
+  reply($info, @lines);
 }
 
 async sub command_info
 {
-  my ($chatid, $uid, $rest) = @_;
-  my $client = await $abon_client->get_p($chatid, "client", "/client/$uid");
-  reply($rest, 
+  my ($fsa, $info) = @_;
+
+  my $uid = $fsa->note("uid");
+  my $client = await $abon_client->get_p($info, "client", "/client/$uid");
+  say Dumper $client;
+  reply($info, 
     sprintf("<u>%s</u>: %d", _("Лицевой счет"), $client->{uid}),
     sprintf("<u>%s</u>: %s", _("Логин"), $client->{login}),
     sprintf("<u>%s</u>: %s", _("ФИО"), $client->{fio}),
@@ -94,17 +94,19 @@ async sub command_info
 
 async sub command_credit
 {
-  my ($chatid, $uid, $rest) = @_;
-  my $money = await $abon_client->get_p($chatid, "client", "/client/$uid/money?human=1");
+  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($rest, sprintf("%s <b>%.2f %s</b>", _("У вас уже установлен кредит"), $money->{credit}, $money->{human}));
+    return reply($info, sprintf("%s <b>%.2f %s</b>", _("У вас уже установлен кредит"), $money->{credit}, $money->{human}));
   }
   
-  reply_with($rest, {
+  reply_with($info, {
     inline_menu => [[
-      { text=>_("Я согласен с условиями"), callback_data=>"/set-credit" },
+      { text=>_("Я согласен с условиями"), callback_data=>"\x00/set-credit" },
     ]],
   },
     _("Вы можете самостоятельно установить кредит на два дня"),
@@ -112,63 +114,68 @@ async sub command_credit
   );
 }
 
-async sub command_set_credit
+async sub callback_set_credit
 {
-  my ($chatid, $uid, $rest) = @_;
+  my ($fsa, $info) = @_;
+  my $uid = $fsa->note("uid");
 
-  my $res = await $abon_client->post_p($chatid, "client", "/client/$uid/credit", {human=>1});
-  return reply($rest, _("Кредит не имеет смысла для бесплатных тарифных планов")) if $res->{credit} == 0;
+  my $res = await $abon_client->post_p($info, "client", "/client/$uid/credit", {human=>1});
+  return reply($info, _("Кредит не имеет смысла для бесплатных тарифных планов")) if $res->{credit} == 0;
   
-  await reply($rest, 
+  await reply($info, 
     sprintf("%s <b>%.2f %s</b>", _("Установлен кредит "), $res->{credit}, $res->{human}), 
     "",
   );
   
-  command_balance($chatid, $uid, $rest);
+  command_balance($fsa, $info);
 }
 
-=cut
-$VAR1 = [
-          {
-            'tariff' => {
-                        'monthly' => '0',
-                        'dayly' => '0',
-                        'entity' => 1000,
-                        'speed' => 0,
-                        'name_ru' => "\x{414}\x{43b}\x{44f} \x{441}\x{43e}\x{442}\x{440}\x{443}\x{434}\x{43d}\x{438}\x{43a}\x{43e}\x{432} MOL"
-                      },
-            'name' => 'pppoe',
-            'name_ru' => 'PPPoE',
-            'entity' => 32320,
-            'disabled' => 0
-          },
-          {
-            'tariff' => {
-                        'speed' => 0,
-                        'name_ru' => "\x{414}\x{43b}\x{44f} \x{441}\x{43e}\x{442}\x{440}\x{443}\x{434}\x{43d}\x{438}\x{43a}\x{43e}\x{432} MOL",
-                        'entity' => 1000,
-                        'monthly' => '0',
-                        'dayly' => '0'
-                      },
-            'name' => 'ipoe',
-            'entity' => 17,
-            'name_ru' => 'IPoE',
-            'disabled' => 0
-          }
-        ];
-=cut
-
 async sub command_service
 {
-  my ($chatid, $uid, $rest) = @_;
+  my ($fsa, $info) = @_;
+  my $uid = $fsa->note("uid");
   
-  my $res = await $abon_client->get_p($chatid, "client", "/client/$uid/service?human=1&as-array=1");
+  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}) } 
      grep { !$_->{disabled} } @$res;
   
-  reply($rest, @list);
+  reply($info, @list);
 };
 
+async sub command_transfer
+{
+  my ($fsa, $info) = @_;
+  reply($info, "Введите лицевой счет пользователя, которому вы хотите перевести деньги со своего счета");
+  
+  $fsa->delete_note("xfer_to");
+  $fsa->delete_note("xfer_amount");
+  $fsa->state("xfer_needs_uid");
+}
+
+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");
+  
+  unless ($to_uid && $amount)
+  {
+    return reply($info, _("Произошла внутренняя ошибка"));
+  }
+  
+  my $res = $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($info, _("Деньги успешно переведены"));
+  command_balance($fsa, $info);
+}
+
 sub format_wd($rec, $cur)
 {
   return _("бесплатно") if $rec->{dayly} == 0 && $rec->{monthly} == 0;
@@ -193,6 +200,5 @@ sub parse_error
 
 1;
 
-# что будет, если, к примеру, установка кредита вернет ошибку?
-# сервисы
-# перевод денег
+# перевод денег
+# локализация

+ 40 - 171
modules/fsa.pm

@@ -1,196 +1,65 @@
-use Modern::Perl;
-use Data::Dumper;
-use Mojo::Base -strict, -signatures;
-
-use FSA::Rules;
-
-our $redis;
-our $config;
-our $client;
-our $commands;
-our $kb_menu;
-
-my %main = (
-   logged_out => {
-     rules => [
-       logged_out => sub($state, $line, $chatid, $rest)
-       {
-         if ($line ne "/start")
-         {
-           notify($chatid, _("Для начала работы наберите <b>/start</b>"), $rest);
-           return 1;
-         }
-
-         return undef;
-       },
-       
-       dummy => sub($state, $line, $chatid, $rest)
-       {
-         my $res = $client->get("client", "/telegram/$chatid/client");
-         if (my $err = $client->error)
-         {
-            if ($err->{code}==410)
-            {
-              reply($rest, _("Вас приветствует провайдер") . " " . $config->{provider});
-              reply($rest, _("Введите номер учетной записи или логин"));
-              $state->result("ask_login");
-            }
-            else
-            {
-              report($chatid, $err);
-              $state->result("error");
-            }
-         }
-         else
-         {
-           $state->result("logined");
-           $state->notes(uid => $res->{uid});
-         }
-  
-         return undef;
-       },
-       
-       needs_login => sub($state, $line, $chatid, $rest)
-       {
-         return $state->result eq "ask_login";
-       },
-       
-       logged_out => sub($state, $line, $chatid, $rest)
-       {
-         return $state->result eq "error";
-       },
-
-       command => 1,
-     ],
-   },
-
-   needs_login => { 
-     rules => [
-       needs_password => sub($state, $login, $chatid, $rest)
-       {
-         $state->notes(login => $login);
-         reply($rest, _("Теперь введите пароль"));
-         1;
-       },
-     ],
-   },
+#!/usr/bin/perl
 
-   needs_password => {
-     rules => [
-       logged_out => sub($state, $password, $chatid, $rest)
-       {
-         my $login = $state->notes("login");
-         my $res = $client->post("client", "/telegram/client", {login=>$login, password=>$password, telegram_id=>$chatid});
-         $state->notes(login => undef);
+# Конечный автомат для телеграм-бота
+# Ю. Жиловец, 20.08.2024
 
-         if (my $err = $client->error)
-         {
-           report($chatid, $err);
-           return 1;
-         }
-         
-         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
-         my $greet = $hour>=4 && $hour<=10 ? _("Доброе утро") : $hour>10 && $hour<6 ? _("Добрый день") : _("Добрый вечер");
-
-         reply($rest, "$greet, " . $res->{fio});
-         $state->notes(uid => $res->{uid});
-
-         return undef;
-       },  
+use Modern::Perl;
+use utf8;
+use Mojo::Base -strict, -signatures, -async_await;
 
-       command => 1,
-     ],
-   },
-   
-   command => {
-     rules => [
-       command => sub($state, $line, $chatid, $rest)
-       {
-         do_command($line, $chatid, $rest);
-         1;
-       }
-     ],
-   },
-   
-   dummy => {
-   },
-);
+package fsa;
 
-our $fsa = FSA::Rules->new(%main);
+sub new($class, $rules, $state, $notes={})
+{
+  $notes->{_state} = $state;
+  bless {rules=>$rules, notes=>$notes}, $class;
+}
 
-sub report($chatid, $err)
+sub state($self, $new_state=undef)
 {
-  if ($err->{code}>=400 && $err->{code}<500 && ref $err->{body} eq "HASH")
-  {
-    notify($chatid, $err->{body}->{text_ru});
-  }
-  else
+  if (defined($new_state))
   {
-    my $code = int(rand(10000));
-    say STDERR "====== $code";
-    say STDERR Dumper $err;
-    notify($chatid, _("Ошибка авторизации. Сообщите в службу технической поддержки код") . " $code");
+    die "Unknown state '$new_state" unless exists $self->{rules}->{$new_state};
+    $self->note(_state => $new_state);
   }
+
+  $self->{notes}->{_state};
 }
 
-######################
- 
-sub save_fsa_state($id)
+sub note($self, $key, $val=undef)
 {
-  my $ks = make_key($id, "state");
-  my $kn = make_key($id, "notes");
+  if (defined $val)
+  {
+    $self->{notes}->{$key} = $val;
+  }
 
-  $redis->set($ks, $fsa->curr_state->name);
-  $redis->expire($ks, 3600*24);
-  $fsa->notes(chatid => undef);
-say "saving state ", $fsa->curr_state->name, Dumper map { ($_, $fsa->notes->{$_}) } grep { defined $fsa->notes->{$_} } keys %{ $fsa->notes };  
-  $redis->hmset($kn, map { ($_, $fsa->notes->{$_}) } grep { defined $fsa->notes->{$_} } keys %{ $fsa->notes });
-  $redis->expire($kn, 3600*24);
+  $self->{notes}->{$key};
 }
 
-sub restore_fsa_state($chat, $from)
+sub notes($self)
 {
-  my $state = $redis->get(make_key($chat, "state"));
-  set_fsa_state($state, $chat, $from);
+  $self->{notes};
 }
 
-sub set_fsa_state($state, $chat, $from)
+sub delete_note($self, $key)
 {
-  if ($state)
-  {
-    my $notes = { $redis->hgetall(make_key($chat, "notes")) };
-    $fsa->notes($_ => $notes->{$_}) for keys %$notes;
-    $fsa->notes(chatid => $chat);
-    $fsa->curr_state($state);
-    say "restore ", $state, Dumper make_key($chat, "notes"), $notes;
-  }
-  else
-  {
-    $fsa->reset;
-    $fsa->notes(chatid => $chat);
-    $fsa->notes(lang => $from->{language_code});
-    $fsa->start;
-  }
+  delete $self->{notes}->{$key};
 }
 
-sub process_input($line, $chatid, $rest)
+async sub switch
 {
-  say "*** current state = ", $fsa->curr_state->name;
-  $fsa->switch($line, $chatid, $rest);
-
-  if (defined (my $new_state = $fsa->notes("new_state")))
-  {
-    say "*** mandatory switching to $new_state";
-    set_fsa_state($new_state, $chatid, $rest);
-    $fsa->notes(new_state => undef);
-  }
-
-  say "*** Switched to ", $fsa->curr_state->name;
-}
+  my ($self, $line, @rest) = @_;
+  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");
+  die "fsa::switch: unknown new state '$state'" unless exists $self->{rules}->{$new_state};
 
-sub set_new_state($name)
-{
-  $fsa->notes(new_state => $name);
+  $self->note("_state", $new_state);
+  $new_state;
 }
 
 1;