├── .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("{$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 |