File Coverage

blib/lib/Mojo/Webqq/Client.pm
Criterion Covered Total %
statement 63 340 18.5
branch 0 156 0.0
condition 0 58 0.0
subroutine 21 50 42.0
pod 0 20 0.0
total 84 624 13.4


line stmt bran cond sub pod time code
1             package Mojo::Webqq::Client;
2 1     1   10 use strict;
  1         2  
  1         29  
3 1     1   6 use POSIX ();
  1         3  
  1         17  
4 1     1   506 use Mojo::IOLoop;
  1         130032  
  1         7  
5 1     1   52 use Mojo::IOLoop::Delay;
  1         2  
  1         9  
6             $Mojo::Webqq::Client::CLIENT_COUNT = 0;
7 1     1   593 use Mojo::Webqq::Message::Handle;
  1         4  
  1         33  
8 1     1   587 use Mojo::Webqq::Client::Remote::_prepare_for_login;
  1         3  
  1         34  
9 1     1   774 use Mojo::Webqq::Client::Remote::_check_verify_code;
  1         2  
  1         33  
10 1     1   399 use Mojo::Webqq::Client::Remote::_get_img_verify_code;
  1         4  
  1         30  
11 1     1   663 use Mojo::Webqq::Client::Remote::_check_login;
  1         3  
  1         32  
12 1     1   420 use Mojo::Webqq::Client::Remote::_get_qrlogin_pic;
  1         2  
  1         30  
13 1     1   1064 use Mojo::Webqq::Client::Remote::_login1;
  1         5  
  1         34  
14 1     1   450 use Mojo::Webqq::Client::Remote::_check_sig;
  1         3  
  1         35  
15 1     1   609 use Mojo::Webqq::Client::Remote::_login2;
  1         3  
  1         34  
16 1     1   541 use Mojo::Webqq::Client::Remote::_get_vfwebqq;
  1         3  
  1         35  
17 1     1   517 use Mojo::Webqq::Client::Remote::_cookie_proxy;
  1         3  
  1         32  
18 1     1   500 use Mojo::Webqq::Client::Remote::change_state;
  1         3  
  1         31  
19 1     1   409 use Mojo::Webqq::Client::Remote::_get_offpic;
  1         3  
  1         30  
20 1     1   418 use Mojo::Webqq::Client::Remote::_get_group_pic;
  1         3  
  1         29  
21 1     1   617 use Mojo::Webqq::Client::Remote::_recv_message;
  1         3  
  1         34  
22 1     1   739 use Mojo::Webqq::Client::Remote::_relink;
  1         3  
  1         33  
23 1     1   503 use Mojo::Webqq::Client::Remote::logout;
  1         2  
  1         3475  
24              
25             sub run{
26 0     0 0   my $self = shift;
27 0 0         $self->ready() if not $self->is_ready;
28 0           $self->emit("run");
29 0 0         $self->ioloop->start unless $self->ioloop->is_running;
30             }
31              
32             sub steps {
33 0     0 0   my $self = shift;
34             Mojo::IOLoop::Delay->new(ioloop=>$self->ioloop)->steps(@_)->catch(sub {
35 0     0     my ($delay, $err) = @_;
36 0           $self->error("steps error: $err");
37 0           })->wait;
38 0           $self;
39             }
40             sub stop{
41 0     0 0   my $self = shift;
42 0 0         return if $self->is_stop;
43 0           $self->is_stop(1);
44 0           $self->state('stop');
45 0           $self->emit("stop");
46 0           $self->info("客户端停止运行");
47 0           CORE::exit;
48             }
49             sub ready{
50 0     0 0   my $self = shift;
51 0           $self->state('loading');
52             #加载插件
53 0           my $plugins = $self->plugins;
54 0           for(
55 0           sort {$plugins->{$b}{priority} <=> $plugins->{$a}{priority} }
56 0 0         grep {defined $plugins->{$_}{auto_call} and $plugins->{$_}{auto_call} == 1} keys %{$plugins}
  0            
57             ){
58 0           $self->call($_);
59             }
60 0           $self->state('loading');
61 0           $self->emit("after_load_plugin");
62 0 0         $self->login() if $self->login_state ne 'success';
63 0 0         $self->relogin() if $self->get_model_status() == 0;
64              
65             $self->interval($self->update_interval || 600,sub{
66 0 0   0     return if $self->is_stop;
67 0 0         return if not $self->is_update_group;
68 0           $self->update_group(is_blocking=>0,is_update_group_ext=>0,is_update_group_member=>$self->is_update_group_member,is_update_group_member_ext=>$self->is_update_group_member_ext);
69 0   0       });
70              
71             $self->timer(60,sub{
72             $self->interval($self->update_interval || 600,sub{
73 0 0         return if $self->is_stop;
74 0 0         return if not $self->is_update_discuss;
75 0           $self->update_discuss(is_blocking=>0,is_update_discuss_member=>1);
76 0   0 0     });
77 0           });
78              
79             $self->timer(60+60,sub{
80             $self->interval($self->update_interval || 600,sub{
81 0 0         return if $self->is_stop;
82 0 0         return if not $self->is_update_friend;
83 0           $self->update_friend(is_blocking=>0,is_update_friend_ext=>0);
84 0   0 0     });
85 0           });
86              
87             $self->timer(60+60+60,sub{
88             $self->interval($self->update_interval || 600,sub{
89 0 0         return if $self->is_stop;
90 0 0         return if not $self->is_update_user;
91 0           $self->update_user(is_blocking=>0);
92 0   0 0     });
93 0           });
94              
95             #接收消息
96 0     0     $self->on(poll_over=>sub{ $self->state('running');my $self = $_[0];$self->timer(1,sub{$self->_recv_message()}) } );
  0            
  0            
  0            
  0            
97             $self->on(run=>sub{
98 0     0     my $self = $_[0];
99 0           $self->info("开始接收消息...");
100 0           $self->state('running');
101 0           $self->_recv_message();
102 0           });
103 0           $self->is_ready(1);
104 0           $self->emit("ready");
105 0           return $self;
106             }
107              
108             sub timer {
109 0     0 0   my $self = shift;
110 0           return $self->ioloop->timer(@_);
111             }
112             sub interval{
113 0     0 0   my $self = shift;
114 0           return $self->ioloop->recurring(@_);
115             }
116             sub relogin{
117 0     0 0   my $self = shift;
118 0           $self->info("正在重新登录...\n");
119 0 0         if(defined $self->poll_connection_id){
120 0           eval{
121 0           $self->ioloop->remove($self->poll_connection_id);
122 0           $self->is_polling(0);
123 0           $self->info("停止接收消息...");
124             };
125 0 0         $self->info("停止接收消息失败: $@") if $@;
126             }
127 0           $self->logout();
128 0           $self->login_state("relogin");
129 0           $self->sess_sig_cache(Mojo::Webqq::Cache->new);
130 0           $self->id_to_qq_cache(Mojo::Webqq::Cache->new);
131             #$self->clear_cookie();
132 0           $self->poll_failure_count(0);
133 0           $self->send_failure_count(0);
134 0           $self->qrcode_count(0);
135 0           $self->csrf_token(undef);
136 0           $self->model_ext(0);
137              
138 0           $self->user(+{});
139 0           $self->friend([]);
140 0           $self->group([]);
141 0           $self->discuss([]);
142 0           $self->model_status(+{});
143              
144 0           $self->login(delay=>0);
145 0           $self->info("重新开始接收消息...");
146 0           $self->_recv_message();
147 0           $self->emit("relogin");
148             }
149             sub relink {
150 0     0 0   my $self = shift;
151 0           $self->info("尝试进行重新连接(1)...");
152 0 0 0       if($self->_get_vfwebqq() && $self->_login2()){
153 0           $self->info("重新连接(1)成功");
154             }
155             else{
156 0           $self->info("重新连接(1)失败");
157 0           $self->relogin();
158             }
159             }
160             sub login {
161 0     0 0   my $self = shift;
162 0 0         return if $self->login_state eq 'success';
163 0           my %p = @_;
164 0           my $is_scan = 0;
165 0 0         my $delay = defined $p{delay}?$p{delay}:0;
166 0 0         if($self->is_first_login == -1){
    0          
167 0           $self->is_first_login(1);
168             }
169             elsif($self->is_first_login == 1){
170 0           $self->is_first_login(0);
171             }
172 0 0         if($self->is_first_login){
173             #$self->load_cookie(); #转移到new的时候就调用,这里不再需要
174 0           my $ptwebqq = $self->search_cookie("ptwebqq");
175 0           my $skey = $self->search_cookie("skey");
176 0 0         $self->ptwebqq($ptwebqq) if defined $ptwebqq;
177 0 0         $self->skey($skey) if defined $skey;
178             }
179            
180 0 0 0       if($self->_prepare_for_login() && $self->_check_login()){
181 0 0 0       if(not $self->_check_sig() && $self->_get_vfwebqq() && $self->_login2()){
      0        
182 0           $self->relogin();
183 0           return;
184             }
185 0           $is_scan = 0;
186             }
187             else{
188 0 0         if($self->login_type eq 'login'){
189 0           $is_scan = 0;
190 0   0       my $ret = $self->model_ext_authorize()
191             && $self->_prepare_for_login()
192             && $self->_check_login()
193             && $self->_check_sig()
194             && $self->_get_vfwebqq()
195             && $self->_login2();
196 0 0         if($ret == 1){
197 0           $self->info("账号密码方式登录成功");
198 0           $self->uid($self->account);
199 0           $self->ptwebqq($self->search_cookie('ptwebqq'));
200             }
201             else{
202 0           $self->warn("账号密码登录方式失败,尝试使用二维码登录");
203 0           $self->login_type('qrlogin');
204             }
205             }
206 0 0 0       if(
      0        
      0        
207             $self->login_type eq 'qrlogin'
208             && $self->_check_verify_code()
209             && $self->_get_img_verify_code()
210             && $self->_get_qrlogin_pic()
211              
212             ){
213 0           while(1){
214 0           $self->check_controller();
215 0           my $ret = $self->_login1();
216             #if($ret == -1){#验证码输入错误
217             # $self->_get_img_verify_code();
218             # next;
219             #}
220             #if($ret == -2){#帐号或密码错误
221             # $self->error("登录失败,尝试更换加密算法计算方式,重新登录...");
222             # $self->encrypt_method("js");
223             # $self->relogin();
224             # return;
225             #}
226 0 0 0       if($ret == -4 ){#等待二维码扫描
    0          
    0          
    0          
227 0           sleep 3;
228 0           next;
229             }
230             elsif($ret == -5 ){#二维码已经扫描 等待手机端进行授权登录
231 0           sleep 3;
232 0           next;
233             }
234             elsif($ret == -3 or $ret == -6){#二维码已经过期,或临时切换到二维码登录方式,重新下载二维码
235 0           $self->emit("qrcode_expire");
236 0           $self->_get_qrlogin_pic();
237 0           next;
238             }
239             elsif($ret == 1){#登录成功
240 0           $is_scan = 1;
241 0 0 0       $self->_check_sig()
242             && $self->_get_vfwebqq()
243             && $self->_login2();
244 0           last;
245             }
246             else{
247 0           last;
248             }
249             }
250             }
251             }
252              
253             #登录不成功,客户端退出运行
254 0 0         if($self->login_state ne 'success'){
255 0           $self->fatal("登录失败,客户端退出(可能网络不稳定,请多尝试几次)");
256 0           $self->stop();
257             }
258             else{
259 0           $self->qrcode_count(0);
260 0   0       $self->info("帐号(" .( $self->uid // $self->account) . ")登录成功");
261 0 0         $self->login_type eq "qrlogin"?$self->clean_qrcode():$self->clean_verifycode();
262             #$self->model_ext_authorize() if $self->login_type eq 'qrlogin' and not defined $self->model_ext;
263 0 0         $self->model_ext_authorize() if not defined $self->model_ext;
264 0           $self->state('updating');
265 0           $self->update_user;
266 0 0         $self->update_friend(is_blocking=>1,is_update_friend_ext=>1) if $self->is_init_friend;
267 0 0         $self->update_group(is_blocking=>1,is_update_group_ext=>1,is_update_group_member_ext=>0,is_update_group_member=>0) if $self->is_init_group;
268 0 0         $self->update_discuss(is_blocking=>1,is_update_discuss_member=>0) if $self->is_init_discuss;
269 0           $self->emit("login",$is_scan);
270             }
271 0           return $self;
272             }
273              
274             sub mail{
275 0     0 0   my $self = shift;
276 0           my $callback ;
277 0           my $is_blocking = 1;
278 0 0         if(ref $_[-1] eq "CODE"){
279 0           $callback = pop;
280 0           $is_blocking = 0;
281             }
282 0           my %opt = @_;
283             #smtp
284             #port
285             #tls
286             #tls_ca
287             #tls_cert
288             #tls_key
289             #user
290             #pass
291             #from
292             #to
293             #cc
294             #subject
295             #charset
296             #html
297             #text
298             #data MIME::Lite产生的发送数据
299 0           eval{ require Mojo::SMTP::Client; } ;
  0            
300 0 0         if($@){
301 0           $self->error("发送邮件,请先安装模块 Mojo::SMTP::Client");
302 0           return;
303             }
304             my %new = (
305             address => $opt{smtp},
306 0   0       port => $opt{port} || 25,
307             autodie => $is_blocking,
308             );
309 0           for(qw(tls tls_ca tls_cert tls_key)){
310 0 0         $new{$_} = $opt{$_} if defined $opt{$_};
311             }
312 0 0 0       $new{tls} = 1 if($new{port} == 465 and !defined $new{tls});
313 0           my $smtp = Mojo::SMTP::Client->new(%new);
314 0 0         unless(defined $smtp){
315 0           $self->error("Mojo::SMTP::Client客户端初始化失败");
316 0           return;
317             }
318 0           my $data;
319 0 0         if(defined $opt{data}){$data = $opt{data}}
  0            
320             else{
321 0           my @data;
322 0           push @data,("From: $opt{from}","To: $opt{to}");
323 0 0         push @data,"Cc: $opt{cc}" if defined $opt{cc};
324 0           require MIME::Base64;
325 0 0         my $charset = defined $opt{charset}?$opt{charset}:"UTF-8";
326 0           push @data,"Subject: =?$charset?B?" . MIME::Base64::encode_base64($opt{subject},"") . "?=";
327 0 0         if(defined $opt{text}){
    0          
328 0           push @data,("Content-Type: text/plain; charset=$charset",'',$opt{text});
329             }
330             elsif(defined $opt{html}){
331 0           push @data,("Content-Type: text/html; charset=$charset",'',$opt{html});
332             }
333 0           $data = join "\r\n",@data;
334             }
335 0 0         if(defined $callback){#non-blocking send
336             $smtp->send(
337             auth => {login=>$opt{user},password=>$opt{pass}},
338             from => $opt{from},
339             to => $opt{to},
340             data => $data,
341             quit => 1,
342             sub{
343 0     0     my ($smtp, $resp) = @_;
344 0 0         if($resp->error){
345 0           $self->error("邮件[ To: $opt{to}|Subject: $opt{subject} ]发送失败: " . $resp->error );
346 0 0         $callback->(0,$resp->error) if ref $callback eq "CODE";
347 0           return;
348             }
349             else{
350 0           $self->debug("邮件[ To: $opt{to}|Subject: $opt{subject} ]发送成功");
351 0 0         $callback->(1) if ref $callback eq "CODE";
352             }
353             },
354 0           );
355             }
356             else{#blocking send
357 0           eval{
358             $smtp->send(
359             auth => {login=>$opt{user},password=>$opt{pass}},
360             from => $opt{from},
361             to => $opt{to},
362 0           data => $data,
363             quit => 1,
364             );
365             };
366 0 0         return $@?(0,$@):(1,);
367             }
368            
369             }
370              
371             sub spawn {
372 0     0 0   my $self = shift;
373 0           my %opt = @_;
374 0           require Mojo::Webqq::Run;
375 0           my $is_blocking = delete $opt{is_blocking};
376 0 0         my $run = Mojo::Webqq::Run->new(ioloop=>($is_blocking?Mojo::IOLoop->new:$self->ioloop),log=>$self->log);
377 0 0         $run->max_forks(delete $opt{max_forks}) if defined $opt{max_forks};
378 0           $run->spawn(%opt);
379 0 0         $run->start if $is_blocking;
380 0           $run;
381             }
382             sub clean_qrcode{
383 0     0 0   my $self = shift;
384 0 0         return if not defined $self->qrcode_path;
385 0 0         return if not -f $self->qrcode_path;
386 0           $self->info("清除残留的历史二维码图片");
387 0 0         unlink $self->qrcode_path or $self->warn("删除二维码图片[ " . $self->qrcode_path . " ]失败: $!");
388             }
389             sub clean_verifycode{
390 0     0 0   my $self = shift;
391 0 0         return if not defined $self->verifycode_path;
392 0 0         return if not -f $self->verifycode_path;
393 0           $self->info("清除残留的历史验证码图片");
394 0 0         unlink $self->verifycode_path or $self->warn("删除验证码图片[ ". $self->verifycode_path . " ]失败: $!");
395             }
396              
397             sub add_job {
398 0     0 0   my $self = shift;
399 0           require Mojo::Webqq::Client::Cron;
400 0           $self->Mojo::Webqq::Client::Cron::add_job(@_);
401             }
402              
403             sub check_pid {
404 0     0 0   my $self = shift;
405 0 0         return if not $self->pid_path;
406 0           eval{
407 0 0         if(not -f $self->pid_path){
408 0           $self->spurt($$,$self->pid_path);
409             }
410             else{
411 0           my $pid = $self->slurp($self->pid_path);
412 0 0 0       if( $pid=~/^\d+$/ and kill(0, $pid) ){
413 0           $self->warn("检测到该账号有其他运行中的客户端(pid:$pid), 请先将其关闭");
414 0           $self->stop();
415             }
416             else{
417 0           $self->spurt($$,$self->pid_path);
418             }
419             }
420             };
421 0 0         $self->warn("进程检测遇到异常: $@") if $@;
422            
423             }
424              
425              
426             sub clean_pid {
427 0     0 0   my $self = shift;
428 0 0         return if not defined $self->pid_path;
429 0 0         return if not -f $self->pid_path;
430 0           $self->info("清除残留的pid文件");
431 0 0         unlink $self->pid_path or $self->warn("删除pid文件[ " . $self->pid_path . " ]失败: $!");
432             }
433              
434             sub save_state{
435 0     0 0   my $self = shift;
436 0           my($previous_state,$current_state) = @_;
437 0           my @attr = qw(
438             account
439             version
440             start_time
441             mode
442             http_debug
443             log_encoding
444             log_path
445             log_level
446             log_console
447             disable_color
448             tmpdir
449             cookie_path
450             qrcode_path
451             pid_path
452             state_path
453             keep_cookie
454             ua_retry_times
455             qrcode_count_max
456             state
457             );
458             # pid
459             # os
460 0           eval{
461 0           my $json = {plugin => []};
462 0           for my $attr (@attr){
463 0           $json->{$attr} = $self->$attr;
464             }
465 0           $json->{previous_state} = $previous_state;
466 0           $json->{pid} = $$;
467 0           $json->{os} = $^O;
468 0           for my $p (keys %{ $self->plugins }){
  0            
469 0           push @{ $json->{plugin} } , { name=>$self->plugins->{$p}{name},priority=>$self->plugins->{$p}{priority},auto_call=>$self->plugins->{$p}{auto_call},call_on_load=>$self->plugins->{$p}{call_on_load} } ;
  0            
470             }
471 0           $self->spurt($self->to_json($json),$self->state_path);
472             };
473 0 0         $self->warn("客户端状态信息保存失败:$@") if $@;
474             }
475              
476             sub is_load_plugin {
477 0     0 0   my $self = shift;
478 0           my $plugin = shift;
479 0 0         if(substr($plugin,0,1) eq '+'){
480 0           substr($plugin,0,1) = "";
481             }
482             else{
483 0           $plugin = "Mojo::Webqq::Plugin::$plugin";
484             }
485 0           return exists $self->plugins->{$plugin};
486             }
487              
488             sub check_controller {
489 0     0 0   my $self = shift;
490 0           my $once = shift;
491 0 0 0       if($^O ne 'MSWin32' and defined $self->controller_pid ){
492 0 0         if($once){
493 0           $self->info("启用Controller[". $self->controller_pid ."]状态检查");
494             $self->interval(5=>sub{
495 0     0     $self->check_controller();
496 0           });
497             }
498             else{
499 0           my $ppid = POSIX::getppid();
500 0 0 0       if( $ppid=~/^\d+$/ and $ppid == 1 or $ppid != $self->controller_pid ) {
      0        
501 0           $self->warn("检测到脱离Controller进程管理,程序即将终止");
502 0           $self->stop();
503             }
504             }
505             }
506             }
507              
508             sub check_notice {
509 0     0 0   my $self = shift;
510 0 0         return if not $self->is_fetch_notice;
511 0           $self->info("获取最新公告信息...");
512 0           my $notice = $self->http_get($self->notice_api);
513 0 0         if($notice){
514 0           $self->info("-" x 40);
515 0           $self->info({content_color=>'green'},$notice);
516 0           $self->info("-" x 40);
517             }
518             }
519             1;