Commit 2e046fce by HanSon

修复了路径bug,增加了@demo,增加了文档

1 parent 7ac668c3
* Group 群组 (group_list & group_user)
* Public 公众号
* Special 特殊账号
* Contact (公众号 & 联系人)
# 安装
Server 登录、初始化
Http 处理HTTP请求
Qrcode 处理二维码
## 环境要求
获取List数据
* PHP >= 7(代码中使用了一些PHP7的特性)
Handler 处理消息
## 安装
```
composer require hanson/robot
```
# 文档
## 例子
[自动回复](https://github.com/HanSon/vbot/blob/before/example/tuling.php)
[红包提醒](https://github.com/HanSon/vbot/blob/before/example/hongbao.php)
[轰炸群](https://github.com/HanSon/vbot/blob/before/example/groups.php)
[发送消息到某群名](https://github.com/HanSon/vbot/blob/before/example/group.php)
[消息转发](https://github.com/HanSon/vbot/blob/before/example/forward.php)
[自定义处理器](https://github.com/HanSon/vbot/blob/before/example/custom.php)
[是否@了我](https://github.com/HanSon/vbot/blob/before/example/is_at.php)
## 基本使用
```
# 图灵API自动回复
require_once __DIR__ . './../vendor/autoload.php';
use Hanson\Robot\Foundation\Robot;
use Hanson\Robot\Message\Message;
$robot = new Robot([
'tmp' => '/path/to/tmp/', # 用于生成登录二维码以及文件保存
'debug' => true # 用于是否输出用户组的json
]);
$robot->server->setMessageHandler(function($message){
if($message->type === 'Text'){
$url = 'http://www.tuling123.com/openapi/api';
$result = http()->post($url, [
'key' => 'your tuling api key',
'info' => $message->content
], true);
return $result['text'];
}
});
$robot->server->run();
```
Robot->server
Robot->public
Robot->special
Robot->contact
Robot->group
# API
## server
```
# 消息处理处,接收到微信消息时的处理器
$robot->server->setMessageHandler(function($message){
});
```
```
# 自定义处理器,一直执行
$robot->server->setCustomHandler(function(){
});
```
## Message
### sender
### 属性
| 类型 | 名称 | 解释 |
| --- | --- | --- |
| array| from | 消息来源 |
| array| sender| 当消息来自于群组时,from为群组 ,而sender为消息发送者, 假若不为 group,sender 为空 |
| string| username| 消息来源的username |
| array| to | 消息接收者,一般为自己 |
| string | content | 经过处理的消息内容 |
| carbon | time | 消息接收的的时间 |
| string | fromType | 消息发送者的类型 |
| string | type | 消息内容的类型 |
### 方法
`bool send($word, $username)`
发送消息给username的用户或者群组
* 参数
* `string` `word` 回复的文字
* `stirng` `username` 用户或者群组的username
### type 消息类型
* `Text` 文字消息
* `Location` 位置
* ` Image` 图片
* `Voice` 语音
* ` AddUser` 添加朋友
* `Recommend` 推荐名片
* ` Animation `
* `Share` 链接分享
* `Video` 小视频
* `VideoCall` 视频聊天
* `Redraw`
* `RedPacket` 红包
* `Unknown` 未知
### fromType 消息发送者类型
* ` System` 系统消息
* `FriendRequest` 加好友申请
* ` Self` 自己
* `FileHelper` 文件助手
* ` Group` 群组
* `Contact` 联系人
* `Official` 公众号
* `Special` qq邮件, 微信团队 , 漂流瓶等特殊账号
* `Unknown` 未知
### 账号
无论是group, contact都有多个账号组成,而账号组成如下
```
{
"@d5b4e97cd7bdf68152393e8e6c30ab67ba57e8fa57b4fcb5917490407c93fb06": {
"Uin": 0,
"UserName": "@d5b4e97cd7bdf68152393e8e6c30ab67ba57e8fa57b4fcb5917490407c93fb06",
"NickName": "wendy",
"HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?***",
"ContactFlag": 3,
"MemberCount": 0,
"MemberList": [],
"RemarkName": "",
"HideInputBarFlag": 0,
"Sex": 0,
"Signature": "",
"VerifyFlag": 0,
"OwnerUin": 0,
"PYInitial": "WENDY",
"PYQuanPin": "wendy",
"RemarkPYInitial": "",
"RemarkPYQuanPin": "",
"StarFriend": 0,
"AppAccountFlag": 0,
"Statues": 0,
"AttrStatus": 135269,
"Province": "",
"City": "",
"Alias": "",
"SnsFlag": 17,
"UniFriend": 0,
"DisplayName": "",
"ChatRoomId": 0,
"KeyWord": "",
"EncryChatRoomId": ""
}
}
```
最重要的信息为
| 字段名 | 含义|
| --- | --- |
| UserName| 每个账号唯一的ID,每次登录随机生成 |
| NickName| 账号的昵称|
| Alias| 微信号|
## 全局方法
本库用了大量的单例模式,为了方便写了一些方便的[全局方法](https://github.com/HanSon/vbot/blob/before/src/Support/helpers.php),contact,group,member等均继承了[illuminate/support/Collection](https://github.com/illuminate/support/blob/master/Collection.php)
相关文档: [中文文档](https://laravel-china.org/docs/5.3/collections) [英文文档](https://laravel.com/docs/5.3/collections)
### account()
#### 属性
| 类型 | 名称 | 解释 |
| --- | --- | --- |
| Hanson\Robot\Collections\Group| group | 群组 |
| Hanson\Robot\Collections\Contact| contact| 联系人 |
#### 方法
`getAccount($username)` 根据username返回账号
`返回值 array`
| 参数名 | 类型 | 解释 |
| ------ | ---- | ---- |
| username| string | 账号数组 |
### contact()
#### 方法
`getContactByUsername($username)` 根据username获取Contact
`返回值 array`
| 参数名 | 类型 | 解释 |
| ------ | ---- | ---- |
| username| string | 联系人的username|
`getContactById($id)` 根据微信号获取Contact
`返回值 array`
| 参数名 | 类型 | 解释 |
| ------ | ---- | ---- |
| id| string | 联系人的微信号|
`getUsernameById($id)` 根据微信号获取 username
`返回值 array`
| 参数名 | 类型 | 解释 |
| ------ | ---- | ---- |
| id| string | 联系人的微信号|
### group()
#### 方法
`isGroup($userName)` 根据username判断是否群组
`返回值 bool`
| 参数名 | 类型 | 解释 |
| ------ | ---- | ---- |
| username| string | 联系人的username|
`getGroupsByNickname($name, $blur = false, $onlyUsername = false)` 根据名称筛选群组
`返回值 array`
| 参数名 | 类型 | 解释 |
| ------ | ---- | ---- |
| name| string | 需要筛选的名称|
| blur| bool | 是否模糊匹配|
| onlyUsername | bool | 是否只筛选出username|
### member()
#### 方法
`getMemberByUsername($username)` 根据username获取成员
`返回值 array`
| 参数名 | 类型 | 解释 |
| ------ | ---- | ---- |
| name| string | 成员的username|
### myself()
#### 属性
* userName
* nickname 昵称
* sex 性别(0-女 1-男)
### http()
#### 方法
`get($url, array $query = [])` ajax get请求
`返回值 string`
| 参数名 | 类型 | 解释 |
| ------ | ---- | ---- |
| url| string | 请求链接|
| query| array | 请求参数数组|
`post($url, array $query = [], $json = false)` ajax post请求
`返回值 string|array`
| 参数名 | 类型 | 解释 |
| ------ | ---- | ---- |
| url| string | 请求链接|
| query| array | 请求参数数组|
| array| bool | 是否进行json_decode处理|
`json($url, array $query = [], $json = false)` ajax post json请求
`返回值 string|array`
| 参数名 | 类型 | 解释 |
| ------ | ---- | ---- |
| url| string | 请求链接|
| query| array | 请求参数数组|
| array| bool | 是否进行json_decode处理|
# 特别感谢
* user 发送者
> * uid 发送者ID
> *
* gid 群聊ID
* type 消息发送者类型
[liuwons/wxBot](https://github.com/liuwons/wxBot) 参考了整个微信的登录流程与消息处理
[overtrue/wechat](https://github.com/overtrue/wechat) 参考了部分代码的书写格式与设计思路
# what can wx-robot do?
# 已知bug
* 转发消息
* 记录特别消息
* 特别关注某人
\ No newline at end of file
* 20% 的几率初始化失败(暂时无解,如清楚问题欢迎PR)
\ No newline at end of file
......@@ -12,7 +12,7 @@ use Hanson\Robot\Foundation\Robot;
use Hanson\Robot\Message\Message;
$robot = new Robot([
'tmp' => __DIR__ . './../tmp/',
'tmp' => __DIR__ . '/./../tmp/',
]);
$flag = false;
......
......@@ -12,14 +12,14 @@ use Hanson\Robot\Foundation\Robot;
use Hanson\Robot\Message\Message;
$robot = new Robot([
'tmp' => __DIR__ . './../tmp/',
'tmp' => __DIR__ . '/./../tmp/',
'debug' => true
]);
$robot->server->setMessageHandler(function($message){
if($message->type === 'Text'){
/** @var $message Message */
$contact = contact()->getUsernameById('L907159127');
$contact = contact()->getUsernameById('hanson1994');
Message::send($message->content, $contact);
}
});
......
......@@ -12,7 +12,7 @@ use Hanson\Robot\Foundation\Robot;
use Hanson\Robot\Message\Message;
$robot = new Robot([
'tmp' => __DIR__ . './../tmp/',
'tmp' => __DIR__ . '/./../tmp/',
]);
$robot->server->setMessageHandler(function($message){
......
......@@ -12,7 +12,7 @@ use Hanson\Robot\Foundation\Robot;
use Hanson\Robot\Message\Message;
$robot = new Robot([
'tmp' => __DIR__ . './../tmp/',
'tmp' => __DIR__ . '/./../tmp/',
'debug' => true
]);
......
......@@ -12,7 +12,7 @@ use Hanson\Robot\Foundation\Robot;
use Hanson\Robot\Message\Message;
$robot = new Robot([
'tmp' => __DIR__ . './../tmp/',
'tmp' => __DIR__ . '/./../tmp/',
'debug' => true
]);
......
<?php
/**
* Created by PhpStorm.
* User: HanSon
* Date: 2016/12/7
* Time: 16:33
*/
require_once __DIR__ . './../vendor/autoload.php';
use Hanson\Robot\Foundation\Robot;
use Hanson\Robot\Message\Message;
use Hanson\Robot\Support\Console;
$robot = new Robot([
'tmp' => __DIR__ . '/./../tmp/',
]);
$robot->server->setMessageHandler(function($message){
/** @var $message Message */
if($message->type === 'RedPacket'){
if($message->fromType == 'Group'){
$nickname = group()->get($message->username)['NickName'];
}else{
$nickname = contact()->get($message->username)['NickName'];
}
Console::log("收到来自 {$nickname} 的红包");
}
});
$robot->server->run();
<?php
/**
* Created by PhpStorm.
* User: HanSon
* Date: 2016/12/7
* Time: 16:33
*/
require_once __DIR__ . './../vendor/autoload.php';
use Hanson\Robot\Foundation\Robot;
use Hanson\Robot\Message\Message;
use Hanson\Robot\Support\Console;
$robot = new Robot([
'tmp' => __DIR__ . '/./../tmp/',
]);
$robot->server->setMessageHandler(function($message){
/** @var $message Message */
if($message->fromType === 'Group' && $message->type === 'Text'){
if($message->isAt){
$groupName = group()->get($message->username)['NickName'];
$memberName = member()->get($message->sender['UserName'])['NickName'];
// ex: wechat群 的 HanSon at 了我
Console::log("{$groupName}{$memberName} at 了我");
}
}
});
$robot->server->run();
......@@ -12,7 +12,7 @@ use Hanson\Robot\Foundation\Robot;
use Hanson\Robot\Message\Message;
$robot = new Robot([
'tmp' => __DIR__ . './../tmp/',
'tmp' => __DIR__ . '/./../tmp/',
]);
$robot->server->setMessageHandler(function($message){
......
......@@ -11,17 +11,23 @@ namespace Hanson\Robot\Collections;
use Illuminate\Support\Collection;
class Account extends Collection
class Account
{
/**
* @var Account
* @var Group
*/
static $instance = null;
static $group;
const NORMAL_MEMBER = 'normal_member';
/**
* @var Contact
*/
static $contact;
const GROUP_MEMBER = 'group_member';
/**
* @var Account
*/
static $instance = null;
/**
* create a single instance
......@@ -32,137 +38,24 @@ class Account extends Collection
{
if(static::$instance === null){
static::$instance = new Account();
static::$group = group();
static::$contact = contact();
}
return static::$instance;
}
/**
* 增加群聊天
*
* @param $id
* @param $groupMember
*/
public function addGroupMember($id, $groupMember)
{
$account = static::$instance->all();
$account[static::GROUP_MEMBER][$id] = $groupMember;
static::$instance = static::$instance->make($account);
}
/**
* 增加联系人聊天
*
* @param $id
* @param $normalMember
*/
public function addNormalMember($id, $normalMember)
{
$account = static::$instance->all();
$account[static::NORMAL_MEMBER][$id] = $normalMember;
static::$instance = static::$instance->make($account);
}
/**
* 获取联系人名称
*
* @param string $id
* @param string $type 群或者联系人
* @param bool $prefer 返回最佳名称或名称数组
* @return array|null
*/
public function getContactName($id, $type, $prefer = false)
{
$target = static::$instance->get($type);
$user = $target[$id];
$name = [];
if(isset($user['RemarkName'])){
$name['remarkName'] = $user['RemarkName'];
}
if(isset($user['NickName'])){
$name['nickName'] = $user['NickName'];
}
if(isset($user['DisplayName'])){
$name['displayName'] = $user['DisplayName'];
}
if(!$name){
return null;
}
return $prefer ? current($name) : $name;
}
/**
* 获取联系人
*
* @param $id
* @return array
*/
public function getContactByUsername($id)
{
$target = static::$instance->get(static::NORMAL_MEMBER);
return $target[$id] ?? null;
}
public function getGroupMember($id)
{
$target = static::$instance->get(static::GROUP_MEMBER);
return $target[$id] ?? null;
}
/**
* 获取联系人列表
*
* @return Collection
*/
public function getNormalMembers()
{
$target = static::$instance->get(static::NORMAL_MEMBER);
return collect($target);
}
/**
* 根据微信号获取联系人
*
* @param $id
* @return mixed
*/
public function getContactById($id)
{
$contact = $this->getNormalMembers()->filter(function($item, $key) use ($id){
if($item['info']['Alias'] === $id){
return true;
}
})->first();
return $contact;
}
/**
* 根据微信号获取联系username
* 根据username获取账号
*
* @param $id
* @param $username
* @return mixed
*/
public function getUsernameById($id)
public function getAccount($username)
{
$contact = $this->getNormalMembers()->search(function($item, $key) use ($id){
if($item['info']['Alias'] === $id){
return true;
}
});
$account = static::$group->get($username, null);
return $contact;
return $account ? : static::$contact->get($username, null);
}
}
\ No newline at end of file
......@@ -33,11 +33,6 @@ class Contact extends Collection
return static::$instance;
}
public function isContact($id)
{
return static::$instance->get($id, false);
}
/**
* 根据username获取联系人
*
......
......@@ -9,6 +9,7 @@
namespace Hanson\Robot\Core;
use GuzzleHttp\Client as HttpClient;
use Hanson\Robot\Support\Console;
class Http
{
......@@ -29,18 +30,18 @@ class Http
return static::$instance;
}
public function get($url, array $options = [])
public function get($url, array $query = [])
{
$query = $options ? ['query' => $options] : [];
$query = $query ? ['query' => $query] : [];
return $this->request($url, 'GET', $query);
}
public function post($url, $options = [], $array = false)
public function post($url, $query = [], $array = false)
{
$key = is_array($options) ? 'form_params' : 'body';
$key = is_array($query) ? 'form_params' : 'body';
$content = $this->request($url, 'POST', [$key => $options]);
$content = $this->request($url, 'POST', [$key => $query]);
return $array ? json_decode($content, true) : $content;
}
......@@ -75,9 +76,15 @@ class Http
public function request($url, $method = 'GET', $options = [])
{
$response = $this->getClient()->request($method, $url, $options);
try{
$response = $this->getClient()->request($method, $url, $options);
return $response->getBody()->getContents();
}catch (\Exception $e){
Console::log('http链接失败:' . $e->getMessage());
Console::log('错误URL:' . $url);
}
return $response->getBody()->getContents();
}
......
......@@ -14,7 +14,7 @@ class Location
public static function isLocation($content)
{
return str_contains('webwxgetpubliclinkimg', $content);
return str_contains('webwxgetpubliclinkimg', $content['Content']) && $content['Url'];
}
public static function getLocationText($content)
......
......@@ -60,6 +60,8 @@ class Message
*/
public $type;
public $isAt = false;
const USER_TYPE = [
0 => 'Init',
1 => 'Self',
......@@ -82,6 +84,7 @@ class Message
$this->setTo();
$this->setFromType();
$this->setType();
$this->rawMsg['selector'] = $selector;
return $this;
}
......@@ -112,10 +115,10 @@ class Message
$this->fromType = 'FileHelper';
} elseif (substr($this->rawMsg['FromUserName'], 0, 2) === '@@') { # group
$this->fromType = 'Group';
} elseif (Contact::getInstance()->isContact($this->rawMsg['FromUserName'])) {
} elseif (contact()->getContactByUsername($this->rawMsg['FromUserName'])) {
$this->fromType = 'Contact';
} elseif (OfficialAccount::getInstance()->isPublic($this->rawMsg['FromUserName'])) {
$this->fromType = 'Public';
$this->fromType = 'Official';
} elseif (SpecialAccount::getInstance()->get($this->rawMsg['FromUserName'], false)) {
$this->fromType = 'Special';
} else {
......@@ -141,7 +144,7 @@ class Message
$this->type = 'Empty';
}elseif ($this->fromType === 'FileHelper'){ # File Helper
$this->type = 'Text';
$this->content->msg = $this->formatContent($this->rawMsg['Content']);
$this->content = $this->formatContent($this->rawMsg['Content']);
}elseif ($this->fromType === 'Group'){
$this->handleGroupContent($this->rawMsg['Content']);
}
......@@ -154,7 +157,7 @@ class Message
{
switch($this->rawMsg['MsgType']){
case 1:
if(Location::isLocation($this->rawMsg['Content'])){
if(Location::isLocation($this->rawMsg)){
$this->type = 'Location';
$this->content = Location::getLocationText($this->rawMsg['Content']);
}else{
......@@ -166,13 +169,13 @@ class Message
$this->type = 'Image';
$this->content = Server::BASE_URI . sprintf('/webwxgetmsgimg?MsgID=%s&skey=%s', $this->rawMsg['MsgId'], server()->skey);
$content = http()->get($this->content);
FileManager::download($this->rawMsg['MsgId'], $content, 'jpg');
FileManager::download(time().$this->rawMsg['MsgId'].'.jpg', $content, 'jpg');
break;
case 34:
$this->type = 'Voice';
$this->content = Server::BASE_URI . sprintf('/webwxgetvoice?msgid=%s&skey=%s', $this->rawMsg['MsgId'], server()->skey);
$content = http()->get($this->content);
FileManager::download($this->rawMsg['MsgId'], $content, 'mp3');
FileManager::download(time().$this->rawMsg['MsgId'].'.mp3', $content, 'mp3');
break;
case 37:
$this->type = 'AddUser';
......@@ -197,7 +200,11 @@ class Message
$this->type = 'Redraw';
break;
case 10000:
$this->type = 'Unknown';
if($this->rawMsg['Status'] == 4){
$this->type = 'RedPacket'; // 红包
}else{
$this->type = 'Unknown';
}
break;
default:
$this->type = 'Unknown';
......@@ -212,25 +219,29 @@ class Message
*/
private function handleGroupContent($content)
{
if(!$content){
return;
}
list($uid, $content) = explode('<br/>', $content, 2);
$this->sender = member()->getMemberByUsername(substr($uid, 0, -1));
$this->rawMsg['Content'] = $this->formatContent($content);
$this->isAt = str_contains($this->rawMsg['Content'], '@'.myself()->nickname);
}
private function formatContent($content)
{
return str_replace('<br/>', '\n', $content);
return str_replace('<br/>', "\n", $content);
}
/**
* 发送消息
*
* @param $word string 消息内容
* @param $fromUser string 目标username
* @param $username string 目标username
* @return bool
*/
public static function send($word, $fromUser)
public static function send($word, $username)
{
if(!$word && !is_string($word)){
return false;
......@@ -244,7 +255,7 @@ class Message
'Type' => 1,
'Content' => $word,
'FromUserName' => myself()->userName,
'ToUserName' => $fromUser,
'ToUserName' => $username,
'LocalID' => $random,
'ClientMsgId' => $random,
],
......
......@@ -12,14 +12,14 @@ namespace Hanson\Robot\Support;
class FileManager
{
public static function download($msgId, $data, $type)
public static function download($name, $data, $path)
{
$path = server()->config['tmp'] . $type;
$path = server()->config['tmp'] . $path;
if(!is_dir(realpath($path))){
mkdir($path, 0700, true);
}
file_put_contents("$path/$msgId.$type", $data);
file_put_contents("$path/$name", $data);
}
}
\ No newline at end of file
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!