Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

167 lines
8.2KB

  1. # -*- coding: utf-8 -*-
  2. import io
  3. import os
  4. import time
  5. import imghdr
  6. import requests
  7. from bridge.context import *
  8. from bridge.reply import *
  9. from channel.chat_channel import ChatChannel
  10. from channel.wechatmp.wechatmp_client import WechatMPClient
  11. from channel.wechatmp.common import *
  12. from common.log import logger
  13. from common.singleton import singleton
  14. from config import conf
  15. import asyncio
  16. from threading import Thread
  17. import web
  18. # If using SSL, uncomment the following lines, and modify the certificate path.
  19. # from cheroot.server import HTTPServer
  20. # from cheroot.ssl.builtin import BuiltinSSLAdapter
  21. # HTTPServer.ssl_adapter = BuiltinSSLAdapter(
  22. # certificate='/ssl/cert.pem',
  23. # private_key='/ssl/cert.key')
  24. @singleton
  25. class WechatMPChannel(ChatChannel):
  26. def __init__(self, passive_reply=True):
  27. super().__init__()
  28. self.passive_reply = passive_reply
  29. self.NOT_SUPPORT_REPLYTYPE = []
  30. self.client = WechatMPClient()
  31. if self.passive_reply:
  32. # Cache the reply to the user's first message
  33. self.cache_dict = dict()
  34. # Record whether the current message is being processed
  35. self.running = set()
  36. # Count the request from wechat official server by message_id
  37. self.request_cnt = dict()
  38. # The permanent media need to be deleted to avoid media number limit
  39. self.delete_media_loop = asyncio.new_event_loop()
  40. t = Thread(target=self.start_loop, args=(self.delete_media_loop,))
  41. t.setDaemon(True)
  42. t.start()
  43. def startup(self):
  44. if self.passive_reply:
  45. urls = ("/wx", "channel.wechatmp.passive_reply.Query")
  46. else:
  47. urls = ("/wx", "channel.wechatmp.active_reply.Query")
  48. app = web.application(urls, globals(), autoreload=False)
  49. port = conf().get("wechatmp_port", 8080)
  50. web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
  51. def start_loop(self, loop):
  52. asyncio.set_event_loop(loop)
  53. loop.run_forever()
  54. async def delete_media(self, media_id):
  55. logger.debug("[wechatmp] permanent media {} will be deleted in 10s".format(media_id))
  56. await asyncio.sleep(10)
  57. self.client.delete_permanent_media(media_id)
  58. logger.info("[wechatmp] permanent media {} has been deleted".format(media_id))
  59. def send(self, reply: Reply, context: Context):
  60. receiver = context["receiver"]
  61. if self.passive_reply:
  62. if reply.type == ReplyType.TEXT or reply.type == ReplyType.INFO or reply.type == ReplyType.ERROR:
  63. reply_text = reply.content
  64. logger.info("[wechatmp] reply to {} cached:\n{}".format(receiver, reply_text))
  65. self.cache_dict[receiver] = ("text", reply_text)
  66. elif reply.type == ReplyType.VOICE:
  67. voice_file_path = reply.content
  68. logger.info("[wechatmp] voice file path {}".format(voice_file_path))
  69. with open(voice_file_path, 'rb') as f:
  70. filename = receiver + "-" + context["msg"].msg_id + ".mp3"
  71. media_id = self.client.upload_permanent_media("voice", (filename, f, "audio/mpeg"))
  72. # 根据文件大小估计一个微信自动审核的时间,审核结束前返回将会导致语音无法播放,这个估计有待验证
  73. f_size = os.fstat(f.fileno()).st_size
  74. print(f_size)
  75. time.sleep(1.0 + 2 * f_size / 1024 / 1024)
  76. logger.info("[wechatmp] voice reply to {} uploaded: {}".format(receiver, media_id))
  77. self.cache_dict[receiver] = ("voice", media_id)
  78. elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
  79. img_url = reply.content
  80. pic_res = requests.get(img_url, stream=True)
  81. print(pic_res.headers)
  82. image_storage = io.BytesIO()
  83. for block in pic_res.iter_content(1024):
  84. image_storage.write(block)
  85. image_storage.seek(0)
  86. image_type = imghdr.what(image_storage)
  87. filename = receiver + "-" + context["msg"].msg_id + "." + image_type
  88. content_type = "image/" + image_type
  89. media_id = self.client.upload_permanent_media("image", (filename, image_storage, content_type))
  90. logger.info("[wechatmp] image reply to {} uploaded: {}".format(receiver, media_id))
  91. self.cache_dict[receiver] = ("image", media_id)
  92. elif reply.type == ReplyType.IMAGE: # 从文件读取图片
  93. image_storage = reply.content
  94. image_storage.seek(0)
  95. image_type = imghdr.what(image_storage)
  96. filename = receiver + "-" + context["msg"].msg_id + "." + image_type
  97. content_type = "image/" + image_type
  98. media_id = self.client.upload_permanent_media("image", (filename, image_storage, content_type))
  99. logger.info("[wechatmp] image reply to {} uploaded: {}".format(receiver, media_id))
  100. self.cache_dict[receiver] = ("image", media_id)
  101. else:
  102. if reply.type == ReplyType.TEXT or reply.type == ReplyType.INFO or reply.type == ReplyType.ERROR:
  103. reply_text = reply.content
  104. self.client.send_text(receiver, reply_text)
  105. logger.info("[wechatmp] Do send to {}: {}".format(receiver, reply_text))
  106. elif reply.type == ReplyType.VOICE:
  107. voice_file_path = reply.content
  108. logger.info("[wechatmp] voice file path {}".format(voice_file_path))
  109. with open(voice_file_path, 'rb') as f:
  110. filename = receiver + "-" + context["msg"].msg_id + ".mp3"
  111. media_id = self.client.upload_media("voice", (filename, f, "audio/mpeg"))
  112. self.client.send_voice(receiver, media_id)
  113. logger.info("[wechatmp] Do send voice to {}".format(receiver))
  114. elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
  115. img_url = reply.content
  116. pic_res = requests.get(img_url, stream=True)
  117. print(pic_res.headers)
  118. image_storage = io.BytesIO()
  119. for block in pic_res.iter_content(1024):
  120. image_storage.write(block)
  121. image_storage.seek(0)
  122. image_type = imghdr.what(image_storage)
  123. filename = receiver + "-" + context["msg"].msg_id + "." + image_type
  124. content_type = "image/" + image_type
  125. # content_type = pic_res.headers.get('content-type')
  126. media_id = self.client.upload_media("image", (filename, image_storage, content_type))
  127. self.client.send_image(receiver, media_id)
  128. logger.info("[wechatmp] sendImage url={}, receiver={}".format(img_url, receiver))
  129. elif reply.type == ReplyType.IMAGE: # 从文件读取图片
  130. image_storage = reply.content
  131. image_storage.seek(0)
  132. image_type = imghdr.what(image_storage)
  133. filename = receiver + "-" + context["msg"].msg_id + "." + image_type
  134. content_type = "image/" + image_type
  135. media_id = self.client.upload_media("image", (filename, image_storage, content_type))
  136. self.client.send_image(receiver, media_id)
  137. logger.info("[wechatmp] sendImage, receiver={}".format(receiver))
  138. return
  139. def _success_callback(self, session_id, context, **kwargs): # 线程异常结束时的回调函数
  140. logger.debug(
  141. "[wechatmp] Success to generate reply, msgId={}".format(
  142. context["msg"].msg_id
  143. )
  144. )
  145. if self.passive_reply:
  146. self.running.remove(session_id)
  147. def _fail_callback(self, session_id, exception, context, **kwargs): # 线程异常结束时的回调函数
  148. logger.exception(
  149. "[wechatmp] Fail to generate reply to user, msgId={}, exception={}".format(
  150. context["msg"].msg_id, exception
  151. )
  152. )
  153. if self.passive_reply:
  154. assert session_id not in self.cache_dict
  155. self.running.remove(session_id)