Browse Source

+ command_purge
+ command_help

Yuriy Zhilovets 2 years ago
parent
commit
3cf55cdcfc
8 changed files with 1307 additions and 69 deletions
  1. 58 68
      djinn.pl
  2. 360 0
      lib/Mojo/AsyncAwait.pm
  3. 276 0
      lib/darsan_auth.pm
  4. 371 0
      lib/darsan_client.pm
  5. 159 0
      lib/telnet.pm
  6. 67 0
      modules/commands.pm
  7. 1 1
      run
  8. 15 0
      start

+ 58 - 68
djinn.pl

@@ -16,11 +16,17 @@ use Mojo::JSON qw/j/;
 use NetAddr::IP;
 use HTML::Restrict;
 
-my $NAME = "djinn";
-my $confdir = "config/".app->mode;
-
 use FindBin qw/$Bin/;
 use lib "$Bin/lib";
+use lib "$Bin/modules";
+
+use darsan_auth;
+use darsan_client;
+
+use commands;
+
+my $NAME = "djinn";
+my $confdir = "config/".app->mode;
 
 use rabbit_async_rec;
 
@@ -51,6 +57,9 @@ my $html_strip = HTML::Restrict->new(rules => {
   pre => [],
 });
 
+my $auth   = darsan_auth->as_server($config->{darsan}->{auth}, "system", "$confdir/system.private");
+our $client = darsan_client->new($auth, $config->{darsan}->{servers});
+
 my $term;
 my $int;
 my $hup;
@@ -83,7 +92,7 @@ hook after_dispatch => sub
 };
 #=cut
 
-##########################
+###############################################
 
 get "/health" => sub
 {
@@ -93,6 +102,8 @@ get "/health" => sub
 post "/:token" => sub
 {
   my $c = shift;
+
+  $c->render(text=>"ok");
   
   unless ($c->param("token") eq $config->{token})
   {
@@ -100,79 +111,26 @@ post "/:token" => sub
   }
   
   my $body = j($c->req->body);
-  my $m = $body->{message};
-  
+  my $m = $body->{message} || $body->{edited_message};
+
   my $chatid = $m->{chat}->{id};
-  my $from = ($m->{from}->{first_name}||"") . " " . ($m->{from}->{last_name}||"") . " (" . ($m->{from}->{username}||"") . ")";
+
+  if ($m->{chat}->{type} ne "supergroup")
+  {
+    return notify($chatid, "Общение с ботом возможно только в чате");
+  }
+  
+  my $from = $m->{from};
   my $cmd = $m->{text};
   my $msgid = $m->{message_id};
   
-  $c->render(text=>"ok");
+  return unless substr($cmd, 0, 1) eq "/"; # Бот не должен мешать общению, даже если его добавили админом
   
   do_command($cmd, $chatid, {msgid=>$msgid, from=>$from});
 };
 
 ##################################
 
-sub command::help
-{
-  my $cmd = shift;
-  my $args = shift;
-  my $chatid = shift;
-  my $rest = shift;
-  
-  notify($chatid,"*/purge-tree* Очистить дерево PON", $rest);
-}
-
-sub command::purge_tree
-{
-  my $cmd = shift;
-  my $args = shift;
-  my $chatid = shift;
-  my $rest = shift;
-  
-  notify($chatid,"Команда purge-tree: ".Dumper($args, $chatid, $rest), $rest);
-}
-
-sub command::start
-{
-  my $cmd = shift;
-  my $args = shift;
-  my $chatid = shift;
-  my $rest = shift;
-  
-  notify($chatid, "$config->{title}.\nДля получения списка команд наберите */help*");
-}
-
-############################
-
-sub do_command
-{
-  my $cmd = shift;
-  my $chatid = shift;
-  my $rest = shift;
-  
-  my ($c,@args) = split(/ /,$cmd);
-  $c =~ s|^/||;
-  
-  my $sub = reference("command::$c");
-  unless ($sub)
-  {
-    return notify($chatid, "Неизвестная команда. Введите */help*, чтобы увидеть список команд", $rest);
-  }
-  
-  eval {
-    $sub->($c, \@args, $chatid, $rest);
-  };
-  
-  if ($@)
-  {
-    $log->error("$cmd from $chatid [$rest->{from}]: $@");
-    notify($chatid, "Ошибка при выполнении команды", $rest);
-    return;
-  }
-}
-
 sub terminate
 {
   request("setWebhook", {url=>""})->then(sub
@@ -250,6 +208,38 @@ sub notify
   return $promise;
 }
 
+sub do_command
+{
+  my $cmd = shift;
+  my $chatid = shift;
+  my $rest = shift;
+  
+  local($Data::Dumper::Terse) = 1;
+  
+  my ($c,@args) = split(/ /,$cmd);
+  $c =~ s|^/||;
+  $c =~ s/\@MolDjinnBot$//;
+
+  my $sub = reference("command_$c");
+
+  unless ($sub)
+  {
+    return notify($chatid, "Неизвестная команда. Введите <b>/help</b>, чтобы увидеть список команд", $rest);
+  }
+  
+  eval {
+    $sub->($c, \@args, $chatid, $rest);
+  };
+  
+  if ($@)
+  {
+    my $msg = ref $@ eq "HASH" ? Dumper($@) : $@;
+    $log->error("$cmd from $chatid: $msg");
+    notify($chatid, "Ошибка при выполнении команды $cmd: $msg");
+    return;
+  }
+}
+
 sub refpath
 {
   my $name = shift;
@@ -264,7 +254,7 @@ sub reference
   return exists(&{$name}) ? \&{$name} : undef;
 }
 
-##################################
+######################################
 
 $log->info("Started (".app->mode.")");
 

+ 360 - 0
lib/Mojo/AsyncAwait.pm

@@ -0,0 +1,360 @@
+package Mojo::AsyncAwait;
+use Mojo::Base -strict;
+
+use Carp ();
+use Coro::State ();
+use Mojo::Util;
+use Mojo::Promise;
+use Scalar::Util ();
+use Sub::Util ();
+
+use Exporter 'import';
+
+our @EXPORT = (qw/async await/);
+
+my $main = Coro::State->new;
+$main->{desc} = 'Mojo::AsyncAwait/$main';
+
+# LIFO stack of coroutines waiting to come back to
+# always has $main as the bottom of the stack
+my @stack = ($main);
+
+# Coroutines that are ostensible done but need someone to kill them
+my @clean;
+
+# _push adds a coroutine to the stack and enters it
+# when control returns to the original pusher, it will clean up
+# any coroutines that are waiting to be cleaned up
+
+sub _push {
+  push @stack, @_;
+  $stack[-2]->transfer($stack[-1]);
+  $_->cancel for @clean;
+  @clean = ();
+}
+
+# _pop pops the current coroutine off the stack. If given a callback, it calls
+# a callback on it, otherwise, schedules it for cleanup. It then transfers to
+# the next one on the stack. Note that it can't pop-and-return (which would
+# make more sense) because any action on it must happen before control is
+# transfered away from it
+
+sub _pop (;&) {
+  Carp::croak "Cannot leave the main thread"
+    if $stack[-1] == $main;
+  my ($cb) = @_;
+  my $current = pop @stack;
+  if ($cb) { $cb->($current)       }
+  else     { push @clean, $current }
+  $current->transfer($stack[-1]);
+}
+
+sub async {
+  my $body   = pop;
+  my $opts   = _parse_opts(@_);
+  my @caller = caller;
+
+  my $subname  = "$caller[0]::__ASYNCSUB__";
+  my $bodyname = "$caller[0]::__ASYNCBODY__";
+  if (defined(my $name = $opts->{-name})) {
+    $subname  = $opts->{-install} ? "$caller[0]::$name" : "$subname($name)";
+    $bodyname .= "($name)";
+  }
+  my $desc = "declared at $caller[1] line $caller[2]";
+
+  Sub::Util::set_subname($bodyname => $body)
+    if Sub::Util::subname($body) =~ /::__ANON__$/;
+
+  my $wrapped = sub {
+    my @caller  = caller;
+    my $promise = Mojo::Promise->new;
+    my $coro    = Coro::State->new(sub {
+      eval {
+        BEGIN { $^H{'Mojo::AsyncAwait/async'} = 1 }
+        $promise->resolve($body->(@_)); 1
+      } or $promise->reject($@);
+      _pop;
+    }, @_);
+    $coro->{desc} = "$subname called at $caller[1] line $caller[2], $desc";
+    _push $coro;
+    return $promise;
+  };
+
+  if ($opts->{-install}) {
+    Mojo::Util::monkey_patch $caller[0], $opts->{-name} => $wrapped;
+    return;
+  }
+
+  Sub::Util::set_subname $subname => $wrapped;
+  return $wrapped;
+}
+
+# this prototype prevents the perl tokenizer from seeing await as an
+# indirect method
+
+sub await (*) {
+  {
+    # check that our caller is actually an async function
+    no warnings 'uninitialized';
+    my $level = 1;
+    my ($caller, $hints) = (caller($level))[3, 10];
+
+    # being inside of an eval is ok too
+    ($caller, $hints) = (caller(++$level))[3, 10] while $caller eq '(eval)';
+
+    Carp::croak 'await may only be called from in async function'
+      unless $hints->{'Mojo::AsyncAwait/async'};
+  }
+
+  my $promise = shift;
+  $promise = Mojo::Promise->new->resolve($promise)
+    unless Scalar::Util::blessed($promise) && $promise->can('then');
+
+  my (@retvals, $err);
+  _pop {
+    my $current = shift;
+    $promise->then(
+      sub {
+        @retvals = @_;
+        _push $current;
+      },
+      sub {
+        $err = shift;
+        _push $current;
+      }
+    );
+  };
+
+  # "_push $current" in the above callback brings us here
+  Carp::croak($err) if $err;
+  return wantarray ? @retvals : $retvals[0];
+}
+
+sub _parse_opts {
+  return {} unless @_;
+  return {
+    -name    => shift,
+    -install => 1,
+  } if @_ == 1;
+
+  my %opts = @_;
+  Carp::croak 'Cannot install a sub without a name'
+    if $opts{-install} && !defined $opts{-name};
+
+  return \%opts;
+}
+
+1;
+
+=encoding utf8
+
+=head1 NAME
+
+Mojo::AsyncAwait - An Async/Await implementation for Mojolicious
+
+=head1 SYNOPSIS
+
+  use Mojolicious::Lite -signatures;
+  use Mojo::AsyncAwait;
+
+  get '/' => async sub ($c) {
+
+    my $mojo = await $c->ua->get_p('https://mojolicious.org');
+    my $cpan = await $c->ua->get_p('https://metacpan.org');
+
+    $c->render(json => {
+      mojo => $mojo->result->code,
+      cpan => $cpan->result->code
+    });
+  };
+
+  app->start;
+
+=head1 DESCRIPTION
+
+Async/await is a language-independent pattern that allows nonblocking
+asynchronous code to be structured simliarly to blocking code. This is done by
+allowing execution to be suspended by the await keyword and returning once the
+promise passed to await has been fulfilled.
+
+This pattern simplies the use of both promises and nonblocking code in general
+and is therefore a very exciting development for writing asynchronous systems.
+
+If you are going to use this module to create async controllers actions in
+L<Mojolicious> applications (as seen in the L</SYNOPSIS>), you are highly
+encouraged to also use L<Mojolicious::Plugin::PromiseActions> in order to
+properly handle exceptions in your action.
+
+=head1 GOALS
+
+The primary goal of this module is to provide a useful Async/Await
+implementation for users of the Mojolicious ecosystem. It is for this reason
+that L<Mojo::Promise> is used when new promises are created. Because this is
+the primary goal, the intention is for it to remain useful even as other goals
+are considered.
+
+Secondarily, it is intended to be a testbed for early implementations of
+Async/Await in the Perl 5 language. It is for this reason that the
+implementation details are intended to be replaceable. This may manifest as a
+pluggable backend or rather as wholesale rewrites of the internals. The result
+should hopefully be backwards compatible, mostly because the interface is so
+simple, just two keywords.
+
+Of course, I always intend as much as possible that Mojolicious-focused code is
+as useful as practically possible for the broader Perl 5 ecosystem. It is for
+this reason that while this module returns L<Mojo::Promise>s, it can accept any
+then-able (read: promise) which conforms enough to the Promises/A+ standard.
+The Promises/A+ standard is intended to increase the interoperability of
+promises, and while that line becomes more gray in Perl 5 where we don't have a
+single ioloop implementation, we try our best.
+
+As implementations stabilze, or change, certain portions may be spun off. The
+initial implementation depends on L<Coro>. Should that change, or should users
+want to use it with other promise implementations, perhaps that implementation
+will be spun off to be used apart from L<Mojolicious> and/or L<Mojo::Promise>,
+perhaps not.
+
+Finally the third goal is to improve the mobility of the knowledge of this
+pattern between languages. Users of Javascript probably are already familiar
+with this patthern; when coming to Perl 5 they will want to continue to use it.
+Likewise, as Perl 5 users take on new languages, if they are familiar with
+common patterns in their new language, they will have an easier time learning.
+Having a useable Async/Await library in Perl 5 is key to keeping Perl 5
+relevent in moderning coding.
+
+=head1 CAVEATS
+
+First and foremost, this is all a little bit crazy. Please consider carefully
+before using this code in production.
+
+While many languages have async/await as a core language feature, currently in
+Perl we must rely on modules that provide the mechanism of suspending and
+resuming execution.
+
+The default implementation relies on L<Coro> which does some very magical
+things to the Perl interpreter. Other less magical implementations are in the
+works however none are available yet. In the future if additional
+implementations are available, this module might well be made pluggable. Please
+do not rely on L<Coro> being the implmementation of choice.
+
+Also note that while a L<Coro>-based implementation need not rely on L</await>
+being called directly from an L</async> function, it is currently prohibitied
+because it is likely that other/future implementations will rely on that
+behavior and thus it should not be relied upon.
+
+=head1 KEYWORDS
+
+L<Mojo::AsyncAwait> provides two keywords (i.e. functions), both exported by
+default.
+
+=head2 async
+
+  my $sub = async sub { ... };
+
+The async keyword wraps a subroutine as an asynchronous subroutine which is
+able to be suspended via L</await>. The return value(s) of the subroutine, when
+called, will be wrapped in a L<Mojo::Promise>.
+
+The async keyword must be called with a subroutine reference, which will be the
+body of the async subroutine.
+
+Note that the returned subroutine reference is not invoked for you.
+If you want to immediately invoke it, you need to so manually.
+
+  my $promise = async(sub{ ... })->();
+
+If called with a preceding name, the subroutine will be installed into the current package with that name.
+
+  async installed_sub => sub { ... };
+  installed_sub();
+
+If called with key-value arguments starting with a dash, the following options are available.
+
+=over
+
+=item -install
+
+If set to a true value, the subroutine will be installed into the current package.
+Default is false.
+Setting this value to true without a C<-name> is an error.
+
+=item -name
+
+If C<-install> is false, this is a diagnostic name to be included in the subname for debugging purposes.
+This name is seen in diagnostic information, like stack traces.
+
+  my $named_sub = async -name => my_name => sub { ... };
+  $named_sub->();
+
+Otherwise this is the name that will be installed into the current package.
+
+=back
+
+Therefore, passing a bare name as is identical to setting both C<-name> and C<< -install => 1 >>.
+
+  async -name => installed_sub, -install => 1 => sub { ... };
+  installed_sub();
+
+If the subroutine is installed, whether by passing a bare name or the C<-install> option, nothing is returned.
+Otherwise the return value is the wrapped async subroutine reference.
+
+=head2 await
+
+  my $tx = await Mojo::UserAgent->new->get_p('https://mojolicious.org');
+  my @results = await (async sub { ...; return @async_results })->();
+
+The await keyword suspends execution of an async sub until a promise is
+fulfilled, returning the promise's results. In list context all promise results
+are returned. For ease of use, in scalar context the first promise result is
+returned and the remainder are discarded.
+
+If the value passed to await is not a promise (defined as having a C<then>
+method>), it will be wrapped in a Mojo::Promise for consistency. This is mostly
+inconsequential to the user.
+
+Note that await can only take one promise as an argument. If you wanted to
+await multiple promises you probably want L<Mojo::Promise/all> or less likely
+L<Mojo::Promise/race>.
+
+  my $results = await Mojo::Promise->all(@promises);
+
+=head1 AUTHORS
+
+Joel Berger <joel.a.berger@gmail.com>
+
+Marcus Ramberg <mramberg@cpan.org>
+
+=head1 CONTRIBUTORS
+
+Sebastian Riedel <kraih@mojolicious.org>
+
+=head1 ADDITIONAL THANKS
+
+Matt S Trout (mst)
+
+Paul Evans (LeoNerd)
+
+John Susek
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (C) 2018, L</AUTHORS> and L</CONTRIBUTORS>.
+
+This program is free software, you can redistribute it and/or modify it under
+the terms of the Artistic License version 2.0.
+
+=head1 SEE ALSO
+
+L<Mojo::Promise>
+
+L<Mojolicious::Plugin::PromiseActions>
+
+L<MDN Async/Await|https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function>
+
+L<Coro::State>
+
+L<Future::AsyncAwait>
+
+L<PerlX::AsyncAwait>
+
+=cut

+ 276 - 0
lib/darsan_auth.pm

@@ -0,0 +1,276 @@
+#!/usr/bin/perl
+
+use Modern::Perl;
+
+# Авторизация для Дарсана
+# Ю. Жиловец, 17.02.2015
+
+package darsan_auth;
+
+use Crypt::OpenSSL::RSA;
+use Mojo::JSON qw/j/;
+use Mojo::UserAgent;
+use Convert::Ascii85 qw/ascii85_encode/;
+use Time::timegm qw/timegm/;
+use POSIX::strptime;
+use Data::Dumper;
+use Mojo::Promise;
+
+sub error
+{
+  return $_[0]->{error};
+}
+
+sub source_ip
+{   
+  my $self = shift;
+  my $ip = shift;
+  $self->{ua}->local_address($ip);
+}
+
+sub as_server
+{
+  my $class = shift;
+  my $auth_servers = shift;
+  my $name = shift;
+  my $private = shift;
+
+  if (ref($private) ne "Crypt::OpenSSL::RSA")
+  {
+    open(K,$private) or do die "No private key found: $private: $!";
+    my $key = do { local($/); <K>};
+    close(K);
+    $private = Crypt::OpenSSL::RSA->new_private_key($key);
+    $private->use_sha1_hash;
+    $private->use_pkcs1_padding;
+  }
+
+  $auth_servers = [ $auth_servers ] unless ref $auth_servers;
+  return bless {auth_servers=>$auth_servers, name=>$name, private=>$private, error=>undef, ua=>new Mojo::UserAgent},$class;
+}
+
+sub as_user
+{
+  my $class = shift;
+  my $auth_servers = shift;
+  my $login = shift;
+  my $password = shift;
+
+  $auth_servers = [ $auth_servers ] unless ref $auth_servers;
+  return bless {auth_servers=>$auth_servers, login=>$login, password=>$password, error=>undef, ua=>new Mojo::UserAgent},$class;
+}
+
+sub set_token
+{
+  my $self = shift;
+  my $token = shift;
+  my $exp_stamp = shift;
+
+  $self->{token} = $token;
+  $self->{expire} = $exp_stamp;
+}
+
+sub token
+{
+  my $self = shift;
+  my $sub = shift;
+ 
+  my $deferred;
+
+  if ($sub && $sub eq "promise")
+  {
+    $deferred = new Mojo::Promise;
+    $sub = sub {
+
+      my ($err,$body) = @_;
+      if ($err)
+      {
+        $deferred->reject($err);
+      }
+      else
+      {
+        $deferred->resolve($body);
+      }
+    };
+  }
+
+  undef $self->{error};
+  if ($self->{token} && $self->{expire}>time())
+  {
+    if ($sub)
+    {
+      $sub->(undef,$self->{token});
+      return $deferred;
+    }
+    else
+    {
+      return $self->{token};
+    }
+  }
+
+  if ($sub)
+  {
+    $self->_token(sub {
+      my ($err,$token,$expire) = @_;
+
+      if ($err)
+      {
+        $self->{error} = $err;
+        return $sub->($err);
+      }
+
+      $self->{token} = $token;
+      $self->{expire} = $expire;
+      $sub->(undef,$token);
+    });
+
+    return $deferred;
+  }
+  else
+  {
+    my ($err,$token,$expire) = $self->_token;
+    if ($err)
+    {
+      $self->{error} = $err;
+      return undef;
+    }
+
+    $self->{token} = $token;
+    $self->{expire} = $expire;
+
+    return $token;
+  }
+}
+
+sub _token
+{
+  my $self = shift;
+  my $sub = shift;
+
+  if ($self->{login})
+  {
+    return $self->_fetch_token($sub, "token",
+                               {ver=>2, login=>$self->{login}, password=>$self->{password}} );
+  }
+  else
+  {
+    my $t = time();
+    my $sign = $self->{private}->sign("$self->{name}/$t");
+    return $self->_fetch_token($sub, "server-token/$self->{name}",
+                               {ver=>2, time=>$t, sign=>ascii85_encode($sign)} );
+  }
+}
+
+sub _fetch_token
+{
+  my $self = shift;
+  my $sub = shift;
+  my $url = shift;
+  my $form = shift;
+
+  if ($sub)
+  {
+    $self->_fetch_token_async($sub,$url,$form);
+  }
+  else
+  {
+    $self->_fetch_token_sync($url,$form);
+  }
+}
+
+sub _fetch_token_sync
+{
+  my $self = shift;
+  my $url = shift;
+  my $form = shift;
+
+  my $tx;
+
+  LOOP: {
+  foreach my $server (@{ $self->{auth_servers} })
+  {
+    $tx = $self->{ua}->post("$server/$url" => form => $form);
+
+    last LOOP if $tx->result;
+    last if $tx->error && $tx->error->{status} && $tx->error->{status}==400;
+  }
+
+  my $e = $tx->error;
+  $e->{code} ||= 500;
+  my $err = {code=>$e->{code}, message=> $e->{message}, body => $tx->res->body};
+  return ($err);
+}
+
+  my $body = j($tx->res->body);
+  my $token = $body->{token};
+  my $expires = $body->{expires};
+  my $exp = $self->parse_time($expires);
+
+  return (undef,$token,$exp);
+}
+
+sub _fetch_token_async
+{
+  my $self = shift;
+  my $sub = shift;
+  my $url = shift;
+  my $form = shift;
+
+  my $server = $self->{auth_servers}->[0];
+  $self->{ua}->post_p("$server/$url" => form => $form)
+    ->then(sub
+  {
+    my $tx = shift;
+    
+    if (my $e = $tx->error)
+    {
+      return $sub->( {code=>$e->{code}, url=>"$server/$url", message=>$e->{message}, body=>$tx->result->body} );
+    }
+                      
+    my $body = j($tx->result->body);
+    my $token = $body->{token};
+    my $expires = $body->{expires};
+    my $exp = $self->parse_time($expires);
+    $sub->(undef,$token,$exp);
+  })
+    ->catch(sub
+  {
+    my $e = shift;
+    unless (ref $e)
+    {
+      $sub->( {code=>500, url=>"$server/$url", message=>$e, body=>""} );
+    }
+    else
+    {
+      $e->{code} ||= 500;
+      $sub->( {code=>$e->{code}, url=>"$server/$url", message=>$e->{message}, body=>""} );
+    }
+  });
+}
+
+sub parse_time
+{
+  no warnings;
+  my $self = shift;
+  my $s = shift;
+
+  $s =~ /([+-])(\d{4})$/;
+  my $tz_sign = $1;
+  my $tz = $2;
+  $s =~ s/[+-]\d{4}$//;
+
+  my $time = timegm(POSIX::strptime($s,"%Y-%m-%d %H:%M:%S%z"));
+
+  if ($tz)
+  {
+    my $h = substr($tz,0,2);
+    my $s = substr($tz,2,2);
+    my $offset = ($h*60+$s)*60;
+    $offset = -$offset if $tz_sign eq "+";
+    $time += $offset;
+  }
+
+  return $time;
+}
+
+1;

+ 371 - 0
lib/darsan_client.pm

@@ -0,0 +1,371 @@
+#!/usr/bin/perl
+
+use Modern::Perl;
+
+# Клиент для Дарсана, версия 2
+# Ю. Жиловец, 17.02.2015
+
+package darsan_client;
+
+use Mojo::UserAgent;
+use URI::Query;
+use Mojo::JSON qw/j/;
+use Data::Dumper;
+use utf8;
+
+sub new
+{
+  my $class = shift;
+  my $auth = shift;
+  my $tpl = shift;
+
+  return bless {auth=>$auth,tpl=>$tpl,error=>undef, ua => new Mojo::UserAgent}, $class;
+}
+
+sub source_ip
+{
+  my $self = shift;
+  my $ip = shift;
+  $self->{ua}->local_address($ip);
+}
+
+sub error
+{
+  return $_[0]->{error};
+}
+
+sub _query
+{
+  my $self = shift;
+  my $method = shift;
+  my $url = shift;
+  my $sub = pop;
+  my @rest = @_;
+
+  undef $self->{error};
+
+  my $is_raw = 0;
+  if ($method eq "GET_RAW")
+  {
+    $method = "GET";
+    $is_raw = 1;
+  }
+
+  my $headers = {};
+  $headers = shift(@rest) if @rest && ref($rest[0]) eq "HASH";
+
+   my $make_query = sub
+   {
+      my $token = shift;  
+      $headers->{Authorization} = "Darsan2 $token";
+      return $self->{ua}->build_tx($method,$url,$headers,@rest);
+   };
+
+  if ($sub && $sub eq "promise")
+  {
+    return $self->_promise_query($make_query, $is_raw);
+  }
+  elsif ($sub)
+  {
+    return $self->_async_query($sub, $make_query, $is_raw);
+  }
+  else
+  {
+    return $self->_sync_query($make_query, $is_raw);
+  }
+}
+
+sub _sync_query
+{
+  my $self = shift;
+  my $make_query = shift;
+  my $is_raw = shift;
+  
+  my $token = $self->{auth}->token;
+  if ($self->{auth}->error)
+  {
+    $self->{error} = "darsan_auth: ".$self->{auth}->error;
+    return undef;
+  }
+
+  my $tx = $self->{ua}->start($make_query->($token));
+  my $resp = $tx->result;
+
+ if ($resp->is_error)
+ {
+    my $e = $tx->error; 
+    $e->{code} ||= 500;
+    $self->{error} = "$e->{code} $e->{message}/".substr($tx->res->body,0,500);
+    return undef;
+  }
+
+  return $resp if $is_raw;
+  return $resp->headers->content_type =~ m|application/json| ? j($resp->body) : $resp->body;
+}
+
+sub _async_query
+{
+  my $self = shift;
+  my $sub = shift;
+  my $make_query = shift;
+  my $is_raw = shift;
+  
+  $self->{auth}->token(sub 
+  {
+      my ($err,$token) = @_;
+      return $sub->({code=>500, message=>"darsan_auth: cannot get token: $err->{message}"}) if $err;
+
+      my $tx = $self->{ua}->start($make_query->($token) => sub
+      { 
+        my ($ua, $tx) = @_;
+        my $resp = $tx->result;
+      
+        if ($resp->is_error)
+        {
+          my $e = $tx->error;
+          $e->{code} ||= 500;
+          my $error = { code=>$e->{code}, message=>$e->{message}, response=>$tx->res };
+          return $sub->($error);
+        }
+        
+        $sub->(undef,$is_raw ? $resp : j($resp->body));
+      });
+  });
+}
+
+sub _promise_query
+{
+  my $self = shift;
+  my $make_query = shift;
+  my $is_raw = shift;
+  
+  return $self->{auth}->token("promise")->then(sub
+  {
+      my $token = shift;
+
+      my $tx = $self->{ua}->start_p($make_query->($token))->then(sub
+      { 
+        my $tx = shift;
+        my $resp = $tx->result;
+        
+        if ($resp->is_success)
+        {
+           $is_raw ? $resp : j($resp->body);
+        }
+        else
+        {
+          my $body = $resp->body;
+          utf8::decode($body);
+          die {code=>$resp->{code}, message=>$resp->{message}, body=>$body, content_type=>$resp->headers->header("Content-Type")};
+        }
+      });
+  }, sub 
+  {
+    my $err = shift;
+    die {code=>500, message=>"darsan_auth: cannot get token from $err->{url}: $err->{code} $err->{message}"};
+  });
+}
+
+sub get
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+  my $sub = shift;
+  
+  my $q = URI::Query->new($params);
+  $q = "?$q" if $q;
+  my $url = $self->_make_server($topic).$path.$q;
+  return $self->_query(GET => $url => $sub);
+}
+
+sub get_p
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+
+  return $self->get($topic, $path, $params, "promise");
+}
+
+sub get_raw
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+  my $sub = shift;
+            
+  my $q = URI::Query->new($params);
+  $q = "?$q" if $q;
+  my $url = $self->_make_server($topic).$path.$q;
+  return $self->_query(GET_RAW => $url => $sub);
+}
+
+sub get_raw_p
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+            
+  return $self->get_raw($topic, $path, $params, "promise");            
+}
+
+sub post
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+  my $sub = shift;
+  
+  my $url = $self->_make_server($topic).$path;
+  return $self->_query(POST => $url => form => $params => $sub);
+}
+
+sub post_p
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+
+  return $self->post($topic, $path, $params, "promise");  
+}
+
+sub post_json
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+  my $sub = shift;
+  
+  my $url = $self->_make_server($topic).$path;
+  return $self->_query(POST => $url => { "Content-Type"=>"application/json" } => j($params) => $sub);
+}
+
+sub post_json_p
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+  
+  my $url = $self->_make_server($topic).$path;
+  return $self->_query(POST => $url => { "Content-Type"=>"application/json" } => j($params) => "promise");
+}
+
+sub delete
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+  my $sub = shift;
+
+  my $url = $self->_make_server($topic).$path;
+  return $self->_query(DELETE => $url => form => $params => $sub);
+}
+
+sub delete_p
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+
+  $self->delete($topic, $path, $params, "promise");  
+}
+
+sub put
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+  my $sub = shift;
+  
+  my $url = $self->_make_server($topic).$path;
+  return $self->_query(PUT => $url => form => $params => $sub);
+}
+
+sub put_p
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+
+  $self->put_p($topic, $path, $params, "promise");  
+}
+
+sub patch
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+  my $sub = shift;
+
+  my $url = $self->_make_server($topic).$path;
+  return $self->_query(PATCH => $url => form => $params => $sub);
+}
+
+sub patch_json
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+  my $sub = shift;
+
+  my $url = $self->_make_server($topic).$path;
+  return $self->_query(PATCH => $url => { "Content-Type"=>"application/json" } => j($params) => $sub);
+}
+
+sub patch_p
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+
+  $self->patch($topic, $path, $params, "promise");  
+}
+
+sub patch_json_p
+{
+  my $self = shift;
+  my $topic = shift;
+  my $path = shift;
+  my $params = shift || {};
+
+  $self->patch_json($topic, $path, $params, "promise");  
+}
+
+sub map
+{
+  my $self = shift;
+  my $map = shift;
+  $self->{map} = $map;
+}
+
+sub _make_server
+{
+  my $self = shift;
+  my $topic = shift;
+
+  return $self->{map}->{$topic} if $self->{map} && exists $self->{map}->{$topic};
+
+  my $server = $self->{tpl};
+  $server =~ s/\{entity\}/$topic/;
+  $server =~ s/darsan-darsan/darsan/;
+
+  return $server;
+}
+
+1;

+ 159 - 0
lib/telnet.pm

@@ -0,0 +1,159 @@
+#!/usr/bin/perl
+
+use Modern::Perl;
+use experimental qw/smartmatch/;
+
+package telnet;
+
+use AnyEvent::Handle qw/connect/;
+use Mojo::Promise;
+use Data::Dumper;
+
+sub new
+{
+  my $class = shift;
+  my $ip = shift;
+
+  return bless {ip => $ip},"telnet";
+}
+
+sub connect
+{
+  my $self = shift;
+  
+  my $promise = new Mojo::Promise;
+  
+  $self->{handle} = new AnyEvent::Handle
+   connect => [$self->{ip}, 23],
+   on_prepare => sub { 30 }, # time-out
+   on_connect => sub { $promise->resolve },
+   on_connect_error => sub { $promise->reject("$self->{ip}: " . $_[1]) }, 
+   on_rtimeout => sub 
+   { 
+     if ($self->{promise})
+     {
+       $self->{promise}->reject("$self->{ip}: read timeout");
+     }
+     else
+     {
+       say "telnet error: timeout";
+     }
+   },
+   on_error => sub { 
+      my ($hdl, $fatal, $msg) = @_;
+      $hdl->destroy;
+      if ($self->{promise})
+      {
+        $self->{promise}->reject("$self->{ip}: $msg");
+      }
+      else
+      {
+        say "telnet error: $msg";
+      }
+   };
+  
+  return $promise;
+}
+
+sub print
+{
+  my $self = shift;
+  my $line = shift;
+  
+  say STDERR ">>> $line" if $self->{debug};
+  $self->{handle}->push_write("$line\r\n");
+}
+
+sub _new_promise
+{
+  my $self = shift;
+  
+  $self->{promise} = new Mojo::Promise;
+}
+
+sub _wait_for
+{
+  my $self = shift;
+  my $re = shift;
+  
+  say STDERR "### waiting for ", $re if $self->{debug};
+  
+  $self->{handle}->rtimeout(60);
+  $self->{handle}->push_read(regex => $re, sub 
+  {
+    my ($h, $text) = @_;
+  
+    $h->rtimeout(0);
+    my @lines = split(/\n/, $text); 
+    
+    if ($self->{debug})
+    {
+      say "<<< $_" for @lines;
+    }
+    
+    shift(@lines);
+    
+    @lines = map { s/\r//g; $_ } @lines;
+    
+    $self->{promise}->resolve(@lines) if $self->{promise};
+  });
+  
+  return $self->{promise};
+}
+
+sub waitfor
+{
+  my $self = shift;
+  my $re = shift;
+  
+  $self->_new_promise;
+  return $self->_wait_for($re);
+}
+
+sub prompt
+{
+   my $self = shift;
+   my $re = shift;
+   
+   $self->{prompt} = $re;
+}
+
+sub cmd
+{
+  my $self = shift;
+  my $line = shift;
+  
+  $self->_new_promise;
+  $self->print($line);
+
+  return $self->_wait_for($self->{prompt});
+}
+
+sub reply
+{
+  my $self = shift;
+  my $re = shift;
+  my $line = shift;
+  
+  $self->_new_promise;
+  
+  return $self->_wait_for($re)->then(sub
+  {
+    $self->print($line);
+  });
+}
+
+sub close
+{
+  my $self = shift;
+  undef $self->{handle};
+}
+
+sub debug
+{
+  my $self = shift;
+  my $flag = shift;
+  $self->{debug} = $flag;
+}
+
+1;

+ 67 - 0
modules/commands.pm

@@ -0,0 +1,67 @@
+use Modern::Perl;
+use utf8;
+
+use Mojo::AsyncAwait;
+use Data::Dumper;
+use Sub::Install;
+
+our $client;
+
+##############################################
+
+sub command_help
+{
+  my $cmd = shift;
+  my $args = shift;
+  my $chatid = shift;
+  my $rest = shift;
+  
+  notify($chatid, "<b>/purge</b> <i>ip-адрес-устройства номер-дерева</i> - Очистить дерево PON", $rest);
+}
+
+# purge 172.1.1.1 2 - почистить дерево 2 на ПОН
+async command_purge => sub
+{
+  my $cmd = shift;
+  my $args = shift;
+  my $chatid = shift;
+  my $rest = shift;
+
+  my $ip = $args->[0];
+  my $tree = $args->[1];
+
+  return notify($chatid, "Не указан IP-адрес") unless $ip;
+  return notify($chatid, "Не указан номер дерева") unless $tree;
+
+  return notify($chatid, "Неправильный IP-адрес: $ip") unless is_valid_ip($ip);
+  return notify($chatid, "Неправильный номер дерева: $tree") unless $tree =~ /^\d+$/;
+
+  my $res;
+  eval {
+    $res = await $client->get_p("device", "/pon?query=ip=$ip");
+  };
+  return notify($chatid, "Ошибка: " . parse_error($@)) if $@;
+  return notify($chatid, "Устройство с IP=$ip не найдено") if @$res<1;
+  
+  my $dev = $res->[0];
+  
+  notify($chatid, "Очищаю дерево $tree на устройстве $ip '$dev->{name}'...");
+};
+
+###################################
+
+sub is_valid_ip
+{
+  return $_[0] =~ /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
+}
+
+sub parse_error
+{
+  my $e = shift;
+
+  return $e unless ref $e;
+  
+  return "$e->{code} $e->{message} $e->{body}";
+}
+
+1;

+ 1 - 1
run

@@ -1 +1 @@
-MOJO_LOG_LEVEL=debug perl djinn.pl daemon --mode test --listen http://*:2217
+MOJO_CLIENT_DEBUG=1 MOJO_LOG_LEVEL=debug morbo djinn.pl daemon --mode test --listen http://*:2217

+ 15 - 0
start

@@ -0,0 +1,15 @@
+#!/bin/bash
+
+MODE=$1
+LC_MESSAGES=C
+
+if [ -z $MODE ]; 
+then 
+  echo "Usage: start mode" 
+  exit 1
+fi
+
+cd /var/opt/minion
+./djinn.pl daemon --mode $MODE --listen http://127.0.0.1:2217
+
+