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.

wechat_channel.py 12KB

7 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
10 miesięcy temu
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. # encoding:utf-8
  2. """
  3. wechat channel
  4. """
  5. import io
  6. import json
  7. import os
  8. import threading
  9. import time
  10. import requests
  11. from bridge.context import *
  12. from bridge.reply import *
  13. from channel.chat_channel import ChatChannel
  14. from channel import chat_channel
  15. from channel.wechat.wechat_message import *
  16. from common.expired_dict import ExpiredDict
  17. from common.log import logger
  18. from common.singleton import singleton
  19. from common.time_check import time_checker
  20. from config import conf, get_appdata_dir
  21. from lib import itchat
  22. from lib.itchat.content import *
  23. @itchat.msg_register([TEXT, VOICE, PICTURE, NOTE, ATTACHMENT, SHARING])
  24. def handler_single_msg(msg):
  25. try:
  26. cmsg = WechatMessage(msg, False)
  27. except NotImplementedError as e:
  28. logger.debug("[WX]single message {} skipped: {}".format(msg["MsgId"], e))
  29. return None
  30. WechatChannel().handle_single(cmsg)
  31. return None
  32. @itchat.msg_register([TEXT, VOICE, PICTURE, NOTE, ATTACHMENT, SHARING], isGroupChat=True)
  33. def handler_group_msg(msg):
  34. try:
  35. cmsg = WechatMessage(msg, True)
  36. except NotImplementedError as e:
  37. logger.debug("[WX]group message {} skipped: {}".format(msg["MsgId"], e))
  38. return None
  39. WechatChannel().handle_group(cmsg)
  40. return None
  41. def _check(func):
  42. def wrapper(self, cmsg: ChatMessage):
  43. msgId = cmsg.msg_id
  44. if msgId in self.receivedMsgs:
  45. logger.info("Wechat message {} already received, ignore".format(msgId))
  46. return
  47. self.receivedMsgs[msgId] = True
  48. create_time = cmsg.create_time # 消息时间戳
  49. if conf().get("hot_reload") == True and int(create_time) < int(time.time()) - 60: # 跳过1分钟前的历史消息
  50. logger.debug("[WX]history message {} skipped".format(msgId))
  51. return
  52. if cmsg.my_msg and not cmsg.is_group:
  53. logger.debug("[WX]my message {} skipped".format(msgId))
  54. return
  55. return func(self, cmsg)
  56. return wrapper
  57. # 可用的二维码生成接口
  58. # https://api.qrserver.com/v1/create-qr-code/?size=400×400&data=https://www.abc.com
  59. # https://api.isoyu.com/qr/?m=1&e=L&p=20&url=https://www.abc.com
  60. def qrCallback(uuid, status, qrcode):
  61. # logger.debug("qrCallback: {} {}".format(uuid,status))
  62. if status == "0":
  63. try:
  64. from PIL import Image
  65. img = Image.open(io.BytesIO(qrcode))
  66. _thread = threading.Thread(target=img.show, args=("QRCode",))
  67. _thread.setDaemon(True)
  68. _thread.start()
  69. except Exception as e:
  70. pass
  71. import qrcode
  72. url = f"https://login.weixin.qq.com/l/{uuid}"
  73. qr_api1 = "https://api.isoyu.com/qr/?m=1&e=L&p=20&url={}".format(url)
  74. qr_api2 = "https://api.qrserver.com/v1/create-qr-code/?size=400×400&data={}".format(url)
  75. qr_api3 = "https://api.pwmqr.com/qrcode/create/?url={}".format(url)
  76. qr_api4 = "https://my.tv.sohu.com/user/a/wvideo/getQRCode.do?text={}".format(url)
  77. print("You can also scan QRCode in any website below:")
  78. print(qr_api3)
  79. print(qr_api4)
  80. print(qr_api2)
  81. print(qr_api1)
  82. _send_qr_code([qr_api3, qr_api4, qr_api2, qr_api1])
  83. qr = qrcode.QRCode(border=1)
  84. qr.add_data(url)
  85. qr.make(fit=True)
  86. qr.print_ascii(invert=True)
  87. @singleton
  88. class WechatChannel(ChatChannel):
  89. NOT_SUPPORT_REPLYTYPE = []
  90. def __init__(self):
  91. super().__init__()
  92. self.receivedMsgs = ExpiredDict(conf().get("expires_in_seconds"))
  93. self.auto_login_times = 0
  94. def startup(self):
  95. try:
  96. itchat.instance.receivingRetryCount = 600 # 修改断线超时时间
  97. # login by scan QRCode
  98. hotReload = conf().get("hot_reload", False)
  99. status_path = os.path.join(get_appdata_dir(), "itchat.pkl")
  100. itchat.auto_login(
  101. enableCmdQR=2,
  102. hotReload=hotReload,
  103. statusStorageDir=status_path,
  104. qrCallback=qrCallback,
  105. exitCallback=self.exitCallback,
  106. loginCallback=self.loginCallback
  107. )
  108. self.user_id = itchat.instance.storageClass.userName
  109. self.name = itchat.instance.storageClass.nickName
  110. logger.info("Wechat login success, user_id: {}, nickname: {}".format(self.user_id, self.name))
  111. # start message listener
  112. itchat.run()
  113. except Exception as e:
  114. logger.exception(e)
  115. def exitCallback(self):
  116. try:
  117. from common.linkai_client import chat_client
  118. if chat_client.client_id and conf().get("use_linkai"):
  119. _send_logout()
  120. time.sleep(2)
  121. self.auto_login_times += 1
  122. if self.auto_login_times < 100:
  123. chat_channel.handler_pool._shutdown = False
  124. self.startup()
  125. except Exception as e:
  126. pass
  127. def loginCallback(self):
  128. logger.debug("Login success")
  129. _send_login_success()
  130. # handle_* 系列函数处理收到的消息后构造Context,然后传入produce函数中处理Context和发送回复
  131. # Context包含了消息的所有信息,包括以下属性
  132. # type 消息类型, 包括TEXT、VOICE、IMAGE_CREATE
  133. # content 消息内容,如果是TEXT类型,content就是文本内容,如果是VOICE类型,content就是语音文件名,如果是IMAGE_CREATE类型,content就是图片生成命令
  134. # kwargs 附加参数字典,包含以下的key:
  135. # session_id: 会话id
  136. # isgroup: 是否是群聊
  137. # receiver: 需要回复的对象
  138. # msg: ChatMessage消息对象
  139. # origin_ctype: 原始消息类型,语音转文字后,私聊时如果匹配前缀失败,会根据初始消息是否是语音来放宽触发规则
  140. # desire_rtype: 希望回复类型,默认是文本回复,设置为ReplyType.VOICE是语音回复
  141. @time_checker
  142. @_check
  143. def handle_single(self, cmsg: ChatMessage):
  144. # filter system message
  145. if cmsg.other_user_id in ["weixin"]:
  146. return
  147. if cmsg.ctype == ContextType.VOICE:
  148. if conf().get("speech_recognition") != True:
  149. return
  150. logger.debug("[WX]receive voice msg: {}".format(cmsg.content))
  151. elif cmsg.ctype == ContextType.IMAGE:
  152. logger.debug("[WX]receive image msg: {}".format(cmsg.content))
  153. elif cmsg.ctype == ContextType.PATPAT:
  154. logger.debug("[WX]receive patpat msg: {}".format(cmsg.content))
  155. elif cmsg.ctype == ContextType.TEXT:
  156. logger.debug("[WX]receive text msg: {}, cmsg={}".format(json.dumps(cmsg._rawmsg, ensure_ascii=False), cmsg))
  157. else:
  158. logger.debug("[WX]receive msg: {}, cmsg={}".format(cmsg.content, cmsg))
  159. context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=False, msg=cmsg)
  160. if context:
  161. self.produce(context)
  162. @time_checker
  163. @_check
  164. def handle_group(self, cmsg: ChatMessage):
  165. if cmsg.ctype == ContextType.VOICE:
  166. if conf().get("group_speech_recognition") != True:
  167. return
  168. logger.debug("[WX]receive voice for group msg: {}".format(cmsg.content))
  169. elif cmsg.ctype == ContextType.IMAGE:
  170. logger.debug("[WX]receive image for group msg: {}".format(cmsg.content))
  171. elif cmsg.ctype in [ContextType.JOIN_GROUP, ContextType.PATPAT, ContextType.ACCEPT_FRIEND, ContextType.EXIT_GROUP]:
  172. logger.debug("[WX]receive note msg: {}".format(cmsg.content))
  173. elif cmsg.ctype == ContextType.TEXT:
  174. # logger.debug("[WX]receive group msg: {}, cmsg={}".format(json.dumps(cmsg._rawmsg, ensure_ascii=False), cmsg))
  175. pass
  176. elif cmsg.ctype == ContextType.FILE:
  177. logger.debug(f"[WX]receive attachment msg, file_name={cmsg.content}")
  178. else:
  179. logger.debug("[WX]receive group msg: {}".format(cmsg.content))
  180. context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=True, msg=cmsg)
  181. if context:
  182. self.produce(context)
  183. # 统一的发送函数,每个Channel自行实现,根据reply的type字段发送不同类型的消息
  184. def send(self, reply: Reply, context: Context):
  185. receiver = context["receiver"]
  186. if reply.type == ReplyType.TEXT:
  187. itchat.send(reply.content, toUserName=receiver)
  188. logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver))
  189. elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO:
  190. itchat.send(reply.content, toUserName=receiver)
  191. logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver))
  192. elif reply.type == ReplyType.VOICE:
  193. itchat.send_file(reply.content, toUserName=receiver)
  194. logger.info("[WX] sendFile={}, receiver={}".format(reply.content, receiver))
  195. elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
  196. img_url = reply.content
  197. logger.debug(f"[WX] start download image, img_url={img_url}")
  198. pic_res = requests.get(img_url, stream=True)
  199. image_storage = io.BytesIO()
  200. size = 0
  201. for block in pic_res.iter_content(1024):
  202. size += len(block)
  203. image_storage.write(block)
  204. logger.info(f"[WX] download image success, size={size}, img_url={img_url}")
  205. image_storage.seek(0)
  206. itchat.send_image(image_storage, toUserName=receiver)
  207. logger.info("[WX] sendImage url={}, receiver={}".format(img_url, receiver))
  208. elif reply.type == ReplyType.IMAGE: # 从文件读取图片
  209. image_storage = reply.content
  210. image_storage.seek(0)
  211. itchat.send_image(image_storage, toUserName=receiver)
  212. logger.info("[WX] sendImage, receiver={}".format(receiver))
  213. elif reply.type == ReplyType.FILE: # 新增文件回复类型
  214. file_storage = reply.content
  215. itchat.send_file(file_storage, toUserName=receiver)
  216. logger.info("[WX] sendFile, receiver={}".format(receiver))
  217. elif reply.type == ReplyType.VIDEO: # 新增视频回复类型
  218. video_storage = reply.content
  219. itchat.send_video(video_storage, toUserName=receiver)
  220. logger.info("[WX] sendFile, receiver={}".format(receiver))
  221. elif reply.type == ReplyType.VIDEO_URL: # 新增视频URL回复类型
  222. video_url = reply.content
  223. logger.debug(f"[WX] start download video, video_url={video_url}")
  224. video_res = requests.get(video_url, stream=True)
  225. video_storage = io.BytesIO()
  226. size = 0
  227. for block in video_res.iter_content(1024):
  228. size += len(block)
  229. video_storage.write(block)
  230. logger.info(f"[WX] download video success, size={size}, video_url={video_url}")
  231. video_storage.seek(0)
  232. itchat.send_video(video_storage, toUserName=receiver)
  233. logger.info("[WX] sendVideo url={}, receiver={}".format(video_url, receiver))
  234. def _send_login_success():
  235. try:
  236. from common.linkai_client import chat_client
  237. if chat_client.client_id:
  238. chat_client.send_login_success()
  239. except Exception as e:
  240. pass
  241. def _send_logout():
  242. try:
  243. from common.linkai_client import chat_client
  244. if chat_client.client_id:
  245. chat_client.send_logout()
  246. except Exception as e:
  247. pass
  248. def _send_qr_code(qrcode_list: list):
  249. try:
  250. from common.linkai_client import chat_client
  251. if chat_client.client_id:
  252. chat_client.send_qrcode(qrcode_list)
  253. except Exception as e:
  254. pass