Yuriy Zhilovets 5 maanden geleden
bovenliggende
commit
b9cfc8efb1
5 gewijzigde bestanden met toevoegingen van 420 en 40 verwijderingen
  1. 12 4
      abonbot.pl
  2. 60 22
      lib/rabbit_async.pm
  3. 161 5
      modules/commands.pm
  4. 1 0
      modules/fsa.pm
  5. 186 9
      modules/rules.pm

+ 12 - 4
abonbot.pl

@@ -149,7 +149,7 @@ post "/:token" => async sub
   $c->render(text=>"ok");
 
   my $body = j($c->req->body);
-#say Dumper $body;
+# say Dumper $body;
 
   my ($chatid, $line, $from);
   
@@ -419,7 +419,7 @@ async sub do_command
     return reply($info, _("Неизвестная команда"));
   }
 
-  await $sub->($fsa, $info);
+  await $sub->($fsa, $info, @args);
 }
 
 sub refpath
@@ -471,12 +471,17 @@ sub parse_time
   return $time;
 }
 
-sub format_time($str)
+sub format_timestamp($ts)
 {
-  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(parse_time($str));
+  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($ts);
   return sprintf("%2d.%02d.%d %2d:%02d", $mday, $mon+1, $year+1900, $hour, $min);
 }
 
+sub format_time($str)
+{
+  return format_timestamp(parse_time($str));
+}
+
 sub format_date($str)
 {
   my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(parse_time($str));
@@ -513,3 +518,6 @@ request("setWebhook",{url=>""})->then(sub
 });
 
 app->start;
+
+# не принимать edited_message вообще и не реагировать на них, если их не ждут где-то явным образом
+# редактирование заявки

+ 60 - 22
lib/rabbit_async.pm

@@ -12,7 +12,9 @@ use AnyEvent::RabbitMQ;
 use Data::Dumper;
 use Mojo::JSON qw/j/;
 use Data::UUID;
-use Promises qw/deferred collect/;
+use Mojo::Promise;
+
+use constant DEFAULT_CALL_TIMEOUT => 15;
 
 my $uuid_gen = new Data::UUID;
 
@@ -23,7 +25,7 @@ sub new
   my $callback = shift;
     
   my $self = $class->create($arg);
-  $self->connect($callback)->done(sub { $callback->($self) if $callback });
+  $self->connect($callback)->then(sub { $callback->($self) if $callback });
   
   return $self;
 }
@@ -63,7 +65,7 @@ sub _connect
 {
   my $self = shift;
   
-  my $def = deferred;
+  my $def = Mojo::Promise->new;
   my %params = (
     host => $self->{host},
     port => 5672,
@@ -96,7 +98,7 @@ sub _connect
   );
   
   $self->{rabbit}->connect(%params);
-  return $def->promise;
+  return $def;
 }
 
 sub connect
@@ -121,7 +123,7 @@ sub _open_channel
 {
   my $self = shift;
         
-  my $deferred = deferred;
+  my $deferred = Mojo::Promise->new;
   my %params = ();
           
   $params{on_success} = sub { $deferred->resolve(shift) };
@@ -138,7 +140,7 @@ sub _open_channel
   };
   
   $self->{rabbit}->open_channel(%params);
-  return $deferred->promise;
+  return $deferred;
 }
 
 sub send
@@ -194,6 +196,8 @@ sub _publish
   });
 }
 
+my $ttt;
+
 sub call
 {
   my $self = shift;
@@ -201,9 +205,19 @@ sub call
   my $key = shift;
   my $obj = shift || {};
   my $header = shift || {};
+  my $timeout = shift || DEFAULT_CALL_TIMEOUT;
 
   my $uniq = $uuid_gen->create_str();
   $self->{rpc_calls}->{$uniq} = $sub;
+  
+  my $timer = AnyEvent->timer(after=>$timeout, cb => sub
+  {
+    my $sub = delete $self->{rpc_calls}->{$uniq};
+    $sub->({error=>"Timeout"}) if $sub;
+    delete $self->{rpc_calls}->{"$uniq-timer"};
+  });
+  $ttt=$timer;
+  $self->{rpc_calls}->{"$uniq-timer"} = $timer;
 
   $self->_reply_queue->then(sub {
     my $queue = shift;
@@ -218,7 +232,7 @@ sub call
 sub _reply_queue
 {
   my $self = shift;
-  return deferred->resolve($self->{reply_queue})->promise if $self->{reply_queue};
+  return Mojo::Promise->new->resolve($self->{reply_queue}) if $self->{reply_queue};
   
   return $self->_declare_queue(
     queue => "",
@@ -239,15 +253,13 @@ sub _reply_queue
           my $msg = shift;
 
           my $corr_id = $msg->{header}->{correlation_id};
-          if (exists $self->{rpc_calls}->{$corr_id})
-          {
-            (delete $self->{rpc_calls}->{$corr_id})->({
+          my $sub = delete $self->{rpc_calls}->{$corr_id};
+          $sub->({
               message => $msg,
               content => j($msg->{body}->{payload}) || {},
               header =>  $msg->{header},
               routing_key => $msg->{deliver}->{method_frame}->{routing_key},
-            });
-          }
+          }) if $sub;
         });
      })
      ->then(sub { $self->{reply_queue} })
@@ -259,7 +271,7 @@ sub _declare_queue
 {
   my $self = shift;
   my %params = @_;
-  my $deferred = deferred;
+  my $deferred = Mojo::Promise->new;
 
   $params{on_success} = sub {
     my $method = shift;
@@ -270,7 +282,7 @@ sub _declare_queue
   $params{on_failure} = sub { $deferred->reject("_declare_queue failure [".$params{queue}."]: ".$_[2]) };
   
   $self->{chan}->declare_queue(%params);
-  return $deferred->promise;
+  return $deferred;
 }
 
 sub _bind_queue
@@ -279,7 +291,7 @@ sub _bind_queue
   my $queue = shift;
   my $key = shift;
 
-  my $deferred = deferred;
+  my $deferred = Mojo::Promise->new;
   
   $self->{chan}->bind_queue(
     queue => $queue,
@@ -289,7 +301,7 @@ sub _bind_queue
     on_failure => sub { $deferred->reject("_bind_queue failure [$queue/$key]: ".$_[0]) },
   );
 
-  return $deferred->promise;
+  return $deferred;
 }
 
 sub _bind_queue_to_many_keys
@@ -297,8 +309,8 @@ sub _bind_queue_to_many_keys
   my $self = shift;
   my $queue = shift;
   my @keys = @_;
-  
-  return collect(map { $self->_bind_queue($queue,$_) } @keys)->then(sub { $queue });
+
+  return Mojo::Promise->all(map { $self->_bind_queue($queue,$_) } @keys)->then(sub { $queue });
 }
 
 sub _consume
@@ -307,14 +319,14 @@ sub _consume
   my $sub = pop;
   my %params = @_;
 
-  my $deferred = deferred;
+  my $deferred = Mojo::Promise->new;
   
   $params{on_failure} = sub { $deferred->reject("_consume failure [".$params{queue}."]: ".$_[2]) };
   $params{on_success} = sub { $deferred->resolve() };
   $params{on_consume} = $sub;
   
   $self->{chan}->consume(%params);
-  return $deferred->promise;
+  return $deferred;
 }
 
 sub subscribe
@@ -323,8 +335,18 @@ sub subscribe
   my $keys = shift;
   my $callback = shift;
   
+  $self->subscribe_queue("", $keys, $callback);
+}
+
+sub subscribe_queue
+{
+  my $self = shift;
+  my $queue = shift;
+  my $keys = shift;
+  my $callback = shift;
+  
   $self->_declare_queue(
-    queue => "",
+    queue => $queue,
     no_ack => 1,
     durable => 0,
     exclusive => 1
@@ -363,7 +385,7 @@ sub listen_queue
     durable => 0,
     auto_delete => 0,
   )->then(sub {
-      return $self->_bind_queue($queue,$bind)
+      return $self->_bind_queue_to_many_keys($queue, ref($bind) ? @$bind : $bind)
   })->then(sub {
     return $self->_consume(
         queue => $queue,
@@ -433,6 +455,22 @@ sub reply
   );
 }
 
+sub prefetch
+{
+  my $self = shift;
+  my $cnt = shift;
+  
+  my $deferred = Mojo::Promise->new;
+  $self->{chan}->qos(
+    on_failure => sub { $deferred->reject(join(" ",@_)) },
+    on_success => sub { $deferred->resolve() },
+    prefetch_count => $cnt,
+    global => 0, # только для канала, хотя особой разницы нет - у нас нету мультиплексированных соединений
+  );
+  
+  return $deferred;
+}
+
 sub DESTROY
 {
   my $self = shift;

+ 161 - 5
modules/commands.pm

@@ -7,6 +7,7 @@ use Data::Dumper;
 our $client;
 our $abon_client;
 our $config;
+our $redis;
 
 sub __
 {
@@ -21,7 +22,12 @@ our $commands = [
   {command=>"balance", name=>__("Баланс"), description=>__("Проверка баланса")},
   {command=>"service", name=>__("Сервисы"), description=>__("Подключенные сервисы")},
   {command=>"credit", name=>__("Кредит"), description=>__("Установка кредита")},
+  {command=>"card", name=>__("Карта пополнения"), description=>__("Оплата карточкой пополнения")},
+  {command=>"payberry", name=>__("Payberry"), description=>__("Оплата через Payberry")},
   {command=>"transfer", name=>__("Перевод"), description=>__("Перевод денег")},
+  {command=>"new_task", name=>__("Новая заявка"), description=>__("Новая заявка")},
+  {command=>"tasks", name=>__("Заявки"), description=>__("Открытые заявки")},
+  {command=>"support", name=>__("Техподдержка"), description=>__("Связь с техподдержкой")},
   {command=>"logout", name=>__("Выход"), description=>__("Выход")},
 ];
 
@@ -82,7 +88,6 @@ async sub command_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}),
@@ -145,7 +150,7 @@ async sub command_service
 async sub command_transfer
 {
   my ($fsa, $info) = @_;
-  reply($info, "Введите лицевой счет пользователя, которому вы хотите перевести деньги со своего счета");
+  reply($info, _("Введите лицевой счет пользователя, которому вы хотите перевести деньги со своего счета"));
   
   $fsa->delete_note("xfer_to");
   $fsa->delete_note("xfer_amount");
@@ -176,6 +181,160 @@ async sub callback_transfer
   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 $params = {
+     description => $fsa->note("task_text"),
+     "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($info, "Заявка размещена");
+
+   $fsa->delete_note("task_text");  
+   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, _("Заявка отменена"));  
+}
+
+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");
+  
+  return reply($info, _("Открытых заявок нет")) unless $res->{total};
+  
+  my @str = map {
+    _("Заявка") . " <b>$_->{number}</b>\n$_->{description}\n<i>" . _("Комментариев") . " " . $_->{total_comments} . ", " . _(" не прочитано") . " " . $_->{unread_comments} . "</i>"
+  } @{$res->{data}};
+  
+  my $menu = [[
+    map {
+      { text => _("Комментарии к заявке") . " " . $_->{number}, callback_data => "\x00/task $_->{entity}" }
+    }  @{$res->{data}}
+  ]];
+  
+  reply_with($info, {inline_menu=>$menu}, @str);
+};
+
+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=1");
+  
+  my @str = map {
+    format_timestamp($_->{created}) . " " . "<b>" . ($_->{created_by}->[0] eq "worker" ? _("Оператор") : _("Вы")) . ":</b>\n"
+    . $_->{text}
+  } grep { $_->{visibleToUser} } @{ $res->{comments} };
+  
+  my $menu = [[
+    {text => _("Добавить комментарий"), callback_data => "\x00comment $task_id"},
+  ]];  
+  
+  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, _("Введите текст вашего ответа"));
+}
+
+##################################################
+
+use constant FAIL_BLOCK => 10;
+
+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}, _("Для оплаты перейдите по ссылке"));
+}
+
+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 цифр, можно разделять их знаком '-')"));
+
+  $fsa->delete_note("card_code");
+  $fsa->delete_note("card_serial");
+  
+  $fsa->state("card_needs_code");
+}
+
+sub command_support
+{
+   my ($fsa, $info) = @_;
+   return reply($info, _("Телефоны техподдержки:"), @{$config->{support_phones}});
+}
+
+##################################################
+
 sub format_wd($rec, $cur)
 {
   return _("бесплатно") if $rec->{dayly} == 0 && $rec->{monthly} == 0;
@@ -188,8 +347,6 @@ sub format_wd($rec, $cur)
   return $d if $d;  
 }
 
-################################################
-
 sub parse_error
 {
   my $e = shift;
@@ -200,5 +357,4 @@ sub parse_error
 
 1;
 
-# перевод денег
 # локализация

+ 1 - 0
modules/fsa.pm

@@ -56,6 +56,7 @@ async sub switch
   
   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};
   use Data::Dumper;
   say Dumper $new_state;
   die "fsa::switch: unknown new state '$new_state'" unless exists $self->{rules}->{$new_state};

+ 186 - 9
modules/rules.pm

@@ -4,6 +4,8 @@ use Mojo::Base -strict, -signatures, -async_await;
 use Mojo::Util;
 use Scalar::Util;
 
+use experimental qw/switch/;
+
 use fsa;
 
 our $redis;
@@ -13,6 +15,8 @@ our $commands;
 our $kb_menu;
 our $abon_client;
 
+use constant FAIL_ASK_SERIAL => 3;
+
 ##################################################
 
 my $rules = {
@@ -30,7 +34,11 @@ my $rules = {
      # Поступила команда /start
      
      # У нас есть uid в автомате? Это значит, что мы прочитали состояние из внешней базы
-     return "command" if $fsa->note("uid");
+     if ($fsa->note("uid"))
+     {
+       reply($info, _("Для получения списка доступных команд введите /help"));
+       return "command";
+     }
      
      # Проверяем, логинился ли уже пользователь в нашем боте
      my $res = eval { await $client->get_p("client", "/telegram/$info->{id}/client") };
@@ -51,7 +59,7 @@ my $rules = {
        }
      }
 
-     reply($info, greet($res->{fio}));
+     reply($info, greet($res->{fio}), _("Для получения списка доступных команд введите /help"));
      $fsa->note(uid => $res->{uid});
      return "command";
    },
@@ -67,6 +75,8 @@ my $rules = {
    {
       my ($fsa, $line, $info) = @_;
       
+      request("deleteMessage", { chat_id=>$info->{id}, message_id=>$info->{msgid} });
+      
       my $res = eval { await $client->post_p("client", "/telegram/client", {login=>$fsa->note("login"), password=>$line, telegram_id=>$info->{id}}) };
       if ($@)
       {
@@ -74,8 +84,8 @@ my $rules = {
         $fsa->delete_note("login");
         die $@;
       }
-         
-      reply($info, greet($res->{fio}));
+
+      reply($info, greet($res->{fio}), _("Для получения списка доступных команд введите /help"));         
       $fsa->note(uid => $res->{uid});
       $fsa->delete_note("login");
       
@@ -99,6 +109,17 @@ my $rules = {
      
      $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, _("Нельзя перевести деньги себе самому"));
@@ -136,15 +157,20 @@ my $rules = {
      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 "command";
+       reply($info, _("Вы ввели неправильную сумму"), _("Введите ее заново или пустую строку, если передумали пополнять"));
+       return "xfer_needs_amount";
      }
      
      $fsa->note(xfer_amount => $amount);
@@ -158,16 +184,162 @@ my $rules = {
      
      return "command";
    },
+   
+   #### Заявки
+   
+   task_needs_descr => async sub
+   {
+     my ($fsa, $descr, $info) = @_;
+     my $uid = $fsa->note("uid");
+     
+     $fsa->note(task_text => $descr);
+
+     reply_with($info, {
+         inline_menu => [[
+           { text=>_("Отправить"), callback_data=>"\x00/task_post" },
+           { text=>_("Отменить"), callback_data=>"\x00/task_cancel" },
+         ]]
+       },
+       _("Перед отправкой заявки вы можете ее отредактировать средствами Телеграм")
+     );
+
+     return "command";
+   },
+
+   task_needs_comment => async sub
+   {
+     my ($fsa, $comment, $info) = @_;
+     my $uid = $fsa->note("uid");
+     my $task_id = $fsa->note("task_id");
+     
+     my $params = {
+       text => $comment,
+       "for-client" => $uid,
+     };     
+
+     $fsa->state("command"); # на случай die
+     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
+   {
+     my ($fsa, $code, $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;
+     
+     if ($fails > FAIL_ASK_SERIAL)
+     {
+       $fsa->note(card_code => $code);
+       reply($info, _("Теперь введите номер карточки (7 цифр, можно разделять знаком'-')"));
+       return "card_needs_serial";
+     }
+     else
+     {
+       pay_from_card($info, $uid, $code, "");
+       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";
+   },
+
 };
 
 sub greet($whom)
 {
   my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
-  my $greet = $hour>=4 && $hour<=10 ? _("Доброе утро") : $hour>10 && $hour<6 ? _("Добрый день") : _("Добрый вечер");
+  my $greet = $hour>=4 && $hour<=10 ? _("Доброе утро") : $hour>10 && $hour<19 ? _("Добрый день") : _("Добрый вечер");
 
   "$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)
@@ -179,9 +351,12 @@ sub save_fsa($fsa, $chatid)
 {
   my $key = make_key($chatid);
   say "saving state ", $fsa->state, Dumper $fsa->notes;
+  
+  my $notes = $fsa->notes;
+  utf8::encode($notes->{$_}) for keys %$notes;
 
   $redis->del($key);
-  $redis->hmset($key, %{ $fsa->notes });
+  $redis->hmset($key, %$notes);
   $redis->expire($key, 3600*24);
 }
 
@@ -189,8 +364,10 @@ sub make_fsa($chatid, $from)
 {
   my $id = make_key($chatid);
   my $notes = { $redis->hgetall($id) };
+  
   if (keys %$notes)
   {
+    utf8::decode($notes->{$_}) for keys %$notes;
     return fsa->new($rules, $notes->{_state}, $notes); 
   }
   else