201 line
8.7KB

  1. import time
  2. import asyncio
  3. import web
  4. from channel.wechatmp.wechatmp_message import WeChatMPMessage
  5. from bridge.context import *
  6. from channel.wechatmp.common import *
  7. from channel.wechatmp.wechatmp_channel import WechatMPChannel
  8. from common.log import logger
  9. from config import conf
  10. from wechatpy import parse_message
  11. from wechatpy.replies import create_reply, ImageReply, VoiceReply
  12. # This class is instantiated once per query
  13. class Query:
  14. def GET(self):
  15. return verify_server(web.input())
  16. def POST(self):
  17. try:
  18. request_time = time.time()
  19. channel = WechatMPChannel()
  20. message = web.data() # todo crypto
  21. msg = parse_message(message)
  22. logger.debug("[wechatmp] Receive post data:\n" + message.decode("utf-8"))
  23. if msg.type == "event":
  24. logger.info(
  25. "[wechatmp] Event {} from {}".format(
  26. msg.event, msg.source
  27. )
  28. )
  29. reply_text = subscribe_msg()
  30. replyPost = create_reply(reply_text, msg)
  31. return replyPost.render()
  32. wechatmp_msg = WeChatMPMessage(msg, client=channel.client)
  33. if wechatmp_msg.ctype in [ContextType.TEXT, ContextType.IMAGE, ContextType.VOICE]:
  34. from_user = wechatmp_msg.from_user_id
  35. content = wechatmp_msg.content
  36. message_id = wechatmp_msg.msg_id
  37. supported = True
  38. if "【收到不支持的消息类型,暂无法显示】" in content:
  39. supported = False # not supported, used to refresh
  40. # New request
  41. if (
  42. from_user not in channel.cache_dict
  43. and from_user not in channel.running
  44. or content.startswith("#")
  45. and message_id not in channel.request_cnt # insert the godcmd
  46. ):
  47. # The first query begin
  48. if (msg.type == "voice" and wechatmp_msg.ctype == ContextType.TEXT):
  49. origin_ctype = ContextType.VOICE
  50. context = channel._compose_context(
  51. wechatmp_msg.ctype, content, isgroup=False, origin_ctype=origin_ctype, msg=wechatmp_msg
  52. )
  53. else:
  54. context = channel._compose_context(
  55. wechatmp_msg.ctype, content, isgroup=False, msg=wechatmp_msg
  56. )
  57. logger.debug(
  58. "[wechatmp] context: {} {} {}".format(context, wechatmp_msg, supported)
  59. )
  60. if supported and context:
  61. # set private openai_api_key
  62. # if from_user is not changed in itchat, this can be placed at chat_channel
  63. user_data = conf().get_user_data(from_user)
  64. context["openai_api_key"] = user_data.get("openai_api_key")
  65. channel.running.add(from_user)
  66. channel.produce(context)
  67. else:
  68. trigger_prefix = conf().get("single_chat_prefix", [""])
  69. if trigger_prefix or not supported:
  70. if trigger_prefix:
  71. reply_text = textwrap.dedent(
  72. f"""\
  73. 请输入'{trigger_prefix}'接你想说的话跟我说话。
  74. 例如:
  75. {trigger_prefix}你好,很高兴见到你。"""
  76. )
  77. else:
  78. reply_text = textwrap.dedent(
  79. """\
  80. 你好,很高兴见到你。
  81. 请跟我说话吧。"""
  82. )
  83. else:
  84. logger.error(f"[wechatmp] unknown error")
  85. reply_text = textwrap.dedent(
  86. """\
  87. 未知错误,请稍后再试"""
  88. )
  89. replyPost = create_reply(reply_text, msg)
  90. return replyPost.render()
  91. # Wechat official server will request 3 times (5 seconds each), with the same message_id.
  92. # Because the interval is 5 seconds, here assumed that do not have multithreading problems.
  93. request_cnt = channel.request_cnt.get(message_id, 0) + 1
  94. channel.request_cnt[message_id] = request_cnt
  95. logger.info(
  96. "[wechatmp] Request {} from {} {}\n{}\n{}:{}".format(
  97. request_cnt,
  98. from_user,
  99. message_id,
  100. content,
  101. web.ctx.env.get("REMOTE_ADDR"),
  102. web.ctx.env.get("REMOTE_PORT"),
  103. )
  104. )
  105. task_running = True
  106. waiting_until = request_time + 4
  107. while time.time() < waiting_until:
  108. if from_user in channel.running:
  109. time.sleep(0.1)
  110. else:
  111. task_running = False
  112. break
  113. reply_text = ""
  114. if task_running:
  115. if request_cnt < 3:
  116. # waiting for timeout (the POST request will be closed by Wechat official server)
  117. time.sleep(2)
  118. # and do nothing, waiting for the next request
  119. return "success"
  120. else: # request_cnt == 3:
  121. # return timeout message
  122. reply_text = "【正在思考中,回复任意文字尝试获取回复】"
  123. replyPost = create_reply(reply_text, msg)
  124. return replyPost.render()
  125. # reply is ready
  126. channel.request_cnt.pop(message_id)
  127. # no return because of bandwords or other reasons
  128. if (
  129. from_user not in channel.cache_dict
  130. and from_user not in channel.running
  131. ):
  132. return "success"
  133. # Only one request can access to the cached data
  134. try:
  135. (reply_type, content) = channel.cache_dict.pop(from_user)
  136. except KeyError:
  137. return "success"
  138. if (reply_type == "text"):
  139. if len(content.encode("utf8")) <= MAX_UTF8_LEN:
  140. reply_text = content
  141. else:
  142. continue_text = "\n【未完待续,回复任意文字以继续】"
  143. splits = split_string_by_utf8_length(
  144. content,
  145. MAX_UTF8_LEN - len(continue_text.encode("utf-8")),
  146. max_split=1,
  147. )
  148. reply_text = splits[0] + continue_text
  149. channel.cache_dict[from_user] = ("text", splits[1])
  150. logger.info(
  151. "[wechatmp] Request {} do send to {} {}: {}\n{}".format(
  152. request_cnt,
  153. from_user,
  154. message_id,
  155. content,
  156. reply_text,
  157. )
  158. )
  159. replyPost = create_reply(reply_text, msg)
  160. return replyPost.render()
  161. elif (reply_type == "voice"):
  162. media_id = content
  163. asyncio.run_coroutine_threadsafe(channel.delete_media(media_id), channel.delete_media_loop)
  164. replyPost = VoiceReply(message=msg)
  165. replyPost.media_id = media_id
  166. return replyPost.render()
  167. elif (reply_type == "image"):
  168. media_id = content
  169. asyncio.run_coroutine_threadsafe(channel.delete_media(media_id), channel.delete_media_loop)
  170. replyPost = ImageReply(message=msg)
  171. replyPost.media_id = media_id
  172. return replyPost.render()
  173. else:
  174. logger.info("暂且不处理")
  175. return "success"
  176. except Exception as exc:
  177. logger.exception(exc)
  178. return exc