diff --git a/python_wechat.py b/python_wechat.py index e56c893..d87dea0 100644 --- a/python_wechat.py +++ b/python_wechat.py @@ -17,9 +17,6 @@ from itchat.content import * # 初始化 my = itchat.new_instance() my.auto_login(hotReload=False, enableCmdQR=-2) -my.global_keys = ["创业", "算法", "人工智能", "机器学习"] -my.to_user_name = "filehelper" - # my还包括的以下属性,注意用点.查看: # (1) alive 是否还活着,isLogging 是否已登陆 # (2) loginInfo 登陆信息,其中的User属性为自己的信息User字典类,包括UserName, NickName, RemarkName, Sex(1 or 2), Signature, Province, City等 @@ -27,58 +24,157 @@ my.to_user_name = "filehelper" # (4) chatroomList 群聊列表,每一项为一个Chatroom字典类,包括UserName, NickName, RemarkName, MemberCount, MemberList, Self等 # (5) mpList 订阅号列表,每一项为一个MassivePlatform字典类,包括UserName, NickName等 -# 获取并更新通讯录: {UserName: UserInstance} -my.friends = {user["UserName"]: user for user in my.get_friends(update=True)} - -# 消息存储队列 -my.msg_store = {} +my.global_keys = ["创业", "算法", "人工智能", "机器学习"] +my.to_user_name = "filehelper" # 消息接受者 +my.update_time = time.time() # 信息更新时间 +my.msg_store = {} # 消息存储队列 +my.friends = {} # 好友字典列表 +my.groups = {} # 群聊字典列表 -# 消息提取函数 -def get_msg_list(msg): +def update_my_infos(): """ - 提取消息内容,消息来源分类: - (1)来自好友的消息 - (2)来自群的消息 - (3)来自文件传输助手的消息 - 提取消息内容,消息类型分类: - (1)文字(2)图片(3)语音(4)视频(5)地址(6)名片(7)提醒(8)分享(9)附件 + 更新信息 """ - # logging.warning("message: %s", msg) - msg_id = msg["MsgId"] # 消息ID - from_user_name = msg["FromUserName"] # 消息发送者ID,如果为群消息,则为群ID - to_user_name = msg["ToUserName"] # 消息接受者ID,如果为群消息,则为群ID - - msg_type = msg["MsgType"] # 消息类型 - msg_content = msg["Content"] # 消息内容 - msg_time = msg["CreateTime"] # 消息发送时间 - - msg_file = msg["FileName"] # 消息中所带文件的名称 - msg_file_length = msg["FileSize"] # 消息中所带文件的大小 - msg_voice_length = msg["VoiceLength"] # 消息中所带语音的长度(毫秒) - msg_play_length = msg["PlayLength"] # 消息中所带视频的长度(秒) - msg_url = msg["Url"] # 消息中所带链接的地址 - - wind_name = msg["User"]["RemarkName"] if msg["User"].get("RemarkName") else ( - msg["User"]["NickName"] if msg["User"].get("NickName") else to_user_name - ) # 窗口名称:群名或好友名 - - msg_user_name = from_user_name # 消息发送者的ID - msg_nick_name = wind_name # 消息发送者的昵称 - if from_user_name.startswith("@@") or to_user_name.startswith("@@"): - msg_user_name = msg["ActualUserName"] - msg_nick_name = msg["ActualNickName"] if (msg_user_name not in my.friends) or (not my.friends[msg_user_name]["RemarkName"]) else my.friends[msg_user_name]["RemarkName"] - - is_at = msg.get("IsAt", None) # 是否在群内被@ - we_type = msg["Type"] # 消息类型 - we_text = msg["Text"] # 消息内容 - - logging.warning("show: msg_nick_name=%s, wind_name=%s, we_type=%s, we_text=%s", msg_nick_name, wind_name, we_type, we_text) - return msg_id, from_user_name, to_user_name, msg_type, msg_content, msg_time, msg_file, msg_file_length, msg_voice_length, msg_play_length, msg_url, \ - wind_name, msg_user_name, msg_nick_name, is_at, we_type, we_text + # 获取并更新通讯录: {UserName: UserInstance} + my.friends = {user["UserName"]: user for user in my.get_friends(update=True)} + # 获取并更新群列表: {UserName: UserInstance} + my.groups = {group["UserName"]: group for group in my.get_chatrooms(update=True)} + return +update_my_infos() + + +class Message(object): + """ + 消息类 + """ + def __init__(self, msg): + """ + 构造函数:提取消息内容 + 消息来源分类: + (1)来自好友的消息 + (2)来自群的消息 + 提取消息内容,消息类型分类: + (1)文字(2)图片(3)语音(4)视频(5)地址(6)名片(7)提醒(8)分享(9)附件 + """ + # 更新信息,十分钟更新一次 + # logging.warning("message: %s", msg) + if time.time() - my.update_time > 600: + update_my_infos() + my.update_time = time.time() + + self.msg_id = msg["MsgId"] # 消息ID + self.from_user_name = msg["FromUserName"] # 消息发送者ID,如果为群消息,则为群ID + + self.msg_type = msg["MsgType"] # 消息类型,这里参考下边的we_type + self.msg_content = msg["Content"] # 消息内容,这里参考下边的we_text + self.msg_time = msg["CreateTime"] # 消息发送时间,时间戳格式 + + self.msg_file = msg["FileName"] # 消息中所带文件的名称 + self.msg_file_length = msg["FileSize"] # 消息中所带文件的大小,字符串类型 + self.msg_voice_length = msg["VoiceLength"] # 消息中所带语音的长度(毫秒) + self.msg_play_length = msg["PlayLength"] # 消息中所带视频的长度(秒) + self.msg_url = msg["Url"] # 消息中所带链接的地址 + + self.user_user_name = msg["User"].get("UserName", "") # 消息发送者ID,如果为群消息,则为群ID + self.user_nick_name = msg["User"].get("NickName", "") # 消息发送者昵称,如果为群消息,则为群名 + self.user_remark_name = msg["User"].get("RemarkName", "") # 消息发送者备注名称,如果为群消息,则为群备注名称 + self.wind_name = self.user_remark_name if self.user_remark_name else ( + self.user_nick_name if self.user_nick_name else ( + my.friends[self.user_user_name]["NickName"] if self.user_user_name in my.friends else ( + my.groups[self.user_user_name]["NickName"] if self.user_user_name in my.groups else "未知窗口" + ) + ) + ) + + self.actual_user_name = msg.get("ActualUserName", "") # 群消息中,消息发送者的ID + self.actual_nick_name = msg.get("ActualNickName", "") # 群消息中,消息发送者的群昵称 + self.actual_remark_name = self.actual_nick_name \ + if (self.actual_user_name not in my.friends) or (not my.friends[self.actual_user_name]["RemarkName"]) \ + else my.friends[self.actual_user_name]["RemarkName"] + + self.is_at = msg.get("IsAt", None) # 是否在群内被@ + self.we_type = msg["Type"] # 消息类型 + self.we_text = msg["Text"] # 消息内容 + + logging.warning("show: wind_name=%s, actual_send_name=%s, we_type=%s, we_text=%s", self.wind_name, self.actual_remark_name, self.we_type, self.we_text) + return + + +def process_message_group(msg): + """ + 处理群消息 + """ + # 消息过滤, 只监测文字、图片、语音、名片、注解、分享等 + if msg.we_type not in ["Text", "Picture", "Recording", "Card", "Note", "Sharing"]: + logging.warning("process_message_group: message type isn't included, ignored") + return + + # ==== 处理红包消息 ==== + if msg.we_type == "Note" and msg.we_text.find("收到红包,请在手机上查看") >= 0: + my.send("【%s】中有人发红包啦,快抢!" % msg.wind_name, toUserName=my.to_user_name) + + # ==== 处理关键词消息 ==== + for key in my.global_keys: + if msg.we_type == "Text" and msg.we_text.find(key) >= 0: + my.send("【%s】中【%s】提及了关键字:%s" % (msg.wind_name, msg.actual_remark_name, key), toUserName=my.to_user_name) + my.send(msg.we_text, toUserName=my.to_user_name) + break + + # ==== 群内是否被@ ==== + if msg.we_type == "Text" and msg.is_at: + my.send("【%s】中【%s】@了你" % (msg.wind_name, msg.actual_remark_name), toUserName=my.to_user_name) + my.send(msg.we_text, toUserName=my.to_user_name) + return + + +def process_message_revoke(msg): + """ + 处理撤回消息 + """ + # 消息过滤, 只监测文字、图片、语音、名片、注解、分享等 + if msg.we_type not in ["Text", "Picture", "Recording", "Card", "Note", "Sharing"]: + logging.warning("process_message_revoke: message type isn't included, ignored") + return + + # 消息存储,删除过期消息 + my.msg_store[msg.msg_id] = msg + for _id in [_id for _id in my.msg_store if time.time() - my.msg_store[_id].msg_time > 120]: + my.msg_store.pop(_id) + + # 保存消息中的内容(图片、语音等) + if msg.we_type in ["Picture", "Recording"]: + try: + msg.we_text(".Cache/" + msg.msg_file) + logging.warning("process_message_revoke: download %s to .Cache/", msg.msg_file) + except Exception as excep: + logging.error("process_message_revoke: download %s to .Cache/ error: %s", msg.msg_file, excep) + + # ==== 撤回消息处理(必须为最后一步) ==== + if msg.we_type == "Note" and msg.we_text.find("撤回了一条消息") >= 0: + old_msg = my.msg_store.get(msg.msg_content[msg.msg_content.find("")+7: msg.msg_content.find("")]) + if not old_msg: + logging.warning("process_message_revoke: no message id in my.msg_store") + return + + if old_msg.from_user_name.startswith("@@"): + my.send("【%s】中【%s】撤回了自己发送的消息:\nType: %s\nTime: %s\n%s" % + (old_msg.wind_name, old_msg.actual_remark_name, old_msg.we_type, old_msg.msg_time, old_msg.msg_file), toUserName=my.to_user_name) + else: + my.send("【%s】撤回了自己发送的消息:\nType: %s\nTime: %s\n%s" % + (old_msg.wind_name, old_msg.we_type, old_msg.msg_time, old_msg.msg_file), toUserName=my.to_user_name) + + if old_msg.we_type in ["Text", "Card"]: + my.send(str(old_msg.we_text), toUserName=my.to_user_name) + elif old_msg.we_type == "Sharing": + my.send(old_msg.we_text + "\n" + old_msg.msg_url, toUserName=my.to_user_name) + elif old_msg.we_type == "Picture": + my.send_image(".Cache/" + old_msg.msg_file, toUserName=my.to_user_name) + elif old_msg.we_type == "Recording": + my.send_file(".Cache/" + old_msg.msg_file, toUserName=my.to_user_name) + return -# 消息注册,主要处理群消息 @my.msg_register([TEXT, PICTURE, RECORDING, VIDEO, MAP, CARD, NOTE, SHARING, ATTACHMENT], isFriendChat=True, isGroupChat=True) def text_reply(msg): """ @@ -89,64 +185,14 @@ def text_reply(msg): return # 消息提取 - msg_list = get_msg_list(msg) - msg_id, from_user_name, to_user_name, msg_type, msg_content, msg_time, msg_file, msg_file_length, msg_voice_length, msg_play_length, msg_url, \ - wind_name, msg_user_name, msg_nick_name, is_at, we_type, we_text = msg_list + msg = Message(msg) - # 消息过滤, 只监测文字、图片、语音、名片、注解、分享等 - if we_type not in ["Text", "Picture", "Recording", "Card", "Note", "Sharing"]: - logging.warning("message type isn't included, ignored") - return - - # 消息存储,删除过期消息 - my.msg_store[msg_id] = msg_list - for _id in [_id for _id in my.msg_store if time.time() - my.msg_store[_id][5] > 120]: - my.msg_store.pop(_id) - - # 保存消息中的内容(图片、语音等),不保存动态图片 - if (we_type in ["Picture", "Recording"]) and (not msg_file.endswith(".gif")): - try: - we_text(".Cache/" + msg_file) - logging.warning("downloading %s to .Cache/", msg_file) - except Exception as excep: - logging.error("downloading %s to .Cache/ error: %s", msg_file, excep) - - # ==== 处理群消息 ==== - if from_user_name.startswith("@@"): - # ==== 处理红包消息 ==== - if we_type == "Note" and we_text.find("收到红包,请在手机上查看") >= 0: - my.send("【%s】中有人发红包啦,快抢!" % wind_name, toUserName=my.to_user_name) - # ==== 处理关键词消息 ==== - for key in my.global_keys: - if we_type == "Text" and we_text.find(key) >= 0: - my.send("【%s】中【%s】提及了关键字:%s" % (wind_name, msg_nick_name, key), toUserName=my.to_user_name) - my.send(we_text, toUserName=my.to_user_name) - break - # ==== 群内是否被@ ==== - if we_type == "Text" and is_at: - my.send("【%s】中【%s】@了你" % (wind_name, msg_nick_name), toUserName=my.to_user_name) - my.send(we_text, toUserName=my.to_user_name) - - # ==== 撤回消息处理(必须为最后一步) ==== - if we_type == "Note" and we_text.find("撤回了一条消息") >= 0: - msg_list = my.msg_store.get(msg_content[msg_content.find("")+7: msg_content.find("")]) - if not msg_list: - logging.warning("not message id in my.msg_store") - return - - msg_id, from_user_name, to_user_name, msg_type, msg_content, msg_time, msg_file, msg_file_length, msg_voice_length, msg_play_length, msg_url, \ - wind_name, msg_user_name, msg_nick_name, is_at, we_type, we_text = msg_list - my.send("【%s】中【%s】撤回了自己发送的消息:\nType: %s\nTime: %s\n%s" % (wind_name, msg_nick_name, we_type, msg_time, msg_file), toUserName=my.to_user_name) - - if we_type in ["Text", "Card"]: - my.send(str(we_text), toUserName=my.to_user_name) - elif we_type == "Sharing": - my.send(we_text + "\n" + msg_url, toUserName=my.to_user_name) - elif we_type == "Recording": - my.send_file(".Cache/" + msg_file, toUserName=my.to_user_name) - elif we_type == "Picture" and (not msg_file.endswith(".gif")): - my.send_image(".Cache/" + msg_file, toUserName=my.to_user_name) + # 处理群消息 + if msg.from_user_name.startswith("@@"): + process_message_group(msg) + # 处理撤回消息 + process_message_revoke(msg) return @@ -332,4 +378,75 @@ my.run(debug=False) 'Type': 'Text', 'Text': '就是那个,那个协议我们手上有吗' } + +警示消息:好友类 +{ + 'MsgId': '1529895072288746571', + 'FromUserName': '@4076708be2e09ef83f249f168553d0dd55b4f734aee7d276e92ddbe98625476a', + 'ToUserName': '@f97583d8ffbaee6189854116897c677f', + 'MsgType': 10000, + 'Content': '你已添加了呼啸而过的小青春,现在可以开始聊天了。', + 'Status': 4, + 'ImgStatus': 1, + 'CreateTime': 1498533407, + 'VoiceLength': 0, + 'PlayLength': 0, + 'FileName': '', + 'FileSize': '', + 'MediaId': '', + 'Url': '', + 'AppMsgType': 0, + 'StatusNotifyCode': 0, + 'StatusNotifyUserName': '', + 'HasProductId': 0, + 'Ticket': '', + 'ImgHeight': 0, + 'ImgWidth': 0, + 'SubMsgType': 0, + 'NewMsgId': 1529895072288746571, + 'OriContent': '', + 'User': + }>, + 'Type': 'Note', + 'Text': '你已添加了呼啸而过的小青春,现在可以开始聊天了。' +} + +警示消息:群类 +{ + 'MsgId': '1049646282086057263', + 'FromUserName': '@@300f57b68ca7ef593ae3221eef7dba5377466c86122aaa15a8ffc1031310e210', + 'ToUserName': '@006f63e8086ab07fcbe3771dc824c4a6', + 'MsgType': 10000, + 'Content': '你邀请"大姐"加入了群聊', + 'Status': 3, + 'ImgStatus': 1, + 'CreateTime': 1498533901, + 'VoiceLength': 0, + 'PlayLength': 0, + 'FileName': '', + 'FileSize': '', + 'MediaId': '', + 'Url': '', + 'AppMsgType': 0, + 'StatusNotifyCode': 0, + 'StatusNotifyUserName': '', + 'HasProductId': 0, + 'Ticket': '', + 'ImgHeight': 0, + 'ImgWidth': 0, + 'SubMsgType': 0, + 'NewMsgId': 1049646282086057263, + 'OriContent': '', + 'ActualUserName': '@006f63e8086ab07fcbe3771dc824c4a6', + 'ActualNickName': '某某某', + 'IsAt': False, + 'User': + }>, + 'Type': 'Note', + 'Text': '你邀请"大姐"加入了群聊' +} """