diff --git a/.gitignore b/.gitignore index 7dea058..0a652e3 100644 --- a/.gitignore +++ b/.gitignore @@ -94,4 +94,8 @@ ENV/ .idea *.pyc -saved/qrcodes/qrcode.jpg \ No newline at end of file +wxbot_demo_py3/saved/ +wxbot_project_py2.7/tmp_data/ +wxbot_project_py2.7/wechat/wechat_js_backup/ +wxbot_project_py2.7/config/wechat.conf +wxbot_project_py2.7/wiki/ diff --git a/README.md b/README.md index c5b7c82..d62c138 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,42 @@ -# 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-3-ff69b4.svg) +# 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,包含终端版微信及微信机器人 -## Demo -为了运行 `weixin.py` 示例脚本,你需要有安装 `qrcode` 包,你可以通过 `pip install qrcode` 来安装。 +## Contents +* [Demo](#Demo) +* [Web Weixin Pipeline](#Web-Weixin-Pipeline) +* [Web Weixin API](#Web-Weixin-API) +* [Discussion Group](#Discussion-Group) +* [Recent Update](#Recent-Update) -![1](screenshot/1.png) +## Demo +为了确保能正常运行示例脚本,请安装所需的第三方包。 + +``` +pip install -r requirements.txt +``` + +注:下面演示的图片与功能可能不是最新的,具体请看源码。 + +
+ +
按照操作指示在手机微信上扫描二维码然后登录,你可以选择是否开启自动回复模式。 -![2](screenshot/2.png) +![2](imgs/2.png) 开启自动回复模式后,如果接收到的是文字消息就会自动回复,包括群消息。 -![3](screenshot/3.png) +![3](imgs/3.png) -现在,名片,链接,动画表情和地址位置消息都可以正常接收。 +名片,链接,动画表情和地址位置消息。 -![4](screenshot/4.png) +![4](imgs/4.png) -![5](screenshot/5.png) +![5](imgs/5.png) -**目前支持的命令**: - -`->[昵称或ID]:[内容]` 给好友发送消息 - -`m->[昵称或ID]:[文件路径]` 给好友发送文件中的内容 - -![6](screenshot/6.png) - -`f->[昵称或ID]:[文件路径]` 给好友发送文件 - -`i->[昵称或ID]:[图片路径]` 给好友发送图片 - -`e->[昵称或ID]:[文件路径]` 给好友发送表情(jpg/gif) - -`quit` 退出程序 - -![7](screenshot/7.png) - -注意,以上命令均不包含方括号。 +网页版上有的功能目前基本上都能支持。 ## Web Weixin Pipeline @@ -69,14 +66,7 @@ |QRCode Scaned| +-------------+ ``` -## Discussion Group -如果你希望和 WeixinBot 的其他开发者交流,或者有什么问题和建议,欢迎大家加入微信群【Youth fed the dog】一起讨论。扫描下面的二维码添加机器人为好友,并回复【Aidog】获取入群链接。 -
-join us -
- -注:这个不是群的二维码,是机器人拉你入群,记得回复机器人【Aidog】哦~ (secret code: Aidog) ## Web Weixin API @@ -95,12 +85,29 @@ window.QRLogin.code = 200; window.QRLogin.uuid = "xxx" ``` > 注:这里的appid就是在微信开放平台注册的应用的AppID。网页版微信有两个AppID,早期的是`wx782c26e4c19acffb`,在微信客户端上显示为应用名称为`Web微信`;现在用的是`wxeb7ec651dd0aefa9`,显示名称为`微信网页版`。 -![6](screenshot/8.jpg) +
+ +
+ +
+| 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'} + +通过这种方式可以省掉扫二维码这步操作,更加方便 +```
| API | 生成二维码 | | --- | --------- | | url | https://login.weixin.qq.com/l/ `uuid` | +| method | GET |
| API | 二维码扫描登录 | @@ -299,7 +306,7 @@ window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?tic | API | synccheck | | --- | --------- | | protocol | https | -| host | webpush.weixin.qq.com
webpush2.weixin.qq.com
webpush.wechat.com
webpush1.wechat.com
webpush2.wechat.com
webpush.wechatapp.com
webpush1.wechatapp.com | +| host | webpush.weixin.qq.com
webpush.wx2.qq.com
webpush.wx8.qq.com
webpush.wx.qq.com
webpush.web2.wechat.com
webpush.web.wechat.com | | path | /cgi-bin/mmwebwx-bin/synccheck | | method | GET | | data | URL Encode | @@ -396,6 +403,24 @@ selector: } ``` +| API | webwxrevokemsg | +| --- | ------------ | +| url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxrevokemsg | +| method | POST | +| data | JSON | +| header | ContentType: application/json; charset=UTF-8 | +| params | {
     BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx },
     SvrMsgId: msg_id,
     ToUserName: user_id,
     ClientMsgId: local_msg_id
} | + +返回数据(JSON): +``` +{ + "BaseResponse": { + "Ret": 0, + "ErrMsg": "" + } +} +``` + #### 发送表情 | API | webwxsendmsgemotion | @@ -497,10 +522,10 @@ selector: | 1 | 文本消息 | | 3 | 图片消息 | | 34 | 语音消息 | -| 37 | VERIFYMSG | +| 37 | 好友确认消息 | | 40 | POSSIBLEFRIEND_MSG | | 42 | 共享名片 | -| 43 | 视频通话消息 | +| 43 | 视频消息 | | 47 | 动画表情 | | 48 | 位置消息 | | 49 | 分享链接 | @@ -757,13 +782,17 @@ Content: "收到红包,请在手机上查看" ``` -持续更新中 ... -## Todo -- [x] 发送图片或者文件功能 -- [ ] 主动给群聊发送消息 -- [ ] 建立群聊 -- [x] 群发消息 -- [ ] 补充更多的接口及完善文档 +## Discussion Group +如果你希望和 WeixinBot 的其他开发者交流,或者有什么问题和建议,欢迎大家加入微信群【Youth fed the dog】一起讨论。扫描下面的二维码添加机器人为好友,并回复【Aidog】获取入群链接。 -P.S. 还有啥要补充的也可以在[issue #8](https://github.com/Urinx/WeixinBot/issues/8)下留言 +
+join us +
+ +注:这个不是群的二维码,是机器人拉你入群,记得回复机器人【Aidog】哦~ (secret code: Aidog) + +## Recent Update + +- association_login + 目前网页版微信已经可以脱离扫码,但是依然需要在客户端进行确认登录。 diff --git a/genReq.sh b/genReq.sh deleted file mode 100644 index 051bf6b..0000000 --- a/genReq.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -pip freeze |grep -v wheel | gawk -F"==" ' { print $1 } ' > requirements.txt diff --git a/screenshot/1.png b/imgs/1.png similarity index 100% rename from screenshot/1.png rename to imgs/1.png diff --git a/screenshot/2.png b/imgs/2.png similarity index 100% rename from screenshot/2.png rename to imgs/2.png diff --git a/screenshot/3.png b/imgs/3.png similarity index 100% rename from screenshot/3.png rename to imgs/3.png diff --git a/screenshot/4.png b/imgs/4.png similarity index 100% rename from screenshot/4.png rename to imgs/4.png diff --git a/screenshot/5.png b/imgs/5.png similarity index 100% rename from screenshot/5.png rename to imgs/5.png diff --git a/imgs/8.jpg b/imgs/8.jpg new file mode 100644 index 0000000..921a816 Binary files /dev/null and b/imgs/8.jpg differ diff --git a/saved/qrcodes/groupQrcode.jpg b/imgs/groupQrcode.jpg similarity index 100% rename from saved/qrcodes/groupQrcode.jpg rename to imgs/groupQrcode.jpg diff --git a/saved/qrcodes/qrcode.jpg b/saved/qrcodes/qrcode.jpg deleted file mode 100644 index 80262a3..0000000 Binary files a/saved/qrcodes/qrcode.jpg and /dev/null differ diff --git a/screenshot/6.png b/screenshot/6.png deleted file mode 100644 index 7703178..0000000 Binary files a/screenshot/6.png and /dev/null differ diff --git a/screenshot/7.png b/screenshot/7.png deleted file mode 100644 index d8dce41..0000000 Binary files a/screenshot/7.png and /dev/null differ diff --git a/screenshot/8.jpg b/screenshot/8.jpg deleted file mode 100644 index 0c9329d..0000000 Binary files a/screenshot/8.jpg and /dev/null differ diff --git a/requirements.txt b/wxbot_demo_py3/requirements.txt similarity index 100% rename from requirements.txt rename to wxbot_demo_py3/requirements.txt diff --git a/weixin.py b/wxbot_demo_py3/weixin.py similarity index 100% rename from weixin.py rename to wxbot_demo_py3/weixin.py diff --git a/wxbot_project_py2.7/README.md b/wxbot_project_py2.7/README.md new file mode 100644 index 0000000..b6ae52c --- /dev/null +++ b/wxbot_project_py2.7/README.md @@ -0,0 +1,37 @@ +# 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 +├── server +│   ├── index.html +│   └── upload.html +├── tmp_data +├── wechat +│   ├── __init__.py +│   ├── utils.py +│   ├── wechat.py +│   ├── wechat_apis.py +│   └── wechat_js_backup +│   └── index_40649b7.js +├── weixin_bot.py +└── wx_handler + ├── __init__.py + ├── bot.py + └── wechat_msg_processor.py +``` diff --git a/wxbot_project_py2.7/config/__init__.py b/wxbot_project_py2.7/config/__init__.py new file mode 100644 index 0000000..8cd6be0 --- /dev/null +++ b/wxbot_project_py2.7/config/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +# coding: utf-8 + +from config_manager import ConfigManager +from constant import Constant +from log import Log \ No newline at end of file diff --git a/wxbot_project_py2.7/config/config_manager.py b/wxbot_project_py2.7/config/config_manager.py new file mode 100644 index 0000000..92a6fe8 --- /dev/null +++ b/wxbot_project_py2.7/config/config_manager.py @@ -0,0 +1,105 @@ +#!/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 diff --git a/wxbot_project_py2.7/config/constant.py b/wxbot_project_py2.7/config/constant.py new file mode 100644 index 0000000..f3aa544 --- /dev/null +++ b/wxbot_project_py2.7/config/constant.py @@ -0,0 +1,215 @@ +#!/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'' + + 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 = 'Server/upload.html' + SERVER_PAGE_INDEX = 'Server/index.html' + + RUN_RESULT_SUCCESS = '成功 %ds\n' + RUN_RESULT_FAIL = '失败\n[*] 退出程序\n' + MAIN_RESTART = '[*] wait for restart' + LOG_MSG_FILE = 'WeChat-Msgs-%Y-%m-%d.json' + LOG_MSG_GROUP_LIST_FILE = 'group_list.json' + LOG_MSG_QUIT = '\n[*] Force quit.\n' + LOG_MSG_FAIL = '失败\n' + LOG_MSG_SUCCESS = '成功\n' + LOG_MSG_START = '[*] 微信网页版 ... 开动\n' + LOG_MSG_RECOVER = '[*] 从配置文件中恢复 ... ' + LOG_MSG_RECOVER_CONTACT = '[*] 从文件中恢复联系人数据 ... ' + LOG_MSG_TRY_INIT = '[*] 尝试初始化 ... ' + LOG_MSG_ASSOCIATION_LOGIN = '[*] 通过关联登录 ... ' + LOG_MSG_GET_UUID = '[*] 正在获取 uuid ... ' + LOG_MSG_GET_QRCODE = '[*] 正在获取二维码 ... 成功\n' + LOG_MSG_SCAN_QRCODE = '[*] 请使用微信扫描二维码以登录 ... \n' + LOG_MSG_CONFIRM_LOGIN = '[*] 请在手机上点击确认以登录 ... \n' + LOG_MSG_WAIT_LOGIN_ERR1 = '[登陆超时] \n' + LOG_MSG_WAIT_LOGIN_ERR2 = '[登陆异常] \n' + LOG_MSG_LOGIN = '[*] 正在登录 ... ' + LOG_MSG_INIT = '[*] 微信初始化 ... ' + LOG_MSG_STATUS_NOTIFY = '[*] 开启状态通知 ... ' + LOG_MSG_GET_CONTACT = '[*] 获取联系人 ... ' + LOG_MSG_CONTACT_COUNT = '[*] 应有 %s 个联系人,读取到联系人 %d 个\n' + LOG_MSG_OTHER_CONTACT_COUNT = '[*] 共有 %d 个群 | %d 个直接联系人 | %d 个特殊账号 | %d 公众号或服务号\n' + LOG_MSG_GET_GROUP_MEMBER = '[*] 拉取群聊成员 ... ' + LOG_MSG_SNAPSHOT = '[*] 保存配置 ... ' + LOG_MSG_LOGOUT = '[*] 你在手机上登出了微信\n' + LOG_MSG_LOGIN_OTHERWHERE = '[*] 你在其他地方登录了 WEB 版微信\n' + LOG_MSG_QUIT_ON_PHONE = '[*] 你在手机上主动退出了\n' + LOG_MSG_RUNTIME = '[*] Total run: %s\n' + LOG_MSG_KILL_PROCESS = 'kill %d' + LOG_MSG_NEW_MSG = '>>> %d 条新消息\n' + LOG_MSG_LOCATION = '[位置] %s' + LOG_MSG_PICTURE = '[图片] %s' + LOG_MSG_VOICE = '[语音] %s' + LOG_MSG_RECALL = '撤回了一条消息' + LOG_MSG_ADD_FRIEND = '%s 请求添加你为好友' + LOG_MSG_UNKNOWN_MSG = '[*] 该消息类型为: %d,内容: %s' + LOG_MSG_VIDEO = '[小视频] %s' + LOG_MSG_NOTIFY_PHONE = '[*] 提示手机网页版微信登录状态\n' + LOG_MSG_EMOTION = '[表情] %s' + LOG_MSG_NAME_CARD = ( + '[名片]\n' + '=========================\n' + '= 昵称: %s\n' + '= 微信号: %s\n' + '= 地区: %s %s\n' + '= 性别: %s\n' + '=========================' + ) + LOG_MSG_SEX_OPTION = ['未知', '男', '女'] + LOG_MSG_APP_LINK = ( + '[%s]\n' + '=========================\n' + '= 标题: %s\n' + '= 描述: %s\n' + '= 链接: %s\n' + '= 来自: %s\n' + '=========================' + ) + LOG_MSG_APP_LINK_TYPE = {5: '链接', 3: '音乐', 7: '微博'} + LOG_MSG_APP_IMG = ( + '[图片]\n' + '=========================\n' + '= 文件: %s\n' + '= 来自: %s\n' + '=========================' + ) + LOG_MSG_SYSTEM = '系统消息' + LOG_MSG_UNKNOWN_NAME = '未知_' + LOG_MSG_UNKNOWN_GROUP_NAME = '未知群_' + + TABLE_GROUP_MSG_LOG = 'WeChatRoomMessage' + TABLE_GROUP_MSG_LOG_COL = """ + MsgID text, + RoomOwnerID text, + RoomName text, + UserCount text, + FromUserName text, + ToUserName text, + AttrStatus text, + DisplayName text, + Name text, + MsgType text, + FaceMsg text, + TextMsg text, + ImageMsg text, + VideoMsg text, + SoundMsg text, + LinkMsg text, + NameCardMsg text, + LocationMsg text, + RecallMsgID text, + SysMsg text, + MsgTime text, + MsgTimestamp text + """ + + @staticmethod + def TABLE_GROUP_LIST(): + return 'WeChatRoom_' + time.strftime('%Y%m%d', time.localtime()) + + TABLE_GROUP_LIST_COL = """ + RoomName text, + RoomID text, + RoomOwnerID text, + UserCount text, + RoomIcon text + """ + + @staticmethod + def TABLE_GROUP_USER_LIST(): + return 'WeChatRoomMember_' + time.strftime('%Y%m%d', time.localtime()) + + TABLE_GROUP_USER_LIST_COL = """ + RoomID text, + MemberID text, + MemberNickName text, + MemberDisplayName text, + MemberAttrStatus text + """ + TABLE_RECORD_ENTER_GROUP = 'WeChatEnterGroupRecord' + TABLE_RECORD_ENTER_GROUP_COL = """ + MsgID text, + RoomName text, + FromUserName text, + ToUserName text, + Name text, + EnterTime text + """ + TABLE_RECORD_RENAME_GROUP = 'WeChatRenameGroupRecord' + TABLE_RECORD_RENAME_GROUP_COL = """ + MsgID text, + FromName text, + ToName text, + ModifyPeople text, + ModifyTime text + """ + + API_APPID = 'wx782c26e4c19acffb' + API_WXAPPID = 'wx299208e619de7026' # Weibo + # 'wxeb7ec651dd0aefa9' # Weixin + API_LANG = 'zh_CN' + API_USER_AGENT = ( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/48.0.2564.109 Safari/537.36' + ) + API_SPECIAL_USER = [ + 'newsapp', 'filehelper', 'weibo', 'qqmail', + 'fmessage', 'tmessage', 'qmessage', 'qqsync', + 'floatbottle', 'lbsapp', 'shakeapp', 'medianote', + 'qqfriend', 'readerapp', 'blogapp', 'facebookapp', + 'masssendapp', 'meishiapp', 'feedsapp', 'voip', + 'blogappweixin', 'brandsessionholder', 'weixin', + 'weixinreminder', 'officialaccounts', 'wxitil', + 'notification_messages', 'wxid_novlwrv3lqwv11', + 'gh_22b87fa7cb3c', 'userexperience_alarm', + ] + + EMOTICON = [ + '[Smile]', '[Grimace]', '[Drool]', '[Scowl]', '[CoolGuy]', '[Sob]', '[Shy]', + '[Silent]', '[Sleep]', '[Cry]', '[Awkward]', '[Angry]', '[Tongue]', '[Grin]', + '[Surprise]', '[Frown]', '[Ruthless]', '[Blush]', '[Scream]', '[Puke]', + '[Chuckle]', '[Joyful]', '[Slight]', '[Smug]', '[Hungry]', '[Drowsy]', '[Panic]', + '[Sweat]', '[Laugh]', '[Commando]', '[Determined]', '[Scold]', '[Shocked]', '[Shhh]', + '[Dizzy]', '[Tormented]', '[Toasted]', '[Skull]', '[Hammer]', '[Wave]', + '[Relief]', '[DigNose]', '[Clap]', '[Shame]', '[Trick]',' [Bah!L]','[Bah!R]', + '[Yawn]', '[Lookdown]', '[Wronged]', '[Puling]', '[Sly]', '[Kiss]', '[Uh-oh]', + '[Whimper]', '[Cleaver]', '[Melon]', '[Beer]', '[Basketball]', '[PingPong]', + '[Coffee]', '[Rice]', '[Pig]', '[Rose]', '[Wilt]', '[Lip]', '[Heart]', + '[BrokenHeart]', '[Cake]', '[Lightning]', '[Bomb]', '[Dagger]', '[Soccer]', '[Ladybug]', + '[Poop]', '[Moon]', '[Sun]', '[Gift]', '[Hug]', '[Strong]', + '[Weak]', '[Shake]', '[Victory]', '[Admire]', '[Beckon]', '[Fist]', '[Pinky]', + '[Love]', '[No]', '[OK]', '[InLove]', '[Blowkiss]', '[Waddle]', '[Tremble]', + '[Aaagh!]', '[Twirl]', '[Kotow]', '[Lookback]', '[Jump]', '[Give-in]', + u'\U0001f604', u'\U0001f637', u'\U0001f639', u'\U0001f61d', u'\U0001f632', u'\U0001f633', + u'\U0001f631', u'\U0001f64d', u'\U0001f609', u'\U0001f60c', u'\U0001f612', u'\U0001f47f', + u'\U0001f47b', u'\U0001f49d', u'\U0001f64f', u'\U0001f4aa', u'\U0001f4b5', u'\U0001f382', + u'\U0001f388', u'\U0001f4e6', + ] + BOT_ZHIHU_URL_LATEST = 'http://news-at.zhihu.com/api/4/news/latest' + BOT_ZHIHU_URL_DAILY = 'http://daily.zhihu.com/story/' + BOT_TULING_API_KEY = '55e7f30895a0a10535984bae5ad294d1' + BOT_TULING_API_URL = 'http://www.tuling123.com/openapi/api?key=%s&info=%s&userid=%s' + BOT_TULING_BOT_REPLY = u'麻烦说的清楚一点,我听不懂你在说什么' diff --git a/wxbot_project_py2.7/config/log.py b/wxbot_project_py2.7/config/log.py new file mode 100644 index 0000000..eac4933 --- /dev/null +++ b/wxbot_project_py2.7/config/log.py @@ -0,0 +1,23 @@ +#!/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') diff --git a/wxbot_project_py2.7/config/requirements.txt b/wxbot_project_py2.7/config/requirements.txt new file mode 100644 index 0000000..b0cf8fe --- /dev/null +++ b/wxbot_project_py2.7/config/requirements.txt @@ -0,0 +1,5 @@ +qrcode +flask +requests +requests_toolbelt +pymysql diff --git a/wxbot_project_py2.7/config/wechat.conf.bak b/wxbot_project_py2.7/config/wechat.conf.bak new file mode 100644 index 0000000..2dba3e0 --- /dev/null +++ b/wxbot_project_py2.7/config/wechat.conf.bak @@ -0,0 +1,72 @@ +[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 + +[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 = + diff --git a/wxbot_project_py2.7/db/__init__.py b/wxbot_project_py2.7/db/__init__.py new file mode 100644 index 0000000..5d5ff9a --- /dev/null +++ b/wxbot_project_py2.7/db/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +# coding: utf-8 + +from sqlite_db import SqliteDB +from mysql_db import MysqlDB \ No newline at end of file diff --git a/wxbot_project_py2.7/db/mysql_db.py b/wxbot_project_py2.7/db/mysql_db.py new file mode 100644 index 0000000..30df872 --- /dev/null +++ b/wxbot_project_py2.7/db/mysql_db.py @@ -0,0 +1,200 @@ +#!/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() + + \ No newline at end of file diff --git a/wxbot_project_py2.7/db/sqlite_db.py b/wxbot_project_py2.7/db/sqlite_db.py new file mode 100644 index 0000000..5abc522 --- /dev/null +++ b/wxbot_project_py2.7/db/sqlite_db.py @@ -0,0 +1,173 @@ +#!/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 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() diff --git a/wxbot_project_py2.7/docker/Dockerfile b/wxbot_project_py2.7/docker/Dockerfile new file mode 100644 index 0000000..0bab668 --- /dev/null +++ b/wxbot_project_py2.7/docker/Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu:16.04 +MAINTAINER Urinx + +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 [""] diff --git a/wxbot_project_py2.7/docker/README.md b/wxbot_project_py2.7/docker/README.md new file mode 100644 index 0000000..4072aca --- /dev/null +++ b/wxbot_project_py2.7/docker/README.md @@ -0,0 +1,49 @@ +# 本地构建 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 +``` diff --git a/wxbot_project_py2.7/server/index.html b/wxbot_project_py2.7/server/index.html new file mode 100644 index 0000000..4482b9f --- /dev/null +++ b/wxbot_project_py2.7/server/index.html @@ -0,0 +1,210 @@ + + + + + WeChat Bot Server + + + + +
+
+	#####################
+	# WeChat Bot Server #
+	#####################
+
+
+## APIs
+
+- /qrcode
+  ------------------------
+  @brief      login qrcode
+  @method     get
+  @return     raw image
+  ------------------------
+
+- /runtime
+  -----------------------------
+  @brief      basic info
+  @method     get
+  @return     application/json
+              {
+                'ret': 0,
+                'runtime': '',
+                'total_size': '',
+                'db_size': '',
+                'msg_count': '',
+                'image_count': '',
+                'voice_count': '',
+                'video_count': '',
+              }
+  -----------------------------
+
+- /group_list
+  ------------------------
+  @brief      list groups
+  @method     get
+  ------------------------
+
+- /group_member_list/<g_id>
+  -----------------------------
+  @brief      list group member
+  @method     get
+  @param      g_id String
+  -----------------------------
+
+- /group_chat_log/<g_name>
+  -------------------------------
+  @brief      list group chat log
+  @method     get
+  @param      g_name String
+  -------------------------------
+
+- /upload
+  -------------------------------
+  @brief      upload a file
+  @method     get/post
+  @return     application/json
+              {
+                'ret': 0,
+                'msg': '',
+              }
+  -------------------------------
+
+- /send_msg/<to>/<msg>
+  -------------------------------------------
+  @brief      send message to user or gourp
+  @method     get
+  @param      to: String, user id or group id
+  @param      msg: String, words
+  @return     application/json
+              {
+                'ret': 0,
+              }
+  -------------------------------------------
+
+- /send_img/<to>/<img>
+  -------------------------------------------
+  @brief      send image to user or gourp
+  @method     get
+  @param      to: String, user id or group id
+  @param      img: String, image file name
+  @return     application/json
+              {
+                'ret': 0,
+              }
+  -------------------------------------------
+
+- /send_emot/<to>/<emot>
+  -------------------------------------------
+  @brief      send emotion to user or gourp
+  @method     get
+  @param      to: String, user id or group id
+  @param      emot: String, emotion file name
+  @return     application/json
+              {
+                'ret': 0,
+              }
+  -------------------------------------------
+
+- /send_file/<to>/<file>
+  -------------------------------------------
+  @brief      send emotion to user or gourp
+  @method     get
+  @param      to: String, user id or group id
+  @param      file: String, file name
+  @return     application/json
+              {
+                'ret': 0,
+              }
+  -------------------------------------------
+
+- /mass_send_msg
+  -------------------------------------------
+  @brief      send text to mass users or gourps
+  @method     post
+  @param      application/json
+              {
+                'to_list': [
+                  'group_id',
+                  ...
+                ],
+                'msg': '',
+              }
+  @return     application/json
+              {
+                'ret': 0,
+                'unsend_list': [],
+              }
+  -------------------------------------------
+
+- /mass_send_img
+  -------------------------------------------
+  @brief      send image to mass users or gourps
+  @method     post
+  @param      application/json
+              {
+                'to_list': [
+                  'group_id',
+                  ...
+                ],
+                'msg': '',
+              }
+  @return     application/json
+              {
+                'ret': 0,
+                'unsend_list': [],
+              }
+  -------------------------------------------
+
+- /mass_send_emot
+  -------------------------------------------
+  @brief      send emoticon to mass users or gourps
+  @method     post
+  @param      application/json
+              {
+                'to_list': [
+                  'group_id',
+                  ...
+                ],
+                'msg': '',
+              }
+  @return     application/json
+              {
+                'ret': 0,
+                'unsend_list': [],
+              }
+  -------------------------------------------
+
+- /mass_send_file
+  -------------------------------------------
+  @brief      send file to mass users or gourps
+  @method     post
+  @param      application/json
+              {
+                'to_list': [
+                  'group_id',
+                  ...
+                ],
+                'msg': '',
+              }
+  @return     application/json
+              {
+                'ret': 0,
+                'unsend_list': [],
+              }
+  -------------------------------------------
+
+
+ + + + diff --git a/wxbot_project_py2.7/server/upload.html b/wxbot_project_py2.7/server/upload.html new file mode 100644 index 0000000..ecb88ba --- /dev/null +++ b/wxbot_project_py2.7/server/upload.html @@ -0,0 +1,7 @@ + +Upload new File +

Upload new File

+
+

+ +

\ No newline at end of file diff --git a/__init__.py b/wxbot_project_py2.7/wechat/__init__.py similarity index 59% rename from __init__.py rename to wxbot_project_py2.7/wechat/__init__.py index 394a0a0..1877c14 100644 --- a/__init__.py +++ b/wxbot_project_py2.7/wechat/__init__.py @@ -1,2 +1,4 @@ #!/usr/bin/env python # coding: utf-8 + +from wechat import WeChat \ No newline at end of file diff --git a/wxbot_project_py2.7/wechat/utils.py b/wxbot_project_py2.7/wechat/utils.py new file mode 100644 index 0000000..06b8d48 --- /dev/null +++ b/wxbot_project_py2.7/wechat/utils.py @@ -0,0 +1,359 @@ +#!/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 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}>([^<]+)'.format(key), content) + if not pm: + pm = re.search( + '<{0}><\!\[CDATA\[(.*?)\]\]>'.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] + diff --git a/wxbot_project_py2.7/wechat/wechat.py b/wxbot_project_py2.7/wechat/wechat.py new file mode 100644 index 0000000..b68ab7f --- /dev/null +++ b/wxbot_project_py2.7/wechat/wechat.py @@ -0,0 +1,697 @@ +#!/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" + \ + "=========================" + return description + + def __init__(self, host='wx.qq.com'): + super(WeChat, self).__init__(host) + + self.db = None + self.save_data_folder = '' # 保存图片,语音,小视频的文件夹 + self.last_login = 0 # 上次退出的时间 + self.time_out = 5 # 同步时间间隔(单位:秒) + self.msg_handler = None + self.start_time = time.time() + self.bot = None + + cm = ConfigManager() + self.save_data_folders = cm.get_wechat_media_dir() + self.cookie_file = cm.get_cookie() + self.pickle_file = cm.get_pickle_files() + self.log_mode = cm.get('setting', 'log_mode') == 'True' + self.exit_code = 0 + + def start(self): + echo(Constant.LOG_MSG_START) + run(Constant.LOG_MSG_RECOVER, self.recover) + + timeOut = time.time() - self.last_login + echo(Constant.LOG_MSG_TRY_INIT) + if self.webwxinit(): + echo(Constant.LOG_MSG_SUCCESS) + run(Constant.LOG_MSG_RECOVER_CONTACT, self.recover_contacts) + else: + echo(Constant.LOG_MSG_FAIL) + + while True: + # first try to login by uin without qrcode + echo(Constant.LOG_MSG_ASSOCIATION_LOGIN) + if self.association_login(): + echo(Constant.LOG_MSG_SUCCESS) + else: + echo(Constant.LOG_MSG_FAIL) + # scan qrcode to login + run(Constant.LOG_MSG_GET_UUID, self.getuuid) + echo(Constant.LOG_MSG_GET_QRCODE) + self.genqrcode() + echo(Constant.LOG_MSG_SCAN_QRCODE) + + if not self.waitforlogin(): + continue + echo(Constant.LOG_MSG_CONFIRM_LOGIN) + if not self.waitforlogin(0): + continue + break + + run(Constant.LOG_MSG_LOGIN, self.login) + run(Constant.LOG_MSG_INIT, self.webwxinit) + run(Constant.LOG_MSG_STATUS_NOTIFY, self.webwxstatusnotify) + run(Constant.LOG_MSG_GET_CONTACT, self.webwxgetcontact) + echo(Constant.LOG_MSG_CONTACT_COUNT % ( + self.MemberCount, len(self.MemberList) + )) + echo(Constant.LOG_MSG_OTHER_CONTACT_COUNT % ( + len(self.GroupList), len(self.ContactList), + len(self.SpecialUsersList), len(self.PublicUsersList) + )) + run(Constant.LOG_MSG_GET_GROUP_MEMBER, self.fetch_group_contacts) + + run(Constant.LOG_MSG_SNAPSHOT, self.snapshot) + + while True: + [retcode, selector] = self.synccheck() + Log.debug('retcode: %s, selector: %s' % (retcode, selector)) + self.exit_code = int(retcode) + + if retcode == '1100': + echo(Constant.LOG_MSG_LOGOUT) + break + if retcode == '1101': + echo(Constant.LOG_MSG_LOGIN_OTHERWHERE) + break + if retcode == '1102': + echo(Constant.LOG_MSG_QUIT_ON_PHONE) + break + elif retcode == '0': + if selector == '2': + r = self.webwxsync() + if r is not None: + try: + self.handle_msg(r) + except: + Log.error(traceback.format_exc()) + elif selector == '7': + r = self.webwxsync() + elif selector == '0': + time.sleep(self.time_out) + elif selector == '4': + # 保存群聊到通讯录 + # 修改群名称 + # 新增或删除联系人 + # 群聊成员数目变化 + r = self.webwxsync() + if r is not None: + try: + self.handle_mod(r) + except: + Log.error(traceback.format_exc()) + elif selector == '3' or selector == '6': + break + else: + r = self.webwxsync() + Log.debug('webwxsync: %s\n' % json.dumps(r)) + + # 执行定时任务 + if self.msg_handler: + self.msg_handler.check_schedule_task() + + # if self.bot: + # r = self.bot.time_schedule() + # if r: + # for g in self.GroupList: + # echo('[*] 推送 -> %s: %s' % (g['NickName'], r)) + # g_id = g['UserName'] + # self.webwxsendmsg(r, g_id) + + def get_run_time(self): + """ + @brief get how long this run + @return String + """ + totalTime = int(time.time() - self.start_time) + t = timedelta(seconds=totalTime) + return '%s Day %s' % (t.days, t) + + def stop(self): + """ + @brief Save some data and use shell to kill this process + """ + run(Constant.LOG_MSG_SNAPSHOT, self.snapshot) + echo(Constant.LOG_MSG_RUNTIME % self.get_run_time()) + # close database connect + self.db.close() + + def fetch_group_contacts(self): + """ + @brief Fetches all groups contacts. + @return Bool: whether operation succeed. + @note This function must be finished in 180s + """ + Log.debug('fetch_group_contacts') + # clean database + if self.msg_handler: + self.msg_handler.clean_db() + + # sqlite + # ---------------------------------------------------- + # group max_thread_num max_fetch_group_num time(s) + # 197 10 10 108 + # 197 10 15 95 + # 197 20 10 103 + # 197 10 20 55 + # 197 5 30 39 + # 197 4 50 35 + # ---------------------------------------------------- + # mysql + # ---------------------------------------------------- + # group max_thread_num max_fetch_group_num time(s) + # 197 4 50 20 + # ---------------------------------------------------- + + max_thread_num = 4 + max_fetch_group_num = 50 + group_list_queue = Queue.Queue() + + class GroupListThread(threading.Thread): + + def __init__(self, group_list_queue, wechat): + threading.Thread.__init__(self) + self.group_list_queue = group_list_queue + self.wechat = wechat + + def run(self): + while not self.group_list_queue.empty(): + g_list = self.group_list_queue.get() + gid_list = [] + g_dict = {} + for g in g_list: + gid = g['UserName'] + gid_list.append(gid) + g_dict[gid] = g + + group_member_list = self.wechat.webwxbatchgetcontact(gid_list) + + for member_list in group_member_list: + gid = member_list['UserName'] + g = g_dict[gid] + g['MemberCount'] = member_list['MemberCount'] + g['OwnerUin'] = member_list['OwnerUin'] + self.wechat.GroupMemeberList[gid] = member_list['MemberList'] + + # 如果使用 Mysql 则可以在多线程里操作数据库 + # 否则请注释下列代码在主线程里更新群列表 + # ----------------------------------- + # 处理群成员 + # if self.wechat.msg_handler: + # self.wechat.msg_handler.handle_group_member_list(gid, member_list['MemberList']) + # ----------------------------------- + + self.group_list_queue.task_done() + + for g_list in split_array(self.GroupList, max_fetch_group_num): + group_list_queue.put(g_list) + + for i in range(max_thread_num): + t = GroupListThread(group_list_queue, self) + t.setDaemon(True) + t.start() + + group_list_queue.join() + + if self.msg_handler: + # 处理群 + if self.GroupList: + self.msg_handler.handle_group_list(self.GroupList) + + # 这个是用 sqlite 来存储群列表,sqlite 对多线程的支持不太好 + # ---------------------------------------------------- + # 处理群成员 + for (gid, member_list) in self.GroupMemeberList.items(): + self.msg_handler.handle_group_member_list(gid, member_list) + # ---------------------------------------------------- + + return True + + def snapshot(self): + """ + @brief Save basic infos for next login. + @return Bool: whether operation succeed. + """ + try: + conf = { + 'uuid': self.uuid, + 'redirect_uri': self.redirect_uri, + 'uin': self.uin, + 'sid': self.sid, + 'skey': self.skey, + 'pass_ticket': self.pass_ticket, + 'synckey': self.synckey, + 'device_id': self.device_id, + 'last_login': time.time(), + } + cm = ConfigManager() + Log.debug('save wechat config') + cm.set_wechat_config(conf) + + # save cookie + Log.debug('save cookie') + if self.cookie: + self.cookie.save(ignore_discard=True) + + # save contacts + Log.debug('save contacts') + self.save_contacts() + except Exception, e: + Log.error(traceback.format_exc()) + return False + return True + + def recover(self): + """ + @brief Recover from snapshot data. + @return Bool: whether operation succeed. + """ + cm = ConfigManager() + [self.uuid, self.redirect_uri, self.uin, + self.sid, self.skey, self.pass_ticket, + self.synckey, device_id, self.last_login] = cm.get_wechat_config() + + if device_id: + self.device_id = device_id + + self.base_request = { + 'Uin': int(self.uin), + 'Sid': self.sid, + 'Skey': self.skey, + 'DeviceID': self.device_id, + } + + # set cookie + Log.debug('set cookie') + self.cookie = set_cookie(self.cookie_file) + + return True + + def save_contacts(self): + """ + @brief Save contacts. + """ + pickle_save(self.User, self.pickle_file['User']) + pickle_save(self.MemberList, self.pickle_file['MemberList']) + pickle_save(self.GroupList, self.pickle_file['GroupList']) + pickle_save(self.GroupMemeberList, self.pickle_file['GroupMemeberList']) + pickle_save(self.SpecialUsersList, self.pickle_file['SpecialUsersList']) + + def recover_contacts(self): + """ + @brief recover contacts. + @return Bool: whether operation succeed. + """ + try: + self.User = pickle_load(self.pickle_file['User']) + self.MemberList = pickle_load(self.pickle_file['MemberList']) + self.GroupList = pickle_load(self.pickle_file['GroupList']) + self.GroupMemeberList = pickle_load(self.pickle_file['GroupMemeberList']) + self.SpecialUsersList = pickle_load(self.pickle_file['SpecialUsersList']) + return True + except Exception, e: + Log.error(traceback.format_exc()) + return False + + def handle_mod(self, r): + # ModContactCount: 变更联系人或群聊成员数目 + # ModContactList: 变更联系人或群聊列表,或群名称改变 + Log.debug('handle modify') + self.handle_msg(r) + for m in r['ModContactList']: + if m['UserName'][:2] == '@@': + # group + in_list = False + g_id = m['UserName'] + for g in self.GroupList: + # group member change + if g_id == g['UserName']: + g['MemberCount'] = m['MemberCount'] + g['NickName'] = m['NickName'] + self.GroupMemeberList[g_id] = m['MemberList'] + in_list = True + if self.msg_handler: + self.msg_handler.handle_group_member_change(g_id, m['MemberList']) + break + if not in_list: + # a new group + self.GroupList.append(m) + self.GroupMemeberList[g_id] = m['MemberList'] + if self.msg_handler: + self.msg_handler.handle_group_list_change(m) + self.msg_handler.handle_group_member_change(g_id, m['MemberList']) + + elif m['UserName'][0] == '@': + # user + in_list = False + for u in self.MemberList: + u_id = m['UserName'] + if u_id == u['UserName']: + u = m + in_list = True + break + # if don't have then add it + if not in_list: + self.MemberList.append(m) + + def handle_msg(self, r): + """ + @brief Recover from snapshot data. + @param r Dict: message json + """ + Log.debug('handle message') + if self.msg_handler: + self.msg_handler.handle_wxsync(r) + + n = len(r['AddMsgList']) + if n == 0: + return + + if self.log_mode: + echo(Constant.LOG_MSG_NEW_MSG % n) + + for msg in r['AddMsgList']: + + msgType = msg['MsgType'] + msgId = msg['MsgId'] + content = msg['Content'].replace('<', '<').replace('>', '>') + raw_msg = None + + if msgType == self.wx_conf['MSGTYPE_TEXT']: + # 地理位置消息 + if content.find('pictype=location') != -1: + location = content.split('
')[1][:-1] + raw_msg = { + 'raw_msg': msg, + 'location': location, + 'log': Constant.LOG_MSG_LOCATION % location + } + # 普通文本消息 + else: + text = content.split(':
')[-1] + raw_msg = { + 'raw_msg': msg, + 'text': text, + 'log': text.replace('
', '\n') + } + elif msgType == self.wx_conf['MSGTYPE_IMAGE']: + data = self.webwxgetmsgimg(msgId) + fn = 'img_' + msgId + '.jpg' + dir = self.save_data_folders['webwxgetmsgimg'] + path = save_file(fn, data, dir) + raw_msg = {'raw_msg': msg, + 'image': path, + 'log': Constant.LOG_MSG_PICTURE % path} + elif msgType == self.wx_conf['MSGTYPE_VOICE']: + data = self.webwxgetvoice(msgId) + fn = 'voice_' + msgId + '.mp3' + dir = self.save_data_folders['webwxgetvoice'] + path = save_file(fn, data, dir) + raw_msg = {'raw_msg': msg, + 'voice': path, + 'log': Constant.LOG_MSG_VOICE % path} + elif msgType == self.wx_conf['MSGTYPE_SHARECARD']: + info = msg['RecommendInfo'] + card = Constant.LOG_MSG_NAME_CARD % ( + info['NickName'], + info['Alias'], + info['Province'], info['City'], + Constant.LOG_MSG_SEX_OPTION[info['Sex']] + ) + namecard = '%s %s %s %s %s' % ( + info['NickName'], info['Alias'], info['Province'], + info['City'], Constant.LOG_MSG_SEX_OPTION[info['Sex']] + ) + raw_msg = { + 'raw_msg': msg, + 'namecard': namecard, + 'log': card + } + elif msgType == self.wx_conf['MSGTYPE_EMOTICON']: + url = search_content('cdnurl', content) + raw_msg = {'raw_msg': msg, + 'emoticon': url, + 'log': Constant.LOG_MSG_EMOTION % url} + elif msgType == self.wx_conf['MSGTYPE_APP']: + card = '' + # 链接, 音乐, 微博 + if msg['AppMsgType'] in [ + self.wx_conf['APPMSGTYPE_AUDIO'], + self.wx_conf['APPMSGTYPE_URL'], + self.wx_conf['APPMSGTYPE_OPEN'] + ]: + card = Constant.LOG_MSG_APP_LINK % ( + Constant.LOG_MSG_APP_LINK_TYPE[msg['AppMsgType']], + msg['FileName'], + search_content('des', content, 'xml'), + msg['Url'], + search_content('appname', content, 'xml') + ) + raw_msg = { + 'raw_msg': msg, + 'link': msg['Url'], + 'log': card + } + # 图片 + elif msg['AppMsgType'] == self.wx_conf['APPMSGTYPE_IMG']: + data = self.webwxgetmsgimg(msgId) + fn = 'img_' + msgId + '.jpg' + dir = self.save_data_folders['webwxgetmsgimg'] + path = save_file(fn, data, dir) + card = Constant.LOG_MSG_APP_IMG % ( + path, + search_content('appname', content, 'xml') + ) + raw_msg = { + 'raw_msg': msg, + 'image': path, + 'log': card + } + else: + raw_msg = { + 'raw_msg': msg, + 'log': Constant.LOG_MSG_UNKNOWN_MSG % (msgType, content) + } + elif msgType == self.wx_conf['MSGTYPE_STATUSNOTIFY']: + Log.info(Constant.LOG_MSG_NOTIFY_PHONE) + elif msgType == self.wx_conf['MSGTYPE_MICROVIDEO']: + data = self.webwxgetvideo(msgId) + fn = 'video_' + msgId + '.mp4' + dir = self.save_data_folders['webwxgetvideo'] + path = save_file(fn, data, dir) + raw_msg = {'raw_msg': msg, + 'video': path, + 'log': Constant.LOG_MSG_VIDEO % path} + elif msgType == self.wx_conf['MSGTYPE_RECALLED']: + recall_id = search_content('msgid', content, 'xml') + text = Constant.LOG_MSG_RECALL + raw_msg = { + 'raw_msg': msg, + 'text': text, + 'recall_msg_id': recall_id, + 'log': text + } + elif msgType == self.wx_conf['MSGTYPE_SYS']: + raw_msg = { + 'raw_msg': msg, + 'sys_notif': content, + 'log': content + } + elif msgType == self.wx_conf['MSGTYPE_VERIFYMSG']: + name = search_content('fromnickname', content) + raw_msg = { + 'raw_msg': msg, + 'log': Constant.LOG_MSG_ADD_FRIEND % name + } + else: + raw_msg = { + 'raw_msg': msg, + 'log': Constant.LOG_MSG_UNKNOWN_MSG % (msgType, content) + } + + isGroupMsg = '@@' in msg['FromUserName']+msg['ToUserName'] + if self.msg_handler and raw_msg: + if isGroupMsg: + # handle group messages + g_msg = self.make_group_msg(raw_msg) + self.msg_handler.handle_group_msg(g_msg) + else: + # handle personal messages + self.msg_handler.handle_user_msg(raw_msg) + + if self.log_mode: + self.show_msg(raw_msg) + + def make_group_msg(self, msg): + """ + @brief Package the group message for storage. + @param msg Dict: raw msg + @return raw_msg Dict: packged msg + """ + Log.debug('make group message') + raw_msg = { + 'raw_msg': msg['raw_msg'], + 'msg_id': msg['raw_msg']['MsgId'], + 'group_owner_uin': '', + 'group_name': '', + 'group_count': '', + 'from_user_name': msg['raw_msg']['FromUserName'], + 'to_user_name': msg['raw_msg']['ToUserName'], + 'user_attrstatus': '', + 'user_display_name': '', + 'user_nickname': '', + 'msg_type': msg['raw_msg']['MsgType'], + 'text': '', + 'link': '', + 'image': '', + 'video': '', + 'voice': '', + 'emoticon': '', + 'namecard': '', + 'location': '', + 'recall_msg_id': '', + 'sys_notif': '', + 'time': '', + 'timestamp': '', + 'log': '', + } + content = msg['raw_msg']['Content'].replace( + '<', '<').replace('>', '>') + + group = None + src = None + + if msg['raw_msg']['FromUserName'][:2] == '@@': + # 接收到来自群的消息 + g_id = msg['raw_msg']['FromUserName'] + group = self.get_group_by_id(g_id) + + if re.search(":
", content, re.IGNORECASE): + u_id = content.split(':
')[0] + src = self.get_group_user_by_id(u_id, g_id) + + elif msg['raw_msg']['ToUserName'][:2] == '@@': + # 自己发给群的消息 + g_id = msg['raw_msg']['ToUserName'] + u_id = msg['raw_msg']['FromUserName'] + src = self.get_group_user_by_id(u_id, g_id) + group = self.get_group_by_id(g_id) + + if src: + raw_msg['user_attrstatus'] = src['AttrStatus'] + raw_msg['user_display_name'] = src['DisplayName'] + raw_msg['user_nickname'] = src['NickName'] + if group: + raw_msg['group_count'] = group['MemberCount'] + raw_msg['group_owner_uin'] = group['OwnerUin'] + raw_msg['group_name'] = group['ShowName'] + + raw_msg['timestamp'] = msg['raw_msg']['CreateTime'] + t = time.localtime(float(raw_msg['timestamp'])) + raw_msg['time'] = time.strftime("%Y-%m-%d %T", t) + + for key in [ + 'text', 'link', 'image', 'video', 'voice', + 'emoticon', 'namecard', 'location', 'log', + 'recall_msg_id', 'sys_notif' + ]: + if key in msg: + raw_msg[key] = msg[key] + + return raw_msg + + def show_msg(self, message): + """ + @brief Log the message to stdout + @param message Dict + """ + msg = message + src = None + dst = None + group = None + + if msg and msg['raw_msg']: + + content = msg['raw_msg']['Content'] + content = content.replace('<', '<').replace('>', '>') + msg_id = msg['raw_msg']['MsgId'] + + if msg['raw_msg']['FromUserName'][:2] == '@@': + # 接收到来自群的消息 + g_id = msg['raw_msg']['FromUserName'] + group = self.get_group_by_id(g_id) + + if re.search(":
", content, re.IGNORECASE): + u_id = content.split(':
')[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']) + )) diff --git a/wxbot_project_py2.7/wechat/wechat_apis.py b/wxbot_project_py2.7/wechat/wechat_apis.py new file mode 100644 index 0000000..6f02c42 --- /dev/null +++ b/wxbot_project_py2.7/wechat/wechat_apis.py @@ -0,0 +1,969 @@ +#!/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) + 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([ + "" % data['appid'], # 可使用其它AppID + "%s" % data['title'], + "", + "", + "%d" % data['type'], + "", + "", + "", + "", + "%d" % data['totallen'], + "%s" % data['attachid'], + "%s" % data['fileext'], + "", + "", + "", + ]) + 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 + """ + dic = self.webwxsendmsg(text, user_id) + return dic['BaseResponse']['Ret'] == 0 + + 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 diff --git a/wxbot_project_py2.7/weixin_bot.py b/wxbot_project_py2.7/weixin_bot.py new file mode 100755 index 0000000..96bb745 --- /dev/null +++ b/wxbot_project_py2.7/weixin_bot.py @@ -0,0 +1,278 @@ +#!/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 db import SqliteDB +from db import MysqlDB +from config import ConfigManager +from config import Constant +from config import Log +#--------------------------------------------------- +import flask +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.Flask(__name__) +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) + + +@app.route('/') +def index(): + with open(Constant.SERVER_PAGE_INDEX, 'r') as f: + return f.read() + + +@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 flask.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()) + j = {'count': len(result), 'group': result} + return flask.Response(json.dumps(j), mimetype='application/json') + + +@app.route('/group_member_list/') +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) + j = {'count': len(result), 'member': result} + return flask.Response(json.dumps(j), mimetype='application/json') + + +@app.route('/group_chat_log/') +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) + j = {'count': len(result), 'chats': result} + return flask.Response(json.dumps(j), mimetype='application/json') + + +@app.route('/upload', methods=['GET', 'POST']) +def upload_file(): + if flask.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 flask.request.files: + j['msg'] = 'No file part' + return flask.Response(json.dumps(j), mimetype='application/json') + + # if user does not select file, browser also + # submit a empty part without filename + file = flask.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 flask.Response(json.dumps(j), mimetype='application/json') + else: + with open(Constant.SERVER_PAGE_UPLOAD, 'r') as f: + return f.read() + + +@app.route('/send_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 + """ + j = {'ret': 0 if wechat.send_text(to, msg) else 1} + return flask.Response(json.dumps(j), mimetype='application/json') + + +@app.route('/send_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) + j = {'ret': 0 if wechat.send_img(to, img_path) else 1} + return flask.Response(json.dumps(j), mimetype='application/json') + + +@app.route('/send_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) + j = {'ret': 0 if wechat.send_emot(to, emot_path) else 1} + return flask.Response(json.dumps(j), mimetype='application/json') + + +@app.route('/send_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) + j = {'ret': 0 if wechat.send_file(to, file_path) else 1} + return flask.Response(json.dumps(j), mimetype='application/json') + + +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(flask.request.method, flask.request.json, wechat.send_text) + return flask.Response(json.dumps(j), mimetype='application/json') + + +@app.route('/mass_send_img', methods=["GET", "POST"]) +def mass_send_img(): + """ + @brief send iamge to mass users or gourps + """ + j = mass_send(flask.request.method, flask.request.json, wechat.webwxsendmsgimg) + return flask.Response(json.dumps(j), mimetype='application/json') + + +@app.route('/mass_send_emot', methods=["GET", "POST"]) +def mass_send_emot(): + """ + @brief send emoticon to mass users or gourps + """ + j = mass_send(flask.request.method, flask.request.json, wechat.webwxsendemoticon) + return flask.Response(json.dumps(j), mimetype='application/json') + + +@app.route('/mass_send_file', methods=["GET", "POST"]) +def mass_send_file(): + """ + @brief send file to mass users or gourps + """ + j = mass_send(flask.request.method, flask.request.json, wechat.webwxsendappmsg) + return flask.Response(json.dumps(j), mimetype='application/json') + + +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() + + if wechat.exit_code == 0: + echo(Constant.MAIN_RESTART) + else: + # kill process + os.system(Constant.LOG_MSG_KILL_PROCESS % os.getpid()) + diff --git a/wxbot_project_py2.7/wx_handler/__init__.py b/wxbot_project_py2.7/wx_handler/__init__.py new file mode 100644 index 0000000..19c28f2 --- /dev/null +++ b/wxbot_project_py2.7/wx_handler/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +# coding: utf-8 + +from wechat_msg_processor import WeChatMsgProcessor +from bot import Bot \ No newline at end of file diff --git a/wxbot_project_py2.7/wx_handler/bot.py b/wxbot_project_py2.7/wx_handler/bot.py new file mode 100644 index 0000000..b3420bf --- /dev/null +++ b/wxbot_project_py2.7/wx_handler/bot.py @@ -0,0 +1,47 @@ +#!/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 '' diff --git a/wxbot_project_py2.7/wx_handler/wechat_msg_processor.py b/wxbot_project_py2.7/wx_handler/wechat_msg_processor.py new file mode 100644 index 0000000..d70f54b --- /dev/null +++ b/wxbot_project_py2.7/wx_handler/wechat_msg_processor.py @@ -0,0 +1,246 @@ +#!/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']) + + + 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 == 'test': + # wechat.send_file(g_id, 'test/Data/upload/shake.wav') + wechat.send_img(g_id, 'test/emotion/7.gif') + elif cmd == 'runtime': + wechat.webwxsendmsg(wechat.get_run_time(), g_id) + else: + # reply bot + # --------- + # if wechat.bot: + # r = wechat.bot.reply(cmd) + # if r: + # wechat.webwxsendmsg(r, g_id) + # else: + # pass + 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) + + 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() +