File Coverage

blib/lib/Telegram/Bot/Brain.pm
Criterion Covered Total %
statement 69 302 22.8
branch 14 138 10.1
condition 0 93 0.0
subroutine 17 39 43.5
pod 10 19 52.6
total 110 591 18.6


line stmt bran cond sub pod time code
1             package Telegram::Bot::Brain;
2             $Telegram::Bot::Brain::VERSION = '0.029';
3             # ABSTRACT: A base class to make your very own Telegram bot
4              
5              
6 2     2   345888 use Mojo::Base -base;
  2         30789  
  2         41  
7              
8 2     2   655 use strict;
  2         5  
  2         54  
9 2     2   14 use warnings;
  2         4  
  2         101  
10              
11 2     2   1614 use Mojo::IOLoop;
  2         957919  
  2         15  
12 2     2   1699 use Mojo::UserAgent;
  2         270658  
  2         21  
13 2     2   146 use Mojo::JSON qw/to_json/;
  2         3  
  2         148  
14 2     2   12 use Carp qw/croak/;
  2         4  
  2         130  
15 2     2   1330 use Log::Any;
  2         19808  
  2         11  
16 2     2   110 use Data::Dumper;
  2         4  
  2         147  
17              
18 2     2   1297 use Telegram::Bot::Object::Message;
  2         7  
  2         10  
19 2     2   92 use Telegram::Bot::Object::InlineQuery;
  2         5  
  2         14  
20 2     2   1217 use Telegram::Bot::Object::CallbackQuery;
  2         8  
  2         18  
21 2     2   1279 use Telegram::Bot::Object::ChatJoinRequest;
  2         8  
  2         19  
22 2     2   1734 use Telegram::Bot::Object::ChatMemberUpdated;
  2         7  
  2         15  
23 2     2   76 use Telegram::Bot::Object::ChatMember;
  2         5  
  2         6  
24              
25             # base class for building telegram robots with Mojolicious
26             has longpoll_time => 60;
27             has ua => sub { Mojo::UserAgent->new->inactivity_timeout(shift->longpoll_time + 15) };
28             has token => sub { croak "you need to supply your own token"; };
29              
30             has tasks => sub { [] };
31             has listeners => sub { [] };
32              
33             has log => sub { Log::Any->get_logger };
34              
35              
36             sub add_repeating_task {
37 0     0 1 0 my $self = shift;
38 0         0 my $seconds = shift;
39 0         0 my $task = shift;
40              
41             my $repeater = sub {
42              
43             # Perform operation every $seconds seconds
44 0     0   0 my $last_check = time();
45             Mojo::IOLoop->recurring(0.1 => sub {
46 0         0 my $loop = shift;
47 0         0 my $now = time();
48 0 0       0 return unless ($now - $last_check) >= $seconds;
49 0         0 $last_check = $now;
50 0         0 $task->($self);
51 0         0 });
52 0         0 };
53              
54             # keep a copy
55 0         0 push @{ $self->tasks }, $repeater;
  0         0  
56              
57             # kick it off
58 0         0 $repeater->();
59             }
60              
61              
62             sub add_listener {
63 2     2 1 1861 my $self = shift;
64 2         3 my $coderef = shift;
65              
66 2         5 push @{ $self->listeners }, $coderef;
  2         9  
67             }
68              
69             sub init {
70 0     0 0 0 die "init was not overridden!";
71             }
72              
73              
74             sub think {
75 0     0 1 0 my $self = shift;
76 0         0 $self->init();
77              
78 0         0 $self->_add_getUpdates_handler;
79 0 0       0 Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
80             }
81              
82              
83              
84             sub getMe {
85 0     0 1 0 my $self = shift;
86 0   0     0 my $token = $self->token || croak "no token?";
87              
88 0         0 my $url = "https://api.telegram.org/bot${token}/getMe";
89 0         0 my $api_response = $self->_post_request($url);
90              
91 0         0 return Telegram::Bot::Object::User->create_from_hash($api_response, $self);
92             }
93              
94              
95             sub sendMessage {
96 0     0 1 0 my $self = shift;
97 0   0     0 my $args = shift || {};
98              
99 0         0 my $send_args = {};
100 0 0       0 croak "no chat_id supplied" unless $args->{chat_id};
101 0         0 $send_args->{chat_id} = $args->{chat_id};
102              
103 0 0       0 croak "no text supplied" unless $args->{text};
104 0         0 $send_args->{text} = $args->{text};
105              
106             # these are optional, send if they are supplied
107 0 0       0 $send_args->{parse_mode} = $args->{parse_mode} if exists $args->{parse_mode};
108 0 0       0 $send_args->{disable_web_page_preview} = $args->{disable_web_page_preview} if exists $args->{disable_web_page_preview};
109 0 0       0 $send_args->{disable_notification} = $args->{disable_notification} if exists $args->{disable_notification};
110             $send_args->{reply_parameters} = to_json($args->{reply_parameters}->as_hashref)
111 0 0       0 if exists $args->{reply_parameters};
112             # deprecated reply_to_message_id
113 0 0       0 if (exists $args->{reply_to_message_id}) {
114 0         0 $send_args->{reply_parameters} = to_json({message_id => $args->{reply_to_message_id}});
115             }
116             # $send_args->{reply_to_message_id} = $args->{reply_to_message_id} if exists $args->{reply_to_message_id};
117              
118             # check reply_markup is the right kind
119 0 0       0 if (exists $args->{reply_markup}) {
120 0         0 my $reply_markup = $args->{reply_markup};
121 0 0 0     0 die "bad reply_markup supplied"
      0        
      0        
122             if ( ref($reply_markup) ne 'Telegram::Bot::Object::InlineKeyboardMarkup' &&
123             ref($reply_markup) ne 'Telegram::Bot::Object::ReplyKeyboardMarkup' &&
124             ref($reply_markup) ne 'Telegram::Bot::Object::ReplyKeyboardRemove' &&
125             ref($reply_markup) ne 'Telegram::Bot::Object::ForceReply' );
126 0         0 $send_args->{reply_markup} = to_json($reply_markup->as_hashref);
127             }
128              
129 0   0     0 my $token = $self->token || croak "no token?";
130 0         0 my $url = "https://api.telegram.org/bot${token}/sendMessage";
131 0         0 my $api_response = $self->_post_request($url, $send_args);
132              
133 0         0 return Telegram::Bot::Object::Message->create_from_hash($api_response, $self);
134             }
135              
136              
137             sub forwardMessage {
138 0     0 1 0 my $self = shift;
139 0   0     0 my $args = shift || {};
140 0         0 my $send_args = {};
141 0 0       0 croak "no chat_id supplied" unless $args->{chat_id};
142 0         0 $send_args->{chat_id} = $args->{chat_id};
143              
144 0 0       0 croak "no from_chat_id supplied" unless $args->{from_chat_id};
145 0         0 $send_args->{from_chat_id} = $args->{from_chat_id};
146              
147 0 0       0 croak "no message_id supplied" unless $args->{message_id};
148 0         0 $send_args->{message_id} = $args->{message_id};
149              
150             # these are optional, send if they are supplied
151 0 0       0 $send_args->{disable_notification} = $args->{disable_notification} if exists $args->{disable_notification};
152              
153 0   0     0 my $token = $self->token || croak "no token?";
154 0         0 my $url = "https://api.telegram.org/bot${token}/forwardMessage";
155 0         0 my $api_response = $self->_post_request($url, $send_args);
156              
157 0         0 return Telegram::Bot::Object::Message->create_from_hash($api_response, $self);
158             }
159              
160              
161             sub deleteMessage {
162 0     0 1 0 my $self = shift;
163 0   0     0 my $args = shift || {};
164 0         0 my $send_args = {};
165 0 0       0 croak "no message_id supplied" unless $args->{message_id};
166 0         0 $send_args->{message_id} = $args->{message_id};
167            
168 0   0     0 my $token = $self->token || croak "no token?";
169 0         0 my $url = "https://api.telegram.org/bot${token}/deleteMessage";
170 0         0 my $api_response = $self->_post_request($url, $send_args);
171            
172 0         0 return $api_response;
173             }
174              
175             sub editMessageText {
176 0     0 0 0 my $self = shift;
177 0   0     0 my $args = shift || {};
178 0         0 my $send_args = {};
179 0 0       0 croak "no chat_id supplied" unless $args->{chat_id};
180 0         0 $send_args->{chat_id} = $args->{chat_id};
181 0 0       0 croak "no message_id supplied" unless $args->{message_id};
182 0         0 $send_args->{message_id} = $args->{message_id};
183              
184             # these are optional, send if they are supplied
185 0 0       0 $send_args->{text} = $args->{text} if exists $args->{text};
186 0 0       0 $send_args->{parse_mode} = $args->{parse_mode} if exists $args->{parse_mode};
187 0 0       0 $send_args->{disable_web_page_preview} = $args->{disable_web_page_preview} if exists $args->{disable_web_page_preview};
188              
189 0 0       0 if (exists $args->{reply_markup}) {
190 0         0 my $reply_markup = $args->{reply_markup};
191 0 0 0     0 die "bad reply_markup supplied"
      0        
      0        
192             if ( ref($reply_markup) ne 'Telegram::Bot::Object::InlineKeyboardMarkup' &&
193             ref($reply_markup) ne 'Telegram::Bot::Object::ReplyKeyboardMarkup' &&
194             ref($reply_markup) ne 'Telegram::Bot::Object::ReplyKeyboardRemove' &&
195             ref($reply_markup) ne 'Telegram::Bot::Object::ForceReply' );
196 0         0 $send_args->{reply_markup} = to_json($reply_markup->as_hashref);
197             }
198 0   0     0 my $token = $self->token || croak "no token?";
199 0         0 my $url = "https://api.telegram.org/bot${token}/editMessageText";
200 0         0 my $api_response = $self->_post_request($url, $send_args);
201              
202 0         0 return Telegram::Bot::Object::Message->create_from_hash($api_response, $self);
203              
204             }
205              
206              
207             sub sendPhoto {
208 0     0 1 0 my $self = shift;
209 0   0     0 my $args = shift || {};
210              
211 0 0       0 croak "no chat_id supplied" unless $args->{chat_id};
212              
213             # photo can be a string (which might be either a URL for telegram servers
214             # to fetch, or a file_id string) or a file on disk to upload - we need
215             # to handle that last case here as it changes the way we create the HTTP
216             # request
217 0 0       0 croak "no photo supplied" unless $args->{photo};
218 0 0       0 if (-e $args->{photo}) {
219 0         0 $args->{photo} = { file => $args->{photo} };
220             }
221              
222 0   0     0 my $token = $self->token || croak "no token?";
223 0         0 my $url = "https://api.telegram.org/bot${token}/sendPhoto";
224 0         0 my $api_response = $self->_post_request($url, $args);
225              
226 0         0 return Telegram::Bot::Object::Message->create_from_hash($api_response, $self);
227             }
228              
229              
230             sub sendDocument {
231 0     0 1 0 my $self = shift;
232 0   0     0 my $args = shift || {};
233 0         0 my $send_args = {};
234              
235 0 0       0 croak "no chat_id supplied" unless $args->{chat_id};
236 0         0 $send_args->{chat_id} = $args->{chat_id};
237              
238             # document can be a string (which might be either a URL for telegram servers
239             # to fetch, or a file_id string) or a file on disk to upload - we need
240             # to handle that last case here as it changes the way we create the HTTP
241             # request
242 0 0       0 croak "no document supplied" unless $args->{document};
243 0 0       0 if (-e $args->{document}) {
244 0         0 $send_args->{document} = { document => { file => $args->{document} } };
245             }
246             else {
247 0         0 $send_args->{document} = $args->{document};
248             }
249              
250 0   0     0 my $token = $self->token || croak "no token?";
251 0         0 my $url = "https://api.telegram.org/bot${token}/sendDocument";
252 0         0 my $api_response = $self->_post_request($url, $send_args);
253              
254 0         0 return Telegram::Bot::Object::Message->create_from_hash($api_response, $self);
255             }
256              
257              
258             sub answerInlineQuery {
259 0     0 1 0 my $self = shift;
260 0   0     0 my $args = shift || {};
261 0         0 my $send_args = {};
262              
263 0 0       0 croak "no inline_query_id supplied" unless $args->{inline_query_id};
264 0         0 $send_args->{inline_query_id} = $args->{inline_query_id};
265              
266 0 0       0 croak "no results supplied" unless $args->{results};
267 0         0 $send_args->{results} = $args->{results};
268              
269             # these are optional, send if they are supplied
270 0 0       0 $send_args->{cache_time} = $args->{cache_time} if exists $args->{cache_time};
271 0 0       0 $send_args->{is_personal} = $args->{is_personal} if exists $args->{is_personal};
272 0 0       0 $send_args->{next_offset} = $args->{next_offset} if exists $args->{next_offset};
273 0 0       0 $send_args->{button} = $args->{button} if exists $args->{button};
274              
275 0   0     0 my $token = $self->token || croak "no token?";
276 0         0 my $url = "https://api.telegram.org/bot${token}/answerInlineQuery";
277 0         0 my $api_response = $self->_post_request($url, $send_args);
278              
279 0         0 return $api_response;
280             }
281              
282             sub setMyCommands {
283 0     0 0 0 my $self = shift;
284 0   0     0 my $args = shift || {};
285            
286 0         0 my $send_args = {};
287 0 0       0 croak "no commands supplied" unless $args->{commands};
288 0         0 $send_args->{commands} = to_json $args->{commands};
289 0 0 0     0 $send_args->{language_code} = $args->{language_code} if ($args->{language_code}//'') ne '';
290 0   0     0 my $token = $self->token || croak "no token?";
291 0         0 my $url = "https://api.telegram.org/bot${token}/setMyCommands";
292 0         0 return $self->_post_request($url, $send_args);
293             }
294              
295             sub answerCallbackQuery {
296 0     0 0 0 my $self = shift;
297 0   0     0 my $args = shift || {};
298              
299 0         0 my $answer_args = {};
300 0 0       0 croak "no callback_query_id supplied" unless $args->{callback_query_id};
301 0         0 $answer_args->{callback_query_id} = $args->{callback_query_id};
302              
303             # optional args
304 0 0       0 $answer_args->{text} = $args->{text} if exists $args->{text};
305 0 0       0 $answer_args->{show_alert} = $args->{show_alert} if exists $args->{show_alert};
306 0 0       0 $answer_args->{url} = $args->{url} if exists $args->{url};
307 0 0       0 $answer_args->{cache_time} = $args->{cache_time} if exists $args->{cache_time};
308 0   0     0 my $token = $self->token || croak "no token?";
309 0         0 my $url = "https://api.telegram.org/bot${token}/answerCallbackQuery";
310 0         0 my $api_response = $self->_post_request($url, $answer_args);
311              
312 0         0 return 1;
313             }
314              
315             sub getChatMember {
316 0     0 0 0 my $self = shift;
317 0         0 my $chat_id = shift;
318 0         0 my $user_id = shift;
319              
320 0         0 my $gcm_args = {};
321 0 0       0 croak "no chat_id supplied to getChatMember" unless $chat_id;
322 0 0       0 croak "no user_id supplied to getChatMember" unless $user_id;
323 0         0 $gcm_args->{user_id} = $user_id;
324 0         0 $gcm_args->{chat_id} = $chat_id;
325              
326 0   0     0 my $token = $self->token || croak "no token?";
327 0         0 my $url = "https://api.telegram.org/bot${token}/getChatMember";
328 0         0 my $api_response = $self->_post_request($url, $gcm_args);
329              
330 0         0 return Telegram::Bot::Object::ChatMember->create_from_hash($api_response, $self);
331             }
332              
333             sub approveChatJoinRequest {
334 0     0 0 0 my $self = shift;
335 0   0     0 my $args = shift || {};
336              
337 0         0 my $answer_args = {};
338 0 0       0 croak "no chat_id supplied" unless $args->{chat_id};
339 0         0 $answer_args->{chat_id} = $args->{chat_id};
340 0 0       0 croak "no user_id supplied" unless $args->{user_id};
341 0         0 $answer_args->{user_id} = $args->{user_id};
342              
343             # no optional args
344              
345 0   0     0 my $token = $self->token || croak "no token?";
346 0         0 my $url = "https://api.telegram.org/bot${token}/approveChatJoinRequest";
347 0         0 my $api_response = $self->_post_request($url, $answer_args);
348              
349 0         0 return 1;
350             }
351              
352             sub declineChatJoinRequest {
353 0     0 0 0 my $self = shift;
354 0   0     0 my $args = shift || {};
355              
356 0         0 my $answer_args = {};
357 0 0       0 croak "no chat_id supplied" unless $args->{chat_id};
358 0         0 $answer_args->{chat_id} = $args->{chat_id};
359 0 0       0 croak "no user_id supplied" unless $args->{user_id};
360 0         0 $answer_args->{user_id} = $args->{user_id};
361              
362             # no optional args
363              
364 0   0     0 my $token = $self->token || croak "no token?";
365 0         0 my $url = "https://api.telegram.org/bot${token}/declineChatJoinRequest";
366 0         0 my $api_response = $self->_post_request($url, $answer_args);
367              
368 0         0 return 1;
369             }
370              
371             sub banChatMember {
372 0     0 0 0 my $self = shift;
373 0   0     0 my $args = shift || {};
374              
375 0         0 my $answer_args = {};
376 0 0       0 croak "no chat_id supplied" unless $args->{chat_id};
377 0         0 $answer_args->{chat_id} = $args->{chat_id};
378 0 0       0 croak "no user_id supplied" unless $args->{user_id};
379 0         0 $answer_args->{user_id} = $args->{user_id};
380              
381             # optional args
382 0 0       0 $answer_args->{until_date} = $args->{until_date} if exists $args->{until_date};
383 0 0       0 $answer_args->{revoke_messages} = $args->{revoke_messages} if exists $args->{revoke_messages};
384            
385 0   0     0 my $token = $self->token || croak "no token?";
386 0         0 my $url = "https://api.telegram.org/bot${token}/banChatMember";
387 0         0 my $api_response = $self->_post_request($url, $answer_args);
388              
389 0         0 return 1;
390             }
391              
392             sub unbanChatMember {
393 0     0 0 0 my $self = shift;
394 0   0     0 my $args = shift || {};
395              
396 0         0 my $answer_args = {};
397 0 0       0 croak "no chat_id supplied" unless $args->{chat_id};
398 0         0 $answer_args->{chat_id} = $args->{chat_id};
399 0 0       0 croak "no user_id supplied" unless $args->{user_id};
400 0         0 $answer_args->{user_id} = $args->{user_id};
401              
402             # optional args
403 0 0       0 $answer_args->{only_if_banned} = $args->{only_if_banned} if exists $args->{only_if_banned};
404              
405 0   0     0 my $token = $self->token || croak "no token?";
406 0         0 my $url = "https://api.telegram.org/bot${token}/unbanChatMember";
407 0         0 my $api_response = $self->_post_request($url, $answer_args);
408              
409 0         0 return 1;
410             }
411              
412             sub _add_getUpdates_handler {
413 0     0   0 my $self = shift;
414              
415 0         0 my $http_active = 0;
416 0         0 my $last_update_id = -1;
417 0         0 my $token = $self->token;
418              
419             Mojo::IOLoop->recurring(0.1 => sub {
420             # do nothing if our previous longpoll is still going
421 0 0   0   0 return if $http_active;
422              
423 0         0 my $offset = $last_update_id + 1;
424 0         0 my $updateURL = "https://api.telegram.org/bot${token}/getUpdates?offset=${offset}&timeout=60";
425 0         0 $http_active = 1;
426              
427             $self->ua->get_p($updateURL)->then(sub {
428 0         0 my ($tx) = @_;
429 0         0 my $res = $tx->res->json;
430 0         0 my $items = $res->{result};
431 0         0 foreach my $item (@$items) {
432 0         0 $last_update_id = $item->{update_id};
433 0         0 $self->_process_message($item);
434             }
435              
436 0         0 $http_active = 0;
437             })->catch(sub {
438 0         0 my ($error) = @_;
439 0         0 warn "Connection error: $error";
440            
441 0         0 $http_active = 0;
442 0         0 });
443 0         0 });
444             }
445              
446             # process a message which arrived via getUpdates
447             sub _process_message {
448 6     6   3484 my $self = shift;
449 6         10 my $item = shift;
450              
451 6         12 my $update_id = $item->{update_id};
452             # There can be several types of responses. But only one response.
453             # https://core.telegram.org/bots/api#update
454 6         13 my $update;
455 6 100       45 $update = Telegram::Bot::Object::Message->create_from_hash($item->{message}, $self) if $item->{message};
456 6 100       24 $update = Telegram::Bot::Object::Message->create_from_hash($item->{edited_message}, $self) if $item->{edited_message};
457 6 50       17 $update = Telegram::Bot::Object::Message->create_from_hash($item->{channel_post}, $self) if $item->{channel_post};
458 6 50       15 $update = Telegram::Bot::Object::Message->create_from_hash($item->{edited_channel_post}, $self) if $item->{edited_channel_post};
459 6 50       13 $update = Telegram::Bot::Object::InlineQuery->create_from_hash($item->{inline_query}, $self) if $item->{inline_query};
460 6 50       15 $update = Telegram::Bot::Object::Message->create_from_hash($item->{my_chat_member}, $self) if $item->{my_chat_member}; # after v0.025
461 6 50       14 $update = Telegram::Bot::Object::CallbackQuery->create_from_hash($item->{callback_query}, $self) if $item->{callback_query};
462 6 50       14 $update = Telegram::Bot::Object::ChatJoinRequest->create_from_hash($item->{chat_join_request}, $self) if $item->{chat_join_request};
463 6 50       13 $update = Telegram::Bot::Object::ChatMemberUpdated->create_from_hash($item->{chat_member}, $self) if $item->{chat_member};
464 6 50       13 $update = Telegram::Bot::Object::ChatMemberUpdated->create_from_hash($item->{my_chat_member}, $self) if $item->{my_chat_member};
465             # chat_member => someone else
466             # my_chat_member => actually us
467              
468             # if we got to this point without creating a response, it must be a type we
469             # don't handle yet
470 6 100       30 if (! $update) {
471 1         8 warn "Do not know how to handle this update: " . Dumper($item);
472 1         174 return;
473             }
474              
475 5         9 foreach my $listener (@{ $self->listeners }) {
  5         22  
476             # call the listener code, supplying ourself and the update
477 8         83 $listener->($self, $update);
478             }
479             }
480              
481              
482             sub _post_request {
483 0     0     my $self = shift;
484 0           my $url = shift;
485 0   0       my $form_args = shift || {};
486              
487 0           my $res = $self->ua->post($url, form => $form_args)->result;
488 0 0         if ($res->is_success) { return $res->json->{result}; }
  0 0          
489 0           elsif ($res->is_error) { warn "Failed to post: " . $res->json->{description} . "\n" . Data::Dumper::Dumper($form_args); }
490 0           else { die "Not sure what went wrong"; }
491             }
492              
493              
494             1;
495              
496             __END__