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.

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