您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

306 行
14KB

  1. # encoding:utf-8
  2. """
  3. wechat channel
  4. """
  5. import os
  6. from lib import itchat
  7. import json
  8. from lib.itchat.content import *
  9. from bridge.reply import *
  10. from bridge.context import *
  11. from channel.channel import Channel
  12. from concurrent.futures import ThreadPoolExecutor
  13. from common.log import logger
  14. from common.tmp_dir import TmpDir
  15. from config import conf
  16. from common.time_check import time_checker
  17. from plugins import *
  18. import requests
  19. import io
  20. import time
  21. thread_pool = ThreadPoolExecutor(max_workers=8)
  22. def thread_pool_callback(worker):
  23. worker_exception = worker.exception()
  24. if worker_exception:
  25. logger.exception("Worker return exception: {}".format(worker_exception))
  26. @itchat.msg_register(TEXT)
  27. def handler_single_msg(msg):
  28. WechatChannel().handle_text(msg)
  29. return None
  30. @itchat.msg_register(TEXT, isGroupChat=True)
  31. def handler_group_msg(msg):
  32. WechatChannel().handle_group(msg)
  33. return None
  34. @itchat.msg_register(VOICE)
  35. def handler_single_voice(msg):
  36. WechatChannel().handle_voice(msg)
  37. return None
  38. @itchat.msg_register(VOICE, isGroupChat=True)
  39. def handler_group_voice(msg):
  40. WechatChannel().handle_group_voice(msg)
  41. return None
  42. class WechatChannel(Channel):
  43. def __init__(self):
  44. pass
  45. def startup(self):
  46. itchat.instance.receivingRetryCount = 600 # 修改断线超时时间
  47. # login by scan QRCode
  48. hotReload = conf().get('hot_reload', False)
  49. try:
  50. itchat.auto_login(enableCmdQR=2, hotReload=hotReload)
  51. except Exception as e:
  52. if hotReload:
  53. logger.error("Hot reload failed, try to login without hot reload")
  54. itchat.logout()
  55. os.remove("itchat.pkl")
  56. itchat.auto_login(enableCmdQR=2, hotReload=hotReload)
  57. else:
  58. raise e
  59. # start message listener
  60. itchat.run()
  61. # handle_* 系列函数处理收到的消息后构造Context,然后传入handle函数中处理Context和发送回复
  62. # Context包含了消息的所有信息,包括以下属性
  63. # type 消息类型, 包括TEXT、VOICE、IMAGE_CREATE
  64. # content 消息内容,如果是TEXT类型,content就是文本内容,如果是VOICE类型,content就是语音文件名,如果是IMAGE_CREATE类型,content就是图片生成命令
  65. # kwargs 附加参数字典,包含以下的key:
  66. # session_id: 会话id
  67. # isgroup: 是否是群聊
  68. # receiver: 需要回复的对象
  69. # msg: itchat的原始消息对象
  70. def handle_voice(self, msg):
  71. if conf().get('speech_recognition') != True:
  72. return
  73. logger.debug("[WX]receive voice msg: " + msg['FileName'])
  74. from_user_id = msg['FromUserName']
  75. other_user_id = msg['User']['UserName']
  76. if from_user_id == other_user_id:
  77. context = Context(ContextType.VOICE,msg['FileName'])
  78. context.kwargs = {'isgroup': False, 'msg': msg, 'receiver': other_user_id, 'session_id': other_user_id}
  79. thread_pool.submit(self.handle, context).add_done_callback(thread_pool_callback)
  80. @time_checker
  81. def handle_text(self, msg):
  82. logger.debug("[WX]receive text msg: " + json.dumps(msg, ensure_ascii=False))
  83. content = msg['Text']
  84. from_user_id = msg['FromUserName']
  85. to_user_id = msg['ToUserName'] # 接收人id
  86. other_user_id = msg['User']['UserName'] # 对手方id
  87. create_time = msg['CreateTime'] # 消息时间
  88. match_prefix = check_prefix(content, conf().get('single_chat_prefix'))
  89. if conf().get('hot_reload') == True and int(create_time) < int(time.time()) - 60: #跳过1分钟前的历史消息
  90. logger.debug("[WX]history message skipped")
  91. return
  92. if "」\n- - - - - - - - - - - - - - -" in content:
  93. logger.debug("[WX]reference query skipped")
  94. return
  95. if match_prefix:
  96. content = content.replace(match_prefix, '', 1).strip()
  97. elif match_prefix is None:
  98. return
  99. context = Context()
  100. context.kwargs = {'isgroup': False, 'msg': msg, 'receiver': other_user_id, 'session_id': other_user_id}
  101. img_match_prefix = check_prefix(content, conf().get('image_create_prefix'))
  102. if img_match_prefix:
  103. content = content.replace(img_match_prefix, '', 1).strip()
  104. context.type = ContextType.IMAGE_CREATE
  105. else:
  106. context.type = ContextType.TEXT
  107. context.content = content
  108. thread_pool.submit(self.handle, context).add_done_callback(thread_pool_callback)
  109. @time_checker
  110. def handle_group(self, msg):
  111. logger.debug("[WX]receive group msg: " + json.dumps(msg, ensure_ascii=False))
  112. group_name = msg['User'].get('NickName', None)
  113. group_id = msg['User'].get('UserName', None)
  114. create_time = msg['CreateTime'] # 消息时间
  115. if conf().get('hot_reload') == True and int(create_time) < int(time.time()) - 60: #跳过1分钟前的历史消息
  116. logger.debug("[WX]history group message skipped")
  117. return
  118. if not group_name:
  119. return ""
  120. origin_content = msg['Content']
  121. content = msg['Content']
  122. content_list = content.split(' ', 1)
  123. context_special_list = content.split('\u2005', 1)
  124. if len(context_special_list) == 2:
  125. content = context_special_list[1]
  126. elif len(content_list) == 2:
  127. content = content_list[1]
  128. if "」\n- - - - - - - - - - - - - - -" in content:
  129. logger.debug("[WX]reference query skipped")
  130. return ""
  131. config = conf()
  132. match_prefix = (msg['IsAt'] and not config.get("group_at_off", False)) or check_prefix(origin_content, config.get('group_chat_prefix')) \
  133. or check_contain(origin_content, config.get('group_chat_keyword'))
  134. if ('ALL_GROUP' in config.get('group_name_white_list') or group_name in config.get('group_name_white_list') or check_contain(group_name, config.get('group_name_keyword_white_list'))) and match_prefix:
  135. context = Context()
  136. context.kwargs = { 'isgroup': True, 'msg': msg, 'receiver': group_id}
  137. img_match_prefix = check_prefix(content, conf().get('image_create_prefix'))
  138. if img_match_prefix:
  139. content = content.replace(img_match_prefix, '', 1).strip()
  140. context.type = ContextType.IMAGE_CREATE
  141. else:
  142. context.type = ContextType.TEXT
  143. context.content = content
  144. group_chat_in_one_session = conf().get('group_chat_in_one_session', [])
  145. if ('ALL_GROUP' in group_chat_in_one_session or
  146. group_name in group_chat_in_one_session or
  147. check_contain(group_name, group_chat_in_one_session)):
  148. context['session_id'] = group_id
  149. else:
  150. context['session_id'] = msg['ActualUserName']
  151. thread_pool.submit(self.handle, context).add_done_callback(thread_pool_callback)
  152. def handle_group_voice(self, msg):
  153. if conf().get('group_speech_recognition') != True:
  154. return
  155. logger.debug("[WX]receive voice for group msg: " + msg['FileName'])
  156. group_name = msg['User'].get('NickName', None)
  157. group_id = msg['User'].get('UserName', None)
  158. create_time = msg['CreateTime'] # 消息时间
  159. if conf().get('hot_reload') == True and int(create_time) < int(time.time()) - 60: #跳过1分钟前的历史消息
  160. logger.debug("[WX]history group message skipped")
  161. return
  162. # 验证群名
  163. if not group_name:
  164. return ""
  165. if ('ALL_GROUP' in conf().get('group_name_white_list') or group_name in conf().get('group_name_white_list') or check_contain(group_name, conf().get('group_name_keyword_white_list'))):
  166. context = Context(ContextType.VOICE,msg['FileName'])
  167. context.kwargs = {'isgroup': True, 'msg': msg, 'receiver': group_id}
  168. group_chat_in_one_session = conf().get('group_chat_in_one_session', [])
  169. if ('ALL_GROUP' in group_chat_in_one_session or
  170. group_name in group_chat_in_one_session or
  171. check_contain(group_name, group_chat_in_one_session)):
  172. context['session_id'] = group_id
  173. else:
  174. context['session_id'] = msg['ActualUserName']
  175. thread_pool.submit(self.handle, context).add_done_callback(thread_pool_callback)
  176. # 统一的发送函数,每个Channel自行实现,根据reply的type字段发送不同类型的消息
  177. def send(self, reply : Reply, receiver):
  178. if reply.type == ReplyType.TEXT:
  179. itchat.send(reply.content, toUserName=receiver)
  180. logger.info('[WX] sendMsg={}, receiver={}'.format(reply, receiver))
  181. elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO:
  182. itchat.send(reply.content, toUserName=receiver)
  183. logger.info('[WX] sendMsg={}, receiver={}'.format(reply, receiver))
  184. elif reply.type == ReplyType.VOICE:
  185. itchat.send_file(reply.content, toUserName=receiver)
  186. logger.info('[WX] sendFile={}, receiver={}'.format(reply.content, receiver))
  187. elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
  188. img_url = reply.content
  189. pic_res = requests.get(img_url, stream=True)
  190. image_storage = io.BytesIO()
  191. for block in pic_res.iter_content(1024):
  192. image_storage.write(block)
  193. image_storage.seek(0)
  194. itchat.send_image(image_storage, toUserName=receiver)
  195. logger.info('[WX] sendImage url={}, receiver={}'.format(img_url,receiver))
  196. elif reply.type == ReplyType.IMAGE: # 从文件读取图片
  197. image_storage = reply.content
  198. image_storage.seek(0)
  199. itchat.send_image(image_storage, toUserName=receiver)
  200. logger.info('[WX] sendImage, receiver={}'.format(receiver))
  201. # 处理消息 TODO: 如果wechaty解耦,此处逻辑可以放置到父类
  202. def handle(self, context):
  203. reply = Reply()
  204. logger.debug('[WX] ready to handle context: {}'.format(context))
  205. # reply的构建步骤
  206. e_context = PluginManager().emit_event(EventContext(Event.ON_HANDLE_CONTEXT, {'channel' : self, 'context': context, 'reply': reply}))
  207. reply = e_context['reply']
  208. if not e_context.is_pass():
  209. logger.debug('[WX] ready to handle context: type={}, content={}'.format(context.type, context.content))
  210. if context.type == ContextType.TEXT or context.type == ContextType.IMAGE_CREATE: # 文字和图片消息
  211. reply = super().build_reply_content(context.content, context)
  212. elif context.type == ContextType.VOICE: # 语音消息
  213. msg = context['msg']
  214. file_name = TmpDir().path() + context.content
  215. msg.download(file_name)
  216. reply = super().build_voice_to_text(file_name)
  217. if reply.type != ReplyType.ERROR and reply.type != ReplyType.INFO:
  218. context.content = reply.content # 语音转文字后,将文字内容作为新的context
  219. # 如果是群消息,判断是否触发关键字
  220. if context['isgroup']:
  221. match_prefix = check_prefix(context.content, conf().get('group_chat_prefix')) or check_contain(context.content, conf().get('group_chat_keyword'))
  222. if match_prefix != True:
  223. return
  224. context.type = ContextType.TEXT
  225. reply = super().build_reply_content(context.content, context)
  226. if reply.type == ReplyType.TEXT:
  227. if conf().get('voice_reply_voice'):
  228. if context['isgroup']:
  229. reply.content = '@' + context['msg']['ActualNickName'] + ' ' + reply.content
  230. reply = super().build_text_to_voice(reply.content)
  231. else:
  232. logger.error('[WX] unknown context type: {}'.format(context.type))
  233. return
  234. logger.debug('[WX] ready to decorate reply: {}'.format(reply))
  235. # reply的包装步骤
  236. if reply and reply.type:
  237. e_context = PluginManager().emit_event(EventContext(Event.ON_DECORATE_REPLY, {'channel' : self, 'context': context, 'reply': reply}))
  238. reply=e_context['reply']
  239. if not e_context.is_pass() and reply and reply.type:
  240. if reply.type == ReplyType.TEXT:
  241. reply_text = reply.content
  242. if context['isgroup']:
  243. reply_text = '@' + context['msg']['ActualNickName'] + ' ' + reply_text.strip()
  244. reply_text = conf().get("group_chat_reply_prefix", "")+reply_text
  245. else:
  246. reply_text = conf().get("single_chat_reply_prefix", "")+reply_text
  247. reply.content = reply_text
  248. elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO:
  249. reply.content = str(reply.type)+":\n" + reply.content
  250. elif reply.type == ReplyType.IMAGE_URL or reply.type == ReplyType.VOICE or reply.type == ReplyType.IMAGE:
  251. pass
  252. else:
  253. logger.error('[WX] unknown reply type: {}'.format(reply.type))
  254. return
  255. # reply的发送步骤
  256. if reply and reply.type:
  257. e_context = PluginManager().emit_event(EventContext(Event.ON_SEND_REPLY, {'channel' : self, 'context': context, 'reply': reply}))
  258. reply=e_context['reply']
  259. if not e_context.is_pass() and reply and reply.type:
  260. logger.debug('[WX] ready to send reply: {} to {}'.format(reply, context['receiver']))
  261. self.send(reply, context['receiver'])
  262. def check_prefix(content, prefix_list):
  263. for prefix in prefix_list:
  264. if content.startswith(prefix):
  265. return prefix
  266. return None
  267. def check_contain(content, keyword_list):
  268. if not keyword_list:
  269. return None
  270. for ky in keyword_list:
  271. if content.find(ky) != -1:
  272. return True
  273. return None