将消息输出到sqlite数据库

This commit is contained in:
2021-05-04 14:48:41 +08:00
parent d9edcd2c92
commit 736fc74849
36 changed files with 603 additions and 4607 deletions
+51
View File
@@ -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才能打开。程序只负责记录数据,没有读取能力。
### 其它
关于表情包,如果是从表情商店里添加的表情包网页版微信是获取不到的,为了保护创作者的权益。这倒是可以理解。
如果发现无法登录先试试在浏览器登录网页版微信,有可能是腾讯为了“安全”限制了你的微信帐号在网页登录。
最后,祝腾讯早点倒闭。
-799
View File
@@ -1,799 +0,0 @@
# WeixinBot [![star this repo](http://github-svg-buttons.herokuapp.com/star.svg?user=Urinx&repo=WeixinBot&style=flat&background=1081C1)](http://github.com/Urinx/WeixinBot) [![fork this repo](http://github-svg-buttons.herokuapp.com/fork.svg?user=Urinx&repo=WeixinBot&style=flat&background=1081C1)](http://github.com/Urinx/WeixinBot/fork) ![python](https://img.shields.io/badge/python-2.7%20&%203.6-ff69b4.svg)
网页版微信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>
按照操作指示在手机微信上扫描二维码然后登录,你可以选择是否开启自动回复模式。
![2](imgs/2.png)
开启自动回复模式后,如果接收到的是文字消息就会自动回复,包括群消息。
![3](imgs/3.png)
名片,链接,动画表情和地址位置消息。
![4](imgs/4.png)
![5](imgs/5.png)
网页版上有的功能目前基本上都能支持。
## <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> &nbsp;&nbsp;&nbsp;&nbsp; BaseRequest: { <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Uin: xxx, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Sid: xxx, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Skey: xxx, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; DeviceID: xxx, <br> &nbsp;&nbsp;&nbsp;&nbsp; } <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> &nbsp;&nbsp;&nbsp;&nbsp; BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> &nbsp;&nbsp;&nbsp;&nbsp; Code: 3, <br> &nbsp;&nbsp;&nbsp;&nbsp; FromUserName: `自己ID`, <br> &nbsp;&nbsp;&nbsp;&nbsp; ToUserName: `自己ID`, <br> &nbsp;&nbsp;&nbsp;&nbsp; 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> &nbsp;&nbsp;&nbsp;&nbsp; BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> &nbsp;&nbsp;&nbsp;&nbsp; Count: `群数量`, <br> &nbsp;&nbsp;&nbsp;&nbsp; List: [ <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { UserName: `群ID`, EncryChatRoomId: "" }, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ... <br> &nbsp;&nbsp;&nbsp;&nbsp; ], <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> &nbsp;&nbsp;&nbsp;&nbsp; BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> &nbsp;&nbsp;&nbsp;&nbsp; SyncKey: xxx, <br> &nbsp;&nbsp;&nbsp;&nbsp; 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> &nbsp;&nbsp;&nbsp;&nbsp; BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> &nbsp;&nbsp;&nbsp;&nbsp; Msg: { <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type: 1 `文字消息`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Content: `要发送的消息`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FromUserName: `自己ID`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ToUserName: `好友ID`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; LocalID: `与clientMsgId相同`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ClientMsgId: `时间戳左移4位随后补上4位随机数` <br> &nbsp;&nbsp;&nbsp;&nbsp; } <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> &nbsp;&nbsp;&nbsp;&nbsp; BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> &nbsp;&nbsp;&nbsp;&nbsp; SvrMsgId: msg_id, <br> &nbsp;&nbsp;&nbsp;&nbsp; ToUserName: user_id, <br> &nbsp;&nbsp;&nbsp;&nbsp; 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> &nbsp;&nbsp;&nbsp;&nbsp; BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx }, <br> &nbsp;&nbsp;&nbsp;&nbsp; Msg: { <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type: 47 `emoji消息`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; EmojiFlag: 2, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; MediaId: `表情上传后的媒体ID`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; FromUserName: `自己ID`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ToUserName: `好友ID`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; LocalID: `与clientMsgId相同`, <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ClientMsgId: `时间戳左移4位随后补上4位随机数` <br> &nbsp;&nbsp;&nbsp;&nbsp; } <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> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 一般个人公众号/服务号:8 <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 一般企业的服务号:24 <br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 微信官方账号`微信团队`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&amp;guid=ffffffffc104ea2964a111cf3ff3edaf&amp;fromtag=46
</dataurl>
<lowdataurl>
http://ws.stream.qqmusic.qq.com/C100003i9hMt1bgui0.m4a?vkey=6867EF99F3684&amp;guid=ffffffffc104ea2964a111cf3ff3edaf&amp;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
目前网页版微信已经可以脱离扫码,但是依然需要在客户端进行确认登录。
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

+180
View File
@@ -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('&lt;', '<').replace('&gt;', '>')
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
pyqrcode
certifi
argparse
sqlcipher3-binary
+2 -16
View File
@@ -23,7 +23,6 @@ from collections import defaultdict
from urllib.parse import urlparse
from lxml import html
from socket import timeout as timeout_error
#import pdb
# for media upload
import mimetypes
@@ -616,7 +615,7 @@ class WebWeixin(object):
def webwxgetmsgimg(self, msgid):
url = self.base_uri + \
'/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey)
data = self._get(url)
data = self._get(url, 'webwxgetmsgimg')
if data == '':
return ''
fn = 'img_' + msgid + '.jpg'
@@ -1104,7 +1103,7 @@ class WebWeixin(object):
request.add_header('Range', 'bytes=0-')
try:
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()
else:
data = response.read().decode('utf-8')
@@ -1204,16 +1203,3 @@ class UnicodeStreamFilter:
def flush(self):
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()
+367
View File
@@ -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": ""
}
}
-35
View File
@@ -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
```
-6
View File
@@ -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
-215
View File
@@ -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]',' [BahL]','[BahR]',
'[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'麻烦说的清楚一点,我听不懂你在说什么'
-23
View File
@@ -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 =
-5
View File
@@ -1,5 +0,0 @@
#!/usr/bin/env python
# coding: utf-8
from sqlite_db import SqliteDB
from mysql_db import MysqlDB
-200
View File
@@ -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()
-189
View File
@@ -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()
-18
View File
@@ -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 [""]
-49
View File
@@ -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/&lt;g_id&gt;
-----------------------------
@brief list group member
@method get
@param g_id String
-----------------------------
- /group_chat_log/&lt;g_name&gt;
-------------------------------
@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/&lt;to&gt;/&lt;msg&gt;
-------------------------------------------
@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/&lt;to&gt;/&lt;img&gt;
-------------------------------------------
@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/&lt;to&gt;/&lt;emot&gt;
-------------------------------------------
@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/&lt;to&gt;/&lt;file&gt;
-------------------------------------------
@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>
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env python
# coding: utf-8
from wechat import WeChat
-365
View File
@@ -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]
-698
View File
@@ -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('&lt;', '<').replace('&gt;', '>')
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(
'&lt;', '<').replace('&gt;', '>')
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('&lt;', '<').replace('&gt;', '>')
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'])
))
-973
View File
@@ -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
-281
View File
@@ -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
-47
View File
@@ -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()