選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

wechatmp_channel.py 9.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. # -*- coding: utf-8 -*-
  2. import web
  3. import time
  4. import math
  5. import hashlib
  6. import textwrap
  7. from channel.chat_channel import ChatChannel
  8. import channel.wechatmp.reply as reply
  9. import channel.wechatmp.receive as receive
  10. from common.singleton import singleton
  11. from common.log import logger
  12. from config import conf
  13. from bridge.reply import *
  14. from bridge.context import *
  15. from plugins import *
  16. import traceback
  17. # If using SSL, uncomment the following lines, and modify the certificate path.
  18. # from cheroot.server import HTTPServer
  19. # from cheroot.ssl.builtin import BuiltinSSLAdapter
  20. # HTTPServer.ssl_adapter = BuiltinSSLAdapter(
  21. # certificate='/ssl/cert.pem',
  22. # private_key='/ssl/cert.key')
  23. # from concurrent.futures import ThreadPoolExecutor
  24. # thread_pool = ThreadPoolExecutor(max_workers=8)
  25. @singleton
  26. class WechatMPChannel(ChatChannel):
  27. def __init__(self):
  28. super().__init__()
  29. self.cache_dict = dict()
  30. self.query1 = dict()
  31. self.query2 = dict()
  32. self.query3 = dict()
  33. def startup(self):
  34. urls = (
  35. '/wx', 'SubsribeAccountQuery',
  36. )
  37. app = web.application(urls, globals())
  38. app.run()
  39. def send(self, reply: Reply, context: Context):
  40. reply_cnt = math.ceil(len(reply.content) / 600)
  41. receiver = context["receiver"]
  42. self.cache_dict[receiver] = (reply_cnt, reply.content)
  43. logger.debug("[send] reply to {} saved to cache: {}".format(receiver, reply))
  44. def verify_server():
  45. try:
  46. data = web.input()
  47. if len(data) == 0:
  48. return "None"
  49. signature = data.signature
  50. timestamp = data.timestamp
  51. nonce = data.nonce
  52. echostr = data.echostr
  53. token = conf().get('wechatmp_token') #请按照公众平台官网\基本配置中信息填写
  54. data_list = [token, timestamp, nonce]
  55. data_list.sort()
  56. sha1 = hashlib.sha1()
  57. # map(sha1.update, data_list) #python2
  58. sha1.update("".join(data_list).encode('utf-8'))
  59. hashcode = sha1.hexdigest()
  60. print("handle/GET func: hashcode, signature: ", hashcode, signature)
  61. if hashcode == signature:
  62. return echostr
  63. else:
  64. return ""
  65. except Exception as Argument:
  66. return Argument
  67. # This class is instantiated once per query
  68. class SubsribeAccountQuery():
  69. def GET(self):
  70. return verify_server()
  71. def POST(self):
  72. channel_instance = WechatMPChannel()
  73. try:
  74. query_time = time.time()
  75. webData = web.data()
  76. # logger.debug("[wechatmp] Receive request:\n" + webData.decode("utf-8"))
  77. wechat_msg = receive.parse_xml(webData)
  78. if wechat_msg.msg_type == 'text':
  79. from_user = wechat_msg.from_user_id
  80. to_user = wechat_msg.to_user_id
  81. message = wechat_msg.content.decode("utf-8")
  82. message_id = wechat_msg.msg_id
  83. logger.info("[wechatmp] {}:{} Receive post query {} {}: {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), from_user, message_id, message))
  84. cache_key = from_user
  85. cache = channel_instance.cache_dict.get(cache_key)
  86. reply_text = ""
  87. # New request
  88. if cache == None:
  89. # The first query begin, reset the cache
  90. channel_instance.cache_dict[cache_key] = (0, "")
  91. context = channel_instance._compose_context(ContextType.TEXT, message, isgroup=False, msg=wechat_msg)
  92. if context:
  93. # set private openai_api_key
  94. # if from_user is not changed in itchat, this can be placed at chat_channel
  95. user_data = conf().get_user_data(from_user)
  96. context['openai_api_key'] = user_data.get('openai_api_key') # None or user openai_api_key
  97. channel_instance.produce(context)
  98. channel_instance.query1[cache_key] = False
  99. channel_instance.query2[cache_key] = False
  100. channel_instance.query3[cache_key] = False
  101. # Request again
  102. elif cache[0] == 0 and channel_instance.query1.get(cache_key) == True and channel_instance.query2.get(cache_key) == True and channel_instance.query3.get(cache_key) == True:
  103. channel_instance.query1[cache_key] = False #To improve waiting experience, this can be set to True.
  104. channel_instance.query2[cache_key] = False #To improve waiting experience, this can be set to True.
  105. channel_instance.query3[cache_key] = False
  106. elif cache[0] >= 1:
  107. # Skip the waiting phase
  108. channel_instance.query1[cache_key] = True
  109. channel_instance.query2[cache_key] = True
  110. channel_instance.query3[cache_key] = True
  111. cache = channel_instance.cache_dict.get(cache_key)
  112. if channel_instance.query1.get(cache_key) == False:
  113. # The first query from wechat official server
  114. logger.debug("[wechatmp] query1 {}".format(cache_key))
  115. channel_instance.query1[cache_key] = True
  116. cnt = 0
  117. while cache[0] == 0 and cnt < 45:
  118. cnt = cnt + 1
  119. time.sleep(0.1)
  120. cache = channel_instance.cache_dict.get(cache_key)
  121. if cnt == 45:
  122. # waiting for timeout (the POST query will be closed by wechat official server)
  123. time.sleep(5)
  124. # and do nothing
  125. return
  126. else:
  127. pass
  128. elif channel_instance.query2.get(cache_key) == False:
  129. # The second query from wechat official server
  130. logger.debug("[wechatmp] query2 {}".format(cache_key))
  131. channel_instance.query2[cache_key] = True
  132. cnt = 0
  133. while cache[0] == 0 and cnt < 45:
  134. cnt = cnt + 1
  135. time.sleep(0.1)
  136. cache = channel_instance.cache_dict.get(cache_key)
  137. if cnt == 45:
  138. # waiting for timeout (the POST query will be closed by wechat official server)
  139. time.sleep(5)
  140. # and do nothing
  141. return
  142. else:
  143. pass
  144. elif channel_instance.query3.get(cache_key) == False:
  145. # The third query from wechat official server
  146. logger.debug("[wechatmp] query3 {}".format(cache_key))
  147. channel_instance.query3[cache_key] = True
  148. cnt = 0
  149. while cache[0] == 0 and cnt < 45:
  150. cnt = cnt + 1
  151. time.sleep(0.1)
  152. cache = channel_instance.cache_dict.get(cache_key)
  153. if cnt == 45:
  154. # Have waiting for 3x5 seconds
  155. # return timeout message
  156. reply_text = "【正在响应中,回复任意文字尝试获取回复】"
  157. logger.info("[wechatmp] Three queries has finished For {}: {}".format(from_user, message_id))
  158. replyPost = reply.TextMsg(from_user, to_user, reply_text).send()
  159. return replyPost
  160. else:
  161. pass
  162. if float(time.time()) - float(query_time) > 4.8:
  163. logger.info("[wechatmp] Timeout for {} {}".format(from_user, message_id))
  164. return
  165. if cache[0] > 1:
  166. reply_text = cache[1][:600] + "\n【未完待续,回复任意文字以继续】" #wechatmp auto_reply length limit
  167. channel_instance.cache_dict[cache_key] = (cache[0] - 1, cache[1][600:])
  168. elif cache[0] == 1:
  169. reply_text = cache[1]
  170. channel_instance.cache_dict.pop(cache_key)
  171. logger.info("[wechatmp] {}:{} Do send {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), reply_text))
  172. replyPost = reply.TextMsg(from_user, to_user, reply_text).send()
  173. return replyPost
  174. elif wechat_msg.msg_type == 'event':
  175. logger.info("[wechatmp] Event {} from {}".format(wechat_msg.Event, wechat_msg.from_user_id))
  176. content = textwrap.dedent("""\
  177. 感谢您的关注!
  178. 这里是ChatGPT,可以自由对话。
  179. 资源有限,回复较慢,请勿着急。
  180. 支持通用表情输入。
  181. 暂时不支持图片输入。
  182. 支持图片输出,画字开头的问题将回复图片链接。
  183. 支持角色扮演和文字冒险两种定制模式对话。
  184. 输入'#帮助' 查看详细指令。""")
  185. replyMsg = reply.TextMsg(wechat_msg.from_user_id, wechat_msg.to_user_id, content)
  186. return replyMsg.send()
  187. else:
  188. logger.info("暂且不处理")
  189. return "success"
  190. except Exception as exc:
  191. logger.exception(exc)
  192. return exc