├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── demo ├── diancan.php ├── extra │ ├── ducheng.png │ └── longxing.jpg └── vbot.php ├── example ├── bainian.php ├── custom.php ├── forward.php ├── group.php ├── index.php └── qunfa.php └── src ├── Collections ├── Account.php ├── BaseCollection.php ├── Contact.php ├── Group.php ├── Member.php ├── Message.php ├── Official.php └── Special.php ├── Core ├── ContactFactory.php ├── Http.php ├── MessageFactory.php ├── MessageHandler.php ├── Myself.php ├── Server.php └── Sync.php ├── Foundation ├── ServiceProviders │ └── ServerServiceProvider.php └── Vbot.php ├── Message ├── Entity │ ├── Emoticon.php │ ├── File.php │ ├── GroupChange.php │ ├── Image.php │ ├── Location.php │ ├── Message.php │ ├── Mina.php │ ├── NewFriend.php │ ├── Official.php │ ├── Recall.php │ ├── Recommend.php │ ├── RedPacket.php │ ├── RequestFriend.php │ ├── Share.php │ ├── Text.php │ ├── Touch.php │ ├── Transfer.php │ ├── Video.php │ └── Voice.php ├── MediaInterface.php ├── MediaTrait.php ├── MessageInterface.php ├── ShareFactory.php └── UploadAble.php ├── Support ├── Console.php ├── Content.php ├── FileManager.php ├── Path.php └── System.php └── helpers.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .idea 3 | /tmp/ 4 | composer.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 HanSon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | 6 |

7 | Special thanks to the generous sponsorship by: 8 |

9 | 10 | 11 | 12 |

13 | 14 | ## 安装 15 | 16 | ### 环境要求 17 | 18 | - PHP >= 7.0 19 | - [PHP fileinfo 拓展](http://php.net/manual/en/book.fileinfo.php) 储存文件需要用到 20 | - [PHP gd 拓展](http://php.net/manual/en/book.image.php) 控制台显示二维码 21 | - [PHP 系统命令 拓展](https://secure.php.net/manual/en/book.exec.php) 执行clear命令 22 | - [PHP SimpleXML 拓展](https://secure.php.net/manual/en/book.simplexml.php) 解析XML 23 | 24 | ### 安装 25 | 26 | **请确保已经会使用composer!** 27 | 28 | **运行微信账号的语言设置务必设置为简体中文!!否则可能出现未知的错误!** 29 | 30 | 1、git 31 | 32 | ``` 33 | git clone https://github.com/HanSon/vbot.git 34 | cd vbot 35 | composer install 36 | ``` 37 | 38 | 2、composer 39 | 40 | ``` 41 | composer require hanson/vbot 42 | ``` 43 | 44 | ### 运行 45 | 46 | 正常运行 47 | 48 | ``` php example/index.php ``` 49 | 50 | 带session运行 51 | 52 | ``` php example/index.php --session yoursession``` 53 | 54 | 关于session : 55 | 56 | 带session运行会自动寻找设定session指定的cookies,如不存在则新建一个文件夹位于 `/tmp/session` 中,当下次修改代码时再执行就会免扫码自动登录。 57 | 58 | 如果不设置,vbot会自动设置一个6位的字符的session值,下次登录也可以直接设定此值进行面扫码登录。 59 | 60 | PS:运行后二维码将保存于设置的缓存目录,命名为qr.png,控制台也会显示二维码,扫描即可(linux用户请确保已经打开ANSI COLOR) 61 | 62 | *警告!执行前请先查看`index.php`的代码,注释掉你认为不需要的代码,避免对其他人好友造成困扰* 63 | 64 | **请在terminal运行!请在terminal运行!请在terminal运行!** 65 | 66 | 67 | ## 目录结构 68 | 69 | - vbot 70 | - demo (vbot 当前在运行的代码,也欢迎大家提供自己的一些实战例子) 71 | - example (较为初级的实例) 72 | - src (源码) 73 |  - tmp (假设缓存目录设置在此) 74 | - session 75 | - hanson (设定值 `php index.php --session hanson`) 76 | - 523eb1 (随机值) 77 | - users 78 | - 23534234345 (微信账号的UIN值) 79 | - file (文件) 80 | - gif (表情) 81 | - jpg (图片) 82 | - mp3 (语音) 83 | - mp4 (视频) 84 | - contact.json (联系人 debug模式下存在) 85 | - group.json (群组 debug模式下存在) 86 | - member.json (所有群的所有成员 debug模式下存在) 87 | - official.json (公众号 debug模式下存在) 88 | - special.json (特殊账号 debug模式下存在) 89 | - message.json (消息) 90 | 91 | ## 体验 92 | 93 | 94 | 95 | 扫码后,验证输入“上山打老虎”即可自动加为好友并且拉入vbot群。 96 | 97 | vbot并非24小时执行,有时会因为开发调试等原因暂停功能。如果碰巧遇到关闭情况,可加Q群 492548647 了解开放时间。执行后发送“拉我”即可自动邀请进群。 98 | 99 | vbot示例源码为 https://github.com/HanSon/vbot/tree/master/demo/vbot.php 100 | 101 | 102 | ## 文档 103 | 104 | 详细文档在[wiki](https://github.com/HanSon/vbot/wiki)中 105 | 106 | ### 小DEMO 107 | 108 | [vbot 实例](demo/vbot.php) 109 | 110 | [购书半自动处理](http://t.laravel-china.org/laravel-tutorial/5.1/buy-it) 111 | 112 | [红包提醒](example/hongbao.php) 113 | 114 | [轰炸消息到某群名](xample/group.php) 115 | 116 | [消息转发](xample/forward.php) 117 | 118 | [自定义处理器](xample/custom.php) 119 | 120 | [一键拜年](xample/bainian.php) 121 | 122 | [聊天操作](xample/contact.php) 123 | 124 | 125 | ### 基本使用 126 | 127 | ``` 128 | // 图灵API自动回复 129 | require_once __DIR__ . './../vendor/autoload.php'; 130 | 131 | use Hanson\Vbot\Foundation\Vbot; 132 | use Hanson\Vbot\Message\Entity\Message; 133 | use Hanson\Vbot\Message\Entity\Text; 134 | 135 | $robot = new Vbot([ 136 | 'user_path' => '/path/to/tmp/', # 用于生成登录二维码以及文件保存 137 | 'debug' => true # 用于是否输出用户组的json 138 | ]); 139 | 140 | $robot->server->setMessageHandler(function($message){ 141 | // 文字信息 142 | if ($message instanceof Text) { 143 | /** @var $message Text */ 144 | // 联系人自动回复 145 | if ($message->fromType === 'Contact') { 146 | return 'hello vbot'; 147 | // 群组@我回复 148 | } elseif ($message->fromType === 'Group' && $message->isAt) { 149 | return 'hello everyone'; 150 | } 151 | } 152 | }); 153 | 154 | $robot->server->run(); 155 | 156 | ``` 157 | 158 | ## to do list 159 | 160 | vbot 已实现以及待实现的功能列表 [点击查看](https://github.com/HanSon/vbot/wiki/todolist) 161 | 162 | ## 参考项目 163 | 164 | [lbbniu/WebWechat](https://github.com/lbbniu/WebWechat) 165 | 166 | [littlecodersh/ItChat](https://github.com/littlecodersh/ItChat) 167 | 168 | 感谢楼上两位作者曾对本人耐心解答 169 | 170 | [liuwons/wxBot](https://github.com/liuwons/wxBot) 参考了整个微信的登录流程与消息处理 171 | 172 | ## 贡献者 173 | 174 | 排名不分先后,时间排序 175 | 176 | [zhuanxuhit](https://github.com/zhuanxuhit) terminal显示二维码 177 | 178 | [littlecodersh](https://github.com/littlecodersh) 分次加载好友数量方案 179 | 180 | [yuanshi2016](https://github.com/yuanshi2016) 分次加载好友数量方案、登录域名方案以及测试 181 | 182 | ## Q&A 183 | 184 | 常见问题[点击查看](https://github.com/HanSon/vbot/wiki/Q&A) 185 | 186 | 有问题或者建议都可以提issue 187 | 188 | 或者加入vbot的QQ群:492548647 189 | 190 | ## donate 名单 191 | 192 | 193 | vbot 的发展离不开大家的支持,无论是star或者donate,本人都衷心的感谢各位,也会尽自己的绵薄之力把 vbot 做到最好。 194 | 195 | donate 名单 (排名按时间倒序) 196 | 197 | |捐助者|金额| 198 | |-----|----| 199 | |[xingchenboy](https://github.com/xingchenboy)| ¥28.80| 200 | |匿名| ¥6.66| 201 | |[包菜网](http://baocai.us)| ¥16.88| 202 | |[BEIBEI123](https://github.com/beibei123)| ¥28.88| 203 | |[Steven Lei](https://github.com/stevenlei)| ¥88| 204 | |9688| ¥8.88| 205 | |[kisexu](https://github.com/kisexu)| ¥88| 206 | |匿名的某师兄| ¥181.80| 207 | |[summer](https://github.com/summerblue) 以及这是用vbot实现的半自动购书流程[Laravel 入门教程(推荐)](http://t.laravel-china.org/laravel-tutorial/5.1/buy-it)|¥66.66| 208 | |A梦|¥18.88 * 4 | 209 | |[toby2016](https://github.com/toby2016)|¥5| 210 | 211 | 打赏时请记得备注上你的github账号或者其他链接,谢谢支持! 212 | 213 | 214 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hanson/vbot", 3 | "type": "library", 4 | "license": "mit", 5 | "authors": [ 6 | { 7 | "name": "HanSon", 8 | "email": "h@hanc.cc" 9 | } 10 | ], 11 | "require": { 12 | "guzzlehttp/guzzle": "^6.2", 13 | "endroid/qrcode": "^1.7", 14 | "pimple/pimple": "^3.0", 15 | "illuminate/support": "^5.3", 16 | "nesbot/carbon": "^1.21", 17 | "aferrandini/phpqrcode": "^1.0", 18 | "symfony/console": "3.*" 19 | }, 20 | "minimum-stability": "dev", 21 | "autoload": { 22 | "files": [ 23 | "src/helpers.php" 24 | ], 25 | "psr-4": { 26 | "Hanson\\Vbot\\": "src/" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /demo/diancan.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/./../tmp/', 18 | 'debug' => true 19 | ]); 20 | 21 | $isSendToday = false; 22 | $isNewDay = false; 23 | $menu = []; 24 | 25 | function outputMenu($menu, $canteen) 26 | { 27 | $order = $canteen === '都城' ? "当前都城菜单:\n" : "当前龙兴菜单:\n"; 28 | 29 | foreach ($menu as $item) { 30 | if($item['canteen'] === $canteen){ 31 | $order .= "{$item['nickname']} {$item['food']} {$item['price']}\n"; 32 | } 33 | } 34 | 35 | return $order; 36 | } 37 | 38 | function displayName($array) 39 | { 40 | if(isset($array['DisplayName']) && $array['DisplayName']){ 41 | return $array['DisplayName']; 42 | }else { 43 | return $array['NickName']; 44 | } 45 | } 46 | 47 | $robot->server->setCustomerHandler(function () use (&$isSendToday, &$isNewDay, &$menu) { 48 | 49 | if (!$isSendToday && Carbon::now()->gt(Carbon::now()->hour(17)->minute(53))) { 50 | $username = group()->getUsernameByNickname('三年二班'); 51 | Text::send($username, "小伙伴们,报名时间截止到10:30\n 52 | 点餐格式如下:\n 53 | '点餐都城辣子鸡饭16','点餐龙兴冬菇焖鸡饭13'\n 54 | 请记得添加我为好友给我转账当天餐费,加好友验证输入'dmc'即可自动添加好友\n 55 | 想取消点餐输入'取消点餐'即可"); 56 | Image::send($username, __DIR__ . '/extra/longxing.jpg'); 57 | Image::send($username, __DIR__ . '/extra/ducheng.png'); 58 | $isSendToday = true; 59 | } 60 | 61 | if (!Carbon::now()->hour) { 62 | $isNewDay = true; 63 | } 64 | 65 | if ($isNewDay) { 66 | $isSendToday = false; 67 | $menu = []; 68 | } 69 | }); 70 | 71 | $robot->server->setMessageHandler(function ($message) use (&$menu) { 72 | if ($message instanceof Text) { 73 | /** @var $message Text */ 74 | if($message->from['NickName'] === '三年二班'){ 75 | if (starts_with($message->content, '点餐')) { 76 | $content = str_replace('点餐', '', $message->content); 77 | $canteen = substr($content, 0, 6); 78 | if (in_array($canteen, ['龙兴', '都城'])) { 79 | $foodAndPrice = str_replace($canteen, '', $content); 80 | $isMatch = preg_match('/(.+)(\d+)/s', $foodAndPrice, $match); 81 | if (!$isMatch) { 82 | return '点餐格式不对 或者 请在最后输入价格!'; 83 | } 84 | 85 | $menu[$message->sender['UserName']]['nickname'] = displayName($message->sender); 86 | $menu[$message->sender['UserName']]['canteen'] = $canteen; 87 | $menu[$message->sender['UserName']]['food'] = $match[1]; 88 | $menu[$message->sender['UserName']]['price'] = $match[2]; 89 | 90 | Text::send($message->from['UserName'], '点餐成功!'); 91 | 92 | return outputMenu($menu, $canteen); 93 | } else { 94 | return '不存在此菜单'; 95 | } 96 | }elseif ($message->content === '取消点餐'){ 97 | if(isset($menu[$message->sender['UserName']])){ 98 | $canteen = $menu[$message->sender['UserName']]['canteen']; 99 | unset($menu[$message->sender['UserName']]); 100 | Text::send($message->from['UserName'], '取消点餐成功!'); 101 | return outputMenu($menu, $canteen); 102 | }else{ 103 | return '你没有点餐呢!'; 104 | } 105 | }else{ 106 | \Hanson\Vbot\Support\Console::debug($message->content); 107 | } 108 | }else{ 109 | \Hanson\Vbot\Support\Console::debug($message->from['NickName']); 110 | } 111 | } 112 | }); 113 | 114 | $robot->server->run(); 115 | -------------------------------------------------------------------------------- /demo/extra/ducheng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/vbot/aac0df98665be2c814c56b6aaf3b977565ab733a/demo/extra/ducheng.png -------------------------------------------------------------------------------- /demo/extra/longxing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easychen/vbot/aac0df98665be2c814c56b6aaf3b977565ab733a/demo/extra/longxing.jpg -------------------------------------------------------------------------------- /demo/vbot.php: -------------------------------------------------------------------------------- 1 | $path, 34 | 'debug' => true 35 | ]); 36 | 37 | // 图灵自动回复 38 | function reply($str) 39 | { 40 | $result = http()->post('http://www.tuling123.com/openapi/api', [ 41 | 'key' => '1dce02aef026258eff69635a06b0ab7d', 42 | 'info' => $str 43 | ], true); 44 | 45 | return isset($result['url']) ? $result['text'] . $result['url'] : $result['text']; 46 | } 47 | 48 | // 设置管理员 49 | function isAdmin($message) 50 | { 51 | $adminAlias = 'hanson1994'; 52 | 53 | if (in_array($message->fromType, ['Contact', 'Group'])) { 54 | if ($message->fromType === 'Contact') { 55 | return $message->from['Alias'] === $adminAlias; 56 | } else { 57 | return isset($message->sender['Alias']) && $message->sender['Alias'] === $adminAlias; 58 | } 59 | } 60 | 61 | return false; 62 | } 63 | 64 | $groupMap = [ 65 | [ 66 | 'nickname' => 'vbot 测试群', 67 | 'id' => 1 68 | ] 69 | ]; 70 | 71 | $robot->server->setOnceHandler(function () use ($groupMap) { 72 | 73 | group()->each(function ($group, $key) use ($groupMap) { 74 | foreach ($groupMap as $map) { 75 | if ($group['NickName'] === $map['nickname']) { 76 | $group['id'] = $map['id']; 77 | $groupMap[$key] = $map['id']; 78 | group()->setMap($key, $map['id']); 79 | } 80 | } 81 | return $group; 82 | }); 83 | }); 84 | 85 | $robot->server->setMessageHandler(function ($message) use ($path) { 86 | /** @var $message Message */ 87 | 88 | // 位置信息 返回位置文字 89 | if ($message instanceof Location) { 90 | /** @var $message Location */ 91 | Text::send($message->from['UserName'], '地图链接:' . $message->url); 92 | return '位置:' . $message; 93 | } 94 | 95 | // 文字信息 96 | if ($message instanceof Text) { 97 | /** @var $message Text */ 98 | 99 | if ($message->from['NickName'] === '华广stackoverflow' && preg_match('/@(.+)\s加人(.+)/', $message->content, $match)){ 100 | $nickname = $match[1]; 101 | $members = group()->getMembersByNickname($message->from['UserName'], $nickname); 102 | if ($members) { 103 | $member = current($members); 104 | contact()->add($member['UserName'], $match[2]); 105 | } 106 | } 107 | 108 | if (str_contains($message->content, 'vbot') && !$message->isAt) { 109 | return "你好,我叫vbot,我爸是HanSon\n我的项目地址是 https://github.com/HanSon/vbot \n欢迎来给我star!"; 110 | } 111 | 112 | // 联系人自动回复 113 | if ($message->fromType === 'Contact') { 114 | if ($message->content === '拉我') { 115 | $username = group()->getUsernameById(1); 116 | 117 | group()->addMember($username, $message->from['UserName']); 118 | return false; 119 | } 120 | 121 | return reply($message->content); 122 | // 群组@我回复 123 | } elseif ($message->fromType === 'Group') { 124 | 125 | if (str_contains($message->content, '设置群名称')) { 126 | if (isAdmin($message)) { 127 | group()->setGroupName($message->from['UserName'], str_replace('设置群名称', '', $message->content)); 128 | } else { 129 | return '你没有此权限'; 130 | } 131 | } 132 | 133 | if (str_contains($message->content, '搜人')) { 134 | $nickname = str_replace('搜人', '', $message->content); 135 | $members = group()->getMembersByNickname($message->from['UserName'], $nickname, true); 136 | $result = '搜索结果 数量:' . count($members) . "\n"; 137 | foreach ($members as $member) { 138 | $result .= $member['NickName'] . ' ' . $member['UserName'] . "\n"; 139 | } 140 | return $result; 141 | } 142 | 143 | if (str_contains($message->content, '踢人')) { 144 | if (isAdmin($message)) { 145 | $username = str_replace('踢人', '', $message->content); 146 | group()->deleteMember($message->from['UserName'], $username); 147 | } else { 148 | return '你没有此权限'; 149 | } 150 | } 151 | 152 | if (str_contains($message->content, '踢我') && $message->isAt) { 153 | Text::send($message->from['UserName'], '拜拜 ' . $message->sender['NickName']); 154 | group()->deleteMember($message->from['UserName'], $message->sender['UserName']); 155 | } 156 | 157 | if (substr($message->content, 0, 1) === '@' && preg_match('/@(.+)\s自作孽不可活/', $message->content, $match)) { 158 | if (isAdmin($message)) { 159 | $nickname = $match[1]; 160 | $members = group()->getMembersByNickname($message->from['UserName'], $nickname); 161 | if ($members) { 162 | $member = current($members); 163 | Text::send($message->from['UserName'], '拜拜 ' . $member['NickName'] . ' ,君让臣死,臣不得不死'); 164 | group()->deleteMember($message->from['UserName'], $member['UserName']); 165 | } 166 | } else { 167 | return '你没有此权限'; 168 | } 169 | } 170 | 171 | if ($message->isAt) { 172 | return reply($message->content); 173 | } 174 | } 175 | } 176 | 177 | // 表情信息 返回接收到的表情 178 | if ($message instanceof Emoticon && random_int(0, 1) && random_int(0, 1)) { 179 | Emoticon::sendRandom($message->from['UserName']); 180 | } 181 | 182 | // 撤回信息 183 | if ($message instanceof Recall && $message->raw['FromUserName'] !== myself()->username) { 184 | /** @var $message Recall */ 185 | if ($message->origin instanceof Image) { 186 | Text::send($message->raw['FromUserName'], "{$message->nickname} 撤回了一张照片"); 187 | Image::sendByMsgId($message->raw['FromUserName'], $message->origin->raw['MsgId']); 188 | } elseif ($message->origin instanceof Emoticon) { 189 | Text::send($message->raw['FromUserName'], "{$message->nickname} 撤回了一个表情"); 190 | Emoticon::sendByMsgId($message->raw['FromUserName'], $message->origin->raw['MsgId']); 191 | } elseif ($message->origin instanceof Video) { 192 | Text::send($message->raw['FromUserName'], "{$message->nickname} 撤回了一个视频"); 193 | Video::sendByMsgId($message->raw['FromUserName'], $message->origin->raw['MsgId']); 194 | } elseif ($message->origin instanceof Voice) { 195 | Text::send($message->raw['FromUserName'], "{$message->nickname} 撤回了一条语音"); 196 | } else { 197 | Text::send($message->raw['FromUserName'], "{$message->nickname} 撤回了一条信息 \"{$message->origin->message}\""); 198 | } 199 | } 200 | 201 | // 红包信息 202 | if ($message instanceof RedPacket) { 203 | return $message->content . ' 来自 ' . $message->from['NickName']; 204 | } 205 | 206 | // 转账信息 207 | if ($message instanceof Transfer) { 208 | /** @var $message Transfer */ 209 | return $message->content . ' 收到金额 ' . $message->fee . ' 转账说明: ' . $message->memo ?: '空'; 210 | } 211 | 212 | // 推荐名片信息 213 | if ($message instanceof Recommend) { 214 | /** @var $message Recommend */ 215 | if ($message->isOfficial) { 216 | return $message->from['NickName'] . ' 向你推荐了公众号 ' . $message->province . $message->city . 217 | " {$message->info['NickName']} 公众号信息: {$message->description}"; 218 | } else { 219 | return $message->from['NickName'] . ' 向你推荐了 ' . $message->province . $message->city . 220 | " {$message->info['NickName']} 头像链接: {$message->bigAvatar}"; 221 | } 222 | } 223 | 224 | // 请求添加信息 225 | if ($message instanceof RequestFriend) { 226 | /** @var $message RequestFriend */ 227 | 228 | if ($message->info['Content'] === '上山打老虎') { 229 | $message->verifyUser($message::VIA); 230 | } else { 231 | } 232 | } 233 | 234 | //分享信息 235 | if ($message instanceof Share) { 236 | /** @var $message Share */ 237 | $reply = "收到分享\n标题:{$message->title}\n描述:{$message->description}\n链接:{$message->url}"; 238 | if ($message->app) { 239 | $reply .= "\n来源APP:{$message->app}"; 240 | } 241 | return $reply; 242 | } 243 | 244 | // 分享小程序信息 245 | if ($message instanceof Mina) { 246 | /** @var $message Mina */ 247 | $reply = "收到小程序\n小程序名词:{$message->title}\n链接:{$message->url}"; 248 | return $reply; 249 | } 250 | 251 | // 新增好友 252 | if ($message instanceof NewFriend) { 253 | \Hanson\Vbot\Support\Console::debug('新加好友:' . $message->from['NickName']); 254 | Text::send($message->from['UserName'], "客官,等你很久了!感谢跟 vbot 交朋友,如果可以帮我点个star,谢谢了!https://github.com/HanSon/vbot"); 255 | group()->addMember(group()->getUsernameById(1), $message->from['UserName']); 256 | return '现在拉你进去vbot的测试群,进去后为了避免轰炸记得设置免骚扰哦!如果被不小心踢出群,跟我说声“拉我”我就会拉你进群的了。'; 257 | } 258 | 259 | // 群组变动 260 | if ($message instanceof GroupChange) { 261 | /** @var $message GroupChange */ 262 | if ($message->action === 'ADD') { 263 | \Hanson\Vbot\Support\Console::debug('新人进群'); 264 | if ($message->from['NickName'] === '华广stackoverflow') { 265 | return "欢迎 {$message->nickname} 同学加入华广技术交流群!我是这里的群管家vbot,进群先给我点个star吧, https://github.com/HanSon/vbot"; 266 | } else { 267 | return '欢迎新人 ' . $message->nickname; 268 | } 269 | } elseif ($message->action === 'REMOVE') { 270 | \Hanson\Vbot\Support\Console::debug('群主踢人了'); 271 | return $message->content; 272 | } elseif ($message->action === 'RENAME') { 273 | // \Hanson\Vbot\Support\Console::log($message->from['NickName'] . ' 改名为 ' . $message->rename); 274 | if (group()->getUsernameById(1) == $message->from['UserName'] && $message->rename !== 'vbot 测试群') { 275 | group()->setGroupName($message->from['UserName'], 'vbot 测试群'); 276 | return '行不改名,坐不改姓!'; 277 | } 278 | } elseif ($message->action === 'BE_REMOVE') { 279 | \Hanson\Vbot\Support\Console::debug('你被踢出了群 ' . $message->group['NickName']); 280 | } elseif ($message->action === 'INVITE') { 281 | \Hanson\Vbot\Support\Console::debug('你被邀请进群 ' . $message->from['NickName']); 282 | } 283 | } 284 | 285 | return false; 286 | 287 | }); 288 | 289 | $robot->server->setExitHandler(function () { 290 | \Hanson\Vbot\Support\Console::log('其他设备登录'); 291 | }); 292 | 293 | $robot->server->setExceptionHandler(function () { 294 | \Hanson\Vbot\Support\Console::log('异常退出'); 295 | }); 296 | 297 | $robot->server->run(); 298 | -------------------------------------------------------------------------------- /example/bainian.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/./../tmp/', 16 | 'debug' => true 17 | ]); 18 | 19 | $robot->server->setCustomerHandler(function () { 20 | $whiteList = ['some remark name...', 'some remark name...']; 21 | $blackList = ['some remark name...', 'some remark name...']; 22 | contact()->each(function($item, $username) use ($whiteList, $blackList){ 23 | // 发送白名单 24 | if($item['RemarkName'] && in_array($item['RemarkName'], $whiteList)){ 25 | Text::send($username, $item['RemarkName'] . ' 新年快乐'); 26 | sleep(2); 27 | } 28 | // 黑名单不发送 29 | // if($item['RemarkName'] && !in_array($item['RemarkName'], $blackList)){ 30 | // Text::send($username, $item['RemarkName'] . ' 新年快乐'); 31 | // } 32 | // 全部人发送 33 | // if($item['RemarkName']){ 34 | // Text::send($username, $item['RemarkName'] . ' 新年快乐'); 35 | // } 36 | }); 37 | exit; 38 | }); 39 | 40 | $robot->server->run(); 41 | -------------------------------------------------------------------------------- /example/custom.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/./../tmp/', 16 | ]); 17 | 18 | $flag = false; 19 | 20 | $robot->server->setCustomerHandler(function() use (&$flag){ 21 | // RemarkName,代表的改用户在你通讯录的名字 22 | $contact = contact()->getUsernameByRemarkName('hanson'); 23 | if ($contact === false){ 24 | echo("找不到你要的联系人,请确认联系人姓名"); 25 | return; 26 | } 27 | if(!$flag){ 28 | Text::send($contact, '来轰炸吧'); 29 | $flag = true; 30 | } 31 | 32 | Text::send($contact, '测试' . \Carbon\Carbon::now()->toDateTimeString()); 33 | 34 | }); 35 | 36 | $robot->server->run(); 37 | -------------------------------------------------------------------------------- /example/forward.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/./../tmp/', 16 | 'debug' => true 17 | ]); 18 | 19 | $robot->server->setMessageHandler(function ($message) { 20 | if ($message instanceof Text) { 21 | /** @var $message Text */ 22 | $contact = contact()->getUsernameByAlias('hanson'); 23 | Text::send($contact, $message); 24 | } 25 | }); 26 | 27 | $robot->server->run(); 28 | -------------------------------------------------------------------------------- /example/group.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/./../tmp/', 17 | 'debug' => true 18 | ]); 19 | 20 | $robot->server->setMessageHandler(function ($message) { 21 | if(str_contains($message->content, '设置备注')){ 22 | $result = contact()->setRemarkName($message->from['UserName'], str_replace('设置备注', '', $message->content)); 23 | Console::log('设置备注:' . ($result ? '成功' : '失败')); 24 | } 25 | 26 | if(str_contains($message->content, '设置置顶')){ 27 | $result = contact()->setStick($message->from['UserName']); 28 | Console::log('设置置顶:' . ($result ? '成功' : '失败')); 29 | } 30 | 31 | if(str_contains($message->content, '取消置顶')){ 32 | $result = contact()->setStick($message->from['UserName'], false); 33 | Console::log('取消置顶:' . ($result ? '成功' : '失败')); 34 | } 35 | 36 | if(str_contains($message->content, '拉群测试')){ 37 | $username[] = contact()->getUsernameByAlias('...'); 38 | $username[] = contact()->getUsernameByAlias('...'); 39 | $group = group()->create($username); 40 | Text::send($group['UserName'], '创建群聊天成功'); 41 | } 42 | 43 | if(str_contains($message->content, '拉人')){ 44 | $nicknames = explode(',', str_replace('拉人', '', $message->content)); 45 | $members = []; 46 | foreach ($nicknames as $nickname) { 47 | $members[] = contact()->getUsernameByNickname($nickname); 48 | } 49 | $result = group()->addMember($message->from['UserName'], $members); 50 | Console::log($result ? '拉人成功' : '拉人失败'); 51 | } 52 | 53 | if(str_contains($message->content, '踢人')){ 54 | $nicknames = explode(',', str_replace('踢人', '', $message->content)); 55 | $members = []; 56 | foreach ($nicknames as $nickname) { 57 | $members[] = contact()->getUsernameByNickname($nickname); 58 | } 59 | $result = group()->deleteMember($message->from['UserName'], $members); 60 | Console::log($result ? '踢人成功' : '踢人失败'); 61 | } 62 | 63 | if(str_contains($message->content, '设置群名称')){ 64 | $result = group()->setGroupName($message->from['UserName'], str_replace('设置群名称', '', $message->content)); 65 | Console::log('设置群名称:' . ($result ? '成功' : '失败')); 66 | } 67 | 68 | }); 69 | 70 | $robot->server->run(); 71 | -------------------------------------------------------------------------------- /example/index.php: -------------------------------------------------------------------------------- 1 | $path, 34 | 'debug' => true 35 | ]); 36 | 37 | // 图灵自动回复 38 | function reply($str) 39 | { 40 | return http()->post('http://www.tuling123.com/openapi/api', [ 41 | 'key' => '1dce02aef026258eff69635a06b0ab7d', 42 | 'info' => $str 43 | ], true)['text']; 44 | 45 | } 46 | 47 | $robot->server->setMessageHandler(function ($message) use ($path) { 48 | /** @var $message Message */ 49 | 50 | // 位置信息 返回位置文字 51 | if ($message instanceof Location) { 52 | /** @var $message Location */ 53 | Text::send($message->from['UserName'], '地图链接:' . $message->url); 54 | return '位置:' . $message; 55 | } 56 | 57 | // 文字信息 58 | if ($message instanceof Text) { 59 | /** @var $message Text */ 60 | // 联系人自动回复 61 | if ($message->fromType === 'Contact') { 62 | return reply($message->content); 63 | // 群组@我回复 64 | } elseif ($message->fromType === 'Group') { 65 | 66 | if (str_contains($message->content, '设置群名称') && $message->from['Alias'] === 'hanson1994') { 67 | group()->setGroupName($message->from['UserName'], str_replace('设置群名称', '', $message->content)); 68 | } 69 | 70 | if ($message->isAt) { 71 | return reply($message->content); 72 | } 73 | } 74 | } 75 | 76 | // 图片信息 返回接收到的图片 77 | if ($message instanceof Image) { 78 | // return $message; 79 | } 80 | 81 | // 视频信息 返回接收到的视频 82 | if ($message instanceof Video) { 83 | // return $message; 84 | } 85 | 86 | // 表情信息 返回接收到的表情 87 | if ($message instanceof Emoticon) { 88 | Emoticon::sendRandom($message->from['UserName']); 89 | } 90 | 91 | // 语音消息 92 | if ($message instanceof Voice) { 93 | /** @var $message Voice */ 94 | // return '收到一条语音并下载在' . $message::getPath($message::$folder) . "/{$message->msg['MsgId']}.mp3"; 95 | } 96 | 97 | // 撤回信息 98 | if ($message instanceof Recall && $message->msg['FromUserName'] !== myself()->username) { 99 | /** @var $message Recall */ 100 | if ($message->origin instanceof Image) { 101 | Text::send($message->msg['FromUserName'], "{$message->nickname} 撤回了一张照片"); 102 | Image::sendByMsgId($message->msg['FromUserName'], $message->origin->msg['MsgId']); 103 | } elseif ($message->origin instanceof Emoticon) { 104 | Text::send($message->msg['FromUserName'], "{$message->nickname} 撤回了一个表情"); 105 | Emoticon::sendByMsgId($message->msg['FromUserName'], $message->origin->msg['MsgId']); 106 | } elseif ($message->origin instanceof Video) { 107 | Text::send($message->msg['FromUserName'], "{$message->nickname} 撤回了一个视频"); 108 | Video::sendByMsgId($message->msg['FromUserName'], $message->origin->msg['MsgId']); 109 | } elseif ($message->origin instanceof Voice) { 110 | Text::send($message->msg['FromUserName'], "{$message->nickname} 撤回了一条语音"); 111 | } else { 112 | Text::send($message->msg['FromUserName'], "{$message->nickname} 撤回了一条信息 \"{$message->origin->msg['Content']}\""); 113 | } 114 | } 115 | 116 | // 红包信息 117 | if ($message instanceof RedPacket) { 118 | // do something to notify if you want ... 119 | return $message->content . ' 来自 ' . $message->from['NickName']; 120 | } 121 | 122 | // 转账信息 123 | if ($message instanceof Transfer) { 124 | /** @var $message Transfer */ 125 | return $message->content . ' 收到金额 ' . $message->fee; 126 | } 127 | 128 | // 推荐名片信息 129 | if ($message instanceof Recommend) { 130 | /** @var $message Recommend */ 131 | if ($message->isOfficial) { 132 | return $message->from['NickName'] . ' 向你推荐了公众号 ' . $message->province . $message->city . 133 | " {$message->info['NickName']} 公众号信息: {$message->description}"; 134 | } else { 135 | return $message->from['NickName'] . ' 向你推荐了 ' . $message->province . $message->city . 136 | " {$message->info['NickName']} 头像链接: {$message->bigAvatar}"; 137 | } 138 | } 139 | 140 | // 请求添加信息 141 | if ($message instanceof RequestFriend) { 142 | /** @var $message RequestFriend */ 143 | 144 | if ($message->info['Content'] === '上山打老虎') { 145 | $message->verifyUser($message::VIA); 146 | } 147 | } 148 | 149 | // 分享信息 150 | if ($message instanceof Share) { 151 | /** @var $message Share */ 152 | $reply = "收到分享\n标题:{$message->title}\n描述:{$message->description}\n链接:{$message->url}"; 153 | if ($message->app) { 154 | $reply .= "\n来源APP:{$message->app}"; 155 | } 156 | return $reply; 157 | } 158 | 159 | // 分享小程序信息 160 | if ($message instanceof Mina) { 161 | /** @var $message Mina */ 162 | $reply = "收到小程序\n小程序名词:{$message->title}\n链接:{$message->url}"; 163 | return $reply; 164 | } 165 | 166 | // 公众号推送信息 167 | if ($message instanceof Official) { 168 | /** @var $message Official */ 169 | $reply = "收到公众号推送\n标题:{$message->title}\n描述:{$message->description}\n链接:{$message->url}\n来源公众号名称:{$message->app}"; 170 | return $reply; 171 | } 172 | 173 | // 手机点击聊天事件 174 | if ($message instanceof Touch) { 175 | // Text::send($message->msg['ToUserName'], "我点击了此聊天"); 176 | } 177 | 178 | // 新增好友 179 | if ($message instanceof \Hanson\Vbot\Message\Entity\NewFriend) { 180 | \Hanson\Vbot\Support\Console::log('新加好友:' . $message->from['NickName']); 181 | } 182 | 183 | // 群组变动 184 | if ($message instanceof GroupChange) { 185 | /** @var $message GroupChange */ 186 | if ($message->action === 'ADD') { 187 | \Hanson\Vbot\Support\Console::log('新人进群'); 188 | return '欢迎新人 ' . $message->nickname; 189 | } elseif ($message->action === 'REMOVE') { 190 | \Hanson\Vbot\Support\Console::log('群主踢人了'); 191 | return $message->content; 192 | } elseif ($message->action === 'RENAME') { 193 | // \Hanson\Vbot\Support\Console::log($message->from['NickName'] . ' 改名为 ' . $message->rename); 194 | if ($message->rename !== 'vbot 测试群'){ 195 | group()->setGroupName($message->from['UserName'], 'vbot 测试群'); 196 | return '行不改名,坐不改姓!'; 197 | } 198 | } 199 | } 200 | 201 | return false; 202 | 203 | }); 204 | 205 | $robot->server->setExitHandler(function () { 206 | \Hanson\Vbot\Support\Console::log('其他设备登录'); 207 | }); 208 | 209 | $robot->server->setExceptionHandler(function () { 210 | \Hanson\Vbot\Support\Console::log('异常退出'); 211 | }); 212 | 213 | $robot->server->run(); 214 | -------------------------------------------------------------------------------- /example/qunfa.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/./../tmp/', 17 | 'debug' => true 18 | ]); 19 | 20 | $robot->server->setCustomerHandler(function(){ 21 | 22 | contact()->each(function($item, $username){ 23 | $word = '新年快乐'; 24 | Console::log("send to username: $username nickname:{$item['NickName']}"); 25 | Text::send($username, $word); 26 | sleep(2); 27 | }); 28 | exit; 29 | 30 | }); 31 | 32 | $robot->server->run(); 33 | -------------------------------------------------------------------------------- /src/Collections/Account.php: -------------------------------------------------------------------------------- 1 | get($username); 44 | }else{ 45 | $account = contact()->get($username, null); 46 | 47 | $account = $account ?: member()->get($username, null); 48 | 49 | $account = $account ?: official()->get($username, null); 50 | 51 | return $account ?: Special::getInstance()->get($username, null); 52 | } 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /src/Collections/BaseCollection.php: -------------------------------------------------------------------------------- 1 | getUsername($nickname, 'NickName', $blur); 23 | } 24 | 25 | /** 26 | * 根据备注获取对象 27 | * 28 | * @param $remark 29 | * @param $blur 30 | * @return mixed 31 | */ 32 | public function getUsernameByRemarkName($remark, $blur = false) 33 | { 34 | return $this->getUsername($remark, 'RemarkName', $blur); 35 | } 36 | 37 | /** 38 | * 获取Username 39 | * 40 | * @param $search 41 | * @param $key 42 | * @param bool $blur 43 | * @return string 44 | */ 45 | public function getUsername($search, $key, $blur = false) 46 | { 47 | return $this->search(function ($item) use ($search, $key, $blur) { 48 | 49 | if (!isset($item[$key])) return false; 50 | 51 | if ($blur && str_contains($item[$key], $search)) { 52 | return true; 53 | } elseif (!$blur && $item[$key] === $search) { 54 | return true; 55 | } 56 | 57 | return false; 58 | }); 59 | } 60 | 61 | /** 62 | * 获取整个数组 63 | * 64 | * @param $search 65 | * @param $key 66 | * @param bool $first 67 | * @param bool $blur 68 | * @return mixed|static 69 | */ 70 | public function getObject($search, $key, $first = false, $blur = false) 71 | { 72 | $objects = $this->filter(function ($item) use ($search, $key, $blur) { 73 | 74 | if (!isset($item[$key])) return false; 75 | 76 | if ($blur && str_contains($item[$key], $search)) { 77 | return true; 78 | } elseif (!$blur && $item[$key] === $search) { 79 | return true; 80 | } 81 | 82 | return false; 83 | }); 84 | 85 | return $first ? $objects->first() : $objects; 86 | } 87 | 88 | /** 89 | * 存储时处理emoji 90 | * 91 | * @param mixed $key 92 | * @param mixed $value 93 | * @return Collection 94 | */ 95 | public function put($key, $value) 96 | { 97 | $value = $this->format($value); 98 | 99 | return parent::put($key, $value); 100 | } 101 | 102 | /** 103 | * 处理联系人 104 | * 105 | * @param $contact 106 | * @return mixed 107 | */ 108 | public function format($contact) 109 | { 110 | if (isset($contact['DisplayName'])) { 111 | $contact['DisplayName'] = Content::emojiHandle($contact['DisplayName']); 112 | } 113 | 114 | if (isset($contact['RemarkName'])) { 115 | $contact['RemarkName'] = Content::emojiHandle($contact['RemarkName']); 116 | } 117 | 118 | if (isset($contact['Signature'])) { 119 | $contact['Signature'] = Content::emojiHandle($contact['Signature']); 120 | } 121 | 122 | $contact['NickName'] = Content::emojiHandle($contact['NickName']); 123 | 124 | return $contact; 125 | } 126 | 127 | /** 128 | * 通过接口更新群组信息 129 | * 130 | * @param $username 131 | * @param $list 132 | * @return array 133 | */ 134 | public function update($username, $list) :array 135 | { 136 | if(is_string($username)) 137 | $username = [$username]; 138 | 139 | $url = server()->baseUri . '/webwxbatchgetcontact?type=ex&r=' . time(); 140 | 141 | $data = [ 142 | 'BaseRequest' => server()->baseRequest, 143 | 'Count' => count($username), 144 | 'List' => $list 145 | ]; 146 | 147 | $response = http()->json($url, $data, true); 148 | 149 | foreach ($response['ContactList'] as $item) { 150 | $this->put($item['UserName'], $item); 151 | } 152 | 153 | return is_string($username) ? head($response['ContactList']) : $response['ContactList']; 154 | } 155 | 156 | } -------------------------------------------------------------------------------- /src/Collections/Contact.php: -------------------------------------------------------------------------------- 1 | getContactByAlias($alias); 43 | } 44 | 45 | /** 46 | * 根据微信号获取联系人 47 | * 48 | * @param $alias 49 | * @return mixed 50 | */ 51 | public function getContactByAlias($alias) 52 | { 53 | return $this->getObject($alias, 'Alias', true); 54 | } 55 | 56 | /** 57 | * 根据微信号获取联系username 58 | * @deprecated 59 | * @param $alias 60 | * @return mixed 61 | */ 62 | public function getUsernameById($alias) 63 | { 64 | return $this->getUsernameByAlias($alias); 65 | } 66 | 67 | /** 68 | * 根据微信号获取联系username 69 | * 70 | * @param $alias 71 | * @return mixed 72 | */ 73 | public function getUsernameByAlias($alias) 74 | { 75 | return $this->getUsername($alias, 'Alias'); 76 | } 77 | 78 | /** 79 | * 设置备注 80 | * 81 | * @param $username 82 | * @param $remarkName 83 | * @return bool 84 | */ 85 | public function setRemarkName($username, $remarkName) 86 | { 87 | $url = sprintf('%s/webwxoplog?lang=zh_CN&pass_ticket=%s', server()->baseUri, server()->passTicket); 88 | 89 | $result = http()->post($url, json_encode([ 90 | 'UserName' => $username, 91 | 'CmdId' => 2, 92 | 'RemarkName' => $remarkName, 93 | 'BaseRequest' => server()->baseRequest 94 | ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true); 95 | 96 | return $result['BaseResponse']['Ret'] == 0; 97 | } 98 | 99 | /** 100 | * 设置是否置顶 101 | * 102 | * @param $username 103 | * @param bool $isStick 104 | * @return bool 105 | */ 106 | public function setStick($username, $isStick = true) 107 | { 108 | $url = sprintf('%s/webwxoplog?lang=zh_CN&pass_ticket=%s', server()->baseUri, server()->passTicket); 109 | 110 | $result = http()->json($url, [ 111 | 'UserName' => $username, 112 | 'CmdId' => 3, 113 | 'OP' => (int)$isStick, 114 | 'BaseRequest' => server()->baseRequest 115 | ], true); 116 | 117 | return $result['BaseResponse']['Ret'] == 0; 118 | } 119 | 120 | /** 121 | * 主动添加好友 122 | * 123 | * @param $username 124 | * @param null $content 125 | */ 126 | public function add($username, $content = null) 127 | { 128 | $this->verifyUser($username, $content); 129 | } 130 | 131 | /** 132 | * 验证通过好友 133 | * 134 | * @param $username 135 | * @param null $content 136 | * @return bool 137 | */ 138 | public function verifyUser($username, $content = null) 139 | { 140 | $url = sprintf(server()->baseUri . '/webwxverifyuser?lang=zh_CN&r=%s', time() * 1000); 141 | $data = [ 142 | 'BaseRequest' => server()->baseRequest, 143 | 'Opcode' => 2, 144 | 'VerifyUserListSize' => 1, 145 | 'VerifyUserList' => $this->verifyTicket($username), 146 | 'VerifyContent' => $content, 147 | 'SceneListCount' => 1, 148 | 'SceneList' => [33], 149 | 'skey' => server()->skey 150 | ]; 151 | 152 | $result = http()->post($url, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true); 153 | 154 | return $result['BaseResponse']['Ret'] == 0; 155 | } 156 | 157 | /** 158 | * 返回通过好友申请所需的数组 159 | * 160 | * @param null $username 161 | * @return array 162 | */ 163 | public function verifyTicket($username) 164 | { 165 | return [ 166 | 'Value' => $username, 167 | 'VerifyUserTicket' => '' 168 | ]; 169 | } 170 | 171 | 172 | /** 173 | * 更新群组 174 | * 175 | * @param $username 176 | * @param null $list 177 | * @return array 178 | */ 179 | public function update($username, $list = null) :array 180 | { 181 | $username = is_array($username) ?: [$username]; 182 | return parent::update($username, $this->makeUsernameList($username)); 183 | } 184 | 185 | /** 186 | * 生成username list 格式 187 | * 188 | * @param $username 189 | * @return array 190 | */ 191 | public function makeUsernameList($username) 192 | { 193 | $usernameList = []; 194 | 195 | foreach ($username as $item) { 196 | $usernameList[] = ['UserName' => $item, 'EncryChatRoomId' => '']; 197 | } 198 | 199 | return $usernameList; 200 | } 201 | 202 | } -------------------------------------------------------------------------------- /src/Collections/Group.php: -------------------------------------------------------------------------------- 1 | id 21 | * 22 | * @var array 23 | */ 24 | public $map = []; 25 | 26 | /** 27 | * create a single instance 28 | * 29 | * @return Group 30 | */ 31 | public static function getInstance() 32 | { 33 | if(static::$instance === null){ 34 | static::$instance = new Group(); 35 | } 36 | 37 | return static::$instance; 38 | } 39 | 40 | /** 41 | * 判断是否群组 42 | * 43 | * @param $userName 44 | * @return bool 45 | */ 46 | public static function isGroup($userName){ 47 | return strstr($userName, '@@') !== false; 48 | } 49 | 50 | /** 51 | * 根据群名筛选群组 52 | * 53 | * @param $nickname 54 | * @param bool $blur 55 | * @return static 56 | */ 57 | public function getGroupsByNickname($nickname, $blur = false) 58 | { 59 | return $this->getObject($nickname, 'NickName', false, $blur); 60 | } 61 | 62 | /** 63 | * 根据昵称搜索群成员 64 | * 65 | * @param $groupUsername 66 | * @param $memberNickname 67 | * @param bool $blur 68 | * @return array|bool 69 | */ 70 | public function getMembersByNickname($groupUsername, $memberNickname, $blur = false) 71 | { 72 | $group = $this->get($groupUsername); 73 | 74 | if(!$group) return false; 75 | 76 | $result = []; 77 | 78 | foreach ($group['MemberList'] as $member) { 79 | if ($blur && str_contains($member['NickName'], $memberNickname)) { 80 | $result[] = $member; 81 | } elseif (!$blur && $member['NickName'] === $memberNickname) { 82 | $result[] = $member; 83 | } 84 | } 85 | 86 | return $result; 87 | } 88 | 89 | /** 90 | * 根据ID获取群username 91 | * 92 | * @param $id 93 | * @return mixed 94 | */ 95 | public function getUsernameById($id) 96 | { 97 | return array_search($id, $this->map); 98 | } 99 | 100 | /** 101 | * 设置map 102 | * 103 | * @param $username 104 | * @param $id 105 | */ 106 | public function setMap($username, $id) 107 | { 108 | $this->map[$username] = $id; 109 | } 110 | 111 | /** 112 | * 创建群聊天 113 | * 114 | * @param array $contacts 115 | * @return bool 116 | */ 117 | public function create(array $contacts) 118 | { 119 | $url = sprintf('%s/webwxcreatechatroom?lang=zh_CN&r=%s', server()->baseUri, time()); 120 | 121 | $result = http()->json($url, [ 122 | 'MemberCount' => count($contacts), 123 | 'MemberList' => $this->makeMemberList($contacts), 124 | 'Topic' => '', 125 | 'BaseRequest' => server()->baseRequest 126 | ], true); 127 | 128 | if($result['BaseResponse']['Ret'] != 0){ 129 | return false; 130 | } 131 | 132 | return $this->add($result['ChatRoomName']); 133 | } 134 | 135 | /** 136 | * 删除群成员 137 | * 138 | * @param $group 139 | * @param $members 140 | * @return bool 141 | */ 142 | public function deleteMember($group, $members) 143 | { 144 | $members = is_string($members) ? [$members] : $members; 145 | $result = http()->json(sprintf('%s/webwxupdatechatroom?fun=delmember&pass_ticket=%s', server()->baseUri, server()->passTicket), [ 146 | 'BaseRequest' => server()->baseRequest, 147 | 'ChatRoomName' => $group, 148 | 'DelMemberList' => implode(',', $members) 149 | ], true); 150 | 151 | return $result['BaseResponse']['Ret'] == 0; 152 | } 153 | 154 | /** 155 | * 添加群成员 156 | * 157 | * @param $groupUsername 158 | * @param $members 159 | * @return bool 160 | */ 161 | public function addMember($groupUsername, $members) 162 | { 163 | if(!$groupUsername) return false; 164 | $group = group()->get($groupUsername); 165 | 166 | if(!$group) return false; 167 | 168 | $groupCount = count($group['MemberList']); 169 | list($fun, $key) = $groupCount > 40 ? ['invitemember', 'InviteMemberList'] : ['addmember', 'AddMemberList']; 170 | $members = is_string($members) ? [$members] : $members; 171 | 172 | $result = http()->json(sprintf('%s/webwxupdatechatroom?fun=%s&pass_ticket=%s', server()->baseUri, $fun, server()->passTicket), [ 173 | 'BaseRequest' => server()->baseRequest, 174 | 'ChatRoomName' => $groupUsername, 175 | $key => implode(',', $members) 176 | ], true); 177 | 178 | return $result['BaseResponse']['Ret'] == 0; 179 | } 180 | 181 | /** 182 | * 设置群名称 183 | * 184 | * @param $group 185 | * @param $name 186 | * @return bool 187 | */ 188 | public function setGroupName($group, $name) 189 | { 190 | $result = http()->post(sprintf('%s/webwxupdatechatroom?fun=modtopic&pass_ticket=%s', server()->baseUri, server()->passTicket), 191 | json_encode([ 192 | 'BaseRequest' => server()->baseRequest, 193 | 'ChatRoomName' => $group, 194 | 'NewTopic' => $name 195 | ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true); 196 | 197 | return $result['BaseResponse']['Ret'] == 0; 198 | } 199 | 200 | /** 201 | * 增加群聊天到group 202 | * 203 | * @param $username 204 | * @return bool 205 | */ 206 | private function add($username) 207 | { 208 | $result = http()->json(sprintf('%s/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s', server()->baseUri, time(), server()->passTicket), [ 209 | 'Count' => 1, 210 | 'BaseRequest' => server()->baseRequest, 211 | 'List' => [ 212 | [ 213 | 'ChatRoomId' => '', 214 | 'UserName' => $username 215 | ] 216 | ] 217 | ], true); 218 | 219 | if($result['BaseResponse']['Ret'] != 0){ 220 | Console::log('增加聊天群组失败 '.$username, Console::WARNING); 221 | return false; 222 | } 223 | 224 | group()->put($username, $result['ContactList'][0]); 225 | 226 | return $result['ContactList'][0]; 227 | } 228 | 229 | /** 230 | * 更新群组 231 | * 232 | * @param $username 233 | * @param null $list 234 | * @return array 235 | */ 236 | public function update($username, $list = null) :array 237 | { 238 | $username = is_array($username) ?: [$username]; 239 | return parent::update($username, $this->makeUsernameList($username)); 240 | } 241 | 242 | /** 243 | * 生成username list 格式 244 | * 245 | * @param $username 246 | * @return array 247 | */ 248 | public function makeUsernameList($username) 249 | { 250 | $usernameList = []; 251 | 252 | foreach ($username as $item) { 253 | $usernameList[] = ['UserName' => $item, 'ChatRoomId' => '']; 254 | } 255 | 256 | return $usernameList; 257 | } 258 | 259 | /** 260 | * 生成member list 格式 261 | * 262 | * @param $contacts 263 | * @return array 264 | */ 265 | private function makeMemberList($contacts) 266 | { 267 | $memberList = []; 268 | 269 | foreach ($contacts as $contact) { 270 | $memberList[] = ['UserName' => $contact]; 271 | } 272 | return $memberList; 273 | } 274 | 275 | /** 276 | * 存储群组前批量修改群成员nickname 277 | * 278 | * @param mixed $key 279 | * @param mixed $value 280 | * @return \Illuminate\Support\Collection 281 | */ 282 | public function put($key, $value) 283 | { 284 | foreach ($value['MemberList'] as &$member) { 285 | $member = $this->format($member); 286 | } 287 | 288 | return parent::put($key, $value); 289 | } 290 | 291 | /** 292 | * 修改群组获取,为空时更新群组 293 | * 294 | * @param mixed $key 295 | * @param null $default 296 | * @return mixed 297 | */ 298 | public function get($key, $default = null) 299 | { 300 | return parent::get($key, $this->update($key)); 301 | } 302 | 303 | } -------------------------------------------------------------------------------- /src/Collections/Member.php: -------------------------------------------------------------------------------- 1 | getContacts(); 31 | } 32 | 33 | public function getContacts() 34 | { 35 | $this->makeContactList(); 36 | 37 | $contact = contact()->get(myself()->username); 38 | myself()->alias = isset($contact['Alias']) ? $contact['Alias'] : myself()->nickname ?: myself()->username; 39 | 40 | $this->getBatchGroupMembers(); 41 | 42 | if (server()->config['debug']) { 43 | FileManager::saveToUserPath('contact.json', json_encode(contact()->all())); 44 | FileManager::saveToUserPath('member.json', json_encode(member()->all())); 45 | FileManager::saveToUserPath('group.json', json_encode(group()->all())); 46 | FileManager::saveToUserPath('official.json', json_encode(official()->all())); 47 | FileManager::saveToUserPath('special.json', json_encode(Special::getInstance()->all())); 48 | } 49 | } 50 | 51 | /** 52 | * make instance model 53 | * @param int $seq 54 | */ 55 | public function makeContactList($seq = 0) 56 | { 57 | $url = sprintf(server()->baseUri . '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s&seq=%s', server()->passTicket, server()->skey, time(), $seq); 58 | 59 | $result = http()->json($url, [], true); 60 | 61 | if (isset($result['MemberList']) && $result['MemberList']) { 62 | $this->setCollections($result['MemberList']); 63 | } 64 | 65 | if (isset($result['Seq']) && $result['Seq'] != 0) { 66 | $this->makeContactList($result['Seq']); 67 | } 68 | } 69 | 70 | /** 71 | * 设置联系人到collection 72 | * 73 | * @param $memberList 74 | */ 75 | public function setCollections($memberList) 76 | { 77 | foreach ($memberList as $contact) { 78 | if (in_array($contact['UserName'], static::SPECIAL_USERS)) { # 特殊账户 79 | Special::getInstance()->put($contact['UserName'], $contact); 80 | } elseif (official()->isOfficial($contact['VerifyFlag'])) { # 公众号 81 | Official::getInstance()->put($contact['UserName'], $contact); 82 | } elseif (strstr($contact['UserName'], '@@') !== false) { # 群聊 83 | group()->put($contact['UserName'], $contact); 84 | } else { 85 | contact()->put($contact['UserName'], $contact); 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * 获取群组成员 92 | */ 93 | public function getBatchGroupMembers() 94 | { 95 | $url = sprintf(server()->baseUri . '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s', time(), server()->passTicket); 96 | 97 | $list = []; 98 | group()->each(function ($item, $key) use (&$list) { 99 | $list[] = ['UserName' => $key, 'EncryChatRoomId' => '']; 100 | }); 101 | 102 | $content = http()->json($url, [ 103 | 'BaseRequest' => server()->baseRequest, 104 | 'Count' => group()->count(), 105 | 'List' => $list 106 | ], true); 107 | 108 | $this->initGroupMembers($content); 109 | } 110 | 111 | /** 112 | * 初始化群组成员 113 | * 114 | * @param $array 115 | */ 116 | private function initGroupMembers($array) 117 | { 118 | if (isset($array['ContactList']) && $array['ContactList']) { 119 | foreach ($array['ContactList'] as $group) { 120 | $groupAccount = group()->get($group['UserName']); 121 | $groupAccount['MemberList'] = $group['MemberList']; 122 | $groupAccount['ChatRoomId'] = $group['EncryChatRoomId']; 123 | group()->put($group['UserName'], $groupAccount); 124 | foreach ($group['MemberList'] as $member) { 125 | member()->put($member['UserName'], $member); 126 | } 127 | } 128 | } 129 | 130 | } 131 | 132 | } -------------------------------------------------------------------------------- /src/Core/Http.php: -------------------------------------------------------------------------------- 1 | request($url, 'GET', $options); 48 | } 49 | 50 | public function post($url, $query = [], $array = false) 51 | { 52 | $key = is_array($query) ? 'form_params' : 'body'; 53 | 54 | $content = $this->request($url, 'POST', [$key => $query]); 55 | 56 | return $array ? json_decode($content, true) : $content; 57 | } 58 | 59 | public function json($url, $params = [], $array = false, $extra = []) 60 | { 61 | $params = array_merge(['json' => $params], $extra); 62 | 63 | $content = $this->request($url, 'POST', $params); 64 | 65 | return $array ? json_decode($content, true) : $content; 66 | } 67 | 68 | public function setClient(HttpClient $client) 69 | { 70 | $this->client = $client; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * Return GuzzleHttp\Client instance. 77 | * 78 | * @return \GuzzleHttp\Client 79 | */ 80 | public function getClient() 81 | { 82 | if (!($this->client instanceof HttpClient)) { 83 | $this->cookieJar = new FileCookieJar(Path::getCurrentSessionPath() . 'cookies', true); 84 | $this->client = new HttpClient(['cookies' => $this->cookieJar]); 85 | } 86 | 87 | return $this->client; 88 | } 89 | 90 | /** 91 | * @param $url 92 | * @param string $method 93 | * @param array $options 94 | * @return string 95 | */ 96 | public function request($url, $method = 'GET', $options = []) 97 | { 98 | $response = $this->getClient()->request($method, $url, $options); 99 | 100 | $this->cookieJar->save(Path::getCurrentSessionPath() . 'cookies'); 101 | 102 | return $response->getBody()->getContents(); 103 | } 104 | 105 | 106 | } -------------------------------------------------------------------------------- /src/Core/MessageFactory.php: -------------------------------------------------------------------------------- 1 | handleMessageByType($msg); 35 | } 36 | 37 | 38 | /** 39 | * 处理消息类型 40 | * @param $msg 41 | * @return Message 42 | */ 43 | private function handleMessageByType($msg) 44 | { 45 | switch($msg['MsgType']){ 46 | case 1: //文本消息 47 | if(Location::isLocation($msg)){ 48 | return new Location($msg); 49 | }elseif(contact()->get($msg['FromUserName']) && str_contains($msg['Content'], '过了你的朋友验证请求')){ 50 | return new NewFriend($msg); 51 | }else{ 52 | return new Text($msg); 53 | } 54 | case 3: // 图片消息 55 | return new Image($msg); 56 | case 34: // 语音消息 57 | return new Voice($msg); 58 | case 43: // 视频 59 | return new Video($msg); 60 | case 47: // 动画表情 61 | return new Emoticon($msg); 62 | case 10002: 63 | return new Recall($msg); 64 | case 10000: 65 | if(str_contains($msg['Content'], '利是') || str_contains($msg['Content'], '红包')){ 66 | return new RedPacket($msg); 67 | } 68 | else if(str_contains($msg['Content'], '添加') || str_contains($msg['Content'], '打招呼')){ 69 | # 添加好友 70 | return new NewFriend($msg); 71 | }else if(str_contains($msg['Content'], '加入了群聊') || str_contains($msg['Content'], '移出了群聊') || str_contains($msg['Content'], '改群名为') || str_contains($msg['Content'], '移出群聊') || str_contains($msg['Content'], '邀请你') || str_contains($msg['Content'], '分享的二维码加入群聊')){ 72 | return new GroupChange($msg); 73 | } 74 | break; 75 | case 49: 76 | if($msg['Status'] == 3 && $msg['FileName'] === '微信转账'){ 77 | return new Transfer($msg); 78 | }elseif ($msg['Content'] === '该类型暂不支持,请在手机上查看'){ 79 | return null; 80 | }else{ 81 | return (new ShareFactory())->make($msg); 82 | } 83 | case 37: // 好友验证 84 | return new RequestFriend($msg); 85 | case 42: //共享名片 86 | return new Recommend($msg); 87 | case 62: 88 | //Video 89 | break; 90 | case 51: 91 | if($msg['ToUserName'] === $msg['StatusNotifyUserName']){ 92 | return new Touch($msg); 93 | } 94 | break; 95 | case 53: 96 | //VideoCall 97 | break; 98 | default: 99 | //Unknown 100 | break; 101 | } 102 | return null; 103 | } 104 | } -------------------------------------------------------------------------------- /src/Core/MessageHandler.php: -------------------------------------------------------------------------------- 1 | sync = new Sync(); 45 | $this->messageFactory = new MessageFactory(); 46 | } 47 | 48 | /** 49 | * 设置单例模式 50 | * 51 | * @return MessageHandler 52 | */ 53 | public static function getInstance() 54 | { 55 | if (static::$instance === null) { 56 | static::$instance = new MessageHandler(); 57 | } 58 | 59 | return static::$instance; 60 | } 61 | 62 | /** 63 | * 消息处理器 64 | * 65 | * @param Closure $closure 66 | * @throws \Exception 67 | */ 68 | public function setMessageHandler(Closure $closure) 69 | { 70 | if (!$closure instanceof Closure) { 71 | throw new \Exception('message handler must be a closure!'); 72 | } 73 | 74 | $this->handler = $closure; 75 | } 76 | 77 | /** 78 | * 自定义处理器 79 | * 80 | * @param Closure $closure 81 | * @throws \Exception 82 | */ 83 | public function setCustomHandler(Closure $closure) 84 | { 85 | if (!$closure instanceof Closure) { 86 | throw new \Exception('custom handler must be a closure!'); 87 | } 88 | 89 | $this->customHandler = $closure; 90 | } 91 | 92 | /** 93 | * 退出处理器 94 | * 95 | * @param Closure $closure 96 | * @throws \Exception 97 | */ 98 | public function setExitHandler(Closure $closure) 99 | { 100 | if (!$closure instanceof Closure) { 101 | throw new \Exception('exit handler must be a closure!'); 102 | } 103 | 104 | $this->exitHandler = $closure; 105 | } 106 | 107 | /** 108 | * 异常处理器 109 | * 110 | * @param Closure $closure 111 | * @throws \Exception 112 | */ 113 | public function setExceptionHandler(Closure $closure) 114 | { 115 | if (!$closure instanceof Closure) { 116 | throw new \Exception('exit handler must be a closure!'); 117 | } 118 | 119 | $this->exceptionHandler = $closure; 120 | } 121 | 122 | /** 123 | * 执行一次的处理器 124 | * 125 | * @param Closure $closure 126 | * @throws \Exception 127 | */ 128 | public function setOnceHandler(Closure $closure) 129 | { 130 | if (!$closure instanceof Closure) { 131 | throw new \Exception('exit handler must be a closure!'); 132 | } 133 | 134 | $this->onceHandler = $closure; 135 | } 136 | 137 | /** 138 | * 轮询消息API接口 139 | */ 140 | public function listen() 141 | { 142 | if ($this->onceHandler instanceof Closure) { 143 | call_user_func_array($this->onceHandler, []); 144 | } 145 | 146 | $time = 0; 147 | 148 | while (true) { 149 | if ($this->customHandler instanceof Closure) { 150 | call_user_func_array($this->customHandler, []); 151 | } 152 | 153 | if (time() - $time > 1800) { 154 | Text::send('filehelper', '心跳 ' . Carbon::now()->toDateTimeString()); 155 | $time = time(); 156 | } 157 | 158 | list($retCode, $selector) = $this->sync->checkSync(); 159 | 160 | if (!$this->handleCheckSync($retCode, $selector)) { 161 | break; 162 | } 163 | } 164 | Console::log('程序结束'); 165 | } 166 | 167 | public function handleCheckSync($retCode, $selector, $test = false) 168 | { 169 | if (in_array($retCode, ['1100', '1101'])) { # 微信客户端上登出或者其他设备登录 170 | Console::log('微信客户端正常退出'); 171 | if ($this->exitHandler) { 172 | call_user_func_array($this->exitHandler, []); 173 | } 174 | return false; 175 | } elseif ($retCode == 0) { 176 | if(!$test){ 177 | $this->handlerMessage($selector); 178 | } 179 | return true; 180 | } else { 181 | Console::log('微信客户端异常退出'); 182 | if ($this->exceptionHandler) { 183 | call_user_func_array($this->exitHandler, []); 184 | } 185 | return false; 186 | } 187 | } 188 | 189 | /** 190 | * 处理消息 191 | * 192 | * @param $selector 193 | */ 194 | private function handlerMessage($selector) 195 | { 196 | if ($selector === 0) { 197 | return; 198 | } 199 | 200 | $message = $this->sync->sync(); 201 | 202 | if (count($message['ModContactList']) > 0) { 203 | foreach ($message['ModContactList'] as $contact) { 204 | if (str_contains($contact['UserName'], '@@')) { 205 | group()->put($contact['UserName'], $contact); 206 | } else { 207 | contact()->put($contact['UserName'], $contact); 208 | } 209 | } 210 | } 211 | 212 | if ($message['AddMsgList']) { 213 | foreach ($message['AddMsgList'] as $msg) { 214 | $content = $this->messageFactory->make($msg); 215 | if ($content) { 216 | $this->debugMessage($content); 217 | $this->addToMessageCollection($content); 218 | if ($this->handler) { 219 | $reply = call_user_func_array($this->handler, [$content]); 220 | if ($reply) { 221 | if ($reply instanceof Image) { 222 | Image::sendByMsgId($content->from['UserName'], $reply->raw['MsgId']); 223 | } elseif ($reply instanceof Video) { 224 | Video::sendByMsgId($content->from['UserName'], $reply->raw['MsgId']); 225 | } elseif ($reply instanceof Emoticon) { 226 | Emoticon::sendByMsgId($content->from['UserName'], $reply->raw['MsgId']); 227 | } else { 228 | Text::send($content->from['UserName'], $reply); 229 | } 230 | } 231 | } 232 | } 233 | } 234 | } 235 | } 236 | 237 | /** 238 | * @param $message Message 239 | */ 240 | private function addToMessageCollection($message) 241 | { 242 | message()->put($message->raw['MsgId'], $message); 243 | 244 | foreach (message()->all() as $msgId => $item){ 245 | if($item->raw['CreateTime'] + 120 < time()){ 246 | message()->pull($msgId); 247 | }else{ 248 | break; 249 | } 250 | } 251 | 252 | if (server()->config['debug']) { 253 | $file = fopen(Path::getCurrentUinPath() . 'message.json', 'a'); 254 | fwrite($file, json_encode($message) . PHP_EOL); 255 | fclose($file); 256 | } 257 | } 258 | 259 | /** 260 | * debug出消息 261 | * 262 | * @param $content 263 | */ 264 | private function debugMessage(Message $content) 265 | { 266 | if(server()->config['debug']){ 267 | Console::log("[{$content->raw['MsgId']}] " . $content->content, Console::MESSAGE); 268 | } 269 | } 270 | 271 | } -------------------------------------------------------------------------------- /src/Core/Myself.php: -------------------------------------------------------------------------------- 1 | put($user['UserName'], $user); 42 | $this->nickname = Content::emojiHandle($user['NickName']); 43 | $this->username = $user['UserName']; 44 | $this->sex = $user['Sex']; 45 | $this->uin = $user['Uin']; 46 | Console::log('当前用户昵称:' . $this->nickname); 47 | Console::log('当前用户ID:' . $this->username); 48 | Console::log('当前用户UIN:' . $this->uin); 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/Core/Server.php: -------------------------------------------------------------------------------- 1 | config = $config; 58 | 59 | $this->config['debug'] = isset($this->config['debug']) ? $this->config['debug'] : false; 60 | } 61 | 62 | /** 63 | * @param array $config 64 | * @return Server 65 | */ 66 | public static function getInstance($config = []) 67 | { 68 | if(!static::$instance){ 69 | static::$instance = new Server($config); 70 | } 71 | 72 | return static::$instance; 73 | } 74 | 75 | /** 76 | * start a wechat trip 77 | */ 78 | public function run() 79 | { 80 | if(!$this->tryLogin()){ 81 | $this->prepare(); 82 | } 83 | 84 | $this->init(); 85 | Console::log('初始化成功'); 86 | 87 | $this->statusNotify(); 88 | Console::log('当前session:' . $this->config['session']); 89 | Console::log('开始初始化联系人'); 90 | $this->initContact(); 91 | Console::log('初始化联系人成功'); 92 | Console::log(sprintf("群数量: %d", group()->count())); 93 | Console::log(sprintf("联系人数量: %d", contact()->count())); 94 | Console::log(sprintf("公众号数量: %d", official()->count())); 95 | 96 | MessageHandler::getInstance()->listen(); 97 | } 98 | 99 | /** 100 | * 尝试登录 101 | * 102 | * @return bool 103 | */ 104 | private function tryLogin() :bool 105 | { 106 | System::isWin() ? system('cls') : system('clear'); 107 | 108 | if(is_file(Path::getCurrentSessionPath() . 'cookies') && is_file(Path::getCurrentSessionPath() . 'server.json')){ 109 | 110 | $configs = json_decode(file_get_contents(Path::getCurrentSessionPath() . 'server.json'), true); 111 | 112 | foreach ($configs as $key => $config) { 113 | $this->{$key} = $config; 114 | } 115 | 116 | list($retCode, $selector) = (new Sync())->checkSync(); 117 | $result = (new MessageHandler())->handleCheckSync($retCode, $selector, true); 118 | 119 | if($result && (new Sync())->sync()){ 120 | Console::log('免扫码登录成功'); 121 | return true; 122 | } 123 | } 124 | 125 | return false; 126 | } 127 | 128 | /** 129 | * 微信登录流程 130 | */ 131 | public function prepare() 132 | { 133 | $this->getUuid(); 134 | $this->generateQrCode(); 135 | Console::showQrCode('https://login.weixin.qq.com/l/' . $this->uuid); 136 | Console::log('请扫描二维码登录'); 137 | 138 | $this->waitForLogin(); 139 | $this->login(); 140 | Console::log('登录成功'); 141 | } 142 | 143 | /** 144 | * get uuid 145 | * 146 | * @throws \Exception 147 | */ 148 | protected function getUuid() 149 | { 150 | $content = http()->get('https://login.weixin.qq.com/jslogin', [ 151 | 'appid' => 'wx782c26e4c19acffb', 152 | 'fun' => 'new', 153 | 'lang' => 'zh_CN', 154 | // '_' => time() * 1000 . random_int(1, 999) 155 | '_' => time() 156 | ]); 157 | 158 | preg_match('/window.QRLogin.code = (\d+); window.QRLogin.uuid = \"(\S+?)\"/', $content, $matches); 159 | 160 | if(!$matches){ 161 | Console::log('获取UUID失败', Console::ERROR); 162 | exit; 163 | } 164 | 165 | $this->uuid = $matches[2]; 166 | } 167 | 168 | /** 169 | * generate a login qrcode 170 | */ 171 | public function generateQrCode() 172 | { 173 | $url = 'https://login.weixin.qq.com/l/' . $this->uuid; 174 | 175 | $qrCode = new QrCode($url); 176 | 177 | $file = Path::getCurrentSessionPath() . 'qr.png'; 178 | 179 | FileManager::saveTo($file, file_get_contents($url)); 180 | 181 | $qrCode->save($file); 182 | } 183 | 184 | /** 185 | * waiting user to login 186 | * 187 | * @throws \Exception 188 | */ 189 | protected function waitForLogin() 190 | { 191 | $retryTime = 10; 192 | $tip = 1; 193 | 194 | while($retryTime > 0){ 195 | $url = sprintf('https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s', $tip, $this->uuid, time()); 196 | 197 | $content = http()->get($url); 198 | 199 | preg_match('/window.code=(\d+);/', $content, $matches); 200 | 201 | $code = $matches[1]; 202 | switch($code){ 203 | case '201': 204 | Console::log('请点击确认登录微信'); 205 | $tip = 0; 206 | break; 207 | case '200': 208 | preg_match('/window.redirect_uri="(https:\/\/(\S+?)\/\S+?)";/', $content, $matches); 209 | 210 | $this->redirectUri = $matches[1] . '&fun=new'; 211 | $url = 'https://%s/cgi-bin/mmwebwx-bin'; 212 | $this->fileUri = sprintf($url, 'file.'.$matches[2]); 213 | $this->pushUri = sprintf($url, 'webpush.'.$matches[2]); 214 | $this->baseUri = sprintf($url, $matches[2]); 215 | return; 216 | case '408': 217 | Console::log('登录超时,请重试', Console::WARNING); 218 | $tip = 1; 219 | $retryTime -= 1; 220 | sleep(1); 221 | break; 222 | default: 223 | Console::log("登录失败,错误码:$code 。请重试", Console::ERROR); 224 | $tip = 1; 225 | $retryTime -= 1; 226 | sleep(1); 227 | break; 228 | } 229 | } 230 | 231 | Console::log('登录超时,退出应用', Console::ERROR); 232 | exit; 233 | } 234 | 235 | /** 236 | * login wechat 237 | * @throws \Exception 238 | */ 239 | public function login() 240 | { 241 | $content = http()->get($this->redirectUri); 242 | 243 | $data = (array)simplexml_load_string($content, 'SimpleXMLElement', LIBXML_NOCDATA); 244 | 245 | $this->skey = $data['skey']; 246 | $this->sid = $data['wxsid']; 247 | $this->uin = $data['wxuin']; 248 | $this->passTicket = $data['pass_ticket']; 249 | 250 | if(in_array('', [$this->skey, $this->sid, $this->uin, $this->passTicket])){ 251 | Console::log('登录失败', Console::ERROR); 252 | exit; 253 | } 254 | 255 | $this->deviceId = 'e' .substr(mt_rand().mt_rand(), 1, 15); 256 | 257 | $this->baseRequest = [ 258 | 'Uin' => intval($this->uin), 259 | 'Sid' => $this->sid, 260 | 'Skey' => $this->skey, 261 | 'DeviceID' => $this->deviceId 262 | ]; 263 | 264 | $this->saveServer(); 265 | } 266 | 267 | /** 268 | * 保存server至本地 269 | */ 270 | private function saveServer() 271 | { 272 | $config = json_encode([ 273 | 'skey' => $this->skey, 274 | 'sid' => $this->sid, 275 | 'uin' => $this->uin, 276 | 'passTicket' => $this->passTicket, 277 | 'baseRequest' => $this->baseRequest, 278 | 'baseUri' => $this->baseUri, 279 | 'fileUri' => $this->fileUri, 280 | 'pushUri' => $this->pushUri, 281 | 'config' => $this->config 282 | ]); 283 | 284 | FileManager::saveTo(Path::getCurrentSessionPath() . 'server.json', $config); 285 | } 286 | 287 | protected function init($first = true) 288 | { 289 | $url = sprintf($this->baseUri . '/webwxinit?r=%d', time()); 290 | 291 | $content = http()->json($url, [ 292 | 'BaseRequest' => $this->baseRequest 293 | ]); 294 | 295 | $result = json_decode($content, true); 296 | $this->generateSyncKey($result, $first); 297 | 298 | myself()->init($result['User']); 299 | 300 | $this->initContactList($result['ContactList']); 301 | 302 | if($result['BaseResponse']['Ret'] != 0){ 303 | System::deleteDir(Path::getCurrentSessionPath()); 304 | Console::log('初始化失败,链接:' . $url, Console::ERROR); 305 | exit; 306 | } 307 | } 308 | 309 | protected function initContactList($contactList) 310 | { 311 | if($contactList){ 312 | (new ContactFactory())->setCollections($contactList); 313 | } 314 | } 315 | 316 | protected function initContact() 317 | { 318 | new ContactFactory(); 319 | } 320 | 321 | /** 322 | * open wechat status notify 323 | */ 324 | protected function statusNotify() 325 | { 326 | $url = sprintf($this->baseUri . '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s', $this->passTicket); 327 | 328 | http()->json($url, [ 329 | 'BaseRequest' => $this->baseRequest, 330 | 'Code' => 3, 331 | 'FromUserName' => myself()->username, 332 | 'ToUserName' => myself()->username, 333 | 'ClientMsgId' => time() 334 | ]); 335 | } 336 | 337 | protected function generateSyncKey($result, $first) 338 | { 339 | $this->syncKey = $result['SyncKey']; 340 | 341 | $syncKey = []; 342 | 343 | if(is_array($this->syncKey['List'])){ 344 | foreach ($this->syncKey['List'] as $item) { 345 | $syncKey[] = $item['Key'] . '_' . $item['Val']; 346 | } 347 | }elseif($first){ 348 | $this->init(false); 349 | } 350 | 351 | $this->syncKeyStr = implode('|', $syncKey); 352 | } 353 | 354 | public function setMessageHandler(\Closure $closure) 355 | { 356 | MessageHandler::getInstance()->setMessageHandler($closure); 357 | } 358 | 359 | public function setCustomerHandler(\Closure $closure) 360 | { 361 | MessageHandler::getInstance()->setCustomHandler($closure); 362 | } 363 | 364 | public function setExitHandler(\Closure $closure) 365 | { 366 | MessageHandler::getInstance()->setExitHandler($closure); 367 | } 368 | 369 | public function setExceptionHandler(\Closure $closure) 370 | { 371 | MessageHandler::getInstance()->setExceptionHandler($closure); 372 | } 373 | 374 | public function setOnceHandler(\Closure $closure) 375 | { 376 | MessageHandler::getInstance()->setOnceHandler($closure); 377 | } 378 | } -------------------------------------------------------------------------------- /src/Core/Sync.php: -------------------------------------------------------------------------------- 1 | pushUri . '/synccheck?' . http_build_query([ 26 | 'r' => time(), 27 | 'sid' => server()->sid, 28 | 'uin' => server()->uin, 29 | 'skey' => server()->skey, 30 | 'deviceid' => server()->deviceId, 31 | 'synckey' => server()->syncKeyStr, 32 | '_' => time() 33 | ]); 34 | 35 | try{ 36 | $content = http()->get($url, [], ['timeout' => 35]); 37 | 38 | preg_match('/window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}/', $content, $matches); 39 | 40 | return [$matches[1], $matches[2]]; 41 | }catch (\Exception $e){ 42 | if($retry == 5){ 43 | Console::log('synccheck 请求错误:' . $e->getMessage()); 44 | return [-1, -1]; 45 | } 46 | return $this->checkSync($retry + 1); 47 | } 48 | } 49 | 50 | public function sync($retry = 0) 51 | { 52 | $url = sprintf(server()->baseUri . '/webwxsync?sid=%s&skey=%s&lang=zh_CN&pass_ticket=%s', server()->sid, server()->skey, server()->passTicket); 53 | 54 | try{ 55 | $result = http()->json($url, [ 56 | 'BaseRequest' => server()->baseRequest, 57 | 'SyncKey' => server()->syncKey, 58 | 'rr' => ~time() 59 | ], true); 60 | 61 | if($result['BaseResponse']['Ret'] == 0){ 62 | $this->generateSyncKey($result); 63 | } 64 | 65 | return $result; 66 | }catch (\Exception $e){ 67 | if($retry == 5){ 68 | Console::log('webwxsync 请求错误:' . $e->getMessage()); 69 | return false; 70 | } 71 | return $this->sync($retry + 1); 72 | } 73 | } 74 | 75 | /** 76 | * generate a sync key 77 | * 78 | * @param $result 79 | */ 80 | public function generateSyncKey($result) 81 | { 82 | server()->syncKey = $result['SyncKey']; 83 | 84 | $syncKey = []; 85 | 86 | if(is_array(server()->syncKey['List'])){ 87 | foreach (server()->syncKey['List'] as $item) { 88 | $syncKey[] = $item['Key'] . '_' . $item['Val']; 89 | } 90 | } 91 | 92 | server()->syncKeyStr = implode('|', $syncKey); 93 | } 94 | } -------------------------------------------------------------------------------- /src/Foundation/ServiceProviders/ServerServiceProvider.php: -------------------------------------------------------------------------------- 1 | setConfig($config); 40 | 41 | $this->registerProviders(); 42 | } 43 | 44 | /** 45 | * 设置Config 46 | * 47 | * @param $config 48 | */ 49 | private function setConfig($config) 50 | { 51 | $config = array_merge($config, Console::getParams()); 52 | 53 | $this->setPath($config); 54 | 55 | $this['config'] = function () use ($config) { 56 | return new Collection($config); 57 | }; 58 | } 59 | 60 | /** 61 | * 设置session目录以及 62 | * 63 | * @param $config 64 | * @return mixed 65 | * @throws \Exception 66 | */ 67 | private function setPath(&$config) 68 | { 69 | Path::setConfig($config); 70 | } 71 | 72 | /** 73 | * Register providers. 74 | */ 75 | private function registerProviders() 76 | { 77 | foreach ($this->providers as $provider) { 78 | $this->register(new $provider()); 79 | } 80 | } 81 | 82 | /** 83 | * Magic get access. 84 | * 85 | * @param string $id 86 | * 87 | * @return mixed 88 | */ 89 | public function __get($id) 90 | { 91 | return $this->offsetGet($id); 92 | } 93 | 94 | /** 95 | * Magic set access. 96 | * 97 | * @param string $id 98 | * @param mixed $value 99 | */ 100 | public function __set($id, $value) 101 | { 102 | $this->offsetSet($id, $value); 103 | } 104 | } -------------------------------------------------------------------------------- /src/Message/Entity/Emoticon.php: -------------------------------------------------------------------------------- 1 | make(); 31 | } 32 | 33 | public static function send($username, $file) 34 | { 35 | $response = static::uploadMedia($username, $file); 36 | 37 | if (!$response) { 38 | return false; 39 | } 40 | 41 | $mediaId = $response['MediaId']; 42 | 43 | $url = sprintf(server()->baseUri . '/webwxsendemoticon?fun=sys&f=json&pass_ticket=%s', server()->passTicket); 44 | $data = [ 45 | 'BaseRequest' => server()->baseRequest, 46 | 'Msg' => [ 47 | 'Type' => 47, 48 | "EmojiFlag" => 2, 49 | 'MediaId' => $mediaId, 50 | 'FromUserName' => myself()->username, 51 | 'ToUserName' => $username, 52 | 'LocalID' => time() * 1e4, 53 | 'ClientMsgId' => time() * 1e4 54 | ] 55 | ]; 56 | $result = http()->json($url, $data, true); 57 | 58 | if ($result['BaseResponse']['Ret'] != 0) { 59 | Console::log('发送表情失败', Console::WARNING); 60 | return false; 61 | } 62 | 63 | return true; 64 | } 65 | 66 | /** 67 | * 根据MsgID发送文件 68 | * 69 | * @param $username 70 | * @param $msgId 71 | * @return mixed 72 | */ 73 | public static function sendByMsgId($username, $msgId) 74 | { 75 | $path = static::getPath(static::$folder); 76 | 77 | static::send($username, $path . $msgId . '.gif'); 78 | } 79 | 80 | /** 81 | * 从当前账号的本地表情库随机发送一个 82 | * 83 | * @param $username 84 | */ 85 | public static function sendRandom($username) 86 | { 87 | $path = static::getPath(static::$folder); 88 | 89 | $files = scandir($path); 90 | unset($files[0], $files[1]); 91 | $msgId = $files[array_rand($files)]; 92 | 93 | static::send($username, $path . $msgId); 94 | } 95 | 96 | /** 97 | * 下载文件 98 | * 99 | * @return mixed 100 | */ 101 | public function download() 102 | { 103 | $url = server()->baseUri . sprintf('/webwxgetmsgimg?MsgID=%s&skey=%s', $this->raw['MsgId'], server()->skey); 104 | $content = http()->get($url); 105 | if($content){ 106 | FileManager::saveToUserPath(static::$folder . DIRECTORY_SEPARATOR . $this->raw['MsgId'] . '.gif', $content); 107 | } 108 | } 109 | 110 | public function make() 111 | { 112 | $this->download(); 113 | 114 | $this->content = '[动画表情]'; 115 | } 116 | } -------------------------------------------------------------------------------- /src/Message/Entity/File.php: -------------------------------------------------------------------------------- 1 | make(); 30 | } 31 | 32 | public function make() 33 | { 34 | $array = (array)simplexml_load_string($this->message, 'SimpleXMLElement', LIBXML_NOCDATA); 35 | 36 | $info = (array)$array['appmsg']; 37 | 38 | $this->title = $info['title']; 39 | 40 | $this->download(); 41 | } 42 | 43 | public function download() 44 | { 45 | $url = server()->fileUri . '/webwxgetmedia'; 46 | $content = http()->get($url, [ 47 | 'sender' => $this->raw['FromUserName'], 48 | 'mediaid' => $this->raw['MediaId'], 49 | 'filename' => $this->raw['FileName'], 50 | 'fromuser' => myself()->username, 51 | 'pass_ticket' => server()->passTicket, 52 | 'webwx_data_ticket' => static::getTicket() 53 | ]); 54 | FileManager::saveToUserPath(static::$folder . DIRECTORY_SEPARATOR . $this->raw['FileName'], $content); 55 | 56 | $this->content = '[文件]'; 57 | } 58 | } -------------------------------------------------------------------------------- /src/Message/Entity/GroupChange.php: -------------------------------------------------------------------------------- 1 | make(); 47 | } 48 | 49 | public function make() 50 | { 51 | if (str_contains($this->message, '邀请你')) { 52 | $this->action = 'INVITE'; 53 | } elseif (str_contains($this->message, '加入了群聊') || str_contains($this->message, '分享的二维码加入群聊')) { 54 | $isMatch = preg_match('/邀请"(.+)"加入了群聊/', $this->message, $match); 55 | if(!$isMatch){ 56 | preg_match('/"(.+)"通过扫描.+分享的二维码加入群聊/', $this->message, $match); 57 | } 58 | $this->action = 'ADD'; 59 | $this->nickname = $match[1]; 60 | group()->update($this->raw['FromUserName']); 61 | } elseif (str_contains($this->message, '移出了群聊')) { 62 | $this->action = 'REMOVE'; 63 | } elseif (str_contains($this->message, '改群名为')) { 64 | $this->action = 'RENAME'; 65 | preg_match('/改群名为“(.+)”/', $this->message, $match); 66 | $this->updateGroupName($match[1]); 67 | } elseif (str_contains($this->message, '移出群聊')) { 68 | $this->action = 'BE_REMOVE'; 69 | $this->group = group()->pull($this->from['UserName']); 70 | } 71 | 72 | $this->content = $this->message; 73 | } 74 | 75 | private function updateGroupName($name) 76 | { 77 | $group = group()->get($this->from['UserName']); 78 | $group['NickName'] = $this->rename = $name; 79 | group()->put($group['UserName'], $group); 80 | } 81 | } -------------------------------------------------------------------------------- /src/Message/Entity/Image.php: -------------------------------------------------------------------------------- 1 | make(); 31 | } 32 | 33 | public static function sendByMsgId($username, $msgId) 34 | { 35 | $path = static::getPath(static::$folder); 36 | 37 | static::send($username, $path . $msgId . '.jpg'); 38 | } 39 | 40 | public static function send($username, $file) 41 | { 42 | $response = static::uploadMedia($username, $file); 43 | 44 | if (!$response) { 45 | Console::log("图片 {$file} 上传失败", Console::WARNING); 46 | return false; 47 | } 48 | 49 | $mediaId = $response['MediaId']; 50 | 51 | $url = sprintf(server()->baseUri . '/webwxsendmsgimg?fun=async&f=json&pass_ticket=%s', server()->passTicket); 52 | $data = [ 53 | 'BaseRequest' => server()->baseRequest, 54 | 'Msg' => [ 55 | 'Type' => 3, 56 | 'MediaId' => $mediaId, 57 | 'FromUserName' => myself()->username, 58 | 'ToUserName' => $username, 59 | 'LocalID' => time() * 1e4, 60 | 'ClientMsgId' => time() * 1e4 61 | ] 62 | ]; 63 | $result = http()->json($url, $data, true); 64 | 65 | if ($result['BaseResponse']['Ret'] != 0) { 66 | Console::log('发送图片失败', Console::WARNING); 67 | return false; 68 | } 69 | 70 | return true; 71 | } 72 | 73 | public function make() 74 | { 75 | $this->download(); 76 | 77 | $this->content = '[图片]'; 78 | } 79 | 80 | public function download() 81 | { 82 | $url = server()->baseUri . sprintf('/webwxgetmsgimg?MsgID=%s&skey=%s', $this->raw['MsgId'], server()->skey); 83 | $content = http()->get($url); 84 | FileManager::saveToUserPath(static::$folder . DIRECTORY_SEPARATOR . $this->raw['MsgId'] . '.jpg', $content); 85 | } 86 | } -------------------------------------------------------------------------------- /src/Message/Entity/Location.php: -------------------------------------------------------------------------------- 1 | make(); 27 | } 28 | 29 | /** 30 | * 判断是否位置消息 31 | * 32 | * @param $content 33 | * @return bool 34 | */ 35 | public static function isLocation($content) 36 | { 37 | return str_contains($content['Content'], 'webwxgetpubliclinkimg') && $content['Url']; 38 | } 39 | 40 | /** 41 | * 设置位置文字信息 42 | */ 43 | private function setLocationText() 44 | { 45 | $this->content = current(explode(":\n", $this->message)); 46 | 47 | $this->url = $this->raw['Url']; 48 | } 49 | 50 | public function make() 51 | { 52 | $this->setLocationText(); 53 | } 54 | } -------------------------------------------------------------------------------- /src/Message/Entity/Message.php: -------------------------------------------------------------------------------- 1 | raw = $this->msg = $msg; 65 | 66 | $this->setFrom(); 67 | $this->setFromType(); 68 | 69 | $this->message = Content::formatContent($this->raw['Content']); 70 | if($this->fromType === 'Group'){ 71 | $this->handleGroupContent($this->message); 72 | } 73 | 74 | $this->time = $msg['CreateTime']; 75 | } 76 | 77 | /** 78 | * 设置消息发送者 79 | */ 80 | private function setFrom() 81 | { 82 | $this->from = account()->getAccount($this->raw['FromUserName']); 83 | } 84 | 85 | private function setFromType() 86 | { 87 | if ($this->raw['MsgType'] == 51) { 88 | $this->fromType = 'System'; 89 | } elseif ($this->raw['FromUserName'] === myself()->username) { 90 | $this->fromType = 'Self'; 91 | $this->from = account()->getAccount($this->raw['ToUserName']); 92 | } elseif (substr($this->raw['FromUserName'], 0, 2) === '@@') { # group 93 | $this->fromType = 'Group'; 94 | } elseif (contact()->get($this->raw['FromUserName'])) { 95 | $this->fromType = 'Contact'; 96 | } elseif (official()->get($this->raw['FromUserName'])) { 97 | $this->fromType = 'Official'; 98 | } elseif (Special::getInstance()->get($this->raw['FromUserName'], false)) { 99 | $this->fromType = 'Special'; 100 | } else { 101 | $this->fromType = 'Unknown'; 102 | } 103 | } 104 | 105 | /** 106 | * 处理群发消息的内容 107 | * 108 | * @param $content string 内容 109 | */ 110 | private function handleGroupContent($content) 111 | { 112 | if(!$content || !str_contains($content, ":\n")){ 113 | return; 114 | } 115 | list($uid, $content) = explode(":\n", $content, 2); 116 | 117 | $this->sender = account()->getAccount($uid); 118 | $this->message = Content::replaceBr($content); 119 | } 120 | 121 | public function __toString() 122 | { 123 | return $this->content; 124 | } 125 | 126 | } -------------------------------------------------------------------------------- /src/Message/Entity/Mina.php: -------------------------------------------------------------------------------- 1 | make(); 25 | } 26 | 27 | public function make() 28 | { 29 | $array = (array)simplexml_load_string($this->message, 'SimpleXMLElement', LIBXML_NOCDATA); 30 | 31 | $info = (array)$array['appmsg']; 32 | 33 | $this->title = $info['title']; 34 | $this->url = $info['url']; 35 | 36 | $this->content = '[小程序]'; 37 | } 38 | } -------------------------------------------------------------------------------- /src/Message/Entity/NewFriend.php: -------------------------------------------------------------------------------- 1 | update($msg['FromUserName']); 20 | 21 | parent::__construct($msg); 22 | 23 | $this->make(); 24 | } 25 | 26 | public function make() 27 | { 28 | $this->content = $this->message; 29 | } 30 | } -------------------------------------------------------------------------------- /src/Message/Entity/Official.php: -------------------------------------------------------------------------------- 1 | make(); 29 | } 30 | 31 | public function make() 32 | { 33 | $array = (array)simplexml_load_string($this->message, 'SimpleXMLElement', LIBXML_NOCDATA); 34 | 35 | $info = (array)$array['appmsg']; 36 | 37 | $this->title = $info['title']; 38 | $this->description = $info['des']; 39 | 40 | $appInfo = (array)$array['appinfo']; 41 | 42 | $this->app = $appInfo['appname']; 43 | 44 | $this->url = $this->raw['Url']; 45 | 46 | $this->content = '[公众号推送]'; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Message/Entity/Recall.php: -------------------------------------------------------------------------------- 1 | make(); 33 | } 34 | 35 | /** 36 | * 解析message获取msgId 37 | * 38 | * @param $xml 39 | * @return string msgId 40 | */ 41 | private function parseMsgId($xml) 42 | { 43 | preg_match('/(\d+)<\/msgid>/', $xml, $matches); 44 | return $matches[1]; 45 | } 46 | 47 | public function make() 48 | { 49 | $msgId = $this->parseMsgId($this->message); 50 | 51 | /** @var Message $message */ 52 | $this->origin = message()->get($msgId, null); 53 | 54 | if($this->origin){ 55 | $this->nickname = $this->origin->sender ? $this->origin->sender['NickName'] : account()->getAccount($this->origin->raw['FromUserName'])['NickName']; 56 | $this->setContent(); 57 | } 58 | 59 | } 60 | 61 | private function setContent() 62 | { 63 | $this->content = "{$this->nickname} 刚撤回了消息"; 64 | } 65 | } -------------------------------------------------------------------------------- /src/Message/Entity/Recommend.php: -------------------------------------------------------------------------------- 1 | make(); 48 | } 49 | 50 | public function make() 51 | { 52 | $this->info = $this->raw['RecommendInfo']; 53 | $this->parseContent(); 54 | $this->content = '[名片推荐]'; 55 | } 56 | 57 | private function parseContent() 58 | { 59 | $isMatch = preg_match('/bigheadimgurl="(http:\/\/.+?)"\ssmallheadimgurl="(http:\/\/.+?)".+province="(.+?)"\scity="(.+?)".+certflag="(\d+)"\scertinfo="(.+?)"/', $this->message, $matches); 60 | 61 | if($isMatch){ 62 | $this->bigAvatar = $matches[1]; 63 | $this->smallAvatar = $matches[2]; 64 | $this->province = $matches[3]; 65 | $this->city = $matches[4]; 66 | $flag = $matches[5]; 67 | $desc = $matches[6]; 68 | if(official()->isOfficial($flag)){ 69 | $this->isOfficial = true; 70 | $this->description = $desc; 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/Message/Entity/RedPacket.php: -------------------------------------------------------------------------------- 1 | make(); 21 | } 22 | 23 | public function make() 24 | { 25 | $this->content = $this->message; 26 | } 27 | } -------------------------------------------------------------------------------- /src/Message/Entity/RequestFriend.php: -------------------------------------------------------------------------------- 1 | make(); 31 | } 32 | 33 | public function make() 34 | { 35 | $this->info = $this->raw['RecommendInfo']; 36 | $this->parseContent(); 37 | } 38 | 39 | private function parseContent() 40 | { 41 | $isMatch = preg_match('/bigheadimgurl="(.+?)"/', $this->message, $matches); 42 | 43 | if ($isMatch) { 44 | $this->avatar = $matches[1]; 45 | } 46 | } 47 | 48 | /** 49 | * 验证通过好友 50 | * 51 | * @param $code 52 | * @param null $ticket 53 | * @return bool 54 | */ 55 | public function verifyUser($code, $ticket = null) 56 | { 57 | $url = sprintf(server()->baseUri . '/webwxverifyuser?lang=zh_CN&r=%s&pass_ticket=%s', time() * 1000, server()->passTicket); 58 | $data = [ 59 | 'BaseRequest' => server()->baseRequest, 60 | 'Opcode' => $code, 61 | 'VerifyUserListSize' => 1, 62 | 'VerifyUserList' => [$ticket ?: $this->verifyTicket()], 63 | 'VerifyContent' => '', 64 | 'SceneListCount' => 1, 65 | 'SceneList' => [33], 66 | 'skey' => server()->skey 67 | ]; 68 | 69 | $result = http()->json($url, $data, true); 70 | 71 | return $result['BaseResponse']['Ret'] == 0; 72 | } 73 | 74 | /** 75 | * 返回通过好友申请所需的数组 76 | * 77 | * @return array 78 | */ 79 | public function verifyTicket() 80 | { 81 | return [ 82 | 'Value' => $this->info['UserName'], 83 | 'VerifyUserTicket' => $this->info['Ticket'] 84 | ]; 85 | } 86 | } -------------------------------------------------------------------------------- /src/Message/Entity/Share.php: -------------------------------------------------------------------------------- 1 | make(); 29 | } 30 | 31 | public function make() 32 | { 33 | $array = (array)simplexml_load_string($this->message, 'SimpleXMLElement', LIBXML_NOCDATA); 34 | 35 | $info = (array)$array['appmsg']; 36 | 37 | $this->title = $info['title']; 38 | $this->description = $info['des']; 39 | 40 | $appInfo = (array)$array['appinfo']; 41 | 42 | $this->app = $appInfo['appname']; 43 | 44 | $this->url = $this->raw['Url']; 45 | $this->content = '[分享]'; 46 | } 47 | } -------------------------------------------------------------------------------- /src/Message/Entity/Text.php: -------------------------------------------------------------------------------- 1 | make(); 25 | } 26 | 27 | /** 28 | * 发送消息 29 | * 30 | * @param $word string|Text 消息内容 31 | * @param $username string 目标username 32 | * @return bool 33 | */ 34 | public static function send($username, $word) 35 | { 36 | if (!$word || !$username) { 37 | return false; 38 | } 39 | 40 | $word = is_string($word) ? $word : $word->content; 41 | 42 | $random = strval(time() * 1000) . '0' . strval(rand(100, 999)); 43 | 44 | $data = [ 45 | 'BaseRequest' => server()->baseRequest, 46 | 'Msg' => [ 47 | 'Type' => 1, 48 | 'Content' => $word, 49 | 'FromUserName' => myself()->username, 50 | 'ToUserName' => $username, 51 | 'LocalID' => $random, 52 | 'ClientMsgId' => $random, 53 | ], 54 | 'Scene' => 0 55 | ]; 56 | $result = http()->post(server()->baseUri . '/webwxsendmsg?pass_ticket=' . server()->passTicket, 57 | json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true 58 | ); 59 | 60 | if ($result['BaseResponse']['Ret'] != 0) { 61 | Console::log('发送消息失败 ' . time(), Console::WARNING); 62 | return false; 63 | } 64 | 65 | return true; 66 | } 67 | 68 | public function make() 69 | { 70 | $this->content = $this->message; 71 | 72 | $this->isAt = str_contains($this->content, '@' . myself()->nickname); 73 | } 74 | } -------------------------------------------------------------------------------- /src/Message/Entity/Touch.php: -------------------------------------------------------------------------------- 1 | make(); 21 | } 22 | 23 | public function make() 24 | { 25 | $this->content = '[点击事件]'; 26 | } 27 | } -------------------------------------------------------------------------------- /src/Message/Entity/Transfer.php: -------------------------------------------------------------------------------- 1 | make(); 35 | } 36 | 37 | public function make() 38 | { 39 | $array = (array)simplexml_load_string($this->message, 'SimpleXMLElement', LIBXML_NOCDATA); 40 | 41 | $des = (array)$array['appmsg']->des; 42 | $fee = (array)$array['appmsg']->wcpayinfo; 43 | 44 | $this->content = current($des); 45 | 46 | $this->memo = is_string($fee['pay_memo']) ? $fee['pay_memo'] : null; 47 | $this->fee = substr($fee['feedesc'], 3); 48 | } 49 | } -------------------------------------------------------------------------------- /src/Message/Entity/Video.php: -------------------------------------------------------------------------------- 1 | make(); 30 | } 31 | 32 | public static function send($username, $file) 33 | { 34 | $response = static::uploadMedia($username, $file); 35 | 36 | if (!$response) { 37 | Console::log("视频 {$file} 上传失败", Console::WARNING); 38 | return false; 39 | } 40 | 41 | $mediaId = $response['MediaId']; 42 | 43 | $url = sprintf(server()->baseUri . '/webwxsendvideomsg?fun=async&f=json&pass_ticket=%s', server()->passTicket); 44 | $data = [ 45 | 'BaseRequest' => server()->baseRequest, 46 | 'Msg' => [ 47 | 'Type' => 43, 48 | 'MediaId' => $mediaId, 49 | 'FromUserName' => myself()->username, 50 | 'ToUserName' => $username, 51 | 'LocalID' => time() * 1e4, 52 | 'ClientMsgId' => time() * 1e4 53 | ] 54 | ]; 55 | $result = http()->json($url, $data, true); 56 | 57 | if ($result['BaseResponse']['Ret'] != 0) { 58 | Console::log('发送视频失败', Console::WARNING); 59 | return false; 60 | } 61 | 62 | return true; 63 | } 64 | 65 | /** 66 | * 根据MsgID发送文件 67 | * 68 | * @param $username 69 | * @param $msgId 70 | * @return mixed 71 | */ 72 | public static function sendByMsgId($username, $msgId) 73 | { 74 | $path = static::getPath(static::$folder); 75 | 76 | static::send($username, $path . $msgId . '.mp4'); 77 | } 78 | 79 | /** 80 | * 下载文件 81 | * 82 | * @return mixed 83 | */ 84 | public function download() 85 | { 86 | $url = server()->baseUri . sprintf('/webwxgetvideo?msgid=%s&skey=%s', $this->raw['MsgId'], server()->skey); 87 | $content = http()->request($url, 'get', [ 88 | 'headers' => [ 89 | 'Range' => 'bytes=0-' 90 | ] 91 | ]); 92 | if(strlen($content) === 0){ 93 | Console::log('下载视频失败', Console::WARNING); 94 | Console::log('url:'. $url); 95 | }else{ 96 | FileManager::saveToUserPath(static::$folder . DIRECTORY_SEPARATOR . $this->raw['MsgId'] . '.mp4', $content); 97 | } 98 | } 99 | 100 | public function make() 101 | { 102 | $this->download(); 103 | $this->content = '[视频]'; 104 | } 105 | } -------------------------------------------------------------------------------- /src/Message/Entity/Voice.php: -------------------------------------------------------------------------------- 1 | make(); 29 | } 30 | 31 | /** 32 | * 下载文件 33 | * 34 | * @return mixed 35 | */ 36 | public function download() 37 | { 38 | $url = server()->baseUri . sprintf('/webwxgetvoice?msgid=%s&skey=%s', $this->raw['MsgId'], server()->skey); 39 | $content = http()->get($url); 40 | FileManager::saveToUserPath(static::$folder . DIRECTORY_SEPARATOR . $this->raw['MsgId'] . '.mp3', $content); 41 | } 42 | 43 | public function make() 44 | { 45 | $this->download(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Message/MediaInterface.php: -------------------------------------------------------------------------------- 1 | parse($xml); 29 | 30 | if($this->type == 6){ 31 | return new File($msg); 32 | }else if(official()->get($msg['FromUserName'])){ 33 | return new Official($msg); 34 | }else if($this->type == 33){ 35 | return new Mina($msg); 36 | }else{ 37 | return new Share($msg); 38 | } 39 | } 40 | 41 | private function parse($xml) 42 | { 43 | if(starts_with($xml, '@')){ 44 | $xml = preg_replace('/(@\S+:\\n)/', '', $xml); 45 | } 46 | 47 | $array = (array)simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA); 48 | 49 | $this->xml = $info = (array)$array['appmsg']; 50 | 51 | $this->type = $info['type']; 52 | } 53 | } -------------------------------------------------------------------------------- /src/Message/UploadAble.php: -------------------------------------------------------------------------------- 1 | fileUri . '/webwxuploadmedia?f=json'; 33 | static::$mediaCount = ++static::$mediaCount; 34 | static::$file = $file; 35 | 36 | list($mime, $mediaType) = static::getMediaType($file); 37 | 38 | $data = [ 39 | 'id' => 'WU_FILE_' .static::$mediaCount, 40 | 'name' => basename($file), 41 | 'type' => $mime, 42 | 'lastModifieDate' => gmdate('D M d Y H:i:s TO', filemtime($file)).' (CST)', 43 | 'size' => filesize($file), 44 | 'mediatype' => $mediaType, 45 | 'uploadmediarequest' => json_encode([ 46 | 'BaseRequest' => server()->baseRequest, 47 | 'ClientMediaId' => time(), 48 | 'TotalLen' => filesize($file), 49 | 'StartPos' => 0, 50 | 'DataLen' => filesize($file), 51 | 'MediaType' => 4, 52 | 'UploadType' => 2, 53 | 'FromUserName' => myself()->username, 54 | 'ToUserName' => $username, 55 | 'FileMd5' => md5_file($file) 56 | ], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), 57 | 'webwx_data_ticket' => static::getTicket(), 58 | 'pass_ticket' => (server()->passTicket), 59 | 'filename' => fopen($file, 'r'), 60 | ]; 61 | 62 | $data = static::dataToMultipart($data); 63 | 64 | $result = http()->request($url, 'post', [ 65 | 'multipart' => $data 66 | ]); 67 | $result = json_decode($result, true); 68 | 69 | if($result['BaseResponse']['Ret'] == 0){ 70 | return $result; 71 | } 72 | 73 | return false; 74 | } 75 | 76 | 77 | public static function send($username, $file) 78 | { 79 | $response = static::uploadMedia($username, $file); 80 | 81 | if(!$response){ 82 | Console::log("文件 {$file} 上传失败", Console::WARNING); 83 | return false; 84 | } 85 | 86 | $mediaId = $response['MediaId']; 87 | 88 | $url = sprintf(server()->baseUri . '/webwxsendappmsg?fun=async&f=json' , server()->passTicket); 89 | $data = [ 90 | 'BaseRequest'=> server()->baseRequest, 91 | 'Msg'=> [ 92 | 'Type'=> 6, 93 | 'Content' => sprintf("%s6%s%s%s", basename($file), filesize($file), $mediaId, end(explode('.', $file))), 94 | 'FromUserName'=> myself()->username, 95 | 'ToUserName'=> $username, 96 | 'LocalID'=> time() * 1e4, 97 | 'ClientMsgId'=> time() * 1e4 98 | ] 99 | ]; 100 | $result = http()->json($url, $data, true); 101 | 102 | if($result['BaseResponse']['Ret'] != 0){ 103 | Console::log('发送文件失败', Console::WARNING); 104 | return false; 105 | } 106 | 107 | return true; 108 | } 109 | 110 | /** 111 | * 获取媒体类型 112 | * 113 | * @param $file 114 | * @return array 115 | */ 116 | private static function getMediaType($file) 117 | { 118 | $info = finfo_open(FILEINFO_MIME_TYPE); 119 | $mime = finfo_file($info, $file); 120 | finfo_close($info); 121 | 122 | $fileExplode = explode('.', $file); 123 | $fileExtension = end($fileExplode); 124 | 125 | return [$mime, $fileExtension === 'jpg' ? 'pic' : ($fileExtension === 'mp4' ? 'video' : 'doc')]; 126 | } 127 | 128 | /** 129 | * 获取cookie的ticket 130 | * 131 | * @return mixed 132 | */ 133 | private static function getTicket() 134 | { 135 | $cookies = http()->getClient()->getConfig('cookies')->toArray(); 136 | 137 | $key = array_search('webwx_data_ticket', array_column($cookies, 'Name')); 138 | 139 | return $cookies[$key]['Value']; 140 | } 141 | 142 | /** 143 | * 把请求数组转为multipart模式 144 | * 145 | * @param $data 146 | * @return array 147 | */ 148 | private static function dataToMultipart($data) 149 | { 150 | $result = []; 151 | 152 | foreach ($data as $key => $item) { 153 | $field = [ 154 | 'name' => $key, 155 | 'contents' => $item 156 | ]; 157 | if($key === 'filename'){ 158 | $field['filename'] = basename(static::$file); 159 | } 160 | $result[] = $field; 161 | } 162 | 163 | return $result; 164 | } 165 | 166 | } -------------------------------------------------------------------------------- /src/Support/Console.php: -------------------------------------------------------------------------------- 1 | toDateTimeString() . ']' . "[{$level}] " . $str . PHP_EOL; 41 | } 42 | 43 | /** 44 | * debug 模式下调试输出 45 | * 46 | * @param $str 47 | */ 48 | public static function debug($str) 49 | { 50 | if (server()->config['debug']) { 51 | static::log($str, 'DEBUG'); 52 | } 53 | } 54 | 55 | /** 56 | * 初始化二维码style 57 | * 58 | * @param OutputInterface $output 59 | */ 60 | private static function initQrcodeStyle(OutputInterface $output) { 61 | $style = new OutputFormatterStyle('black', 'black', array('bold')); 62 | $output->getFormatter()->setStyle('blackc', $style); 63 | $style = new OutputFormatterStyle('white', 'white', array('bold')); 64 | $output->getFormatter()->setStyle('whitec', $style); 65 | } 66 | 67 | 68 | /** 69 | * 控制台显示二维码 70 | * 71 | * @param $text 72 | */ 73 | public static function showQrCode($text) 74 | { 75 | $output = new ConsoleOutput(); 76 | static::initQrcodeStyle($output); 77 | 78 | if(System::isWin()){ 79 | $pxMap = ['mm', ' ']; 80 | }else{ 81 | $pxMap = [' ', ' ']; 82 | } 83 | 84 | $text = QRcode::text($text); 85 | 86 | $length = strlen($text[0]); 87 | 88 | foreach ($text as $line) { 89 | $output->write($pxMap[0]); 90 | for ($i = 0; $i < $length; $i++) { 91 | $type = substr($line, $i, 1); 92 | $output->write($pxMap[$type]); 93 | } 94 | $output->writeln($pxMap[0]); 95 | } 96 | } 97 | 98 | /** 99 | * 获取命令行参数 100 | * 101 | * @return array 102 | */ 103 | public static function getParams() 104 | { 105 | return getopt("", ["session:"]); 106 | } 107 | } -------------------------------------------------------------------------------- /src/Support/Content.php: -------------------------------------------------------------------------------- 1 | '1f601', 23 | '1f639' => '1f602', 24 | '1f63a' => '1f603', 25 | '1f4ab' => '1f616', 26 | '1f64d' => '1f614', 27 | '1f63b' => '1f60d', 28 | '1f63d' => '1f618', 29 | '1f64e' => '1f621', 30 | '1f63f' => '1f622', 31 | ]; 32 | 33 | /** 34 | * 格式化Content 35 | * 36 | * @param $content 37 | * @return string 38 | */ 39 | public static function formatContent($content) 40 | { 41 | $content = self::emojiHandle($content); 42 | $content = self::replaceBr($content); 43 | return self::htmlDecode($content); 44 | } 45 | 46 | public static function htmlDecode($content) 47 | { 48 | return html_entity_decode($content); 49 | } 50 | 51 | public static function replaceBr($content) 52 | { 53 | return str_replace('
', "\n", $content); 54 | } 55 | 56 | /** 57 | * 处理微信EMOJI 58 | * 59 | * @param string $content 60 | * @return mixed 61 | */ 62 | public static function emojiHandle(string $content) 63 | { 64 | // 微信的坑 65 | $content = str_replace('', $content); 66 | preg_match_all('/<\/span>/', $content, $match); 67 | 68 | foreach($match[1] as &$unicode){ 69 | $unicode = array_get(self::EMOJI_MAP, $unicode, $unicode); 70 | $unicode = html_entity_decode("&#x{$unicode};"); 71 | } 72 | return str_replace($match[0], $match[1], $content); 73 | } 74 | } -------------------------------------------------------------------------------- /src/Support/FileManager.php: -------------------------------------------------------------------------------- 1 | config['session_path']; 42 | } 43 | 44 | /** 45 | * 获取当前用户资源路径 46 | * 47 | * @return string 48 | */ 49 | public static function getCurrentUinPath() :string 50 | { 51 | return server()->config['user_path'] . myself()->uin . DIRECTORY_SEPARATOR; 52 | } 53 | 54 | /** 55 | * 获取real path 56 | * 57 | * @param $path 58 | * @return string 59 | */ 60 | public static function getRealPath($path) 61 | { 62 | if(!is_dir($path)){ 63 | mkdir($path, 0700, true); 64 | } 65 | 66 | return realpath($path); 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /src/Support/System.php: -------------------------------------------------------------------------------- 1 |