Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Please, describe in documentation how to implement bots "main cycle" in async mode #15

Closed
DRVTiny opened this issue Jan 19, 2017 · 6 comments

Comments

@DRVTiny
Copy link

@DRVTiny DRVTiny commented Jan 19, 2017

I was trying to implement bot, which receives updates in async mode, but this is not work: code of my callback does not executed for some unknown reasons.

Code:

my $bot=WWW::Telegram::BotAPI->new('token'=>$botToken);

my $ua=$bot->agent;
$ua->proxy->http($proxyConString)->https($proxyConString);

try {
  say $bot->getMe->{'result'}{'username'};
} catch {
  croak 'Catched error from WWW::Telegram::BotAPI, getMe method: ',$_;  
};

my $cv=AnyEvent->condvar;

my $doOnGetUpdate;
$doOnGetUpdate=sub {
    my ($ua, $tx)=@_;
    my $rq=$ua->res->json;
    say $ua->res->body;
    my $chan_id=$rq->{'result'}{'message'}{'chat'}{'id'};
    $cv->begin;
    say STDERR "+++++++++++++++++++++++++ $doOnGetUpdate +++++++++++++++++++++++++++++";
    $bot->api_request('getUpdates', {}, $doOnGetUpdate);
    $cv->end;
};

$cv->begin;
$bot->api_request('getUpdates', {}, $doOnGetUpdate);

 Mojo::IOLoop->start;

===

So it will be very helpful if you add some short and clear example of bots ""main loop" implementation in async mode.

Thank you!

@Robertof
Copy link
Owner

@Robertof Robertof commented Jan 19, 2017

Hi,

first things first: when using Mojo::UserAgent you don't need AnyEvent or condvars at all - everything is handled by Mojo::IOLoop transparently.

I have written an example for you showing how to make a (very) basic async Telegram bot:

#!/usr/bin/env perl
# Async Telegram bot implementation using WWW::Telegram::BotAPI
# See also https://gist.github.com/Robertof/d9358b80d95850e2eb34
use Mojo::Base -strict;
use WWW::Telegram::BotAPI;

my $api = WWW::Telegram::BotAPI->new (
    token => (shift or die "ERROR: a token is required!\n"),
    async => 1
);

# Increase the inactivity timeout of Mojo::UserAgent
$api->agent->inactivity_timeout (45);

# Fetch bot information asynchronously
my $me;

$api->getMe (wrap_async (sub {
    $me = shift;
    say "Received bot information from server: hello world, I'm $me->{first_name}!";
}));

# Async update handling
sub fetch_updates {
    state $offset = 0;
    $api->getUpdates ({
        timeout => 30,
        allowed_updates => ["message"], # remove me in production if you need multiple update types
        $offset ? (offset => $offset) : ()
    } => wrap_async (sub {
        my $updates = shift;
        for my $update (@$updates) {
            $offset = $update->{update_id} + 1 if $update->{update_id} >= $offset;
            # Handle text messages. No checks are needed because of `allowed_updates`
            my $message_text = $update->{message}{text};
            say "> Incoming message from \@$update->{message}{from}{username} at ",
                scalar localtime;
            say ">> $message_text";
            # For this example, we're just going to handle messages containing '/say something'
            if ($message_text =~ m!^/say (.+)!i) {
                # For this example, we don't care about the result of sendMessage.
                $api->sendMessage ({
                    chat_id => $update->{message}{chat}{id},
                    text    => $1
                } => sub { say "> Replied at ", scalar localtime })
            }
        }
        # Run the request again using ourselves as the handler :-)
        fetch_updates();
    }));
};

fetch_updates();

Mojo::IOLoop->start;

sub wrap_async {
    my $callback = shift;
    sub {
        my (undef, $tx) = @_;
        my $response = $tx->res->json;
        unless ($tx->success && $response && $response->{ok}) {
            # TODO: if using this in production code, do NOT die on error, but handle them
            # gracefully
            say "ERROR: ", ($response->{error_code} ? "code $response->{error_code}: " : ""),
                $response->{description} ?
                    $response->{description} :
                    ($tx->error || {})->{message} || "something went wrong!";
            Mojo::IOLoop->stop;
            exit
        }
        $callback->($response->{result});
    }
}

Basically:

  • the script is executed like perl script.pl bottoken
  • wrap_async handles errors returned by Mojo::UserAgent callbacks, and returns a callback suitable for usage directly in the WWW::Telegram::BotAPI calls. Ideally, wrap_async should handle errors gracefully
  • fetch_updates calls getUpdates and retrieves current updates using long polling, then handles (for this demonstration) the command /say something, which echoes something you input. After update processing is finished, it calls itself again waiting for updates

Hope it helps!
Have a good day,

Roberto

@DRVTiny
Copy link
Author

@DRVTiny DRVTiny commented Jan 20, 2017

Oh, thats great!
I've rewrite your code slightly (utf8 + add proxy autodetect + moved fetch_updates to getMe because, i think, if bot cant proceed with getMe - it cant work at all)

#!/usr/bin/env perl
# Async Telegram bot implementation using WWW::Telegram::BotAPI
# See also https://gist.github.com/Robertof/d9358b80d95850e2eb34
use constant {
    INACT_TIMEOUT=>45,
    GET_UPD_TIMEOUT=>30,
};

use utf8;
use Mojo::Base -strict;
use WWW::Telegram::BotAPI;
use JSON::XS;

binmode $_, ':utf8' for *STDOUT, *STDERR;

my $api = WWW::Telegram::BotAPI->new (
    'token' => (shift or die "ERROR: a token is required!\n"),
    'async' => 1
);

die 'You must install Mojo::UserAgent to start telegram bot in async mode'
    unless $api->agent->isa('Mojo::UserAgent');
    
# Increase the inactivity timeout of Mojo::UserAgent
$api->agent->inactivity_timeout (INACT_TIMEOUT);

# If we have detected that "some_proxy" environment variables was set, tell our Mojo::UserAgent instance to use them
if ( grep defined $_, @ENV{map 'http'.$_.'_proxy', 's', ''} ) {
    $api->agent->proxy->detect;
}

# Fetch bot information asynchronously
$api->getMe (wrap_async (sub {
    my $me = shift;
    say "Telegram bot << $me->{'first_name'} >> initialized and ready to fetch updates!";
    fetch_updates();
}));

Mojo::IOLoop->start;

# Async update handling
sub fetch_updates {
    state $offset = 0;
    $api->getUpdates ({
        'timeout' => GET_UPD_TIMEOUT,
        'allowed_updates' => ['message'], # remove me in production if you need multiple update types
        $offset ? ('offset' => $offset) : ()
    } => wrap_async (sub {
        my $updates = shift;
        for my $update (@$updates) {
            say "Received update: ".JSON::XS->new->utf8->pretty->encode($update);
            $offset = $update->{'update_id'} + 1 if $update->{'update_id'} >= $offset;
            # Handle text messages. No checks are needed because of `allowed_updates`
            my $message=$update->{'message'};
            my $message_text = $message->{'text'};
            printf "> Incoming message from \@%s at %s\n", 
                join(' ', @{$message->{'from'}}{map $_.'_name', qw(first last)}), scalar localtime;
            say ">> $message_text";
            # For this example, we're just going to handle messages containing '/say something'
            if ($message_text =~ m!^/say(?:\s+(.*))?$!i) {
                # For this example, we don't care about the result of sendMessage.
                $api->sendMessage ({
                    'chat_id' => $message->{'chat'}{'id'},
                    'text'    => $1?'You say: '.$1:'Oh, you do you really have nothing to say?'
                } => sub { say "> Replied at ", scalar localtime })
            }
        }
        # Run the request again using ourselves as the handler :-)
        fetch_updates();
    }));
};

sub wrap_async {
    my $callback = shift;
    sub {
        my (undef, $tx) = @_;
        my $response = eval { $tx->res->json } || {};
        unless (ref $response eq 'HASH' && %{$response} && $tx->success && $response->{'ok'}) {
            # TODO: if using this in production code, do NOT die on error, but handle them
            # gracefully
            say "ERROR: ", ($response->{'error_code'} ? "code $response->{'error_code'}: " : ""),
                $response->{'description'} ?
                    $response->{'description'} :
                    ($tx->error || {})->{'message'} || "something went wrong (for some unknown reasons :) )!";
            Mojo::IOLoop->stop;
            exit
        }
        $callback->($response->{'result'});
    }
}

@DRVTiny
Copy link
Author

@DRVTiny DRVTiny commented Jan 20, 2017

My monitoring system will be very glad to have possibility to annoy our duty in the Telegram group chat (after i've implement the bot based on ideas described in your example) :)
Excellent!
Thank you so much for very clear and useful example!

@DRVTiny
Copy link
Author

@DRVTiny DRVTiny commented Jan 20, 2017

Sorry, JSON::XS->new->pretty->encode($update) must be instead of JSON::XS->new->utf8->pretty->encode($update) (because of binmode on STDOUT)

@DRVTiny
Copy link
Author

@DRVTiny DRVTiny commented Jan 20, 2017

Your example works perfectly in conjuction with AnyEvent->timer handlers.
Though i wil use async bot with AnyEvent::SMTP::Server

@DRVTiny DRVTiny closed this Jan 20, 2017
@Robertof
Copy link
Owner

@Robertof Robertof commented Jan 22, 2017

Thank you for your valuable contribution! I appreciate your edits, and let me give some tips for you:

  • Use JSON::MaybeXS instead of JSON::XS (which - guess what - is required & included by this module so you already have it!), which defaults to Cpanel::JSON::XS that is much better than JSON::XS
  • If Mojo::UserAgent is not available and async is requested, then the module croaks automatically for you (so no manual check is necessary)
  • use utf8 is already included in Mojo::Base

Thanks again and have a good day!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
2 participants