mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-07 22:36:02 +00:00
* feat(monitoring): link feedback to LangBot message ID and add feedback export - Add pipeline→adapter notification hook so monitoring message ID is passed back to WecomBotAdapter after creation - Store stream_id→monitoring_message_id mapping with 10-min TTL cleanup - Replace feedback record stream_id with LangBot monitoring message ID so feedback can be linked to actual message records - Rename streamId label to "Related Query ID" in all 7 i18n locales - Remove non-functional message ID jump button from FeedbackList - Add feedback export option to ExportDropdown (backend already implemented) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(monitoring): add combined refresh handler for monitoring and feedback data * fix(wecombot): improve stream ID mapping and error logging in WecomBotAdapter * feat(lark): add monitoring message ID mapping for feedback correlation * feat(lark): rename monitoring message ID mappings for clarity and consistency feat(feedback): add button to view conversation for feedback items * feat(bot-session-monitor): add feedback handling for bot messages with visual indicators * feat(bot-session-monitor): enhance feedback display with hover content for like/dislike indicators * fix(dingtalk): use voice recognition text instead of raw audio binary When DingTalk sends a voice message to the bot, the callback JSON contains a 'recognition' field with the speech-to-text result (powered by Qwen). Previously, LangBot only extracted the 'downloadCode' to download the raw audio binary and passed it as 'file_base64' to LLM APIs, which caused 400 errors since most models don't support this content type. This patch: - Extracts the 'recognition' field from DingTalk audio message content - Uses it as plain text input to the LLM instead of raw audio - Falls back to audio binary only when no recognition text is available - Fixes duplicate text issue for audio messages with recognition Fixes voice messages returning 'Request failed' on all LLM models. * fix: add filereader for dingtalk,lark (#2122) * fix: add filereader for dingtalk * feat: add lark * feat: update uv.lock * chore: update version to 4.9.6 in pyproject.toml, __init__.py, and uv.lock * fix: update langbot-plugin version to 0.3.8 * fix: update langbot-plugin version to 0.3.8 * fix(wecombot): extend StreamSession TTL for feedback sessions to prevent context data loss StreamSessionManager.cleanup() removes sessions after 60s TTL, but feedback events (like → cancel → dislike) can arrive later. When the session expires before the dislike event, all context fields (session_id, user_id, message_id, stream_id) are lost because get_session_by_feedback_id() returns None. Fix: Sessions with registered feedback_ids now use a 10-minute TTL, aligned with the adapter's _stream_to_monitoring_msg TTL in wecombot.py. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: 6mvp6 <13727783693@163.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: fdc310 <2213070223@qq.com> Co-authored-by: haiyangbg <zhouhaiyangaa@gmail.com> Co-authored-by: Guanchao Wang <wangcham233@gmail.com> Co-authored-by: Rock Chin <1010553892@qq.com>
534 lines
23 KiB
Python
534 lines
23 KiB
Python
from __future__ import annotations
|
||
import typing
|
||
import asyncio
|
||
import time
|
||
import traceback
|
||
|
||
import datetime
|
||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
||
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
||
from ..logger import EventLogger
|
||
from langbot.libs.wecom_ai_bot_api.wecombotevent import WecomBotEvent
|
||
from langbot.libs.wecom_ai_bot_api.api import WecomBotClient
|
||
from langbot.libs.wecom_ai_bot_api.ws_client import WecomBotWsClient
|
||
|
||
|
||
class WecomBotMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
||
@staticmethod
|
||
async def yiri2target(message_chain: platform_message.MessageChain):
|
||
content = ''
|
||
for msg in message_chain:
|
||
if type(msg) is platform_message.Plain:
|
||
content += msg.text
|
||
return content
|
||
|
||
@staticmethod
|
||
async def target2yiri(event: WecomBotEvent, bot_name: str = ''):
|
||
yiri_msg_list = []
|
||
if event.type == 'group':
|
||
yiri_msg_list.append(platform_message.At(target=event.ai_bot_id))
|
||
|
||
yiri_msg_list.append(platform_message.Source(id=event.message_id, time=datetime.datetime.now()))
|
||
|
||
if event.content:
|
||
content = event.content
|
||
if bot_name:
|
||
content = content.replace(f'@{bot_name}', '').strip()
|
||
yiri_msg_list.append(platform_message.Plain(text=content))
|
||
|
||
images = []
|
||
if event.images:
|
||
images.extend([img for img in event.images if img])
|
||
if not images and event.picurl:
|
||
images.append(event.picurl)
|
||
for image_base64 in images:
|
||
if image_base64:
|
||
yiri_msg_list.append(platform_message.Image(base64=image_base64))
|
||
|
||
file_info = event.file or {}
|
||
if file_info:
|
||
file_url = (
|
||
file_info.get('download_url')
|
||
or file_info.get('url')
|
||
or file_info.get('fileurl')
|
||
or file_info.get('path')
|
||
)
|
||
file_base64 = file_info.get('base64')
|
||
file_name = file_info.get('filename') or file_info.get('name')
|
||
file_size = file_info.get('filesize') or file_info.get('size')
|
||
file_data = file_url or file_base64
|
||
if file_data or file_name:
|
||
file_kwargs = {}
|
||
if file_data:
|
||
file_kwargs['url'] = file_data
|
||
if file_name:
|
||
file_kwargs['name'] = file_name
|
||
if file_size is not None:
|
||
file_kwargs['size'] = file_size
|
||
try:
|
||
yiri_msg_list.append(platform_message.File(**file_kwargs))
|
||
except Exception:
|
||
# 兜底
|
||
yiri_msg_list.append(platform_message.Unknown(text='[file message unsupported]'))
|
||
|
||
voice_info = event.voice or {}
|
||
if voice_info:
|
||
voice_payload = voice_info.get('base64') or voice_info.get('url')
|
||
if voice_payload:
|
||
if voice_info.get('base64') and not voice_payload.startswith('data:'):
|
||
voice_payload = f'data:audio/mpeg;base64,{voice_info.get("base64")}'
|
||
try:
|
||
yiri_msg_list.append(platform_message.Voice(base64=voice_payload))
|
||
except Exception:
|
||
try:
|
||
voice_kwargs = {'url': voice_payload}
|
||
voice_name = voice_info.get('filename') or voice_info.get('name')
|
||
voice_size = voice_info.get('filesize') or voice_info.get('size')
|
||
if voice_name:
|
||
voice_kwargs['name'] = voice_name
|
||
if voice_size is not None:
|
||
voice_kwargs['size'] = voice_size
|
||
yiri_msg_list.append(platform_message.File(**voice_kwargs))
|
||
except Exception:
|
||
yiri_msg_list.append(platform_message.Unknown(text='[voice message unsupported]'))
|
||
|
||
video_info = event.video or {}
|
||
if video_info:
|
||
video_payload = (
|
||
video_info.get('base64')
|
||
or video_info.get('url')
|
||
or video_info.get('download_url')
|
||
or video_info.get('fileurl')
|
||
)
|
||
if video_payload:
|
||
video_kwargs = {'url': video_payload}
|
||
video_name = video_info.get('filename') or video_info.get('name')
|
||
video_size = video_info.get('filesize') or video_info.get('size')
|
||
if video_name:
|
||
video_kwargs['name'] = video_name
|
||
if video_size is not None:
|
||
video_kwargs['size'] = video_size
|
||
try:
|
||
# 没有专门的视频类型,沿用 File 传递给上层
|
||
yiri_msg_list.append(platform_message.File(**video_kwargs))
|
||
except Exception:
|
||
yiri_msg_list.append(platform_message.Unknown(text='[video message unsupported]'))
|
||
|
||
if event.msgtype == 'link' and event.link:
|
||
link = event.link
|
||
summary = '\n'.join(
|
||
filter(
|
||
None,
|
||
[link.get('title', ''), link.get('description') or link.get('digest', ''), link.get('url', '')],
|
||
)
|
||
)
|
||
if summary:
|
||
yiri_msg_list.append(platform_message.Plain(text=summary))
|
||
|
||
# Handle quoted message (引用消息) - important for group chat file references
|
||
# Extract files/images/voice from quote and add them as top-level components
|
||
# so that plugins like FileReader can process them the same way as direct messages
|
||
quote_info = event.quote or {}
|
||
if quote_info:
|
||
# Process quote text content - add as Plain for context
|
||
if quote_info.get('content'):
|
||
yiri_msg_list.append(platform_message.Plain(text=f'[引用消息] {quote_info.get("content")}'))
|
||
|
||
# Process quote images - add as top-level Image components
|
||
quote_images = quote_info.get('images', [])
|
||
if not quote_images and quote_info.get('picurl'):
|
||
quote_images = [quote_info.get('picurl')]
|
||
for img_data in quote_images:
|
||
if img_data:
|
||
yiri_msg_list.append(platform_message.Image(base64=img_data))
|
||
|
||
# Process quote file - add as top-level File component (same as private chat)
|
||
quote_file = quote_info.get('file') or {}
|
||
if quote_file:
|
||
file_url = (
|
||
quote_file.get('base64')
|
||
or quote_file.get('download_url')
|
||
or quote_file.get('url')
|
||
or quote_file.get('fileurl')
|
||
)
|
||
file_name = quote_file.get('filename') or quote_file.get('name')
|
||
file_size = quote_file.get('filesize') or quote_file.get('size')
|
||
if file_url or file_name:
|
||
file_kwargs = {}
|
||
if file_url:
|
||
file_kwargs['url'] = file_url
|
||
if file_name:
|
||
file_kwargs['name'] = file_name
|
||
if file_size is not None:
|
||
file_kwargs['size'] = file_size
|
||
try:
|
||
yiri_msg_list.append(platform_message.File(**file_kwargs))
|
||
except Exception:
|
||
yiri_msg_list.append(platform_message.Unknown(text='[quoted file unsupported]'))
|
||
|
||
# Process quote voice - add as top-level Voice/File component
|
||
quote_voice = quote_info.get('voice') or {}
|
||
if quote_voice:
|
||
voice_payload = quote_voice.get('base64') or quote_voice.get('url')
|
||
if voice_payload:
|
||
if quote_voice.get('base64') and not voice_payload.startswith('data:'):
|
||
voice_payload = f'data:audio/mpeg;base64,{quote_voice.get("base64")}'
|
||
try:
|
||
yiri_msg_list.append(platform_message.Voice(base64=voice_payload))
|
||
except Exception:
|
||
try:
|
||
voice_kwargs = {'url': voice_payload}
|
||
voice_name = quote_voice.get('filename') or quote_voice.get('name')
|
||
voice_size = quote_voice.get('filesize') or quote_voice.get('size')
|
||
if voice_name:
|
||
voice_kwargs['name'] = voice_name
|
||
if voice_size is not None:
|
||
voice_kwargs['size'] = voice_size
|
||
yiri_msg_list.append(platform_message.File(**voice_kwargs))
|
||
except Exception:
|
||
yiri_msg_list.append(platform_message.Unknown(text='[quoted voice unsupported]'))
|
||
|
||
# Process quote video - add as top-level File component
|
||
quote_video = quote_info.get('video') or {}
|
||
if quote_video:
|
||
video_payload = (
|
||
quote_video.get('base64')
|
||
or quote_video.get('url')
|
||
or quote_video.get('download_url')
|
||
or quote_video.get('fileurl')
|
||
)
|
||
if video_payload:
|
||
video_kwargs = {'url': video_payload}
|
||
video_name = quote_video.get('filename') or quote_video.get('name')
|
||
video_size = quote_video.get('filesize') or quote_video.get('size')
|
||
if video_name:
|
||
video_kwargs['name'] = video_name
|
||
if video_size is not None:
|
||
video_kwargs['size'] = video_size
|
||
try:
|
||
yiri_msg_list.append(platform_message.File(**video_kwargs))
|
||
except Exception:
|
||
yiri_msg_list.append(platform_message.Unknown(text='[quoted video unsupported]'))
|
||
|
||
# Process quote link - add as Plain text
|
||
quote_link = quote_info.get('link') or {}
|
||
if quote_link:
|
||
link_summary = '\n'.join(
|
||
filter(
|
||
None,
|
||
[
|
||
quote_link.get('title', ''),
|
||
quote_link.get('description') or quote_link.get('digest', ''),
|
||
quote_link.get('url', ''),
|
||
],
|
||
)
|
||
)
|
||
if link_summary:
|
||
yiri_msg_list.append(platform_message.Plain(text=f'[引用链接] {link_summary}'))
|
||
|
||
has_content_element = any(
|
||
not isinstance(element, (platform_message.Source, platform_message.At)) for element in yiri_msg_list
|
||
)
|
||
if not has_content_element:
|
||
fallback_type = event.msgtype or 'unknown'
|
||
yiri_msg_list.append(platform_message.Unknown(text=f'[unsupported wecom msgtype: {fallback_type}]'))
|
||
chain = platform_message.MessageChain(yiri_msg_list)
|
||
|
||
return chain
|
||
|
||
|
||
class WecomBotEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||
def __init__(self, bot_name: str = ''):
|
||
self.bot_name = bot_name
|
||
|
||
@staticmethod
|
||
async def yiri2target(event: platform_events.MessageEvent):
|
||
return event.source_platform_object
|
||
|
||
async def target2yiri(self, event: WecomBotEvent):
|
||
message_chain = await WecomBotMessageConverter.target2yiri(event, bot_name=self.bot_name)
|
||
if event.type == 'single':
|
||
return platform_events.FriendMessage(
|
||
sender=platform_entities.Friend(
|
||
id=event.userid,
|
||
nickname=event.username,
|
||
remark='',
|
||
),
|
||
message_chain=message_chain,
|
||
time=datetime.datetime.now().timestamp(),
|
||
source_platform_object=event,
|
||
)
|
||
elif event.type == 'group':
|
||
try:
|
||
sender = platform_entities.GroupMember(
|
||
id=event.userid,
|
||
permission='MEMBER',
|
||
member_name=event.username,
|
||
group=platform_entities.Group(
|
||
id=str(event.chatid),
|
||
name=event.chatname,
|
||
permission=platform_entities.Permission.Member,
|
||
),
|
||
special_title='',
|
||
)
|
||
time = datetime.datetime.now().timestamp()
|
||
return platform_events.GroupMessage(
|
||
sender=sender,
|
||
message_chain=message_chain,
|
||
time=time,
|
||
source_platform_object=event,
|
||
)
|
||
except Exception:
|
||
print(traceback.format_exc())
|
||
|
||
|
||
class WecomBotAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||
bot: typing.Union[WecomBotClient, WecomBotWsClient]
|
||
bot_account_id: str
|
||
message_converter: WecomBotMessageConverter = WecomBotMessageConverter()
|
||
event_converter: WecomBotEventConverter
|
||
config: dict
|
||
bot_uuid: str = None
|
||
_ws_mode: bool = False
|
||
bot_name: str = ''
|
||
listeners: dict = {}
|
||
_stream_to_monitoring_msg: dict = {} # Maps stream_id to (monitoring_message_id, timestamp)
|
||
_STREAM_MAPPING_TTL = 600 # 10 minutes
|
||
|
||
def __init__(self, config: dict, logger: EventLogger):
|
||
enable_webhook = config.get('enable-webhook', False)
|
||
bot_name = config.get('robot_name', '')
|
||
|
||
if not enable_webhook:
|
||
bot = WecomBotWsClient(
|
||
bot_id=config['BotId'],
|
||
secret=config['Secret'],
|
||
logger=logger,
|
||
encoding_aes_key=config.get('EncodingAESKey', ''),
|
||
)
|
||
else:
|
||
# Webhook callback mode
|
||
required_keys = ['Token', 'EncodingAESKey', 'Corpid']
|
||
missing_keys = [key for key in required_keys if key not in config or not config[key]]
|
||
if missing_keys:
|
||
raise Exception(f'WecomBot webhook mode missing config: {missing_keys}')
|
||
|
||
bot = WecomBotClient(
|
||
Token=config['Token'],
|
||
EnCodingAESKey=config['EncodingAESKey'],
|
||
Corpid=config['Corpid'],
|
||
logger=logger,
|
||
unified_mode=True,
|
||
)
|
||
|
||
bot_account_id = config.get('BotId', '')
|
||
event_converter = WecomBotEventConverter(bot_name=bot_name)
|
||
super().__init__(
|
||
config=config,
|
||
logger=logger,
|
||
bot=bot,
|
||
bot_account_id=bot_account_id,
|
||
bot_name=bot_name,
|
||
event_converter=event_converter,
|
||
listeners={},
|
||
_stream_to_monitoring_msg={},
|
||
)
|
||
|
||
async def reply_message(
|
||
self,
|
||
message_source: platform_events.MessageEvent,
|
||
message: platform_message.MessageChain,
|
||
quote_origin: bool = False,
|
||
):
|
||
content = await self.message_converter.yiri2target(message)
|
||
_ws_mode = not self.config.get('enable-webhook', False)
|
||
|
||
if _ws_mode:
|
||
event = message_source.source_platform_object
|
||
req_id = event.get('req_id', '')
|
||
if req_id:
|
||
await self.bot.reply_text(req_id, content)
|
||
else:
|
||
await self.bot.set_message(event.message_id, content)
|
||
else:
|
||
await self.bot.set_message(message_source.source_platform_object.message_id, content)
|
||
|
||
async def reply_message_chunk(
|
||
self,
|
||
message_source: platform_events.MessageEvent,
|
||
bot_message,
|
||
message: platform_message.MessageChain,
|
||
quote_origin: bool = False,
|
||
is_final: bool = False,
|
||
):
|
||
content = await self.message_converter.yiri2target(message)
|
||
msg_id = message_source.source_platform_object.message_id
|
||
_ws_mode = not self.config.get('enable-webhook', False)
|
||
|
||
if _ws_mode:
|
||
success = await self.bot.push_stream_chunk(msg_id, content, is_final=is_final)
|
||
if not success and is_final:
|
||
event = message_source.source_platform_object
|
||
req_id = event.get('req_id', '')
|
||
if req_id:
|
||
await self.bot.reply_text(req_id, content)
|
||
return {'stream': success}
|
||
else:
|
||
success = await self.bot.push_stream_chunk(msg_id, content, is_final=is_final)
|
||
if not success and is_final:
|
||
await self.bot.set_message(msg_id, content)
|
||
return {'stream': success}
|
||
|
||
async def is_stream_output_supported(self) -> bool:
|
||
"""Whether streaming output is enabled for this bot instance."""
|
||
return self.config.get('enable-stream-reply', True)
|
||
|
||
async def send_message(self, target_type, target_id, message):
|
||
_ws_mode = not self.config.get('enable-webhook', False)
|
||
if _ws_mode:
|
||
content = await self.message_converter.yiri2target(message)
|
||
await self.bot.send_message(target_id, content)
|
||
else:
|
||
pass
|
||
|
||
async def on_message(self, event: WecomBotEvent):
|
||
try:
|
||
lb_event = await self.event_converter.target2yiri(event)
|
||
if lb_event:
|
||
await self.listeners[type(lb_event)](lb_event, self)
|
||
except Exception:
|
||
await self.logger.error(f'Error in wecombot callback: {traceback.format_exc()}')
|
||
print(traceback.format_exc())
|
||
|
||
def register_listener(
|
||
self,
|
||
event_type: typing.Type[platform_events.Event],
|
||
callback: typing.Callable[
|
||
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
|
||
],
|
||
):
|
||
self.listeners[event_type] = callback
|
||
|
||
try:
|
||
if event_type == platform_events.FriendMessage:
|
||
self.bot.on_message('single')(self.on_message)
|
||
elif event_type == platform_events.GroupMessage:
|
||
self.bot.on_message('group')(self.on_message)
|
||
elif event_type == platform_events.FeedbackEvent:
|
||
if hasattr(self.bot, 'on_feedback'):
|
||
self.bot.on_feedback()(self._on_feedback)
|
||
except Exception:
|
||
print(traceback.format_exc())
|
||
|
||
def set_bot_uuid(self, bot_uuid: str):
|
||
"""设置 bot UUID(用于生成 webhook URL)"""
|
||
self.bot_uuid = bot_uuid
|
||
|
||
async def on_monitoring_message_created(self, query, monitoring_message_id: str):
|
||
"""Called by pipeline after monitoring message is created, to map stream_id to monitoring message ID."""
|
||
try:
|
||
stream_id = query.message_event.source_platform_object.stream_id
|
||
if stream_id:
|
||
self._stream_to_monitoring_msg[stream_id] = (monitoring_message_id, time.time())
|
||
self._cleanup_stream_mapping()
|
||
except Exception as e:
|
||
await self.logger.debug(f'Failed to map stream_id to monitoring message: {e}')
|
||
|
||
def _cleanup_stream_mapping(self):
|
||
"""Remove entries older than TTL from the stream_id to monitoring message mapping."""
|
||
now = time.time()
|
||
expired = [k for k, (_, ts) in self._stream_to_monitoring_msg.items() if now - ts > self._STREAM_MAPPING_TTL]
|
||
for k in expired:
|
||
del self._stream_to_monitoring_msg[k]
|
||
|
||
async def _on_feedback(self, **kwargs):
|
||
"""Handle feedback event from WeChat Work AI Bot SDK and dispatch as FeedbackEvent."""
|
||
try:
|
||
feedback_id = kwargs.get('feedback_id', '')
|
||
feedback_type = kwargs.get('feedback_type', 0)
|
||
feedback_content = kwargs.get('feedback_content', '') or None
|
||
inaccurate_reasons = kwargs.get('inaccurate_reasons', []) or None
|
||
# WeChat Work returns integer reason codes, but FeedbackEvent expects strings
|
||
if inaccurate_reasons:
|
||
inaccurate_reasons = [str(r) for r in inaccurate_reasons]
|
||
session = kwargs.get('session')
|
||
|
||
session_id = None
|
||
user_id = None
|
||
message_id = None
|
||
stream_id = None
|
||
if session:
|
||
if session.chat_id:
|
||
session_id = f'group_{session.chat_id}'
|
||
elif session.user_id:
|
||
session_id = f'person_{session.user_id}'
|
||
user_id = session.user_id
|
||
message_id = session.msg_id
|
||
stream_id = session.stream_id
|
||
|
||
# Resolve stream_id to LangBot monitoring message ID if available
|
||
monitoring_msg_id = None
|
||
if stream_id and stream_id in self._stream_to_monitoring_msg:
|
||
monitoring_msg_id = self._stream_to_monitoring_msg[stream_id][0]
|
||
|
||
await self.logger.info(
|
||
f'Feedback event: feedback_id={feedback_id}, type={feedback_type}, '
|
||
f'session_id={session_id}, user_id={user_id}, message_id={message_id}'
|
||
)
|
||
|
||
event = platform_events.FeedbackEvent(
|
||
feedback_id=feedback_id,
|
||
feedback_type=feedback_type,
|
||
feedback_content=feedback_content,
|
||
inaccurate_reasons=inaccurate_reasons,
|
||
user_id=user_id,
|
||
session_id=session_id,
|
||
message_id=message_id,
|
||
stream_id=monitoring_msg_id or stream_id,
|
||
source_platform_object=session,
|
||
)
|
||
|
||
if platform_events.FeedbackEvent in self.listeners:
|
||
await self.listeners[platform_events.FeedbackEvent](event, self)
|
||
except Exception:
|
||
await self.logger.error(f'Error in wecombot feedback callback: {traceback.format_exc()}')
|
||
|
||
async def handle_unified_webhook(self, bot_uuid: str, path: str, request):
|
||
_ws_mode = not self.config.get('enable-webhook', False)
|
||
if _ws_mode:
|
||
return None
|
||
return await self.bot.handle_unified_webhook(request)
|
||
|
||
async def run_async(self):
|
||
_ws_mode = not self.config.get('enable-webhook', False)
|
||
if _ws_mode:
|
||
await self.bot.connect()
|
||
else:
|
||
|
||
async def keep_alive():
|
||
while True:
|
||
await asyncio.sleep(1)
|
||
|
||
await keep_alive()
|
||
|
||
async def kill(self) -> bool:
|
||
_ws_mode = not self.config.get('enable-webhook', False)
|
||
if _ws_mode:
|
||
await self.bot.disconnect()
|
||
return True
|
||
return False
|
||
|
||
async def unregister_listener(
|
||
self,
|
||
event_type: type,
|
||
callback: typing.Callable[
|
||
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
|
||
],
|
||
):
|
||
return super().unregister_listener(event_type, callback)
|
||
|
||
async def is_muted(self, group_id: int) -> bool:
|
||
pass
|