Merge pull request #1011 from Teahouse-Studios/dev/NextsTep
Dev/NextsTep
This commit is contained in:
commit
8c662631e8
14 changed files with 832 additions and 13 deletions
3
bot.py
3
bot.py
|
@ -15,7 +15,8 @@ from database import BotDBUtil, session, DBVersion
|
|||
encode = 'UTF-8'
|
||||
|
||||
bots_required_configs = {'aiocqhttp': ['qq_host', 'qq_account'], 'discord': ['dc_token'], 'aiogram': ['tg_token'],
|
||||
'kook': ['kook_token'], 'matrix': ['matrix_homeserver', 'matrix_user', 'matrix_token'], }
|
||||
'kook': ['kook_token'], 'matrix': ['matrix_homeserver', 'matrix_user', 'matrix_token'],
|
||||
'lagrange': ['lagrange_host']}
|
||||
|
||||
|
||||
class RestartBot(Exception):
|
||||
|
|
|
@ -6,12 +6,14 @@ import sys
|
|||
|
||||
import ujson as json
|
||||
from aiocqhttp import Event
|
||||
from datetime import datetime
|
||||
|
||||
from bots.aiocqhttp.client import bot
|
||||
from bots.aiocqhttp.info import client_name
|
||||
from bots.aiocqhttp.message import MessageSession, FetchTarget
|
||||
from config import Config
|
||||
from core.builtins import EnableDirtyWordCheck, PrivateAssets, Url
|
||||
from core.builtins import EnableDirtyWordCheck, PrivateAssets, Url, Temp
|
||||
from core.scheduler import Scheduler, IntervalTrigger
|
||||
from core.parser.message import parser
|
||||
from core.types import MsgInfo, Session
|
||||
from core.utils.bot import load_prompt, init_async
|
||||
|
@ -25,6 +27,12 @@ qq_account = str(Config("qq_account"))
|
|||
enable_listening_self_message = Config("qq_enable_listening_self_message")
|
||||
|
||||
|
||||
@Scheduler.scheduled_job(IntervalTrigger(seconds=20))
|
||||
async def check_lagrange_alive():
|
||||
if 'lagrange_keepalive' in Temp.data:
|
||||
Temp.data['lagrange_status'] = (datetime.now().timestamp() - Temp.data['lagrange_keepalive']) < 20
|
||||
|
||||
|
||||
@bot.on_startup
|
||||
async def startup():
|
||||
await init_async()
|
||||
|
|
|
@ -20,6 +20,7 @@ from core.builtins.message import MessageSession as MessageSessionT
|
|||
from core.builtins.message.chain import MessageChain
|
||||
from core.exceptions import SendMessageFailed
|
||||
from core.logger import Logger
|
||||
from core.queue import JobQueue
|
||||
from core.types import FetchTarget as FetchTargetT, FinishedSession as FinS
|
||||
from core.utils.image import msgchain2image
|
||||
from core.utils.storedata import get_stored_list
|
||||
|
@ -45,6 +46,7 @@ class FinishedSession(FinS):
|
|||
last_send_typing_time = {}
|
||||
Temp.data['is_group_message_blocked'] = False
|
||||
Temp.data['waiting_for_send_group_message'] = []
|
||||
group_info = {}
|
||||
|
||||
|
||||
async def resending_group_message():
|
||||
|
@ -88,10 +90,28 @@ class MessageSession(MessageSessionT):
|
|||
|
||||
async def send_message(self, message_chain, quote=True, disable_secret_check=False,
|
||||
allow_split_image=True) -> FinishedSession:
|
||||
|
||||
message_chain = MessageChain(message_chain)
|
||||
if self.target.target_from == 'QQ|Group' and Temp.data.get('lagrange_status', False):
|
||||
lagrange_available_groups = Temp.data.get('lagrange_available_groups', [])
|
||||
if self.session.target in lagrange_available_groups:
|
||||
choose = random.randint(0, 1)
|
||||
Logger.info(f'choose: {choose}')
|
||||
if choose:
|
||||
can_sends = []
|
||||
for x in message_chain.value:
|
||||
if isinstance(x, (Plain, Image)):
|
||||
can_sends.append(x)
|
||||
message_chain.value.remove(x)
|
||||
if can_sends:
|
||||
await JobQueue.send_message('Lagrange', self.target.target_id,
|
||||
MessageChain(can_sends).to_list())
|
||||
if not message_chain.value:
|
||||
return
|
||||
msg = MessageSegment.text('')
|
||||
if quote and self.target.target_from == 'QQ|Group' and self.session.message:
|
||||
msg = MessageSegment.reply(self.session.message.message_id)
|
||||
message_chain = MessageChain(message_chain)
|
||||
|
||||
if not message_chain.is_safe and not disable_secret_check:
|
||||
return await self.send_message(Plain(ErrorMessage(self.locale.t("error.message.chain.unsafe"))))
|
||||
self.sent.append(message_chain)
|
||||
|
@ -113,12 +133,21 @@ class MessageSession(MessageSessionT):
|
|||
send = await bot.send_group_msg(group_id=self.session.target, message=MessageSegment.text(
|
||||
self.locale.t("error.message.timeout")))
|
||||
except aiocqhttp.exceptions.ActionFailed:
|
||||
message_chain.insert(0, Plain(self.locale.t("error.message.limited.msg2img")))
|
||||
msg2img = MessageSegment.image(Path(await msgchain2image(message_chain)).as_uri())
|
||||
img_chain = message_chain.copy()
|
||||
img_chain.insert(0, Plain(self.locale.t("error.message.limited.msg2img")))
|
||||
msg2img = MessageSegment.image(Path(await msgchain2image(img_chain)).as_uri())
|
||||
try:
|
||||
send = await bot.send_group_msg(group_id=self.session.target, message=msg2img)
|
||||
except aiocqhttp.exceptions.ActionFailed as e:
|
||||
raise SendMessageFailed(e.result['wording'])
|
||||
if self.target.target_from == 'QQ|Group' and Temp.data.get('lagrange_status', False):
|
||||
lagrange_available_groups = Temp.data.get('lagrange_available_groups', [])
|
||||
if self.session.target in lagrange_available_groups:
|
||||
await JobQueue.send_message('Lagrange', self.target.target_id,
|
||||
message_chain.to_list())
|
||||
else:
|
||||
raise SendMessageFailed(e.result['wording'])
|
||||
else:
|
||||
raise SendMessageFailed(e.result['wording'])
|
||||
|
||||
if Temp.data['is_group_message_blocked']:
|
||||
asyncio.create_task(resending_group_message())
|
||||
|
|
0
bots/lagrange/__init__.py
Normal file
0
bots/lagrange/__init__.py
Normal file
191
bots/lagrange/bot.py
Normal file
191
bots/lagrange/bot.py
Normal file
|
@ -0,0 +1,191 @@
|
|||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from aiocqhttp import Event
|
||||
|
||||
from bots.lagrange.client import bot
|
||||
from bots.lagrange.info import client_name
|
||||
from bots.lagrange.message import MessageSession, FetchTarget
|
||||
from bots.lagrange.utils import get_plain_msg
|
||||
from config import Config
|
||||
from core.builtins import EnableDirtyWordCheck, PrivateAssets, Url
|
||||
from core.logger import Logger
|
||||
from core.parser.message import parser
|
||||
from core.queue import check_job_queue
|
||||
from core.scheduler import Scheduler, IntervalTrigger
|
||||
from core.types import MsgInfo, Session
|
||||
from core.utils.bot import load_prompt, init_async
|
||||
from core.utils.info import Info
|
||||
from core.queue import JobQueue
|
||||
|
||||
PrivateAssets.set(os.path.abspath(os.path.dirname(__file__) + '/assets'))
|
||||
EnableDirtyWordCheck.status = True if Config('enable_dirty_check') else False
|
||||
Url.disable_mm = False if Config('enable_urlmanager') else True
|
||||
qq_account = str(Config("qq_account"))
|
||||
|
||||
|
||||
@Scheduler.scheduled_job(IntervalTrigger(seconds=1))
|
||||
async def job():
|
||||
await check_job_queue()
|
||||
|
||||
|
||||
@bot.on_startup
|
||||
async def startup():
|
||||
# await init_async()
|
||||
Scheduler.start()
|
||||
bot.logger.setLevel(logging.WARNING)
|
||||
|
||||
|
||||
@bot.on_meta_event('heartbeat')
|
||||
async def _(event: Event):
|
||||
if event['status']['online']:
|
||||
await JobQueue.add_job('QQ', 'lagrange_keepalive', event['status'], wait=False)
|
||||
glist = await bot.call_action('get_group_list')
|
||||
g = []
|
||||
for i in glist:
|
||||
g.append(i['group_id'])
|
||||
await JobQueue.add_job('QQ', 'lagrange_available_groups', g, wait=False)
|
||||
|
||||
|
||||
async def message_handler(event: Event):
|
||||
if event.detail_type == 'private':
|
||||
if event.sub_type == 'group':
|
||||
if Config('qq_disable_temp_session'):
|
||||
return await bot.send(event, '请先添加好友后再进行命令交互。')
|
||||
|
||||
message = get_plain_msg(event.message)
|
||||
"""
|
||||
filter_msg = re.match(r'.*?\\[CQ:(?:json|xml).*?\\].*?|.*?<\\?xml.*?>.*?', event.message, re.MULTILINE | re.DOTALL)
|
||||
if filter_msg:
|
||||
match_json = re.match(r'.*?\\[CQ:json,data=(.*?)\\].*?', event.message, re.MULTILINE | re.DOTALL)
|
||||
if match_json:
|
||||
load_json = json.loads(html.unescape(match_json.group(1)))
|
||||
if load_json['app'] == 'com.tencent.multimsg':
|
||||
event.message = f'[CQ:forward,id={load_json["meta"]["detail"]["resid"]}]'
|
||||
else:
|
||||
return
|
||||
else:
|
||||
return
|
||||
reply_id = None
|
||||
match_reply = re.match(r'^\\[CQ:reply,id=(.*?)].*', event.message)
|
||||
if match_reply:
|
||||
reply_id = int(match_reply.group(1))
|
||||
|
||||
prefix = None
|
||||
if match_at := re.match(r'^\\[CQ:at,qq=(.*?)](.*)', event.message):
|
||||
if match_at.group(1) == qq_account:
|
||||
event.message = match_at.group(2)
|
||||
if event.message in ['', ' ']:
|
||||
event.message = 'help'
|
||||
prefix = ['']"""
|
||||
if event.detail_type == 'group':
|
||||
if event.group_id not in Config('lagrange_avaliable_groups'):
|
||||
return
|
||||
|
||||
target_id = 'QQ|' + (f'Group|{str(event.group_id)}' if event.detail_type == 'group' else str(event.user_id))
|
||||
|
||||
msg = MessageSession(MsgInfo(target_id=target_id,
|
||||
sender_id=f'QQ|{str(event.user_id)}',
|
||||
target_from='QQ|Group' if event.detail_type == 'group' else 'QQ',
|
||||
sender_from='QQ', sender_name=event.sender['nickname'], client_name=client_name,
|
||||
message_id=event.message_id,
|
||||
reply_id=None),
|
||||
Session(message=message,
|
||||
target=event.group_id if event.detail_type == 'group' else event.user_id,
|
||||
sender=event.user_id))
|
||||
await parser(msg, running_mention=True, prefix=['N~'])
|
||||
|
||||
"""@bot.on_message('group', 'private')
|
||||
async def _(event: Event):
|
||||
await message_handler(event)"""
|
||||
|
||||
"""
|
||||
if enable_listening_self_message:
|
||||
@bot.on('message_sent')
|
||||
async def _(event: Event):
|
||||
await message_handler(event)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class GuildAccountInfo:
|
||||
tiny_id = None
|
||||
|
||||
|
||||
@bot.on_message('guild')
|
||||
async def _(event):
|
||||
if GuildAccountInfo.tiny_id is None:
|
||||
profile = await bot.call_action('get_guild_service_profile')
|
||||
GuildAccountInfo.tiny_id = profile['tiny_id']
|
||||
tiny_id = event.user_id
|
||||
if tiny_id == GuildAccountInfo.tiny_id:
|
||||
return
|
||||
reply_id = None
|
||||
match_reply = re.match(r'^\\[CQ:reply,id=(.*?)].*', event.message)
|
||||
if match_reply:
|
||||
reply_id = int(match_reply.group(1))
|
||||
target_id = f'QQ|Guild|{str(event.guild_id)}|{str(event.channel_id)}'
|
||||
msg = MessageSession(MsgInfo(target_id=target_id,
|
||||
sender_id=f'QQ|Tiny|{str(event.user_id)}',
|
||||
target_from='QQ|Guild',
|
||||
sender_from='QQ|Tiny', sender_name=event.sender['nickname'], client_name=client_name,
|
||||
message_id=event.message_id,
|
||||
reply_id=reply_id),
|
||||
Session(message=event,
|
||||
target=f'{str(event.guild_id)}|{str(event.channel_id)}',
|
||||
sender=event.user_id))
|
||||
await parser(msg, running_mention=True)
|
||||
|
||||
|
||||
@bot.on('request.friend')
|
||||
async def _(event: Event):
|
||||
if BotDBUtil.SenderInfo('QQ|' + str(event.user_id)).query.isInBlockList:
|
||||
return {'approve': False}
|
||||
return {'approve': True}
|
||||
|
||||
|
||||
@bot.on('request.group.invite')
|
||||
async def _(event: Event):
|
||||
if BotDBUtil.SenderInfo('QQ|' + str(event.user_id)).query.isSuperUser:
|
||||
return {'approve': True}
|
||||
if not Config('allow_bot_auto_approve_group_invite'):
|
||||
await bot.send_private_msg(user_id=event.user_id,
|
||||
message='你好!本机器人暂时不主动同意入群请求。\n'
|
||||
f'请至{Config("qq_join_group_application_link")}申请入群。')
|
||||
else:
|
||||
return {'approve': True}
|
||||
|
||||
|
||||
@bot.on_notice('group_ban')
|
||||
async def _(event: Event):
|
||||
if event.user_id == int(qq_account):
|
||||
result = BotDBUtil.UnfriendlyActions(target_id=event.group_id,
|
||||
sender_id=event.operator_id).add_and_check('mute', str(event.duration))
|
||||
if event.duration >= 259200:
|
||||
result = True
|
||||
if result:
|
||||
await bot.call_action('set_group_leave', group_id=event.group_id)
|
||||
BotDBUtil.SenderInfo('QQ|' + str(event.operator_id)).edit('isInBlockList', True)
|
||||
await bot.call_action('delete_friend', friend_id=event.operator_id)
|
||||
|
||||
|
||||
|
||||
@bot.on_message('group')
|
||||
async def _(event: Event):
|
||||
result = BotDBUtil.isGroupInAllowList(f'QQ|Group|{str(event.group_id)}')
|
||||
if not result:
|
||||
await bot.send(event=event, message='此群不在白名单中,已自动退群。'
|
||||
'\n如需申请白名单,请至https://github.com/Teahouse-Studios/bot/issues/new/choose发起issue。')
|
||||
await bot.call_action('set_group_leave', group_id=event.group_id)
|
||||
"""
|
||||
|
||||
qq_host = Config("lagrange_host")
|
||||
if qq_host:
|
||||
argv = sys.argv
|
||||
if 'subprocess' in sys.argv:
|
||||
Info.subprocess = True
|
||||
host, port = qq_host.split(':')
|
||||
bot.run(host=host, port=port, debug=False)
|
52
bots/lagrange/client.py
Normal file
52
bots/lagrange/client.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
import traceback
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from aiocqhttp import CQHttp
|
||||
from aiocqhttp.event import Event
|
||||
|
||||
|
||||
class EventModded(Event):
|
||||
@staticmethod
|
||||
def from_payload(payload: Dict[str, Any]) -> 'Optional[Event]':
|
||||
"""
|
||||
从 OneBot 事件数据构造 `Event` 对象。
|
||||
"""
|
||||
try:
|
||||
e = EventModded(payload)
|
||||
_ = e.type, e.detail_type
|
||||
return e
|
||||
except KeyError:
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
@property
|
||||
def detail_type(self) -> str:
|
||||
"""
|
||||
事件具体类型,依 `type` 的不同而不同,以 ``message`` 类型为例,有
|
||||
``private``、``group``、``discuss`` 等。
|
||||
"""
|
||||
if self.type == 'message_sent':
|
||||
return self['message_type']
|
||||
return self[f'{self.type}_type']
|
||||
|
||||
|
||||
class CQHttpModded(CQHttp):
|
||||
|
||||
async def _handle_event(self, payload: Dict[str, Any]) -> Any:
|
||||
ev = EventModded.from_payload(payload)
|
||||
if not ev:
|
||||
return
|
||||
|
||||
event_name = ev.name
|
||||
self.logger.info(f'received event: {event_name}')
|
||||
|
||||
if self._message_class and 'message' in ev:
|
||||
ev['message'] = self._message_class(ev['message'])
|
||||
results = list(
|
||||
filter(lambda r: r is not None, await
|
||||
self._bus.emit(event_name, ev)))
|
||||
# return the first non-none result
|
||||
return results[0] if results else None
|
||||
|
||||
|
||||
bot = CQHttpModded()
|
1
bots/lagrange/info.py
Normal file
1
bots/lagrange/info.py
Normal file
|
@ -0,0 +1 @@
|
|||
client_name = 'Lagrange'
|
422
bots/lagrange/message.py
Normal file
422
bots/lagrange/message.py
Normal file
|
@ -0,0 +1,422 @@
|
|||
import asyncio
|
||||
import datetime
|
||||
import html
|
||||
import random
|
||||
import re
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from typing import List, Union
|
||||
|
||||
import aiocqhttp.exceptions
|
||||
import ujson as json
|
||||
from aiocqhttp import MessageSegment
|
||||
|
||||
from bots.lagrange.client import bot
|
||||
from bots.lagrange.info import client_name
|
||||
from config import Config
|
||||
from core.builtins import Bot, ErrorMessage, base_superuser_list
|
||||
from core.builtins import Plain, Image, Voice, Temp, command_prefix
|
||||
from core.builtins.message import MessageSession as MessageSessionT
|
||||
from core.builtins.message.chain import MessageChain
|
||||
from core.exceptions import SendMessageFailed
|
||||
from core.logger import Logger
|
||||
from core.types import FetchTarget as FetchTargetT, FinishedSession as FinS
|
||||
from core.utils.image import msgchain2image
|
||||
from core.utils.storedata import get_stored_list
|
||||
from database import BotDBUtil
|
||||
|
||||
enable_analytics = Config('enable_analytics')
|
||||
|
||||
|
||||
class FinishedSession(FinS):
|
||||
async def delete(self):
|
||||
"""
|
||||
用于删除这条消息。
|
||||
"""
|
||||
if self.session.target.target_from in ['QQ|Group', 'QQ']:
|
||||
try:
|
||||
for x in self.message_id:
|
||||
if x != 0:
|
||||
await bot.call_action('delete_msg', message_id=x)
|
||||
except Exception:
|
||||
Logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
last_send_typing_time = {}
|
||||
Temp.data['is_group_message_blocked'] = False
|
||||
Temp.data['waiting_for_send_group_message'] = []
|
||||
|
||||
|
||||
async def resending_group_message():
|
||||
falied_list = []
|
||||
try:
|
||||
if targets := Temp.data['waiting_for_send_group_message']:
|
||||
for x in targets:
|
||||
try:
|
||||
if x['i18n']:
|
||||
await x['fetch'].send_direct_message(x['fetch'].parent.locale.t(x['message'], **x['kwargs']))
|
||||
else:
|
||||
await x['fetch'].send_direct_message(x['message'])
|
||||
Temp.data['waiting_for_send_group_message'].remove(x)
|
||||
await asyncio.sleep(30)
|
||||
except SendMessageFailed:
|
||||
Logger.error(traceback.format_exc())
|
||||
falied_list.append(x)
|
||||
if len(falied_list) > 3:
|
||||
raise SendMessageFailed
|
||||
Temp.data['is_group_message_blocked'] = False
|
||||
except SendMessageFailed:
|
||||
Logger.error(traceback.format_exc())
|
||||
Temp.data['is_group_message_blocked'] = True
|
||||
for bu in base_superuser_list:
|
||||
fetch_base_superuser = await FetchTarget.fetch_target(bu)
|
||||
if fetch_base_superuser:
|
||||
await fetch_base_superuser. \
|
||||
send_direct_message(fetch_base_superuser.parent.locale.t("error.message.paused",
|
||||
prefix=command_prefix[0]))
|
||||
|
||||
|
||||
class MessageSession(MessageSessionT):
|
||||
class Feature:
|
||||
image = True
|
||||
voice = False
|
||||
embed = False
|
||||
forward = False
|
||||
delete = True
|
||||
wait = True
|
||||
quote = False
|
||||
|
||||
async def send_message(self, message_chain, quote=True, disable_secret_check=False,
|
||||
allow_split_image=True) -> FinishedSession:
|
||||
msg = []
|
||||
"""
|
||||
if quote and self.target.target_from == 'QQ|Group' and self.session.message:
|
||||
msg = MessageSegment.reply(self.session.message.message_id)
|
||||
"""
|
||||
message_chain = MessageChain(message_chain)
|
||||
if not message_chain.is_safe and not disable_secret_check:
|
||||
return await self.send_message(Plain(ErrorMessage(self.locale.t("error.message.chain.unsafe"))))
|
||||
self.sent.append(message_chain)
|
||||
count = 0
|
||||
for x in message_chain.as_sendable(locale=self.locale.locale, embed=False):
|
||||
if isinstance(x, Plain):
|
||||
msg.append({
|
||||
"type": "text",
|
||||
"data": {
|
||||
"text": x.text
|
||||
}
|
||||
})
|
||||
elif isinstance(x, Image):
|
||||
msg.append({
|
||||
"type": "image",
|
||||
"data": {
|
||||
"file": "base64://" + await x.get_base64()
|
||||
}
|
||||
})
|
||||
count += 1
|
||||
Logger.info(f'[Bot] -> [{self.target.target_id}]: {msg}')
|
||||
if self.target.target_from == 'QQ|Group':
|
||||
try:
|
||||
send = await bot.send_group_msg(group_id=int(self.session.target), message=msg)
|
||||
except aiocqhttp.exceptions.NetworkError:
|
||||
send = await bot.send_group_msg(group_id=int(self.session.target), message=MessageSegment.text(
|
||||
self.locale.t("error.message.timeout")))
|
||||
except aiocqhttp.exceptions.ActionFailed:
|
||||
message_chain.insert(0, Plain(self.locale.t("error.message.limited.msg2img")))
|
||||
msg2img = MessageSegment.image(Path(await msgchain2image(message_chain)).as_uri())
|
||||
try:
|
||||
send = await bot.send_group_msg(group_id=int(self.session.target), message=msg2img)
|
||||
except aiocqhttp.exceptions.ActionFailed as e:
|
||||
raise SendMessageFailed(e.result['wording'])
|
||||
|
||||
if Temp.data['is_group_message_blocked']:
|
||||
asyncio.create_task(resending_group_message())
|
||||
|
||||
elif self.target.target_from == 'QQ|Guild':
|
||||
match_guild = re.match(r'(.*)\|(.*)', self.session.target)
|
||||
send = await bot.call_action('send_guild_channel_msg', guild_id=int(match_guild.group(1)),
|
||||
channel_id=int(match_guild.group(2)), message=msg)
|
||||
else:
|
||||
try:
|
||||
send = await bot.send_private_msg(user_id=self.session.target, message=msg)
|
||||
except aiocqhttp.exceptions.ActionFailed as e:
|
||||
if self.session.message.detail_type == 'private' and self.session.message.sub_type == 'group':
|
||||
return FinishedSession(self, 0, [{}])
|
||||
else:
|
||||
raise e
|
||||
return FinishedSession(self, send['message_id'], [send])
|
||||
|
||||
async def check_native_permission(self):
|
||||
if self.target.target_from == 'QQ':
|
||||
return True
|
||||
elif self.target.target_from == 'QQ|Group':
|
||||
get_member_info = await bot.call_action('get_group_member_info', group_id=self.session.target,
|
||||
user_id=self.session.sender)
|
||||
if get_member_info['role'] in ['owner', 'admin']:
|
||||
return True
|
||||
elif self.target.target_from == 'QQ|Guild':
|
||||
match_guild = re.match(r'(.*)\|(.*)', self.session.target)
|
||||
get_member_info = await bot.call_action('get_guild_member_profile', guild_id=match_guild.group(1),
|
||||
user_id=self.session.sender)
|
||||
for m in get_member_info['roles']:
|
||||
if m['role_id'] == "2":
|
||||
return True
|
||||
get_guild_info = await bot.call_action('get_guild_meta_by_guest', guild_id=match_guild.group(1))
|
||||
if get_guild_info['owner_id'] == self.session.sender:
|
||||
return True
|
||||
return False
|
||||
return False
|
||||
|
||||
def as_display(self, text_only=False):
|
||||
"""m = html.unescape(self.session.message.message)
|
||||
if text_only:
|
||||
return ''.join(
|
||||
re.split(r'\\[CQ:.*?]', m)).strip()
|
||||
m = re.sub(r'\\[CQ:at,qq=(.*?)]', r'QQ|\1', m)
|
||||
m = re.sub(r'\\[CQ:forward,id=(.*?)]', r'\\[Ke:forward,id=\1]', m)
|
||||
|
||||
return ''.join(
|
||||
re.split(r'\\[CQ:.*?]', m)).strip()"""
|
||||
return self.session.message
|
||||
|
||||
async def fake_forward_msg(self, nodelist):
|
||||
if self.target.target_from == 'QQ|Group':
|
||||
get_ = get_stored_list(Bot.FetchTarget, 'forward_msg')
|
||||
if not get_['status']:
|
||||
await self.send_message(self.locale.t("core.message.forward_msg.disabled"))
|
||||
raise
|
||||
await bot.call_action('send_group_forward_msg', group_id=int(self.session.target), messages=nodelist)
|
||||
|
||||
async def delete(self):
|
||||
if self.target.target_from in ['QQ', 'QQ|Group']:
|
||||
try:
|
||||
if isinstance(self.session.message, list):
|
||||
for x in self.session.message:
|
||||
await bot.call_action('delete_msg', message_id=x['message_id'])
|
||||
else:
|
||||
await bot.call_action('delete_msg', message_id=self.session.message['message_id'])
|
||||
except Exception:
|
||||
Logger.error(traceback.format_exc())
|
||||
|
||||
async def get_text_channel_list(self):
|
||||
match_guild = re.match(r'(.*)\|(.*)', self.session.target)
|
||||
get_channels_info = await bot.call_action('get_guild_channel_list', guild_id=match_guild.group(1),
|
||||
no_cache=True)
|
||||
lst = []
|
||||
for m in get_channels_info:
|
||||
if m['channel_type'] == 1:
|
||||
lst.append(f'{m["owner_guild_id"]}|{m["channel_id"]}')
|
||||
return lst
|
||||
|
||||
async def to_message_chain(self):
|
||||
m = html.unescape(self.session.message.message)
|
||||
m = re.sub(r'\[CQ:at,qq=(.*?)]', r'QQ|\1', m)
|
||||
m = re.sub(r'\[CQ:forward,id=(.*?)]', r'\[Ke:forward,id=\1]', m)
|
||||
spl = re.split(r'(\[CQ:.*?])', m)
|
||||
lst = []
|
||||
for s in spl:
|
||||
if s == '':
|
||||
continue
|
||||
if s.startswith('[CQ:'):
|
||||
if s.startswith('[CQ:image'):
|
||||
sspl = s.split(',')
|
||||
for ss in sspl:
|
||||
if ss.startswith('url='):
|
||||
lst.append(Image(ss[4:-1]))
|
||||
else:
|
||||
lst.append(Plain(s))
|
||||
|
||||
return MessageChain(lst)
|
||||
|
||||
async def call_api(self, action, **params):
|
||||
return await bot.call_action(action, **params)
|
||||
|
||||
sendMessage = send_message
|
||||
asDisplay = as_display
|
||||
toMessageChain = to_message_chain
|
||||
checkNativePermission = check_native_permission
|
||||
|
||||
class Typing:
|
||||
def __init__(self, msg: MessageSessionT):
|
||||
self.msg = msg
|
||||
|
||||
async def __aenter__(self):
|
||||
"""if self.msg.target.target_from == 'QQ|Group':
|
||||
if self.msg.session.sender in last_send_typing_time:
|
||||
if datetime.datetime.now().timestamp() - last_send_typing_time[self.msg.session.sender] <= 3600:
|
||||
return
|
||||
last_send_typing_time[self.msg.session.sender] = datetime.datetime.now().timestamp()
|
||||
await bot.send_group_msg(group_id=self.msg.session.target,
|
||||
message=f'[CQ:poke,qq={self.msg.session.sender}]')"""
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
pass
|
||||
|
||||
|
||||
class FetchTarget(FetchTargetT):
|
||||
name = client_name
|
||||
|
||||
@staticmethod
|
||||
async def fetch_target(target_id, sender_id=None) -> Union[Bot.FetchedSession]:
|
||||
match_target = re.match(r'^(QQ\|Group|QQ\|Guild|QQ)\|(.*)', target_id)
|
||||
if match_target:
|
||||
target_from = sender_from = match_target.group(1)
|
||||
target_id = match_target.group(2)
|
||||
if sender_id:
|
||||
match_sender = re.match(r'^(QQ\|Tiny|QQ)\|(.*)', sender_id)
|
||||
if match_sender:
|
||||
sender_from = match_sender.group(1)
|
||||
sender_id = match_sender.group(2)
|
||||
else:
|
||||
sender_id = target_id
|
||||
|
||||
return Bot.FetchedSession(target_from, target_id, sender_from, sender_id)
|
||||
|
||||
@staticmethod
|
||||
async def fetch_target_list(target_list: list) -> List[Bot.FetchedSession]:
|
||||
lst = []
|
||||
group_list_raw = await bot.call_action('get_group_list')
|
||||
group_list = []
|
||||
for g in group_list_raw:
|
||||
group_list.append(g['group_id'])
|
||||
friend_list_raw = await bot.call_action('get_friend_list')
|
||||
friend_list = []
|
||||
guild_list_raw = await bot.call_action('get_guild_list')
|
||||
guild_list = []
|
||||
for g in guild_list_raw:
|
||||
get_channel_list = await bot.call_action('get_guild_channel_list', guild_id=g['guild_id'])
|
||||
for channel in get_channel_list:
|
||||
if channel['channel_type'] == 1:
|
||||
guild_list.append(f"{str(g['guild_id'])}|{str(channel['channel_id'])}")
|
||||
for f in friend_list_raw:
|
||||
friend_list.append(f)
|
||||
for x in target_list:
|
||||
fet = await FetchTarget.fetch_target(x)
|
||||
if fet:
|
||||
if fet.target.target_from == 'QQ|Group':
|
||||
if fet.session.target not in group_list:
|
||||
continue
|
||||
if fet.target.target_from == 'QQ':
|
||||
if fet.session.target not in friend_list:
|
||||
continue
|
||||
if fet.target.target_from == 'QQ|Guild':
|
||||
if fet.session.target not in guild_list:
|
||||
continue
|
||||
lst.append(fet)
|
||||
return lst
|
||||
|
||||
@staticmethod
|
||||
async def post_message(module_name, message, user_list: List[Bot.FetchedSession] = None, i18n=False, **kwargs):
|
||||
_tsk = []
|
||||
blocked = False
|
||||
|
||||
async def post_(fetch_: Bot.FetchedSession):
|
||||
nonlocal _tsk
|
||||
nonlocal blocked
|
||||
try:
|
||||
if Temp.data['is_group_message_blocked'] and fetch_.target.target_from == 'QQ|Group':
|
||||
Temp.data['waiting_for_send_group_message'].append({'fetch': fetch_, 'message': message,
|
||||
'i18n': i18n, 'kwargs': kwargs})
|
||||
else:
|
||||
if i18n:
|
||||
await fetch_.send_direct_message(fetch_.parent.locale.t(message, **kwargs))
|
||||
|
||||
else:
|
||||
await fetch_.send_direct_message(message)
|
||||
if _tsk:
|
||||
_tsk = []
|
||||
if enable_analytics:
|
||||
BotDBUtil.Analytics(fetch_).add('', module_name, 'schedule')
|
||||
await asyncio.sleep(0.5)
|
||||
except SendMessageFailed as e:
|
||||
if e.args[0] == 'send group message failed: blocked by server':
|
||||
if len(_tsk) >= 3:
|
||||
blocked = True
|
||||
if not blocked:
|
||||
_tsk.append({'fetch': fetch_, 'message': message, 'i18n': i18n, 'kwargs': kwargs})
|
||||
else:
|
||||
Temp.data['is_group_message_blocked'] = True
|
||||
Temp.data['waiting_for_send_group_message'].append({'fetch': fetch_, 'message': message,
|
||||
'i18n': i18n, 'kwargs': kwargs})
|
||||
if _tsk:
|
||||
for t in _tsk:
|
||||
Temp.data['waiting_for_send_group_message'].append(t)
|
||||
_tsk = []
|
||||
for bu in base_superuser_list:
|
||||
fetch_base_superuser = await FetchTarget.fetch_target(bu)
|
||||
if fetch_base_superuser:
|
||||
await fetch_base_superuser. \
|
||||
send_direct_message(fetch_base_superuser.parent.locale.t("error.message.paused",
|
||||
prefix=command_prefix[0]))
|
||||
except Exception:
|
||||
Logger.error(traceback.format_exc())
|
||||
|
||||
if user_list is not None:
|
||||
for x in user_list:
|
||||
await post_(x)
|
||||
else:
|
||||
get_target_id = BotDBUtil.TargetInfo.get_enabled_this(module_name, "QQ")
|
||||
group_list_raw = await bot.call_action('get_group_list')
|
||||
group_list = [g['group_id'] for g in group_list_raw]
|
||||
friend_list_raw = await bot.call_action('get_friend_list')
|
||||
friend_list = [f['user_id'] for f in friend_list_raw]
|
||||
guild_list_raw = await bot.call_action('get_guild_list')
|
||||
guild_list = []
|
||||
for g in guild_list_raw:
|
||||
try:
|
||||
get_channel_list = await bot.call_action('get_guild_channel_list', guild_id=g['guild_id'],
|
||||
no_cache=True)
|
||||
for channel in get_channel_list:
|
||||
if channel['channel_type'] == 1:
|
||||
guild_list.append(f"{str(g['guild_id'])}|{str(channel['channel_id'])}")
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
in_whitelist = []
|
||||
else_ = []
|
||||
for x in get_target_id:
|
||||
fetch = await FetchTarget.fetch_target(x.targetId)
|
||||
Logger.debug(fetch)
|
||||
if fetch:
|
||||
if fetch.target.target_from == 'QQ|Group':
|
||||
if int(fetch.session.target) not in group_list:
|
||||
continue
|
||||
if fetch.target.target_from == 'QQ':
|
||||
if int(fetch.session.target) not in friend_list:
|
||||
continue
|
||||
if fetch.target.target_from == 'QQ|Guild':
|
||||
if fetch.session.target not in guild_list:
|
||||
continue
|
||||
|
||||
if fetch.target.target_from in ['QQ', 'QQ|Guild']:
|
||||
in_whitelist.append(post_(fetch))
|
||||
else:
|
||||
load_options: dict = json.loads(x.options)
|
||||
if load_options.get('in_post_whitelist', False):
|
||||
in_whitelist.append(post_(fetch))
|
||||
else:
|
||||
else_.append(post_(fetch))
|
||||
|
||||
async def post_in_whitelist(lst):
|
||||
for l in lst:
|
||||
await l
|
||||
await asyncio.sleep(random.randint(1, 5))
|
||||
|
||||
if in_whitelist:
|
||||
asyncio.create_task(post_in_whitelist(in_whitelist))
|
||||
|
||||
async def post_not_in_whitelist(lst):
|
||||
for f in lst:
|
||||
await f
|
||||
await asyncio.sleep(random.randint(15, 30))
|
||||
|
||||
if else_:
|
||||
asyncio.create_task(post_not_in_whitelist(else_))
|
||||
Logger.info(f"The task of posting messages to whitelisted groups is complete. "
|
||||
f"Posting message to {len(else_)} groups not in whitelist.")
|
||||
|
||||
|
||||
Bot.MessageSession = MessageSession
|
||||
Bot.FetchTarget = FetchTarget
|
||||
Bot.client_name = client_name
|
15
bots/lagrange/utils.py
Normal file
15
bots/lagrange/utils.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from bots.lagrange.client import bot
|
||||
from aiocqhttp.exceptions import ActionFailed
|
||||
from config import Config
|
||||
|
||||
|
||||
def get_plain_msg(array_msg: list) -> str:
|
||||
text = []
|
||||
for msg in array_msg:
|
||||
if msg['type'] == 'text':
|
||||
text.append(msg['data']['text'])
|
||||
return '\n'.join(text)
|
||||
|
||||
|
||||
async def get_group_list():
|
||||
return await bot.call_action('get_group_list')
|
|
@ -11,6 +11,7 @@ from .message.internal import *
|
|||
from .tasks import *
|
||||
from .temp import *
|
||||
from .utils import *
|
||||
from ..logger import Logger
|
||||
|
||||
|
||||
class Bot:
|
||||
|
@ -26,11 +27,12 @@ class Bot:
|
|||
disable_secret_check=False,
|
||||
allow_split_image=True):
|
||||
if isinstance(target, str):
|
||||
target = Bot.FetchTarget.fetch_target(target)
|
||||
target = await Bot.FetchTarget.fetch_target(target)
|
||||
if not target:
|
||||
raise ValueError("Target not found")
|
||||
if isinstance(msg, list):
|
||||
msg = MessageChain(msg)
|
||||
Logger.info(target.__dict__)
|
||||
await target.send_direct_message(msg, disable_secret_check, allow_split_image)
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -14,7 +14,7 @@ from core.types.message import MessageChain as MessageChainT
|
|||
class MessageChain(MessageChainT):
|
||||
def __init__(self, elements: Union[str, List[Union[Plain, Image, Voice, Embed, Url]],
|
||||
Tuple[Union[Plain, Image, Voice, Embed, Url]],
|
||||
Plain, Image, Voice, Embed, Url]):
|
||||
Plain, Image, Voice, Embed, Url] = None):
|
||||
self.value = []
|
||||
if isinstance(elements, ErrorMessage):
|
||||
elements = str(elements)
|
||||
|
@ -38,6 +38,19 @@ class MessageChain(MessageChainT):
|
|||
self.value += match_kecode(e.text)
|
||||
else:
|
||||
self.value.append(e)
|
||||
elif isinstance(e, dict):
|
||||
if e['type'] in ['plain', 'text']:
|
||||
self.value.append(Plain(e['data']['text']))
|
||||
elif e['type'] == 'image':
|
||||
self.value.append(Image(e['data']['path']))
|
||||
elif e['type'] == 'voice':
|
||||
self.value.append(Voice(e['data']['path']))
|
||||
elif e['type'] == 'embed':
|
||||
self.value.append(
|
||||
Embed(e['data']['title'], e['data']['description'], e['data']['url'],
|
||||
e['data']['timestamp'],
|
||||
e['data']['color'], Image(e['data']['image']), Image(e['data']['thumbnail']),
|
||||
e['data']['author'], e['data']['footer'], e['data']['fields']))
|
||||
elif isinstance(e, str):
|
||||
if e != '':
|
||||
self.value += match_kecode(e)
|
||||
|
@ -45,6 +58,8 @@ class MessageChain(MessageChainT):
|
|||
Logger.error(f'Unexpected message type: {elements}')
|
||||
elif isinstance(elements, MessageChain):
|
||||
self.value = elements.value
|
||||
elif elements is None:
|
||||
pass
|
||||
else:
|
||||
Logger.error(f'Unexpected message type: {elements}')
|
||||
|
||||
|
@ -99,19 +114,33 @@ class MessageChain(MessageChainT):
|
|||
for x in self.value:
|
||||
if isinstance(x, Embed) and not embed:
|
||||
value += x.to_message_chain()
|
||||
elif isinstance(x, ErrorMessage):
|
||||
value.append(ErrorMessage(x.error_message, locale=locale))
|
||||
elif isinstance(x, Plain):
|
||||
if x.text != '':
|
||||
value.append(x)
|
||||
else:
|
||||
Plain(ErrorMessage('{error.message.chain.plain.empty}', locale=locale))
|
||||
value.append(Plain(ErrorMessage('{error.message.chain.plain.empty}', locale=locale)))
|
||||
else:
|
||||
value.append(x)
|
||||
if not value:
|
||||
value.append(Plain(ErrorMessage('{error.message.chain.empty}', locale=locale)))
|
||||
return value
|
||||
|
||||
def to_list(self, locale="zh_cn", embed=True):
|
||||
value = []
|
||||
for x in self.value:
|
||||
if isinstance(x, Embed) and not embed:
|
||||
value += x.to_message_chain().to_list()
|
||||
elif isinstance(x, Plain):
|
||||
if x.text != '':
|
||||
value.append(x.to_dict())
|
||||
else:
|
||||
value.append(Plain(ErrorMessage('{error.message.chain.plain.empty}', locale=locale)).to_dict())
|
||||
else:
|
||||
value.append(x.to_dict())
|
||||
if not value:
|
||||
value.append(Plain(ErrorMessage('{error.message.chain.empty}', locale=locale)).to_dict())
|
||||
return value
|
||||
|
||||
def append(self, element):
|
||||
self.value.append(element)
|
||||
|
||||
|
@ -121,6 +150,9 @@ class MessageChain(MessageChainT):
|
|||
def insert(self, index, element):
|
||||
self.value.insert(index, element)
|
||||
|
||||
def copy(self):
|
||||
return MessageChain(self.value.copy())
|
||||
|
||||
def __str__(self):
|
||||
return f'[{", ".join([x.__repr__() for x in self.value])}]'
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import base64
|
||||
import re
|
||||
import uuid
|
||||
from os.path import abspath
|
||||
|
@ -28,6 +29,9 @@ class Plain(PlainT):
|
|||
def __repr__(self):
|
||||
return f'Plain(text="{self.text}")'
|
||||
|
||||
def to_dict(self):
|
||||
return {'type': 'plain', 'data': {'text': self.text}}
|
||||
|
||||
|
||||
class Url(UrlT):
|
||||
mm = False
|
||||
|
@ -69,6 +73,9 @@ class ErrorMessage(EMsg):
|
|||
def __repr__(self):
|
||||
return self.error_message
|
||||
|
||||
def to_dict(self):
|
||||
return {'type': 'error', 'data': {'error': self.error_message}}
|
||||
|
||||
|
||||
class Image(ImageT):
|
||||
def __init__(self,
|
||||
|
@ -100,12 +107,20 @@ class Image(ImageT):
|
|||
image_cache.write(raw)
|
||||
return img_path
|
||||
|
||||
async def get_base64(self):
|
||||
file = await self.get()
|
||||
with open(file, 'rb') as f:
|
||||
return str(base64.b64encode(f.read()), "UTF-8")
|
||||
|
||||
def __str__(self):
|
||||
return self.path
|
||||
|
||||
def __repr__(self):
|
||||
return f'Image(path="{self.path}", headers={self.headers})'
|
||||
|
||||
def to_dict(self):
|
||||
return {'type': 'image', 'data': {'path': self.path}}
|
||||
|
||||
|
||||
class Voice(VoiceT):
|
||||
def __init__(self,
|
||||
|
@ -118,6 +133,9 @@ class Voice(VoiceT):
|
|||
def __repr__(self):
|
||||
return f'Voice(path={self.path})'
|
||||
|
||||
def to_dict(self):
|
||||
return {'type': 'voice', 'data': {'path': self.path}}
|
||||
|
||||
|
||||
class EmbedField(EmbedFieldT):
|
||||
def __init__(self,
|
||||
|
@ -134,6 +152,9 @@ class EmbedField(EmbedFieldT):
|
|||
def __repr__(self):
|
||||
return f'EmbedField(name="{self.name}", value="{self.value}", inline={self.inline})'
|
||||
|
||||
def to_dict(self):
|
||||
return {'type': 'field', 'data': {'name': self.name, 'value': self.value, 'inline': self.inline}}
|
||||
|
||||
|
||||
class Embed(EmbedT):
|
||||
def __init__(self,
|
||||
|
@ -192,5 +213,20 @@ class Embed(EmbedT):
|
|||
f'thumbnail={self.thumbnail.__repr__()}, author="{self.author}", footer="{self.footer}", ' \
|
||||
f'fields={self.fields})'
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'type': 'embed',
|
||||
'data': {
|
||||
'title': self.title,
|
||||
'description': self.description,
|
||||
'url': self.url,
|
||||
'timestamp': self.timestamp,
|
||||
'color': self.color,
|
||||
'image': self.image,
|
||||
'thumbnail': self.thumbnail,
|
||||
'author': self.author,
|
||||
'footer': self.footer,
|
||||
'fields': self.fields}}
|
||||
|
||||
|
||||
__all__ = ["Plain", "Image", "Voice", "Embed", "EmbedField", "Url", "ErrorMessage"]
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import asyncio
|
||||
from datetime import datetime
|
||||
import traceback
|
||||
|
||||
import ujson as json
|
||||
|
||||
from core.builtins import Bot
|
||||
from core.builtins import Bot, Temp, MessageChain
|
||||
from core.logger import Logger
|
||||
from core.utils.info import get_all_clients_name
|
||||
from core.utils.ip import append_ip, fetch_ip_info
|
||||
|
@ -47,6 +48,10 @@ class JobQueue:
|
|||
for target in get_all_clients_name():
|
||||
await cls.add_job(target, 'secret_append_ip', ip_info, wait=False)
|
||||
|
||||
@classmethod
|
||||
async def send_message(cls, target_client: str, target_id: str, message):
|
||||
await cls.add_job(target_client, 'send_message', {'target_id': target_id, 'message': message})
|
||||
|
||||
|
||||
def return_val(tsk, value: dict, status=True):
|
||||
value = value.update({'status': status})
|
||||
|
@ -63,17 +68,30 @@ async def check_job_queue():
|
|||
for tsk in get_all:
|
||||
Logger.debug(f'Received job queue task {tsk.taskid}, action: {tsk.action}')
|
||||
args = json.loads(tsk.args)
|
||||
Logger.debug(f'Args: {args}')
|
||||
try:
|
||||
if tsk.action == 'validate_permission':
|
||||
fetch = await Bot.FetchTarget.fetch_target(args['target_id'], args['sender_id'])
|
||||
if fetch:
|
||||
return_val(tsk, {'value': await fetch.parent.check_permission()})
|
||||
else:
|
||||
return_val(tsk, {'value': False})
|
||||
if tsk.action == 'trigger_hook':
|
||||
await Bot.Hook.trigger(args['module_or_hook_name'], args['args'])
|
||||
return_val(tsk, {})
|
||||
if tsk.action == 'secret_append_ip':
|
||||
append_ip(args)
|
||||
return_val(tsk, {})
|
||||
if tsk.action == 'send_message':
|
||||
return_val(tsk, {})
|
||||
fetch = await Bot.send_message(args['target_id'], MessageChain(args['message']))
|
||||
Logger.debug(f'Send message to {args["target_id"]}')
|
||||
if tsk.action == 'lagrange_keepalive':
|
||||
Temp.data['lagrange_keepalive'] = datetime.now().timestamp()
|
||||
return_val(tsk, {})
|
||||
if tsk.action == 'lagrange_available_groups':
|
||||
Temp.data['lagrange_available_groups'] = args
|
||||
return_val(tsk, {})
|
||||
|
||||
except Exception as e:
|
||||
return_val(tsk, {'traceback': traceback.format_exc()}, status=False)
|
||||
|
|
|
@ -10,7 +10,7 @@ class MessageChain:
|
|||
|
||||
def __init__(self, elements: Union[str, List[Union[Plain, Image, Voice, Embed, Url]],
|
||||
Tuple[Union[Plain, Image, Voice, Embed, Url]],
|
||||
Plain, Image, Voice, Embed, Url]):
|
||||
Plain, Image, Voice, Embed, Url] = None):
|
||||
"""
|
||||
:param elements: 消息链元素
|
||||
"""
|
||||
|
@ -29,6 +29,18 @@ class MessageChain:
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def to_list(self) -> list:
|
||||
"""
|
||||
将消息链转换为列表。
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def from_list(self, lst: list):
|
||||
"""
|
||||
从列表构造消息链。
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def append(self, element):
|
||||
"""
|
||||
添加一个消息链元素到末尾。
|
||||
|
|
Reference in a new issue