You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

230 lines
9.1KB

  1. # encoding:utf-8
  2. """
  3. wechat channel
  4. """
  5. import itchat
  6. import json
  7. from itchat.content import *
  8. from channel.channel import Channel
  9. from concurrent.futures import ThreadPoolExecutor
  10. from common.log import logger
  11. from common.tmp_dir import TmpDir
  12. from config import conf
  13. import requests
  14. import io
  15. thread_pool = ThreadPoolExecutor(max_workers=8)
  16. @itchat.msg_register(TEXT)
  17. def handler_single_msg(msg):
  18. WechatChannel().handle_text(msg)
  19. return None
  20. @itchat.msg_register(TEXT, isGroupChat=True)
  21. def handler_group_msg(msg):
  22. WechatChannel().handle_group(msg)
  23. return None
  24. @itchat.msg_register(VOICE)
  25. def handler_single_voice(msg):
  26. WechatChannel().handle_voice(msg)
  27. return None
  28. class WechatChannel(Channel):
  29. def __init__(self):
  30. pass
  31. def startup(self):
  32. # login by scan QRCode
  33. itchat.auto_login(enableCmdQR=2)
  34. # start message listener
  35. itchat.run()
  36. # handle_* 系列函数处理收到的消息后构造context,然后调用handle函数处理context
  37. # context是一个字典,包含了消息的所有信息,包括以下key
  38. # type: 消息类型,包括TEXT、VOICE、CMD_IMAGE_CREATE
  39. # content: 消息内容,如果是TEXT类型,content就是文本内容,如果是VOICE类型,content就是语音文件名,如果是CMD_IMAGE_CREATE类型,content就是图片生成命令
  40. # session_id: 会话id
  41. # isgroup: 是否是群聊
  42. # msg: 原始消息对象
  43. # receiver: 需要回复的对象
  44. def handle_voice(self, msg):
  45. if conf().get('speech_recognition') != True:
  46. return
  47. logger.debug("[WX]receive voice msg: " + msg['FileName'])
  48. from_user_id = msg['FromUserName']
  49. other_user_id = msg['User']['UserName']
  50. if from_user_id == other_user_id:
  51. context = {'isgroup': False, 'msg': msg, 'receiver': other_user_id}
  52. context['type'] = 'VOICE'
  53. context['session_id'] = other_user_id
  54. thread_pool.submit(self.handle, context)
  55. def handle_text(self, msg):
  56. logger.debug("[WX]receive text msg: " + json.dumps(msg, ensure_ascii=False))
  57. content = msg['Text']
  58. from_user_id = msg['FromUserName']
  59. to_user_id = msg['ToUserName'] # 接收人id
  60. other_user_id = msg['User']['UserName'] # 对手方id
  61. match_prefix = check_prefix(content, conf().get('single_chat_prefix'))
  62. if "」\n- - - - - - - - - - - - - - -" in content:
  63. logger.debug("[WX]reference query skipped")
  64. return
  65. if match_prefix:
  66. content = content.replace(match_prefix, '', 1).strip()
  67. else:
  68. return
  69. context = {'isgroup': False, 'msg': msg, 'receiver': other_user_id}
  70. context['session_id'] = other_user_id
  71. img_match_prefix = check_prefix(content, conf().get('image_create_prefix'))
  72. if img_match_prefix:
  73. content = content.replace(img_match_prefix, '', 1).strip()
  74. context['type'] = 'CMD_IMAGE_CREATE'
  75. else:
  76. context['type'] = 'TEXT'
  77. context['content'] = content
  78. thread_pool.submit(self.handle, context)
  79. def handle_group(self, msg):
  80. logger.debug("[WX]receive group msg: " + json.dumps(msg, ensure_ascii=False))
  81. group_name = msg['User'].get('NickName', None)
  82. group_id = msg['User'].get('UserName', None)
  83. if not group_name:
  84. return ""
  85. origin_content = msg['Content']
  86. content = msg['Content']
  87. content_list = content.split(' ', 1)
  88. context_special_list = content.split('\u2005', 1)
  89. if len(context_special_list) == 2:
  90. content = context_special_list[1]
  91. elif len(content_list) == 2:
  92. content = content_list[1]
  93. if "」\n- - - - - - - - - - - - - - -" in content:
  94. logger.debug("[WX]reference query skipped")
  95. return ""
  96. config = conf()
  97. match_prefix = (msg['IsAt'] and not config.get("group_at_off", False)) or check_prefix(origin_content, config.get('group_chat_prefix')) \
  98. or check_contain(origin_content, config.get('group_chat_keyword'))
  99. 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:
  100. context = { 'isgroup': True, 'msg': msg, 'receiver': group_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'] = 'CMD_IMAGE_CREATE'
  105. else:
  106. context['type'] = 'TEXT'
  107. context['content'] = content
  108. group_chat_in_one_session = conf().get('group_chat_in_one_session', [])
  109. if ('ALL_GROUP' in group_chat_in_one_session or
  110. group_name in group_chat_in_one_session or
  111. check_contain(group_name, group_chat_in_one_session)):
  112. context['session_id'] = group_id
  113. else:
  114. context['session_id'] = msg['ActualUserName']
  115. thread_pool.submit(self.handle, context)
  116. # 统一的发送函数,根据reply的type字段发送不同类型的消息
  117. def send(self, reply, receiver):
  118. if reply['type'] == 'TEXT':
  119. itchat.send(reply['content'], toUserName=receiver)
  120. logger.info('[WX] sendMsg={}, receiver={}'.format(reply, receiver))
  121. elif reply['type'] == 'ERROR' or reply['type'] == 'INFO':
  122. itchat.send(reply['content'], toUserName=receiver)
  123. logger.info('[WX] sendMsg={}, receiver={}'.format(reply, receiver))
  124. elif reply['type'] == 'VOICE':
  125. itchat.send_file(reply['content'], toUserName=receiver)
  126. logger.info('[WX] sendFile={}, receiver={}'.format(reply['content'], receiver))
  127. elif reply['type']=='IMAGE_URL': # 从网络下载图片
  128. img_url = reply['content']
  129. pic_res = requests.get(img_url, stream=True)
  130. image_storage = io.BytesIO()
  131. for block in pic_res.iter_content(1024):
  132. image_storage.write(block)
  133. image_storage.seek(0)
  134. itchat.send_image(image_storage, toUserName=receiver)
  135. logger.info('[WX] sendImage url=, receiver={}'.format(img_url,receiver))
  136. elif reply['type']=='IMAGE': # 从文件读取图片
  137. image_storage = reply['content']
  138. image_storage.seek(0)
  139. itchat.send_image(image_storage, toUserName=receiver)
  140. logger.info('[WX] sendImage, receiver={}'.format(receiver))
  141. # 处理消息
  142. def handle(self, context):
  143. content = context['content']
  144. reply = None
  145. logger.debug('[WX] ready to handle context: {}'.format(context))
  146. # reply的构建步骤
  147. if context['type'] == 'TEXT' or context['type'] == 'CMD_IMAGE_CREATE':
  148. reply = super().build_reply_content(content, context)
  149. elif context['type'] == 'VOICE':
  150. msg = context['msg']
  151. file_name = TmpDir().path() + msg['FileName']
  152. msg.download(file_name)
  153. reply = super().build_voice_to_text(file_name)
  154. if reply['type'] != 'ERROR' and reply['type'] != 'INFO':
  155. reply = super().build_reply_content(reply['content'], context)
  156. if reply['type'] == 'TEXT':
  157. if conf().get('voice_reply_voice'):
  158. reply = super().build_text_to_voice(reply['content'])
  159. else:
  160. logger.error('[WX] unknown context type: {}'.format(context['type']))
  161. return
  162. logger.debug('[WX] ready to decorate reply: {}'.format(reply))
  163. # reply的包装步骤
  164. if reply:
  165. if reply['type'] == 'TEXT':
  166. reply_text = reply['content']
  167. if context['isgroup']:
  168. reply_text = '@' + \
  169. context['msg']['ActualNickName'] + \
  170. ' ' + reply_text.strip()
  171. reply_text = conf().get("group_chat_reply_prefix", "")+reply_text
  172. else:
  173. reply_text = conf().get("single_chat_reply_prefix", "")+reply_text
  174. reply['content'] = reply_text
  175. elif reply['type'] == 'ERROR' or reply['type'] == 'INFO':
  176. reply['content'] = reply['type']+": " + reply['content']
  177. elif reply['type'] == 'IMAGE_URL' or reply['type'] == 'VOICE':
  178. pass
  179. else:
  180. logger.error(
  181. '[WX] unknown reply type: {}'.format(reply['type']))
  182. return
  183. if reply:
  184. logger.debug('[WX] ready to send reply: {} to {}'.format(
  185. reply, context['receiver']))
  186. self.send(reply, context['receiver'])
  187. def check_prefix(content, prefix_list):
  188. for prefix in prefix_list:
  189. if content.startswith(prefix):
  190. return prefix
  191. return None
  192. def check_contain(content, keyword_list):
  193. if not keyword_list:
  194. return None
  195. for ky in keyword_list:
  196. if content.find(ky) != -1:
  197. return True
  198. return None