=pod

=encoding utf8

=head1 NAME

Mojo::Webqq - A Webqq Client Framework base on Mojolicious

=head1 SYNOPSIS

    use Mojo::Webqq;
    use Mojo::Util qw(md5_sum);

    #初始化一个客户端对象
    my $client=Mojo::Webqq->new(ua_debug=>0);

    my $qq = 12345678;
    my $pwd = "your password";
    my $pwd_md5 = md5_sum($pwd);#得到原始密码的32位长度md5

    #客户端进行登录
    $client->login(qq=>$qq,pwd=>$pwd_md5);

    #客户端加载ShowMsg插件，用于打印发送和接收的消息到终端
    $client->load("ShowMsg");

    #设置接收消息事件的回调函数，在回调函数中对消息以相同内容进行回复
    $client->on(receive_message=>sub{
        my ($client,$msg)=@_;
        #已以相同内容回复接收到的消息
        $client->reply_message($msg,$msg->content);
        #你也可以使用$msg->dump() 来打印消息结构
    });

    #客户端开始运行
    $client->run();

=head1 DESCRIPTION

通过该项目，你可以完成基本的登录、接收和发送消息，在此基础上你可以通过插件的形式实现更多附加功能，比如：

群管理、聊天记录统计、QQ消息报警、QQ机器人、在QQ群中执行Perl代码，查询Perldoc文档、消息转发、QQ和IRC联通等等

原先的L<Webqq::Client>模块采用的是LWP+AnyEvent实现阻塞和非阻塞请求

代码比较混乱，存在诸多bug，易用性和可扩展性较差

此项目是L<Webqq::Client>模块的重构，基于L<Mojolicious>框架，具体更多良好特性，比如：

基于L<Mojo::Base>更好的对象模型、基于L<Mojo::EventEmitter>灵活的事件管理机制、

基于L<Mojo::UserAgent>统一的阻塞和非阻塞HTTP请求、基于L<Mojo::Log>轻量级的日志记录框架 等等

推荐你在使用本模块的同时也更多的了解L<Mojolicious>

=head1 数据类型

数据类型包含 个人、好友、群组、讨论组、最近联系人 几大类型，每种类型都专门设计了类

所有的类都继承Mojo::Webqq::Base具有dump的方法，这些数据类型均由系统自动创建和维护，一般情况下，你只需要对对象进行只读操作

=head2 Mojo::Webqq::User 个人

    属性:

    birthday        #生日
    phone           #电话
    occupation      #职业
    college         #大学
    qq              #qq帐号
    id              #本次登录唯一标识，发送消息时需要用到
    blood           #血型
    constel         #星座
    homepage        #主页
    state           #在线状态 online|away|busy|callme|silent|hidden
    country         #国家
    city            #城市
    nick            #昵称    
    displayname     #当前和昵称完全一样
    shengxiao       #生效
    email           #邮箱
    client_type     #固定值"web"
    province        #省份
    gender          #性别
    mobile          #手机
    signature       #个性签名

实际想要获取该对象信息时，是通过客户端提供的 user 属性来获取
    
    $client->user       #返回的是一个Mojo::Webqq::User对象
    $client->user->nick #获取帐号昵称
    $client->user->id   #获取帐号id
    

=head2 Mojo::Webqq::Friend 好友

    属性：

    qq              #好友的qq号码，当前获取qq号第一次都需要实时调用api获取，效率较低，应避免使用
    id              #好友的id，仅在本次登录期间唯一，多次登录可能会发生变化
    categorie       #好友所属的分组
    nick            #好友昵称
    markname        #好友备注名称
    displayname     #如果设置了markname 就返回markname 否则返回nick
    is_vip          #是否是vip会员
    vip_level       #vip等级
    state           #好友状况 online|away|busy|silent|offline
    client_type     #好友客户端类型 pc|mobile|iphone|unknown

    方法:
    send            #给好友对象发送消息

    代码示例：

    my $friend = $client->search_friend(id=>xxx);
    print "好友昵称为: " ,$friend->nick,"\n";
    $friend->dump();

    $friend->send("hello world"); #比$client->send_message($friend,"hello world")更简洁

    #遍历好友
    $client->each_friend(sub{
        my($client,$friend) = @_;
        ...;
    });
    
    for my $friend ($client->friends){
        ...;
    }
    

=head2 Mojo::Webqq::Group 群组

    属性：

    gid             #群的id，仅在本次登录期间唯一，多次登录可能会发生变化
    gtype           #create|attend|manage 分别表示创建的群|加入的群|管理的群,需要安装Webqq::Qun模块,暂未实现
    gnumber         #群号码 需要安装Webqq::Qun模块,暂未实现
    gname           #群名称
    gmemo           #群说明
    gcreatetime     #群创建时间 需要安装Webqq::Qun模块,暂未实现
    glevel          #群等级
    gowner          #群拥有者的id
    gmarkname       #群备注名称
    displayname     #如果设置了gmarkname 就返回gmarkname 否则返回gname
    member          #群成员，此属性是一个数组引用，数组中每个元素是一个Mojo::Webqq::Group::Member的对象

    方法：
    search_group_member  #根据群成员属性搜索群，标量上下文返回第一个群成员对象，列表上下文返回全部，搜索失败返回undef
    each_group_member    #在群中遍历所有的群成员
    members              #返回群成员对象列表  
    send                 #给群对象发送消息 $group->send("hello world")

    示例代码：
    my $group = $client->search_group_member(gname=>"PERL学习交流群"); #返回一个Mojo::Webqq::Group对象
    $group->search_group_member(id=>xxx,nick=>xxx); #返回一个Mojo::Webqq::Group::Member对象
    $group->each_group_member(
        my($client,$member) = @_;
        ...;
    );
    for my $group ($client->groups){
        ...;
    }
    

=head2 Mojo::Webqq::Group::Member 群成员

    属性：

    nick            #群成员昵称
    province        #省份
    gender          #性别
    id              #唯一id，仅在本次登录期间唯一，多次登录可能会发生变化
    country         #国家
    city            #城市
    card            #群名片
    displayname     #设置了card就返回card否则返回nick
    state           #状态
    client_type     #客户端类型
    qq              #qq号码，当前获取qq号第一次都需要实时调用api获取，效率较低，应避免使用
    qage            #q龄 需要安装Webqq::Qun模块,暂未实现
    join_time       #入群时间
    last_speak_time #最后发言时间
    level           ##等级 灌水|传说|潜水 等, 需要安装Webqq::Qun模块,暂未实现
    role            #角色 admin|owner|member, 需要安装Webqq::Qun模块,暂未实现
    bad_record      #是否包含不良记录 0|1,需要安装Webqq::Qun模块,暂未实现
    gid             #所在群的id
    gtype           #所在群的类型,  需要安装Webqq::Qun模块,暂未实现
    gnumber         #所在群的群号码, 需要安装Webqq::Qun模块,暂未实现
    gname           #所在群的名称
    gmemo           #所在群的说明
    gcreatetime     #所在群的创建时间
    glevel          #所在群的等级
    gowner          #所在群的拥有者id
    gmarkname       #所在群的备注名称

    方法:
    send            #给群成员对象发送消息 $member->send("hello world")

=head2 Mojo::Webqq::Discuss 讨论组

    属性：

    did         #讨论组id
    dname       #讨论组名称
    displayname #当前返回的和dname一样
    downer      #讨论组建立者id
    member      #讨论组成员，一个数组引用，每个元素是一个Mojo::Webqq::Discuss::Member对象

    方法:

    search_discuss_member   #搜索讨论组成员
    each_discuss_member     #遍历讨论组成员
    members                 #返回讨论组成员对象列表
    send                    #给讨论组对象发送消息

=head2 Mojo::Webqq::Discuss::Member 讨论组成员

    属性：

    nick
    id
    qq
    ruin
    state
    client_type
    dname
    displayname             #当前和dname一样
    did
    downer

    方法：
    send                    #给讨论组成员发送临时消息

=head2 Mojo::Webqq::Recent::Friend 

    属性:
    id
    type    #friend
    
=head2 Mojo::Webqq::Recent::Group 
    
    属性：
    gid
    type    #group

=head2 Mojo::Webqq::Recent::Discuss
    
    属性：
    did
    type    #discuss

=head1 消息类型

消息类型主要包含好友消息、群消息、讨论组消息、群临时消息、讨论组临时消息

所有的消息类都继承Mojo::Webqq::Message::Base，具备dump方法

消息的对象均由系统创建和维护，你只需要对消息对象进行只读操作即可

=head2 Mojo::Webqq::Message::Recv::Message 接收到的好友消息

    属性:

    type            #常量"message"
    msg_class       #常量"recv"
    msg_from        #消息的来源，比如来自某个插件，可自由定义，默认"none"
    ttl             #默认值5 当ttl减为0会被消息队列丢弃
    allow_plugin    #默认1
    msg_id          #消息id
    sender_id       #发送者id
    receiver_id     #接收者id
    sender          #发送者对象，Mojo::Webqq::Friend
    receiver        #接收者对象，Mojo::Webqq::User
    msg_time        #消息发送时间
    content         #消息内容
    raw_content     #消息原始内容 一个数组引用

    方法：
    to_json         #将消息转换为json形式的字符串，方便和外部系统交互
                    #常见的接收和发送消息都支持该方法，下文不再赘述

    示例代码:
    $msg->sender->nick; #从接收到的好友消息中 获取好友的昵称 更多好友相关的属性，参考Mojo::Webqq::Friend
    $msg->to_json();
    $msg->dump();
    
=head2  Mojo::Webqq::Message::Send::Message 发送的好友消息

    属性:

    type            #常量"message"
    msg_class       #常量"send"
    msg_from        #消息的来源，比如来自某个插件，可自由定义，默认"none"
    ttl             #默认值5 当ttl减为0会被消息队列丢弃
    allow_plugin    #默认1
    msg_id          #消息id
    sender_id       #发送者id
    receiver_id     #接收者id
    sender          #发送者对象，Mojo::Webqq::User
    receiver        #接收者对象，Mojo::Webqq::Friend
    msg_time        #消息发送时间
    content         #消息内容
    raw_content     #消息原始内容 一个数组引用

和接收的好友消息具有类似的属性，不同之处在于

msg_class 是常量"send"，你可以通过每个消息的msg_class属性来判断消息是发送消息还是接收消息

对于好友消息，sender是自己(Mojo::Webqq::User) receiver是好友(Mojo::Webqq::Friend)

对于群消息，sender是群中的自己(Mojo::Webqq::Group::Member) receiver是群成员(Mojo::Webqq::Group::Member)

对于讨论组消息，sender是讨论组中自己(Mojo::Webqq::Discuss::Member) receiver是讨论组成员(Mojo::Webqq::Discuss::Member)

对于临时消息，sender是讨论组或群中的自己 receiver是讨论组或群中的成员

其他消息类型也都存在类似的情况，不再赘述


=head2 Mojo::Webqq::Message::Recv::GroupMessage 接收到的群消息

    属性:

    type            #常量"group_message"
    msg_class       #常量"recv"
    msg_from        #消息的来源，比如来自某个插件，可自由定义，默认"none"
    ttl             #默认值5 当ttl减为0会被消息队列丢弃
    allow_plugin    #默认1
    msg_id          #消息id
    group_id        #群gid
    sender_id       #发送消息的群成员id
    receiver_id     #接收者id，也就是自己的id
    sender          #发送消息的群成员对象 Mojo::Webqq::Group::Member
    receiver        #群中的自己 Mojo::Webqq::Group::Member对象
    group           #群对象 Mojo::Webqq::Group
    msg_time        #消息时间
    content         
    raw_content

=head2 Mojo::Webqq::Message::Send::GroupMessage 发送的群消息

=head2 Mojo::Webqq::Message::Recv::DiscussMessage 接收到的讨论组消息

    属性：

    type            #常量"discuss_message"
    msg_class       #常量"recv"
    msg_from        #消息的来源，比如来自某个插件，可自由定义，默认"none"
    ttl             #默认值5 当ttl减为0会被消息队列丢弃
    allow_plugin    #默认1
    msg_id          #消息id
    discuss_id      #讨论组did
    sender_id       #发送消息的讨论组成员id
    receiver_id     #自己的id
    sender          #发送消息的讨论组成员对象 Mojo::Webqq::Discuss::Member
    receiver        #讨论组中的自己 Mojo::Webqq::Group::Member对象
    discuss         #群对象 Mojo::Webqq::Discuss
    msg_time        #消息时间
    content         
    raw_content  

=head2 Mojo::Webqq::Message::Send::DiscussMessage 发送的讨论组消息

=head2 Mojo::Webqq::Message::Recv::SessMessage 接收到的临时消息

    属性：

    type            #常量"sess_message"
    msg_class       #常量"recv"
    msg_from        #消息的来源，比如来自某个插件，可自由定义，默认"none"
    ttl             #默认值5 当ttl减为0会被消息队列丢弃
    allow_plugin    #默认1
    msg_id          #消息id
    group_id        #群gid
    discuss_id      #讨论组did
    sender_id       #发送消息的讨论组成员id或群成员id
    receiver_id     #Mojo::Webqq::User对象
    sender          #发送消息的讨论组成员对象或群成员对象
    group           #群对象 Mojo::Webqq::Group
    discuss         #讨论组对象 Mojo::Webqq::Discuss
    receiver        #群成员对象或者讨论组成员对象
    msg_time        #消息时间
    content         
    raw_content
    via             #如果是群临时消息 则是"group" 如果是讨论组临时消息则是"discuss"

=head2 Mojo::Webqq::Message::Send::SessMessage 发送的临时消息

=head2 Mojo::Webqq::Message::Send::Status 发送消息状态

    属性：

    code    #0 表示成功 非零表示失败
    msg     #"发送成功"|"发送失败"
    
    方法:
    is_success

    该消息结构主要用于判断发送消息是否成功

=head1 客户端属性

    $client->security   #是否开启安全加密
    $client->state      #登录状态
    $client->type       #类型 固定值 smartqq，Mojo::Webqq只支持smartqq
    $client->ua_debug   #http请求是否打印debug信息
    $client->log_level  #日志记录等级 默认info
    $client->log_path   #日志记录路径，默认undef，打印到STDERR
    $client->version    #客户端版本
    $client->qq         #登录帐号
    $client->pwd        #密码32位md5值
    $client->encrypt_method        #perl|js 登录加密算法，系统会会自动选择

    $client->user;       #获取Mojo::Webqq::User对象
    $client->group;      #返回客户端存储所有群组的数组引用，一般情况下你不应该直接操作该数据
    $client->discuss;    #返回客户端存储所有讨论组的数组引用，一般情况下你不应该直接操作该数据
    $client->recent;     #返回客户端存储最近联系人列表的数组引用，一般情况下你不应该直接操作该数据
    $client->plugins;    #存储客户端已经加载插件，一般情况下你不应该直接操作该数据

    #获取登录帐号相关信息
    $client->user->id;   #获取登录用户id
    $client->user->nick;   #获取登录用户昵称

=head1 客户端方法

=head2 new()

    security    #设置该参数，将使得发送和接收消息使用https加密
    state       #设置登录状态，默认是online，支持online|away|busy|silent|hidden|offline
    ua_debug    #设置该参数，打印调试信息
    log_level   #默认级别为info，可以设置debug|info|warn|error|fatal
    log_path    #默认客户端输出内容打印到STDERR 设置该参数可以将客户端输出重定向到文件
    encrypt_method #perl|js 系统会自动判断，一般情况下你不需要自己设置登录加密算法

    ioloop      #客户端使用的事件驱动，默认是Mojo::IOLoop->singleton 如果你不清楚这是什么可以不用理会
    keep_cookie #默认为1，0表示不保存登录cookie，1表示保存登录cookie方便下次直接恢复登录
    cookie_dir  #登录cookie的保存目录，默认为临时目录
    friend_pic_dir #设置好友图片保留目录，默认是临时目录，只有注册了receive_friend_pic事件 此路径才有意义

    $client->new(security=>0,ua_debug=>0);

=head2 on()

基于Mojo::EventEmitter的事件注册方法，可支持同时设置多个事件回调
    
    $client->on("event1"=>sub{...},"event2"=>sub{...},);

参考下文客户端支持的事件 

=head2 login() 

客户端登录

    $client->login(qq=>xxxx,pwd=>xxxx,[delay=>0|1]);    

    qq     登录的qq帐号    
    pwd    必须是原始密码经过md5加密后的32位字符串
    delay  0表示不延迟，立即登录 1表示在after_load_plugin事件触发后登录，即全部插件加载完毕后再执行登录
           为了兼容旧版本，delay默认值为0，推荐尽量将delay设置为1

    delay选项主要是为了方便配合某些插件的执行, 比如 Mojo::Webqq::Plugin::PostImgVerifycode 插件


注意：推荐安装L<Crypt::RSA> 或者 L<Crypt::OpenSSL::RSA> + L<Crypt::OpenSSL::Bignum>模块，能够极大的提升登录加密算法的计算速度（Centos可以使用 yum -y install perl-Crypt-OpenSSL-* 来方便安装）

=head2 relogin()

重新登录，客户端默认会自动尝试重新登录

    $client->relogin()

=head2 timer($seconds,$callback)

指定多少秒之后执行对应的回调函数

    $client->timer(10,sub{ print "hello world\n" }); #10s后打印hello world

=head2 interval($seconds,$callback)

设置每隔多少秒重复执行对应的回调函数

    $client->interval(10,sub{ print "hello world\n" }); #每隔10s后打印hello world
    
=head2 run()

客户端进入事件循环，正式开始运行，一般放在代码的最后，不可或缺

    $client->run();

=head2 multi_run()

登录多个qq帐号时使用，详情参见ready()方法介绍

=head2 ready()

当你想要同时运行多个qq帐号或者当你想要自己基于事件驱动的代码（往往也是Mojo的代码）中调用该模块时

$client->run()会阻塞你的代码，导致无法同时使用多个帐号或使你其他的代码无法执行

这种情况下，你应该放弃使用$client->run() 而是使用自己的事件驱动来驱动L<Mojo::Webqq>，你会需要ready()方法

    $other_ioloop = Mojo::IOLoop->new;
    $webqq_client = Mojo::Webqq->new(ioloop=>$other_ioloop);
    $webqq_client->ready(); #不是 $webqq_client->run();
    $other_ioloop->start;   #启动你自己的事件循环

    #同时登录多个qq帐号
    my $client1 = Mojo::Webqq->new;
    my $client2 = Mojo::Webqq->new;
    
    $client1->login(qq=>xxx,pass=>xxx);
    $client2->login(qq=>xxx,pass=>xxx);
    
    $client1->ready;
    $client2->ready;
    
    Mojo::IOLoop->multi_run(); 

=head2 stop(["auto"|"noexit"])

客户端终止接收和发送消息，其他非接收和发送消息类的功能仍然可以继续执行

    $client->stop(["auto"]) #默认参数是auto，当多个qq帐号客户端中的最后一个stop时，整个程序也会退出(exit)
    $client->stop("noexit") #即使所有的qq帐号都stop了，整个客户端也不调用exit退出

=head2 add_job($job_name,$time,$callback);

定时执行任务
    
    #支持的时间格式为 HH:MM
    $client->add_job("定时提醒","07:00",sub{$client->send_message($friend,"起床啦");});

=head2 spawn(%opt)

在单独的进程中执行代码或命令

客户端采用的是单进程异步事件驱动模式，如果在代码中执行system/exec等来调用其他命令

或者执行某些阻塞的代码，比如sleep等 均会导致客户端主进程被阻塞而影响正常接收和发送消息

这种情况下，可以采用客户端提供的spawn方法，将阻塞的代码放置到单独的进程中执行，捕获进程的标准输出和标准错误

在回调函数中获取到进程执行的结果

该方法实际上参考L<Mojo::Run>模块 并在该模块的基础上做了进一步完善

    #支持的参数：
    max_forks       #产生的最大进程数
    cmd             #要执行的命令或代码
    param           #命令的参数
    exec_timeout    #命令或代码的执行超时时间
    stdout_cb       #命令或代码执行过程中 STDOUT 一旦有数据则会触发此回调
    stderr_cb       #命令或代码执行过程中 STDERR 一旦有数据则会触发此回调
    exit_cb         #命令或代码执行结束的回调

    代码示例：
    $client->spawn(
        cmd => sub {print "hello world";return "ok"},
        exec_timeout => 3,
        exit_cb => sub{
            my($pid,$hash) = @_;
            #$pid 是执行程序的进程号
            #$hash是一个执行结果的hash引用，结构如下：
            #{
            #    'cmd'                  => 'CODE',              #执行的命令或代码
            #    'time_stopped'         => '1441011558.30559',  #进程停止时间
            #    'time_started'         => '1441011557.30242',  #进程开始时间
            #    'time_duration_total'  => '1.00345897674561',  #进程执行总时间
            #    'time_duration_exec'   => '1.00317192077637',  #进程执行时长
            #    'is_timeout'           => undef,               #是否是超时退出
            #    'exit_status'          => 1,                   #进程退出返回值
            #    'exit_core'            => 0,                   #是否有core
            #    'exit_signal'          => 0,                   #进程退出信号
            #    'param'                => undef,               #命令或代码执行参数
            #    'stderr'               => '',                  #进程的标准错误输出结果
            #    'stdout'               => 'hello world',       #进程的标准输出结果
            #    'result'               => [                    #代码的返回值
            #                                   'ok'
            #                              ]
            #}

        },
    );

    $client->spawn(
        cmd             => "ping www.qq.com", #或者写成 ['ping','www.qq.com']
        exec_timeout    => 3,
        stdout_cb       => sub{
            my($pid,$chunk) = @_;
            print "从标准输出中实时收到数据:",$chunk,"\n";
        },
        stderr_cb       => sub {
            my($pid,$chunk) = @_;
            print "从标准错误中实时收到数据:",$chunk,"\n"; 
        },
        exit_cb => sub{
            my($pid,$res) = @_;
            print "从标准输出中接收的全部数据:",$res->{stdout},"\n";
            print "从标准错误中接收的全部数据:",$res->{stderr},"\n";
        } 
    );


=head2 mail(%opt,[$callback]) 非阻塞发送邮件

该方法实际上是L<Mojo::SMTP::Client>的封装，使用该方法之前请确保你已经安装了L<Mojo::SMTP::Client>模块

    发送邮件需要设置的参数：
    smtp        #smtp服务器地址，例如smtp.163.com
    port        #smtp服务器端口，默认25
    tls         #0|1 是否使用tls，默认为 0
    tls_ca      #tls证书路径
    tls_cert    #tls公钥路径
    tls_key     #tls密钥路径
    user        #smtp帐号
    pass        #smtp密码
    from        #发送邮箱
    to          #接收邮箱
    cc          #抄送邮箱
    subject     #主题 utf8编码
    html        #邮件正文内容，html格式 utf8编码
    text        #邮件正文内容，纯文本格式 utf8编码
    data        #设置该选项表示使用MIME::Lite生成的发送数据

    $client->mail(smtp=>smtp.163.com,user=>xxx,pass=>xxx,from=>xxx,to=>xxx,subject=>"邮件测试",text=>"hello world",sub{
        my ($send_status,$err) = @_;
        if($send_status){print "发送成功"} 
        else{print "发送失败"} 
    });

=head2 update_friend()

更新全部好友信息，客户端会自动调用该方法，通常你不需要主动使用

=head2 update_group([$group])

更新群消息，如果设置了$group(Mojo::Webqq::Group对象) 则更新指定群消息，不设置参数更新全部群信息

客户端会自动调用该方法，通常你不需要主动使用

=head2 update_discuss([$discuss])

更新讨论组消息，如果设置了$discuss(Mojo::Webqq::Disucss对象) 则更新指定讨论组消息，不设置参数更新全部讨论组信息

客户端会自动调用该方法，通常你不需要主动使用

=head2 update_recent()

更新最近联系人信息，由于较少使用到，客户端仅在首次登录时调用该方法，更新一次

=head2 each_friend(sub{...})  

遍历所有的好友

    $client->each_friend(sub{
        my($client,$friend) = @_; #$friend是一个Mojo::Webqq::Friend对象
        ...;
    });

=head2 friends()

返回好友对象数组，数组中的每个元素是一个Mojo::Webqq::Friend对象

    my @friends = $client->friends();

=head2 each_group(sub{...})

遍历所有的群

    $client->each_group(sub{
        my($client,$group) = @_; #$group是一个Mojo::Webqq::Group对象
        ...;
    });

=head2 groups()

返回群对象数组，数组中的每个元素是一个Mojo::Webqq::Group对象

    my @groups = $client->groups();

=head2 each_group_member(sub{...})

遍历所有群中的所有群成员    

    $client->each_group_member(sub{
        my($client,$member) = @_; #$member是一个Mojo::Webqq::Group::Member对象
        ...;
    });

    注意，想要遍历某个群中的所有群成员可以使用群对象中的遍历方法：

    $group->each_group_member(sub{
        my($client,$member)=@_;
        ...;
    });

=head2 each_discuss(sub{...})  

遍历所有的讨论组，和遍历好友或群类似 不再赘述

=head2 discusss() 

返回讨论组对象数组，数组中的每个元素是一个Mojo::Webqq::Discuss对象

    my @discusss = $client->discusss;

=head2 each_discuss_member(sub{...}) 

遍历所有讨论组中的所有讨论组成员，和遍历好友或群类似 不再赘述

=head2 each_recent(sub{...})

遍历最近联系人列表

=head2 recents() 

返回最近联系人对象数组，数组中的每个元素是一个Mojo::Webqq::Recent对象

    my @recents = $client->recents();

=head2 send_message($friend,$content,[$callback])
    
发送好友消息
    
    my $friend = $client->search_friend(id=>xxx);#$friend是一个Mojo::Webqq::Friend的对象
    $client->send_message($friend,"hello world");#发送内容必须是utf8编码

    在callback中对发送的消息进行预处理
    $client->send_message($friend,"hello world",sub{
        my ($client,$msg) = @_;
        #对即将发送的消息进行预处理
        $msg->msg_from("自定义的来源标识");
    
        #设置该条消息发送完毕后的回调函数
        $msg->cb(sub{
            my($client,$msg,$status)=@_;
            print $msg->msg_id . "发送失败\n" if  not $status->is_success;
        });
    });

=head2 send_group_message($group,$content,[$callback])
    
发送群消息

    my $group = $client->search_group(gname=>"PERL学习交流");#$group是一个Mojo::Webqq::Group的对象
    $client->send_group_message($group,"hello world");

=head2 send_discuss_message($discuss,$content,[$callback])

发送讨论组消息

    my $discuss = $client->search_discuss(dname=>"讨论组");#$discuss是一个Mojo::Webqq::Discuss的对象
    $client->send_discuss_message($discuss,"hello world");

注意：由于webqq自身限制，当前无法成功发送讨论组消息

=head2 send_sess_message($group_member|$discuss_member,$content,[$callback])
    
发送群临时消息或者讨论组临时消息

    #直接在整个数据库中搜索指定群成员
    my $group_member = $client->search_group_member(gname=>"PERL学习交流",nick=>"小灰");
    #$group_member是一个Mojo::Webqq::Group::Member的对象
    $client->send_sess_message($group_member,"hello world");

    #先找到群，再使用群搜索到群成员，比上述的全局搜索方法更高效一些
    my $group = $client->search_group(gname=>"PERL学习交流");
    my $member = $group->search_group_member(nick=>"小灰");
    $client->send_sess_message($member,"hello world");

    my $discuss_member = $client->search_discuss_member(dname=>"讨论组",nick=>"小灰");
    #$discuss_member是一个Mojo::Webqq::Discuss::Member的对象
    $client->send_sess_message($discuss_member,"hello world");
     

=head2 reply_message($msg,$content,[$callback])

对收到的消息进行回复，send_message()/send_group_message()/send_discuss_message()/send_sess_message()

这一类方法适合对特定的对象主动发送消息，但很多场景下，我们不需要关心对方是谁

只需要对接收到的消息进行回复，使用reply_message会比较方便

    $client->reply_message($msg,$content);

=head2 http_get http阻塞或非阻塞get请求

该方法为Mojo::UserAgent的get方法的封装，调用方式基本和Mojo::UserAgent->get相同，但也存在细微差别

阻塞http请求：

    #标量上下文  返回http请求内容，若请求失败，返回内容为undef
    my $http_body = $client->http_get($url,$header);

    #列表上下文，返回http请求内容以及$ua,$tx
    my ($http_body,$ua,$tx) = $client->http_get($url,$header);
    
    #可以在http header设置一些请求相关的选项，比如：
    #json=>1 表示将响应的json数据进行json decode得到perl的hash引用
    #retry_times=>3 表示请求失败自动重试次数，默认使用$client->ua_retry_times的值
    my $json_decode_hash = $client->http_get($url,{json=>1,retry_times=>3,Host=>"www.qq.com"});

    #http post请求
    $client->http_post($url,$header,form=>$post_data);

非阻塞http请求：

    $client->http_get($url,$header,sub{
        my($http_body,$ua,$tx) = @_;    
        #请求失败 $http_body 返回undef
    });

注意：由于采用事件驱动，因此，你应该始终选择使用非阻塞的http请求模式，如果采用阻塞的http请求，在http请求完成之前

整个程序都是被阻塞的，无法做其他任何事（包括接收和发送消息等）

=head2 http_post http阻塞或非阻塞post请求

和 http_get 方法类似，不再赘述

=head1 客户端事件

常见的事件发生时，比如接收到消息，有人加入群中等等，客户端会设置对应的事件名称，并在事件完成时进行触发

你可以使用$client->on(event=>sub{xxx})的方式对你感兴趣的事件注册回调函数，回调函数的第一个参数永远是客户端对象本身

注意：
    
    new_friend/new_group/new_discuss/new_group_member/new_discuss_member
    lose_friend/lose_group/lose_discuss/lose_group_member/lose_discuss_member

这类事件采用的是较为特殊的处理方式，比如新增群成员时，如果你的帐号是管理员权限，你会收到相关的通知

但如果是非管理员权限,则完全不会收到任何通知提醒，以新增群成员为例，为了统一实现功能，采用两种机制:

1）客户端定期更新好友、群组、讨论组数据，和原始数据对比来发现新增或者丢失的成员，当前定期更新频率为10分钟

2）当群成员在群里发言时，客户端会马上在数据库中搜索相关群成员，如果搜索不到则判断为新增群成员，马上更新数据库，并触发事件

因此，你可能会发现当群成员加入后，过了10分钟才会触发相关的事件，或者新增群成员一旦发言也会发送触发事件


当前核心事件的触发顺序(采用延迟登录)

                                                                                +->before_send_message
    plugin_load -+                                                              |->receive_message
                 |->after_load_plugin->input_img_verifycode->login->ready->run->|->send_message
    plugin_call -+                                                          |   |->new_friend
                                                                            |   |->receive_friend_pic
                                                            relogin<-login<-+   |->lose_friend
                                                                                ...

=head2 reveive_message 

接收到消息事件，传递给回调函数的参数是接收到的消息对象

    $client->on(receive_message=>sub{my ($client,$msg)=@_;$msg->dump});

=head2 send_message     

发送消息事件，传递给回调函数的参数是 接收到的消息对象 和 发送状态对象

发送状态对象是Mojo::Webqq::Message::Send::Status的实例
    
    $client->on(send_message=>sub{
        my ($client,$msg,$status)=@_; 
        #$status是一个Mojo::Webqq::Message::Send::Status的对象,主要用于判断消息是否发送成功
        print $msg->msg_id,"发送成功" if $status->is_success  
    });

=head2 login

登录完成时触发事件，无额外的回调参数

=head2 relogin

重新登录完成时触发事件，无额外的回调参数

=head2 input_img_verifycode

登录需要输入验证码，参数为验证码文件的路径

=head2 ready

客户端准备就绪事件

=head2 run 

客户端一切准备就绪，开始进入事件循环之前

=head2 plugin_call

插件被执行时，参数为插件名称

=head2 plugin_load

插件被加载完成时，参数为插件名称

=head2 after_load_plugin

全部插件加载完成后触发的事件

=head2 new_friend

新增好友时触发事件，回调参数为新增的好友对象

    $client->on(new_friend=>sub{my ($client,$friend)=@_});

=head2 lose_friend

失去好友时触发事件，回调参数为失去的好友对象

    $client->on(lose_friend=>sub{my ($client,$friend)=@_});

=head2 new_group               

新加入群时触发事件，回调参数为新增群的对象

    $client->on(new_group=>sub{my ($client,$group)=@_});

=head2 lose_group

退出群事件

    $client->on(lose_group=>sub{my ($client,$group)=@_});

=head2 new_group_member        

新增群成员事件

    $client->on(new_group_member=>sub{my ($client,$group_member)=@_});

=head2 lose_group_member       

群成员退群事件

    $client->on(lose_group_member=>sub{my ($client,$group_member)=@_});
        
=head2 new_discuss             

新加入讨论组事件

    $client->on(new_discuss=>sub{my ($client,$discuss)=@_});
    
=head2 lose_discuss            

退出讨论组事件

    $client->on(lose_discuss=>sub{my ($client,$discuss)=@_});

=head2 new_discuss_member      

讨论组新增成员事件

    $client->on(new_discuss_member=>sub{my ($client,$discuss_member)=@_});

=head2 lose_discuss_member     

讨论组成员退出事件

    $client->on(lose_discuss_member=>sub{my ($client,$discuss_member)=@_});

=head2 receive_friend_pic

接收到好友发送的图片，图片默认下载到临时目录下，在回调执行结束后会自动删除

由于腾讯smartqq本身的限制，只能接收好友图片，不支持接收群图片

    $client->on(receive_friend_pic=>sub{ 
        my($client,$fh,$filepath,$friend)=@_;
        #$fh      图片文件的只读句柄
        #$filepath 图片文件的绝对路径
        #$friend  发送图片的好友对象
    });

=head2 first_talk

第一次接收到某人消息事件，第一次的定义为：收到某个用户在4小时内第一次发送的消息

    $client->on(first_talk=>sub{
        my($client,$sender,$msg) = @_;
        #$sender 发送消息的好友 或者 群成员 或者 讨论组成员
        #$msg    接收到的消息
        $sender->send("怎么这么久才想起我?");
    });
    
=head2 before_send_message 

消息发送之前的事件，一般用于在即将发送消息前对消息进行预处理

    $client->on(before_send_message=>sub{
        my($client,$msg) = @_;
        my $content = $msg->content;
        $content .=  "我是可爱的小尾巴";
        $msg->content($content);
    });

=head1 关于插件

=head2 load

加载一个或者多个插件，多个插件使用数组引用，支持的插件参数包括:

    priority        #可选，设置插件优先级，默认是0，较高的优先级能够使得插件优先执行
    auto_call       #可选，设置是否加载完成后自动执行，默认为1
    data            #可选，设置加载插件时可以携带的数据，将会在call的时候传递给插件本身

    $client->load(["plugin1","plugin2"],data=>[1,2,3,]);
    $client->load("plugin",priority=>0,auto_call=>1);

加载插件时，可以通过auto_call设置是否自动执行（默认在run的时候会执行），priority可以设置插件执行的优先级

数字越大，优先级越高，插件会被优先执行

=head2 call

手动执行一个插件、适合auto_call=>0的插件的手动执行模式，当auto_call=>1时，会自动执行call

    $client->call("plugin",[可选参数]);

客户端实现了一个简单的插件管理机制，插件是一个简单的call函数，包名默认是Mojo:Webqq::Plugin::

比如，我编写一个简单的hello world插件，效果是对接收到的任意消息回复一个"hello world"

编写一个包 Mojo:Webqq::Plugin::HelloWorld

    package Mojo:Webqq::Plugin::HelloWorld;
    $Mojo::Webqq::Plugin::HelloWorld::PRIORITY = 10; #可省略，除了在load中使用priority设置优先级，也可以通过包变量设置
    $Mojo::Webqq::Plugin::HelloWorld::AUTO_CALL = 1; #可省略，通过包变量设置插件是否默认加载后立刻执行
    sub call{
        my $client = shift;
        my $data   = shift; #可能包含的data数据
        $client->on(receive_message=>sub{
            my($client,$msg)=@_;
            $client->reply_message($msg,"hello world");
        });
    }
    1;

客户端加载和执行插件的操作：

    #如果你的插件并非Mojo::Webqq::Plugin::相对命名规则，则可以在名称前使用"+"表示插件绝对名称
    $client->load("HelloWorld",auto_call=>1); 
    $client->run();
   
当客户端运行时，插件将会被加载并自动执行，收到消息时会自动回复hello world 

注意：

当多个消息处理类的插件对同一个消息进行处理时，往往存在冲突的情况

比如一个插件对消息处理完并不希望其他插件再继续处理该消息（默认情况下，receive_message事件会广播给所有订阅该事件的回调）

这种情况下，可以通过设置不同的插件优先级，使得事件被触发时，优先级较高的插件获得优先执行

执行完成后，再通过设置$msg->allow_plugin(0) 来禁止其他插件继续处理该消息，每个消息都带有一个allow_plugin的属性

这是一种建议性的插件协议，并非强制遵守

除此之外，也可以采用插件的手动执行模式，自己根据需要来执行插件

=head1 插件列表

=head2 Mojo::Webqq::Plugin::StockInfo

查询股票信息，聊天内容中输入 "gp 000001" 或者 "股票 000001" 触发

目前仅支持上海和深圳6位数字股票代码

=head2 Mojo::Webqq::Plugin::MsgSync

实现QQ群和IRC的联通，彼此的消息自动同步，采用的是irc bot转发的形式，需要依赖L<Mojo::IRC>模块

该插件功能不够强大，体验也不够好，完全可以被插件 Mojo::Webqq::Plugin::IRCShell 取代了

    $client->load("MsgSync",data=>{
        irc=>{nick=>"xxxx",user=>"xxxx",pass=>"xxxx",server=>"irc.perfi.wang"},
        pairs=>[
            ["#ChinaPerl",$m->search_group(gname=>"PERL学习交流")],
            ["#Mojolicious",$m->search_group(gname=>"Mojolicious")],
        ]
    });    

=head2 Mojo::Webqq::Plugin::IRCShell

将qq协议转换成irc协议，启动一个本地的irc服务器，使用任意irc客户端（irc user设置为qq号）连接后即可以按照irc的方式使用qq

需要依赖模块 L<Mojo::IRC::Server>

    $client->load("IRCShell",data=>{host=>"127.0.0.1",port=>6667,});#开启本地的irc server
    #支持的参数包括：
    host                #默认0.0.0.0
    port                #默认6667
    master_irc_user     #和qq匹配的irc user帐号，默认按照和qq号相同的user或者客户端ip是本机地址作为识别规则
    load_friend         #0|1 默认是1 是否初始为每个好友生成irc虚拟帐号并加入频道 #我的好友

qq好友会默认加入到irc的 '#我的好友' 频道中

每个qq群也会在irc上创建对应的频道，比如qq群[PERL学习交流]对应的irc频道为'#PERL学习交流'

使用任意的irc客户端连接到服务器,你便可以在irc上完成和qq好友的聊天，群聊等

本插件更适合想要在Linux环境下使用qq的irc爱好者

欢迎加入irc.perfi.wang的#PERL学习交流，该频道已经和QQ群[PERL学习交流]联通

欢迎加入irc.perfi.wang的#Mojolicious，该频道已经和QQ群[Mojolicious]联通
        

=head2 Mojo::Webqq::Plugin::Perldoc

实现通过QQ消息查询perldoc文档，支持perldoc -f|-v xxx
    
    $client->load("Perldoc");
    #由于该插件处理完的消息不应该再由其他插件处理，因此插件优先级应该设置成比其他插件优先级更高，插件默认优先级是0

=head2 Mojo::Webqq::Plugin::Perlcode

通过QQ消息执行Perl代码，仅支持在linux系统上使用

    $client->load("Perlcode");
    #由于该插件处理完的消息不应该再由其他插件处理，因此插件优先级应该设置成比其他插件优先级更高，插件默认优先级是0

触发条件：消息以 >>> 开头，比如：

    >>> print "hello world";

=head2 Mojo::Webqq::Plugin::KnowledgeBase

通过QQ消息自定义问答知识库

    $client->load("KnowledgeBase");

触发条件： 消息以如下格式发送可以设定问题和答案，如果问题或答案包含空格可以使用引号 比如：

    learn 今天天气怎么样  天气很好
    学习  "你吃了吗"      当然吃了
    learn '哈哈 你真笨'   "就你聪明"

    del   今天天气怎么样
    删除  '哈哈 你真笨'

=head2 Mojo::Webqq::Plugin::FuckDaShen

对qq消息中出现的"大神"关键词进行鄙视

    $client->load("FuckDaShen");

=head2 Mojo::Webqq::Plugin::PostImgVerifycode

登录过程如果需要验证码，会发邮件到指定的邮箱，通过点击邮件中的链接远程提交验证码，适合配合手机使用

注意： 由于需要发送邮件附件，依赖模块 L<Mojo::SMTP::Client> L<MIME::Lite>，需要单独安装

    #插件需要使用到 input_img_verifycode 事件
    #需要login中开启delay参数（设置为延迟登录）
    #$client->login(qq=>xxxx,pwd=>xxxx,delay=>1); 

    $client->load("PostImgVerifycode",data=>{
        smtp    =>  'xxxx', #邮箱的smtp地址  
        port    =>  'xxxx', #smtp服务器端口，默认25
        from    =>  'xxxx', #发件人
        to      =>  'xxxx', #收件人
        user    =>  'xxxx', #smtp登录帐号
        pass    =>  'xxxx', #smtp登录密码
        post_host => 'xxx.xxx.xxx.xxx' , #本机公网IP地址，需要远程访问
        post_port => 'xxxx'            , #提交验证码的链接地址中使用的端口，默认1987
    });

    收到的邮件内容如下：

    主题：QQ帐号 xxxx 登录验证码
    附件：webqq_img_verfiy_xxxx.jpg
    正文：请点击以下链接提交验证码：http://xxx.xxx.xxx.xxx:xxxx/check_code

=head2 Mojo::Webqq::Plugin::SmartReply

实现机器人的智能回复，支持好友消息、群消息、群临时消息、讨论组临时消息的自动回复

为避免对群内成员产生影响，群内需要使用 @帐号昵称 来触发

    $client->load("SmartReply");

=head2 Mojo::Webqq::Plugin::Openqq

提供HTTP API接口，方便获取客户端帐号、好友、群、讨论组信息，以及通过接口发送和接收好友消息、群消息、群临时消息和讨论组临时消息

    #兼容老版本，data是一个ARRAY引用,可以设置多个监听的地址和端口
    $client->load("Openqq",data=>[ {host=>"127.0.0.1",port=>5000}, ]); 

    #新版本，data可以是一个HASH引用
    $client->load("Openqq",data=>{                                                 
        listen => [ {host=>"127.0.0.1",port=>5000}, ] , #监听的地址和端口，支持多个
        auth   => sub {my($param,$controller) = @_},    #可选，认证回调函数，用于进行请求鉴权
        post_api => 'http://xxxx',                      #可选，设置接收消息的上报接口
    });

    若data中设置了auth函数引用，则表示api接口开启认证
    认证函数返回值为真，认证通过，函数返回值为假，认证失败，接口返回403

    认证回调函数的第一个参数是一个HASH引用，包含get或post提交的参数信息
    第二个参数是一个Mojolicious的controller对象，适合对Mojolicious比较熟悉的情况下，利用controller进行高级的认证控制

    auth函数示例：

    #简单的时间戳过期防盗链 
    #http://127.0.0.1/openqq/send_message?id=xxxx&content=xxxx&key=xxxx&exp=xxxx
    sub {
        my $param = shift;
        my $secret = 'this is your secret key';
        return 0 if time() >= $param->{exp}; #参数值的exp为过期时间，超过过期时间链接已失效
        if($param->{key} eq md5_sum($secret . join "",@$param{qw(id gid did content exp)} )){
            return 1; #secret和相关参数值拼接成一个字符串后计算md5 再和参数key的值进行比较
        }
        else{
            return 0; 
        }
    }

    #利用controller允许指定的IP可以访问，更多关于controller的资料，可以参考 Mojolicious::Controller
    sub{
        my ($param,$controller) = @_;
        if($controller->tx->remote_address eq "127.0.0.1"){
            return 1;
        }
        return 0;
    }

    接收消息上报接口示例：
    $client->load("Openqq",data=>{
        listen => [{host=>xxx,port=>xxx}],
        post_api=> 'http://127.0.0.1:3000/post_api',
    });

    接收到消息后，插件会通过HTTP POST请求的方式将json格式的消息上报到http://127.0.0.1:3000/post_api
    
    connect to 127.0.0.1 port 3000    
    POST /post_api
    
    {   "receiver":"小灰",
        "msg_time":"1442542632",
        "content":"测试一下",
        "msg_class":"recv",
        "sender_id":"2372835507",
        "receiver_id":"4072574066",
        "group":"PERL学习交流",
        "group_id":"2617047292",
        "sender":"灰灰",
        "msg_id":"10856",
        "type":"group_message"
    }
    
    支持好友消息、群消息、讨论组消息、临时消息的上报
        

当前支持的信息获取和发送消息的API接口（均返回json格式数据）：

    #信息获取
    /openqq/get_user_info           #查询用户信息
    /openqq/get_friend_info         #查询好友信息
    /openqq/get_group_info          #查询群信息
    /openqq/get_discuss_info        #查询讨论组信息
    /openqq/get_recent_info         #查询最近联系人列表信息
    
    #消息发送，均支持GET和POST
    /openqq/send_message            #发送好友消息 参数id=xxx&content=xxx 
    /openqq/send_group_message      #发送群消息   参数gid=xxx&content=xxx
    /openqq/send_discuss_message    #发送讨论组消息 参数did=xxx&content=xxx （由于腾讯限制，当前无法成功发送）
    /openqq/send_sess_message       #发送群临时消息  参数 gid=xxx&id=xxx&content=xxx
    /openqq/send_sess_message       #发送讨论组临时消息 参数 did=xxx&id=xxx&content=xxx

调用示例

    http://127.0.0.1:5000/openqq/get_user_info
    http://127.0.0.1:5000/openqq/send_message?id=xxx&content=hello (中文需要utf8编码，且需要做urlencode)

=head1 SEE ALSO

L<Webqq::Client>

L<Webqq::Qun>

L<Weixin::Client>

=head1 AUTHOR

sjdy521, E<lt>sjdy521@163.comE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2014 by sjdy521

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.10.0 or,
at your option, any later version of Perl 5 you may have available.

=cut
