=pod

=encoding utf8

=head1 NAME

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

=head1 SYNOPSIS

    #注意: 
    #程序内部数据全部使用UTF8编码，因此二次开发源代码也请尽量使用UTF8编码进行编写，否则需要自己做编码处理
    #在终端上执行程序，会自动检查终端的编码进行转换，以防止乱码
    #如果在某些IDE的控制台中查看执行结果，程序无法自动检测输出编码，可能会出现乱码，可以手动设置输出编码
    #手动设置输出编码参考文档中关于 log_encoding 的说明

    #帐号可能进入保护模式的原因:
    #多次发言中包含网址
    #短时间内多次发言中包含敏感词汇
    #短时间多次发送相同内容
    #频繁异地登陆

    #推荐手机安装[QQ安全中心]APP，方便随时掌握自己帐号的情况
    #通过本程序登录QQ之前请先关闭帐号的密保功能

    #初始化一个客户端，采用二维码登录方式
    use Mojo::Webqq;
    my $client=Mojo::Webqq->new(
        http_debug  =>  0,         #是否打印详细的debug信息
        log_level   =>  "info",    #日志打印级别，debug|info|msg|warn|error|fatal
        login_type  =>  "qrlogin", #登录方式，qrlogin 表示二维码登录
    );

    #初始化一个客户端，采用账号密码登录方式，需要显式设置账号和密码
    use Mojo::Webqq;
    use Digest::MD5 qw(md5_hex);
    my $client=Mojo::Webqq->new(
        account     => 123456,      #QQ账号
        pwd         => md5_hex('你的QQ账号登录密码'),  #登录密码
        http_debug  =>  0,          #是否打印详细的debug信息
        log_level   =>  "info",     #日志打印级别，debug|info|msg|warn|error|fatal
        login_type  =>  "login",    #登录方式，login 表示账号密码登录
    );
    
    #注意: 原生的SmartQQ是不支持账号密码登录的
    #程序实际上是通过 http://qun.qq.com 页面账号密码登录然后和SmartQQ共享登录状态，从而实现账号密码登录
    #所以，账号密码的登录方式并不稳定，一旦失败，程序会再次自动尝试使用二维码扫描登录
    #请关闭帐号的密保功能，不支持密保登录
    #另外，基于账号密码的登录方式，一旦登录所在地发生较大变化，则腾讯服务器可能需要你输入图片验证码
    #这样就很难实现自动化操作，为了避免这种情况，你需要尽量在pl脚本所在的网络中用浏览器多登录一下 http://qun.qq.com
    #让腾讯服务器消除登录异常的判断，你可以在服务端搭建ssh隧道，socks5代理，支持SSL转发（CONNECT方法）的http代理等方式
    #然后浏览器通过服务端代理访问
    #参考github issue: https://github.com/sjdy521/Mojo-Webqq/issues/183

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

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

    #ready事件触发时 表示客户端一切准备就绪：已经成功登录、已经加载完个人/好友/群信息等
    #你的代码建议尽量写在 ready 事件中
    $client->on(ready=>sub{
        my $client = shift;

        #你的代码写在此处 

    });

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

    #run相当于执行一个死循环，不会跳出循环之外
    #所以run应该总是放在代码最后执行，并且不要在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 数据类型

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

这些类都继承L<Mojo::Webqq::Model::Base>具有dump的方法，这些数据类型均由系统自动创建和维护

一般情况下，你只需要对对象进行只读操作

=head2 Mojo::Webqq::User 个人

    属性:

    id              #本次登录唯一标识，发送消息时需要用到，id和qq号是两个不同的概念
    uid             #qq帐号
    name            #昵称    
    type            #固定值"user"

    phone           #电话
    birthday        #生日
    occupation      #职业
    college         #大学
    blood           #血型
    constel         #星座
    homepage        #主页
    state           #在线状态 online|away|busy|callme|silent|hidden
    country         #国家
    city            #城市
    displayname     #当前和昵称完全一样
    shengxiao       #生效
    email           #邮箱
    client_type     #固定值"web"
    province        #省份
    sex             #性别
    mobile          #手机
    signature       #个性签名

    方法：
    dump
    is_me           #判断对象是否是自己
    
实际想要获取该对象信息时，是通过客户端提供的 user 属性来获取
    
    $client->user       #返回的是一个Mojo::Webqq::User对象
    $client->user->name #获取帐号昵称
    $client->user->id   #获取帐号id
    $client->user->dump

=head2 Mojo::Webqq::Friend 好友

    属性：

    id              #好友的id，仅在本次登录期间唯一，多次登录可能会发生变化，记住 id不是qq号
    uid             #好友的qq号码
    type            #固定值"friend"
    category        #好友所属的分组
    name            #好友昵称
    markname        #好友备注名称
    displayname     #如果设置了markname 就返回markname 否则返回nick

    is_vip          #是否是vip会员
    vip_level       #vip等级
    state           #好友状况 online|away|busy|silent|offline
    client_type     #好友客户端类型 pc|mobile|iphone|unknown

    方法:
    send            #给好友对象发送消息
    is_friend       #判断对象是否是好友
    dump

    代码示例：

    my $friend = $client->search_friend(name=>xxx);
    $client->print("好友分组为: ",$friend->category,"\n"); #使用$client->print()可以自动检测终端编码，防止乱码
    $friend->dump();

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

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

=head2 Mojo::Webqq::Group 群组

    属性：

    id              #群的id，仅在本次登录期间唯一，多次登录可能会发生变化
    type            #固定值"group"
    uid             #群号码
    name            #群名称
    memo            #群说明
    role            #create|attend|manage 分别表示创建的群|加入的群|管理的群
    owner_id        #群拥有者的id
    owner_uid       #群拥有者的qq
    markname        #群备注名称
    displayname     #如果设置了gmarkname 就返回gmarkname 否则返回gname
    createtime      #群创建时间
    level           #群等级
    member          #群成员，此属性是一个数组引用，数组中每个元素是一个Mojo::Webqq::Group::Member的对象
    max_member      #最大群成员数量
    max_admin       #最大管理员数量

    方法：
    search_group_member     #根据群成员属性搜索，标量上下文返回第一个群成员对象，列表上下文返回全部，搜索失败返回undef
    each_group_member       #在群中遍历所有的群成员
    invite_friend           #邀请好友入群
    kick_group_member       #踢出群成员
    set_group_admin         #设置群成员为管理员
    remove_group_admin      #取消群成员的管理员身份
    set_group_member_card   #设置群名片
    shutup_group_member     #对群成员进行禁言
    speakup_group_member    #取消群成员禁言
    members                 #返回群成员对象列表  
    member_count            #返回当前群成员数量
    send                    #给群对象发送消息
    me                      #返回自己在群成员中的对象 (Mojo::Webqq::Group::Member)
    is_group                #判断对象是否是一个群
    dump                    #打印群对象结构，方便调试
    qiandao                 #签到

    示例代码：
    my $group = $client->search_group(name=>"PERL学习交流群"); #返回一个Mojo::Webqq::Group对象
    $group->send("hello world"); #在指定的群中发送消息

    my $member = $group->search_group_member(id=>xxx,name=>xxx); #返回一个Mojo::Webqq::Group::Member对象
    $member->send("hello world"); #给群中指定的群成员发送临时消息

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

    for my $group ($client->groups){
        ...;
    }

    #获取自己在群中的显示名称
    $group->me->displayname 

    #邀请好友加入群，
    $group->invite_friend( $friend1 [,$friend2,...] ); 

    #踢出群成员，需要管理员权限
    $group->kick_group_member($member1 [,$member2,...]);

    #设置指定成员为管理员，需要群主身份
    $group->set_group_admin($member1 [,$member2,...]);

    #取消指定成员的管理员身份，需要群主身份
    $group->remove_group_admin($member1 [,$member2,...]);

    #对指定的群成员进行禁言，禁言时间单位为秒，建议尽量是分钟的整数倍，最小60秒
    $group->shutup_group_member(60,$member1 [,$member2,...]);

    #对指定的群成员取消禁言操作
    $group->speakup_group_member($member1 [,$member2,...]);

    #设置自己的群名片
    $group->set_group_member_card($group->me, $card);
    $group->me->set_card($card);

    #取消群名片
    $group->me->set_card(undef);

    #设置群成员的群名片，需要管理员权限
    $group->set_group_member_card($member,$card); 

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

    属性：

    id              #唯一id，仅在本次登录期间唯一，多次登录可能会发生变化
    uid             #qq号码，当前获取qq号第一次都需要实时调用api获取，效率较低，应避免使用
    type            #固定值"group_member"
    name            #群成员昵称
    province        #省份
    sex             #性别
    country         #国家
    city            #城市
    card            #群名片（由于smartqq限制，当前只能获取到群名片的前21个字节，过长会被截断）
    displayname     #设置了card就返回card否则返回nick
    state           #状态
    client_type     #客户端类型
    qage            #q龄
    join_time       #入群时间
    last_speak_time #最后发言时间
    level           #等级 灌水|传说|潜水 等
    role            #角色 admin|owner|member
    bad_record      #是否包含不良记录 0|1

    方法:
    send            #给群成员对象发送消息 $member->send("hello world")
    dump
    is_me           #判断对象是否是群中的自己
    is_group_member #判断是否是一个群对象
    set_card        #设置群名片，设置其他群成员需要管理员权限
    group           #获取成员所属的群对象

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

    属性：

    id          #讨论组id
    type        #固定值"discuss"
    name        #讨论组名称
    displayname #当前返回的和dname一样
    owner_id    #讨论组建立者id
    member      #讨论组成员，一个数组引用，每个元素是一个Mojo::Webqq::Discuss::Member对象

    方法:

    search_discuss_member   #搜索讨论组成员
    each_discuss_member     #遍历讨论组成员
    members                 #返回讨论组成员对象列表
    member_count            #返回当前讨论组成员数量
    send                    #给讨论组对象发送消息
    me                      #获取自己在讨论组中的对象 (Mojo::Webqq::Discuss::Member)
    dump
    is_discuss          

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

    属性：

    name
    type                    #固定值"discuss_member"
    id
    uid
    state
    client_type
    displayname             #当前和name一样

    方法：
    send                    #给讨论组成员发送临时消息
    dump
    is_me                   #判断是否是讨论组中的自己
    is_discuss_member
    discuss                 #获取成员所属的讨论组对象


=head2 Mojo::Webqq::Message 消息类

    #消息主要包含好友消息、群消息、讨论组消息、群临时消息、讨论组临时消息
    #所有的消息类都继承Mojo::Webqq::Message::Base，具备dump方法
    #消息的对象均由系统创建和维护，你只需要对消息对象进行只读操作即可

    属性:

    type            #消息类型：friend_message|group_message|discuss_message|sess_message
    class           #发送或接收的消息：recv|send
    from            #消息的来源，比如来自某个插件，可自由定义，默认"none"
    ttl             #默认值5 当ttl减为0会被消息队列丢弃
    allow_plugin    #默认1
    id              #消息id
    sender_id       #发送者id
    receiver_id     #接收者id
    sender          #发送者对象，可以是好友|群成员|讨论组成员
    receiver        #接收者对象，好友|群成员|讨论组成员
    group           #消息相关的群组对象
    discuss         #消息相关的讨论组对象
    time            #消息时间
    content         #消息内容
    text            #纯文本消息内容，不包含表情图片等
    raw_content     #消息原始内容 一个数组引用
    via             #临时消息的来源：group|discuss
    
    code            #发送消息状态，0表示成功，非0失败
    msg             #发送消息状态中文描述：发送成功|发送失败
    info            #发送消息更多错误描述信息

    方法：
    dump            #打印消息结构，主要用于调试
    to_json_hash    #将消息转换为json兼容的hash，方便和外部系统交互
                    #常见的接收和发送消息都支持该方法，下文不再赘述
    is_at           #消息是否艾特某人，常见的接收和发送消息都支持该方法，下文不再赘述
    faces           #返回消息中的表情内容
    images          #获取到消息中图片内容后执行指定回调，适合对具体某个消息进行图片处理
                    #参考 receive_pic 事件，可以设置全局接收图片的处理回调
    reply           #对消息进行回复
                    #如果class为recv，相当于发送内容给消息发送者，如果class为send，相当于发送内容给消息的接收者

    is_success      #判断发送消息是否成功

    示例代码:
    $msg->sender->displayname; #从消息中 获取发送者的名称

    my $json_hash = $msg->to_json_hash();    #获取到hash引用
    my $json_text = $client->to_json($json_hash); #将hash转换为json字符串

    $msg->dump();
    $msg->reply("hello world"); #比$client->reply_message($msg,"hello world")更简洁

    $msg->is_at();          #判断发送或接收的消息中是否有艾特自己
    $msg->is_at($member);   #判断发送或接收的消息中是否有艾特某个成员
    $msg->is_at($friend);   #判断发送或接收的消息中是否有艾特某个好友

    my @faces = $msg->faces(); #("[微笑]","[流泪]","[害羞]")
    
    $msg->images(sub{
        my($client,$image_path,$sender) = @_;
    });

    #全局图片处理事件
    $client->on(receive_pic=>sub{
        my($client,$image_path,$sender) = @_;
    });
    
    text            #纯文本消息内容，不包含表情图片等
    raw_content     #消息原始内容 一个数组引用

=head1 客户端属性

    $client->security   #是否开启安全加密
    $client->mode       #登录状态
    $client->type       #类型 固定值 smartqq，Mojo::Webqq只支持smartqq
    $client->htto_debug #http请求是否打印debug信息
    $client->log_level  #日志记录等级 默认info
    $client->log_path   #日志记录路径，默认undef，打印到STDOUT
    $client->version    #客户端版本
    $client->account    #登录帐号，不必是真实的qq号码，可以自由定义，根据扫码确定实际登陆qq

=head1 客户端方法

=head2 new()

    account           #登录帐号，默认default,不必是真实qq号码，可自由定义，根据扫码确定实际登陆qq
    security          #设置该参数，将使得发送和接收消息使用https加密
    mode              #设置登录状态，默认是online，支持online|away|busy|silent|hidden|offline|callme
    http_debug        #设置该参数，打印调试信息
    keep_cookie       #默认为1，0表示不保存登录cookie，1表示保存登录cookie方便下次直接恢复登录
    disable_color     #默认为0，是否禁用终端彩色打印
    send_interval     #默认为3s，全局发送消息时间间隔，间隔太小会导致消息发送频繁，可能有被封号的风险
    ignore_send_retcode #对发送消息返回这些状态码不认为发送失败不重试,参数是一个数组引用
                        #设置示例 Mojo::Webqq->new(ignore_send_retcode=>[1202,100100]);
                        #默认值 [1202,100100] 不清楚作用请不要随意设置

    ignore_poll_retcode #对接收消息返回这些状态码不认为接收失败，不重新登录，避免频繁掉线
                        #设置示例 Mojo::Webqq->new(ignore_poll_retcode=>[102,109,110,1202,100012]);
                        #默认值 [102,109,110,1202,100012] 不清楚作用请不要随意设置

    ignore_unknown_id #其他设备上自己发送的消息，在webqq上会以接受消息的形式再次接收到
                      #消息发送者是一个未知的id，非常影响很多功能的正常工作，该选项决定是否忽略掉这种消息
                      #默认值为 1
    allow_message_sync #是否允许同步来自其他设备登录账号发送的消息 默认为0
                       #由于webqq自身发送消息后也会收到服务端重复的消息
                       #且没办法和来自其他设备的消息区分，会导致出现一些不期望的结果，比如某些插件会陷入死循环等
                       #因此默认不开启消息同步，如果你只是用来irc聊天，则可以考虑开启此选项
                       #可以同步来自其他设备的消息，体验会更好一些

    log_console     #打印到文件的同时也打印到屏幕，方便调试，默认 1
    log_level       #默认级别为info，可以设置debug|info|warn|error|fatal
    log_path        #默认客户端输出内容打印到STDOUT 设置该参数可以将客户端输出重定向到文件
    log_encoding    #输出日志的编码，默认自动检测，如果出现乱码可以尝试手动设置一下输出编码
                    #编码必须是 Encode 模块支持的格式，比如utf8 gbk等

    tmpdir          #程序使用的临时目录，主要用于保存一些验证码、二维码等数据，默认为系统临时目录
    pic_dir         #图片接收默认地址，默认为 tmpdir 参数所设置的目录

    verifycode_path #图片验证码保存路径，默认是 tmpdir 目录下固定文件名
    qrcode_path     #二维码保存路径，默认是 tmpdir 目录下固定文件名
    cookie_path     #登录cookie接收保存地址，默认为 tmpdir 参数所设置的目录下固定文件名

    #程序会根据实际需要自动按需加载好友、群、讨论组信息，首次登录加载这些信息是不必须的
    #如果你的群组数量非常惊人，首次加载会严重影响运行速度，这种情况下建议选择性的取消某些默认加载行为

    is_init_friend      #是否在首次登录时初始化好友信息，默认值 1，据测试设置为0可能影响登录
    is_init_group       #是否在首次登录时初始化群组信息，默认值 1
    is_init_discuss     #是否在首次登录时初始化讨论组信息，默认值 1

    #程序会定期更新个人、好友、群、讨论组信息，目的是为了可以感知新增的成员、失去的好友等
    #如果你完全不关注这些特性，可以关闭定期更新
    is_update_user      #是否定期更新个人信息，默认值 0
    is_update_group     #是否定期更新群组信息，默认值 1
    is_update_group_member #是否定期更新群成员信息，默认值 1
    is_update_group_member_ext  #是否定期更新群成员扩展信息，扩展信息包含QQ号，默认值0，即新增成员才会更新
    is_update_friend    #是否定期更新好友信息，默认值 1
    is_update_discuss   #是否定期更新讨论组信息，默认值 1
    update_interval     #定时更新群组、好友、讨论组等信息的时间间隔，默认600秒

    group_member_use_friend_markname #群成员使用好友备注名(如果是好友关系并且备注存在)

    is_fetch_notice     #是否获取公告信息，默认值 1

    msg_ttl             #发送失败的消息，重试次数，默认是5

    qrcode_count_max    #每个二维码都有一个过期时间，过期后会重新下载一个二维码继续等待扫描
                        #如果等待扫码次数超过 qrcode_count_max 客户端会停止运行，默认10次

    #注意：所有参数都支持通过环境变量来设置，参数对应的环境变量格式 MOJO_WEBQQ_{参数名大写}
    #比如: MOJO_WEBQQ_ACCOUNT、 MOJO_WEBQQ_HTTP_DEBUG

    $client->new(http_debug=>0,account=>'xxxxxx',log_level=>'info');

=head2 on()

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

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

=head2 login() 

客户端登录

    $client->login();    

注意：程序会自动调用该方法，一般情况下你不需要自己调用

=head2 relogin() 重新登录

重新登录前，会先清空登录相关cookie，好友、群等数据，客户端在必要时默认会自动尝试重新登录

    $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 stop() 停止收发消息

=head2 add_job($job_name,$time,$callback); 指定时刻执行

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

=head2 spawn(%opt) 执行外部命令

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

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

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

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

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

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

    #支持的参数：
    is_blocking     #是否阻塞执行命令，默认0
    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) = @_;
            $client->print("从标准输出中实时收到数据:",$chunk,"\n");
        },
        stderr_cb       => sub {
            my($pid,$chunk) = @_;
            $client->print("从标准错误中实时收到数据:",$chunk,"\n"); 
        },
        exit_cb => sub{
            my($pid,$res) = @_;
            $client->print("从标准输出中接收的全部数据:",$res->{stdout},"\n");
            $client->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     #主题
    html        #邮件正文内容，html格式
    text        #邮件正文内容，纯文本格式
    charset     #主题，邮件正文的编码格式，默认UTF-8
    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 "发送失败"} 
    });

    其实也支持阻塞发送
    my ($send_status,$err) = $client->mail(...);

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

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

=head2 send_group_message($group,$content,[$callback]) 发送群消息
    
    my $group = $client->search_group(name=>"PERL学习交流");#$group是一个Mojo::Webqq::Group的对象
    $client->send_group_message($group,"hello world");

=head2 send_discuss_message($discuss,$content,[$callback]) 发送讨论组消息

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

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

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

    #先找到群，再使用群搜索到群成员
    my $group = $client->search_group(name=>"PERL学习交流");
    if(not defined $group){
        #没有搜索到群成员，处理你的异常
        ...;
    }
    else{
        my $member = $group->search_group_member(name=>"小灰");
        $client->send_sess_message($member,"hello world") if defined $member;
    }

=head2 reply_message($msg,$content,[$callback]) 对收到的消息进行回复

send_friend_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数据进行转换得到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 方法类似，不再赘述

=head2 print 打印内容，能够自动识别输出终端编码，以防止乱码

    $client->print("妈妈再也不用担心我会乱码了...");

=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分钟才会触发相关的事件，或者新增群成员一旦发言也会发送触发事件

=head2 reveive_message 接收消息事件

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

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

=head2 send_message 发送消息事件

传递给回调函数的参数是 发送出去的消息对象
    
    $client->on(send_message=>sub{
        my ($client,$msg)=@_; 
        print $msg->id,"发送成功" if $msg->is_success  
    });

=head2 login 登录成功事件

登录完成时触发事件

    $client->on(login=>sub{
        my($client,$is_scan) = @_;
        #$is_scan   值为1表示本次登录是扫描二维码登录的 好友，群组对象的id可能会发生变化
        #           值为0表示本次登录是使用cookie直接恢复登录，未经过二维码扫描，对象id一般不会发生变化
    });

=head2 relogin 重新登录成功事件

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

=head2 input_img_verifycode 等待输入验证码

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

=head2 input_qrcode 等待扫描二维码

登录需要扫描二维码，参数分别为二维码文件的本地路径

=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_pic 接收图片(已失效)

    $client->on(receive_pic=>sub{ 
        my($client,$filepath,$sender)=@_;
        #$filepath 图片文件的绝对路径
        #$sender   发送图片的对象，可能是好友、群成员
        if($sender->is_friend){...;}
        elsif($sender->is_group_member){...;}
        elsif($sender->is_discuss_member){...;}
    });


=head2 receive_friend_pic 接收好友图片(已失效)

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

=head2 receive_sess_pic 接收陌生人图片(已失效)

    $client->on(receive_sess_pic=>sub{ 
        my($client,$filepath,$sender)=@_;
        #$filepath 图片文件的绝对路径
        #$sender   发送图片的群成员或讨论组成员
    });

=head2 receive_group_pic 接收到群图片(已失效)

    $client->on(receive_group_pic=>sub{ 
        my($client,$filepath,$sender)=@_;
        #$fh       图片文件的只读句柄
        #$filepath 图片文件的绝对路径
        #$sender   发送图片的群成员对象
    });

=head2 before_send_message 消息即将发送

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

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

=head2 friend_property_change 好友对象属性发生改变

    $client->on(friend_property_change=>sub{
        my($client,$friend,$property,$old_value,$new_value)=@_;
    });

=head2 group_property_change 群对象属性发生改变

    $client->on(group_property_change=>sub{
        my($client,$group,$property,$old_value,$new_value)=@_;
    });

=head2 group_member_property_change 群成员对象属性发生改变

    $client->on(group_member_property_change=>sub{
        my($client,$member,$property,$old_value,$new_value)=@_;
    });

=head2 discuss_property_change 讨论组属性发生改变

    $client->on(discuss_property_change=>sub{
        my($client,$discuss,$property,$old_value,$new_value)=@_;
    });

=head2 discuss_member_property_change 讨论组成员属性发生改变

    $client->on(discuss_member_property_change=>sub{
        my($client,$member,$property,$old_value,$new_value)=@_;
    });

=head2 model_update_fail 更新好友、群、讨论组信息失败

程序目前依靠定时更新好友、群、讨论组等数据来判断新增好友、新增群成员等

但当前发现运行一段时间，可能由于cookie过期等原因无法再成功更新这些信息，连续多次更新失败会触发该事件

但并不影响消息接收，可能影响消息发送或者其他部分功能受限 

如果你很在意这些数据更新是否成功，你可以考虑在该事件触发时重新登录，便可以恢复数据更新功能

    $client->on(model_update_fail=>sub{
        $client = shift;
        $client->relogin();
    });

=head2 receive_raw_message 接收到原始消息内容

    $client->on(receive_raw_message=>sub{
        my ($client,$bytes,$hash) = @_;
        #$bytes 是接收到的原始消息 json 格式数据，未做任何处理
        #$hash 是将 $bytes 进行 Mojo::JSON::from_json 之后得到的 perl hash 结构
    });

=head1 关于插件

=head2 load

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

    priority        #可选，设置插件优先级，默认是0，较高的优先级能够使得插件优先执行
    auto_call       #可选，设置是否加载完成后自动执行，默认为1
    call_on_load    #可选，加载完插件马上执行，默认为0
    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;
    our $PRIORITY = 10; #可省略，除了在load中使用priority设置优先级，也可以通过包变量设置
    our $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"); 
    $client->run();
   
当客户端运行时，插件将会被加载并自动执行，收到消息时会自动回复hello world 

注意：

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

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

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

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

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

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

=head1 插件列表

=head2 Mojo::Webqq::Plugin::ShowMsg

打印消息到终端

    $client->load("ShowMsg",data=>{
        allow_group => ["PERL学习交流"],  #可选，允许插件的群，可以是群名称或群号码
        ban_group   => ["私人群",123456], #可选，禁用该插件的群，可以是群名称或群号码
    });

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

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

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

作者 shalk  L<https://github.com/shalk>

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

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

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

    $client->load("IRCShell",data=>{
        listen=>[ {host=>"0.0.0.0",port=>6667},], #可选，IRC服务器监听的地址+端口，默认0.0.0.0:6667
        load_friend => 0, #可选,是否初始为每个好友生成irc虚拟帐号并加入频道 #我的好友
    });
    #支持的参数包括：
    listen              #监听的地址和端口，数组的形式来支持多个地址
                        # listen=>[ {host=>"127.0.0.1",port=>6667}, {host=>"127.0.0.1",port=>6668}]
                        # 表示监听127.0.0.1:6667和 127.0.0.1:6668两个端口
    allow_group         # allow_group=>["群组1名称","群组2号码",], #可选，允许在irc上创建的QQ群名称或群号码
    ban_group           # ban_group=>["群组1名称","群组1号码"],  #可选，禁止在irc上创建的QQ群名称
    master_irc_nick     # 和QQ关联的irc客户端昵称，默认按照和QQ相同的昵称或者客户端ip是本机地址作为识别规则
                        # 用于识别哪个连接到服务器上的irc用户是你自己
    auto_join_channel   #0|1 默认为1，开启该选项，收到未加入的频道消息会自动加入频道并接收消息，主要为了防止漏收
    load_friend         #0|1 默认是0 是否初始为每个好友生成irc虚拟帐号并加入频道 #我的好友
    upload_api          #图床api地址，将图片转为连接，方便在irc上查看图片
                        #支持兼容 http://img.vim-cn.com/ 的接口，考虑到隐私问题，默认不开启

    #以下参数需要 Mojo::IRC::Server::Chinese v1.8.1及以上版本

    #TLS支持

    #代码示例
    $client->load("IRCShell",data=>{
        listen=>[ 
            {host=>"0.0.0.0",port=>6667},         #监听普调端口
            {host=>"0.0.0.0",port=>6697,tls=>1},  #监听6697端口，开启TLS加密
        ],
    });

    #支持的TLS参数：
    #更多详细参数使用说明参见 https://metacpan.org/pod/Mojo::IOLoop::Server#tls
    tls                 #0|1 是否启用tls加密
    tls_ca              #可选，本地ca证书路径 例如 /etc/tls/ca.crt
    tls_cert            #可选，服务器证书路径，默认会生成一个测试证书
    tls_ciphers         #可选，加密套件，例如 AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH
    tls_key             #可选，服务器证书key文件路径
    tls_verify          #可选，验证模式
    tls_version         #可选，协议版本，例如 TLSv1_2

    create_chanserv_user #0|1 默认为1，是否创建频道时自动生成昵称为 ChanServ 的虚拟用户
                         #主要用于解决频道人数为空时，HexChat等某些客户端/list无法获取到频道信息的问题

    auth                 #登录验证函数，在客户端登录后被执行，返回 1 验证通过，返回 0 验证失败

    代码举例：
    $client->load("IRCShell",data=>{
        auth => sub{
            my($nick,$user,$pass) = @_; #传递给验证函数的三个参数依次是 irc客户端的NICK/USER/PASS
            if($nick eq '小灰'){#对于昵称"小灰"进行密码验证
                return 0 if not defined $pass; #没有设置密码，验证失败
                return 0 if $pass ne '123456'; #设置的密码不是 "123456" 验证失败
                return 1; #验证成功，允许正常登录
            }
            else{return 1;}#对其他登录昵称不进行验证，全部允许登录
        }
    });

当设置了 load_friend =>1 参数，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]联通

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

=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::ProgramCode

通过QQ消息执行各种编程语言代码，当前支持26种编程语言:

ruby/perl/clojure/coffeescript/bash/cpp/c/assembly/java/scala

csharp/d/erlang/go/idris/rust/php/elixir/fsharp/haskell/javascript

julia/lua/nim/ocaml/python

加载插件方法:

    $client->load("ProgramCode");

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

=head3 php

        code|php>>>
        <?php
        echo "Hello World\n";

=head3 c

        code|c>>>
        #include <stdio.h>
        int main() {
            printf("Hello World!\n");
            return 0;
        }

=head3 cpp

        code|cpp>>>
        #include <iostream>
        using namespace std;
        int main() {
            cout << "Hello World!";
            return 0;
        }

=head3 java

        code|java>>>
        public class Main {
            public static void main(String[] args) {
                System.out.println("Hello World!");
            }
        }

作者 limengyu1990 L<https://github.com/limengyu1990>

=head2 Mojo::Webqq::Plugin::MobileInfo

通过QQ消息查询手机归属地，触发消息格式：

    手机 1888888888

作者 limengyu1990 L<https://github.com/limengyu1990>

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

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

    $client->load("KnowledgeBase");

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

    learn 今天天气怎么样  天气很好
    学习  "你吃了吗"      当然吃了
    learn '哈哈 你真笨'   "就你聪明"
    learn* "你吃了吗"      当然吃了 （指令结尾有*表示添加全局知识库，默认只添加到所在的群组或好友知识库中）

    del   今天天气怎么样
    删除  '哈哈 你真笨'
    del*  今天天气怎么样  （删除全局知识库）

其他更多参数设置

    $client->load("KnowledgeBase",data=>{
        allow_group => ["PERL学习交流"],  #可选，允许插件的群，可以是群名称或群号码
        ban_group   => ["私人群",123456], #可选，禁用该插件的群，可以是群名称或群号码
        file => './KnowledgeBase.txt', #数据库保存路径，纯文本形式，可以编辑
        learn_command  => 'learn',     #可选，自定义学习指令关键字
        delete_command =>'del',      #可选，自定义删除指令关键字
        learn_operator => [12345,678910], #允许学习权限的操作人qq号
        delete_operator => [12345,678910], #允许删除权限的操作人qq号
        mode => 'fuzzy', # fuzzy|regex|exact 分别表示模糊|正则|精确, 默认模糊
        check_time => 10, #默认10秒检查一次文件变更
        show_keyword => 1, #消息是否包含触发关键字信息，默认为0
    });

1）知识库采用文本文件形式，可编辑，格式如下：

分组空间（群名称)  #  匹配关键字（或正则表达式） #  回复内容

文件中存储的数据格式示例：

    __全局__         #   你好                      # 很高兴认识
    __我的好友__     #   多谢                      # 不客气
    我的群组1        #   今天天气怎么样            # 没有雨\n不用带伞 （\n表示换行）
    我的群组1        #   今天天气怎么样            # 不知道
    我的群组1        #   今天天气怎么样            # 自己百度 （相同关键字可以设置多行的回复内容，随机选取）
    我的群组2        #   ^在吗(\?|)$               # 在呀，啥事
    我的群组2        #   大(神|婶)                 # 大神你妹啊
    我的群组3        #   井号怎么打出来            # 我发给你：\# 不客气 （井号要转义）

2）定时检查知识库变动，自动实时加载，插件中的check_time参数可以设置定时检测间隔


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

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

    $client->load("FuckDaShen");

=head2 Mojo::Webqq::Plugin::Translation

多国语言翻译功能，自动检测输入输出语言，消息指令格式：

    翻译 hello
    翻译 你好
    翻译 こんにちは

=head2 Mojo::Webqq::Plugin::Riddle

输入关键字进行猜谜，作者 limengyu1990 L<https://github.com/limengyu1990>

    $client->load("Riddle");
    
    $client->load("Riddle",data=>{
        allow_group => ["PERL学习交流"],  #可选，允许插件的群，可以是群名称或群号码
        ban_group   => ["私人群",123456], #可选，禁用该插件的群，可以是群名称或群号码
        ban_user    => ["坏蛋",123456], #可选，禁用该插件的用户，可以是用户的显示名称或qq号码
        command     => "猜谜",   #可选，触发关键字
        apikey      => "xxxx",   #可选，参见 http://apistore.baidu.com/apiworks/servicedetail/440.html?qq-pf-to=pcqq.c2c
        timeout     => 30, #等待答案的超时时间，超时后会自动公布答案
    });

=head2 Mojo::Webqq::Plugin::ShowQRcode

调用系统的图片查看程序来展示二维码图片，方便扫描，作者 autodataming L<https://github.com/autodataming>

    $client->load("ShowQRcode");#当前仅支持win
    
=head2 Mojo::Webqq::Plugin::GasPrice

输入关键字进行猜谜，作者 hyvinlam L<https://github.com/hyvinlam>

    $client->load("GasPrice");
    
    $client->load("GasPrice",data=>{
        allow_group => ["PERL学习交流"],  #可选，允许插件的群，可以是群名称或群号码
        ban_group   => ["私人群",123456], #可选，禁用该插件的群，可以是群名称或群号码
        command     => "油价",   #可选，触发关键字
        apikey      => "xxxx",   #可选，参见 http://apistore.baidu.com/apiworks/servicedetail/710.html
        msg_tail    => "消息尾巴", #可选
        is_need_at  => 0, #是否需要艾特,默认值为0
    });

=head2 Mojo::Webqq::Plugin::Qiandao 

QQ群每日签到插件

    $client->load("Qiandao");
    
    $client->load("Qiandao",data=>{
        allow_group => ["PERL学习交流"],  #可选，允许插件的群，可以是群名称或群号码
        ban_group   => ["私人群",123456], #可选，禁用该插件的群，可以是群名称或群号码
        is_qiandao_on_login => 0 ,        #可选，是否登录时进行签到，默认值为0
        qiandao_time => "09:30" ,         #可选，每日签到的时间，默认是 09:30
    });

=head2 Mojo::Webqq::Plugin::GroupManage

常用群管理功能，包括 新人入群欢迎、成员离开提醒、限制发言频率、限制发图频率

    $client->load("GroupManage",data=>{ 
        allow_group => ["PERL学习交流"],  #可选，允许插件的群，可以是群名称或群号码
        ban_group   => ["私人群",123456], #可选，禁用该插件的群，可以是群名称或群号码
        is_new_group_notice => 1,         #可选，是否发送新加入群提醒消息，默认 1
        is_new_group_member_notice => 1,  #可选，是否发送新增群成员提醒消息，默认 1
        is_lose_group_member_notice => 1, #可选，是否发送退出群成员离开提醒消息，默认 1
        is_group_member_property_change_notice => 0, #可选，是否发送群名片变更提醒消息，默认 0
        new_group_member => '欢迎新成员 @%s 入群[鼓掌][鼓掌][鼓掌]', #新成员入群欢迎语，%s会被替换成群成员名称
        lose_group_member => '很遗憾 @%s 离开了本群[流泪][流泪][流泪]', #成员离群提醒
        speak_limit => {#发送消息频率限制
            period          => 10, #统计周期，单位是秒
            warn_limit      => 8, #统计周期内达到该次数，发送警告信息
            warn_message    => '@%s 警告, 您发言过于频繁，可能会被禁言或踢出本群', #警告内容
            shutup_limit    => 10, #统计周期内达到该次数，成员会被禁言
            shutup_time     => 600, #禁言时长
            #kick_limit      => 15,   #统计周期内达到该次数，成员会被踢出本群
        },
        pic_limit => {#发图频率限制
            period          => 600,
            warn_limit      => 6,
            warn_message   => '@%s 警告, 您发图过多，可能会被禁言或踢出本群',
            shutup_limit    => 8,
            kick_limit      => 10,
        },
        keyword_limit => {
            period=> 600,
            keyword=>[qw(fuck 傻逼 你妹 滚)],
            warn_limit=>3,
            shutup_limit=>5,
            #kick_limit=>undef,
        },
    });

=head2 Mojo::Webqq::Plugin::UploadQRcode

将二维码图片上传至腾讯云对象存储，获取到公网可以访问的url地址

    $client->load("UploadQRcode");

=head2 Mojo::Webqq::Plugin::PostQRcode

登录过程如果需要手机扫描二维码，会将二维码以邮件附件的形式发送到指定邮箱，再通过手机QQ扫描二维码

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

    $client->load("PostQRcode",data=>{
        smtp    =>  'xxxx', #邮箱的smtp地址  
        port    =>  'xxxx', #smtp服务器端口，默认25
        from    =>  'xxxx', #发件人
        to      =>  'xxxx', #收件人
        user    =>  'xxxx', #smtp登录帐号
        pass    =>  'xxxx', #smtp登录密码
        tls     =>  0,      #可选，是否使用SMTPS协议，默认为0
                            #在没有设置的情况下，如果使用的端口为465，则该选项会自动被设置为1
    });

    收到的邮件内容如下：

    主题：QQ帐号 xxxx 扫描二维码
    附件：webqq_qrcode_xxxx.png
    正文：请使用手机QQ扫描附件中的二维码


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

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

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

    $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::GCM

接收消息通过谷歌提供的GCM接口发送到android手机

    $client->load("GCM",data=>{
        api_url         => 'https://gcm-http.googleapis.com/gcm/send',
        api_key         =>'xxx', #需要向谷歌申请
        registration_ids=>["xxx"],#android设备的注册id
        allow_group     =>["允许推送消息的群名称1","允许推送消息的群名称2","或者群号码"],
        ban_group       =>["禁止推送消息的群名称","或者群号码"],
        allow_dicuss    =>["允许推送消息的讨论组名称1","允许推送消息的讨论组名称2"],
        ban_discuss     =>["禁止推送消息的讨论组名称",],
    });

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

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

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

    $client->load("SmartReply",data=>{
        apikey          => '4c53b48522ac4efdfe5dfb4f6149ae51', #可选，参考http://www.tuling123.com/html/doc/apikey.html
        friend_reply    => 1, #是否针对好友消息智能回复
        group_reply     => 1, #是否针对群消息智能回复
        allow_group     => ["PERL学习交流"],  #可选，允许插件的群，可以是群名称或群号码
        ban_group       => ["私人群",123456], #可选，禁用该插件的群，可以是群名称或群号码
        ban_user        => ["坏蛋",123456], #可选，禁用该插件的用户，可以是用户的显示名称或qq号码
        notice_reply    => ["对不起，请不要这么频繁的艾特我","对不起，您的艾特次数太多"], #可选，提醒时用语
        notice_limit    => 8 ,  #可选，达到该次数提醒对话次数太多，提醒语来自默认或 notice_reply
        warn_limit      => 10,  #可选,达到该次数，会被警告
        ban_limit       => 12,  #可选,达到该次数会被列入黑名单不再进行回复
        ban_time        => 1200,#可选，拉入黑名单时间，默认1200秒
        period          => 600, #可选，限制周期，单位 秒
        is_need_at      => 1,  #默认是1 是否需要艾特才触发回复 仅针对群消息
        keyword         => [qw(小灰 小红 小猪)], #触发智能回复的关键字
    });

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

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

CPAN文档可能比较旧或者表达的信息不够丰富，请参考github上API相关说明L<https://github.com/sjdy521/Mojo-Webqq/blob/master/API.md>

    #同时支持http/https请求
    $client->load("Openqq",data=>{
        listen => [
            {   host=>"0.0.0.0",
                port=>5000,                     #监听5000端口的http请求
            },
            {
                host    =>"0.0.0.0",
                port    =>443,                    #可选，https请求监听端口，默认443
                tls         =>1,                      #必选，开启https请求支持
                tls_ca      =>"/etc/tls/ca.crt"       #可选，ca证书路径
                tls_cert    =>"/etc/tls/server.crt"   #可选，服务器证书路径，默认生成一个测试证书
                tls_key     =>"/etc/tls/server.key"   #可选，证书对应的key文件，默认生成一个测试key文件
            },#更多tls参数支持参考 https://metacpan.org/pod/Mojo::IOLoop::Server#tls
        ],
    });
    
    #data是一个HASH引用
    $client->load("Openqq",data=>{                                                 
        listen => [ {host=>"0.0.0.0",port=>5000}, ] , #监听的地址和端口，支持多个
        auth   => sub {my($param,$controller) = @_},    #可选，认证回调函数，用于进行请求鉴权
        post_api => 'http://xxxx',                      #可选，自定义的接收消息上报接口,并可以根据接口返回内容回复消息
        post_stdout => 0                                #可选，上报数据是否打印到stdout，适合管道交互信息方式，默认0
    });

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

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

    #auth函数示例：

    #简单的时间戳过期防盗链 
    #http://127.0.0.1/openqq/send_friend_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:5000/post_api',
    });

    #接收到消息后，插件会通过HTTP POST请求的方式将json格式的消息上报到http://127.0.0.1:3000/post_api
    
    connect to 127.0.0.1 port 3000    
    POST /post_api
    Accept: */*
    Content-Length: xxx
    Content-Type: application/json
    
    {
        "time":"1442542632",
        "content":"测试一下",
        "class":"recv",
        "sender":"灰灰",
        "sender_id":"2372835507",
        "sender_uid":"456789",
        "receiver":"小灰",
        "receiver_id":"4072574066",
        "receiver_uid":"123456",
        "group":"PERL学习交流",
        "group_id":"2617047292",
        "group_uid":"67890",
        "id":"10856",
        "type":"group_message"
    }

    一般情况下，post_api接口返回的响应内容可以是随意，会被忽略
    post_api接口返回的数据类型如果是 text/json 或者 application/json，并且json格式形式如下：
    
        {
            "reply":"xxxxx",    #要回复消息，必须包含reply的属性
            "shutup": 1,        #可选，是否对消息发送者禁言
            "shutup_time": 60,  #可选，禁言时长，默认60s
        } 

    则表示希望通过post_api响应的内容来直接回复该消息，post_api的返回结果比如

    HTTP/1.1 200 OK
    Connection: close
    Content-Type: application/json;charset=UTF-8
    Date: Mon, 29 Feb 2016 05:53:31 GMT
    Content-Length: 27
    Server: Mojolicious (Perl)

    {"reply":"你好","code":0}
    
    则会直接对上报的消息进行回复，回复的内容为 "你好"
    支持好友消息、群消息、讨论组消息、临时消息的上报
        

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

    #信息获取
    /openqq/get_user_info           #查询用户信息
    /openqq/get_friend_info         #查询好友信息
    /openqq/get_group_info          #查询群信息
    /openqq/get_discuss_info        #查询讨论组信息
    
    #消息发送，均支持GET和POST
    /openqq/send_friend_message     #发送好友消息 参数id=xxx&content=xxx 或 uid=xxx&content=xxx
    /openqq/send_group_message      #发送群消息   参数id=xxx&content=xxx 或 uid=xxx&content=xxx
    /openqq/send_discuss_message    #发送讨论组消息 参数id=xxx&content=xxx （由于腾讯限制，当前无法成功发送）
    /openqq/send_sess_message       #发送群临时消息  参数 group_id=xxx&id=xxx&content=xxx 或 group_uid=xxx&uid=xxx&content=xxx （由于腾讯限制，当前无法成功发送）
    /openqq/send_sess_message       #发送讨论组临时消息 参数 discuss_id=xxx&id=xxx&content=xxx 或 dusciss_id=xxx&uid=xxx&content=xxx （由于腾讯限制，当前无法成功发送）

调用示例

    http://127.0.0.1:5000/openqq/get_user_info
    http://127.0.0.1:5000/openqq/send_friend_message?id=xxx&content=hello 
    http://127.0.0.1:5000/openqq/send_friend_message?id=xxx&content=%e4%bd%a0%e5%a5%bd (中文需要utf8编码并进行urlencode)

更多完整文档请参考github上API相关说明L<https://github.com/sjdy521/Mojo-Webqq/blob/master/API.md>

=head2 Mojo::Webqq::Plugin::Pu

输入关键字 "周易","占卜","算命","八卦" 发送占卜消息 作者L<https://github.com/bollwarm>

    $client->load("Pu");

=head2 Mojo::Webqq::Plugin::ZiYue

输入关键字 "子曰","论语","之乎者也" 发送论语语句 作者L<https://github.com/bollwarm>

    $client->load("ZiYue");

=head1 SEE ALSO

L<Mojo::Weixin>

L<https://github.com/sjdy521/Mojo-Webqq>

L<https://github.com/sjdy521/Mojo-Weixin>

=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.1 or,
at your option, any later version of Perl 5 you may have available.

=cut
