mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2025-12-15 06:12:53 +08:00
* ♻️ 使用Uninfo重构PlatformUtils基础方法 * 🩹 优化插件加载与模块格式转换逻辑 * 🚑 修复商店道具无法使用 * 🚑 修复道具无法正常使用 * 🔧 增加Bot状态管理及模块禁用功能 * 🎨 优化Web UI代码结构,修改target方法 * 🚨 auto fix by pre-commit hooks * 🎨 添加菜单API及优化异常处理 * 🐛 优化菜单API及模型结构,修复WebUi插件列表Api * 📝 更新仓库readme * 🚨 add mdlint file * 📝 Add help chapter. * 🐛 修复优化AuthChecker逻辑 * 🐛 优化数据库API,移除冗余导入及修正SQL_DICT引用 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: BalconyJH <balconyjh@gmail.com>
550 lines
18 KiB
Python
550 lines
18 KiB
Python
from collections.abc import Awaitable, Callable
|
||
import random
|
||
from typing import Literal
|
||
|
||
import httpx
|
||
import nonebot
|
||
from nonebot.adapters import Bot
|
||
from nonebot.utils import is_coroutine_callable
|
||
from nonebot_plugin_alconna import SupportScope
|
||
from nonebot_plugin_alconna.uniseg import Receipt, Target, UniMessage
|
||
from nonebot_plugin_uninfo import SceneType, Uninfo, get_interface
|
||
from nonebot_plugin_uninfo.model import Member
|
||
from pydantic import BaseModel
|
||
|
||
from zhenxun.configs.config import BotConfig
|
||
from zhenxun.models.friend_user import FriendUser
|
||
from zhenxun.models.group_console import GroupConsole
|
||
from zhenxun.services.log import logger
|
||
from zhenxun.utils.exception import NotFindSuperuser
|
||
from zhenxun.utils.http_utils import AsyncHttpx
|
||
from zhenxun.utils.message import MessageUtils
|
||
|
||
driver = nonebot.get_driver()
|
||
|
||
|
||
class UserData(BaseModel):
|
||
name: str
|
||
"""昵称"""
|
||
card: str | None = None
|
||
"""名片/备注"""
|
||
user_id: str
|
||
"""用户id"""
|
||
group_id: str | None = None
|
||
"""群组id"""
|
||
channel_id: str | None = None
|
||
"""频道id"""
|
||
role: str | None = None
|
||
"""角色"""
|
||
avatar_url: str | None = None
|
||
"""头像url"""
|
||
join_time: int | None = None
|
||
"""加入时间"""
|
||
|
||
|
||
class PlatformUtils:
|
||
@classmethod
|
||
def is_qbot(cls, session: Uninfo | Bot) -> bool:
|
||
"""判断bot是否为qq官bot
|
||
|
||
参数:
|
||
session: Uninfo
|
||
|
||
返回:
|
||
bool: 是否为官bot
|
||
"""
|
||
if isinstance(session, Bot):
|
||
return bool(BotConfig.get_qbot_uid(session.self_id))
|
||
return session.scope == SupportScope.qq_api
|
||
|
||
@classmethod
|
||
async def ban_user(cls, bot: Bot, user_id: str, group_id: str, duration: int):
|
||
"""禁言
|
||
|
||
参数:
|
||
bot: Bot
|
||
user_id: 用户id
|
||
group_id: 群组id
|
||
duration: 禁言时长(分钟)
|
||
"""
|
||
if cls.get_platform(bot) == "qq":
|
||
await bot.set_group_ban(
|
||
group_id=int(group_id),
|
||
user_id=int(user_id),
|
||
duration=duration * 60,
|
||
)
|
||
|
||
@classmethod
|
||
async def send_superuser(
|
||
cls,
|
||
bot: Bot,
|
||
message: UniMessage | str,
|
||
superuser_id: str | None = None,
|
||
) -> Receipt | None:
|
||
"""发送消息给超级用户
|
||
|
||
参数:
|
||
bot: Bot
|
||
message: 消息
|
||
superuser_id: 指定超级用户id.
|
||
|
||
异常:
|
||
NotFindSuperuser: 未找到超级用户id
|
||
|
||
返回:
|
||
Receipt | None: Receipt
|
||
"""
|
||
if not superuser_id:
|
||
if platform := cls.get_platform(bot):
|
||
if platform_superusers := BotConfig.get_superuser(platform):
|
||
superuser_id = random.choice(platform_superusers)
|
||
else:
|
||
raise NotFindSuperuser()
|
||
if isinstance(message, str):
|
||
message = MessageUtils.build_message(message)
|
||
return await cls.send_message(bot, superuser_id, None, message)
|
||
|
||
@classmethod
|
||
async def get_group_member_list(cls, bot: Bot, group_id: str) -> list[UserData]:
|
||
"""获取群组/频道成员列表
|
||
|
||
参数:
|
||
bot: Bot
|
||
group_id: 群组/频道id
|
||
|
||
返回:
|
||
list[UserData]: 用户数据列表
|
||
"""
|
||
if interface := get_interface(bot):
|
||
members: list[Member] = await interface.get_members(
|
||
SceneType.GROUP, group_id
|
||
)
|
||
return [
|
||
UserData(
|
||
name=member.user.name or "",
|
||
card=member.nick,
|
||
user_id=member.user.id,
|
||
group_id=group_id,
|
||
role=member.role.id if member.role else "",
|
||
avatar_url=member.user.avatar,
|
||
join_time=int(member.joined_at.timestamp())
|
||
if member.joined_at
|
||
else None,
|
||
)
|
||
for member in members
|
||
]
|
||
return []
|
||
|
||
@classmethod
|
||
async def get_user(
|
||
cls,
|
||
bot: Bot,
|
||
user_id: str,
|
||
group_id: str | None = None,
|
||
channel_id: str | None = None,
|
||
) -> UserData | None:
|
||
"""获取用户信息
|
||
|
||
参数:
|
||
bot: Bot
|
||
user_id: 用户id
|
||
group_id: 群组id.
|
||
channel_id: 频道id.
|
||
|
||
返回:
|
||
UserData | None: 用户数据
|
||
"""
|
||
if interface := get_interface(bot):
|
||
member = None
|
||
user = None
|
||
if channel_id:
|
||
member = await interface.get_member(
|
||
SceneType.CHANNEL_TEXT, channel_id, user_id
|
||
)
|
||
if member:
|
||
user = member.user
|
||
elif group_id:
|
||
member = await interface.get_member(SceneType.GROUP, group_id, user_id)
|
||
if member:
|
||
user = member.user
|
||
else:
|
||
user = await interface.get_user(user_id)
|
||
if not user:
|
||
return None
|
||
if member:
|
||
return UserData(
|
||
name=user.name or "",
|
||
card=member.nick,
|
||
user_id=user.id,
|
||
group_id=group_id,
|
||
channel_id=channel_id,
|
||
role=member.role.id if member.role else None,
|
||
join_time=int(member.joined_at.timestamp())
|
||
if member.joined_at
|
||
else None,
|
||
)
|
||
else:
|
||
return UserData(
|
||
name=user.name or "",
|
||
user_id=user.id,
|
||
group_id=group_id,
|
||
channel_id=channel_id,
|
||
)
|
||
return None
|
||
|
||
@classmethod
|
||
async def get_user_avatar(
|
||
cls, user_id: str, platform: str, appid: str | None = None
|
||
) -> bytes | None:
|
||
"""快捷获取用户头像
|
||
|
||
参数:
|
||
user_id: 用户id
|
||
platform: 平台
|
||
"""
|
||
url = None
|
||
if platform == "qq":
|
||
if user_id.isdigit():
|
||
url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=160"
|
||
else:
|
||
url = f"https://q.qlogo.cn/qqapp/{appid}/{user_id}/100"
|
||
return await AsyncHttpx.get_content(url) if url else None
|
||
|
||
@classmethod
|
||
def get_user_avatar_url(
|
||
cls, user_id: str, platform: str, appid: str | None = None
|
||
) -> str | None:
|
||
"""快捷获取用户头像url
|
||
|
||
参数:
|
||
user_id: 用户id
|
||
platform: 平台
|
||
"""
|
||
if platform == "qq":
|
||
return (
|
||
f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=160"
|
||
if user_id.isdigit()
|
||
else f"https://q.qlogo.cn/qqapp/{appid}/{user_id}/100"
|
||
)
|
||
else:
|
||
return None
|
||
|
||
@classmethod
|
||
async def get_group_avatar(cls, gid: str, platform: str) -> bytes | None:
|
||
"""快捷获取用群头像
|
||
|
||
参数:
|
||
gid: 群组id
|
||
platform: 平台
|
||
"""
|
||
if platform == "qq":
|
||
url = f"http://p.qlogo.cn/gh/{gid}/{gid}/640/"
|
||
async with httpx.AsyncClient() as client:
|
||
for _ in range(3):
|
||
try:
|
||
return (await client.get(url)).content
|
||
except Exception:
|
||
logger.error(
|
||
"获取群头像错误", "Util", target=gid, platform=platform
|
||
)
|
||
return None
|
||
|
||
@classmethod
|
||
async def send_message(
|
||
cls,
|
||
bot: Bot,
|
||
user_id: str | None,
|
||
group_id: str | None,
|
||
message: str | UniMessage,
|
||
) -> Receipt | None:
|
||
"""发送消息
|
||
|
||
参数:
|
||
bot: Bot
|
||
user_id: 用户id
|
||
group_id: 群组id或频道id
|
||
message: 消息文本
|
||
|
||
返回:
|
||
Receipt | None: 是否发送成功
|
||
"""
|
||
if target := cls.get_target(user_id=user_id, group_id=group_id):
|
||
send_message = (
|
||
MessageUtils.build_message(message)
|
||
if isinstance(message, str)
|
||
else message
|
||
)
|
||
return await send_message.send(target=target, bot=bot)
|
||
return None
|
||
|
||
@classmethod
|
||
async def update_group(cls, bot: Bot) -> int:
|
||
"""更新群组信息
|
||
|
||
参数:
|
||
bot: Bot
|
||
|
||
返回:
|
||
int: 更新个数
|
||
"""
|
||
create_list = []
|
||
update_list = []
|
||
group_list, platform = await cls.get_group_list(bot)
|
||
if group_list:
|
||
db_group = await GroupConsole.all()
|
||
db_group_id: list[tuple[str, str]] = [
|
||
(group.group_id, group.channel_id) for group in db_group
|
||
]
|
||
for group in group_list:
|
||
group.platform = platform
|
||
if (group.group_id, group.channel_id) not in db_group_id:
|
||
create_list.append(group)
|
||
logger.debug(
|
||
"群聊信息更新成功",
|
||
"更新群信息",
|
||
target=f"{group.group_id}:{group.channel_id}",
|
||
)
|
||
else:
|
||
_group = next(
|
||
g
|
||
for g in db_group
|
||
if g.group_id == group.group_id
|
||
and g.channel_id == group.channel_id
|
||
)
|
||
_group.group_name = group.group_name
|
||
_group.max_member_count = group.max_member_count
|
||
_group.member_count = group.member_count
|
||
update_list.append(_group)
|
||
if create_list:
|
||
await GroupConsole.bulk_create(create_list, 10)
|
||
if group_list:
|
||
await GroupConsole.bulk_update(
|
||
update_list, ["group_name", "max_member_count", "member_count"], 10
|
||
)
|
||
return len(create_list)
|
||
|
||
@classmethod
|
||
def get_platform(cls, t: Bot | Uninfo) -> str:
|
||
"""获取平台
|
||
|
||
参数:
|
||
bot: Bot
|
||
|
||
返回:
|
||
str | None: 平台
|
||
"""
|
||
if isinstance(t, Bot):
|
||
if interface := get_interface(t):
|
||
info = interface.basic_info()
|
||
platform = info["scope"].lower()
|
||
return "qq" if platform.startswith("qq") else platform
|
||
else:
|
||
platform = t.basic["scope"].lower()
|
||
return "qq" if platform.startswith("qq") else platform
|
||
return "unknown"
|
||
|
||
@classmethod
|
||
async def get_group_list(
|
||
cls, bot: Bot, only_group: bool = False
|
||
) -> tuple[list[GroupConsole], str]:
|
||
"""获取群组列表
|
||
|
||
参数:
|
||
bot: Bot
|
||
only_group: 是否只获取群组(不获取channel)
|
||
|
||
返回:
|
||
tuple[list[GroupConsole], str]: 群组列表, 平台
|
||
"""
|
||
if interface := get_interface(bot):
|
||
platform = cls.get_platform(bot)
|
||
result_list = []
|
||
scenes = await interface.get_scenes(SceneType.GROUP)
|
||
for scene in scenes:
|
||
group_id = scene.id
|
||
result_list.append(
|
||
GroupConsole(
|
||
group_id=scene.id,
|
||
group_name=scene.name,
|
||
)
|
||
)
|
||
if not only_group and platform != "qq":
|
||
if channel_list := await interface.get_scenes(
|
||
parent_scene_id=group_id
|
||
):
|
||
for channel in channel_list:
|
||
result_list.append(
|
||
GroupConsole(
|
||
group_id=scene.id,
|
||
group_name=channel.name,
|
||
channel_id=channel.id,
|
||
)
|
||
)
|
||
return result_list, platform
|
||
return [], ""
|
||
|
||
@classmethod
|
||
async def update_friend(cls, bot: Bot) -> int:
|
||
"""更新好友信息
|
||
|
||
参数:
|
||
bot: Bot
|
||
|
||
返回:
|
||
int: 更新个数
|
||
"""
|
||
create_list = []
|
||
friend_list, platform = await cls.get_friend_list(bot)
|
||
if friend_list:
|
||
user_id_list = await FriendUser.all().values_list("user_id", flat=True)
|
||
for friend in friend_list:
|
||
friend.platform = platform
|
||
if friend.user_id not in user_id_list:
|
||
create_list.append(friend)
|
||
if create_list:
|
||
await FriendUser.bulk_create(create_list, 10)
|
||
return len(create_list)
|
||
|
||
@classmethod
|
||
async def get_friend_list(cls, bot: Bot) -> tuple[list[FriendUser], str]:
|
||
"""获取好友列表
|
||
|
||
参数:
|
||
bot: Bot
|
||
|
||
返回:
|
||
list[FriendUser]: 好友列表
|
||
"""
|
||
if interface := get_interface(bot):
|
||
user_list = await interface.get_users()
|
||
return [
|
||
FriendUser(user_id=u.id, user_name=u.name) for u in user_list
|
||
], cls.get_platform(bot)
|
||
return [], ""
|
||
|
||
@classmethod
|
||
def get_target(
|
||
cls,
|
||
*,
|
||
user_id: str | None = None,
|
||
group_id: str | None = None,
|
||
channel_id: str | None = None,
|
||
):
|
||
"""获取发生Target
|
||
|
||
参数:
|
||
bot: Bot
|
||
user_id: 用户id
|
||
group_id: 频道id或群组id
|
||
channel_id: 频道id
|
||
|
||
返回:
|
||
target: 对应平台Target
|
||
"""
|
||
target = None
|
||
if group_id and channel_id:
|
||
target = Target(channel_id, parent_id=group_id, channel=True)
|
||
elif group_id:
|
||
target = Target(group_id)
|
||
elif user_id:
|
||
target = Target(user_id, private=True)
|
||
return target
|
||
|
||
|
||
async def broadcast_group(
|
||
message: str | UniMessage,
|
||
bot: Bot | list[Bot] | None = None,
|
||
bot_id: str | set[str] | None = None,
|
||
ignore_group: set[int] | None = None,
|
||
check_func: Callable[[Bot, str], Awaitable] | None = None,
|
||
log_cmd: str | None = None,
|
||
platform: Literal["qq", "dodo", "kaiheila"] | None = None,
|
||
):
|
||
"""获取所有Bot或指定Bot对象广播群聊
|
||
|
||
参数:
|
||
message: 广播消息内容
|
||
bot: 指定bot对象.
|
||
bot_id: 指定bot id.
|
||
ignore_group: 忽略群聊列表.
|
||
check_func: 发送前对群聊检测方法,判断是否发送.
|
||
log_cmd: 日志标记.
|
||
platform: 指定平台
|
||
"""
|
||
if platform and platform not in ["qq", "dodo", "kaiheila"]:
|
||
raise ValueError("指定平台不支持")
|
||
if not message:
|
||
raise ValueError("群聊广播消息不能为空")
|
||
bot_dict = nonebot.get_bots()
|
||
bot_list: list[Bot] = []
|
||
if bot:
|
||
if isinstance(bot, list):
|
||
bot_list = bot
|
||
else:
|
||
bot_list.append(bot)
|
||
elif bot_id:
|
||
_bot_id_list = bot_id
|
||
if isinstance(bot_id, str):
|
||
_bot_id_list = [bot_id]
|
||
for id_ in _bot_id_list:
|
||
if bot_id in bot_dict:
|
||
bot_list.append(bot_dict[bot_id])
|
||
else:
|
||
logger.warning(f"Bot:{id_} 对象未连接或不存在")
|
||
else:
|
||
bot_list = list(bot_dict.values())
|
||
_used_group = []
|
||
for _bot in bot_list:
|
||
try:
|
||
if platform and platform != PlatformUtils.get_platform(_bot):
|
||
continue
|
||
group_list, _ = await PlatformUtils.get_group_list(_bot)
|
||
if group_list:
|
||
for group in group_list:
|
||
key = f"{group.group_id}:{group.channel_id}"
|
||
try:
|
||
if (
|
||
ignore_group
|
||
and (
|
||
group.group_id in ignore_group
|
||
or group.channel_id in ignore_group
|
||
)
|
||
) or key in _used_group:
|
||
logger.debug(
|
||
"广播方法群组重复, 已跳过...",
|
||
log_cmd,
|
||
group_id=group.group_id,
|
||
)
|
||
continue
|
||
is_run = False
|
||
if check_func:
|
||
if is_coroutine_callable(check_func):
|
||
is_run = await check_func(_bot, group.group_id)
|
||
else:
|
||
is_run = check_func(_bot, group.group_id)
|
||
if not is_run:
|
||
logger.debug(
|
||
"广播方法检测运行方法为 False, 已跳过...",
|
||
log_cmd,
|
||
group_id=group.group_id,
|
||
)
|
||
continue
|
||
target = PlatformUtils.get_target(
|
||
user_id=None,
|
||
group_id=group.group_id,
|
||
channel_id=group.channel_id,
|
||
)
|
||
if target:
|
||
_used_group.append(key)
|
||
message_list = message
|
||
await MessageUtils.build_message(message_list).send(
|
||
target, _bot
|
||
)
|
||
logger.debug("发送成功", log_cmd, target=key)
|
||
else:
|
||
logger.warning("target为空", log_cmd, target=key)
|
||
except Exception as e:
|
||
logger.error("发送失败", log_cmd, target=key, e=e)
|
||
except Exception as e:
|
||
logger.error(f"Bot: {_bot.self_id} 获取群聊列表失败", command=log_cmd, e=e)
|