mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-24 05:30:08 +08:00
🎨 修复bug,移除plugins文件夹
This commit is contained in:
+1
-1
@@ -119,7 +119,7 @@ fixture-parentheses = false
|
||||
mark-parentheses = false
|
||||
|
||||
[tool.pyright]
|
||||
pythonVersion = "3.9"
|
||||
pythonVersion = "3.10"
|
||||
pythonPlatform = "All"
|
||||
defineConstant = { PYDANTIC_V2 = true }
|
||||
executionEnvironments = [
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
# from nonebot.adapters.discord import Bot as DiscordBot
|
||||
# from nonebot.adapters.dodo import Bot as DodoBot
|
||||
from nonebot.adapters.dodo.models import MemberInfo
|
||||
|
||||
# from nonebot.adapters.kaiheila import Bot as KaiheilaBot
|
||||
from nonebot.adapters.onebot.v11 import Bot as v11Bot
|
||||
from nonebot.adapters.onebot.v12 import Bot as v12Bot
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.models.group_member_info import GroupInfoUser
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.models.group_member_info import GroupInfoUser
|
||||
|
||||
# from nonebot.adapters.discord import Bot as DiscordBot
|
||||
# from nonebot.adapters.dodo import Bot as DodoBot
|
||||
|
||||
|
||||
class MemberUpdateManage:
|
||||
@@ -98,9 +96,10 @@ class MemberUpdateManage:
|
||||
user_id = str(user_info["user_id"])
|
||||
nickname = user_info["card"] or user_info["nickname"]
|
||||
role = user_info["role"]
|
||||
if default_auth:
|
||||
if role in ["owner", "admin"] and not await LevelUser.is_group_flag(
|
||||
user_id, group_id
|
||||
if (
|
||||
default_auth
|
||||
and role in ["owner", "admin"]
|
||||
and not await LevelUser.is_group_flag(user_id, group_id)
|
||||
):
|
||||
if role == "owner":
|
||||
await LevelUser.set_level(user_id, group_id, default_auth + 1)
|
||||
@@ -108,19 +107,16 @@ class MemberUpdateManage:
|
||||
await LevelUser.set_level(user_id, group_id, default_auth)
|
||||
if user_id in bot.config.superusers:
|
||||
await LevelUser.set_level(user_id, group_id, 9)
|
||||
join_time = datetime.strptime(
|
||||
time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S", time.localtime(user_info["join_time"])
|
||||
),
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
).replace(tzinfo=timezone(timedelta(hours=8)))
|
||||
join_time = datetime.fromtimestamp(
|
||||
user_info["join_time"], timezone(timedelta(hours=8))
|
||||
)
|
||||
if cnt := db_user_uid.count(user_id):
|
||||
users = [u for u in db_user if u.user_id == user_id]
|
||||
user = users[0]
|
||||
if cnt > 1:
|
||||
for u in users[1:]:
|
||||
delete_list.append(u.id)
|
||||
if nickname != uid2name.get(user_id):
|
||||
user = users[0]
|
||||
user.user_name = nickname
|
||||
update_list.append(user)
|
||||
else:
|
||||
@@ -156,7 +152,7 @@ class MemberUpdateManage:
|
||||
user_id__in=delete_member_list, group_id=group_id
|
||||
).delete()
|
||||
logger.info(
|
||||
f"删除已退群用户", "更新群组成员信息", group_id=group_id, platform="qq"
|
||||
"删除已退群用户", "更新群组成员信息", group_id=group_id, platform="qq"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -165,7 +161,8 @@ class MemberUpdateManage:
|
||||
pass
|
||||
# exist_member_list = []
|
||||
# default_auth = Config.get_config("admin_bot_manage", "ADMIN_DEFAULT_AUTH")
|
||||
# group_member_list: list[GetGroupMemberInfoResp] = await bot.get_group_member_list(
|
||||
# group_member_list: list[GetGroupMemberInfoResp] =
|
||||
# await bot.get_group_member_list(
|
||||
# group_id=group_id
|
||||
# )
|
||||
# for user_info in group_member_list:
|
||||
@@ -180,7 +177,8 @@ class MemberUpdateManage:
|
||||
# if str(user_id) in bot.config.superusers:
|
||||
# await LevelUser.set_level(str(user_id), group_id, 9)
|
||||
# join_time = datetime.strptime(
|
||||
# time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(user_info["join_time"])),
|
||||
# time.strftime("%Y-%m-%d %H:%M:%S",
|
||||
# time.localtime(user_info["join_time"])),
|
||||
# "%Y-%m-%d %H:%M:%S",
|
||||
# )
|
||||
# await GroupInfoUser.update_or_create(
|
||||
@@ -194,7 +192,8 @@ class MemberUpdateManage:
|
||||
# },
|
||||
# )
|
||||
# exist_member_list.append(str(user_id))
|
||||
# logger.debug("更新成功", "更新群组成员信息", session=user_id, group_id=group_id)
|
||||
# logger.debug("更新成功", "更新群组成员信息",
|
||||
# session=user_id, group_id=group_id)
|
||||
# if delete_member_list := list(
|
||||
# set(exist_member_list).difference(
|
||||
# set(await GroupInfoUser.get_group_member_id_list(group_id))
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from nonebot.rule import to_me
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me
|
||||
from nonebot_plugin_alconna import Alconna, Args, Match, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
from nonebot_plugin_alconna import Args, Match, Alconna, on_alconna
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.configs.utils import RegisterConfig, PluginExtraData
|
||||
|
||||
from ._data_source import UpdateManage
|
||||
|
||||
@@ -32,12 +32,6 @@ __plugin_meta__ = PluginMetadata(
|
||||
help="是否检测更新版本",
|
||||
default_value=True,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="UPDATE_REMIND",
|
||||
value=True,
|
||||
help="是否检测更新版本",
|
||||
default_value=True,
|
||||
),
|
||||
],
|
||||
).dict(),
|
||||
)
|
||||
|
||||
@@ -3,9 +3,9 @@ from pathlib import Path
|
||||
import nonebot
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
|
||||
nonebot.load_plugins(str(Path(__file__).parent.resolve()))
|
||||
|
||||
@@ -20,7 +20,8 @@ async def _(bot: Bot):
|
||||
参数:
|
||||
bot: Bot
|
||||
"""
|
||||
if PlatformUtils.get_platform(bot) == "qq":
|
||||
if PlatformUtils.get_platform(bot) != "qq":
|
||||
return
|
||||
logger.debug(f"更新Bot: {bot.self_id} 的群认证...")
|
||||
group_list, _ = await PlatformUtils.get_group_list(bot)
|
||||
db_group_list = await GroupConsole.all().values_list("group_id", flat=True)
|
||||
@@ -37,5 +38,6 @@ async def _(bot: Bot):
|
||||
else:
|
||||
await GroupConsole.filter(group_id__in=update_id).update(group_flag=1)
|
||||
logger.debug(
|
||||
f"更新Bot: {bot.self_id} 的群认证完成,共创建 {len(create_list)} 条数据,共修改 {len(update_id)} 条数据..."
|
||||
f"更新Bot: {bot.self_id} 的群认证完成,共创建 {len(create_list)} 条数据,"
|
||||
"共修改 {len(update_id)} 条数据..."
|
||||
)
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import nonebot
|
||||
import aiofiles
|
||||
import ujson as json
|
||||
from nonebot import get_loaded_plugins
|
||||
from nonebot.drivers import Driver
|
||||
from nonebot.plugin import Plugin
|
||||
from ruamel.yaml import YAML
|
||||
from nonebot.plugin import Plugin
|
||||
from nonebot.drivers import Driver
|
||||
from nonebot import get_loaded_plugins
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.configs.path_config import DATA_PATH
|
||||
from zhenxun.configs.utils import PluginExtraData, PluginSetting
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.plugin_limit import PluginLimit
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.configs.utils import PluginSetting, PluginExtraData
|
||||
from zhenxun.utils.enum import (
|
||||
BlockType,
|
||||
PluginType,
|
||||
LimitCheckType,
|
||||
LimitWatchType,
|
||||
PluginLimitType,
|
||||
PluginType,
|
||||
)
|
||||
|
||||
_yaml = YAML(pure=True)
|
||||
@@ -31,7 +32,7 @@ async def _handle_setting(
|
||||
plugin: Plugin,
|
||||
plugin_list: list[PluginInfo],
|
||||
limit_list: list[PluginLimit],
|
||||
task_list: list[TaskInfo],
|
||||
task_list: list[tuple[bool, TaskInfo]],
|
||||
):
|
||||
"""处理插件设置
|
||||
|
||||
@@ -40,8 +41,7 @@ async def _handle_setting(
|
||||
plugin_list: 插件列表
|
||||
limit_list: 插件限制列表
|
||||
"""
|
||||
metadata = plugin.metadata
|
||||
if metadata:
|
||||
if metadata := plugin.metadata:
|
||||
extra = metadata.extra
|
||||
extra_data = PluginExtraData(**extra)
|
||||
logger.debug(f"{metadata.name}:{plugin.name} -> {extra}", "初始化插件数据")
|
||||
@@ -71,8 +71,7 @@ async def _handle_setting(
|
||||
)
|
||||
)
|
||||
if extra_data.limits:
|
||||
for limit in extra_data.limits:
|
||||
limit_list.append(
|
||||
limit_list.extend(
|
||||
PluginLimit(
|
||||
module=plugin.name,
|
||||
module_path=plugin.module_name,
|
||||
@@ -84,17 +83,22 @@ async def _handle_setting(
|
||||
cd=getattr(limit, "cd", None),
|
||||
max_count=getattr(limit, "max_count", None),
|
||||
)
|
||||
for limit in extra_data.limits
|
||||
)
|
||||
if extra_data.tasks:
|
||||
for task in extra_data.tasks:
|
||||
task_list.append((task.create_status,
|
||||
task_list.extend(
|
||||
(
|
||||
task.create_status,
|
||||
TaskInfo(
|
||||
module=task.module,
|
||||
name=task.name,
|
||||
status=task.status,
|
||||
run_time=task.run_time,
|
||||
default_status=task.default_status,
|
||||
),
|
||||
)
|
||||
for task in extra_data.tasks
|
||||
)
|
||||
))
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
@@ -104,7 +108,7 @@ async def _():
|
||||
"""
|
||||
plugin_list: list[PluginInfo] = []
|
||||
limit_list: list[PluginLimit] = []
|
||||
task_list: list[TaskInfo] = []
|
||||
task_list = []
|
||||
module2id = {}
|
||||
load_plugin = []
|
||||
if module_list := await PluginInfo.all().values("id", "module_path"):
|
||||
@@ -132,9 +136,10 @@ async def _():
|
||||
update_list.append(plugin)
|
||||
if create_list:
|
||||
await PluginInfo.bulk_create(create_list, 10)
|
||||
if update_list:
|
||||
# TODO: 批量更新无法更新plugin_type: tortoise.exceptions.OperationalError: column "superuser" does not exist
|
||||
pass
|
||||
# if update_list:
|
||||
# # TODO: 批量更新无法更新plugin_type: tortoise.exceptions.OperationalError:
|
||||
# column "superuser" does not exist
|
||||
# pass
|
||||
# await PluginInfo.bulk_update(
|
||||
# update_list,
|
||||
# ["name", "author", "version", "admin_level", "plugin_type"],
|
||||
@@ -147,8 +152,8 @@ async def _():
|
||||
plugins = await PluginInfo.filter(module_path__in=module_path_list).all()
|
||||
if plugins:
|
||||
for limit in limit_list:
|
||||
if l := [p for p in plugins if p.module_path == limit.module_path]:
|
||||
plugin = l[0]
|
||||
if lmt := [p for p in plugins if p.module_path == limit.module_path]:
|
||||
plugin = lmt[0]
|
||||
limit_type_list = [
|
||||
_limit.limit_type for _limit in await plugin.plugin_limit.all() # type: ignore
|
||||
]
|
||||
@@ -202,14 +207,14 @@ async def limit_migration():
|
||||
count_file = DATA_PATH / "configs" / "plugins2count.yaml"
|
||||
limit_data: dict[str, list[tuple[str, dict]]] = {}
|
||||
if cd_file.exists():
|
||||
with open(cd_file, encoding="utf8") as f:
|
||||
if data := _yaml.load(f):
|
||||
async with aiofiles.open(cd_file, encoding="utf8") as f:
|
||||
if data := _yaml.load(await f.read()):
|
||||
for k in data["PluginCdLimit"]:
|
||||
limit_data[k] = [("CD", data["PluginCdLimit"][k])]
|
||||
cd_file.unlink()
|
||||
if block_file.exists():
|
||||
with open(block_file, encoding="utf8") as f:
|
||||
if data := _yaml.load(f):
|
||||
async with aiofiles.open(block_file, encoding="utf8") as f:
|
||||
if data := _yaml.load(await f.read()):
|
||||
for k in data["PluginBlockLimit"]:
|
||||
if k in limit_data:
|
||||
limit_data[k].append(("BLOCK", data["PluginBlockLimit"][k]))
|
||||
@@ -217,8 +222,8 @@ async def limit_migration():
|
||||
limit_data[k] = [("BLOCK", data["PluginBlockLimit"][k])]
|
||||
block_file.unlink()
|
||||
if count_file.exists():
|
||||
with open(count_file, encoding="utf8") as f:
|
||||
if data := _yaml.load(f):
|
||||
async with aiofiles.open(count_file, encoding="utf8") as f:
|
||||
if data := _yaml.load(await f.read()):
|
||||
for k in data["PluginCountLimit"]:
|
||||
if k in limit_data:
|
||||
limit_data[k].append(("COUNT", data["PluginCountLimit"][k]))
|
||||
@@ -294,7 +299,8 @@ async def limit_migration():
|
||||
max_count=_limit.get("max_count"),
|
||||
)
|
||||
)
|
||||
# TODO: 批量错误 tortoise.exceptions.OperationalError: syntax error at or near "ALL"
|
||||
# TODO: 批量错误 tortoise.exceptions.OperationalError:
|
||||
# syntax error at or near "ALL"
|
||||
# if update_list:
|
||||
# await PluginLimit.bulk_update(
|
||||
# update_list,
|
||||
@@ -318,8 +324,8 @@ async def plugin_migration():
|
||||
setting_file = DATA_PATH / "configs" / "plugins2settings.yaml"
|
||||
plugin_file = DATA_PATH / "manager" / "plugins_manager.json"
|
||||
if setting_file.exists():
|
||||
with open(setting_file, encoding="utf8") as f:
|
||||
if data := _yaml.load(f):
|
||||
async with aiofiles.open(setting_file, encoding="utf8") as f:
|
||||
if data := _yaml.load(await f.read()):
|
||||
logger.info("开始迁移插件setting数据...")
|
||||
data = data["PluginSettings"]
|
||||
plugins = await PluginInfo.filter(module__in=data.keys())
|
||||
@@ -349,8 +355,8 @@ async def plugin_migration():
|
||||
setting_file.unlink()
|
||||
logger.info("迁移插件setting数据完成!")
|
||||
if plugin_file.exists():
|
||||
with open(plugin_file, encoding="utf8") as f:
|
||||
if data := json.load(f):
|
||||
async with aiofiles.open(plugin_file, encoding="utf8") as f:
|
||||
if data := json.loads(await f.read()):
|
||||
logger.info("开始迁移插件数据...")
|
||||
plugins = await PluginInfo.filter(module__in=data.keys())
|
||||
for plugin in plugins:
|
||||
@@ -366,7 +372,8 @@ async def plugin_migration():
|
||||
block_type = BlockType.GROUP
|
||||
plugin.block_type = block_type
|
||||
await plugin.save(update_fields=["status", "block_type"])
|
||||
# TODO: tortoise.exceptions.OperationalError: syntax error at or near "ALL"
|
||||
# TODO: tortoise.exceptions.OperationalError: syntax error at
|
||||
# or near "ALL"
|
||||
# await PluginInfo.bulk_update(plugins, ["status", "block_type"], 10)
|
||||
plugin_file.unlink()
|
||||
logger.info("迁移插件数据完成!")
|
||||
@@ -378,22 +385,20 @@ async def group_migration():
|
||||
"""
|
||||
group_file = DATA_PATH / "manager" / "group_manager.json"
|
||||
if group_file.exists():
|
||||
with open(group_file, encoding="utf8") as f:
|
||||
if data := json.load(f):
|
||||
async with aiofiles.open(group_file, encoding="utf8") as f:
|
||||
if data := json.loads(await f.read()):
|
||||
logger.info("开始迁移群组数据...")
|
||||
update_list = []
|
||||
create_list = []
|
||||
white_group = data["white_group"]
|
||||
close_task = data["close_task"]
|
||||
old_group_list: dict = data["group_manager"]
|
||||
if close_task:
|
||||
if close_task := data["close_task"]:
|
||||
"""全局被动关闭"""
|
||||
await TaskInfo.filter(module__in=close_task).update(status=False)
|
||||
group_list = await GroupConsole.filter(
|
||||
group_id__in=old_group_list.keys()
|
||||
)
|
||||
for old_group_id in old_group_list:
|
||||
old_group = old_group_list[old_group_id]
|
||||
for old_group_id, old_group in old_group_list.items():
|
||||
block_plugin = ""
|
||||
block_task = ""
|
||||
status = old_group.get("status", True)
|
||||
|
||||
@@ -19,11 +19,11 @@ from nonebot.adapters.onebot.v12 import (
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.utils import FreqLimiter
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.models.fg_request import FgRequest
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
from zhenxun.configs.config import Config, BotConfig
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.group_member_info import GroupInfoUser
|
||||
@@ -242,7 +242,7 @@ async def _(bot: Bot, event: GroupIncreaseNoticeEvent | GroupMemberIncreaseEvent
|
||||
img_file = path / f"{i}.png"
|
||||
if img_file.exists():
|
||||
msg_list.append(img_file)
|
||||
if not await TaskInfo.is_block("group_welcome", group_id):
|
||||
if not await CommonUtils.is_block("group_welcome", group_id):
|
||||
logger.info("发送群欢迎消息...", "入群检测", group_id=group_id)
|
||||
if msg_list:
|
||||
await MessageUtils.build_message(msg_list).send()
|
||||
@@ -314,5 +314,5 @@ async def _(bot: Bot, event: GroupDecreaseNoticeEvent | GroupMemberDecreaseEvent
|
||||
)
|
||||
operator_name = operator["card"] or operator["nickname"]
|
||||
result = f"{user_name} 被 {operator_name} 送走了."
|
||||
if not await TaskInfo.is_block("refund_group_remind", str(event.group_id)):
|
||||
if not await CommonUtils.is_block("refund_group_remind", str(event.group_id)):
|
||||
await group_decrease_handle.send(f"{result}")
|
||||
|
||||
@@ -3,19 +3,20 @@ import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import aiofiles
|
||||
import ujson as json
|
||||
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.image_utils import BuildImage, ImageTemplate, RowStyle
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.utils.image_utils import RowStyle, BuildImage, ImageTemplate
|
||||
|
||||
from .config import (
|
||||
BASE_PATH,
|
||||
CONFIG_INDEX_CDN_URL,
|
||||
CONFIG_INDEX_URL,
|
||||
CONFIG_URL,
|
||||
DOWNLOAD_URL,
|
||||
CONFIG_INDEX_URL,
|
||||
CONFIG_INDEX_CDN_URL,
|
||||
)
|
||||
|
||||
|
||||
@@ -30,8 +31,7 @@ def row_style(column: str, text: str) -> RowStyle:
|
||||
RowStyle: RowStyle
|
||||
"""
|
||||
style = RowStyle()
|
||||
if column in ["-"]:
|
||||
if text == "已安装":
|
||||
if column == "-" and text == "已安装":
|
||||
style.font_color = "#67C23A"
|
||||
return style
|
||||
|
||||
@@ -39,8 +39,8 @@ def row_style(column: str, text: str) -> RowStyle:
|
||||
async def recurrence_get_url(
|
||||
url: str,
|
||||
data_list: list[tuple[str, str]],
|
||||
ignore_list: list[str] = [],
|
||||
api_url: str = None,
|
||||
ignore_list: list[str] | None = None,
|
||||
api_url: str | None = None,
|
||||
):
|
||||
"""递归获取目录下所有文件
|
||||
|
||||
@@ -51,14 +51,15 @@ async def recurrence_get_url(
|
||||
异常:
|
||||
ValueError: 访问错误
|
||||
"""
|
||||
if ignore_list is None:
|
||||
ignore_list = []
|
||||
logger.debug(f"访问插件下载信息 URL: {url}", "插件管理")
|
||||
res = await AsyncHttpx.get(url)
|
||||
if res.status_code != 200:
|
||||
raise ValueError(f"访问错误, code: {res.status_code}")
|
||||
json_data = res.json()
|
||||
if isinstance(json_data, list):
|
||||
for v in json_data:
|
||||
data_list.append((v.get("download_url"), v["path"]))
|
||||
data_list.extend((v.get("download_url"), v["path"]) for v in json_data)
|
||||
else:
|
||||
data_list.append((json_data.get("download_url"), json_data["path"]))
|
||||
for download_url, path in data_list:
|
||||
@@ -69,7 +70,7 @@ async def recurrence_get_url(
|
||||
await recurrence_get_url(_url, data_list, ignore_list, api_url)
|
||||
|
||||
|
||||
async def download_file(url: str, _is: bool = False, api_url: str = None):
|
||||
async def download_file(url: str, _is: bool = False, api_url: str | None = None):
|
||||
"""下载文件
|
||||
|
||||
参数:
|
||||
@@ -88,14 +89,13 @@ async def download_file(url: str, _is: bool = False, api_url: str = None):
|
||||
base_path = "zhenxun/plugins/" if _is else "zhenxun/"
|
||||
file = Path(f"{base_path}{path}")
|
||||
file.parent.mkdir(parents=True, exist_ok=True)
|
||||
print(download_url)
|
||||
r = await AsyncHttpx.get(download_url)
|
||||
if r.status_code != 200:
|
||||
raise ValueError(f"文件下载错误, code: {r.status_code}")
|
||||
content = r.text.replace("\r\n", "\n") # 统一换行符为 UNIX 风格
|
||||
with open(file, "w", encoding="utf8") as f:
|
||||
async with aiofiles.open(file, "w", encoding="utf8") as f:
|
||||
logger.debug(f"写入文件: {file}", "插件管理")
|
||||
f.write(content)
|
||||
await f.write(content)
|
||||
|
||||
|
||||
def install_requirement(plugin_path: Path):
|
||||
@@ -116,22 +116,23 @@ def install_requirement(plugin_path: Path):
|
||||
result = subprocess.run(
|
||||
["pip", "install", "-r", str(existing_requirements)],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
logger.debug(
|
||||
f"Successfully installed dependencies for plugin: {plugin_path.name}. Output:\n{result.stdout}",
|
||||
"Successfully installed dependencies for"
|
||||
f" plugin: {plugin_path.name}. Output:\n{result.stdout}",
|
||||
"插件管理",
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
except subprocess.CalledProcessError:
|
||||
logger.error(
|
||||
f"Failed to install dependencies for plugin: {plugin_path.name}. Error:\n{e.stderr}"
|
||||
f"Failed to install dependencies for plugin: {plugin_path.name}. "
|
||||
" Error:\n{e.stderr}"
|
||||
)
|
||||
|
||||
|
||||
class ShopManage:
|
||||
type2name = {
|
||||
type2name = { # noqa: RUF012
|
||||
"NORMAL": "普通插件",
|
||||
"ADMIN": "管理员插件",
|
||||
"SUPERUSER": "超级用户插件",
|
||||
@@ -169,8 +170,7 @@ class ShopManage:
|
||||
@classmethod
|
||||
def version_check(cls, plugin_info: dict, suc_plugin: dict[str, str]):
|
||||
module = plugin_info["module"]
|
||||
if module in suc_plugin:
|
||||
if plugin_info["version"] != suc_plugin[module]:
|
||||
if module in suc_plugin and plugin_info["version"] != suc_plugin[module]:
|
||||
return f"{suc_plugin[module]} (有更新->{plugin_info['version']})"
|
||||
return plugin_info["version"]
|
||||
|
||||
@@ -226,7 +226,7 @@ class ShopManage:
|
||||
]
|
||||
return await ImageTemplate.table_page(
|
||||
"插件列表",
|
||||
f"通过安装/卸载插件 ID 来管理插件",
|
||||
"通过安装/卸载插件 ID 来管理插件",
|
||||
column_name,
|
||||
data_list,
|
||||
text_style=row_style,
|
||||
@@ -254,7 +254,9 @@ class ShopManage:
|
||||
logger.debug(f"尝试下载插件 URL: {url_path}", "插件管理")
|
||||
github_url = plugin_info.get("github_url")
|
||||
if github_url:
|
||||
github_path = re.search(r"github\.com/([^/]+/[^/]+)", github_url).group(1)
|
||||
if not (r := re.search(r"github\.com/([^/]+/[^/]+)", github_url)):
|
||||
return "github地址格式错误"
|
||||
github_path = r[1]
|
||||
api_url = f"https://api.github.com/repos/{github_path}/contents/"
|
||||
download_url = f"{api_url}{url_path}?ref=main"
|
||||
else:
|
||||
@@ -265,28 +267,27 @@ class ShopManage:
|
||||
|
||||
# 安装依赖
|
||||
plugin_path = BASE_PATH / "/".join(module_path_split)
|
||||
if url_path and github_url:
|
||||
if url_path and github_url and api_url:
|
||||
plugin_path = BASE_PATH / "plugins" / "/".join(module_path_split)
|
||||
res = await AsyncHttpx.get(api_url)
|
||||
if res.status_code != 200:
|
||||
return f"访问错误, code: {res.status_code}"
|
||||
json_data = res.json()
|
||||
requirement_file = next(
|
||||
if requirement_file := next(
|
||||
(
|
||||
v
|
||||
for v in json_data
|
||||
if v["name"] in ["requirements.txt", "requirement.txt"]
|
||||
),
|
||||
None,
|
||||
)
|
||||
if requirement_file:
|
||||
):
|
||||
r = await AsyncHttpx.get(requirement_file.get("download_url"))
|
||||
if r.status_code != 200:
|
||||
raise ValueError(f"文件下载错误, code: {r.status_code}")
|
||||
requirement_path = plugin_path / requirement_file["name"]
|
||||
with open(requirement_path, "w", encoding="utf8") as f:
|
||||
async with aiofiles.open(requirement_path, "w", encoding="utf8") as f:
|
||||
logger.debug(f"写入文件: {requirement_path}", "插件管理")
|
||||
f.write(r.text)
|
||||
await f.write(r.text)
|
||||
|
||||
install_requirement(plugin_path)
|
||||
|
||||
@@ -308,8 +309,7 @@ class ShopManage:
|
||||
plugin_key = list(data.keys())[plugin_id]
|
||||
plugin_info = data[plugin_key]
|
||||
path = BASE_PATH
|
||||
github_url = plugin_info.get("github_url")
|
||||
if github_url:
|
||||
if plugin_info.get("github_url"):
|
||||
path = BASE_PATH / "plugins"
|
||||
for p in plugin_info["module_path"].split("."):
|
||||
path = path / p
|
||||
@@ -335,7 +335,6 @@ class ShopManage:
|
||||
BuildImage | str: 返回消息
|
||||
"""
|
||||
data: dict = await cls.__get_data()
|
||||
column_name = ["-", "ID", "名称", "简介", "作者", "版本", "类型"]
|
||||
for k in data.copy():
|
||||
if data[k]["plugin_type"]:
|
||||
data[k]["plugin_type"] = cls.type2name[data[k]["plugin_type"]]
|
||||
@@ -364,9 +363,10 @@ class ShopManage:
|
||||
]
|
||||
if not data_list:
|
||||
return "未找到相关插件..."
|
||||
column_name = ["-", "ID", "名称", "简介", "作者", "版本", "类型"]
|
||||
return await ImageTemplate.table_page(
|
||||
"插件列表",
|
||||
f"通过添加/移除插件 ID 来管理插件",
|
||||
"通过添加/移除插件 ID 来管理插件",
|
||||
column_name,
|
||||
data_list,
|
||||
text_style=row_style,
|
||||
@@ -394,7 +394,9 @@ class ShopManage:
|
||||
logger.debug(f"尝试下载插件 URL: {url_path}", "插件管理")
|
||||
github_url = plugin_info.get("github_url")
|
||||
if github_url:
|
||||
github_path = re.search(r"github\.com/([^/]+/[^/]+)", github_url).group(1)
|
||||
if not (r := re.search(r"github\.com/([^/]+/[^/]+)", github_url)):
|
||||
return "github地址格式错误..."
|
||||
github_path = r[1]
|
||||
api_url = f"https://api.github.com/repos/{github_path}/contents/"
|
||||
download_url = f"{api_url}{url_path}?ref=main"
|
||||
else:
|
||||
@@ -405,28 +407,27 @@ class ShopManage:
|
||||
|
||||
# 安装依赖
|
||||
plugin_path = BASE_PATH / "/".join(module_path_split)
|
||||
if url_path and github_url:
|
||||
if url_path and github_url and api_url:
|
||||
plugin_path = BASE_PATH / "plugins" / "/".join(module_path_split)
|
||||
res = await AsyncHttpx.get(api_url)
|
||||
if res.status_code != 200:
|
||||
return f"访问错误, code: {res.status_code}"
|
||||
json_data = res.json()
|
||||
requirement_file = next(
|
||||
if requirement_file := next(
|
||||
(
|
||||
v
|
||||
for v in json_data
|
||||
if v["name"] in ["requirements.txt", "requirement.txt"]
|
||||
),
|
||||
None,
|
||||
)
|
||||
if requirement_file:
|
||||
):
|
||||
r = await AsyncHttpx.get(requirement_file.get("download_url"))
|
||||
if r.status_code != 200:
|
||||
raise ValueError(f"文件下载错误, code: {r.status_code}")
|
||||
requirement_path = plugin_path / requirement_file["name"]
|
||||
with open(requirement_path, "w", encoding="utf8") as f:
|
||||
async with aiofiles.open(requirement_path, "w", encoding="utf8") as f:
|
||||
logger.debug(f"写入文件: {requirement_path}", "插件管理")
|
||||
f.write(r.text)
|
||||
await f.write(r.text)
|
||||
|
||||
install_requirement(plugin_path)
|
||||
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import nonebot
|
||||
from nonebot import on_message, on_request
|
||||
from nonebot.adapters.onebot.v11 import ActionFailed
|
||||
from nonebot.adapters.onebot.v11 import Bot as v11Bot
|
||||
from nonebot.adapters.onebot.v11 import FriendRequestEvent, GroupRequestEvent
|
||||
from nonebot.adapters.onebot.v12 import Bot as v12Bot
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
from nonebot import on_message, on_request
|
||||
from nonebot_plugin_session import EventSession
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
from nonebot.adapters.onebot.v11 import Bot as v11Bot
|
||||
from nonebot.adapters.onebot.v12 import Bot as v12Bot
|
||||
from nonebot.adapters.onebot.v11 import (
|
||||
ActionFailed,
|
||||
GroupRequestEvent,
|
||||
FriendRequestEvent,
|
||||
)
|
||||
|
||||
from zhenxun.configs.config import BotConfig, Config
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.models.fg_request import FgRequest
|
||||
from zhenxun.models.friend_user import FriendUser
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType, RequestHandleType, RequestType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.models.fg_request import FgRequest
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
from zhenxun.models.friend_user import FriendUser
|
||||
from zhenxun.configs.config import Config, BotConfig
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.configs.utils import RegisterConfig, PluginExtraData
|
||||
from zhenxun.utils.enum import PluginType, RequestType, RequestHandleType
|
||||
|
||||
base_config = Config.get("invite_manager")
|
||||
|
||||
@@ -46,13 +48,11 @@ __plugin_meta__ = PluginMetadata(
|
||||
|
||||
|
||||
class Timer:
|
||||
data: dict[str, float] = {}
|
||||
data: dict[str, float] = {} # noqa: RUF012
|
||||
|
||||
@classmethod
|
||||
def check(cls, uid: int | str):
|
||||
if uid not in cls.data:
|
||||
return True
|
||||
return time.time() - cls.data[uid] > 5 * 60
|
||||
return True if uid not in cls.data else time.time() - cls.data[uid] > 5 * 60
|
||||
|
||||
@classmethod
|
||||
def clear(cls):
|
||||
@@ -71,7 +71,7 @@ _t = on_message(priority=999, block=False, rule=lambda: False)
|
||||
async def _(bot: v12Bot | v11Bot, event: FriendRequestEvent, session: EventSession):
|
||||
superuser = BotConfig.get_superuser("qq")
|
||||
if event.user_id and Timer.check(event.user_id):
|
||||
logger.debug(f"收录好友请求...", "好友请求", target=event.user_id)
|
||||
logger.debug("收录好友请求...", "好友请求", target=event.user_id)
|
||||
user = await bot.get_stranger_info(user_id=event.user_id)
|
||||
nickname = user["nickname"]
|
||||
# sex = user["sex"]
|
||||
@@ -87,7 +87,7 @@ async def _(bot: v12Bot | v11Bot, event: FriendRequestEvent, session: EventSessi
|
||||
).send(target=PlatformUtils.get_target(bot, superuser))
|
||||
if base_config.get("AUTO_ADD_FRIEND"):
|
||||
logger.debug(
|
||||
f"已开启好友请求自动同意,成功通过该请求",
|
||||
"已开启好友请求自动同意,成功通过该请求",
|
||||
"好友请求",
|
||||
target=event.user_id,
|
||||
)
|
||||
@@ -112,22 +112,33 @@ async def _(bot: v12Bot | v11Bot, event: FriendRequestEvent, session: EventSessi
|
||||
comment=comment,
|
||||
)
|
||||
else:
|
||||
logger.debug(f"好友请求五分钟内重复, 已忽略", "好友请求", target=event.user_id)
|
||||
logger.debug("好友请求五分钟内重复, 已忽略", "好友请求", target=event.user_id)
|
||||
|
||||
|
||||
@group_req.handle()
|
||||
async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSession):
|
||||
superuser = BotConfig.get_superuser("qq")
|
||||
# 邀请
|
||||
if event.sub_type == "invite":
|
||||
if str(event.user_id) in bot.config.superusers:
|
||||
try:
|
||||
logger.debug(
|
||||
f"超级用户自动同意加入群聊",
|
||||
"超级用户自动同意加入群聊",
|
||||
"群聊请求",
|
||||
session=event.user_id,
|
||||
target=event.group_id,
|
||||
)
|
||||
group, _ = await GroupConsole.update_or_create(
|
||||
group_id=str(event.group_id),
|
||||
defaults={
|
||||
"group_name": "",
|
||||
"max_member_count": 0,
|
||||
"member_count": 0,
|
||||
"group_flag": 1,
|
||||
},
|
||||
)
|
||||
await bot.set_group_add_request(
|
||||
flag=event.flag, sub_type="invite", approve=True
|
||||
)
|
||||
if isinstance(bot, v11Bot):
|
||||
group_info = await bot.get_group_info(group_id=event.group_id)
|
||||
max_member_count = group_info["max_member_count"]
|
||||
@@ -136,17 +147,11 @@ async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSessio
|
||||
group_info = await bot.get_group_info(group_id=str(event.group_id))
|
||||
max_member_count = 0
|
||||
member_count = 0
|
||||
await GroupConsole.update_or_create(
|
||||
group_id=str(event.group_id),
|
||||
defaults={
|
||||
"group_name": group_info["group_name"],
|
||||
"max_member_count": max_member_count,
|
||||
"member_count": member_count,
|
||||
"group_flag": 1,
|
||||
},
|
||||
)
|
||||
await bot.set_group_add_request(
|
||||
flag=event.flag, sub_type="invite", approve=True
|
||||
group.max_member_count = max_member_count
|
||||
group.member_count = member_count
|
||||
group.group_name = group_info["group_name"]
|
||||
await group.save(
|
||||
update_fields=["group_name", "max_member_count", "member_count"]
|
||||
)
|
||||
except ActionFailed as e:
|
||||
logger.error(
|
||||
@@ -156,8 +161,7 @@ async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSessio
|
||||
target=event.group_id,
|
||||
e=e,
|
||||
)
|
||||
else:
|
||||
if Timer.check(f"{event.user_id}:{event.group_id}"):
|
||||
elif Timer.check(f"{event.user_id}:{event.group_id}"):
|
||||
logger.debug(
|
||||
f"收录 用户[{event.user_id}] 群聊[{event.group_id}] 群聊请求",
|
||||
"群聊请求",
|
||||
@@ -166,7 +170,8 @@ async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSessio
|
||||
nickname = await FriendUser.get_user_name(str(event.user_id))
|
||||
await PlatformUtils.send_superuser(
|
||||
bot,
|
||||
f"*****一份入群申请*****\n申请人:{nickname}({event.user_id})\n群聊:{event.group_id}\n邀请日期:{datetime.now().replace(microsecond=0)}",
|
||||
f"*****一份入群申请*****\n申请人:{nickname}({event.user_id})\n群聊:"
|
||||
f"{event.group_id}\n邀请日期:{datetime.now().replace(microsecond=0)}",
|
||||
superuser,
|
||||
)
|
||||
await bot.send_private_msg(
|
||||
@@ -193,7 +198,7 @@ async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSessio
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"群聊请求五分钟内重复, 已忽略",
|
||||
"群聊请求五分钟内重复, 已忽略",
|
||||
"群聊请求",
|
||||
target=f"{event.user_id}:{event.group_id}",
|
||||
)
|
||||
@@ -205,3 +210,7 @@ async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSessio
|
||||
)
|
||||
async def _():
|
||||
Timer.clear()
|
||||
|
||||
|
||||
async def _():
|
||||
Timer.clear()
|
||||
|
||||
@@ -3,8 +3,8 @@ from pathlib import Path
|
||||
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.configs.config import Config
|
||||
|
||||
Config.add_plugin_config(
|
||||
"_backup",
|
||||
@@ -18,14 +18,7 @@ Config.add_plugin_config(
|
||||
Config.add_plugin_config(
|
||||
"_backup",
|
||||
"BACKUP_DIR_OR_FILE",
|
||||
[
|
||||
"data/black_word",
|
||||
"data/configs",
|
||||
"data/statistics",
|
||||
"data/word_bank",
|
||||
"data/manager",
|
||||
"configs",
|
||||
],
|
||||
["data"],
|
||||
help="备份的文件夹或文件",
|
||||
default_value=[],
|
||||
type=list[str],
|
||||
@@ -39,7 +32,8 @@ Config.add_plugin_config(
|
||||
minute=25,
|
||||
)
|
||||
async def _():
|
||||
if Config.get_config("_backup", "BACKUP_FLAG"):
|
||||
if not Config.get_config("_backup", "BACKUP_FLAG"):
|
||||
return
|
||||
_backup_path = Path() / "backup"
|
||||
_backup_path.mkdir(exist_ok=True, parents=True)
|
||||
if backup_dir_or_file := Config.get_config("_backup", "BACKUP_DIR_OR_FILE"):
|
||||
|
||||
@@ -2,14 +2,15 @@ import nonebot
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
from zhenxun.configs.utils import PluginExtraData, Task
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
from zhenxun.utils.platform import broadcast_group
|
||||
from zhenxun.configs.utils import Task, PluginExtraData
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="早晚安被动技能",
|
||||
@@ -37,7 +38,7 @@ async def _():
|
||||
|
||||
|
||||
async def check(group_id: str) -> bool:
|
||||
return not await TaskInfo.is_block("morning_goodnight", group_id)
|
||||
return not await CommonUtils.is_block("morning_goodnight", group_id)
|
||||
|
||||
|
||||
# 早上好
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import nonebot_plugin_alconna as alc
|
||||
from nonebot.adapters import Bot
|
||||
import nonebot_plugin_alconna as alc
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
# from nonebot.adapters.discord import Bot as DiscordBot
|
||||
# from nonebot.adapters.dodo import Bot as DodoBot
|
||||
@@ -7,12 +8,11 @@ from nonebot.adapters import Bot
|
||||
# from nonebot.adapters.onebot.v11 import Bot as v11Bot
|
||||
# from nonebot.adapters.onebot.v12 import Bot as v12Bot
|
||||
from nonebot_plugin_alconna import Image, UniMsg
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
|
||||
|
||||
class BroadcastManage:
|
||||
@@ -42,7 +42,7 @@ class BroadcastManage:
|
||||
error_count = 0
|
||||
for group in group_list:
|
||||
try:
|
||||
if not await TaskInfo.is_block(
|
||||
if not await CommonUtils.is_block(
|
||||
group.group_id,
|
||||
"broadcast", # group.channel_id
|
||||
):
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
import contextlib
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
import psutil
|
||||
import ujson as json
|
||||
from fastapi import Depends, HTTPException
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
from nonebot.utils import run_sync
|
||||
from fastapi import Depends, HTTPException
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.path_config import DATA_PATH
|
||||
|
||||
from .base_model import SystemFolderSize, SystemStatus, User
|
||||
from .base_model import User, SystemStatus, SystemFolderSize
|
||||
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||
@@ -24,10 +24,8 @@ token_file = DATA_PATH / "web_ui" / "token.json"
|
||||
token_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
token_data = {"token": []}
|
||||
if token_file.exists():
|
||||
try:
|
||||
token_data = json.load(open(token_file, "r", encoding="utf8"))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
with contextlib.suppress(json.JSONDecodeError):
|
||||
token_data = json.load(open(token_file, encoding="utf8"))
|
||||
|
||||
|
||||
def get_user(uname: str) -> User | None:
|
||||
@@ -52,7 +50,7 @@ def create_token(user: User, expires_delta: timedelta | None = None):
|
||||
user: 用户信息
|
||||
expires_delta: 过期时间.
|
||||
"""
|
||||
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
|
||||
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=15))
|
||||
return jwt.encode(
|
||||
claims={"sub": user.username, "exp": expire},
|
||||
key=Config.get_config("web-ui", "secret"),
|
||||
@@ -71,8 +69,10 @@ def authentication():
|
||||
# if token not in token_data["token"]:
|
||||
def inner(token: str = Depends(oauth2_scheme)):
|
||||
try:
|
||||
payload = jwt.decode(token, Config.get_config("web-ui", "secret"), algorithms=[ALGORITHM])
|
||||
username, expire = payload.get("sub"), payload.get("exp")
|
||||
payload = jwt.decode(
|
||||
token, Config.get_config("web-ui", "secret"), algorithms=[ALGORITHM]
|
||||
)
|
||||
username, _ = payload.get("sub"), payload.get("exp")
|
||||
user = get_user(username) # type: ignore
|
||||
if user is None:
|
||||
raise JWTError
|
||||
@@ -90,10 +90,10 @@ def _get_dir_size(dir_path: Path) -> float:
|
||||
参数:
|
||||
dir_path: 文件夹路径
|
||||
"""
|
||||
size = 0
|
||||
for root, dirs, files in os.walk(dir_path):
|
||||
size += sum([os.path.getsize(os.path.join(root, name)) for name in files])
|
||||
return size
|
||||
return sum(
|
||||
sum(os.path.getsize(os.path.join(root, name)) for name in files)
|
||||
for root, dirs, files in os.walk(dir_path)
|
||||
)
|
||||
|
||||
|
||||
@run_sync
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import copy
|
||||
from typing import Any
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, Set, Type
|
||||
from collections.abc import Callable
|
||||
|
||||
import cattrs
|
||||
from pydantic import BaseModel
|
||||
from ruamel.yaml import YAML
|
||||
from pydantic import BaseModel
|
||||
from ruamel.yaml.scanner import ScannerError
|
||||
|
||||
from zhenxun.configs.path_config import DATA_PATH
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import BlockType, LimitWatchType, PluginLimitType, PluginType
|
||||
from zhenxun.configs.path_config import DATA_PATH
|
||||
from zhenxun.utils.enum import BlockType, PluginType, LimitWatchType, PluginLimitType
|
||||
|
||||
_yaml = YAML(pure=True)
|
||||
_yaml.indent = 2
|
||||
@@ -63,7 +64,7 @@ class ConfigGroup(BaseModel):
|
||||
"""模块名"""
|
||||
name: str | None = None
|
||||
"""插件名"""
|
||||
configs: Dict[str, ConfigModel] = {}
|
||||
configs: dict[str, ConfigModel] = {}
|
||||
"""配置项列表"""
|
||||
|
||||
def get(self, c: str, default: Any = None) -> Any:
|
||||
@@ -139,6 +140,8 @@ class Task(BaseBlock):
|
||||
"""全局开关状态"""
|
||||
create_status: bool = False
|
||||
"""初次加载默认开关状态"""
|
||||
default_status: bool = True
|
||||
"""进群时默认状态"""
|
||||
run_time: str | None = None
|
||||
"""运行时间"""
|
||||
|
||||
@@ -168,7 +171,7 @@ class PluginExtraData(BaseModel):
|
||||
"""技能被动"""
|
||||
superuser_help: str | None = None
|
||||
"""超级用户帮助"""
|
||||
aliases: Set[str] = set()
|
||||
aliases: set[str] = set()
|
||||
"""额外名称"""
|
||||
sql_list: list[str] | None = None
|
||||
"""常用sql"""
|
||||
@@ -184,7 +187,7 @@ class ConfigsManager:
|
||||
"""
|
||||
|
||||
def __init__(self, file: Path):
|
||||
self._data: Dict[str, ConfigGroup] = {}
|
||||
self._data: dict[str, ConfigGroup] = {}
|
||||
self._simple_data: dict = {}
|
||||
self._simple_file = DATA_PATH / "config.yaml"
|
||||
_yaml = YAML()
|
||||
@@ -194,14 +197,14 @@ class ConfigsManager:
|
||||
self.load_data()
|
||||
if self._simple_file.exists():
|
||||
try:
|
||||
with open(self._simple_file, "r", encoding="utf8") as f:
|
||||
with self._simple_file.open(encoding="utf8") as f:
|
||||
self._simple_data = _yaml.load(f)
|
||||
except ScannerError as e:
|
||||
raise ScannerError(
|
||||
f"{e}\n**********************************************\n"
|
||||
f"****** 可能为config.yaml配置文件填写不规范 ******\n"
|
||||
f"**********************************************"
|
||||
)
|
||||
) from e
|
||||
|
||||
def set_name(self, module: str, name: str):
|
||||
"""设置插件配置中文名出
|
||||
@@ -226,7 +229,7 @@ class ConfigsManager:
|
||||
*,
|
||||
help: str | None = None,
|
||||
default_value: Any = None,
|
||||
type: Type | None = None,
|
||||
type: type | None = None,
|
||||
arg_parser: Callable | None = None,
|
||||
_override: bool = False,
|
||||
):
|
||||
@@ -314,30 +317,29 @@ class ConfigsManager:
|
||||
Any: 配置值
|
||||
"""
|
||||
logger.debug(
|
||||
f"尝试获取配置 MODULE: [<u><y>{module}</y></u>] | KEY: [<u><y>{key}</y></u>]"
|
||||
f"尝试获取配置MODULE: [<u><y>{module}</y></u>] | KEY: [<u><y>{key}</y></u>]"
|
||||
)
|
||||
key = key.upper()
|
||||
value = None
|
||||
if module in self._data.keys():
|
||||
config = self._data[module].configs.get(key)
|
||||
if not config:
|
||||
config = self._data[module].configs.get(key)
|
||||
config = self._data[module].configs.get(key) or self._data[
|
||||
module
|
||||
].configs.get(key)
|
||||
if not config:
|
||||
raise NoSuchConfig(
|
||||
f"未查询到配置项 MODULE: [ {module} ] | KEY: [ {key} ]"
|
||||
)
|
||||
try:
|
||||
if config.arg_parser:
|
||||
value = config.arg_parser(value or config.default_value)
|
||||
else:
|
||||
try:
|
||||
if config.value is not None:
|
||||
elif config.value is not None:
|
||||
# try:
|
||||
value = (
|
||||
cattrs.structure(config.value, config.type)
|
||||
if config.type
|
||||
else config.value
|
||||
)
|
||||
else:
|
||||
if config.default_value is not None:
|
||||
elif config.default_value is not None:
|
||||
value = (
|
||||
cattrs.structure(config.default_value, config.type)
|
||||
if config.type
|
||||
@@ -345,14 +347,16 @@ class ConfigsManager:
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"配置项类型转换 MODULE: [<u><y>{module}</y></u>] | KEY: [<u><y>{key}</y></u>]",
|
||||
f"配置项类型转换 MODULE: [<u><y>{module}</y></u>]"
|
||||
" | KEY: [<u><y>{key}</y></u>]",
|
||||
e=e,
|
||||
)
|
||||
value = config.value or config.default_value
|
||||
if value is None:
|
||||
value = default
|
||||
logger.debug(
|
||||
f"获取配置 MODULE: [<u><y>{module}</y></u>] | KEY: [<u><y>{key}</y></u>] -> [<u><c>{value}</c></u>]"
|
||||
f"获取配置 MODULE: [<u><y>{module}</y></u>] | "
|
||||
" KEY: [<u><y>{key}</y></u>] -> [<u><c>{value}</c></u>]"
|
||||
)
|
||||
return value
|
||||
|
||||
@@ -403,7 +407,7 @@ class ConfigsManager:
|
||||
"""重新加载配置文件"""
|
||||
_yaml = YAML()
|
||||
if self._simple_file.exists():
|
||||
with open(self._simple_file, "r", encoding="utf8") as f:
|
||||
with open(self._simple_file, encoding="utf8") as f:
|
||||
self._simple_data = _yaml.load(f)
|
||||
for key in self._simple_data.keys():
|
||||
for k in self._simple_data[key].keys():
|
||||
@@ -416,8 +420,9 @@ class ConfigsManager:
|
||||
异常:
|
||||
ValueError: 配置文件为空!
|
||||
"""
|
||||
if self.file.exists():
|
||||
with open(self.file, "r", encoding="utf8") as f:
|
||||
if not self.file.exists():
|
||||
return
|
||||
with open(self.file, encoding="utf8") as f:
|
||||
temp_data = _yaml.load(f)
|
||||
if not temp_data:
|
||||
self.file.unlink()
|
||||
@@ -431,16 +436,15 @@ class ConfigsManager:
|
||||
for module in temp_data:
|
||||
config_group = ConfigGroup(module=module)
|
||||
for config in temp_data[module]:
|
||||
config_group.configs[config] = ConfigModel(
|
||||
**temp_data[module][config]
|
||||
)
|
||||
config_group.configs[config] = ConfigModel(**temp_data[module][config])
|
||||
count += 1
|
||||
self._data[module] = config_group
|
||||
logger.info(
|
||||
f"加载配置完成,共加载 <u><y>{len(temp_data)}</y></u> 个配置组及对应 <u><y>{count}</y></u> 个配置项"
|
||||
f"加载配置完成,共加载 <u><y>{len(temp_data)}</y></u> 个配置组及对应"
|
||||
" <u><y>{count}</y></u> 个配置项"
|
||||
)
|
||||
|
||||
def get_data(self) -> Dict[str, ConfigGroup]:
|
||||
def get_data(self) -> dict[str, ConfigGroup]:
|
||||
return copy.deepcopy(self._data)
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
|
||||
@@ -33,7 +33,7 @@ class FgRequest(Model):
|
||||
)
|
||||
"""处理类型"""
|
||||
|
||||
class Meta:
|
||||
class Meta: # type: ignore
|
||||
table = "fg_request"
|
||||
table_description = "好友群组请求"
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
from tortoise import fields
|
||||
from typing import Any
|
||||
from typing_extensions import Self
|
||||
|
||||
from tortoise import fields
|
||||
from tortoise.backends.base.client import BaseDBAsyncClient
|
||||
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.services.db_context import Model
|
||||
|
||||
|
||||
@@ -35,11 +39,64 @@ class GroupConsole(Model):
|
||||
platform = fields.CharField(255, default="qq", description="所属平台")
|
||||
"""所属平台"""
|
||||
|
||||
class Meta:
|
||||
class Meta: # type: ignore
|
||||
table = "group_console"
|
||||
table_description = "群组信息表"
|
||||
unique_together = ("group_id", "channel_id")
|
||||
|
||||
@classmethod
|
||||
async def create(
|
||||
cls, using_db: BaseDBAsyncClient | None = None, **kwargs: Any
|
||||
) -> Self:
|
||||
"""覆盖create方法"""
|
||||
group = await super().create(using_db=using_db, **kwargs)
|
||||
if modules := await TaskInfo.filter(default_status=False).values_list(
|
||||
"module", flat=True
|
||||
):
|
||||
group.block_task = ",".join(modules) + "," # type: ignore
|
||||
await group.save(using_db=using_db, update_fields=["block_task"])
|
||||
return group
|
||||
|
||||
@classmethod
|
||||
async def get_or_create(
|
||||
cls,
|
||||
defaults: dict | None = None,
|
||||
using_db: BaseDBAsyncClient | None = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[Self, bool]:
|
||||
"""覆盖get_or_create方法"""
|
||||
group, is_create = await super().get_or_create(
|
||||
defaults=defaults, using_db=using_db, **kwargs
|
||||
)
|
||||
if is_create and (
|
||||
modules := await TaskInfo.filter(default_status=False).values_list(
|
||||
"module", flat=True
|
||||
)
|
||||
):
|
||||
group.block_task = ",".join(modules) + "," # type: ignore
|
||||
await group.save(using_db=using_db, update_fields=["block_task"])
|
||||
return group, is_create
|
||||
|
||||
@classmethod
|
||||
async def update_or_create(
|
||||
cls,
|
||||
defaults: dict | None = None,
|
||||
using_db: BaseDBAsyncClient | None = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[Self, bool]:
|
||||
"""覆盖update_or_create方法"""
|
||||
group, is_create = await super().update_or_create(
|
||||
defaults=defaults, using_db=using_db, **kwargs
|
||||
)
|
||||
if is_create and (
|
||||
modules := await TaskInfo.filter(default_status=False).values_list(
|
||||
"module", flat=True
|
||||
)
|
||||
):
|
||||
group.block_task = ",".join(modules) + "," # type: ignore
|
||||
await group.save(using_db=using_db, update_fields=["block_task"])
|
||||
return group, is_create
|
||||
|
||||
@classmethod
|
||||
async def get_group(
|
||||
cls, group_id: str, channel_id: str | None = None
|
||||
@@ -67,9 +124,7 @@ class GroupConsole(Model):
|
||||
返回:
|
||||
bool: 是否超级用户指定群
|
||||
"""
|
||||
if group := await cls.get_group(group_id):
|
||||
return group.is_super
|
||||
return False
|
||||
return group.is_super if (group := await cls.get_group(group_id)) else False
|
||||
|
||||
@classmethod
|
||||
async def is_super_block_plugin(
|
||||
|
||||
@@ -2,9 +2,6 @@ from tortoise import fields
|
||||
|
||||
from zhenxun.services.db_context import Model
|
||||
|
||||
from .ban_console import BanConsole
|
||||
from .group_console import GroupConsole
|
||||
|
||||
|
||||
class TaskInfo(Model):
|
||||
id = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
@@ -15,41 +12,20 @@ class TaskInfo(Model):
|
||||
"""被动技能名称"""
|
||||
status = fields.BooleanField(default=True, description="全局开关状态")
|
||||
"""全局开关状态"""
|
||||
default_status = fields.BooleanField(default=True, description="进群默认开关状态")
|
||||
"""全局开关状态"""
|
||||
run_time = fields.CharField(255, null=True, description="运行时间")
|
||||
"""运行时间"""
|
||||
run_count = fields.IntField(default=0, description="运行次数")
|
||||
"""运行次数"""
|
||||
|
||||
class Meta:
|
||||
class Meta: # type: ignore
|
||||
table = "task_info"
|
||||
table_description = "被动技能基本信息"
|
||||
|
||||
@classmethod
|
||||
async def is_block(cls, module: str, group_id: str | None) -> bool:
|
||||
"""判断被动技能是否可以发送
|
||||
|
||||
参数:
|
||||
module: 被动技能模块名
|
||||
group_id: 群组id
|
||||
|
||||
返回:
|
||||
bool: 是否可以发送
|
||||
"""
|
||||
if task := await cls.get_or_none(module=module):
|
||||
"""被动全局状态"""
|
||||
if not task.status:
|
||||
return True
|
||||
if group_id:
|
||||
if await GroupConsole.is_block_task(group_id, module):
|
||||
"""群组是否禁用被动"""
|
||||
return True
|
||||
if g := await GroupConsole.get_or_none(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
):
|
||||
"""群组权限是否小于0"""
|
||||
if g.level < 0:
|
||||
return True
|
||||
if await BanConsole.is_ban(None, group_id):
|
||||
"""群组是否被ban"""
|
||||
return True
|
||||
return False
|
||||
async def _run_script(cls):
|
||||
return [
|
||||
"ALTER TABLE task_info ADD default_status boolean DEFAULT true;",
|
||||
# 默认状态
|
||||
]
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
from typing import List
|
||||
|
||||
from nonebot import on_message
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import BotConfig, Config
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.models.friend_user import FriendUser
|
||||
from zhenxun.models.group_member_info import GroupInfoUser
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.depends import UserName
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from .data_source import get_chat_result, hello, no_result
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="AI",
|
||||
description="屑Ai",
|
||||
usage=f"""
|
||||
与{BotConfig.self_nickname}普普通通的对话吧!
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
module="alapi",
|
||||
key="ALAPI_TOKEN",
|
||||
value=None,
|
||||
help="在 https://admin.alapi.cn/user/login 登录后获取token",
|
||||
),
|
||||
RegisterConfig(key="TL_KEY", value=[], help="图灵Key", type=List[str]),
|
||||
RegisterConfig(
|
||||
key="ALAPI_AI_CHECK",
|
||||
value=False,
|
||||
help="是否检测青云客骂娘回复",
|
||||
default_value=False,
|
||||
type=bool,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="TEXT_FILTER",
|
||||
value=["鸡", "口交"],
|
||||
help="文本过滤器,将敏感词更改为*",
|
||||
type=List[str],
|
||||
),
|
||||
],
|
||||
).dict(),
|
||||
)
|
||||
|
||||
|
||||
ai = on_message(rule=to_me(), priority=998)
|
||||
|
||||
|
||||
@ai.handle()
|
||||
async def _(message: UniMsg, session: EventSession, uname: str = UserName()):
|
||||
if not message or message.extract_plain_text() in [
|
||||
"你好啊",
|
||||
"你好",
|
||||
"在吗",
|
||||
"在不在",
|
||||
"您好",
|
||||
"您好啊",
|
||||
"你好",
|
||||
"在",
|
||||
]:
|
||||
await hello().finish()
|
||||
if not session.id1:
|
||||
await Text("用户id不存在...").finish()
|
||||
gid = session.id3 or session.id2
|
||||
if gid:
|
||||
nickname = await GroupInfoUser.get_user_nickname(session.id1, gid)
|
||||
else:
|
||||
nickname = await FriendUser.get_user_nickname(session.id1)
|
||||
if not nickname:
|
||||
nickname = uname
|
||||
result = await get_chat_result(message, session.id1, nickname)
|
||||
logger.info(f"问题:{message} ---- 回答:{result}", "ai", session=session)
|
||||
if result:
|
||||
result = str(result)
|
||||
for t in Config.get_config("ai", "TEXT_FILTER"):
|
||||
result = result.replace(t, "*")
|
||||
await MessageUtils.build_message(result).finish()
|
||||
else:
|
||||
await no_result().finish()
|
||||
@@ -1,241 +0,0 @@
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
|
||||
import ujson as json
|
||||
from nonebot_plugin_alconna import UniMessage, UniMsg
|
||||
|
||||
from zhenxun.configs.config import BotConfig, Config
|
||||
from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from .utils import ai_message_manager
|
||||
|
||||
url = "http://openapi.tuling123.com/openapi/api/v2"
|
||||
|
||||
check_url = "https://v2.alapi.cn/api/censor/text"
|
||||
|
||||
index = 0
|
||||
|
||||
anime_data = json.load(open(DATA_PATH / "anime.json", "r", encoding="utf8"))
|
||||
|
||||
|
||||
async def get_chat_result(
|
||||
message: UniMsg, user_id: str, nickname: str
|
||||
) -> UniMessage | None:
|
||||
"""获取 AI 返回值,顺序: 特殊回复 -> 图灵 -> 青云客
|
||||
|
||||
参数:
|
||||
text: 问题
|
||||
img_url: 图片链接
|
||||
user_id: 用户id
|
||||
nickname: 用户昵称
|
||||
|
||||
返回
|
||||
str: 回答
|
||||
"""
|
||||
global index
|
||||
text = message.extract_plain_text()
|
||||
ai_message_manager.add_message(user_id, text)
|
||||
special_rst = await ai_message_manager.get_result(user_id, nickname)
|
||||
if special_rst:
|
||||
ai_message_manager.add_result(user_id, special_rst)
|
||||
return MessageUtils.build_message(special_rst)
|
||||
if index == 5:
|
||||
index = 0
|
||||
if len(text) < 6 and random.random() < 0.6:
|
||||
keys = anime_data.keys()
|
||||
for key in keys:
|
||||
if text.find(key) != -1:
|
||||
return random.choice(anime_data[key]).replace("你", nickname)
|
||||
rst = await tu_ling(text, "", user_id)
|
||||
if not rst:
|
||||
rst = await xie_ai(text)
|
||||
if not rst:
|
||||
return None
|
||||
if nickname:
|
||||
if len(nickname) < 5:
|
||||
if random.random() < 0.5:
|
||||
nickname = "~".join(nickname) + "~"
|
||||
if random.random() < 0.2:
|
||||
if nickname.find("大人") == -1:
|
||||
nickname += "大~人~"
|
||||
rst = str(rst).replace("小主人", nickname).replace("小朋友", nickname)
|
||||
ai_message_manager.add_result(user_id, rst)
|
||||
for t in Config.get_config("ai", "TEXT_FILTER"):
|
||||
rst = rst.replace(t, "*")
|
||||
return MessageUtils.build_message(rst)
|
||||
|
||||
|
||||
# 图灵接口
|
||||
async def tu_ling(text: str, img_url: str, user_id: str) -> str | None:
|
||||
"""获取图灵接口的回复
|
||||
|
||||
参数:
|
||||
text: 问题
|
||||
img_url: 图片链接
|
||||
user_id: 用户id
|
||||
|
||||
返回
|
||||
str: 图灵回复
|
||||
"""
|
||||
global index
|
||||
TL_KEY = Config.get_config("ai", "TL_KEY")
|
||||
req = None
|
||||
if not TL_KEY:
|
||||
return None
|
||||
try:
|
||||
if text:
|
||||
req = {
|
||||
"perception": {
|
||||
"inputText": {"text": text},
|
||||
"selfInfo": {
|
||||
"location": {
|
||||
"city": "陨石坑",
|
||||
"province": "火星",
|
||||
"street": "第5坑位",
|
||||
}
|
||||
},
|
||||
},
|
||||
"userInfo": {"apiKey": TL_KEY[index], "userId": str(user_id)},
|
||||
}
|
||||
elif img_url:
|
||||
req = {
|
||||
"reqType": 1,
|
||||
"perception": {
|
||||
"inputImage": {"url": img_url},
|
||||
"selfInfo": {
|
||||
"location": {
|
||||
"city": "陨石坑",
|
||||
"province": "火星",
|
||||
"street": "第5坑位",
|
||||
}
|
||||
},
|
||||
},
|
||||
"userInfo": {"apiKey": TL_KEY[index], "userId": str(user_id)},
|
||||
}
|
||||
except IndexError:
|
||||
index = 0
|
||||
return None
|
||||
text = ""
|
||||
response = await AsyncHttpx.post(url, json=req)
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
resp_payload = json.loads(response.text)
|
||||
if int(resp_payload["intent"]["code"]) in [4003]:
|
||||
return None
|
||||
if resp_payload["results"]:
|
||||
for result in resp_payload["results"]:
|
||||
if result["resultType"] == "text":
|
||||
text = result["values"]["text"]
|
||||
if "请求次数超过" in text:
|
||||
text = ""
|
||||
return text
|
||||
|
||||
|
||||
# 屑 AI
|
||||
async def xie_ai(text: str) -> str:
|
||||
"""获取青云客回复
|
||||
|
||||
参数:
|
||||
text: 问题
|
||||
|
||||
返回:
|
||||
str: 青云可回复
|
||||
"""
|
||||
res = await AsyncHttpx.get(
|
||||
f"http://api.qingyunke.com/api.php?key=free&appid=0&msg={text}"
|
||||
)
|
||||
content = ""
|
||||
try:
|
||||
data = json.loads(res.text)
|
||||
if data["result"] == 0:
|
||||
content = data["content"]
|
||||
if "菲菲" in content:
|
||||
content = content.replace("菲菲", BotConfig.self_nickname)
|
||||
if "艳儿" in content:
|
||||
content = content.replace("艳儿", BotConfig.self_nickname)
|
||||
if "公众号" in content:
|
||||
content = ""
|
||||
if "{br}" in content:
|
||||
content = content.replace("{br}", "\n")
|
||||
if "提示" in content:
|
||||
content = content[: content.find("提示")]
|
||||
if "淘宝" in content or "taobao.com" in content:
|
||||
return ""
|
||||
while True:
|
||||
r = re.search("{face:(.*)}", content)
|
||||
if r:
|
||||
id_ = r.group(1)
|
||||
content = content.replace("{" + f"face:{id_}" + "}", "")
|
||||
else:
|
||||
break
|
||||
return (
|
||||
content
|
||||
if not content and not Config.get_config("ai", "ALAPI_AI_CHECK")
|
||||
else await check_text(content)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ai xie_ai 发生错误", e=e)
|
||||
return ""
|
||||
|
||||
|
||||
def hello() -> UniMessage:
|
||||
"""一些打招呼的内容"""
|
||||
result = random.choice(
|
||||
(
|
||||
"哦豁?!",
|
||||
"你好!Ov<",
|
||||
f"库库库,呼唤{BotConfig.self_nickname}做什么呢",
|
||||
"我在呢!",
|
||||
"呼呼,叫俺干嘛",
|
||||
)
|
||||
)
|
||||
img = random.choice(os.listdir(IMAGE_PATH / "zai"))
|
||||
return MessageUtils.build_message([IMAGE_PATH / "zai" / img, result])
|
||||
|
||||
|
||||
def no_result() -> UniMessage:
|
||||
"""
|
||||
没有回答时的回复
|
||||
"""
|
||||
return MessageUtils.build_message(
|
||||
[
|
||||
random.choice(
|
||||
[
|
||||
"你在说啥子?",
|
||||
f"纯洁的{BotConfig.self_nickname}没听懂",
|
||||
"下次再告诉你(下次一定)",
|
||||
"你觉得我听懂了吗?嗯?",
|
||||
"我!不!知!道!",
|
||||
]
|
||||
),
|
||||
IMAGE_PATH
|
||||
/ "noresult"
|
||||
/ random.choice(os.listdir(IMAGE_PATH / "noresult")),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def check_text(text: str) -> str:
|
||||
"""ALAPI文本检测,主要针对青云客API,检测为恶俗文本改为无回复的回答
|
||||
|
||||
参数:
|
||||
text: 回复
|
||||
|
||||
返回:
|
||||
str: 检测文本
|
||||
"""
|
||||
if not Config.get_config("alapi", "ALAPI_TOKEN"):
|
||||
return text
|
||||
params = {"token": Config.get_config("alapi", "ALAPI_TOKEN"), "text": text}
|
||||
try:
|
||||
data = (await AsyncHttpx.get(check_url, timeout=2, params=params)).json()
|
||||
if data["code"] == 200:
|
||||
if data["data"]["conclusion_type"] == 2:
|
||||
return ""
|
||||
except Exception as e:
|
||||
logger.error(f"检测违规文本错误...", e=e)
|
||||
return text
|
||||
@@ -1,153 +0,0 @@
|
||||
import random
|
||||
import time
|
||||
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.models.ban_console import BanConsole
|
||||
|
||||
|
||||
class AiMessageManager:
|
||||
def __init__(self):
|
||||
self._data = {}
|
||||
self._same_message = [
|
||||
"为什么要发一样的话?",
|
||||
"请不要再重复对我说一句话了,不然我就要生气了!",
|
||||
"别再发这句话了,我已经知道了...",
|
||||
"你是只会说这一句话吗?",
|
||||
"[*],你发我也发!",
|
||||
"[uname],[*]",
|
||||
f"救命!有笨蛋一直给{BotConfig.self_nickname}发一样的话!",
|
||||
"这句话你已经给我发了{}次了,再发就生气!",
|
||||
]
|
||||
self._repeat_message = [
|
||||
f"请不要学{BotConfig.self_nickname}说话",
|
||||
f"为什么要一直学{BotConfig.self_nickname}说话?",
|
||||
"你再学!你再学我就生气了!",
|
||||
f"呜呜,你是想欺负{BotConfig.self_nickname}嘛..",
|
||||
"[uname]不要再学我说话了!",
|
||||
"再学我说话,我就把你拉进黑名单(生气",
|
||||
"你再学![uname]是个笨蛋!",
|
||||
"你已经学我说话{}次了!别再学了!",
|
||||
]
|
||||
|
||||
def add_message(self, user_id: str, message: str):
|
||||
"""添加用户消息
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
message: 消息内容
|
||||
"""
|
||||
if message:
|
||||
if self._data.get(user_id) is None:
|
||||
self._data[user_id] = {
|
||||
"time": time.time(),
|
||||
"message": [],
|
||||
"result": [],
|
||||
"repeat_count": 0,
|
||||
}
|
||||
if time.time() - self._data[user_id]["time"] > 60 * 10:
|
||||
self._data[user_id]["message"].clear()
|
||||
self._data[user_id]["time"] = time.time()
|
||||
self._data[user_id]["message"].append(message.strip())
|
||||
|
||||
def add_result(self, user_id: str, message: str):
|
||||
"""添加回复用户的消息
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
message: 回复消息内容
|
||||
"""
|
||||
if message:
|
||||
if self._data.get(user_id) is None:
|
||||
self._data[user_id] = {
|
||||
"time": time.time(),
|
||||
"message": [],
|
||||
"result": [],
|
||||
"repeat_count": 0,
|
||||
}
|
||||
if time.time() - self._data[user_id]["time"] > 60 * 10:
|
||||
self._data[user_id]["result"].clear()
|
||||
self._data[user_id]["repeat_count"] = 0
|
||||
self._data[user_id]["time"] = time.time()
|
||||
self._data[user_id]["result"].append(message.strip())
|
||||
|
||||
async def get_result(self, user_id: str, nickname: str) -> str | None:
|
||||
"""特殊消息特殊回复
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
nickname: 用户昵称
|
||||
|
||||
返回:
|
||||
str | None: 回答
|
||||
"""
|
||||
try:
|
||||
if len(self._data[user_id]["message"]) < 2:
|
||||
return None
|
||||
except KeyError:
|
||||
return None
|
||||
msg = await self._get_user_repeat_message_result(user_id)
|
||||
if not msg:
|
||||
msg = await self._get_user_same_message_result(user_id)
|
||||
if msg:
|
||||
if "[uname]" in msg:
|
||||
msg = msg.replace("[uname]", nickname)
|
||||
if not msg.startswith("生气了!你好烦,闭嘴!") and "[*]" in msg:
|
||||
msg = msg.replace("[*]", self._data[user_id]["message"][-1])
|
||||
return msg
|
||||
|
||||
async def _get_user_same_message_result(self, user_id: str) -> str | None:
|
||||
"""重复消息回复
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
|
||||
返回:
|
||||
str | None: 回答
|
||||
"""
|
||||
msg = self._data[user_id]["message"][-1]
|
||||
cnt = 0
|
||||
_tmp = self._data[user_id]["message"][:-1]
|
||||
_tmp.reverse()
|
||||
for s in _tmp:
|
||||
if s == msg:
|
||||
cnt += 1
|
||||
else:
|
||||
break
|
||||
if cnt > 1:
|
||||
if random.random() < 0.5 and cnt > 3:
|
||||
rand = random.randint(60, 300)
|
||||
await BanConsole.ban(user_id, None, 9, rand, None)
|
||||
self._data[user_id]["message"].clear()
|
||||
return f"生气了!你好烦,闭嘴!给我老实安静{rand}秒"
|
||||
return random.choice(self._same_message).format(cnt)
|
||||
return None
|
||||
|
||||
async def _get_user_repeat_message_result(self, user_id: str) -> str | None:
|
||||
"""复读真寻的消息回复
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
|
||||
返回:
|
||||
str | None: 回答
|
||||
"""
|
||||
msg = self._data[user_id]["message"][-1]
|
||||
if self._data[user_id]["result"]:
|
||||
rst = self._data[user_id]["result"][-1]
|
||||
else:
|
||||
return None
|
||||
if msg == rst:
|
||||
self._data[user_id]["repeat_count"] += 1
|
||||
cnt = self._data[user_id]["repeat_count"]
|
||||
if cnt > 1:
|
||||
if random.random() < 0.5 and cnt > 3:
|
||||
rand = random.randint(60, 300)
|
||||
await BanConsole.ban(user_id, None, 9, rand, None)
|
||||
self._data[user_id]["result"].clear()
|
||||
self._data[user_id]["repeat_count"] = 0
|
||||
return f"生气了!你好烦,闭嘴!给我老实安静{rand}秒"
|
||||
return random.choice(self._repeat_message).format(cnt)
|
||||
return None
|
||||
|
||||
|
||||
ai_message_manager = AiMessageManager()
|
||||
@@ -1,14 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
import nonebot
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
|
||||
Config.add_plugin_config(
|
||||
"alapi",
|
||||
"ALAPI_TOKEN",
|
||||
None,
|
||||
help="在https://admin.alapi.cn/user/login登录后获取token",
|
||||
)
|
||||
|
||||
nonebot.load_plugins(str(Path(__file__).parent.resolve()))
|
||||
@@ -1,29 +0,0 @@
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
|
||||
|
||||
async def get_data(url: str, params: dict | None = None) -> tuple[dict | str, int]:
|
||||
"""获取ALAPI数据
|
||||
|
||||
参数:
|
||||
url: 请求链接
|
||||
params: 参数
|
||||
|
||||
返回:
|
||||
tuple[dict | str, int]: 返回信息
|
||||
"""
|
||||
if not params:
|
||||
params = {}
|
||||
params["token"] = Config.get_config("alapi", "ALAPI_TOKEN")
|
||||
try:
|
||||
data = (await AsyncHttpx.get(url, params=params, timeout=5)).json()
|
||||
if data["code"] == 200:
|
||||
if not data["data"]:
|
||||
return "没有搜索到...", 997
|
||||
return data, 200
|
||||
else:
|
||||
if data["code"] == 101:
|
||||
return "缺失ALAPI TOKEN,请在配置文件中填写!", 999
|
||||
return f'发生了错误...code:{data["code"]}', 999
|
||||
except TimeoutError:
|
||||
return "超时了....", 998
|
||||
@@ -1,57 +0,0 @@
|
||||
from nonebot import on_regex
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ._data_source import get_data
|
||||
|
||||
comments_163_url = "https://v2.alapi.cn/api/comment"
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="网易云热评",
|
||||
description="生了个人,我很抱歉",
|
||||
usage="""
|
||||
到点了,还是防不了下塔
|
||||
指令:
|
||||
网易云热评/到点了/12点了
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
).dict(),
|
||||
)
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna("网易云热评"),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
_matcher.shortcut(
|
||||
"(到点了|12点了)",
|
||||
command="网易云热评",
|
||||
arguments=[],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(session: EventSession, arparma: Arparma):
|
||||
data, code = await get_data(comments_163_url)
|
||||
if code != 200 and isinstance(data, str):
|
||||
await MessageUtils.build_message(data).finish(reply_to=True)
|
||||
data = data["data"] # type: ignore
|
||||
comment = data["comment_content"] # type: ignore
|
||||
song_name = data["title"] # type: ignore
|
||||
await MessageUtils.build_message(f"{comment}\n\t——《{song_name}》").send(
|
||||
reply_to=True
|
||||
)
|
||||
logger.info(
|
||||
f" 发送网易云热评: {comment} \n\t\t————{song_name}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
@@ -1,46 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Args, Arparma, Image, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ._data_source import get_data
|
||||
|
||||
cover_url = "https://v2.alapi.cn/api/bilibili/cover"
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="b封面",
|
||||
description="快捷的b站视频封面获取方式",
|
||||
usage="""
|
||||
b封面 [链接/av/bv/cv/直播id]
|
||||
示例:b封面 av86863038
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier", version="0.1", menu_type="一些工具"
|
||||
).dict(),
|
||||
)
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna("b封面", Args["url", str]),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(session: EventSession, arparma: Arparma, url: str):
|
||||
params = {"c": url}
|
||||
data, code = await get_data(cover_url, params)
|
||||
if code != 200 and isinstance(data, str):
|
||||
await MessageUtils.build_message(data).finish(reply_to=True)
|
||||
data = data["data"] # type: ignore
|
||||
title = data["title"] # type: ignore
|
||||
img = data["cover"] # type: ignore
|
||||
await MessageUtils.build_message([f"title:{title}\n", Image(url=img)]).send(
|
||||
reply_to=True
|
||||
)
|
||||
logger.info(
|
||||
f" 获取b站封面: {title} url:{img}", arparma.header_result, session=session
|
||||
)
|
||||
@@ -1,48 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ._data_source import get_data
|
||||
|
||||
url = "https://v2.alapi.cn/api/soul"
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="鸡汤",
|
||||
description="喏,亲手为你煮的鸡汤",
|
||||
usage="""
|
||||
不喝点什么感觉有点不舒服
|
||||
指令:
|
||||
鸡汤
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
).dict(),
|
||||
)
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna("鸡汤"),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(session: EventSession, arparma: Arparma):
|
||||
try:
|
||||
data, code = await get_data(url)
|
||||
if code != 200 and isinstance(data, str):
|
||||
await MessageUtils.build_message(data).finish(reply_to=True)
|
||||
await MessageUtils.build_message(data["data"]["content"]).send(reply_to=True) # type: ignore
|
||||
logger.info(
|
||||
f" 发送鸡汤:" + data["data"]["content"], # type:ignore
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
except Exception as e:
|
||||
await MessageUtils.build_message("鸡汤煮坏掉了...").send()
|
||||
logger.error(f"鸡汤煮坏掉了", e=e)
|
||||
@@ -1,57 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ._data_source import get_data
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="古诗",
|
||||
description="为什么突然文艺起来了!",
|
||||
usage="""
|
||||
平白无故念首诗
|
||||
示例:念诗/来首诗/念首诗
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
).dict(),
|
||||
)
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna("念诗"),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
_matcher.shortcut(
|
||||
"(来首诗|念首诗)",
|
||||
command="念诗",
|
||||
arguments=[],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
|
||||
poetry_url = "https://v2.alapi.cn/api/shici"
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(session: EventSession, arparma: Arparma):
|
||||
data, code = await get_data(poetry_url)
|
||||
if code != 200 and isinstance(data, str):
|
||||
await MessageUtils.build_message(data).finish(reply_to=True)
|
||||
data = data["data"] # type: ignore
|
||||
content = data["content"] # type: ignore
|
||||
title = data["origin"] # type: ignore
|
||||
author = data["author"] # type: ignore
|
||||
await MessageUtils.build_message(f"{content}\n\t——{author}《{title}》").send(
|
||||
reply_to=True
|
||||
)
|
||||
logger.info(
|
||||
f" 发送古诗: f'{content}\n\t--{author}《{title}》'",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
@@ -1,5 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
import nonebot
|
||||
|
||||
nonebot.load_plugins(str(Path(__file__).parent.resolve()))
|
||||
@@ -1,62 +0,0 @@
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.message import run_preprocessor
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.models.ban_console import BanConsole
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
|
||||
from .utils import black_word_manager
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="敏感词文本监听",
|
||||
description="敏感词文本监听",
|
||||
usage="".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
menu_type="其他",
|
||||
plugin_type=PluginType.DEPENDANT,
|
||||
).dict(),
|
||||
)
|
||||
|
||||
base_config = Config.get("black_word")
|
||||
|
||||
|
||||
# 黑名单词汇检测
|
||||
@run_preprocessor
|
||||
async def _(
|
||||
bot: Bot, message: UniMsg, matcher: Matcher, event: Event, session: EventSession
|
||||
):
|
||||
gid = session.id3 or session.id2
|
||||
if session.id1:
|
||||
if (
|
||||
event.is_tome()
|
||||
and matcher.plugin_name == "black_word"
|
||||
and not await BanConsole.is_ban(session.id1, gid)
|
||||
):
|
||||
msg = message.extract_plain_text()
|
||||
if session.id1 in bot.config.superusers:
|
||||
return logger.debug(
|
||||
f"超级用户跳过黑名单词汇检查 Message: {msg}", target=session.id1
|
||||
)
|
||||
if gid:
|
||||
"""屏蔽群权限-1的群"""
|
||||
group, _ = await GroupConsole.get_or_create(
|
||||
group_id=gid, channel_id__isnull=True
|
||||
)
|
||||
if group.level < 0:
|
||||
return
|
||||
if await BanConsole.is_ban(None, gid):
|
||||
"""屏蔽群被ban的群"""
|
||||
return
|
||||
if await black_word_manager.check(bot, session, msg) and base_config.get(
|
||||
"CONTAIN_BLACK_STOP_PROPAGATION"
|
||||
):
|
||||
matcher.stop_propagation()
|
||||
@@ -1,235 +0,0 @@
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, Option, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from .data_source import set_user_punish, show_black_text_image
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="敏感词检测",
|
||||
description="请注意你的发言!",
|
||||
usage="""
|
||||
惩罚机制: 检测内容提示
|
||||
设置惩罚 [uid] [id] [level]: 设置惩罚内容, 此id需要通过`记录名单 -u:uid`来获取
|
||||
记录名单: 查看检测记录名单
|
||||
记录名单:
|
||||
-u [uid] 指定用户记录名单
|
||||
-g [gid] 指定群组记录名单
|
||||
-d [date] 指定日期
|
||||
-dt ['=', '>', '<'] 大于小于等于指定日期
|
||||
|
||||
示例:
|
||||
设置惩罚 123123123 0 1
|
||||
记录名单 -u 123123123
|
||||
记录名单 -g 333333
|
||||
记录名单 -d 2022-11-11
|
||||
记录名单 -d 2022-11-11 -dt >
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
plugin_type=PluginType.SUPERUSER,
|
||||
menu_type="其他",
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
key="CYCLE_DAYS",
|
||||
value=30,
|
||||
help="黑名单词汇记录周期",
|
||||
default_value=30,
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="TOLERATE_COUNT",
|
||||
value=[5, 1, 1, 1, 1],
|
||||
help="各个级别惩罚的容忍次数, 依次为: 1, 2, 3, 4, 5",
|
||||
default_value=[5, 1, 1, 1, 1],
|
||||
type=List[int],
|
||||
),
|
||||
RegisterConfig(
|
||||
key="AUTO_PUNISH",
|
||||
value=True,
|
||||
help="是否启动自动惩罚机制",
|
||||
default_value=True,
|
||||
type=bool,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="BAN_4_DURATION",
|
||||
value=360,
|
||||
help="Ban时长(分钟),四级惩罚,可以为指定数字或指定列表区间(随机),例如 [30, 360]",
|
||||
default_value=360,
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="BAN_3_DURATION",
|
||||
value=7,
|
||||
help="Ban时长(天),三级惩罚,可以为指定数字或指定列表区间(随机),例如 [7, 30]",
|
||||
default_value=7,
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="WARNING_RESULT",
|
||||
value=f"请注意对{BotConfig.self_nickname}的发言内容",
|
||||
help="口头警告内容",
|
||||
default_value=None,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="AUTO_ADD_PUNISH_LEVEL",
|
||||
value=360,
|
||||
help="自动提级机制,当周期内处罚次数大于某一特定值就提升惩罚等级",
|
||||
default_value=360,
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="ADD_PUNISH_LEVEL_TO_COUNT",
|
||||
value=3,
|
||||
help="在CYCLE_DAYS周期内触发指定惩罚次数后提升惩罚等级",
|
||||
default_value=3,
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="ALAPI_CHECK_FLAG",
|
||||
value=False,
|
||||
help="当未检测到已收录的敏感词时,开启ALAPI文本检测并将疑似文本发送给超级用户",
|
||||
default_value=False,
|
||||
type=bool,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="CONTAIN_BLACK_STOP_PROPAGATION",
|
||||
value=True,
|
||||
help="当文本包含任意敏感词时,停止向下级插件传递,即不触发ai",
|
||||
default_value=True,
|
||||
type=bool,
|
||||
),
|
||||
],
|
||||
).dict(),
|
||||
)
|
||||
|
||||
|
||||
_punish_matcher = on_alconna(
|
||||
Alconna("设置惩罚", Args["uid", str]["id", int]["punish_level", int]),
|
||||
priority=1,
|
||||
permission=SUPERUSER,
|
||||
block=True,
|
||||
)
|
||||
|
||||
|
||||
_show_matcher = on_alconna(
|
||||
Alconna(
|
||||
"记录名单",
|
||||
Option("-u|--uid", Args["uid", str]),
|
||||
Option("-g|--group", Args["gid", str]),
|
||||
Option("-d|--date", Args["date", str]),
|
||||
Option("-dt|--type", Args["date_type", ["=", ">", "<"]], default="="),
|
||||
),
|
||||
priority=1,
|
||||
permission=SUPERUSER,
|
||||
block=True,
|
||||
)
|
||||
|
||||
_show_punish_matcher = on_alconna(
|
||||
Alconna("惩罚机制"), aliases={"敏感词检测"}, priority=1, block=True
|
||||
)
|
||||
|
||||
|
||||
@_show_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot, uid: Match[str], gid: Match[str], date: Match[str], date_type: Match[str]
|
||||
):
|
||||
user_id = None
|
||||
group_id = None
|
||||
date_ = None
|
||||
date_str = None
|
||||
date_type_ = "="
|
||||
if uid.available:
|
||||
user_id = uid.result
|
||||
if gid.available:
|
||||
group_id = gid.result
|
||||
if date.available:
|
||||
date_str = date.result
|
||||
if date_type.available:
|
||||
date_type_ = date_type.result
|
||||
if date_str:
|
||||
try:
|
||||
date_ = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
await MessageUtils.build_message("日期格式错误,需要:年-月-日").finish()
|
||||
result = await show_black_text_image(
|
||||
user_id,
|
||||
group_id,
|
||||
date_,
|
||||
date_type_,
|
||||
)
|
||||
await MessageUtils.build_message(result).send()
|
||||
|
||||
|
||||
@_show_punish_matcher.handle()
|
||||
async def _():
|
||||
text = f"""
|
||||
** 惩罚机制 **
|
||||
|
||||
惩罚前包含容忍机制,在指定周期内会容忍偶尔少次数的敏感词只会进行警告提醒
|
||||
|
||||
多次触发同级惩罚会使惩罚等级提高,即惩罚自动提级机制
|
||||
|
||||
目前公开的惩罚等级:
|
||||
|
||||
1级:永久ban
|
||||
|
||||
2级:删除好友
|
||||
|
||||
3级:ban指定/随机天数
|
||||
|
||||
4级:ban指定/随机时长
|
||||
|
||||
5级:警告
|
||||
|
||||
备注:
|
||||
|
||||
该功能为测试阶段,如果你有被误封情况,请联系管理员,会从数据库中提取出你的数据进行审核后判断
|
||||
|
||||
目前该功能暂不完善,部分情况会由管理员鉴定,请注意对真寻的发言
|
||||
|
||||
关于敏感词:
|
||||
|
||||
记住不要骂{BotConfig.self_nickname}就对了!
|
||||
""".strip()
|
||||
max_width = 0
|
||||
for m in text.split("\n"):
|
||||
max_width = len(m) * 20 if len(m) * 20 > max_width else max_width
|
||||
max_height = len(text.split("\n")) * 24
|
||||
A = BuildImage(
|
||||
max_width, max_height, font="CJGaoDeGuo.otf", font_size=24, color="#E3DBD1"
|
||||
)
|
||||
await A.text((10, 10), text)
|
||||
await MessageUtils.build_message(A).send()
|
||||
|
||||
|
||||
@_punish_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
uid: str,
|
||||
id: int,
|
||||
punish_level: int,
|
||||
):
|
||||
result = await set_user_punish(
|
||||
bot, uid, session.id2 or session.id3, id, punish_level
|
||||
)
|
||||
await MessageUtils.build_message(result).send(reply_to=True)
|
||||
logger.info(
|
||||
f"设置惩罚 uid:{uid} id_:{id} punish_level:{punish_level} --> {result}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
@@ -1,103 +0,0 @@
|
||||
from datetime import datetime
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
from zhenxun.models.friend_user import FriendUser
|
||||
from zhenxun.models.group_member_info import GroupInfoUser
|
||||
from zhenxun.utils.image_utils import BuildImage, ImageTemplate
|
||||
|
||||
from .model import BlackWord
|
||||
from .utils import Config, _get_punish
|
||||
|
||||
|
||||
async def show_black_text_image(
|
||||
user_id: str | None,
|
||||
group_id: str | None,
|
||||
date: datetime | None,
|
||||
data_type: str = "=",
|
||||
) -> BuildImage:
|
||||
"""展示记录名单
|
||||
|
||||
参数:
|
||||
bot: bot
|
||||
user: 用户id
|
||||
group_id: 群组id
|
||||
date: 日期
|
||||
data_type: 日期搜索类型
|
||||
|
||||
返回:
|
||||
BuildImage: 数据图片
|
||||
"""
|
||||
data_list = await BlackWord.get_black_data(user_id, group_id, date, data_type)
|
||||
column_name = [
|
||||
"ID",
|
||||
"昵称",
|
||||
"UID",
|
||||
"GID",
|
||||
"文本",
|
||||
"检测内容",
|
||||
"检测等级",
|
||||
"惩罚",
|
||||
"平台",
|
||||
"记录日期",
|
||||
]
|
||||
column_list = []
|
||||
uid_list = [u for u in data_list]
|
||||
uid2name = {
|
||||
u.user_id: u.user_name for u in await FriendUser.filter(user_id__in=uid_list)
|
||||
}
|
||||
for i, data in enumerate(data_list):
|
||||
uname = uid2name.get(data.user_id)
|
||||
if not uname:
|
||||
if u := await GroupInfoUser.get_or_none(
|
||||
user_id=data.user_id, group_id=data.group_id
|
||||
):
|
||||
uname = u.user_name
|
||||
if len(data.plant_text) > 30:
|
||||
data.plant_text = data.plant_text[:30] + "..."
|
||||
column_list.append(
|
||||
[
|
||||
i,
|
||||
uname or data.user_id,
|
||||
data.user_id,
|
||||
data.group_id,
|
||||
data.plant_text,
|
||||
data.black_word,
|
||||
data.punish_level,
|
||||
data.punish,
|
||||
data.platform,
|
||||
data.create_time,
|
||||
]
|
||||
)
|
||||
A = await ImageTemplate.table_page(
|
||||
"记录名单", "一个都不放过!", column_name, column_list
|
||||
)
|
||||
return A
|
||||
|
||||
|
||||
async def set_user_punish(
|
||||
bot: Bot, user_id: str, group_id: str | None, id_: int, punish_level: int
|
||||
) -> str:
|
||||
"""设置惩罚
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
group_id: 群组id或频道id
|
||||
id_: 记录下标
|
||||
punish_level: 惩罚等级
|
||||
|
||||
返回:
|
||||
str: 结果
|
||||
"""
|
||||
result = await _get_punish(bot, punish_level, user_id, group_id)
|
||||
punish = {
|
||||
1: "永久ban",
|
||||
2: "删除好友",
|
||||
3: f"ban {result} 天",
|
||||
4: f"ban {result} 分钟",
|
||||
5: "口头警告",
|
||||
}
|
||||
if await BlackWord.set_user_punish(user_id, punish[punish_level], id_=id_):
|
||||
return f"已对 USER {user_id} 进行 {punish[punish_level]} 处罚。"
|
||||
else:
|
||||
return "操作失败,可能未找到用户,id或敏感词"
|
||||
@@ -1,154 +0,0 @@
|
||||
from datetime import datetime, timedelta
|
||||
from email.policy import default
|
||||
|
||||
import pytz
|
||||
from tortoise import fields
|
||||
|
||||
from zhenxun.services.db_context import Model
|
||||
|
||||
|
||||
class BlackWord(Model):
|
||||
|
||||
id = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
"""自增id"""
|
||||
user_id = fields.CharField(255)
|
||||
"""用户id"""
|
||||
group_id = fields.CharField(255, null=True)
|
||||
"""群聊id"""
|
||||
plant_text = fields.TextField()
|
||||
"""检测文本"""
|
||||
black_word = fields.TextField()
|
||||
"""黑名单词语"""
|
||||
punish = fields.TextField(default="")
|
||||
"""惩罚内容"""
|
||||
punish_level = fields.IntField()
|
||||
"""惩罚等级"""
|
||||
create_time = fields.DatetimeField(auto_now_add=True)
|
||||
"""创建时间"""
|
||||
platform = fields.CharField(255, null=True)
|
||||
"""平台"""
|
||||
|
||||
class Meta:
|
||||
table = "black_word"
|
||||
table_description = "惩罚机制数据表"
|
||||
|
||||
@classmethod
|
||||
async def set_user_punish(
|
||||
cls,
|
||||
user_id: str,
|
||||
punish: str,
|
||||
black_word: str | None = None,
|
||||
id_: int | None = None,
|
||||
) -> bool:
|
||||
"""设置处罚
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
punish: 处罚
|
||||
black_word: 黑名单词汇
|
||||
id_: 记录下标
|
||||
"""
|
||||
user = None
|
||||
if (not black_word and id_ is None) or not punish:
|
||||
return False
|
||||
if black_word:
|
||||
user = (
|
||||
await cls.filter(user_id=user_id, black_word=black_word, punish="")
|
||||
.order_by("id")
|
||||
.first()
|
||||
)
|
||||
elif id_ is not None:
|
||||
user_list = await cls.filter(user_id=user_id).order_by("id").all()
|
||||
if len(user_list) == 0 or (id_ < 0 or id_ > len(user_list)):
|
||||
return False
|
||||
user = user_list[id_]
|
||||
if not user:
|
||||
return False
|
||||
user.punish = f"{user.punish}{punish} "
|
||||
await user.save(update_fields=["punish"])
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def get_user_count(
|
||||
cls, user_id: str, days: int = 7, punish_level: int | None = None
|
||||
) -> int:
|
||||
"""获取用户规定周期内的犯事次数
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
days: 周期天数
|
||||
punish_level: 惩罚等级
|
||||
"""
|
||||
query = cls.filter(
|
||||
user_id=user_id,
|
||||
create_time__gte=datetime.now() - timedelta(days=days),
|
||||
punish_level__not_in=[-1],
|
||||
)
|
||||
if punish_level is not None:
|
||||
query = query.filter(punish_level=punish_level)
|
||||
return await query.count()
|
||||
|
||||
@classmethod
|
||||
async def get_user_punish_level(cls, user_id: str, days: int = 7) -> int | None:
|
||||
"""获取用户最近一次的惩罚记录等级
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
days: 周期天数
|
||||
"""
|
||||
if (
|
||||
user := await cls.filter(
|
||||
user_id=user_id,
|
||||
create_time__gte=datetime.now() - timedelta(days=days),
|
||||
)
|
||||
.order_by("id")
|
||||
.first()
|
||||
):
|
||||
return user.punish_level
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def get_black_data(
|
||||
cls,
|
||||
user_id: str | None,
|
||||
group_id: str | None,
|
||||
date: datetime | None,
|
||||
date_type: str = "=",
|
||||
) -> list["BlackWord"]:
|
||||
"""通过指定条件查询数据
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
group_id: 群号
|
||||
date: 日期
|
||||
date_type: 日期查询类型
|
||||
"""
|
||||
query = cls
|
||||
if user_id:
|
||||
query = query.filter(user_id=user_id)
|
||||
if group_id:
|
||||
query = query.filter(group_id=group_id)
|
||||
if date:
|
||||
if date_type == "=":
|
||||
query = query.filter(
|
||||
create_time__range=[date, date + timedelta(days=1)]
|
||||
)
|
||||
elif date_type == ">":
|
||||
query = query.filter(create_time__gte=date)
|
||||
elif date_type == "<":
|
||||
query = query.filter(create_time__lte=date)
|
||||
data_list = await query.all().order_by("id")
|
||||
for data in data_list:
|
||||
data.create_time = data.create_time.astimezone(
|
||||
pytz.timezone("Asia/Shanghai")
|
||||
)
|
||||
return data_list # type: ignore
|
||||
|
||||
@classmethod
|
||||
async def _run_script(cls):
|
||||
return [
|
||||
"ALTER TABLE black_word RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id
|
||||
"ALTER TABLE black_word ALTER COLUMN user_id TYPE character varying(255);",
|
||||
"ALTER TABLE black_word ALTER COLUMN group_id TYPE character varying(255);",
|
||||
"ALTER TABLE black_word ADD COLUMN platform character varying(255);",
|
||||
]
|
||||
@@ -1,374 +0,0 @@
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
import ujson as json
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.adapters.onebot.v11 import ActionFailed
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.path_config import DATA_PATH
|
||||
from zhenxun.models.ban_console import BanConsole
|
||||
from zhenxun.models.group_member_info import GroupInfoUser
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
from zhenxun.utils.utils import cn2py
|
||||
|
||||
from .model import BlackWord
|
||||
|
||||
|
||||
class BlackWordManager:
|
||||
"""
|
||||
敏感词管理( 拒绝恶意
|
||||
"""
|
||||
|
||||
def __init__(self, word_file: Path, py_file: Path):
|
||||
self._word_list = {
|
||||
"1": [],
|
||||
"2": [],
|
||||
"3": [],
|
||||
"4": ["sb", "nmsl", "mdzz", "2b", "jb", "操", "废物", "憨憨", "cnm", "rnm"],
|
||||
"5": [],
|
||||
}
|
||||
self._py_list = {
|
||||
"1": [],
|
||||
"2": [],
|
||||
"3": [],
|
||||
"4": [
|
||||
"shabi",
|
||||
"wocaonima",
|
||||
"sima",
|
||||
"sabi",
|
||||
"zhizhang",
|
||||
"naocan",
|
||||
"caonima",
|
||||
"rinima",
|
||||
"simadongxi",
|
||||
"simawanyi",
|
||||
"hanbi",
|
||||
"hanpi",
|
||||
"laji",
|
||||
"fw",
|
||||
],
|
||||
"5": [],
|
||||
}
|
||||
word_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
if word_file.exists():
|
||||
# 清空默认配置
|
||||
with open(word_file, "r", encoding="utf8") as f:
|
||||
self._word_list = json.load(f)
|
||||
else:
|
||||
with open(word_file, "w", encoding="utf8") as f:
|
||||
json.dump(
|
||||
self._word_list,
|
||||
f,
|
||||
ensure_ascii=False,
|
||||
indent=4,
|
||||
)
|
||||
if py_file.exists():
|
||||
# 清空默认配置
|
||||
with open(py_file, "r", encoding="utf8") as f:
|
||||
self._py_list = json.load(f)
|
||||
else:
|
||||
with open(py_file, "w", encoding="utf8") as f:
|
||||
json.dump(
|
||||
self._py_list,
|
||||
f,
|
||||
ensure_ascii=False,
|
||||
indent=4,
|
||||
)
|
||||
|
||||
async def check(
|
||||
self, bot: Bot, session: EventSession, message: str
|
||||
) -> str | bool | None:
|
||||
"""检查是否包含黑名单词汇
|
||||
|
||||
参数:
|
||||
bot: Bot
|
||||
session: EventSession
|
||||
message: 消息
|
||||
"""
|
||||
logger.debug(
|
||||
f"检查文本是否含有黑名单词汇: {message}", "敏感词检测", session=session
|
||||
)
|
||||
if session.id1:
|
||||
if data := self._check(message):
|
||||
if data[0]:
|
||||
await _add_user_black_word(
|
||||
bot,
|
||||
session.id1,
|
||||
session.id2 or session.id3,
|
||||
data[0],
|
||||
message,
|
||||
int(data[1]),
|
||||
)
|
||||
return True
|
||||
if Config.get_config(
|
||||
"black_word", "ALAPI_CHECK_FLAG"
|
||||
) and not await check_text(message):
|
||||
await send_msg(
|
||||
bot,
|
||||
"",
|
||||
None,
|
||||
f"用户 {session.id1} 群组 {session.id3 or session.id2} ALAPI 疑似检测:{message}",
|
||||
)
|
||||
return False
|
||||
|
||||
def _check(self, message: str) -> tuple[str | None, int]:
|
||||
"""检测文本是否违规
|
||||
|
||||
参数:
|
||||
message: 检测消息
|
||||
"""
|
||||
# 移除空格
|
||||
message = message.replace(" ", "")
|
||||
py_msg = cn2py(message).lower()
|
||||
# 完全匹配
|
||||
for x in [self._word_list, self._py_list]:
|
||||
for level in x:
|
||||
if message in x[level] or py_msg in x[level]:
|
||||
return message if message in x[level] else py_msg, int(level)
|
||||
# 模糊匹配
|
||||
for x in [self._word_list, self._py_list]:
|
||||
for level in x:
|
||||
for m in x[level]:
|
||||
if m in message or m in py_msg:
|
||||
return m, -1
|
||||
return None, 0
|
||||
|
||||
|
||||
async def _add_user_black_word(
|
||||
bot: Bot,
|
||||
user_id: str,
|
||||
group_id: str | None,
|
||||
black_word: str,
|
||||
message: str,
|
||||
punish_level: int,
|
||||
):
|
||||
"""添加敏感词数据
|
||||
|
||||
参数:
|
||||
bot: Bot
|
||||
user_id: 用户id
|
||||
group_id: 群组id或频道id
|
||||
black_word: 触发的黑名单词汇
|
||||
message: 原始文本
|
||||
punish_level: 惩罚等级
|
||||
"""
|
||||
cycle_days = Config.get_config("black_word", "CYCLE_DAYS") or 7
|
||||
user_count = await BlackWord.get_user_count(user_id, cycle_days, punish_level)
|
||||
add_punish_level_to_count = Config.get_config(
|
||||
"black_word", "ADD_PUNISH_LEVEL_TO_COUNT"
|
||||
)
|
||||
# 周期内超过次数直接提升惩罚
|
||||
if (
|
||||
Config.get_config("black_word", "AUTO_ADD_PUNISH_LEVEL")
|
||||
and add_punish_level_to_count
|
||||
):
|
||||
punish_level -= 1
|
||||
await BlackWord.create(
|
||||
user_id=user_id,
|
||||
group_id=group_id,
|
||||
plant_text=message,
|
||||
black_word=black_word,
|
||||
punish_level=punish_level,
|
||||
platform=PlatformUtils.get_platform(bot),
|
||||
)
|
||||
logger.info(
|
||||
f"已将 USER {user_id} GROUP {group_id} 添加至黑名单词汇记录 Black_word:{black_word} Plant_text:{message}"
|
||||
)
|
||||
# 自动惩罚
|
||||
if Config.get_config("black_word", "AUTO_PUNISH") and punish_level != -1:
|
||||
await _punish_handle(bot, user_id, group_id, punish_level, black_word)
|
||||
|
||||
|
||||
async def _punish_handle(
|
||||
bot: Bot,
|
||||
user_id: str,
|
||||
group_id: str | None,
|
||||
punish_level: int,
|
||||
black_word: str,
|
||||
):
|
||||
"""惩罚措施,级别越低惩罚越严
|
||||
|
||||
参数:
|
||||
bot: Bot
|
||||
user_id: 用户id
|
||||
group_id: 群组id或频道id
|
||||
black_word: 触发的黑名单词汇
|
||||
channel_id: 频道id
|
||||
"""
|
||||
logger.info(f"BlackWord USER {user_id} 触发 {punish_level} 级惩罚...")
|
||||
# 周期天数
|
||||
cycle_days = Config.get_config("black_word", "CYCLE_DAYS") or 7
|
||||
# 用户周期内触发punish_level级惩罚的次数
|
||||
user_count = await BlackWord.get_user_count(user_id, cycle_days, punish_level)
|
||||
# 获取最近一次的惩罚等级,将在此基础上增加
|
||||
punish_level = (
|
||||
await BlackWord.get_user_punish_level(user_id, cycle_days) or punish_level
|
||||
)
|
||||
# 容忍次数:List[int]
|
||||
tolerate_count = Config.get_config("black_word", "TOLERATE_COUNT")
|
||||
if not tolerate_count or len(tolerate_count) < 5:
|
||||
tolerate_count = [5, 2, 2, 2, 2]
|
||||
if punish_level == 1 and user_count > tolerate_count[punish_level - 1]:
|
||||
# 永久ban
|
||||
await _get_punish(bot, 1, user_id, group_id)
|
||||
await BlackWord.set_user_punish(user_id, "永久ban 删除好友", black_word)
|
||||
elif punish_level == 2 and user_count > tolerate_count[punish_level - 1]:
|
||||
# 删除好友
|
||||
await _get_punish(bot, 2, user_id, group_id)
|
||||
await BlackWord.set_user_punish(user_id, "删除好友", black_word)
|
||||
elif punish_level == 3 and user_count > tolerate_count[punish_level - 1]:
|
||||
# 永久ban
|
||||
ban_day = await _get_punish(bot, 3, user_id, group_id)
|
||||
await BlackWord.set_user_punish(user_id, f"ban {ban_day} 天", black_word)
|
||||
elif punish_level == 4 and user_count > tolerate_count[punish_level - 1]:
|
||||
# ban指定时长
|
||||
ban_time = await _get_punish(bot, 4, user_id, group_id)
|
||||
await BlackWord.set_user_punish(user_id, f"ban {ban_time} 分钟", black_word)
|
||||
elif punish_level == 5 and user_count > tolerate_count[punish_level - 1]:
|
||||
# 口头警告
|
||||
warning_result = await _get_punish(bot, 5, user_id, group_id)
|
||||
await BlackWord.set_user_punish(
|
||||
user_id, f"口头警告:{warning_result}", black_word
|
||||
)
|
||||
else:
|
||||
await BlackWord.set_user_punish(user_id, f"提示!", black_word)
|
||||
await send_msg(
|
||||
bot,
|
||||
user_id,
|
||||
group_id,
|
||||
f"BlackWordChecker:该条发言已被记录,目前你在{cycle_days}天内的发表{punish_level}级"
|
||||
f"言论记录次数为:{user_count}次,请注意你的发言\n"
|
||||
f"* 如果你不清楚惩罚机制,请发送“惩罚机制” *",
|
||||
)
|
||||
|
||||
|
||||
async def _get_punish(
|
||||
bot: Bot,
|
||||
id_: int,
|
||||
user_id: str,
|
||||
group_id: str | None = None,
|
||||
) -> int | str | None:
|
||||
"""通过id_获取惩罚
|
||||
|
||||
参数:
|
||||
bot: Bot
|
||||
id_: id
|
||||
user_id: 用户id
|
||||
group_id: 群组id或频道id
|
||||
"""
|
||||
# 忽略的群聊
|
||||
# _ignore_group = Config.get_config("black_word", "IGNORE_GROUP")
|
||||
# 处罚 id 4 ban 时间:int,List[int]
|
||||
ban_3_duration = Config.get_config("black_word", "BAN_3_DURATION") or 7
|
||||
# 处罚 id 4 ban 时间:int,List[int]
|
||||
ban_4_duration = Config.get_config("black_word", "BAN_4_DURATION") or 360
|
||||
# 口头警告内容
|
||||
warning_result = Config.get_config("black_word", "WARNING_RESULT")
|
||||
if user := await GroupInfoUser.get_or_none(user_id=user_id, group_id=group_id):
|
||||
uname = user.user_name
|
||||
else:
|
||||
uname = user_id
|
||||
# 永久ban
|
||||
if id_ == 1:
|
||||
if str(user_id) not in bot.config.superusers:
|
||||
await BanConsole.ban(user_id, group_id, 10, -1, None)
|
||||
await send_msg(
|
||||
bot,
|
||||
user_id,
|
||||
group_id,
|
||||
f"BlackWordChecker 永久ban USER {uname}({user_id})",
|
||||
)
|
||||
logger.info(f"BlackWord 永久封禁 USER {user_id}...")
|
||||
# 删除好友(有的话
|
||||
elif id_ == 2:
|
||||
if str(user_id) not in bot.config.superusers:
|
||||
try:
|
||||
await bot.delete_friend(user_id=user_id)
|
||||
await send_msg(
|
||||
bot,
|
||||
user_id,
|
||||
group_id,
|
||||
f"BlackWordChecker 删除好友 USER {uname}({user_id})",
|
||||
)
|
||||
logger.info(f"BlackWord 删除好友 {user_id}...")
|
||||
except ActionFailed:
|
||||
pass
|
||||
# 封禁用户指定时间,默认7天
|
||||
elif id_ == 3:
|
||||
if isinstance(ban_3_duration, list):
|
||||
ban_3_duration = random.randint(ban_3_duration[0], ban_3_duration[1])
|
||||
await BanConsole.ban(user_id, group_id, 9, ban_4_duration * 60 * 24)
|
||||
await send_msg(
|
||||
bot,
|
||||
user_id,
|
||||
group_id,
|
||||
f"BlackWordChecker 对用户 USER {uname}({user_id}) 进行封禁 {ban_3_duration} 天处罚。",
|
||||
)
|
||||
logger.info(f"BlackWord 封禁 USER {uname}({user_id}) {ban_3_duration} 天...")
|
||||
return ban_3_duration
|
||||
# 封禁用户指定时间,默认360分钟
|
||||
elif id_ == 4:
|
||||
if isinstance(ban_4_duration, list):
|
||||
ban_4_duration = random.randint(ban_4_duration[0], ban_4_duration[1])
|
||||
await BanConsole.ban(user_id, group_id, 9, ban_4_duration * 60)
|
||||
await send_msg(
|
||||
bot,
|
||||
user_id,
|
||||
group_id,
|
||||
f"BlackWordChecker 对用户 USER {uname}({user_id}) 进行封禁 {ban_4_duration} 分钟处罚。",
|
||||
)
|
||||
logger.info(f"BlackWord 封禁 USER {uname}({user_id}) {ban_4_duration} 分钟...")
|
||||
return ban_4_duration
|
||||
# 口头警告
|
||||
elif id_ == 5:
|
||||
await PlatformUtils.send_message(bot, user_id, group_id, warning_result)
|
||||
logger.info(f"BlackWord 口头警告 USER {user_id}")
|
||||
return warning_result
|
||||
return None
|
||||
|
||||
|
||||
async def send_msg(bot: Bot, user_id: str, group_id: str | None, message: str):
|
||||
"""发送消息
|
||||
|
||||
参数:
|
||||
bot: Bot
|
||||
user_id: user_id
|
||||
group_id: group_id
|
||||
message: message
|
||||
"""
|
||||
if not user_id:
|
||||
platform = PlatformUtils.get_platform(bot)
|
||||
user_id = bot.config.platform_superusers[platform][0]
|
||||
await PlatformUtils.send_message(bot, user_id, group_id, message)
|
||||
|
||||
|
||||
async def check_text(text: str) -> bool:
|
||||
"""ALAPI文本检测,检测输入违规
|
||||
|
||||
参数:
|
||||
text: 回复
|
||||
"""
|
||||
if not Config.get_config("alapi", "ALAPI_TOKEN"):
|
||||
return True
|
||||
params = {"token": Config.get_config("alapi", "ALAPI_TOKEN"), "text": text}
|
||||
try:
|
||||
data = (
|
||||
await AsyncHttpx.get(
|
||||
"https://v2.alapi.cn/api/censor/text", timeout=4, params=params
|
||||
)
|
||||
).json()
|
||||
if data["code"] == 200:
|
||||
return data["data"]["conclusion_type"] == 2
|
||||
except Exception as e:
|
||||
logger.error(f"检测违规文本错误...", e=e)
|
||||
return True
|
||||
|
||||
|
||||
black_word_manager = BlackWordManager(
|
||||
DATA_PATH / "black_word" / "black_word.json",
|
||||
DATA_PATH / "black_word" / "black_py.json",
|
||||
)
|
||||
@@ -1,78 +0,0 @@
|
||||
from asyncio.exceptions import TimeoutError
|
||||
|
||||
from httpx import ConnectTimeout
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.rules import ensure_private
|
||||
|
||||
from .data_source import get_bt_info
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="磁力搜索",
|
||||
description="bt(磁力搜索)[仅支持私聊,懂的都懂]",
|
||||
usage="""
|
||||
* 拒绝反冲斗士! *
|
||||
指令:
|
||||
bt [关键词] ?[页数]
|
||||
示例: bt 钢铁侠
|
||||
示例: bt 钢铁侠 3
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
key="BT_MAX_NUM",
|
||||
value=10,
|
||||
help="单次BT搜索返回最大消息数量",
|
||||
default_value=10,
|
||||
type=int,
|
||||
),
|
||||
],
|
||||
).dict(),
|
||||
)
|
||||
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna("bt", Args["keyword", str]["page?", int]),
|
||||
rule=ensure_private,
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
keyword: str,
|
||||
page: Match[int],
|
||||
):
|
||||
send_flag = False
|
||||
try:
|
||||
async for title, type_, create_time, file_size, link in get_bt_info(
|
||||
keyword, page.result if page.available else 1
|
||||
):
|
||||
await MessageUtils.build_message(
|
||||
f"标题:{title}\n"
|
||||
f"类型:{type_}\n"
|
||||
f"创建时间:{create_time}\n"
|
||||
f"文件大小:{file_size}\n"
|
||||
f"种子:{link}"
|
||||
).send()
|
||||
send_flag = True
|
||||
except (TimeoutError, ConnectTimeout):
|
||||
await MessageUtils.build_message(f"搜索 {keyword} 超时...").finish()
|
||||
except Exception as e:
|
||||
logger.error(f"bt 错误", arparma.header_result, session=session, e=e)
|
||||
await MessageUtils.build_message(f"bt 其他未知错误..").finish()
|
||||
if not send_flag:
|
||||
await MessageUtils.build_message(f"{keyword} 未搜索到...").send()
|
||||
logger.info(
|
||||
f"BT搜索 {keyword} 第 {page} 页", arparma.header_result, session=session
|
||||
)
|
||||
@@ -1,54 +0,0 @@
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.utils.http_utils import AsyncHttpx, AsyncPlaywright
|
||||
|
||||
url = "http://www.eclzz.ink"
|
||||
|
||||
|
||||
async def get_bt_info(keyword: str, page: int):
|
||||
"""获取资源信息
|
||||
|
||||
参数:
|
||||
keyword: 关键词
|
||||
page: 页数
|
||||
"""
|
||||
global url
|
||||
text = (await AsyncHttpx.get(f"{url}/s/{keyword}_rel_{page}.html", timeout=30)).text
|
||||
if "301 Moved Permanently" in text:
|
||||
async with AsyncPlaywright.new_page() as _page:
|
||||
await _page.goto(url)
|
||||
url = _page.url
|
||||
text = (
|
||||
await AsyncHttpx.get(f"{url}/s/{keyword}_rel_{page}.html", timeout=30)
|
||||
).text
|
||||
if "大约0条结果" in text:
|
||||
return
|
||||
soup = BeautifulSoup(text, "lxml")
|
||||
item_lst = soup.find_all("div", {"class": "search-item"})
|
||||
bt_max_num = Config.get_config("bt", "BT_MAX_NUM") or 10
|
||||
bt_max_num = bt_max_num if bt_max_num < len(item_lst) else len(item_lst)
|
||||
for item in item_lst[:bt_max_num]:
|
||||
divs = item.find_all("div")
|
||||
title = (
|
||||
str(divs[0].find("a").text).replace("<em>", "").replace("</em>", "").strip()
|
||||
)
|
||||
spans = divs[2].find_all("span")
|
||||
type_ = spans[0].text
|
||||
create_time = spans[1].find("b").text
|
||||
file_size = spans[2].find("b").text
|
||||
link = await get_download_link(divs[0].find("a")["href"])
|
||||
yield title, type_, create_time, file_size, link
|
||||
|
||||
|
||||
async def get_download_link(_url: str) -> str | None:
|
||||
"""获取资源下载地址
|
||||
|
||||
参数:
|
||||
_url: 链接
|
||||
"""
|
||||
text = (await AsyncHttpx.get(f"{url}{_url}")).text
|
||||
soup = BeautifulSoup(text, "lxml")
|
||||
if fd := soup.find("a", {"id": "down-url"}):
|
||||
return fd["href"] # type: ignore
|
||||
return None
|
||||
@@ -1,40 +0,0 @@
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me
|
||||
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from .data_source import Check
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="服务器自我检查",
|
||||
description="查看服务器当前状态",
|
||||
usage="""
|
||||
查看服务器当前状态
|
||||
指令:
|
||||
自检
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier", version="0.1", plugin_type=PluginType.SUPERUSER
|
||||
).dict(),
|
||||
)
|
||||
|
||||
|
||||
check = Check()
|
||||
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna("自检"), rule=to_me(), permission=SUPERUSER, block=True, priority=1
|
||||
)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(session: EventSession, arparma: Arparma):
|
||||
image = await check.show()
|
||||
await MessageUtils.build_message(image).send()
|
||||
logger.info("自检", arparma.header_result, session=session)
|
||||
@@ -1,83 +0,0 @@
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import psutil
|
||||
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
|
||||
|
||||
class Check:
|
||||
def __init__(self):
|
||||
self.cpu = None
|
||||
self.memory = None
|
||||
self.disk = None
|
||||
self.user = None
|
||||
self.baidu = 200
|
||||
self.google = 200
|
||||
|
||||
async def check_all(self):
|
||||
await self.check_network()
|
||||
await asyncio.sleep(0.1)
|
||||
self.check_system()
|
||||
self.check_user()
|
||||
|
||||
def check_system(self):
|
||||
self.cpu = psutil.cpu_percent()
|
||||
self.memory = psutil.virtual_memory().percent
|
||||
self.disk = psutil.disk_usage("/").percent
|
||||
|
||||
async def check_network(self):
|
||||
try:
|
||||
await AsyncHttpx.get("https://www.baidu.com/", timeout=5)
|
||||
except Exception as e:
|
||||
logger.warning(f"访问BaiDu失败... {type(e)}: {e}")
|
||||
self.baidu = 404
|
||||
try:
|
||||
await AsyncHttpx.get("https://www.google.com/", timeout=5)
|
||||
except Exception as e:
|
||||
logger.warning(f"访问Google失败... {type(e)}: {e}")
|
||||
self.google = 404
|
||||
|
||||
def check_user(self):
|
||||
result = ""
|
||||
for user in psutil.users():
|
||||
result += f'[{user.name}] {time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(user.started))}\n'
|
||||
self.user = result[:-1]
|
||||
|
||||
async def show(self) -> BuildImage:
|
||||
await self.check_all()
|
||||
font = BuildImage.load_font(font_size=24)
|
||||
result = (
|
||||
f'[Time] {str(datetime.now()).split(".")[0]}\n'
|
||||
f"-----System-----\n"
|
||||
f"[CPU] {self.cpu}%\n"
|
||||
f"[Memory] {self.memory}%\n"
|
||||
f"[Disk] {self.disk}%\n"
|
||||
f"-----Network-----\n"
|
||||
f"[BaiDu] {self.baidu}\n"
|
||||
f"[Google] {self.google}\n"
|
||||
)
|
||||
if self.user:
|
||||
result += "-----User-----\n" + self.user
|
||||
width = 0
|
||||
height = 0
|
||||
for x in result.split("\n"):
|
||||
w, h = BuildImage.get_text_size(x, font)
|
||||
if w > width:
|
||||
width = w
|
||||
height += 30
|
||||
A = BuildImage(width + 50, height + 10, font_size=24)
|
||||
await A.transparent(1)
|
||||
await A.text((10, 10), result)
|
||||
max_width = max(width, height)
|
||||
bk = BuildImage(
|
||||
max_width + 100,
|
||||
max_width + 100,
|
||||
background=IMAGE_PATH / "background" / "check" / "0.jpg",
|
||||
)
|
||||
await bk.paste(A, center_type="center")
|
||||
return bk
|
||||
@@ -1,90 +0,0 @@
|
||||
import time
|
||||
from typing import Tuple
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Args, Arparma, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.path_config import TEMP_PATH
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.withdraw_manage import WithdrawManager
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="coser",
|
||||
description="三次元也不戳,嘿嘿嘿",
|
||||
usage="""
|
||||
?N连cos/coser
|
||||
示例: cos
|
||||
示例: 5连cos (单次请求张数小于9)
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
key="WITHDRAW_COS_MESSAGE",
|
||||
value=(0, 1),
|
||||
help="自动撤回,参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)",
|
||||
default_value=(0, 1),
|
||||
type=Tuple[int, int],
|
||||
),
|
||||
],
|
||||
).dict(),
|
||||
)
|
||||
|
||||
_matcher = on_alconna(Alconna("get-cos", Args["num", int, 1]), priority=5, block=True)
|
||||
|
||||
_matcher.shortcut(
|
||||
r"cos",
|
||||
command="get-cos",
|
||||
arguments=["1"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_matcher.shortcut(
|
||||
r"(?P<num>\d)(张|个|条|连)cos",
|
||||
command="get-cos",
|
||||
arguments=["{num}"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
|
||||
# 纯cos,较慢:https://picture.yinux.workers.dev
|
||||
# 比较杂,有福利姬,较快:https://api.jrsgslb.cn/cos/url.php?return=img
|
||||
url = "https://picture.yinux.workers.dev"
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
num: int,
|
||||
):
|
||||
withdraw_time = Config.get_config("coser", "WITHDRAW_COS_MESSAGE")
|
||||
for _ in range(num):
|
||||
path = TEMP_PATH / f"cos_cc{int(time.time())}.jpeg"
|
||||
try:
|
||||
await AsyncHttpx.download_file(url, path)
|
||||
receipt = await MessageUtils.build_message(path).send()
|
||||
message_id = receipt.msg_ids[0]["message_id"]
|
||||
if message_id and WithdrawManager.check(session, withdraw_time):
|
||||
WithdrawManager.append(
|
||||
bot,
|
||||
message_id,
|
||||
withdraw_time[0],
|
||||
)
|
||||
logger.info(f"发送cos", arparma.header_result, session=session)
|
||||
except Exception as e:
|
||||
await MessageUtils.build_message("你cos给我看!").send()
|
||||
logger.error(
|
||||
f"cos错误",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
e=e,
|
||||
)
|
||||
@@ -1,158 +0,0 @@
|
||||
import nonebot
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import At as alcAt
|
||||
from nonebot_plugin_alconna import Target, Text, UniMsg
|
||||
from nonebot_plugin_session import EventSession
|
||||
from nonebot_plugin_userinfo import EventUserInfo, UserInfo
|
||||
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from ._data_source import DialogueManage
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="联系管理员",
|
||||
description="跨越空间与时间跟管理员对话",
|
||||
usage="""
|
||||
滴滴滴- ?[文本] ?[图片]
|
||||
示例:滴滴滴- 我喜欢你
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
menu_type="联系管理员",
|
||||
superuser_help="""
|
||||
/t: 查看当前存储的消息
|
||||
/t [user_id] [group_id] [文本]: 在group回复指定用户
|
||||
/t [user_id] [文本]: 私聊用户
|
||||
/t -1 [group_id] [文本]: 在group内发送消息
|
||||
/t [id] [文本]: 回复指定id的对话,id在 /t 中获取
|
||||
示例:/t 73747222 32848432 你好啊
|
||||
示例:/t 73747222 你好不好
|
||||
示例:/t -1 32848432 我不太好
|
||||
示例:/t 0 我收到你的话了
|
||||
""".strip(),
|
||||
).dict(),
|
||||
)
|
||||
|
||||
config = nonebot.get_driver().config
|
||||
|
||||
|
||||
_dialogue_matcher = on_command("滴滴滴-", priority=5, block=True)
|
||||
_reply_matcher = on_command("/t", priority=1, permission=SUPERUSER, block=True)
|
||||
|
||||
|
||||
@_dialogue_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot,
|
||||
message: UniMsg,
|
||||
session: EventSession,
|
||||
user_info: UserInfo = EventUserInfo(),
|
||||
):
|
||||
if session.id1:
|
||||
message[0] = Text(str(message[0]).replace("滴滴滴-", "", 1))
|
||||
platform = PlatformUtils.get_platform(bot)
|
||||
superuser_id = None
|
||||
try:
|
||||
if platform:
|
||||
superuser_id = BotConfig.get_superuser(platform)
|
||||
except IndexError:
|
||||
await MessageUtils.build_message("管理员失联啦...").finish()
|
||||
if not superuser_id:
|
||||
await MessageUtils.build_message("管理员失联啦...").finish()
|
||||
uname = user_info.user_displayname or user_info.user_name
|
||||
group_name = ""
|
||||
gid = session.id3 or session.id2
|
||||
if gid:
|
||||
if g := await GroupConsole.get(group_id=gid):
|
||||
group_name = g.group_name
|
||||
logger.info(
|
||||
f"发送消息至{platform}管理员: {message}", "滴滴滴-", session=session
|
||||
)
|
||||
message.insert(0, Text("消息:\n"))
|
||||
if gid:
|
||||
message.insert(0, Text(f"群组: {group_name}({gid})\n"))
|
||||
message.insert(0, Text(f"昵称: {uname}({session.id1})\n"))
|
||||
message.insert(0, Text(f"Id: {DialogueManage._index}\n"))
|
||||
message.insert(0, Text("*****一份交流报告*****\n"))
|
||||
DialogueManage.add(uname, session.id1, gid, group_name, message, platform)
|
||||
await message.send(bot=bot, target=Target(superuser_id, private=True))
|
||||
await MessageUtils.build_message("已成功发送给管理员啦!").send(reply_to=True)
|
||||
else:
|
||||
await MessageUtils.build_message("用户id为空...").send()
|
||||
|
||||
|
||||
@_reply_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot,
|
||||
message: UniMsg,
|
||||
session: EventSession,
|
||||
):
|
||||
message[0] = Text(str(message[0]).replace("/t", "", 1).strip())
|
||||
if session.id1:
|
||||
msg = message.extract_plain_text()
|
||||
if not msg:
|
||||
platform = PlatformUtils.get_platform(bot)
|
||||
data = DialogueManage._data
|
||||
if not data:
|
||||
await MessageUtils.build_message("暂无待回复消息...").finish()
|
||||
if platform:
|
||||
data = [data[d] for d in data if data[d].platform == platform]
|
||||
for d in data:
|
||||
await d.message.send(
|
||||
bot=bot, target=Target(session.id1, private=True)
|
||||
)
|
||||
else:
|
||||
msg = msg.split()
|
||||
group_id = ""
|
||||
user_id = ""
|
||||
if msg[0].replace("-", "", 1).isdigit():
|
||||
if len(msg[0]) < 4:
|
||||
_id = int(msg[0])
|
||||
if _id >= 0:
|
||||
if model := DialogueManage.get(_id):
|
||||
user_id = model.user_id
|
||||
group_id = model.group_id
|
||||
else:
|
||||
return MessageUtils.build_message("未获取此id数据").finish()
|
||||
message[0] = Text(" ".join(str(message[0]).split(" ")[1:]))
|
||||
else:
|
||||
user_id = 0
|
||||
if msg[1].isdigit():
|
||||
group_id = msg[1]
|
||||
message[0] = Text(" ".join(str(message[0]).split(" ")[2:]))
|
||||
else:
|
||||
await MessageUtils.build_message("群组id错误...").finish(
|
||||
at_sender=True
|
||||
)
|
||||
DialogueManage.remove(_id)
|
||||
else:
|
||||
user_id = msg[0]
|
||||
if msg[1].isdigit() and len(msg[1]) > 5:
|
||||
group_id = msg[1]
|
||||
message[0] = Text(" ".join(str(message[0]).split(" ")[2:]))
|
||||
else:
|
||||
group_id = 0
|
||||
message[0] = Text(" ".join(str(message[0]).split(" ")[1:]))
|
||||
else:
|
||||
await MessageUtils.build_message("参数错误...").finish(at_sender=True)
|
||||
if group_id:
|
||||
if user_id:
|
||||
message.insert(0, alcAt("user", user_id))
|
||||
message.insert(1, Text("\n管理员回复\n=======\n"))
|
||||
await message.send(Target(group_id), bot)
|
||||
await MessageUtils.build_message("消息发送成功!").finish(at_sender=True)
|
||||
elif user_id:
|
||||
await message.send(Target(user_id, private=True), bot)
|
||||
await MessageUtils.build_message("消息发送成功!").finish(at_sender=True)
|
||||
else:
|
||||
await MessageUtils.build_message("群组id与用户id为空...").send()
|
||||
else:
|
||||
await MessageUtils.build_message("用户id为空...").send()
|
||||
@@ -1,55 +0,0 @@
|
||||
from typing import Dict
|
||||
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class DialogueData(BaseModel):
|
||||
|
||||
name: str
|
||||
"""用户名称"""
|
||||
user_id: str
|
||||
"""用户id"""
|
||||
group_id: str | None
|
||||
"""群组id"""
|
||||
group_name: str | None
|
||||
"""群组名称"""
|
||||
message: UniMsg
|
||||
"""UniMsg"""
|
||||
platform: str | None
|
||||
"""平台"""
|
||||
|
||||
|
||||
class DialogueManage:
|
||||
|
||||
_data: Dict[int, DialogueData] = {}
|
||||
_index = 0
|
||||
|
||||
@classmethod
|
||||
def add(
|
||||
cls,
|
||||
name: str,
|
||||
uid: str,
|
||||
gid: str | None,
|
||||
group_name: str | None,
|
||||
message: UniMsg,
|
||||
platform: str | None,
|
||||
):
|
||||
cls._data[cls._index] = DialogueData(
|
||||
name=name,
|
||||
user_id=uid,
|
||||
group_id=gid,
|
||||
group_name=group_name,
|
||||
message=message,
|
||||
platform=platform,
|
||||
)
|
||||
cls._index += 1
|
||||
|
||||
@classmethod
|
||||
def remove(cls, index: int):
|
||||
if index in cls._data:
|
||||
del cls._data[index]
|
||||
|
||||
@classmethod
|
||||
def get(cls, k: int):
|
||||
return cls._data.get(k)
|
||||
@@ -1,293 +0,0 @@
|
||||
import asyncio
|
||||
import traceback
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import nonebot
|
||||
from cn2an import cn2an
|
||||
from nonebot import on_keyword, on_message, on_regex
|
||||
from nonebot.log import logger
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.params import RegexGroup
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.typing import T_Handler
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from .handles.azur_handle import AzurHandle
|
||||
from .handles.ba_handle import BaHandle
|
||||
from .handles.base_handle import BaseHandle
|
||||
from .handles.fgo_handle import FgoHandle
|
||||
from .handles.genshin_handle import GenshinHandle
|
||||
from .handles.guardian_handle import GuardianHandle
|
||||
from .handles.onmyoji_handle import OnmyojiHandle
|
||||
from .handles.pcr_handle import PcrHandle
|
||||
from .handles.pretty_handle import PrettyHandle
|
||||
from .handles.prts_handle import PrtsHandle
|
||||
from .rule import rule
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="游戏抽卡",
|
||||
description="就算是模拟抽卡也不能改变自己是个非酋",
|
||||
usage="""
|
||||
usage:
|
||||
模拟赛马娘,原神,明日方舟,坎公骑冠剑,公主连结(国/台),碧蓝航线,FGO,阴阳师,碧蓝档案进行抽卡
|
||||
指令:
|
||||
原神[1-180]抽: 原神常驻池
|
||||
原神角色[1-180]抽: 原神角色UP池子
|
||||
原神角色2池[1-180]抽: 原神角色UP池子
|
||||
原神武器[1-180]抽: 原神武器UP池子
|
||||
重置原神抽卡: 清空当前卡池的抽卡次数[即从0开始计算UP概率]
|
||||
方舟[1-300]抽: 方舟卡池,当有当期UP时指向UP池
|
||||
赛马娘[1-200]抽: 赛马娘卡池,当有当期UP时指向UP池
|
||||
坎公骑冠剑[1-300]抽: 坎公骑冠剑卡池,当有当期UP时指向UP池
|
||||
pcr/公主连接[1-300]抽: 公主连接卡池
|
||||
碧蓝航线/碧蓝[重型/轻型/特型/活动][1-300]抽: 碧蓝航线重型/轻型/特型/活动卡池
|
||||
fgo[1-300]抽: fgo卡池 (已失效)
|
||||
阴阳师[1-300]抽: 阴阳师卡池
|
||||
ba/碧蓝档案[1-200]抽:碧蓝档案卡池
|
||||
* 以上指令可以通过 XX一井 来指定最大抽取数量 *
|
||||
* 示例:原神一井 *
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
menu_type="抽卡相关",
|
||||
superuser_help="""
|
||||
更新方舟信息
|
||||
重载方舟卡池
|
||||
更新原神信息
|
||||
重载原神卡池
|
||||
更新赛马娘信息
|
||||
重载赛马娘卡池
|
||||
更新坎公骑冠剑信息
|
||||
更新碧蓝航线信息
|
||||
更新fgo信息
|
||||
更新阴阳师信息
|
||||
""",
|
||||
).dict(),
|
||||
)
|
||||
|
||||
_hidden = on_message(rule=lambda: False)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Game:
|
||||
keywords: set[str]
|
||||
handle: BaseHandle
|
||||
flag: bool
|
||||
config_name: str
|
||||
max_count: int = 300 # 一次最大抽卡数
|
||||
reload_time: int | None = None # 重载UP池时间(小时)
|
||||
has_other_pool: bool = False
|
||||
|
||||
|
||||
games = (
|
||||
Game(
|
||||
{"azur", "碧蓝航线"},
|
||||
AzurHandle(),
|
||||
Config.get_config("draw_card", "AZUR_FLAG", True),
|
||||
"AZUR_FLAG",
|
||||
),
|
||||
Game(
|
||||
{"fgo", "命运冠位指定"},
|
||||
FgoHandle(),
|
||||
Config.get_config("draw_card", "FGO_FLAG", True),
|
||||
"FGO_FLAG",
|
||||
),
|
||||
Game(
|
||||
{"genshin", "原神"},
|
||||
GenshinHandle(),
|
||||
Config.get_config("draw_card", "GENSHIN_FLAG", True),
|
||||
"GENSHIN_FLAG",
|
||||
max_count=180,
|
||||
reload_time=18,
|
||||
has_other_pool=True,
|
||||
),
|
||||
Game(
|
||||
{"guardian", "坎公骑冠剑"},
|
||||
GuardianHandle(),
|
||||
Config.get_config("draw_card", "GUARDIAN_FLAG", True),
|
||||
"GUARDIAN_FLAG",
|
||||
reload_time=4,
|
||||
),
|
||||
Game(
|
||||
{"onmyoji", "阴阳师"},
|
||||
OnmyojiHandle(),
|
||||
Config.get_config("draw_card", "ONMYOJI_FLAG", True),
|
||||
"ONMYOJI_FLAG",
|
||||
),
|
||||
Game(
|
||||
{"pcr", "公主连结", "公主连接", "公主链接", "公主焊接"},
|
||||
PcrHandle(),
|
||||
Config.get_config("draw_card", "PCR_FLAG", True),
|
||||
"PCR_FLAG",
|
||||
),
|
||||
Game(
|
||||
{"pretty", "马娘", "赛马娘"},
|
||||
PrettyHandle(),
|
||||
Config.get_config("draw_card", "PRETTY_FLAG", True),
|
||||
"PRETTY_FLAG",
|
||||
max_count=200,
|
||||
reload_time=4,
|
||||
),
|
||||
Game(
|
||||
{"prts", "方舟", "明日方舟"},
|
||||
PrtsHandle(),
|
||||
Config.get_config("draw_card", "PRTS_FLAG", True),
|
||||
"PRTS_FLAG",
|
||||
reload_time=4,
|
||||
),
|
||||
Game(
|
||||
{"ba", "碧蓝档案"},
|
||||
BaHandle(),
|
||||
Config.get_config("draw_card", "BA_FLAG", True),
|
||||
"BA_FLAG",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def create_matchers():
|
||||
def draw_handler(game: Game) -> T_Handler:
|
||||
async def handler(
|
||||
session: EventSession,
|
||||
args: tuple[Any, ...] = RegexGroup(),
|
||||
):
|
||||
pool_name, pool_type_, num, unit = args
|
||||
if num == "单":
|
||||
num = 1
|
||||
else:
|
||||
try:
|
||||
num = int(cn2an(num, mode="smart"))
|
||||
except ValueError:
|
||||
await MessageUtils.build_message("必!须!是!数!字!").finish(
|
||||
reply_to=True
|
||||
)
|
||||
if unit == "井":
|
||||
num *= game.max_count
|
||||
if num < 1:
|
||||
await MessageUtils.build_message("虚空抽卡???").finish(reply_to=True)
|
||||
elif num > game.max_count:
|
||||
await MessageUtils.build_message(
|
||||
"一井都满不足不了你嘛!快爬开!"
|
||||
).finish(reply_to=True)
|
||||
pool_name = (
|
||||
pool_name.replace("池", "")
|
||||
.replace("武器", "arms")
|
||||
.replace("角色", "char")
|
||||
.replace("卡牌", "card")
|
||||
.replace("卡", "card")
|
||||
)
|
||||
try:
|
||||
if pool_type_ in ["2池", "二池"]:
|
||||
pool_name = pool_name + "1"
|
||||
res = await game.handle.draw(
|
||||
num, pool_name=pool_name, user_id=session.id1
|
||||
)
|
||||
logger.info(
|
||||
f"游戏抽卡 类型: {list(game.keywords)[1]} 卡池: {pool_name} 数量: {num}",
|
||||
"游戏抽卡",
|
||||
session=session,
|
||||
)
|
||||
except:
|
||||
logger.warning(traceback.format_exc())
|
||||
await MessageUtils.build_message("出错了...").finish(reply_to=True)
|
||||
await res.send()
|
||||
|
||||
return handler
|
||||
|
||||
def update_handler(game: Game) -> T_Handler:
|
||||
async def handler(matcher: Matcher):
|
||||
await game.handle.update_info()
|
||||
await matcher.finish("更新完成!")
|
||||
|
||||
return handler
|
||||
|
||||
def reload_handler(game: Game) -> T_Handler:
|
||||
async def handler(matcher: Matcher):
|
||||
res = await game.handle.reload_pool()
|
||||
if res:
|
||||
await res.send()
|
||||
|
||||
return handler
|
||||
|
||||
def reset_handler(game: Game) -> T_Handler:
|
||||
async def handler(matcher: Matcher, session: EventSession):
|
||||
if not session.id1:
|
||||
await MessageUtils.build_message("获取用户id失败...").finish()
|
||||
if game.handle.reset_count(session.id1):
|
||||
await MessageUtils.build_message("重置成功!").send()
|
||||
|
||||
return handler
|
||||
|
||||
def scheduled_job(game: Game) -> T_Handler:
|
||||
async def handler():
|
||||
await game.handle.reload_pool()
|
||||
|
||||
return handler
|
||||
|
||||
for game in games:
|
||||
pool_pattern = r"([^\s单0-9零一二三四五六七八九百十]{0,3})"
|
||||
num_pattern = r"(单|[0-9零一二三四五六七八九百十]{1,3})"
|
||||
unit_pattern = r"([抽|井|连])"
|
||||
pool_type = "()"
|
||||
if game.has_other_pool:
|
||||
pool_type = r"([2二]池)?"
|
||||
draw_regex = r".*?(?:{})\s*{}\s*{}\s*{}\s*{}".format(
|
||||
"|".join(game.keywords), pool_pattern, pool_type, num_pattern, unit_pattern
|
||||
)
|
||||
update_keywords = {f"更新{keyword}信息" for keyword in game.keywords}
|
||||
reload_keywords = {f"重载{keyword}卡池" for keyword in game.keywords}
|
||||
reset_keywords = {f"重置{keyword}抽卡" for keyword in game.keywords}
|
||||
on_regex(draw_regex, priority=5, block=True, rule=rule(game)).append_handler(
|
||||
draw_handler(game)
|
||||
)
|
||||
on_keyword(
|
||||
update_keywords, priority=1, block=True, permission=SUPERUSER
|
||||
).append_handler(update_handler(game))
|
||||
on_keyword(
|
||||
reload_keywords, priority=1, block=True, permission=SUPERUSER
|
||||
).append_handler(reload_handler(game))
|
||||
on_keyword(reset_keywords, priority=5, block=True).append_handler(
|
||||
reset_handler(game)
|
||||
)
|
||||
if game.reload_time:
|
||||
scheduler.add_job(
|
||||
scheduled_job(game), trigger="cron", hour=game.reload_time, minute=1
|
||||
)
|
||||
|
||||
|
||||
create_matchers()
|
||||
|
||||
|
||||
# 更新资源
|
||||
@scheduler.scheduled_job(
|
||||
"cron",
|
||||
hour=4,
|
||||
minute=1,
|
||||
)
|
||||
async def _():
|
||||
tasks = []
|
||||
for game in games:
|
||||
if game.flag:
|
||||
tasks.append(asyncio.ensure_future(game.handle.update_info()))
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
async def _():
|
||||
tasks = []
|
||||
for game in games:
|
||||
if game.flag:
|
||||
game.handle.init_data()
|
||||
if not game.handle.data_exists():
|
||||
tasks.append(asyncio.ensure_future(game.handle.update_info()))
|
||||
await asyncio.gather(*tasks)
|
||||
@@ -1,203 +0,0 @@
|
||||
import nonebot
|
||||
import ujson as json
|
||||
from pydantic import BaseModel, Extra, ValidationError
|
||||
|
||||
from zhenxun.configs.config import Config as AConfig
|
||||
from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
|
||||
# 原神
|
||||
class GenshinConfig(BaseModel, extra=Extra.ignore):
|
||||
GENSHIN_FIVE_P: float = 0.006
|
||||
GENSHIN_FOUR_P: float = 0.051
|
||||
GENSHIN_THREE_P: float = 0.43
|
||||
GENSHIN_G_FIVE_P: float = 0.016
|
||||
GENSHIN_G_FOUR_P: float = 0.13
|
||||
I72_ADD: float = 0.0585
|
||||
|
||||
|
||||
# 明日方舟
|
||||
class PrtsConfig(BaseModel, extra=Extra.ignore):
|
||||
PRTS_SIX_P: float = 0.02
|
||||
PRTS_FIVE_P: float = 0.08
|
||||
PRTS_FOUR_P: float = 0.48
|
||||
PRTS_THREE_P: float = 0.42
|
||||
|
||||
|
||||
# 赛马娘
|
||||
class PrettyConfig(BaseModel, extra=Extra.ignore):
|
||||
PRETTY_THREE_P: float = 0.03
|
||||
PRETTY_TWO_P: float = 0.18
|
||||
PRETTY_ONE_P: float = 0.79
|
||||
|
||||
|
||||
# 坎公骑冠剑
|
||||
class GuardianConfig(BaseModel, extra=Extra.ignore):
|
||||
GUARDIAN_THREE_CHAR_P: float = 0.0275
|
||||
GUARDIAN_TWO_CHAR_P: float = 0.19
|
||||
GUARDIAN_ONE_CHAR_P: float = 0.7825
|
||||
GUARDIAN_THREE_CHAR_UP_P: float = 0.01375
|
||||
GUARDIAN_THREE_CHAR_OTHER_P: float = 0.01375
|
||||
GUARDIAN_EXCLUSIVE_ARMS_P: float = 0.03
|
||||
GUARDIAN_FIVE_ARMS_P: float = 0.03
|
||||
GUARDIAN_FOUR_ARMS_P: float = 0.09
|
||||
GUARDIAN_THREE_ARMS_P: float = 0.27
|
||||
GUARDIAN_TWO_ARMS_P: float = 0.58
|
||||
GUARDIAN_EXCLUSIVE_ARMS_UP_P: float = 0.01
|
||||
GUARDIAN_EXCLUSIVE_ARMS_OTHER_P: float = 0.02
|
||||
|
||||
|
||||
# 公主连结
|
||||
class PcrConfig(BaseModel, extra=Extra.ignore):
|
||||
PCR_THREE_P: float = 0.025
|
||||
PCR_TWO_P: float = 0.18
|
||||
PCR_ONE_P: float = 0.795
|
||||
PCR_G_THREE_P: float = 0.025
|
||||
PCR_G_TWO_P: float = 0.975
|
||||
|
||||
|
||||
# 碧蓝航线
|
||||
class AzurConfig(BaseModel, extra=Extra.ignore):
|
||||
AZUR_FIVE_P: float = 0.012
|
||||
AZUR_FOUR_P: float = 0.07
|
||||
AZUR_THREE_P: float = 0.12
|
||||
AZUR_TWO_P: float = 0.51
|
||||
AZUR_ONE_P: float = 0.3
|
||||
|
||||
|
||||
# 命运-冠位指定
|
||||
class FgoConfig(BaseModel, extra=Extra.ignore):
|
||||
FGO_SERVANT_FIVE_P: float = 0.01
|
||||
FGO_SERVANT_FOUR_P: float = 0.03
|
||||
FGO_SERVANT_THREE_P: float = 0.4
|
||||
FGO_CARD_FIVE_P: float = 0.04
|
||||
FGO_CARD_FOUR_P: float = 0.12
|
||||
FGO_CARD_THREE_P: float = 0.4
|
||||
|
||||
|
||||
# 阴阳师
|
||||
class OnmyojiConfig(BaseModel, extra=Extra.ignore):
|
||||
ONMYOJI_SP: float = 0.0025
|
||||
ONMYOJI_SSR: float = 0.01
|
||||
ONMYOJI_SR: float = 0.2
|
||||
ONMYOJI_R: float = 0.7875
|
||||
|
||||
|
||||
# 碧蓝档案
|
||||
class BaConfig(BaseModel, extra=Extra.ignore):
|
||||
BA_THREE_P: float = 0.025
|
||||
BA_TWO_P: float = 0.185
|
||||
BA_ONE_P: float = 0.79
|
||||
BA_G_TWO_P: float = 0.975
|
||||
|
||||
|
||||
class Config(BaseModel, extra=Extra.ignore):
|
||||
# 开关
|
||||
PRTS_FLAG: bool = True
|
||||
GENSHIN_FLAG: bool = True
|
||||
PRETTY_FLAG: bool = True
|
||||
GUARDIAN_FLAG: bool = True
|
||||
PCR_FLAG: bool = True
|
||||
AZUR_FLAG: bool = True
|
||||
FGO_FLAG: bool = True
|
||||
ONMYOJI_FLAG: bool = True
|
||||
BA_FLAG: bool = True
|
||||
|
||||
# 其他配置
|
||||
PCR_TAI: bool = False
|
||||
SEMAPHORE: int = 5
|
||||
|
||||
# 抽卡概率
|
||||
prts: PrtsConfig = PrtsConfig()
|
||||
genshin: GenshinConfig = GenshinConfig()
|
||||
pretty: PrettyConfig = PrettyConfig()
|
||||
guardian: GuardianConfig = GuardianConfig()
|
||||
pcr: PcrConfig = PcrConfig()
|
||||
azur: AzurConfig = AzurConfig()
|
||||
fgo: FgoConfig = FgoConfig()
|
||||
onmyoji: OnmyojiConfig = OnmyojiConfig()
|
||||
ba: BaConfig = BaConfig()
|
||||
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
|
||||
# DRAW_PATH = Path("data/draw_card").absolute()
|
||||
DRAW_PATH = IMAGE_PATH / "draw_card"
|
||||
# try:
|
||||
# DRAW_PATH = Path(global_config.draw_path).absolute()
|
||||
# except:
|
||||
# pass
|
||||
config_path = DATA_PATH / "draw_card" / "draw_card_config" / "draw_card_config.json"
|
||||
|
||||
draw_config: Config = Config()
|
||||
|
||||
for game_flag, game_name in zip(
|
||||
[
|
||||
"PRTS_FLAG",
|
||||
"GENSHIN_FLAG",
|
||||
"PRETTY_FLAG",
|
||||
"GUARDIAN_FLAG",
|
||||
"PCR_FLAG",
|
||||
"AZUR_FLAG",
|
||||
"FGO_FLAG",
|
||||
"ONMYOJI_FLAG",
|
||||
"PCR_TAI",
|
||||
"BA_FLAG",
|
||||
],
|
||||
[
|
||||
"明日方舟",
|
||||
"原神",
|
||||
"赛马娘",
|
||||
"坎公骑冠剑",
|
||||
"公主连结",
|
||||
"碧蓝航线",
|
||||
"命运-冠位指定(FGO)",
|
||||
"阴阳师",
|
||||
"pcr台服卡池",
|
||||
"碧蓝档案",
|
||||
],
|
||||
):
|
||||
AConfig.add_plugin_config(
|
||||
"draw_card",
|
||||
game_flag,
|
||||
True,
|
||||
help=f"{game_name} 抽卡开关",
|
||||
default_value=True,
|
||||
type=bool,
|
||||
)
|
||||
AConfig.add_plugin_config(
|
||||
"draw_card",
|
||||
"SEMAPHORE",
|
||||
5,
|
||||
help=f"异步数据下载数量限制",
|
||||
default_value=5,
|
||||
type=int,
|
||||
)
|
||||
AConfig.set_name("draw_card", "游戏抽卡")
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
def check_config():
|
||||
global draw_config
|
||||
|
||||
if not config_path.exists():
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
draw_config = Config()
|
||||
logger.warning("draw_card:配置文件不存在,已重新生成配置文件...")
|
||||
else:
|
||||
with config_path.open("r", encoding="utf8") as fp:
|
||||
data = json.load(fp)
|
||||
try:
|
||||
draw_config = Config.parse_obj({**data})
|
||||
except ValidationError:
|
||||
draw_config = Config()
|
||||
logger.warning("draw_card:配置文件格式错误,已重新生成配置文件...")
|
||||
|
||||
with config_path.open("w", encoding="utf8") as fp:
|
||||
json.dump(
|
||||
draw_config.dict(),
|
||||
fp,
|
||||
indent=4,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
@@ -1,149 +0,0 @@
|
||||
from typing import Optional, TypeVar, Generic
|
||||
from pydantic import BaseModel
|
||||
from cachetools import TTLCache
|
||||
|
||||
|
||||
class BaseUserCount(BaseModel):
|
||||
count: int = 0 # 当前抽卡次数
|
||||
|
||||
|
||||
TCount = TypeVar("TCount", bound="BaseUserCount")
|
||||
|
||||
|
||||
class DrawCountManager(Generic[TCount]):
|
||||
"""
|
||||
抽卡统计保底
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, game_draw_count_rule: tuple, star2name: tuple, max_draw_count: int
|
||||
):
|
||||
"""
|
||||
初始化保底统计
|
||||
|
||||
例如:DrawCountManager((10, 90, 180), ("4", "5", "5"))
|
||||
|
||||
抽卡保底需要的次数和返回的对应名称,例如星级等
|
||||
|
||||
:param game_draw_count_rule:抽卡规则
|
||||
:param star2name:星级对应的名称
|
||||
:param max_draw_count:最大累计抽卡次数,当下次单次抽卡超过该次数时将会清空数据
|
||||
|
||||
"""
|
||||
# 只有保底
|
||||
# 超过60秒重置抽卡次数
|
||||
self._data: TTLCache[int, TCount] = TTLCache(maxsize=1000, ttl=60)
|
||||
self._guarantee_tuple = game_draw_count_rule
|
||||
self._star2name = star2name
|
||||
self._max_draw_count = max_draw_count
|
||||
|
||||
@classmethod
|
||||
def get_count_class(cls) -> TCount:
|
||||
raise NotImplementedError
|
||||
|
||||
def _get_count(self, key: int) -> TCount:
|
||||
if self._data.get(key) is None:
|
||||
self._data[key] = self.get_count_class()
|
||||
else:
|
||||
self._data[key] = self._data[key]
|
||||
return self._data[key]
|
||||
|
||||
def increase(self, key: int, value: int = 1):
|
||||
"""
|
||||
用户抽卡次数加1
|
||||
"""
|
||||
self._get_count(key).count += value
|
||||
|
||||
def get_max_guarantee(self):
|
||||
"""
|
||||
获取最大保底抽卡次数
|
||||
"""
|
||||
return self._guarantee_tuple[-1]
|
||||
|
||||
def get_user_count(self, key: int) -> int:
|
||||
"""
|
||||
获取当前抽卡次数
|
||||
"""
|
||||
return self._get_count(key).count
|
||||
|
||||
def reset(self, key: int):
|
||||
"""
|
||||
清空记录
|
||||
"""
|
||||
self._data.pop(key, None)
|
||||
|
||||
|
||||
class GenshinUserCount(BaseUserCount):
|
||||
five_index: int = 0 # 获取五星时的抽卡次数
|
||||
four_index: int = 0 # 获取四星时的抽卡次数
|
||||
is_up: bool = False # 下次五星是否必定为up
|
||||
|
||||
|
||||
class GenshinCountManager(DrawCountManager[GenshinUserCount]):
|
||||
@classmethod
|
||||
def get_count_class(cls) -> GenshinUserCount:
|
||||
return GenshinUserCount()
|
||||
|
||||
def set_is_up(self, key: int, value: bool):
|
||||
"""
|
||||
设置下次是否必定up
|
||||
"""
|
||||
self._get_count(key).is_up = value
|
||||
|
||||
def is_up(self, key: int) -> bool:
|
||||
"""
|
||||
判断该次保底是否必定为up
|
||||
"""
|
||||
return self._get_count(key).is_up
|
||||
|
||||
def get_user_five_index(self, key: int) -> int:
|
||||
"""
|
||||
获取用户上次获取五星的次数
|
||||
"""
|
||||
return self._get_count(key).five_index
|
||||
|
||||
def get_user_four_index(self, key: int) -> int:
|
||||
"""
|
||||
获取用户上次获取四星的次数
|
||||
"""
|
||||
return self._get_count(key).four_index
|
||||
|
||||
def mark_five_index(self, key: int):
|
||||
"""
|
||||
标记用户该次次数为五星
|
||||
"""
|
||||
self._get_count(key).five_index = self._get_count(key).count
|
||||
|
||||
def mark_four_index(self, key: int):
|
||||
"""
|
||||
标记用户该次次数为四星
|
||||
"""
|
||||
self._get_count(key).four_index = self._get_count(key).count
|
||||
|
||||
def check_count(self, key: int, count: int):
|
||||
"""
|
||||
检查用户该次抽卡次数累计是否超过最大限制次数
|
||||
"""
|
||||
if self._get_count(key).count + count > self._max_draw_count:
|
||||
self._data.pop(key, None)
|
||||
|
||||
def get_user_guarantee_count(self, key: int) -> int:
|
||||
user = self._get_count(key)
|
||||
return (
|
||||
self.get_max_guarantee()
|
||||
- (user.count % self.get_max_guarantee() - user.five_index)
|
||||
) % self.get_max_guarantee() or self.get_max_guarantee()
|
||||
|
||||
def check(self, key: int) -> Optional[int]:
|
||||
"""
|
||||
是否保底
|
||||
"""
|
||||
# print(self._data)
|
||||
user = self._get_count(key)
|
||||
if user.count - user.five_index == 90:
|
||||
user.five_index = user.count
|
||||
return 5
|
||||
if user.count - user.four_index == 10:
|
||||
user.four_index = user.count
|
||||
return 4
|
||||
return None
|
||||
@@ -1,308 +0,0 @@
|
||||
import random
|
||||
from urllib.parse import unquote
|
||||
|
||||
import dateparser
|
||||
import ujson as json
|
||||
from lxml import etree
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
from PIL import ImageDraw
|
||||
from pydantic import ValidationError
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ..config import draw_config
|
||||
from ..util import cn2py, load_font, remove_prohibited_str
|
||||
from .base_handle import BaseData, BaseHandle
|
||||
from .base_handle import UpChar as _UpChar
|
||||
from .base_handle import UpEvent as _UpEvent
|
||||
|
||||
|
||||
class AzurChar(BaseData):
|
||||
type_: str # 舰娘类型
|
||||
|
||||
@property
|
||||
def star_str(self) -> str:
|
||||
return ["白", "蓝", "紫", "金"][self.star - 1]
|
||||
|
||||
|
||||
class UpChar(_UpChar):
|
||||
type_: str # 舰娘类型
|
||||
|
||||
|
||||
class UpEvent(_UpEvent):
|
||||
up_char: list[UpChar] # up对象
|
||||
|
||||
|
||||
class AzurHandle(BaseHandle[AzurChar]):
|
||||
def __init__(self):
|
||||
super().__init__("azur", "碧蓝航线")
|
||||
self.max_star = 4
|
||||
self.config = draw_config.azur
|
||||
self.ALL_CHAR: list[AzurChar] = []
|
||||
self.UP_EVENT: UpEvent | None = None
|
||||
|
||||
def get_card(self, pool_name: str, **kwargs) -> AzurChar:
|
||||
if pool_name == "轻型":
|
||||
type_ = ["驱逐", "轻巡", "维修"]
|
||||
elif pool_name == "重型":
|
||||
type_ = ["重巡", "战列", "战巡", "重炮"]
|
||||
else:
|
||||
type_ = ["维修", "潜艇", "重巡", "轻航", "航母"]
|
||||
up_pool_flag = pool_name == "活动"
|
||||
# Up
|
||||
up_ship = (
|
||||
[x for x in self.UP_EVENT.up_char if x.zoom > 0] if self.UP_EVENT else []
|
||||
)
|
||||
# print(up_ship)
|
||||
acquire_char = None
|
||||
if up_ship and up_pool_flag:
|
||||
up_zoom: list[tuple[float, float]] = [(0, up_ship[0].zoom / 100)]
|
||||
# 初始化概率
|
||||
cur_ = up_ship[0].zoom / 100
|
||||
for i in range(len(up_ship)):
|
||||
try:
|
||||
up_zoom.append((cur_, cur_ + up_ship[i + 1].zoom / 100))
|
||||
cur_ += up_ship[i + 1].zoom / 100
|
||||
except IndexError:
|
||||
pass
|
||||
rand = random.random()
|
||||
# 抽取up
|
||||
for i, zoom in enumerate(up_zoom):
|
||||
if zoom[0] <= rand <= zoom[1]:
|
||||
try:
|
||||
acquire_char = [
|
||||
x for x in self.ALL_CHAR if x.name == up_ship[i].name
|
||||
][0]
|
||||
except IndexError:
|
||||
pass
|
||||
# 没有up或者未抽取到up
|
||||
if not acquire_char:
|
||||
star = self.get_star(
|
||||
# [4, 3, 2, 1],
|
||||
[4, 3, 2, 2],
|
||||
[
|
||||
self.config.AZUR_FOUR_P,
|
||||
self.config.AZUR_THREE_P,
|
||||
self.config.AZUR_TWO_P,
|
||||
self.config.AZUR_ONE_P,
|
||||
],
|
||||
)
|
||||
acquire_char = random.choice(
|
||||
[
|
||||
x
|
||||
for x in self.ALL_CHAR
|
||||
if x.star == star and x.type_ in type_ and not x.limited
|
||||
]
|
||||
)
|
||||
return acquire_char
|
||||
|
||||
async def draw(self, count: int, **kwargs) -> UniMessage:
|
||||
index2card = self.get_cards(count, **kwargs)
|
||||
cards = [card[0] for card in index2card]
|
||||
up_list = [x.name for x in self.UP_EVENT.up_char] if self.UP_EVENT else []
|
||||
result = self.format_result(index2card, **{**kwargs, "up_list": up_list})
|
||||
gen_img = await self.generate_img(cards)
|
||||
return MessageUtils.build_message([gen_img.pic2bytes(), result])
|
||||
|
||||
async def generate_card_img(self, card: AzurChar) -> BuildImage:
|
||||
sep_w = 5
|
||||
sep_t = 5
|
||||
sep_b = 20
|
||||
w = 100
|
||||
h = 100
|
||||
bg = BuildImage(w + sep_w * 2, h + sep_t + sep_b)
|
||||
frame_path = str(self.img_path / f"{card.star}_star.png")
|
||||
frame = BuildImage(w, h, background=frame_path)
|
||||
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
|
||||
img = BuildImage(w, h, background=img_path)
|
||||
# 加圆角
|
||||
await frame.circle_corner(6)
|
||||
await img.circle_corner(6)
|
||||
await bg.paste(img, (sep_w, sep_t))
|
||||
await bg.paste(frame, (sep_w, sep_t))
|
||||
# 加名字
|
||||
text = card.name[:6] + "..." if len(card.name) > 7 else card.name
|
||||
font = load_font(fontsize=14)
|
||||
text_w, text_h = BuildImage.get_text_size(text, font)
|
||||
draw = ImageDraw.Draw(bg.markImg)
|
||||
draw.text(
|
||||
(sep_w + (w - text_w) / 2, h + sep_t + (sep_b - text_h) / 2),
|
||||
text,
|
||||
font=font,
|
||||
fill=["#808080", "#3b8bff", "#8000ff", "#c90", "#ee494c"][card.star - 1],
|
||||
)
|
||||
return bg
|
||||
|
||||
def _init_data(self):
|
||||
self.ALL_CHAR = [
|
||||
AzurChar(
|
||||
name=value["名称"],
|
||||
star=int(value["星级"]),
|
||||
limited="可以建造" not in value["获取途径"],
|
||||
type_=value["类型"],
|
||||
)
|
||||
for value in self.load_data().values()
|
||||
]
|
||||
self.load_up_char()
|
||||
|
||||
def load_up_char(self):
|
||||
try:
|
||||
data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json")
|
||||
self.UP_EVENT = UpEvent.parse_obj(data.get("char", {}))
|
||||
except ValidationError:
|
||||
logger.warning(f"{self.game_name}_up_char 解析出错")
|
||||
|
||||
def dump_up_char(self):
|
||||
if self.UP_EVENT:
|
||||
data = {"char": json.loads(self.UP_EVENT.json())}
|
||||
self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
|
||||
self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
|
||||
|
||||
async def _update_info(self):
|
||||
info = {}
|
||||
# 更新图鉴
|
||||
url = "https://wiki.biligame.com/blhx/舰娘图鉴"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"更新 {self.game_name_cn} 出错")
|
||||
return
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
contents = dom.xpath(
|
||||
"//div[@class='mw-body-content mw-content-ltr']/div[@class='mw-parser-output']"
|
||||
)
|
||||
for index, content in enumerate(contents):
|
||||
char_list = content.xpath("./div[@id='CardSelectTr']/div")
|
||||
for char in char_list:
|
||||
try:
|
||||
name = char.xpath("./span/a/text()")[0]
|
||||
frame = char.xpath("./div/div/a/img/@alt")[0]
|
||||
avatar = char.xpath("./div/img/@srcset")[0]
|
||||
except IndexError:
|
||||
continue
|
||||
member_dict = {
|
||||
"名称": remove_prohibited_str(name),
|
||||
"头像": unquote(str(avatar).split(" ")[-2]),
|
||||
"星级": self.parse_star(frame),
|
||||
"类型": char.xpath("./@data-param1")[0].split(",")[-1],
|
||||
}
|
||||
info[member_dict["名称"]] = member_dict
|
||||
# 更新额外信息
|
||||
for key in info.keys():
|
||||
# TODO: 各种舰娘·改获取错误
|
||||
url = f"https://wiki.biligame.com/blhx/{key}"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
info[key]["获取途径"] = []
|
||||
logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
|
||||
continue
|
||||
try:
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
time = dom.xpath(
|
||||
"//table[@class='wikitable sv-general']/tbody[1]/tr[4]/td[2]//text()"
|
||||
)[0]
|
||||
sources = []
|
||||
if "无法建造" in time:
|
||||
sources.append("无法建造")
|
||||
elif "活动已关闭" in time:
|
||||
sources.append("活动限定")
|
||||
else:
|
||||
sources.append("可以建造")
|
||||
info[key]["获取途径"] = sources
|
||||
except IndexError:
|
||||
info[key]["获取途径"] = []
|
||||
logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
|
||||
self.dump_data(info)
|
||||
logger.info(f"{self.game_name_cn} 更新成功")
|
||||
# 下载头像
|
||||
for value in info.values():
|
||||
await self.download_img(value["头像"], value["名称"])
|
||||
# 下载头像框
|
||||
idx = 1
|
||||
BLHX_URL = "https://patchwiki.biligame.com/images/blhx"
|
||||
for url in [
|
||||
"/1/15/pxho13xsnkyb546tftvh49etzdh74cf.png",
|
||||
"/a/a9/k8t7nx6c8pan5vyr8z21txp45jxeo66.png",
|
||||
"/a/a5/5whkzvt200zwhhx0h0iz9qo1kldnidj.png",
|
||||
"/a/a2/ptog1j220x5q02hytpwc8al7f229qk9.png",
|
||||
"/6/6d/qqv5oy3xs40d3055cco6bsm0j4k4gzk.png",
|
||||
]:
|
||||
await self.download_img(BLHX_URL + url, f"{idx}_star")
|
||||
idx += 1
|
||||
await self.update_up_char()
|
||||
|
||||
@staticmethod
|
||||
def parse_star(star: str) -> int:
|
||||
if star in ["舰娘头像外框普通.png", "舰娘头像外框白色.png"]:
|
||||
return 1
|
||||
elif star in ["舰娘头像外框稀有.png", "舰娘头像外框蓝色.png"]:
|
||||
return 2
|
||||
elif star in ["舰娘头像外框精锐.png", "舰娘头像外框紫色.png"]:
|
||||
return 3
|
||||
elif star in ["舰娘头像外框超稀有.png", "舰娘头像外框金色.png"]:
|
||||
return 4
|
||||
elif star in ["舰娘头像外框海上传奇.png", "舰娘头像外框彩色.png"]:
|
||||
return 5
|
||||
elif star in [
|
||||
"舰娘头像外框最高方案.png",
|
||||
"舰娘头像外框决战方案.png",
|
||||
"舰娘头像外框超稀有META.png",
|
||||
"舰娘头像外框精锐META.png",
|
||||
]:
|
||||
return 6
|
||||
else:
|
||||
return 6
|
||||
|
||||
async def update_up_char(self):
|
||||
url = "https://wiki.biligame.com/blhx/游戏活动表"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"{self.game_name_cn}获取活动表出错")
|
||||
return
|
||||
try:
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
dd = dom.xpath("//div[@class='timeline2']/dl/dd/a")[0]
|
||||
url = "https://wiki.biligame.com" + dd.xpath("./@href")[0]
|
||||
title = dd.xpath("string(.)")
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"{self.game_name_cn}获取活动页面出错")
|
||||
return
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
timer = dom.xpath("//span[@class='eventTimer']")[0]
|
||||
start_time = dateparser.parse(timer.xpath("./@data-start")[0])
|
||||
end_time = dateparser.parse(timer.xpath("./@data-end")[0])
|
||||
ships = dom.xpath("//table[@class='shipinfo']")
|
||||
up_chars = []
|
||||
for ship in ships:
|
||||
name = ship.xpath("./tbody/tr/td[2]/p/a/@title")[0]
|
||||
type_ = ship.xpath("./tbody/tr/td[2]/p/small/text()")[0] # 舰船类型
|
||||
try:
|
||||
p = float(str(ship.xpath(".//sup/text()")[0]).strip("%"))
|
||||
except (IndexError, ValueError):
|
||||
p = 0
|
||||
star = self.parse_star(
|
||||
ship.xpath("./tbody/tr/td[1]/div/div/div/a/img/@alt")[0]
|
||||
)
|
||||
up_chars.append(
|
||||
UpChar(name=name, star=star, limited=False, zoom=p, type_=type_)
|
||||
)
|
||||
self.UP_EVENT = UpEvent(
|
||||
title=title,
|
||||
pool_img="",
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
up_char=up_chars,
|
||||
)
|
||||
self.dump_up_char()
|
||||
except Exception as e:
|
||||
logger.warning(f"{self.game_name_cn}UP更新出错", e=e)
|
||||
|
||||
async def _reload_pool(self) -> UniMessage | None:
|
||||
await self.update_up_char()
|
||||
self.load_up_char()
|
||||
if self.UP_EVENT:
|
||||
return MessageUtils.build_message(
|
||||
f"重载成功!\n当前活动:{self.UP_EVENT.title}"
|
||||
)
|
||||
@@ -1,157 +0,0 @@
|
||||
import random
|
||||
|
||||
from PIL import ImageDraw
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
|
||||
from ..config import draw_config
|
||||
from ..util import cn2py, load_font
|
||||
from .base_handle import BaseData, BaseHandle
|
||||
|
||||
|
||||
class BaChar(BaseData):
|
||||
pass
|
||||
|
||||
|
||||
class BaHandle(BaseHandle[BaChar]):
|
||||
def __init__(self):
|
||||
super().__init__("ba", "碧蓝档案")
|
||||
self.max_star = 3
|
||||
self.config = draw_config.ba
|
||||
self.ALL_CHAR: list[BaChar] = []
|
||||
|
||||
def get_card(self, mode: int = 1) -> BaChar:
|
||||
if mode == 2:
|
||||
star = self.get_star(
|
||||
[3, 2], [self.config.BA_THREE_P, self.config.BA_G_TWO_P]
|
||||
)
|
||||
else:
|
||||
star = self.get_star(
|
||||
[3, 2, 1],
|
||||
[self.config.BA_THREE_P, self.config.BA_TWO_P, self.config.BA_ONE_P],
|
||||
)
|
||||
chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited]
|
||||
return random.choice(chars)
|
||||
|
||||
def get_cards(self, count: int, **kwargs) -> list[tuple[BaChar, int]]:
|
||||
card_list = []
|
||||
card_count = 0 # 保底计算
|
||||
for i in range(count):
|
||||
card_count += 1
|
||||
# 十连保底
|
||||
if card_count == 10:
|
||||
card = self.get_card(2)
|
||||
card_count = 0
|
||||
else:
|
||||
card = self.get_card(1)
|
||||
if card.star > self.max_star - 2:
|
||||
card_count = 0
|
||||
card_list.append((card, i + 1))
|
||||
return card_list
|
||||
|
||||
async def generate_card_img(self, card: BaChar) -> BuildImage:
|
||||
sep_w = 5
|
||||
sep_h = 5
|
||||
star_h = 15
|
||||
img_w = 90
|
||||
img_h = 100
|
||||
font_h = 20
|
||||
bar_h = 20
|
||||
bar_w = 90
|
||||
bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5")
|
||||
img_path = self.img_path / f"{cn2py(card.name)}.png"
|
||||
img = BuildImage(
|
||||
img_w, img_h, background=img_path if img_path.exists() else None
|
||||
)
|
||||
bar = BuildImage(bar_w, bar_h, color="#6495ED")
|
||||
await bg.paste(img, (sep_w, sep_h))
|
||||
await bg.paste(bar, (sep_w, img_h - bar_h + sep_h))
|
||||
if card.star == 1:
|
||||
star_path = str(self.img_path / "star-1.png")
|
||||
star_w = 15
|
||||
elif card.star == 2:
|
||||
star_path = str(self.img_path / "star-2.png")
|
||||
star_w = 30
|
||||
else:
|
||||
star_path = str(self.img_path / "star-3.png")
|
||||
star_w = 45
|
||||
star = BuildImage(star_w, star_h, background=star_path)
|
||||
await bg.paste(star, (img_w // 2 - 15 * (card.star - 1) // 2, img_h - star_h))
|
||||
text = card.name[:5] + "..." if len(card.name) > 6 else card.name
|
||||
font = load_font(fontsize=14)
|
||||
text_w, text_h = BuildImage.get_text_size(text, font)
|
||||
draw = ImageDraw.Draw(bg.markImg)
|
||||
draw.text(
|
||||
(sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2),
|
||||
text,
|
||||
font=font,
|
||||
fill="gray",
|
||||
)
|
||||
return bg
|
||||
|
||||
def _init_data(self):
|
||||
self.ALL_CHAR = [
|
||||
BaChar(
|
||||
name=value["名称"],
|
||||
star=int(value["星级"]),
|
||||
limited=True if "(" in key else False,
|
||||
)
|
||||
for key, value in self.load_data().items()
|
||||
]
|
||||
|
||||
def title2star(self, title: int):
|
||||
if title == "Star-3.png":
|
||||
return 3
|
||||
elif title == "Star-2.png":
|
||||
return 2
|
||||
else:
|
||||
return 1
|
||||
|
||||
async def _update_info(self):
|
||||
# TODO: ba获取链接失效
|
||||
info = {}
|
||||
url = "https://schale.gg/data/cn/students.min.json?v=49"
|
||||
result = (await AsyncHttpx.get(url)).json()
|
||||
if not result:
|
||||
logger.warning(f"更新 {self.game_name_cn} 出错")
|
||||
return
|
||||
else:
|
||||
for char in result:
|
||||
try:
|
||||
name = char["Name"]
|
||||
id = str(char["Id"])
|
||||
avatar = (
|
||||
"https://github.com/SchaleDB/SchaleDB/raw/main/images/student/icon/"
|
||||
+ id
|
||||
+ ".webp"
|
||||
)
|
||||
star = char["StarGrade"]
|
||||
star = char["StarGrade"]
|
||||
except IndexError:
|
||||
continue
|
||||
member_dict = {
|
||||
"头像": avatar,
|
||||
"名称": name,
|
||||
"星级": star,
|
||||
}
|
||||
info[member_dict["名称"]] = member_dict
|
||||
self.dump_data(info)
|
||||
logger.info(f"{self.game_name_cn} 更新成功")
|
||||
# 下载头像
|
||||
for value in info.values():
|
||||
await self.download_img(value["头像"], value["名称"])
|
||||
# 下载星星
|
||||
await self.download_img(
|
||||
"https://patchwiki.biligame.com/images/bluearchive/thumb/e/e0/82nj2x9sxko473g7782r14fztd4zyky.png/15px-Star-1.png",
|
||||
"star-1",
|
||||
)
|
||||
await self.download_img(
|
||||
"https://patchwiki.biligame.com/images/bluearchive/thumb/0/0b/msaff2g0zk6nlyl1rrn7n1ri4yobcqc.png/30px-Star-2.png",
|
||||
"star-2",
|
||||
)
|
||||
await self.download_img(
|
||||
"https://patchwiki.biligame.com/images/bluearchive/thumb/8/8a/577yv79x1rwxk8efdccpblo0lozl158.png/46px-Star-3.png",
|
||||
"star-3",
|
||||
)
|
||||
@@ -1,295 +0,0 @@
|
||||
import asyncio
|
||||
import random
|
||||
from asyncio.exceptions import TimeoutError
|
||||
from datetime import datetime
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
import aiohttp
|
||||
import anyio
|
||||
import ujson as json
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
from PIL import Image
|
||||
from pydantic import BaseModel, Extra
|
||||
|
||||
from zhenxun.configs.path_config import DATA_PATH
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ..config import DRAW_PATH, draw_config
|
||||
from ..util import circled_number, cn2py
|
||||
|
||||
|
||||
class BaseData(BaseModel, extra=Extra.ignore):
|
||||
name: str # 名字
|
||||
star: int # 星级
|
||||
limited: bool # 限定
|
||||
|
||||
def __eq__(self, other: "BaseData"):
|
||||
return self.name == other.name
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
@property
|
||||
def star_str(self) -> str:
|
||||
return "".join(["★" for _ in range(self.star)])
|
||||
|
||||
|
||||
class UpChar(BaseData):
|
||||
zoom: float # up提升倍率
|
||||
|
||||
|
||||
class UpEvent(BaseModel):
|
||||
title: str # up池标题
|
||||
pool_img: str # up池封面
|
||||
start_time: datetime | None # 开始时间
|
||||
end_time: datetime | None # 结束时间
|
||||
up_char: list[UpChar] # up对象
|
||||
|
||||
|
||||
TC = TypeVar("TC", bound="BaseData")
|
||||
|
||||
|
||||
class BaseHandle(Generic[TC]):
|
||||
def __init__(self, game_name: str, game_name_cn: str):
|
||||
self.game_name = game_name
|
||||
self.game_name_cn = game_name_cn
|
||||
self.max_star = 1 # 最大星级
|
||||
self.game_card_color: str = "#ffffff"
|
||||
self.data_path = DATA_PATH / "draw_card"
|
||||
self.img_path = DRAW_PATH / f"{self.game_name}"
|
||||
self.up_path = DATA_PATH / "draw_card" / "draw_card_up"
|
||||
self.img_path.mkdir(parents=True, exist_ok=True)
|
||||
self.up_path.mkdir(parents=True, exist_ok=True)
|
||||
self.data_files: list[str] = [f"{self.game_name}.json"]
|
||||
|
||||
async def draw(self, count: int, **kwargs) -> UniMessage:
|
||||
index2card = self.get_cards(count, **kwargs)
|
||||
cards = [card[0] for card in index2card]
|
||||
result = self.format_result(index2card)
|
||||
gen_img = await self.generate_img(cards)
|
||||
return MessageUtils.build_message([gen_img, result])
|
||||
|
||||
# 抽取卡池
|
||||
def get_card(self, **kwargs) -> TC:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_cards(self, count: int, **kwargs) -> list[tuple[TC, int]]:
|
||||
return [(self.get_card(**kwargs), i) for i in range(count)]
|
||||
|
||||
# 获取星级
|
||||
@staticmethod
|
||||
def get_star(star_list: list[int], probability_list: list[float]) -> int:
|
||||
return random.choices(star_list, weights=probability_list, k=1)[0]
|
||||
|
||||
def format_result(self, index2card: list[tuple[TC, int]], **kwargs) -> str:
|
||||
card_list = [card[0] for card in index2card]
|
||||
results = [
|
||||
self.format_star_result(card_list, **kwargs),
|
||||
self.format_max_star(index2card, **kwargs),
|
||||
self.format_max_card(card_list, **kwargs),
|
||||
]
|
||||
results = [rst for rst in results if rst]
|
||||
return "\n".join(results)
|
||||
|
||||
def format_star_result(self, card_list: list[TC], **kwargs) -> str:
|
||||
star_dict: dict[str, int] = {} # 记录星级及其次数
|
||||
|
||||
card_list_sorted = sorted(card_list, key=lambda c: c.star, reverse=True)
|
||||
for card in card_list_sorted:
|
||||
try:
|
||||
star_dict[card.star_str] += 1
|
||||
except KeyError:
|
||||
star_dict[card.star_str] = 1
|
||||
|
||||
rst = ""
|
||||
for star_str, count in star_dict.items():
|
||||
rst += f"[{star_str}×{count}] "
|
||||
return rst.strip()
|
||||
|
||||
def format_max_star(
|
||||
self, card_list: list[tuple[TC, int]], up_list: list[str] = [], **kwargs
|
||||
) -> str:
|
||||
up_list = up_list or kwargs.get("up_list", [])
|
||||
rst = ""
|
||||
for card, index in card_list:
|
||||
if card.star == self.max_star:
|
||||
if card.name in up_list:
|
||||
rst += f"第 {index} 抽获取UP {card.name}\n"
|
||||
else:
|
||||
rst += f"第 {index} 抽获取 {card.name}\n"
|
||||
return rst.strip()
|
||||
|
||||
def format_max_card(self, card_list: list[TC], **kwargs) -> str:
|
||||
card_dict: dict[TC, int] = {} # 记录卡牌抽取次数
|
||||
|
||||
for card in card_list:
|
||||
try:
|
||||
card_dict[card] += 1
|
||||
except KeyError:
|
||||
card_dict[card] = 1
|
||||
|
||||
max_count = max(card_dict.values())
|
||||
max_card = list(card_dict.keys())[list(card_dict.values()).index(max_count)]
|
||||
if max_count <= 1:
|
||||
return ""
|
||||
return f"抽取到最多的是{max_card.name},共抽取了{max_count}次"
|
||||
|
||||
async def generate_img(
|
||||
self,
|
||||
cards: list[TC],
|
||||
num_per_line: int = 5,
|
||||
max_per_line: tuple[int, int] = (40, 10),
|
||||
) -> BuildImage:
|
||||
"""
|
||||
生成统计图片
|
||||
cards: 卡牌列表
|
||||
num_per_line: 单行角色显示数量
|
||||
max_per_line: 当card_list超过一定数值时,更改单行数量
|
||||
"""
|
||||
if len(cards) > max_per_line[0]:
|
||||
num_per_line = max_per_line[1]
|
||||
if len(cards) > 90:
|
||||
card_dict: dict[TC, int] = {} # 记录卡牌抽取次数
|
||||
for card in cards:
|
||||
try:
|
||||
card_dict[card] += 1
|
||||
except KeyError:
|
||||
card_dict[card] = 1
|
||||
card_list = list(card_dict)
|
||||
num_list = list(card_dict.values())
|
||||
else:
|
||||
card_list = cards
|
||||
num_list = [1] * len(cards)
|
||||
|
||||
card_imgs: list[BuildImage] = []
|
||||
for card, num in zip(card_list, num_list):
|
||||
card_img = await self.generate_card_img(card)
|
||||
# 数量 > 1 时加数字上标
|
||||
if num > 1:
|
||||
label = circled_number(num)
|
||||
label_w = int(min(card_img.width, card_img.height) / 7)
|
||||
label = label.resize(
|
||||
(
|
||||
int(label_w * label.width / label.height),
|
||||
label_w,
|
||||
),
|
||||
Image.ANTIALIAS, # type: ignore
|
||||
)
|
||||
await card_img.paste(label)
|
||||
|
||||
card_imgs.append(card_img)
|
||||
|
||||
# img_w = card_imgs[0].width
|
||||
# img_h = card_imgs[0].height
|
||||
# if len(card_imgs) < num_per_line:
|
||||
# w = img_w * len(card_imgs)
|
||||
# else:
|
||||
# w = img_w * num_per_line
|
||||
# h = img_h * math.ceil(len(card_imgs) / num_per_line)
|
||||
# img = BuildImage(w, h, img_w, img_h, color=self.game_card_color)
|
||||
# for card_img in card_imgs:
|
||||
# await img.paste(card_img)
|
||||
return await BuildImage.auto_paste(card_imgs, 10, color=self.game_card_color) # type: ignore
|
||||
|
||||
async def generate_card_img(self, card: TC) -> BuildImage:
|
||||
img = str(self.img_path / f"{cn2py(card.name)}.png")
|
||||
return BuildImage(100, 100, background=img)
|
||||
|
||||
def load_data(self, filename: str = "") -> dict:
|
||||
if not filename:
|
||||
filename = f"{self.game_name}.json"
|
||||
filepath = self.data_path / filename
|
||||
if not filepath.exists():
|
||||
return {}
|
||||
with filepath.open("r", encoding="utf8") as f:
|
||||
return json.load(f)
|
||||
|
||||
def dump_data(self, data: dict, filename: str = ""):
|
||||
if not filename:
|
||||
filename = f"{self.game_name}.json"
|
||||
filepath = self.data_path / filename
|
||||
with filepath.open("w", encoding="utf8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
def data_exists(self) -> bool:
|
||||
for file in self.data_files:
|
||||
if not (self.data_path / file).exists():
|
||||
return False
|
||||
return True
|
||||
|
||||
def _init_data(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def init_data(self):
|
||||
try:
|
||||
self._init_data()
|
||||
except Exception as e:
|
||||
logger.warning(f"{self.game_name_cn} 导入角色数据错误:{type(e)}:{e}")
|
||||
|
||||
async def _update_info(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def client(self) -> aiohttp.ClientSession:
|
||||
headers = {
|
||||
"User-Agent": '"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)"'
|
||||
}
|
||||
return aiohttp.ClientSession(headers=headers)
|
||||
|
||||
async def update_info(self):
|
||||
try:
|
||||
async with asyncio.Semaphore(draw_config.SEMAPHORE):
|
||||
async with self.client() as session:
|
||||
self.session = session
|
||||
await self._update_info()
|
||||
except Exception as e:
|
||||
logger.warning(f"{self.game_name_cn} 更新数据错误:{type(e)}:{e}")
|
||||
self.init_data()
|
||||
|
||||
async def get_url(self, url: str) -> str:
|
||||
result = ""
|
||||
retry = 5
|
||||
for i in range(retry):
|
||||
try:
|
||||
async with self.session.get(url, timeout=10) as response:
|
||||
result = await response.text()
|
||||
break
|
||||
except TimeoutError:
|
||||
logger.warning(f"访问 {url} 超时, 重试 {i + 1}/{retry}")
|
||||
await asyncio.sleep(1)
|
||||
return result
|
||||
|
||||
async def download_img(self, url: str, name: str) -> bool:
|
||||
img_path = self.img_path / f"{cn2py(name)}.png"
|
||||
if img_path.exists():
|
||||
return True
|
||||
try:
|
||||
async with self.session.get(url, timeout=10) as response:
|
||||
async with await anyio.open_file(img_path, "wb") as f:
|
||||
await f.write(await response.read())
|
||||
return True
|
||||
except TimeoutError:
|
||||
logger.warning(
|
||||
f"下载 {self.game_name_cn} 图片超时,名称:{name},url:{url}"
|
||||
)
|
||||
return False
|
||||
except:
|
||||
logger.warning(
|
||||
f"下载 {self.game_name_cn} 链接错误,名称:{name},url:{url}"
|
||||
)
|
||||
return False
|
||||
|
||||
async def _reload_pool(self) -> UniMessage | None:
|
||||
return None
|
||||
|
||||
async def reload_pool(self) -> UniMessage | None:
|
||||
try:
|
||||
async with self.client() as session:
|
||||
self.session = session
|
||||
return await self._reload_pool()
|
||||
except Exception as e:
|
||||
logger.warning(f"{self.game_name_cn} 重载UP池错误", e=e)
|
||||
|
||||
def reset_count(self, user_id: str) -> bool:
|
||||
return False
|
||||
@@ -1,223 +0,0 @@
|
||||
import random
|
||||
|
||||
import ujson as json
|
||||
from lxml import etree
|
||||
from PIL import ImageDraw
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
|
||||
from ..config import draw_config
|
||||
from ..util import cn2py, load_font, remove_prohibited_str
|
||||
from .base_handle import BaseData, BaseHandle
|
||||
|
||||
|
||||
class FgoData(BaseData):
|
||||
pass
|
||||
|
||||
|
||||
class FgoChar(FgoData):
|
||||
pass
|
||||
|
||||
|
||||
class FgoCard(FgoData):
|
||||
pass
|
||||
|
||||
|
||||
class FgoHandle(BaseHandle[FgoData]):
|
||||
def __init__(self):
|
||||
super().__init__("fgo", "命运-冠位指定")
|
||||
self.data_files.append("fgo_card.json")
|
||||
self.max_star = 5
|
||||
self.config = draw_config.fgo
|
||||
self.ALL_CHAR: list[FgoChar] = []
|
||||
self.ALL_CARD: list[FgoCard] = []
|
||||
|
||||
def get_card(self, mode: int = 1) -> FgoData:
|
||||
if mode == 1:
|
||||
star = self.get_star(
|
||||
[8, 7, 6, 5, 4, 3],
|
||||
[
|
||||
self.config.FGO_SERVANT_FIVE_P,
|
||||
self.config.FGO_SERVANT_FOUR_P,
|
||||
self.config.FGO_SERVANT_THREE_P,
|
||||
self.config.FGO_CARD_FIVE_P,
|
||||
self.config.FGO_CARD_FOUR_P,
|
||||
self.config.FGO_CARD_THREE_P,
|
||||
],
|
||||
)
|
||||
elif mode == 2:
|
||||
star = self.get_star(
|
||||
[5, 4], [self.config.FGO_CARD_FIVE_P, self.config.FGO_CARD_FOUR_P]
|
||||
)
|
||||
else:
|
||||
star = self.get_star(
|
||||
[8, 7, 6],
|
||||
[
|
||||
self.config.FGO_SERVANT_FIVE_P,
|
||||
self.config.FGO_SERVANT_FOUR_P,
|
||||
self.config.FGO_SERVANT_THREE_P,
|
||||
],
|
||||
)
|
||||
if star > 5:
|
||||
star -= 3
|
||||
chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited]
|
||||
else:
|
||||
chars = [x for x in self.ALL_CARD if x.star == star and not x.limited]
|
||||
return random.choice(chars)
|
||||
|
||||
def get_cards(self, count: int, **kwargs) -> list[tuple[FgoData, int]]:
|
||||
card_list = [] # 获取所有角色
|
||||
servant_count = 0 # 保底计算
|
||||
card_count = 0 # 保底计算
|
||||
for i in range(count):
|
||||
servant_count += 1
|
||||
card_count += 1
|
||||
if card_count == 9: # 四星卡片保底
|
||||
mode = 2
|
||||
elif servant_count == 10: # 三星从者保底
|
||||
mode = 3
|
||||
else: # 普通抽
|
||||
mode = 1
|
||||
card = self.get_card(mode)
|
||||
if isinstance(card, FgoCard) and card.star > self.max_star - 2:
|
||||
card_count = 0
|
||||
if isinstance(card, FgoChar):
|
||||
servant_count = 0
|
||||
card_list.append((card, i + 1))
|
||||
return card_list
|
||||
|
||||
async def generate_card_img(self, card: FgoData) -> BuildImage:
|
||||
sep_w = 5
|
||||
sep_t = 5
|
||||
sep_b = 20
|
||||
w = 128
|
||||
h = 140
|
||||
bg = BuildImage(w + sep_w * 2, h + sep_t + sep_b)
|
||||
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
|
||||
img = BuildImage(w, h, background=img_path)
|
||||
await bg.paste(img, (sep_w, sep_t))
|
||||
# 加名字
|
||||
text = card.name[:6] + "..." if len(card.name) > 7 else card.name
|
||||
font = load_font(fontsize=16)
|
||||
text_w, text_h = BuildImage.get_text_size(text, font)
|
||||
draw = ImageDraw.Draw(bg.markImg)
|
||||
draw.text(
|
||||
(sep_w + (w - text_w) / 2, h + sep_t + (sep_b - text_h) / 2),
|
||||
text,
|
||||
font=font,
|
||||
fill="gray",
|
||||
)
|
||||
return bg
|
||||
|
||||
def _init_data(self):
|
||||
self.ALL_CHAR = [
|
||||
FgoChar(
|
||||
name=value["名称"],
|
||||
star=int(value["星级"]),
|
||||
limited=(
|
||||
True
|
||||
if not (
|
||||
"圣晶石召唤" in value["入手方式"]
|
||||
or "圣晶石召唤(Story卡池)" in value["入手方式"]
|
||||
)
|
||||
else False
|
||||
),
|
||||
)
|
||||
for value in self.load_data().values()
|
||||
]
|
||||
self.ALL_CARD = [
|
||||
FgoCard(name=value["名称"], star=int(value["星级"]), limited=False)
|
||||
for value in self.load_data("fgo_card.json").values()
|
||||
]
|
||||
|
||||
async def _update_info(self):
|
||||
# TODO: fgo获取链接失效
|
||||
fgo_info = {}
|
||||
for i in range(500):
|
||||
url = f"http://fgo.vgtime.com/servant/ajax?card=&wd=&ids=&sort=12777&o=desc&pn={i}"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"更新 {self.game_name_cn} page {i} 出错")
|
||||
continue
|
||||
fgo_data = json.loads(result)
|
||||
if int(fgo_data["nums"]) <= 0:
|
||||
break
|
||||
for x in fgo_data["data"]:
|
||||
name = remove_prohibited_str(x["name"])
|
||||
member_dict = {
|
||||
"id": x["id"],
|
||||
"card_id": x["charid"],
|
||||
"头像": x["icon"],
|
||||
"名称": remove_prohibited_str(x["name"]),
|
||||
"职阶": x["classes"],
|
||||
"星级": int(x["star"]),
|
||||
"hp": x["lvmax4hp"],
|
||||
"atk": x["lvmax4atk"],
|
||||
"card_quick": x["cardquick"],
|
||||
"card_arts": x["cardarts"],
|
||||
"card_buster": x["cardbuster"],
|
||||
"宝具": x["tprop"],
|
||||
}
|
||||
fgo_info[name] = member_dict
|
||||
# 更新额外信息
|
||||
for key in fgo_info.keys():
|
||||
url = f'http://fgo.vgtime.com/servant/{fgo_info[key]["id"]}'
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
fgo_info[key]["入手方式"] = ["圣晶石召唤"]
|
||||
logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
|
||||
continue
|
||||
try:
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
obtain = dom.xpath(
|
||||
"//table[contains(string(.),'入手方式')]/tr[8]/td[3]/text()"
|
||||
)[0]
|
||||
obtain = str(obtain).strip()
|
||||
if "限时活动免费获取 活动结束后无法获得" in obtain:
|
||||
obtain = ["活动获取"]
|
||||
elif "非限时UP无法获得" in obtain:
|
||||
obtain = ["限时召唤"]
|
||||
else:
|
||||
if "&" in obtain:
|
||||
obtain = obtain.split("&")
|
||||
else:
|
||||
obtain = obtain.split(" ")
|
||||
obtain = [s.strip() for s in obtain if s.strip()]
|
||||
fgo_info[key]["入手方式"] = obtain
|
||||
except IndexError:
|
||||
fgo_info[key]["入手方式"] = ["圣晶石召唤"]
|
||||
logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
|
||||
self.dump_data(fgo_info)
|
||||
logger.info(f"{self.game_name_cn} 更新成功")
|
||||
# fgo_card.json
|
||||
fgo_card_info = {}
|
||||
for i in range(500):
|
||||
url = f"http://fgo.vgtime.com/equipment/ajax?wd=&ids=&sort=12958&o=desc&pn={i}"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"更新 {self.game_name_cn}卡牌 page {i} 出错")
|
||||
continue
|
||||
fgo_data = json.loads(result)
|
||||
if int(fgo_data["nums"]) <= 0:
|
||||
break
|
||||
for x in fgo_data["data"]:
|
||||
name = remove_prohibited_str(x["name"])
|
||||
member_dict = {
|
||||
"id": x["id"],
|
||||
"card_id": x["equipid"],
|
||||
"头像": x["icon"],
|
||||
"名称": name,
|
||||
"星级": int(x["star"]),
|
||||
"hp": x["lvmax_hp"],
|
||||
"atk": x["lvmax_atk"],
|
||||
"skill_e": str(x["skill_e"]).split("<br />")[:-1],
|
||||
}
|
||||
fgo_card_info[name] = member_dict
|
||||
self.dump_data(fgo_card_info, "fgo_card.json")
|
||||
logger.info(f"{self.game_name_cn} 卡牌更新成功")
|
||||
# 下载头像
|
||||
for value in fgo_info.values():
|
||||
await self.download_img(value["头像"], value["名称"])
|
||||
for value in fgo_card_info.values():
|
||||
await self.download_img(value["头像"], value["名称"])
|
||||
@@ -1,461 +0,0 @@
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import unquote
|
||||
|
||||
import dateparser
|
||||
import ujson as json
|
||||
from lxml import etree
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
from PIL import Image, ImageDraw
|
||||
from pydantic import ValidationError
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ..config import draw_config
|
||||
from ..count_manager import GenshinCountManager
|
||||
from ..util import cn2py, load_font, remove_prohibited_str
|
||||
from .base_handle import BaseData, BaseHandle, UpChar, UpEvent
|
||||
|
||||
|
||||
class GenshinData(BaseData):
|
||||
pass
|
||||
|
||||
|
||||
class GenshinChar(GenshinData):
|
||||
pass
|
||||
|
||||
|
||||
class GenshinArms(GenshinData):
|
||||
pass
|
||||
|
||||
|
||||
class GenshinHandle(BaseHandle[GenshinData]):
|
||||
def __init__(self):
|
||||
super().__init__("genshin", "原神")
|
||||
self.data_files.append("genshin_arms.json")
|
||||
self.max_star = 5
|
||||
self.game_card_color = "#ebebeb"
|
||||
self.config = draw_config.genshin
|
||||
|
||||
self.ALL_CHAR: list[GenshinData] = []
|
||||
self.ALL_ARMS: list[GenshinData] = []
|
||||
self.UP_CHAR: UpEvent | None = None
|
||||
self.UP_CHAR_LIST: UpEvent | None = []
|
||||
self.UP_ARMS: UpEvent | None = None
|
||||
|
||||
self.count_manager = GenshinCountManager((10, 90), ("4", "5"), 180)
|
||||
|
||||
# 抽取卡池
|
||||
def get_card(
|
||||
self,
|
||||
pool_name: str,
|
||||
mode: int = 1,
|
||||
add: float = 0.0,
|
||||
is_up: bool = False,
|
||||
card_index: int = 0,
|
||||
):
|
||||
"""
|
||||
mode 1:普通抽 2:四星保底 3:五星保底
|
||||
"""
|
||||
if mode == 1:
|
||||
star = self.get_star(
|
||||
[5, 4, 3],
|
||||
[
|
||||
self.config.GENSHIN_FIVE_P + add,
|
||||
self.config.GENSHIN_FOUR_P,
|
||||
self.config.GENSHIN_THREE_P,
|
||||
],
|
||||
)
|
||||
elif mode == 2:
|
||||
star = self.get_star(
|
||||
[5, 4],
|
||||
[self.config.GENSHIN_G_FIVE_P + add, self.config.GENSHIN_G_FOUR_P],
|
||||
)
|
||||
else:
|
||||
star = 5
|
||||
|
||||
if pool_name == "char":
|
||||
up_event = self.UP_CHAR_LIST[card_index]
|
||||
all_list = self.ALL_CHAR + [
|
||||
x for x in self.ALL_ARMS if x.star == star and x.star < 5
|
||||
]
|
||||
elif pool_name == "arms":
|
||||
up_event = self.UP_ARMS
|
||||
all_list = self.ALL_ARMS + [
|
||||
x for x in self.ALL_CHAR if x.star == star and x.star < 5
|
||||
]
|
||||
else:
|
||||
up_event = None
|
||||
all_list = self.ALL_ARMS + self.ALL_CHAR
|
||||
|
||||
acquire_char = None
|
||||
# 是否UP
|
||||
if up_event and star > 3:
|
||||
# 获取up角色列表
|
||||
up_list = [x.name for x in up_event.up_char if x.star == star]
|
||||
# 成功获取up角色
|
||||
if random.random() < 0.5 or is_up:
|
||||
up_name = random.choice(up_list)
|
||||
try:
|
||||
acquire_char = [x for x in all_list if x.name == up_name][0]
|
||||
except IndexError:
|
||||
pass
|
||||
if not acquire_char:
|
||||
chars = [x for x in all_list if x.star == star and not x.limited]
|
||||
acquire_char = random.choice(chars)
|
||||
return acquire_char
|
||||
|
||||
def get_cards(
|
||||
self, count: int, user_id: int, pool_name: str, card_index: int = 0
|
||||
) -> list[tuple[GenshinData, int]]:
|
||||
card_list = [] # 获取角色列表
|
||||
add = 0.0
|
||||
count_manager = self.count_manager
|
||||
count_manager.check_count(user_id, count) # 检查次数累计
|
||||
pool = self.UP_CHAR_LIST[card_index] if pool_name == "char" else self.UP_ARMS
|
||||
for i in range(count):
|
||||
count_manager.increase(user_id)
|
||||
star = count_manager.check(user_id) # 是否有四星或五星保底
|
||||
if (
|
||||
count_manager.get_user_count(user_id)
|
||||
- count_manager.get_user_five_index(user_id)
|
||||
) % count_manager.get_max_guarantee() >= 72:
|
||||
add += draw_config.genshin.I72_ADD
|
||||
if star:
|
||||
if star == 4:
|
||||
card = self.get_card(pool_name, 2, add=add, card_index=card_index)
|
||||
else:
|
||||
card = self.get_card(
|
||||
pool_name,
|
||||
3,
|
||||
add,
|
||||
count_manager.is_up(user_id),
|
||||
card_index=card_index,
|
||||
)
|
||||
else:
|
||||
card = self.get_card(
|
||||
pool_name,
|
||||
1,
|
||||
add,
|
||||
count_manager.is_up(user_id),
|
||||
card_index=card_index,
|
||||
)
|
||||
# print(f"{count_manager.get_user_count(user_id)}:",
|
||||
# count_manager.get_user_five_index(user_id), star, card.star, add)
|
||||
# 四星角色
|
||||
if card.star == 4:
|
||||
count_manager.mark_four_index(user_id)
|
||||
# 五星角色
|
||||
elif card.star == self.max_star:
|
||||
add = 0
|
||||
count_manager.mark_five_index(user_id) # 记录五星保底
|
||||
count_manager.mark_four_index(user_id) # 记录四星保底
|
||||
if pool and card.name in [
|
||||
x.name for x in pool.up_char if x.star == self.max_star
|
||||
]:
|
||||
count_manager.set_is_up(user_id, True)
|
||||
else:
|
||||
count_manager.set_is_up(user_id, False)
|
||||
card_list.append((card, count_manager.get_user_count(user_id)))
|
||||
return card_list
|
||||
|
||||
async def generate_card_img(self, card: GenshinData) -> BuildImage:
|
||||
sep_w = 10
|
||||
sep_h = 5
|
||||
frame_w = 112
|
||||
frame_h = 132
|
||||
img_w = 106
|
||||
img_h = 106
|
||||
bg = BuildImage(frame_w + sep_w * 2, frame_h + sep_h * 2, color="#EBEBEB")
|
||||
frame_path = str(self.img_path / "avatar_frame.png")
|
||||
frame = Image.open(frame_path)
|
||||
# 加名字
|
||||
text = card.name
|
||||
font = load_font(fontsize=14)
|
||||
text_w, text_h = BuildImage.get_text_size(text, font)
|
||||
draw = ImageDraw.Draw(frame)
|
||||
draw.text(
|
||||
((frame_w - text_w) / 2, frame_h - 15 - text_h / 2),
|
||||
text,
|
||||
font=font,
|
||||
fill="gray",
|
||||
)
|
||||
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
|
||||
img = BuildImage(img_w, img_h, background=img_path)
|
||||
if isinstance(card, GenshinArms):
|
||||
# 武器卡背景不是透明的,切去上方两个圆弧
|
||||
r = 12
|
||||
circle = Image.new("L", (r * 2, r * 2), 0)
|
||||
alpha = Image.new("L", img.size, 255)
|
||||
alpha.paste(circle, (-r - 3, -r - 3)) # 左上角
|
||||
alpha.paste(circle, (img_h - r + 3, -r - 3)) # 右上角
|
||||
img.markImg.putalpha(alpha)
|
||||
star_path = str(self.img_path / f"{card.star}_star.png")
|
||||
star = Image.open(star_path)
|
||||
await bg.paste(frame, (sep_w, sep_h))
|
||||
await bg.paste(img, (sep_w + 3, sep_h + 3))
|
||||
await bg.paste(star, (sep_w + int((frame_w - star.width) / 2), sep_h - 6))
|
||||
return bg
|
||||
|
||||
def format_pool_info(self, pool_name: str, card_index: int = 0) -> str:
|
||||
info = ""
|
||||
up_event = None
|
||||
if pool_name == "char":
|
||||
up_event = self.UP_CHAR_LIST[card_index]
|
||||
elif pool_name == "arms":
|
||||
up_event = self.UP_ARMS
|
||||
if up_event:
|
||||
star5_list = [x.name for x in up_event.up_char if x.star == 5]
|
||||
star4_list = [x.name for x in up_event.up_char if x.star == 4]
|
||||
if star5_list:
|
||||
info += f"五星UP:{' '.join(star5_list)}\n"
|
||||
if star4_list:
|
||||
info += f"四星UP:{' '.join(star4_list)}\n"
|
||||
info = f"当前up池:{up_event.title}\n{info}"
|
||||
return info.strip()
|
||||
|
||||
async def draw(
|
||||
self, count: int, user_id: int, pool_name: str = "", **kwargs
|
||||
) -> UniMessage:
|
||||
card_index = 0
|
||||
if "1" in pool_name:
|
||||
card_index = 1
|
||||
pool_name = pool_name.replace("1", "")
|
||||
index2cards = self.get_cards(count, user_id, pool_name, card_index)
|
||||
cards = [card[0] for card in index2cards]
|
||||
up_event = None
|
||||
if pool_name == "char":
|
||||
if card_index == 1 and len(self.UP_CHAR_LIST) == 1:
|
||||
return MessageUtils.build_message("当前没有第二个角色UP池")
|
||||
up_event = self.UP_CHAR_LIST[card_index]
|
||||
elif pool_name == "arms":
|
||||
up_event = self.UP_ARMS
|
||||
up_list = [x.name for x in up_event.up_char] if up_event else []
|
||||
result = self.format_star_result(cards)
|
||||
result += (
|
||||
"\n" + max_star_str
|
||||
if (max_star_str := self.format_max_star(index2cards, up_list=up_list))
|
||||
else ""
|
||||
)
|
||||
result += f"\n距离保底发还剩 {self.count_manager.get_user_guarantee_count(user_id)} 抽"
|
||||
# result += "\n【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】"
|
||||
pool_info = self.format_pool_info(pool_name, card_index)
|
||||
img = await self.generate_img(cards)
|
||||
bk = BuildImage(img.width, img.height + 50, font_size=20, color="#ebebeb")
|
||||
await bk.paste(img)
|
||||
await bk.text(
|
||||
(0, img.height + 10),
|
||||
"【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】",
|
||||
)
|
||||
return MessageUtils.build_message([pool_info, bk, result])
|
||||
|
||||
def _init_data(self):
|
||||
self.ALL_CHAR = [
|
||||
GenshinChar(
|
||||
name=value["名称"],
|
||||
star=int(value["星级"]),
|
||||
limited=value["常驻/限定"] == "限定UP",
|
||||
)
|
||||
for key, value in self.load_data().items()
|
||||
if "旅行者" not in key
|
||||
]
|
||||
self.ALL_ARMS = [
|
||||
GenshinArms(
|
||||
name=value["名称"],
|
||||
star=int(value["星级"]),
|
||||
limited="祈愿" not in value["获取途径"],
|
||||
)
|
||||
for value in self.load_data("genshin_arms.json").values()
|
||||
]
|
||||
self.load_up_char()
|
||||
|
||||
def load_up_char(self):
|
||||
try:
|
||||
data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json")
|
||||
self.UP_CHAR_LIST.append(UpEvent.parse_obj(data.get("char", {})))
|
||||
self.UP_CHAR_LIST.append(UpEvent.parse_obj(data.get("char1", {})))
|
||||
self.UP_ARMS = UpEvent.parse_obj(data.get("arms", {}))
|
||||
except ValidationError:
|
||||
logger.warning(f"{self.game_name}_up_char 解析出错")
|
||||
|
||||
def dump_up_char(self):
|
||||
if self.UP_CHAR_LIST and self.UP_ARMS:
|
||||
data = {
|
||||
"char": json.loads(self.UP_CHAR_LIST[0].json()),
|
||||
"arms": json.loads(self.UP_ARMS.json()),
|
||||
}
|
||||
if len(self.UP_CHAR_LIST) > 1:
|
||||
data["char1"] = json.loads(self.UP_CHAR_LIST[1].json())
|
||||
self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
|
||||
|
||||
async def _update_info(self):
|
||||
# genshin.json
|
||||
char_info = {}
|
||||
url = "https://wiki.biligame.com/ys/角色筛选"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"更新 {self.game_name_cn} 出错")
|
||||
else:
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
|
||||
for char in char_list:
|
||||
try:
|
||||
name = char.xpath("./td[1]/a/@title")[0]
|
||||
avatar = char.xpath("./td[1]/a/img/@srcset")[0]
|
||||
star = char.xpath("./td[3]/text()")[0]
|
||||
except IndexError:
|
||||
continue
|
||||
member_dict = {
|
||||
"头像": unquote(str(avatar).split(" ")[-2]),
|
||||
"名称": remove_prohibited_str(name),
|
||||
"星级": int(str(star).strip()[:1]),
|
||||
}
|
||||
char_info[member_dict["名称"]] = member_dict
|
||||
# 更新额外信息
|
||||
for key in char_info.keys():
|
||||
result = await self.get_url(f"https://wiki.biligame.com/ys/{key}")
|
||||
if not result:
|
||||
char_info[key]["常驻/限定"] = "未知"
|
||||
logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
|
||||
continue
|
||||
try:
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
limit = dom.xpath(
|
||||
"//table[contains(string(.),'常驻/限定')]/tbody/tr[6]/td/text()"
|
||||
)[0]
|
||||
char_info[key]["常驻/限定"] = str(limit).strip()
|
||||
except IndexError:
|
||||
char_info[key]["常驻/限定"] = "未知"
|
||||
logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
|
||||
self.dump_data(char_info)
|
||||
logger.info(f"{self.game_name_cn} 更新成功")
|
||||
# genshin_arms.json
|
||||
arms_info = {}
|
||||
url = "https://wiki.biligame.com/ys/武器图鉴"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"更新 {self.game_name_cn} 出错")
|
||||
else:
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
|
||||
for char in char_list:
|
||||
try:
|
||||
name = char.xpath("./td[1]/a/@title")[0]
|
||||
avatar = char.xpath("./td[1]/a/img/@srcset")[0]
|
||||
star = char.xpath("./td[4]/img/@alt")[0]
|
||||
sources = str(char.xpath("./td[5]/text()")[0]).split(",")
|
||||
except IndexError:
|
||||
continue
|
||||
member_dict = {
|
||||
"头像": unquote(str(avatar).split(" ")[-2]),
|
||||
"名称": remove_prohibited_str(name),
|
||||
"星级": int(str(star).strip()[:1]),
|
||||
"获取途径": [s.strip() for s in sources if s.strip()],
|
||||
}
|
||||
arms_info[member_dict["名称"]] = member_dict
|
||||
self.dump_data(arms_info, "genshin_arms.json")
|
||||
logger.info(f"{self.game_name_cn} 武器更新成功")
|
||||
# 下载头像
|
||||
for value in char_info.values():
|
||||
await self.download_img(value["头像"], value["名称"])
|
||||
for value in arms_info.values():
|
||||
await self.download_img(value["头像"], value["名称"])
|
||||
# 下载星星
|
||||
idx = 1
|
||||
YS_URL = "https://patchwiki.biligame.com/images/ys"
|
||||
for url in [
|
||||
"/1/13/7xzg7tgf8dsr2hjpmdbm5gn9wvzt2on.png",
|
||||
"/b/bc/sd2ige6d7lvj7ugfumue3yjg8gyi0d1.png",
|
||||
"/e/ec/l3mnhy56pyailhn3v7r873htf2nofau.png",
|
||||
"/9/9c/sklp02ffk3aqszzvh8k1c3139s0awpd.png",
|
||||
"/c/c7/qu6xcndgj6t14oxvv7yz2warcukqv1m.png",
|
||||
]:
|
||||
await self.download_img(YS_URL + url, f"{idx}_star")
|
||||
idx += 1
|
||||
# 下载头像框
|
||||
await self.download_img(
|
||||
YS_URL + "/2/2e/opbcst4xbtcq0i4lwerucmosawn29ti.png", f"avatar_frame"
|
||||
)
|
||||
await self.update_up_char()
|
||||
|
||||
async def update_up_char(self):
|
||||
self.UP_CHAR_LIST = []
|
||||
url = "https://wiki.biligame.com/ys/祈愿"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"{self.game_name_cn}获取祈愿页面出错")
|
||||
return
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
tables = dom.xpath(
|
||||
"//div[@class='mw-parser-output']/div[@class='row']/div/table[@class='wikitable']/tbody"
|
||||
)
|
||||
if not tables or len(tables) < 2:
|
||||
logger.warning(f"{self.game_name_cn}获取活动祈愿出错")
|
||||
return
|
||||
try:
|
||||
for index, table in enumerate(tables):
|
||||
title = table.xpath("./tr[1]/th/img/@title")[0]
|
||||
title = str(title).split("」")[0] + "」" if "」" in title else title
|
||||
pool_img = str(table.xpath("./tr[1]/th/img/@srcset")[0]).split(" ")[-2]
|
||||
time = table.xpath("./tr[2]/td/text()")[0]
|
||||
star5_list = table.xpath("./tr[3]/td/a/@title")
|
||||
star4_list = table.xpath("./tr[4]/td/a/@title")
|
||||
start, end = str(time).split("~")
|
||||
start_time = dateparser.parse(start)
|
||||
end_time = dateparser.parse(end)
|
||||
if not start_time and end_time:
|
||||
start_time = end_time - timedelta(days=20)
|
||||
if start_time and end_time and start_time <= datetime.now() <= end_time:
|
||||
up_event = UpEvent(
|
||||
title=title,
|
||||
pool_img=pool_img,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
up_char=[
|
||||
UpChar(name=name, star=5, limited=False, zoom=50)
|
||||
for name in star5_list
|
||||
]
|
||||
+ [
|
||||
UpChar(name=name, star=4, limited=False, zoom=50)
|
||||
for name in star4_list
|
||||
],
|
||||
)
|
||||
if "神铸赋形" not in title:
|
||||
self.UP_CHAR_LIST.append(up_event)
|
||||
else:
|
||||
self.UP_ARMS = up_event
|
||||
if self.UP_CHAR_LIST and self.UP_ARMS:
|
||||
self.dump_up_char()
|
||||
char_title = " & ".join([x.title for x in self.UP_CHAR_LIST])
|
||||
logger.info(
|
||||
f"成功获取{self.game_name_cn}当前up信息...当前up池: {char_title} & {self.UP_ARMS.title}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"{self.game_name_cn}UP更新出错", e=e)
|
||||
|
||||
def reset_count(self, user_id: str) -> bool:
|
||||
self.count_manager.reset(user_id)
|
||||
return True
|
||||
|
||||
async def _reload_pool(self) -> UniMessage | None:
|
||||
await self.update_up_char()
|
||||
self.load_up_char()
|
||||
if self.UP_CHAR_LIST and self.UP_ARMS:
|
||||
if len(self.UP_CHAR_LIST) > 1:
|
||||
return MessageUtils.build_message(
|
||||
[
|
||||
f"重载成功!\n当前UP池子:{self.UP_CHAR_LIST[0].title} & {self.UP_CHAR_LIST[1].title} & {self.UP_ARMS.title}",
|
||||
self.UP_CHAR_LIST[0].pool_img,
|
||||
self.UP_CHAR_LIST[1].pool_img,
|
||||
self.UP_ARMS.pool_img,
|
||||
]
|
||||
)
|
||||
return UniMessage(
|
||||
[
|
||||
f"重载成功!\n当前UP池子:{char_title} & {self.UP_ARMS.title}",
|
||||
self.UP_CHAR_LIST[0].pool_img,
|
||||
self.UP_ARMS.pool_img,
|
||||
]
|
||||
)
|
||||
@@ -1,400 +0,0 @@
|
||||
import random
|
||||
import re
|
||||
from datetime import datetime
|
||||
from urllib.parse import unquote
|
||||
|
||||
import dateparser
|
||||
import ujson as json
|
||||
from lxml import etree
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
from PIL import ImageDraw
|
||||
from pydantic import ValidationError
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ..config import draw_config
|
||||
from ..util import cn2py, load_font, remove_prohibited_str
|
||||
from .base_handle import BaseData, BaseHandle, UpChar, UpEvent
|
||||
|
||||
|
||||
class GuardianData(BaseData):
|
||||
pass
|
||||
|
||||
|
||||
class GuardianChar(GuardianData):
|
||||
pass
|
||||
|
||||
|
||||
class GuardianArms(GuardianData):
|
||||
pass
|
||||
|
||||
|
||||
class GuardianHandle(BaseHandle[GuardianData]):
|
||||
def __init__(self):
|
||||
super().__init__("guardian", "坎公骑冠剑")
|
||||
self.data_files.append("guardian_arms.json")
|
||||
self.config = draw_config.guardian
|
||||
|
||||
self.ALL_CHAR: list[GuardianChar] = []
|
||||
self.ALL_ARMS: list[GuardianArms] = []
|
||||
self.UP_CHAR: UpEvent | None = None
|
||||
self.UP_ARMS: UpEvent | None = None
|
||||
|
||||
def get_card(self, pool_name: str, mode: int = 1) -> GuardianData:
|
||||
if pool_name == "char":
|
||||
if mode == 1:
|
||||
star = self.get_star(
|
||||
[3, 2, 1],
|
||||
[
|
||||
self.config.GUARDIAN_THREE_CHAR_P,
|
||||
self.config.GUARDIAN_TWO_CHAR_P,
|
||||
self.config.GUARDIAN_ONE_CHAR_P,
|
||||
],
|
||||
)
|
||||
else:
|
||||
star = self.get_star(
|
||||
[3, 2],
|
||||
[
|
||||
self.config.GUARDIAN_THREE_CHAR_P,
|
||||
self.config.GUARDIAN_TWO_CHAR_P,
|
||||
],
|
||||
)
|
||||
up_event = self.UP_CHAR
|
||||
self.max_star = 3
|
||||
all_data = self.ALL_CHAR
|
||||
else:
|
||||
if mode == 1:
|
||||
star = self.get_star(
|
||||
[5, 4, 3, 2],
|
||||
[
|
||||
self.config.GUARDIAN_FIVE_ARMS_P,
|
||||
self.config.GUARDIAN_FOUR_ARMS_P,
|
||||
self.config.GUARDIAN_THREE_ARMS_P,
|
||||
self.config.GUARDIAN_TWO_ARMS_P,
|
||||
],
|
||||
)
|
||||
else:
|
||||
star = self.get_star(
|
||||
[5, 4],
|
||||
[
|
||||
self.config.GUARDIAN_FIVE_ARMS_P,
|
||||
self.config.GUARDIAN_FOUR_ARMS_P,
|
||||
],
|
||||
)
|
||||
up_event = self.UP_ARMS
|
||||
self.max_star = 5
|
||||
all_data = self.ALL_ARMS
|
||||
|
||||
acquire_char = None
|
||||
# 是否UP
|
||||
if up_event and star == self.max_star and pool_name:
|
||||
# 获取up角色列表
|
||||
up_list = [x.name for x in up_event.up_char if x.star == star]
|
||||
# 成功获取up角色
|
||||
if random.random() < 0.5:
|
||||
up_name = random.choice(up_list)
|
||||
try:
|
||||
acquire_char = [x for x in all_data if x.name == up_name][0]
|
||||
except IndexError:
|
||||
pass
|
||||
if not acquire_char:
|
||||
chars = [x for x in all_data if x.star == star and not x.limited]
|
||||
acquire_char = random.choice(chars)
|
||||
return acquire_char
|
||||
|
||||
def get_cards(self, count: int, pool_name: str) -> list[tuple[GuardianData, int]]:
|
||||
card_list = []
|
||||
card_count = 0 # 保底计算
|
||||
for i in range(count):
|
||||
card_count += 1
|
||||
# 十连保底
|
||||
if card_count == 10:
|
||||
card = self.get_card(pool_name, 2)
|
||||
card_count = 0
|
||||
else:
|
||||
card = self.get_card(pool_name, 1)
|
||||
if card.star > self.max_star - 2:
|
||||
card_count = 0
|
||||
card_list.append((card, i + 1))
|
||||
return card_list
|
||||
|
||||
def format_pool_info(self, pool_name: str) -> str:
|
||||
info = ""
|
||||
up_event = self.UP_CHAR if pool_name == "char" else self.UP_ARMS
|
||||
if up_event:
|
||||
if pool_name == "char":
|
||||
up_list = [x.name for x in up_event.up_char if x.star == 3]
|
||||
info += f'三星UP:{" ".join(up_list)}\n'
|
||||
else:
|
||||
up_list = [x.name for x in up_event.up_char if x.star == 5]
|
||||
info += f'五星UP:{" ".join(up_list)}\n'
|
||||
info = f"当前up池:{up_event.title}\n{info}"
|
||||
return info.strip()
|
||||
|
||||
async def draw(self, count: int, pool_name: str, **kwargs) -> UniMessage:
|
||||
index2card = self.get_cards(count, pool_name)
|
||||
cards = [card[0] for card in index2card]
|
||||
up_event = self.UP_CHAR if pool_name == "char" else self.UP_ARMS
|
||||
up_list = [x.name for x in up_event.up_char] if up_event else []
|
||||
result = self.format_result(index2card, up_list=up_list)
|
||||
pool_info = self.format_pool_info(pool_name)
|
||||
img = await self.generate_img(cards)
|
||||
return MessageUtils.build_message([pool_info, img, result])
|
||||
|
||||
async def generate_card_img(self, card: GuardianData) -> BuildImage:
|
||||
sep_w = 1
|
||||
sep_h = 1
|
||||
block_w = 170
|
||||
block_h = 90
|
||||
img_w = 90
|
||||
img_h = 90
|
||||
if isinstance(card, GuardianChar):
|
||||
block_color = "#2e2923"
|
||||
font_color = "#e2ccad"
|
||||
star_w = 90
|
||||
star_h = 30
|
||||
star_name = f"{card.star}_star.png"
|
||||
frame_path = ""
|
||||
else:
|
||||
block_color = "#EEE4D5"
|
||||
font_color = "#A65400"
|
||||
star_w = 45
|
||||
star_h = 45
|
||||
star_name = f"{card.star}_star_rank.png"
|
||||
frame_path = str(self.img_path / "avatar_frame.png")
|
||||
bg = BuildImage(block_w + sep_w * 2, block_h + sep_h * 2, color="#F6F4ED")
|
||||
block = BuildImage(block_w, block_h, color=block_color)
|
||||
star_path = str(self.img_path / star_name)
|
||||
star = BuildImage(star_w, star_h, background=star_path)
|
||||
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
|
||||
img = BuildImage(img_w, img_h, background=img_path)
|
||||
await block.paste(img, (0, 0))
|
||||
if frame_path:
|
||||
frame = BuildImage(img_w, img_h, background=frame_path)
|
||||
await block.paste(frame, (0, 0))
|
||||
await block.paste(
|
||||
star,
|
||||
(int((block_w + img_w - star_w) / 2), block_h - star_h - 30),
|
||||
)
|
||||
# 加名字
|
||||
text = card.name[:4] + "..." if len(card.name) > 5 else card.name
|
||||
font = load_font(fontsize=14)
|
||||
text_w, _ = BuildImage.get_text_size(text, font)
|
||||
draw = ImageDraw.Draw(block.markImg)
|
||||
draw.text(
|
||||
((block_w + img_w - text_w) / 2, 55),
|
||||
text,
|
||||
font=font,
|
||||
fill=font_color,
|
||||
)
|
||||
await bg.paste(block, (sep_w, sep_h))
|
||||
return bg
|
||||
|
||||
def _init_data(self):
|
||||
self.ALL_CHAR = [
|
||||
GuardianChar(name=value["名称"], star=int(value["星级"]), limited=False)
|
||||
for value in self.load_data().values()
|
||||
]
|
||||
self.ALL_ARMS = [
|
||||
GuardianArms(name=value["名称"], star=int(value["星级"]), limited=False)
|
||||
for value in self.load_data("guardian_arms.json").values()
|
||||
]
|
||||
self.load_up_char()
|
||||
|
||||
def load_up_char(self):
|
||||
try:
|
||||
data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json")
|
||||
self.UP_CHAR = UpEvent.parse_obj(data.get("char", {}))
|
||||
self.UP_ARMS = UpEvent.parse_obj(data.get("arms", {}))
|
||||
except ValidationError:
|
||||
logger.warning(f"{self.game_name}_up_char 解析出错")
|
||||
|
||||
def dump_up_char(self):
|
||||
if self.UP_CHAR and self.UP_ARMS:
|
||||
data = {
|
||||
"char": json.loads(self.UP_CHAR.json()),
|
||||
"arms": json.loads(self.UP_ARMS.json()),
|
||||
}
|
||||
self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
|
||||
|
||||
async def _update_info(self):
|
||||
# guardian.json
|
||||
guardian_info = {}
|
||||
url = "https://wiki.biligame.com/gt/英雄筛选表"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"更新 {self.game_name_cn} 出错")
|
||||
else:
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
|
||||
for char in char_list:
|
||||
try:
|
||||
# name = char.xpath("./td[1]/a/@title")[0]
|
||||
# avatar = char.xpath("./td[1]/a/img/@src")[0]
|
||||
# star = char.xpath("./td[1]/span/img/@alt")[0]
|
||||
name = char.xpath("./th[1]/a[1]/@title")[0]
|
||||
avatar = char.xpath("./th[1]/a/img/@src")[0]
|
||||
star = char.xpath("./th[1]/span/img/@alt")[0]
|
||||
except IndexError:
|
||||
continue
|
||||
member_dict = {
|
||||
"头像": unquote(str(avatar)),
|
||||
"名称": remove_prohibited_str(name),
|
||||
"星级": int(str(star).split(" ")[0].replace("Rank", "")),
|
||||
}
|
||||
guardian_info[member_dict["名称"]] = member_dict
|
||||
self.dump_data(guardian_info)
|
||||
logger.info(f"{self.game_name_cn} 更新成功")
|
||||
# guardian_arms.json
|
||||
guardian_arms_info = {}
|
||||
url = "https://wiki.biligame.com/gt/武器"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"更新 {self.game_name_cn} 武器出错")
|
||||
else:
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
|
||||
for char in char_list:
|
||||
try:
|
||||
name = char.xpath("./td[2]/a/@title")[0]
|
||||
avatar = char.xpath("./td[1]/div/div/div/a/img/@src")[0]
|
||||
url = char.xpath("./td[3]/img/@srcset")[0]
|
||||
if r := re.search(r"Rank-mini-star_(\d).png", url):
|
||||
star = r.group(1)
|
||||
else:
|
||||
continue
|
||||
except IndexError:
|
||||
continue
|
||||
member_dict = {
|
||||
"头像": unquote(str(avatar)),
|
||||
"名称": remove_prohibited_str(name),
|
||||
"星级": int(str(star).strip()),
|
||||
}
|
||||
guardian_arms_info[member_dict["名称"]] = member_dict
|
||||
self.dump_data(guardian_arms_info, "guardian_arms.json")
|
||||
logger.info(f"{self.game_name_cn} 武器更新成功")
|
||||
url = "https://wiki.biligame.com/gt/盾牌"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"更新 {self.game_name_cn} 盾牌出错")
|
||||
else:
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
char_list = dom.xpath(
|
||||
"//div[@class='resp-tabs-container']/div[2]/div/table[1]/tbody/tr"
|
||||
)
|
||||
for char in char_list:
|
||||
try:
|
||||
name = char.xpath("./td[2]/a/@title")[0]
|
||||
avatar = char.xpath("./td[1]/div/div/div/a/img/@src")[0]
|
||||
star = char.xpath("./td[3]/text()")[0]
|
||||
except IndexError:
|
||||
continue
|
||||
member_dict = {
|
||||
"头像": unquote(str(avatar)),
|
||||
"名称": remove_prohibited_str(name),
|
||||
"星级": int(str(star).strip()),
|
||||
}
|
||||
guardian_arms_info[member_dict["名称"]] = member_dict
|
||||
self.dump_data(guardian_arms_info, "guardian_arms.json")
|
||||
logger.info(f"{self.game_name_cn} 盾牌更新成功")
|
||||
# 下载头像
|
||||
for value in guardian_info.values():
|
||||
await self.download_img(value["头像"], value["名称"])
|
||||
for value in guardian_arms_info.values():
|
||||
await self.download_img(value["头像"], value["名称"])
|
||||
# 下载星星
|
||||
idx = 1
|
||||
GT_URL = "https://patchwiki.biligame.com/images/gt"
|
||||
for url in [
|
||||
"/4/4b/ardr3bi2yf95u4zomm263tc1vke6i3i.png",
|
||||
"/5/55/6vow7lh76gzus6b2g9cfn325d1sugca.png",
|
||||
"/b/b9/du8egrd2vyewg0cuyra9t8jh0srl0ds.png",
|
||||
]:
|
||||
await self.download_img(GT_URL + url, f"{idx}_star")
|
||||
idx += 1
|
||||
# 另一种星星
|
||||
idx = 1
|
||||
for url in [
|
||||
"/6/66/4e2tfa9kvhfcbikzlyei76i9crva145.png",
|
||||
"/1/10/r9ihsuvycgvsseyneqz4xs22t53026m.png",
|
||||
"/7/7a/o0k86ru9k915y04azc26hilxead7xp1.png",
|
||||
"/c/c9/rxz99asysz0rg391j3b02ta09mnpa7v.png",
|
||||
"/2/2a/sfxz0ucv1s6ewxveycz9mnmrqs2rw60.png",
|
||||
]:
|
||||
await self.download_img(GT_URL + url, f"{idx}_star_rank")
|
||||
idx += 1
|
||||
# 头像框
|
||||
await self.download_img(
|
||||
GT_URL + "/8/8e/ogbqslbhuykjhnc8trtoa0p0nhfzohs.png", f"avatar_frame"
|
||||
)
|
||||
await self.update_up_char()
|
||||
|
||||
async def update_up_char(self):
|
||||
url = "https://wiki.biligame.com/gt/首页"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"{self.game_name_cn}获取公告出错")
|
||||
return
|
||||
try:
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
announcement = dom.xpath(
|
||||
"//div[@class='mw-parser-output']/div/div[3]/div[2]/div/div[2]/div[3]"
|
||||
)[0]
|
||||
title = announcement.xpath("./font/p/b/text()")[0]
|
||||
match = re.search(r"从(.*?)开始.*?至(.*?)结束", title)
|
||||
if not match:
|
||||
logger.warning(f"{self.game_name_cn}找不到UP时间")
|
||||
return
|
||||
start, end = match.groups()
|
||||
start_time = dateparser.parse(start.replace("月", "/").replace("日", ""))
|
||||
end_time = dateparser.parse(end.replace("月", "/").replace("日", ""))
|
||||
if not (start_time and end_time) or not (
|
||||
start_time <= datetime.now() <= end_time
|
||||
):
|
||||
return
|
||||
divs = announcement.xpath("./font/div")
|
||||
char_index = 0
|
||||
arms_index = 0
|
||||
for index, div in enumerate(divs):
|
||||
if div.xpath("string(.)") == "角色":
|
||||
char_index = index
|
||||
elif div.xpath("string(.)") == "武器":
|
||||
arms_index = index
|
||||
chars = divs[char_index + 1 : arms_index]
|
||||
arms = divs[arms_index + 1 :]
|
||||
up_chars = []
|
||||
up_arms = []
|
||||
for char in chars:
|
||||
name = char.xpath("./p/a/@title")[0]
|
||||
up_chars.append(UpChar(name=name, star=3, limited=False, zoom=0))
|
||||
for arm in arms:
|
||||
name = arm.xpath("./p/a/@title")[0]
|
||||
up_arms.append(UpChar(name=name, star=5, limited=False, zoom=0))
|
||||
self.UP_CHAR = UpEvent(
|
||||
title=title,
|
||||
pool_img="",
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
up_char=up_chars,
|
||||
)
|
||||
self.UP_ARMS = UpEvent(
|
||||
title=title,
|
||||
pool_img="",
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
up_char=up_arms,
|
||||
)
|
||||
self.dump_up_char()
|
||||
logger.info(f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}")
|
||||
except Exception as e:
|
||||
logger.warning(f"{self.game_name_cn}UP更新出错 {type(e)}:{e}")
|
||||
|
||||
async def _reload_pool(self) -> UniMessage | None:
|
||||
await self.update_up_char()
|
||||
self.load_up_char()
|
||||
if self.UP_CHAR and self.UP_ARMS:
|
||||
return MessageUtils.build_message(
|
||||
f"重载成功!\n当前UP池子:{self.UP_CHAR.title}"
|
||||
)
|
||||
@@ -1,178 +0,0 @@
|
||||
import random
|
||||
|
||||
import ujson as json
|
||||
from lxml import etree
|
||||
from PIL import Image, ImageDraw
|
||||
from PIL.Image import Image as IMG
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
|
||||
from ..config import draw_config
|
||||
from ..util import cn2py, load_font, remove_prohibited_str
|
||||
from .base_handle import BaseData, BaseHandle
|
||||
|
||||
|
||||
class OnmyojiChar(BaseData):
|
||||
@property
|
||||
def star_str(self) -> str:
|
||||
return ["N", "R", "SR", "SSR", "SP"][self.star - 1]
|
||||
|
||||
|
||||
class OnmyojiHandle(BaseHandle[OnmyojiChar]):
|
||||
def __init__(self):
|
||||
super().__init__("onmyoji", "阴阳师")
|
||||
self.max_star = 5
|
||||
self.config = draw_config.onmyoji
|
||||
self.ALL_CHAR: list[OnmyojiChar] = []
|
||||
|
||||
def get_card(self, **kwargs) -> OnmyojiChar:
|
||||
star = self.get_star(
|
||||
[5, 4, 3, 2],
|
||||
[
|
||||
self.config.ONMYOJI_SP,
|
||||
self.config.ONMYOJI_SSR,
|
||||
self.config.ONMYOJI_SR,
|
||||
self.config.ONMYOJI_R,
|
||||
],
|
||||
)
|
||||
chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited]
|
||||
return random.choice(chars)
|
||||
|
||||
def format_max_star(self, card_list: list[tuple[OnmyojiChar, int]]) -> str:
|
||||
rst = ""
|
||||
for card, index in card_list:
|
||||
if card.star == self.max_star:
|
||||
rst += f"第 {index} 抽获取SP {card.name}\n"
|
||||
elif card.star == self.max_star - 1:
|
||||
rst += f"第 {index} 抽获取SSR {card.name}\n"
|
||||
return rst.strip()
|
||||
|
||||
@staticmethod
|
||||
def star_label(star: int) -> IMG:
|
||||
text, color1, color2 = [
|
||||
("N", "#7E7E82", "#F5F6F7"),
|
||||
("R", "#014FA8", "#37C6FD"),
|
||||
("SR", "#6E0AA4", "#E94EFD"),
|
||||
("SSR", "#E5511D", "#FAF905"),
|
||||
("SP", "#FA1F2D", "#FFBBAF"),
|
||||
][star - 1]
|
||||
w = 200
|
||||
h = 110
|
||||
# 制作渐变色图片
|
||||
base = Image.new("RGBA", (w, h), color1)
|
||||
top = Image.new("RGBA", (w, h), color2)
|
||||
mask = Image.new("L", (w, h))
|
||||
mask_data = []
|
||||
for y in range(h):
|
||||
mask_data.extend([int(255 * (y / h))] * w)
|
||||
mask.putdata(mask_data)
|
||||
base.paste(top, (0, 0), mask)
|
||||
# 透明图层
|
||||
font = load_font("gorga.otf", 100)
|
||||
alpha = Image.new("L", (w, h))
|
||||
draw = ImageDraw.Draw(alpha)
|
||||
draw.text((20, -30), text, fill="white", font=font)
|
||||
base.putalpha(alpha)
|
||||
# stroke
|
||||
bg = Image.new("RGBA", (w, h))
|
||||
draw = ImageDraw.Draw(bg)
|
||||
draw.text(
|
||||
(20, -30),
|
||||
text,
|
||||
font=font,
|
||||
fill="gray",
|
||||
stroke_width=3,
|
||||
stroke_fill="gray",
|
||||
)
|
||||
bg.paste(base, (0, 0), base)
|
||||
return bg
|
||||
|
||||
async def generate_img(self, card_list: list[OnmyojiChar]) -> BuildImage:
|
||||
return await super().generate_img(card_list, num_per_line=10)
|
||||
|
||||
async def generate_card_img(self, card: OnmyojiChar) -> BuildImage:
|
||||
bg = BuildImage(73, 240, color="#F1EFE9")
|
||||
img_path = str(self.img_path / f"{cn2py(card.name)}_mark_btn.png")
|
||||
img = BuildImage(0, 0, background=img_path)
|
||||
img = Image.open(img_path).convert("RGBA")
|
||||
label = self.star_label(card.star).resize((60, 33), Image.ANTIALIAS)
|
||||
await bg.paste(img, (0, 0))
|
||||
await bg.paste(label, (0, 135))
|
||||
font = load_font("msyh.ttf", 16)
|
||||
draw = ImageDraw.Draw(bg.markImg)
|
||||
text = "\n".join([t for t in card.name[:4]])
|
||||
_, text_h = font.getsize_multiline(text, spacing=0)
|
||||
draw.text(
|
||||
(40, 150 + (90 - text_h) / 2), text, font=font, fill="gray", spacing=0
|
||||
)
|
||||
return bg
|
||||
|
||||
def _init_data(self):
|
||||
self.ALL_CHAR = [
|
||||
OnmyojiChar(
|
||||
name=value["名称"],
|
||||
star=["N", "R", "SR", "SSR", "SP"].index(value["星级"]) + 1,
|
||||
limited=(
|
||||
True
|
||||
if key
|
||||
in [
|
||||
"奴良陆生",
|
||||
"卖药郎",
|
||||
"鬼灯",
|
||||
"阿香",
|
||||
"蜜桃&芥子",
|
||||
"犬夜叉",
|
||||
"杀生丸",
|
||||
"桔梗",
|
||||
"朽木露琪亚",
|
||||
"黑崎一护",
|
||||
"灶门祢豆子",
|
||||
"灶门炭治郎",
|
||||
]
|
||||
else False
|
||||
),
|
||||
)
|
||||
for key, value in self.load_data().items()
|
||||
]
|
||||
|
||||
async def _update_info(self):
|
||||
info = {}
|
||||
url = "https://yys.res.netease.com/pc/zt/20161108171335/js/app/all_shishen.json?v74="
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"更新 {self.game_name_cn} 出错")
|
||||
return
|
||||
data = json.loads(result)
|
||||
for x in data:
|
||||
name = remove_prohibited_str(x["name"])
|
||||
member_dict = {
|
||||
"id": x["id"],
|
||||
"名称": name,
|
||||
"星级": x["level"],
|
||||
}
|
||||
info[name] = member_dict
|
||||
# logger.info(f"{name} is update...")
|
||||
# 更新头像
|
||||
for key in info.keys():
|
||||
url = f'https://yys.163.com/shishen/{info[key]["id"]}.html'
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
info[key]["头像"] = ""
|
||||
continue
|
||||
try:
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
avatar = dom.xpath("//div[@class='pic_wrap']/img/@src")[0]
|
||||
avatar = "https:" + avatar
|
||||
info[key]["头像"] = avatar
|
||||
except IndexError:
|
||||
info[key]["头像"] = ""
|
||||
logger.warning(f"{self.game_name_cn} 获取头像错误 {key}")
|
||||
self.dump_data(info)
|
||||
logger.info(f"{self.game_name_cn} 更新成功")
|
||||
# 下载头像
|
||||
for value in info.values():
|
||||
await self.download_img(value["头像"], value["名称"])
|
||||
# 下载书签形式的头像
|
||||
url = f"https://yys.res.netease.com/pc/zt/20161108171335/data/mark_btn/{value['id']}.png"
|
||||
await self.download_img(url, value["名称"] + "_mark_btn")
|
||||
@@ -1,149 +0,0 @@
|
||||
import random
|
||||
from urllib.parse import unquote
|
||||
|
||||
from lxml import etree
|
||||
from PIL import ImageDraw
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
|
||||
from ..config import draw_config
|
||||
from ..util import cn2py, load_font, remove_prohibited_str
|
||||
from .base_handle import BaseData, BaseHandle
|
||||
|
||||
|
||||
class PcrChar(BaseData):
|
||||
pass
|
||||
|
||||
|
||||
class PcrHandle(BaseHandle[PcrChar]):
|
||||
def __init__(self):
|
||||
super().__init__("pcr", "公主连结")
|
||||
self.max_star = 3
|
||||
self.config = draw_config.pcr
|
||||
self.ALL_CHAR: list[PcrChar] = []
|
||||
|
||||
def get_card(self, mode: int = 1) -> PcrChar:
|
||||
if mode == 2:
|
||||
star = self.get_star(
|
||||
[3, 2], [self.config.PCR_G_THREE_P, self.config.PCR_G_TWO_P]
|
||||
)
|
||||
else:
|
||||
star = self.get_star(
|
||||
[3, 2, 1],
|
||||
[self.config.PCR_THREE_P, self.config.PCR_TWO_P, self.config.PCR_ONE_P],
|
||||
)
|
||||
chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited]
|
||||
return random.choice(chars)
|
||||
|
||||
def get_cards(self, count: int, **kwargs) -> list[tuple[PcrChar, int]]:
|
||||
card_list = []
|
||||
card_count = 0 # 保底计算
|
||||
for i in range(count):
|
||||
card_count += 1
|
||||
# 十连保底
|
||||
if card_count == 10:
|
||||
card = self.get_card(2)
|
||||
card_count = 0
|
||||
else:
|
||||
card = self.get_card(1)
|
||||
if card.star > self.max_star - 2:
|
||||
card_count = 0
|
||||
card_list.append((card, i + 1))
|
||||
return card_list
|
||||
|
||||
async def generate_card_img(self, card: PcrChar) -> BuildImage:
|
||||
sep_w = 5
|
||||
sep_h = 5
|
||||
star_h = 15
|
||||
img_w = 90
|
||||
img_h = 90
|
||||
font_h = 20
|
||||
bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5")
|
||||
star_path = str(self.img_path / "star.png")
|
||||
star = BuildImage(star_h, star_h, background=star_path)
|
||||
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
|
||||
img = BuildImage(img_w, img_h, background=img_path)
|
||||
await bg.paste(img, (sep_w, sep_h))
|
||||
for i in range(card.star):
|
||||
await bg.paste(star, (sep_w + img_w - star_h * (i + 1), sep_h))
|
||||
# 加名字
|
||||
text = card.name[:5] + "..." if len(card.name) > 6 else card.name
|
||||
font = load_font(fontsize=14)
|
||||
text_w, text_h = BuildImage.get_text_size(text, font)
|
||||
draw = ImageDraw.Draw(bg.markImg)
|
||||
draw.text(
|
||||
(sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2),
|
||||
text,
|
||||
font=font,
|
||||
fill="gray",
|
||||
)
|
||||
return bg
|
||||
|
||||
def _init_data(self):
|
||||
self.ALL_CHAR = [
|
||||
PcrChar(
|
||||
name=value["名称"],
|
||||
star=int(value["星级"]),
|
||||
limited=True if "(" in key else False,
|
||||
)
|
||||
for key, value in self.load_data().items()
|
||||
]
|
||||
|
||||
async def _update_info(self):
|
||||
info = {}
|
||||
if draw_config.PCR_TAI:
|
||||
url = "https://wiki.biligame.com/pcr/角色图鉴"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"更新 {self.game_name_cn} 出错")
|
||||
return
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
# TODO: PCR台湾更新失败
|
||||
char_list = dom.xpath(
|
||||
"//*[@id='CardSelectCard']/div[@class='unit-icon trcard']"
|
||||
)
|
||||
for char in char_list:
|
||||
try:
|
||||
name = char.xpath("./a/@title")[0]
|
||||
avatar = char.xpath("./a/img/@srcset")[0]
|
||||
star = len(char.xpath("./div[1]/img"))
|
||||
except IndexError:
|
||||
continue
|
||||
member_dict = {
|
||||
"头像": unquote(str(avatar).split(" ")[-2]),
|
||||
"名称": remove_prohibited_str(name),
|
||||
"星级": star,
|
||||
}
|
||||
info[member_dict["名称"]] = member_dict
|
||||
else:
|
||||
url = "https://wiki.biligame.com/pcr/角色筛选表"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"更新 {self.game_name_cn} 出错")
|
||||
return
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
|
||||
for char in char_list:
|
||||
try:
|
||||
name = char.xpath("./td[1]/a/@title")[0]
|
||||
avatar = char.xpath("./td[1]/a/img/@srcset")[0]
|
||||
star = char.xpath("./td[4]/text()")[0]
|
||||
except IndexError:
|
||||
continue
|
||||
member_dict = {
|
||||
"头像": unquote(str(avatar).split(" ")[-2]),
|
||||
"名称": remove_prohibited_str(name),
|
||||
"星级": int(str(star).strip()),
|
||||
}
|
||||
info[member_dict["名称"]] = member_dict
|
||||
self.dump_data(info)
|
||||
logger.info(f"{self.game_name_cn} 更新成功")
|
||||
# 下载头像
|
||||
for value in info.values():
|
||||
await self.download_img(value["头像"], value["名称"])
|
||||
# 下载星星
|
||||
await self.download_img(
|
||||
"https://patchwiki.biligame.com/images/pcr/0/02/s75ys2ecqhu2xbdw1wf1v9ccscnvi5g.png",
|
||||
"star",
|
||||
)
|
||||
@@ -1,423 +0,0 @@
|
||||
import random
|
||||
import re
|
||||
from datetime import datetime
|
||||
from urllib.parse import unquote
|
||||
|
||||
import dateparser
|
||||
import ujson as json
|
||||
from bs4 import BeautifulSoup
|
||||
from lxml import etree
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
from PIL import ImageDraw
|
||||
from pydantic import ValidationError
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ..config import draw_config
|
||||
from ..util import cn2py, load_font, remove_prohibited_str
|
||||
from .base_handle import BaseData, BaseHandle, UpChar, UpEvent
|
||||
|
||||
|
||||
class PrettyData(BaseData):
|
||||
pass
|
||||
|
||||
|
||||
class PrettyChar(PrettyData):
|
||||
pass
|
||||
|
||||
|
||||
class PrettyCard(PrettyData):
|
||||
@property
|
||||
def star_str(self) -> str:
|
||||
return ["R", "SR", "SSR"][self.star - 1]
|
||||
|
||||
|
||||
class PrettyHandle(BaseHandle[PrettyData]):
|
||||
def __init__(self):
|
||||
super().__init__("pretty", "赛马娘")
|
||||
self.data_files.append("pretty_card.json")
|
||||
self.max_star = 3
|
||||
self.game_card_color = "#eff2f5"
|
||||
self.config = draw_config.pretty
|
||||
|
||||
self.ALL_CHAR: list[PrettyChar] = []
|
||||
self.ALL_CARD: list[PrettyCard] = []
|
||||
self.UP_CHAR: UpEvent | None = None
|
||||
self.UP_CARD: UpEvent | None = None
|
||||
|
||||
def get_card(self, pool_name: str, mode: int = 1) -> PrettyData:
|
||||
if mode == 1:
|
||||
star = self.get_star(
|
||||
[3, 2, 1],
|
||||
[
|
||||
self.config.PRETTY_THREE_P,
|
||||
self.config.PRETTY_TWO_P,
|
||||
self.config.PRETTY_ONE_P,
|
||||
],
|
||||
)
|
||||
else:
|
||||
star = self.get_star(
|
||||
[3, 2], [self.config.PRETTY_THREE_P, self.config.PRETTY_TWO_P]
|
||||
)
|
||||
up_pool = None
|
||||
if pool_name == "char":
|
||||
up_pool = self.UP_CHAR
|
||||
all_list = self.ALL_CHAR
|
||||
else:
|
||||
up_pool = self.UP_CARD
|
||||
all_list = self.ALL_CARD
|
||||
|
||||
all_char = [x for x in all_list if x.star == star and not x.limited]
|
||||
acquire_char = None
|
||||
# 有UP池子
|
||||
if up_pool and star in [x.star for x in up_pool.up_char]:
|
||||
up_list = [x.name for x in up_pool.up_char if x.star == star]
|
||||
# 抽到UP
|
||||
if random.random() < 1 / len(all_char) * (0.7 / 0.1385):
|
||||
up_name = random.choice(up_list)
|
||||
try:
|
||||
acquire_char = [x for x in all_list if x.name == up_name][0]
|
||||
except IndexError:
|
||||
pass
|
||||
if not acquire_char:
|
||||
acquire_char = random.choice(all_char)
|
||||
return acquire_char
|
||||
|
||||
def get_cards(self, count: int, pool_name: str) -> list[tuple[PrettyData, int]]:
|
||||
card_list = []
|
||||
card_count = 0 # 保底计算
|
||||
for i in range(count):
|
||||
card_count += 1
|
||||
# 十连保底
|
||||
if card_count == 10:
|
||||
card = self.get_card(pool_name, 2)
|
||||
card_count = 0
|
||||
else:
|
||||
card = self.get_card(pool_name, 1)
|
||||
if card.star > self.max_star - 2:
|
||||
card_count = 0
|
||||
card_list.append((card, i + 1))
|
||||
return card_list
|
||||
|
||||
def format_pool_info(self, pool_name: str) -> str:
|
||||
info = ""
|
||||
up_event = self.UP_CHAR if pool_name == "char" else self.UP_CARD
|
||||
if up_event:
|
||||
star3_list = [x.name for x in up_event.up_char if x.star == 3]
|
||||
star2_list = [x.name for x in up_event.up_char if x.star == 2]
|
||||
star1_list = [x.name for x in up_event.up_char if x.star == 1]
|
||||
if star3_list:
|
||||
if pool_name == "char":
|
||||
info += f'三星UP:{" ".join(star3_list)}\n'
|
||||
else:
|
||||
info += f'SSR UP:{" ".join(star3_list)}\n'
|
||||
if star2_list:
|
||||
if pool_name == "char":
|
||||
info += f'二星UP:{" ".join(star2_list)}\n'
|
||||
else:
|
||||
info += f'SR UP:{" ".join(star2_list)}\n'
|
||||
if star1_list:
|
||||
if pool_name == "char":
|
||||
info += f'一星UP:{" ".join(star1_list)}\n'
|
||||
else:
|
||||
info += f'R UP:{" ".join(star1_list)}\n'
|
||||
info = f"当前up池:{up_event.title}\n{info}"
|
||||
return info.strip()
|
||||
|
||||
async def draw(self, count: int, pool_name: str, **kwargs) -> UniMessage:
|
||||
pool_name = "char" if not pool_name else pool_name
|
||||
index2card = self.get_cards(count, pool_name)
|
||||
cards = [card[0] for card in index2card]
|
||||
up_event = self.UP_CHAR if pool_name == "char" else self.UP_CARD
|
||||
up_list = [x.name for x in up_event.up_char] if up_event else []
|
||||
result = self.format_result(index2card, up_list=up_list)
|
||||
pool_info = self.format_pool_info(pool_name)
|
||||
img = await self.generate_img(cards)
|
||||
return MessageUtils.build_message([pool_info, img, result])
|
||||
|
||||
async def generate_card_img(self, card: PrettyData) -> BuildImage:
|
||||
if isinstance(card, PrettyChar):
|
||||
star_h = 30
|
||||
img_w = 200
|
||||
img_h = 219
|
||||
font_h = 50
|
||||
bg = BuildImage(img_w, img_h + font_h, color="#EFF2F5")
|
||||
star_path = str(self.img_path / "star.png")
|
||||
star = BuildImage(star_h, star_h, background=star_path)
|
||||
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
|
||||
img = BuildImage(img_w, img_h, background=img_path)
|
||||
star_w = star_h * card.star
|
||||
for i in range(card.star):
|
||||
await bg.paste(star, (int((img_w - star_w) / 2) + star_h * i, 0))
|
||||
await bg.paste(img, (0, 0))
|
||||
# 加名字
|
||||
text = card.name[:5] + "..." if len(card.name) > 6 else card.name
|
||||
font = load_font(fontsize=30)
|
||||
text_w, _ = font.getsize(text)
|
||||
draw = ImageDraw.Draw(bg.markImg)
|
||||
draw.text(
|
||||
((img_w - text_w) / 2, img_h),
|
||||
text,
|
||||
font=font,
|
||||
fill="gray",
|
||||
)
|
||||
return bg
|
||||
else:
|
||||
sep_w = 10
|
||||
img_w = 200
|
||||
img_h = 267
|
||||
font_h = 75
|
||||
bg = BuildImage(img_w + sep_w * 2, img_h + font_h, color="#EFF2F5")
|
||||
label_path = str(self.img_path / f"{card.star}_label.png")
|
||||
label = BuildImage(40, 40, background=label_path)
|
||||
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
|
||||
img = BuildImage(img_w, img_h, background=img_path)
|
||||
await bg.paste(img, (sep_w, 0))
|
||||
await bg.paste(label, (30, 3))
|
||||
# 加名字
|
||||
text = ""
|
||||
texts = []
|
||||
font = load_font(fontsize=25)
|
||||
for t in card.name:
|
||||
if BuildImage.get_text_size((text + t), font)[0] > 190:
|
||||
texts.append(text)
|
||||
text = ""
|
||||
if len(texts) >= 2:
|
||||
texts[-1] += "..."
|
||||
break
|
||||
else:
|
||||
text += t
|
||||
if text:
|
||||
texts.append(text)
|
||||
text = "\n".join(texts)
|
||||
text_w, _ = font.getsize_multiline(text)
|
||||
draw = ImageDraw.Draw(bg.markImg)
|
||||
draw.text(
|
||||
((img_w - text_w) / 2, img_h),
|
||||
text,
|
||||
font=font,
|
||||
align="center",
|
||||
fill="gray",
|
||||
)
|
||||
return bg
|
||||
|
||||
def _init_data(self):
|
||||
self.ALL_CHAR = [
|
||||
PrettyChar(
|
||||
name=value["名称"],
|
||||
star=int(value["初始星级"]),
|
||||
limited=False,
|
||||
)
|
||||
for value in self.load_data().values()
|
||||
]
|
||||
self.ALL_CARD = [
|
||||
PrettyCard(
|
||||
name=value["中文名"],
|
||||
star=["R", "SR", "SSR"].index(value["稀有度"]) + 1,
|
||||
limited=True if "卡池" not in value["获取方式"] else False,
|
||||
)
|
||||
for value in self.load_data("pretty_card.json").values()
|
||||
]
|
||||
self.load_up_char()
|
||||
|
||||
def load_up_char(self):
|
||||
try:
|
||||
data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json")
|
||||
self.UP_CHAR = UpEvent.parse_obj(data.get("char", {}))
|
||||
self.UP_CARD = UpEvent.parse_obj(data.get("card", {}))
|
||||
except ValidationError:
|
||||
logger.warning(f"{self.game_name}_up_char 解析出错")
|
||||
|
||||
def dump_up_char(self):
|
||||
if self.UP_CHAR and self.UP_CARD:
|
||||
data = {
|
||||
"char": json.loads(self.UP_CHAR.json()),
|
||||
"card": json.loads(self.UP_CARD.json()),
|
||||
}
|
||||
self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
|
||||
|
||||
async def _update_info(self):
|
||||
# pretty.json
|
||||
pretty_info = {}
|
||||
url = "https://wiki.biligame.com/umamusume/赛马娘图鉴"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"更新 {self.game_name_cn} 出错")
|
||||
else:
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
|
||||
for char in char_list:
|
||||
try:
|
||||
name = char.xpath("./td[1]/a/@title")[0]
|
||||
avatar = char.xpath("./td[1]/a/img/@srcset")[0]
|
||||
star = len(char.xpath("./td[3]/img"))
|
||||
except IndexError:
|
||||
continue
|
||||
member_dict = {
|
||||
"头像": unquote(str(avatar).split(" ")[-2]),
|
||||
"名称": remove_prohibited_str(name),
|
||||
"初始星级": star,
|
||||
}
|
||||
pretty_info[member_dict["名称"]] = member_dict
|
||||
self.dump_data(pretty_info)
|
||||
logger.info(f"{self.game_name_cn} 更新成功")
|
||||
# pretty_card.json
|
||||
pretty_card_info = {}
|
||||
url = "https://wiki.biligame.com/umamusume/支援卡图鉴"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"更新 {self.game_name_cn} 卡牌出错")
|
||||
else:
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
|
||||
for char in char_list:
|
||||
try:
|
||||
name = char.xpath("./td[1]/div/a/@title")[0]
|
||||
name_cn = char.xpath("./td[3]/a/text()")[0]
|
||||
avatar = char.xpath("./td[1]/div/a/img/@srcset")[0]
|
||||
star = str(char.xpath("./td[5]/text()")[0]).strip()
|
||||
sources = str(char.xpath("./td[7]/text()")[0]).strip()
|
||||
except IndexError:
|
||||
continue
|
||||
member_dict = {
|
||||
"头像": unquote(str(avatar).split(" ")[-2]),
|
||||
"名称": remove_prohibited_str(name),
|
||||
"中文名": remove_prohibited_str(name_cn),
|
||||
"稀有度": star,
|
||||
"获取方式": [sources] if sources else [],
|
||||
}
|
||||
pretty_card_info[member_dict["中文名"]] = member_dict
|
||||
self.dump_data(pretty_card_info, "pretty_card.json")
|
||||
logger.info(f"{self.game_name_cn} 卡牌更新成功")
|
||||
# 下载头像
|
||||
for value in pretty_info.values():
|
||||
await self.download_img(value["头像"], value["名称"])
|
||||
for value in pretty_card_info.values():
|
||||
await self.download_img(value["头像"], value["中文名"])
|
||||
# 下载星星
|
||||
PRETTY_URL = "https://patchwiki.biligame.com/images/umamusume"
|
||||
await self.download_img(
|
||||
PRETTY_URL + "/1/13/e1hwjz4vmhtvk8wlyb7c0x3ld1s2ata.png", "star"
|
||||
)
|
||||
# 下载稀有度标志
|
||||
idx = 1
|
||||
for url in [
|
||||
"/f/f7/afqs7h4snmvovsrlifq5ib8vlpu2wvk.png",
|
||||
"/3/3b/d1jmpwrsk4irkes1gdvoos4ic6rmuht.png",
|
||||
"/0/06/q23szwkbtd7pfkqrk3wcjlxxt9z595o.png",
|
||||
]:
|
||||
await self.download_img(PRETTY_URL + url, f"{idx}_label")
|
||||
idx += 1
|
||||
await self.update_up_char()
|
||||
|
||||
async def update_up_char(self):
|
||||
announcement_url = "https://wiki.biligame.com/umamusume/公告"
|
||||
result = await self.get_url(announcement_url)
|
||||
if not result:
|
||||
logger.warning(f"{self.game_name_cn}获取公告出错")
|
||||
return
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
announcements = dom.xpath("//div[@id='mw-content-text']/div/div/span/a")
|
||||
title = ""
|
||||
url = ""
|
||||
for announcement in announcements:
|
||||
try:
|
||||
title = announcement.xpath("./@title")[0]
|
||||
url = "https://wiki.biligame.com/" + announcement.xpath("./@href")[0]
|
||||
if re.match(r".*?\d{8}$", title) or re.match(
|
||||
r"^\d{1,2}月\d{1,2}日.*?", title
|
||||
):
|
||||
break
|
||||
except IndexError:
|
||||
continue
|
||||
if not title:
|
||||
logger.warning(f"{self.game_name_cn}未找到新UP公告")
|
||||
return
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"{self.game_name_cn}获取UP公告出错")
|
||||
return
|
||||
try:
|
||||
start_time = None
|
||||
end_time = None
|
||||
char_img = ""
|
||||
card_img = ""
|
||||
up_chars = []
|
||||
up_cards = []
|
||||
soup = BeautifulSoup(result, "lxml")
|
||||
heads = soup.find_all("span", {"class": "mw-headline"})
|
||||
for head in heads:
|
||||
if "时间" in head.text:
|
||||
time = head.find_next("p").text.split("\n")[0]
|
||||
if "~" in time:
|
||||
start, end = time.split("~")
|
||||
start_time = dateparser.parse(start)
|
||||
end_time = dateparser.parse(end)
|
||||
elif "赛马娘" in head.text:
|
||||
char_img = head.find_next("a", {"class": "image"}).find("img")[
|
||||
"src"
|
||||
]
|
||||
lines = str(head.find_next("p").text).split("\n")
|
||||
chars = [
|
||||
line
|
||||
for line in lines
|
||||
if "★" in line and "(" in line and ")" in line
|
||||
]
|
||||
for char in chars:
|
||||
star = char.count("★")
|
||||
name = re.split(r"[()]", char)[-2].strip()
|
||||
up_chars.append(
|
||||
UpChar(name=name, star=star, limited=False, zoom=70)
|
||||
)
|
||||
elif "支援卡" in head.text:
|
||||
card_img = head.find_next("a", {"class": "image"}).find("img")[
|
||||
"src"
|
||||
]
|
||||
lines = str(head.find_next("p").text).split("\n")
|
||||
cards = [
|
||||
line
|
||||
for line in lines
|
||||
if "R" in line and "(" in line and ")" in line
|
||||
]
|
||||
for card in cards:
|
||||
star = 3 if "SSR" in card else 2 if "SR" in card else 1
|
||||
name = re.split(r"[()]", card)[-2].strip()
|
||||
up_cards.append(
|
||||
UpChar(name=name, star=star, limited=False, zoom=70)
|
||||
)
|
||||
if start_time and end_time:
|
||||
if start_time <= datetime.now() <= end_time:
|
||||
self.UP_CHAR = UpEvent(
|
||||
title=title,
|
||||
pool_img=char_img,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
up_char=up_chars,
|
||||
)
|
||||
self.UP_CARD = UpEvent(
|
||||
title=title,
|
||||
pool_img=card_img,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
up_char=up_cards,
|
||||
)
|
||||
self.dump_up_char()
|
||||
logger.info(
|
||||
f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"{self.game_name_cn}UP更新出错", e=e)
|
||||
|
||||
async def _reload_pool(self) -> UniMessage | None:
|
||||
await self.update_up_char()
|
||||
self.load_up_char()
|
||||
if self.UP_CHAR and self.UP_CARD:
|
||||
return MessageUtils.build_message(
|
||||
[
|
||||
f"重载成功!\n当前UP池子:{self.UP_CHAR.title}",
|
||||
self.UP_CHAR.pool_img,
|
||||
self.UP_CARD.pool_img,
|
||||
]
|
||||
)
|
||||
@@ -1,344 +0,0 @@
|
||||
import random
|
||||
import re
|
||||
from datetime import datetime
|
||||
from urllib.parse import unquote
|
||||
|
||||
import dateparser
|
||||
import ujson as json
|
||||
from lxml import etree
|
||||
from lxml.etree import _Element
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
from PIL import ImageDraw
|
||||
from pydantic import ValidationError
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ..config import draw_config
|
||||
from ..util import cn2py, load_font, remove_prohibited_str
|
||||
from .base_handle import BaseData, BaseHandle, UpChar, UpEvent
|
||||
|
||||
|
||||
class Operator(BaseData):
|
||||
recruit_only: bool # 公招限定
|
||||
event_only: bool # 活动获得干员
|
||||
core_only: bool # 中坚干员
|
||||
# special_only: bool # 升变/异格干员
|
||||
|
||||
|
||||
class PrtsHandle(BaseHandle[Operator]):
|
||||
def __init__(self):
|
||||
super().__init__(game_name="prts", game_name_cn="明日方舟")
|
||||
self.max_star = 6
|
||||
self.game_card_color = "#eff2f5"
|
||||
self.config = draw_config.prts
|
||||
|
||||
self.ALL_OPERATOR: list[Operator] = []
|
||||
self.UP_EVENT: UpEvent | None = None
|
||||
|
||||
def get_card(self, add: float) -> Operator:
|
||||
star = self.get_star(
|
||||
star_list=[6, 5, 4, 3],
|
||||
probability_list=[
|
||||
self.config.PRTS_SIX_P + add,
|
||||
self.config.PRTS_FIVE_P,
|
||||
self.config.PRTS_FOUR_P,
|
||||
self.config.PRTS_THREE_P,
|
||||
],
|
||||
)
|
||||
|
||||
all_operators = [
|
||||
x
|
||||
for x in self.ALL_OPERATOR
|
||||
if x.star == star
|
||||
and not any([x.limited, x.recruit_only, x.event_only, x.core_only])
|
||||
]
|
||||
acquire_operator = None
|
||||
|
||||
if self.UP_EVENT:
|
||||
up_operators = [x for x in self.UP_EVENT.up_char if x.star == star]
|
||||
# UPs
|
||||
try:
|
||||
zooms = [x.zoom for x in up_operators]
|
||||
zoom_sum = sum(zooms)
|
||||
if random.random() < zoom_sum:
|
||||
up_name = random.choices(up_operators, weights=zooms, k=1)[0].name
|
||||
acquire_operator = [
|
||||
x for x in self.ALL_OPERATOR if x.name == up_name
|
||||
][0]
|
||||
except IndexError:
|
||||
pass
|
||||
if not acquire_operator:
|
||||
acquire_operator = random.choice(all_operators)
|
||||
return acquire_operator
|
||||
|
||||
def get_cards(self, count: int, **kwargs) -> list[tuple[Operator, int]]:
|
||||
card_list = [] # 获取所有角色
|
||||
add = 0.0
|
||||
count_idx = 0
|
||||
for i in range(count):
|
||||
count_idx += 1
|
||||
card = self.get_card(add)
|
||||
if card.star == self.max_star:
|
||||
add = 0.0
|
||||
count_idx = 0
|
||||
elif count_idx > 50:
|
||||
add += 0.02
|
||||
card_list.append((card, i + 1))
|
||||
return card_list
|
||||
|
||||
def format_pool_info(self) -> str:
|
||||
info = ""
|
||||
if self.UP_EVENT:
|
||||
star6_list = [x.name for x in self.UP_EVENT.up_char if x.star == 6]
|
||||
star5_list = [x.name for x in self.UP_EVENT.up_char if x.star == 5]
|
||||
star4_list = [x.name for x in self.UP_EVENT.up_char if x.star == 4]
|
||||
if star6_list:
|
||||
info += f"六星UP:{' '.join(star6_list)}\n"
|
||||
if star5_list:
|
||||
info += f"五星UP:{' '.join(star5_list)}\n"
|
||||
if star4_list:
|
||||
info += f"四星UP:{' '.join(star4_list)}\n"
|
||||
info = f"当前up池: {self.UP_EVENT.title}\n{info}"
|
||||
return info.strip()
|
||||
|
||||
async def draw(self, count: int, **kwargs) -> UniMessage:
|
||||
index2card = self.get_cards(count)
|
||||
"""这里cards修复了抽卡图文不符的bug"""
|
||||
cards = [card[0] for card in index2card]
|
||||
up_list = [x.name for x in self.UP_EVENT.up_char] if self.UP_EVENT else []
|
||||
result = self.format_result(index2card, up_list=up_list)
|
||||
pool_info = self.format_pool_info()
|
||||
img = await self.generate_img(cards)
|
||||
return MessageUtils.build_message([pool_info, img, result])
|
||||
|
||||
async def generate_card_img(self, card: Operator) -> BuildImage:
|
||||
sep_w = 5
|
||||
sep_h = 5
|
||||
star_h = 15
|
||||
img_w = 120
|
||||
img_h = 120
|
||||
font_h = 20
|
||||
bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5")
|
||||
star_path = str(self.img_path / "star.png")
|
||||
star = BuildImage(star_h, star_h, background=star_path)
|
||||
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
|
||||
img = BuildImage(img_w, img_h, background=img_path)
|
||||
await bg.paste(img, (sep_w, sep_h))
|
||||
for i in range(card.star):
|
||||
await bg.paste(star, (sep_w + img_w - 5 - star_h * (i + 1), sep_h))
|
||||
# 加名字
|
||||
text = card.name[:7] + "..." if len(card.name) > 8 else card.name
|
||||
font = load_font(fontsize=16)
|
||||
text_w, text_h = BuildImage.get_text_size(text, font)
|
||||
draw = ImageDraw.Draw(bg.markImg)
|
||||
draw.text(
|
||||
(sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2),
|
||||
text,
|
||||
font=font,
|
||||
fill="gray",
|
||||
)
|
||||
return bg
|
||||
|
||||
def _init_data(self):
|
||||
self.ALL_OPERATOR = [
|
||||
Operator(
|
||||
name=value["名称"],
|
||||
star=int(value["星级"]),
|
||||
limited="标准寻访" not in value["获取途径"]
|
||||
and "中坚寻访" not in value["获取途径"],
|
||||
recruit_only=(
|
||||
True
|
||||
if "标准寻访" not in value["获取途径"]
|
||||
and "中坚寻访" not in value["获取途径"]
|
||||
and "公开招募" in value["获取途径"]
|
||||
else False
|
||||
),
|
||||
event_only=True if "活动获取" in value["获取途径"] else False,
|
||||
core_only=(
|
||||
True
|
||||
if "标准寻访" not in value["获取途径"]
|
||||
and "中坚寻访" in value["获取途径"]
|
||||
else False
|
||||
),
|
||||
)
|
||||
for key, value in self.load_data().items()
|
||||
if "阿米娅" not in key
|
||||
]
|
||||
self.load_up_char()
|
||||
|
||||
def load_up_char(self):
|
||||
try:
|
||||
data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json")
|
||||
"""这里的 waring 有点模糊,更新游戏信息时没有up池的情况下也会报错,所以细分了一下"""
|
||||
if not data:
|
||||
logger.warning(f"当前无UP池或 {self.game_name}_up_char.json 文件不存在")
|
||||
else:
|
||||
self.UP_EVENT = UpEvent.parse_obj(data.get("char", {}))
|
||||
except ValidationError:
|
||||
logger.warning(f"{self.game_name}_up_char 解析出错")
|
||||
|
||||
def dump_up_char(self):
|
||||
if self.UP_EVENT:
|
||||
data = {"char": json.loads(self.UP_EVENT.json())}
|
||||
self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
|
||||
|
||||
async def _update_info(self):
|
||||
"""更新信息"""
|
||||
info = {}
|
||||
url = "https://wiki.biligame.com/arknights/干员数据表"
|
||||
result = await self.get_url(url)
|
||||
if not result:
|
||||
logger.warning(f"更新 {self.game_name_cn} 出错")
|
||||
return
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
char_list: list[_Element] = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
|
||||
for char in char_list:
|
||||
try:
|
||||
avatar = char.xpath("./td[1]/div/div/div/a/img/@srcset")[0]
|
||||
name = char.xpath("./td[1]/center/a/text()")[0]
|
||||
star = char.xpath("./td[2]/text()")[0]
|
||||
"""这里sources修好了干员获取标签有问题的bug,如三星只能抽到卡缇就是这个原因"""
|
||||
sources = [_.strip("\n") for _ in char.xpath("./td[7]/text()")]
|
||||
except IndexError:
|
||||
continue
|
||||
member_dict = {
|
||||
"头像": unquote(str(avatar).split(" ")[-2]),
|
||||
"名称": remove_prohibited_str(str(name).strip()),
|
||||
"星级": int(str(star).strip()),
|
||||
"获取途径": sources,
|
||||
}
|
||||
info[member_dict["名称"]] = member_dict
|
||||
self.dump_data(info)
|
||||
logger.info(f"{self.game_name_cn} 更新成功")
|
||||
# 下载头像
|
||||
for value in info.values():
|
||||
await self.download_img(value["头像"], value["名称"])
|
||||
# 下载星星
|
||||
await self.download_img(
|
||||
"https://patchwiki.biligame.com/images/pcr/0/02/s75ys2ecqhu2xbdw1wf1v9ccscnvi5g.png",
|
||||
"star",
|
||||
)
|
||||
await self.update_up_char()
|
||||
|
||||
async def update_up_char(self):
|
||||
"""重载卡池"""
|
||||
announcement_url = "https://ak.hypergryph.com/news.html"
|
||||
result = await self.get_url(announcement_url)
|
||||
if not result:
|
||||
logger.warning(f"{self.game_name_cn}获取公告出错")
|
||||
return
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
activity_urls = dom.xpath(
|
||||
"//ol[@class='articlelist' and @data-category-key='ACTIVITY']/li/a/@href"
|
||||
)
|
||||
start_time = None
|
||||
end_time = None
|
||||
up_chars = []
|
||||
pool_img = ""
|
||||
for activity_url in activity_urls[:10]: # 减少响应时间, 10个就够了
|
||||
activity_url = f"https://ak.hypergryph.com{activity_url}"
|
||||
result = await self.get_url(activity_url)
|
||||
if not result:
|
||||
logger.warning(f"{self.game_name_cn}获取公告 {activity_url} 出错")
|
||||
continue
|
||||
|
||||
"""因为鹰角的前端太自由了,这里重写了匹配规则以尽可能避免因为前端乱七八糟而导致的重载失败"""
|
||||
dom = etree.HTML(result, etree.HTMLParser())
|
||||
contents = dom.xpath(
|
||||
"//div[@class='article-content']/p/text() | //div[@class='article-content']/p/span/text() | //div[@class='article-content']/div[@class='media-wrap image-wrap']/img/@src"
|
||||
)
|
||||
title = ""
|
||||
time = ""
|
||||
chars: list[str] = []
|
||||
for index, content in enumerate(contents):
|
||||
if re.search("(.*)(寻访|复刻).*?开启", content):
|
||||
title = re.split(r"[【】]", content)
|
||||
title = "".join(title[1:-1]) if "-" in title else title[1]
|
||||
lines = [
|
||||
contents[index - 2 + _] for _ in range(8)
|
||||
] # 从 -2 开始是因为xpath获取的时间有的会在寻访开启这一句之前
|
||||
lines.append("") # 防止IndexError,加个空字符串
|
||||
for idx, line in enumerate(lines):
|
||||
match = re.search(
|
||||
r"(\d{1,2}月\d{1,2}日.*?-.*?\d{1,2}月\d{1,2}日.*?$)", line
|
||||
)
|
||||
if match:
|
||||
time = match.group(1)
|
||||
"""因为 <p> 的诡异排版,所以有了下面的一段"""
|
||||
if ("★★" in line and "%" in line) or (
|
||||
"★★" in line and "%" in lines[idx + 1]
|
||||
):
|
||||
(
|
||||
chars.append(line)
|
||||
if ("★★" in line and "%" in line)
|
||||
else chars.append(line + lines[idx + 1])
|
||||
)
|
||||
if not time:
|
||||
continue
|
||||
start, end = (
|
||||
time.replace("月", "/").replace("日", " ").split("-")[:2]
|
||||
) # 日替换为空格是因为有日后面不接空格的情况,导致 split 出问题
|
||||
start_time = dateparser.parse(start)
|
||||
end_time = dateparser.parse(end)
|
||||
pool_img = contents[index - 2]
|
||||
r"""两类格式:用/分割,用\分割;★+(概率)+名字,★+名字+(概率)"""
|
||||
for char in chars:
|
||||
star = char.split("(")[0].count("★")
|
||||
name = (
|
||||
re.split(r"[:(]", char)[1]
|
||||
if "★(" not in char
|
||||
else re.split("):", char)[1]
|
||||
) # 有的括号在前面有的在后面
|
||||
dual_up = False
|
||||
if "\\" in name:
|
||||
names = name.split("\\")
|
||||
dual_up = True
|
||||
elif "/" in name:
|
||||
names = name.split("/")
|
||||
dual_up = True
|
||||
else:
|
||||
names = [name] # 既有用/分割的,又有用\分割的
|
||||
|
||||
names = [name.replace("[限定]", "").strip() for name in names]
|
||||
zoom = 1
|
||||
if "权值" in char:
|
||||
zoom = 0.03
|
||||
else:
|
||||
match = re.search(r"(占.*?的.*?(\d+).*?%)", char)
|
||||
if dual_up == True:
|
||||
zoom = float(match.group(1)) / 2
|
||||
else:
|
||||
zoom = float(match.group(1))
|
||||
zoom = zoom / 100 if zoom > 1 else zoom
|
||||
for name in names:
|
||||
up_chars.append(
|
||||
UpChar(name=name, star=star, limited=False, zoom=zoom)
|
||||
)
|
||||
break # 这里break会导致个问题:如果一个公告里有两个池子,会漏掉下面的池子,比如 5.19 的定向寻访。但目前我也没啥好想法解决
|
||||
if title and start_time and end_time:
|
||||
if start_time <= datetime.now() <= end_time:
|
||||
self.UP_EVENT = UpEvent(
|
||||
title=title,
|
||||
pool_img=pool_img,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
up_char=up_chars,
|
||||
)
|
||||
self.dump_up_char()
|
||||
logger.info(
|
||||
f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}"
|
||||
)
|
||||
break
|
||||
|
||||
async def _reload_pool(self) -> UniMessage | None:
|
||||
await self.update_up_char()
|
||||
self.load_up_char()
|
||||
if self.UP_EVENT:
|
||||
return MessageUtils.build_message(
|
||||
[
|
||||
f"重载成功!\n当前UP池子:{self.UP_EVENT.title}",
|
||||
self.UP_EVENT.pool_img,
|
||||
]
|
||||
)
|
||||
@@ -1,10 +0,0 @@
|
||||
from nonebot.internal.rule import Rule
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
|
||||
|
||||
def rule(game) -> Rule:
|
||||
async def _rule() -> bool:
|
||||
return Config.get_config("draw_card", game.config_name, True)
|
||||
|
||||
return Rule(_rule)
|
||||
@@ -1,61 +0,0 @@
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
import pypinyin
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from PIL.Image import Image as IMG
|
||||
from PIL.ImageFont import FreeTypeFont
|
||||
|
||||
from zhenxun.configs.path_config import FONT_PATH
|
||||
from zhenxun.utils._build_image import BuildImage
|
||||
|
||||
dir_path = Path(__file__).parent.absolute()
|
||||
|
||||
|
||||
def cn2py(word) -> str:
|
||||
"""保存声调,防止出现类似方舟干员红与吽拼音相同声调不同导致红照片无法保存的问题"""
|
||||
temp = ""
|
||||
for i in pypinyin.pinyin(word, style=pypinyin.Style.TONE3):
|
||||
temp += "".join(i)
|
||||
return temp
|
||||
|
||||
|
||||
# 移除windows和linux下特殊字符
|
||||
def remove_prohibited_str(name: str) -> str:
|
||||
if platform.system().lower() == "windows":
|
||||
tmp = ""
|
||||
for i in name:
|
||||
if i not in ["\\", "/", ":", "*", "?", '"', "<", ">", "|"]:
|
||||
tmp += i
|
||||
name = tmp
|
||||
else:
|
||||
name = name.replace("/", "\\")
|
||||
return name
|
||||
|
||||
|
||||
def load_font(fontname: str = "msyh.ttf", fontsize: int = 16) -> FreeTypeFont:
|
||||
return ImageFont.truetype(
|
||||
str(FONT_PATH / f"{fontname}"), fontsize, encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
def circled_number(num: int) -> IMG:
|
||||
font = load_font(fontsize=450)
|
||||
text = str(num)
|
||||
text_w = BuildImage.get_text_size(text, font=font)[0]
|
||||
w = 240 + text_w
|
||||
w = w if w >= 500 else 500
|
||||
img = Image.new("RGBA", (w, 500))
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.ellipse(((0, 0), (500, 500)), fill="red")
|
||||
draw.ellipse(((w - 500, 0), (w, 500)), fill="red")
|
||||
draw.rectangle(((250, 0), (w - 250, 500)), fill="red")
|
||||
draw.text(
|
||||
(120, -60),
|
||||
text,
|
||||
font=font,
|
||||
fill="white",
|
||||
stroke_width=10,
|
||||
stroke_fill="white",
|
||||
)
|
||||
return img
|
||||
@@ -1,40 +0,0 @@
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.adapters.onebot.v11 import Bot as v11Bot
|
||||
from nonebot.adapters.onebot.v12 import Bot as v12Bot
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Arparma, UniMessage, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from .data_source import get_epic_free
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="epic免费游戏",
|
||||
description="可以不玩,不能没有,每日白嫖",
|
||||
usage="""
|
||||
epic
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="AkashiCoin",
|
||||
version="0.1",
|
||||
).dict(),
|
||||
)
|
||||
|
||||
_matcher = on_alconna(Alconna("epic"), priority=5, block=True)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(bot: Bot, session: EventSession, arparma: Arparma):
|
||||
gid = session.id3 or session.id2
|
||||
type_ = "Group" if gid else "Private"
|
||||
msg_list, code = await get_epic_free(bot, type_)
|
||||
if code == 404 and isinstance(msg_list, str):
|
||||
await MessageUtils.build_message(msg_list).finish()
|
||||
elif isinstance(bot, (v11Bot, v12Bot)) and isinstance(msg_list, list):
|
||||
await bot.send_group_forward_msg(group_id=gid, messages=msg_list)
|
||||
elif isinstance(msg_list, UniMessage):
|
||||
await msg_list.send()
|
||||
logger.info(f"获取epic免费游戏", arparma.header_result, session=session)
|
||||
@@ -1,180 +0,0 @@
|
||||
from datetime import datetime
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.adapters.onebot.v11 import Bot as v11Bot
|
||||
from nonebot.adapters.onebot.v12 import Bot as v12Bot
|
||||
from nonebot_plugin_alconna import Image, UniMessage
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
|
||||
# 获取所有 Epic Game Store 促销游戏
|
||||
# 方法参考:RSSHub /epicgames 路由
|
||||
# https://github.com/DIYgod/RSSHub/blob/master/lib/v2/epicgames/index.js
|
||||
async def get_epic_game() -> dict | None:
|
||||
epic_url = "https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions?locale=zh-CN&country=CN&allowCountries=CN"
|
||||
headers = {
|
||||
"Referer": "https://www.epicgames.com/store/zh-CN/",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36",
|
||||
}
|
||||
try:
|
||||
res = await AsyncHttpx.get(epic_url, headers=headers, timeout=10)
|
||||
res_json = res.json()
|
||||
games = res_json["data"]["Catalog"]["searchStore"]["elements"]
|
||||
return games
|
||||
except Exception as e:
|
||||
logger.error(f"Epic 访问接口错误", e=e)
|
||||
return None
|
||||
|
||||
|
||||
# 此处用于获取游戏简介
|
||||
async def get_epic_game_desp(name) -> dict | None:
|
||||
desp_url = (
|
||||
"https://store-content-ipv4.ak.epicgames.com/api/zh-CN/content/products/"
|
||||
+ str(name)
|
||||
)
|
||||
headers = {
|
||||
"Referer": "https://store.epicgames.com/zh-CN/p/" + str(name),
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36",
|
||||
}
|
||||
try:
|
||||
res = await AsyncHttpx.get(desp_url, headers=headers, timeout=10)
|
||||
res_json = res.json()
|
||||
gamesDesp = res_json["pages"][0]["data"]["about"]
|
||||
return gamesDesp
|
||||
except Exception as e:
|
||||
logger.error(f"Epic 访问接口错误", e=e)
|
||||
return None
|
||||
|
||||
|
||||
# 获取 Epic Game Store 免费游戏信息
|
||||
# 处理免费游戏的信息方法借鉴 pip 包 epicstore_api 示例
|
||||
# https://github.com/SD4RK/epicstore_api/blob/master/examples/free_games_example.py
|
||||
async def get_epic_free(
|
||||
bot: Bot, type_event: str
|
||||
) -> tuple[UniMessage | list | str, int]:
|
||||
games = await get_epic_game()
|
||||
if not games:
|
||||
return "Epic 可能又抽风啦,请稍后再试(", 404
|
||||
else:
|
||||
msg_list = []
|
||||
for game in games:
|
||||
game_name = game["title"]
|
||||
game_corp = game["seller"]["name"]
|
||||
game_price = game["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
|
||||
# 赋初值以避免 local variable referenced before assignment
|
||||
game_thumbnail, game_dev, game_pub = None, game_corp, game_corp
|
||||
try:
|
||||
game_promotions = game["promotions"]["promotionalOffers"]
|
||||
upcoming_promotions = game["promotions"]["upcomingPromotionalOffers"]
|
||||
if not game_promotions and upcoming_promotions:
|
||||
# 促销暂未上线,但即将上线
|
||||
promotion_data = upcoming_promotions[0]["promotionalOffers"][0]
|
||||
start_date_iso, end_date_iso = (
|
||||
promotion_data["startDate"][:-1],
|
||||
promotion_data["endDate"][:-1],
|
||||
)
|
||||
# 删除字符串中最后一个 "Z" 使 Python datetime 可处理此时间
|
||||
start_date = datetime.fromisoformat(start_date_iso).strftime(
|
||||
"%b.%d %H:%M"
|
||||
)
|
||||
end_date = datetime.fromisoformat(end_date_iso).strftime(
|
||||
"%b.%d %H:%M"
|
||||
)
|
||||
if type_event == "Group":
|
||||
_message = f"\n由 {game_corp} 公司发行的游戏 {game_name} ({game_price}) 在 UTC 时间 {start_date} 即将推出免费游玩,预计截至 {end_date}。"
|
||||
msg_list.append(_message)
|
||||
else:
|
||||
msg = "\n由 {} 公司发行的游戏 {} ({}) 在 UTC 时间 {} 即将推出免费游玩,预计截至 {}。".format(
|
||||
game_corp, game_name, game_price, start_date, end_date
|
||||
)
|
||||
msg_list.append(msg)
|
||||
else:
|
||||
for image in game["keyImages"]:
|
||||
if (
|
||||
image.get("url")
|
||||
and not game_thumbnail
|
||||
and image["type"]
|
||||
in [
|
||||
"Thumbnail",
|
||||
"VaultOpened",
|
||||
"DieselStoreFrontWide",
|
||||
"OfferImageWide",
|
||||
]
|
||||
):
|
||||
game_thumbnail = image["url"]
|
||||
break
|
||||
for pair in game["customAttributes"]:
|
||||
if pair["key"] == "developerName":
|
||||
game_dev = pair["value"]
|
||||
if pair["key"] == "publisherName":
|
||||
game_pub = pair["value"]
|
||||
if game.get("productSlug"):
|
||||
if gamesDesp := await get_epic_game_desp(game["productSlug"]):
|
||||
try:
|
||||
# 是否存在简短的介绍
|
||||
if "shortDescription" in gamesDesp:
|
||||
game_desp = gamesDesp["shortDescription"]
|
||||
except KeyError:
|
||||
game_desp = gamesDesp["description"]
|
||||
else:
|
||||
game_desp = game["description"]
|
||||
try:
|
||||
end_date_iso = game["promotions"]["promotionalOffers"][0][
|
||||
"promotionalOffers"
|
||||
][0]["endDate"][:-1]
|
||||
end_date = datetime.fromisoformat(end_date_iso).strftime(
|
||||
"%b.%d %H:%M"
|
||||
)
|
||||
except IndexError:
|
||||
end_date = "未知"
|
||||
# API 返回不包含游戏商店 URL,此处自行拼接,可能出现少数游戏 404 请反馈
|
||||
if game.get("productSlug"):
|
||||
game_url = "https://store.epicgames.com/zh-CN/p/{}".format(
|
||||
game["productSlug"].replace("/home", "")
|
||||
)
|
||||
elif game.get("url"):
|
||||
game_url = game["url"]
|
||||
else:
|
||||
slugs = (
|
||||
[
|
||||
x["pageSlug"]
|
||||
for x in game.get("offerMappings", [])
|
||||
if x.get("pageType") == "productHome"
|
||||
]
|
||||
+ [
|
||||
x["pageSlug"]
|
||||
for x in game.get("catalogNs", {}).get("mappings", [])
|
||||
if x.get("pageType") == "productHome"
|
||||
]
|
||||
+ [
|
||||
x["value"]
|
||||
for x in game.get("customAttributes", [])
|
||||
if "productSlug" in x.get("key")
|
||||
]
|
||||
)
|
||||
game_url = "https://store.epicgames.com/zh-CN{}".format(
|
||||
f"/p/{slugs[0]}" if len(slugs) else ""
|
||||
)
|
||||
if isinstance(bot, (v11Bot, v12Bot)) and type_event == "Group":
|
||||
_message = [
|
||||
Image(url=game_thumbnail),
|
||||
f"\nFREE now :: {game_name} ({game_price})\n{game_desp}\n此游戏由 {game_dev} 开发、{game_pub} 发行,将在 UTC 时间 {end_date} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{game_url}\n",
|
||||
]
|
||||
msg_list.append(_message)
|
||||
else:
|
||||
_message = []
|
||||
if game_thumbnail:
|
||||
_message.append(Image(url=game_thumbnail))
|
||||
_message.append(
|
||||
f"\n\nFREE now :: {game_name} ({game_price})\n{game_desp}\n此游戏由 {game_dev} 开发、{game_pub} 发行,将在 UTC 时间 {end_date} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{game_url}\n"
|
||||
)
|
||||
return MessageUtils.build_message(_message), 200
|
||||
except TypeError as e:
|
||||
# logger.info(str(e))
|
||||
pass
|
||||
return MessageUtils.template2forward(msg_list, bot.self_id), 200
|
||||
@@ -1,151 +0,0 @@
|
||||
import random
|
||||
|
||||
from nonebot import on_message
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Image as alcImg
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import BotConfig, Config
|
||||
from zhenxun.configs.path_config import TEMP_PATH
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.image_utils import get_download_image_hash
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.rules import ensure_group
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="复读",
|
||||
description="群友的本质是什么?是复读机哒!",
|
||||
usage="""
|
||||
usage:
|
||||
重复3次相同的消息时会复读
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
menu_type="其他",
|
||||
plugin_type=PluginType.DEPENDANT,
|
||||
tasks=[Task(module="fudu", name="复读")],
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
key="FUDU_PROBABILITY",
|
||||
value=0.7,
|
||||
help="复读概率",
|
||||
default_value=0.7,
|
||||
type=float,
|
||||
),
|
||||
RegisterConfig(
|
||||
module="_task",
|
||||
key="DEFAULT_FUDU",
|
||||
value=True,
|
||||
help="被动 复读 进群默认开关状态",
|
||||
default_value=True,
|
||||
type=bool,
|
||||
),
|
||||
],
|
||||
).dict(),
|
||||
)
|
||||
|
||||
|
||||
class Fudu:
|
||||
def __init__(self):
|
||||
self.data = {}
|
||||
|
||||
def append(self, key, content):
|
||||
self._create(key)
|
||||
self.data[key]["data"].append(content)
|
||||
|
||||
def clear(self, key):
|
||||
self._create(key)
|
||||
self.data[key]["data"] = []
|
||||
self.data[key]["is_repeater"] = False
|
||||
|
||||
def size(self, key) -> int:
|
||||
self._create(key)
|
||||
return len(self.data[key]["data"])
|
||||
|
||||
def check(self, key, content) -> bool:
|
||||
self._create(key)
|
||||
return self.data[key]["data"][0] == content
|
||||
|
||||
def get(self, key):
|
||||
self._create(key)
|
||||
return self.data[key]["data"][0]
|
||||
|
||||
def is_repeater(self, key):
|
||||
self._create(key)
|
||||
return self.data[key]["is_repeater"]
|
||||
|
||||
def set_repeater(self, key):
|
||||
self._create(key)
|
||||
self.data[key]["is_repeater"] = True
|
||||
|
||||
def _create(self, key):
|
||||
if self.data.get(key) is None:
|
||||
self.data[key] = {"is_repeater": False, "data": []}
|
||||
|
||||
|
||||
_manage = Fudu()
|
||||
|
||||
|
||||
base_config = Config.get("fudu")
|
||||
|
||||
|
||||
_matcher = on_message(rule=ensure_group, priority=999)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(message: UniMsg, event: Event, session: EventSession):
|
||||
group_id = session.id2 or ""
|
||||
if await TaskInfo.is_block("fudu", group_id):
|
||||
return
|
||||
if event.is_tome():
|
||||
return
|
||||
plain_text = message.extract_plain_text()
|
||||
image_list = []
|
||||
for m in message:
|
||||
if isinstance(m, alcImg):
|
||||
if m.url:
|
||||
image_list.append(m.url)
|
||||
if not plain_text and not image_list:
|
||||
return
|
||||
if plain_text and plain_text.startswith(f"@可爱的{BotConfig.self_nickname}"):
|
||||
await MessageUtils.build_message("复制粘贴的虚空艾特?").send(reply_to=True)
|
||||
if image_list:
|
||||
img_hash = await get_download_image_hash(image_list[0], group_id)
|
||||
else:
|
||||
img_hash = ""
|
||||
add_msg = plain_text + "|-|" + img_hash
|
||||
if _manage.size(group_id) == 0:
|
||||
_manage.append(group_id, add_msg)
|
||||
elif _manage.check(group_id, add_msg):
|
||||
_manage.append(group_id, add_msg)
|
||||
else:
|
||||
_manage.clear(group_id)
|
||||
_manage.append(group_id, add_msg)
|
||||
if _manage.size(group_id) > 2:
|
||||
if random.random() < base_config.get(
|
||||
"FUDU_PROBABILITY"
|
||||
) and not _manage.is_repeater(group_id):
|
||||
if random.random() < 0.2:
|
||||
if plain_text.startswith("打断施法"):
|
||||
await MessageUtils.build_message("打断" + plain_text).finish()
|
||||
else:
|
||||
await MessageUtils.build_message("打断施法!").finish()
|
||||
_manage.set_repeater(group_id)
|
||||
rst = None
|
||||
if image_list and plain_text:
|
||||
rst = MessageUtils.build_message(
|
||||
[plain_text, TEMP_PATH / f"compare_download_{group_id}_img.jpg"]
|
||||
)
|
||||
elif image_list:
|
||||
rst = MessageUtils.build_message(
|
||||
TEMP_PATH / f"compare_download_{group_id}_img.jpg"
|
||||
)
|
||||
elif plain_text:
|
||||
rst = MessageUtils.build_message(plain_text)
|
||||
if rst:
|
||||
await rst.finish()
|
||||
@@ -1,349 +0,0 @@
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from apscheduler.jobstores.base import JobLookupError
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.exception import ActionFailed
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me
|
||||
from nonebot_plugin_alconna import Alconna, Args, Arparma, At, Match, Option, on_alconna
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.depends import GetConfig, UserName
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
from zhenxun.utils.rules import ensure_group
|
||||
|
||||
from .config import FESTIVE_KEY, FestiveRedBagManage
|
||||
from .data_source import RedBagManager
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="金币红包",
|
||||
description="运气项目又来了",
|
||||
usage="""
|
||||
塞红包 [金币数] ?[红包数=5] ?[at指定人]: 塞入红包
|
||||
开/抢: 打开红包
|
||||
退回红包: 退回未开完的红包,必须在一分钟后使用
|
||||
|
||||
* 不同群组同一个节日红包用户只能开一次
|
||||
|
||||
示例:
|
||||
塞红包 1000
|
||||
塞红包 1000 10
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
superuser_help="""
|
||||
节日红包 [金额] [红包数] ?[指定主题文字] ? -g [群id] [群id] ...
|
||||
|
||||
* 不同群组同一个节日红包用户只能开一次
|
||||
|
||||
示例:
|
||||
节日红包 10000 20 今日出道贺金
|
||||
节日红包 10000 20 明日出道贺金 -g 123123123
|
||||
|
||||
""",
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
key="DEFAULT_TIMEOUT",
|
||||
value=600,
|
||||
help="普通红包默认超时时间",
|
||||
default_value=600,
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="DEFAULT_INTERVAL",
|
||||
value=60,
|
||||
help="用户发送普通红包最小间隔时间",
|
||||
default_value=60,
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="RANK_NUM",
|
||||
value=10,
|
||||
help="结算排行显示前N位",
|
||||
default_value=10,
|
||||
type=int,
|
||||
),
|
||||
],
|
||||
limits=[PluginCdBlock(result="急什么急什么,待会再发!")],
|
||||
).dict(),
|
||||
)
|
||||
|
||||
|
||||
# def rule(session: EventSession) -> bool:
|
||||
# if gid := session.id3 or session.id2:
|
||||
# if group_red_bag := RedBagManager.get_group_data(gid):
|
||||
# return group_red_bag.check_open(gid)
|
||||
# return False
|
||||
|
||||
|
||||
# async def rule_group(session: EventSession):
|
||||
# return rule(session) and ensure_group(session)
|
||||
|
||||
|
||||
_red_bag_matcher = on_alconna(
|
||||
Alconna("塞红包", Args["amount", int]["num", int, 5]["user?", At]),
|
||||
aliases={"金币红包"},
|
||||
priority=5,
|
||||
block=True,
|
||||
rule=ensure_group,
|
||||
)
|
||||
|
||||
_open_matcher = on_alconna(
|
||||
Alconna("开"),
|
||||
aliases={"抢", "开红包", "抢红包"},
|
||||
priority=5,
|
||||
block=True,
|
||||
rule=ensure_group,
|
||||
)
|
||||
|
||||
_return_matcher = on_alconna(
|
||||
Alconna("退回红包"), aliases={"退还红包"}, priority=5, block=True, rule=ensure_group
|
||||
)
|
||||
|
||||
_festive_matcher = on_alconna(
|
||||
Alconna(
|
||||
"节日红包",
|
||||
Args["amount", int]["num", int]["text?", str],
|
||||
Option("-g|--group", Args["groups", str] / "\n", help_text="指定群"),
|
||||
),
|
||||
priority=1,
|
||||
block=True,
|
||||
permission=SUPERUSER,
|
||||
rule=to_me(),
|
||||
)
|
||||
|
||||
|
||||
@_red_bag_matcher.handle()
|
||||
async def _(
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
amount: int,
|
||||
num: int,
|
||||
user: Match[At],
|
||||
default_interval: int = GetConfig(config="DEFAULT_INTERVAL"),
|
||||
user_name: str = UserName(),
|
||||
):
|
||||
at_user = None
|
||||
if user.available:
|
||||
at_user = user.result.target
|
||||
# group_id = session.id3 or session.id2
|
||||
group_id = session.id2
|
||||
"""以频道id为键"""
|
||||
user_id = session.id1
|
||||
if not user_id:
|
||||
await MessageUtils.build_message("用户id为空").finish()
|
||||
if not group_id:
|
||||
await MessageUtils.build_message("群组id为空").finish()
|
||||
group_red_bag = RedBagManager.get_group_data(group_id)
|
||||
# 剩余过期时间
|
||||
time_remaining = group_red_bag.check_timeout(user_id)
|
||||
if time_remaining != -1:
|
||||
# 判断用户红包是否存在且是否过时覆盖
|
||||
if user_red_bag := group_red_bag.get_user_red_bag(user_id):
|
||||
now = time.time()
|
||||
if now < user_red_bag.start_time + default_interval:
|
||||
await MessageUtils.build_message(
|
||||
f"你的红包还没消化完捏...还剩下 {user_red_bag.num - len(user_red_bag.open_user)} 个! 请等待红包领取完毕..."
|
||||
f"(或等待{time_remaining}秒红包cd)"
|
||||
).finish()
|
||||
result = await RedBagManager.check_gold(user_id, amount, session.platform)
|
||||
if result:
|
||||
await MessageUtils.build_message(result).finish(at_sender=True)
|
||||
await group_red_bag.add_red_bag(
|
||||
f"{user_name}的红包",
|
||||
int(amount),
|
||||
1 if at_user else num,
|
||||
user_name,
|
||||
user_id,
|
||||
assigner=at_user,
|
||||
platform=session.platform,
|
||||
)
|
||||
image = await RedBagManager.random_red_bag_background(
|
||||
user_id, platform=session.platform
|
||||
)
|
||||
message_list: list = [f"{user_name}发起了金币红包\n金额: {amount}\n数量: {num}\n"]
|
||||
if at_user:
|
||||
message_list.append("指定人: ")
|
||||
message_list.append(At(flag="user", target=at_user))
|
||||
message_list.append("\n")
|
||||
message_list.append(image)
|
||||
await MessageUtils.build_message(message_list).send()
|
||||
|
||||
logger.info(
|
||||
f"塞入 {num} 个红包,共 {amount} 金币", arparma.header_result, session=session
|
||||
)
|
||||
|
||||
|
||||
@_open_matcher.handle()
|
||||
async def _(
|
||||
session: EventSession,
|
||||
rank_num: int = GetConfig(config="RANK_NUM"),
|
||||
):
|
||||
# group_id = session.id3 or session.id2
|
||||
group_id = session.id2
|
||||
"""以频道id为键"""
|
||||
user_id = session.id1
|
||||
if not user_id:
|
||||
await MessageUtils.build_message("用户id为空").finish()
|
||||
if not group_id:
|
||||
await MessageUtils.build_message("群组id为空").finish()
|
||||
if group_red_bag := RedBagManager.get_group_data(group_id):
|
||||
open_data, settlement_list = await group_red_bag.open(user_id, session.platform)
|
||||
# send_msg = Text("没有红包给你开!")
|
||||
send_msg = []
|
||||
for _, item in open_data.items():
|
||||
amount, red_bag = item
|
||||
result_image = await RedBagManager.build_open_result_image(
|
||||
red_bag, user_id, amount, session.platform
|
||||
)
|
||||
send_msg.append(f"开启了 {red_bag.promoter} 的红包, 获取 {amount} 个金币\n")
|
||||
send_msg.append(result_image)
|
||||
send_msg.append("\n")
|
||||
logger.info(
|
||||
f"抢到了 {red_bag.promoter}({red_bag.promoter_id}) 的红包,获取了{amount}个金币",
|
||||
"开红包",
|
||||
session=session,
|
||||
)
|
||||
send_msg = (
|
||||
MessageUtils.build_message(send_msg[:-1])
|
||||
if send_msg
|
||||
else MessageUtils.build_message("没有红包给你开!")
|
||||
)
|
||||
await send_msg.send(reply_to=True)
|
||||
if settlement_list:
|
||||
for red_bag in settlement_list:
|
||||
result_image = await red_bag.build_amount_rank(
|
||||
rank_num, session.platform
|
||||
)
|
||||
await MessageUtils.build_message(
|
||||
[f"{red_bag.name}已结算\n", result_image]
|
||||
).send()
|
||||
|
||||
|
||||
@_return_matcher.handle()
|
||||
async def _(
|
||||
session: EventSession,
|
||||
default_interval: int = GetConfig(config="DEFAULT_INTERVAL"),
|
||||
rank_num: int = GetConfig(config="RANK_NUM"),
|
||||
):
|
||||
group_id = session.id3 or session.id2
|
||||
user_id = session.id1
|
||||
if not user_id:
|
||||
await MessageUtils.build_message("用户id为空").finish()
|
||||
if not group_id:
|
||||
await MessageUtils.build_message("群组id为空").finish()
|
||||
if group_red_bag := RedBagManager.get_group_data(group_id):
|
||||
if user_red_bag := group_red_bag.get_user_red_bag(user_id):
|
||||
now = time.time()
|
||||
if now - user_red_bag.start_time < default_interval:
|
||||
await MessageUtils.build_message(
|
||||
f"你的红包还没有过时, 在 {int(default_interval - now + user_red_bag.start_time)} "
|
||||
f"秒后可以退回..."
|
||||
).finish(reply_to=True)
|
||||
user_red_bag = group_red_bag.get_user_red_bag(user_id)
|
||||
if user_red_bag and (
|
||||
data := await group_red_bag.settlement(user_id, session.platform)
|
||||
):
|
||||
image_result = await user_red_bag.build_amount_rank(
|
||||
rank_num, session.platform
|
||||
)
|
||||
logger.info(f"退回了红包 {data[0]} 金币", "红包退回", session=session)
|
||||
await MessageUtils.build_message(
|
||||
[
|
||||
f"已成功退还了 " f"{data[0]} 金币\n",
|
||||
image_result,
|
||||
]
|
||||
).finish(reply_to=True)
|
||||
await MessageUtils.build_message("目前没有红包可以退回...").finish(reply_to=True)
|
||||
|
||||
|
||||
@_festive_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
amount: int,
|
||||
num: int,
|
||||
text: Match[str],
|
||||
groups: Match[str],
|
||||
):
|
||||
greetings = "恭喜发财 大吉大利"
|
||||
if text.available:
|
||||
greetings = text.result
|
||||
gl = []
|
||||
if groups.available:
|
||||
gl = groups.result.strip().split()
|
||||
else:
|
||||
g_l, platform = await PlatformUtils.get_group_list(bot)
|
||||
gl = [g.channel_id or g.group_id for g in g_l]
|
||||
_uuid = str(uuid.uuid1())
|
||||
FestiveRedBagManage.add(_uuid)
|
||||
_suc_cnt = 0
|
||||
for g in gl:
|
||||
if target := PlatformUtils.get_target(bot, group_id=g):
|
||||
group_red_bag = RedBagManager.get_group_data(g)
|
||||
if festive_red_bag := group_red_bag.get_festive_red_bag():
|
||||
group_red_bag.remove_festive_red_bag()
|
||||
if festive_red_bag.uuid:
|
||||
FestiveRedBagManage.remove(festive_red_bag.uuid)
|
||||
rank_image = await festive_red_bag.build_amount_rank(10, platform)
|
||||
try:
|
||||
await MessageUtils.build_message(
|
||||
[
|
||||
f"{BotConfig.self_nickname}的节日红包过时了,一共开启了 "
|
||||
f"{len(festive_red_bag.open_user)}"
|
||||
f" 个红包,共 {sum(festive_red_bag.open_user.values())} 金币\n",
|
||||
rank_image,
|
||||
]
|
||||
).send(target=target, bot=bot)
|
||||
except ActionFailed:
|
||||
pass
|
||||
try:
|
||||
scheduler.remove_job(f"{FESTIVE_KEY}_{g}")
|
||||
await RedBagManager.end_red_bag(
|
||||
g, is_festive=True, platform=session.platform
|
||||
)
|
||||
except JobLookupError:
|
||||
pass
|
||||
await group_red_bag.add_red_bag(
|
||||
f"{BotConfig.self_nickname}的红包",
|
||||
amount,
|
||||
num,
|
||||
BotConfig.self_nickname,
|
||||
FESTIVE_KEY,
|
||||
_uuid,
|
||||
platform=session.platform,
|
||||
)
|
||||
scheduler.add_job(
|
||||
RedBagManager._auto_end_festive_red_bag,
|
||||
"date",
|
||||
run_date=(datetime.now() + timedelta(hours=24)).replace(microsecond=0),
|
||||
id=f"{FESTIVE_KEY}_{g}",
|
||||
args=[bot, g, session.platform],
|
||||
)
|
||||
try:
|
||||
image_result = await RedBagManager.random_red_bag_background(
|
||||
bot.self_id, greetings, session.platform
|
||||
)
|
||||
await MessageUtils.build_message(
|
||||
[
|
||||
f"{BotConfig.self_nickname}发起了节日金币红包\n金额: {amount}\n数量: {num}\n",
|
||||
image_result,
|
||||
]
|
||||
).send(target=target, bot=bot)
|
||||
_suc_cnt += 1
|
||||
logger.debug("节日红包图片信息发送成功...", "节日红包", group_id=g)
|
||||
except ActionFailed:
|
||||
logger.warning(f"节日红包图片信息发送失败...", "节日红包", group_id=g)
|
||||
if gl:
|
||||
await MessageUtils.build_message(
|
||||
f"节日红包发送成功,累计成功发送 {_suc_cnt} 个群组!"
|
||||
).send()
|
||||
@@ -1,372 +0,0 @@
|
||||
import random
|
||||
import time
|
||||
from io import BytesIO
|
||||
from typing import Dict
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from zhenxun.models.group_member_info import GroupInfoUser
|
||||
from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
from zhenxun.utils.utils import get_user_avatar
|
||||
|
||||
from .model import RedbagUser
|
||||
|
||||
FESTIVE_KEY = "FESTIVE"
|
||||
"""节日红包KEY"""
|
||||
|
||||
|
||||
class FestiveRedBagManage:
|
||||
|
||||
_data: Dict[str, list[str]] = {}
|
||||
|
||||
@classmethod
|
||||
def add(cls, uuid: str):
|
||||
cls._data[uuid] = []
|
||||
|
||||
@classmethod
|
||||
def open(cls, uuid: str, uid: str):
|
||||
if uuid in cls._data and uid not in cls._data[uuid]:
|
||||
cls._data[uuid].append(uid)
|
||||
|
||||
@classmethod
|
||||
def remove(cls, uuid: str):
|
||||
if uuid in cls._data:
|
||||
del cls._data[uuid]
|
||||
|
||||
@classmethod
|
||||
def check(cls, uuid: str, uid: str):
|
||||
if uuid in cls._data:
|
||||
return uid not in cls._data[uuid]
|
||||
return False
|
||||
|
||||
|
||||
class RedBag(BaseModel):
|
||||
"""
|
||||
红包
|
||||
"""
|
||||
|
||||
group_id: str
|
||||
"""所属群聊"""
|
||||
name: str
|
||||
"""红包名称"""
|
||||
amount: int
|
||||
"""总金币"""
|
||||
num: int
|
||||
"""红包数量"""
|
||||
promoter: str
|
||||
"""发起人昵称"""
|
||||
promoter_id: str
|
||||
"""发起人id"""
|
||||
is_festival: bool
|
||||
"""是否为节日红包"""
|
||||
timeout: int
|
||||
"""过期时间"""
|
||||
assigner: str | None = None
|
||||
"""指定人id"""
|
||||
start_time: float
|
||||
"""红包发起时间"""
|
||||
open_user: Dict[str, int] = {}
|
||||
"""开启用户"""
|
||||
red_bag_list: list[int]
|
||||
"""红包金额列表"""
|
||||
uuid: str | None
|
||||
"""uuid"""
|
||||
|
||||
async def build_amount_rank(self, num: int, platform: str) -> BuildImage:
|
||||
"""生成结算红包图片
|
||||
|
||||
参数:
|
||||
num: 查看的排名数量.
|
||||
platform: 平台.
|
||||
|
||||
返回:
|
||||
BuildImage: 结算红包图片
|
||||
"""
|
||||
user_image_list = []
|
||||
if self.open_user:
|
||||
sort_data = sorted(
|
||||
self.open_user.items(), key=lambda item: item[1], reverse=True
|
||||
)
|
||||
num = num if num < len(self.open_user) else len(self.open_user)
|
||||
user_id_list = [sort_data[i][0] for i in range(num)]
|
||||
group_user_list = await GroupInfoUser.filter(
|
||||
group_id=self.group_id, user_id__in=user_id_list
|
||||
).all()
|
||||
for i in range(num):
|
||||
user_background = BuildImage(600, 100, font_size=30)
|
||||
user_id, amount = sort_data[i]
|
||||
user_ava_bytes = await PlatformUtils.get_user_avatar(user_id, platform)
|
||||
user_ava = None
|
||||
if user_ava_bytes:
|
||||
user_ava = BuildImage(80, 80, background=BytesIO(user_ava_bytes))
|
||||
else:
|
||||
user_ava = BuildImage(80, 80)
|
||||
await user_ava.circle_corner(10)
|
||||
await user_background.paste(user_ava, (130, 10))
|
||||
no_image = BuildImage(100, 100, font_size=65, font="CJGaoDeGuo.otf")
|
||||
await no_image.text((0, 0), f"{i+1}", center_type="center")
|
||||
await no_image.line((99, 10, 99, 90), "#b9b9b9")
|
||||
await user_background.paste(no_image)
|
||||
name = [
|
||||
user.user_name
|
||||
for user in group_user_list
|
||||
if user_id == user.user_id
|
||||
]
|
||||
await user_background.text((225, 15), name[0] if name else "")
|
||||
amount_image = await BuildImage.build_text_image(
|
||||
f"{amount} 元", size=30, font_color="#cdac72"
|
||||
)
|
||||
await user_background.paste(
|
||||
amount_image, (user_background.width - amount_image.width - 20, 50)
|
||||
)
|
||||
await user_background.line((225, 99, 590, 99), "#b9b9b9")
|
||||
user_image_list.append(user_background)
|
||||
background = BuildImage(600, 150 + len(user_image_list) * 100)
|
||||
top = BuildImage(600, 100, color="#f55545", font_size=30)
|
||||
promoter_ava_bytes = await PlatformUtils.get_user_avatar(
|
||||
self.promoter_id, platform
|
||||
)
|
||||
promoter_ava = None
|
||||
if promoter_ava_bytes:
|
||||
promoter_ava = BuildImage(60, 60, background=BytesIO(promoter_ava_bytes))
|
||||
else:
|
||||
promoter_ava = BuildImage(60, 60)
|
||||
await promoter_ava.circle()
|
||||
await top.paste(promoter_ava, (10, 0), "height")
|
||||
await top.text((80, 33), self.name, (255, 255, 255))
|
||||
right_text = BuildImage(150, 100, color="#f55545", font_size=30)
|
||||
await right_text.text((10, 33), "结算排行", (255, 255, 255))
|
||||
await right_text.line((4, 10, 4, 90), (255, 255, 255), 2)
|
||||
await top.paste(right_text, (460, 0))
|
||||
await background.paste(top)
|
||||
cur_h = 110
|
||||
for user_image in user_image_list:
|
||||
await background.paste(user_image, (0, cur_h))
|
||||
cur_h += user_image.height
|
||||
return background
|
||||
|
||||
|
||||
class GroupRedBag:
|
||||
"""
|
||||
群组红包管理
|
||||
"""
|
||||
|
||||
def __init__(self, group_id: str):
|
||||
self.group_id = group_id
|
||||
self._data: Dict[str, RedBag] = {}
|
||||
"""红包列表"""
|
||||
|
||||
def remove_festive_red_bag(self):
|
||||
"""删除节日红包"""
|
||||
_key = None
|
||||
for k, red_bag in self._data.items():
|
||||
if red_bag.is_festival:
|
||||
_key = k
|
||||
break
|
||||
if _key:
|
||||
del self._data[_key]
|
||||
|
||||
def get_festive_red_bag(self) -> RedBag | None:
|
||||
"""获取节日红包
|
||||
|
||||
返回:
|
||||
RedBag | None: 节日红包
|
||||
"""
|
||||
for _, red_bag in self._data.items():
|
||||
if red_bag.is_festival:
|
||||
return red_bag
|
||||
return None
|
||||
|
||||
def get_user_red_bag(self, user_id: str) -> RedBag | None:
|
||||
"""获取用户塞红包数据
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
|
||||
返回:
|
||||
RedBag | None: RedBag
|
||||
"""
|
||||
return self._data.get(str(user_id))
|
||||
|
||||
def check_open(self, user_id: str) -> bool:
|
||||
"""检查是否有可开启的红包
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
|
||||
返回:
|
||||
bool: 是否有可开启的红包
|
||||
"""
|
||||
user_id = str(user_id)
|
||||
for _, red_bag in self._data.items():
|
||||
if red_bag.assigner:
|
||||
if red_bag.assigner == user_id:
|
||||
return True
|
||||
else:
|
||||
if user_id not in red_bag.open_user:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_timeout(self, user_id: str) -> int:
|
||||
"""判断用户红包是否过期
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
|
||||
返回:
|
||||
int: 距离过期时间
|
||||
"""
|
||||
if user_id in self._data:
|
||||
reg_bag = self._data[user_id]
|
||||
now = time.time()
|
||||
if now < reg_bag.timeout + reg_bag.start_time:
|
||||
return int(reg_bag.timeout + reg_bag.start_time - now)
|
||||
return -1
|
||||
|
||||
async def open(
|
||||
self, user_id: str, platform: str | None = None
|
||||
) -> tuple[Dict[str, tuple[int, RedBag]], list[RedBag]]:
|
||||
"""开启红包
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
platform: 所属平台
|
||||
|
||||
返回:
|
||||
Dict[str, tuple[int, RedBag]]: 键为发起者id, 值为开启金额以及对应RedBag
|
||||
list[RedBag]: 开完的红包
|
||||
"""
|
||||
open_data = {}
|
||||
settlement_list: list[RedBag] = []
|
||||
for _, red_bag in self._data.items():
|
||||
if red_bag.num > len(red_bag.open_user):
|
||||
if red_bag.is_festival and red_bag.uuid:
|
||||
if not FestiveRedBagManage.check(red_bag.uuid, user_id):
|
||||
continue
|
||||
FestiveRedBagManage.open(red_bag.uuid, user_id)
|
||||
is_open = False
|
||||
if red_bag.assigner:
|
||||
is_open = red_bag.assigner == user_id
|
||||
else:
|
||||
is_open = user_id not in red_bag.open_user
|
||||
if is_open:
|
||||
random_amount = red_bag.red_bag_list.pop()
|
||||
await RedbagUser.add_redbag_data(
|
||||
user_id, self.group_id, "get", random_amount
|
||||
)
|
||||
await UserConsole.add_gold(
|
||||
user_id, random_amount, "gold_redbag", platform
|
||||
)
|
||||
red_bag.open_user[user_id] = random_amount
|
||||
open_data[red_bag.promoter_id] = (random_amount, red_bag)
|
||||
if red_bag.num == len(red_bag.open_user):
|
||||
# 红包开完,结算
|
||||
settlement_list.append(red_bag)
|
||||
if settlement_list:
|
||||
for uid in [red_bag.promoter_id for red_bag in settlement_list]:
|
||||
if uid in self._data:
|
||||
del self._data[uid]
|
||||
return open_data, settlement_list
|
||||
|
||||
def festive_red_bag_expire(self) -> RedBag | None:
|
||||
"""节日红包过期
|
||||
|
||||
返回:
|
||||
RedBag | None: 过期的节日红包
|
||||
"""
|
||||
if FESTIVE_KEY in self._data:
|
||||
red_bag = self._data[FESTIVE_KEY]
|
||||
del self._data[FESTIVE_KEY]
|
||||
return red_bag
|
||||
return None
|
||||
|
||||
async def settlement(
|
||||
self, user_id: str, platform: str | None = None
|
||||
) -> tuple[int | None, RedBag | None]:
|
||||
"""红包退回
|
||||
|
||||
参数:
|
||||
user_id: 用户id, 指定id时结算指定用户红包.
|
||||
platform: 用户平台
|
||||
|
||||
返回:
|
||||
tuple[int | None, RedBag | None]: 退回金币, 红包
|
||||
"""
|
||||
if red_bag := self._data.get(user_id):
|
||||
del self._data[user_id]
|
||||
if red_bag.is_festival and red_bag.uuid:
|
||||
FestiveRedBagManage.remove(red_bag.uuid)
|
||||
if red_bag.red_bag_list:
|
||||
"""退还剩余金币"""
|
||||
if amount := sum(red_bag.red_bag_list):
|
||||
await UserConsole.add_gold(user_id, amount, "gold_redbag", platform)
|
||||
return amount, red_bag
|
||||
return None, None
|
||||
|
||||
async def add_red_bag(
|
||||
self,
|
||||
name: str,
|
||||
amount: int,
|
||||
num: int,
|
||||
promoter: str,
|
||||
promoter_id: str,
|
||||
festival_uuid: str | None = None,
|
||||
timeout: int = 60,
|
||||
assigner: str | None = None,
|
||||
platform: str | None = None,
|
||||
):
|
||||
"""添加红包
|
||||
|
||||
参数:
|
||||
name: 红包名称
|
||||
amount: 金币数量
|
||||
num: 红包数量
|
||||
promoter: 发起人昵称
|
||||
promoter_id: 发起人id
|
||||
festival_uuid: 节日红包uuid.
|
||||
timeout: 超时时间.
|
||||
assigner: 指定人.
|
||||
platform: 用户平台.
|
||||
"""
|
||||
user = await UserConsole.get_user(promoter_id, platform)
|
||||
if not festival_uuid and (amount < 1 or user.gold < amount):
|
||||
raise ValueError("红包金币不足或用户金币不足")
|
||||
red_bag_list = self._random_red_bag(amount, num)
|
||||
if not festival_uuid:
|
||||
user.gold -= amount
|
||||
await RedbagUser.add_redbag_data(promoter_id, self.group_id, "send", amount)
|
||||
await user.save(update_fields=["gold"])
|
||||
self._data[promoter_id] = RedBag(
|
||||
group_id=self.group_id,
|
||||
name=name,
|
||||
amount=amount,
|
||||
num=num,
|
||||
promoter=promoter,
|
||||
promoter_id=promoter_id,
|
||||
is_festival=bool(festival_uuid),
|
||||
timeout=timeout,
|
||||
start_time=time.time(),
|
||||
assigner=assigner,
|
||||
red_bag_list=red_bag_list,
|
||||
uuid=festival_uuid,
|
||||
)
|
||||
|
||||
def _random_red_bag(self, amount: int, num: int) -> list[int]:
|
||||
"""初始化红包金币
|
||||
|
||||
参数:
|
||||
amount: 金币数量
|
||||
num: 红包数量
|
||||
|
||||
返回:
|
||||
list[int]: 红包列表
|
||||
"""
|
||||
red_bag_list = []
|
||||
for _ in range(num - 1):
|
||||
tmp = int(amount / random.choice(range(3, num + 3)))
|
||||
red_bag_list.append(tmp)
|
||||
amount -= tmp
|
||||
red_bag_list.append(amount)
|
||||
return red_bag_list
|
||||
@@ -1,234 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
import random
|
||||
from io import BytesIO
|
||||
from typing import Dict
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.exception import ActionFailed
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
|
||||
from zhenxun.configs.config import BotConfig, Config
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from .config import FestiveRedBagManage, GroupRedBag, RedBag
|
||||
|
||||
|
||||
class RedBagManager:
|
||||
|
||||
_data: Dict[str, GroupRedBag] = {}
|
||||
|
||||
@classmethod
|
||||
def get_group_data(cls, group_id: str) -> GroupRedBag:
|
||||
"""获取群组红包数据
|
||||
|
||||
参数:
|
||||
group_id: 群组id
|
||||
|
||||
返回:
|
||||
GroupRedBag | None: GroupRedBag
|
||||
"""
|
||||
if group_id not in cls._data:
|
||||
cls._data[group_id] = GroupRedBag(group_id)
|
||||
return cls._data[group_id]
|
||||
|
||||
@classmethod
|
||||
async def _auto_end_festive_red_bag(cls, bot: Bot, group_id: str, platform: str):
|
||||
"""自动结算节日红包
|
||||
|
||||
参数:
|
||||
bot: Bot
|
||||
group_id: 群组id
|
||||
platform: 平台
|
||||
"""
|
||||
if target := PlatformUtils.get_target(bot, group_id=group_id):
|
||||
rank_num = Config.get_config("gold_redbag", "RANK_NUM") or 10
|
||||
group_red_bag = cls.get_group_data(group_id)
|
||||
red_bag = group_red_bag.get_festive_red_bag()
|
||||
if not red_bag:
|
||||
return
|
||||
rank_image = await red_bag.build_amount_rank(rank_num, platform)
|
||||
if red_bag.is_festival and red_bag.uuid:
|
||||
FestiveRedBagManage.remove(red_bag.uuid)
|
||||
await asyncio.sleep(random.randint(1, 5))
|
||||
try:
|
||||
await MessageUtils.build_message(
|
||||
[
|
||||
f"{BotConfig.self_nickname}的节日红包过时了,一共开启了 "
|
||||
f"{len(red_bag.open_user)}"
|
||||
f" 个红包,共 {sum(red_bag.open_user.values())} 金币\n",
|
||||
rank_image,
|
||||
]
|
||||
).send(target=target, bot=bot)
|
||||
except ActionFailed:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
async def end_red_bag(
|
||||
cls,
|
||||
group_id: str,
|
||||
user_id: str | None = None,
|
||||
is_festive: bool = False,
|
||||
platform: str = "",
|
||||
) -> UniMessage | None:
|
||||
"""结算红包
|
||||
|
||||
参数:
|
||||
group_id: 群组id或频道id
|
||||
user_id: 用户id
|
||||
is_festive: 是否节日红包
|
||||
platform: 用户平台
|
||||
"""
|
||||
rank_num = Config.get_config("gold_redbag", "RANK_NUM") or 10
|
||||
group_red_bag = cls.get_group_data(group_id)
|
||||
if not group_red_bag:
|
||||
return None
|
||||
if is_festive:
|
||||
if festive_red_bag := group_red_bag.festive_red_bag_expire():
|
||||
rank_image = await festive_red_bag.build_amount_rank(rank_num, platform)
|
||||
return MessageUtils.build_message(
|
||||
[
|
||||
f"{BotConfig.self_nickname}的节日红包过时了,一共开启了 "
|
||||
f"{len(festive_red_bag.open_user)}"
|
||||
f" 个红包,共 {sum(festive_red_bag.open_user.values())} 金币\n",
|
||||
rank_image,
|
||||
]
|
||||
)
|
||||
else:
|
||||
if not user_id:
|
||||
return None
|
||||
return_gold, red_bag = await group_red_bag.settlement(user_id, platform)
|
||||
if red_bag:
|
||||
rank_image = await red_bag.build_amount_rank(rank_num, platform)
|
||||
return MessageUtils.build_message(
|
||||
[
|
||||
f"已成功退还了 " f"{return_gold} 金币\n",
|
||||
rank_image.pic2bytes(),
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def check_gold(cls, user_id: str, amount: int, platform: str) -> str | None:
|
||||
"""检查金币数量是否合法
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
amount: 金币数量
|
||||
platform: 所属平台
|
||||
|
||||
返回:
|
||||
tuple[bool, str]: 是否合法以及提示语
|
||||
"""
|
||||
user = await UserConsole.get_user(user_id, platform)
|
||||
if amount < 1:
|
||||
return "小气鬼,要别人倒贴金币给你嘛!"
|
||||
if user.gold < amount:
|
||||
return "没有金币的话请不要发红包..."
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def random_red_bag_background(
|
||||
cls, user_id: str, msg: str = "恭喜发财 大吉大利", platform: str = ""
|
||||
) -> BuildImage:
|
||||
"""构造发送红包图片
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
msg: 红包消息.
|
||||
platform: 平台.
|
||||
|
||||
异常:
|
||||
ValueError: 图片背景列表为空
|
||||
|
||||
返回:
|
||||
BuildImage: 构造后的图片
|
||||
"""
|
||||
background_list = os.listdir(f"{IMAGE_PATH}/prts/redbag_2")
|
||||
if not background_list:
|
||||
raise ValueError("prts/redbag_1 背景图列表为空...")
|
||||
random_redbag = random.choice(background_list)
|
||||
redbag = BuildImage(
|
||||
0,
|
||||
0,
|
||||
font_size=38,
|
||||
background=IMAGE_PATH / "prts" / "redbag_2" / random_redbag,
|
||||
)
|
||||
ava_byte = await PlatformUtils.get_user_avatar(user_id, platform)
|
||||
ava = None
|
||||
if ava_byte:
|
||||
ava = BuildImage(65, 65, background=BytesIO(ava_byte))
|
||||
else:
|
||||
ava = BuildImage(65, 65, color=(0, 0, 0))
|
||||
await ava.circle()
|
||||
await redbag.text(
|
||||
(int((redbag.size[0] - redbag.getsize(msg)[0]) / 2), 210),
|
||||
msg,
|
||||
(240, 218, 164),
|
||||
)
|
||||
await redbag.paste(ava, (int((redbag.size[0] - ava.size[0]) / 2), 130))
|
||||
return redbag
|
||||
|
||||
@classmethod
|
||||
async def build_open_result_image(
|
||||
cls, red_bag: RedBag, user_id: str, amount: int, platform: str
|
||||
) -> BuildImage:
|
||||
"""构造红包开启图片
|
||||
|
||||
参数:
|
||||
red_bag: RedBag
|
||||
user_id: 开启红包用户id
|
||||
amount: 开启红包获取的金额
|
||||
platform: 平台
|
||||
|
||||
异常:
|
||||
ValueError: 图片背景列表为空
|
||||
|
||||
返回:
|
||||
BuildImage: 构造后的图片
|
||||
"""
|
||||
background_list = os.listdir(f"{IMAGE_PATH}/prts/redbag_1")
|
||||
if not background_list:
|
||||
raise ValueError("prts/redbag_1 背景图列表为空...")
|
||||
random_redbag = random.choice(background_list)
|
||||
head = BuildImage(
|
||||
1000,
|
||||
980,
|
||||
font_size=30,
|
||||
background=IMAGE_PATH / "prts" / "redbag_1" / random_redbag,
|
||||
)
|
||||
size = BuildImage.get_text_size(red_bag.name, font_size=50)
|
||||
ava_bk = BuildImage(100 + size[0], 66, (255, 255, 255, 0), font_size=50)
|
||||
|
||||
ava_byte = await PlatformUtils.get_user_avatar(user_id, platform)
|
||||
ava = None
|
||||
if ava_byte:
|
||||
ava = BuildImage(66, 66, background=BytesIO(ava_byte))
|
||||
else:
|
||||
ava = BuildImage(66, 66, color=(0, 0, 0))
|
||||
await ava_bk.paste(ava)
|
||||
await ava_bk.text((100, 7), red_bag.name)
|
||||
ava_bk_w, ava_bk_h = ava_bk.size
|
||||
await head.paste(ava_bk, (int((1000 - ava_bk_w) / 2), 300))
|
||||
size = BuildImage.get_text_size(str(amount), font_size=150)
|
||||
amount_image = BuildImage(size[0], size[1], (255, 255, 255, 0), font_size=150)
|
||||
await amount_image.text((0, 0), str(amount), fill=(209, 171, 108))
|
||||
# 金币中文
|
||||
await head.paste(amount_image, (int((1000 - size[0]) / 2) - 50, 460))
|
||||
await head.text(
|
||||
(int((1000 - size[0]) / 2 + size[0]) - 50, 500 + size[1] - 70),
|
||||
"金币",
|
||||
fill=(209, 171, 108),
|
||||
)
|
||||
# 剩余数量和金额
|
||||
text = (
|
||||
f"已领取"
|
||||
f"{red_bag.num - len(red_bag.open_user)}"
|
||||
f"/{red_bag.num}个,"
|
||||
f"共{sum(red_bag.open_user.values())}/{red_bag.amount}金币"
|
||||
)
|
||||
await head.text((350, 900), text, (198, 198, 198))
|
||||
return head
|
||||
@@ -1,63 +0,0 @@
|
||||
from tortoise import fields
|
||||
|
||||
from zhenxun.services.db_context import Model
|
||||
|
||||
|
||||
class RedbagUser(Model):
|
||||
|
||||
id = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
"""自增id"""
|
||||
user_id = fields.CharField(255)
|
||||
"""用户id"""
|
||||
group_id = fields.CharField(255)
|
||||
"""群聊id"""
|
||||
send_redbag_count = fields.IntField(default=0)
|
||||
"""发送红包次数"""
|
||||
get_redbag_count = fields.IntField(default=0)
|
||||
"""开启红包次数"""
|
||||
spend_gold = fields.IntField(default=0)
|
||||
"""发送红包花费金额"""
|
||||
get_gold = fields.IntField(default=0)
|
||||
"""开启红包获取金额"""
|
||||
|
||||
class Meta:
|
||||
table = "redbag_users"
|
||||
table_description = "红包统计数据表"
|
||||
unique_together = ("user_id", "group_id")
|
||||
|
||||
@classmethod
|
||||
async def add_redbag_data(
|
||||
cls, user_id: str, group_id: str, i_type: str, money: int
|
||||
):
|
||||
"""添加收发红包数据
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
group_id: 群号
|
||||
i_type: 收或发
|
||||
money: 金钱数量
|
||||
"""
|
||||
|
||||
user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id)
|
||||
if i_type == "get":
|
||||
user.get_redbag_count = user.get_redbag_count + 1
|
||||
user.get_gold = user.get_gold + money
|
||||
else:
|
||||
user.send_redbag_count = user.send_redbag_count + 1
|
||||
user.spend_gold = user.spend_gold + money
|
||||
await user.save(
|
||||
update_fields=[
|
||||
"get_redbag_count",
|
||||
"get_gold",
|
||||
"send_redbag_count",
|
||||
"spend_gold",
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def _run_script(cls):
|
||||
return [
|
||||
"ALTER TABLE redbag_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id
|
||||
"ALTER TABLE redbag_users ALTER COLUMN user_id TYPE character varying(255);",
|
||||
"ALTER TABLE redbag_users ALTER COLUMN group_id TYPE character varying(255);",
|
||||
]
|
||||
@@ -1,62 +0,0 @@
|
||||
import re
|
||||
|
||||
import ujson as json
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.path_config import DATA_PATH
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.rules import ensure_group
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="查看群欢迎消息",
|
||||
description="查看群欢迎消息",
|
||||
usage="""
|
||||
usage:
|
||||
查看群欢迎消息
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
menu_type="其他",
|
||||
).dict(),
|
||||
)
|
||||
|
||||
_matcher = on_alconna(Alconna("群欢迎消息"), rule=ensure_group, priority=5, block=True)
|
||||
|
||||
|
||||
BASE_PATH = DATA_PATH / "welcome_message"
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
):
|
||||
path = BASE_PATH / f"{session.platform or session.bot_type}" / f"{session.id2}"
|
||||
if session.id3:
|
||||
path = (
|
||||
BASE_PATH
|
||||
/ f"{session.platform or session.bot_type}"
|
||||
/ f"{session.id3}"
|
||||
/ f"{session.id2}"
|
||||
)
|
||||
file = path / "text.json"
|
||||
if not file.exists():
|
||||
await MessageUtils.build_message("未设置群欢迎消息...").finish(reply_to=True)
|
||||
message = json.load(open(file, encoding="utf8"))["message"]
|
||||
message_split = re.split(r"\[image:\d+\]", message)
|
||||
if len(message_split) == 1:
|
||||
await MessageUtils.build_message(message_split[0]).finish(reply_to=True)
|
||||
idx = 0
|
||||
data_list = []
|
||||
for msg in message_split[:-1]:
|
||||
data_list.append(msg)
|
||||
data_list.append(path / f"{idx}.png")
|
||||
idx += 1
|
||||
data_list.append(message_split[-1])
|
||||
await MessageUtils.build_message(data_list).send(reply_to=True)
|
||||
logger.info("查看群欢迎消息", arparma.header_result, session=session)
|
||||
@@ -1,69 +0,0 @@
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
import nonebot
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
|
||||
Config.add_plugin_config(
|
||||
"image_management",
|
||||
"IMAGE_DIR_LIST",
|
||||
["美图", "萝莉", "壁纸"],
|
||||
help="公开图库列表,可自定义添加 [如果含有send_setu插件,请不要添加色图库]",
|
||||
default_value=[],
|
||||
type=List[str],
|
||||
)
|
||||
|
||||
Config.add_plugin_config(
|
||||
"image_management",
|
||||
"WITHDRAW_IMAGE_MESSAGE",
|
||||
(0, 1),
|
||||
help="自动撤回,参1:延迟撤回发送图库图片的时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)",
|
||||
default_value=(0, 1),
|
||||
type=Tuple[int, int],
|
||||
)
|
||||
|
||||
Config.add_plugin_config(
|
||||
"image_management:delete_image",
|
||||
"DELETE_IMAGE_LEVEL",
|
||||
7,
|
||||
help="删除图库图片需要的管理员等级",
|
||||
default_value=7,
|
||||
type=int,
|
||||
)
|
||||
|
||||
Config.add_plugin_config(
|
||||
"image_management:move_image",
|
||||
"MOVE_IMAGE_LEVEL",
|
||||
7,
|
||||
help="移动图库图片需要的管理员等级",
|
||||
default_value=7,
|
||||
type=int,
|
||||
)
|
||||
|
||||
Config.add_plugin_config(
|
||||
"image_management:upload_image",
|
||||
"UPLOAD_IMAGE_LEVEL",
|
||||
6,
|
||||
help="上传图库图片需要的管理员等级",
|
||||
default_value=6,
|
||||
type=int,
|
||||
)
|
||||
|
||||
Config.add_plugin_config(
|
||||
"image_management",
|
||||
"SHOW_ID",
|
||||
True,
|
||||
help="是否消息显示图片下标id",
|
||||
default_value=True,
|
||||
type=bool,
|
||||
)
|
||||
|
||||
Config.set_name("image_management", "图库操作")
|
||||
|
||||
|
||||
(IMAGE_PATH / "image_management").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
nonebot.load_plugins(str(Path(__file__).parent.resolve()))
|
||||
@@ -1,14 +0,0 @@
|
||||
from strenum import StrEnum
|
||||
|
||||
|
||||
class ImageHandleType(StrEnum):
|
||||
"""
|
||||
图片处理类型
|
||||
"""
|
||||
|
||||
UPLOAD = "UPLOAD"
|
||||
"""上传"""
|
||||
DELETE = "DELETE"
|
||||
"""删除"""
|
||||
MOVE = "MOVE"
|
||||
"""移动"""
|
||||
@@ -1,196 +0,0 @@
|
||||
import os
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
import aiofiles
|
||||
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.utils import cn2py
|
||||
|
||||
from .image_management_log import ImageHandleType, ImageManagementLog
|
||||
|
||||
BASE_PATH = IMAGE_PATH / "image_management"
|
||||
|
||||
|
||||
class ImageManagementManage:
|
||||
|
||||
@classmethod
|
||||
async def random_image(cls, name: str, file_id: int | None = None) -> Path | None:
|
||||
"""随机图片
|
||||
|
||||
参数:
|
||||
name: 图库名称
|
||||
file_id: 图片id.
|
||||
|
||||
返回:
|
||||
Path | None: 图片路径
|
||||
"""
|
||||
path = BASE_PATH / name
|
||||
file_name = f"{file_id}.jpg"
|
||||
if file_id is None:
|
||||
if file_list := os.listdir(path):
|
||||
file_name = random.choice(file_list)
|
||||
_file = path / file_name
|
||||
if not _file.exists():
|
||||
return None
|
||||
return _file
|
||||
|
||||
@classmethod
|
||||
async def upload_image(
|
||||
cls,
|
||||
image_data: bytes | str,
|
||||
name: str,
|
||||
user_id: str,
|
||||
platform: str | None = None,
|
||||
) -> str | None:
|
||||
"""上传图片
|
||||
|
||||
参数:
|
||||
image_data: 图片bytes
|
||||
name: 图库名称
|
||||
user_id: 用户id
|
||||
platform: 所属平台
|
||||
|
||||
返回:
|
||||
str | None: 文件名称
|
||||
"""
|
||||
path = BASE_PATH / cn2py(name)
|
||||
path.mkdir(exist_ok=True, parents=True)
|
||||
_file_name = 0
|
||||
if file_list := os.listdir(path):
|
||||
file_list.sort()
|
||||
_file_name = int(file_list[-1].split(".")[0]) + 1
|
||||
_file_path = path / f"{_file_name}.jpg"
|
||||
try:
|
||||
await ImageManagementLog.create(
|
||||
user_id=user_id,
|
||||
path=_file_path,
|
||||
handle_type=ImageHandleType.UPLOAD,
|
||||
platform=platform,
|
||||
)
|
||||
if isinstance(image_data, str):
|
||||
await AsyncHttpx.download_file(image_data, _file_path)
|
||||
else:
|
||||
async with aiofiles.open(_file_path, "wb") as f:
|
||||
await f.write(image_data)
|
||||
logger.info(
|
||||
f"上传图片至 {name}, 路径: {_file_path}",
|
||||
"上传图片",
|
||||
session=user_id,
|
||||
)
|
||||
return f"{_file_name}.jpg"
|
||||
except Exception as e:
|
||||
logger.error("上传图片错误", "上传图片", e=e)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def delete_image(
|
||||
cls, name: str, file_id: int, user_id: str, platform: str | None = None
|
||||
) -> bool:
|
||||
"""删除图片
|
||||
|
||||
参数:
|
||||
name: 图库名称
|
||||
file_id: 图片id
|
||||
user_id: 用户id
|
||||
platform: 所属平台.
|
||||
|
||||
返回:
|
||||
bool: 是否删除成功
|
||||
"""
|
||||
path = BASE_PATH / cn2py(name)
|
||||
if not path.exists():
|
||||
return False
|
||||
_file_path = path / f"{file_id}.jpg"
|
||||
if not _file_path.exists():
|
||||
return False
|
||||
try:
|
||||
await ImageManagementLog.create(
|
||||
user_id=user_id,
|
||||
path=_file_path,
|
||||
handle_type=ImageHandleType.DELETE,
|
||||
platform=platform,
|
||||
)
|
||||
_file_path.unlink()
|
||||
logger.info(
|
||||
f"图库: {name}, 删除图片路径: {_file_path}", "删除图片", session=user_id
|
||||
)
|
||||
if file_list := os.listdir(path):
|
||||
file_list.sort()
|
||||
_file_name = file_list[-1].split(".")[0]
|
||||
_move_file = path / f"{_file_name}.jpg"
|
||||
_move_file.rename(_file_path)
|
||||
logger.info(
|
||||
f"图库: {name}, 移动图片名称: {_file_name}.jpg -> {file_id}.jpg",
|
||||
"删除图片",
|
||||
session=user_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("删除图片错误", "删除图片", e=e)
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def move_image(
|
||||
cls,
|
||||
a_name: str,
|
||||
b_name: str,
|
||||
file_id: int,
|
||||
user_id: str,
|
||||
platform: str | None = None,
|
||||
) -> str | None:
|
||||
"""移动图片
|
||||
|
||||
参数:
|
||||
a_name: 源图库
|
||||
b_name: 模板图库
|
||||
file_id: 图片id
|
||||
user_id: 用户id
|
||||
platform: 所属平台.
|
||||
|
||||
返回:
|
||||
bool: 是否移动成功
|
||||
"""
|
||||
source_path = BASE_PATH / cn2py(a_name)
|
||||
if not source_path.exists():
|
||||
return None
|
||||
destination_path = BASE_PATH / cn2py(b_name)
|
||||
destination_path.mkdir(exist_ok=True, parents=True)
|
||||
source_file = source_path / f"{file_id}.jpg"
|
||||
if not source_file.exists():
|
||||
return None
|
||||
_destination_name = 0
|
||||
if file_list := os.listdir(destination_path):
|
||||
file_list.sort()
|
||||
_destination_name = int(file_list[-1].split(".")[0]) + 1
|
||||
destination_file = destination_path / f"{_destination_name}.jpg"
|
||||
try:
|
||||
await ImageManagementLog.create(
|
||||
user_id=user_id,
|
||||
path=source_file,
|
||||
move=destination_file,
|
||||
handle_type=ImageHandleType.MOVE,
|
||||
platform=platform,
|
||||
)
|
||||
source_file.rename(destination_file)
|
||||
logger.info(
|
||||
f"图库: {a_name} -> {b_name}, 移动图片路径: {source_file} -> {destination_file}",
|
||||
"移动图片",
|
||||
session=user_id,
|
||||
)
|
||||
if file_list := os.listdir(source_path):
|
||||
file_list.sort()
|
||||
_file_name = file_list[-1].split(".")[0]
|
||||
_move_file = source_path / f"{_file_name}.jpg"
|
||||
_move_file.rename(source_file)
|
||||
logger.info(
|
||||
f"图库: {a_name}, 移动图片名称: {_file_name}.jpg -> {file_id}.jpg",
|
||||
"移动图片",
|
||||
session=user_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("移动图片错误", "移动图片", e=e)
|
||||
return None
|
||||
return f"{source_file} -> {destination_file}"
|
||||
@@ -1,107 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me
|
||||
from nonebot.typing import T_State
|
||||
from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, UniMessage, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ._data_source import ImageManagementManage
|
||||
|
||||
base_config = Config.get("image_management")
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="删除图片",
|
||||
description="不好看的图片删掉删掉!",
|
||||
usage="""
|
||||
指令:
|
||||
删除图片 [图库] [id]
|
||||
查看图库
|
||||
示例:删除图片 美图 666
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
plugin_type=PluginType.ADMIN,
|
||||
admin_level=base_config.get("DELETE_IMAGE_LEVEL"),
|
||||
).dict(),
|
||||
)
|
||||
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna("删除图片", Args["name?", str]["index?", str]),
|
||||
rule=to_me(),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(
|
||||
name: Match[str],
|
||||
index: Match[str],
|
||||
state: T_State,
|
||||
):
|
||||
image_dir_list = base_config.get("IMAGE_DIR_LIST")
|
||||
if not image_dir_list:
|
||||
await MessageUtils.build_message("未发现任何图库").finish()
|
||||
_text = ""
|
||||
for i, dir in enumerate(image_dir_list):
|
||||
_text += f"{i}. {dir}\n"
|
||||
state["dir_list"] = _text[:-1]
|
||||
if name.available:
|
||||
_matcher.set_path_arg("name", name.result)
|
||||
if index.available:
|
||||
_matcher.set_path_arg("index", index.result)
|
||||
|
||||
|
||||
@_matcher.got_path(
|
||||
"name",
|
||||
prompt=UniMessage.template(
|
||||
"请输入要删除的目标图库(id 或 名称)【发送'取消', '算了'来取消操作】\n{dir_list}"
|
||||
),
|
||||
)
|
||||
async def _(name: str):
|
||||
if name in ["取消", "算了"]:
|
||||
await MessageUtils.build_message("已取消操作...").finish()
|
||||
image_dir_list = base_config.get("IMAGE_DIR_LIST")
|
||||
if name.isdigit():
|
||||
index = int(name)
|
||||
if index <= len(image_dir_list) - 1:
|
||||
name = image_dir_list[index]
|
||||
if name not in image_dir_list:
|
||||
await _matcher.reject_path("name", "此目录不正确,请重新输入目录!")
|
||||
_matcher.set_path_arg("name", name)
|
||||
|
||||
|
||||
@_matcher.got_path("index", "请输入要删除的图片id?【发送'取消', '算了'来取消操作】")
|
||||
async def _(
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
index: str,
|
||||
):
|
||||
if index in ["取消", "算了"]:
|
||||
await MessageUtils.build_message("已取消操作...").finish()
|
||||
if not index.isdigit():
|
||||
await _matcher.reject_path("index", "图片id需要输入数字...")
|
||||
name = _matcher.get_path_arg("name", None)
|
||||
if not name:
|
||||
await MessageUtils.build_message("图库名称为空...").finish()
|
||||
if not session.id1:
|
||||
await MessageUtils.build_message("用户id为空...").finish()
|
||||
if file_name := await ImageManagementManage.delete_image(
|
||||
name, int(index), session.id1, session.platform
|
||||
):
|
||||
logger.info(
|
||||
f"删除图片成功 图库: {name} --- 名称: {file_name}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
await MessageUtils.build_message(
|
||||
f"删除图片成功!\n图库: {name}\n名称: {index}.jpg"
|
||||
).finish()
|
||||
await MessageUtils.build_message("图片删除失败...").finish()
|
||||
@@ -1,27 +0,0 @@
|
||||
from tortoise import fields
|
||||
|
||||
from zhenxun.services.db_context import Model
|
||||
|
||||
from ._config import ImageHandleType
|
||||
|
||||
|
||||
class ImageManagementLog(Model):
|
||||
|
||||
id = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
"""自增id"""
|
||||
user_id = fields.CharField(255, description="用户id")
|
||||
"""用户id"""
|
||||
path = fields.TextField(description="图片路径")
|
||||
"""图片路径"""
|
||||
move = fields.TextField(null=True, description="移动路径")
|
||||
"""移动路径"""
|
||||
handle_type = fields.CharEnumField(ImageHandleType, description="操作类型")
|
||||
"""操作类型"""
|
||||
create_time = fields.DatetimeField(auto_now_add=True, description="创建时间")
|
||||
"""创建时间"""
|
||||
platform = fields.CharField(255, null=True, description="平台")
|
||||
"""平台"""
|
||||
|
||||
class Meta:
|
||||
table = "image_management_log"
|
||||
table_description = "画廊操作记录"
|
||||
@@ -1,137 +0,0 @@
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me
|
||||
from nonebot.typing import T_State
|
||||
from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, UniMessage, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ._data_source import ImageManagementManage
|
||||
|
||||
base_config = Config.get("image_management")
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="移动图片",
|
||||
description="图库间的图片移动操作",
|
||||
usage="""
|
||||
指令:
|
||||
移动图片 [源图库] [目标图库] [id]
|
||||
查看图库
|
||||
示例:移动图片 萝莉 美图 234
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
plugin_type=PluginType.ADMIN,
|
||||
admin_level=base_config.get("MOVE_IMAGE_LEVEL"),
|
||||
).dict(),
|
||||
)
|
||||
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna("移动图片", Args["source?", str]["destination?", str]["index?", str]),
|
||||
rule=to_me(),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
source: Match[str],
|
||||
destination: Match[str],
|
||||
index: Match[str],
|
||||
state: T_State,
|
||||
):
|
||||
image_dir_list = base_config.get("IMAGE_DIR_LIST")
|
||||
if not image_dir_list:
|
||||
await MessageUtils.build_message("未发现任何图库").finish()
|
||||
_text = ""
|
||||
for i, dir in enumerate(image_dir_list):
|
||||
_text += f"{i}. {dir}\n"
|
||||
state["dir_list"] = _text[:-1]
|
||||
if source.available:
|
||||
_matcher.set_path_arg("source", source.result)
|
||||
if destination.available:
|
||||
_matcher.set_path_arg("destination", destination.result)
|
||||
if index.available:
|
||||
_matcher.set_path_arg("index", index.result)
|
||||
|
||||
|
||||
@_matcher.got_path(
|
||||
"source",
|
||||
prompt=UniMessage.template(
|
||||
"要从哪个图库移出?【发送'取消', '算了'来取消操作】\n{dir_list}"
|
||||
),
|
||||
)
|
||||
async def _(source: str):
|
||||
if source in ["取消", "算了"]:
|
||||
await MessageUtils.build_message("已取消操作...").finish()
|
||||
image_dir_list = base_config.get("IMAGE_DIR_LIST")
|
||||
if source.isdigit():
|
||||
index = int(source)
|
||||
if index <= len(image_dir_list) - 1:
|
||||
name = image_dir_list[index]
|
||||
if name not in image_dir_list:
|
||||
await _matcher.reject_path("source", "此目录不正确,请重新输入目录!")
|
||||
_matcher.set_path_arg("source", name)
|
||||
|
||||
|
||||
@_matcher.got_path(
|
||||
"destination",
|
||||
prompt=UniMessage.template(
|
||||
"要移动到哪个图库?【发送'取消', '算了'来取消操作】\n{dir_list}"
|
||||
),
|
||||
)
|
||||
async def _(destination: str):
|
||||
if destination in ["取消", "算了"]:
|
||||
await MessageUtils.build_message("已取消操作...").finish()
|
||||
image_dir_list = base_config.get("IMAGE_DIR_LIST")
|
||||
name = None
|
||||
if destination.isdigit():
|
||||
index = int(destination)
|
||||
if index <= len(image_dir_list) - 1:
|
||||
name = image_dir_list[index]
|
||||
if name not in image_dir_list:
|
||||
await _matcher.reject_path("destination", "此目录不正确,请重新输入目录!")
|
||||
_matcher.set_path_arg("destination", name)
|
||||
|
||||
|
||||
@_matcher.got_path("index", "要移动的图片id是?【发送'取消', '算了'来取消操作】")
|
||||
async def _(
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
index: str,
|
||||
):
|
||||
if index in ["取消", "算了"]:
|
||||
await MessageUtils.build_message("已取消操作...").finish()
|
||||
if not index.isdigit():
|
||||
await _matcher.reject_path("index", "图片id需要输入数字...")
|
||||
source = _matcher.get_path_arg("source", None)
|
||||
destination = _matcher.get_path_arg("destination", None)
|
||||
if not source:
|
||||
await MessageUtils.build_message("转出图库名称为空...").finish()
|
||||
if not destination:
|
||||
await MessageUtils.build_message("转入图库名称为空...").finish()
|
||||
if not session.id1:
|
||||
await MessageUtils.build_message("用户id为空...").finish()
|
||||
if file_name := await ImageManagementManage.move_image(
|
||||
source, destination, int(index), session.id1, session.platform
|
||||
):
|
||||
logger.info(
|
||||
f"移动图片成功 图库: {source} -> {destination} --- 名称: {file_name}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
await MessageUtils.build_message(
|
||||
f"移动图片成功!\n图库: {source} -> {destination}"
|
||||
).finish()
|
||||
await MessageUtils.build_message("图片删除失败...").finish()
|
||||
@@ -1,195 +0,0 @@
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me
|
||||
from nonebot.typing import T_State
|
||||
from nonebot_plugin_alconna import Alconna, Args, Arparma
|
||||
from nonebot_plugin_alconna import Image as alcImage
|
||||
from nonebot_plugin_alconna import Match, UniMessage, UniMsg, image_fetch, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ._data_source import ImageManagementManage
|
||||
|
||||
base_config = Config.get("image_management")
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="上传图片",
|
||||
description="上传图片至指定图库",
|
||||
usage="""
|
||||
指令:
|
||||
查看图库
|
||||
上传图片 [图库] [图片]
|
||||
示例:上传图片 美图 [图片]
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
plugin_type=PluginType.ADMIN,
|
||||
admin_level=base_config.get("UPLOAD_IMAGE_LEVEL"),
|
||||
).dict(),
|
||||
)
|
||||
|
||||
|
||||
_upload_matcher = on_alconna(
|
||||
Alconna("上传图片", Args["name?", str]["img?", alcImage]),
|
||||
rule=to_me(),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
_continuous_upload_matcher = on_alconna(
|
||||
Alconna("连续上传图片", Args["name?", str]),
|
||||
rule=to_me(),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
_show_matcher = on_alconna(Alconna("查看公开图库"), priority=1, block=True)
|
||||
|
||||
|
||||
@_show_matcher.handle()
|
||||
async def _():
|
||||
image_dir_list = base_config.get("IMAGE_DIR_LIST")
|
||||
if not image_dir_list:
|
||||
await MessageUtils.build_message("未发现任何图库").finish()
|
||||
text = "公开图库列表:\n"
|
||||
for i, e in enumerate(image_dir_list):
|
||||
text += f"\t{i+1}.{e}\n"
|
||||
await MessageUtils.build_message(text[:-1]).send()
|
||||
|
||||
|
||||
@_upload_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
name: Match[str],
|
||||
img: Match[bytes],
|
||||
state: T_State,
|
||||
):
|
||||
image_dir_list = base_config.get("IMAGE_DIR_LIST")
|
||||
if not image_dir_list:
|
||||
await MessageUtils.build_message("未发现任何图库").finish()
|
||||
_text = ""
|
||||
for i, dir in enumerate(image_dir_list):
|
||||
_text += f"{i}. {dir}\n"
|
||||
state["dir_list"] = _text[:-1]
|
||||
if name.available:
|
||||
_upload_matcher.set_path_arg("name", name.result)
|
||||
if img.available:
|
||||
result = await AsyncHttpx.get(img.result.url) # type: ignore
|
||||
_upload_matcher.set_path_arg("img", result.content)
|
||||
|
||||
|
||||
@_continuous_upload_matcher.handle()
|
||||
async def _(bot: Bot, state: T_State, name: Match[str]):
|
||||
image_dir_list = base_config.get("IMAGE_DIR_LIST")
|
||||
if not image_dir_list:
|
||||
await MessageUtils.build_message("未发现任何图库").finish()
|
||||
_text = ""
|
||||
for i, dir in enumerate(image_dir_list):
|
||||
_text += f"{i}. {dir}\n"
|
||||
state["dir_list"] = _text[:-1]
|
||||
if name.available:
|
||||
_upload_matcher.set_path_arg("name", name.result)
|
||||
|
||||
|
||||
@_continuous_upload_matcher.got_path(
|
||||
"name",
|
||||
prompt=UniMessage.template(
|
||||
"请选择要上传的图库(id 或 名称)【发送'取消', '算了'来取消操作】\n{dir_list}"
|
||||
),
|
||||
)
|
||||
@_upload_matcher.got_path(
|
||||
"name",
|
||||
prompt=UniMessage.template(
|
||||
"请选择要上传的图库(id 或 名称)【发送'取消', '算了'来取消操作】\n{dir_list}"
|
||||
),
|
||||
)
|
||||
async def _(name: str, state: T_State):
|
||||
if name in ["取消", "算了"]:
|
||||
await MessageUtils.build_message("已取消操作...").finish()
|
||||
image_dir_list = base_config.get("IMAGE_DIR_LIST")
|
||||
if name.isdigit():
|
||||
index = int(name)
|
||||
if index <= len(image_dir_list) - 1:
|
||||
name = image_dir_list[index]
|
||||
if name not in image_dir_list:
|
||||
await _upload_matcher.reject_path("name", "此目录不正确,请重新输入目录!")
|
||||
_upload_matcher.set_path_arg("name", name)
|
||||
|
||||
|
||||
@_upload_matcher.got_path("img", "图呢图呢图呢图呢!GKD!", image_fetch)
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
img: bytes,
|
||||
):
|
||||
name = _upload_matcher.get_path_arg("name", None)
|
||||
if not name:
|
||||
await MessageUtils.build_message("图库名称为空...").finish()
|
||||
if not session.id1:
|
||||
await MessageUtils.build_message("用户id为空...").finish()
|
||||
if file_name := await ImageManagementManage.upload_image(
|
||||
img, name, session.id1, session.platform
|
||||
):
|
||||
logger.info(
|
||||
f"图库: {name} --- 名称: {file_name}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
await MessageUtils.build_message(
|
||||
f"上传图片成功!\n图库: {name}\n名称: {file_name}"
|
||||
).finish()
|
||||
await MessageUtils.build_message("图片上传失败...").finish()
|
||||
|
||||
|
||||
@_continuous_upload_matcher.got(
|
||||
"img", "图呢图呢图呢图呢!GKD!【在最后一张图片中+‘stop’为停止】"
|
||||
)
|
||||
async def _(
|
||||
bot: Bot,
|
||||
arparma: Arparma,
|
||||
session: EventSession,
|
||||
state: T_State,
|
||||
message: UniMsg,
|
||||
):
|
||||
name = _continuous_upload_matcher.get_path_arg("name", None)
|
||||
if not name:
|
||||
await MessageUtils.build_message("图库名称为空...").finish()
|
||||
if not session.id1:
|
||||
await MessageUtils.build_message("用户id为空...").finish()
|
||||
if not state.get("img_list"):
|
||||
state["img_list"] = []
|
||||
msg = message.extract_plain_text().strip().replace(arparma.header_result, "", 1)
|
||||
if msg in ["取消", "算了"]:
|
||||
await MessageUtils.build_message("已取消操作...").finish()
|
||||
if msg != "stop":
|
||||
for msg in message:
|
||||
if isinstance(msg, alcImage):
|
||||
state["img_list"].append(msg.url)
|
||||
await _continuous_upload_matcher.reject("图再来!!【发送‘stop’为停止】")
|
||||
if state["img_list"]:
|
||||
await MessageUtils.build_message("正在下载, 请稍后...").send()
|
||||
file_list = []
|
||||
for img in state["img_list"]:
|
||||
if file_name := await ImageManagementManage.upload_image(
|
||||
img, name, session.id1, session.platform
|
||||
):
|
||||
file_list.append(img)
|
||||
logger.info(
|
||||
f"图库: {name} --- 名称: {file_name}",
|
||||
"上传图片",
|
||||
session=session,
|
||||
)
|
||||
await MessageUtils.build_message(
|
||||
f"上传图片成功!共上传了{len(file_list)}张图片\n图库: {name}\n名称: {', '.join(file_list)}"
|
||||
).finish()
|
||||
await MessageUtils.build_message("图片上传失败...").finish()
|
||||
@@ -1,74 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
from zhenxun.configs.utils import BaseBlock, PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="鲁迅说",
|
||||
description="鲁迅说了啥?",
|
||||
usage="""
|
||||
鲁迅说 [文本]
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
limits=[BaseBlock(result="你的鲁迅正在说,等会")],
|
||||
).dict(),
|
||||
)
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna("luxun", Args["content", str]),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
_matcher.shortcut(
|
||||
"鲁迅说",
|
||||
command="luxun",
|
||||
arguments=["{%0}"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
|
||||
_sign = None
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(content: Match[str]):
|
||||
if content.available:
|
||||
_matcher.set_path_arg("content", content.result)
|
||||
|
||||
|
||||
@_matcher.got_path("content", prompt="你让鲁迅说点啥?")
|
||||
async def _(content: str, session: EventSession, arparma: Arparma):
|
||||
global _sign
|
||||
if content.startswith(",") or content.startswith(","):
|
||||
content = content[1:]
|
||||
A = BuildImage(
|
||||
font_size=37, background=f"{IMAGE_PATH}/other/luxun.jpg", font="msyh.ttf"
|
||||
)
|
||||
text = ""
|
||||
if len(content) > 40:
|
||||
await MessageUtils.build_message("太长了,鲁迅说不完...").finish()
|
||||
while A.getsize(content)[0] > A.width - 50:
|
||||
n = int(len(content) / 2)
|
||||
text += content[:n] + "\n"
|
||||
content = content[n:]
|
||||
text += content
|
||||
if len(text.split("\n")) > 2:
|
||||
await MessageUtils.build_message("太长了,鲁迅说不完...").finish()
|
||||
await A.text(
|
||||
(int((480 - A.getsize(text.split("\n")[0])[0]) / 2), 300), text, (255, 255, 255)
|
||||
)
|
||||
if not _sign:
|
||||
_sign = await BuildImage.build_text_image(
|
||||
"--鲁迅", "msyh.ttf", 30, (255, 255, 255)
|
||||
)
|
||||
await A.paste(_sign, (320, 400))
|
||||
await MessageUtils.build_message(A).send()
|
||||
logger.info(f"鲁迅说: {content}", arparma.header_result, session=session)
|
||||
@@ -1,5 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
import nonebot
|
||||
|
||||
nonebot.load_plugins(str(Path(__file__).parent.resolve()))
|
||||
@@ -1,124 +0,0 @@
|
||||
import time
|
||||
|
||||
import ujson as json
|
||||
from pydantic import BaseModel
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.path_config import DATA_PATH
|
||||
|
||||
base_config = Config.get("mute_setting")
|
||||
|
||||
|
||||
class GroupData(BaseModel):
|
||||
|
||||
count: int
|
||||
"""次数"""
|
||||
time: int
|
||||
"""检测时长"""
|
||||
duration: int
|
||||
"""禁言时长"""
|
||||
message_data: dict = {}
|
||||
"""消息存储"""
|
||||
|
||||
|
||||
class MuteManage:
|
||||
|
||||
file = DATA_PATH / "group_mute_data.json"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._group_data: dict[str, GroupData] = {}
|
||||
if self.file.exists():
|
||||
_data = json.load(open(self.file))
|
||||
for gid in _data:
|
||||
self._group_data[gid] = GroupData(
|
||||
count=_data[gid]["count"],
|
||||
time=_data[gid]["time"],
|
||||
duration=_data[gid]["duration"],
|
||||
)
|
||||
|
||||
def get_group_data(self, group_id: str) -> GroupData:
|
||||
"""获取群组数据
|
||||
|
||||
参数:
|
||||
group_id: 群组id
|
||||
|
||||
返回:
|
||||
GroupData: GroupData
|
||||
"""
|
||||
if group_id not in self._group_data:
|
||||
self._group_data[group_id] = GroupData(
|
||||
count=base_config.get("MUTE_DEFAULT_COUNT", 10) or 10,
|
||||
time=base_config.get("MUTE_DEFAULT_TIME", 7) or 7,
|
||||
duration=base_config.get("MUTE_DEFAULT_DURATION", 10) or 10,
|
||||
)
|
||||
return self._group_data[group_id]
|
||||
|
||||
def reset(self, user_id: str, group_id: str):
|
||||
"""重置用户检查次数
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
"""
|
||||
if group_data := self._group_data.get(group_id):
|
||||
if user_id in group_data.message_data:
|
||||
group_data.message_data[user_id]["count"] = 0
|
||||
|
||||
def save_data(self):
|
||||
"""保存数据"""
|
||||
data = {}
|
||||
for gid in self._group_data:
|
||||
data[gid] = {
|
||||
"count": self._group_data[gid].count,
|
||||
"time": self._group_data[gid].time,
|
||||
"duration": self._group_data[gid].duration,
|
||||
}
|
||||
with open(self.file, "w") as f:
|
||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||
|
||||
def add_message(self, user_id: str, group_id: str, message: str) -> int:
|
||||
"""添加消息
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
message: 消息内容
|
||||
|
||||
返回:
|
||||
int: 禁言时长
|
||||
"""
|
||||
if group_id not in self._group_data:
|
||||
self._group_data[group_id] = GroupData(
|
||||
count=base_config.get("MUTE_DEFAULT_COUNT"),
|
||||
time=base_config.get("MUTE_DEFAULT_TIME"),
|
||||
duration=base_config.get("MUTE_DEFAULT_DURATION"),
|
||||
)
|
||||
group_data = self._group_data[group_id]
|
||||
if group_data.duration == 0:
|
||||
return 0
|
||||
message_data = group_data.message_data
|
||||
if not message_data.get(user_id):
|
||||
message_data[user_id] = {
|
||||
"time": time.time(),
|
||||
"count": 1,
|
||||
"message": message,
|
||||
}
|
||||
else:
|
||||
if message.find(message_data[user_id]["message"]) != -1:
|
||||
message_data[user_id]["count"] += 1
|
||||
else:
|
||||
message_data[user_id]["time"] = time.time()
|
||||
message_data[user_id]["count"] = 1
|
||||
message_data[user_id]["message"] = message
|
||||
if time.time() - message_data[user_id]["time"] > group_data.time:
|
||||
message_data[user_id]["time"] = time.time()
|
||||
message_data[user_id]["count"] = 1
|
||||
if (
|
||||
message_data[user_id]["count"] > group_data.count
|
||||
and time.time() - message_data[user_id]["time"] < group_data.time
|
||||
):
|
||||
return group_data.duration
|
||||
return 0
|
||||
|
||||
|
||||
mute_manage = MuteManage()
|
||||
@@ -1,56 +0,0 @@
|
||||
from nonebot import on_message
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Image as alcImage
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.models.ban_console import BanConsole
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.image_utils import get_download_image_hash
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from ._data_source import mute_manage
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="刷屏监听",
|
||||
description="",
|
||||
usage="",
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
menu_type="其他",
|
||||
plugin_type=PluginType.DEPENDANT,
|
||||
).dict(),
|
||||
)
|
||||
|
||||
_matcher = on_message(priority=1, block=False)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(bot: Bot, session: EventSession, message: UniMsg):
|
||||
group_id = session.id2
|
||||
if not session.id1 or not group_id:
|
||||
return
|
||||
if await BanConsole.is_ban(session.id1, group_id):
|
||||
return
|
||||
plain_text = message.extract_plain_text()
|
||||
image_list = [m.url for m in message if isinstance(m, alcImage) and m.url]
|
||||
img_hash = ""
|
||||
for url in image_list:
|
||||
img_hash += await get_download_image_hash(url, "_mute_")
|
||||
_message = plain_text + img_hash
|
||||
if duration := mute_manage.add_message(session.id1, group_id, _message):
|
||||
try:
|
||||
await PlatformUtils.ban_user(bot, session.id1, group_id, duration)
|
||||
await MessageUtils.build_message(
|
||||
f"检测到恶意刷屏,{BotConfig.self_nickname}要把你关进小黑屋!"
|
||||
).send(at_sender=True)
|
||||
mute_manage.reset(session.id1, group_id)
|
||||
logger.info(f"检测刷屏 被禁言 {duration} 分钟", "禁言检查", session=session)
|
||||
except Exception as e:
|
||||
logger.error("禁言发送错误", "禁言检测", session=session, e=e)
|
||||
@@ -1,117 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, Option, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.rules import ensure_group
|
||||
|
||||
from ._data_source import base_config, mute_manage
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="刷屏禁言",
|
||||
description="刷屏禁言相关操作",
|
||||
usage="""
|
||||
刷屏禁言相关操作,需要 {BotConfig.self_nickname} 有群管理员权限
|
||||
指令:
|
||||
设置刷屏: 查看当前设置
|
||||
-c [count]: 检测最大次数
|
||||
-t [time]: 规定时间内
|
||||
-d [duration]: 禁言时长
|
||||
示例:
|
||||
设置刷屏 -c 10: 设置最大次数为10
|
||||
设置刷屏 -t 100 -d 20: 设置规定时间和禁言时长
|
||||
设置刷屏 -d 10: 设置禁言时长为10
|
||||
* 即 X 秒内发送同样消息 N 次,禁言 M 分钟 *
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
menu_type="其他",
|
||||
plugin_type=PluginType.ADMIN,
|
||||
admin_level=base_config.get("MUTE_LEVEL", 5),
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
key="MUTE_LEVEL",
|
||||
value=5,
|
||||
help="更改禁言设置的管理权限",
|
||||
default_value=5,
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="MUTE_DEFAULT_COUNT",
|
||||
value=10,
|
||||
help="刷屏禁言默认检测次数",
|
||||
default_value=10,
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="MUTE_DEFAULT_TIME",
|
||||
value=7,
|
||||
help="刷屏检测默认规定时间",
|
||||
default_value=7,
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="MUTE_DEFAULT_DURATION",
|
||||
value=10,
|
||||
help="刷屏检测默禁言时长(分钟)",
|
||||
default_value=10,
|
||||
type=int,
|
||||
),
|
||||
],
|
||||
).dict(),
|
||||
)
|
||||
|
||||
|
||||
_setting_matcher = on_alconna(
|
||||
Alconna(
|
||||
"刷屏设置",
|
||||
Option("-t|--time", Args["time", int], help_text="检测时长"),
|
||||
Option("-c|--count", Args["count", int], help_text="检测次数"),
|
||||
Option("-d|--duration", Args["duration", int], help_text="禁言时长"),
|
||||
),
|
||||
rule=ensure_group,
|
||||
block=True,
|
||||
priority=5,
|
||||
)
|
||||
|
||||
|
||||
@_setting_matcher.handle()
|
||||
async def _(
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
time: Match[int],
|
||||
count: Match[int],
|
||||
duration: Match[int],
|
||||
):
|
||||
group_id = session.id2
|
||||
if not session.id1 or not group_id:
|
||||
return
|
||||
_time = time.result if time.available else None
|
||||
_count = count.result if count.available else None
|
||||
_duration = duration.result if duration.available else None
|
||||
group_data = mute_manage.get_group_data(group_id)
|
||||
if _time is None and _count is None and _duration is None:
|
||||
await MessageUtils.build_message(
|
||||
f"最大次数:{group_data.count} 次\n"
|
||||
f"规定时间:{group_data.time} 秒\n"
|
||||
f"禁言时长:{group_data.duration:.2f} 分钟\n"
|
||||
f"【在规定时间内发送相同消息超过最大次数则禁言\n当禁言时长为0时关闭此功能】"
|
||||
).finish(reply_to=True)
|
||||
if _time is not None:
|
||||
group_data.time = _time
|
||||
if _count is not None:
|
||||
group_data.count = _count
|
||||
if _duration is not None:
|
||||
group_data.duration = _duration
|
||||
await MessageUtils.build_message("设置成功!").send(reply_to=True)
|
||||
logger.info(
|
||||
f"设置禁言配置 time: {_time}, count: {_count}, duration: {_duration}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
mute_manage.save_data()
|
||||
@@ -1,62 +0,0 @@
|
||||
import ujson as json
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Args, Arparma, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="能不能好好说话",
|
||||
description="能不能好好说话,说人话",
|
||||
usage="""
|
||||
说人话
|
||||
指令:
|
||||
nbnhhsh [文本]
|
||||
能不能好好说话 [文本]
|
||||
示例:
|
||||
nbnhhsh xsx
|
||||
""".strip(),
|
||||
extra=PluginExtraData(author="HibiKier", version="0.1", aliases={"nbnhhsh"}).dict(),
|
||||
)
|
||||
|
||||
URL = "https://lab.magiconch.com/api/nbnhhsh/guess"
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna("nbnhhsh", Args["text", str]),
|
||||
aliases={"能不能好好说话"},
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(session: EventSession, arparma: Arparma, text: str):
|
||||
response = await AsyncHttpx.post(
|
||||
URL,
|
||||
data=json.dumps({"text": text}), # type: ignore
|
||||
timeout=5,
|
||||
headers={"content-type": "application/json"},
|
||||
)
|
||||
try:
|
||||
data = response.json()
|
||||
tmp = ""
|
||||
result = ""
|
||||
for x in data:
|
||||
trans = ""
|
||||
if x.get("trans"):
|
||||
trans = x["trans"][0]
|
||||
elif x.get("inputting"):
|
||||
trans = ",".join(x["inputting"])
|
||||
tmp += f'{x["name"]} -> {trans}\n'
|
||||
result += trans
|
||||
logger.info(
|
||||
f" 发送能不能好好说话: {text} -> {result}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
await MessageUtils.build_message(f"{tmp}={result}").send(reply_to=True)
|
||||
except (IndexError, KeyError):
|
||||
await MessageUtils.build_message("没有找到对应的翻译....").send()
|
||||
@@ -1,77 +0,0 @@
|
||||
import random
|
||||
from io import BytesIO
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Args, At, Match, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="我有一个朋友",
|
||||
description="我有一个朋友想问问...",
|
||||
usage="""
|
||||
指令:
|
||||
我有一个朋友想问问 [文本] ?[at]: 当at时你的朋友就是艾特对象
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
).dict(),
|
||||
)
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna("one-friend", Args["text", str]["at?", At]), priority=5, block=True
|
||||
)
|
||||
|
||||
_matcher.shortcut(
|
||||
"^我.{0,4}朋友.{0,2}(?:想问问|说|让我问问|想问|让我问|想知道|让我帮他问问|让我帮他问|让我帮忙问|让我帮忙问问|问)(?P<content>.{0,30})$",
|
||||
command="one-friend",
|
||||
arguments=["{content}"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(bot: Bot, text: str, at: Match[At], session: EventSession):
|
||||
gid = session.id3 or session.id2
|
||||
if not gid:
|
||||
await MessageUtils.build_message("群组id为空...").finish()
|
||||
if not session.id1:
|
||||
await MessageUtils.build_message("用户id为空...").finish()
|
||||
at_user = None
|
||||
if at.available:
|
||||
at_user = at.result.target
|
||||
user = None
|
||||
if at_user:
|
||||
user = await PlatformUtils.get_user(bot, at_user, gid)
|
||||
else:
|
||||
if member_list := await PlatformUtils.get_group_member_list(bot, gid):
|
||||
text = text.replace("他", "我").replace("她", "我").replace("它", "我")
|
||||
user = random.choice(member_list)
|
||||
if user:
|
||||
ava_data = None
|
||||
if PlatformUtils.get_platform(bot) == "qq":
|
||||
ava_data = await PlatformUtils.get_user_avatar(user.user_id, "qq")
|
||||
elif user.avatar_url:
|
||||
ava_data = (await AsyncHttpx.get(user.avatar_url)).content
|
||||
ava_img = BuildImage(200, 100, color=(0, 0, 0, 0))
|
||||
if ava_data:
|
||||
ava_img = BuildImage(200, 100, background=BytesIO(ava_data))
|
||||
await ava_img.circle()
|
||||
user_name = "朋友"
|
||||
content = BuildImage(400, 30, font_size=30)
|
||||
await content.text((0, 0), user_name)
|
||||
A = BuildImage(700, 150, font_size=25, color="white")
|
||||
await A.paste(ava_img, (30, 25))
|
||||
await A.paste(content, (150, 38))
|
||||
await A.text((150, 85), text, (125, 125, 125))
|
||||
logger.info(f"发送有一个朋友: {text}", "我有一个朋友", session=session)
|
||||
await MessageUtils.build_message(A).finish()
|
||||
await MessageUtils.build_message("获取用户信息失败...").send()
|
||||
@@ -1,329 +0,0 @@
|
||||
import asyncio
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List
|
||||
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Arparma, Match
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig, Task
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import text2image
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from .command import (
|
||||
_group_open_matcher,
|
||||
_knifes_matcher,
|
||||
_multiple_matcher,
|
||||
_my_open_matcher,
|
||||
_open_matcher,
|
||||
_price_matcher,
|
||||
_reload_matcher,
|
||||
_show_case_matcher,
|
||||
_update_image_matcher,
|
||||
_update_matcher,
|
||||
)
|
||||
from .open_cases_c import (
|
||||
auto_update,
|
||||
get_my_knifes,
|
||||
group_statistics,
|
||||
open_case,
|
||||
open_multiple_case,
|
||||
total_open_statistics,
|
||||
)
|
||||
from .utils import (
|
||||
CASE2ID,
|
||||
KNIFE2ID,
|
||||
CaseManager,
|
||||
build_case_image,
|
||||
download_image,
|
||||
get_skin_case,
|
||||
init_skin_trends,
|
||||
reset_count_daily,
|
||||
update_skin_data,
|
||||
)
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="CSGO开箱",
|
||||
description="csgo模拟开箱[戒赌]",
|
||||
usage="""
|
||||
指令:
|
||||
开箱 ?[武器箱]
|
||||
[1-30]连开箱 ?[武器箱]
|
||||
我的开箱
|
||||
我的金色
|
||||
群开箱统计
|
||||
查看武器箱?[武器箱]
|
||||
* 不包含[武器箱]时随机开箱 *
|
||||
示例: 查看武器箱
|
||||
示例: 查看武器箱英勇
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
superuser_help="""
|
||||
更新皮肤指令
|
||||
重置开箱: 重置今日开箱所有次数
|
||||
指令:
|
||||
更新武器箱 ?[武器箱/ALL]
|
||||
更新皮肤 ?[名称/ALL1]
|
||||
更新皮肤 ?[名称/ALL1] -S: (必定更新罕见皮肤所属箱子)
|
||||
更新武器箱图片
|
||||
* 不指定武器箱时则全部更新 *
|
||||
* 过多的爬取会导致账号API被封 *
|
||||
""".strip(),
|
||||
menu_type="抽卡相关",
|
||||
tasks=[Task(module="open_case_reset_remind", name="每日开箱重置提醒")],
|
||||
limits=[PluginCdBlock(result="着什么急啊,慢慢来!")],
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
key="INITIAL_OPEN_CASE_COUNT",
|
||||
value=20,
|
||||
help="初始每日开箱次数",
|
||||
default_value=20,
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="EACH_IMPRESSION_ADD_COUNT",
|
||||
value=3,
|
||||
help="每 * 点好感度额外增加开箱次数",
|
||||
default_value=3,
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(key="COOKIE", value=None, help="BUFF的cookie"),
|
||||
RegisterConfig(
|
||||
key="DAILY_UPDATE",
|
||||
value=None,
|
||||
help="每日自动更新的武器箱,存在'ALL'时则更新全部武器箱",
|
||||
type=List[str],
|
||||
),
|
||||
RegisterConfig(
|
||||
key="DEFAULT_OPEN_CASE_RESET_REMIND",
|
||||
module="_task",
|
||||
value=True,
|
||||
help="被动 每日开箱重置提醒 进群默认开关状态",
|
||||
default_value=True,
|
||||
type=bool,
|
||||
),
|
||||
],
|
||||
).dict(),
|
||||
)
|
||||
|
||||
|
||||
@_price_matcher.handle()
|
||||
async def _(
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
name: str,
|
||||
skin: str,
|
||||
abrasion: str,
|
||||
day: Match[int],
|
||||
):
|
||||
name = name.replace("武器箱", "").strip()
|
||||
_day = 7
|
||||
if day.available:
|
||||
_day = day.result
|
||||
if _day > 180:
|
||||
await MessageUtils.build_message("天数必须大于0且小于180").finish()
|
||||
result = await init_skin_trends(name, skin, abrasion, _day)
|
||||
if not result:
|
||||
await MessageUtils.build_message("未查询到数据...").finish(reply_to=True)
|
||||
await MessageUtils.build_message(result).send()
|
||||
logger.info(
|
||||
f"查看 [{name}:{skin}({abrasion})] 价格趋势",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
|
||||
|
||||
@_reload_matcher.handle()
|
||||
async def _(session: EventSession, arparma: Arparma):
|
||||
await reset_count_daily()
|
||||
logger.info("重置开箱次数", arparma.header_result, session=session)
|
||||
|
||||
|
||||
@_open_matcher.handle()
|
||||
async def _(session: EventSession, arparma: Arparma, name: Match[str]):
|
||||
gid = session.id3 or session.id2
|
||||
if not session.id1:
|
||||
await MessageUtils.build_message("用户id为空...").finish()
|
||||
if not gid:
|
||||
await MessageUtils.build_message("群组id为空...").finish()
|
||||
case_name = None
|
||||
if name.available:
|
||||
case_name = name.result.replace("武器箱", "").strip()
|
||||
result = await open_case(session.id1, gid, case_name, session)
|
||||
await result.finish(reply_to=True)
|
||||
|
||||
|
||||
@_my_open_matcher.handle()
|
||||
async def _(session: EventSession, arparma: Arparma):
|
||||
gid = session.id3 or session.id2
|
||||
if not session.id1:
|
||||
await MessageUtils.build_message("用户id为空...").finish()
|
||||
if not gid:
|
||||
await MessageUtils.build_message("群组id为空...").finish()
|
||||
await MessageUtils.build_message(
|
||||
await total_open_statistics(session.id1, gid),
|
||||
).send(reply_to=True)
|
||||
logger.info("查询我的开箱", arparma.header_result, session=session)
|
||||
|
||||
|
||||
@_group_open_matcher.handle()
|
||||
async def _(session: EventSession, arparma: Arparma):
|
||||
gid = session.id3 or session.id2
|
||||
if not gid:
|
||||
await MessageUtils.build_message("群组id为空...").finish()
|
||||
result = await group_statistics(gid)
|
||||
await MessageUtils.build_message(result).send(reply_to=True)
|
||||
logger.info("查询群开箱统计", arparma.header_result, session=session)
|
||||
|
||||
|
||||
@_knifes_matcher.handle()
|
||||
async def _(session: EventSession, arparma: Arparma):
|
||||
gid = session.id3 or session.id2
|
||||
if not session.id1:
|
||||
await MessageUtils.build_message("用户id为空...").finish()
|
||||
if not gid:
|
||||
await MessageUtils.build_message("群组id为空...").finish()
|
||||
result = await get_my_knifes(session.id1, gid)
|
||||
await result.send(reply_to=True)
|
||||
logger.info("查询我的金色", arparma.header_result, session=session)
|
||||
|
||||
|
||||
@_multiple_matcher.handle()
|
||||
async def _(session: EventSession, arparma: Arparma, num: int, name: Match[str]):
|
||||
gid = session.id3 or session.id2
|
||||
if not session.id1:
|
||||
await MessageUtils.build_message("用户id为空...").finish()
|
||||
if not gid:
|
||||
await MessageUtils.build_message("群组id为空...").finish()
|
||||
if num > 30:
|
||||
await MessageUtils.build_message("开箱次数不要超过30啊笨蛋!").finish()
|
||||
if num < 0:
|
||||
await MessageUtils.build_message("再负开箱就扣你明天开箱数了!").finish()
|
||||
case_name = None
|
||||
if name.available:
|
||||
case_name = name.result.replace("武器箱", "").strip()
|
||||
result = await open_multiple_case(session.id1, gid, case_name, num, session)
|
||||
await result.send(reply_to=True)
|
||||
logger.info(f"{num}连开箱", arparma.header_result, session=session)
|
||||
|
||||
|
||||
@_update_matcher.handle()
|
||||
async def _(session: EventSession, arparma: Arparma, name: Match[str]):
|
||||
case_name = None
|
||||
if name.available:
|
||||
case_name = name.result.strip()
|
||||
if not case_name:
|
||||
case_list = []
|
||||
skin_list = []
|
||||
for i, case_name in enumerate(CASE2ID):
|
||||
if case_name in CaseManager.CURRENT_CASES:
|
||||
case_list.append(f"{i+1}.{case_name} [已更新]")
|
||||
else:
|
||||
case_list.append(f"{i+1}.{case_name}")
|
||||
for skin_name in KNIFE2ID:
|
||||
skin_list.append(f"{skin_name}")
|
||||
text = "武器箱:\n" + "\n".join(case_list) + "\n皮肤:\n" + ", ".join(skin_list)
|
||||
img = await text2image(text, padding=20, color="#f9f6f2")
|
||||
await MessageUtils.build_message(
|
||||
["未指定武器箱, 当前已包含武器箱/皮肤\n", img]
|
||||
).finish()
|
||||
if case_name in ["ALL", "ALL1"]:
|
||||
if case_name == "ALL":
|
||||
case_list = list(CASE2ID.keys())
|
||||
type_ = "武器箱"
|
||||
else:
|
||||
case_list = list(KNIFE2ID.keys())
|
||||
type_ = "罕见皮肤"
|
||||
await MessageUtils.build_message(f"即将更新所有{type_}, 请稍等").send()
|
||||
for i, case_name in enumerate(case_list):
|
||||
try:
|
||||
info = await update_skin_data(case_name, arparma.find("s"))
|
||||
if "请先登录" in info:
|
||||
await MessageUtils.build_message(
|
||||
f"未登录, 已停止更新, 请配置BUFF token..."
|
||||
).send()
|
||||
return
|
||||
rand = random.randint(300, 500)
|
||||
result = f"更新全部{type_}完成"
|
||||
if i < len(case_list) - 1:
|
||||
next_case = case_list[i + 1]
|
||||
result = f"将在 {rand} 秒后更新下一{type_}: {next_case}"
|
||||
await MessageUtils.build_message(f"{info}, {result}").send()
|
||||
logger.info(f"info, {result}", "更新武器箱", session=session)
|
||||
await asyncio.sleep(rand)
|
||||
except Exception as e:
|
||||
logger.error(f"更新{type_}: {case_name}", session=session, e=e)
|
||||
await MessageUtils.build_message(
|
||||
f"更新{type_}: {case_name} 发生错误: {type(e)}: {e}"
|
||||
).send()
|
||||
await MessageUtils.build_message(f"更新全部{type_}完成").send()
|
||||
else:
|
||||
await MessageUtils.build_message(
|
||||
f"开始{arparma.header_result}: {case_name}, 请稍等"
|
||||
).send()
|
||||
try:
|
||||
await MessageUtils.build_message(
|
||||
await update_skin_data(case_name, arparma.find("s"))
|
||||
).send(at_sender=True)
|
||||
except Exception as e:
|
||||
logger.error(f"{arparma.header_result}: {case_name}", session=session, e=e)
|
||||
await MessageUtils.build_message(
|
||||
f"成功{arparma.header_result}: {case_name} 发生错误: {type(e)}: {e}"
|
||||
).send()
|
||||
|
||||
|
||||
@_show_case_matcher.handle()
|
||||
async def _(session: EventSession, arparma: Arparma, name: Match[str]):
|
||||
case_name = None
|
||||
if name.available:
|
||||
case_name = name.result.strip()
|
||||
result = await build_case_image(case_name)
|
||||
if isinstance(result, str):
|
||||
await MessageUtils.build_message(result).send()
|
||||
else:
|
||||
await MessageUtils.build_message(result).send()
|
||||
logger.info("查看武器箱", arparma.header_result, session=session)
|
||||
|
||||
|
||||
@_update_image_matcher.handle()
|
||||
async def _(session: EventSession, arparma: Arparma, name: Match[str]):
|
||||
case_name = None
|
||||
if name.available:
|
||||
case_name = name.result.strip()
|
||||
await MessageUtils.build_message("开始更新图片...").send(reply_to=True)
|
||||
await download_image(case_name)
|
||||
await MessageUtils.build_message("更新图片完成...").send(at_sender=True)
|
||||
logger.info("更新武器箱图片", arparma.header_result, session=session)
|
||||
|
||||
|
||||
# 重置开箱
|
||||
@scheduler.scheduled_job(
|
||||
"cron",
|
||||
hour=0,
|
||||
minute=1,
|
||||
)
|
||||
async def _():
|
||||
await reset_count_daily()
|
||||
|
||||
|
||||
@scheduler.scheduled_job(
|
||||
"cron",
|
||||
hour=0,
|
||||
minute=10,
|
||||
)
|
||||
async def _():
|
||||
now = datetime.now()
|
||||
hour = random.choice([0, 1, 2, 3])
|
||||
date = now + timedelta(hours=hour)
|
||||
logger.debug(f"将在 {date} 时自动更新武器箱...", "更新武器箱")
|
||||
scheduler.add_job(
|
||||
auto_update,
|
||||
"date",
|
||||
run_date=date.replace(microsecond=0),
|
||||
id=f"auto_update_csgo_cases",
|
||||
)
|
||||
@@ -1,155 +0,0 @@
|
||||
from datetime import timedelta, timezone
|
||||
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.utils import cn2py
|
||||
|
||||
from .config import COLOR2COLOR, COLOR2NAME
|
||||
from .models.buff_skin import BuffSkin
|
||||
|
||||
BASE_PATH = IMAGE_PATH / "csgo_cases"
|
||||
|
||||
ICON_PATH = IMAGE_PATH / "_icon"
|
||||
|
||||
|
||||
async def draw_card(skin: BuffSkin, rand: str) -> BuildImage:
|
||||
"""构造抽取图片
|
||||
|
||||
参数:
|
||||
skin (BuffSkin): BuffSkin
|
||||
rand (str): 磨损
|
||||
|
||||
返回:
|
||||
BuildImage: BuildImage
|
||||
"""
|
||||
name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion
|
||||
file_path = BASE_PATH / cn2py(skin.case_name.split(",")[0]) / f"{cn2py(name)}.jpg"
|
||||
if not file_path.exists():
|
||||
logger.warning(f"皮肤图片: {name} 不存在", "开箱")
|
||||
skin_bk = BuildImage(
|
||||
460, 200, color=(25, 25, 25, 100), font_size=25, font="CJGaoDeGuo.otf"
|
||||
)
|
||||
if file_path.exists():
|
||||
skin_image = BuildImage(205, 153, background=file_path)
|
||||
await skin_bk.paste(skin_image, (10, 30))
|
||||
await skin_bk.line((220, 10, 220, 180))
|
||||
await skin_bk.text((10, 10), skin.name, (255, 255, 255))
|
||||
name_icon = BuildImage(20, 20, background=ICON_PATH / "name_white.png")
|
||||
await skin_bk.paste(name_icon, (240, 13))
|
||||
await skin_bk.text((265, 15), f"名称:", (255, 255, 255), font_size=20)
|
||||
await skin_bk.text(
|
||||
(310, 15),
|
||||
f"{skin.skin_name + ('(St)' if skin.is_stattrak else '')}",
|
||||
(255, 255, 255),
|
||||
)
|
||||
tone_icon = BuildImage(20, 20, background=ICON_PATH / "tone_white.png")
|
||||
await skin_bk.paste(tone_icon, (240, 45))
|
||||
await skin_bk.text((265, 45), "品质:", (255, 255, 255), font_size=20)
|
||||
await skin_bk.text((310, 45), COLOR2NAME[skin.color][:2], COLOR2COLOR[skin.color])
|
||||
type_icon = BuildImage(20, 20, background=ICON_PATH / "type_white.png")
|
||||
await skin_bk.paste(type_icon, (240, 73))
|
||||
await skin_bk.text((265, 75), "类型:", (255, 255, 255), font_size=20)
|
||||
await skin_bk.text((310, 75), skin.weapon_type, (255, 255, 255))
|
||||
price_icon = BuildImage(20, 20, background=ICON_PATH / "price_white.png")
|
||||
await skin_bk.paste(price_icon, (240, 103))
|
||||
await skin_bk.text((265, 105), "价格:", (255, 255, 255), font_size=20)
|
||||
await skin_bk.text((310, 105), str(skin.sell_min_price), (0, 255, 98))
|
||||
abrasion_icon = BuildImage(20, 20, background=ICON_PATH / "abrasion_white.png")
|
||||
await skin_bk.paste(abrasion_icon, (240, 133))
|
||||
await skin_bk.text((265, 135), "磨损:", (255, 255, 255), font_size=20)
|
||||
await skin_bk.text((310, 135), skin.abrasion, (255, 255, 255))
|
||||
await skin_bk.text((228, 165), f"({rand})", (255, 255, 255))
|
||||
return skin_bk
|
||||
|
||||
|
||||
async def generate_skin(skin: BuffSkin, update_count: int) -> BuildImage | None:
|
||||
"""构造皮肤图片
|
||||
|
||||
参数:
|
||||
skin (BuffSkin): BuffSkin
|
||||
|
||||
返回:
|
||||
BuildImage | None: 图片
|
||||
"""
|
||||
name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion
|
||||
file_path = BASE_PATH / cn2py(skin.case_name.split(",")[0]) / f"{cn2py(name)}.jpg"
|
||||
if not file_path.exists():
|
||||
logger.warning(f"皮肤图片: {name} 不存在", "查看武器箱")
|
||||
if skin.color == "CASE":
|
||||
case_bk = BuildImage(
|
||||
700, 200, color=(25, 25, 25, 100), font_size=25, font="CJGaoDeGuo.otf"
|
||||
)
|
||||
if file_path.exists():
|
||||
skin_img = BuildImage(200, 200, background=file_path)
|
||||
await case_bk.paste(skin_img, (10, 10))
|
||||
await case_bk.line((250, 10, 250, 190))
|
||||
await case_bk.line((280, 160, 660, 160))
|
||||
name_icon = BuildImage(30, 30, background=ICON_PATH / "box_white.png")
|
||||
await case_bk.paste(name_icon, (260, 25))
|
||||
await case_bk.text((295, 30), "名称:", (255, 255, 255))
|
||||
await case_bk.text((345, 30), skin.case_name, (255, 0, 38), font_size=30)
|
||||
|
||||
type_icon = BuildImage(30, 30, background=ICON_PATH / "type_white.png")
|
||||
await case_bk.paste(type_icon, (260, 70))
|
||||
await case_bk.text((295, 75), "类型:", (255, 255, 255))
|
||||
await case_bk.text((345, 75), "武器箱", (0, 157, 255), font_size=30)
|
||||
|
||||
price_icon = BuildImage(30, 30, background=ICON_PATH / "price_white.png")
|
||||
await case_bk.paste(price_icon, (260, 114))
|
||||
await case_bk.text((295, 120), "单价:", (255, 255, 255))
|
||||
await case_bk.text(
|
||||
(340, 120), str(skin.sell_min_price), (0, 255, 98), font_size=30
|
||||
)
|
||||
|
||||
update_count_icon = BuildImage(
|
||||
40, 40, background=ICON_PATH / "reload_white.png"
|
||||
)
|
||||
await case_bk.paste(update_count_icon, (575, 10))
|
||||
await case_bk.text((625, 12), str(update_count), (255, 255, 255), font_size=45)
|
||||
|
||||
num_icon = BuildImage(30, 30, background=ICON_PATH / "num_white.png")
|
||||
await case_bk.paste(num_icon, (455, 70))
|
||||
await case_bk.text((490, 75), "在售:", (255, 255, 255))
|
||||
await case_bk.text((535, 75), str(skin.sell_num), (144, 0, 255), font_size=30)
|
||||
|
||||
want_buy_icon = BuildImage(30, 30, background=ICON_PATH / "want_buy_white.png")
|
||||
await case_bk.paste(want_buy_icon, (455, 114))
|
||||
await case_bk.text((490, 120), "求购:", (255, 255, 255))
|
||||
await case_bk.text((535, 120), str(skin.buy_num), (144, 0, 255), font_size=30)
|
||||
|
||||
await case_bk.text((275, 165), "更新时间", (255, 255, 255), font_size=22)
|
||||
date = str(
|
||||
skin.update_time.replace(microsecond=0).astimezone(
|
||||
timezone(timedelta(hours=8))
|
||||
)
|
||||
).split("+")[0]
|
||||
await case_bk.text(
|
||||
(350, 165),
|
||||
date,
|
||||
(255, 255, 255),
|
||||
font_size=30,
|
||||
)
|
||||
return case_bk
|
||||
else:
|
||||
skin_bk = BuildImage(
|
||||
235, 250, color=(25, 25, 25, 100), font_size=25, font="CJGaoDeGuo.otf"
|
||||
)
|
||||
if file_path.exists():
|
||||
skin_image = BuildImage(205, 153, background=file_path)
|
||||
await skin_bk.paste(skin_image, (10, 30))
|
||||
update_count_icon = BuildImage(
|
||||
35, 35, background=ICON_PATH / "reload_white.png"
|
||||
)
|
||||
await skin_bk.line((10, 180, 220, 180))
|
||||
await skin_bk.text((10, 10), skin.name, (255, 255, 255))
|
||||
await skin_bk.paste(update_count_icon, (140, 10))
|
||||
await skin_bk.text((175, 15), str(update_count), (255, 255, 255))
|
||||
await skin_bk.text((10, 185), f"{skin.skin_name}", (255, 255, 255), "width")
|
||||
await skin_bk.text((10, 218), "品质:", (255, 255, 255))
|
||||
await skin_bk.text(
|
||||
(55, 218), COLOR2NAME[skin.color][:2], COLOR2COLOR[skin.color]
|
||||
)
|
||||
await skin_bk.text((100, 218), "类型:", (255, 255, 255))
|
||||
await skin_bk.text((145, 218), skin.weapon_type, (255, 255, 255))
|
||||
return skin_bk
|
||||
@@ -1,75 +0,0 @@
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot_plugin_alconna import Alconna, Args, Option, on_alconna, store_true
|
||||
|
||||
from zhenxun.utils.rules import ensure_group
|
||||
|
||||
_open_matcher = on_alconna(
|
||||
Alconna("开箱", Args["name?", str]), priority=5, block=True, rule=ensure_group
|
||||
)
|
||||
|
||||
_reload_matcher = on_alconna(
|
||||
Alconna("重置开箱"), priority=5, block=True, permission=SUPERUSER, rule=ensure_group
|
||||
)
|
||||
|
||||
_my_open_matcher = on_alconna(
|
||||
Alconna("我的开箱"),
|
||||
aliases={"开箱统计", "开箱查询", "查询开箱"},
|
||||
priority=5,
|
||||
block=True,
|
||||
rule=ensure_group,
|
||||
)
|
||||
|
||||
_group_open_matcher = on_alconna(
|
||||
Alconna("群开箱统计"), priority=5, block=True, rule=ensure_group
|
||||
)
|
||||
|
||||
_multiple_matcher = on_alconna(
|
||||
Alconna("multiple-open", Args["num", int]["name?", str]),
|
||||
priority=5,
|
||||
block=True,
|
||||
rule=ensure_group,
|
||||
)
|
||||
|
||||
_multiple_matcher.shortcut(
|
||||
r"(?P<num>\d+)连开箱(?P<name>.*?)",
|
||||
command="multiple-open",
|
||||
arguments=["{num}", "{name}"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_update_matcher = on_alconna(
|
||||
Alconna(
|
||||
"更新武器箱",
|
||||
Args["name?", str],
|
||||
Option("-s", action=store_true, help_text="是否必定更新所属箱子"),
|
||||
),
|
||||
aliases={"更新皮肤"},
|
||||
priority=1,
|
||||
permission=SUPERUSER,
|
||||
block=True,
|
||||
)
|
||||
|
||||
_update_image_matcher = on_alconna(
|
||||
Alconna("更新武器箱图片", Args["name?", str]),
|
||||
priority=1,
|
||||
permission=SUPERUSER,
|
||||
block=True,
|
||||
)
|
||||
|
||||
_show_case_matcher = on_alconna(
|
||||
Alconna("查看武器箱", Args["name?", str]), priority=5, block=True
|
||||
)
|
||||
|
||||
_knifes_matcher = on_alconna(
|
||||
Alconna("我的金色"), priority=5, block=True, rule=ensure_group
|
||||
)
|
||||
|
||||
_show_skin_matcher = on_alconna(Alconna("查看皮肤"), priority=5, block=True)
|
||||
|
||||
_price_matcher = on_alconna(
|
||||
Alconna(
|
||||
"价格趋势", Args["name", str]["skin", str]["abrasion", str]["day?", int, 7]
|
||||
),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
@@ -1,253 +0,0 @@
|
||||
import random
|
||||
from enum import Enum
|
||||
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
from .models.buff_skin import BuffSkin
|
||||
|
||||
BLUE = 0.7981
|
||||
BLUE_ST = 0.0699
|
||||
PURPLE = 0.1626
|
||||
PURPLE_ST = 0.0164
|
||||
PINK = 0.0315
|
||||
PINK_ST = 0.0048
|
||||
RED = 0.0057
|
||||
RED_ST = 0.00021
|
||||
KNIFE = 0.0021
|
||||
KNIFE_ST = 0.000041
|
||||
|
||||
# 崭新
|
||||
FACTORY_NEW_S = 0
|
||||
FACTORY_NEW_E = 0.0699999
|
||||
# 略磨
|
||||
MINIMAL_WEAR_S = 0.07
|
||||
MINIMAL_WEAR_E = 0.14999
|
||||
# 久经
|
||||
FIELD_TESTED_S = 0.15
|
||||
FIELD_TESTED_E = 0.37999
|
||||
# 破损
|
||||
WELL_WORN_S = 0.38
|
||||
WELL_WORN_E = 0.44999
|
||||
# 战痕
|
||||
BATTLE_SCARED_S = 0.45
|
||||
BATTLE_SCARED_E = 0.99999
|
||||
|
||||
|
||||
class UpdateType(Enum):
|
||||
"""
|
||||
更新类型
|
||||
"""
|
||||
|
||||
CASE = "case"
|
||||
WEAPON_TYPE = "weapon_type"
|
||||
|
||||
|
||||
NAME2COLOR = {
|
||||
"消费级": "WHITE",
|
||||
"工业级": "LIGHTBLUE",
|
||||
"军规级": "BLUE",
|
||||
"受限": "PURPLE",
|
||||
"保密": "PINK",
|
||||
"隐秘": "RED",
|
||||
"非凡": "KNIFE",
|
||||
}
|
||||
|
||||
COLOR2NAME = {
|
||||
"WHITE": "消费级",
|
||||
"LIGHTBLUE": "工业级",
|
||||
"BLUE": "军规级",
|
||||
"PURPLE": "受限",
|
||||
"PINK": "保密",
|
||||
"RED": "隐秘",
|
||||
"KNIFE": "非凡",
|
||||
}
|
||||
|
||||
COLOR2COLOR = {
|
||||
"WHITE": (255, 255, 255),
|
||||
"LIGHTBLUE": (0, 179, 255),
|
||||
"BLUE": (0, 85, 255),
|
||||
"PURPLE": (149, 0, 255),
|
||||
"PINK": (255, 0, 162),
|
||||
"RED": (255, 34, 0),
|
||||
"KNIFE": (255, 225, 0),
|
||||
}
|
||||
|
||||
ABRASION_SORT = ["崭新出厂", "略有磨损", "久经沙场", "破损不堪", "战横累累"]
|
||||
|
||||
CASE_BACKGROUND = IMAGE_PATH / "csgo_cases" / "_background" / "shu"
|
||||
|
||||
# 刀
|
||||
KNIFE2ID = {
|
||||
"鲍伊猎刀": "weapon_knife_survival_bowie",
|
||||
"蝴蝶刀": "weapon_knife_butterfly",
|
||||
"弯刀": "weapon_knife_falchion",
|
||||
"折叠刀": "weapon_knife_flip",
|
||||
"穿肠刀": "weapon_knife_gut",
|
||||
"猎杀者匕首": "weapon_knife_tactical",
|
||||
"M9刺刀": "weapon_knife_m9_bayonet",
|
||||
"刺刀": "weapon_bayonet",
|
||||
"爪子刀": "weapon_knife_karambit",
|
||||
"暗影双匕": "weapon_knife_push",
|
||||
"短剑": "weapon_knife_stiletto",
|
||||
"熊刀": "weapon_knife_ursus",
|
||||
"折刀": "weapon_knife_gypsy_jackknife",
|
||||
"锯齿爪刀": "weapon_knife_widowmaker",
|
||||
"海豹短刀": "weapon_knife_css",
|
||||
"系绳匕首": "weapon_knife_cord",
|
||||
"求生匕首": "weapon_knife_canis",
|
||||
"流浪者匕首": "weapon_knife_outdoor",
|
||||
"骷髅匕首": "weapon_knife_skeleton",
|
||||
"血猎手套": "weapon_bloodhound_gloves",
|
||||
"驾驶手套": "weapon_driver_gloves",
|
||||
"手部束带": "weapon_hand_wraps",
|
||||
"摩托手套": "weapon_moto_gloves",
|
||||
"专业手套": "weapon_specialist_gloves",
|
||||
"运动手套": "weapon_sport_gloves",
|
||||
"九头蛇手套": "weapon_hydra_gloves",
|
||||
"狂牙手套": "weapon_brokenfang_gloves",
|
||||
}
|
||||
|
||||
WEAPON2ID = {}
|
||||
|
||||
# 武器箱
|
||||
CASE2ID = {
|
||||
"变革": "set_community_32",
|
||||
"反冲": "set_community_31",
|
||||
"梦魇": "set_community_30",
|
||||
"激流": "set_community_29",
|
||||
"蛇噬": "set_community_28",
|
||||
"狂牙大行动": "set_community_27",
|
||||
"裂空": "set_community_26",
|
||||
"棱彩2号": "set_community_25",
|
||||
"CS20": "set_community_24",
|
||||
"裂网大行动": "set_community_23",
|
||||
"棱彩": "set_community_22",
|
||||
"头号特训": "set_community_21",
|
||||
"地平线": "set_community_20",
|
||||
"命悬一线": "set_community_19",
|
||||
"光谱2号": "set_community_18",
|
||||
"九头蛇大行动": "set_community_17",
|
||||
"光谱": "set_community_16",
|
||||
"手套": "set_community_15",
|
||||
"伽玛2号": "set_gamma_2",
|
||||
"伽玛": "set_community_13",
|
||||
"幻彩3号": "set_community_12",
|
||||
"野火大行动": "set_community_11",
|
||||
"左轮": "set_community_10",
|
||||
"暗影": "set_community_9",
|
||||
"弯曲猎手": "set_community_8",
|
||||
"幻彩2号": "set_community_7",
|
||||
"幻彩": "set_community_6",
|
||||
"先锋": "set_community_5",
|
||||
"电竞2014夏季": "set_esports_iii",
|
||||
"突围大行动": "set_community_4",
|
||||
"猎杀者": "set_community_3",
|
||||
"凤凰": "set_community_2",
|
||||
"电竞2013冬季": "set_esports_ii",
|
||||
"冬季攻势": "set_community_1",
|
||||
"军火交易3号": "set_weapons_iii",
|
||||
"英勇": "set_bravo_i",
|
||||
"电竞2013": "set_esports",
|
||||
"军火交易2号": "set_weapons_ii",
|
||||
"军火交易": "set_weapons_i",
|
||||
}
|
||||
|
||||
|
||||
def get_wear(rand: float) -> str:
|
||||
"""判断磨损度
|
||||
|
||||
Args:
|
||||
rand (float): 随机rand
|
||||
|
||||
Returns:
|
||||
str: 磨损名称
|
||||
"""
|
||||
if rand <= FACTORY_NEW_E:
|
||||
return "崭新出厂"
|
||||
if MINIMAL_WEAR_S <= rand <= MINIMAL_WEAR_E:
|
||||
return "略有磨损"
|
||||
if FIELD_TESTED_S <= rand <= FIELD_TESTED_E:
|
||||
return "久经沙场"
|
||||
if WELL_WORN_S <= rand <= WELL_WORN_E:
|
||||
return "破损不堪"
|
||||
return "战痕累累"
|
||||
|
||||
|
||||
def random_color_and_st(rand: float) -> tuple[str, bool]:
|
||||
"""获取皮肤品质及是否暗金
|
||||
|
||||
参数:
|
||||
rand (float): 随机rand
|
||||
|
||||
返回:
|
||||
tuple[str, bool]: 品质,是否暗金
|
||||
"""
|
||||
if rand <= KNIFE:
|
||||
if random.random() <= KNIFE_ST:
|
||||
return ("KNIFE", True)
|
||||
return ("KNIFE", False)
|
||||
elif KNIFE < rand <= RED:
|
||||
if random.random() <= RED_ST:
|
||||
return ("RED", True)
|
||||
return ("RED", False)
|
||||
elif RED < rand <= PINK:
|
||||
if random.random() <= PINK_ST:
|
||||
return ("PINK", True)
|
||||
return ("PINK", False)
|
||||
elif PINK < rand <= PURPLE:
|
||||
if random.random() <= PURPLE_ST:
|
||||
return ("PURPLE", True)
|
||||
return ("PURPLE", False)
|
||||
else:
|
||||
if random.random() <= BLUE_ST:
|
||||
return ("BLUE", True)
|
||||
return ("BLUE", False)
|
||||
|
||||
|
||||
async def random_skin(num: int, case_name: str) -> list[tuple[BuffSkin, float]]:
|
||||
"""
|
||||
随机抽取皮肤
|
||||
"""
|
||||
case_name = case_name.replace("武器箱", "").replace(" ", "")
|
||||
color_map = {}
|
||||
for _ in range(num):
|
||||
rand = random.random()
|
||||
# 尝试降低磨损
|
||||
if rand > MINIMAL_WEAR_E:
|
||||
for _ in range(2):
|
||||
if random.random() < 0.5:
|
||||
logger.debug(f"[START]开箱随机磨损触发降低磨损条件: {rand}")
|
||||
if random.random() < 0.2:
|
||||
rand /= 3
|
||||
else:
|
||||
rand /= 2
|
||||
logger.debug(f"[END]开箱随机磨损触发降低磨损条件: {rand}")
|
||||
break
|
||||
abrasion = get_wear(rand)
|
||||
logger.debug(f"开箱随机磨损: {rand} | {abrasion}")
|
||||
color, is_stattrak = random_color_and_st(rand)
|
||||
if not color_map.get(color):
|
||||
color_map[color] = {}
|
||||
if is_stattrak:
|
||||
if not color_map[color].get(f"{abrasion}_st"):
|
||||
color_map[color][f"{abrasion}_st"] = []
|
||||
color_map[color][f"{abrasion}_st"].append(rand)
|
||||
else:
|
||||
if not color_map[color].get(abrasion):
|
||||
color_map[color][f"{abrasion}"] = []
|
||||
color_map[color][f"{abrasion}"].append(rand)
|
||||
skin_list = []
|
||||
for color in color_map:
|
||||
for abrasion in color_map[color]:
|
||||
rand_list = color_map[color][abrasion]
|
||||
is_stattrak = "_st" in abrasion
|
||||
abrasion = abrasion.replace("_st", "")
|
||||
skin_list_ = await BuffSkin.random_skin(
|
||||
len(rand_list), color, abrasion, is_stattrak, case_name
|
||||
)
|
||||
skin_list += [(skin, rand) for skin, rand in zip(skin_list_, rand_list)]
|
||||
return skin_list
|
||||
|
||||
|
||||
# M249(StatTrak™) | 等高线
|
||||
@@ -1,22 +0,0 @@
|
||||
from tortoise import fields
|
||||
|
||||
from zhenxun.services.db_context import Model
|
||||
|
||||
# 1.狂牙武器箱
|
||||
|
||||
|
||||
class BuffPrice(Model):
|
||||
|
||||
id = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
"""自增id"""
|
||||
case_id = fields.IntField()
|
||||
"""箱子id"""
|
||||
skin_name = fields.CharField(255, unique=True)
|
||||
"""皮肤名称"""
|
||||
skin_price = fields.FloatField()
|
||||
"""皮肤价格"""
|
||||
update_date = fields.DatetimeField()
|
||||
|
||||
class Meta:
|
||||
table = "buff_prices"
|
||||
table_description = "Buff价格数据表"
|
||||
@@ -1,113 +0,0 @@
|
||||
from tortoise import fields
|
||||
from tortoise.contrib.postgres.functions import Random
|
||||
|
||||
from zhenxun.services.db_context import Model
|
||||
|
||||
|
||||
class BuffSkin(Model):
|
||||
|
||||
id = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
"""自增id"""
|
||||
case_name: str = fields.CharField(255) # type: ignore
|
||||
"""箱子名称"""
|
||||
name: str = fields.CharField(255) # type: ignore
|
||||
"""武器/手套/刀名称"""
|
||||
skin_name: str = fields.CharField(255) # type: ignore
|
||||
"""皮肤名称"""
|
||||
is_stattrak = fields.BooleanField(default=False)
|
||||
"""是否暗金(计数)"""
|
||||
abrasion = fields.CharField(255)
|
||||
"""磨损度"""
|
||||
color = fields.CharField(255)
|
||||
"""颜色(品质)"""
|
||||
skin_id = fields.CharField(255, null=True, unique=True)
|
||||
"""皮肤id"""
|
||||
|
||||
img_url = fields.CharField(255)
|
||||
"""图片url"""
|
||||
steam_price = fields.FloatField(default=0)
|
||||
"""steam价格"""
|
||||
weapon_type = fields.CharField(255)
|
||||
"""枪械类型"""
|
||||
buy_max_price = fields.FloatField(default=0)
|
||||
"""最大求购价格"""
|
||||
buy_num = fields.IntField(default=0)
|
||||
"""求购数量"""
|
||||
sell_min_price = fields.FloatField(default=0)
|
||||
"""售卖最低价格"""
|
||||
sell_num = fields.IntField(default=0)
|
||||
"""出售个数"""
|
||||
sell_reference_price = fields.FloatField(default=0)
|
||||
"""参考价格"""
|
||||
|
||||
create_time = fields.DatetimeField(auto_add_now=True)
|
||||
"""创建日期"""
|
||||
update_time = fields.DatetimeField(auto_add=True)
|
||||
"""更新日期"""
|
||||
|
||||
class Meta:
|
||||
table = "buff_skin"
|
||||
table_description = "Buff皮肤数据表"
|
||||
# unique_together = ("case_name", "name", "skin_name", "abrasion", "is_stattrak")
|
||||
|
||||
def __eq__(self, other: "BuffSkin"):
|
||||
|
||||
return self.skin_id == other.skin_id
|
||||
|
||||
def __hash__(self):
|
||||
|
||||
return hash(self.case_name + self.name + self.skin_name + str(self.is_stattrak))
|
||||
|
||||
@classmethod
|
||||
async def random_skin(
|
||||
cls,
|
||||
num: int,
|
||||
color: str,
|
||||
abrasion: str,
|
||||
is_stattrak: bool = False,
|
||||
case_name: str | None = None,
|
||||
) -> list["BuffSkin"]: # type: ignore
|
||||
"""随机皮肤
|
||||
|
||||
参数:
|
||||
num: 数量
|
||||
color: 品质
|
||||
abrasion: 磨损度
|
||||
is_stattrak: 是否暗金
|
||||
case_name: 箱子名称
|
||||
|
||||
返回:
|
||||
list["BuffSkin"]: 皮肤列表
|
||||
"""
|
||||
query = cls
|
||||
if case_name:
|
||||
query = query.filter(case_name__contains=case_name)
|
||||
query = query.filter(abrasion=abrasion, is_stattrak=is_stattrak, color=color)
|
||||
skin_list = await query.annotate(rand=Random()).limit(num) # type:ignore
|
||||
num_ = num
|
||||
cnt = 0
|
||||
while len(skin_list) < num:
|
||||
cnt += 1
|
||||
num_ = num - len(skin_list)
|
||||
skin_list += await query.annotate(rand=Random()).limit(num_)
|
||||
if cnt > 10:
|
||||
break
|
||||
return skin_list # type: ignore
|
||||
|
||||
@classmethod
|
||||
async def _run_script(cls):
|
||||
return [
|
||||
"ALTER TABLE buff_skin ADD img_url varchar(255);", # 新增img_url
|
||||
"ALTER TABLE buff_skin ADD skin_id varchar(255);", # 新增skin_id
|
||||
"ALTER TABLE buff_skin ADD steam_price float DEFAULT 0;", # 新增steam_price
|
||||
"ALTER TABLE buff_skin ADD weapon_type varchar(255);", # 新增type
|
||||
"ALTER TABLE buff_skin ADD buy_max_price float DEFAULT 0;", # 新增buy_max_price
|
||||
"ALTER TABLE buff_skin ADD buy_num Integer DEFAULT 0;", # 新增buy_max_price
|
||||
"ALTER TABLE buff_skin ADD sell_min_price float DEFAULT 0;", # 新增sell_min_price
|
||||
"ALTER TABLE buff_skin ADD sell_num Integer DEFAULT 0;", # 新增sell_num
|
||||
"ALTER TABLE buff_skin ADD sell_reference_price float DEFAULT 0;", # 新增sell_reference_price
|
||||
"ALTER TABLE buff_skin DROP COLUMN skin_price;", # 删除skin_price
|
||||
"alter table buff_skin drop constraint if EXISTS uid_buff_skin_case_na_c35c93;", # 删除唯一约束
|
||||
"UPDATE buff_skin set case_name='手套' where case_name='手套武器箱'",
|
||||
"UPDATE buff_skin set case_name='左轮' where case_name='左轮武器箱'",
|
||||
]
|
||||
@@ -1,50 +0,0 @@
|
||||
from tortoise import fields
|
||||
|
||||
from zhenxun.services.db_context import Model
|
||||
|
||||
|
||||
class BuffSkinLog(Model):
|
||||
|
||||
id = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
"""自增id"""
|
||||
case_name = fields.CharField(255)
|
||||
"""箱子名称"""
|
||||
name = fields.CharField(255)
|
||||
"""武器/手套/刀名称"""
|
||||
skin_name = fields.CharField(255)
|
||||
"""皮肤名称"""
|
||||
is_stattrak = fields.BooleanField(default=False)
|
||||
"""是否暗金(计数)"""
|
||||
abrasion = fields.CharField(255)
|
||||
"""磨损度"""
|
||||
color = fields.CharField(255)
|
||||
"""颜色(品质)"""
|
||||
|
||||
steam_price = fields.FloatField(default=0)
|
||||
"""steam价格"""
|
||||
weapon_type = fields.CharField(255)
|
||||
"""枪械类型"""
|
||||
buy_max_price = fields.FloatField(default=0)
|
||||
"""最大求购价格"""
|
||||
buy_num = fields.IntField(default=0)
|
||||
"""求购数量"""
|
||||
sell_min_price = fields.FloatField(default=0)
|
||||
"""售卖最低价格"""
|
||||
sell_num = fields.IntField(default=0)
|
||||
"""出售个数"""
|
||||
sell_reference_price = fields.FloatField(default=0)
|
||||
"""参考价格"""
|
||||
|
||||
create_time = fields.DatetimeField(auto_add_now=True)
|
||||
"""创建日期"""
|
||||
|
||||
class Meta:
|
||||
table = "buff_skin_log"
|
||||
table_description = "Buff皮肤更新日志表"
|
||||
|
||||
@classmethod
|
||||
async def _run_script(cls):
|
||||
return [
|
||||
"UPDATE buff_skin_log set case_name='手套' where case_name='手套武器箱'",
|
||||
"UPDATE buff_skin_log set case_name='左轮' where case_name='左轮武器箱'",
|
||||
]
|
||||
@@ -1,44 +0,0 @@
|
||||
from tortoise import fields
|
||||
from tortoise.contrib.postgres.functions import Random
|
||||
|
||||
from zhenxun.services.db_context import Model
|
||||
|
||||
|
||||
class OpenCasesLog(Model):
|
||||
|
||||
id = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
"""自增id"""
|
||||
user_id = fields.CharField(255)
|
||||
"""用户id"""
|
||||
group_id = fields.CharField(255)
|
||||
"""群聊id"""
|
||||
case_name = fields.CharField(255)
|
||||
"""箱子名称"""
|
||||
name = fields.CharField(255)
|
||||
"""武器/手套/刀名称"""
|
||||
skin_name = fields.CharField(255)
|
||||
"""皮肤名称"""
|
||||
is_stattrak = fields.BooleanField(default=False)
|
||||
"""是否暗金(计数)"""
|
||||
abrasion = fields.CharField(255)
|
||||
"""磨损度"""
|
||||
abrasion_value = fields.FloatField()
|
||||
"""磨损数值"""
|
||||
color = fields.CharField(255)
|
||||
"""颜色(品质)"""
|
||||
price = fields.FloatField(default=0)
|
||||
"""价格"""
|
||||
create_time = fields.DatetimeField(auto_add_now=True)
|
||||
"""创建日期"""
|
||||
|
||||
class Meta:
|
||||
table = "open_cases_log"
|
||||
table_description = "开箱日志表"
|
||||
|
||||
@classmethod
|
||||
async def _run_script(cls):
|
||||
return [
|
||||
"ALTER TABLE open_cases_log RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id
|
||||
"ALTER TABLE open_cases_log ALTER COLUMN user_id TYPE character varying(255);",
|
||||
"ALTER TABLE open_cases_log ALTER COLUMN group_id TYPE character varying(255);",
|
||||
]
|
||||
@@ -1,60 +0,0 @@
|
||||
from tortoise import fields
|
||||
|
||||
from zhenxun.services.db_context import Model
|
||||
|
||||
|
||||
class OpenCasesUser(Model):
|
||||
|
||||
id = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
"""自增id"""
|
||||
user_id = fields.CharField(255)
|
||||
"""用户id"""
|
||||
group_id = fields.CharField(255)
|
||||
"""群聊id"""
|
||||
total_count = fields.IntField(default=0)
|
||||
"""总开启次数"""
|
||||
blue_count = fields.IntField(default=0)
|
||||
"""蓝色"""
|
||||
blue_st_count = fields.IntField(default=0)
|
||||
"""蓝色暗金"""
|
||||
purple_count = fields.IntField(default=0)
|
||||
"""紫色"""
|
||||
purple_st_count = fields.IntField(default=0)
|
||||
"""紫色暗金"""
|
||||
pink_count = fields.IntField(default=0)
|
||||
"""粉色"""
|
||||
pink_st_count = fields.IntField(default=0)
|
||||
"""粉色暗金"""
|
||||
red_count = fields.IntField(default=0)
|
||||
"""紫色"""
|
||||
red_st_count = fields.IntField(default=0)
|
||||
"""紫色暗金"""
|
||||
knife_count = fields.IntField(default=0)
|
||||
"""金色"""
|
||||
knife_st_count = fields.IntField(default=0)
|
||||
"""金色暗金"""
|
||||
spend_money = fields.IntField(default=0)
|
||||
"""花费金币"""
|
||||
make_money = fields.FloatField(default=0)
|
||||
"""赚取金币"""
|
||||
today_open_total = fields.IntField(default=0)
|
||||
"""今日开箱数量"""
|
||||
open_cases_time_last = fields.DatetimeField()
|
||||
"""最后开箱日期"""
|
||||
knifes_name = fields.TextField(default="")
|
||||
"""已获取金色"""
|
||||
|
||||
class Meta:
|
||||
table = "open_cases_users"
|
||||
table_description = "开箱统计数据表"
|
||||
unique_together = ("user_id", "group_id")
|
||||
|
||||
@classmethod
|
||||
async def _run_script(cls):
|
||||
return [
|
||||
"alter table open_cases_users alter COLUMN make_money type float;", # 将make_money字段改为float
|
||||
"alter table open_cases_users alter COLUMN spend_money type float;", # 将spend_money字段改为float
|
||||
"ALTER TABLE open_cases_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id
|
||||
"ALTER TABLE open_cases_users ALTER COLUMN user_id TYPE character varying(255);",
|
||||
"ALTER TABLE open_cases_users ALTER COLUMN group_id TYPE character varying(255);",
|
||||
]
|
||||
@@ -1,481 +0,0 @@
|
||||
import asyncio
|
||||
import random
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
from zhenxun.models.sign_user import SignUser
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.utils import cn2py
|
||||
|
||||
from .build_image import draw_card
|
||||
from .config import *
|
||||
from .models.open_cases_log import OpenCasesLog
|
||||
from .models.open_cases_user import OpenCasesUser
|
||||
from .utils import CaseManager, update_skin_data
|
||||
|
||||
RESULT_MESSAGE = {
|
||||
"BLUE": ["这样看着才舒服", "是自己人,大伙把刀收好", "非常舒适~"],
|
||||
"PURPLE": ["还行吧,勉强接受一下下", "居然不是蓝色,太假了", "运气-1-1-1-1-1..."],
|
||||
"PINK": ["开始不适....", "你妈妈买菜必涨价!涨三倍!", "你最近不适合出门,真的"],
|
||||
"RED": [
|
||||
"已经非常不适",
|
||||
"好兄弟你开的什么箱子啊,一般箱子不是只有蓝色的吗",
|
||||
"开始拿阳寿开箱子了?",
|
||||
],
|
||||
"KNIFE": [
|
||||
"你的好运我收到了,你可以去喂鲨鱼了",
|
||||
"最近该吃啥就迟点啥吧,哎,好好的一个人怎么就....哎",
|
||||
"众所周知,欧皇寿命极短.",
|
||||
],
|
||||
}
|
||||
|
||||
COLOR2NAME = {
|
||||
"BLUE": "军规",
|
||||
"PURPLE": "受限",
|
||||
"PINK": "保密",
|
||||
"RED": "隐秘",
|
||||
"KNIFE": "罕见",
|
||||
}
|
||||
|
||||
COLOR2CN = {"BLUE": "蓝", "PURPLE": "紫", "PINK": "粉", "RED": "红", "KNIFE": "金"}
|
||||
|
||||
|
||||
def add_count(user: OpenCasesUser, skin: BuffSkin, case_price: float):
|
||||
if skin.color == "BLUE":
|
||||
if skin.is_stattrak:
|
||||
user.blue_st_count += 1
|
||||
else:
|
||||
user.blue_count += 1
|
||||
elif skin.color == "PURPLE":
|
||||
if skin.is_stattrak:
|
||||
user.purple_st_count += 1
|
||||
else:
|
||||
user.purple_count += 1
|
||||
elif skin.color == "PINK":
|
||||
if skin.is_stattrak:
|
||||
user.pink_st_count += 1
|
||||
else:
|
||||
user.pink_count += 1
|
||||
elif skin.color == "RED":
|
||||
if skin.is_stattrak:
|
||||
user.red_st_count += 1
|
||||
else:
|
||||
user.red_count += 1
|
||||
elif skin.color == "KNIFE":
|
||||
if skin.is_stattrak:
|
||||
user.knife_st_count += 1
|
||||
else:
|
||||
user.knife_count += 1
|
||||
user.make_money += skin.sell_min_price
|
||||
user.spend_money += int(17 + case_price)
|
||||
|
||||
|
||||
async def get_user_max_count(user_id: str) -> int:
|
||||
"""获取用户每日最大开箱次数
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
|
||||
返回:
|
||||
int: 最大开箱次数
|
||||
"""
|
||||
user, _ = await SignUser.get_or_create(user_id=user_id)
|
||||
impression = int(user.impression)
|
||||
initial_open_case_count = Config.get_config("open_cases", "INITIAL_OPEN_CASE_COUNT")
|
||||
each_impression_add_count = Config.get_config(
|
||||
"open_cases", "EACH_IMPRESSION_ADD_COUNT"
|
||||
)
|
||||
return int(initial_open_case_count + impression / each_impression_add_count) # type: ignore
|
||||
|
||||
|
||||
async def open_case(
|
||||
user_id: str, group_id: str, case_name: str | None, session: EventSession
|
||||
) -> UniMessage:
|
||||
"""开箱
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
group_id : 群号
|
||||
case_name: 武器箱名称. Defaults to "狂牙大行动".
|
||||
session: EventSession
|
||||
|
||||
返回:
|
||||
Union[str, Message]: 回复消息
|
||||
"""
|
||||
user_id = str(user_id)
|
||||
group_id = str(group_id)
|
||||
if not CaseManager.CURRENT_CASES:
|
||||
return MessageUtils.build_message("未收录任何武器箱")
|
||||
if not case_name:
|
||||
case_name = random.choice(CaseManager.CURRENT_CASES) # type: ignore
|
||||
if case_name not in CaseManager.CURRENT_CASES:
|
||||
return "武器箱未收录, 当前可用武器箱:\n" + ", ".join(CaseManager.CURRENT_CASES) # type: ignore
|
||||
logger.debug(
|
||||
f"尝试开启武器箱: {case_name}", "开箱", session=user_id, group_id=group_id
|
||||
)
|
||||
case = cn2py(case_name) # type: ignore
|
||||
user = await OpenCasesUser.get_or_none(user_id=user_id, group_id=group_id)
|
||||
if not user:
|
||||
user = await OpenCasesUser.create(
|
||||
user_id=user_id, group_id=group_id, open_cases_time_last=datetime.now()
|
||||
)
|
||||
max_count = await get_user_max_count(user_id)
|
||||
# 一天次数上限
|
||||
if user.today_open_total >= max_count:
|
||||
return MessageUtils.build_message(
|
||||
f"今天已达开箱上限了喔,明天再来吧\n(提升好感度可以增加每日开箱数 #疯狂暗示)"
|
||||
)
|
||||
skin_list = await random_skin(1, case_name) # type: ignore
|
||||
if not skin_list:
|
||||
return MessageUtils.build_message("未抽取到任何皮肤")
|
||||
skin, rand = skin_list[0]
|
||||
rand = str(rand)[:11]
|
||||
case_price = 0
|
||||
if case_skin := await BuffSkin.get_or_none(case_name=case_name, color="CASE"):
|
||||
case_price = case_skin.sell_min_price
|
||||
user.today_open_total += 1
|
||||
user.total_count += 1
|
||||
user.open_cases_time_last = datetime.now()
|
||||
await user.save(
|
||||
update_fields=["today_open_total", "total_count", "open_cases_time_last"]
|
||||
)
|
||||
add_count(user, skin, case_price)
|
||||
ridicule_result = random.choice(RESULT_MESSAGE[skin.color])
|
||||
price_result = skin.sell_min_price
|
||||
name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion
|
||||
img_path = IMAGE_PATH / "csgo_cases" / case / f"{cn2py(name)}.jpg"
|
||||
logger.info(
|
||||
f"开启{case_name}武器箱获得 {skin.name}{'(StatTrak™)' if skin.is_stattrak else ''} | {skin.skin_name} ({skin.abrasion}) 磨损: [{rand}] 价格: {skin.sell_min_price}",
|
||||
"开箱",
|
||||
session=session,
|
||||
)
|
||||
await user.save()
|
||||
await OpenCasesLog.create(
|
||||
user_id=user_id,
|
||||
group_id=group_id,
|
||||
case_name=case_name,
|
||||
name=skin.name,
|
||||
skin_name=skin.skin_name,
|
||||
is_stattrak=skin.is_stattrak,
|
||||
abrasion=skin.abrasion,
|
||||
color=skin.color,
|
||||
price=skin.sell_min_price,
|
||||
abrasion_value=rand,
|
||||
create_time=datetime.now(),
|
||||
)
|
||||
logger.debug(f"添加 1 条开箱日志", "开箱", session=session)
|
||||
over_count = max_count - user.today_open_total
|
||||
img = await draw_card(skin, rand)
|
||||
return MessageUtils.build_message(
|
||||
[
|
||||
f"开启{case_name}武器箱.\n剩余开箱次数:{over_count}.\n",
|
||||
img,
|
||||
f"\n箱子单价:{case_price}\n花费:{17 + case_price:.2f}\n:{ridicule_result}",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def open_multiple_case(
|
||||
user_id: str,
|
||||
group_id: str,
|
||||
case_name: str | None,
|
||||
num: int = 10,
|
||||
session: EventSession | None = None,
|
||||
) -> UniMessage:
|
||||
"""多连开箱
|
||||
|
||||
参数:
|
||||
user_id (int): 用户id
|
||||
group_id (int): 群号
|
||||
case_name (str): 箱子名称
|
||||
num (int, optional): 数量. Defaults to 10.
|
||||
session: EventSession
|
||||
|
||||
返回:
|
||||
_type_: _description_
|
||||
"""
|
||||
user_id = str(user_id)
|
||||
group_id = str(group_id)
|
||||
if not CaseManager.CURRENT_CASES:
|
||||
return MessageUtils.build_message("未收录任何武器箱")
|
||||
if not case_name:
|
||||
case_name = random.choice(CaseManager.CURRENT_CASES) # type: ignore
|
||||
if case_name not in CaseManager.CURRENT_CASES:
|
||||
return MessageUtils.build_message(
|
||||
"武器箱未收录, 当前可用武器箱:\n" + ", ".join(CaseManager.CURRENT_CASES)
|
||||
)
|
||||
user, _ = await OpenCasesUser.get_or_create(
|
||||
user_id=user_id,
|
||||
group_id=group_id,
|
||||
defaults={"open_cases_time_last": datetime.now()},
|
||||
)
|
||||
max_count = await get_user_max_count(user_id)
|
||||
if user.today_open_total >= max_count:
|
||||
return MessageUtils.build_message(
|
||||
f"今天已达开箱上限了喔,明天再来吧\n(提升好感度可以增加每日开箱数 #疯狂暗示)"
|
||||
)
|
||||
if max_count - user.today_open_total < num:
|
||||
return MessageUtils.build_message(
|
||||
f"今天开箱次数不足{num}次噢,请单抽试试看(也许单抽运气更好?)"
|
||||
f"\n剩余开箱次数:{max_count - user.today_open_total}"
|
||||
)
|
||||
logger.debug(f"尝试开启武器箱: {case_name}", "开箱", session=session)
|
||||
case = cn2py(case_name) # type: ignore
|
||||
skin_count = {}
|
||||
img_list = []
|
||||
skin_list = await random_skin(num, case_name) # type: ignore
|
||||
if not skin_list:
|
||||
return MessageUtils.build_message("未抽取到任何皮肤...")
|
||||
total_price = 0
|
||||
log_list = []
|
||||
now = datetime.now()
|
||||
user.today_open_total += num
|
||||
user.total_count += num
|
||||
user.open_cases_time_last = datetime.now()
|
||||
await user.save(
|
||||
update_fields=["today_open_total", "total_count", "open_cases_time_last"]
|
||||
)
|
||||
case_price = 0
|
||||
if case_skin := await BuffSkin.get_or_none(case_name=case_name, color="CASE"):
|
||||
case_price = case_skin.sell_min_price
|
||||
img_w, img_h = 0, 0
|
||||
for skin, rand in skin_list:
|
||||
img = await draw_card(skin, str(rand)[:11])
|
||||
img_w, img_h = img.size
|
||||
total_price += skin.sell_min_price
|
||||
color_name = COLOR2CN[skin.color]
|
||||
if not skin_count.get(color_name):
|
||||
skin_count[color_name] = 0
|
||||
skin_count[color_name] += 1
|
||||
add_count(user, skin, case_price)
|
||||
img_list.append(img)
|
||||
logger.info(
|
||||
f"开启{case_name}武器箱获得 {skin.name}{'(StatTrak™)' if skin.is_stattrak else ''} | {skin.skin_name} ({skin.abrasion}) 磨损: [{rand:.11f}] 价格: {skin.sell_min_price}",
|
||||
"开箱",
|
||||
session=session,
|
||||
)
|
||||
log_list.append(
|
||||
OpenCasesLog(
|
||||
user_id=user_id,
|
||||
group_id=group_id,
|
||||
case_name=case_name,
|
||||
name=skin.name,
|
||||
skin_name=skin.skin_name,
|
||||
is_stattrak=skin.is_stattrak,
|
||||
abrasion=skin.abrasion,
|
||||
color=skin.color,
|
||||
price=skin.sell_min_price,
|
||||
abrasion_value=rand,
|
||||
create_time=now,
|
||||
)
|
||||
)
|
||||
await user.save()
|
||||
if log_list:
|
||||
await OpenCasesLog.bulk_create(log_list, 10)
|
||||
logger.debug(f"添加 {len(log_list)} 条开箱日志", "开箱", session=session)
|
||||
img_w += 10
|
||||
img_h += 10
|
||||
w = img_w * 5
|
||||
if num < 5:
|
||||
h = img_h - 10
|
||||
w = img_w * num
|
||||
elif not num % 5:
|
||||
h = img_h * int(num / 5)
|
||||
else:
|
||||
h = img_h * int(num / 5) + img_h
|
||||
mark_image = BuildImage(w - 10, h - 10, color=(255, 255, 255))
|
||||
mark_image = await mark_image.auto_paste(img_list, 5, padding=20)
|
||||
over_count = max_count - user.today_open_total
|
||||
result = ""
|
||||
for color_name in skin_count:
|
||||
result += f"[{color_name}:{skin_count[color_name]}] "
|
||||
return MessageUtils.build_message(
|
||||
[
|
||||
f"开启{case_name}武器箱\n剩余开箱次数:{over_count}\n",
|
||||
mark_image,
|
||||
f"\n{result[:-1]}\n箱子单价:{case_price}\n总获取金额:{total_price:.2f}\n总花费:{(17 + case_price) * num:.2f}",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def total_open_statistics(user_id: str, group_id: str) -> str:
|
||||
user, _ = await OpenCasesUser.get_or_create(user_id=user_id, group_id=group_id)
|
||||
return (
|
||||
f"开箱总数:{user.total_count}\n"
|
||||
f"今日开箱:{user.today_open_total}\n"
|
||||
f"蓝色军规:{user.blue_count}\n"
|
||||
f"蓝色暗金:{user.blue_st_count}\n"
|
||||
f"紫色受限:{user.purple_count}\n"
|
||||
f"紫色暗金:{user.purple_st_count}\n"
|
||||
f"粉色保密:{user.pink_count}\n"
|
||||
f"粉色暗金:{user.pink_st_count}\n"
|
||||
f"红色隐秘:{user.red_count}\n"
|
||||
f"红色暗金:{user.red_st_count}\n"
|
||||
f"金色罕见:{user.knife_count}\n"
|
||||
f"金色暗金:{user.knife_st_count}\n"
|
||||
f"花费金额:{user.spend_money}\n"
|
||||
f"获取金额:{user.make_money:.2f}\n"
|
||||
f"最后开箱日期:{user.open_cases_time_last.date()}"
|
||||
)
|
||||
|
||||
|
||||
async def group_statistics(group_id: str):
|
||||
user_list = await OpenCasesUser.filter(group_id=str(group_id)).all()
|
||||
# lan zi fen hong jin pricei
|
||||
uplist = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0, 0]
|
||||
for user in user_list:
|
||||
uplist[0] += user.blue_count
|
||||
uplist[1] += user.blue_st_count
|
||||
uplist[2] += user.purple_count
|
||||
uplist[3] += user.purple_st_count
|
||||
uplist[4] += user.pink_count
|
||||
uplist[5] += user.pink_st_count
|
||||
uplist[6] += user.red_count
|
||||
uplist[7] += user.red_st_count
|
||||
uplist[8] += user.knife_count
|
||||
uplist[9] += user.knife_st_count
|
||||
uplist[10] += user.make_money
|
||||
uplist[11] += user.total_count
|
||||
uplist[12] += user.today_open_total
|
||||
return (
|
||||
f"群开箱总数:{uplist[11]}\n"
|
||||
f"群今日开箱:{uplist[12]}\n"
|
||||
f"蓝色军规:{uplist[0]}\n"
|
||||
f"蓝色暗金:{uplist[1]}\n"
|
||||
f"紫色受限:{uplist[2]}\n"
|
||||
f"紫色暗金:{uplist[3]}\n"
|
||||
f"粉色保密:{uplist[4]}\n"
|
||||
f"粉色暗金:{uplist[5]}\n"
|
||||
f"红色隐秘:{uplist[6]}\n"
|
||||
f"红色暗金:{uplist[7]}\n"
|
||||
f"金色罕见:{uplist[8]}\n"
|
||||
f"金色暗金:{uplist[9]}\n"
|
||||
f"花费金额:{uplist[11] * 17}\n"
|
||||
f"获取金额:{uplist[10]:.2f}"
|
||||
)
|
||||
|
||||
|
||||
async def get_my_knifes(user_id: str, group_id: str) -> UniMessage:
|
||||
"""获取我的金色
|
||||
|
||||
参数:
|
||||
user_id (str): 用户id
|
||||
group_id (str): 群号
|
||||
|
||||
返回:
|
||||
MessageFactory: 回复消息或图片
|
||||
"""
|
||||
data_list = await get_old_knife(str(user_id), str(group_id))
|
||||
data_list += await OpenCasesLog.filter(
|
||||
user_id=user_id, group_id=group_id, color="KNIFE"
|
||||
).all()
|
||||
if not data_list:
|
||||
return MessageUtils.build_message("您木有开出金色级别的皮肤喔...")
|
||||
length = len(data_list)
|
||||
if length < 5:
|
||||
h = 600
|
||||
w = length * 540
|
||||
elif length % 5 == 0:
|
||||
h = 600 * int(length / 5)
|
||||
w = 540 * 5
|
||||
else:
|
||||
h = 600 * int(length / 5) + 600
|
||||
w = 540 * 5
|
||||
A = BuildImage(w, h)
|
||||
image_list = []
|
||||
for skin in data_list:
|
||||
name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion
|
||||
img_path = (
|
||||
IMAGE_PATH / "csgo_cases" / cn2py(skin.case_name) / f"{cn2py(name)}.jpg"
|
||||
)
|
||||
knife_img = BuildImage(470, 600, font_size=20)
|
||||
await knife_img.paste(
|
||||
BuildImage(470, 470, background=img_path if img_path.exists() else None),
|
||||
(0, 0),
|
||||
)
|
||||
await knife_img.text(
|
||||
(5, 500), f"\t{skin.name}|{skin.skin_name}({skin.abrasion})"
|
||||
)
|
||||
await knife_img.text((5, 530), f"\t磨损:{skin.abrasion_value}")
|
||||
await knife_img.text((5, 560), f"\t价格:{skin.price}")
|
||||
image_list.append(knife_img)
|
||||
A = await A.auto_paste(image_list, 5)
|
||||
return MessageUtils.build_message(A)
|
||||
|
||||
|
||||
async def get_old_knife(user_id: str, group_id: str) -> list[OpenCasesLog]:
|
||||
"""获取旧数据字段
|
||||
|
||||
参数:
|
||||
user_id (str): 用户id
|
||||
group_id (str): 群号
|
||||
|
||||
返回:
|
||||
list[OpenCasesLog]: 旧数据兼容
|
||||
"""
|
||||
user, _ = await OpenCasesUser.get_or_create(user_id=user_id, group_id=group_id)
|
||||
knifes_name = user.knifes_name
|
||||
data_list = []
|
||||
if knifes_name:
|
||||
knifes_list = knifes_name[:-1].split(",")
|
||||
for knife in knifes_list:
|
||||
try:
|
||||
if r := re.search(
|
||||
"(.*)\|\|(.*) \| (.*)\((.*)\) 磨损:(.*), 价格:(.*)", knife
|
||||
):
|
||||
case_name_py = r.group(1)
|
||||
name = r.group(2)
|
||||
skin_name = r.group(3)
|
||||
abrasion = r.group(4)
|
||||
abrasion_value = r.group(5)
|
||||
price = r.group(6)
|
||||
name = name.replace("(StatTrak™)", "")
|
||||
data_list.append(
|
||||
OpenCasesLog(
|
||||
user_id=user_id,
|
||||
group_id=group_id,
|
||||
name=name.strip(),
|
||||
case_name=case_name_py.strip(),
|
||||
skin_name=skin_name.strip(),
|
||||
abrasion=abrasion.strip(),
|
||||
abrasion_value=abrasion_value,
|
||||
price=price,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"获取兼容旧数据错误: {knife}",
|
||||
"我的金色",
|
||||
session=user_id,
|
||||
group_id=group_id,
|
||||
e=e,
|
||||
)
|
||||
return data_list
|
||||
|
||||
|
||||
async def auto_update():
|
||||
"""自动更新武器箱"""
|
||||
if case_list := Config.get_config("open_cases", "DAILY_UPDATE"):
|
||||
logger.debug("尝试自动更新武器箱", "更新武器箱")
|
||||
if "ALL" in case_list:
|
||||
case_list = CASE2ID.keys()
|
||||
logger.debug(f"预计自动更新武器箱 {len(case_list)} 个", "更新武器箱")
|
||||
for case_name in case_list:
|
||||
logger.debug(f"开始自动更新武器箱: {case_name}", "更新武器箱")
|
||||
try:
|
||||
await update_skin_data(case_name)
|
||||
rand = random.randint(300, 500)
|
||||
logger.info(
|
||||
f"成功自动更新武器箱: {case_name}, 将在 {rand} 秒后再次更新下一武器箱",
|
||||
"更新武器箱",
|
||||
)
|
||||
await asyncio.sleep(rand)
|
||||
except Exception as e:
|
||||
logger.error(f"自动更新武器箱: {case_name}", e=e)
|
||||
@@ -1,657 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import nonebot
|
||||
from tortoise.functions import Count
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.image_utils import BuildImage, BuildMat, MatType
|
||||
from zhenxun.utils.utils import cn2py
|
||||
|
||||
from .build_image import generate_skin
|
||||
from .config import (
|
||||
CASE2ID,
|
||||
CASE_BACKGROUND,
|
||||
COLOR2NAME,
|
||||
KNIFE2ID,
|
||||
NAME2COLOR,
|
||||
UpdateType,
|
||||
)
|
||||
from .models.buff_skin import BuffSkin
|
||||
from .models.buff_skin_log import BuffSkinLog
|
||||
from .models.open_cases_user import OpenCasesUser
|
||||
|
||||
# from zhenxun.utils.utils import broadcast_group, cn2py
|
||||
|
||||
|
||||
URL = "https://buff.163.com/api/market/goods"
|
||||
|
||||
SELL_URL = "https://buff.163.com/goods"
|
||||
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
|
||||
BASE_PATH = IMAGE_PATH / "csgo_cases"
|
||||
|
||||
|
||||
class CaseManager:
|
||||
|
||||
CURRENT_CASES = []
|
||||
|
||||
@classmethod
|
||||
async def reload(cls):
|
||||
cls.CURRENT_CASES = []
|
||||
case_list = await BuffSkin.filter(color="CASE").values_list(
|
||||
"case_name", flat=True
|
||||
)
|
||||
for case_name in (
|
||||
await BuffSkin.filter(case_name__not="未知武器箱")
|
||||
.annotate()
|
||||
.distinct()
|
||||
.values_list("case_name", flat=True)
|
||||
):
|
||||
for name in case_name.split(","): # type: ignore
|
||||
if name not in cls.CURRENT_CASES and name in case_list:
|
||||
cls.CURRENT_CASES.append(name)
|
||||
|
||||
|
||||
async def update_skin_data(name: str, is_update_case_name: bool = False) -> str:
|
||||
"""更新箱子内皮肤数据
|
||||
|
||||
参数:
|
||||
name (str): 箱子名称
|
||||
is_update_case_name (bool): 是否必定更新所属箱子
|
||||
|
||||
返回:
|
||||
str: 回复内容
|
||||
"""
|
||||
type_ = None
|
||||
if name in CASE2ID:
|
||||
type_ = UpdateType.CASE
|
||||
if name in KNIFE2ID:
|
||||
type_ = UpdateType.WEAPON_TYPE
|
||||
if not type_:
|
||||
return "未在指定武器箱或指定武器类型内"
|
||||
session = Config.get_config("open_cases", "COOKIE")
|
||||
if not session:
|
||||
return "BUFF COOKIE为空捏!"
|
||||
weapon2case = {}
|
||||
if type_ == UpdateType.WEAPON_TYPE:
|
||||
db_data = await BuffSkin.filter(name__contains=name).all()
|
||||
weapon2case = {
|
||||
item.name + item.skin_name: item.case_name
|
||||
for item in db_data
|
||||
if item.case_name != "未知武器箱"
|
||||
}
|
||||
data_list, total = await search_skin_page(name, 1, type_)
|
||||
if isinstance(data_list, str):
|
||||
return data_list
|
||||
for page in range(2, total + 1):
|
||||
rand_time = random.randint(20, 50)
|
||||
logger.debug(f"访问随机等待时间: {rand_time}", "开箱更新")
|
||||
await asyncio.sleep(rand_time)
|
||||
data_list_, total = await search_skin_page(name, page, type_)
|
||||
if isinstance(data_list_, list):
|
||||
data_list += data_list_
|
||||
create_list: list[BuffSkin] = []
|
||||
update_list: list[BuffSkin] = []
|
||||
log_list = []
|
||||
now = datetime.now()
|
||||
exists_id_list = []
|
||||
new_weapon2case = {}
|
||||
for skin in data_list:
|
||||
if skin.skin_id in exists_id_list:
|
||||
continue
|
||||
if skin.case_name:
|
||||
skin.case_name = (
|
||||
skin.case_name.replace("”", "")
|
||||
.replace("“", "")
|
||||
.replace("武器箱", "")
|
||||
.replace(" ", "")
|
||||
)
|
||||
skin.name = skin.name.replace("(★ StatTrak™)", "").replace("(★)", "")
|
||||
exists_id_list.append(skin.skin_id)
|
||||
key = skin.name + skin.skin_name
|
||||
name_ = skin.name + skin.skin_name + skin.abrasion
|
||||
skin.create_time = now
|
||||
skin.update_time = now
|
||||
if UpdateType.WEAPON_TYPE and not skin.case_name:
|
||||
if is_update_case_name:
|
||||
case_name = new_weapon2case.get(key)
|
||||
else:
|
||||
case_name = weapon2case.get(key)
|
||||
if not case_name:
|
||||
if case_list := await get_skin_case(skin.skin_id):
|
||||
case_name = ",".join(case_list)
|
||||
rand = random.randint(10, 20)
|
||||
logger.debug(
|
||||
f"获取 {skin.name} | {skin.skin_name} 皮肤所属武器箱: {case_name}, 访问随机等待时间: {rand}",
|
||||
"开箱更新",
|
||||
)
|
||||
await asyncio.sleep(rand)
|
||||
if not case_name:
|
||||
case_name = "未知武器箱"
|
||||
else:
|
||||
weapon2case[key] = case_name
|
||||
new_weapon2case[key] = case_name
|
||||
if skin.case_name == "反恐精英20周年":
|
||||
skin.case_name = "CS20"
|
||||
skin.case_name = case_name
|
||||
if await BuffSkin.exists(skin_id=skin.skin_id):
|
||||
update_list.append(skin)
|
||||
else:
|
||||
create_list.append(skin)
|
||||
log_list.append(
|
||||
BuffSkinLog(
|
||||
name=skin.name,
|
||||
case_name=skin.case_name,
|
||||
skin_name=skin.skin_name,
|
||||
is_stattrak=skin.is_stattrak,
|
||||
abrasion=skin.abrasion,
|
||||
color=skin.color,
|
||||
steam_price=skin.steam_price,
|
||||
weapon_type=skin.weapon_type,
|
||||
buy_max_price=skin.buy_max_price,
|
||||
buy_num=skin.buy_num,
|
||||
sell_min_price=skin.sell_min_price,
|
||||
sell_num=skin.sell_num,
|
||||
sell_reference_price=skin.sell_reference_price,
|
||||
create_time=now,
|
||||
)
|
||||
)
|
||||
name_ = skin.name + "-" + skin.skin_name + "-" + skin.abrasion
|
||||
for c_name_ in skin.case_name.split(","):
|
||||
file_path = BASE_PATH / cn2py(c_name_) / f"{cn2py(name_)}.jpg"
|
||||
if not file_path.exists():
|
||||
logger.debug(f"下载皮肤 {name} 图片: {skin.img_url}...", "开箱更新")
|
||||
await AsyncHttpx.download_file(skin.img_url, file_path)
|
||||
rand_time = random.randint(1, 10)
|
||||
await asyncio.sleep(rand_time)
|
||||
logger.debug(f"图片下载随机等待时间: {rand_time}", "开箱更新")
|
||||
else:
|
||||
logger.debug(f"皮肤 {name_} 图片已存在...", "开箱更新")
|
||||
if create_list:
|
||||
logger.debug(
|
||||
f"更新武器箱/皮肤: [<u><e>{name}</e></u>], 创建 {len(create_list)} 个皮肤!"
|
||||
)
|
||||
await BuffSkin.bulk_create(set(create_list), 10)
|
||||
if update_list:
|
||||
abrasion_list = []
|
||||
name_list = []
|
||||
skin_name_list = []
|
||||
for skin in update_list:
|
||||
if skin.abrasion not in abrasion_list:
|
||||
abrasion_list.append(skin.abrasion)
|
||||
if skin.name not in name_list:
|
||||
name_list.append(skin.name)
|
||||
if skin.skin_name not in skin_name_list:
|
||||
skin_name_list.append(skin.skin_name)
|
||||
db_data = await BuffSkin.filter(
|
||||
case_name__contains=name,
|
||||
skin_name__in=skin_name_list,
|
||||
name__in=name_list,
|
||||
abrasion__in=abrasion_list,
|
||||
).all()
|
||||
_update_list = []
|
||||
for data in db_data:
|
||||
for skin in update_list:
|
||||
if (
|
||||
data.name == skin.name
|
||||
and data.skin_name == skin.skin_name
|
||||
and data.abrasion == skin.abrasion
|
||||
):
|
||||
data.steam_price = skin.steam_price
|
||||
data.buy_max_price = skin.buy_max_price
|
||||
data.buy_num = skin.buy_num
|
||||
data.sell_min_price = skin.sell_min_price
|
||||
data.sell_num = skin.sell_num
|
||||
data.sell_reference_price = skin.sell_reference_price
|
||||
data.update_time = skin.update_time
|
||||
_update_list.append(data)
|
||||
logger.debug(
|
||||
f"更新武器箱/皮肤: [<u><c>{name}</c></u>], 更新 {len(create_list)} 个皮肤!"
|
||||
)
|
||||
await BuffSkin.bulk_update(
|
||||
_update_list,
|
||||
[
|
||||
"steam_price",
|
||||
"buy_max_price",
|
||||
"buy_num",
|
||||
"sell_min_price",
|
||||
"sell_num",
|
||||
"sell_reference_price",
|
||||
"update_time",
|
||||
],
|
||||
10,
|
||||
)
|
||||
if log_list:
|
||||
logger.debug(
|
||||
f"更新武器箱/皮肤: [<u><e>{name}</e></u>], 新增 {len(log_list)} 条皮肤日志!"
|
||||
)
|
||||
await BuffSkinLog.bulk_create(log_list)
|
||||
if name not in CaseManager.CURRENT_CASES:
|
||||
CaseManager.CURRENT_CASES.append(name) # type: ignore
|
||||
return f"更新武器箱/皮肤: [{name}] 成功, 共更新 {len(update_list)} 个皮肤, 新创建 {len(create_list)} 个皮肤!"
|
||||
|
||||
|
||||
async def search_skin_page(
|
||||
s_name: str, page_index: int, type_: UpdateType
|
||||
) -> tuple[list[BuffSkin] | str, int]:
|
||||
"""查询箱子皮肤
|
||||
|
||||
参数:
|
||||
s_name (str): 箱子/皮肤名称
|
||||
page_index (int): 页数
|
||||
|
||||
返回:
|
||||
tuple[list[BuffSkin] | str, int]: BuffSkin
|
||||
"""
|
||||
logger.debug(
|
||||
f"尝试访问武器箱/皮肤: [<u><e>{s_name}</e></u>] 页数: [<u><y>{page_index}</y></u>]",
|
||||
"开箱更新",
|
||||
)
|
||||
cookie = {"session": Config.get_config("open_cases", "COOKIE")}
|
||||
params = {
|
||||
"game": "csgo",
|
||||
"page_num": page_index,
|
||||
"page_size": 80,
|
||||
"_": time.time(),
|
||||
"use_suggestio": 0,
|
||||
}
|
||||
if type_ == UpdateType.CASE:
|
||||
params["itemset"] = CASE2ID[s_name]
|
||||
elif type_ == UpdateType.WEAPON_TYPE:
|
||||
params["category"] = KNIFE2ID[s_name]
|
||||
proxy = None
|
||||
if ip := Config.get_config("open_cases", "BUFF_PROXY"):
|
||||
proxy = {"http://": ip, "https://": ip}
|
||||
response = None
|
||||
error = ""
|
||||
for i in range(3):
|
||||
try:
|
||||
response = await AsyncHttpx.get(
|
||||
URL,
|
||||
proxy=proxy,
|
||||
params=params,
|
||||
cookies=cookie, # type: ignore
|
||||
)
|
||||
if response.status_code == 200:
|
||||
break
|
||||
rand = random.randint(3, 7)
|
||||
logger.debug(
|
||||
f"尝试访问武器箱/皮肤第 {i+1} 次访问异常, code: {response.status_code}",
|
||||
"开箱更新",
|
||||
)
|
||||
await asyncio.sleep(rand)
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
f"尝试访问武器箱/皮肤第 {i+1} 次访问发生错误 {type(e)}: {e}", "开箱更新"
|
||||
)
|
||||
error = f"{type(e)}: {e}"
|
||||
if not response:
|
||||
return f"访问发生异常: {error}", -1
|
||||
if response.status_code == 200:
|
||||
# logger.debug(f"访问BUFF API: {response.text}", "开箱更新")
|
||||
json_data = response.json()
|
||||
update_data = []
|
||||
if json_data["code"] == "OK":
|
||||
data_list = json_data["data"]["items"]
|
||||
for data in data_list:
|
||||
obj = {}
|
||||
if type_ == UpdateType.CASE:
|
||||
obj["case_name"] = s_name
|
||||
name = data["name"]
|
||||
try:
|
||||
logger.debug(
|
||||
f"武器箱: [<u><e>{s_name}</e></u>] 页数: [<u><y>{page_index}</y></u>] 正在收录皮肤: [<u><c>{name}</c></u>]...",
|
||||
"开箱更新",
|
||||
)
|
||||
obj["skin_id"] = str(data["id"])
|
||||
obj["buy_max_price"] = data["buy_max_price"] # 求购最大金额
|
||||
obj["buy_num"] = data["buy_num"] # 当前求购
|
||||
goods_info = data["goods_info"]
|
||||
info = goods_info["info"]
|
||||
tags = info["tags"]
|
||||
obj["weapon_type"] = tags["type"]["localized_name"] # 枪械类型
|
||||
if obj["weapon_type"] in ["音乐盒", "印花", "探员"]:
|
||||
continue
|
||||
elif obj["weapon_type"] in ["匕首", "手套"]:
|
||||
obj["color"] = "KNIFE"
|
||||
obj["name"] = data["short_name"].split("(")[0].strip() # 名称
|
||||
elif obj["weapon_type"] in ["武器箱"]:
|
||||
obj["color"] = "CASE"
|
||||
obj["name"] = data["short_name"]
|
||||
else:
|
||||
obj["color"] = NAME2COLOR[tags["rarity"]["localized_name"]]
|
||||
obj["name"] = tags["weapon"]["localized_name"] # 名称
|
||||
if obj["weapon_type"] not in ["武器箱"]:
|
||||
obj["abrasion"] = tags["exterior"]["localized_name"] # 磨损
|
||||
obj["is_stattrak"] = "StatTrak" in tags["quality"]["localized_name"] # type: ignore # 是否暗金
|
||||
if not obj["color"]:
|
||||
obj["color"] = NAME2COLOR[
|
||||
tags["rarity"]["localized_name"]
|
||||
] # 品质颜色
|
||||
else:
|
||||
obj["abrasion"] = "CASE"
|
||||
obj["skin_name"] = (
|
||||
data["short_name"].split("|")[-1].strip()
|
||||
) # 皮肤名称
|
||||
obj["img_url"] = goods_info["original_icon_url"] # 图片url
|
||||
obj["steam_price"] = goods_info["steam_price_cny"] # steam价格
|
||||
obj["sell_min_price"] = data["sell_min_price"] # 售卖最低价格
|
||||
obj["sell_num"] = data["sell_num"] # 售卖数量
|
||||
obj["sell_reference_price"] = data[
|
||||
"sell_reference_price"
|
||||
] # 参考价格
|
||||
update_data.append(BuffSkin(**obj))
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"更新武器箱: [<u><e>{s_name}</e></u>] 皮肤: [<u><c>{s_name}</c></u>] 错误",
|
||||
e=e,
|
||||
)
|
||||
logger.debug(
|
||||
f"访问武器箱: [<u><e>{s_name}</e></u>] 页数: [<u><y>{page_index}</y></u>] 成功并收录完成",
|
||||
"开箱更新",
|
||||
)
|
||||
return update_data, json_data["data"]["total_page"]
|
||||
else:
|
||||
logger.warning(f'访问BUFF失败: {json_data["error"]}')
|
||||
return f'访问失败: {json_data["error"]}', -1
|
||||
return f"访问失败, 状态码: {response.status_code}", -1
|
||||
|
||||
|
||||
async def build_case_image(case_name: str | None) -> BuildImage | str:
|
||||
"""构造武器箱图片
|
||||
|
||||
参数:
|
||||
case_name (str): 名称
|
||||
|
||||
返回:
|
||||
BuildImage | str: 图片
|
||||
"""
|
||||
background = random.choice(os.listdir(CASE_BACKGROUND))
|
||||
background_img = BuildImage(0, 0, background=CASE_BACKGROUND / background)
|
||||
if case_name:
|
||||
log_list = (
|
||||
await BuffSkinLog.filter(case_name__contains=case_name)
|
||||
.annotate(count=Count("id"))
|
||||
.group_by("skin_name")
|
||||
.values_list("skin_name", "count")
|
||||
)
|
||||
skin_list_ = await BuffSkin.filter(case_name__contains=case_name).all()
|
||||
skin2count = {item[0]: item[1] for item in log_list}
|
||||
case = None
|
||||
skin_list: list[BuffSkin] = []
|
||||
exists_name = []
|
||||
for skin in skin_list_:
|
||||
if skin.color == "CASE":
|
||||
case = skin
|
||||
else:
|
||||
name = skin.name + skin.skin_name
|
||||
if name not in exists_name:
|
||||
skin_list.append(skin)
|
||||
exists_name.append(name)
|
||||
generate_img = {}
|
||||
for skin in skin_list:
|
||||
skin_img = await generate_skin(skin, skin2count.get(skin.skin_name, 0))
|
||||
if skin_img:
|
||||
if not generate_img.get(skin.color):
|
||||
generate_img[skin.color] = []
|
||||
generate_img[skin.color].append(skin_img)
|
||||
skin_image_list = []
|
||||
for color in COLOR2NAME:
|
||||
if generate_img.get(color):
|
||||
skin_image_list = skin_image_list + generate_img[color]
|
||||
img = skin_image_list[0]
|
||||
img_w, img_h = img.size
|
||||
total_size = (img_w + 25) * (img_h + 10) * len(skin_image_list) # 总面积
|
||||
new_size = get_bk_image_size(total_size, background_img.size, img.size, 250)
|
||||
A = BuildImage(
|
||||
new_size[0] + 50, new_size[1], background=CASE_BACKGROUND / background
|
||||
)
|
||||
await A.filter("GaussianBlur", 2)
|
||||
if case:
|
||||
case_img = await generate_skin(
|
||||
case, skin2count.get(f"{case_name}武器箱", 0)
|
||||
)
|
||||
if case_img:
|
||||
await A.paste(case_img, (25, 25))
|
||||
w = 25
|
||||
h = 230
|
||||
skin_image_list.reverse()
|
||||
for image in skin_image_list:
|
||||
await A.paste(image, (w, h))
|
||||
w += image.width + 20
|
||||
if w + image.width - 25 > A.width:
|
||||
h += image.height + 10
|
||||
w = 25
|
||||
if h + img_h + 100 < A.height:
|
||||
await A.crop((0, 0, A.width, h + img_h + 100))
|
||||
return A
|
||||
else:
|
||||
log_list = (
|
||||
await BuffSkinLog.filter(color="CASE")
|
||||
.annotate(count=Count("id"))
|
||||
.group_by("case_name")
|
||||
.values_list("case_name", "count")
|
||||
)
|
||||
name2count = {item[0]: item[1] for item in log_list}
|
||||
skin_list = await BuffSkin.filter(color="CASE").all()
|
||||
image_list: list[BuildImage] = []
|
||||
for skin in skin_list:
|
||||
if img := await generate_skin(skin, name2count[skin.case_name]):
|
||||
image_list.append(img)
|
||||
if not image_list:
|
||||
return "未收录武器箱"
|
||||
w = 25
|
||||
h = 150
|
||||
img = image_list[0]
|
||||
img_w, img_h = img.size
|
||||
total_size = (img_w + 25) * (img_h + 10) * len(image_list) # 总面积
|
||||
|
||||
new_size = get_bk_image_size(total_size, background_img.size, img.size, 155)
|
||||
A = BuildImage(
|
||||
new_size[0] + 50, new_size[1], background=CASE_BACKGROUND / background
|
||||
)
|
||||
await A.filter("GaussianBlur", 2)
|
||||
bk_img = BuildImage(
|
||||
img_w, 120, color=(25, 25, 25, 100), font_size=60, font="CJGaoDeGuo.otf"
|
||||
)
|
||||
await bk_img.text(
|
||||
(0, 0),
|
||||
f"已收录 {len(image_list)} 个武器箱",
|
||||
(255, 255, 255),
|
||||
center_type="center",
|
||||
)
|
||||
await A.paste(bk_img, (10, 10), "width")
|
||||
for image in image_list:
|
||||
await A.paste(image, (w, h))
|
||||
w += image.width + 20
|
||||
if w + image.width - 25 > A.width:
|
||||
h += image.height + 10
|
||||
w = 25
|
||||
if h + img_h + 100 < A.height:
|
||||
await A.crop((0, 0, A.width, h + img_h + 100))
|
||||
return A
|
||||
|
||||
|
||||
def get_bk_image_size(
|
||||
total_size: int,
|
||||
base_size: tuple[int, int],
|
||||
img_size: tuple[int, int],
|
||||
extra_height: int = 0,
|
||||
) -> tuple[int, int]:
|
||||
"""获取所需背景大小且不改变图片长宽比
|
||||
|
||||
参数:
|
||||
total_size (int): 总面积
|
||||
base_size (Tuple[int, int]): 初始背景大小
|
||||
img_size (Tuple[int, int]): 贴图大小
|
||||
|
||||
返回:
|
||||
tuple[int, int]: 满足所有贴图大小
|
||||
"""
|
||||
bk_w, bk_h = base_size
|
||||
img_w, img_h = img_size
|
||||
is_add_title_size = False
|
||||
left_dis = 0
|
||||
right_dis = 0
|
||||
old_size = (0, 0)
|
||||
new_size = (0, 0)
|
||||
ratio = 1.1
|
||||
while 1:
|
||||
w_ = int(ratio * bk_w)
|
||||
h_ = int(ratio * bk_h)
|
||||
size = w_ * h_
|
||||
if size < total_size:
|
||||
left_dis = size
|
||||
else:
|
||||
right_dis = size
|
||||
r = w_ / (img_w + 25)
|
||||
if right_dis and r - int(r) < 0.1:
|
||||
if not is_add_title_size and extra_height:
|
||||
total_size = int(total_size + w_ * extra_height)
|
||||
is_add_title_size = True
|
||||
right_dis = 0
|
||||
continue
|
||||
if total_size - left_dis > right_dis - total_size:
|
||||
new_size = (w_, h_)
|
||||
else:
|
||||
new_size = old_size
|
||||
break
|
||||
old_size = (w_, h_)
|
||||
ratio += 0.1
|
||||
return new_size
|
||||
|
||||
|
||||
async def get_skin_case(id_: str) -> list[str] | None:
|
||||
"""获取皮肤所在箱子
|
||||
|
||||
参数:
|
||||
id_ (str): 皮肤id
|
||||
|
||||
返回:
|
||||
list[str] | None: 武器箱名称
|
||||
"""
|
||||
url = f"{SELL_URL}/{id_}"
|
||||
proxy = None
|
||||
if ip := Config.get_config("open_cases", "BUFF_PROXY"):
|
||||
proxy = {"http://": ip, "https://": ip}
|
||||
response = await AsyncHttpx.get(
|
||||
url,
|
||||
proxy=proxy,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
text = response.text
|
||||
if r := re.search('<meta name="description"(.*?)>', text):
|
||||
case_list = []
|
||||
for s in r.group(1).split(","):
|
||||
if "武器箱" in s:
|
||||
case_list.append(
|
||||
s.replace("”", "")
|
||||
.replace("“", "")
|
||||
.replace('"', "")
|
||||
.replace("'", "")
|
||||
.replace("武器箱", "")
|
||||
.replace(" ", "")
|
||||
)
|
||||
return case_list
|
||||
else:
|
||||
logger.debug(f"访问皮肤所属武器箱异常 url: {url} code: {response.status_code}")
|
||||
return None
|
||||
|
||||
|
||||
async def init_skin_trends(
|
||||
name: str, skin: str, abrasion: str, day: int = 7
|
||||
) -> BuildImage | None:
|
||||
date = datetime.now() - timedelta(days=day)
|
||||
log_list = (
|
||||
await BuffSkinLog.filter(
|
||||
name__contains=name.upper(),
|
||||
skin_name=skin,
|
||||
abrasion__contains=abrasion,
|
||||
create_time__gt=date,
|
||||
is_stattrak=False,
|
||||
)
|
||||
.order_by("create_time")
|
||||
.limit(day * 5)
|
||||
.all()
|
||||
)
|
||||
if not log_list:
|
||||
return None
|
||||
date_list = []
|
||||
price_list = []
|
||||
for log in log_list:
|
||||
date = str(log.create_time.date())
|
||||
if date not in date_list:
|
||||
date_list.append(date)
|
||||
price_list.append(log.sell_min_price)
|
||||
graph = BuildMat(MatType.LINE)
|
||||
graph.data = price_list
|
||||
graph.title = f"{name}({skin})价格趋势({day})"
|
||||
graph.x_index = date_list
|
||||
return await graph.build()
|
||||
|
||||
|
||||
async def reset_count_daily():
|
||||
"""
|
||||
重置每日开箱
|
||||
"""
|
||||
try:
|
||||
await OpenCasesUser.all().update(today_open_total=0)
|
||||
# await broadcast_group(
|
||||
# "[[_task|open_case_reset_remind]]今日开箱次数重置成功",
|
||||
# log_cmd="开箱重置提醒",
|
||||
# )
|
||||
except Exception as e:
|
||||
logger.error(f"开箱重置错误", e=e)
|
||||
|
||||
|
||||
async def download_image(case_name: str | None = None):
|
||||
"""下载皮肤图片
|
||||
|
||||
参数:
|
||||
case_name: 箱子名称.
|
||||
"""
|
||||
skin_list = (
|
||||
await BuffSkin.filter(case_name=case_name).all()
|
||||
if case_name
|
||||
else await BuffSkin.all()
|
||||
)
|
||||
for skin in skin_list:
|
||||
name_ = skin.name + "-" + skin.skin_name + "-" + skin.abrasion
|
||||
for c_name_ in skin.case_name.split(","):
|
||||
try:
|
||||
pass
|
||||
# file_path = BASE_PATH / cn2py(c_name_) / f"{cn2py(name_)}.jpg"
|
||||
# if not file_path.exists():
|
||||
# logger.debug(
|
||||
# f"下载皮肤 {c_name_}/{skin.name} 图片: {skin.img_url}...",
|
||||
# "开箱图片更新",
|
||||
# )
|
||||
# await AsyncHttpx.download_file(skin.img_url, file_path)
|
||||
# rand_time = random.randint(1, 5)
|
||||
# await asyncio.sleep(rand_time)
|
||||
# logger.debug(f"图片下载随机等待时间: {rand_time}", "开箱图片更新")
|
||||
# else:
|
||||
# logger.debug(
|
||||
# f"皮肤 {c_name_}/{skin.name} 图片已存在...", "开箱图片更新"
|
||||
# )
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"下载皮肤 {c_name_}/{skin.name} 图片: {skin.img_url}",
|
||||
"开箱图片更新",
|
||||
e=e,
|
||||
)
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
async def _():
|
||||
await CaseManager.reload()
|
||||
@@ -1,186 +0,0 @@
|
||||
import re
|
||||
import time
|
||||
|
||||
import ujson as json
|
||||
from nonebot import on_message
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Hyper, Image, UniMsg
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.path_config import TEMP_PATH
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from .information_container import InformationContainer
|
||||
from .parse_url import parse_bili_url
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="B站内容解析",
|
||||
description="B站内容解析",
|
||||
usage="""
|
||||
usage:
|
||||
被动监听插件,解析B站视频、直播、专栏,支持小程序卡片及文本链接,5分钟内不解析相同内容
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="leekooyo",
|
||||
version="0.1",
|
||||
plugin_type=PluginType.DEPENDANT,
|
||||
menu_type="其他",
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
module="_task",
|
||||
key="DEFAULT_BILIBILI_PARSE",
|
||||
value=True,
|
||||
default_value=True,
|
||||
help="被动 B站转发解析 进群默认开关状态",
|
||||
type=bool,
|
||||
)
|
||||
],
|
||||
tasks=[Task(module="bilibili_parse", name="b站转发解析")],
|
||||
).dict(),
|
||||
)
|
||||
|
||||
|
||||
async def _rule(session: EventSession) -> bool:
|
||||
return not await TaskInfo.is_block("bilibili_parse", session.id3 or session.id2)
|
||||
|
||||
|
||||
_matcher = on_message(priority=1, block=False, rule=_rule)
|
||||
|
||||
_tmp = {}
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(session: EventSession, message: UniMsg):
|
||||
information_container = InformationContainer()
|
||||
# 判断文本消息内容是否相关
|
||||
match = None
|
||||
# 判断文本消息和小程序的内容是否指向一个b站链接
|
||||
get_url = None
|
||||
# 判断文本消息是否包含视频相关内容
|
||||
vd_flag = False
|
||||
# 设定时间阈值,阈值之下不会解析重复内容
|
||||
repet_second = 300
|
||||
# 尝试解析小程序消息
|
||||
data = message[0]
|
||||
if isinstance(data, Hyper) and data.raw:
|
||||
try:
|
||||
data = json.loads(data.raw)
|
||||
except (IndexError, KeyError):
|
||||
data = None
|
||||
if data:
|
||||
# 获取相关数据
|
||||
meta_data = data.get("meta", {})
|
||||
news_value = meta_data.get("news", {})
|
||||
detail_1_value = meta_data.get("detail_1", {})
|
||||
qqdocurl_value = detail_1_value.get("qqdocurl", {})
|
||||
jumpUrl_value = news_value.get("jumpUrl", {})
|
||||
get_url = (qqdocurl_value if qqdocurl_value else jumpUrl_value).split("?")[
|
||||
0
|
||||
]
|
||||
# 解析文本消息
|
||||
elif msg := message.extract_plain_text():
|
||||
# 消息中含有视频号
|
||||
if "bv" in msg.lower() or "av" in msg.lower():
|
||||
match = re.search(r"((?=(?:bv|av))([A-Za-z0-9]+))", msg, re.IGNORECASE)
|
||||
vd_flag = True
|
||||
|
||||
# 消息中含有b23的链接,包括视频、专栏、动态、直播
|
||||
elif "https://b23.tv" in msg:
|
||||
match = re.search(r"https://b23\.tv/[^?\s]+", msg, re.IGNORECASE)
|
||||
|
||||
# 检查消息中是否含有直播、专栏、动态链接
|
||||
elif any(
|
||||
keyword in msg
|
||||
for keyword in [
|
||||
"https://live.bilibili.com/",
|
||||
"https://www.bilibili.com/read/",
|
||||
"https://www.bilibili.com/opus/",
|
||||
"https://t.bilibili.com/",
|
||||
]
|
||||
):
|
||||
pattern = r"https://(live|www\.bilibili\.com/read|www\.bilibili\.com/opus|t\.bilibili\.com)/[^?\s]+"
|
||||
match = re.search(pattern, msg)
|
||||
|
||||
# 匹配成功,则获取链接
|
||||
if match:
|
||||
if vd_flag:
|
||||
number = match.group(1)
|
||||
get_url = f"https://www.bilibili.com/video/{number}"
|
||||
else:
|
||||
get_url = match.group()
|
||||
|
||||
if get_url:
|
||||
# 将链接统一发送给处理函数
|
||||
data = await parse_bili_url(get_url, information_container)
|
||||
if data.vd_info:
|
||||
# 判断一定时间内是否解析重复内容,或者是第一次解析
|
||||
if (
|
||||
data.vd_url in _tmp.keys()
|
||||
and time.time() - _tmp[data.vd_url] > repet_second
|
||||
) or data.vd_url not in _tmp.keys():
|
||||
pic = data.vd_info.get("pic", "") # 封面
|
||||
aid = data.vd_info.get("aid", "") # av号
|
||||
title = data.vd_info.get("title", "") # 标题
|
||||
author = data.vd_info.get("owner", {}).get("name", "") # UP主
|
||||
reply = data.vd_info.get("stat", {}).get("reply", "") # 回复
|
||||
favorite = data.vd_info.get("stat", {}).get("favorite", "") # 收藏
|
||||
coin = data.vd_info.get("stat", {}).get("coin", "") # 投币
|
||||
like = data.vd_info.get("stat", {}).get("like", "") # 点赞
|
||||
danmuku = data.vd_info.get("stat", {}).get("danmaku", "") # 弹幕
|
||||
ctime = data.vd_info["ctime"]
|
||||
date = time.strftime("%Y-%m-%d", time.localtime(ctime))
|
||||
logger.info(
|
||||
f"解析bilibili转发 {data.vd_url}", "b站解析", session=session
|
||||
)
|
||||
_tmp[data.vd_url] = time.time()
|
||||
_path = TEMP_PATH / f"{aid}.jpg"
|
||||
await AsyncHttpx.download_file(pic, _path)
|
||||
await MessageUtils.build_message(
|
||||
[
|
||||
_path,
|
||||
f"av{aid}\n标题:{title}\nUP:{author}\n上传日期:{date}\n回复:{reply},收藏:{favorite},投币:{coin}\n点赞:{like},弹幕:{danmuku}\n{data.vd_url}",
|
||||
]
|
||||
).send()
|
||||
|
||||
elif data.live_info:
|
||||
if (
|
||||
data.live_url in _tmp.keys()
|
||||
and time.time() - _tmp[data.live_url] > repet_second
|
||||
) or data.live_url not in _tmp.keys():
|
||||
uid = data.live_info.get("uid", "") # 主播uid
|
||||
title = data.live_info.get("title", "") # 直播间标题
|
||||
description = data.live_info.get(
|
||||
"description", ""
|
||||
) # 简介,可能会出现标签
|
||||
user_cover = data.live_info.get("user_cover", "") # 封面
|
||||
keyframe = data.live_info.get("keyframe", "") # 关键帧画面
|
||||
live_time = data.live_info.get("live_time", "") # 开播时间
|
||||
area_name = data.live_info.get("area_name", "") # 分区
|
||||
parent_area_name = data.live_info.get("parent_area_name", "") # 父分区
|
||||
logger.info(
|
||||
f"解析bilibili转发 {data.live_url}", "b站解析", session=session
|
||||
)
|
||||
_tmp[data.live_url] = time.time()
|
||||
await MessageUtils.build_message(
|
||||
[
|
||||
Image(url=user_cover),
|
||||
f"开播用户:https://space.bilibili.com/{uid}\n开播时间:{live_time}\n直播分区:{parent_area_name}——>{area_name}\n标题:{title}\n简介:{description}\n直播截图:\n",
|
||||
Image(url=keyframe),
|
||||
f"{data.live_url}",
|
||||
]
|
||||
).send()
|
||||
elif data.image_info:
|
||||
if (
|
||||
data.image_url in _tmp.keys()
|
||||
and time.time() - _tmp[data.image_url] > repet_second
|
||||
) or data.image_url not in _tmp.keys():
|
||||
logger.info(
|
||||
f"解析bilibili转发 {data.image_url}", "b站解析", session=session
|
||||
)
|
||||
_tmp[data.image_url] = time.time()
|
||||
await data.image_info.send()
|
||||
@@ -1,106 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
|
||||
from zhenxun.configs.path_config import TEMP_PATH
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncPlaywright
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.user_agent import get_user_agent_str
|
||||
|
||||
|
||||
async def resize(path: Path):
|
||||
"""调整图像大小的异步函数
|
||||
|
||||
参数:
|
||||
path (str): 图像文件路径
|
||||
"""
|
||||
A = BuildImage.open(path)
|
||||
await A.resize(0.8)
|
||||
await A.save(path)
|
||||
|
||||
|
||||
async def get_image(url) -> UniMessage | None:
|
||||
"""获取Bilibili链接的截图,并返回base64格式的图片
|
||||
|
||||
参数:
|
||||
url (str): Bilibili链接
|
||||
|
||||
返回:
|
||||
Image: Image
|
||||
"""
|
||||
cv_match = None
|
||||
opus_match = None
|
||||
t_opus_match = None
|
||||
|
||||
cv_number = None
|
||||
opus_number = None
|
||||
t_opus_number = None
|
||||
|
||||
# 提取cv、opus、t_opus的编号
|
||||
url = url.split("?")[0]
|
||||
cv_match = re.search(r"read/cv([A-Za-z0-9]+)", url, re.IGNORECASE)
|
||||
opus_match = re.search(r"opus/([A-Za-z0-9]+)", url, re.IGNORECASE)
|
||||
t_opus_match = re.search(r"https://t\.bilibili\.com/(\d+)", url, re.IGNORECASE)
|
||||
|
||||
if cv_match:
|
||||
cv_number = cv_match.group(1)
|
||||
elif opus_match:
|
||||
opus_number = opus_match.group(1)
|
||||
elif t_opus_match:
|
||||
t_opus_number = t_opus_match.group(1)
|
||||
|
||||
screenshot_path = None
|
||||
|
||||
# 根据编号构建保存路径
|
||||
if cv_number:
|
||||
screenshot_path = TEMP_PATH / "bilibili_cv_{cv_number}.png"
|
||||
elif opus_number:
|
||||
screenshot_path = TEMP_PATH / "bilibili_opus_{opus_number}.png"
|
||||
elif t_opus_number:
|
||||
screenshot_path = TEMP_PATH / "bilibili_opus_{t_opus_number}.png"
|
||||
# t.bilibili.com和https://www.bilibili.com/opus在内容上是一样的,为便于维护,调整url至https://www.bilibili.com/opus/
|
||||
url = f"https://www.bilibili.com/opus/{t_opus_number}"
|
||||
if screenshot_path:
|
||||
try:
|
||||
# 如果文件不存在,进行截图
|
||||
if not screenshot_path.exists():
|
||||
# 创建页面
|
||||
try:
|
||||
async with AsyncPlaywright.new_page() as page:
|
||||
await page.set_viewport_size({"width": 5120, "height": 2560})
|
||||
# 设置请求拦截器
|
||||
await page.route(
|
||||
re.compile(r"(\.png$)|(\.jpg$)"),
|
||||
lambda route: route.abort(),
|
||||
)
|
||||
# 访问链接
|
||||
await page.goto(url, wait_until="networkidle", timeout=10000)
|
||||
# 根据不同的链接结构,设置对应的CSS选择器
|
||||
if cv_number:
|
||||
css = "#app > div"
|
||||
elif opus_number or t_opus_number:
|
||||
css = "#app > div.opus-detail > div.bili-opus-view"
|
||||
# 点击对应的元素
|
||||
await page.click(css)
|
||||
# 查询目标元素
|
||||
div = await page.query_selector(css)
|
||||
# 对目标元素进行截图
|
||||
await div.screenshot( # type: ignore
|
||||
path=screenshot_path,
|
||||
timeout=100000,
|
||||
animations="disabled",
|
||||
type="png",
|
||||
)
|
||||
# 异步执行调整截图大小的操作
|
||||
await resize(screenshot_path)
|
||||
except Exception as e:
|
||||
logger.warning(f"尝试解析bilibili转发失败", e=e)
|
||||
return None
|
||||
return MessageUtils.build_message(screenshot_path)
|
||||
except Exception as e:
|
||||
logger.error(f"尝试解析bilibili转发失败", e=e)
|
||||
return None
|
||||
@@ -1,53 +0,0 @@
|
||||
class InformationContainer:
|
||||
def __init__(
|
||||
self,
|
||||
vd_info=None,
|
||||
live_info=None,
|
||||
vd_url=None,
|
||||
live_url=None,
|
||||
image_info=None,
|
||||
image_url=None,
|
||||
):
|
||||
self._vd_info = vd_info
|
||||
self._live_info = live_info
|
||||
self._vd_url = vd_url
|
||||
self._live_url = live_url
|
||||
self._image_info = image_info
|
||||
self._image_url = image_url
|
||||
|
||||
@property
|
||||
def vd_info(self):
|
||||
return self._vd_info
|
||||
|
||||
@property
|
||||
def live_info(self):
|
||||
return self._live_info
|
||||
|
||||
@property
|
||||
def vd_url(self):
|
||||
return self._vd_url
|
||||
|
||||
@property
|
||||
def live_url(self):
|
||||
return self._live_url
|
||||
|
||||
@property
|
||||
def image_info(self):
|
||||
return self._image_info
|
||||
|
||||
@property
|
||||
def image_url(self):
|
||||
return self._image_url
|
||||
|
||||
def update(self, updates):
|
||||
"""
|
||||
更新多个信息的通用方法
|
||||
Args:
|
||||
updates (dict): 包含信息类型和对应新值的字典
|
||||
"""
|
||||
for info_type, new_value in updates.items():
|
||||
if hasattr(self, f"_{info_type}"):
|
||||
setattr(self, f"_{info_type}", new_value)
|
||||
|
||||
def get_information(self):
|
||||
return self
|
||||
@@ -1,65 +0,0 @@
|
||||
import aiohttp
|
||||
from bilireq import live, video
|
||||
|
||||
from zhenxun.utils.user_agent import get_user_agent
|
||||
|
||||
from .get_image import get_image
|
||||
from .information_container import InformationContainer
|
||||
|
||||
|
||||
async def parse_bili_url(get_url: str, information_container: InformationContainer):
|
||||
"""解析Bilibili链接,获取相关信息
|
||||
|
||||
参数:
|
||||
get_url (str): 待解析的Bilibili链接
|
||||
information_container (InformationContainer): 信息容器
|
||||
|
||||
返回:
|
||||
dict: 包含解析得到的信息的字典
|
||||
"""
|
||||
response_url = ""
|
||||
|
||||
# 去除链接末尾的斜杠
|
||||
if get_url[-1] == "/":
|
||||
get_url = get_url[:-1]
|
||||
|
||||
# 发起HTTP请求,获取重定向后的链接
|
||||
async with aiohttp.ClientSession(headers=get_user_agent()) as session:
|
||||
async with session.get(
|
||||
get_url,
|
||||
timeout=7,
|
||||
) as response:
|
||||
response_url = str(response.url).split("?")[0]
|
||||
|
||||
# 去除重定向后链接末尾的斜杠
|
||||
if response_url[-1] == "/":
|
||||
response_url = response_url[:-1]
|
||||
|
||||
# 根据不同类型的链接进行处理
|
||||
if response_url.startswith(
|
||||
("https://www.bilibili.com/video", "https://m.bilibili.com/video/")
|
||||
):
|
||||
vd_url = response_url
|
||||
vid = vd_url.split("/")[-1]
|
||||
vd_info = await video.get_video_base_info(vid)
|
||||
information_container.update({"vd_info": vd_info, "vd_url": vd_url})
|
||||
|
||||
elif response_url.startswith("https://live.bilibili.com"):
|
||||
live_url = response_url
|
||||
liveid = live_url.split("/")[-1]
|
||||
live_info = await live.get_room_info_by_id(liveid)
|
||||
information_container.update({"live_info": live_info, "live_url": live_url})
|
||||
|
||||
elif response_url.startswith("https://www.bilibili.com/read"):
|
||||
cv_url = response_url
|
||||
image_info = await get_image(cv_url)
|
||||
information_container.update({"image_info": image_info, "image_url": cv_url})
|
||||
|
||||
elif response_url.startswith(
|
||||
("https://www.bilibili.com/opus", "https://t.bilibili.com")
|
||||
):
|
||||
opus_url = response_url
|
||||
image_info = await get_image(opus_url)
|
||||
information_container.update({"image_info": image_info, "image_url": opus_url})
|
||||
|
||||
return information_container.get_information()
|
||||
@@ -1,125 +0,0 @@
|
||||
from asyncio.exceptions import TimeoutError
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.path_config import TEMP_PATH
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.utils import change_pixiv_image_links
|
||||
from zhenxun.utils.withdraw_manage import WithdrawManager
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="pid搜索",
|
||||
description="通过 pid 搜索图片",
|
||||
usage="""
|
||||
usage:
|
||||
通过 pid 搜索图片
|
||||
指令:
|
||||
p搜 [pid]
|
||||
""".strip(),
|
||||
extra=PluginExtraData(author="HibiKier", version="0.1").dict(),
|
||||
)
|
||||
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;"
|
||||
" rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
|
||||
"Referer": "https://www.pixiv.net",
|
||||
}
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna("p搜", Args["pid", str]), aliases={"P搜"}, priority=5, block=True
|
||||
)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(pid: Match[int]):
|
||||
if pid.available:
|
||||
_matcher.set_path_arg("pid", pid.result)
|
||||
|
||||
|
||||
@_matcher.got_path("pid", prompt="需要查询的图片PID是?或发送'取消'结束搜索")
|
||||
async def _(bot: Bot, session: EventSession, arparma: Arparma, pid: str):
|
||||
url = Config.get_config("hibiapi", "HIBIAPI") + "/api/pixiv/illust"
|
||||
if pid in ["取消", "算了"]:
|
||||
await Text("已取消操作...").finish()
|
||||
if not pid.isdigit():
|
||||
await Text("pid必须为数字...").finish()
|
||||
for _ in range(3):
|
||||
try:
|
||||
data = (
|
||||
await AsyncHttpx.get(
|
||||
url,
|
||||
params={"id": pid},
|
||||
timeout=5,
|
||||
)
|
||||
).json()
|
||||
except TimeoutError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"pixiv pid 搜索发生了一些错误...",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
e=e,
|
||||
)
|
||||
await MessageUtils.build_message(f"发生了一些错误..{type(e)}:{e}").finish()
|
||||
else:
|
||||
if data.get("error"):
|
||||
await MessageUtils.build_message(data["error"]["user_message"]).finish(
|
||||
reply_to=True
|
||||
)
|
||||
data = data["illust"]
|
||||
if not data["width"] and not data["height"]:
|
||||
await MessageUtils.build_message(
|
||||
f"没有搜索到 PID:{pid} 的图片"
|
||||
).finish(reply_to=True)
|
||||
pid = data["id"]
|
||||
title = data["title"]
|
||||
author = data["user"]["name"]
|
||||
author_id = data["user"]["id"]
|
||||
image_list = []
|
||||
try:
|
||||
image_list.append(data["meta_single_page"]["original_image_url"])
|
||||
except KeyError:
|
||||
for image_url in data["meta_pages"]:
|
||||
image_list.append(image_url["image_urls"]["original"])
|
||||
for i, img_url in enumerate(image_list):
|
||||
img_url = change_pixiv_image_links(img_url)
|
||||
if not await AsyncHttpx.download_file(
|
||||
img_url,
|
||||
TEMP_PATH / f"pid_search_{session.id1}_{i}.png",
|
||||
headers=headers,
|
||||
):
|
||||
await MessageUtils.build_message("图片下载失败了...").finish(
|
||||
reply_to=True
|
||||
)
|
||||
tmp = ""
|
||||
if session.id3 or session.id2:
|
||||
tmp = "\n【注】将在30后撤回......"
|
||||
receipt = await MessageUtils.build_message(
|
||||
[
|
||||
f"title:{title}\n"
|
||||
f"pid:{pid}\n"
|
||||
f"author:{author}\n"
|
||||
f"author_id:{author_id}\n",
|
||||
TEMP_PATH / f"pid_search_{session.id1}_{i}.png",
|
||||
f"{tmp}",
|
||||
]
|
||||
).send()
|
||||
logger.info(
|
||||
f" 查询图片 PID:{pid}", arparma.header_result, session=session
|
||||
)
|
||||
if session.id3 or session.id2:
|
||||
await WithdrawManager.withdraw_message(
|
||||
bot, receipt.msg_ids[0]["message_id"], 30 # type: ignore
|
||||
)
|
||||
break
|
||||
else:
|
||||
await Text("图片下载失败了...").send(reply_to=True)
|
||||
@@ -1,62 +0,0 @@
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
import nonebot
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
|
||||
Config.add_plugin_config(
|
||||
"hibiapi",
|
||||
"HIBIAPI",
|
||||
"https://api.obfs.dev",
|
||||
help="如果没有自建或其他hibiapi请不要修改",
|
||||
default_value="https://api.obfs.dev",
|
||||
)
|
||||
Config.add_plugin_config("pixiv", "PIXIV_NGINX_URL", "i.pximg.cf", help="Pixiv反向代理")
|
||||
Config.add_plugin_config(
|
||||
"pix",
|
||||
"PIX_IMAGE_SIZE",
|
||||
"master",
|
||||
help="PIX图库下载的画质 可能的值:original:原图,master:缩略图(加快发送速度)",
|
||||
default_value="master",
|
||||
)
|
||||
Config.add_plugin_config(
|
||||
"pix",
|
||||
"SEARCH_HIBIAPI_BOOKMARKS",
|
||||
5000,
|
||||
help="最低收藏,PIX使用HIBIAPI搜索图片时达到最低收藏才会添加至图库",
|
||||
default_value=5000,
|
||||
type=int,
|
||||
)
|
||||
Config.add_plugin_config(
|
||||
"pix",
|
||||
"WITHDRAW_PIX_MESSAGE",
|
||||
(0, 1),
|
||||
help="自动撤回,参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)",
|
||||
default_value=(0, 1),
|
||||
type=Tuple[int, int],
|
||||
)
|
||||
Config.add_plugin_config(
|
||||
"pix",
|
||||
"PIX_OMEGA_PIXIV_RATIO",
|
||||
(10, 0),
|
||||
help="PIX图库 与 额外图库OmegaPixivIllusts 混合搜索的比例 参1:PIX图库 参2:OmegaPixivIllusts扩展图库(没有此图库请设置为0)",
|
||||
default_value=(10, 0),
|
||||
type=Tuple[int, int],
|
||||
)
|
||||
Config.add_plugin_config(
|
||||
"pix", "TIMEOUT", 10, help="下载图片超时限制(秒)", default_value=10, type=int
|
||||
)
|
||||
|
||||
Config.add_plugin_config(
|
||||
"pix",
|
||||
"SHOW_INFO",
|
||||
True,
|
||||
help="是否显示图片的基本信息,如PID等",
|
||||
default_value=True,
|
||||
type=bool,
|
||||
)
|
||||
|
||||
Config.set_name("pix", "PIX图库")
|
||||
|
||||
nonebot.load_plugins(str(Path(__file__).parent.resolve()))
|
||||
@@ -1,426 +0,0 @@
|
||||
import asyncio
|
||||
import math
|
||||
from asyncio.exceptions import TimeoutError
|
||||
from asyncio.locks import Semaphore
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
import aiofiles
|
||||
from httpx import ConnectError
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.path_config import TEMP_PATH
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.utils import change_img_md5, change_pixiv_image_links
|
||||
|
||||
from ._model.omega_pixiv_illusts import OmegaPixivIllusts
|
||||
from ._model.pixiv import Pixiv
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;"
|
||||
" rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
|
||||
"Referer": "https://www.pixiv.net/",
|
||||
}
|
||||
|
||||
HIBIAPI = Config.get_config("hibiapi", "HIBIAPI")
|
||||
if not HIBIAPI:
|
||||
HIBIAPI = "https://api.obfs.dev"
|
||||
HIBIAPI = HIBIAPI[:-1] if HIBIAPI[-1] == "/" else HIBIAPI
|
||||
|
||||
|
||||
async def start_update_image_url(
|
||||
current_keyword: list[str], black_pid: list[str], is_pid: bool
|
||||
) -> tuple[int, int]:
|
||||
"""开始更新图片url
|
||||
|
||||
参数:
|
||||
current_keyword: 关键词
|
||||
black_pid: 黑名单pid
|
||||
is_pid: pid强制更新不受限制
|
||||
|
||||
返回:
|
||||
tuple[int, int]: pid数量和图片数量
|
||||
"""
|
||||
global HIBIAPI
|
||||
pid_count = 0
|
||||
pic_count = 0
|
||||
tasks = []
|
||||
semaphore = asyncio.Semaphore(10)
|
||||
for keyword in current_keyword:
|
||||
for page in range(1, 110):
|
||||
if keyword.startswith("uid:"):
|
||||
url = f"{HIBIAPI}/api/pixiv/member_illust"
|
||||
params = {"id": keyword[4:], "page": page}
|
||||
if page == 30:
|
||||
break
|
||||
elif keyword.startswith("pid:"):
|
||||
url = f"{HIBIAPI}/api/pixiv/illust"
|
||||
params = {"id": keyword[4:]}
|
||||
else:
|
||||
url = f"{HIBIAPI}/api/pixiv/search"
|
||||
params = {"word": keyword, "page": page}
|
||||
tasks.append(
|
||||
asyncio.ensure_future(
|
||||
search_image(
|
||||
url, keyword, params, semaphore, page, black_pid, is_pid
|
||||
)
|
||||
)
|
||||
)
|
||||
if keyword.startswith("pid:"):
|
||||
break
|
||||
result = await asyncio.gather(*tasks)
|
||||
for x in result:
|
||||
pid_count += x[0]
|
||||
pic_count += x[1]
|
||||
return pid_count, pic_count
|
||||
|
||||
|
||||
async def search_image(
|
||||
url: str,
|
||||
keyword: str,
|
||||
params: dict,
|
||||
semaphore: Semaphore,
|
||||
page: int = 1,
|
||||
black: list[str] = [],
|
||||
is_pid: bool = False,
|
||||
) -> tuple[int, int]:
|
||||
"""搜索图片
|
||||
|
||||
参数:
|
||||
url: 搜索url
|
||||
keyword: 关键词
|
||||
params: params参数
|
||||
semaphore: semaphore
|
||||
page: 页面
|
||||
black: pid黑名单
|
||||
is_pid: pid强制更新不受限制
|
||||
|
||||
返回:
|
||||
tuple[int, int]: pid数量和图片数量
|
||||
"""
|
||||
tmp_pid = []
|
||||
pic_count = 0
|
||||
pid_count = 0
|
||||
async with semaphore:
|
||||
# try:
|
||||
data = (await AsyncHttpx.get(url, params=params)).json()
|
||||
if (
|
||||
not data
|
||||
or data.get("error")
|
||||
or (not data.get("illusts") and not data.get("illust"))
|
||||
):
|
||||
return 0, 0
|
||||
if url != f"{HIBIAPI}/api/pixiv/illust":
|
||||
logger.info(f'{keyword}: 获取数据成功...数据总量:{len(data["illusts"])}')
|
||||
data = data["illusts"]
|
||||
else:
|
||||
logger.info(f'获取数据成功...PID:{params.get("id")}')
|
||||
data = [data["illust"]]
|
||||
img_data = {}
|
||||
for x in data:
|
||||
pid = x["id"]
|
||||
title = x["title"]
|
||||
width = x["width"]
|
||||
height = x["height"]
|
||||
view = x["total_view"]
|
||||
bookmarks = x["total_bookmarks"]
|
||||
uid = x["user"]["id"]
|
||||
author = x["user"]["name"]
|
||||
tags = []
|
||||
for tag in x["tags"]:
|
||||
for i in tag:
|
||||
if tag[i]:
|
||||
tags.append(tag[i])
|
||||
img_urls = []
|
||||
if x["page_count"] == 1:
|
||||
img_urls.append(x["meta_single_page"]["original_image_url"])
|
||||
else:
|
||||
for urls in x["meta_pages"]:
|
||||
img_urls.append(urls["image_urls"]["original"])
|
||||
if (
|
||||
(
|
||||
bookmarks >= Config.get_config("pix", "SEARCH_HIBIAPI_BOOKMARKS")
|
||||
or (
|
||||
url == f"{HIBIAPI}/api/pixiv/member_illust"
|
||||
and bookmarks >= 1500
|
||||
)
|
||||
or (url == f"{HIBIAPI}/api/pixiv/illust")
|
||||
)
|
||||
and len(img_urls) < 10
|
||||
and _check_black(img_urls, black)
|
||||
) or is_pid:
|
||||
img_data[pid] = {
|
||||
"pid": pid,
|
||||
"title": title,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"view": view,
|
||||
"bookmarks": bookmarks,
|
||||
"img_urls": img_urls,
|
||||
"uid": uid,
|
||||
"author": author,
|
||||
"tags": tags,
|
||||
}
|
||||
else:
|
||||
continue
|
||||
for x in img_data.keys():
|
||||
data = img_data[x]
|
||||
data_copy = deepcopy(data)
|
||||
del data_copy["img_urls"]
|
||||
for img_url in data["img_urls"]:
|
||||
img_p = img_url[img_url.rfind("_") + 1 : img_url.rfind(".")]
|
||||
data_copy["img_url"] = img_url
|
||||
data_copy["img_p"] = img_p
|
||||
data_copy["is_r18"] = "R-18" in data["tags"]
|
||||
if not await Pixiv.exists(
|
||||
pid=data["pid"], img_url=img_url, img_p=img_p
|
||||
):
|
||||
data_copy["img_url"] = img_url
|
||||
await Pixiv.create(**data_copy)
|
||||
if data["pid"] not in tmp_pid:
|
||||
pid_count += 1
|
||||
tmp_pid.append(data["pid"])
|
||||
pic_count += 1
|
||||
logger.info(f'存储图片PID:{data["pid"]} IMG_P:{img_p}')
|
||||
else:
|
||||
logger.warning(f'{data["pid"]} | {img_url} 已存在...')
|
||||
# except Exception as e:
|
||||
# logger.warning(f"PIX在线搜索图片错误,已再次调用 {type(e)}:{e}")
|
||||
# await search_image(url, keyword, params, semaphore, page, black)
|
||||
return pid_count, pic_count
|
||||
|
||||
|
||||
async def get_image(img_url: str, user_id: str) -> Path | None:
|
||||
"""下载图片
|
||||
|
||||
参数:
|
||||
img_url: 图片url
|
||||
user_id: 用户id
|
||||
|
||||
返回:
|
||||
Path | None: 图片名称
|
||||
"""
|
||||
if "https://www.pixiv.net/artworks" in img_url:
|
||||
pid = img_url.rsplit("/", maxsplit=1)[-1]
|
||||
params = {"id": pid}
|
||||
for _ in range(3):
|
||||
try:
|
||||
response = await AsyncHttpx.get(
|
||||
f"{HIBIAPI}/api/pixiv/illust", params=params
|
||||
)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if data.get("illust"):
|
||||
if data["illust"]["page_count"] == 1:
|
||||
img_url = data["illust"]["meta_single_page"][
|
||||
"original_image_url"
|
||||
]
|
||||
else:
|
||||
img_url = data["illust"]["meta_pages"][0]["image_urls"][
|
||||
"original"
|
||||
]
|
||||
break
|
||||
except TimeoutError:
|
||||
pass
|
||||
old_img_url = img_url
|
||||
img_url = change_pixiv_image_links(
|
||||
img_url,
|
||||
Config.get_config("pix", "PIX_IMAGE_SIZE"),
|
||||
Config.get_config("pixiv", "PIXIV_NGINX_URL"),
|
||||
)
|
||||
old_img_url = change_pixiv_image_links(
|
||||
old_img_url, None, Config.get_config("pixiv", "PIXIV_NGINX_URL")
|
||||
)
|
||||
for _ in range(3):
|
||||
try:
|
||||
response = await AsyncHttpx.get(
|
||||
img_url,
|
||||
headers=headers,
|
||||
timeout=Config.get_config("pix", "TIMEOUT"),
|
||||
)
|
||||
if response.status_code == 404:
|
||||
img_url = old_img_url
|
||||
continue
|
||||
async with aiofiles.open(
|
||||
TEMP_PATH / f"pix_{user_id}_{img_url.split('/')[-1][:-4]}.jpg", "wb"
|
||||
) as f:
|
||||
await f.write(response.content)
|
||||
change_img_md5(
|
||||
TEMP_PATH / f"pix_{user_id}_{img_url.split('/')[-1][:-4]}.jpg"
|
||||
)
|
||||
return TEMP_PATH / f"pix_{user_id}_{img_url.split('/')[-1][:-4]}.jpg"
|
||||
except TimeoutError:
|
||||
logger.warning(f"PIX:{img_url} 图片下载超时...")
|
||||
except ConnectError:
|
||||
logger.warning(f"PIX:{img_url} 图片下载连接失败...")
|
||||
return None
|
||||
|
||||
|
||||
async def uid_pid_exists(id_: str) -> bool:
|
||||
"""检测 pid/uid 是否有效
|
||||
|
||||
参数:
|
||||
id_: pid/uid
|
||||
|
||||
返回:
|
||||
bool: 是否有效
|
||||
"""
|
||||
if id_.startswith("uid:"):
|
||||
url = f"{HIBIAPI}/api/pixiv/member"
|
||||
elif id_.startswith("pid:"):
|
||||
url = f"{HIBIAPI}/api/pixiv/illust"
|
||||
else:
|
||||
return False
|
||||
params = {"id": int(id_[4:])}
|
||||
data = (await AsyncHttpx.get(url, params=params)).json()
|
||||
if data.get("error"):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
async def get_keyword_num(keyword: str) -> tuple[int, int, int, int, int]:
|
||||
"""查看图片相关 tag 数量
|
||||
|
||||
参数:
|
||||
keyword: 关键词tag
|
||||
|
||||
返回:
|
||||
tuple[int, int, int, int, int]: 总数, r18数, Omg图库总数, Omg图库色图数, Omg图库r18数
|
||||
"""
|
||||
count, r18_count = await Pixiv.get_keyword_num(keyword.split())
|
||||
count_, setu_count, r18_count_ = await OmegaPixivIllusts.get_keyword_num(
|
||||
keyword.split()
|
||||
)
|
||||
return count, r18_count, count_, setu_count, r18_count_
|
||||
|
||||
|
||||
async def remove_image(pid: int, img_p: str | None):
|
||||
"""删除置顶图片
|
||||
|
||||
参数:
|
||||
pid: pid
|
||||
img_p: 图片 p 如 p0,p1 等
|
||||
"""
|
||||
if img_p:
|
||||
if "p" not in img_p:
|
||||
img_p = f"p{img_p}"
|
||||
if img_p:
|
||||
await Pixiv.filter(pid=pid, img_p=img_p).delete()
|
||||
else:
|
||||
await Pixiv.filter(pid=pid).delete()
|
||||
|
||||
|
||||
async def gen_keyword_pic(
|
||||
_pass_keyword: list[str], not_pass_keyword: list[str], is_superuser: bool
|
||||
) -> BuildImage:
|
||||
"""已通过或未通过的所有关键词/uid/pid
|
||||
|
||||
参数:
|
||||
_pass_keyword: 通过列表
|
||||
not_pass_keyword: 未通过列表
|
||||
is_superuser: 是否超级用户
|
||||
|
||||
返回:
|
||||
BuildImage: 数据图片
|
||||
"""
|
||||
_keyword = [
|
||||
x
|
||||
for x in _pass_keyword
|
||||
if not x.startswith("uid:")
|
||||
and not x.startswith("pid:")
|
||||
and not x.startswith("black:")
|
||||
]
|
||||
_uid = [x for x in _pass_keyword if x.startswith("uid:")]
|
||||
_pid = [x for x in _pass_keyword if x.startswith("pid:")]
|
||||
_n_keyword = [
|
||||
x
|
||||
for x in not_pass_keyword
|
||||
if not x.startswith("uid:")
|
||||
and not x.startswith("pid:")
|
||||
and not x.startswith("black:")
|
||||
]
|
||||
_n_uid = [
|
||||
x
|
||||
for x in not_pass_keyword
|
||||
if x.startswith("uid:") and not x.startswith("black:")
|
||||
]
|
||||
_n_pid = [
|
||||
x
|
||||
for x in not_pass_keyword
|
||||
if x.startswith("pid:") and not x.startswith("black:")
|
||||
]
|
||||
img_width = 0
|
||||
img_data = {
|
||||
"_keyword": {"width": 0, "data": _keyword},
|
||||
"_uid": {"width": 0, "data": _uid},
|
||||
"_pid": {"width": 0, "data": _pid},
|
||||
"_n_keyword": {"width": 0, "data": _n_keyword},
|
||||
"_n_uid": {"width": 0, "data": _n_uid},
|
||||
"_n_pid": {"width": 0, "data": _n_pid},
|
||||
}
|
||||
for x in list(img_data.keys()):
|
||||
img_data[x]["width"] = math.ceil(len(img_data[x]["data"]) / 40)
|
||||
img_width += img_data[x]["width"] * 200
|
||||
if not is_superuser:
|
||||
img_width = (
|
||||
img_width
|
||||
- (
|
||||
img_data["_n_keyword"]["width"]
|
||||
+ img_data["_n_uid"]["width"]
|
||||
+ img_data["_n_pid"]["width"]
|
||||
)
|
||||
* 200
|
||||
)
|
||||
del img_data["_n_keyword"]
|
||||
del img_data["_n_pid"]
|
||||
del img_data["_n_uid"]
|
||||
current_width = 0
|
||||
A = BuildImage(img_width, 1100)
|
||||
for x in list(img_data.keys()):
|
||||
if img_data[x]["data"]:
|
||||
# img = BuildImage(img_data[x]["width"] * 200, 1100, 200, 1100, font_size=40)
|
||||
img = BuildImage(img_data[x]["width"] * 200, 1100, font_size=40)
|
||||
start_index = 0
|
||||
end_index = 40
|
||||
total_index = img_data[x]["width"] * 40
|
||||
for _ in range(img_data[x]["width"]):
|
||||
tmp = BuildImage(198, 1100, font_size=20)
|
||||
text_img = BuildImage(198, 100, font_size=50)
|
||||
key_str = "\n".join(
|
||||
[key for key in img_data[x]["data"][start_index:end_index]]
|
||||
)
|
||||
await tmp.text((10, 100), key_str)
|
||||
if x.find("_n") == -1:
|
||||
await text_img.text((24, 24), "已收录")
|
||||
else:
|
||||
await text_img.text((24, 24), "待收录")
|
||||
await tmp.paste(text_img, (0, 0))
|
||||
start_index += 40
|
||||
end_index = (
|
||||
end_index + 40 if end_index + 40 <= total_index else total_index
|
||||
)
|
||||
background_img = BuildImage(200, 1100, color="#FFE4C4")
|
||||
await background_img.paste(tmp, (1, 1))
|
||||
await img.paste(background_img)
|
||||
await A.paste(img, (current_width, 0))
|
||||
current_width += img_data[x]["width"] * 200
|
||||
return A
|
||||
|
||||
|
||||
def _check_black(img_urls: list[str], black: list[str]) -> bool:
|
||||
"""检测pid是否在黑名单中
|
||||
|
||||
参数:
|
||||
img_urls: 图片img列表
|
||||
black: 黑名单
|
||||
|
||||
返回:
|
||||
bool: 是否在黑名单中
|
||||
"""
|
||||
for b in black:
|
||||
for img_url in img_urls:
|
||||
if b in img_url:
|
||||
return False
|
||||
return True
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
|
||||
from tortoise import fields
|
||||
from tortoise.contrib.postgres.functions import Random
|
||||
|
||||
from zhenxun.services.db_context import Model
|
||||
|
||||
|
||||
class OmegaPixivIllusts(Model):
|
||||
|
||||
id = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
"""自增id"""
|
||||
pid = fields.BigIntField()
|
||||
"""pid"""
|
||||
uid = fields.BigIntField()
|
||||
"""uid"""
|
||||
title = fields.CharField(255)
|
||||
"""标题"""
|
||||
uname = fields.CharField(255)
|
||||
"""画师名称"""
|
||||
classified = fields.IntField()
|
||||
"""标记标签, 0=未标记, 1=已人工标记或从可信已标记来源获取"""
|
||||
nsfw_tag = fields.IntField()
|
||||
"""nsfw标签,-1=未标记, 0=safe, 1=setu. 2=r18"""
|
||||
width = fields.IntField()
|
||||
"""宽度"""
|
||||
height = fields.IntField()
|
||||
"""高度"""
|
||||
tags = fields.TextField()
|
||||
"""tags"""
|
||||
url = fields.CharField(255)
|
||||
"""pixiv url链接"""
|
||||
|
||||
class Meta:
|
||||
table = "omega_pixiv_illusts"
|
||||
table_description = "omega图库数据表"
|
||||
unique_together = ("pid", "url")
|
||||
|
||||
@classmethod
|
||||
async def query_images(
|
||||
cls,
|
||||
keywords: list[str] | None = None,
|
||||
uid: int | None = None,
|
||||
pid: int | None = None,
|
||||
nsfw_tag: int | None = 0,
|
||||
num: int = 100,
|
||||
) -> list["OmegaPixivIllusts"]:
|
||||
"""查找符合条件的图片
|
||||
|
||||
参数:
|
||||
keywords: 关键词
|
||||
uid: 画师uid
|
||||
pid: 图片pid
|
||||
nsfw_tag: nsfw标签, 0=safe, 1=setu. 2=r18
|
||||
num: 获取图片数量
|
||||
"""
|
||||
if not num:
|
||||
return []
|
||||
query = cls
|
||||
if nsfw_tag is not None:
|
||||
query = cls.filter(nsfw_tag=nsfw_tag)
|
||||
if keywords:
|
||||
for keyword in keywords:
|
||||
query = query.filter(tags__contains=keyword)
|
||||
elif uid:
|
||||
query = query.filter(uid=uid)
|
||||
elif pid:
|
||||
query = query.filter(pid=pid)
|
||||
query = query.annotate(rand=Random()).limit(num)
|
||||
return await query.all() # type: ignore
|
||||
|
||||
@classmethod
|
||||
async def get_keyword_num(
|
||||
cls, tags: list[str] | None = None
|
||||
) -> tuple[int, int, int]:
|
||||
"""获取相关关键词(keyword, tag)在图库中的数量
|
||||
|
||||
参数:
|
||||
tags: 关键词/Tag
|
||||
"""
|
||||
query = cls
|
||||
if tags:
|
||||
for tag in tags:
|
||||
query = query.filter(tags__contains=tag)
|
||||
else:
|
||||
query = query.all()
|
||||
count = await query.filter(nsfw_tag=0).count()
|
||||
setu_count = await query.filter(nsfw_tag=1).count()
|
||||
r18_count = await query.filter(nsfw_tag=2).count()
|
||||
return count, setu_count, r18_count
|
||||
@@ -1,91 +0,0 @@
|
||||
from tortoise import fields
|
||||
from tortoise.contrib.postgres.functions import Random
|
||||
|
||||
from zhenxun.services.db_context import Model
|
||||
|
||||
|
||||
class Pixiv(Model):
|
||||
|
||||
id = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
"""自增id"""
|
||||
pid = fields.BigIntField()
|
||||
"""pid"""
|
||||
uid = fields.BigIntField()
|
||||
"""uid"""
|
||||
author = fields.CharField(255)
|
||||
"""作者"""
|
||||
title = fields.CharField(255)
|
||||
"""标题"""
|
||||
width = fields.IntField()
|
||||
"""宽度"""
|
||||
height = fields.IntField()
|
||||
"""高度"""
|
||||
view = fields.IntField()
|
||||
"""pixiv查看数"""
|
||||
bookmarks = fields.IntField()
|
||||
"""收藏数"""
|
||||
tags = fields.TextField()
|
||||
"""tags"""
|
||||
img_url = fields.CharField(255)
|
||||
"""pixiv url链接"""
|
||||
img_p = fields.CharField(255)
|
||||
"""图片pN"""
|
||||
is_r18 = fields.BooleanField()
|
||||
|
||||
class Meta:
|
||||
table = "pixiv"
|
||||
table_description = "pix图库数据表"
|
||||
unique_together = ("pid", "img_url", "img_p")
|
||||
|
||||
# 0:非r18 1:r18 2:混合
|
||||
@classmethod
|
||||
async def query_images(
|
||||
cls,
|
||||
keywords: list[str] | None = None,
|
||||
uid: int | None = None,
|
||||
pid: int | None = None,
|
||||
r18: int | None = 0,
|
||||
num: int = 100,
|
||||
) -> list["Pixiv"]:
|
||||
"""查找符合条件的图片
|
||||
|
||||
参数:
|
||||
keywords: 关键词
|
||||
uid: 画师uid
|
||||
pid: 图片pid
|
||||
r18: 是否r18,0:非r18 1:r18 2:混合
|
||||
num: 查找图片的数量
|
||||
"""
|
||||
if not num:
|
||||
return []
|
||||
query = cls
|
||||
if r18 == 0:
|
||||
query = query.filter(is_r18=False)
|
||||
elif r18 == 1:
|
||||
query = query.filter(is_r18=True)
|
||||
if keywords:
|
||||
for keyword in keywords:
|
||||
query = query.filter(tags__contains=keyword)
|
||||
elif uid:
|
||||
query = query.filter(uid=uid)
|
||||
elif pid:
|
||||
query = query.filter(pid=pid)
|
||||
query = query.annotate(rand=Random()).limit(num)
|
||||
return await query.all() # type: ignore
|
||||
|
||||
@classmethod
|
||||
async def get_keyword_num(cls, tags: list[str] | None = None) -> tuple[int, int]:
|
||||
"""获取相关关键词(keyword, tag)在图库中的数量
|
||||
|
||||
参数:
|
||||
tags: 关键词/Tag
|
||||
"""
|
||||
query = cls
|
||||
if tags:
|
||||
for tag in tags:
|
||||
query = query.filter(tags__contains=tag)
|
||||
else:
|
||||
query = query.all()
|
||||
count = await query.filter(is_r18=False).count()
|
||||
r18_count = await query.filter(is_r18=True).count()
|
||||
return count, r18_count
|
||||
@@ -1,52 +0,0 @@
|
||||
from tortoise import fields
|
||||
|
||||
from zhenxun.services.db_context import Model
|
||||
|
||||
|
||||
class PixivKeywordUser(Model):
|
||||
|
||||
id = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
"""自增id"""
|
||||
user_id = fields.CharField(255)
|
||||
"""用户id"""
|
||||
group_id = fields.CharField(255)
|
||||
"""群聊id"""
|
||||
keyword = fields.CharField(255, unique=True)
|
||||
"""关键词"""
|
||||
is_pass = fields.BooleanField()
|
||||
"""是否通过"""
|
||||
|
||||
class Meta:
|
||||
table = "pixiv_keyword_users"
|
||||
table_description = "pixiv关键词数据表"
|
||||
|
||||
@classmethod
|
||||
async def get_current_keyword(cls) -> tuple[list[str], list[str]]:
|
||||
"""获取当前通过与未通过的关键词"""
|
||||
pass_keyword = []
|
||||
not_pass_keyword = []
|
||||
for data in await cls.all().values_list("keyword", "is_pass"):
|
||||
if data[1]:
|
||||
pass_keyword.append(data[0])
|
||||
else:
|
||||
not_pass_keyword.append(data[0])
|
||||
return pass_keyword, not_pass_keyword
|
||||
|
||||
@classmethod
|
||||
async def get_black_pid(cls) -> list[str]:
|
||||
"""获取黑名单PID"""
|
||||
black_pid = []
|
||||
keyword_list = await cls.filter(user_id="114514").values_list(
|
||||
"keyword", flat=True
|
||||
)
|
||||
for image in keyword_list:
|
||||
black_pid.append(image[6:])
|
||||
return black_pid
|
||||
|
||||
@classmethod
|
||||
async def _run_script(cls):
|
||||
return [
|
||||
"ALTER TABLE pixiv_keyword_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id
|
||||
"ALTER TABLE pixiv_keyword_users ALTER COLUMN user_id TYPE character varying(255);",
|
||||
"ALTER TABLE pixiv_keyword_users ALTER COLUMN group_id TYPE character varying(255);",
|
||||
]
|
||||
@@ -1,247 +0,0 @@
|
||||
import random
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
Args,
|
||||
Arparma,
|
||||
Match,
|
||||
Option,
|
||||
on_alconna,
|
||||
store_true,
|
||||
)
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import BaseBlock, PluginExtraData, RegisterConfig
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
from zhenxun.utils.withdraw_manage import WithdrawManager
|
||||
|
||||
from ._data_source import get_image
|
||||
from ._model.omega_pixiv_illusts import OmegaPixivIllusts
|
||||
from ._model.pixiv import Pixiv
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="PIX",
|
||||
description="这里是PIX图库!",
|
||||
usage="""
|
||||
指令:
|
||||
pix ?*[tags]: 通过 tag 获取相似图片,不含tag时随机抽取
|
||||
pid [uid]: 通过uid获取图片
|
||||
pix pid[pid]: 查看图库中指定pid图片
|
||||
示例:pix 萝莉 白丝
|
||||
示例:pix 萝莉 白丝 10 (10为数量)
|
||||
示例:pix #02 (当tag只有1个tag且为数字时,使用#标记,否则将被判定为数量)
|
||||
示例:pix 34582394 (查询指定uid图片)
|
||||
示例:pix pid:12323423 (查询指定pid图片)
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
superuser_help="""
|
||||
指令:
|
||||
pix -s ?*[tags]: 通过tag获取色图,不含tag时随机
|
||||
pix -r ?*[tags]: 通过tag获取r18图,不含tag时随机
|
||||
""",
|
||||
menu_type="来点好康的",
|
||||
limits=[BaseBlock(result="您有PIX图片正在处理,请稍等...")],
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
key="MAX_ONCE_NUM2FORWARD",
|
||||
value=None,
|
||||
help="单次发送的图片数量达到指定值时转发为合并消息",
|
||||
default_value=None,
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="ALLOW_GROUP_SETU",
|
||||
value=False,
|
||||
help="允许非超级用户使用-s参数",
|
||||
default_value=False,
|
||||
type=bool,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="ALLOW_GROUP_R18",
|
||||
value=False,
|
||||
help="允许非超级用户使用-r参数",
|
||||
default_value=False,
|
||||
type=bool,
|
||||
),
|
||||
],
|
||||
).dict(),
|
||||
)
|
||||
|
||||
# pix = on_command("pix", aliases={"PIX", "Pix"}, priority=5, block=True)
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna(
|
||||
"pix",
|
||||
Args["tags?", str] / "\n",
|
||||
Option("-s", action=store_true, help_text="色图"),
|
||||
Option("-r", action=store_true, help_text="r18"),
|
||||
),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
PIX_RATIO = None
|
||||
OMEGA_RATIO = None
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[str]):
|
||||
global PIX_RATIO, OMEGA_RATIO
|
||||
gid = session.id3 or session.id2
|
||||
if not session.id1:
|
||||
await MessageUtils.build_message("用户id为空...").finish()
|
||||
if PIX_RATIO is None:
|
||||
pix_omega_pixiv_ratio = Config.get_config("pix", "PIX_OMEGA_PIXIV_RATIO")
|
||||
PIX_RATIO = pix_omega_pixiv_ratio[0] / (
|
||||
pix_omega_pixiv_ratio[0] + pix_omega_pixiv_ratio[1]
|
||||
)
|
||||
OMEGA_RATIO = 1 - PIX_RATIO
|
||||
num = 1
|
||||
# keyword = arg.extract_plain_text().strip()
|
||||
keyword = ""
|
||||
spt = tags.result.split() if tags.available else []
|
||||
if arparma.find("s"):
|
||||
nsfw_tag = 1
|
||||
elif arparma.find("r"):
|
||||
nsfw_tag = 2
|
||||
else:
|
||||
nsfw_tag = 0
|
||||
if session.id1 not in bot.config.superusers:
|
||||
if (nsfw_tag == 1 and not Config.get_config("pix", "ALLOW_GROUP_SETU")) or (
|
||||
nsfw_tag == 2 and not Config.get_config("pix", "ALLOW_GROUP_R18")
|
||||
):
|
||||
await MessageUtils.build_message(
|
||||
"你不能看这些噢,这些都是是留给管理员看的..."
|
||||
).finish()
|
||||
if (n := len(spt)) == 1:
|
||||
if str(spt[0]).isdigit() and int(spt[0]) < 100:
|
||||
num = int(spt[0])
|
||||
keyword = ""
|
||||
elif spt[0].startswith("#"):
|
||||
keyword = spt[0][1:]
|
||||
elif n > 1:
|
||||
if str(spt[-1]).isdigit():
|
||||
num = int(spt[-1])
|
||||
if num > 10:
|
||||
if session.id1 not in bot.config.superusers or (
|
||||
session.id1 in bot.config.superusers and num > 30
|
||||
):
|
||||
num = random.randint(1, 10)
|
||||
await MessageUtils.build_message(
|
||||
f"太贪心了,就给你发 {num}张 好了"
|
||||
).send()
|
||||
spt = spt[:-1]
|
||||
keyword = " ".join(spt)
|
||||
pix_num = int(num * PIX_RATIO) + 15 if PIX_RATIO != 0 else 0
|
||||
omega_num = num - pix_num + 15
|
||||
if str(keyword).isdigit():
|
||||
if num == 1:
|
||||
pix_num = 15
|
||||
omega_num = 15
|
||||
all_image = await Pixiv.query_images(
|
||||
uid=int(keyword), num=pix_num, r18=1 if nsfw_tag == 2 else 0
|
||||
) + await OmegaPixivIllusts.query_images(
|
||||
uid=int(keyword), num=omega_num, nsfw_tag=nsfw_tag
|
||||
)
|
||||
elif keyword.lower().startswith("pid"):
|
||||
pid = keyword.replace("pid", "").replace(":", "").replace(":", "")
|
||||
if not str(pid).isdigit():
|
||||
await MessageUtils.build_message("PID必须是数字...").finish(reply_to=True)
|
||||
all_image = await Pixiv.query_images(
|
||||
pid=int(pid), r18=1 if nsfw_tag == 2 else 0
|
||||
)
|
||||
if not all_image:
|
||||
all_image = await OmegaPixivIllusts.query_images(
|
||||
pid=int(pid), nsfw_tag=nsfw_tag
|
||||
)
|
||||
num = len(all_image)
|
||||
else:
|
||||
tmp = await Pixiv.query_images(
|
||||
spt, r18=1 if nsfw_tag == 2 else 0, num=pix_num
|
||||
) + await OmegaPixivIllusts.query_images(spt, nsfw_tag=nsfw_tag, num=omega_num)
|
||||
tmp_ = []
|
||||
all_image = []
|
||||
for x in tmp:
|
||||
if x.pid not in tmp_:
|
||||
all_image.append(x)
|
||||
tmp_.append(x.pid)
|
||||
if not all_image:
|
||||
await MessageUtils.build_message(
|
||||
f"未在图库中找到与 {keyword} 相关Tag/UID/PID的图片..."
|
||||
).finish(reply_to=True)
|
||||
msg_list = []
|
||||
for _ in range(num):
|
||||
img_url = None
|
||||
author = None
|
||||
if not all_image:
|
||||
await MessageUtils.build_message("坏了...发完了,没图了...").finish()
|
||||
img = random.choice(all_image)
|
||||
all_image.remove(img) # type: ignore
|
||||
if isinstance(img, OmegaPixivIllusts):
|
||||
img_url = img.url
|
||||
author = img.uname
|
||||
elif isinstance(img, Pixiv):
|
||||
img_url = img.img_url
|
||||
author = img.author
|
||||
pid = img.pid
|
||||
title = img.title
|
||||
uid = img.uid
|
||||
if img_url:
|
||||
_img = await get_image(img_url, session.id1)
|
||||
if _img:
|
||||
if Config.get_config("pix", "SHOW_INFO"):
|
||||
msg_list.append(
|
||||
MessageUtils.build_message(
|
||||
[
|
||||
f"title:{title}\n"
|
||||
f"author:{author}\n"
|
||||
f"PID:{pid}\nUID:{uid}\n",
|
||||
_img,
|
||||
]
|
||||
)
|
||||
)
|
||||
else:
|
||||
msg_list.append(_img)
|
||||
logger.info(
|
||||
f" 查看PIX图库PID: {pid}", arparma.header_result, session=session
|
||||
)
|
||||
else:
|
||||
msg_list.append(MessageUtils.build_message("这张图似乎下载失败了"))
|
||||
logger.info(
|
||||
f" 查看PIX图库PID: {pid},下载图片出错",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
if (
|
||||
Config.get_config("pix", "MAX_ONCE_NUM2FORWARD")
|
||||
and num >= Config.get_config("pix", "MAX_ONCE_NUM2FORWARD")
|
||||
and gid
|
||||
):
|
||||
for msg in msg_list:
|
||||
receipt = await msg.send()
|
||||
if receipt:
|
||||
message_id = receipt.msg_ids[0]["message_id"]
|
||||
await WithdrawManager.withdraw_message(
|
||||
bot,
|
||||
str(message_id),
|
||||
Config.get_config("pix", "WITHDRAW_PIX_MESSAGE"),
|
||||
session,
|
||||
)
|
||||
else:
|
||||
for msg in msg_list:
|
||||
receipt = await msg.send()
|
||||
if receipt:
|
||||
message_id = receipt.msg_ids[0]["message_id"]
|
||||
await WithdrawManager.withdraw_message(
|
||||
bot,
|
||||
message_id,
|
||||
Config.get_config("pix", "WITHDRAW_PIX_MESSAGE"),
|
||||
session,
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user