🎨 修复bug,移除plugins文件夹

This commit is contained in:
HibiKier
2024-08-29 22:01:34 +08:00
parent e10fee74b3
commit 625f17a708
146 changed files with 580 additions and 18320 deletions
+1 -1
View File
@@ -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(),
)
+5 -3
View File
@@ -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)} 条数据..."
)
+45 -40
View File
@@ -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)
+46 -37
View File
@@ -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"):
+6 -5
View 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
):
+16 -16
View File
@@ -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
+33 -29
View File
@@ -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:
+1 -1
View File
@@ -33,7 +33,7 @@ class FgRequest(Model):
)
"""处理类型"""
class Meta:
class Meta: # type: ignore
table = "fg_request"
table_description = "好友群组请求"
+60 -5
View File
@@ -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(
+8 -32
View File
@@ -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;",
# 默认状态
]
-87
View File
@@ -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()
-241
View File
@@ -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
-153
View File
@@ -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()
-14
View File
@@ -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()))
-29
View File
@@ -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
-57
View File
@@ -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,
)
-46
View File
@@ -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
)
-48
View File
@@ -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)
-57
View File
@@ -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,
)
-5
View File
@@ -1,5 +0,0 @@
from pathlib import Path
import nonebot
nonebot.load_plugins(str(Path(__file__).parent.resolve()))
-62
View File
@@ -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()
-235
View File
@@ -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,
)
-103
View File
@@ -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或敏感词"
-154
View File
@@ -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);",
]
-374
View File
@@ -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",
)
-78
View File
@@ -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
)
-54
View File
@@ -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
-40
View File
@@ -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)
-83
View File
@@ -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
-90
View File
@@ -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,
)
-158
View File
@@ -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()
-55
View File
@@ -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)
-293
View File
@@ -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)
-203
View File
@@ -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,
)
-149
View File
@@ -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,
]
)
-10
View File
@@ -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)
-61
View File
@@ -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
-40
View File
@@ -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)
-180
View File
@@ -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
-151
View File
@@ -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()
-349
View File
@@ -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()
-372
View File
@@ -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
-234
View File
@@ -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
-63
View File
@@ -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);",
]
-62
View File
@@ -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()
-74
View File
@@ -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)
-5
View File
@@ -1,5 +0,0 @@
from pathlib import Path
import nonebot
nonebot.load_plugins(str(Path(__file__).parent.resolve()))
-124
View File
@@ -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()
-56
View File
@@ -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)
-117
View File
@@ -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()
-62
View File
@@ -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()
-77
View File
@@ -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()
-329
View File
@@ -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",
)
-155
View File
@@ -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
-75
View File
@@ -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,
)
-253
View File
@@ -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);",
]
-481
View File
@@ -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)
-657
View File
@@ -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()
-186
View File
@@ -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()
-106
View File
@@ -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()
-125
View File
@@ -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)
-62
View File
@@ -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()))
-426
View File
@@ -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);",
]
-247
View File
@@ -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