Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

244 lines
10KB

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