File Coverage

blib/lib/Mojo/Weixin/Client.pm
Criterion Covered Total %
statement 36 264 13.6
branch 0 102 0.0
condition 0 23 0.0
subroutine 12 40 30.0
pod 0 21 0.0
total 48 450 10.6


line stmt bran cond sub pod time code
1             package Mojo::Weixin::Client;
2 1     1   8 use POSIX ();
  1         2  
  1         21  
3 1     1   565 use Mojo::Weixin::Client::Remote::_login;
  1         2  
  1         26  
4 1     1   459 use Mojo::Weixin::Client::Remote::_logout;
  1         3  
  1         27  
5 1     1   453 use Mojo::Weixin::Client::Remote::_get_qrcode_uuid;
  1         2  
  1         26  
6 1     1   375 use Mojo::Weixin::Client::Remote::_get_qrcode_image;
  1         3  
  1         23  
7 1     1   439 use Mojo::Weixin::Client::Remote::_is_need_login;
  1         2  
  1         26  
8 1     1   579 use Mojo::Weixin::Client::Remote::_synccheck;
  1         3  
  1         27  
9 1     1   550 use Mojo::Weixin::Client::Remote::_sync;
  1         3  
  1         27  
10 1     1   406 use Mojo::Weixin::Message::Handle;
  1         3  
  1         30  
11 1     1   7 use Mojo::IOLoop;
  1         2  
  1         9  
12 1     1   29 use Mojo::IOLoop::Delay;
  1         2  
  1         15  
13              
14 1     1   27 use base qw(Mojo::Weixin::Request);
  1         2  
  1         438  
15              
16             sub login{
17 0     0 0   my $self = shift;
18 0 0         return 1 if $self->login_state eq 'success';
19 0 0         if($self->is_first_login == -1){
    0          
20 0           $self->is_first_login(1);
21             }
22             elsif($self->is_first_login == 1){
23 0           $self->is_first_login(0);
24             }
25              
26             #if($self->is_first_login){
27             # #$self->load_cookie();#转移到new的时候就调用,这里不再需要
28             #}
29 0           while(1){
30 0           $self->check_controller();
31 0           my $ret = $self->_login();
32 0           $self->clean_qrcode();
33 0           sleep 2;
34 0 0 0       if($ret and $self->login_state eq "success" and $self->model_init()){
      0        
35 0 0         $self->emit("login"=>($ret==2?1:0));
36 0           return 1;
37             }
38             else{
39 0           $self->logout();
40 0           $self->login_state("init");
41 0           $self->error("登录结果异常,再次尝试...");
42 0           next;
43             }
44             }
45             }
46             sub relogin{
47 0     0 0   my $self = shift;
48 0           my $retcode = shift;
49 0           $self->info("正在重新登录...\n");
50 0 0         if(defined $self->_synccheck_connection_id){
51 0           eval{
52 0           $self->ioloop->remove($self->_synccheck_connection_id);
53 0           $self->_synccheck_running(0);
54 0           $self->info("停止接收消息...");
55             };
56 0 0         $self->info("停止接收消息失败: $@") if $@;
57             }
58 0           $self->logout($retcode);
59 0           $self->login_state("relogin");
60             #$self->clear_cookie();
61              
62 0           $self->sync_key(+{LIST=>[]});
63 0           $self->synccheck_key(undef);
64 0           $self->pass_ticket('');
65 0           $self->skey('');
66 0           $self->wxsid('');
67 0           $self->wxuin('');
68              
69 0           $self->user(+{});
70 0           $self->friend([]);
71 0           $self->group([]);
72 0           $self->data(+{});
73              
74 0           $self->login();
75             $self->timer(2,sub{
76 0     0     $self->info("重新开始接收消息...");
77 0           $self->_synccheck();
78 0           });
79 0           $self->emit("relogin");
80             }
81             sub logout{
82 0     0 0   my $self = shift;
83 0           my $retcode = shift;
84             #my %type = qw(
85             # 1100 0
86             # 1101 1
87             # 1102 1
88             # 1205 1
89             #);
90 0 0         $self->info("客户端正在注销". (defined $retcode?"($retcode)":"") . "...");
91 0           $self->_logout(0);
92 0           $self->_logout(1);
93             }
94             sub steps {
95 0     0 0   my $self = shift;
96             Mojo::IOLoop::Delay->new(ioloop=>$self->ioloop)->steps(@_)->catch(sub {
97 0     0     my ($delay, $err) = @_;
98 0           $self->error("steps error: $err");
99 0           })->wait;
100 0           $self;
101             }
102             sub ready {
103 0     0 0   my $self = shift;
104 0           $self->state('loading');
105             #加载插件
106 0           my $plugins = $self->plugins;
107 0           for(
108 0           sort {$plugins->{$b}{priority} <=> $plugins->{$a}{priority} }
109 0 0         grep {defined $plugins->{$_}{auto_call} and $plugins->{$_}{auto_call} == 1} keys %{$plugins}
  0            
110             ){
111 0           $self->call($_);
112             }
113 0           $self->state('loading');
114 0           $self->emit("after_load_plugin");
115 0 0         $self->login() if $self->login_state ne 'success';
116             #接收消息
117             $self->on(synccheck_over=>sub{
118 0     0     my $self = shift;
119 0           $self->state('running');
120 0           my($retcode,$selector,$status) = @_;
121 0 0         if(not $status){#检查消息异常时,强制把检查消息(synccheck)间隔设置的更久,直到获取消息(sync)正常为止
122 0           $self->debug("检查消息结果异常");
123 0           $self->_synccheck_interval($self->synccheck_interval+$self->synccheck_delay);
124             }
125 0           $self->_parse_synccheck_data($retcode,$selector);
126 0           $self->timer($self->_synccheck_interval, sub{$self->_synccheck()});
  0            
127 0           });
128             $self->on(sync_over=>sub{
129 0     0     my $self = shift;
130 0           my ($json,$status) = @_;
131 0 0         $self->_synccheck_interval($status?$self->synccheck_interval:$self->synccheck_interval+$self->synccheck_delay);
132 0           $self->_parse_sync_data($json);
133 0           });
134             $self->on(run=>sub{
135 0     0     my $self = shift;
136             $self->timer(2,sub{
137 0           $self->info("开始接收消息...");
138 0           $self->state('running');
139 0           $self->_synccheck()}
140 0           );
141 0           });
142 0           $self->is_ready(1);
143 0           $self->emit("ready");
144 0           return $self;
145             }
146             sub run{
147 0     0 0   my $self = shift;
148 0 0         $self->ready() if not $self->is_ready;
149 0           $self->emit("run");
150 0 0         $self->ioloop->start unless $self->ioloop->is_running;
151             }
152              
153             sub multi_run{
154 0 0   0 0   Mojo::IOLoop->singleton->start unless Mojo::IOLoop->singleton->is_running;
155             }
156              
157             sub clean_qrcode{
158 0     0 0   my $self = shift;
159 0 0         return if not defined $self->qrcode_path;
160 0 0         return if not -f $self->qrcode_path;
161 0           $self->info("清除残留的历史二维码图片");
162 0 0         unlink $self->qrcode_path or $self->warn("删除二维码图片[ " . $self->qrcode_path . " ]失败: $!");
163             }
164              
165             sub timer {
166 0     0 0   my $self = shift;
167 0           return $self->ioloop->timer(@_);
168             }
169             sub interval{
170 0     0 0   my $self = shift;
171 0           return $self->ioloop->recurring(@_);
172             }
173              
174             sub exit{
175 0     0 0   my $self = shift;
176 0           my $code = shift;
177 0           $self->state('stop');
178 0           $self->emit("stop");
179 0           $self->info("客户端已退出");
180 0 0         CORE::exit(defined $code?$code+0:0);
181             }
182             sub stop{
183 0     0 0   my $self = shift;
184 0           $self->is_stop(1);
185 0           $self->state('stop');
186 0           $self->emit("stop");
187 0           $self->info("客户端停止运行");
188 0           CORE::exit();
189             }
190              
191             sub spawn {
192 0     0 0   my $self = shift;
193 0           my %opt = @_;
194 0           require Mojo::Weixin::Run;
195 0           my $is_blocking = delete $opt{is_blocking};
196 0 0         my $run = Mojo::Weixin::Run->new(ioloop=>($is_blocking?Mojo::IOLoop->new:$self->ioloop),log=>$self->log);
197 0 0         $run->max_forks(delete $opt{max_forks}) if defined $opt{max_forks};
198 0           $run->spawn(%opt);
199 0 0         $run->start if $is_blocking;
200 0           $run;
201             }
202              
203             sub mail{
204 0     0 0   my $self = shift;
205 0           my $callback ;
206 0           my $is_blocking = 1;
207 0 0         if(ref $_[-1] eq "CODE"){
208 0           $callback = pop;
209 0           $is_blocking = 0;
210             }
211 0           my %opt = @_;
212             #smtp
213             #port
214             #tls
215             #tls_ca
216             #tls_cert
217             #tls_key
218             #user
219             #pass
220             #from
221             #to
222             #cc
223             #subject
224             #charset
225             #html
226             #text
227             #data MIME::Lite产生的发送数据
228 0           eval{ require Mojo::SMTP::Client; } ;
  0            
229 0 0         if($@){
230 0           $self->error("发送邮件,请先安装模块 Mojo::SMTP::Client");
231 0           return;
232             }
233             my %new = (
234             address => $opt{smtp},
235 0   0       port => $opt{port} || 25,
236             autodie => $is_blocking,
237             );
238 0           for(qw(tls tls_ca tls_cert tls_key)){
239 0 0         $new{$_} = $opt{$_} if defined $opt{$_};
240             }
241 0 0 0       $new{tls} = 1 if($new{port} == 465 and !defined $new{tls});
242 0           my $smtp = Mojo::SMTP::Client->new(%new);
243 0 0         unless(defined $smtp){
244 0           $self->error("Mojo::SMTP::Client客户端初始化失败");
245 0           return;
246             }
247 0           my $data;
248 0 0         if(defined $opt{data}){$data = $opt{data}}
  0            
249             else{
250 0           my @data;
251 0           push @data,("From: $opt{from}","To: $opt{to}");
252 0 0         push @data,"Cc: $opt{cc}" if defined $opt{cc};
253 0           require MIME::Base64;
254 0 0         my $charset = defined $opt{charset}?$opt{charset}:"UTF-8";
255 0           push @data,"Subject: =?$charset?B?" . MIME::Base64::encode_base64($opt{subject},"") . "?=";
256 0 0         if(defined $opt{text}){
    0          
257 0           push @data,("Content-Type: text/plain; charset=$charset",'',$opt{text});
258             }
259             elsif(defined $opt{html}){
260 0           push @data,("Content-Type: text/html; charset=$charset",'',$opt{html});
261             }
262 0           $data = join "\r\n",@data;
263             }
264 0 0         if(defined $callback){#non-blocking send
265             $smtp->send(
266             auth => {login=>$opt{user},password=>$opt{pass}},
267             from => $opt{from},
268             to => $opt{to},
269             data => $data,
270             quit => 1,
271             sub{
272 0     0     my ($smtp, $resp) = @_;
273 0 0         if($resp->error){
274 0           $self->error("邮件[ To: $opt{to}|Subject: $opt{subject} ]发送失败: " . $resp->error );
275 0 0         $callback->(0,$resp->error) if ref $callback eq "CODE";
276 0           return;
277             }
278             else{
279 0           $self->debug("邮件[ To: $opt{to}|Subject: $opt{subject} ]发送成功");
280 0 0         $callback->(1) if ref $callback eq "CODE";
281             }
282             },
283 0           );
284             }
285             else{#blocking send
286 0           eval{
287             $smtp->send(
288             auth => {login=>$opt{user},password=>$opt{pass}},
289             from => $opt{from},
290             to => $opt{to},
291 0           data => $data,
292             quit => 1,
293             );
294             };
295 0 0         return $@?(0,$@):(1,);
296             }
297              
298             }
299             sub add_job {
300 0     0 0   my $self = shift;
301 0           require Mojo::Weixin::Client::Cron;
302 0           $self->Mojo::Weixin::Client::Cron::add_job(@_);
303             }
304              
305             sub check_pid {
306 0     0 0   my $self = shift;
307 0 0         return if not $self->pid_path;
308 0           eval{
309 0 0         if(not -f $self->pid_path){
310 0           $self->spurt($$,$self->pid_path);
311             }
312             else{
313 0           my $pid = $self->slurp($self->pid_path);
314 0 0 0       if( $pid=~/^\d+$/ and kill(0, $pid) ){
315             # my $p;
316             #if($^O eq 'MSWin32' and Win32::Process::Open($p,$pid,0)){
317             # $self->warn("检测到该账号有其他运行中的客户端(pid:$pid), 请先将其关闭");
318             # $self->stop();
319             #}
320 0           $self->warn("检测到该账号有其他运行中的客户端(pid:$pid), 请先将其关闭");
321 0           $self->stop();
322             }
323             else{
324 0           $self->spurt($$,$self->pid_path);
325             }
326             }
327             };
328 0 0         $self->warn("进程检测遇到异常: $@") if $@;
329            
330             }
331              
332              
333             sub clean_pid {
334 0     0 0   my $self = shift;
335 0 0         return if not defined $self->pid_path;
336 0 0         return if not -f $self->pid_path;
337 0           $self->info("清除残留的pid文件");
338 0 0         unlink $self->pid_path or $self->warn("删除pid文件[ " . $self->pid_path . " ]失败: $!");
339             }
340             sub save_state{
341 0     0 0   my $self = shift;
342 0           my($previous_state,$current_state) = @_;
343 0           my @attr = qw(
344             account
345             version
346             start_time
347             http_debug
348             log_encoding
349             log_path
350             log_level
351             log_console
352             disable_color
353             download_media
354             tmpdir
355             media_dir
356             cookie_path
357             qrcode_path
358             pid_path
359             state_path
360             keep_cookie
361             fix_media_loop
362             synccheck_interval
363             emoji_to_text
364             stop_with_mobile
365             ua_retry_times
366             qrcode_count_max
367             state
368             );
369             # pid
370             # os
371 0           eval{
372 0           my $json = {plugin => []};
373 0           for my $attr (@attr){
374 0           $json->{$attr} = $self->$attr;
375             }
376 0           $json->{previous_state} = $previous_state;
377 0           $json->{pid} = $$;
378 0           $json->{os} = $^O;
379 0           for my $p (keys %{ $self->plugins }){
  0            
380 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            
381             }
382 0           $self->spurt($self->to_json($json),$self->state_path);
383             };
384 0 0         $self->warn("客户端状态信息保存失败:$@") if $@;
385             }
386              
387             sub is_load_plugin {
388 0     0 0   my $self = shift;
389 0           my $plugin = shift;
390 0 0         if(substr($plugin,0,1) eq '+'){
391 0           substr($plugin,0,1) = "";
392             }
393             else{
394 0           $plugin = "Mojo::Weixin::Plugin::$plugin";
395             }
396 0           return exists $self->plugins->{$plugin};
397             }
398              
399             sub check_controller {
400 0     0 0   my $self = shift;
401 0           my $once = shift;
402 0 0 0       if($^O ne 'MSWin32' and defined $self->controller_pid ){
403 0 0         if($once){
404 0           $self->info("启用Controller[". $self->controller_pid ."]状态检查");
405             $self->interval(5=>sub{
406 0     0     $self->check_controller();
407 0           });
408             }
409             else{
410 0           my $ppid = POSIX::getppid();
411 0 0 0       if( $ppid=~/^\d+$/ and $ppid == 1 or $ppid != $self->controller_pid ) {
      0        
412 0           $self->warn("检测到脱离Controller进程管理,程序即将终止");
413 0           $self->stop();
414             }
415             }
416             }
417             }
418              
419             sub check_notice {
420 0     0 0   my $self = shift;
421 0 0         return if not $self->is_fetch_notice;
422 0           $self->info("获取最新公告信息...");
423 0           my $notice = $self->http_get($self->notice_api);
424 0 0         if($notice){
425 0           $self->info("-" x 40);
426 0           $self->info({content_color=>'green'},$notice);
427 0           $self->info("-" x 40);
428             }
429             }
430              
431             1;