mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-20 13:47:15 +08:00
formatting code
This commit is contained in:
@@ -6,8 +6,10 @@ from bridge.bridge import Bridge
|
||||
from bridge.context import Context
|
||||
from bridge.reply import *
|
||||
|
||||
|
||||
class Channel(object):
|
||||
NOT_SUPPORT_REPLYTYPE = [ReplyType.VOICE, ReplyType.IMAGE]
|
||||
|
||||
def startup(self):
|
||||
"""
|
||||
init channel
|
||||
@@ -27,15 +29,15 @@ class Channel(object):
|
||||
send message to user
|
||||
:param msg: message content
|
||||
:param receiver: receiver channel account
|
||||
:return:
|
||||
:return:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def build_reply_content(self, query, context : Context=None) -> Reply:
|
||||
def build_reply_content(self, query, context: Context = None) -> Reply:
|
||||
return Bridge().fetch_reply_content(query, context)
|
||||
|
||||
def build_voice_to_text(self, voice_file) -> Reply:
|
||||
return Bridge().fetch_voice_to_text(voice_file)
|
||||
|
||||
|
||||
def build_text_to_voice(self, text) -> Reply:
|
||||
return Bridge().fetch_text_to_voice(text)
|
||||
|
||||
@@ -2,25 +2,31 @@
|
||||
channel factory
|
||||
"""
|
||||
|
||||
|
||||
def create_channel(channel_type):
|
||||
"""
|
||||
create a channel instance
|
||||
:param channel_type: channel type code
|
||||
:return: channel instance
|
||||
"""
|
||||
if channel_type == 'wx':
|
||||
if channel_type == "wx":
|
||||
from channel.wechat.wechat_channel import WechatChannel
|
||||
|
||||
return WechatChannel()
|
||||
elif channel_type == 'wxy':
|
||||
elif channel_type == "wxy":
|
||||
from channel.wechat.wechaty_channel import WechatyChannel
|
||||
|
||||
return WechatyChannel()
|
||||
elif channel_type == 'terminal':
|
||||
elif channel_type == "terminal":
|
||||
from channel.terminal.terminal_channel import TerminalChannel
|
||||
|
||||
return TerminalChannel()
|
||||
elif channel_type == 'wechatmp':
|
||||
elif channel_type == "wechatmp":
|
||||
from channel.wechatmp.wechatmp_channel import WechatMPChannel
|
||||
return WechatMPChannel(passive_reply = True)
|
||||
elif channel_type == 'wechatmp_service':
|
||||
|
||||
return WechatMPChannel(passive_reply=True)
|
||||
elif channel_type == "wechatmp_service":
|
||||
from channel.wechatmp.wechatmp_channel import WechatMPChannel
|
||||
return WechatMPChannel(passive_reply = False)
|
||||
|
||||
return WechatMPChannel(passive_reply=False)
|
||||
raise RuntimeError
|
||||
|
||||
@@ -1,137 +1,172 @@
|
||||
|
||||
|
||||
from asyncio import CancelledError
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from common.dequeue import Dequeue
|
||||
from channel.channel import Channel
|
||||
from bridge.reply import *
|
||||
from asyncio import CancelledError
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
|
||||
from bridge.context import *
|
||||
from config import conf
|
||||
from bridge.reply import *
|
||||
from channel.channel import Channel
|
||||
from common.dequeue import Dequeue
|
||||
from common.log import logger
|
||||
from config import conf
|
||||
from plugins import *
|
||||
|
||||
try:
|
||||
from voice.audio_convert import any_to_wav
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
|
||||
# 抽象类, 它包含了与消息通道无关的通用处理逻辑
|
||||
class ChatChannel(Channel):
|
||||
name = None # 登录的用户名
|
||||
user_id = None # 登录的用户id
|
||||
futures = {} # 记录每个session_id提交到线程池的future对象, 用于重置会话时把没执行的future取消掉,正在执行的不会被取消
|
||||
sessions = {} # 用于控制并发,每个session_id同时只能有一个context在处理
|
||||
lock = threading.Lock() # 用于控制对sessions的访问
|
||||
name = None # 登录的用户名
|
||||
user_id = None # 登录的用户id
|
||||
futures = {} # 记录每个session_id提交到线程池的future对象, 用于重置会话时把没执行的future取消掉,正在执行的不会被取消
|
||||
sessions = {} # 用于控制并发,每个session_id同时只能有一个context在处理
|
||||
lock = threading.Lock() # 用于控制对sessions的访问
|
||||
handler_pool = ThreadPoolExecutor(max_workers=8) # 处理消息的线程池
|
||||
|
||||
def __init__(self):
|
||||
_thread = threading.Thread(target=self.consume)
|
||||
_thread.setDaemon(True)
|
||||
_thread.start()
|
||||
|
||||
|
||||
# 根据消息构造context,消息内容相关的触发项写在这里
|
||||
def _compose_context(self, ctype: ContextType, content, **kwargs):
|
||||
context = Context(ctype, content)
|
||||
context.kwargs = kwargs
|
||||
# context首次传入时,origin_ctype是None,
|
||||
# context首次传入时,origin_ctype是None,
|
||||
# 引入的起因是:当输入语音时,会嵌套生成两个context,第一步语音转文本,第二步通过文本生成文字回复。
|
||||
# origin_ctype用于第二步文本回复时,判断是否需要匹配前缀,如果是私聊的语音,就不需要匹配前缀
|
||||
if 'origin_ctype' not in context:
|
||||
context['origin_ctype'] = ctype
|
||||
if "origin_ctype" not in context:
|
||||
context["origin_ctype"] = ctype
|
||||
# context首次传入时,receiver是None,根据类型设置receiver
|
||||
first_in = 'receiver' not in context
|
||||
first_in = "receiver" not in context
|
||||
# 群名匹配过程,设置session_id和receiver
|
||||
if first_in: # context首次传入时,receiver是None,根据类型设置receiver
|
||||
if first_in: # context首次传入时,receiver是None,根据类型设置receiver
|
||||
config = conf()
|
||||
cmsg = context['msg']
|
||||
cmsg = context["msg"]
|
||||
if context.get("isgroup", False):
|
||||
group_name = cmsg.other_user_nickname
|
||||
group_id = cmsg.other_user_id
|
||||
|
||||
group_name_white_list = config.get('group_name_white_list', [])
|
||||
group_name_keyword_white_list = config.get('group_name_keyword_white_list', [])
|
||||
if any([group_name in group_name_white_list, 'ALL_GROUP' in group_name_white_list, check_contain(group_name, group_name_keyword_white_list)]):
|
||||
group_chat_in_one_session = conf().get('group_chat_in_one_session', [])
|
||||
group_name_white_list = config.get("group_name_white_list", [])
|
||||
group_name_keyword_white_list = config.get(
|
||||
"group_name_keyword_white_list", []
|
||||
)
|
||||
if any(
|
||||
[
|
||||
group_name in group_name_white_list,
|
||||
"ALL_GROUP" in group_name_white_list,
|
||||
check_contain(group_name, group_name_keyword_white_list),
|
||||
]
|
||||
):
|
||||
group_chat_in_one_session = conf().get(
|
||||
"group_chat_in_one_session", []
|
||||
)
|
||||
session_id = cmsg.actual_user_id
|
||||
if any([group_name in group_chat_in_one_session, 'ALL_GROUP' in group_chat_in_one_session]):
|
||||
if any(
|
||||
[
|
||||
group_name in group_chat_in_one_session,
|
||||
"ALL_GROUP" in group_chat_in_one_session,
|
||||
]
|
||||
):
|
||||
session_id = group_id
|
||||
else:
|
||||
return None
|
||||
context['session_id'] = session_id
|
||||
context['receiver'] = group_id
|
||||
context["session_id"] = session_id
|
||||
context["receiver"] = group_id
|
||||
else:
|
||||
context['session_id'] = cmsg.other_user_id
|
||||
context['receiver'] = cmsg.other_user_id
|
||||
e_context = PluginManager().emit_event(EventContext(Event.ON_RECEIVE_MESSAGE, {'channel': self, 'context': context}))
|
||||
context = e_context['context']
|
||||
context["session_id"] = cmsg.other_user_id
|
||||
context["receiver"] = cmsg.other_user_id
|
||||
e_context = PluginManager().emit_event(
|
||||
EventContext(
|
||||
Event.ON_RECEIVE_MESSAGE, {"channel": self, "context": context}
|
||||
)
|
||||
)
|
||||
context = e_context["context"]
|
||||
if e_context.is_pass() or context is None:
|
||||
return context
|
||||
if cmsg.from_user_id == self.user_id and not config.get('trigger_by_self', True):
|
||||
if cmsg.from_user_id == self.user_id and not config.get(
|
||||
"trigger_by_self", True
|
||||
):
|
||||
logger.debug("[WX]self message skipped")
|
||||
return None
|
||||
|
||||
# 消息内容匹配过程,并处理content
|
||||
if ctype == ContextType.TEXT:
|
||||
if first_in and "」\n- - - - - - -" in content: # 初次匹配 过滤引用消息
|
||||
if first_in and "」\n- - - - - - -" in content: # 初次匹配 过滤引用消息
|
||||
logger.debug("[WX]reference query skipped")
|
||||
return None
|
||||
|
||||
if context.get("isgroup", False): # 群聊
|
||||
|
||||
if context.get("isgroup", False): # 群聊
|
||||
# 校验关键字
|
||||
match_prefix = check_prefix(content, conf().get('group_chat_prefix'))
|
||||
match_contain = check_contain(content, conf().get('group_chat_keyword'))
|
||||
match_prefix = check_prefix(content, conf().get("group_chat_prefix"))
|
||||
match_contain = check_contain(content, conf().get("group_chat_keyword"))
|
||||
flag = False
|
||||
if match_prefix is not None or match_contain is not None:
|
||||
flag = True
|
||||
if match_prefix:
|
||||
content = content.replace(match_prefix, '', 1).strip()
|
||||
if context['msg'].is_at:
|
||||
content = content.replace(match_prefix, "", 1).strip()
|
||||
if context["msg"].is_at:
|
||||
logger.info("[WX]receive group at")
|
||||
if not conf().get("group_at_off", False):
|
||||
flag = True
|
||||
pattern = f'@{self.name}(\u2005|\u0020)'
|
||||
content = re.sub(pattern, r'', content)
|
||||
|
||||
pattern = f"@{self.name}(\u2005|\u0020)"
|
||||
content = re.sub(pattern, r"", content)
|
||||
|
||||
if not flag:
|
||||
if context["origin_ctype"] == ContextType.VOICE:
|
||||
logger.info("[WX]receive group voice, but checkprefix didn't match")
|
||||
logger.info(
|
||||
"[WX]receive group voice, but checkprefix didn't match"
|
||||
)
|
||||
return None
|
||||
else: # 单聊
|
||||
match_prefix = check_prefix(content, conf().get('single_chat_prefix',['']))
|
||||
if match_prefix is not None: # 判断如果匹配到自定义前缀,则返回过滤掉前缀+空格后的内容
|
||||
content = content.replace(match_prefix, '', 1).strip()
|
||||
elif context["origin_ctype"] == ContextType.VOICE: # 如果源消息是私聊的语音消息,允许不匹配前缀,放宽条件
|
||||
else: # 单聊
|
||||
match_prefix = check_prefix(
|
||||
content, conf().get("single_chat_prefix", [""])
|
||||
)
|
||||
if match_prefix is not None: # 判断如果匹配到自定义前缀,则返回过滤掉前缀+空格后的内容
|
||||
content = content.replace(match_prefix, "", 1).strip()
|
||||
elif (
|
||||
context["origin_ctype"] == ContextType.VOICE
|
||||
): # 如果源消息是私聊的语音消息,允许不匹配前缀,放宽条件
|
||||
pass
|
||||
else:
|
||||
return None
|
||||
|
||||
img_match_prefix = check_prefix(content, conf().get('image_create_prefix'))
|
||||
return None
|
||||
|
||||
img_match_prefix = check_prefix(content, conf().get("image_create_prefix"))
|
||||
if img_match_prefix:
|
||||
content = content.replace(img_match_prefix, '', 1)
|
||||
content = content.replace(img_match_prefix, "", 1)
|
||||
context.type = ContextType.IMAGE_CREATE
|
||||
else:
|
||||
context.type = ContextType.TEXT
|
||||
context.content = content.strip()
|
||||
if 'desire_rtype' not in context and conf().get('always_reply_voice') and ReplyType.VOICE not in self.NOT_SUPPORT_REPLYTYPE:
|
||||
context['desire_rtype'] = ReplyType.VOICE
|
||||
elif context.type == ContextType.VOICE:
|
||||
if 'desire_rtype' not in context and conf().get('voice_reply_voice') and ReplyType.VOICE not in self.NOT_SUPPORT_REPLYTYPE:
|
||||
context['desire_rtype'] = ReplyType.VOICE
|
||||
if (
|
||||
"desire_rtype" not in context
|
||||
and conf().get("always_reply_voice")
|
||||
and ReplyType.VOICE not in self.NOT_SUPPORT_REPLYTYPE
|
||||
):
|
||||
context["desire_rtype"] = ReplyType.VOICE
|
||||
elif context.type == ContextType.VOICE:
|
||||
if (
|
||||
"desire_rtype" not in context
|
||||
and conf().get("voice_reply_voice")
|
||||
and ReplyType.VOICE not in self.NOT_SUPPORT_REPLYTYPE
|
||||
):
|
||||
context["desire_rtype"] = ReplyType.VOICE
|
||||
|
||||
return context
|
||||
|
||||
def _handle(self, context: Context):
|
||||
if context is None or not context.content:
|
||||
return
|
||||
logger.debug('[WX] ready to handle context: {}'.format(context))
|
||||
logger.debug("[WX] ready to handle context: {}".format(context))
|
||||
# reply的构建步骤
|
||||
reply = self._generate_reply(context)
|
||||
|
||||
logger.debug('[WX] ready to decorate reply: {}'.format(reply))
|
||||
logger.debug("[WX] ready to decorate reply: {}".format(reply))
|
||||
# reply的包装步骤
|
||||
reply = self._decorate_reply(context, reply)
|
||||
|
||||
@@ -139,20 +174,31 @@ class ChatChannel(Channel):
|
||||
self._send_reply(context, reply)
|
||||
|
||||
def _generate_reply(self, context: Context, reply: Reply = Reply()) -> Reply:
|
||||
e_context = PluginManager().emit_event(EventContext(Event.ON_HANDLE_CONTEXT, {
|
||||
'channel': self, 'context': context, 'reply': reply}))
|
||||
reply = e_context['reply']
|
||||
e_context = PluginManager().emit_event(
|
||||
EventContext(
|
||||
Event.ON_HANDLE_CONTEXT,
|
||||
{"channel": self, "context": context, "reply": reply},
|
||||
)
|
||||
)
|
||||
reply = e_context["reply"]
|
||||
if not e_context.is_pass():
|
||||
logger.debug('[WX] ready to handle context: type={}, content={}'.format(context.type, context.content))
|
||||
if context.type == ContextType.TEXT or context.type == ContextType.IMAGE_CREATE: # 文字和图片消息
|
||||
logger.debug(
|
||||
"[WX] ready to handle context: type={}, content={}".format(
|
||||
context.type, context.content
|
||||
)
|
||||
)
|
||||
if (
|
||||
context.type == ContextType.TEXT
|
||||
or context.type == ContextType.IMAGE_CREATE
|
||||
): # 文字和图片消息
|
||||
reply = super().build_reply_content(context.content, context)
|
||||
elif context.type == ContextType.VOICE: # 语音消息
|
||||
cmsg = context['msg']
|
||||
cmsg = context["msg"]
|
||||
cmsg.prepare()
|
||||
file_path = context.content
|
||||
wav_path = os.path.splitext(file_path)[0] + '.wav'
|
||||
wav_path = os.path.splitext(file_path)[0] + ".wav"
|
||||
try:
|
||||
any_to_wav(file_path, wav_path)
|
||||
any_to_wav(file_path, wav_path)
|
||||
except Exception as e: # 转换失败,直接使用mp3,对于某些api,mp3也可以识别
|
||||
logger.warning("[WX]any to wav error, use raw path. " + str(e))
|
||||
wav_path = file_path
|
||||
@@ -169,7 +215,8 @@ class ChatChannel(Channel):
|
||||
|
||||
if reply.type == ReplyType.TEXT:
|
||||
new_context = self._compose_context(
|
||||
ContextType.TEXT, reply.content, **context.kwargs)
|
||||
ContextType.TEXT, reply.content, **context.kwargs
|
||||
)
|
||||
if new_context:
|
||||
reply = self._generate_reply(new_context)
|
||||
else:
|
||||
@@ -177,18 +224,21 @@ class ChatChannel(Channel):
|
||||
elif context.type == ContextType.IMAGE: # 图片消息,当前无默认逻辑
|
||||
pass
|
||||
else:
|
||||
logger.error('[WX] unknown context type: {}'.format(context.type))
|
||||
logger.error("[WX] unknown context type: {}".format(context.type))
|
||||
return
|
||||
return reply
|
||||
|
||||
def _decorate_reply(self, context: Context, reply: Reply) -> Reply:
|
||||
if reply and reply.type:
|
||||
e_context = PluginManager().emit_event(EventContext(Event.ON_DECORATE_REPLY, {
|
||||
'channel': self, 'context': context, 'reply': reply}))
|
||||
reply = e_context['reply']
|
||||
desire_rtype = context.get('desire_rtype')
|
||||
e_context = PluginManager().emit_event(
|
||||
EventContext(
|
||||
Event.ON_DECORATE_REPLY,
|
||||
{"channel": self, "context": context, "reply": reply},
|
||||
)
|
||||
)
|
||||
reply = e_context["reply"]
|
||||
desire_rtype = context.get("desire_rtype")
|
||||
if not e_context.is_pass() and reply and reply.type:
|
||||
|
||||
if reply.type in self.NOT_SUPPORT_REPLYTYPE:
|
||||
logger.error("[WX]reply type not support: " + str(reply.type))
|
||||
reply.type = ReplyType.ERROR
|
||||
@@ -196,59 +246,91 @@ class ChatChannel(Channel):
|
||||
|
||||
if reply.type == ReplyType.TEXT:
|
||||
reply_text = reply.content
|
||||
if desire_rtype == ReplyType.VOICE and ReplyType.VOICE not in self.NOT_SUPPORT_REPLYTYPE:
|
||||
if (
|
||||
desire_rtype == ReplyType.VOICE
|
||||
and ReplyType.VOICE not in self.NOT_SUPPORT_REPLYTYPE
|
||||
):
|
||||
reply = super().build_text_to_voice(reply.content)
|
||||
return self._decorate_reply(context, reply)
|
||||
if context.get("isgroup", False):
|
||||
reply_text = '@' + context['msg'].actual_user_nickname + ' ' + reply_text.strip()
|
||||
reply_text = conf().get("group_chat_reply_prefix", "") + reply_text
|
||||
reply_text = (
|
||||
"@"
|
||||
+ context["msg"].actual_user_nickname
|
||||
+ " "
|
||||
+ reply_text.strip()
|
||||
)
|
||||
reply_text = (
|
||||
conf().get("group_chat_reply_prefix", "") + reply_text
|
||||
)
|
||||
else:
|
||||
reply_text = conf().get("single_chat_reply_prefix", "") + reply_text
|
||||
reply_text = (
|
||||
conf().get("single_chat_reply_prefix", "") + reply_text
|
||||
)
|
||||
reply.content = reply_text
|
||||
elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO:
|
||||
reply.content = "["+str(reply.type)+"]\n" + reply.content
|
||||
elif reply.type == ReplyType.IMAGE_URL or reply.type == ReplyType.VOICE or reply.type == ReplyType.IMAGE:
|
||||
reply.content = "[" + str(reply.type) + "]\n" + reply.content
|
||||
elif (
|
||||
reply.type == ReplyType.IMAGE_URL
|
||||
or reply.type == ReplyType.VOICE
|
||||
or reply.type == ReplyType.IMAGE
|
||||
):
|
||||
pass
|
||||
else:
|
||||
logger.error('[WX] unknown reply type: {}'.format(reply.type))
|
||||
logger.error("[WX] unknown reply type: {}".format(reply.type))
|
||||
return
|
||||
if desire_rtype and desire_rtype != reply.type and reply.type not in [ReplyType.ERROR, ReplyType.INFO]:
|
||||
logger.warning('[WX] desire_rtype: {}, but reply type: {}'.format(context.get('desire_rtype'), reply.type))
|
||||
if (
|
||||
desire_rtype
|
||||
and desire_rtype != reply.type
|
||||
and reply.type not in [ReplyType.ERROR, ReplyType.INFO]
|
||||
):
|
||||
logger.warning(
|
||||
"[WX] desire_rtype: {}, but reply type: {}".format(
|
||||
context.get("desire_rtype"), reply.type
|
||||
)
|
||||
)
|
||||
return reply
|
||||
|
||||
def _send_reply(self, context: Context, reply: Reply):
|
||||
if reply and reply.type:
|
||||
e_context = PluginManager().emit_event(EventContext(Event.ON_SEND_REPLY, {
|
||||
'channel': self, 'context': context, 'reply': reply}))
|
||||
reply = e_context['reply']
|
||||
e_context = PluginManager().emit_event(
|
||||
EventContext(
|
||||
Event.ON_SEND_REPLY,
|
||||
{"channel": self, "context": context, "reply": reply},
|
||||
)
|
||||
)
|
||||
reply = e_context["reply"]
|
||||
if not e_context.is_pass() and reply and reply.type:
|
||||
logger.debug('[WX] ready to send reply: {}, context: {}'.format(reply, context))
|
||||
logger.debug(
|
||||
"[WX] ready to send reply: {}, context: {}".format(reply, context)
|
||||
)
|
||||
self._send(reply, context)
|
||||
|
||||
def _send(self, reply: Reply, context: Context, retry_cnt = 0):
|
||||
def _send(self, reply: Reply, context: Context, retry_cnt=0):
|
||||
try:
|
||||
self.send(reply, context)
|
||||
except Exception as e:
|
||||
logger.error('[WX] sendMsg error: {}'.format(str(e)))
|
||||
logger.error("[WX] sendMsg error: {}".format(str(e)))
|
||||
if isinstance(e, NotImplementedError):
|
||||
return
|
||||
logger.exception(e)
|
||||
if retry_cnt < 2:
|
||||
time.sleep(3+3*retry_cnt)
|
||||
self._send(reply, context, retry_cnt+1)
|
||||
time.sleep(3 + 3 * retry_cnt)
|
||||
self._send(reply, context, retry_cnt + 1)
|
||||
|
||||
def _success_callback(self, session_id, **kwargs):# 线程正常结束时的回调函数
|
||||
def _success_callback(self, session_id, **kwargs): # 线程正常结束时的回调函数
|
||||
logger.debug("Worker return success, session_id = {}".format(session_id))
|
||||
|
||||
def _fail_callback(self, session_id, exception, **kwargs): # 线程异常结束时的回调函数
|
||||
def _fail_callback(self, session_id, exception, **kwargs): # 线程异常结束时的回调函数
|
||||
logger.exception("Worker return exception: {}".format(exception))
|
||||
|
||||
def _thread_pool_callback(self, session_id, **kwargs):
|
||||
def func(worker:Future):
|
||||
def func(worker: Future):
|
||||
try:
|
||||
worker_exception = worker.exception()
|
||||
if worker_exception:
|
||||
self._fail_callback(session_id, exception = worker_exception, **kwargs)
|
||||
self._fail_callback(
|
||||
session_id, exception=worker_exception, **kwargs
|
||||
)
|
||||
else:
|
||||
self._success_callback(session_id, **kwargs)
|
||||
except CancelledError as e:
|
||||
@@ -257,15 +339,19 @@ class ChatChannel(Channel):
|
||||
logger.exception("Worker raise exception: {}".format(e))
|
||||
with self.lock:
|
||||
self.sessions[session_id][1].release()
|
||||
|
||||
return func
|
||||
|
||||
def produce(self, context: Context):
|
||||
session_id = context['session_id']
|
||||
session_id = context["session_id"]
|
||||
with self.lock:
|
||||
if session_id not in self.sessions:
|
||||
self.sessions[session_id] = [Dequeue(), threading.BoundedSemaphore(conf().get("concurrency_in_session", 4))]
|
||||
if context.type == ContextType.TEXT and context.content.startswith("#"):
|
||||
self.sessions[session_id][0].putleft(context) # 优先处理管理命令
|
||||
self.sessions[session_id] = [
|
||||
Dequeue(),
|
||||
threading.BoundedSemaphore(conf().get("concurrency_in_session", 4)),
|
||||
]
|
||||
if context.type == ContextType.TEXT and context.content.startswith("#"):
|
||||
self.sessions[session_id][0].putleft(context) # 优先处理管理命令
|
||||
else:
|
||||
self.sessions[session_id][0].put(context)
|
||||
|
||||
@@ -276,44 +362,58 @@ class ChatChannel(Channel):
|
||||
session_ids = list(self.sessions.keys())
|
||||
for session_id in session_ids:
|
||||
context_queue, semaphore = self.sessions[session_id]
|
||||
if semaphore.acquire(blocking = False): # 等线程处理完毕才能删除
|
||||
if semaphore.acquire(blocking=False): # 等线程处理完毕才能删除
|
||||
if not context_queue.empty():
|
||||
context = context_queue.get()
|
||||
logger.debug("[WX] consume context: {}".format(context))
|
||||
future:Future = self.handler_pool.submit(self._handle, context)
|
||||
future.add_done_callback(self._thread_pool_callback(session_id, context = context))
|
||||
future: Future = self.handler_pool.submit(
|
||||
self._handle, context
|
||||
)
|
||||
future.add_done_callback(
|
||||
self._thread_pool_callback(session_id, context=context)
|
||||
)
|
||||
if session_id not in self.futures:
|
||||
self.futures[session_id] = []
|
||||
self.futures[session_id].append(future)
|
||||
elif semaphore._initial_value == semaphore._value+1: # 除了当前,没有任务再申请到信号量,说明所有任务都处理完毕
|
||||
self.futures[session_id] = [t for t in self.futures[session_id] if not t.done()]
|
||||
assert len(self.futures[session_id]) == 0, "thread pool error"
|
||||
elif (
|
||||
semaphore._initial_value == semaphore._value + 1
|
||||
): # 除了当前,没有任务再申请到信号量,说明所有任务都处理完毕
|
||||
self.futures[session_id] = [
|
||||
t for t in self.futures[session_id] if not t.done()
|
||||
]
|
||||
assert (
|
||||
len(self.futures[session_id]) == 0
|
||||
), "thread pool error"
|
||||
del self.sessions[session_id]
|
||||
else:
|
||||
semaphore.release()
|
||||
time.sleep(0.1)
|
||||
|
||||
# 取消session_id对应的所有任务,只能取消排队的消息和已提交线程池但未执行的任务
|
||||
def cancel_session(self, session_id):
|
||||
def cancel_session(self, session_id):
|
||||
with self.lock:
|
||||
if session_id in self.sessions:
|
||||
for future in self.futures[session_id]:
|
||||
future.cancel()
|
||||
cnt = self.sessions[session_id][0].qsize()
|
||||
if cnt>0:
|
||||
logger.info("Cancel {} messages in session {}".format(cnt, session_id))
|
||||
if cnt > 0:
|
||||
logger.info(
|
||||
"Cancel {} messages in session {}".format(cnt, session_id)
|
||||
)
|
||||
self.sessions[session_id][0] = Dequeue()
|
||||
|
||||
|
||||
def cancel_all_session(self):
|
||||
with self.lock:
|
||||
for session_id in self.sessions:
|
||||
for future in self.futures[session_id]:
|
||||
future.cancel()
|
||||
cnt = self.sessions[session_id][0].qsize()
|
||||
if cnt>0:
|
||||
logger.info("Cancel {} messages in session {}".format(cnt, session_id))
|
||||
if cnt > 0:
|
||||
logger.info(
|
||||
"Cancel {} messages in session {}".format(cnt, session_id)
|
||||
)
|
||||
self.sessions[session_id][0] = Dequeue()
|
||||
|
||||
|
||||
|
||||
def check_prefix(content, prefix_list):
|
||||
if not prefix_list:
|
||||
@@ -323,6 +423,7 @@ def check_prefix(content, prefix_list):
|
||||
return prefix
|
||||
return None
|
||||
|
||||
|
||||
def check_contain(content, keyword_list):
|
||||
if not keyword_list:
|
||||
return None
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
"""
|
||||
"""
|
||||
本类表示聊天消息,用于对itchat和wechaty的消息进行统一的封装。
|
||||
|
||||
填好必填项(群聊6个,非群聊8个),即可接入ChatChannel,并支持插件,参考TerminalChannel
|
||||
@@ -20,7 +19,7 @@ other_user_id: 对方的id,如果你是发送者,那这个就是接收者id
|
||||
other_user_nickname: 同上
|
||||
|
||||
is_group: 是否是群消息 (群聊必填)
|
||||
is_at: 是否被at
|
||||
is_at: 是否被at
|
||||
|
||||
- (群消息时,一般会存在实际发送者,是群内某个成员的id和昵称,下列项仅在群消息时存在)
|
||||
actual_user_id: 实际发送者id (群聊必填)
|
||||
@@ -34,20 +33,22 @@ _prepared: 是否已经调用过准备函数
|
||||
_rawmsg: 原始消息对象
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ChatMessage(object):
|
||||
msg_id = None
|
||||
create_time = None
|
||||
|
||||
|
||||
ctype = None
|
||||
content = None
|
||||
|
||||
|
||||
from_user_id = None
|
||||
from_user_nickname = None
|
||||
to_user_id = None
|
||||
to_user_nickname = None
|
||||
other_user_id = None
|
||||
other_user_nickname = None
|
||||
|
||||
|
||||
is_group = False
|
||||
is_at = False
|
||||
actual_user_id = None
|
||||
@@ -57,8 +58,7 @@ class ChatMessage(object):
|
||||
_prepared = False
|
||||
_rawmsg = None
|
||||
|
||||
|
||||
def __init__(self,_rawmsg):
|
||||
def __init__(self, _rawmsg):
|
||||
self._rawmsg = _rawmsg
|
||||
|
||||
def prepare(self):
|
||||
@@ -67,7 +67,7 @@ class ChatMessage(object):
|
||||
self._prepare_fn()
|
||||
|
||||
def __str__(self):
|
||||
return 'ChatMessage: id={}, create_time={}, ctype={}, content={}, from_user_id={}, from_user_nickname={}, to_user_id={}, to_user_nickname={}, other_user_id={}, other_user_nickname={}, is_group={}, is_at={}, actual_user_id={}, actual_user_nickname={}'.format(
|
||||
return "ChatMessage: id={}, create_time={}, ctype={}, content={}, from_user_id={}, from_user_nickname={}, to_user_id={}, to_user_nickname={}, other_user_id={}, other_user_nickname={}, is_group={}, is_at={}, actual_user_id={}, actual_user_nickname={}".format(
|
||||
self.msg_id,
|
||||
self.create_time,
|
||||
self.ctype,
|
||||
@@ -82,4 +82,4 @@ class ChatMessage(object):
|
||||
self.is_at,
|
||||
self.actual_user_id,
|
||||
self.actual_user_nickname,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import sys
|
||||
|
||||
from bridge.context import *
|
||||
from bridge.reply import Reply, ReplyType
|
||||
from channel.chat_channel import ChatChannel, check_prefix
|
||||
from channel.chat_message import ChatMessage
|
||||
import sys
|
||||
|
||||
from config import conf
|
||||
from common.log import logger
|
||||
from config import conf
|
||||
|
||||
|
||||
class TerminalMessage(ChatMessage):
|
||||
def __init__(self, msg_id, content, ctype = ContextType.TEXT, from_user_id = "User", to_user_id = "Chatgpt", other_user_id = "Chatgpt"):
|
||||
def __init__(
|
||||
self,
|
||||
msg_id,
|
||||
content,
|
||||
ctype=ContextType.TEXT,
|
||||
from_user_id="User",
|
||||
to_user_id="Chatgpt",
|
||||
other_user_id="Chatgpt",
|
||||
):
|
||||
self.msg_id = msg_id
|
||||
self.ctype = ctype
|
||||
self.content = content
|
||||
@@ -16,6 +25,7 @@ class TerminalMessage(ChatMessage):
|
||||
self.to_user_id = to_user_id
|
||||
self.other_user_id = other_user_id
|
||||
|
||||
|
||||
class TerminalChannel(ChatChannel):
|
||||
NOT_SUPPORT_REPLYTYPE = [ReplyType.VOICE]
|
||||
|
||||
@@ -23,14 +33,18 @@ class TerminalChannel(ChatChannel):
|
||||
print("\nBot:")
|
||||
if reply.type == ReplyType.IMAGE:
|
||||
from PIL import Image
|
||||
|
||||
image_storage = reply.content
|
||||
image_storage.seek(0)
|
||||
img = Image.open(image_storage)
|
||||
print("<IMAGE>")
|
||||
img.show()
|
||||
elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
|
||||
elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
|
||||
import io
|
||||
|
||||
import requests
|
||||
from PIL import Image
|
||||
import requests,io
|
||||
|
||||
img_url = reply.content
|
||||
pic_res = requests.get(img_url, stream=True)
|
||||
image_storage = io.BytesIO()
|
||||
@@ -59,11 +73,13 @@ class TerminalChannel(ChatChannel):
|
||||
print("\nExiting...")
|
||||
sys.exit()
|
||||
msg_id += 1
|
||||
trigger_prefixs = conf().get("single_chat_prefix",[""])
|
||||
trigger_prefixs = conf().get("single_chat_prefix", [""])
|
||||
if check_prefix(prompt, trigger_prefixs) is None:
|
||||
prompt = trigger_prefixs[0] + prompt # 给没触发的消息加上触发前缀
|
||||
|
||||
context = self._compose_context(ContextType.TEXT, prompt, msg = TerminalMessage(msg_id, prompt))
|
||||
prompt = trigger_prefixs[0] + prompt # 给没触发的消息加上触发前缀
|
||||
|
||||
context = self._compose_context(
|
||||
ContextType.TEXT, prompt, msg=TerminalMessage(msg_id, prompt)
|
||||
)
|
||||
if context:
|
||||
self.produce(context)
|
||||
else:
|
||||
|
||||
@@ -4,40 +4,45 @@
|
||||
wechat channel
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import requests
|
||||
import io
|
||||
import time
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
from bridge.context import *
|
||||
from bridge.reply import *
|
||||
from channel.chat_channel import ChatChannel
|
||||
from channel.wechat.wechat_message import *
|
||||
from common.singleton import singleton
|
||||
from common.expired_dict import ExpiredDict
|
||||
from common.log import logger
|
||||
from common.singleton import singleton
|
||||
from common.time_check import time_checker
|
||||
from config import conf
|
||||
from lib import itchat
|
||||
from lib.itchat.content import *
|
||||
from bridge.reply import *
|
||||
from bridge.context import *
|
||||
from config import conf
|
||||
from common.time_check import time_checker
|
||||
from common.expired_dict import ExpiredDict
|
||||
from plugins import *
|
||||
|
||||
@itchat.msg_register([TEXT,VOICE,PICTURE])
|
||||
|
||||
@itchat.msg_register([TEXT, VOICE, PICTURE])
|
||||
def handler_single_msg(msg):
|
||||
# logger.debug("handler_single_msg: {}".format(msg))
|
||||
if msg['Type'] == PICTURE and msg['MsgType'] == 47:
|
||||
if msg["Type"] == PICTURE and msg["MsgType"] == 47:
|
||||
return None
|
||||
WechatChannel().handle_single(WeChatMessage(msg))
|
||||
return None
|
||||
|
||||
@itchat.msg_register([TEXT,VOICE,PICTURE], isGroupChat=True)
|
||||
|
||||
@itchat.msg_register([TEXT, VOICE, PICTURE], isGroupChat=True)
|
||||
def handler_group_msg(msg):
|
||||
if msg['Type'] == PICTURE and msg['MsgType'] == 47:
|
||||
if msg["Type"] == PICTURE and msg["MsgType"] == 47:
|
||||
return None
|
||||
WechatChannel().handle_group(WeChatMessage(msg,True))
|
||||
WechatChannel().handle_group(WeChatMessage(msg, True))
|
||||
return None
|
||||
|
||||
|
||||
def _check(func):
|
||||
def wrapper(self, cmsg: ChatMessage):
|
||||
msgId = cmsg.msg_id
|
||||
@@ -45,21 +50,27 @@ def _check(func):
|
||||
logger.info("Wechat message {} already received, ignore".format(msgId))
|
||||
return
|
||||
self.receivedMsgs[msgId] = cmsg
|
||||
create_time = cmsg.create_time # 消息时间戳
|
||||
if conf().get('hot_reload') == True and int(create_time) < int(time.time()) - 60: # 跳过1分钟前的历史消息
|
||||
create_time = cmsg.create_time # 消息时间戳
|
||||
if (
|
||||
conf().get("hot_reload") == True
|
||||
and int(create_time) < int(time.time()) - 60
|
||||
): # 跳过1分钟前的历史消息
|
||||
logger.debug("[WX]history message {} skipped".format(msgId))
|
||||
return
|
||||
return func(self, cmsg)
|
||||
|
||||
return wrapper
|
||||
|
||||
#可用的二维码生成接口
|
||||
#https://api.qrserver.com/v1/create-qr-code/?size=400×400&data=https://www.abc.com
|
||||
#https://api.isoyu.com/qr/?m=1&e=L&p=20&url=https://www.abc.com
|
||||
def qrCallback(uuid,status,qrcode):
|
||||
|
||||
# 可用的二维码生成接口
|
||||
# https://api.qrserver.com/v1/create-qr-code/?size=400×400&data=https://www.abc.com
|
||||
# https://api.isoyu.com/qr/?m=1&e=L&p=20&url=https://www.abc.com
|
||||
def qrCallback(uuid, status, qrcode):
|
||||
# logger.debug("qrCallback: {} {}".format(uuid,status))
|
||||
if status == '0':
|
||||
if status == "0":
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
img = Image.open(io.BytesIO(qrcode))
|
||||
_thread = threading.Thread(target=img.show, args=("QRCode",))
|
||||
_thread.setDaemon(True)
|
||||
@@ -68,35 +79,43 @@ def qrCallback(uuid,status,qrcode):
|
||||
pass
|
||||
|
||||
import qrcode
|
||||
|
||||
url = f"https://login.weixin.qq.com/l/{uuid}"
|
||||
|
||||
qr_api1="https://api.isoyu.com/qr/?m=1&e=L&p=20&url={}".format(url)
|
||||
qr_api2="https://api.qrserver.com/v1/create-qr-code/?size=400×400&data={}".format(url)
|
||||
qr_api3="https://api.pwmqr.com/qrcode/create/?url={}".format(url)
|
||||
qr_api4="https://my.tv.sohu.com/user/a/wvideo/getQRCode.do?text={}".format(url)
|
||||
|
||||
qr_api1 = "https://api.isoyu.com/qr/?m=1&e=L&p=20&url={}".format(url)
|
||||
qr_api2 = (
|
||||
"https://api.qrserver.com/v1/create-qr-code/?size=400×400&data={}".format(
|
||||
url
|
||||
)
|
||||
)
|
||||
qr_api3 = "https://api.pwmqr.com/qrcode/create/?url={}".format(url)
|
||||
qr_api4 = "https://my.tv.sohu.com/user/a/wvideo/getQRCode.do?text={}".format(
|
||||
url
|
||||
)
|
||||
print("You can also scan QRCode in any website below:")
|
||||
print(qr_api3)
|
||||
print(qr_api4)
|
||||
print(qr_api2)
|
||||
print(qr_api1)
|
||||
|
||||
|
||||
qr = qrcode.QRCode(border=1)
|
||||
qr.add_data(url)
|
||||
qr.make(fit=True)
|
||||
qr.print_ascii(invert=True)
|
||||
|
||||
|
||||
@singleton
|
||||
class WechatChannel(ChatChannel):
|
||||
NOT_SUPPORT_REPLYTYPE = []
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.receivedMsgs = ExpiredDict(60*60*24)
|
||||
self.receivedMsgs = ExpiredDict(60 * 60 * 24)
|
||||
|
||||
def startup(self):
|
||||
|
||||
itchat.instance.receivingRetryCount = 600 # 修改断线超时时间
|
||||
# login by scan QRCode
|
||||
hotReload = conf().get('hot_reload', False)
|
||||
hotReload = conf().get("hot_reload", False)
|
||||
try:
|
||||
itchat.auto_login(enableCmdQR=2, hotReload=hotReload, qrCallback=qrCallback)
|
||||
except Exception as e:
|
||||
@@ -104,12 +123,18 @@ class WechatChannel(ChatChannel):
|
||||
logger.error("Hot reload failed, try to login without hot reload")
|
||||
itchat.logout()
|
||||
os.remove("itchat.pkl")
|
||||
itchat.auto_login(enableCmdQR=2, hotReload=hotReload, qrCallback=qrCallback)
|
||||
itchat.auto_login(
|
||||
enableCmdQR=2, hotReload=hotReload, qrCallback=qrCallback
|
||||
)
|
||||
else:
|
||||
raise e
|
||||
self.user_id = itchat.instance.storageClass.userName
|
||||
self.name = itchat.instance.storageClass.nickName
|
||||
logger.info("Wechat login success, user_id: {}, nickname: {}".format(self.user_id, self.name))
|
||||
logger.info(
|
||||
"Wechat login success, user_id: {}, nickname: {}".format(
|
||||
self.user_id, self.name
|
||||
)
|
||||
)
|
||||
# start message listener
|
||||
itchat.run()
|
||||
|
||||
@@ -127,24 +152,30 @@ class WechatChannel(ChatChannel):
|
||||
|
||||
@time_checker
|
||||
@_check
|
||||
def handle_single(self, cmsg : ChatMessage):
|
||||
def handle_single(self, cmsg: ChatMessage):
|
||||
if cmsg.ctype == ContextType.VOICE:
|
||||
if conf().get('speech_recognition') != True:
|
||||
if conf().get("speech_recognition") != True:
|
||||
return
|
||||
logger.debug("[WX]receive voice msg: {}".format(cmsg.content))
|
||||
elif cmsg.ctype == ContextType.IMAGE:
|
||||
logger.debug("[WX]receive image msg: {}".format(cmsg.content))
|
||||
else:
|
||||
logger.debug("[WX]receive text msg: {}, cmsg={}".format(json.dumps(cmsg._rawmsg, ensure_ascii=False), cmsg))
|
||||
context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=False, msg=cmsg)
|
||||
logger.debug(
|
||||
"[WX]receive text msg: {}, cmsg={}".format(
|
||||
json.dumps(cmsg._rawmsg, ensure_ascii=False), cmsg
|
||||
)
|
||||
)
|
||||
context = self._compose_context(
|
||||
cmsg.ctype, cmsg.content, isgroup=False, msg=cmsg
|
||||
)
|
||||
if context:
|
||||
self.produce(context)
|
||||
|
||||
@time_checker
|
||||
@_check
|
||||
def handle_group(self, cmsg : ChatMessage):
|
||||
def handle_group(self, cmsg: ChatMessage):
|
||||
if cmsg.ctype == ContextType.VOICE:
|
||||
if conf().get('speech_recognition') != True:
|
||||
if conf().get("speech_recognition") != True:
|
||||
return
|
||||
logger.debug("[WX]receive voice for group msg: {}".format(cmsg.content))
|
||||
elif cmsg.ctype == ContextType.IMAGE:
|
||||
@@ -152,23 +183,25 @@ class WechatChannel(ChatChannel):
|
||||
else:
|
||||
# logger.debug("[WX]receive group msg: {}, cmsg={}".format(json.dumps(cmsg._rawmsg, ensure_ascii=False), cmsg))
|
||||
pass
|
||||
context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=True, msg=cmsg)
|
||||
context = self._compose_context(
|
||||
cmsg.ctype, cmsg.content, isgroup=True, msg=cmsg
|
||||
)
|
||||
if context:
|
||||
self.produce(context)
|
||||
|
||||
|
||||
# 统一的发送函数,每个Channel自行实现,根据reply的type字段发送不同类型的消息
|
||||
def send(self, reply: Reply, context: Context):
|
||||
receiver = context["receiver"]
|
||||
if reply.type == ReplyType.TEXT:
|
||||
itchat.send(reply.content, toUserName=receiver)
|
||||
logger.info('[WX] sendMsg={}, receiver={}'.format(reply, receiver))
|
||||
logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver))
|
||||
elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO:
|
||||
itchat.send(reply.content, toUserName=receiver)
|
||||
logger.info('[WX] sendMsg={}, receiver={}'.format(reply, receiver))
|
||||
logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver))
|
||||
elif reply.type == ReplyType.VOICE:
|
||||
itchat.send_file(reply.content, toUserName=receiver)
|
||||
logger.info('[WX] sendFile={}, receiver={}'.format(reply.content, receiver))
|
||||
elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
|
||||
logger.info("[WX] sendFile={}, receiver={}".format(reply.content, receiver))
|
||||
elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
|
||||
img_url = reply.content
|
||||
pic_res = requests.get(img_url, stream=True)
|
||||
image_storage = io.BytesIO()
|
||||
@@ -176,9 +209,9 @@ class WechatChannel(ChatChannel):
|
||||
image_storage.write(block)
|
||||
image_storage.seek(0)
|
||||
itchat.send_image(image_storage, toUserName=receiver)
|
||||
logger.info('[WX] sendImage url={}, receiver={}'.format(img_url,receiver))
|
||||
elif reply.type == ReplyType.IMAGE: # 从文件读取图片
|
||||
logger.info("[WX] sendImage url={}, receiver={}".format(img_url, receiver))
|
||||
elif reply.type == ReplyType.IMAGE: # 从文件读取图片
|
||||
image_storage = reply.content
|
||||
image_storage.seek(0)
|
||||
itchat.send_image(image_storage, toUserName=receiver)
|
||||
logger.info('[WX] sendImage, receiver={}'.format(receiver))
|
||||
logger.info("[WX] sendImage, receiver={}".format(receiver))
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
|
||||
|
||||
from bridge.context import ContextType
|
||||
from channel.chat_message import ChatMessage
|
||||
from common.tmp_dir import TmpDir
|
||||
from common.log import logger
|
||||
from lib.itchat.content import *
|
||||
from common.tmp_dir import TmpDir
|
||||
from lib import itchat
|
||||
from lib.itchat.content import *
|
||||
|
||||
|
||||
class WeChatMessage(ChatMessage):
|
||||
|
||||
def __init__(self, itchat_msg, is_group=False):
|
||||
super().__init__( itchat_msg)
|
||||
self.msg_id = itchat_msg['MsgId']
|
||||
self.create_time = itchat_msg['CreateTime']
|
||||
super().__init__(itchat_msg)
|
||||
self.msg_id = itchat_msg["MsgId"]
|
||||
self.create_time = itchat_msg["CreateTime"]
|
||||
self.is_group = is_group
|
||||
|
||||
if itchat_msg['Type'] == TEXT:
|
||||
|
||||
if itchat_msg["Type"] == TEXT:
|
||||
self.ctype = ContextType.TEXT
|
||||
self.content = itchat_msg['Text']
|
||||
elif itchat_msg['Type'] == VOICE:
|
||||
self.content = itchat_msg["Text"]
|
||||
elif itchat_msg["Type"] == VOICE:
|
||||
self.ctype = ContextType.VOICE
|
||||
self.content = TmpDir().path() + itchat_msg['FileName'] # content直接存临时目录路径
|
||||
self.content = TmpDir().path() + itchat_msg["FileName"] # content直接存临时目录路径
|
||||
self._prepare_fn = lambda: itchat_msg.download(self.content)
|
||||
elif itchat_msg['Type'] == PICTURE and itchat_msg['MsgType'] == 3:
|
||||
elif itchat_msg["Type"] == PICTURE and itchat_msg["MsgType"] == 3:
|
||||
self.ctype = ContextType.IMAGE
|
||||
self.content = TmpDir().path() + itchat_msg['FileName'] # content直接存临时目录路径
|
||||
self.content = TmpDir().path() + itchat_msg["FileName"] # content直接存临时目录路径
|
||||
self._prepare_fn = lambda: itchat_msg.download(self.content)
|
||||
else:
|
||||
raise NotImplementedError("Unsupported message type: {}".format(itchat_msg['Type']))
|
||||
|
||||
self.from_user_id = itchat_msg['FromUserName']
|
||||
self.to_user_id = itchat_msg['ToUserName']
|
||||
|
||||
raise NotImplementedError(
|
||||
"Unsupported message type: {}".format(itchat_msg["Type"])
|
||||
)
|
||||
|
||||
self.from_user_id = itchat_msg["FromUserName"]
|
||||
self.to_user_id = itchat_msg["ToUserName"]
|
||||
|
||||
user_id = itchat.instance.storageClass.userName
|
||||
nickname = itchat.instance.storageClass.nickName
|
||||
|
||||
|
||||
# 虽然from_user_id和to_user_id用的少,但是为了保持一致性,还是要填充一下
|
||||
# 以下很繁琐,一句话总结:能填的都填了。
|
||||
if self.from_user_id == user_id:
|
||||
self.from_user_nickname = nickname
|
||||
if self.to_user_id == user_id:
|
||||
self.to_user_nickname = nickname
|
||||
try: # 陌生人时候, 'User'字段可能不存在
|
||||
self.other_user_id = itchat_msg['User']['UserName']
|
||||
self.other_user_nickname = itchat_msg['User']['NickName']
|
||||
try: # 陌生人时候, 'User'字段可能不存在
|
||||
self.other_user_id = itchat_msg["User"]["UserName"]
|
||||
self.other_user_nickname = itchat_msg["User"]["NickName"]
|
||||
if self.other_user_id == self.from_user_id:
|
||||
self.from_user_nickname = self.other_user_nickname
|
||||
if self.other_user_id == self.to_user_id:
|
||||
self.to_user_nickname = self.other_user_nickname
|
||||
except KeyError as e: # 处理偶尔没有对方信息的情况
|
||||
except KeyError as e: # 处理偶尔没有对方信息的情况
|
||||
logger.warn("[WX]get other_user_id failed: " + str(e))
|
||||
if self.from_user_id == user_id:
|
||||
self.other_user_id = self.to_user_id
|
||||
@@ -56,6 +56,6 @@ class WeChatMessage(ChatMessage):
|
||||
self.other_user_id = self.from_user_id
|
||||
|
||||
if self.is_group:
|
||||
self.is_at = itchat_msg['IsAt']
|
||||
self.actual_user_id = itchat_msg['ActualUserName']
|
||||
self.actual_user_nickname = itchat_msg['ActualNickName']
|
||||
self.is_at = itchat_msg["IsAt"]
|
||||
self.actual_user_id = itchat_msg["ActualUserName"]
|
||||
self.actual_user_nickname = itchat_msg["ActualNickName"]
|
||||
|
||||
@@ -4,104 +4,118 @@
|
||||
wechaty channel
|
||||
Python Wechaty - https://github.com/wechaty/python-wechaty
|
||||
"""
|
||||
import asyncio
|
||||
import base64
|
||||
import os
|
||||
import time
|
||||
import asyncio
|
||||
from bridge.context import Context
|
||||
from wechaty_puppet import FileBox
|
||||
from wechaty import Wechaty, Contact
|
||||
|
||||
from wechaty import Contact, Wechaty
|
||||
from wechaty.user import Message
|
||||
from bridge.reply import *
|
||||
from wechaty_puppet import FileBox
|
||||
|
||||
from bridge.context import *
|
||||
from bridge.context import Context
|
||||
from bridge.reply import *
|
||||
from channel.chat_channel import ChatChannel
|
||||
from channel.wechat.wechaty_message import WechatyMessage
|
||||
from common.log import logger
|
||||
from common.singleton import singleton
|
||||
from config import conf
|
||||
|
||||
try:
|
||||
from voice.audio_convert import any_to_sil
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
|
||||
@singleton
|
||||
class WechatyChannel(ChatChannel):
|
||||
NOT_SUPPORT_REPLYTYPE = []
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def startup(self):
|
||||
config = conf()
|
||||
token = config.get('wechaty_puppet_service_token')
|
||||
os.environ['WECHATY_PUPPET_SERVICE_TOKEN'] = token
|
||||
token = config.get("wechaty_puppet_service_token")
|
||||
os.environ["WECHATY_PUPPET_SERVICE_TOKEN"] = token
|
||||
asyncio.run(self.main())
|
||||
|
||||
async def main(self):
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
#将asyncio的loop传入处理线程
|
||||
self.handler_pool._initializer= lambda: asyncio.set_event_loop(loop)
|
||||
# 将asyncio的loop传入处理线程
|
||||
self.handler_pool._initializer = lambda: asyncio.set_event_loop(loop)
|
||||
self.bot = Wechaty()
|
||||
self.bot.on('login', self.on_login)
|
||||
self.bot.on('message', self.on_message)
|
||||
self.bot.on("login", self.on_login)
|
||||
self.bot.on("message", self.on_message)
|
||||
await self.bot.start()
|
||||
|
||||
async def on_login(self, contact: Contact):
|
||||
self.user_id = contact.contact_id
|
||||
self.name = contact.name
|
||||
logger.info('[WX] login user={}'.format(contact))
|
||||
logger.info("[WX] login user={}".format(contact))
|
||||
|
||||
# 统一的发送函数,每个Channel自行实现,根据reply的type字段发送不同类型的消息
|
||||
def send(self, reply: Reply, context: Context):
|
||||
receiver_id = context['receiver']
|
||||
receiver_id = context["receiver"]
|
||||
loop = asyncio.get_event_loop()
|
||||
if context['isgroup']:
|
||||
receiver = asyncio.run_coroutine_threadsafe(self.bot.Room.find(receiver_id),loop).result()
|
||||
if context["isgroup"]:
|
||||
receiver = asyncio.run_coroutine_threadsafe(
|
||||
self.bot.Room.find(receiver_id), loop
|
||||
).result()
|
||||
else:
|
||||
receiver = asyncio.run_coroutine_threadsafe(self.bot.Contact.find(receiver_id),loop).result()
|
||||
receiver = asyncio.run_coroutine_threadsafe(
|
||||
self.bot.Contact.find(receiver_id), loop
|
||||
).result()
|
||||
msg = None
|
||||
if reply.type == ReplyType.TEXT:
|
||||
msg = reply.content
|
||||
asyncio.run_coroutine_threadsafe(receiver.say(msg),loop).result()
|
||||
logger.info('[WX] sendMsg={}, receiver={}'.format(reply, receiver))
|
||||
asyncio.run_coroutine_threadsafe(receiver.say(msg), loop).result()
|
||||
logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver))
|
||||
elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO:
|
||||
msg = reply.content
|
||||
asyncio.run_coroutine_threadsafe(receiver.say(msg),loop).result()
|
||||
logger.info('[WX] sendMsg={}, receiver={}'.format(reply, receiver))
|
||||
asyncio.run_coroutine_threadsafe(receiver.say(msg), loop).result()
|
||||
logger.info("[WX] sendMsg={}, receiver={}".format(reply, receiver))
|
||||
elif reply.type == ReplyType.VOICE:
|
||||
voiceLength = None
|
||||
file_path = reply.content
|
||||
sil_file = os.path.splitext(file_path)[0] + '.sil'
|
||||
sil_file = os.path.splitext(file_path)[0] + ".sil"
|
||||
voiceLength = int(any_to_sil(file_path, sil_file))
|
||||
if voiceLength >= 60000:
|
||||
voiceLength = 60000
|
||||
logger.info('[WX] voice too long, length={}, set to 60s'.format(voiceLength))
|
||||
logger.info(
|
||||
"[WX] voice too long, length={}, set to 60s".format(voiceLength)
|
||||
)
|
||||
# 发送语音
|
||||
t = int(time.time())
|
||||
msg = FileBox.from_file(sil_file, name=str(t) + '.sil')
|
||||
msg = FileBox.from_file(sil_file, name=str(t) + ".sil")
|
||||
if voiceLength is not None:
|
||||
msg.metadata['voiceLength'] = voiceLength
|
||||
asyncio.run_coroutine_threadsafe(receiver.say(msg),loop).result()
|
||||
msg.metadata["voiceLength"] = voiceLength
|
||||
asyncio.run_coroutine_threadsafe(receiver.say(msg), loop).result()
|
||||
try:
|
||||
os.remove(file_path)
|
||||
if sil_file != file_path:
|
||||
os.remove(sil_file)
|
||||
except Exception as e:
|
||||
pass
|
||||
logger.info('[WX] sendVoice={}, receiver={}'.format(reply.content, receiver))
|
||||
elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
|
||||
logger.info(
|
||||
"[WX] sendVoice={}, receiver={}".format(reply.content, receiver)
|
||||
)
|
||||
elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
|
||||
img_url = reply.content
|
||||
t = int(time.time())
|
||||
msg = FileBox.from_url(url=img_url, name=str(t) + '.png')
|
||||
asyncio.run_coroutine_threadsafe(receiver.say(msg),loop).result()
|
||||
logger.info('[WX] sendImage url={}, receiver={}'.format(img_url,receiver))
|
||||
elif reply.type == ReplyType.IMAGE: # 从文件读取图片
|
||||
msg = FileBox.from_url(url=img_url, name=str(t) + ".png")
|
||||
asyncio.run_coroutine_threadsafe(receiver.say(msg), loop).result()
|
||||
logger.info("[WX] sendImage url={}, receiver={}".format(img_url, receiver))
|
||||
elif reply.type == ReplyType.IMAGE: # 从文件读取图片
|
||||
image_storage = reply.content
|
||||
image_storage.seek(0)
|
||||
t = int(time.time())
|
||||
msg = FileBox.from_base64(base64.b64encode(image_storage.read()), str(t) + '.png')
|
||||
asyncio.run_coroutine_threadsafe(receiver.say(msg),loop).result()
|
||||
logger.info('[WX] sendImage, receiver={}'.format(receiver))
|
||||
msg = FileBox.from_base64(
|
||||
base64.b64encode(image_storage.read()), str(t) + ".png"
|
||||
)
|
||||
asyncio.run_coroutine_threadsafe(receiver.say(msg), loop).result()
|
||||
logger.info("[WX] sendImage, receiver={}".format(receiver))
|
||||
|
||||
async def on_message(self, msg: Message):
|
||||
"""
|
||||
@@ -110,16 +124,16 @@ class WechatyChannel(ChatChannel):
|
||||
try:
|
||||
cmsg = await WechatyMessage(msg)
|
||||
except NotImplementedError as e:
|
||||
logger.debug('[WX] {}'.format(e))
|
||||
logger.debug("[WX] {}".format(e))
|
||||
return
|
||||
except Exception as e:
|
||||
logger.exception('[WX] {}'.format(e))
|
||||
logger.exception("[WX] {}".format(e))
|
||||
return
|
||||
logger.debug('[WX] message:{}'.format(cmsg))
|
||||
logger.debug("[WX] message:{}".format(cmsg))
|
||||
room = msg.room() # 获取消息来自的群聊. 如果消息不是来自群聊, 则返回None
|
||||
isgroup = room is not None
|
||||
ctype = cmsg.ctype
|
||||
context = self._compose_context(ctype, cmsg.content, isgroup=isgroup, msg=cmsg)
|
||||
if context:
|
||||
logger.info('[WX] receiveMsg={}, context={}'.format(cmsg, context))
|
||||
self.produce(context)
|
||||
logger.info("[WX] receiveMsg={}, context={}".format(cmsg, context))
|
||||
self.produce(context)
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
from wechaty import MessageType
|
||||
from wechaty.user import Message
|
||||
|
||||
from bridge.context import ContextType
|
||||
from channel.chat_message import ChatMessage
|
||||
from common.tmp_dir import TmpDir
|
||||
from common.log import logger
|
||||
from wechaty.user import Message
|
||||
from common.tmp_dir import TmpDir
|
||||
|
||||
|
||||
class aobject(object):
|
||||
"""Inheriting this class allows you to define an async __init__.
|
||||
|
||||
So you can create objects by doing something like `await MyClass(params)`
|
||||
"""
|
||||
|
||||
async def __new__(cls, *a, **kw):
|
||||
instance = super().__new__(cls)
|
||||
await instance.__init__(*a, **kw)
|
||||
@@ -19,17 +23,18 @@ class aobject(object):
|
||||
|
||||
async def __init__(self):
|
||||
pass
|
||||
class WechatyMessage(ChatMessage, aobject):
|
||||
|
||||
|
||||
class WechatyMessage(ChatMessage, aobject):
|
||||
async def __init__(self, wechaty_msg: Message):
|
||||
super().__init__(wechaty_msg)
|
||||
|
||||
|
||||
room = wechaty_msg.room()
|
||||
|
||||
self.msg_id = wechaty_msg.message_id
|
||||
self.create_time = wechaty_msg.payload.timestamp
|
||||
self.is_group = room is not None
|
||||
|
||||
|
||||
if wechaty_msg.type() == MessageType.MESSAGE_TYPE_TEXT:
|
||||
self.ctype = ContextType.TEXT
|
||||
self.content = wechaty_msg.text()
|
||||
@@ -40,12 +45,17 @@ class WechatyMessage(ChatMessage, aobject):
|
||||
|
||||
def func():
|
||||
loop = asyncio.get_event_loop()
|
||||
asyncio.run_coroutine_threadsafe(voice_file.to_file(self.content),loop).result()
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
voice_file.to_file(self.content), loop
|
||||
).result()
|
||||
|
||||
self._prepare_fn = func
|
||||
|
||||
|
||||
else:
|
||||
raise NotImplementedError("Unsupported message type: {}".format(wechaty_msg.type()))
|
||||
|
||||
raise NotImplementedError(
|
||||
"Unsupported message type: {}".format(wechaty_msg.type())
|
||||
)
|
||||
|
||||
from_contact = wechaty_msg.talker() # 获取消息的发送者
|
||||
self.from_user_id = from_contact.contact_id
|
||||
self.from_user_nickname = from_contact.name
|
||||
@@ -54,7 +64,7 @@ class WechatyMessage(ChatMessage, aobject):
|
||||
# wecahty: from是消息实际发送者, to:所在群
|
||||
# itchat: 如果是你发送群消息,from和to是你自己和所在群,如果是别人发群消息,from和to是所在群和你自己
|
||||
# 但这个差别不影响逻辑,group中只使用到:1.用from来判断是否是自己发的,2.actual_user_id来判断实际发送用户
|
||||
|
||||
|
||||
if self.is_group:
|
||||
self.to_user_id = room.room_id
|
||||
self.to_user_nickname = await room.topic()
|
||||
@@ -63,22 +73,22 @@ class WechatyMessage(ChatMessage, aobject):
|
||||
self.to_user_id = to_contact.contact_id
|
||||
self.to_user_nickname = to_contact.name
|
||||
|
||||
if self.is_group or wechaty_msg.is_self(): # 如果是群消息,other_user设置为群,如果是私聊消息,而且自己发的,就设置成对方。
|
||||
if (
|
||||
self.is_group or wechaty_msg.is_self()
|
||||
): # 如果是群消息,other_user设置为群,如果是私聊消息,而且自己发的,就设置成对方。
|
||||
self.other_user_id = self.to_user_id
|
||||
self.other_user_nickname = self.to_user_nickname
|
||||
else:
|
||||
self.other_user_id = self.from_user_id
|
||||
self.other_user_nickname = self.from_user_nickname
|
||||
|
||||
|
||||
|
||||
if self.is_group: # wechaty群聊中,实际发送用户就是from_user
|
||||
if self.is_group: # wechaty群聊中,实际发送用户就是from_user
|
||||
self.is_at = await wechaty_msg.mention_self()
|
||||
if not self.is_at: # 有时候复制粘贴的消息,不算做@,但是内容里面会有@xxx,这里做一下兼容
|
||||
if not self.is_at: # 有时候复制粘贴的消息,不算做@,但是内容里面会有@xxx,这里做一下兼容
|
||||
name = wechaty_msg.wechaty.user_self().name
|
||||
pattern = f'@{name}(\u2005|\u0020)'
|
||||
if re.search(pattern,self.content):
|
||||
logger.debug(f'wechaty message {self.msg_id} include at')
|
||||
pattern = f"@{name}(\u2005|\u0020)"
|
||||
if re.search(pattern, self.content):
|
||||
logger.debug(f"wechaty message {self.msg_id} include at")
|
||||
self.is_at = True
|
||||
|
||||
self.actual_user_id = self.from_user_id
|
||||
|
||||
@@ -21,12 +21,12 @@ pip3 install web.py
|
||||
|
||||
相关的服务器验证代码已经写好,你不需要再添加任何代码。你只需要在本项目根目录的`config.json`中添加
|
||||
```
|
||||
"channel_type": "wechatmp",
|
||||
"channel_type": "wechatmp",
|
||||
"wechatmp_token": "Token", # 微信公众平台的Token
|
||||
"wechatmp_port": 8080, # 微信公众平台的端口,需要端口转发到80或443
|
||||
"wechatmp_app_id": "", # 微信公众平台的appID,仅服务号需要
|
||||
"wechatmp_app_secret": "", # 微信公众平台的appsecret,仅服务号需要
|
||||
```
|
||||
```
|
||||
然后运行`python3 app.py`启动web服务器。这里会默认监听8080端口,但是微信公众号的服务器配置只支持80/443端口,有两种方法来解决这个问题。第一个是推荐的方法,使用端口转发命令将80端口转发到8080端口(443同理,注意需要支持SSL,也就是https的访问,在`wechatmp_channel.py`需要修改相应的证书路径):
|
||||
```
|
||||
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
|
||||
|
||||
@@ -1,46 +1,66 @@
|
||||
import web
|
||||
import time
|
||||
import channel.wechatmp.reply as reply
|
||||
|
||||
import web
|
||||
|
||||
import channel.wechatmp.receive as receive
|
||||
from config import conf
|
||||
from common.log import logger
|
||||
import channel.wechatmp.reply as reply
|
||||
from bridge.context import *
|
||||
from channel.wechatmp.common import *
|
||||
from channel.wechatmp.common import *
|
||||
from channel.wechatmp.wechatmp_channel import WechatMPChannel
|
||||
from common.log import logger
|
||||
from config import conf
|
||||
|
||||
|
||||
# This class is instantiated once per query
|
||||
class Query():
|
||||
|
||||
class Query:
|
||||
def GET(self):
|
||||
return verify_server(web.input())
|
||||
|
||||
def POST(self):
|
||||
# Make sure to return the instance that first created, @singleton will do that.
|
||||
# Make sure to return the instance that first created, @singleton will do that.
|
||||
channel = WechatMPChannel()
|
||||
try:
|
||||
webData = web.data()
|
||||
# logger.debug("[wechatmp] Receive request:\n" + webData.decode("utf-8"))
|
||||
wechatmp_msg = receive.parse_xml(webData)
|
||||
if wechatmp_msg.msg_type == 'text' or wechatmp_msg.msg_type == 'voice':
|
||||
if wechatmp_msg.msg_type == "text" or wechatmp_msg.msg_type == "voice":
|
||||
from_user = wechatmp_msg.from_user_id
|
||||
message = wechatmp_msg.content.decode("utf-8")
|
||||
message_id = wechatmp_msg.msg_id
|
||||
|
||||
logger.info("[wechatmp] {}:{} Receive post query {} {}: {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), from_user, message_id, message))
|
||||
context = channel._compose_context(ContextType.TEXT, message, isgroup=False, msg=wechatmp_msg)
|
||||
logger.info(
|
||||
"[wechatmp] {}:{} Receive post query {} {}: {}".format(
|
||||
web.ctx.env.get("REMOTE_ADDR"),
|
||||
web.ctx.env.get("REMOTE_PORT"),
|
||||
from_user,
|
||||
message_id,
|
||||
message,
|
||||
)
|
||||
)
|
||||
context = channel._compose_context(
|
||||
ContextType.TEXT, message, isgroup=False, msg=wechatmp_msg
|
||||
)
|
||||
if context:
|
||||
# set private openai_api_key
|
||||
# if from_user is not changed in itchat, this can be placed at chat_channel
|
||||
user_data = conf().get_user_data(from_user)
|
||||
context['openai_api_key'] = user_data.get('openai_api_key') # None or user openai_api_key
|
||||
context["openai_api_key"] = user_data.get(
|
||||
"openai_api_key"
|
||||
) # None or user openai_api_key
|
||||
channel.produce(context)
|
||||
# The reply will be sent by channel.send() in another thread
|
||||
return "success"
|
||||
|
||||
elif wechatmp_msg.msg_type == 'event':
|
||||
logger.info("[wechatmp] Event {} from {}".format(wechatmp_msg.Event, wechatmp_msg.from_user_id))
|
||||
elif wechatmp_msg.msg_type == "event":
|
||||
logger.info(
|
||||
"[wechatmp] Event {} from {}".format(
|
||||
wechatmp_msg.Event, wechatmp_msg.from_user_id
|
||||
)
|
||||
)
|
||||
content = subscribe_msg()
|
||||
replyMsg = reply.TextMsg(wechatmp_msg.from_user_id, wechatmp_msg.to_user_id, content)
|
||||
replyMsg = reply.TextMsg(
|
||||
wechatmp_msg.from_user_id, wechatmp_msg.to_user_id, content
|
||||
)
|
||||
return replyMsg.send()
|
||||
else:
|
||||
logger.info("暂且不处理")
|
||||
@@ -48,4 +68,3 @@ class Query():
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
return exc
|
||||
|
||||
|
||||
@@ -1,81 +1,117 @@
|
||||
import web
|
||||
import time
|
||||
import channel.wechatmp.reply as reply
|
||||
|
||||
import web
|
||||
|
||||
import channel.wechatmp.receive as receive
|
||||
from config import conf
|
||||
from common.log import logger
|
||||
import channel.wechatmp.reply as reply
|
||||
from bridge.context import *
|
||||
from channel.wechatmp.common import *
|
||||
from channel.wechatmp.common import *
|
||||
from channel.wechatmp.wechatmp_channel import WechatMPChannel
|
||||
from common.log import logger
|
||||
from config import conf
|
||||
|
||||
|
||||
# This class is instantiated once per query
|
||||
class Query():
|
||||
|
||||
class Query:
|
||||
def GET(self):
|
||||
return verify_server(web.input())
|
||||
|
||||
def POST(self):
|
||||
# Make sure to return the instance that first created, @singleton will do that.
|
||||
# Make sure to return the instance that first created, @singleton will do that.
|
||||
channel = WechatMPChannel()
|
||||
try:
|
||||
query_time = time.time()
|
||||
webData = web.data()
|
||||
logger.debug("[wechatmp] Receive request:\n" + webData.decode("utf-8"))
|
||||
wechatmp_msg = receive.parse_xml(webData)
|
||||
if wechatmp_msg.msg_type == 'text' or wechatmp_msg.msg_type == 'voice':
|
||||
if wechatmp_msg.msg_type == "text" or wechatmp_msg.msg_type == "voice":
|
||||
from_user = wechatmp_msg.from_user_id
|
||||
to_user = wechatmp_msg.to_user_id
|
||||
message = wechatmp_msg.content.decode("utf-8")
|
||||
message_id = wechatmp_msg.msg_id
|
||||
|
||||
logger.info("[wechatmp] {}:{} Receive post query {} {}: {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), from_user, message_id, message))
|
||||
logger.info(
|
||||
"[wechatmp] {}:{} Receive post query {} {}: {}".format(
|
||||
web.ctx.env.get("REMOTE_ADDR"),
|
||||
web.ctx.env.get("REMOTE_PORT"),
|
||||
from_user,
|
||||
message_id,
|
||||
message,
|
||||
)
|
||||
)
|
||||
supported = True
|
||||
if "【收到不支持的消息类型,暂无法显示】" in message:
|
||||
supported = False # not supported, used to refresh
|
||||
supported = False # not supported, used to refresh
|
||||
cache_key = from_user
|
||||
|
||||
reply_text = ""
|
||||
# New request
|
||||
if cache_key not in channel.cache_dict and cache_key not in channel.running:
|
||||
if (
|
||||
cache_key not in channel.cache_dict
|
||||
and cache_key not in channel.running
|
||||
):
|
||||
# The first query begin, reset the cache
|
||||
context = channel._compose_context(ContextType.TEXT, message, isgroup=False, msg=wechatmp_msg)
|
||||
logger.debug("[wechatmp] context: {} {}".format(context, wechatmp_msg))
|
||||
if message_id in channel.received_msgs: # received and finished
|
||||
context = channel._compose_context(
|
||||
ContextType.TEXT, message, isgroup=False, msg=wechatmp_msg
|
||||
)
|
||||
logger.debug(
|
||||
"[wechatmp] context: {} {}".format(context, wechatmp_msg)
|
||||
)
|
||||
if message_id in channel.received_msgs: # received and finished
|
||||
# no return because of bandwords or other reasons
|
||||
return "success"
|
||||
if supported and context:
|
||||
# set private openai_api_key
|
||||
# if from_user is not changed in itchat, this can be placed at chat_channel
|
||||
user_data = conf().get_user_data(from_user)
|
||||
context['openai_api_key'] = user_data.get('openai_api_key') # None or user openai_api_key
|
||||
context["openai_api_key"] = user_data.get(
|
||||
"openai_api_key"
|
||||
) # None or user openai_api_key
|
||||
channel.received_msgs[message_id] = wechatmp_msg
|
||||
channel.running.add(cache_key)
|
||||
channel.produce(context)
|
||||
else:
|
||||
trigger_prefix = conf().get('single_chat_prefix',[''])[0]
|
||||
trigger_prefix = conf().get("single_chat_prefix", [""])[0]
|
||||
if trigger_prefix or not supported:
|
||||
if trigger_prefix:
|
||||
content = textwrap.dedent(f"""\
|
||||
content = textwrap.dedent(
|
||||
f"""\
|
||||
请输入'{trigger_prefix}'接你想说的话跟我说话。
|
||||
例如:
|
||||
{trigger_prefix}你好,很高兴见到你。""")
|
||||
{trigger_prefix}你好,很高兴见到你。"""
|
||||
)
|
||||
else:
|
||||
content = textwrap.dedent("""\
|
||||
content = textwrap.dedent(
|
||||
"""\
|
||||
你好,很高兴见到你。
|
||||
请跟我说话吧。""")
|
||||
请跟我说话吧。"""
|
||||
)
|
||||
else:
|
||||
logger.error(f"[wechatmp] unknown error")
|
||||
content = textwrap.dedent("""\
|
||||
未知错误,请稍后再试""")
|
||||
replyMsg = reply.TextMsg(wechatmp_msg.from_user_id, wechatmp_msg.to_user_id, content)
|
||||
content = textwrap.dedent(
|
||||
"""\
|
||||
未知错误,请稍后再试"""
|
||||
)
|
||||
replyMsg = reply.TextMsg(
|
||||
wechatmp_msg.from_user_id, wechatmp_msg.to_user_id, content
|
||||
)
|
||||
return replyMsg.send()
|
||||
channel.query1[cache_key] = False
|
||||
channel.query2[cache_key] = False
|
||||
channel.query3[cache_key] = False
|
||||
# User request again, and the answer is not ready
|
||||
elif cache_key in channel.running and channel.query1.get(cache_key) == True and channel.query2.get(cache_key) == True and channel.query3.get(cache_key) == True:
|
||||
channel.query1[cache_key] = False #To improve waiting experience, this can be set to True.
|
||||
channel.query2[cache_key] = False #To improve waiting experience, this can be set to True.
|
||||
elif (
|
||||
cache_key in channel.running
|
||||
and channel.query1.get(cache_key) == True
|
||||
and channel.query2.get(cache_key) == True
|
||||
and channel.query3.get(cache_key) == True
|
||||
):
|
||||
channel.query1[
|
||||
cache_key
|
||||
] = False # To improve waiting experience, this can be set to True.
|
||||
channel.query2[
|
||||
cache_key
|
||||
] = False # To improve waiting experience, this can be set to True.
|
||||
channel.query3[cache_key] = False
|
||||
# User request again, and the answer is ready
|
||||
elif cache_key in channel.cache_dict:
|
||||
@@ -84,7 +120,9 @@ class Query():
|
||||
channel.query2[cache_key] = True
|
||||
channel.query3[cache_key] = True
|
||||
|
||||
assert not (cache_key in channel.cache_dict and cache_key in channel.running)
|
||||
assert not (
|
||||
cache_key in channel.cache_dict and cache_key in channel.running
|
||||
)
|
||||
|
||||
if channel.query1.get(cache_key) == False:
|
||||
# The first query from wechat official server
|
||||
@@ -128,14 +166,20 @@ class Query():
|
||||
# Have waiting for 3x5 seconds
|
||||
# return timeout message
|
||||
reply_text = "【正在思考中,回复任意文字尝试获取回复】"
|
||||
logger.info("[wechatmp] Three queries has finished For {}: {}".format(from_user, message_id))
|
||||
logger.info(
|
||||
"[wechatmp] Three queries has finished For {}: {}".format(
|
||||
from_user, message_id
|
||||
)
|
||||
)
|
||||
replyPost = reply.TextMsg(from_user, to_user, reply_text).send()
|
||||
return replyPost
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
if cache_key not in channel.cache_dict and cache_key not in channel.running:
|
||||
if (
|
||||
cache_key not in channel.cache_dict
|
||||
and cache_key not in channel.running
|
||||
):
|
||||
# no return because of bandwords or other reasons
|
||||
return "success"
|
||||
|
||||
@@ -147,26 +191,42 @@ class Query():
|
||||
|
||||
if cache_key in channel.cache_dict:
|
||||
content = channel.cache_dict[cache_key]
|
||||
if len(content.encode('utf8'))<=MAX_UTF8_LEN:
|
||||
if len(content.encode("utf8")) <= MAX_UTF8_LEN:
|
||||
reply_text = channel.cache_dict[cache_key]
|
||||
channel.cache_dict.pop(cache_key)
|
||||
else:
|
||||
continue_text = "\n【未完待续,回复任意文字以继续】"
|
||||
splits = split_string_by_utf8_length(content, MAX_UTF8_LEN - len(continue_text.encode('utf-8')), max_split= 1)
|
||||
splits = split_string_by_utf8_length(
|
||||
content,
|
||||
MAX_UTF8_LEN - len(continue_text.encode("utf-8")),
|
||||
max_split=1,
|
||||
)
|
||||
reply_text = splits[0] + continue_text
|
||||
channel.cache_dict[cache_key] = splits[1]
|
||||
logger.info("[wechatmp] {}:{} Do send {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), reply_text))
|
||||
logger.info(
|
||||
"[wechatmp] {}:{} Do send {}".format(
|
||||
web.ctx.env.get("REMOTE_ADDR"),
|
||||
web.ctx.env.get("REMOTE_PORT"),
|
||||
reply_text,
|
||||
)
|
||||
)
|
||||
replyPost = reply.TextMsg(from_user, to_user, reply_text).send()
|
||||
return replyPost
|
||||
|
||||
elif wechatmp_msg.msg_type == 'event':
|
||||
logger.info("[wechatmp] Event {} from {}".format(wechatmp_msg.content, wechatmp_msg.from_user_id))
|
||||
elif wechatmp_msg.msg_type == "event":
|
||||
logger.info(
|
||||
"[wechatmp] Event {} from {}".format(
|
||||
wechatmp_msg.content, wechatmp_msg.from_user_id
|
||||
)
|
||||
)
|
||||
content = subscribe_msg()
|
||||
replyMsg = reply.TextMsg(wechatmp_msg.from_user_id, wechatmp_msg.to_user_id, content)
|
||||
replyMsg = reply.TextMsg(
|
||||
wechatmp_msg.from_user_id, wechatmp_msg.to_user_id, content
|
||||
)
|
||||
return replyMsg.send()
|
||||
else:
|
||||
logger.info("暂且不处理")
|
||||
return "success"
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
return exc
|
||||
return exc
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from config import conf
|
||||
import hashlib
|
||||
import textwrap
|
||||
|
||||
from config import conf
|
||||
|
||||
MAX_UTF8_LEN = 2048
|
||||
|
||||
|
||||
class WeChatAPIException(Exception):
|
||||
pass
|
||||
|
||||
@@ -16,13 +18,13 @@ def verify_server(data):
|
||||
timestamp = data.timestamp
|
||||
nonce = data.nonce
|
||||
echostr = data.echostr
|
||||
token = conf().get('wechatmp_token') #请按照公众平台官网\基本配置中信息填写
|
||||
token = conf().get("wechatmp_token") # 请按照公众平台官网\基本配置中信息填写
|
||||
|
||||
data_list = [token, timestamp, nonce]
|
||||
data_list.sort()
|
||||
sha1 = hashlib.sha1()
|
||||
# map(sha1.update, data_list) #python2
|
||||
sha1.update("".join(data_list).encode('utf-8'))
|
||||
sha1.update("".join(data_list).encode("utf-8"))
|
||||
hashcode = sha1.hexdigest()
|
||||
print("handle/GET func: hashcode, signature: ", hashcode, signature)
|
||||
if hashcode == signature:
|
||||
@@ -32,9 +34,11 @@ def verify_server(data):
|
||||
except Exception as Argument:
|
||||
return Argument
|
||||
|
||||
|
||||
def subscribe_msg():
|
||||
trigger_prefix = conf().get('single_chat_prefix',[''])[0]
|
||||
msg = textwrap.dedent(f"""\
|
||||
trigger_prefix = conf().get("single_chat_prefix", [""])[0]
|
||||
msg = textwrap.dedent(
|
||||
f"""\
|
||||
感谢您的关注!
|
||||
这里是ChatGPT,可以自由对话。
|
||||
资源有限,回复较慢,请勿着急。
|
||||
@@ -42,22 +46,23 @@ def subscribe_msg():
|
||||
暂时不支持图片输入。
|
||||
支持图片输出,画字开头的问题将回复图片链接。
|
||||
支持角色扮演和文字冒险两种定制模式对话。
|
||||
输入'{trigger_prefix}#帮助' 查看详细指令。""")
|
||||
输入'{trigger_prefix}#帮助' 查看详细指令。"""
|
||||
)
|
||||
return msg
|
||||
|
||||
|
||||
def split_string_by_utf8_length(string, max_length, max_split=0):
|
||||
encoded = string.encode('utf-8')
|
||||
encoded = string.encode("utf-8")
|
||||
start, end = 0, 0
|
||||
result = []
|
||||
while end < len(encoded):
|
||||
if max_split > 0 and len(result) >= max_split:
|
||||
result.append(encoded[start:].decode('utf-8'))
|
||||
result.append(encoded[start:].decode("utf-8"))
|
||||
break
|
||||
end = start + max_length
|
||||
# 如果当前字节不是 UTF-8 编码的开始字节,则向前查找直到找到开始字节为止
|
||||
while end < len(encoded) and (encoded[end] & 0b11000000) == 0b10000000:
|
||||
end -= 1
|
||||
result.append(encoded[start:end].decode('utf-8'))
|
||||
result.append(encoded[start:end].decode("utf-8"))
|
||||
start = end
|
||||
return result
|
||||
return result
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-#
|
||||
# filename: receive.py
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from bridge.context import ContextType
|
||||
from channel.chat_message import ChatMessage
|
||||
from common.log import logger
|
||||
@@ -12,34 +13,35 @@ def parse_xml(web_data):
|
||||
xmlData = ET.fromstring(web_data)
|
||||
return WeChatMPMessage(xmlData)
|
||||
|
||||
|
||||
class WeChatMPMessage(ChatMessage):
|
||||
def __init__(self, xmlData):
|
||||
super().__init__(xmlData)
|
||||
self.to_user_id = xmlData.find('ToUserName').text
|
||||
self.from_user_id = xmlData.find('FromUserName').text
|
||||
self.create_time = xmlData.find('CreateTime').text
|
||||
self.msg_type = xmlData.find('MsgType').text
|
||||
self.to_user_id = xmlData.find("ToUserName").text
|
||||
self.from_user_id = xmlData.find("FromUserName").text
|
||||
self.create_time = xmlData.find("CreateTime").text
|
||||
self.msg_type = xmlData.find("MsgType").text
|
||||
try:
|
||||
self.msg_id = xmlData.find('MsgId').text
|
||||
self.msg_id = xmlData.find("MsgId").text
|
||||
except:
|
||||
self.msg_id = self.from_user_id+self.create_time
|
||||
self.msg_id = self.from_user_id + self.create_time
|
||||
self.is_group = False
|
||||
|
||||
|
||||
# reply to other_user_id
|
||||
self.other_user_id = self.from_user_id
|
||||
|
||||
if self.msg_type == 'text':
|
||||
if self.msg_type == "text":
|
||||
self.ctype = ContextType.TEXT
|
||||
self.content = xmlData.find('Content').text.encode("utf-8")
|
||||
elif self.msg_type == 'voice':
|
||||
self.content = xmlData.find("Content").text.encode("utf-8")
|
||||
elif self.msg_type == "voice":
|
||||
self.ctype = ContextType.TEXT
|
||||
self.content = xmlData.find('Recognition').text.encode("utf-8") # 接收语音识别结果
|
||||
elif self.msg_type == 'image':
|
||||
self.content = xmlData.find("Recognition").text.encode("utf-8") # 接收语音识别结果
|
||||
elif self.msg_type == "image":
|
||||
# not implemented
|
||||
self.pic_url = xmlData.find('PicUrl').text
|
||||
self.media_id = xmlData.find('MediaId').text
|
||||
elif self.msg_type == 'event':
|
||||
self.content = xmlData.find('Event').text
|
||||
else: # video, shortvideo, location, link
|
||||
self.pic_url = xmlData.find("PicUrl").text
|
||||
self.media_id = xmlData.find("MediaId").text
|
||||
elif self.msg_type == "event":
|
||||
self.content = xmlData.find("Event").text
|
||||
else: # video, shortvideo, location, link
|
||||
# not implemented
|
||||
pass
|
||||
pass
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# filename: reply.py
|
||||
import time
|
||||
|
||||
|
||||
class Msg(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
@@ -9,13 +10,14 @@ class Msg(object):
|
||||
def send(self):
|
||||
return "success"
|
||||
|
||||
|
||||
class TextMsg(Msg):
|
||||
def __init__(self, toUserName, fromUserName, content):
|
||||
self.__dict = dict()
|
||||
self.__dict['ToUserName'] = toUserName
|
||||
self.__dict['FromUserName'] = fromUserName
|
||||
self.__dict['CreateTime'] = int(time.time())
|
||||
self.__dict['Content'] = content
|
||||
self.__dict["ToUserName"] = toUserName
|
||||
self.__dict["FromUserName"] = fromUserName
|
||||
self.__dict["CreateTime"] = int(time.time())
|
||||
self.__dict["Content"] = content
|
||||
|
||||
def send(self):
|
||||
XmlForm = """
|
||||
@@ -29,13 +31,14 @@ class TextMsg(Msg):
|
||||
"""
|
||||
return XmlForm.format(**self.__dict)
|
||||
|
||||
|
||||
class ImageMsg(Msg):
|
||||
def __init__(self, toUserName, fromUserName, mediaId):
|
||||
self.__dict = dict()
|
||||
self.__dict['ToUserName'] = toUserName
|
||||
self.__dict['FromUserName'] = fromUserName
|
||||
self.__dict['CreateTime'] = int(time.time())
|
||||
self.__dict['MediaId'] = mediaId
|
||||
self.__dict["ToUserName"] = toUserName
|
||||
self.__dict["FromUserName"] = fromUserName
|
||||
self.__dict["CreateTime"] = int(time.time())
|
||||
self.__dict["MediaId"] = mediaId
|
||||
|
||||
def send(self):
|
||||
XmlForm = """
|
||||
@@ -49,4 +52,4 @@ class ImageMsg(Msg):
|
||||
</Image>
|
||||
</xml>
|
||||
"""
|
||||
return XmlForm.format(**self.__dict)
|
||||
return XmlForm.format(**self.__dict)
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import web
|
||||
import time
|
||||
import json
|
||||
import requests
|
||||
import threading
|
||||
from common.singleton import singleton
|
||||
from common.log import logger
|
||||
from common.expired_dict import ExpiredDict
|
||||
from config import conf
|
||||
from bridge.reply import *
|
||||
import time
|
||||
|
||||
import requests
|
||||
import web
|
||||
|
||||
from bridge.context import *
|
||||
from bridge.reply import *
|
||||
from channel.chat_channel import ChatChannel
|
||||
from channel.wechatmp.common import *
|
||||
from channel.wechatmp.common import *
|
||||
from common.expired_dict import ExpiredDict
|
||||
from common.log import logger
|
||||
from common.singleton import singleton
|
||||
from config import conf
|
||||
|
||||
# If using SSL, uncomment the following lines, and modify the certificate path.
|
||||
# from cheroot.server import HTTPServer
|
||||
@@ -20,13 +22,14 @@ from channel.wechatmp.common import *
|
||||
# certificate='/ssl/cert.pem',
|
||||
# private_key='/ssl/cert.key')
|
||||
|
||||
|
||||
@singleton
|
||||
class WechatMPChannel(ChatChannel):
|
||||
def __init__(self, passive_reply = True):
|
||||
def __init__(self, passive_reply=True):
|
||||
super().__init__()
|
||||
self.passive_reply = passive_reply
|
||||
self.running = set()
|
||||
self.received_msgs = ExpiredDict(60*60*24)
|
||||
self.received_msgs = ExpiredDict(60 * 60 * 24)
|
||||
if self.passive_reply:
|
||||
self.NOT_SUPPORT_REPLYTYPE = [ReplyType.IMAGE, ReplyType.VOICE]
|
||||
self.cache_dict = dict()
|
||||
@@ -36,8 +39,8 @@ class WechatMPChannel(ChatChannel):
|
||||
else:
|
||||
# TODO support image
|
||||
self.NOT_SUPPORT_REPLYTYPE = [ReplyType.IMAGE, ReplyType.VOICE]
|
||||
self.app_id = conf().get('wechatmp_app_id')
|
||||
self.app_secret = conf().get('wechatmp_app_secret')
|
||||
self.app_id = conf().get("wechatmp_app_id")
|
||||
self.app_secret = conf().get("wechatmp_app_secret")
|
||||
self.access_token = None
|
||||
self.access_token_expires_time = 0
|
||||
self.access_token_lock = threading.Lock()
|
||||
@@ -45,13 +48,12 @@ class WechatMPChannel(ChatChannel):
|
||||
|
||||
def startup(self):
|
||||
if self.passive_reply:
|
||||
urls = ('/wx', 'channel.wechatmp.SubscribeAccount.Query')
|
||||
urls = ("/wx", "channel.wechatmp.SubscribeAccount.Query")
|
||||
else:
|
||||
urls = ('/wx', 'channel.wechatmp.ServiceAccount.Query')
|
||||
urls = ("/wx", "channel.wechatmp.ServiceAccount.Query")
|
||||
app = web.application(urls, globals(), autoreload=False)
|
||||
port = conf().get('wechatmp_port', 8080)
|
||||
web.httpserver.runsimple(app.wsgifunc(), ('0.0.0.0', port))
|
||||
|
||||
port = conf().get("wechatmp_port", 8080)
|
||||
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
|
||||
|
||||
def wechatmp_request(self, method, url, **kwargs):
|
||||
r = requests.request(method=method, url=url, **kwargs)
|
||||
@@ -63,7 +65,6 @@ class WechatMPChannel(ChatChannel):
|
||||
return ret
|
||||
|
||||
def get_access_token(self):
|
||||
|
||||
# return the access_token
|
||||
if self.access_token:
|
||||
if self.access_token_expires_time - time.time() > 60:
|
||||
@@ -76,15 +77,15 @@ class WechatMPChannel(ChatChannel):
|
||||
# This happens every 2 hours, so it doesn't affect the experience very much
|
||||
time.sleep(1)
|
||||
self.access_token = None
|
||||
url="https://api.weixin.qq.com/cgi-bin/token"
|
||||
params={
|
||||
url = "https://api.weixin.qq.com/cgi-bin/token"
|
||||
params = {
|
||||
"grant_type": "client_credential",
|
||||
"appid": self.app_id,
|
||||
"secret": self.app_secret
|
||||
"secret": self.app_secret,
|
||||
}
|
||||
data = self.wechatmp_request(method='get', url=url, params=params)
|
||||
self.access_token = data['access_token']
|
||||
self.access_token_expires_time = int(time.time()) + data['expires_in']
|
||||
data = self.wechatmp_request(method="get", url=url, params=params)
|
||||
self.access_token = data["access_token"]
|
||||
self.access_token_expires_time = int(time.time()) + data["expires_in"]
|
||||
logger.info("[wechatmp] access_token: {}".format(self.access_token))
|
||||
self.access_token_lock.release()
|
||||
else:
|
||||
@@ -101,29 +102,37 @@ class WechatMPChannel(ChatChannel):
|
||||
else:
|
||||
receiver = context["receiver"]
|
||||
reply_text = reply.content
|
||||
url="https://api.weixin.qq.com/cgi-bin/message/custom/send"
|
||||
params = {
|
||||
"access_token": self.get_access_token()
|
||||
}
|
||||
url = "https://api.weixin.qq.com/cgi-bin/message/custom/send"
|
||||
params = {"access_token": self.get_access_token()}
|
||||
json_data = {
|
||||
"touser": receiver,
|
||||
"msgtype": "text",
|
||||
"text": {"content": reply_text}
|
||||
"text": {"content": reply_text},
|
||||
}
|
||||
self.wechatmp_request(method='post', url=url, params=params, data=json.dumps(json_data, ensure_ascii=False).encode('utf8'))
|
||||
self.wechatmp_request(
|
||||
method="post",
|
||||
url=url,
|
||||
params=params,
|
||||
data=json.dumps(json_data, ensure_ascii=False).encode("utf8"),
|
||||
)
|
||||
logger.info("[send] Do send to {}: {}".format(receiver, reply_text))
|
||||
return
|
||||
|
||||
|
||||
def _success_callback(self, session_id, context, **kwargs): # 线程异常结束时的回调函数
|
||||
logger.debug("[wechatmp] Success to generate reply, msgId={}".format(context['msg'].msg_id))
|
||||
def _success_callback(self, session_id, context, **kwargs): # 线程异常结束时的回调函数
|
||||
logger.debug(
|
||||
"[wechatmp] Success to generate reply, msgId={}".format(
|
||||
context["msg"].msg_id
|
||||
)
|
||||
)
|
||||
if self.passive_reply:
|
||||
self.running.remove(session_id)
|
||||
|
||||
|
||||
def _fail_callback(self, session_id, exception, context, **kwargs): # 线程异常结束时的回调函数
|
||||
logger.exception("[wechatmp] Fail to generate reply to user, msgId={}, exception={}".format(context['msg'].msg_id, exception))
|
||||
def _fail_callback(self, session_id, exception, context, **kwargs): # 线程异常结束时的回调函数
|
||||
logger.exception(
|
||||
"[wechatmp] Fail to generate reply to user, msgId={}, exception={}".format(
|
||||
context["msg"].msg_id, exception
|
||||
)
|
||||
)
|
||||
if self.passive_reply:
|
||||
assert session_id not in self.cache_dict
|
||||
self.running.remove(session_id)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user