将消息输出到sqlite数据库
@@ -0,0 +1,51 @@
|
|||||||
|
# wxbot
|
||||||
|
|
||||||
|
微信聊天记录自动存sqlite数据库,基于微信网页版。注意,Web版获取不到微信用户的微信ID,所得到的用户名/ID是仅当前会话有效的,因此只有根据备注名在数据库中识别用户比较靠谱。
|
||||||
|
|
||||||
|
项目fork自[@Urinx/WeixinBot](https://github.com/Urinx/WeixinBot),我只是加了个存到sqlite的壳,感谢老哥的网页版微信API。
|
||||||
|
|
||||||
|
## 数据表结构
|
||||||
|
| 字段名 | 类型 | 说明 |
|
||||||
|
| -- | -- | -- |
|
||||||
|
| msg_id| TEXT | 消息ID |
|
||||||
|
|create_time | INTEGER | 创建时间,Unix时间戳 |
|
||||||
|
| msg_type | INTEGER | 消息类型,1-文本,3-图片,34-语音,43-视频,0-其它
|
||||||
|
| content | TEXT | 消息的文本内容
|
||||||
|
| from_id | TEXT | 消息发送者ID(不是微信ID,没啥用),如果是来自群的消息此字段为空
|
||||||
|
| from_name | TEXT | 消息发送者的备注,没备注就昵称,如果是来自群的消息此字段为空
|
||||||
|
| from_nickname | TEXT | 消息发送者的昵称,如果是来自群的消息此字段为空
|
||||||
|
| to_id | TEXT | 接收者ID,如果是发送到群的消息此字段为空
|
||||||
|
| to_name | TEXT | 接收者备注,没备注就昵称,如果是发送到群的消息此字段为空
|
||||||
|
| to_nickname | TEXT | 接收者昵称,如果是发送到群的消息此字段为空
|
||||||
|
| group_id | TEXT | 如果是群消息则为群的ID否则为空,照样,没啥用
|
||||||
|
| group_name | TEXT | 如果是群消息则为群聊名称(应该吧🤣我不关注群消息),否则为空
|
||||||
|
|
||||||
|
消息里的图片等文件以`img_`,`voice_`等前缀 + 消息ID存储于命令行参数给定的数据目录中。
|
||||||
|
|
||||||
|
其它信息请参考[项目@Urinx/WeixinBot](https://github.com/Urinx/WeixinBot)。
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
### 克隆仓库并安装python依赖
|
||||||
|
```shell
|
||||||
|
# 不适用win系统
|
||||||
|
git clone https://github.com/mattuylee/wxbot.git
|
||||||
|
cd wxbot
|
||||||
|
python3 -m venv ./venv
|
||||||
|
source ./venv/bin/activate
|
||||||
|
pip3 install -r ./requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 运行
|
||||||
|
`python ./main.py -f <DB_FILE> -d <DATA_DIR>`
|
||||||
|
其中`DB_FILE`是sqlite数据库文件路径,`DATA_DIR`是图片视频等数据的存储目录。
|
||||||
|
|
||||||
|
支持sqlcipher加密,程序启动会要求输入密码,如不需要加密可留空直接回车。注意,以后每次启动必须输入相同的密码。暂不支持修改密码,可自行安装sqlcipher打开数据库修改密码。
|
||||||
|
|
||||||
|
未加密数据库可直接通过sqlite3打开,加密数据库需要sqlcipher才能打开。程序只负责记录数据,没有读取能力。
|
||||||
|
|
||||||
|
### 其它
|
||||||
|
关于表情包,如果是从表情商店里添加的表情包网页版微信是获取不到的,为了保护创作者的权益。这倒是可以理解。
|
||||||
|
|
||||||
|
如果发现无法登录先试试在浏览器登录网页版微信,有可能是腾讯为了“安全”限制了你的微信帐号在网页登录。
|
||||||
|
|
||||||
|
最后,祝腾讯早点倒闭。
|
||||||
@@ -1,799 +0,0 @@
|
|||||||
# WeixinBot [](http://github.com/Urinx/WeixinBot) [](http://github.com/Urinx/WeixinBot/fork) 
|
|
||||||
|
|
||||||
网页版微信API,包含终端版微信及微信机器人
|
|
||||||
|
|
||||||
## Contents
|
|
||||||
* [Demo](#Demo)
|
|
||||||
* [Web Weixin Pipeline](#Web-Weixin-Pipeline)
|
|
||||||
* [Web Weixin API](#Web-Weixin-API)
|
|
||||||
* [Discussion Group](#Discussion-Group)
|
|
||||||
* [Recent Update](#Recent-Update)
|
|
||||||
|
|
||||||
## <a name="Demo">Demo</a>
|
|
||||||
为了确保能正常运行示例脚本,请安装所需的第三方包。
|
|
||||||
|
|
||||||
```
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
注:下面演示的图片与功能可能不是最新的,具体请看源码。
|
|
||||||
|
|
||||||
<div align=center>
|
|
||||||
<img src="imgs/1.png" width="500" height="550"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
按照操作指示在手机微信上扫描二维码然后登录,你可以选择是否开启自动回复模式。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
开启自动回复模式后,如果接收到的是文字消息就会自动回复,包括群消息。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
名片,链接,动画表情和地址位置消息。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
网页版上有的功能目前基本上都能支持。
|
|
||||||
|
|
||||||
## <a name="Web-Weixin-Pipeline">Web Weixin Pipeline</a>
|
|
||||||
|
|
||||||
```
|
|
||||||
+--------------+ +---------------+ +---------------+
|
|
||||||
| | | | | |
|
|
||||||
| Get UUID | | Get Contact | | Status Notify |
|
|
||||||
| | | | | |
|
|
||||||
+-------+------+ +-------^-------+ +-------^-------+
|
|
||||||
| | |
|
|
||||||
| +-------+ +--------+
|
|
||||||
| | |
|
|
||||||
+-------v------+ +-----+--+------+ +--------------+
|
|
||||||
| | | | | |
|
|
||||||
| Get QRCode | | Weixin Init +------> Sync Check <----+
|
|
||||||
| | | | | | |
|
|
||||||
+-------+------+ +-------^-------+ +-------+------+ |
|
|
||||||
| | | |
|
|
||||||
| | +-----------+
|
|
||||||
| | |
|
|
||||||
+-------v------+ +-------+--------+ +-------v-------+
|
|
||||||
| | Confirm Login | | | |
|
|
||||||
+------> Login +---------------> New Login Page | | Weixin Sync |
|
|
||||||
| | | | | | |
|
|
||||||
| +------+-------+ +----------------+ +---------------+
|
|
||||||
| |
|
|
||||||
|QRCode Scaned|
|
|
||||||
+-------------+
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## <a name="Web-Weixin-API">Web Weixin API</a>
|
|
||||||
|
|
||||||
### 登录
|
|
||||||
|
|
||||||
| API | 获取 UUID |
|
|
||||||
| --- | --------- |
|
|
||||||
| url | https://login.weixin.qq.com/jslogin |
|
|
||||||
| method | POST |
|
|
||||||
| data | URL Encode |
|
|
||||||
| params | **appid**: `应用ID` <br> **fun**: new `应用类型` <br> **lang**: zh\_CN `语言` <br> **_**: `时间戳` |
|
|
||||||
|
|
||||||
返回数据(String):
|
|
||||||
```
|
|
||||||
window.QRLogin.code = 200; window.QRLogin.uuid = "xxx"
|
|
||||||
```
|
|
||||||
> 注:这里的appid就是在微信开放平台注册的应用的AppID。网页版微信有两个AppID,早期的是`wx782c26e4c19acffb`,在微信客户端上显示为应用名称为`Web微信`;现在用的是`wxeb7ec651dd0aefa9`,显示名称为`微信网页版`。
|
|
||||||
|
|
||||||
<div align=center>
|
|
||||||
<img src="imgs/8.jpg" width="320" height="211"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
| API | 绑定登陆(webwxpushloginurl) |
|
|
||||||
| --- | --------- |
|
|
||||||
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxpushloginurl |
|
|
||||||
| method | GET |
|
|
||||||
| params | **uin**: xxx |
|
|
||||||
|
|
||||||
返回数据(String):
|
|
||||||
```
|
|
||||||
{'msg': 'all ok', 'uuid': 'xxx', 'ret': '0'}
|
|
||||||
|
|
||||||
通过这种方式可以省掉扫二维码这步操作,更加方便
|
|
||||||
```
|
|
||||||
<br>
|
|
||||||
|
|
||||||
| API | 生成二维码 |
|
|
||||||
| --- | --------- |
|
|
||||||
| url | https://login.weixin.qq.com/l/ `uuid` |
|
|
||||||
| method | GET |
|
|
||||||
<br>
|
|
||||||
|
|
||||||
| API | 二维码扫描登录 |
|
|
||||||
| --- | --------- |
|
|
||||||
| url | https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login |
|
|
||||||
| method | GET |
|
|
||||||
| params | **tip**: 1 `未扫描` 0 `已扫描` <br> **uuid**: xxx <br> **_**: `时间戳` |
|
|
||||||
|
|
||||||
返回数据(String):
|
|
||||||
```
|
|
||||||
window.code=xxx;
|
|
||||||
|
|
||||||
xxx:
|
|
||||||
408 登陆超时
|
|
||||||
201 扫描成功
|
|
||||||
200 确认登录
|
|
||||||
|
|
||||||
当返回200时,还会有
|
|
||||||
window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=xxx&uuid=xxx&lang=xxx&scan=xxx";
|
|
||||||
```
|
|
||||||
<br>
|
|
||||||
|
|
||||||
| API | webwxnewloginpage |
|
|
||||||
| --- | --------- |
|
|
||||||
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage |
|
|
||||||
| method | GET |
|
|
||||||
| params | **ticket**: xxx <br> **uuid**: xxx <br> **lang**: zh_CN `语言` <br> **scan**: xxx <br> **fun**: new |
|
|
||||||
|
|
||||||
返回数据(XML):
|
|
||||||
```
|
|
||||||
<error>
|
|
||||||
<ret>0</ret>
|
|
||||||
<message>OK</message>
|
|
||||||
<skey>xxx</skey>
|
|
||||||
<wxsid>xxx</wxsid>
|
|
||||||
<wxuin>xxx</wxuin>
|
|
||||||
<pass_ticket>xxx</pass_ticket>
|
|
||||||
<isgrayscale>1</isgrayscale>
|
|
||||||
</error>
|
|
||||||
```
|
|
||||||
<br>
|
|
||||||
|
|
||||||
### 微信初始化
|
|
||||||
|
|
||||||
| API | webwxinit |
|
|
||||||
| --- | --------- |
|
|
||||||
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?pass_ticket=xxx&skey=xxx&r=xxx |
|
|
||||||
| method | POST |
|
|
||||||
| data | JSON |
|
|
||||||
| header | ContentType: application/json; charset=UTF-8 |
|
|
||||||
| params | { <br> BaseRequest: { <br> Uin: xxx, <br> Sid: xxx, <br> Skey: xxx, <br> DeviceID: xxx, <br> } <br> } |
|
|
||||||
|
|
||||||
返回数据(JSON):
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"BaseResponse": {
|
|
||||||
"Ret": 0,
|
|
||||||
"ErrMsg": ""
|
|
||||||
},
|
|
||||||
"Count": 11,
|
|
||||||
"ContactList": [...],
|
|
||||||
"SyncKey": {
|
|
||||||
"Count": 4,
|
|
||||||
"List": [
|
|
||||||
{
|
|
||||||
"Key": 1,
|
|
||||||
"Val": 635705559
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"User": {
|
|
||||||
"Uin": xxx,
|
|
||||||
"UserName": xxx,
|
|
||||||
"NickName": xxx,
|
|
||||||
"HeadImgUrl": xxx,
|
|
||||||
"RemarkName": "",
|
|
||||||
"PYInitial": "",
|
|
||||||
"PYQuanPin": "",
|
|
||||||
"RemarkPYInitial": "",
|
|
||||||
"RemarkPYQuanPin": "",
|
|
||||||
"HideInputBarFlag": 0,
|
|
||||||
"StarFriend": 0,
|
|
||||||
"Sex": 1,
|
|
||||||
"Signature": "Apt-get install B",
|
|
||||||
"AppAccountFlag": 0,
|
|
||||||
"VerifyFlag": 0,
|
|
||||||
"ContactFlag": 0,
|
|
||||||
"WebWxPluginSwitch": 0,
|
|
||||||
"HeadImgFlag": 1,
|
|
||||||
"SnsFlag": 17
|
|
||||||
},
|
|
||||||
"ChatSet": xxx,
|
|
||||||
"SKey": xxx,
|
|
||||||
"ClientVersion": 369297683,
|
|
||||||
"SystemTime": 1453124908,
|
|
||||||
"GrayScale": 1,
|
|
||||||
"InviteStartCount": 40,
|
|
||||||
"MPSubscribeMsgCount": 2,
|
|
||||||
"MPSubscribeMsgList": [...],
|
|
||||||
"ClickReportInterval": 600000
|
|
||||||
}
|
|
||||||
```
|
|
||||||
<br>
|
|
||||||
|
|
||||||
| API | webwxstatusnotify |
|
|
||||||
| --- | --------- |
|
|
||||||
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify?lang=zh_CN&pass_ticket=xxx |
|
|
||||||
| method | POST |
|
|
||||||
| data | JSON |
|
|
||||||
| header | ContentType: application/json; charset=UTF-8 |
|
|
||||||
| params | { <br> BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> Code: 3, <br> FromUserName: `自己ID`, <br> ToUserName: `自己ID`, <br> ClientMsgId: `时间戳` <br> } |
|
|
||||||
|
|
||||||
返回数据(JSON):
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"BaseResponse": {
|
|
||||||
"Ret": 0,
|
|
||||||
"ErrMsg": ""
|
|
||||||
},
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
<br>
|
|
||||||
|
|
||||||
### 获取联系人信息
|
|
||||||
|
|
||||||
| API | webwxgetcontact |
|
|
||||||
| --- | --------- |
|
|
||||||
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin//webwxgetcontact?pass_ticket=xxx&skey=xxx&r=xxx |
|
|
||||||
| method | POST |
|
|
||||||
| data | JSON |
|
|
||||||
| header | ContentType: application/json; charset=UTF-8 |
|
|
||||||
|
|
||||||
返回数据(JSON):
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"BaseResponse": {
|
|
||||||
"Ret": 0,
|
|
||||||
"ErrMsg": ""
|
|
||||||
},
|
|
||||||
"MemberCount": 334,
|
|
||||||
"MemberList": [
|
|
||||||
{
|
|
||||||
"Uin": 0,
|
|
||||||
"UserName": xxx,
|
|
||||||
"NickName": "Urinx",
|
|
||||||
"HeadImgUrl": xxx,
|
|
||||||
"ContactFlag": 3,
|
|
||||||
"MemberCount": 0,
|
|
||||||
"MemberList": [],
|
|
||||||
"RemarkName": "",
|
|
||||||
"HideInputBarFlag": 0,
|
|
||||||
"Sex": 0,
|
|
||||||
"Signature": "你好,我们是地球三体组织。在这里,你将感受到不一样的思维模式,以及颠覆常规的世界观。而我们的目标,就是以三体人的智慧,引领人类未来科学技术500年。",
|
|
||||||
"VerifyFlag": 8,
|
|
||||||
"OwnerUin": 0,
|
|
||||||
"PYInitial": "URINX",
|
|
||||||
"PYQuanPin": "Urinx",
|
|
||||||
"RemarkPYInitial": "",
|
|
||||||
"RemarkPYQuanPin": "",
|
|
||||||
"StarFriend": 0,
|
|
||||||
"AppAccountFlag": 0,
|
|
||||||
"Statues": 0,
|
|
||||||
"AttrStatus": 0,
|
|
||||||
"Province": "",
|
|
||||||
"City": "",
|
|
||||||
"Alias": "Urinxs",
|
|
||||||
"SnsFlag": 0,
|
|
||||||
"UniFriend": 0,
|
|
||||||
"DisplayName": "",
|
|
||||||
"ChatRoomId": 0,
|
|
||||||
"KeyWord": "gh_",
|
|
||||||
"EncryChatRoomId": ""
|
|
||||||
},
|
|
||||||
...
|
|
||||||
],
|
|
||||||
"Seq": 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
<br>
|
|
||||||
|
|
||||||
| API | webwxbatchgetcontact |
|
|
||||||
| --- | --------- |
|
|
||||||
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact?type=ex&r=xxx&pass_ticket=xxx |
|
|
||||||
| method | POST |
|
|
||||||
| data | JSON |
|
|
||||||
| header | ContentType: application/json; charset=UTF-8 |
|
|
||||||
| params | { <br> BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> Count: `群数量`, <br> List: [ <br> { UserName: `群ID`, EncryChatRoomId: "" }, <br> ... <br> ], <br> } |
|
|
||||||
|
|
||||||
返回数据(JSON)同上
|
|
||||||
<br><br>
|
|
||||||
|
|
||||||
### 同步刷新
|
|
||||||
|
|
||||||
| API | synccheck |
|
|
||||||
| --- | --------- |
|
|
||||||
| protocol | https |
|
|
||||||
| host | webpush.weixin.qq.com <br> webpush.wx2.qq.com <br> webpush.wx8.qq.com <br> webpush.wx.qq.com <br> webpush.web2.wechat.com <br> webpush.web.wechat.com |
|
|
||||||
| path | /cgi-bin/mmwebwx-bin/synccheck |
|
|
||||||
| method | GET |
|
|
||||||
| data | URL Encode |
|
|
||||||
| params | **r**: `时间戳` <br> **sid**: xxx <br> **uin**: xxx <br> **skey**: xxx <br> **deviceid**: xxx <br> **synckey**: xxx <br> **_**: `时间戳` |
|
|
||||||
|
|
||||||
返回数据(String):
|
|
||||||
```
|
|
||||||
window.synccheck={retcode:"xxx",selector:"xxx"}
|
|
||||||
|
|
||||||
retcode:
|
|
||||||
0 正常
|
|
||||||
1100 失败/登出微信
|
|
||||||
selector:
|
|
||||||
0 正常
|
|
||||||
2 新的消息
|
|
||||||
7 进入/离开聊天界面
|
|
||||||
```
|
|
||||||
<br>
|
|
||||||
|
|
||||||
| API | webwxsync |
|
|
||||||
| --- | --------- |
|
|
||||||
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=xxx&skey=xxx&pass_ticket=xxx |
|
|
||||||
| method | POST |
|
|
||||||
| data | JSON |
|
|
||||||
| header | ContentType: application/json; charset=UTF-8 |
|
|
||||||
| params | { <br> BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> SyncKey: xxx, <br> rr: `时间戳取反` <br> } |
|
|
||||||
|
|
||||||
返回数据(JSON):
|
|
||||||
```
|
|
||||||
{
|
|
||||||
'BaseResponse': {'ErrMsg': '', 'Ret': 0},
|
|
||||||
'SyncKey': {
|
|
||||||
'Count': 7,
|
|
||||||
'List': [
|
|
||||||
{'Val': 636214192, 'Key': 1},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'ContinueFlag': 0,
|
|
||||||
'AddMsgCount': 1,
|
|
||||||
'AddMsgList': [
|
|
||||||
{
|
|
||||||
'FromUserName': '',
|
|
||||||
'PlayLength': 0,
|
|
||||||
'RecommendInfo': {...},
|
|
||||||
'Content': "",
|
|
||||||
'StatusNotifyUserName': '',
|
|
||||||
'StatusNotifyCode': 5,
|
|
||||||
'Status': 3,
|
|
||||||
'VoiceLength': 0,
|
|
||||||
'ToUserName': '',
|
|
||||||
'ForwardFlag': 0,
|
|
||||||
'AppMsgType': 0,
|
|
||||||
'AppInfo': {'Type': 0, 'AppID': ''},
|
|
||||||
'Url': '',
|
|
||||||
'ImgStatus': 1,
|
|
||||||
'MsgType': 51,
|
|
||||||
'ImgHeight': 0,
|
|
||||||
'MediaId': '',
|
|
||||||
'FileName': '',
|
|
||||||
'FileSize': '',
|
|
||||||
...
|
|
||||||
},
|
|
||||||
...
|
|
||||||
],
|
|
||||||
'ModChatRoomMemberCount': 0,
|
|
||||||
'ModContactList': [],
|
|
||||||
'DelContactList': [],
|
|
||||||
'ModChatRoomMemberList': [],
|
|
||||||
'DelContactCount': 0,
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
<br>
|
|
||||||
|
|
||||||
### 消息接口
|
|
||||||
|
|
||||||
| API | webwxsendmsg |
|
|
||||||
| --- | ------------ |
|
|
||||||
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?pass_ticket=xxx |
|
|
||||||
| method | POST |
|
|
||||||
| data | JSON |
|
|
||||||
| header | ContentType: application/json; charset=UTF-8 |
|
|
||||||
| params | { <br> BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> Msg: { <br> Type: 1 `文字消息`, <br> Content: `要发送的消息`, <br> FromUserName: `自己ID`, <br> ToUserName: `好友ID`, <br> LocalID: `与clientMsgId相同`, <br> ClientMsgId: `时间戳左移4位随后补上4位随机数` <br> } <br> } |
|
|
||||||
|
|
||||||
返回数据(JSON):
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"BaseResponse": {
|
|
||||||
"Ret": 0,
|
|
||||||
"ErrMsg": ""
|
|
||||||
},
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| API | webwxrevokemsg |
|
|
||||||
| --- | ------------ |
|
|
||||||
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxrevokemsg |
|
|
||||||
| method | POST |
|
|
||||||
| data | JSON |
|
|
||||||
| header | ContentType: application/json; charset=UTF-8 |
|
|
||||||
| params | { <br> BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> SvrMsgId: msg_id, <br> ToUserName: user_id, <br> ClientMsgId: local_msg_id <br> } |
|
|
||||||
|
|
||||||
返回数据(JSON):
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"BaseResponse": {
|
|
||||||
"Ret": 0,
|
|
||||||
"ErrMsg": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 发送表情
|
|
||||||
|
|
||||||
| API | webwxsendmsgemotion |
|
|
||||||
| --- | ------------ |
|
|
||||||
| url | https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendemoticon?fun=sys&f=json&pass_ticket=xxx |
|
|
||||||
| method | POST |
|
|
||||||
| data | JSON |
|
|
||||||
| header | ContentType: application/json; charset=UTF-8 |
|
|
||||||
| params | { <br> BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> Msg: { <br> Type: 47 `emoji消息`, <br> EmojiFlag: 2, <br> MediaId: `表情上传后的媒体ID`, <br> FromUserName: `自己ID`, <br> ToUserName: `好友ID`, <br> LocalID: `与clientMsgId相同`, <br> ClientMsgId: `时间戳左移4位随后补上4位随机数` <br> } <br> } |
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
### 图片接口
|
|
||||||
|
|
||||||
| API | webwxgeticon |
|
|
||||||
| --- | ------------ |
|
|
||||||
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgeticon |
|
|
||||||
| method | GET |
|
|
||||||
| params | **seq**: `数字,可为空` <br> **username**: `ID` <br> **skey**: xxx |
|
|
||||||
<br>
|
|
||||||
|
|
||||||
| API | webwxgetheadimg |
|
|
||||||
| --- | --------------- |
|
|
||||||
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetheadimg |
|
|
||||||
| method | GET |
|
|
||||||
| params | **seq**: `数字,可为空` <br> **username**: `群ID` <br> **skey**: xxx |
|
|
||||||
<br>
|
|
||||||
|
|
||||||
| API | webwxgetmsgimg |
|
|
||||||
| --- | --------------- |
|
|
||||||
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg |
|
|
||||||
| method | GET |
|
|
||||||
| params | **MsgID**: `消息ID` <br> **type**: slave `略缩图` or `为空时加载原图` <br> **skey**: xxx |
|
|
||||||
<br>
|
|
||||||
|
|
||||||
### 多媒体接口
|
|
||||||
|
|
||||||
| API | webwxgetvideo |
|
|
||||||
| --- | --------------- |
|
|
||||||
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvideo |
|
|
||||||
| method | GET |
|
|
||||||
| params | **msgid**: `消息ID` <br> **skey**: xxx |
|
|
||||||
<br>
|
|
||||||
|
|
||||||
| API | webwxgetvoice |
|
|
||||||
| --- | --------------- |
|
|
||||||
| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvoice |
|
|
||||||
| method | GET |
|
|
||||||
| params | **msgid**: `消息ID` <br> **skey**: xxx |
|
|
||||||
<br>
|
|
||||||
|
|
||||||
### 账号类型
|
|
||||||
|
|
||||||
| 类型 | 说明 |
|
|
||||||
| :--: | --- |
|
|
||||||
| 个人账号 | 以`@`开头,例如:`@xxx` |
|
|
||||||
| 群聊 | 以`@@`开头,例如:`@@xxx` |
|
|
||||||
| 公众号/服务号 | 以`@`开头,但其`VerifyFlag` & 8 != 0 <br><br> `VerifyFlag`: <br> 一般个人公众号/服务号:8 <br> 一般企业的服务号:24 <br> 微信官方账号`微信团队`:56 |
|
|
||||||
| 特殊账号 | 像文件传输助手之类的账号,有特殊的ID,目前已知的有:<br> `filehelper`, `newsapp`, `fmessage`, `weibo`, `qqmail`, `tmessage`, `qmessage`, `qqsync`, `floatbottle`, `lbsapp`, `shakeapp`, `medianote`, `qqfriend`, `readerapp`, `blogapp`, `facebookapp`, `masssendapp`, `meishiapp`, `feedsapp`, `voip`, `blogappweixin`, `weixin`, `brandsessionholder`, `weixinreminder`, `officialaccounts`, `notification_messages`, `wxitil`, `userexperience_alarm`, `notification_messages` |
|
|
||||||
<br>
|
|
||||||
|
|
||||||
### 消息类型
|
|
||||||
|
|
||||||
消息一般格式:
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"FromUserName": "",
|
|
||||||
"ToUserName": "",
|
|
||||||
"Content": "",
|
|
||||||
"StatusNotifyUserName": "",
|
|
||||||
"ImgWidth": 0,
|
|
||||||
"PlayLength": 0,
|
|
||||||
"RecommendInfo": {...},
|
|
||||||
"StatusNotifyCode": 4,
|
|
||||||
"NewMsgId": "",
|
|
||||||
"Status": 3,
|
|
||||||
"VoiceLength": 0,
|
|
||||||
"ForwardFlag": 0,
|
|
||||||
"AppMsgType": 0,
|
|
||||||
"Ticket": "",
|
|
||||||
"AppInfo": {...},
|
|
||||||
"Url": "",
|
|
||||||
"ImgStatus": 1,
|
|
||||||
"MsgType": 1,
|
|
||||||
"ImgHeight": 0,
|
|
||||||
"MediaId": "",
|
|
||||||
"MsgId": "",
|
|
||||||
"FileName": "",
|
|
||||||
"HasProductId": 0,
|
|
||||||
"FileSize": "",
|
|
||||||
"CreateTime": 1454602196,
|
|
||||||
"SubMsgType": 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
<br>
|
|
||||||
|
|
||||||
| MsgType | 说明 |
|
|
||||||
| ------- | --- |
|
|
||||||
| 1 | 文本消息 |
|
|
||||||
| 3 | 图片消息 |
|
|
||||||
| 34 | 语音消息 |
|
|
||||||
| 37 | 好友确认消息 |
|
|
||||||
| 40 | POSSIBLEFRIEND_MSG |
|
|
||||||
| 42 | 共享名片 |
|
|
||||||
| 43 | 视频消息 |
|
|
||||||
| 47 | 动画表情 |
|
|
||||||
| 48 | 位置消息 |
|
|
||||||
| 49 | 分享链接 |
|
|
||||||
| 50 | VOIPMSG |
|
|
||||||
| 51 | 微信初始化消息 |
|
|
||||||
| 52 | VOIPNOTIFY |
|
|
||||||
| 53 | VOIPINVITE |
|
|
||||||
| 62 | 小视频 |
|
|
||||||
| 9999 | SYSNOTICE |
|
|
||||||
| 10000 | 系统消息 |
|
|
||||||
| 10002 | 撤回消息 |
|
|
||||||
<br>
|
|
||||||
|
|
||||||
**微信初始化消息**
|
|
||||||
```html
|
|
||||||
MsgType: 51
|
|
||||||
FromUserName: 自己ID
|
|
||||||
ToUserName: 自己ID
|
|
||||||
StatusNotifyUserName: 最近联系的联系人ID
|
|
||||||
Content:
|
|
||||||
<msg>
|
|
||||||
<op id='4'>
|
|
||||||
<username>
|
|
||||||
// 最近联系的联系人
|
|
||||||
filehelper,xxx@chatroom,wxid_xxx,xxx,...
|
|
||||||
</username>
|
|
||||||
<unreadchatlist>
|
|
||||||
<chat>
|
|
||||||
<username>
|
|
||||||
// 朋友圈
|
|
||||||
MomentsUnreadMsgStatus
|
|
||||||
</username>
|
|
||||||
<lastreadtime>
|
|
||||||
1454502365
|
|
||||||
</lastreadtime>
|
|
||||||
</chat>
|
|
||||||
</unreadchatlist>
|
|
||||||
<unreadfunctionlist>
|
|
||||||
// 未读的功能账号消息,群发助手,漂流瓶等
|
|
||||||
</unreadfunctionlist>
|
|
||||||
</op>
|
|
||||||
</msg>
|
|
||||||
```
|
|
||||||
|
|
||||||
**文本消息**
|
|
||||||
```
|
|
||||||
MsgType: 1
|
|
||||||
FromUserName: 发送方ID
|
|
||||||
ToUserName: 接收方ID
|
|
||||||
Content: 消息内容
|
|
||||||
```
|
|
||||||
|
|
||||||
**图片消息**
|
|
||||||
```html
|
|
||||||
MsgType: 3
|
|
||||||
FromUserName: 发送方ID
|
|
||||||
ToUserName: 接收方ID
|
|
||||||
MsgId: 用于获取图片
|
|
||||||
Content:
|
|
||||||
<msg>
|
|
||||||
<img length="6503" hdlength="0" />
|
|
||||||
<commenturl></commenturl>
|
|
||||||
</msg>
|
|
||||||
```
|
|
||||||
|
|
||||||
**小视频消息**
|
|
||||||
```html
|
|
||||||
MsgType: 62
|
|
||||||
FromUserName: 发送方ID
|
|
||||||
ToUserName: 接收方ID
|
|
||||||
MsgId: 用于获取小视频
|
|
||||||
Content:
|
|
||||||
<msg>
|
|
||||||
<img length="6503" hdlength="0" />
|
|
||||||
<commenturl></commenturl>
|
|
||||||
</msg>
|
|
||||||
```
|
|
||||||
|
|
||||||
**地理位置消息**
|
|
||||||
```
|
|
||||||
MsgType: 1
|
|
||||||
FromUserName: 发送方ID
|
|
||||||
ToUserName: 接收方ID
|
|
||||||
Content: http://weixin.qq.com/cgi-bin/redirectforward?args=xxx
|
|
||||||
// 属于文本消息,只不过内容是一个跳转到地图的链接
|
|
||||||
```
|
|
||||||
|
|
||||||
**名片消息**
|
|
||||||
```js
|
|
||||||
MsgType: 42
|
|
||||||
FromUserName: 发送方ID
|
|
||||||
ToUserName: 接收方ID
|
|
||||||
Content:
|
|
||||||
<?xml version="1.0"?>
|
|
||||||
<msg bigheadimgurl="" smallheadimgurl="" username="" nickname="" shortpy="" alias="" imagestatus="3" scene="17" province="" city="" sign="" sex="1" certflag="0" certinfo="" brandIconUrl="" brandHomeUrl="" brandSubscriptConfigUrl="" brandFlags="0" regionCode="" />
|
|
||||||
|
|
||||||
RecommendInfo:
|
|
||||||
{
|
|
||||||
"UserName": "xxx", // ID
|
|
||||||
"Province": "xxx",
|
|
||||||
"City": "xxx",
|
|
||||||
"Scene": 17,
|
|
||||||
"QQNum": 0,
|
|
||||||
"Content": "",
|
|
||||||
"Alias": "xxx", // 微信号
|
|
||||||
"OpCode": 0,
|
|
||||||
"Signature": "",
|
|
||||||
"Ticket": "",
|
|
||||||
"Sex": 0, // 1:男, 2:女
|
|
||||||
"NickName": "xxx", // 昵称
|
|
||||||
"AttrStatus": 4293221,
|
|
||||||
"VerifyFlag": 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**语音消息**
|
|
||||||
```html
|
|
||||||
MsgType: 34
|
|
||||||
FromUserName: 发送方ID
|
|
||||||
ToUserName: 接收方ID
|
|
||||||
MsgId: 用于获取语音
|
|
||||||
Content:
|
|
||||||
<msg>
|
|
||||||
<voicemsg endflag="1" cancelflag="0" forwardflag="0" voiceformat="4" voicelength="1580" length="2026" bufid="216825389722501519" clientmsgid="49efec63a9774a65a932a4e5fcd4e923filehelper174_1454602489" fromusername="" />
|
|
||||||
</msg>
|
|
||||||
```
|
|
||||||
|
|
||||||
**动画表情**
|
|
||||||
```html
|
|
||||||
MsgType: 47
|
|
||||||
FromUserName: 发送方ID
|
|
||||||
ToUserName: 接收方ID
|
|
||||||
Content:
|
|
||||||
<msg>
|
|
||||||
<emoji fromusername = "" tousername = "" type="2" idbuffer="media:0_0" md5="e68363487d8f0519c4e1047de403b2e7" len = "86235" productid="com.tencent.xin.emoticon.bilibili" androidmd5="e68363487d8f0519c4e1047de403b2e7" androidlen="86235" s60v3md5 = "e68363487d8f0519c4e1047de403b2e7" s60v3len="86235" s60v5md5 = "e68363487d8f0519c4e1047de403b2e7" s60v5len="86235" cdnurl = "http://emoji.qpic.cn/wx_emoji/eFygWtxcoMF8M0oCCsksMA0gplXAFQNpiaqsmOicbXl1OC4Tyx18SGsQ/" designerid = "" thumburl = "http://mmbiz.qpic.cn/mmemoticon/dx4Y70y9XctRJf6tKsy7FwWosxd4DAtItSfhKS0Czr56A70p8U5O8g/0" encrypturl = "http://emoji.qpic.cn/wx_emoji/UyYVK8GMlq5VnJ56a4GkKHAiaC266Y0me0KtW6JN2FAZcXiaFKccRevA/" aeskey= "a911cc2ec96ddb781b5ca85d24143642" ></emoji>
|
|
||||||
<gameext type="0" content="0" ></gameext>
|
|
||||||
</msg>
|
|
||||||
```
|
|
||||||
|
|
||||||
**普通链接或应用分享消息**
|
|
||||||
```html
|
|
||||||
MsgType: 49
|
|
||||||
AppMsgType: 5
|
|
||||||
FromUserName: 发送方ID
|
|
||||||
ToUserName: 接收方ID
|
|
||||||
Url: 链接地址
|
|
||||||
FileName: 链接标题
|
|
||||||
Content:
|
|
||||||
<msg>
|
|
||||||
<appmsg appid="" sdkver="0">
|
|
||||||
<title></title>
|
|
||||||
<des></des>
|
|
||||||
<type>5</type>
|
|
||||||
<content></content>
|
|
||||||
<url></url>
|
|
||||||
<thumburl></thumburl>
|
|
||||||
...
|
|
||||||
</appmsg>
|
|
||||||
<appinfo>
|
|
||||||
<version></version>
|
|
||||||
<appname></appname>
|
|
||||||
</appinfo>
|
|
||||||
</msg>
|
|
||||||
```
|
|
||||||
|
|
||||||
**音乐链接消息**
|
|
||||||
```html
|
|
||||||
MsgType: 49
|
|
||||||
AppMsgType: 3
|
|
||||||
FromUserName: 发送方ID
|
|
||||||
ToUserName: 接收方ID
|
|
||||||
Url: 链接地址
|
|
||||||
FileName: 音乐名
|
|
||||||
|
|
||||||
AppInfo: // 分享链接的应用
|
|
||||||
{
|
|
||||||
Type: 0,
|
|
||||||
AppID: wx485a97c844086dc9
|
|
||||||
}
|
|
||||||
|
|
||||||
Content:
|
|
||||||
<msg>
|
|
||||||
<appmsg appid="wx485a97c844086dc9" sdkver="0">
|
|
||||||
<title></title>
|
|
||||||
<des></des>
|
|
||||||
<action></action>
|
|
||||||
<type>3</type>
|
|
||||||
<showtype>0</showtype>
|
|
||||||
<mediatagname></mediatagname>
|
|
||||||
<messageext></messageext>
|
|
||||||
<messageaction></messageaction>
|
|
||||||
<content></content>
|
|
||||||
<contentattr>0</contentattr>
|
|
||||||
<url></url>
|
|
||||||
<lowurl></lowurl>
|
|
||||||
<dataurl>
|
|
||||||
http://ws.stream.qqmusic.qq.com/C100003i9hMt1bgui0.m4a?vkey=6867EF99F3684&guid=ffffffffc104ea2964a111cf3ff3edaf&fromtag=46
|
|
||||||
</dataurl>
|
|
||||||
<lowdataurl>
|
|
||||||
http://ws.stream.qqmusic.qq.com/C100003i9hMt1bgui0.m4a?vkey=6867EF99F3684&guid=ffffffffc104ea2964a111cf3ff3edaf&fromtag=46
|
|
||||||
</lowdataurl>
|
|
||||||
<appattach>
|
|
||||||
<totallen>0</totallen>
|
|
||||||
<attachid></attachid>
|
|
||||||
<emoticonmd5></emoticonmd5>
|
|
||||||
<fileext></fileext>
|
|
||||||
</appattach>
|
|
||||||
<extinfo></extinfo>
|
|
||||||
<sourceusername></sourceusername>
|
|
||||||
<sourcedisplayname></sourcedisplayname>
|
|
||||||
<commenturl></commenturl>
|
|
||||||
<thumburl>
|
|
||||||
http://imgcache.qq.com/music/photo/album/63/180_albumpic_143163_0.jpg
|
|
||||||
</thumburl>
|
|
||||||
<md5></md5>
|
|
||||||
</appmsg>
|
|
||||||
<fromusername></fromusername>
|
|
||||||
<scene>0</scene>
|
|
||||||
<appinfo>
|
|
||||||
<version>29</version>
|
|
||||||
<appname>摇一摇搜歌</appname>
|
|
||||||
</appinfo>
|
|
||||||
<commenturl></commenturl>
|
|
||||||
</msg>
|
|
||||||
```
|
|
||||||
|
|
||||||
**群消息**
|
|
||||||
```
|
|
||||||
MsgType: 1
|
|
||||||
FromUserName: @@xxx
|
|
||||||
ToUserName: @xxx
|
|
||||||
Content:
|
|
||||||
@xxx:<br/>xxx
|
|
||||||
```
|
|
||||||
|
|
||||||
**红包消息**
|
|
||||||
```
|
|
||||||
MsgType: 49
|
|
||||||
AppMsgType: 2001
|
|
||||||
FromUserName: 发送方ID
|
|
||||||
ToUserName: 接收方ID
|
|
||||||
Content: 未知
|
|
||||||
```
|
|
||||||
注:根据网页版的代码可以看到未来可能支持查看红包消息,但目前走的是系统消息,见下。
|
|
||||||
|
|
||||||
**系统消息**
|
|
||||||
```
|
|
||||||
MsgType: 10000
|
|
||||||
FromUserName: 发送方ID
|
|
||||||
ToUserName: 自己ID
|
|
||||||
Content:
|
|
||||||
"你已添加了 xxx ,现在可以开始聊天了。"
|
|
||||||
"如果陌生人主动添加你为朋友,请谨慎核实对方身份。"
|
|
||||||
"收到红包,请在手机上查看"
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## <a name="Discussion-Group">Discussion Group</a>
|
|
||||||
如果你希望和 WeixinBot 的其他开发者交流,或者有什么问题和建议,欢迎大家加入微信群【Youth fed the dog】一起讨论。扫描下面的二维码添加机器人为好友,并回复【Aidog】获取入群链接。
|
|
||||||
|
|
||||||
<div align=center>
|
|
||||||
<img src="imgs/groupQrcode.jpg" width="220" height="220" alt="join us"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
注:这个不是群的二维码,是机器人拉你入群,记得回复机器人【Aidog】哦~ (secret code: Aidog)
|
|
||||||
|
|
||||||
## <a name="Recent-Update">Recent Update</a>
|
|
||||||
|
|
||||||
- association_login
|
|
||||||
目前网页版微信已经可以脱离扫码,但是依然需要在客户端进行确认登录。
|
|
||||||
|
Before Width: | Height: | Size: 304 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 68 KiB |
@@ -0,0 +1,180 @@
|
|||||||
|
import argparse
|
||||||
|
import getpass
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from weixin import WebWeixin
|
||||||
|
from sqlcipher3 import dbapi2 as sqlite
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("-f", "--db-file", required=True, help="数据库文件")
|
||||||
|
parser.add_argument("-d", "--data-dir",required=True, help="文件存储目录")
|
||||||
|
args = parser.parse_args()
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
class WXBot(WebWeixin):
|
||||||
|
def getUserNickName(self, id):
|
||||||
|
if id == self.User['UserName']:
|
||||||
|
return self.User['NickName']
|
||||||
|
for member in self.ContactList:
|
||||||
|
if member['UserName'] == id:
|
||||||
|
return member['NickName']
|
||||||
|
return None
|
||||||
|
|
||||||
|
def handleMsg(self, r):
|
||||||
|
for msg in r['AddMsgList']:
|
||||||
|
msgID = msg['MsgId']
|
||||||
|
msgType = msg['MsgType']
|
||||||
|
sql = "INSERT INTO msg (\
|
||||||
|
msg_id,\
|
||||||
|
create_time,\
|
||||||
|
msg_type,\
|
||||||
|
content,\
|
||||||
|
from_id,\
|
||||||
|
from_name,\
|
||||||
|
from_nickname,\
|
||||||
|
to_id,\
|
||||||
|
to_name,\
|
||||||
|
to_nickname,\
|
||||||
|
group_id,\
|
||||||
|
group_name\
|
||||||
|
) VALUES (\
|
||||||
|
:msg_id,\
|
||||||
|
:create_time,\
|
||||||
|
:msg_type,\
|
||||||
|
:content,\
|
||||||
|
:from_id,\
|
||||||
|
:from_name,\
|
||||||
|
:from_nickname,\
|
||||||
|
:to_id,\
|
||||||
|
:to_name,\
|
||||||
|
:to_nickname,\
|
||||||
|
:group_id,\
|
||||||
|
:group_name\
|
||||||
|
)"
|
||||||
|
params = {
|
||||||
|
"msg_id": msgID,
|
||||||
|
"create_time": msg['CreateTime'],
|
||||||
|
"msg_type": msgType,
|
||||||
|
"content": None,
|
||||||
|
"from_id": msg['FromUserName'],
|
||||||
|
"from_name": self.getUserRemarkName(msg['FromUserName']),
|
||||||
|
"from_nickname": self.getUserNickName(msg['FromUserName']),
|
||||||
|
"to_id": msg['ToUserName'],
|
||||||
|
"to_name": self.getUserRemarkName(msg['ToUserName']),
|
||||||
|
"to_nickname": self.getUserNickName(msg['ToUserName']),
|
||||||
|
"group_id": None,
|
||||||
|
"group_name": None
|
||||||
|
}
|
||||||
|
# 过滤特殊帐号消息
|
||||||
|
if msg['FromUserName'] in self.SpecialUsers or msg['ToUserName'] in self.SpecialUsers:
|
||||||
|
continue
|
||||||
|
if msgType == 1:
|
||||||
|
params['content'] = msg['Content'].replace('<', '<').replace('>', '>')
|
||||||
|
elif msgType == 3:
|
||||||
|
params['content'] = "[图片](%s)" % msgID
|
||||||
|
self.webwxgetmsgimg(msgID)
|
||||||
|
elif msgType == 34:
|
||||||
|
params['content'] = "[语音](%s)" % msgID
|
||||||
|
self.webwxgetvoice(msgID)
|
||||||
|
elif msgType == 43:
|
||||||
|
params['content'] = "[视频](%s)" % msgID
|
||||||
|
self.webwxgetvideo(msgID)
|
||||||
|
elif msgType == 62:
|
||||||
|
params['msg_type'] = 43
|
||||||
|
params['content'] = "[小视频](%s)" % msgID
|
||||||
|
self.webwxgetvideo(msgID)
|
||||||
|
elif msgType == 42:
|
||||||
|
params['msg_type'] = 0
|
||||||
|
params['content'] = "[名片](%s)" % msg['RecommendInfo']['NickName']
|
||||||
|
elif msgType == 47:
|
||||||
|
if msg['HasProductId'] == 1:
|
||||||
|
# 有版权的表情包,无法在Web端查看
|
||||||
|
params['msg_type'] = 0
|
||||||
|
params['content'] = "[表情包图片]"
|
||||||
|
else:
|
||||||
|
params['msg_type'] = 3
|
||||||
|
params['content'] = "[动画表情](%s)" % msgID
|
||||||
|
self.webwxgetmsgimg(msgID)
|
||||||
|
elif msgType == 49:
|
||||||
|
params['msg_type'] = 0
|
||||||
|
params['content'] = "[链接:%s](%s)" % (msg['FileName'], msg['Url'])
|
||||||
|
elif (50, 51, 52, 53, 9999, 10000, 10002).index(msgType) >= 0:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
params['msg_type'] = 0
|
||||||
|
params['content'] = ""
|
||||||
|
continue
|
||||||
|
# 群消息
|
||||||
|
if msg['FromUserName'][:2] == '@@':
|
||||||
|
if ":<br/>" in content:
|
||||||
|
[people, content] = msg['Content'].split(':<br/>', 1)
|
||||||
|
params['group_name'] = self.getUserRemarkName(msg['FromUserName'])
|
||||||
|
params['from_id'] = people
|
||||||
|
params['from_name'] = self.getUserRemarkName(people)
|
||||||
|
params['to_id'] = params['to_name'] = params['to_nickname'] = None
|
||||||
|
elif msg['ToUserName'][:2] == '@@':
|
||||||
|
params['group_name'] = self.getUserRemarkName(msg['ToUserName'])
|
||||||
|
params['from_id'] = self.User['UserName']
|
||||||
|
params['from_name'] = self.User['NickName']
|
||||||
|
params['to_id'] = params['to_name'] = params['to_nickname'] = None
|
||||||
|
conn.execute(sql, params)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
args = parse_args()
|
||||||
|
dataDir = args.data_dir
|
||||||
|
if not os.access(os.path.dirname(args.db_file), os.X_OK) and not os.access(args.db_file, os.W_OK):
|
||||||
|
print("无法创建或打开数据库文件")
|
||||||
|
exit()
|
||||||
|
elif not os.access(dataDir, os.X_OK):
|
||||||
|
print("数据目录不存在或权限不足")
|
||||||
|
exit()
|
||||||
|
conn = sqlite.connect(args.db_file)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
passwd = getpass.getpass("输入数据库密码:")
|
||||||
|
if passwd != '':
|
||||||
|
conn.execute("PRAGMA KEY = %s" % passwd)
|
||||||
|
conn.execute("CREATE TABLE IF NOT EXISTS msg (\
|
||||||
|
msg_id TEXT NOT NULL,\
|
||||||
|
create_time INTEGER NOT NULL,\
|
||||||
|
msg_type INTEGER NOT NULL,\
|
||||||
|
content TEXT,\
|
||||||
|
from_id TEXT,\
|
||||||
|
from_name TEXT,\
|
||||||
|
from_nickname TEXT,\
|
||||||
|
to_id TEXT,\
|
||||||
|
to_name TEXT,\
|
||||||
|
to_nickname TEXT,\
|
||||||
|
group_id TEXT,\
|
||||||
|
group_name TEXT\
|
||||||
|
);")
|
||||||
|
break
|
||||||
|
except sqlite.DatabaseError as e:
|
||||||
|
if 'file is not a database' in e.args:
|
||||||
|
print("密码错误,请重试")
|
||||||
|
else:
|
||||||
|
print("打开数据库失败", e)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print("打开数据库失败", e)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if sys.stdout.encoding == 'cp936':
|
||||||
|
sys.stdout = UnicodeStreamFilter(sys.stdout)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
if not sys.platform.startswith('win'):
|
||||||
|
import coloredlogs
|
||||||
|
coloredlogs.install(level='INFO')
|
||||||
|
wxbot = WXBot()
|
||||||
|
wxbot.saveFolder = dataDir
|
||||||
|
wxbot.start()
|
||||||
@@ -8,3 +8,5 @@ six
|
|||||||
requests_toolbelt
|
requests_toolbelt
|
||||||
pyqrcode
|
pyqrcode
|
||||||
certifi
|
certifi
|
||||||
|
argparse
|
||||||
|
sqlcipher3-binary
|
||||||
@@ -23,7 +23,6 @@ from collections import defaultdict
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from lxml import html
|
from lxml import html
|
||||||
from socket import timeout as timeout_error
|
from socket import timeout as timeout_error
|
||||||
#import pdb
|
|
||||||
|
|
||||||
# for media upload
|
# for media upload
|
||||||
import mimetypes
|
import mimetypes
|
||||||
@@ -616,7 +615,7 @@ class WebWeixin(object):
|
|||||||
def webwxgetmsgimg(self, msgid):
|
def webwxgetmsgimg(self, msgid):
|
||||||
url = self.base_uri + \
|
url = self.base_uri + \
|
||||||
'/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
|
'/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
|
||||||
data = self._get(url)
|
data = self._get(url, 'webwxgetmsgimg')
|
||||||
if data == '':
|
if data == '':
|
||||||
return ''
|
return ''
|
||||||
fn = 'img_' + msgid + '.jpg'
|
fn = 'img_' + msgid + '.jpg'
|
||||||
@@ -1104,7 +1103,7 @@ class WebWeixin(object):
|
|||||||
request.add_header('Range', 'bytes=0-')
|
request.add_header('Range', 'bytes=0-')
|
||||||
try:
|
try:
|
||||||
response = urllib.request.urlopen(request, timeout=timeout) if timeout else urllib.request.urlopen(request)
|
response = urllib.request.urlopen(request, timeout=timeout) if timeout else urllib.request.urlopen(request)
|
||||||
if api == 'webwxgetvoice' or api == 'webwxgetvideo':
|
if api == 'webwxgetvoice' or api == 'webwxgetvideo' or api == 'webwxgetmsgimg':
|
||||||
data = response.read()
|
data = response.read()
|
||||||
else:
|
else:
|
||||||
data = response.read().decode('utf-8')
|
data = response.read().decode('utf-8')
|
||||||
@@ -1204,16 +1203,3 @@ class UnicodeStreamFilter:
|
|||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
self.target.flush()
|
self.target.flush()
|
||||||
|
|
||||||
if sys.stdout.encoding == 'cp936':
|
|
||||||
sys.stdout = UnicodeStreamFilter(sys.stdout)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
if not sys.platform.startswith('win'):
|
|
||||||
import coloredlogs
|
|
||||||
coloredlogs.install(level='DEBUG')
|
|
||||||
|
|
||||||
webwx = WebWeixin()
|
|
||||||
webwx.start()
|
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
{
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"name": "core",
|
||||||
|
"chnname": "NOTA核心",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"title": "msg",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "msg_id",
|
||||||
|
"type": "TEXT",
|
||||||
|
"remark": "",
|
||||||
|
"chnname": "消息ID",
|
||||||
|
"notNull": true,
|
||||||
|
"pk": false,
|
||||||
|
"autoIncrement": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_time",
|
||||||
|
"type": "DATETIME",
|
||||||
|
"remark": "Unix时间戳",
|
||||||
|
"chnname": "创建时间",
|
||||||
|
"notNull": true,
|
||||||
|
"pk": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "msg_type",
|
||||||
|
"type": "ENUM_INTEGER",
|
||||||
|
"remark": "0,不受支持的消息,以文本形式存储\n1,文本消息\n3,图片消息\n34,语音消息\n43,视频消息",
|
||||||
|
"chnname": "消息类型",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "content",
|
||||||
|
"type": "TEXT",
|
||||||
|
"remark": "",
|
||||||
|
"chnname": "消息内容"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "from_id",
|
||||||
|
"type": "TEXT_ID",
|
||||||
|
"remark": "",
|
||||||
|
"chnname": "发送者ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "from_name",
|
||||||
|
"type": "TEXT",
|
||||||
|
"remark": "备注或昵称",
|
||||||
|
"chnname": "发送者"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "from_nickname",
|
||||||
|
"type": "TEXT",
|
||||||
|
"remark": "昵称",
|
||||||
|
"chnname": "发送者昵称"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "to_id",
|
||||||
|
"type": "TEXT_ID",
|
||||||
|
"remark": "",
|
||||||
|
"chnname": "接收者ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "to_name",
|
||||||
|
"type": "TEXT",
|
||||||
|
"remark": "",
|
||||||
|
"chnname": "接收者"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "to_nickname",
|
||||||
|
"type": "TEXT",
|
||||||
|
"remark": "",
|
||||||
|
"chnname": "接收者昵称"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "group_id",
|
||||||
|
"type": "TEXT_ID",
|
||||||
|
"remark": "",
|
||||||
|
"chnname": "群组ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "group_name",
|
||||||
|
"type": "TEXT",
|
||||||
|
"remark": "",
|
||||||
|
"chnname": "群组名称"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexs": [],
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"fieldName": "chnname",
|
||||||
|
"relationNoShow": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "name",
|
||||||
|
"relationNoShow": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "type",
|
||||||
|
"relationNoShow": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "dataType",
|
||||||
|
"relationNoShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "remark",
|
||||||
|
"relationNoShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "pk",
|
||||||
|
"relationNoShow": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "notNull",
|
||||||
|
"relationNoShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "autoIncrement",
|
||||||
|
"relationNoShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "defaultValue",
|
||||||
|
"relationNoShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "relationNoShow",
|
||||||
|
"relationNoShow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "uiHint",
|
||||||
|
"relationNoShow": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"chnname": "消息",
|
||||||
|
"remark": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"graphCanvas": {
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"shape": "table",
|
||||||
|
"title": "label",
|
||||||
|
"moduleName": false,
|
||||||
|
"x": 840,
|
||||||
|
"y": 640,
|
||||||
|
"id": "d140666b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shape": "table",
|
||||||
|
"title": "article",
|
||||||
|
"moduleName": false,
|
||||||
|
"x": 180,
|
||||||
|
"y": 285.7142857142857,
|
||||||
|
"id": "59f92335"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shape": "table",
|
||||||
|
"title": "label_group",
|
||||||
|
"moduleName": false,
|
||||||
|
"x": 1200,
|
||||||
|
"y": 650,
|
||||||
|
"id": "b41dde04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shape": "table",
|
||||||
|
"title": "msg",
|
||||||
|
"moduleName": false,
|
||||||
|
"x": 840,
|
||||||
|
"y": 460,
|
||||||
|
"id": "a3e008eb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shape": "table",
|
||||||
|
"title": "user_role",
|
||||||
|
"moduleName": false,
|
||||||
|
"x": 840,
|
||||||
|
"y": 300,
|
||||||
|
"id": "9e383a0c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shape": "table",
|
||||||
|
"title": "article_field",
|
||||||
|
"moduleName": false,
|
||||||
|
"x": 180,
|
||||||
|
"y": 570,
|
||||||
|
"id": "31a11be8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shape": "table",
|
||||||
|
"title": "article_edition",
|
||||||
|
"moduleName": false,
|
||||||
|
"x": 520,
|
||||||
|
"y": 350,
|
||||||
|
"id": "ff924bfe"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": []
|
||||||
|
},
|
||||||
|
"associations": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dataTypeDomains": {
|
||||||
|
"datatype": [
|
||||||
|
{
|
||||||
|
"name": "文本",
|
||||||
|
"code": "TEXT",
|
||||||
|
"apply": {
|
||||||
|
"Sqlite": {
|
||||||
|
"type": "TEXT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "整数",
|
||||||
|
"code": "INTEGER",
|
||||||
|
"apply": {
|
||||||
|
"Sqlite": {
|
||||||
|
"type": "INTEGER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "实数",
|
||||||
|
"code": "REAL",
|
||||||
|
"apply": {
|
||||||
|
"Sqlite": {
|
||||||
|
"type": "REAL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "日期",
|
||||||
|
"code": "DATE",
|
||||||
|
"apply": {
|
||||||
|
"Sqlite": {
|
||||||
|
"type": "INTEGER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "日期时间",
|
||||||
|
"code": "DATETIME",
|
||||||
|
"apply": {
|
||||||
|
"Sqlite": {
|
||||||
|
"type": "INTEGER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "布尔值",
|
||||||
|
"code": "BOOLEAN",
|
||||||
|
"apply": {
|
||||||
|
"Sqlite": {
|
||||||
|
"type": "INTEGER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "文本枚举",
|
||||||
|
"code": "ENUM_TEXT",
|
||||||
|
"apply": {
|
||||||
|
"Sqlite": {
|
||||||
|
"type": "TEXT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "整数枚举",
|
||||||
|
"code": "ENUM_INTEGER",
|
||||||
|
"apply": {
|
||||||
|
"Sqlite": {
|
||||||
|
"type": "INTEGER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "整数主键",
|
||||||
|
"apply": {
|
||||||
|
"Sqlite": {
|
||||||
|
"type": "INTEGER"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"code": "INTEGER_PK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "文本主键",
|
||||||
|
"code": "TEXT_PK",
|
||||||
|
"apply": {
|
||||||
|
"Sqlite": {
|
||||||
|
"type": "TEXT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "整数唯一",
|
||||||
|
"code": "INTEGER_U",
|
||||||
|
"apply": {
|
||||||
|
"Sqlite": {
|
||||||
|
"type": "INTEGER UNIQUE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "文本唯一",
|
||||||
|
"code": "TEXT_U",
|
||||||
|
"apply": {
|
||||||
|
"Sqlite": {
|
||||||
|
"type": "TEXT UNIQUE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "整数标志符",
|
||||||
|
"code": "INTEGER_ID",
|
||||||
|
"apply": {
|
||||||
|
"Sqlite": {
|
||||||
|
"type": "INTEGER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "文本标识符",
|
||||||
|
"code": "TEXT_ID",
|
||||||
|
"apply": {
|
||||||
|
"Sqlite": {
|
||||||
|
"type": "TEXT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "二进制数据",
|
||||||
|
"code": "BLOB",
|
||||||
|
"apply": {
|
||||||
|
"Sqlite": {
|
||||||
|
"type": "BLOB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"database": [
|
||||||
|
{
|
||||||
|
"code": "Sqlite",
|
||||||
|
"defaultDatabase": true,
|
||||||
|
"fileShow": true,
|
||||||
|
"createTableTemplate": "CREATE TABLE {{=it.entity.title}} (\n{{ pkList = [] ; }}\n{{~ it.entity.fields:field:index }}\n {{? field.pk }}{{ pkList.push(field) }}{{?}}\n {{=field.name}} {{=field.type}}{{= field.notNull ? ' NOT NULL' : '' }}{{= field.defaultValue ? it.func.join(' DEFAULT', field.defaultValue, ' ') : '' }}{{= index < it.entity.fields.length - 1 ? ',' : (pkList.length > 0 ? ',' : '') }}\n{{~}}\n{{? pkList.length > 0 }}\n PRIMARY KEY ({{~ pkList:pkField:i }}{{= pkField.name }}{{= pkField.autoIncrement ? ' AUTOINCREMENT' : '' }}{{= i < pkList.length - 1 ? ',' : '' }}{{~}})\n{{?}}\n){{=it.separator}}\n$blankline\n",
|
||||||
|
"deleteTableTemplate": "DROP TABLE {{=it.entity.title}}{{=it.separator}}",
|
||||||
|
"rebuildTableTemplate": "",
|
||||||
|
"createFieldTemplate": "ALTER TABLE {{=it.entity.title}} ADD {{=it.field.name}} {{=it.field.type}} {{? it.field.notNull}}NOT NULL{{?}} {{? it.field.defaultValue}}DEFAULT {{? null==it.field.defaultValue}}NULL{{??}}'{{=it.field.defaultValue}}'{{?}}{{?}} {{? it.field.pk}}PRIMARY KEY{{?}}{{=it.separator}}",
|
||||||
|
"updateFieldTemplate": "",
|
||||||
|
"deleteFieldTemplate": "",
|
||||||
|
"deleteIndexTemplate": "DROP INDEX {{=it.index.name}}{{=it.separator}}",
|
||||||
|
"createIndexTemplate": "CREATE{{? it.index.isUnique }} UNIQUE{{?}} INDEX {{=it.index.name}} ON {{=it.entity.title}} ({{=it.func.join(...it.index.fields,', ') }}){{=it.separator}}\n\n",
|
||||||
|
"updateTableComment": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"defaultFields": [],
|
||||||
|
"defaultFieldsType": "1",
|
||||||
|
"javaConfig": {},
|
||||||
|
"sqlConfig": ";",
|
||||||
|
"dbs": [],
|
||||||
|
"wordTemplateConfig": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# wxbot_project_py2.7
|
|
||||||
|
|
||||||
目录结构:
|
|
||||||
```bash
|
|
||||||
.
|
|
||||||
├── README.md
|
|
||||||
├── config
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── config_manager.py
|
|
||||||
│ ├── constant.py
|
|
||||||
│ ├── log.py
|
|
||||||
│ ├── requirements.txt
|
|
||||||
│ └── wechat.conf.bak
|
|
||||||
├── db
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── mysql_db.py
|
|
||||||
│ └── sqlite_db.py
|
|
||||||
├── docker
|
|
||||||
│ ├── Dockerfile
|
|
||||||
│ └── README.md
|
|
||||||
├── flask_templates
|
|
||||||
│ ├── index.html
|
|
||||||
│ └── upload.html
|
|
||||||
├── wechat
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── utils.py
|
|
||||||
│ ├── wechat_apis.py
|
|
||||||
│ └── wechat.py
|
|
||||||
├── weixin_bot.py
|
|
||||||
└── wx_handler
|
|
||||||
├── __init__.py
|
|
||||||
├── bot.py
|
|
||||||
├── sendgrid_mail.py
|
|
||||||
└── wechat_msg_processor.py
|
|
||||||
```
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
from config_manager import ConfigManager
|
|
||||||
from constant import Constant
|
|
||||||
from log import Log
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
#===================================================
|
|
||||||
from constant import Constant
|
|
||||||
#---------------------------------------------------
|
|
||||||
import ConfigParser
|
|
||||||
import os
|
|
||||||
#===================================================
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigManager(object):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.config = Constant.WECHAT_CONFIG_FILE
|
|
||||||
self.cp = ConfigParser.ConfigParser()
|
|
||||||
self.cp.read(self.config)
|
|
||||||
|
|
||||||
data_dir = self.get('setting', 'prefix')
|
|
||||||
upload_dir = self.getpath('uploaddir')
|
|
||||||
if not os.path.exists(data_dir):
|
|
||||||
os.makedirs(data_dir)
|
|
||||||
if not os.path.exists(upload_dir):
|
|
||||||
os.makedirs(upload_dir)
|
|
||||||
|
|
||||||
def get(self, section, option):
|
|
||||||
return self.cp.get(section, option)
|
|
||||||
|
|
||||||
def set(self, section, option, value):
|
|
||||||
self.cp.set(section, option, value)
|
|
||||||
self.cp.write(open(self.config, 'w'))
|
|
||||||
|
|
||||||
def getpath(self, dir):
|
|
||||||
prefix = self.get('setting', 'prefix')
|
|
||||||
return prefix + self.get('setting', dir)
|
|
||||||
|
|
||||||
def setup_database(self):
|
|
||||||
path = self.get('setting', 'prefix')
|
|
||||||
conf = [
|
|
||||||
path + self.get('setting', 'uploaddir'),
|
|
||||||
path + self.get('setting', 'datadir'),
|
|
||||||
path + self.get('setting', 'logdir'),
|
|
||||||
]
|
|
||||||
return conf
|
|
||||||
|
|
||||||
def set_wechat_config(self, conf):
|
|
||||||
for [key, value] in conf.items():
|
|
||||||
self.cp.set('wechat', key, value)
|
|
||||||
self.cp.write(open(self.config, 'w'))
|
|
||||||
|
|
||||||
def get_wechat_config(self):
|
|
||||||
uin = self.cp.get('wechat', 'uin')
|
|
||||||
last_login = self.cp.get('wechat', 'last_login')
|
|
||||||
conf = [
|
|
||||||
self.cp.get('wechat', 'uuid'),
|
|
||||||
self.cp.get('wechat', 'redirect_uri'),
|
|
||||||
int(uin if uin else 0),
|
|
||||||
self.cp.get('wechat', 'sid'),
|
|
||||||
self.cp.get('wechat', 'skey'),
|
|
||||||
self.cp.get('wechat', 'pass_ticket'),
|
|
||||||
self.cp.get('wechat', 'synckey'),
|
|
||||||
self.cp.get('wechat', 'device_id'),
|
|
||||||
float(last_login if last_login else 0),
|
|
||||||
]
|
|
||||||
return conf
|
|
||||||
|
|
||||||
def get_wechat_media_dir(self):
|
|
||||||
prefix = self.get('setting', 'prefix')
|
|
||||||
path = prefix + self.cp.get('setting', 'mediapath')
|
|
||||||
return {
|
|
||||||
'webwxgeticon': path + '/icons',
|
|
||||||
'webwxgetheadimg': path + '/headimgs',
|
|
||||||
'webwxgetmsgimg': path + '/msgimgs',
|
|
||||||
'webwxgetvideo': path + '/videos',
|
|
||||||
'webwxgetvoice': path + '/voices',
|
|
||||||
'_showQRCodeImg': path + '/qrcodes',
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_pickle_files(self):
|
|
||||||
prefix = self.get('setting', 'prefix')
|
|
||||||
return {
|
|
||||||
'User': prefix + self.get('setting', 'contact_user'),
|
|
||||||
'MemberList': prefix + self.get('setting', 'contact_member_list'),
|
|
||||||
'GroupList': prefix + self.get('setting', 'contact_group_list'),
|
|
||||||
'GroupMemeberList': prefix + self.get('setting', 'contact_group_memeber_list'),
|
|
||||||
'SpecialUsersList': prefix + self.get('setting', 'contact_special_users_list'),
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_cookie(self):
|
|
||||||
prefix = self.get('setting', 'prefix')
|
|
||||||
path = prefix + self.get('setting', 'cookie')
|
|
||||||
basedir = os.path.dirname(path)
|
|
||||||
if not os.path.exists(basedir):
|
|
||||||
os.makedirs(basedir)
|
|
||||||
return path
|
|
||||||
|
|
||||||
def mysql(self):
|
|
||||||
mysql = {
|
|
||||||
'host': self.get('mysql', 'host'),
|
|
||||||
'port': self.cp.getint('mysql', 'port'),
|
|
||||||
'user': self.get('mysql', 'user'),
|
|
||||||
'passwd': self.get('mysql', 'passwd'),
|
|
||||||
'database': self.get('mysql', 'database'),
|
|
||||||
}
|
|
||||||
return mysql
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
import time
|
|
||||||
|
|
||||||
class Constant(object):
|
|
||||||
"""
|
|
||||||
@brief All used constants are listed here
|
|
||||||
"""
|
|
||||||
|
|
||||||
WECHAT_CONFIG_FILE = 'config/wechat.conf'
|
|
||||||
LOGGING_LOGGER_NAME = 'WeChat'
|
|
||||||
|
|
||||||
QRCODE_BLACK = '\033[40m \033[0m'
|
|
||||||
QRCODE_WHITE = '\033[47m \033[0m'
|
|
||||||
|
|
||||||
HTTP_HEADER_USERAGENT = [('User-agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36')]
|
|
||||||
HTTP_HEADER_CONTENTTYPE = ['ContentType', 'application/json; charset=UTF-8']
|
|
||||||
HTTP_HEADER_CONNECTION = ['Connection', 'keep-alive']
|
|
||||||
HTTP_HEADER_REFERER = ['Referer', 'https://wx.qq.com/']
|
|
||||||
HTTP_HEADER_RANGE = ['Range', 'bytes=0-']
|
|
||||||
|
|
||||||
REGEX_EMOJI = r'<span class="emoji emoji(\w+)"></span>'
|
|
||||||
|
|
||||||
SERVER_LOG_FORMAT = '%(asctime)s - %(pathname)s:%(lineno)d - %(name)s - %(levelname)s - %(message)s'
|
|
||||||
SERVER_UPLOAD_ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'])
|
|
||||||
SERVER_PAGE_UPLOAD = 'upload.html'
|
|
||||||
SERVER_PAGE_INDEX = 'index.html'
|
|
||||||
|
|
||||||
RUN_RESULT_SUCCESS = '成功 %ds\n'
|
|
||||||
RUN_RESULT_FAIL = '失败\n[*] 退出程序\n'
|
|
||||||
MAIN_RESTART = '[*] wait for restart'
|
|
||||||
LOG_MSG_FILE = 'WeChat-Msgs-%Y-%m-%d.json'
|
|
||||||
LOG_MSG_GROUP_LIST_FILE = 'group_list.json'
|
|
||||||
LOG_MSG_QUIT = '\n[*] Force quit.\n'
|
|
||||||
LOG_MSG_FAIL = '失败\n'
|
|
||||||
LOG_MSG_SUCCESS = '成功\n'
|
|
||||||
LOG_MSG_START = '[*] 微信网页版 ... 开动\n'
|
|
||||||
LOG_MSG_RECOVER = '[*] 从配置文件中恢复 ... '
|
|
||||||
LOG_MSG_RECOVER_CONTACT = '[*] 从文件中恢复联系人数据 ... '
|
|
||||||
LOG_MSG_TRY_INIT = '[*] 尝试初始化 ... '
|
|
||||||
LOG_MSG_ASSOCIATION_LOGIN = '[*] 通过关联登录 ... '
|
|
||||||
LOG_MSG_GET_UUID = '[*] 正在获取 uuid ... '
|
|
||||||
LOG_MSG_GET_QRCODE = '[*] 正在获取二维码 ... 成功\n'
|
|
||||||
LOG_MSG_SCAN_QRCODE = '[*] 请使用微信扫描二维码以登录 ... \n'
|
|
||||||
LOG_MSG_CONFIRM_LOGIN = '[*] 请在手机上点击确认以登录 ... \n'
|
|
||||||
LOG_MSG_WAIT_LOGIN_ERR1 = '[登陆超时] \n'
|
|
||||||
LOG_MSG_WAIT_LOGIN_ERR2 = '[登陆异常] \n'
|
|
||||||
LOG_MSG_LOGIN = '[*] 正在登录 ... '
|
|
||||||
LOG_MSG_INIT = '[*] 微信初始化 ... '
|
|
||||||
LOG_MSG_STATUS_NOTIFY = '[*] 开启状态通知 ... '
|
|
||||||
LOG_MSG_GET_CONTACT = '[*] 获取联系人 ... '
|
|
||||||
LOG_MSG_CONTACT_COUNT = '[*] 应有 %s 个联系人,读取到联系人 %d 个\n'
|
|
||||||
LOG_MSG_OTHER_CONTACT_COUNT = '[*] 共有 %d 个群 | %d 个直接联系人 | %d 个特殊账号 | %d 公众号或服务号\n'
|
|
||||||
LOG_MSG_GET_GROUP_MEMBER = '[*] 拉取群聊成员 ... '
|
|
||||||
LOG_MSG_SNAPSHOT = '[*] 保存配置 ... '
|
|
||||||
LOG_MSG_LOGOUT = '[*] 你在手机上登出了微信\n'
|
|
||||||
LOG_MSG_LOGIN_OTHERWHERE = '[*] 你在其他地方登录了 WEB 版微信\n'
|
|
||||||
LOG_MSG_QUIT_ON_PHONE = '[*] 你在手机上主动退出了\n'
|
|
||||||
LOG_MSG_RUNTIME = '[*] Total run: %s\n'
|
|
||||||
LOG_MSG_KILL_PROCESS = 'kill %d'
|
|
||||||
LOG_MSG_NEW_MSG = '>>> %d 条新消息\n'
|
|
||||||
LOG_MSG_LOCATION = '[位置] %s'
|
|
||||||
LOG_MSG_PICTURE = '[图片] %s'
|
|
||||||
LOG_MSG_VOICE = '[语音] %s'
|
|
||||||
LOG_MSG_RECALL = '撤回了一条消息'
|
|
||||||
LOG_MSG_ADD_FRIEND = '%s 请求添加你为好友'
|
|
||||||
LOG_MSG_UNKNOWN_MSG = '[*] 该消息类型为: %d,内容: %s'
|
|
||||||
LOG_MSG_VIDEO = '[小视频] %s'
|
|
||||||
LOG_MSG_NOTIFY_PHONE = '[*] 提示手机网页版微信登录状态\n'
|
|
||||||
LOG_MSG_EMOTION = '[表情] %s'
|
|
||||||
LOG_MSG_NAME_CARD = (
|
|
||||||
'[名片]\n'
|
|
||||||
'=========================\n'
|
|
||||||
'= 昵称: %s\n'
|
|
||||||
'= 微信号: %s\n'
|
|
||||||
'= 地区: %s %s\n'
|
|
||||||
'= 性别: %s\n'
|
|
||||||
'========================='
|
|
||||||
)
|
|
||||||
LOG_MSG_SEX_OPTION = ['未知', '男', '女']
|
|
||||||
LOG_MSG_APP_LINK = (
|
|
||||||
'[%s]\n'
|
|
||||||
'=========================\n'
|
|
||||||
'= 标题: %s\n'
|
|
||||||
'= 描述: %s\n'
|
|
||||||
'= 链接: %s\n'
|
|
||||||
'= 来自: %s\n'
|
|
||||||
'========================='
|
|
||||||
)
|
|
||||||
LOG_MSG_APP_LINK_TYPE = {5: '链接', 3: '音乐', 7: '微博'}
|
|
||||||
LOG_MSG_APP_IMG = (
|
|
||||||
'[图片]\n'
|
|
||||||
'=========================\n'
|
|
||||||
'= 文件: %s\n'
|
|
||||||
'= 来自: %s\n'
|
|
||||||
'========================='
|
|
||||||
)
|
|
||||||
LOG_MSG_SYSTEM = '系统消息'
|
|
||||||
LOG_MSG_UNKNOWN_NAME = '未知_'
|
|
||||||
LOG_MSG_UNKNOWN_GROUP_NAME = '未知群_'
|
|
||||||
|
|
||||||
TABLE_GROUP_MSG_LOG = 'WeChatRoomMessage'
|
|
||||||
TABLE_GROUP_MSG_LOG_COL = """
|
|
||||||
MsgID text,
|
|
||||||
RoomOwnerID text,
|
|
||||||
RoomName text,
|
|
||||||
UserCount text,
|
|
||||||
FromUserName text,
|
|
||||||
ToUserName text,
|
|
||||||
AttrStatus text,
|
|
||||||
DisplayName text,
|
|
||||||
Name text,
|
|
||||||
MsgType text,
|
|
||||||
FaceMsg text,
|
|
||||||
TextMsg text,
|
|
||||||
ImageMsg text,
|
|
||||||
VideoMsg text,
|
|
||||||
SoundMsg text,
|
|
||||||
LinkMsg text,
|
|
||||||
NameCardMsg text,
|
|
||||||
LocationMsg text,
|
|
||||||
RecallMsgID text,
|
|
||||||
SysMsg text,
|
|
||||||
MsgTime text,
|
|
||||||
MsgTimestamp text
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def TABLE_GROUP_LIST():
|
|
||||||
return 'WeChatRoom_' + time.strftime('%Y%m%d', time.localtime())
|
|
||||||
|
|
||||||
TABLE_GROUP_LIST_COL = """
|
|
||||||
RoomName text,
|
|
||||||
RoomID text,
|
|
||||||
RoomOwnerID text,
|
|
||||||
UserCount text,
|
|
||||||
RoomIcon text
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def TABLE_GROUP_USER_LIST():
|
|
||||||
return 'WeChatRoomMember_' + time.strftime('%Y%m%d', time.localtime())
|
|
||||||
|
|
||||||
TABLE_GROUP_USER_LIST_COL = """
|
|
||||||
RoomID text,
|
|
||||||
MemberID text,
|
|
||||||
MemberNickName text,
|
|
||||||
MemberDisplayName text,
|
|
||||||
MemberAttrStatus text
|
|
||||||
"""
|
|
||||||
TABLE_RECORD_ENTER_GROUP = 'WeChatEnterGroupRecord'
|
|
||||||
TABLE_RECORD_ENTER_GROUP_COL = """
|
|
||||||
MsgID text,
|
|
||||||
RoomName text,
|
|
||||||
FromUserName text,
|
|
||||||
ToUserName text,
|
|
||||||
Name text,
|
|
||||||
EnterTime text
|
|
||||||
"""
|
|
||||||
TABLE_RECORD_RENAME_GROUP = 'WeChatRenameGroupRecord'
|
|
||||||
TABLE_RECORD_RENAME_GROUP_COL = """
|
|
||||||
MsgID text,
|
|
||||||
FromName text,
|
|
||||||
ToName text,
|
|
||||||
ModifyPeople text,
|
|
||||||
ModifyTime text
|
|
||||||
"""
|
|
||||||
|
|
||||||
API_APPID = 'wx782c26e4c19acffb'
|
|
||||||
API_WXAPPID = 'wx299208e619de7026' # Weibo
|
|
||||||
# 'wxeb7ec651dd0aefa9' # Weixin
|
|
||||||
API_LANG = 'zh_CN'
|
|
||||||
API_USER_AGENT = (
|
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) '
|
|
||||||
'AppleWebKit/537.36 (KHTML, like Gecko) '
|
|
||||||
'Chrome/48.0.2564.109 Safari/537.36'
|
|
||||||
)
|
|
||||||
API_SPECIAL_USER = [
|
|
||||||
'newsapp', 'filehelper', 'weibo', 'qqmail',
|
|
||||||
'fmessage', 'tmessage', 'qmessage', 'qqsync',
|
|
||||||
'floatbottle', 'lbsapp', 'shakeapp', 'medianote',
|
|
||||||
'qqfriend', 'readerapp', 'blogapp', 'facebookapp',
|
|
||||||
'masssendapp', 'meishiapp', 'feedsapp', 'voip',
|
|
||||||
'blogappweixin', 'brandsessionholder', 'weixin',
|
|
||||||
'weixinreminder', 'officialaccounts', 'wxitil',
|
|
||||||
'notification_messages', 'wxid_novlwrv3lqwv11',
|
|
||||||
'gh_22b87fa7cb3c', 'userexperience_alarm',
|
|
||||||
]
|
|
||||||
|
|
||||||
EMOTICON = [
|
|
||||||
'[Smile]', '[Grimace]', '[Drool]', '[Scowl]', '[CoolGuy]', '[Sob]', '[Shy]',
|
|
||||||
'[Silent]', '[Sleep]', '[Cry]', '[Awkward]', '[Angry]', '[Tongue]', '[Grin]',
|
|
||||||
'[Surprise]', '[Frown]', '[Ruthless]', '[Blush]', '[Scream]', '[Puke]',
|
|
||||||
'[Chuckle]', '[Joyful]', '[Slight]', '[Smug]', '[Hungry]', '[Drowsy]', '[Panic]',
|
|
||||||
'[Sweat]', '[Laugh]', '[Commando]', '[Determined]', '[Scold]', '[Shocked]', '[Shhh]',
|
|
||||||
'[Dizzy]', '[Tormented]', '[Toasted]', '[Skull]', '[Hammer]', '[Wave]',
|
|
||||||
'[Relief]', '[DigNose]', '[Clap]', '[Shame]', '[Trick]',' [Bah!L]','[Bah!R]',
|
|
||||||
'[Yawn]', '[Lookdown]', '[Wronged]', '[Puling]', '[Sly]', '[Kiss]', '[Uh-oh]',
|
|
||||||
'[Whimper]', '[Cleaver]', '[Melon]', '[Beer]', '[Basketball]', '[PingPong]',
|
|
||||||
'[Coffee]', '[Rice]', '[Pig]', '[Rose]', '[Wilt]', '[Lip]', '[Heart]',
|
|
||||||
'[BrokenHeart]', '[Cake]', '[Lightning]', '[Bomb]', '[Dagger]', '[Soccer]', '[Ladybug]',
|
|
||||||
'[Poop]', '[Moon]', '[Sun]', '[Gift]', '[Hug]', '[Strong]',
|
|
||||||
'[Weak]', '[Shake]', '[Victory]', '[Admire]', '[Beckon]', '[Fist]', '[Pinky]',
|
|
||||||
'[Love]', '[No]', '[OK]', '[InLove]', '[Blowkiss]', '[Waddle]', '[Tremble]',
|
|
||||||
'[Aaagh!]', '[Twirl]', '[Kotow]', '[Lookback]', '[Jump]', '[Give-in]',
|
|
||||||
u'\U0001f604', u'\U0001f637', u'\U0001f639', u'\U0001f61d', u'\U0001f632', u'\U0001f633',
|
|
||||||
u'\U0001f631', u'\U0001f64d', u'\U0001f609', u'\U0001f60c', u'\U0001f612', u'\U0001f47f',
|
|
||||||
u'\U0001f47b', u'\U0001f49d', u'\U0001f64f', u'\U0001f4aa', u'\U0001f4b5', u'\U0001f382',
|
|
||||||
u'\U0001f388', u'\U0001f4e6',
|
|
||||||
]
|
|
||||||
BOT_ZHIHU_URL_LATEST = 'http://news-at.zhihu.com/api/4/news/latest'
|
|
||||||
BOT_ZHIHU_URL_DAILY = 'http://daily.zhihu.com/story/'
|
|
||||||
BOT_TULING_API_KEY = '55e7f30895a0a10535984bae5ad294d1'
|
|
||||||
BOT_TULING_API_URL = 'http://www.tuling123.com/openapi/api?key=%s&info=%s&userid=%s'
|
|
||||||
BOT_TULING_BOT_REPLY = u'麻烦说的清楚一点,我听不懂你在说什么'
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
#===================================================
|
|
||||||
from constant import Constant
|
|
||||||
from config import ConfigManager
|
|
||||||
#---------------------------------------------------
|
|
||||||
import logging
|
|
||||||
import logging.config
|
|
||||||
#===================================================
|
|
||||||
|
|
||||||
cm = ConfigManager()
|
|
||||||
|
|
||||||
logging.config.fileConfig(Constant.WECHAT_CONFIG_FILE)
|
|
||||||
# create logger
|
|
||||||
Log = logging.getLogger(Constant.LOGGING_LOGGER_NAME)
|
|
||||||
|
|
||||||
# 'application' code
|
|
||||||
# Log.debug('debug message')
|
|
||||||
# Log.info('info message')
|
|
||||||
# Log.warn('warn message')
|
|
||||||
# Log.error('error message')
|
|
||||||
# Log.critical('critical message')
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
qrcode
|
|
||||||
flask
|
|
||||||
requests
|
|
||||||
requests_toolbelt
|
|
||||||
pymysql
|
|
||||||
sendgrid
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
[wechat]
|
|
||||||
host = wx.qq.com
|
|
||||||
uuid =
|
|
||||||
redirect_uri =
|
|
||||||
uin =
|
|
||||||
sid =
|
|
||||||
skey =
|
|
||||||
pass_ticket =
|
|
||||||
device_id =
|
|
||||||
last_login =
|
|
||||||
|
|
||||||
[setting]
|
|
||||||
prefix = tmp_data/
|
|
||||||
database = WeChat.db
|
|
||||||
datadir = Data/infos/
|
|
||||||
logdir = Logs
|
|
||||||
mediapath = Data
|
|
||||||
uploaddir = Data/upload
|
|
||||||
qrcodedir = Data/qrcode
|
|
||||||
server_port = 8080
|
|
||||||
cookie = Cookie/WeChat.cookie
|
|
||||||
contact_user = Pickle/User.pkl
|
|
||||||
contact_member_list = Pickle/MemberList.pkl
|
|
||||||
contact_group_list = Pickle/GroupList.pkl
|
|
||||||
contact_group_memeber_list = Pickle/GroupMemeberList.pkl
|
|
||||||
contact_special_users_list = Pickle/SpecialUsersList.pkl
|
|
||||||
server_mode = False
|
|
||||||
server_log_file = server.log
|
|
||||||
log_mode = False
|
|
||||||
|
|
||||||
[mysql]
|
|
||||||
host = localhost
|
|
||||||
port = 3306
|
|
||||||
user = root
|
|
||||||
passwd = root
|
|
||||||
database = wechat
|
|
||||||
|
|
||||||
[sendgrid]
|
|
||||||
api_key = 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
|
|
||||||
from_email = wxbot@wechat.com
|
|
||||||
to_email = xxx@example.com
|
|
||||||
|
|
||||||
[loggers]
|
|
||||||
keys = root,WeChat
|
|
||||||
|
|
||||||
[handlers]
|
|
||||||
keys = consoleHandler,fileHandler
|
|
||||||
|
|
||||||
[formatters]
|
|
||||||
keys = simpleFormatter
|
|
||||||
|
|
||||||
[logger_root]
|
|
||||||
level = DEBUG
|
|
||||||
handlers = consoleHandler
|
|
||||||
|
|
||||||
[logger_WeChat]
|
|
||||||
level = DEBUG
|
|
||||||
handlers = fileHandler
|
|
||||||
qualname = WeChat
|
|
||||||
propagate = 0
|
|
||||||
|
|
||||||
[handler_consoleHandler]
|
|
||||||
class = StreamHandler
|
|
||||||
level = DEBUG
|
|
||||||
formatter = simpleFormatter
|
|
||||||
args = (sys.stdout,)
|
|
||||||
|
|
||||||
[handler_fileHandler]
|
|
||||||
class = FileHandler
|
|
||||||
level = DEBUG
|
|
||||||
formatter = simpleFormatter
|
|
||||||
args = ('tmp_data/wechat.log',)
|
|
||||||
|
|
||||||
[formatter_simpleFormatter]
|
|
||||||
format = %(asctime)s - %(name)s - %(levelname)s - %(message)s
|
|
||||||
datefmt =
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
from sqlite_db import SqliteDB
|
|
||||||
from mysql_db import MysqlDB
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
#===================================================
|
|
||||||
from config import Log
|
|
||||||
#---------------------------------------------------
|
|
||||||
import pymysql
|
|
||||||
import threading
|
|
||||||
import traceback
|
|
||||||
#===================================================
|
|
||||||
|
|
||||||
def array_join(arr, c):
|
|
||||||
t = ''
|
|
||||||
for a in arr:
|
|
||||||
t += "'%s'" % str(a).replace("'","\\\'") + c
|
|
||||||
return t[:-len(c)]
|
|
||||||
|
|
||||||
class MysqlDB(object):
|
|
||||||
"""
|
|
||||||
修改服务器上的配置文件/etc/my.cnf,在对应位置添加以下设置:
|
|
||||||
[client]
|
|
||||||
default-character-set = utf8mb4
|
|
||||||
|
|
||||||
[mysql]
|
|
||||||
default-character-set = utf8mb4
|
|
||||||
|
|
||||||
[mysqld]
|
|
||||||
character-set-client-handshake = FALSE
|
|
||||||
character-set-server = utf8mb4
|
|
||||||
collation-server = utf8mb4_unicode_ci
|
|
||||||
init_connect='SET NAMES utf8mb4'
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, conf):
|
|
||||||
self.conf = conf
|
|
||||||
config = {
|
|
||||||
'host': conf['host'],
|
|
||||||
'port': conf['port'],
|
|
||||||
'user': conf['user'],
|
|
||||||
'passwd': conf['passwd'],
|
|
||||||
'charset':'utf8mb4', # 支持1-4个字节字符
|
|
||||||
'cursorclass': pymysql.cursors.DictCursor
|
|
||||||
}
|
|
||||||
self.conn = pymysql.connect(**config)
|
|
||||||
self.conn.autocommit(1)
|
|
||||||
# for thread-save
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self.create_db(conf['database'])
|
|
||||||
self.conn.select_db(conf['database'])
|
|
||||||
|
|
||||||
# cache table cols
|
|
||||||
self.table_cols = {}
|
|
||||||
for t in self.show_tables():
|
|
||||||
self.table_cols[t] = self.get_table_column_name(t)
|
|
||||||
|
|
||||||
def show_database(self):
|
|
||||||
c = self.conn.cursor()
|
|
||||||
sql = 'SHOW DATABASES'
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
c.execute(sql)
|
|
||||||
return [r['Database'] for r in c.fetchall()]
|
|
||||||
|
|
||||||
def show_tables(self):
|
|
||||||
c = self.conn.cursor()
|
|
||||||
sql = 'SHOW TABLES'
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
c.execute(sql)
|
|
||||||
return [r['Tables_in_'+self.conf['database']] for r in c.fetchall()]
|
|
||||||
|
|
||||||
def create_db(self, db_name):
|
|
||||||
"""
|
|
||||||
@brief Creates a database
|
|
||||||
@param db_name String
|
|
||||||
"""
|
|
||||||
if self.conf['database'] not in self.show_database():
|
|
||||||
sql = 'CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci' % db_name
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
self.execute(sql)
|
|
||||||
|
|
||||||
def create_table(self, table, cols):
|
|
||||||
"""
|
|
||||||
@brief Creates a table in database
|
|
||||||
@param table String
|
|
||||||
@param cols String, the cols in table
|
|
||||||
"""
|
|
||||||
if table not in self.table_cols:
|
|
||||||
sql = 'CREATE TABLE IF NOT EXISTS %s(id int primary key auto_increment, %s) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci' % (table, cols)
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
self.execute(sql)
|
|
||||||
self.table_cols[table] = ['id'] + [c.strip().split(' ')[0] for c in cols.split(',')]
|
|
||||||
|
|
||||||
def delete_table(self, table):
|
|
||||||
"""
|
|
||||||
@brief Delete a table in database
|
|
||||||
@param table String
|
|
||||||
"""
|
|
||||||
if table in self.table_cols:
|
|
||||||
sql = "DROP TABLE IF EXISTS %s" % table
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
self.execute(sql)
|
|
||||||
self.table_cols.pop(table)
|
|
||||||
|
|
||||||
def insert(self, table, value):
|
|
||||||
"""
|
|
||||||
@brief Insert a row in table
|
|
||||||
@param table String
|
|
||||||
@param value Tuple
|
|
||||||
"""
|
|
||||||
col_name = self.table_cols[table][1:]
|
|
||||||
sql = "INSERT INTO %s(%s) VALUES (%s)" % (table, str(','.join(col_name)), array_join(value, ','))
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
self.execute(sql)
|
|
||||||
|
|
||||||
def insertmany(self, table, values):
|
|
||||||
"""
|
|
||||||
@brief Insert many rows in table
|
|
||||||
@param table String
|
|
||||||
@param values Array of tuple
|
|
||||||
"""
|
|
||||||
col_name = self.table_cols[table][1:]
|
|
||||||
sql = 'INSERT INTO %s(%s) VALUES (%s)' % (table, ','.join(col_name), ','.join(['%s'] * len(values[0])))
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
self.execute(sql, values)
|
|
||||||
|
|
||||||
def select(self, table, field='', condition=''):
|
|
||||||
"""
|
|
||||||
@brief select all result from table
|
|
||||||
@param table String
|
|
||||||
@param field String
|
|
||||||
@param condition String
|
|
||||||
@return result Tuple
|
|
||||||
"""
|
|
||||||
sql = "SELECT * FROM %s" % table
|
|
||||||
if field and condition:
|
|
||||||
sql += " WHERE %s='%s'" % (field, condition)
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
return self.execute(sql)
|
|
||||||
|
|
||||||
def get_table_column_name(self, table):
|
|
||||||
"""
|
|
||||||
@brief select all result from table
|
|
||||||
@param table String
|
|
||||||
@return result Array
|
|
||||||
"""
|
|
||||||
c = self.conn.cursor()
|
|
||||||
c.execute("SELECT * FROM %s" % table)
|
|
||||||
names = list(map(lambda x: x[0], c.description))
|
|
||||||
return names
|
|
||||||
|
|
||||||
def execute(self, sql, values=None):
|
|
||||||
"""
|
|
||||||
@brief execute sql commands, return result if it has
|
|
||||||
@param sql String
|
|
||||||
@param value Tuple
|
|
||||||
@return result Array
|
|
||||||
"""
|
|
||||||
c = self.conn.cursor()
|
|
||||||
self.lock.acquire()
|
|
||||||
hasReturn = sql.lstrip().upper().startswith("SELECT")
|
|
||||||
|
|
||||||
result = []
|
|
||||||
try:
|
|
||||||
if values:
|
|
||||||
c.executemany(sql, values)
|
|
||||||
else:
|
|
||||||
c.execute(sql)
|
|
||||||
|
|
||||||
if hasReturn:
|
|
||||||
result = c.fetchall()
|
|
||||||
|
|
||||||
except Exception, e:
|
|
||||||
Log.error(traceback.format_exc())
|
|
||||||
self.conn.rollback()
|
|
||||||
finally:
|
|
||||||
self.lock.release()
|
|
||||||
|
|
||||||
if hasReturn:
|
|
||||||
return result
|
|
||||||
|
|
||||||
def delete(self, table, field='', condition=''):
|
|
||||||
"""
|
|
||||||
@brief execute sql commands, return result if it has
|
|
||||||
@param table String
|
|
||||||
@param field String
|
|
||||||
@param condition String
|
|
||||||
"""
|
|
||||||
sql = "DELETE FROM %s WHERE %s=%s" % (table, field, condition)
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
self.execute(sql)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""
|
|
||||||
@brief close connection to database
|
|
||||||
"""
|
|
||||||
Log.debug('DB -> close')
|
|
||||||
# 关闭数据库连接
|
|
||||||
self.conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
#===================================================
|
|
||||||
from config import Log
|
|
||||||
#---------------------------------------------------
|
|
||||||
import sqlite3
|
|
||||||
import threading
|
|
||||||
import traceback
|
|
||||||
#===================================================
|
|
||||||
|
|
||||||
|
|
||||||
def _dict_factory(cursor, row):
|
|
||||||
aDict = {}
|
|
||||||
for iField, field in enumerate (cursor.description):
|
|
||||||
aDict [field [0]] = row [iField]
|
|
||||||
return aDict
|
|
||||||
|
|
||||||
|
|
||||||
class SqliteDB(object):
|
|
||||||
|
|
||||||
def __init__(self, db_file):
|
|
||||||
self.db_file = db_file
|
|
||||||
# self.conn = sqlite3.connect(db_file, check_same_thread=False)
|
|
||||||
# use 8-bit strings instead of unicode string
|
|
||||||
self.conn.text_factory = str
|
|
||||||
# not return a tuple but a dict with column name as key
|
|
||||||
self.conn.row_factory = _dict_factory
|
|
||||||
# for thread-save
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
def set_conn(self):
|
|
||||||
self._conn = sqlite3.connect(self.db_file,check_same_thread=False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def conn(self):
|
|
||||||
try:
|
|
||||||
self._conn.execute('select 1;')
|
|
||||||
# check out conn
|
|
||||||
except (sqlite3.ProgrammingError,AttributeError):
|
|
||||||
# Cannot operate on a closed database
|
|
||||||
self.set_conn()
|
|
||||||
|
|
||||||
finally:
|
|
||||||
return self._conn
|
|
||||||
|
|
||||||
|
|
||||||
def create_table(self, table, cols):
|
|
||||||
"""
|
|
||||||
@brief Creates a table in database
|
|
||||||
@param table String
|
|
||||||
@param cols String, the cols in table
|
|
||||||
"""
|
|
||||||
sql = "CREATE TABLE if not exists %s (%s);" % (table, cols)
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
self.execute(sql)
|
|
||||||
|
|
||||||
def delete_table(self, table):
|
|
||||||
"""
|
|
||||||
@brief Delete a table in database
|
|
||||||
@param table String
|
|
||||||
"""
|
|
||||||
sql = "DROP TABLE if exists %s;" % table
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
self.execute(sql)
|
|
||||||
|
|
||||||
def insert(self, table, value):
|
|
||||||
"""
|
|
||||||
@brief Insert a row in table
|
|
||||||
@param table String
|
|
||||||
@param value Tuple
|
|
||||||
"""
|
|
||||||
sql = ("INSERT INTO %s VALUES (" + ",".join(['?'] * len(value)) + ");") % table
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
self.execute(sql, value)
|
|
||||||
|
|
||||||
def insertmany(self, table, values):
|
|
||||||
"""
|
|
||||||
@brief Insert many rows in table
|
|
||||||
@param table String
|
|
||||||
@param values Array of tuple
|
|
||||||
"""
|
|
||||||
c = self.conn.cursor()
|
|
||||||
self.lock.acquire()
|
|
||||||
n = len(values[0])
|
|
||||||
sql = ("INSERT INTO %s VALUES (" + ",".join(['?'] * n) + ");") % table
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
|
|
||||||
try:
|
|
||||||
c.executemany(sql, values)
|
|
||||||
except Exception, e:
|
|
||||||
Log.error(traceback.format_exc())
|
|
||||||
finally:
|
|
||||||
self.lock.release()
|
|
||||||
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
def select(self, table, field='', condition=''):
|
|
||||||
"""
|
|
||||||
@brief select all result from table
|
|
||||||
@param table String
|
|
||||||
@param field String
|
|
||||||
@param condition String
|
|
||||||
@return result Tuple
|
|
||||||
"""
|
|
||||||
result = []
|
|
||||||
if field and condition:
|
|
||||||
cond = (condition,)
|
|
||||||
sql = "SELECT * FROM %s WHERE %s=?" % (table, field)
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
result = self.execute(sql, cond)
|
|
||||||
else:
|
|
||||||
sql = "SELECT * FROM %s" % table
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
result = self.execute(sql)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def update(self, table, dic, condition=''):
|
|
||||||
k_arr = []
|
|
||||||
v_arr = []
|
|
||||||
for (k, v) in dic.items():
|
|
||||||
k_arr.append('%s=?' % k)
|
|
||||||
v_arr.append(v)
|
|
||||||
|
|
||||||
sql = "UPDATE %s SET %s" % (table, ','.join(k_arr))
|
|
||||||
if condition:
|
|
||||||
sql += " WHERE %s" % condition
|
|
||||||
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
self.execute(sql, tuple(v_arr))
|
|
||||||
|
|
||||||
def get_table_column_name(self, table):
|
|
||||||
"""
|
|
||||||
@brief select all result from table
|
|
||||||
@param table String
|
|
||||||
@return result Array
|
|
||||||
"""
|
|
||||||
c = self.conn.cursor()
|
|
||||||
c.execute("SELECT * FROM %s" % table)
|
|
||||||
names = list(map(lambda x: x[0], c.description))
|
|
||||||
return names
|
|
||||||
|
|
||||||
def execute(self, sql, value=None):
|
|
||||||
"""
|
|
||||||
@brief execute sql commands, return result if it has
|
|
||||||
@param sql String
|
|
||||||
@param value Tuple
|
|
||||||
@return result Array
|
|
||||||
"""
|
|
||||||
c = self.conn.cursor()
|
|
||||||
self.lock.acquire()
|
|
||||||
hasReturn = sql.lstrip().upper().startswith("SELECT")
|
|
||||||
|
|
||||||
try:
|
|
||||||
if value:
|
|
||||||
c.execute(sql, value)
|
|
||||||
else:
|
|
||||||
c.execute(sql)
|
|
||||||
|
|
||||||
if hasReturn:
|
|
||||||
result = c.fetchall()
|
|
||||||
except Exception, e:
|
|
||||||
Log.error(traceback.format_exc())
|
|
||||||
finally:
|
|
||||||
self.lock.release()
|
|
||||||
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
if hasReturn:
|
|
||||||
return result
|
|
||||||
|
|
||||||
def delete(self, table, field='', condition=''):
|
|
||||||
"""
|
|
||||||
@brief execute sql commands, return result if it has
|
|
||||||
@param table String
|
|
||||||
@param field String
|
|
||||||
@param condition String
|
|
||||||
"""
|
|
||||||
sql = "DELETE FROM %s WHERE %s=?" % (table, field)
|
|
||||||
Log.debug('DB -> %s' % sql)
|
|
||||||
cond = (condition,)
|
|
||||||
self.execute(sql, cond)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""
|
|
||||||
@brief close connection to database
|
|
||||||
"""
|
|
||||||
Log.debug('DB -> close')
|
|
||||||
self.conn.close()
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
FROM ubuntu:16.04
|
|
||||||
MAINTAINER Urinx <uri.lqy@gmail.com>
|
|
||||||
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y python \
|
|
||||||
python-dev \
|
|
||||||
python-pip && \
|
|
||||||
apt-get clean && \
|
|
||||||
apt-get autoclean && \
|
|
||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
|
||||||
|
|
||||||
ADD weixin_bot.tar.gz /
|
|
||||||
WORKDIR /weixin_bot
|
|
||||||
|
|
||||||
RUN pip install -r config/requirements.txt
|
|
||||||
EXPOSE 80
|
|
||||||
ENTRYPOINT ["./weixin_bot.py"]
|
|
||||||
CMD [""]
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# 本地构建 wechat_bot docker 镜像
|
|
||||||
|
|
||||||
拉下镜像:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull ubuntu:16.04
|
|
||||||
```
|
|
||||||
|
|
||||||
打包本项目,将压缩包放到`docker`目录下:
|
|
||||||
|
|
||||||
```
|
|
||||||
tar -czf weixin_bot.tar.gz wxbot_project_py2.7/
|
|
||||||
```
|
|
||||||
|
|
||||||
切换到`docker`目录,执行`build`命令:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -t wechat-bot .
|
|
||||||
```
|
|
||||||
|
|
||||||
导出镜像:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker save wechat-bot > wechat.tar
|
|
||||||
```
|
|
||||||
|
|
||||||
导入镜像:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker load < wechat.tar
|
|
||||||
```
|
|
||||||
|
|
||||||
运行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d -P --name xxx -v /src/data/dir:/Wechat_bot/test wechat-bot
|
|
||||||
```
|
|
||||||
|
|
||||||
删除镜像:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker rmi -f wechat-bot
|
|
||||||
```
|
|
||||||
|
|
||||||
查看log:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker log wechat-bot
|
|
||||||
```
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>WeChat Bot Server</title>
|
|
||||||
<style type="text/css">
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
div.doc{
|
|
||||||
margin: auto;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="container">
|
|
||||||
|
|
||||||
<div class="doc">
|
|
||||||
<pre>
|
|
||||||
#####################
|
|
||||||
# WeChat Bot Server #
|
|
||||||
#####################
|
|
||||||
|
|
||||||
|
|
||||||
## APIs
|
|
||||||
|
|
||||||
- /qrcode
|
|
||||||
------------------------
|
|
||||||
@brief login qrcode
|
|
||||||
@method get
|
|
||||||
@return raw image
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
- /runtime
|
|
||||||
-----------------------------
|
|
||||||
@brief basic info
|
|
||||||
@method get
|
|
||||||
@return application/json
|
|
||||||
{
|
|
||||||
'ret': 0,
|
|
||||||
'runtime': '',
|
|
||||||
'total_size': '',
|
|
||||||
'db_size': '',
|
|
||||||
'msg_count': '',
|
|
||||||
'image_count': '',
|
|
||||||
'voice_count': '',
|
|
||||||
'video_count': '',
|
|
||||||
}
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
- /group_list
|
|
||||||
------------------------
|
|
||||||
@brief list groups
|
|
||||||
@method get
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
- /group_member_list/<g_id>
|
|
||||||
-----------------------------
|
|
||||||
@brief list group member
|
|
||||||
@method get
|
|
||||||
@param g_id String
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
- /group_chat_log/<g_name>
|
|
||||||
-------------------------------
|
|
||||||
@brief list group chat log
|
|
||||||
@method get
|
|
||||||
@param g_name String
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
- /upload
|
|
||||||
-------------------------------
|
|
||||||
@brief upload a file
|
|
||||||
@method get/post
|
|
||||||
@return application/json
|
|
||||||
{
|
|
||||||
'ret': 0,
|
|
||||||
'msg': '',
|
|
||||||
}
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
- /send_msg/<to>/<msg>
|
|
||||||
-------------------------------------------
|
|
||||||
@brief send message to user or gourp
|
|
||||||
@method get
|
|
||||||
@param to: String, user id or group id
|
|
||||||
@param msg: String, words
|
|
||||||
@return application/json
|
|
||||||
{
|
|
||||||
'ret': 0,
|
|
||||||
}
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
- /send_img/<to>/<img>
|
|
||||||
-------------------------------------------
|
|
||||||
@brief send image to user or gourp
|
|
||||||
@method get
|
|
||||||
@param to: String, user id or group id
|
|
||||||
@param img: String, image file name
|
|
||||||
@return application/json
|
|
||||||
{
|
|
||||||
'ret': 0,
|
|
||||||
}
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
- /send_emot/<to>/<emot>
|
|
||||||
-------------------------------------------
|
|
||||||
@brief send emotion to user or gourp
|
|
||||||
@method get
|
|
||||||
@param to: String, user id or group id
|
|
||||||
@param emot: String, emotion file name
|
|
||||||
@return application/json
|
|
||||||
{
|
|
||||||
'ret': 0,
|
|
||||||
}
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
- /send_file/<to>/<file>
|
|
||||||
-------------------------------------------
|
|
||||||
@brief send emotion to user or gourp
|
|
||||||
@method get
|
|
||||||
@param to: String, user id or group id
|
|
||||||
@param file: String, file name
|
|
||||||
@return application/json
|
|
||||||
{
|
|
||||||
'ret': 0,
|
|
||||||
}
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
- /mass_send_msg
|
|
||||||
-------------------------------------------
|
|
||||||
@brief send text to mass users or gourps
|
|
||||||
@method post
|
|
||||||
@param application/json
|
|
||||||
{
|
|
||||||
'to_list': [
|
|
||||||
'group_id',
|
|
||||||
...
|
|
||||||
],
|
|
||||||
'msg': '',
|
|
||||||
}
|
|
||||||
@return application/json
|
|
||||||
{
|
|
||||||
'ret': 0,
|
|
||||||
'unsend_list': [],
|
|
||||||
}
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
- /mass_send_img
|
|
||||||
-------------------------------------------
|
|
||||||
@brief send image to mass users or gourps
|
|
||||||
@method post
|
|
||||||
@param application/json
|
|
||||||
{
|
|
||||||
'to_list': [
|
|
||||||
'group_id',
|
|
||||||
...
|
|
||||||
],
|
|
||||||
'msg': '',
|
|
||||||
}
|
|
||||||
@return application/json
|
|
||||||
{
|
|
||||||
'ret': 0,
|
|
||||||
'unsend_list': [],
|
|
||||||
}
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
- /mass_send_emot
|
|
||||||
-------------------------------------------
|
|
||||||
@brief send emoticon to mass users or gourps
|
|
||||||
@method post
|
|
||||||
@param application/json
|
|
||||||
{
|
|
||||||
'to_list': [
|
|
||||||
'group_id',
|
|
||||||
...
|
|
||||||
],
|
|
||||||
'msg': '',
|
|
||||||
}
|
|
||||||
@return application/json
|
|
||||||
{
|
|
||||||
'ret': 0,
|
|
||||||
'unsend_list': [],
|
|
||||||
}
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
- /mass_send_file
|
|
||||||
-------------------------------------------
|
|
||||||
@brief send file to mass users or gourps
|
|
||||||
@method post
|
|
||||||
@param application/json
|
|
||||||
{
|
|
||||||
'to_list': [
|
|
||||||
'group_id',
|
|
||||||
...
|
|
||||||
],
|
|
||||||
'msg': '',
|
|
||||||
}
|
|
||||||
@return application/json
|
|
||||||
{
|
|
||||||
'ret': 0,
|
|
||||||
'unsend_list': [],
|
|
||||||
}
|
|
||||||
-------------------------------------------
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<title>Upload new File</title>
|
|
||||||
<h1>Upload new File</h1>
|
|
||||||
<form action="upload" method=post enctype=multipart/form-data>
|
|
||||||
<p><input type=file name=file>
|
|
||||||
<input type=submit value=Upload>
|
|
||||||
</form>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
from wechat import WeChat
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
#===================================================
|
|
||||||
from config import Log
|
|
||||||
from config import Constant
|
|
||||||
#---------------------------------------------------
|
|
||||||
import qrcode
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import urllib
|
|
||||||
import urllib2
|
|
||||||
import cookielib
|
|
||||||
import cPickle as pickle
|
|
||||||
import traceback
|
|
||||||
import time
|
|
||||||
import hashlib
|
|
||||||
#===================================================
|
|
||||||
|
|
||||||
|
|
||||||
def _decode_data(data):
|
|
||||||
"""
|
|
||||||
@brief decode array or dict to utf-8
|
|
||||||
@param data array or dict
|
|
||||||
@return utf-8
|
|
||||||
"""
|
|
||||||
if isinstance(data, dict):
|
|
||||||
rv = {}
|
|
||||||
for key, value in data.iteritems():
|
|
||||||
if isinstance(key, unicode):
|
|
||||||
key = key.encode('utf-8')
|
|
||||||
rv[key] = _decode_data(value)
|
|
||||||
return rv
|
|
||||||
elif isinstance(data, list):
|
|
||||||
rv = []
|
|
||||||
for item in data:
|
|
||||||
item = _decode_data(item)
|
|
||||||
rv.append(item)
|
|
||||||
return rv
|
|
||||||
elif isinstance(data, unicode):
|
|
||||||
return data.encode('utf-8')
|
|
||||||
else:
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def str2qr_terminal(text):
|
|
||||||
"""
|
|
||||||
@brief convert string to qrcode matrix and outprint
|
|
||||||
@param text The string
|
|
||||||
"""
|
|
||||||
Log.debug(text)
|
|
||||||
qr = qrcode.QRCode()
|
|
||||||
qr.border = 1
|
|
||||||
qr.add_data(text)
|
|
||||||
mat = qr.get_matrix()
|
|
||||||
print_qr(mat)
|
|
||||||
|
|
||||||
|
|
||||||
def str2qr_image(text, image_path):
|
|
||||||
"""
|
|
||||||
@brief convert string to qrcode image & save
|
|
||||||
@param text The string
|
|
||||||
@param image_path Save image to the path
|
|
||||||
"""
|
|
||||||
qr = qrcode.QRCode()
|
|
||||||
qr.border = 1
|
|
||||||
qr.add_data(text)
|
|
||||||
qr.make(fit=True)
|
|
||||||
img = qr.make_image()
|
|
||||||
img.save(image_path)
|
|
||||||
|
|
||||||
|
|
||||||
def print_qr(mat):
|
|
||||||
for i in mat:
|
|
||||||
BLACK = Constant.QRCODE_BLACK
|
|
||||||
WHITE = Constant.QRCODE_WHITE
|
|
||||||
echo(''.join([BLACK if j else WHITE for j in i])+'\n')
|
|
||||||
|
|
||||||
|
|
||||||
def echo(str):
|
|
||||||
Log.info(str[:-1])
|
|
||||||
sys.stdout.write(str)
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def run(str, func, *args):
|
|
||||||
t = time.time()
|
|
||||||
echo(str)
|
|
||||||
r = False
|
|
||||||
try:
|
|
||||||
r = func(*args)
|
|
||||||
except:
|
|
||||||
Log.error(traceback.format_exc())
|
|
||||||
if r:
|
|
||||||
totalTime = int(time.time() - t)
|
|
||||||
echo(Constant.RUN_RESULT_SUCCESS % totalTime)
|
|
||||||
else:
|
|
||||||
echo(Constant.RUN_RESULT_FAIL)
|
|
||||||
exit()
|
|
||||||
|
|
||||||
|
|
||||||
def get(url, api=None):
|
|
||||||
"""
|
|
||||||
@brief http get request
|
|
||||||
@param url String
|
|
||||||
@param api wechat api
|
|
||||||
@return http response
|
|
||||||
"""
|
|
||||||
Log.debug('GET -> ' + url)
|
|
||||||
request = urllib2.Request(url=url)
|
|
||||||
request.add_header(*Constant.HTTP_HEADER_CONNECTION)
|
|
||||||
request.add_header(*Constant.HTTP_HEADER_REFERER)
|
|
||||||
if api in ['webwxgetvoice', 'webwxgetvideo']:
|
|
||||||
request.add_header(*Constant.HTTP_HEADER_RANGE)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
response = urllib2.urlopen(request, timeout=30)
|
|
||||||
data = response.read()
|
|
||||||
response.close()
|
|
||||||
if api == None:
|
|
||||||
Log.debug(data)
|
|
||||||
return data
|
|
||||||
except (KeyboardInterrupt, SystemExit):
|
|
||||||
raise
|
|
||||||
except:
|
|
||||||
Log.error(traceback.format_exc())
|
|
||||||
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
|
|
||||||
def post(url, params, jsonfmt=True):
|
|
||||||
"""
|
|
||||||
@brief http post request
|
|
||||||
@param url String
|
|
||||||
@param params Dict, post params
|
|
||||||
@param jsonfmt Bool, whether is json format
|
|
||||||
@return http response
|
|
||||||
"""
|
|
||||||
Log.debug('POST -> '+url)
|
|
||||||
Log.debug(params)
|
|
||||||
if jsonfmt:
|
|
||||||
request = urllib2.Request(url=url, data=json.dumps(params, ensure_ascii=False).encode('utf8'))
|
|
||||||
request.add_header(*Constant.HTTP_HEADER_CONTENTTYPE)
|
|
||||||
else:
|
|
||||||
request = urllib2.Request(url=url, data=urllib.urlencode(params))
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
response = urllib2.urlopen(request, timeout=30)
|
|
||||||
data = response.read()
|
|
||||||
response.close()
|
|
||||||
|
|
||||||
if jsonfmt:
|
|
||||||
Log.debug(data)
|
|
||||||
return json.loads(data, object_hook=_decode_data)
|
|
||||||
return data
|
|
||||||
except (KeyboardInterrupt, SystemExit):
|
|
||||||
raise
|
|
||||||
except:
|
|
||||||
Log.error(traceback.format_exc())
|
|
||||||
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
|
|
||||||
def set_cookie(cookie_file):
|
|
||||||
"""
|
|
||||||
@brief Load cookie from file
|
|
||||||
@param cookie_file
|
|
||||||
@param user_agent
|
|
||||||
@return cookie, LWPCookieJar
|
|
||||||
"""
|
|
||||||
cookie = cookielib.LWPCookieJar(cookie_file)
|
|
||||||
try:
|
|
||||||
cookie.load(ignore_discard=True)
|
|
||||||
except:
|
|
||||||
Log.error(traceback.format_exc())
|
|
||||||
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
|
|
||||||
opener.addheaders = Constant.HTTP_HEADER_USERAGENT
|
|
||||||
urllib2.install_opener(opener)
|
|
||||||
return cookie
|
|
||||||
|
|
||||||
|
|
||||||
def generate_file_name(filename):
|
|
||||||
"""
|
|
||||||
@brief generate file name
|
|
||||||
@return new file name
|
|
||||||
"""
|
|
||||||
i = filename.rfind('.')
|
|
||||||
ext = filename[i:]
|
|
||||||
tmp = filename + str(int(time.time()))
|
|
||||||
hash_md5 = hashlib.md5(tmp)
|
|
||||||
return hash_md5.hexdigest() + ext
|
|
||||||
|
|
||||||
|
|
||||||
def save_file(filename, data, dirName):
|
|
||||||
"""
|
|
||||||
@brief Saves raw data to file.
|
|
||||||
@param filename String
|
|
||||||
@param data Binary data
|
|
||||||
@param dirName String
|
|
||||||
@return file path
|
|
||||||
"""
|
|
||||||
Log.debug('save file: ' + filename)
|
|
||||||
fn = filename
|
|
||||||
if not os.path.exists(dirName):
|
|
||||||
os.makedirs(dirName)
|
|
||||||
fn = os.path.join(dirName, filename)
|
|
||||||
with open(fn, 'wb') as f:
|
|
||||||
f.write(data)
|
|
||||||
return fn
|
|
||||||
|
|
||||||
|
|
||||||
def save_json(filename, data, dirName, mode='w+'):
|
|
||||||
"""
|
|
||||||
@brief Saves dict to json file.
|
|
||||||
@param filename String
|
|
||||||
@param data Dict
|
|
||||||
@param dirName String
|
|
||||||
@return file path
|
|
||||||
"""
|
|
||||||
Log.debug('save json: ' + filename)
|
|
||||||
fn = filename
|
|
||||||
if not os.path.exists(dirName):
|
|
||||||
os.makedirs(dirName)
|
|
||||||
fn = os.path.join(dirName, filename)
|
|
||||||
with open(fn, mode) as f:
|
|
||||||
f.write(json.dumps(data, indent=4)+'\n')
|
|
||||||
return fn
|
|
||||||
|
|
||||||
|
|
||||||
def load_json(filepath):
|
|
||||||
Log.debug('load json: ' + filepath)
|
|
||||||
with open(filepath, 'r') as f:
|
|
||||||
return _decode_data(json.loads(f.read()))
|
|
||||||
|
|
||||||
|
|
||||||
def pickle_save(data, file):
|
|
||||||
"""
|
|
||||||
@brief Use pickle to save python object into file
|
|
||||||
@param data The pyhton data
|
|
||||||
@param file The file
|
|
||||||
"""
|
|
||||||
basedir = os.path.dirname(file)
|
|
||||||
if not os.path.exists(basedir):
|
|
||||||
os.makedirs(basedir)
|
|
||||||
with open(file, 'wb') as f:
|
|
||||||
pickle.dump(data, f, pickle.HIGHEST_PROTOCOL)
|
|
||||||
|
|
||||||
|
|
||||||
def pickle_load(file):
|
|
||||||
"""
|
|
||||||
@brief Use pickle to load python object from file
|
|
||||||
@param file The file
|
|
||||||
@return python data
|
|
||||||
"""
|
|
||||||
if os.path.isfile(file):
|
|
||||||
with open(file, 'rb') as f:
|
|
||||||
return pickle.load(f)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def search_content(key, content, fmat='attr'):
|
|
||||||
"""
|
|
||||||
@brief Search content from xml or html format
|
|
||||||
@param key String
|
|
||||||
@param content String
|
|
||||||
@param fmat attr
|
|
||||||
xml
|
|
||||||
@return String
|
|
||||||
"""
|
|
||||||
if fmat == 'attr':
|
|
||||||
pm = re.search(key + '\s?=\s?"([^"<]+)"', content)
|
|
||||||
if pm:
|
|
||||||
return pm.group(1)
|
|
||||||
elif fmat == 'xml':
|
|
||||||
pm = re.search('<{0}>([^<]+)</{0}>'.format(key), content)
|
|
||||||
if not pm:
|
|
||||||
pm = re.search(
|
|
||||||
'<{0}><\!\[CDATA\[(.*?)\]\]></{0}>'.format(key), content)
|
|
||||||
if pm:
|
|
||||||
return pm.group(1)
|
|
||||||
return 'unknown'
|
|
||||||
|
|
||||||
|
|
||||||
def is_str(s):
|
|
||||||
"""
|
|
||||||
@brief Determines if string.
|
|
||||||
@param s String
|
|
||||||
@return True if string, False otherwise.
|
|
||||||
"""
|
|
||||||
return isinstance(s, basestring)
|
|
||||||
|
|
||||||
|
|
||||||
def trans_coding(data):
|
|
||||||
"""
|
|
||||||
@brief Transform string to unicode
|
|
||||||
@param data String
|
|
||||||
@return unicode
|
|
||||||
"""
|
|
||||||
if not data:
|
|
||||||
return data
|
|
||||||
result = None
|
|
||||||
if type(data) == unicode:
|
|
||||||
result = data
|
|
||||||
elif type(data) == str:
|
|
||||||
result = data.decode('utf-8')
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def trans_emoji(text):
|
|
||||||
"""
|
|
||||||
@brief Transform emoji html text to unicode
|
|
||||||
@param text String
|
|
||||||
@return emoji unicode
|
|
||||||
"""
|
|
||||||
def _emoji(matched):
|
|
||||||
hex = matched.group(1)
|
|
||||||
return ('\\U%08x' % int(hex, 16)).decode('unicode-escape').encode('utf-8')
|
|
||||||
|
|
||||||
replace_t = re.sub(Constant.REGEX_EMOJI, _emoji, text)
|
|
||||||
return replace_t
|
|
||||||
|
|
||||||
|
|
||||||
def auto_reload(mod):
|
|
||||||
"""
|
|
||||||
@brief reload modules
|
|
||||||
@param mod: the need reload modules
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
module = sys.modules[mod]
|
|
||||||
except:
|
|
||||||
Log.error(traceback.format_exc())
|
|
||||||
return False
|
|
||||||
|
|
||||||
filename = module.__file__
|
|
||||||
# .pyc 修改时间不会变
|
|
||||||
# 所以就用 .py 的修改时间
|
|
||||||
if filename.endswith(".pyc"):
|
|
||||||
filename = filename.replace(".pyc", ".py")
|
|
||||||
mod_time = os.path.getmtime(filename)
|
|
||||||
if not "loadtime" in module.__dict__:
|
|
||||||
module.loadtime = 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
if mod_time > module.loadtime:
|
|
||||||
reload(module)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
except:
|
|
||||||
Log.error(traceback.format_exc())
|
|
||||||
return False
|
|
||||||
|
|
||||||
module.loadtime = mod_time
|
|
||||||
|
|
||||||
echo('[*] load \'%s\' successful.\n' % mod)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def split_array(arr, n):
|
|
||||||
for i in xrange(0, len(arr), n):
|
|
||||||
yield arr[i:i+n]
|
|
||||||
|
|
||||||
@@ -1,698 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
#===================================================
|
|
||||||
from utils import *
|
|
||||||
from wechat_apis import WXAPI
|
|
||||||
from config import ConfigManager
|
|
||||||
from config import Constant
|
|
||||||
from config import Log
|
|
||||||
#---------------------------------------------------
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
from collections import defaultdict
|
|
||||||
from datetime import timedelta
|
|
||||||
import traceback
|
|
||||||
import Queue
|
|
||||||
import threading
|
|
||||||
#===================================================
|
|
||||||
|
|
||||||
|
|
||||||
class WeChat(WXAPI):
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
description = \
|
|
||||||
"=========================\n" + \
|
|
||||||
"[#] Web WeChat\n" + \
|
|
||||||
"[#] UUID: " + self.uuid + "\n" + \
|
|
||||||
"[#] Uin: " + str(self.uin) + "\n" + \
|
|
||||||
"[#] Sid: " + self.sid + "\n" + \
|
|
||||||
"[#] Skey: " + self.skey + "\n" + \
|
|
||||||
"[#] DeviceId: " + self.device_id + "\n" + \
|
|
||||||
"[#] PassTicket: " + self.pass_ticket + "\n" + \
|
|
||||||
"[#] Run Time: " + self.get_run_time() + '\n' + \
|
|
||||||
"========================="
|
|
||||||
return description
|
|
||||||
|
|
||||||
def __init__(self, host='wx.qq.com'):
|
|
||||||
super(WeChat, self).__init__(host)
|
|
||||||
|
|
||||||
self.db = None
|
|
||||||
self.save_data_folder = '' # 保存图片,语音,小视频的文件夹
|
|
||||||
self.last_login = 0 # 上次退出的时间
|
|
||||||
self.time_out = 5 # 同步时间间隔(单位:秒)
|
|
||||||
self.msg_handler = None
|
|
||||||
self.start_time = time.time()
|
|
||||||
self.bot = None
|
|
||||||
|
|
||||||
cm = ConfigManager()
|
|
||||||
self.save_data_folders = cm.get_wechat_media_dir()
|
|
||||||
self.cookie_file = cm.get_cookie()
|
|
||||||
self.pickle_file = cm.get_pickle_files()
|
|
||||||
self.log_mode = cm.get('setting', 'log_mode') == 'True'
|
|
||||||
self.exit_code = 0
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
echo(Constant.LOG_MSG_START)
|
|
||||||
run(Constant.LOG_MSG_RECOVER, self.recover)
|
|
||||||
|
|
||||||
timeOut = time.time() - self.last_login
|
|
||||||
echo(Constant.LOG_MSG_TRY_INIT)
|
|
||||||
if self.webwxinit():
|
|
||||||
echo(Constant.LOG_MSG_SUCCESS)
|
|
||||||
run(Constant.LOG_MSG_RECOVER_CONTACT, self.recover_contacts)
|
|
||||||
else:
|
|
||||||
echo(Constant.LOG_MSG_FAIL)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
# first try to login by uin without qrcode
|
|
||||||
echo(Constant.LOG_MSG_ASSOCIATION_LOGIN)
|
|
||||||
if self.association_login():
|
|
||||||
echo(Constant.LOG_MSG_SUCCESS)
|
|
||||||
else:
|
|
||||||
echo(Constant.LOG_MSG_FAIL)
|
|
||||||
# scan qrcode to login
|
|
||||||
run(Constant.LOG_MSG_GET_UUID, self.getuuid)
|
|
||||||
echo(Constant.LOG_MSG_GET_QRCODE)
|
|
||||||
self.genqrcode()
|
|
||||||
echo(Constant.LOG_MSG_SCAN_QRCODE)
|
|
||||||
|
|
||||||
if not self.waitforlogin():
|
|
||||||
continue
|
|
||||||
echo(Constant.LOG_MSG_CONFIRM_LOGIN)
|
|
||||||
if not self.waitforlogin(0):
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
|
|
||||||
run(Constant.LOG_MSG_LOGIN, self.login)
|
|
||||||
run(Constant.LOG_MSG_INIT, self.webwxinit)
|
|
||||||
run(Constant.LOG_MSG_STATUS_NOTIFY, self.webwxstatusnotify)
|
|
||||||
run(Constant.LOG_MSG_GET_CONTACT, self.webwxgetcontact)
|
|
||||||
echo(Constant.LOG_MSG_CONTACT_COUNT % (
|
|
||||||
self.MemberCount, len(self.MemberList)
|
|
||||||
))
|
|
||||||
echo(Constant.LOG_MSG_OTHER_CONTACT_COUNT % (
|
|
||||||
len(self.GroupList), len(self.ContactList),
|
|
||||||
len(self.SpecialUsersList), len(self.PublicUsersList)
|
|
||||||
))
|
|
||||||
run(Constant.LOG_MSG_GET_GROUP_MEMBER, self.fetch_group_contacts)
|
|
||||||
|
|
||||||
run(Constant.LOG_MSG_SNAPSHOT, self.snapshot)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
[retcode, selector] = self.synccheck()
|
|
||||||
Log.debug('retcode: %s, selector: %s' % (retcode, selector))
|
|
||||||
self.exit_code = int(retcode)
|
|
||||||
|
|
||||||
if retcode == '1100':
|
|
||||||
echo(Constant.LOG_MSG_LOGOUT)
|
|
||||||
break
|
|
||||||
if retcode == '1101':
|
|
||||||
echo(Constant.LOG_MSG_LOGIN_OTHERWHERE)
|
|
||||||
break
|
|
||||||
if retcode == '1102':
|
|
||||||
echo(Constant.LOG_MSG_QUIT_ON_PHONE)
|
|
||||||
break
|
|
||||||
elif retcode == '0':
|
|
||||||
if selector == '2':
|
|
||||||
r = self.webwxsync()
|
|
||||||
if r is not None:
|
|
||||||
try:
|
|
||||||
self.handle_msg(r)
|
|
||||||
except:
|
|
||||||
Log.error(traceback.format_exc())
|
|
||||||
elif selector == '7':
|
|
||||||
r = self.webwxsync()
|
|
||||||
elif selector == '0':
|
|
||||||
time.sleep(self.time_out)
|
|
||||||
elif selector == '4':
|
|
||||||
# 保存群聊到通讯录
|
|
||||||
# 修改群名称
|
|
||||||
# 新增或删除联系人
|
|
||||||
# 群聊成员数目变化
|
|
||||||
r = self.webwxsync()
|
|
||||||
if r is not None:
|
|
||||||
try:
|
|
||||||
self.handle_mod(r)
|
|
||||||
except:
|
|
||||||
Log.error(traceback.format_exc())
|
|
||||||
elif selector == '3' or selector == '6':
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
r = self.webwxsync()
|
|
||||||
Log.debug('webwxsync: %s\n' % json.dumps(r))
|
|
||||||
|
|
||||||
# 执行定时任务
|
|
||||||
if self.msg_handler:
|
|
||||||
self.msg_handler.check_schedule_task()
|
|
||||||
|
|
||||||
# if self.bot:
|
|
||||||
# r = self.bot.time_schedule()
|
|
||||||
# if r:
|
|
||||||
# for g in self.GroupList:
|
|
||||||
# echo('[*] 推送 -> %s: %s' % (g['NickName'], r))
|
|
||||||
# g_id = g['UserName']
|
|
||||||
# self.webwxsendmsg(r, g_id)
|
|
||||||
|
|
||||||
def get_run_time(self):
|
|
||||||
"""
|
|
||||||
@brief get how long this run
|
|
||||||
@return String
|
|
||||||
"""
|
|
||||||
totalTime = int(time.time() - self.start_time)
|
|
||||||
t = timedelta(seconds=totalTime)
|
|
||||||
return '%s Day %s' % (t.days, t)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""
|
|
||||||
@brief Save some data and use shell to kill this process
|
|
||||||
"""
|
|
||||||
run(Constant.LOG_MSG_SNAPSHOT, self.snapshot)
|
|
||||||
echo(Constant.LOG_MSG_RUNTIME % self.get_run_time())
|
|
||||||
# close database connect
|
|
||||||
self.db.close()
|
|
||||||
|
|
||||||
def fetch_group_contacts(self):
|
|
||||||
"""
|
|
||||||
@brief Fetches all groups contacts.
|
|
||||||
@return Bool: whether operation succeed.
|
|
||||||
@note This function must be finished in 180s
|
|
||||||
"""
|
|
||||||
Log.debug('fetch_group_contacts')
|
|
||||||
# clean database
|
|
||||||
if self.msg_handler:
|
|
||||||
self.msg_handler.clean_db()
|
|
||||||
|
|
||||||
# sqlite
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# group max_thread_num max_fetch_group_num time(s)
|
|
||||||
# 197 10 10 108
|
|
||||||
# 197 10 15 95
|
|
||||||
# 197 20 10 103
|
|
||||||
# 197 10 20 55
|
|
||||||
# 197 5 30 39
|
|
||||||
# 197 4 50 35
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# mysql
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# group max_thread_num max_fetch_group_num time(s)
|
|
||||||
# 197 4 50 20
|
|
||||||
# ----------------------------------------------------
|
|
||||||
|
|
||||||
max_thread_num = 4
|
|
||||||
max_fetch_group_num = 50
|
|
||||||
group_list_queue = Queue.Queue()
|
|
||||||
|
|
||||||
class GroupListThread(threading.Thread):
|
|
||||||
|
|
||||||
def __init__(self, group_list_queue, wechat):
|
|
||||||
threading.Thread.__init__(self)
|
|
||||||
self.group_list_queue = group_list_queue
|
|
||||||
self.wechat = wechat
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
while not self.group_list_queue.empty():
|
|
||||||
g_list = self.group_list_queue.get()
|
|
||||||
gid_list = []
|
|
||||||
g_dict = {}
|
|
||||||
for g in g_list:
|
|
||||||
gid = g['UserName']
|
|
||||||
gid_list.append(gid)
|
|
||||||
g_dict[gid] = g
|
|
||||||
|
|
||||||
group_member_list = self.wechat.webwxbatchgetcontact(gid_list)
|
|
||||||
|
|
||||||
for member_list in group_member_list:
|
|
||||||
gid = member_list['UserName']
|
|
||||||
g = g_dict[gid]
|
|
||||||
g['MemberCount'] = member_list['MemberCount']
|
|
||||||
g['OwnerUin'] = member_list['OwnerUin']
|
|
||||||
self.wechat.GroupMemeberList[gid] = member_list['MemberList']
|
|
||||||
|
|
||||||
# 如果使用 Mysql 则可以在多线程里操作数据库
|
|
||||||
# 否则请注释下列代码在主线程里更新群列表
|
|
||||||
# -----------------------------------
|
|
||||||
# 处理群成员
|
|
||||||
# if self.wechat.msg_handler:
|
|
||||||
# self.wechat.msg_handler.handle_group_member_list(gid, member_list['MemberList'])
|
|
||||||
# -----------------------------------
|
|
||||||
|
|
||||||
self.group_list_queue.task_done()
|
|
||||||
|
|
||||||
for g_list in split_array(self.GroupList, max_fetch_group_num):
|
|
||||||
group_list_queue.put(g_list)
|
|
||||||
|
|
||||||
for i in range(max_thread_num):
|
|
||||||
t = GroupListThread(group_list_queue, self)
|
|
||||||
t.setDaemon(True)
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
group_list_queue.join()
|
|
||||||
|
|
||||||
if self.msg_handler:
|
|
||||||
# 处理群
|
|
||||||
if self.GroupList:
|
|
||||||
self.msg_handler.handle_group_list(self.GroupList)
|
|
||||||
|
|
||||||
# 这个是用 sqlite 来存储群列表,sqlite 对多线程的支持不太好
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# 处理群成员
|
|
||||||
for (gid, member_list) in self.GroupMemeberList.items():
|
|
||||||
self.msg_handler.handle_group_member_list(gid, member_list)
|
|
||||||
# ----------------------------------------------------
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def snapshot(self):
|
|
||||||
"""
|
|
||||||
@brief Save basic infos for next login.
|
|
||||||
@return Bool: whether operation succeed.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
conf = {
|
|
||||||
'uuid': self.uuid,
|
|
||||||
'redirect_uri': self.redirect_uri,
|
|
||||||
'uin': self.uin,
|
|
||||||
'sid': self.sid,
|
|
||||||
'skey': self.skey,
|
|
||||||
'pass_ticket': self.pass_ticket,
|
|
||||||
'synckey': self.synckey,
|
|
||||||
'device_id': self.device_id,
|
|
||||||
'last_login': time.time(),
|
|
||||||
}
|
|
||||||
cm = ConfigManager()
|
|
||||||
Log.debug('save wechat config')
|
|
||||||
cm.set_wechat_config(conf)
|
|
||||||
|
|
||||||
# save cookie
|
|
||||||
Log.debug('save cookie')
|
|
||||||
if self.cookie:
|
|
||||||
self.cookie.save(ignore_discard=True)
|
|
||||||
|
|
||||||
# save contacts
|
|
||||||
Log.debug('save contacts')
|
|
||||||
self.save_contacts()
|
|
||||||
except Exception, e:
|
|
||||||
Log.error(traceback.format_exc())
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def recover(self):
|
|
||||||
"""
|
|
||||||
@brief Recover from snapshot data.
|
|
||||||
@return Bool: whether operation succeed.
|
|
||||||
"""
|
|
||||||
cm = ConfigManager()
|
|
||||||
[self.uuid, self.redirect_uri, self.uin,
|
|
||||||
self.sid, self.skey, self.pass_ticket,
|
|
||||||
self.synckey, device_id, self.last_login] = cm.get_wechat_config()
|
|
||||||
|
|
||||||
if device_id:
|
|
||||||
self.device_id = device_id
|
|
||||||
|
|
||||||
self.base_request = {
|
|
||||||
'Uin': int(self.uin),
|
|
||||||
'Sid': self.sid,
|
|
||||||
'Skey': self.skey,
|
|
||||||
'DeviceID': self.device_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
# set cookie
|
|
||||||
Log.debug('set cookie')
|
|
||||||
self.cookie = set_cookie(self.cookie_file)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def save_contacts(self):
|
|
||||||
"""
|
|
||||||
@brief Save contacts.
|
|
||||||
"""
|
|
||||||
pickle_save(self.User, self.pickle_file['User'])
|
|
||||||
pickle_save(self.MemberList, self.pickle_file['MemberList'])
|
|
||||||
pickle_save(self.GroupList, self.pickle_file['GroupList'])
|
|
||||||
pickle_save(self.GroupMemeberList, self.pickle_file['GroupMemeberList'])
|
|
||||||
pickle_save(self.SpecialUsersList, self.pickle_file['SpecialUsersList'])
|
|
||||||
|
|
||||||
def recover_contacts(self):
|
|
||||||
"""
|
|
||||||
@brief recover contacts.
|
|
||||||
@return Bool: whether operation succeed.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.User = pickle_load(self.pickle_file['User'])
|
|
||||||
self.MemberList = pickle_load(self.pickle_file['MemberList'])
|
|
||||||
self.GroupList = pickle_load(self.pickle_file['GroupList'])
|
|
||||||
self.GroupMemeberList = pickle_load(self.pickle_file['GroupMemeberList'])
|
|
||||||
self.SpecialUsersList = pickle_load(self.pickle_file['SpecialUsersList'])
|
|
||||||
return True
|
|
||||||
except Exception, e:
|
|
||||||
Log.error(traceback.format_exc())
|
|
||||||
return False
|
|
||||||
|
|
||||||
def handle_mod(self, r):
|
|
||||||
# ModContactCount: 变更联系人或群聊成员数目
|
|
||||||
# ModContactList: 变更联系人或群聊列表,或群名称改变
|
|
||||||
Log.debug('handle modify')
|
|
||||||
self.handle_msg(r)
|
|
||||||
for m in r['ModContactList']:
|
|
||||||
if m['UserName'][:2] == '@@':
|
|
||||||
# group
|
|
||||||
in_list = False
|
|
||||||
g_id = m['UserName']
|
|
||||||
for g in self.GroupList:
|
|
||||||
# group member change
|
|
||||||
if g_id == g['UserName']:
|
|
||||||
g['MemberCount'] = m['MemberCount']
|
|
||||||
g['NickName'] = m['NickName']
|
|
||||||
self.GroupMemeberList[g_id] = m['MemberList']
|
|
||||||
in_list = True
|
|
||||||
if self.msg_handler:
|
|
||||||
self.msg_handler.handle_group_member_change(g_id, m['MemberList'])
|
|
||||||
break
|
|
||||||
if not in_list:
|
|
||||||
# a new group
|
|
||||||
self.GroupList.append(m)
|
|
||||||
self.GroupMemeberList[g_id] = m['MemberList']
|
|
||||||
if self.msg_handler:
|
|
||||||
self.msg_handler.handle_group_list_change(m)
|
|
||||||
self.msg_handler.handle_group_member_change(g_id, m['MemberList'])
|
|
||||||
|
|
||||||
elif m['UserName'][0] == '@':
|
|
||||||
# user
|
|
||||||
in_list = False
|
|
||||||
for u in self.MemberList:
|
|
||||||
u_id = m['UserName']
|
|
||||||
if u_id == u['UserName']:
|
|
||||||
u = m
|
|
||||||
in_list = True
|
|
||||||
break
|
|
||||||
# if don't have then add it
|
|
||||||
if not in_list:
|
|
||||||
self.MemberList.append(m)
|
|
||||||
|
|
||||||
def handle_msg(self, r):
|
|
||||||
"""
|
|
||||||
@brief Recover from snapshot data.
|
|
||||||
@param r Dict: message json
|
|
||||||
"""
|
|
||||||
Log.debug('handle message')
|
|
||||||
if self.msg_handler:
|
|
||||||
self.msg_handler.handle_wxsync(r)
|
|
||||||
|
|
||||||
n = len(r['AddMsgList'])
|
|
||||||
if n == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.log_mode:
|
|
||||||
echo(Constant.LOG_MSG_NEW_MSG % n)
|
|
||||||
|
|
||||||
for msg in r['AddMsgList']:
|
|
||||||
|
|
||||||
msgType = msg['MsgType']
|
|
||||||
msgId = msg['MsgId']
|
|
||||||
content = msg['Content'].replace('<', '<').replace('>', '>')
|
|
||||||
raw_msg = None
|
|
||||||
|
|
||||||
if msgType == self.wx_conf['MSGTYPE_TEXT']:
|
|
||||||
# 地理位置消息
|
|
||||||
if content.find('pictype=location') != -1:
|
|
||||||
location = content.split('<br/>')[1][:-1]
|
|
||||||
raw_msg = {
|
|
||||||
'raw_msg': msg,
|
|
||||||
'location': location,
|
|
||||||
'log': Constant.LOG_MSG_LOCATION % location
|
|
||||||
}
|
|
||||||
# 普通文本消息
|
|
||||||
else:
|
|
||||||
text = content.split(':<br/>')[-1]
|
|
||||||
raw_msg = {
|
|
||||||
'raw_msg': msg,
|
|
||||||
'text': text,
|
|
||||||
'log': text.replace('<br/>', '\n')
|
|
||||||
}
|
|
||||||
elif msgType == self.wx_conf['MSGTYPE_IMAGE']:
|
|
||||||
data = self.webwxgetmsgimg(msgId)
|
|
||||||
fn = 'img_' + msgId + '.jpg'
|
|
||||||
dir = self.save_data_folders['webwxgetmsgimg']
|
|
||||||
path = save_file(fn, data, dir)
|
|
||||||
raw_msg = {'raw_msg': msg,
|
|
||||||
'image': path,
|
|
||||||
'log': Constant.LOG_MSG_PICTURE % path}
|
|
||||||
elif msgType == self.wx_conf['MSGTYPE_VOICE']:
|
|
||||||
data = self.webwxgetvoice(msgId)
|
|
||||||
fn = 'voice_' + msgId + '.mp3'
|
|
||||||
dir = self.save_data_folders['webwxgetvoice']
|
|
||||||
path = save_file(fn, data, dir)
|
|
||||||
raw_msg = {'raw_msg': msg,
|
|
||||||
'voice': path,
|
|
||||||
'log': Constant.LOG_MSG_VOICE % path}
|
|
||||||
elif msgType == self.wx_conf['MSGTYPE_SHARECARD']:
|
|
||||||
info = msg['RecommendInfo']
|
|
||||||
card = Constant.LOG_MSG_NAME_CARD % (
|
|
||||||
info['NickName'],
|
|
||||||
info['Alias'],
|
|
||||||
info['Province'], info['City'],
|
|
||||||
Constant.LOG_MSG_SEX_OPTION[info['Sex']]
|
|
||||||
)
|
|
||||||
namecard = '%s %s %s %s %s' % (
|
|
||||||
info['NickName'], info['Alias'], info['Province'],
|
|
||||||
info['City'], Constant.LOG_MSG_SEX_OPTION[info['Sex']]
|
|
||||||
)
|
|
||||||
raw_msg = {
|
|
||||||
'raw_msg': msg,
|
|
||||||
'namecard': namecard,
|
|
||||||
'log': card
|
|
||||||
}
|
|
||||||
elif msgType == self.wx_conf['MSGTYPE_EMOTICON']:
|
|
||||||
url = search_content('cdnurl', content)
|
|
||||||
raw_msg = {'raw_msg': msg,
|
|
||||||
'emoticon': url,
|
|
||||||
'log': Constant.LOG_MSG_EMOTION % url}
|
|
||||||
elif msgType == self.wx_conf['MSGTYPE_APP']:
|
|
||||||
card = ''
|
|
||||||
# 链接, 音乐, 微博
|
|
||||||
if msg['AppMsgType'] in [
|
|
||||||
self.wx_conf['APPMSGTYPE_AUDIO'],
|
|
||||||
self.wx_conf['APPMSGTYPE_URL'],
|
|
||||||
self.wx_conf['APPMSGTYPE_OPEN']
|
|
||||||
]:
|
|
||||||
card = Constant.LOG_MSG_APP_LINK % (
|
|
||||||
Constant.LOG_MSG_APP_LINK_TYPE[msg['AppMsgType']],
|
|
||||||
msg['FileName'],
|
|
||||||
search_content('des', content, 'xml'),
|
|
||||||
msg['Url'],
|
|
||||||
search_content('appname', content, 'xml')
|
|
||||||
)
|
|
||||||
raw_msg = {
|
|
||||||
'raw_msg': msg,
|
|
||||||
'link': msg['Url'],
|
|
||||||
'log': card
|
|
||||||
}
|
|
||||||
# 图片
|
|
||||||
elif msg['AppMsgType'] == self.wx_conf['APPMSGTYPE_IMG']:
|
|
||||||
data = self.webwxgetmsgimg(msgId)
|
|
||||||
fn = 'img_' + msgId + '.jpg'
|
|
||||||
dir = self.save_data_folders['webwxgetmsgimg']
|
|
||||||
path = save_file(fn, data, dir)
|
|
||||||
card = Constant.LOG_MSG_APP_IMG % (
|
|
||||||
path,
|
|
||||||
search_content('appname', content, 'xml')
|
|
||||||
)
|
|
||||||
raw_msg = {
|
|
||||||
'raw_msg': msg,
|
|
||||||
'image': path,
|
|
||||||
'log': card
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
raw_msg = {
|
|
||||||
'raw_msg': msg,
|
|
||||||
'log': Constant.LOG_MSG_UNKNOWN_MSG % (msgType, content)
|
|
||||||
}
|
|
||||||
elif msgType == self.wx_conf['MSGTYPE_STATUSNOTIFY']:
|
|
||||||
Log.info(Constant.LOG_MSG_NOTIFY_PHONE)
|
|
||||||
elif msgType == self.wx_conf['MSGTYPE_MICROVIDEO']:
|
|
||||||
data = self.webwxgetvideo(msgId)
|
|
||||||
fn = 'video_' + msgId + '.mp4'
|
|
||||||
dir = self.save_data_folders['webwxgetvideo']
|
|
||||||
path = save_file(fn, data, dir)
|
|
||||||
raw_msg = {'raw_msg': msg,
|
|
||||||
'video': path,
|
|
||||||
'log': Constant.LOG_MSG_VIDEO % path}
|
|
||||||
elif msgType == self.wx_conf['MSGTYPE_RECALLED']:
|
|
||||||
recall_id = search_content('msgid', content, 'xml')
|
|
||||||
text = Constant.LOG_MSG_RECALL
|
|
||||||
raw_msg = {
|
|
||||||
'raw_msg': msg,
|
|
||||||
'text': text,
|
|
||||||
'recall_msg_id': recall_id,
|
|
||||||
'log': text
|
|
||||||
}
|
|
||||||
elif msgType == self.wx_conf['MSGTYPE_SYS']:
|
|
||||||
raw_msg = {
|
|
||||||
'raw_msg': msg,
|
|
||||||
'sys_notif': content,
|
|
||||||
'log': content
|
|
||||||
}
|
|
||||||
elif msgType == self.wx_conf['MSGTYPE_VERIFYMSG']:
|
|
||||||
name = search_content('fromnickname', content)
|
|
||||||
raw_msg = {
|
|
||||||
'raw_msg': msg,
|
|
||||||
'log': Constant.LOG_MSG_ADD_FRIEND % name
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
raw_msg = {
|
|
||||||
'raw_msg': msg,
|
|
||||||
'log': Constant.LOG_MSG_UNKNOWN_MSG % (msgType, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
isGroupMsg = '@@' in msg['FromUserName']+msg['ToUserName']
|
|
||||||
if self.msg_handler and raw_msg:
|
|
||||||
if isGroupMsg:
|
|
||||||
# handle group messages
|
|
||||||
g_msg = self.make_group_msg(raw_msg)
|
|
||||||
self.msg_handler.handle_group_msg(g_msg)
|
|
||||||
else:
|
|
||||||
# handle personal messages
|
|
||||||
self.msg_handler.handle_user_msg(raw_msg)
|
|
||||||
|
|
||||||
if self.log_mode:
|
|
||||||
self.show_msg(raw_msg)
|
|
||||||
|
|
||||||
def make_group_msg(self, msg):
|
|
||||||
"""
|
|
||||||
@brief Package the group message for storage.
|
|
||||||
@param msg Dict: raw msg
|
|
||||||
@return raw_msg Dict: packged msg
|
|
||||||
"""
|
|
||||||
Log.debug('make group message')
|
|
||||||
raw_msg = {
|
|
||||||
'raw_msg': msg['raw_msg'],
|
|
||||||
'msg_id': msg['raw_msg']['MsgId'],
|
|
||||||
'group_owner_uin': '',
|
|
||||||
'group_name': '',
|
|
||||||
'group_count': '',
|
|
||||||
'from_user_name': msg['raw_msg']['FromUserName'],
|
|
||||||
'to_user_name': msg['raw_msg']['ToUserName'],
|
|
||||||
'user_attrstatus': '',
|
|
||||||
'user_display_name': '',
|
|
||||||
'user_nickname': '',
|
|
||||||
'msg_type': msg['raw_msg']['MsgType'],
|
|
||||||
'text': '',
|
|
||||||
'link': '',
|
|
||||||
'image': '',
|
|
||||||
'video': '',
|
|
||||||
'voice': '',
|
|
||||||
'emoticon': '',
|
|
||||||
'namecard': '',
|
|
||||||
'location': '',
|
|
||||||
'recall_msg_id': '',
|
|
||||||
'sys_notif': '',
|
|
||||||
'time': '',
|
|
||||||
'timestamp': '',
|
|
||||||
'log': '',
|
|
||||||
}
|
|
||||||
content = msg['raw_msg']['Content'].replace(
|
|
||||||
'<', '<').replace('>', '>')
|
|
||||||
|
|
||||||
group = None
|
|
||||||
src = None
|
|
||||||
|
|
||||||
if msg['raw_msg']['FromUserName'][:2] == '@@':
|
|
||||||
# 接收到来自群的消息
|
|
||||||
g_id = msg['raw_msg']['FromUserName']
|
|
||||||
group = self.get_group_by_id(g_id)
|
|
||||||
|
|
||||||
if re.search(":<br/>", content, re.IGNORECASE):
|
|
||||||
u_id = content.split(':<br/>')[0]
|
|
||||||
src = self.get_group_user_by_id(u_id, g_id)
|
|
||||||
|
|
||||||
elif msg['raw_msg']['ToUserName'][:2] == '@@':
|
|
||||||
# 自己发给群的消息
|
|
||||||
g_id = msg['raw_msg']['ToUserName']
|
|
||||||
u_id = msg['raw_msg']['FromUserName']
|
|
||||||
src = self.get_group_user_by_id(u_id, g_id)
|
|
||||||
group = self.get_group_by_id(g_id)
|
|
||||||
|
|
||||||
if src:
|
|
||||||
raw_msg['user_attrstatus'] = src['AttrStatus']
|
|
||||||
raw_msg['user_display_name'] = src['DisplayName']
|
|
||||||
raw_msg['user_nickname'] = src['NickName']
|
|
||||||
if group:
|
|
||||||
raw_msg['group_count'] = group['MemberCount']
|
|
||||||
raw_msg['group_owner_uin'] = group['OwnerUin']
|
|
||||||
raw_msg['group_name'] = group['ShowName']
|
|
||||||
|
|
||||||
raw_msg['timestamp'] = msg['raw_msg']['CreateTime']
|
|
||||||
t = time.localtime(float(raw_msg['timestamp']))
|
|
||||||
raw_msg['time'] = time.strftime("%Y-%m-%d %T", t)
|
|
||||||
|
|
||||||
for key in [
|
|
||||||
'text', 'link', 'image', 'video', 'voice',
|
|
||||||
'emoticon', 'namecard', 'location', 'log',
|
|
||||||
'recall_msg_id', 'sys_notif'
|
|
||||||
]:
|
|
||||||
if key in msg:
|
|
||||||
raw_msg[key] = msg[key]
|
|
||||||
|
|
||||||
return raw_msg
|
|
||||||
|
|
||||||
def show_msg(self, message):
|
|
||||||
"""
|
|
||||||
@brief Log the message to stdout
|
|
||||||
@param message Dict
|
|
||||||
"""
|
|
||||||
msg = message
|
|
||||||
src = None
|
|
||||||
dst = None
|
|
||||||
group = None
|
|
||||||
|
|
||||||
if msg and msg['raw_msg']:
|
|
||||||
|
|
||||||
content = msg['raw_msg']['Content']
|
|
||||||
content = content.replace('<', '<').replace('>', '>')
|
|
||||||
msg_id = msg['raw_msg']['MsgId']
|
|
||||||
|
|
||||||
if msg['raw_msg']['FromUserName'][:2] == '@@':
|
|
||||||
# 接收到来自群的消息
|
|
||||||
g_id = msg['raw_msg']['FromUserName']
|
|
||||||
group = self.get_group_by_id(g_id)
|
|
||||||
|
|
||||||
if re.search(":<br/>", content, re.IGNORECASE):
|
|
||||||
u_id = content.split(':<br/>')[0]
|
|
||||||
src = self.get_group_user_by_id(u_id, g_id)
|
|
||||||
dst = {'ShowName': 'GROUP'}
|
|
||||||
else:
|
|
||||||
u_id = msg['raw_msg']['ToUserName']
|
|
||||||
src = {'ShowName': 'SYSTEM'}
|
|
||||||
dst = self.get_group_user_by_id(u_id, g_id)
|
|
||||||
elif msg['raw_msg']['ToUserName'][:2] == '@@':
|
|
||||||
# 自己发给群的消息
|
|
||||||
g_id = msg['raw_msg']['ToUserName']
|
|
||||||
u_id = msg['raw_msg']['FromUserName']
|
|
||||||
group = self.get_group_by_id(g_id)
|
|
||||||
src = self.get_group_user_by_id(u_id, g_id)
|
|
||||||
dst = {'ShowName': 'GROUP'}
|
|
||||||
else:
|
|
||||||
# 非群聊消息
|
|
||||||
src = self.get_user_by_id(msg['raw_msg']['FromUserName'])
|
|
||||||
dst = self.get_user_by_id(msg['raw_msg']['ToUserName'])
|
|
||||||
|
|
||||||
if group:
|
|
||||||
echo('%s |%s| %s -> %s: %s\n' % (
|
|
||||||
msg_id,
|
|
||||||
trans_emoji(group['ShowName']),
|
|
||||||
trans_emoji(src['ShowName']),
|
|
||||||
dst['ShowName'],
|
|
||||||
trans_emoji(msg['log'])
|
|
||||||
))
|
|
||||||
else:
|
|
||||||
echo('%s %s -> %s: %s\n' % (
|
|
||||||
msg_id,
|
|
||||||
trans_emoji(src['ShowName']),
|
|
||||||
trans_emoji(dst['ShowName']),
|
|
||||||
trans_emoji(msg['log'])
|
|
||||||
))
|
|
||||||
@@ -1,973 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
#===================================================
|
|
||||||
from utils import *
|
|
||||||
from config import Constant
|
|
||||||
from config import Log
|
|
||||||
#---------------------------------------------------
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import cookielib
|
|
||||||
import random
|
|
||||||
import requests
|
|
||||||
import time
|
|
||||||
import xml.dom.minidom
|
|
||||||
# for media upload
|
|
||||||
import mimetypes
|
|
||||||
from requests_toolbelt.multipart.encoder import MultipartEncoder
|
|
||||||
#===================================================
|
|
||||||
|
|
||||||
|
|
||||||
class WXAPI(object):
|
|
||||||
|
|
||||||
def __init__(self, host):
|
|
||||||
self.wx_host = host
|
|
||||||
self.wx_filehost = ''
|
|
||||||
self.wx_conf = {}
|
|
||||||
# jsLogin时这个appid只能使用: wx782c26e4c19acffb
|
|
||||||
self.appid = Constant.API_APPID
|
|
||||||
self.uuid = ''
|
|
||||||
self.redirect_uri = ''
|
|
||||||
self.skey = ''
|
|
||||||
self.sid = ''
|
|
||||||
self.uin = ''
|
|
||||||
self.pass_ticket = ''
|
|
||||||
self.base_request = {}
|
|
||||||
self.synckey_dic = {}
|
|
||||||
self.synckey = ''
|
|
||||||
self.device_id = 'e' + repr(random.random())[2:17]
|
|
||||||
# device_id: 登录手机设备
|
|
||||||
# web wechat 的格式为: e123456789012345 (e+15位随机数)
|
|
||||||
# mobile wechat 的格式为: A1234567890abcde (A+15位随机数字或字母)
|
|
||||||
self.user_agent = Constant.API_USER_AGENT
|
|
||||||
self.cookie = None
|
|
||||||
|
|
||||||
self.conf_factory()
|
|
||||||
|
|
||||||
self.User = [] # 登陆账号信息
|
|
||||||
self.MemberList = [] # 好友+群聊+公众号+特殊账号
|
|
||||||
self.MemberCount = 0
|
|
||||||
self.ContactList = [] # 好友
|
|
||||||
self.GroupList = [] # 群
|
|
||||||
self.GroupMemeberList = {} # 群聊成员字典
|
|
||||||
# "group_id": [
|
|
||||||
# {member}, ...
|
|
||||||
# ]
|
|
||||||
self.PublicUsersList = [] # 公众号/服务号
|
|
||||||
self.SpecialUsersList = [] # 特殊账号
|
|
||||||
|
|
||||||
self.media_count = 0
|
|
||||||
|
|
||||||
def conf_factory(self):
|
|
||||||
e = self.wx_host # wx.qq.com
|
|
||||||
t, o, n = "login.weixin.qq.com", "file.wx.qq.com", "webpush.weixin.qq.com"
|
|
||||||
|
|
||||||
if e.find("wx2.qq.com") > -1:
|
|
||||||
t, o, n = "login.wx2.qq.com", "file.wx2.qq.com", "webpush.wx2.qq.com"
|
|
||||||
elif e.find("wx8.qq.com") > -1:
|
|
||||||
t, o, n = "login.wx8.qq.com", "file.wx8.qq.com", "webpush.wx8.qq.com"
|
|
||||||
elif e.find("qq.com") > -1:
|
|
||||||
t, o, n = "login.wx.qq.com", "file.wx.qq.com", "webpush.wx.qq.com"
|
|
||||||
elif e.find("web2.wechat.com") > -1:
|
|
||||||
t, o, n = "login.web2.wechat.com", "file.web2.wechat.com", "webpush.web2.wechat.com"
|
|
||||||
elif e.find("wechat.com") > -1:
|
|
||||||
t, o, n = "login.web.wechat.com", "file.web.wechat.com", "webpush.web.wechat.com"
|
|
||||||
|
|
||||||
self.wx_filehost = o
|
|
||||||
conf = {
|
|
||||||
'LANG': Constant.API_LANG,
|
|
||||||
'SpecialUsers': Constant.API_SPECIAL_USER,
|
|
||||||
'API_jsLogin': "https://" + t + "/jslogin",
|
|
||||||
'API_qrcode': "https://login.weixin.qq.com/l/",
|
|
||||||
'API_qrcode_img': "https://login.weixin.qq.com/qrcode/",
|
|
||||||
'API_login': "https://" + t + "/cgi-bin/mmwebwx-bin/login",
|
|
||||||
'API_synccheck': "https://" + n + "/cgi-bin/mmwebwx-bin/synccheck",
|
|
||||||
'API_webwxdownloadmedia': "https://" + o + "/cgi-bin/mmwebwx-bin/webwxgetmedia",
|
|
||||||
'API_webwxuploadmedia': "https://" + o + "/cgi-bin/mmwebwx-bin/webwxuploadmedia",
|
|
||||||
'API_webwxpreview': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxpreview",
|
|
||||||
'API_webwxinit': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxinit",
|
|
||||||
'API_webwxgetcontact': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetcontact",
|
|
||||||
'API_webwxsync': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsync",
|
|
||||||
'API_webwxbatchgetcontact': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxbatchgetcontact",
|
|
||||||
'API_webwxgeticon': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgeticon",
|
|
||||||
'API_webwxsendmsg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendmsg",
|
|
||||||
'API_webwxsendmsgimg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendmsgimg",
|
|
||||||
'API_webwxsendmsgvedio': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendvideomsg",
|
|
||||||
'API_webwxsendemoticon': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendemoticon",
|
|
||||||
'API_webwxsendappmsg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendappmsg",
|
|
||||||
'API_webwxgetheadimg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetheadimg",
|
|
||||||
'API_webwxgetmsgimg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetmsgimg",
|
|
||||||
'API_webwxgetmedia': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetmedia",
|
|
||||||
'API_webwxgetvideo': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetvideo",
|
|
||||||
'API_webwxlogout': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxlogout",
|
|
||||||
'API_webwxgetvoice': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxgetvoice",
|
|
||||||
'API_webwxupdatechatroom': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxupdatechatroom",
|
|
||||||
'API_webwxcreatechatroom': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxcreatechatroom",
|
|
||||||
'API_webwxstatusnotify': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxstatusnotify",
|
|
||||||
'API_webwxcheckurl': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxcheckurl",
|
|
||||||
'API_webwxverifyuser': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxverifyuser",
|
|
||||||
'API_webwxfeedback': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsendfeedback",
|
|
||||||
'API_webwxreport': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxstatreport",
|
|
||||||
'API_webwxsearch': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxsearchcontact",
|
|
||||||
'API_webwxoplog': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxoplog",
|
|
||||||
'API_checkupload': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxcheckupload",
|
|
||||||
'API_webwxrevokemsg': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxrevokemsg",
|
|
||||||
'API_webwxpushloginurl': "https://" + e + "/cgi-bin/mmwebwx-bin/webwxpushloginurl",
|
|
||||||
'CONTACTFLAG_CONTACT': 1,
|
|
||||||
'CONTACTFLAG_CHATCONTACT': 2,
|
|
||||||
'CONTACTFLAG_CHATROOMCONTACT': 4,
|
|
||||||
'CONTACTFLAG_BLACKLISTCONTACT': 8,
|
|
||||||
'CONTACTFLAG_DOMAINCONTACT': 16,
|
|
||||||
'CONTACTFLAG_HIDECONTACT': 32,
|
|
||||||
'CONTACTFLAG_FAVOURCONTACT': 64,
|
|
||||||
'CONTACTFLAG_3RDAPPCONTACT': 128,
|
|
||||||
'CONTACTFLAG_SNSBLACKLISTCONTACT': 256,
|
|
||||||
'CONTACTFLAG_NOTIFYCLOSECONTACT': 512,
|
|
||||||
'CONTACTFLAG_TOPCONTACT': 2048,
|
|
||||||
'MSGTYPE_TEXT': 1,
|
|
||||||
'MSGTYPE_IMAGE': 3,
|
|
||||||
'MSGTYPE_VOICE': 34,
|
|
||||||
'MSGTYPE_VIDEO': 43,
|
|
||||||
'MSGTYPE_MICROVIDEO': 62,
|
|
||||||
'MSGTYPE_EMOTICON': 47,
|
|
||||||
'MSGTYPE_APP': 49,
|
|
||||||
'MSGTYPE_VOIPMSG': 50,
|
|
||||||
'MSGTYPE_VOIPNOTIFY': 52,
|
|
||||||
'MSGTYPE_VOIPINVITE': 53,
|
|
||||||
'MSGTYPE_LOCATION': 48,
|
|
||||||
'MSGTYPE_STATUSNOTIFY': 51,
|
|
||||||
'MSGTYPE_SYSNOTICE': 9999,
|
|
||||||
'MSGTYPE_POSSIBLEFRIEND_MSG': 40,
|
|
||||||
'MSGTYPE_VERIFYMSG': 37,
|
|
||||||
'MSGTYPE_SHARECARD': 42,
|
|
||||||
'MSGTYPE_SYS': 10000,
|
|
||||||
'MSGTYPE_RECALLED': 10002,
|
|
||||||
'APPMSGTYPE_TEXT': 1,
|
|
||||||
'APPMSGTYPE_IMG': 2,
|
|
||||||
'APPMSGTYPE_AUDIO': 3,
|
|
||||||
'APPMSGTYPE_VIDEO': 4,
|
|
||||||
'APPMSGTYPE_URL': 5,
|
|
||||||
'APPMSGTYPE_ATTACH': 6,
|
|
||||||
'APPMSGTYPE_OPEN': 7,
|
|
||||||
'APPMSGTYPE_EMOJI': 8,
|
|
||||||
'APPMSGTYPE_VOICE_REMIND': 9,
|
|
||||||
'APPMSGTYPE_SCAN_GOOD': 10,
|
|
||||||
'APPMSGTYPE_GOOD': 13,
|
|
||||||
'APPMSGTYPE_EMOTION': 15,
|
|
||||||
'APPMSGTYPE_CARD_TICKET': 16,
|
|
||||||
'APPMSGTYPE_REALTIME_SHARE_LOCATION': 17,
|
|
||||||
'APPMSGTYPE_TRANSFERS': 2e3,
|
|
||||||
'APPMSGTYPE_RED_ENVELOPES': 2001,
|
|
||||||
'APPMSGTYPE_READER_TYPE': 100001,
|
|
||||||
'UPLOAD_MEDIA_TYPE_IMAGE': 1,
|
|
||||||
'UPLOAD_MEDIA_TYPE_VIDEO': 2,
|
|
||||||
'UPLOAD_MEDIA_TYPE_AUDIO': 3,
|
|
||||||
'UPLOAD_MEDIA_TYPE_ATTACHMENT': 4,
|
|
||||||
}
|
|
||||||
self.wx_conf = conf
|
|
||||||
|
|
||||||
def getuuid(self):
|
|
||||||
"""
|
|
||||||
@brief Gets the uuid just used for login.
|
|
||||||
@return Bool: whether operation succeed.
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_jsLogin']
|
|
||||||
params = {
|
|
||||||
'appid': self.appid,
|
|
||||||
'fun': 'new',
|
|
||||||
'lang': self.wx_conf['LANG'],
|
|
||||||
'_': int(time.time()),
|
|
||||||
}
|
|
||||||
data = post(url, params, False)
|
|
||||||
regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"'
|
|
||||||
pm = re.search(regx, data)
|
|
||||||
if pm:
|
|
||||||
code = pm.group(1)
|
|
||||||
self.uuid = pm.group(2)
|
|
||||||
return code == '200'
|
|
||||||
return False
|
|
||||||
|
|
||||||
def genqrcode(self):
|
|
||||||
"""
|
|
||||||
@brief outprint the qrcode to stdout on macos/linux
|
|
||||||
or open image on windows
|
|
||||||
"""
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
url = self.wx_conf['API_qrcode_img'] + self.uuid
|
|
||||||
params = {
|
|
||||||
't': 'webwx',
|
|
||||||
'_': int(time.time())
|
|
||||||
}
|
|
||||||
data = post(url, params, False)
|
|
||||||
if data == '':
|
|
||||||
return
|
|
||||||
qrcode_path = save_file('qrcode.jpg', data, './')
|
|
||||||
os.startfile(qrcode_path)
|
|
||||||
else:
|
|
||||||
str2qr_terminal(self.wx_conf['API_qrcode'] + self.uuid)
|
|
||||||
|
|
||||||
def waitforlogin(self, tip=1):
|
|
||||||
"""
|
|
||||||
@brief wait for scaning qrcode to login
|
|
||||||
@param tip 1: wait for scan qrcode
|
|
||||||
0: wait for confirm
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
time.sleep(tip)
|
|
||||||
url = self.wx_conf['API_login'] + '?tip=%s&uuid=%s&_=%s' % (
|
|
||||||
tip, self.uuid, int(time.time()))
|
|
||||||
data = get(url)
|
|
||||||
pm = re.search(r'window.code=(\d+);', data)
|
|
||||||
code = pm.group(1)
|
|
||||||
|
|
||||||
if code == '201':
|
|
||||||
return True
|
|
||||||
elif code == '200':
|
|
||||||
pm = re.search(r'window.redirect_uri="(\S+?)";', data)
|
|
||||||
r_uri = pm.group(1) + '&fun=new'
|
|
||||||
self.redirect_uri = r_uri
|
|
||||||
self.wx_host = r_uri.split('://')[1].split('/')[0]
|
|
||||||
self.conf_factory()
|
|
||||||
return True
|
|
||||||
elif code == '408':
|
|
||||||
echo(Constant.LOG_MSG_WAIT_LOGIN_ERR1)
|
|
||||||
else:
|
|
||||||
echo(Constant.LOG_MSG_WAIT_LOGIN_ERR2)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def login(self):
|
|
||||||
"""
|
|
||||||
@brief login
|
|
||||||
redirect_uri 有效时间是从扫码成功后算起,
|
|
||||||
大概是 300 秒,在此期间可以重新登录,但获取的联系人和群ID会改变
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
data = get(self.redirect_uri)
|
|
||||||
doc = xml.dom.minidom.parseString(data)
|
|
||||||
root = doc.documentElement
|
|
||||||
|
|
||||||
for node in root.childNodes:
|
|
||||||
if node.nodeName == 'ret':
|
|
||||||
if node.childNodes[0].data != "0":
|
|
||||||
return False
|
|
||||||
elif node.nodeName == 'skey':
|
|
||||||
self.skey = node.childNodes[0].data
|
|
||||||
elif node.nodeName == 'wxsid':
|
|
||||||
self.sid = node.childNodes[0].data
|
|
||||||
elif node.nodeName == 'wxuin':
|
|
||||||
self.uin = node.childNodes[0].data
|
|
||||||
elif node.nodeName == 'pass_ticket':
|
|
||||||
self.pass_ticket = node.childNodes[0].data
|
|
||||||
|
|
||||||
if '' in (self.skey, self.sid, self.uin, self.pass_ticket):
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.base_request = {
|
|
||||||
'Uin': int(self.uin),
|
|
||||||
'Sid': self.sid,
|
|
||||||
'Skey': self.skey,
|
|
||||||
'DeviceID': self.device_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def webwxinit(self):
|
|
||||||
"""
|
|
||||||
@brief wechat initial
|
|
||||||
掉线后 300 秒可以重新使用此 api 登录
|
|
||||||
获取的联系人和群ID保持不变
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxinit'] + \
|
|
||||||
'?pass_ticket=%s&skey=%s&r=%s' % (
|
|
||||||
self.pass_ticket, self.skey, int(time.time())
|
|
||||||
)
|
|
||||||
params = {
|
|
||||||
'BaseRequest': self.base_request
|
|
||||||
}
|
|
||||||
dic = post(url, params)
|
|
||||||
self.User = dic['User']
|
|
||||||
self.make_synckey(dic)
|
|
||||||
|
|
||||||
return dic['BaseResponse']['Ret'] == 0
|
|
||||||
|
|
||||||
def webwxstatusnotify(self):
|
|
||||||
"""
|
|
||||||
@brief notify the mobile phone, this not necessary
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxstatusnotify'] + \
|
|
||||||
'?lang=%s&pass_ticket=%s' % (
|
|
||||||
self.wx_conf['LANG'], self.pass_ticket
|
|
||||||
)
|
|
||||||
params = {
|
|
||||||
'BaseRequest': self.base_request,
|
|
||||||
"Code": 3,
|
|
||||||
"FromUserName": self.User['UserName'],
|
|
||||||
"ToUserName": self.User['UserName'],
|
|
||||||
"ClientMsgId": int(time.time())
|
|
||||||
}
|
|
||||||
dic = post(url, params)
|
|
||||||
|
|
||||||
return dic['BaseResponse']['Ret'] == 0
|
|
||||||
|
|
||||||
def webwxgetcontact(self):
|
|
||||||
"""
|
|
||||||
@brief get all contacts: people, group, public user, special user
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
SpecialUsers = self.wx_conf['SpecialUsers']
|
|
||||||
url = self.wx_conf['API_webwxgetcontact'] + \
|
|
||||||
'?pass_ticket=%s&skey=%s&r=%s' % (
|
|
||||||
self.pass_ticket, self.skey, int(time.time())
|
|
||||||
)
|
|
||||||
dic = post(url, {})
|
|
||||||
|
|
||||||
self.MemberCount = dic['MemberCount']
|
|
||||||
self.MemberList = dic['MemberList']
|
|
||||||
ContactList = self.MemberList[:]
|
|
||||||
GroupList = self.GroupList[:]
|
|
||||||
PublicUsersList = self.PublicUsersList[:]
|
|
||||||
SpecialUsersList = self.SpecialUsersList[:]
|
|
||||||
|
|
||||||
for i in xrange(len(ContactList) - 1, -1, -1):
|
|
||||||
Contact = ContactList[i]
|
|
||||||
if Contact['VerifyFlag'] & 8 != 0: # 公众号/服务号
|
|
||||||
ContactList.remove(Contact)
|
|
||||||
self.PublicUsersList.append(Contact)
|
|
||||||
elif Contact['UserName'] in SpecialUsers: # 特殊账号
|
|
||||||
ContactList.remove(Contact)
|
|
||||||
self.SpecialUsersList.append(Contact)
|
|
||||||
elif Contact['UserName'].find('@@') != -1: # 群聊
|
|
||||||
ContactList.remove(Contact)
|
|
||||||
self.GroupList.append(Contact)
|
|
||||||
elif Contact['UserName'] == self.User['UserName']: # 自己
|
|
||||||
ContactList.remove(Contact)
|
|
||||||
self.ContactList = ContactList
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def webwxbatchgetcontact(self, gid_list):
|
|
||||||
"""
|
|
||||||
@brief get group contacts
|
|
||||||
@param gid_list, The list of group id
|
|
||||||
@return List, list of group contacts
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxbatchgetcontact'] + \
|
|
||||||
'?type=ex&r=%s&pass_ticket=%s' % (
|
|
||||||
int(time.time()), self.pass_ticket
|
|
||||||
)
|
|
||||||
params = {
|
|
||||||
'BaseRequest': self.base_request,
|
|
||||||
"Count": len(gid_list),
|
|
||||||
"List": [{"UserName": gid, "EncryChatRoomId": ""} for gid in gid_list]
|
|
||||||
}
|
|
||||||
dic = post(url, params)
|
|
||||||
return dic['ContactList']
|
|
||||||
|
|
||||||
def synccheck(self):
|
|
||||||
"""
|
|
||||||
@brief check whether there's a message
|
|
||||||
@return [retcode, selector]
|
|
||||||
retcode: 0 successful
|
|
||||||
1100 logout
|
|
||||||
1101 login otherwhere
|
|
||||||
selector: 0 nothing
|
|
||||||
2 message
|
|
||||||
6 unkonwn
|
|
||||||
7 webwxsync
|
|
||||||
"""
|
|
||||||
params = {
|
|
||||||
'r': int(time.time()),
|
|
||||||
'sid': self.sid,
|
|
||||||
'uin': self.uin,
|
|
||||||
'skey': self.skey,
|
|
||||||
'deviceid': self.device_id,
|
|
||||||
'synckey': self.synckey,
|
|
||||||
'_': int(time.time()),
|
|
||||||
}
|
|
||||||
url = self.wx_conf['API_synccheck'] + '?' + urllib.urlencode(params)
|
|
||||||
data = get(url)
|
|
||||||
reg = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}'
|
|
||||||
pm = re.search(reg, data)
|
|
||||||
retcode = pm.group(1)
|
|
||||||
selector = pm.group(2)
|
|
||||||
return [retcode, selector]
|
|
||||||
|
|
||||||
def webwxsync(self):
|
|
||||||
"""
|
|
||||||
@brief sync the messages
|
|
||||||
@return Dict{}
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxsync'] + \
|
|
||||||
'?sid=%s&skey=%s&pass_ticket=%s' % (
|
|
||||||
self.sid, self.skey, self.pass_ticket
|
|
||||||
)
|
|
||||||
params = {
|
|
||||||
'BaseRequest': self.base_request,
|
|
||||||
'SyncKey': self.synckey_dic,
|
|
||||||
'rr': ~int(time.time())
|
|
||||||
}
|
|
||||||
dic = post(url, params)
|
|
||||||
|
|
||||||
if dic['BaseResponse']['Ret'] == 0:
|
|
||||||
self.make_synckey(dic)
|
|
||||||
return dic
|
|
||||||
|
|
||||||
def webwxgetmsgimg(self, msgid):
|
|
||||||
"""
|
|
||||||
@brief get image in message
|
|
||||||
@param msgid The id of message
|
|
||||||
@return binary data of image
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxgetmsgimg'] + \
|
|
||||||
'?MsgID=%s&skey=%s' % (msgid, self.skey)
|
|
||||||
data = get(url, api='webwxgetmsgimg')
|
|
||||||
return data
|
|
||||||
|
|
||||||
def webwxgetvoice(self, msgid):
|
|
||||||
"""
|
|
||||||
@brief get voice in message
|
|
||||||
@param msgid The id of message
|
|
||||||
@return binary data of voice
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxgetvoice'] + \
|
|
||||||
'?msgid=%s&skey=%s' % (msgid, self.skey)
|
|
||||||
data = get(url, api='webwxgetvoice')
|
|
||||||
return data
|
|
||||||
|
|
||||||
def webwxgetvideo(self, msgid):
|
|
||||||
"""
|
|
||||||
@brief get video in message
|
|
||||||
@param msgid The id of message
|
|
||||||
@return binary data of video
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxgetvideo'] + \
|
|
||||||
'?msgid=%s&skey=%s' % (msgid, self.skey)
|
|
||||||
data = get(url, api='webwxgetvideo')
|
|
||||||
return data
|
|
||||||
|
|
||||||
def webwxgeticon(self, id):
|
|
||||||
"""
|
|
||||||
@brief get user small icon
|
|
||||||
@param id String
|
|
||||||
@return binary data of icon
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxgeticon'] + \
|
|
||||||
'?username=%s&skey=%s' % (id, self.skey)
|
|
||||||
data = get(url, api='webwxgeticon')
|
|
||||||
return data
|
|
||||||
|
|
||||||
def webwxgetheadimg(self, id):
|
|
||||||
"""
|
|
||||||
@brief get user head image
|
|
||||||
@param id String
|
|
||||||
@return binary data of image
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxgetheadimg'] + \
|
|
||||||
'?username=%s&skey=%s' % (id, self.skey)
|
|
||||||
data = get(url, api='webwxgetheadimg')
|
|
||||||
return data
|
|
||||||
|
|
||||||
def webwxsendmsg(self, word, to='filehelper'):
|
|
||||||
"""
|
|
||||||
@brief send text message
|
|
||||||
@param word String
|
|
||||||
@param to User id
|
|
||||||
@return dic Dict
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxsendmsg'] + \
|
|
||||||
'?pass_ticket=%s' % (self.pass_ticket)
|
|
||||||
clientMsgId = str(int(time.time() * 1000)) + \
|
|
||||||
str(random.random())[:5].replace('.', '')
|
|
||||||
params = {
|
|
||||||
'BaseRequest': self.base_request,
|
|
||||||
'Msg': {
|
|
||||||
"Type": 1,
|
|
||||||
"Content": trans_coding(word),
|
|
||||||
"FromUserName": self.User['UserName'],
|
|
||||||
"ToUserName": to,
|
|
||||||
"LocalID": clientMsgId,
|
|
||||||
"ClientMsgId": clientMsgId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dic = post(url, params)
|
|
||||||
return dic
|
|
||||||
|
|
||||||
def webwxuploadmedia(self, file_path):
|
|
||||||
"""
|
|
||||||
@brief upload image
|
|
||||||
@param file_path String
|
|
||||||
@return Dict: json
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxuploadmedia'] + '?f=json'
|
|
||||||
# 计数器
|
|
||||||
self.media_count = self.media_count + 1
|
|
||||||
fn = file_path
|
|
||||||
# mime_type:
|
|
||||||
# 'application/pdf'
|
|
||||||
# 'image/jpeg'
|
|
||||||
# 'image/png'
|
|
||||||
# ...
|
|
||||||
mime_type = mimetypes.guess_type(fn, strict=False)[0]
|
|
||||||
if not mime_type:
|
|
||||||
mime_type = 'text/plain'
|
|
||||||
# 文档格式
|
|
||||||
# 微信服务器目前应该支持3种类型:
|
|
||||||
# pic 直接显示,包含图片,表情
|
|
||||||
# video 不清楚
|
|
||||||
# doc 显示为文件,包含PDF等
|
|
||||||
media_type = 'pic' if mime_type.split('/')[0] == 'image' else 'doc'
|
|
||||||
time_format = "%a %b %d %Y %T GMT%z (%Z)"
|
|
||||||
last_modifie_date = time.strftime(time_format, time.localtime())
|
|
||||||
file_size = os.path.getsize(fn)
|
|
||||||
pass_ticket = self.pass_ticket
|
|
||||||
client_media_id = str(int(time.time() * 1000)) + \
|
|
||||||
str(random.random())[:5].replace('.', '')
|
|
||||||
|
|
||||||
webwx_data_ticket = ''
|
|
||||||
for item in self.cookie:
|
|
||||||
if item.name == 'webwx_data_ticket':
|
|
||||||
webwx_data_ticket = item.value
|
|
||||||
break
|
|
||||||
if (webwx_data_ticket == ''):
|
|
||||||
Log.error("No Cookie\n")
|
|
||||||
return None
|
|
||||||
|
|
||||||
uploadmediarequest = json.dumps({
|
|
||||||
"BaseRequest": self.base_request,
|
|
||||||
"ClientMediaId": client_media_id,
|
|
||||||
"TotalLen": file_size,
|
|
||||||
"StartPos": 0,
|
|
||||||
"DataLen": file_size,
|
|
||||||
"MediaType": 4
|
|
||||||
}, ensure_ascii=False).encode('utf8')
|
|
||||||
|
|
||||||
multipart_encoder = MultipartEncoder(
|
|
||||||
fields={
|
|
||||||
'id': 'WU_FILE_' + str(self.media_count),
|
|
||||||
'name': fn,
|
|
||||||
'type': mime_type,
|
|
||||||
'lastModifieDate': last_modifie_date,
|
|
||||||
'size': str(file_size),
|
|
||||||
'mediatype': media_type,
|
|
||||||
'uploadmediarequest': uploadmediarequest,
|
|
||||||
'webwx_data_ticket': webwx_data_ticket,
|
|
||||||
'pass_ticket': pass_ticket,
|
|
||||||
'filename': (
|
|
||||||
fn,
|
|
||||||
open(fn, 'rb'),
|
|
||||||
mime_type.split('/')[1]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
boundary=(
|
|
||||||
'-----------------------------'
|
|
||||||
'1575017231431605357584454111'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'Host': self.wx_filehost,
|
|
||||||
'User-Agent': self.user_agent,
|
|
||||||
'Accept': (
|
|
||||||
'text/html,application/xhtml+xml,'
|
|
||||||
'application/xml;q=0.9,*/*;q=0.8'
|
|
||||||
),
|
|
||||||
'Accept-Language': 'en-US,en;q=0.5',
|
|
||||||
'Accept-Encoding': 'gzip, deflate',
|
|
||||||
'Referer': 'https://' + self.wx_host,
|
|
||||||
'Content-Type': multipart_encoder.content_type,
|
|
||||||
'Origin': 'https://' + self.wx_host,
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
'Pragma': 'no-cache',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
}
|
|
||||||
|
|
||||||
r = requests.post(url, data=multipart_encoder, headers=headers)
|
|
||||||
dic = json.loads(r.text) #修复无法发送Media消息BUG
|
|
||||||
if dic['BaseResponse']['Ret'] == 0:
|
|
||||||
return dic
|
|
||||||
return None
|
|
||||||
|
|
||||||
def webwxsendmsgimg(self, user_id, media_id):
|
|
||||||
"""
|
|
||||||
@brief send image
|
|
||||||
@param user_id String
|
|
||||||
@param media_id String
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxsendmsgimg'] + \
|
|
||||||
'?fun=async&f=json&pass_ticket=%s' % self.pass_ticket
|
|
||||||
clientMsgId = str(int(time.time() * 1000)) + \
|
|
||||||
str(random.random())[:5].replace('.', '')
|
|
||||||
data_json = {
|
|
||||||
"BaseRequest": self.base_request,
|
|
||||||
"Msg": {
|
|
||||||
"Type": 3,
|
|
||||||
"MediaId": media_id,
|
|
||||||
"FromUserName": self.User['UserName'],
|
|
||||||
"ToUserName": user_id,
|
|
||||||
"LocalID": clientMsgId,
|
|
||||||
"ClientMsgId": clientMsgId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r = post(url, data_json)
|
|
||||||
return dic['BaseResponse']['Ret'] == 0
|
|
||||||
|
|
||||||
def webwxsendemoticon(self, user_id, media_id):
|
|
||||||
"""
|
|
||||||
@brief send image
|
|
||||||
@param user_id String
|
|
||||||
@param media_id String
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxsendemoticon'] + \
|
|
||||||
'?fun=sys&f=json&pass_ticket=%s' % self.pass_ticket
|
|
||||||
clientMsgId = str(int(time.time() * 1000)) + \
|
|
||||||
str(random.random())[:5].replace('.', '')
|
|
||||||
data_json = {
|
|
||||||
"BaseRequest": self.base_request,
|
|
||||||
"Msg": {
|
|
||||||
"Type": 47,
|
|
||||||
"EmojiFlag": 2,
|
|
||||||
"MediaId": media_id,
|
|
||||||
"FromUserName": self.User['UserName'],
|
|
||||||
"ToUserName": user_id,
|
|
||||||
"LocalID": clientMsgId,
|
|
||||||
"ClientMsgId": clientMsgId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r = post(url, data_json)
|
|
||||||
return dic['BaseResponse']['Ret'] == 0
|
|
||||||
|
|
||||||
def webwxsendappmsg(self, user_id, data):
|
|
||||||
"""
|
|
||||||
@brief send app msg
|
|
||||||
@param user_id String
|
|
||||||
@param data Dict
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxsendappmsg'] + \
|
|
||||||
'?fun=sys&f=json&pass_ticket=%s' % self.pass_ticket
|
|
||||||
clientMsgId = str(int(time.time() * 1000)) + \
|
|
||||||
str(random.random())[:5].replace('.', '')
|
|
||||||
content = ''.join([
|
|
||||||
"<appmsg appid='%s' sdkver=''>" % data['appid'], # 可使用其它AppID
|
|
||||||
"<title>%s</title>" % data['title'],
|
|
||||||
"<des></des>",
|
|
||||||
"<action></action>",
|
|
||||||
"<type>%d</type>" % data['type'],
|
|
||||||
"<content></content>",
|
|
||||||
"<url></url>",
|
|
||||||
"<lowurl></lowurl>",
|
|
||||||
"<appattach>",
|
|
||||||
"<totallen>%d</totallen>" % data['totallen'],
|
|
||||||
"<attachid>%s</attachid>" % data['attachid'],
|
|
||||||
"<fileext>%s</fileext>" % data['fileext'],
|
|
||||||
"</appattach>",
|
|
||||||
"<extinfo></extinfo>",
|
|
||||||
"</appmsg>",
|
|
||||||
])
|
|
||||||
data_json = {
|
|
||||||
"BaseRequest": self.base_request,
|
|
||||||
"Msg": {
|
|
||||||
"Type": data['type'],
|
|
||||||
"Content": content,
|
|
||||||
"FromUserName": self.User['UserName'],
|
|
||||||
"ToUserName": user_id,
|
|
||||||
"LocalID": clientMsgId,
|
|
||||||
"ClientMsgId": clientMsgId
|
|
||||||
},
|
|
||||||
"Scene": 0
|
|
||||||
}
|
|
||||||
r = post(url, data_json)
|
|
||||||
return dic['BaseResponse']['Ret'] == 0
|
|
||||||
|
|
||||||
def webwxcreatechatroom(self, uid_arr):
|
|
||||||
"""
|
|
||||||
@brief create a chat group
|
|
||||||
@param uid_arr [String]
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxcreatechatroom'] + '?r=%s' % int(time.time())
|
|
||||||
params = {
|
|
||||||
'BaseRequest': self.base_request,
|
|
||||||
'Topic': '',
|
|
||||||
'MemberCount': len(uid_arr),
|
|
||||||
'MemberList': [{'UserName': uid} for uid in uid_arr],
|
|
||||||
}
|
|
||||||
dic = post(url, params)
|
|
||||||
return dic['BaseResponse']['Ret'] == 0
|
|
||||||
|
|
||||||
def webwxupdatechatroom(self, add_arr, del_arr, invite_arr):
|
|
||||||
"""
|
|
||||||
@brief add/delete/invite member in chat group
|
|
||||||
@param add_arr [uid: String]
|
|
||||||
@param del_arr [uid: String]
|
|
||||||
@param invite_arr [uid: String]
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxupdatechatroom'] + '?r=%s' % int(time.time())
|
|
||||||
params = {
|
|
||||||
'BaseRequest': self.base_request,
|
|
||||||
'ChatRoomName': '',
|
|
||||||
'NewTopic': '',
|
|
||||||
'AddMemberList': add_arr,
|
|
||||||
'DelMemberList': del_arr,
|
|
||||||
'InviteMemberList': invite_arr,
|
|
||||||
}
|
|
||||||
dic = post(url, params)
|
|
||||||
return dic['BaseResponse']['Ret'] == 0
|
|
||||||
|
|
||||||
def webwxrevokemsg(self, msgid, user_id, client_msgid):
|
|
||||||
"""
|
|
||||||
@brief revoke a message
|
|
||||||
@param msgid String
|
|
||||||
@param user_id String
|
|
||||||
@param client_msgid String
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxrevokemsg'] + '?r=%s' % int(time.time())
|
|
||||||
params = {
|
|
||||||
'BaseRequest': self.base_request,
|
|
||||||
'SvrMsgId': msgid,
|
|
||||||
'ToUserName': user_id,
|
|
||||||
'ClientMsgId': client_msgid
|
|
||||||
}
|
|
||||||
dic = post(url, params)
|
|
||||||
return dic['BaseResponse']['Ret'] == 0
|
|
||||||
|
|
||||||
def webwxpushloginurl(self, uin):
|
|
||||||
"""
|
|
||||||
@brief push a login confirm alert to mobile device
|
|
||||||
@param uin String
|
|
||||||
@return dic Dict
|
|
||||||
"""
|
|
||||||
url = self.wx_conf['API_webwxpushloginurl'] + '?uin=%s' % uin
|
|
||||||
dic = eval(get(url))
|
|
||||||
return dic
|
|
||||||
|
|
||||||
def association_login(self):
|
|
||||||
"""
|
|
||||||
@brief login without scan qrcode
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
if self.uin != '':
|
|
||||||
dic = self.webwxpushloginurl(self.uin)
|
|
||||||
if dic['ret'] == '0':
|
|
||||||
self.uuid = dic['uuid']
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def send_text(self, user_id, text):
|
|
||||||
"""
|
|
||||||
@brief send text
|
|
||||||
@param user_id String
|
|
||||||
@param text String
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
dic = self.webwxsendmsg(text, user_id)
|
|
||||||
return dic['BaseResponse']['Ret'] == 0
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def send_img(self, user_id, file_path):
|
|
||||||
"""
|
|
||||||
@brief send image
|
|
||||||
@param user_id String
|
|
||||||
@param file_path String
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
response = self.webwxuploadmedia(file_path)
|
|
||||||
media_id = ""
|
|
||||||
if response is not None:
|
|
||||||
media_id = response['MediaId']
|
|
||||||
return self.webwxsendmsgimg(user_id, media_id)
|
|
||||||
|
|
||||||
def send_emot(self, user_id, file_path):
|
|
||||||
"""
|
|
||||||
@brief send emotion
|
|
||||||
@param user_id String
|
|
||||||
@param file_path String
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
response = self.webwxuploadmedia(file_path)
|
|
||||||
media_id = ""
|
|
||||||
if response is not None:
|
|
||||||
media_id = response['MediaId']
|
|
||||||
return self.webwxsendemoticon(user_id, media_id)
|
|
||||||
|
|
||||||
def send_file(self, user_id, file_path):
|
|
||||||
"""
|
|
||||||
@brief send file
|
|
||||||
@param user_id String
|
|
||||||
@param file_path String
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
title = file_path.split('/')[-1]
|
|
||||||
data = {
|
|
||||||
'appid': Constant.API_WXAPPID,
|
|
||||||
'title': title,
|
|
||||||
'totallen': '',
|
|
||||||
'attachid': '',
|
|
||||||
'type': self.wx_conf['APPMSGTYPE_ATTACH'],
|
|
||||||
'fileext': title.split('.')[-1],
|
|
||||||
}
|
|
||||||
|
|
||||||
response = self.webwxuploadmedia(file_path)
|
|
||||||
if response is not None:
|
|
||||||
data['totallen'] = response['StartPos']
|
|
||||||
data['attachid'] = response['MediaId']
|
|
||||||
else:
|
|
||||||
Log.error('File upload error')
|
|
||||||
|
|
||||||
return self.webwxsendappmsg(user_id, data)
|
|
||||||
|
|
||||||
def make_synckey(self, dic):
|
|
||||||
self.synckey_dic = dic['SyncKey']
|
|
||||||
|
|
||||||
def foo(x):
|
|
||||||
return str(x['Key']) + '_' + str(x['Val'])
|
|
||||||
|
|
||||||
# synckey for synccheck
|
|
||||||
self.synckey = '|'.join(
|
|
||||||
[foo(keyVal) for keyVal in self.synckey_dic['List']])
|
|
||||||
|
|
||||||
def revoke_msg(self, msgid, user_id, client_msgid):
|
|
||||||
"""
|
|
||||||
@brief revoke a message
|
|
||||||
@param msgid String
|
|
||||||
@param user_id String
|
|
||||||
@param client_msgid String
|
|
||||||
@return Bool: whether operation succeed
|
|
||||||
"""
|
|
||||||
return self.webwxrevokemsg(msgid, user_id, client_msgid)
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
|
||||||
# The following is getting user & group infomation apis
|
|
||||||
def get_user_by_id(self, user_id):
|
|
||||||
"""
|
|
||||||
@brief get all type of name by user id
|
|
||||||
@param user_id The id of user
|
|
||||||
@return Dict: {
|
|
||||||
'UserName' # 微信动态ID
|
|
||||||
'RemarkName' # 备注
|
|
||||||
'NickName' # 微信昵称
|
|
||||||
'ShowName' # Log显示用的
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
UnknownPeople = Constant.LOG_MSG_UNKNOWN_NAME + user_id
|
|
||||||
name = {
|
|
||||||
'UserName': user_id,
|
|
||||||
'RemarkName': '',
|
|
||||||
'NickName': '',
|
|
||||||
'ShowName': '',
|
|
||||||
}
|
|
||||||
name['ShowName'] = UnknownPeople
|
|
||||||
|
|
||||||
# yourself
|
|
||||||
if user_id == self.User['UserName']:
|
|
||||||
name['RemarkName'] = self.User['RemarkName']
|
|
||||||
name['NickName'] = self.User['NickName']
|
|
||||||
name['ShowName'] = name['NickName']
|
|
||||||
else:
|
|
||||||
# 联系人
|
|
||||||
for member in self.MemberList:
|
|
||||||
if member['UserName'] == user_id:
|
|
||||||
r, n = member['RemarkName'], member['NickName']
|
|
||||||
name['RemarkName'] = r
|
|
||||||
name['NickName'] = n
|
|
||||||
name['ShowName'] = r if r else n
|
|
||||||
break
|
|
||||||
# 特殊帐号
|
|
||||||
for member in self.SpecialUsersList:
|
|
||||||
if member['UserName'] == user_id:
|
|
||||||
name['RemarkName'] = user_id
|
|
||||||
name['NickName'] = user_id
|
|
||||||
name['ShowName'] = user_id
|
|
||||||
break
|
|
||||||
|
|
||||||
return name
|
|
||||||
|
|
||||||
def get_group_user_by_id(self, user_id, group_id):
|
|
||||||
"""
|
|
||||||
@brief get group user by user id
|
|
||||||
@param user_id The id of user
|
|
||||||
@param group_id The id of group
|
|
||||||
@return Dict: {
|
|
||||||
'UserName' # 微信动态ID
|
|
||||||
'NickName' # 微信昵称
|
|
||||||
'DisplayName' # 群聊显示名称
|
|
||||||
'ShowName' # Log显示用的
|
|
||||||
'AttrStatus' # 群用户id
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
UnknownPeople = Constant.LOG_MSG_UNKNOWN_NAME + user_id
|
|
||||||
name = {
|
|
||||||
'UserName': user_id,
|
|
||||||
'NickName': '',
|
|
||||||
'DisplayName': '',
|
|
||||||
'ShowName': '',
|
|
||||||
'AttrStatus': '',
|
|
||||||
}
|
|
||||||
name['ShowName'] = UnknownPeople
|
|
||||||
|
|
||||||
# 群友
|
|
||||||
if group_id in self.GroupMemeberList:
|
|
||||||
for member in self.GroupMemeberList[group_id]:
|
|
||||||
if member['UserName'] == user_id:
|
|
||||||
n, d = member['NickName'], member['DisplayName']
|
|
||||||
name['NickName'] = n
|
|
||||||
name['DisplayName'] = d
|
|
||||||
name['AttrStatus'] = member['AttrStatus']
|
|
||||||
name['ShowName'] = d if d else n
|
|
||||||
break
|
|
||||||
|
|
||||||
return name
|
|
||||||
|
|
||||||
def get_group_by_id(self, group_id):
|
|
||||||
"""
|
|
||||||
@brief get basic info by group id
|
|
||||||
@param group_id The id of group
|
|
||||||
@return Dict: {
|
|
||||||
'UserName' # 微信动态ID
|
|
||||||
'NickName' # 微信昵称
|
|
||||||
'DisplayName' # 群聊显示名称
|
|
||||||
'ShowName' # Log显示用的
|
|
||||||
'OwnerUin' # 群主ID
|
|
||||||
'MemberCount' # 群人数
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
UnknownGroup = Constant.LOG_MSG_UNKNOWN_GROUP_NAME + group_id
|
|
||||||
group = {
|
|
||||||
'UserName': group_id,
|
|
||||||
'NickName': '',
|
|
||||||
'DisplayName': '',
|
|
||||||
'ShowName': '',
|
|
||||||
'OwnerUin': '',
|
|
||||||
'MemberCount': '',
|
|
||||||
}
|
|
||||||
|
|
||||||
for member in self.GroupList:
|
|
||||||
if member['UserName'] == group_id:
|
|
||||||
group['NickName'] = member['NickName']
|
|
||||||
group['DisplayName'] = member.get('DisplayName', '')
|
|
||||||
group['ShowName'] = member.get('NickName', UnknownGroup)
|
|
||||||
group['OwnerUin'] = member.get('OwnerUin', '')
|
|
||||||
group['MemberCount'] = member['MemberCount']
|
|
||||||
break
|
|
||||||
|
|
||||||
return group
|
|
||||||
|
|
||||||
def get_user_id(self, name):
|
|
||||||
"""
|
|
||||||
@brief Gets the user id.
|
|
||||||
@param name The user nickname or remarkname
|
|
||||||
@return The user id.
|
|
||||||
"""
|
|
||||||
for member in self.MemberList:
|
|
||||||
if name == member['RemarkName'] or name == member['NickName']:
|
|
||||||
return member['UserName']
|
|
||||||
return None
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
#===================================================
|
|
||||||
from wechat import WeChat
|
|
||||||
from wechat.utils import *
|
|
||||||
from wx_handler import WeChatMsgProcessor
|
|
||||||
from wx_handler import Bot
|
|
||||||
from wx_handler import SGMail
|
|
||||||
from db import SqliteDB
|
|
||||||
from db import MysqlDB
|
|
||||||
from config import ConfigManager
|
|
||||||
from config import Constant
|
|
||||||
from config import Log
|
|
||||||
#---------------------------------------------------
|
|
||||||
from flask import Flask, render_template, send_file, jsonify, request
|
|
||||||
import threading
|
|
||||||
import traceback
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
#===================================================
|
|
||||||
|
|
||||||
|
|
||||||
cm = ConfigManager()
|
|
||||||
db = SqliteDB(cm.getpath('database'))
|
|
||||||
# db = MysqlDB(cm.mysql())
|
|
||||||
wechat_msg_processor = WeChatMsgProcessor(db)
|
|
||||||
wechat = WeChat(cm.get('wechat', 'host'))
|
|
||||||
wechat.db = db
|
|
||||||
wechat.bot = Bot()
|
|
||||||
wechat.msg_handler = wechat_msg_processor
|
|
||||||
wechat_msg_processor.wechat = wechat
|
|
||||||
|
|
||||||
PORT = int(cm.get('setting', 'server_port'))
|
|
||||||
app = Flask(__name__, template_folder='flask_templates')
|
|
||||||
app.config['UPLOAD_FOLDER'] = cm.getpath('uploaddir')
|
|
||||||
|
|
||||||
logger = logging.getLogger('werkzeug')
|
|
||||||
log_format_str = Constant.SERVER_LOG_FORMAT
|
|
||||||
formatter = logging.Formatter(log_format_str)
|
|
||||||
flask_log_handler = logging.FileHandler(cm.getpath('server_log_file'))
|
|
||||||
flask_log_handler.setLevel(logging.INFO)
|
|
||||||
flask_log_handler.setFormatter(formatter)
|
|
||||||
logger.addHandler(flask_log_handler)
|
|
||||||
app.logger.addHandler(flask_log_handler)
|
|
||||||
|
|
||||||
# sendgrid mail
|
|
||||||
sg_apikey = cm.get('sendgrid', 'api_key')
|
|
||||||
from_email = cm.get('sendgrid', 'from_email')
|
|
||||||
to_email = cm.get('sendgrid', 'to_email')
|
|
||||||
sg = SGMail(sg_apikey, from_email, to_email)
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def index():
|
|
||||||
return render_template(Constant.SERVER_PAGE_INDEX)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/qrcode')
|
|
||||||
def qrcode():
|
|
||||||
qdir = cm.getpath('qrcodedir')
|
|
||||||
if not os.path.exists(qdir):
|
|
||||||
os.makedirs(qdir)
|
|
||||||
image_path = '%s/%s_%d.png' % (qdir, wechat.uuid, int(time.time()*100))
|
|
||||||
s = wechat.wx_conf['API_qrcode'] + wechat.uuid
|
|
||||||
str2qr_image(s, image_path)
|
|
||||||
return send_file(image_path, mimetype='image/png')
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/group_list")
|
|
||||||
def group_list():
|
|
||||||
"""
|
|
||||||
@brief list groups
|
|
||||||
"""
|
|
||||||
result = wechat.db.select(Constant.TABLE_GROUP_LIST())
|
|
||||||
return jsonify({'count': len(result), 'group': result})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/group_member_list/<g_id>')
|
|
||||||
def group_member_list(g_id):
|
|
||||||
"""
|
|
||||||
@brief list group member
|
|
||||||
@param g_id String
|
|
||||||
"""
|
|
||||||
result = wechat.db.select(Constant.TABLE_GROUP_USER_LIST(), 'RoomID', g_id)
|
|
||||||
return jsonify({'count': len(result), 'member': result})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/group_chat_log/<g_name>')
|
|
||||||
def group_chat_log(g_name):
|
|
||||||
"""
|
|
||||||
@brief list group chat log
|
|
||||||
@param g_name String
|
|
||||||
"""
|
|
||||||
result = wechat.db.select(Constant.TABLE_GROUP_MSG_LOG, 'RoomName', g_name)
|
|
||||||
return jsonify({'count': len(result), 'chats': result})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/upload', methods=['GET', 'POST'])
|
|
||||||
def upload_file():
|
|
||||||
if request.method == 'POST':
|
|
||||||
def allowed_file(filename):
|
|
||||||
return '.' in filename and \
|
|
||||||
filename.rsplit('.', 1)[1] in Constant.SERVER_UPLOAD_ALLOWED_EXTENSIONS
|
|
||||||
|
|
||||||
j = {'ret': 1, 'msg': ''}
|
|
||||||
|
|
||||||
# check if the post request has the file part
|
|
||||||
if 'file' not in request.files:
|
|
||||||
j['msg'] = 'No file part'
|
|
||||||
return jsonify(j)
|
|
||||||
|
|
||||||
# if user does not select file, browser also
|
|
||||||
# submit a empty part without filename
|
|
||||||
file = request.files['file']
|
|
||||||
if file.filename == '':
|
|
||||||
j['msg'] = 'No selected file'
|
|
||||||
elif file and allowed_file(file.filename):
|
|
||||||
filename = generate_file_name(file.filename)
|
|
||||||
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
|
||||||
file.save(file_path)
|
|
||||||
j['ret'] = 0
|
|
||||||
j['msg'] = filename
|
|
||||||
else:
|
|
||||||
j['msg'] = 'File type not support'
|
|
||||||
return jsonify(j)
|
|
||||||
else:
|
|
||||||
return render_template(Constant.SERVER_PAGE_UPLOAD)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/send_msg/<to>/<msg>')
|
|
||||||
def send_msg(to, msg):
|
|
||||||
"""
|
|
||||||
@brief send message to user or gourp
|
|
||||||
@param to: String, user id or group id
|
|
||||||
@param msg: String, words
|
|
||||||
"""
|
|
||||||
return jsonify({'ret': 0 if wechat.send_text(to, msg) else 1})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/send_img/<to>/<img>')
|
|
||||||
def send_img(to, img):
|
|
||||||
"""
|
|
||||||
@brief send image to user or gourp
|
|
||||||
@param to: String, user id or group id
|
|
||||||
@param img: String, image file name
|
|
||||||
"""
|
|
||||||
img_path = os.path.join(app.config['UPLOAD_FOLDER'], img)
|
|
||||||
return jsonify({'ret': 0 if wechat.send_img(to, img_path) else 1})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/send_emot/<to>/<emot>')
|
|
||||||
def send_emot(to, emot):
|
|
||||||
"""
|
|
||||||
@brief send emotion to user or gourp
|
|
||||||
@param to: String, user id or group id
|
|
||||||
@param emot: String, emotion file name
|
|
||||||
"""
|
|
||||||
emot_path = os.path.join(app.config['UPLOAD_FOLDER'], emot)
|
|
||||||
return jsonify({'ret': 0 if wechat.send_emot(to, emot_path) else 1})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/send_file/<to>/<file>')
|
|
||||||
def send_file(to, file):
|
|
||||||
"""
|
|
||||||
@brief send file to user or gourp
|
|
||||||
@param to: String, user id or group id
|
|
||||||
@param file: String, file name
|
|
||||||
"""
|
|
||||||
file_path = os.path.join(app.config['UPLOAD_FOLDER'], file)
|
|
||||||
return jsonify({'ret': 0 if wechat.send_file(to, file_path) else 1})
|
|
||||||
|
|
||||||
|
|
||||||
def mass_send(method, data, func):
|
|
||||||
j = {'ret': -1, 'unsend_list':[]}
|
|
||||||
if method == 'POST' and data:
|
|
||||||
to_list = data['to_list']
|
|
||||||
msg = data['msg']
|
|
||||||
media_type = data.get('media_type', '')
|
|
||||||
|
|
||||||
if media_type in ['img', 'emot']:
|
|
||||||
file_path = os.path.join(app.config['UPLOAD_FOLDER'], msg)
|
|
||||||
response = wechat.webwxuploadmedia(file_path)
|
|
||||||
if response is not None:
|
|
||||||
msg = response['MediaId']
|
|
||||||
elif media_type == 'file':
|
|
||||||
file_path = os.path.join(app.config['UPLOAD_FOLDER'], msg)
|
|
||||||
data = {
|
|
||||||
'appid': Constant.API_WXAPPID,
|
|
||||||
'title': msg,
|
|
||||||
'totallen': '',
|
|
||||||
'attachid': '',
|
|
||||||
'type': wechat.wx_conf['APPMSGTYPE_ATTACH'],
|
|
||||||
'fileext': msg.split('.')[-1],
|
|
||||||
}
|
|
||||||
response = wechat.webwxuploadmedia(file_path)
|
|
||||||
if response is not None:
|
|
||||||
data['totallen'] = response['StartPos']
|
|
||||||
data['attachid'] = response['MediaId']
|
|
||||||
else:
|
|
||||||
Log.error('File upload error')
|
|
||||||
msg = data
|
|
||||||
|
|
||||||
for groups in split_array(to_list, 20):
|
|
||||||
for g in groups:
|
|
||||||
r = func(g, msg)
|
|
||||||
if not r:
|
|
||||||
j['unsend_list'].append(g)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
j['ret'] = len(j['unsend_list'])
|
|
||||||
|
|
||||||
return j
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mass_send_msg/', methods=["GET", "POST"])
|
|
||||||
def mass_send_msg():
|
|
||||||
"""
|
|
||||||
@brief send text to mass users or gourps
|
|
||||||
"""
|
|
||||||
j = mass_send(request.method, request.json, wechat.send_text)
|
|
||||||
return jsonify(j)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mass_send_img', methods=["GET", "POST"])
|
|
||||||
def mass_send_img():
|
|
||||||
"""
|
|
||||||
@brief send iamge to mass users or gourps
|
|
||||||
"""
|
|
||||||
j = mass_send(request.method, request.json, wechat.webwxsendmsgimg)
|
|
||||||
return jsonify(j)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mass_send_emot', methods=["GET", "POST"])
|
|
||||||
def mass_send_emot():
|
|
||||||
"""
|
|
||||||
@brief send emoticon to mass users or gourps
|
|
||||||
"""
|
|
||||||
j = mass_send(request.method, request.json, wechat.webwxsendemoticon)
|
|
||||||
return jsonify(j)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mass_send_file', methods=["GET", "POST"])
|
|
||||||
def mass_send_file():
|
|
||||||
"""
|
|
||||||
@brief send file to mass users or gourps
|
|
||||||
"""
|
|
||||||
j = mass_send(request.method, request.json, wechat.webwxsendappmsg)
|
|
||||||
return jsonify(j)
|
|
||||||
|
|
||||||
|
|
||||||
def run_server():
|
|
||||||
app.run(port=PORT)
|
|
||||||
|
|
||||||
if cm.get('setting', 'server_mode') == 'True':
|
|
||||||
serverProcess = threading.Thread(target=run_server)
|
|
||||||
serverProcess.start()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
wechat.start()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
echo(Constant.LOG_MSG_QUIT)
|
|
||||||
wechat.exit_code = 1
|
|
||||||
else:
|
|
||||||
Log.error(traceback.format_exc())
|
|
||||||
finally:
|
|
||||||
wechat.stop()
|
|
||||||
|
|
||||||
# send a mail to tell the wxbot is failing
|
|
||||||
subject = 'wxbot stop message'
|
|
||||||
log_file = open(eval(cm.get('handler_fileHandler', 'args'))[0], 'r')
|
|
||||||
mail_content = '<pre>' + str(wechat) + '\n\n-----\nLogs:\n-----\n\n' + ''.join(log_file.readlines()[-100:]) + '</pre>'
|
|
||||||
sg.send_mail(subject, mail_content, 'text/html')
|
|
||||||
log_file.close()
|
|
||||||
|
|
||||||
if wechat.exit_code == 0:
|
|
||||||
echo(Constant.MAIN_RESTART)
|
|
||||||
else:
|
|
||||||
# kill process
|
|
||||||
os.system(Constant.LOG_MSG_KILL_PROCESS % os.getpid())
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
from wechat_msg_processor import WeChatMsgProcessor
|
|
||||||
from bot import Bot
|
|
||||||
from sendgrid_mail import SGMail
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
#===================================================
|
|
||||||
from wechat.utils import *
|
|
||||||
from config import Constant
|
|
||||||
#---------------------------------------------------
|
|
||||||
import random, time, json
|
|
||||||
#===================================================
|
|
||||||
|
|
||||||
|
|
||||||
class Bot(object):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.emoticons = Constant.EMOTICON
|
|
||||||
self.gifs = []
|
|
||||||
self.last_time = time.time()
|
|
||||||
|
|
||||||
def time_schedule(self):
|
|
||||||
r = ''
|
|
||||||
now = time.time()
|
|
||||||
if int(now - self.last_time) > 3600:
|
|
||||||
self.last_time = now
|
|
||||||
url_latest = Constant.BOT_ZHIHU_URL_LATEST
|
|
||||||
url_daily = Constant.BOT_ZHIHU_URL_DAILY
|
|
||||||
data = get(url_latest)
|
|
||||||
j = json.loads(data)
|
|
||||||
story = j['stories'][random.randint(0, len(j['stories'])-1)]
|
|
||||||
r = story['title'] + '\n' + url_daily + str(story['id'])
|
|
||||||
return r.encode('utf-8')
|
|
||||||
|
|
||||||
def reply(self, text):
|
|
||||||
APIKEY = Constant.BOT_TULING_API_KEY
|
|
||||||
api_url = Constant.BOT_TULING_API_URL % (APIKEY, text, '12345678')
|
|
||||||
r = json.loads(get(api_url))
|
|
||||||
if r.get('code') == 100000 and r.get('text') != Constant.BOT_TULING_BOT_REPLY:
|
|
||||||
p = random.randint(1, 10)
|
|
||||||
if p > 3:
|
|
||||||
return r['text']
|
|
||||||
elif p > 1:
|
|
||||||
# send emoji
|
|
||||||
if random.randint(1, 10) > 5:
|
|
||||||
n = random.randint(0, len(self.emoticons)-1)
|
|
||||||
m = random.randint(1, 3)
|
|
||||||
reply = self.emoticons[n].encode('utf-8') * m
|
|
||||||
return reply
|
|
||||||
return ''
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
#===================================================
|
|
||||||
import sendgrid
|
|
||||||
from sendgrid.helpers.mail import *
|
|
||||||
#===================================================
|
|
||||||
|
|
||||||
class SGMail(object):
|
|
||||||
|
|
||||||
def __init__(self, apikey, from_email, to_email):
|
|
||||||
self.sg = sendgrid.SendGridAPIClient(apikey=apikey)
|
|
||||||
self.from_email = Email(from_email)
|
|
||||||
self.to_email = Email(to_email)
|
|
||||||
|
|
||||||
def send_mail(self, subject, plain_content, type='text/plain'):
|
|
||||||
content = Content(type, plain_content)
|
|
||||||
mail = Mail(self.from_email, subject, self.to_email, content)
|
|
||||||
response = self.sg.client.mail.send.post(request_body=mail.get())
|
|
||||||
return response.status_code == 202
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
#===================================================
|
|
||||||
from wechat.utils import *
|
|
||||||
from config import ConfigManager
|
|
||||||
from config import Constant
|
|
||||||
from config import Log
|
|
||||||
#---------------------------------------------------
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
#===================================================
|
|
||||||
|
|
||||||
|
|
||||||
class WeChatMsgProcessor(object):
|
|
||||||
"""
|
|
||||||
Process fetched data
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, db):
|
|
||||||
self.db = db
|
|
||||||
self.wechat = None # recieve `WeChat` class instance
|
|
||||||
# for call some wechat apis
|
|
||||||
|
|
||||||
# read config
|
|
||||||
cm = ConfigManager()
|
|
||||||
[self.upload_dir, self.data_dir, self.log_dir] = cm.setup_database()
|
|
||||||
|
|
||||||
def clean_db(self):
|
|
||||||
"""
|
|
||||||
@brief clean database, delete table & create table
|
|
||||||
"""
|
|
||||||
self.db.delete_table(Constant.TABLE_GROUP_LIST())
|
|
||||||
self.db.delete_table(Constant.TABLE_GROUP_USER_LIST())
|
|
||||||
self.db.create_table(Constant.TABLE_GROUP_MSG_LOG, Constant.TABLE_GROUP_MSG_LOG_COL)
|
|
||||||
self.db.create_table(Constant.TABLE_GROUP_LIST(), Constant.TABLE_GROUP_LIST_COL)
|
|
||||||
self.db.create_table(Constant.TABLE_GROUP_USER_LIST(), Constant.TABLE_GROUP_USER_LIST_COL)
|
|
||||||
self.db.create_table(Constant.TABLE_RECORD_ENTER_GROUP, Constant.TABLE_RECORD_ENTER_GROUP_COL)
|
|
||||||
self.db.create_table(Constant.TABLE_RECORD_RENAME_GROUP, Constant.TABLE_RECORD_RENAME_GROUP_COL)
|
|
||||||
|
|
||||||
def handle_wxsync(self, msg):
|
|
||||||
"""
|
|
||||||
@brief Recieve webwxsync message, saved into json
|
|
||||||
@param msg Dict: webwxsync msg
|
|
||||||
"""
|
|
||||||
fn = time.strftime(Constant.LOG_MSG_FILE, time.localtime())
|
|
||||||
save_json(fn, msg, self.log_dir, 'a+')
|
|
||||||
|
|
||||||
def handle_group_list(self, group_list):
|
|
||||||
"""
|
|
||||||
@brief handle group list & saved in DB
|
|
||||||
@param group_list Array
|
|
||||||
"""
|
|
||||||
fn = Constant.LOG_MSG_GROUP_LIST_FILE
|
|
||||||
save_json(fn, group_list, self.data_dir)
|
|
||||||
cols = [(
|
|
||||||
g['NickName'],
|
|
||||||
g['UserName'],
|
|
||||||
g['OwnerUin'],
|
|
||||||
g['MemberCount'],
|
|
||||||
g['HeadImgUrl']
|
|
||||||
) for g in group_list]
|
|
||||||
self.db.insertmany(Constant.TABLE_GROUP_LIST(), cols)
|
|
||||||
|
|
||||||
def handle_group_member_list(self, group_id, member_list):
|
|
||||||
"""
|
|
||||||
@brief handle group member list & saved in DB
|
|
||||||
@param member_list Array
|
|
||||||
"""
|
|
||||||
fn = group_id + '.json'
|
|
||||||
save_json(fn, member_list, self.data_dir)
|
|
||||||
cols = [(
|
|
||||||
group_id,
|
|
||||||
m['UserName'],
|
|
||||||
m['NickName'],
|
|
||||||
m['DisplayName'],
|
|
||||||
m['AttrStatus']
|
|
||||||
) for m in member_list]
|
|
||||||
self.db.insertmany(Constant.TABLE_GROUP_USER_LIST(), cols)
|
|
||||||
|
|
||||||
def handle_group_list_change(self, new_group):
|
|
||||||
"""
|
|
||||||
@brief handle adding a new group & saved in DB
|
|
||||||
@param new_group Dict
|
|
||||||
"""
|
|
||||||
self.handle_group_list([new_group])
|
|
||||||
|
|
||||||
def handle_group_member_change(self, group_id, member_list):
|
|
||||||
"""
|
|
||||||
@brief handle group member changes & saved in DB
|
|
||||||
@param group_id Dict
|
|
||||||
@param member_list Dict
|
|
||||||
"""
|
|
||||||
self.db.delete(Constant.TABLE_GROUP_USER_LIST(), "RoomID", group_id)
|
|
||||||
self.handle_group_member_list(group_id, member_list)
|
|
||||||
|
|
||||||
def handle_group_msg(self, msg):
|
|
||||||
"""
|
|
||||||
@brief Recieve group messages
|
|
||||||
@param msg Dict: packaged msg
|
|
||||||
"""
|
|
||||||
# rename media files
|
|
||||||
for k in ['image', 'video', 'voice']:
|
|
||||||
if msg[k]:
|
|
||||||
t = time.localtime(float(msg['timestamp']))
|
|
||||||
time_str = time.strftime("%Y%m%d%H%M%S", t)
|
|
||||||
# format: 时间_消息ID_群名
|
|
||||||
file_name = '/%s_%s_%s.' % (time_str, msg['msg_id'], msg['group_name'])
|
|
||||||
new_name = re.sub(r'\/\w+\_\d+\.', file_name, msg[k])
|
|
||||||
Log.debug('rename file to %s' % new_name)
|
|
||||||
os.rename(msg[k], new_name)
|
|
||||||
msg[k] = new_name
|
|
||||||
|
|
||||||
if msg['msg_type'] == 10000:
|
|
||||||
# record member enter in group
|
|
||||||
m = re.search(r'邀请(.+)加入了群聊', msg['sys_notif'])
|
|
||||||
if m:
|
|
||||||
name = m.group(1)
|
|
||||||
col_enter_group = (
|
|
||||||
msg['msg_id'],
|
|
||||||
msg['group_name'],
|
|
||||||
msg['from_user_name'],
|
|
||||||
msg['to_user_name'],
|
|
||||||
name,
|
|
||||||
msg['time'],
|
|
||||||
)
|
|
||||||
self.db.insert(Constant.TABLE_RECORD_ENTER_GROUP, col_enter_group)
|
|
||||||
|
|
||||||
# record rename group
|
|
||||||
n = re.search(r'(.+)修改群名为“(.+)”', msg['sys_notif'])
|
|
||||||
if n:
|
|
||||||
people = n.group(1)
|
|
||||||
to_name = n.group(2)
|
|
||||||
col_rename_group = (
|
|
||||||
msg['msg_id'],
|
|
||||||
msg['group_name'],
|
|
||||||
to_name,
|
|
||||||
people,
|
|
||||||
msg['time'],
|
|
||||||
)
|
|
||||||
self.db.insert(Constant.TABLE_RECORD_RENAME_GROUP, col_rename_group)
|
|
||||||
|
|
||||||
# upadte group in GroupList
|
|
||||||
for g in self.wechat.GroupList:
|
|
||||||
if g['UserName'] == msg['from_user_name']:
|
|
||||||
g['NickName'] = to_name
|
|
||||||
break
|
|
||||||
|
|
||||||
# normal group message
|
|
||||||
col = (
|
|
||||||
msg['msg_id'],
|
|
||||||
msg['group_owner_uin'],
|
|
||||||
msg['group_name'],
|
|
||||||
msg['group_count'],
|
|
||||||
msg['from_user_name'],
|
|
||||||
msg['to_user_name'],
|
|
||||||
msg['user_attrstatus'],
|
|
||||||
msg['user_display_name'],
|
|
||||||
msg['user_nickname'],
|
|
||||||
msg['msg_type'],
|
|
||||||
msg['emoticon'],
|
|
||||||
msg['text'],
|
|
||||||
msg['image'],
|
|
||||||
msg['video'],
|
|
||||||
msg['voice'],
|
|
||||||
msg['link'],
|
|
||||||
msg['namecard'],
|
|
||||||
msg['location'],
|
|
||||||
msg['recall_msg_id'],
|
|
||||||
msg['sys_notif'],
|
|
||||||
msg['time'],
|
|
||||||
msg['timestamp']
|
|
||||||
)
|
|
||||||
self.db.insert(Constant.TABLE_GROUP_MSG_LOG, col)
|
|
||||||
|
|
||||||
text = msg['text']
|
|
||||||
if text and text[0] == '@':
|
|
||||||
n = trans_coding(text).find(u'\u2005')
|
|
||||||
name = trans_coding(text)[1:n].encode('utf-8')
|
|
||||||
if name in [self.wechat.User['NickName'], self.wechat.User['RemarkName']]:
|
|
||||||
self.handle_command(trans_coding(text)[n+1:].encode('utf-8'), msg)
|
|
||||||
|
|
||||||
def handle_user_msg(self, msg):
|
|
||||||
"""
|
|
||||||
@brief Recieve personal messages
|
|
||||||
@param msg Dict
|
|
||||||
"""
|
|
||||||
wechat = self.wechat
|
|
||||||
|
|
||||||
text = trans_coding(msg['text']).encode('utf-8')
|
|
||||||
uid = msg['raw_msg']['FromUserName']
|
|
||||||
|
|
||||||
if text == 'test_revoke': # 撤回消息测试
|
|
||||||
dic = wechat.webwxsendmsg('这条消息将被撤回', uid)
|
|
||||||
wechat.revoke_msg(dic['MsgID'], uid, dic['LocalID'])
|
|
||||||
elif text == 'reply':
|
|
||||||
wechat.send_text(uid, '自动回复')
|
|
||||||
|
|
||||||
|
|
||||||
def handle_command(self, cmd, msg):
|
|
||||||
"""
|
|
||||||
@brief handle msg of `@yourself cmd`
|
|
||||||
@param cmd String
|
|
||||||
@param msg Dict
|
|
||||||
"""
|
|
||||||
wechat = self.wechat
|
|
||||||
g_id = ''
|
|
||||||
for g in wechat.GroupList:
|
|
||||||
if g['NickName'] == msg['group_name']:
|
|
||||||
g_id = g['UserName']
|
|
||||||
|
|
||||||
cmd = cmd.strip()
|
|
||||||
if cmd == 'runtime':
|
|
||||||
wechat.send_text(g_id, wechat.get_run_time())
|
|
||||||
elif cmd == 'test_sendimg':
|
|
||||||
wechat.send_img(g_id, 'test/emotion/7.gif')
|
|
||||||
elif cmd == 'test_sendfile':
|
|
||||||
wechat.send_file(g_id, 'test/Data/upload/shake.wav')
|
|
||||||
elif cmd == 'test_bot':
|
|
||||||
# reply bot
|
|
||||||
# ---------
|
|
||||||
if wechat.bot:
|
|
||||||
r = wechat.bot.reply(cmd)
|
|
||||||
if r:
|
|
||||||
wechat.send_text(g_id, r)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
elif cmd == 'test_emot':
|
|
||||||
img_name = [
|
|
||||||
'0.jpg', '1.jpeg', '2.gif', '3.jpg', '4.jpeg',
|
|
||||||
'5.gif', '6.gif', '7.gif', '8.jpg', '9.jpg'
|
|
||||||
]
|
|
||||||
name = img_name[int(time.time()) % 10]
|
|
||||||
emot_path = os.path.join('test/emotion/', name)
|
|
||||||
wechat.send_emot(g_id, emot_path)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def check_schedule_task(self):
|
|
||||||
# update group member list at 00:00 am every morning
|
|
||||||
t = time.localtime()
|
|
||||||
if t.tm_hour == 0 and t.tm_min <= 1:
|
|
||||||
# update group member
|
|
||||||
Log.debug('update group member list everyday')
|
|
||||||
self.db.delete_table(Constant.TABLE_GROUP_LIST())
|
|
||||||
self.db.delete_table(Constant.TABLE_GROUP_USER_LIST())
|
|
||||||
self.db.create_table(Constant.TABLE_GROUP_LIST(), Constant.TABLE_GROUP_LIST_COL)
|
|
||||||
self.db.create_table(Constant.TABLE_GROUP_USER_LIST(), Constant.TABLE_GROUP_USER_LIST_COL)
|
|
||||||
self.wechat.fetch_group_contacts()
|
|
||||||
|
|
||||||