package Mojo::Webqq::Client;
use strict;
use POSIX ();
use Mojo::IOLoop;
use Mojo::IOLoop::Delay;
$Mojo::Webqq::Client::CLIENT_COUNT = 0;
use Mojo::Webqq::Message::Handle;
use Mojo::Webqq::Client::Remote::_prepare_for_login;
use Mojo::Webqq::Client::Remote::_check_verify_code;
use Mojo::Webqq::Client::Remote::_get_img_verify_code;
use Mojo::Webqq::Client::Remote::_check_login;
use Mojo::Webqq::Client::Remote::_get_qrlogin_pic;
use Mojo::Webqq::Client::Remote::_login1;
use Mojo::Webqq::Client::Remote::_check_sig;
use Mojo::Webqq::Client::Remote::_login2;
use Mojo::Webqq::Client::Remote::_get_vfwebqq;
use Mojo::Webqq::Client::Remote::_cookie_proxy;
use Mojo::Webqq::Client::Remote::change_state;
use Mojo::Webqq::Client::Remote::_get_offpic;
use Mojo::Webqq::Client::Remote::_get_group_pic;
use Mojo::Webqq::Client::Remote::_recv_message;
use Mojo::Webqq::Client::Remote::_relink;
use Mojo::Webqq::Client::Remote::logout;
sub run{
my $self = shift;
$self->ready() if not $self->is_ready;
$self->emit("run");
$self->ioloop->start unless $self->ioloop->is_running;
}
sub steps {
my $self = shift;
Mojo::IOLoop::Delay->new(ioloop=>$self->ioloop)->steps(@_)->catch(sub {
my ($delay, $err) = @_;
$self->error("steps error: $err");
})->wait;
$self;
}
sub stop{
my $self = shift;
return if $self->is_stop;
$self->is_stop(1);
$self->state('stop');
$self->emit("stop");
$self->info("客户端停止运行");
CORE::exit;
}
sub ready{
my $self = shift;
$self->state('loading');
#加载插件
my $plugins = $self->plugins;
for(
sort {$plugins->{$b}{priority} <=> $plugins->{$a}{priority} }
grep {defined $plugins->{$_}{auto_call} and $plugins->{$_}{auto_call} == 1} keys %{$plugins}
){
$self->call($_);
}
$self->state('loading');
$self->emit("after_load_plugin");
$self->login() if $self->login_state ne 'success';
$self->relogin() if $self->get_model_status() == 0;
$self->interval($self->update_interval || 600,sub{
return if $self->is_stop;
return if not $self->is_update_group;
$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);
});
$self->timer(60,sub{
$self->interval($self->update_interval || 600,sub{
return if $self->is_stop;
return if not $self->is_update_discuss;
$self->update_discuss(is_blocking=>0,is_update_discuss_member=>1);
});
});
$self->timer(60+60,sub{
$self->interval($self->update_interval || 600,sub{
return if $self->is_stop;
return if not $self->is_update_friend;
$self->update_friend(is_blocking=>0,is_update_friend_ext=>0);
});
});
$self->timer(60+60+60,sub{
$self->interval($self->update_interval || 600,sub{
return if $self->is_stop;
return if not $self->is_update_user;
$self->update_user(is_blocking=>0);
});
});
#接收消息
$self->on(poll_over=>sub{ $self->state('running');my $self = $_[0];$self->timer(1,sub{$self->_recv_message()}) } );
$self->on(run=>sub{
my $self = $_[0];
$self->info("开始接收消息...");
$self->state('running');
$self->_recv_message();
});
$self->is_ready(1);
$self->emit("ready");
return $self;
}
sub timer {
my $self = shift;
return $self->ioloop->timer(@_);
}
sub interval{
my $self = shift;
return $self->ioloop->recurring(@_);
}
sub relogin{
my $self = shift;
$self->info("正在重新登录...\n");
if(defined $self->poll_connection_id){
eval{
$self->ioloop->remove($self->poll_connection_id);
$self->is_polling(0);
$self->info("停止接收消息...");
};
$self->info("停止接收消息失败: $@") if $@;
}
$self->logout();
$self->login_state("relogin");
$self->sess_sig_cache(Mojo::Webqq::Cache->new);
$self->id_to_qq_cache(Mojo::Webqq::Cache->new);
#$self->clear_cookie();
$self->poll_failure_count(0);
$self->send_failure_count(0);
$self->qrcode_count(0);
$self->csrf_token(undef);
$self->model_ext(0);
$self->user(+{});
$self->friend([]);
$self->group([]);
$self->discuss([]);
$self->model_status(+{});
$self->login(delay=>0);
$self->info("重新开始接收消息...");
$self->_recv_message();
$self->emit("relogin");
}
sub relink {
my $self = shift;
$self->info("尝试进行重新连接(1)...");
if($self->_get_vfwebqq() && $self->_login2()){
$self->info("重新连接(1)成功");
}
else{
$self->info("重新连接(1)失败");
$self->relogin();
}
}
sub login {
my $self = shift;
return if $self->login_state eq 'success';
my %p = @_;
my $is_scan = 0;
my $delay = defined $p{delay}?$p{delay}:0;
if($self->is_first_login == -1){
$self->is_first_login(1);
}
elsif($self->is_first_login == 1){
$self->is_first_login(0);
}
if($self->is_first_login){
#$self->load_cookie(); #转移到new的时候就调用,这里不再需要
my $ptwebqq = $self->search_cookie("ptwebqq");
my $skey = $self->search_cookie("skey");
$self->ptwebqq($ptwebqq) if defined $ptwebqq;
$self->skey($skey) if defined $skey;
}
if($self->_prepare_for_login() && $self->_check_login()){
if(not $self->_check_sig() && $self->_get_vfwebqq() && $self->_login2()){
$self->relogin();
return;
}
$is_scan = 0;
}
else{
if($self->login_type eq 'login'){
$is_scan = 0;
my $ret = $self->model_ext_authorize()
&& $self->_prepare_for_login()
&& $self->_check_login()
&& $self->_check_sig()
&& $self->_get_vfwebqq()
&& $self->_login2();
if($ret == 1){
$self->info("账号密码方式登录成功");
$self->uid($self->account);
$self->ptwebqq($self->search_cookie('ptwebqq'));
}
else{
$self->warn("账号密码登录方式失败,尝试使用二维码登录");
$self->login_type('qrlogin');
}
}
if(
$self->login_type eq 'qrlogin'
&& $self->_check_verify_code()
&& $self->_get_img_verify_code()
&& $self->_get_qrlogin_pic()
){
while(1){
$self->check_controller();
my $ret = $self->_login1();
#if($ret == -1){#验证码输入错误
# $self->_get_img_verify_code();
# next;
#}
#if($ret == -2){#帐号或密码错误
# $self->error("登录失败,尝试更换加密算法计算方式,重新登录...");
# $self->encrypt_method("js");
# $self->relogin();
# return;
#}
if($ret == -4 ){#等待二维码扫描
sleep 3;
next;
}
elsif($ret == -5 ){#二维码已经扫描 等待手机端进行授权登录
sleep 3;
next;
}
elsif($ret == -3 or $ret == -6){#二维码已经过期,或临时切换到二维码登录方式,重新下载二维码
$self->emit("qrcode_expire");
$self->_get_qrlogin_pic();
next;
}
elsif($ret == 1){#登录成功
$is_scan = 1;
$self->_check_sig()
&& $self->_get_vfwebqq()
&& $self->_login2();
last;
}
else{
last;
}
}
}
}
#登录不成功,客户端退出运行
if($self->login_state ne 'success'){
$self->fatal("登录失败,客户端退出(可能网络不稳定,请多尝试几次)");
$self->stop();
}
else{
$self->qrcode_count(0);
$self->info("帐号(" .( $self->uid // $self->account) . ")登录成功");
$self->login_type eq "qrlogin"?$self->clean_qrcode():$self->clean_verifycode();
#$self->model_ext_authorize() if $self->login_type eq 'qrlogin' and not defined $self->model_ext;
$self->model_ext_authorize() if not defined $self->model_ext;
$self->state('updating');
$self->update_user;
$self->update_friend(is_blocking=>1,is_update_friend_ext=>1) if $self->is_init_friend;
$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;
$self->update_discuss(is_blocking=>1,is_update_discuss_member=>0) if $self->is_init_discuss;
$self->emit("login",$is_scan);
}
return $self;
}
sub mail{
my $self = shift;
my $callback ;
my $is_blocking = 1;
if(ref $_[-1] eq "CODE"){
$callback = pop;
$is_blocking = 0;
}
my %opt = @_;
#smtp
#port
#tls
#tls_ca
#tls_cert
#tls_key
#user
#pass
#from
#to
#cc
#subject
#charset
#html
#text
#data MIME::Lite产生的发送数据
eval{ require Mojo::SMTP::Client; } ;
if($@){
$self->error("发送邮件,请先安装模块 Mojo::SMTP::Client");
return;
}
my %new = (
address => $opt{smtp},
port => $opt{port} || 25,
autodie => $is_blocking,
);
for(qw(tls tls_ca tls_cert tls_key)){
$new{$_} = $opt{$_} if defined $opt{$_};
}
$new{tls} = 1 if($new{port} == 465 and !defined $new{tls});
my $smtp = Mojo::SMTP::Client->new(%new);
unless(defined $smtp){
$self->error("Mojo::SMTP::Client客户端初始化失败");
return;
}
my $data;
if(defined $opt{data}){$data = $opt{data}}
else{
my @data;
push @data,("From: $opt{from}","To: $opt{to}");
push @data,"Cc: $opt{cc}" if defined $opt{cc};
require MIME::Base64;
my $charset = defined $opt{charset}?$opt{charset}:"UTF-8";
push @data,"Subject: =?$charset?B?" . MIME::Base64::encode_base64($opt{subject},"") . "?=";
if(defined $opt{text}){
push @data,("Content-Type: text/plain; charset=$charset",'',$opt{text});
}
elsif(defined $opt{html}){
push @data,("Content-Type: text/html; charset=$charset",'',$opt{html});
}
$data = join "\r\n",@data;
}
if(defined $callback){#non-blocking send
$smtp->send(
auth => {login=>$opt{user},password=>$opt{pass}},
from => $opt{from},
to => $opt{to},
data => $data,
quit => 1,
sub{
my ($smtp, $resp) = @_;
if($resp->error){
$self->error("邮件[ To: $opt{to}|Subject: $opt{subject} ]发送失败: " . $resp->error );
$callback->(0,$resp->error) if ref $callback eq "CODE";
return;
}
else{
$self->debug("邮件[ To: $opt{to}|Subject: $opt{subject} ]发送成功");
$callback->(1) if ref $callback eq "CODE";
}
},
);
}
else{#blocking send
eval{
$smtp->send(
auth => {login=>$opt{user},password=>$opt{pass}},
from => $opt{from},
to => $opt{to},
data => $data,
quit => 1,
);
};
return $@?(0,$@):(1,);
}
}
sub spawn {
my $self = shift;
my %opt = @_;
require Mojo::Webqq::Run;
my $is_blocking = delete $opt{is_blocking};
my $run = Mojo::Webqq::Run->new(ioloop=>($is_blocking?Mojo::IOLoop->new:$self->ioloop),log=>$self->log);
$run->max_forks(delete $opt{max_forks}) if defined $opt{max_forks};
$run->spawn(%opt);
$run->start if $is_blocking;
$run;
}
sub clean_qrcode{
my $self = shift;
return if not defined $self->qrcode_path;
return if not -f $self->qrcode_path;
$self->info("清除残留的历史二维码图片");
unlink $self->qrcode_path or $self->warn("删除二维码图片[ " . $self->qrcode_path . " ]失败: $!");
}
sub clean_verifycode{
my $self = shift;
return if not defined $self->verifycode_path;
return if not -f $self->verifycode_path;
$self->info("清除残留的历史验证码图片");
unlink $self->verifycode_path or $self->warn("删除验证码图片[ ". $self->verifycode_path . " ]失败: $!");
}
sub add_job {
my $self = shift;
require Mojo::Webqq::Client::Cron;
$self->Mojo::Webqq::Client::Cron::add_job(@_);
}
sub check_pid {
my $self = shift;
return if not $self->pid_path;
eval{
if(not -f $self->pid_path){
$self->spurt($$,$self->pid_path);
}
else{
my $pid = $self->slurp($self->pid_path);
if( $pid=~/^\d+$/ and kill(0, $pid) ){
$self->warn("检测到该账号有其他运行中的客户端(pid:$pid), 请先将其关闭");
$self->stop();
}
else{
$self->spurt($$,$self->pid_path);
}
}
};
$self->warn("进程检测遇到异常: $@") if $@;
}
sub clean_pid {
my $self = shift;
return if not defined $self->pid_path;
return if not -f $self->pid_path;
$self->info("清除残留的pid文件");
unlink $self->pid_path or $self->warn("删除pid文件[ " . $self->pid_path . " ]失败: $!");
}
sub save_state{
my $self = shift;
my($previous_state,$current_state) = @_;
my @attr = qw(
account
version
start_time
mode
http_debug
log_encoding
log_path
log_level
log_console
disable_color
tmpdir
cookie_path
qrcode_path
pid_path
state_path
keep_cookie
ua_retry_times
qrcode_count_max
state
);
# pid
# os
eval{
my $json = {plugin => []};
for my $attr (@attr){
$json->{$attr} = $self->$attr;
}
$json->{previous_state} = $previous_state;
$json->{pid} = $$;
$json->{os} = $^O;
for my $p (keys %{ $self->plugins }){
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} } ;
}
$self->spurt($self->to_json($json),$self->state_path);
};
$self->warn("客户端状态信息保存失败:$@") if $@;
}
sub is_load_plugin {
my $self = shift;
my $plugin = shift;
if(substr($plugin,0,1) eq '+'){
substr($plugin,0,1) = "";
}
else{
$plugin = "Mojo::Webqq::Plugin::$plugin";
}
return exists $self->plugins->{$plugin};
}
sub check_controller {
my $self = shift;
my $once = shift;
if($^O ne 'MSWin32' and defined $self->controller_pid ){
if($once){
$self->info("启用Controller[". $self->controller_pid ."]状态检查");
$self->interval(5=>sub{
$self->check_controller();
});
}
else{
my $ppid = POSIX::getppid();
if( $ppid=~/^\d+$/ and $ppid == 1 or $ppid != $self->controller_pid ) {
$self->warn("检测到脱离Controller进程管理,程序即将终止");
$self->stop();
}
}
}
}
sub check_notice {
my $self = shift;
return if not $self->is_fetch_notice;
$self->info("获取最新公告信息...");
my $notice = $self->http_get($self->notice_api);
if($notice){
$self->info("-" x 40);
$self->info({content_color=>'green'},$notice);
$self->info("-" x 40);
}
}
1;