zhenxun_bot/zhenxun/utils/platform.py
HibiKier 35014e4048
重构webui适配 (#1801)
* ♻️ 使用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>
2024-12-25 12:03:49 +08:00

550 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)