Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

258 rindas
11KB

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