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.

247 lines
11KB

  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['content'] = msg['FileName']
  59. context['session_id'] = other_user_id
  60. thread_pool.submit(self.handle, context).add_done_callback(thread_pool_callback)
  61. def handle_text(self, msg):
  62. logger.debug("[WX]receive text msg: " + json.dumps(msg, ensure_ascii=False))
  63. content = msg['Text']
  64. from_user_id = msg['FromUserName']
  65. to_user_id = msg['ToUserName'] # 接收人id
  66. other_user_id = msg['User']['UserName'] # 对手方id
  67. match_prefix = check_prefix(content, conf().get('single_chat_prefix'))
  68. if "」\n- - - - - - - - - - - - - - -" in content:
  69. logger.debug("[WX]reference query skipped")
  70. return
  71. if match_prefix:
  72. content = content.replace(match_prefix, '', 1).strip()
  73. else:
  74. return
  75. context = {'isgroup': False, 'msg': msg, 'receiver': other_user_id}
  76. context['session_id'] = other_user_id
  77. img_match_prefix = check_prefix(content, conf().get('image_create_prefix'))
  78. if img_match_prefix:
  79. content = content.replace(img_match_prefix, '', 1).strip()
  80. context['type'] = 'IMAGE_CREATE'
  81. else:
  82. context['type'] = 'TEXT'
  83. context['content'] = content
  84. thread_pool.submit(self.handle, context).add_done_callback(thread_pool_callback)
  85. def handle_group(self, msg):
  86. logger.debug("[WX]receive group msg: " + json.dumps(msg, ensure_ascii=False))
  87. group_name = msg['User'].get('NickName', None)
  88. group_id = msg['User'].get('UserName', None)
  89. if not group_name:
  90. return ""
  91. origin_content = msg['Content']
  92. content = msg['Content']
  93. content_list = content.split(' ', 1)
  94. context_special_list = content.split('\u2005', 1)
  95. if len(context_special_list) == 2:
  96. content = context_special_list[1]
  97. elif len(content_list) == 2:
  98. content = content_list[1]
  99. if "」\n- - - - - - - - - - - - - - -" in content:
  100. logger.debug("[WX]reference query skipped")
  101. return ""
  102. config = conf()
  103. match_prefix = (msg['IsAt'] and not config.get("group_at_off", False)) or check_prefix(origin_content, config.get('group_chat_prefix')) \
  104. or check_contain(origin_content, config.get('group_chat_keyword'))
  105. 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:
  106. context = { 'isgroup': True, 'msg': msg, 'receiver': group_id}
  107. img_match_prefix = check_prefix(content, conf().get('image_create_prefix'))
  108. if img_match_prefix:
  109. content = content.replace(img_match_prefix, '', 1).strip()
  110. context['type'] = 'IMAGE_CREATE'
  111. else:
  112. context['type'] = 'TEXT'
  113. context['content'] = content
  114. group_chat_in_one_session = conf().get('group_chat_in_one_session', [])
  115. if ('ALL_GROUP' in group_chat_in_one_session or
  116. group_name in group_chat_in_one_session or
  117. check_contain(group_name, group_chat_in_one_session)):
  118. context['session_id'] = group_id
  119. else:
  120. context['session_id'] = msg['ActualUserName']
  121. thread_pool.submit(self.handle, context).add_done_callback(thread_pool_callback)
  122. # 统一的发送函数,每个Channel自行实现,根据reply的type字段发送不同类型的消息
  123. def send(self, reply, receiver):
  124. if reply['type'] == 'TEXT':
  125. itchat.send(reply['content'], toUserName=receiver)
  126. logger.info('[WX] sendMsg={}, receiver={}'.format(reply, receiver))
  127. elif reply['type'] == 'ERROR' or reply['type'] == 'INFO':
  128. itchat.send(reply['content'], toUserName=receiver)
  129. logger.info('[WX] sendMsg={}, receiver={}'.format(reply, receiver))
  130. elif reply['type'] == 'VOICE':
  131. itchat.send_file(reply['content'], toUserName=receiver)
  132. logger.info('[WX] sendFile={}, receiver={}'.format(reply['content'], receiver))
  133. elif reply['type']=='IMAGE_URL': # 从网络下载图片
  134. img_url = reply['content']
  135. pic_res = requests.get(img_url, stream=True)
  136. image_storage = io.BytesIO()
  137. for block in pic_res.iter_content(1024):
  138. image_storage.write(block)
  139. image_storage.seek(0)
  140. itchat.send_image(image_storage, toUserName=receiver)
  141. logger.info('[WX] sendImage url=, receiver={}'.format(img_url,receiver))
  142. elif reply['type']=='IMAGE': # 从文件读取图片
  143. image_storage = reply['content']
  144. image_storage.seek(0)
  145. itchat.send_image(image_storage, toUserName=receiver)
  146. logger.info('[WX] sendImage, receiver={}'.format(receiver))
  147. # 处理消息 TODO: 如果wechaty解耦,此处逻辑可以放置到父类
  148. def handle(self, context):
  149. reply = {}
  150. logger.debug('[WX] ready to handle context: {}'.format(context))
  151. # reply的构建步骤
  152. e_context = PluginManager().emit_event(EventContext(Event.ON_HANDLE_CONTEXT, {'channel' : self, 'context': context, 'reply': reply}))
  153. reply=e_context['reply']
  154. if not e_context.is_pass():
  155. logger.debug('[WX] ready to handle context: type={}, content={}'.format(context['type'], context['content']))
  156. if context['type'] == 'TEXT' or context['type'] == 'IMAGE_CREATE':
  157. reply = super().build_reply_content(context['content'], context)
  158. elif context['type'] == 'VOICE':
  159. msg = context['msg']
  160. file_name = TmpDir().path() + context['content']
  161. msg.download(file_name)
  162. reply = super().build_voice_to_text(file_name)
  163. if reply['type'] != 'ERROR' and reply['type'] != 'INFO':
  164. context['content'] = reply['content'] # 语音转文字后,将文字内容作为新的context
  165. context['type'] = reply['type']
  166. reply = super().build_reply_content(context['content'], context)
  167. if reply['type'] == 'TEXT':
  168. if conf().get('voice_reply_voice'):
  169. reply = super().build_text_to_voice(reply['content'])
  170. else:
  171. logger.error('[WX] unknown context type: {}'.format(context['type']))
  172. return
  173. logger.debug('[WX] ready to decorate reply: {}'.format(reply))
  174. # reply的包装步骤
  175. if reply and reply['type']:
  176. e_context = PluginManager().emit_event(EventContext(Event.ON_DECORATE_REPLY, {'channel' : self, 'context': context, 'reply': reply}))
  177. reply=e_context['reply']
  178. if not e_context.is_pass() and reply and reply['type']:
  179. if reply['type'] == 'TEXT':
  180. reply_text = reply['content']
  181. if context['isgroup']:
  182. reply_text = '@' + context['msg']['ActualNickName'] + ' ' + reply_text.strip()
  183. reply_text = conf().get("group_chat_reply_prefix", "")+reply_text
  184. else:
  185. reply_text = conf().get("single_chat_reply_prefix", "")+reply_text
  186. reply['content'] = reply_text
  187. elif reply['type'] == 'ERROR' or reply['type'] == 'INFO':
  188. reply['content'] = reply['type']+":\n" + reply['content']
  189. elif reply['type'] == 'IMAGE_URL' or reply['type'] == 'VOICE' or reply['type'] == 'IMAGE':
  190. pass
  191. else:
  192. logger.error('[WX] unknown reply type: {}'.format(reply['type']))
  193. return
  194. # reply的发送步骤
  195. if reply and reply['type']:
  196. e_context = PluginManager().emit_event(EventContext(Event.ON_SEND_REPLY, {'channel' : self, 'context': context, 'reply': reply}))
  197. reply=e_context['reply']
  198. if not e_context.is_pass() and reply and reply['type']:
  199. logger.debug('[WX] ready to send reply: {} to {}'.format(reply, context['receiver']))
  200. self.send(reply, context['receiver'])
  201. def check_prefix(content, prefix_list):
  202. for prefix in prefix_list:
  203. if content.startswith(prefix):
  204. return prefix
  205. return None
  206. def check_contain(content, keyword_list):
  207. if not keyword_list:
  208. return None
  209. for ky in keyword_list:
  210. if content.find(ky) != -1:
  211. return True
  212. return None