\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,
+ )
diff --git a/zhenxun/plugins/dialogue/__init__.py b/zhenxun/plugins/dialogue/__init__.py
new file mode 100644
index 00000000..5524cf9a
--- /dev/null
+++ b/zhenxun/plugins/dialogue/__init__.py
@@ -0,0 +1,161 @@
+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.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)
+ try:
+ superuser_id = config.platform_superusers["qq"][0]
+ if platform == "dodo":
+ superuser_id = config.platform_superusers["dodo"][0]
+ if platform == "kaiheila":
+ superuser_id = config.platform_superusers["kaiheila"][0]
+ if platform == "discord":
+ superuser_id = config.platform_superusers["discord"][0]
+ 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()
diff --git a/zhenxun/plugins/dialogue/_data_source.py b/zhenxun/plugins/dialogue/_data_source.py
new file mode 100644
index 00000000..440c8176
--- /dev/null
+++ b/zhenxun/plugins/dialogue/_data_source.py
@@ -0,0 +1,55 @@
+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)
diff --git a/plugins/draw_card/__init__.py b/zhenxun/plugins/draw_card/__init__.py
similarity index 64%
rename from plugins/draw_card/__init__.py
rename to zhenxun/plugins/draw_card/__init__.py
index f3864344..2aeeb30e 100644
--- a/plugins/draw_card/__init__.py
+++ b/zhenxun/plugins/draw_card/__init__.py
@@ -1,305 +1,293 @@
-import asyncio
-import traceback
-from dataclasses import dataclass
-from typing import Any, Optional, Set, Tuple
-
-import nonebot
-from cn2an import cn2an
-from configs.config import Config
-from nonebot import on_keyword, on_message, on_regex
-from nonebot.adapters.onebot.v11 import MessageEvent
-from nonebot.log import logger
-from nonebot.matcher import Matcher
-from nonebot.params import RegexGroup
-from nonebot.permission import SUPERUSER
-from nonebot.typing import T_Handler
-from nonebot_plugin_apscheduler import scheduler
-
-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
-
-__zx_plugin_name__ = "游戏抽卡"
-__plugin_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()
-__plugin_superuser_usage__ = """
-usage:
- 卡池方面的更新
- 指令:
- 更新方舟信息
- 重载方舟卡池
- 更新原神信息
- 重载原神卡池
- 更新赛马娘信息
- 重载赛马娘卡池
- 更新坎公骑冠剑信息
- 更新碧蓝航线信息
- 更新fgo信息
- 更新阴阳师信息
-""".strip()
-__plugin_des__ = "就算是模拟抽卡也不能改变自己是个非酋"
-__plugin_cmd__ = [
- "原神[1-180]抽",
- "原神角色[1-180]抽",
- "原神武器[1-180]抽",
- "重置原神抽卡",
- "方舟[1-300]抽",
- "赛马娘[1-200]抽",
- "坎公骑冠剑[1-300]抽",
- "pcr/公主连接[1-300]抽",
- "fgo[1-300]抽",
- "阴阳师[1-300]抽",
- "碧蓝档案[1-200]抽",
- "更新方舟信息 [_superuser]",
- "重载方舟卡池 [_superuser]",
- "更新原神信息 [_superuser]",
- "重载原神卡池 [_superuser]",
- "更新赛马娘信息 [_superuser]",
- "重载赛马娘卡池 [_superuser]",
- "更新坎公骑冠剑信息 [_superuser]",
- "更新碧蓝航线信息 [_superuser]",
- "更新fgo信息 [_superuser]",
- "更新阴阳师信息 [_superuser]",
- "更新碧蓝档案信息 [_superuser]",
-]
-__plugin_type__ = ("抽卡相关", 1)
-__plugin_version__ = 0.1
-__plugin_author__ = "HibiKier"
-__plugin_settings__ = {
- "level": 5,
- "default_status": True,
- "limit_superuser": False,
- "cmd": ["游戏抽卡", "抽卡"],
-}
-
-
-x = on_message(rule=lambda: False)
-
-
-@dataclass
-class Game:
- keywords: Set[str]
- handle: BaseHandle
- flag: bool
- config_name: str
- max_count: int = 300 # 一次最大抽卡数
- reload_time: Optional[int] = 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(
- matcher: Matcher, event: MessageEvent, 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 matcher.finish("必!须!是!数!字!")
- if unit == "井":
- num *= game.max_count
- if num < 1:
- await matcher.finish("虚空抽卡???")
- elif num > game.max_count:
- await matcher.finish("一井都满不足不了你嘛!快爬开!")
- 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 = game.handle.draw(num, pool_name=pool_name, user_id=event.user_id)
- except:
- logger.warning(traceback.format_exc())
- await matcher.finish("出错了...")
- await matcher.finish(res, at_sender=True)
-
- 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 matcher.finish(res)
-
- return handler
-
- def reset_handler(game: Game) -> T_Handler:
- async def handler(matcher: Matcher, event: MessageEvent):
- if game.handle.reset_count(event.user_id):
- await matcher.finish("重置成功!")
-
- 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)
+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)
diff --git a/plugins/draw_card/config.py b/zhenxun/plugins/draw_card/config.py
similarity index 87%
rename from plugins/draw_card/config.py
rename to zhenxun/plugins/draw_card/config.py
index d809b1fd..0aff3ef8 100644
--- a/plugins/draw_card/config.py
+++ b/zhenxun/plugins/draw_card/config.py
@@ -1,204 +1,203 @@
-from pathlib import Path
-
-import nonebot
-from nonebot.log import logger
-from pydantic import BaseModel, Extra, ValidationError
-
-from configs.config import Config as AConfig
-from configs.path_config import DATA_PATH, IMAGE_PATH
-
-try:
- import ujson as json
-except ModuleNotFoundError:
- import json
-
-
-# 原神
-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 = True
- 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,
- name="游戏抽卡",
- help_=f"{game_name} 抽卡开关",
- default_value=True,
- type=bool,
- )
-AConfig.add_plugin_config(
- "draw_card", "SEMAPHORE", 5, help_=f"异步数据下载数量限制", default_value=5, type=int
-)
-
-
-@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,
- )
+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,
+ )
diff --git a/plugins/draw_card/count_manager.py b/zhenxun/plugins/draw_card/count_manager.py
similarity index 100%
rename from plugins/draw_card/count_manager.py
rename to zhenxun/plugins/draw_card/count_manager.py
diff --git a/plugins/draw_card/handles/azur_handle.py b/zhenxun/plugins/draw_card/handles/azur_handle.py
similarity index 85%
rename from plugins/draw_card/handles/azur_handle.py
rename to zhenxun/plugins/draw_card/handles/azur_handle.py
index 0ae2dade..67242a77 100644
--- a/plugins/draw_card/handles/azur_handle.py
+++ b/zhenxun/plugins/draw_card/handles/azur_handle.py
@@ -1,304 +1,308 @@
-import random
-import dateparser
-from lxml import etree
-from typing import List, Optional, Tuple
-from PIL import ImageDraw
-from urllib.parse import unquote
-from pydantic import ValidationError
-from nonebot.log import logger
-from nonebot.adapters.onebot.v11 import Message, MessageSegment
-
-from utils.message_builder import image
-from .base_handle import BaseHandle, BaseData, UpEvent as _UpEvent, UpChar as _UpChar
-from ..config import draw_config
-from ..util import remove_prohibited_str, cn2py, load_font
-from utils.image_utils import BuildImage
-
-try:
- import ujson as json
-except ModuleNotFoundError:
- import json
-
-
-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: Optional[UpEvent] = 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],
- [
- 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
-
- def draw(self, count: int, **kwargs) -> Message:
- 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})
- return image(b64=self.generate_img(cards).pic2bs4()) + result
-
- 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)
- # 加圆角
- frame.circle_corner(6)
- img.circle_corner(6)
- bg.paste(img, (sep_w, sep_t), alpha=True)
- bg.paste(frame, (sep_w, sep_t), alpha=True)
- # 加名字
- text = card.name[:6] + "..." if len(card.name) > 7 else card.name
- font = load_font(fontsize=14)
- text_w, text_h = font.getsize(text)
- 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("./div/a/@title")[0]
- frame = char.xpath("./div/div/a/img/@alt")[0]
- avatar = char.xpath("./div/a/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():
- 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更新出错 {type(e)}:{e}")
-
- async def _reload_pool(self) -> Optional[Message]:
- await self.update_up_char()
- self.load_up_char()
- if self.UP_EVENT:
- return Message(f"重载成功!\n当前活动:{self.UP_EVENT.title}")
+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}"
+ )
diff --git a/plugins/draw_card/handles/ba_handle.py b/zhenxun/plugins/draw_card/handles/ba_handle.py
similarity index 84%
rename from plugins/draw_card/handles/ba_handle.py
rename to zhenxun/plugins/draw_card/handles/ba_handle.py
index 658bb2df..d347504a 100644
--- a/plugins/draw_card/handles/ba_handle.py
+++ b/zhenxun/plugins/draw_card/handles/ba_handle.py
@@ -1,15 +1,13 @@
import random
-from typing import List, Tuple
-from urllib.parse import unquote
-from lxml import etree
-from nonebot.log import logger
from PIL import ImageDraw
-from utils.http_utils import AsyncHttpx
-from utils.image_utils import BuildImage
+
+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, remove_prohibited_str
+from ..util import cn2py, load_font
from .base_handle import BaseData, BaseHandle
@@ -22,7 +20,7 @@ class BaHandle(BaseHandle[BaChar]):
super().__init__("ba", "碧蓝档案")
self.max_star = 3
self.config = draw_config.ba
- self.ALL_CHAR: List[BaChar] = []
+ self.ALL_CHAR: list[BaChar] = []
def get_card(self, mode: int = 1) -> BaChar:
if mode == 2:
@@ -37,7 +35,7 @@ class BaHandle(BaseHandle[BaChar]):
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]]:
+ def get_cards(self, count: int, **kwargs) -> list[tuple[BaChar, int]]:
card_list = []
card_count = 0 # 保底计算
for i in range(count):
@@ -53,7 +51,7 @@ class BaHandle(BaseHandle[BaChar]):
card_list.append((card, i + 1))
return card_list
- def generate_card_img(self, card: BaChar) -> BuildImage:
+ async def generate_card_img(self, card: BaChar) -> BuildImage:
sep_w = 5
sep_h = 5
star_h = 15
@@ -63,11 +61,13 @@ class BaHandle(BaseHandle[BaChar]):
bar_h = 20
bar_w = 90
bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5")
- img_path = str(self.img_path / f"{cn2py(card.name)}.png")
- img = BuildImage(img_w, img_h, background=img_path)
+ 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")
- bg.paste(img, (sep_w, sep_h), alpha=True)
- bg.paste(bar, (sep_w, img_h - bar_h + sep_h), alpha=True)
+ 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
@@ -78,12 +78,10 @@ class BaHandle(BaseHandle[BaChar]):
star_path = str(self.img_path / "star-3.png")
star_w = 45
star = BuildImage(star_w, star_h, background=star_path)
- bg.paste(
- star, (img_w // 2 - 15 * (card.star - 1) // 2, img_h - star_h), alpha=True
- )
+ 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 = font.getsize(text)
+ 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),
@@ -112,6 +110,7 @@ class BaHandle(BaseHandle[BaChar]):
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()
@@ -129,6 +128,7 @@ class BaHandle(BaseHandle[BaChar]):
+ ".webp"
)
star = char["StarGrade"]
+ star = char["StarGrade"]
except IndexError:
continue
member_dict = {
diff --git a/plugins/draw_card/handles/base_handle.py b/zhenxun/plugins/draw_card/handles/base_handle.py
similarity index 72%
rename from plugins/draw_card/handles/base_handle.py
rename to zhenxun/plugins/draw_card/handles/base_handle.py
index ac0ee79e..3483d246 100644
--- a/plugins/draw_card/handles/base_handle.py
+++ b/zhenxun/plugins/draw_card/handles/base_handle.py
@@ -1,27 +1,23 @@
-import math
-import anyio
-import random
-import aiohttp
import asyncio
-from PIL import Image
-from datetime import datetime
-from pydantic import BaseModel, Extra
+import random
from asyncio.exceptions import TimeoutError
-from typing import Dict, List, Optional, TypeVar, Generic, Tuple
-from nonebot.adapters.onebot.v11 import Message, MessageSegment
-from nonebot.log import logger
+from datetime import datetime
+from typing import Generic, TypeVar
-from configs.path_config import DATA_PATH
-from utils.message_builder import image
+import aiohttp
+import anyio
+import ujson as json
+from nonebot_plugin_alconna import UniMessage
+from PIL import Image
+from pydantic import BaseModel, Extra
-try:
- import ujson as json
-except ModuleNotFoundError:
- import json
+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 utils.image_utils import BuildImage
from ..config import DRAW_PATH, draw_config
-from ..util import cn2py, circled_number
+from ..util import circled_number, cn2py
class BaseData(BaseModel, extra=Extra.ignore):
@@ -47,9 +43,9 @@ class UpChar(BaseData):
class UpEvent(BaseModel):
title: str # up池标题
pool_img: str # up池封面
- start_time: Optional[datetime] # 开始时间
- end_time: Optional[datetime] # 结束时间
- up_char: List[UpChar] # up对象
+ start_time: datetime | None # 开始时间
+ end_time: datetime | None # 结束时间
+ up_char: list[UpChar] # up对象
TC = TypeVar("TC", bound="BaseData")
@@ -66,27 +62,28 @@ class BaseHandle(Generic[TC]):
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"]
+ self.data_files: list[str] = [f"{self.game_name}.json"]
- def draw(self, count: int, **kwargs) -> Message:
+ 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)
- return image(b64=self.generate_img(cards).pic2bs4()) + result
+ 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]]:
+ 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:
+ 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:
+ 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),
@@ -96,8 +93,8 @@ class BaseHandle(Generic[TC]):
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] = {} # 记录星级及其次数
+ 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:
@@ -112,7 +109,7 @@ class BaseHandle(Generic[TC]):
return rst.strip()
def format_max_star(
- self, card_list: List[Tuple[TC, int]], up_list: List[str] = [], **kwargs
+ self, card_list: list[tuple[TC, int]], up_list: list[str] = [], **kwargs
) -> str:
up_list = up_list or kwargs.get("up_list", [])
rst = ""
@@ -124,8 +121,8 @@ class BaseHandle(Generic[TC]):
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] = {} # 记录卡牌抽取次数
+ def format_max_card(self, card_list: list[TC], **kwargs) -> str:
+ card_dict: dict[TC, int] = {} # 记录卡牌抽取次数
for card in card_list:
try:
@@ -139,22 +136,22 @@ class BaseHandle(Generic[TC]):
return ""
return f"抽取到最多的是{max_card.name},共抽取了{max_count}次"
- def generate_img(
+ async def generate_img(
self,
- cards: List[TC],
+ cards: list[TC],
num_per_line: int = 5,
- max_per_line: Tuple[int, int] = (40, 10),
+ max_per_line: tuple[int, int] = (40, 10),
) -> BuildImage:
"""
生成统计图片
- :param cards: 卡牌列表
- :param num_per_line: 单行角色显示数量
- :param max_per_line: 当card_list超过一定数值时,更改单行数量
+ 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] = {} # 记录卡牌抽取次数
+ card_dict: dict[TC, int] = {} # 记录卡牌抽取次数
for card in cards:
try:
card_dict[card] += 1
@@ -166,37 +163,37 @@ class BaseHandle(Generic[TC]):
card_list = cards
num_list = [1] * len(cards)
- card_imgs: List[BuildImage] = []
+ card_imgs: list[BuildImage] = []
for card, num in zip(card_list, num_list):
- card_img = self.generate_card_img(card)
+ card_img = await self.generate_card_img(card)
# 数量 > 1 时加数字上标
if num > 1:
label = circled_number(num)
- label_w = int(min(card_img.w, card_img.h) / 7)
+ 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,
+ Image.ANTIALIAS, # type: ignore
)
- card_img.paste(label, alpha=True)
+ await card_img.paste(label)
card_imgs.append(card_img)
- img_w = card_imgs[0].w
- img_h = card_imgs[0].h
- 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:
- img.paste(card_img)
- return 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
- def generate_card_img(self, card: TC) -> BuildImage:
+ 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)
@@ -273,22 +270,26 @@ class BaseHandle(Generic[TC]):
await f.write(await response.read())
return True
except TimeoutError:
- logger.warning(f"下载 {self.game_name_cn} 图片超时,名称:{name},url:{url}")
+ logger.warning(
+ f"下载 {self.game_name_cn} 图片超时,名称:{name},url:{url}"
+ )
return False
except:
- logger.warning(f"下载 {self.game_name_cn} 链接错误,名称:{name},url:{url}")
+ logger.warning(
+ f"下载 {self.game_name_cn} 链接错误,名称:{name},url:{url}"
+ )
return False
- async def _reload_pool(self) -> Optional[Message]:
+ async def _reload_pool(self) -> UniMessage | None:
return None
- async def reload_pool(self) -> Optional[Message]:
+ 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池错误:{type(e)}:{e}")
+ logger.warning(f"{self.game_name_cn} 重载UP池错误", e=e)
- def reset_count(self, user_id: int) -> bool:
+ def reset_count(self, user_id: str) -> bool:
return False
diff --git a/plugins/draw_card/handles/fgo_handle.py b/zhenxun/plugins/draw_card/handles/fgo_handle.py
similarity index 88%
rename from plugins/draw_card/handles/fgo_handle.py
rename to zhenxun/plugins/draw_card/handles/fgo_handle.py
index a491b906..5acb8c5f 100644
--- a/plugins/draw_card/handles/fgo_handle.py
+++ b/zhenxun/plugins/draw_card/handles/fgo_handle.py
@@ -1,221 +1,223 @@
-import random
-from lxml import etree
-from typing import List, Tuple
-from PIL import ImageDraw
-from nonebot.log import logger
-
-try:
- import ujson as json
-except ModuleNotFoundError:
- import json
-
-from .base_handle import BaseHandle, BaseData
-from ..config import draw_config
-from ..util import remove_prohibited_str, cn2py, load_font
-from utils.image_utils import BuildImage
-
-
-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
-
- 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)
- bg.paste(img, (sep_w, sep_t), alpha=True)
- # 加名字
- text = card.name[:6] + "..." if len(card.name) > 7 else card.name
- font = load_font(fontsize=16)
- text_w, text_h = font.getsize(text)
- 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):
- # fgo.json
- 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("
")[:-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["名称"])
+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("
")[:-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["名称"])
diff --git a/plugins/draw_card/handles/genshin_handle.py b/zhenxun/plugins/draw_card/handles/genshin_handle.py
similarity index 84%
rename from plugins/draw_card/handles/genshin_handle.py
rename to zhenxun/plugins/draw_card/handles/genshin_handle.py
index 71e79544..61edcf30 100644
--- a/plugins/draw_card/handles/genshin_handle.py
+++ b/zhenxun/plugins/draw_card/handles/genshin_handle.py
@@ -1,448 +1,461 @@
-import random
-import dateparser
-from lxml import etree
-from PIL import Image, ImageDraw
-from urllib.parse import unquote
-from typing import List, Optional, Tuple
-from pydantic import ValidationError
-from datetime import datetime, timedelta
-from nonebot.adapters.onebot.v11 import Message, MessageSegment
-from nonebot.log import logger
-
-from utils.message_builder import image
-
-try:
- import ujson as json
-except ModuleNotFoundError:
- import json
-
-from .base_handle import BaseHandle, BaseData, UpChar, UpEvent
-from ..config import draw_config
-from ..count_manager import GenshinCountManager
-from ..util import remove_prohibited_str, cn2py, load_font
-from utils.image_utils import BuildImage
-
-
-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: Optional[UpEvent] = None
- self.UP_CHAR_LIST: Optional[UpEvent] = []
- self.UP_ARMS: Optional[UpEvent] = 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
-
- 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 = font.getsize(text)
- 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)
- bg.paste(frame, (sep_w, sep_h), alpha=True)
- bg.paste(img, (sep_w + 3, sep_h + 3), alpha=True)
- bg.paste(star, (sep_w + int((frame_w - star.width) / 2), sep_h - 6), alpha=True)
- 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()
-
- def draw(self, count: int, user_id: int, pool_name: str = "", **kwargs) -> Message:
- 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 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 = self.generate_img(cards)
- bk = BuildImage(img.w, img.h + 50, font_size=20, color="#ebebeb")
- bk.paste(img)
- bk.text((0, img.h + 10), "【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】")
- return pool_info + image(b64=bk.pic2bs4()) + 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更新出错 {type(e)}:{e}")
-
- def reset_count(self, user_id: int) -> bool:
- self.count_manager.reset(user_id)
- return True
-
- async def _reload_pool(self) -> Optional[Message]:
- 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 Message(
- Message.template("重载成功!\n当前UP池子:{} & {} & {}{:image}{:image}{:image}").format(
- 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 Message(
- Message.template("重载成功!\n当前UP池子:{} & {}{:image}{:image}").format(
- char_title,
- self.UP_ARMS.title,
- self.UP_CHAR_LIST[0].pool_img,
- self.UP_ARMS.pool_img,
- )
- )
+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,
+ ]
+ )
diff --git a/plugins/draw_card/handles/guardian_handle.py b/zhenxun/plugins/draw_card/handles/guardian_handle.py
similarity index 88%
rename from plugins/draw_card/handles/guardian_handle.py
rename to zhenxun/plugins/draw_card/handles/guardian_handle.py
index 33e9449b..517f126d 100644
--- a/plugins/draw_card/handles/guardian_handle.py
+++ b/zhenxun/plugins/draw_card/handles/guardian_handle.py
@@ -1,400 +1,400 @@
-import re
-import random
-import dateparser
-from lxml import etree
-from PIL import ImageDraw
-from datetime import datetime
-from urllib.parse import unquote
-from typing import List, Optional, Tuple
-from pydantic import ValidationError
-from nonebot.adapters.onebot.v11 import Message, MessageSegment
-from nonebot.log import logger
-
-from utils.message_builder import image
-
-try:
- import ujson as json
-except ModuleNotFoundError:
- import json
-
-from .base_handle import BaseHandle, BaseData, UpChar, UpEvent
-from ..config import draw_config
-from ..util import remove_prohibited_str, cn2py, load_font
-from utils.image_utils import BuildImage
-
-
-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: Optional[UpEvent] = None
- self.UP_ARMS: Optional[UpEvent] = 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()
-
- def draw(self, count: int, pool_name: str, **kwargs) -> Message:
- 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)
- return pool_info + image(b64=self.generate_img(cards).pic2bs4()) + result
-
- 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)
- block.paste(img, (0, 0), alpha=True)
- if frame_path:
- frame = BuildImage(img_w, img_h, background=frame_path)
- block.paste(frame, (0, 0), alpha=True)
- block.paste(
- star,
- (int((block_w + img_w - star_w) / 2), block_h - star_h - 30),
- alpha=True,
- )
- # 加名字
- text = card.name[:4] + "..." if len(card.name) > 5 else card.name
- font = load_font(fontsize=14)
- text_w, _ = font.getsize(text)
- draw = ImageDraw.Draw(block.markImg)
- draw.text(
- ((block_w + img_w - text_w) / 2, 55),
- text,
- font=font,
- fill=font_color,
- )
- 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(
- "//div[@class='resp-tabs-container']/div[1]/div/table[2]/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} 武器更新成功")
- 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) -> Optional[Message]:
- await self.update_up_char()
- self.load_up_char()
- if self.UP_CHAR and self.UP_ARMS:
- return Message(f"重载成功!\n当前UP池子:{self.UP_CHAR.title}")
+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}"
+ )
diff --git a/plugins/draw_card/handles/onmyoji_handle.py b/zhenxun/plugins/draw_card/handles/onmyoji_handle.py
similarity index 78%
rename from plugins/draw_card/handles/onmyoji_handle.py
rename to zhenxun/plugins/draw_card/handles/onmyoji_handle.py
index 5797e830..25d05c38 100644
--- a/plugins/draw_card/handles/onmyoji_handle.py
+++ b/zhenxun/plugins/draw_card/handles/onmyoji_handle.py
@@ -1,179 +1,178 @@
-import random
-from lxml import etree
-from typing import List, Tuple
-from nonebot.log import logger
-from PIL import Image, ImageDraw
-from PIL.Image import Image as IMG
-
-try:
- import ujson as json
-except ModuleNotFoundError:
- import json
-
-from .base_handle import BaseHandle, BaseData
-from ..config import draw_config
-from ..util import remove_prohibited_str, cn2py, load_font
-from utils.image_utils import BuildImage
-
-
-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
-
- def generate_img(self, card_list: List[OnmyojiChar]) -> BuildImage:
- return super().generate_img(card_list, num_per_line=10)
-
- 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)
- bg.paste(img, (0, 0), alpha=True)
- bg.paste(label, (0, 135), alpha=True)
- 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")
+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")
diff --git a/plugins/draw_card/handles/pcr_handle.py b/zhenxun/plugins/draw_card/handles/pcr_handle.py
similarity index 86%
rename from plugins/draw_card/handles/pcr_handle.py
rename to zhenxun/plugins/draw_card/handles/pcr_handle.py
index d82ee27a..666a6842 100644
--- a/plugins/draw_card/handles/pcr_handle.py
+++ b/zhenxun/plugins/draw_card/handles/pcr_handle.py
@@ -1,147 +1,149 @@
-import random
-from lxml import etree
-from typing import List, Tuple
-from PIL import ImageDraw
-from urllib.parse import unquote
-from nonebot.log import logger
-
-from .base_handle import BaseHandle, BaseData
-from ..config import draw_config
-from ..util import remove_prohibited_str, cn2py, load_font
-from utils.image_utils import BuildImage
-
-
-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
-
- 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)
- bg.paste(img, (sep_w, sep_h), alpha=True)
- for i in range(card.star):
- bg.paste(star, (sep_w + img_w - star_h * (i + 1), sep_h), alpha=True)
- # 加名字
- text = card.name[:5] + "..." if len(card.name) > 6 else card.name
- font = load_font(fontsize=14)
- text_w, text_h = font.getsize(text)
- 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())
- char_list = dom.xpath(
- "//div[@class='resp-tab-content']/div[@class='unit-icon']"
- )
- 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",
- )
+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",
+ )
diff --git a/plugins/draw_card/handles/pretty_handle.py b/zhenxun/plugins/draw_card/handles/pretty_handle.py
similarity index 88%
rename from plugins/draw_card/handles/pretty_handle.py
rename to zhenxun/plugins/draw_card/handles/pretty_handle.py
index 5558e268..535e2b19 100644
--- a/plugins/draw_card/handles/pretty_handle.py
+++ b/zhenxun/plugins/draw_card/handles/pretty_handle.py
@@ -1,424 +1,423 @@
-import re
-import random
-import dateparser
-from lxml import etree
-from PIL import ImageDraw
-from bs4 import BeautifulSoup
-from datetime import datetime
-from urllib.parse import unquote
-from typing import List, Optional, Tuple
-from pydantic import ValidationError
-from nonebot.adapters.onebot.v11 import Message, MessageSegment
-from nonebot.log import logger
-
-from utils.message_builder import image
-
-try:
- import ujson as json
-except ModuleNotFoundError:
- import json
-
-from .base_handle import BaseHandle, BaseData, UpChar, UpEvent
-from ..config import draw_config
-from ..util import remove_prohibited_str, cn2py, load_font
-from utils.image_utils import BuildImage
-
-
-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: Optional[UpEvent] = None
- self.UP_CARD: Optional[UpEvent] = 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()
-
- def draw(self, count: int, pool_name: str, **kwargs) -> Message:
- 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)
- return pool_info + image(b64=self.generate_img(cards).pic2bs4()) + result
-
- 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):
- bg.paste(star, (int((img_w - star_w) / 2) + star_h * i, 0), alpha=True)
- bg.paste(img, (0, 0), alpha=True)
- # 加名字
- 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)
- bg.paste(img, (sep_w, 0), alpha=True)
- bg.paste(label, (30, 3), alpha=True)
- # 加名字
- text = ""
- texts = []
- font = load_font(fontsize=25)
- for t in card.name:
- if font.getsize(text + t)[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更新出错 {type(e)}:{e}")
-
- async def _reload_pool(self) -> Optional[Message]:
- await self.update_up_char()
- self.load_up_char()
- if self.UP_CHAR and self.UP_CARD:
- return Message(
- Message.template("重载成功!\n当前UP池子:{}{:image}{:image}").format(
- self.UP_CHAR.title,
- self.UP_CHAR.pool_img,
- self.UP_CARD.pool_img,
- )
- )
+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,
+ ]
+ )
diff --git a/plugins/draw_card/handles/prts_handle.py b/zhenxun/plugins/draw_card/handles/prts_handle.py
similarity index 74%
rename from plugins/draw_card/handles/prts_handle.py
rename to zhenxun/plugins/draw_card/handles/prts_handle.py
index af0c5be2..18a86fc3 100644
--- a/plugins/draw_card/handles/prts_handle.py
+++ b/zhenxun/plugins/draw_card/handles/prts_handle.py
@@ -1,325 +1,344 @@
-import random
-import re
-from datetime import datetime
-from typing import List, Optional, Tuple
-from urllib.parse import unquote
-
-import dateparser
-from PIL import ImageDraw
-from lxml import etree
-from nonebot.adapters.onebot.v11 import Message, MessageSegment
-from nonebot.log import logger
-from pydantic import ValidationError
-
-from utils.message_builder import image
-
-try:
- import ujson as json
-except ModuleNotFoundError:
- import json
-
-from .base_handle import BaseHandle, BaseData, UpChar, UpEvent
-from ..config import draw_config
-from ..util import remove_prohibited_str, cn2py, load_font
-from utils.image_utils import BuildImage
-
-
-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: Optional[UpEvent] = 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()
-
- def draw(self, count: int, **kwargs) -> Message:
- 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()
- return (
- pool_info
- + image(b64=self.generate_img(cards).pic2bs4())
- + result
- )
-
- 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)
- bg.paste(img, (sep_w, sep_h), alpha=True)
- for i in range(card.star):
- bg.paste(star, (sep_w + img_w - 5 - star_h * (i + 1), sep_h), alpha=True)
- # 加名字
- text = card.name[:7] + "..." if len(card.name) > 8 else card.name
- font = load_font(fontsize=16)
- text_w, text_h = font.getsize(text)
- 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 = 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[2]/a/text()")[0]
- star = char.xpath("./td[5]/text()")[0]
- """这里sources修好了干员获取标签有问题的bug,如三星只能抽到卡缇就是这个原因"""
- sources = [_.strip('\n') for _ in char.xpath("./td[8]/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)
- """因为 的诡异排版,所以有了下面的一段"""
- 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) -> Optional[Message]:
- await self.update_up_char()
- self.load_up_char()
- if self.UP_EVENT:
- return f"重载成功!\n当前UP池子:{self.UP_EVENT.title}" + MessageSegment.image(
- self.UP_EVENT.pool_img
- )
+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)
+ """因为
的诡异排版,所以有了下面的一段"""
+ 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,
+ ]
+ )
diff --git a/plugins/draw_card/rule.py b/zhenxun/plugins/draw_card/rule.py
similarity index 81%
rename from plugins/draw_card/rule.py
rename to zhenxun/plugins/draw_card/rule.py
index fbb42096..49746d95 100644
--- a/plugins/draw_card/rule.py
+++ b/zhenxun/plugins/draw_card/rule.py
@@ -1,6 +1,6 @@
from nonebot.internal.rule import Rule
-from configs.config import Config
+from zhenxun.configs.config import Config
def rule(game) -> Rule:
@@ -8,4 +8,3 @@ def rule(game) -> Rule:
return Config.get_config("draw_card", game.config_name, True)
return Rule(_rule)
-
diff --git a/plugins/draw_card/util.py b/zhenxun/plugins/draw_card/util.py
similarity index 90%
rename from plugins/draw_card/util.py
rename to zhenxun/plugins/draw_card/util.py
index 860e35eb..d0cefc91 100644
--- a/plugins/draw_card/util.py
+++ b/zhenxun/plugins/draw_card/util.py
@@ -1,60 +1,61 @@
-import platform
-import pypinyin
-from pathlib import Path
-from PIL.ImageFont import FreeTypeFont
-from PIL import Image, ImageDraw, ImageFont
-from PIL.Image import Image as IMG
-
-from configs.path_config import FONT_PATH
-
-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 = font.getsize(text)[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
-
+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
diff --git a/zhenxun/plugins/epic/__init__.py b/zhenxun/plugins/epic/__init__.py
new file mode 100644
index 00000000..23c084aa
--- /dev/null
+++ b/zhenxun/plugins/epic/__init__.py
@@ -0,0 +1,40 @@
+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)
diff --git a/plugins/epic/data_source.py b/zhenxun/plugins/epic/data_source.py
old mode 100755
new mode 100644
similarity index 66%
rename from plugins/epic/data_source.py
rename to zhenxun/plugins/epic/data_source.py
index f9b5f4ea..87bc5a63
--- a/plugins/epic/data_source.py
+++ b/zhenxun/plugins/epic/data_source.py
@@ -1,196 +1,182 @@
-from datetime import datetime
-from nonebot.log import logger
-from nonebot.adapters.onebot.v11 import Bot
-from configs.config import NICKNAME
-from utils.http_utils import AsyncHttpx
-
-
-# 获取所有 Epic Game Store 促销游戏
-# 方法参考:RSSHub /epicgames 路由
-# https://github.com/DIYgod/RSSHub/blob/master/lib/v2/epicgames/index.js
-async def get_epic_game():
- 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 访问接口错误 {type(e)}:{e}")
- return None
-
-# 此处用于获取游戏简介
-async def get_epic_game_desp(name):
- 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 访问接口错误 {type(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):
- 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 = "\n由 {} 公司发行的游戏 {} ({}) 在 UTC 时间 {} 即将推出免费游玩,预计截至 {}。".format(
- game_corp, game_name, game_price, start_date, end_date
- )
- data = {
- "type": "node",
- "data": {
- "name": f"这里是{NICKNAME}酱",
- "uin": f"{bot.self_id}",
- "content": _message,
- },
- }
- msg_list.append(data)
- 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"):
- 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 type_event == "Group":
- _message = "[CQ:image,file={}]\n\nFREE now :: {} ({})\n{}\n此游戏由 {} 开发、{} 发行,将在 UTC 时间 {} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{}\n".format(
- game_thumbnail,
- game_name,
- game_price,
- game_desp,
- game_dev,
- game_pub,
- end_date,
- game_url,
- )
- data = {
- "type": "node",
- "data": {
- "name": f"这里是{NICKNAME}酱",
- "uin": f"{bot.self_id}",
- "content": _message,
- },
- }
- msg_list.append(data)
- else:
- msg = "[CQ:image,file={}]\n\nFREE now :: {} ({})\n{}\n此游戏由 {} 开发、{} 发行,将在 UTC 时间 {} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{}\n".format(
- game_thumbnail,
- game_name,
- game_price,
- game_desp,
- game_dev,
- game_pub,
- end_date,
- game_url,
- )
- msg_list.append(msg)
- except TypeError as e:
- # logger.info(str(e))
- pass
- return msg_list, 200
+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.configs.config import NICKNAME
+from zhenxun.services.log import logger
+from zhenxun.utils._build_image import BuildImage
+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
diff --git a/zhenxun/plugins/fudu.py b/zhenxun/plugins/fudu.py
new file mode 100644
index 00000000..6b1348a0
--- /dev/null
+++ b/zhenxun/plugins/fudu.py
@@ -0,0 +1,151 @@
+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 NICKNAME, 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.HIDDEN,
+ 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"@可爱的{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()
diff --git a/zhenxun/plugins/gold_redbag/__init__.py b/zhenxun/plugins/gold_redbag/__init__.py
new file mode 100644
index 00000000..f46c6639
--- /dev/null
+++ b/zhenxun/plugins/gold_redbag/__init__.py
@@ -0,0 +1,349 @@
+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 NICKNAME
+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"{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"{NICKNAME}的红包",
+ amount,
+ num,
+ 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"{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()
diff --git a/plugins/gold_redbag/config.py b/zhenxun/plugins/gold_redbag/config.py
similarity index 55%
rename from plugins/gold_redbag/config.py
rename to zhenxun/plugins/gold_redbag/config.py
index a2fe4c55..da8c0d39 100644
--- a/plugins/gold_redbag/config.py
+++ b/zhenxun/plugins/gold_redbag/config.py
@@ -1,23 +1,48 @@
import random
import time
-from datetime import datetime
from io import BytesIO
-from typing import Dict, List, Optional, Tuple, Union, overload
+from typing import Dict
from pydantic import BaseModel
-from models.bag_user import BagUser
-from models.group_member_info import GroupInfoUser
-from plugins.gold_redbag.model import RedbagUser
-from utils.image_utils import BuildImage
-from utils.utils import get_user_avatar
+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 RedBag(BaseModel):
+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):
"""
红包
"""
@@ -38,19 +63,23 @@ class RedBag(BaseModel):
"""是否为节日红包"""
timeout: int
"""过期时间"""
- assigner: Optional[str] = None
+ assigner: str | None = None
"""指定人id"""
start_time: float
"""红包发起时间"""
open_user: Dict[str, int] = {}
"""开启用户"""
- red_bag_list: List[int]
+ red_bag_list: list[int]
+ """红包金额列表"""
+ uuid: str | None
+ """uuid"""
- async def build_amount_rank(self, num: int = 10) -> BuildImage:
+ async def build_amount_rank(self, num: int, platform: str) -> BuildImage:
"""生成结算红包图片
参数:
num: 查看的排名数量.
+ platform: 平台.
返回:
BuildImage: 结算红包图片
@@ -68,78 +97,100 @@ class RedBag(BaseModel):
for i in range(num):
user_background = BuildImage(600, 100, font_size=30)
user_id, amount = sort_data[i]
- user_ava_bytes = await get_user_avatar(user_id)
+ 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.acircle_corner(10)
- await user_background.apaste(user_ava, (130, 10), True)
+ 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.atext((0, 0), f"{i+1}", center_type="center")
- await no_image.aline((99, 10, 99, 90), "#b9b9b9")
- await user_background.apaste(no_image)
+ 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.atext((225, 15), name[0] if name else "")
- amount_image = BuildImage(
- 0, 0, plain_text=f"{amount} 元", font_size=30, font_color="#cdac72"
+ 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.apaste(
- amount_image, (user_background.w - amount_image.w - 20, 50), True
+ await user_background.paste(
+ amount_image, (user_background.width - amount_image.width - 20, 50)
)
- await user_background.aline((225, 99, 590, 99), "#b9b9b9")
+ 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 get_user_avatar(self.promoter_id)
+ 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.acircle()
- await top.apaste(promoter_ava, (10, 0), True, "by_height")
- await top.atext((80, 33), self.name, (255, 255, 255))
+ 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.atext((10, 33), "结算排行", (255, 255, 255))
- await right_text.aline((4, 10, 4, 90), (255, 255, 255), 2)
- await top.apaste(right_text, (460, 0))
- await background.apaste(top)
+ 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.apaste(user_image, (0, cur_h))
- cur_h += user_image.h
+ await background.paste(user_image, (0, cur_h))
+ cur_h += user_image.height
return background
class GroupRedBag:
-
"""
群组红包管理
"""
- def __init__(self, group_id: Union[int, str]):
- self.group_id = str(group_id)
+ def __init__(self, group_id: str):
+ self.group_id = group_id
self._data: Dict[str, RedBag] = {}
"""红包列表"""
- def get_user_red_bag(self, user_id: Union[str, int]) -> Optional[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
返回:
- Optional[RedBag]: RedBag
+ RedBag | None: RedBag
"""
return self._data.get(str(user_id))
- def check_open(self, user_id: Union[str, int]) -> bool:
+ def check_open(self, user_id: str) -> bool:
"""检查是否有可开启的红包
参数:
@@ -158,7 +209,7 @@ class GroupRedBag:
return True
return False
- def check_timeout(self, user_id: Union[int, str]) -> int:
+ def check_timeout(self, user_id: str) -> int:
"""判断用户红包是否过期
参数:
@@ -167,7 +218,6 @@ class GroupRedBag:
返回:
int: 距离过期时间
"""
- user_id = str(user_id)
if user_id in self._data:
reg_bag = self._data[user_id]
now = time.time()
@@ -176,22 +226,26 @@ class GroupRedBag:
return -1
async def open(
- self, user_id: Union[int, str]
- ) -> Tuple[Dict[str, Tuple[int, RedBag]], List[RedBag]]:
+ 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]: 开完的红包
+ Dict[str, tuple[int, RedBag]]: 键为发起者id, 值为开启金额以及对应RedBag
+ list[RedBag]: 开完的红包
"""
- user_id = str(user_id)
open_data = {}
- settlement_list: List[RedBag] = []
+ 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
@@ -202,7 +256,9 @@ class GroupRedBag:
await RedbagUser.add_redbag_data(
user_id, self.group_id, "get", random_amount
)
- await BagUser.add_gold(user_id, self.group_id, 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):
@@ -214,11 +270,11 @@ class GroupRedBag:
del self._data[uid]
return open_data, settlement_list
- def festive_red_bag_expire(self) -> Optional[RedBag]:
+ def festive_red_bag_expire(self) -> RedBag | None:
"""节日红包过期
返回:
- Optional[RedBag]: 过期的节日红包
+ RedBag | None: 过期的节日红包
"""
if FESTIVE_KEY in self._data:
red_bag = self._data[FESTIVE_KEY]
@@ -227,26 +283,27 @@ class GroupRedBag:
return None
async def settlement(
- self, user_id: Optional[Union[int, str]] = None
- ) -> Optional[int]:
+ self, user_id: str, platform: str | None = None
+ ) -> tuple[int | None, RedBag | None]:
"""红包退回
参数:
user_id: 用户id, 指定id时结算指定用户红包.
+ platform: 用户平台
返回:
- int: 退回金币
+ tuple[int | None, RedBag | None]: 退回金币, 红包
"""
- user_id = str(user_id)
- if user_id:
- if red_bag := self._data.get(user_id):
- del self._data[user_id]
- if red_bag.red_bag_list:
- # 退还剩余金币
- if amount := sum(red_bag.red_bag_list):
- await BagUser.add_gold(user_id, self.group_id, amount)
- return amount
- return 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,
@@ -255,9 +312,10 @@ class GroupRedBag:
num: int,
promoter: str,
promoter_id: str,
- is_festival: bool = False,
+ festival_uuid: str | None = None,
timeout: int = 60,
- assigner: Optional[str] = None,
+ assigner: str | None = None,
+ platform: str | None = None,
):
"""添加红包
@@ -267,17 +325,19 @@ class GroupRedBag:
num: 红包数量
promoter: 发起人昵称
promoter_id: 发起人id
- is_festival: 是否为节日红包.
+ festival_uuid: 节日红包uuid.
timeout: 超时时间.
assigner: 指定人.
+ platform: 用户平台.
"""
- user_gold = await BagUser.get_gold(promoter_id, self.group_id)
- if not is_festival and (amount < 1 or user_gold < amount):
+ 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 is_festival:
- await BagUser.spend_gold(promoter_id, self.group_id, amount)
+ 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,
@@ -285,14 +345,15 @@ class GroupRedBag:
num=num,
promoter=promoter,
promoter_id=promoter_id,
- is_festival=is_festival,
+ 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]:
+ def _random_red_bag(self, amount: int, num: int) -> list[int]:
"""初始化红包金币
参数:
@@ -300,7 +361,7 @@ class GroupRedBag:
num: 红包数量
返回:
- List[int]: 红包列表
+ list[int]: 红包列表
"""
red_bag_list = []
for _ in range(num - 1):
diff --git a/zhenxun/plugins/gold_redbag/data_source.py b/zhenxun/plugins/gold_redbag/data_source.py
new file mode 100644
index 00000000..ec903100
--- /dev/null
+++ b/zhenxun/plugins/gold_redbag/data_source.py
@@ -0,0 +1,234 @@
+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 NICKNAME, 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"{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"{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
diff --git a/plugins/gold_redbag/model.py b/zhenxun/plugins/gold_redbag/model.py
old mode 100755
new mode 100644
similarity index 86%
rename from plugins/gold_redbag/model.py
rename to zhenxun/plugins/gold_redbag/model.py
index 76755b0d..a8e9359a
--- a/plugins/gold_redbag/model.py
+++ b/zhenxun/plugins/gold_redbag/model.py
@@ -1,8 +1,6 @@
-from typing import List
-
from tortoise import fields
-from services.db_context import Model
+from zhenxun.services.db_context import Model
class RedbagUser(Model):
@@ -31,14 +29,13 @@ class RedbagUser(Model):
async def add_redbag_data(
cls, user_id: str, group_id: str, i_type: str, money: int
):
- """
- 说明:
- 添加收发红包数据
+ """添加收发红包数据
+
参数:
- :param user_id: 用户id
- :param group_id: 群号
- :param i_type: 收或发
- :param money: 金钱数量
+ user_id: 用户id
+ group_id: 群号
+ i_type: 收或发
+ money: 金钱数量
"""
user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id)
diff --git a/zhenxun/plugins/group_welcome_msg.py b/zhenxun/plugins/group_welcome_msg.py
new file mode 100644
index 00000000..7148e8e9
--- /dev/null
+++ b/zhenxun/plugins/group_welcome_msg.py
@@ -0,0 +1,62 @@
+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)
diff --git a/plugins/image_management/__init__.py b/zhenxun/plugins/image_management/__init__.py
old mode 100755
new mode 100644
similarity index 57%
rename from plugins/image_management/__init__.py
rename to zhenxun/plugins/image_management/__init__.py
index 16a9738f..8f24386d
--- a/plugins/image_management/__init__.py
+++ b/zhenxun/plugins/image_management/__init__.py
@@ -3,15 +3,14 @@ from typing import List, Tuple
import nonebot
-from configs.config import Config
-from configs.path_config import IMAGE_PATH
+from zhenxun.configs.config import Config
+from zhenxun.configs.path_config import IMAGE_PATH
Config.add_plugin_config(
"image_management",
"IMAGE_DIR_LIST",
["美图", "萝莉", "壁纸"],
- name="图库操作",
- help_="公开图库列表,可自定义添加 [如果含有send_setu插件,请不要添加色图库]",
+ help="公开图库列表,可自定义添加 [如果含有send_setu插件,请不要添加色图库]",
default_value=[],
type=List[str],
)
@@ -20,35 +19,34 @@ Config.add_plugin_config(
"image_management",
"WITHDRAW_IMAGE_MESSAGE",
(0, 1),
- name="图库操作",
- help_="自动撤回,参1:延迟撤回发送图库图片的时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)",
+ 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 [LEVEL]",
+ "DELETE_IMAGE_LEVEL",
7,
- help_="删除图库图片需要的管理员等级",
+ help="删除图库图片需要的管理员等级",
default_value=7,
type=int,
)
Config.add_plugin_config(
"image_management:move_image",
- "MOVE_IMAGE_LEVEL [LEVEL]",
+ "MOVE_IMAGE_LEVEL",
7,
- help_="移动图库图片需要的管理员等级",
+ help="移动图库图片需要的管理员等级",
default_value=7,
type=int,
)
Config.add_plugin_config(
"image_management:upload_image",
- "UPLOAD_IMAGE_LEVEL [LEVEL]",
+ "UPLOAD_IMAGE_LEVEL",
6,
- help_="上传图库图片需要的管理员等级",
+ help="上传图库图片需要的管理员等级",
default_value=6,
type=int,
)
@@ -57,11 +55,13 @@ Config.add_plugin_config(
"image_management",
"SHOW_ID",
True,
- help_="是否消息显示图片下标id",
+ help="是否消息显示图片下标id",
default_value=True,
type=bool,
)
+Config.set_name("image_management", "图库操作")
+
(IMAGE_PATH / "image_management").mkdir(parents=True, exist_ok=True)
diff --git a/zhenxun/plugins/image_management/_config.py b/zhenxun/plugins/image_management/_config.py
new file mode 100644
index 00000000..d5e01f58
--- /dev/null
+++ b/zhenxun/plugins/image_management/_config.py
@@ -0,0 +1,14 @@
+from strenum import StrEnum
+
+
+class ImageHandleType(StrEnum):
+ """
+ 图片处理类型
+ """
+
+ UPLOAD = "UPLOAD"
+ """上传"""
+ DELETE = "DELETE"
+ """删除"""
+ MOVE = "MOVE"
+ """移动"""
diff --git a/zhenxun/plugins/image_management/_data_source.py b/zhenxun/plugins/image_management/_data_source.py
new file mode 100644
index 00000000..bc26b74f
--- /dev/null
+++ b/zhenxun/plugins/image_management/_data_source.py
@@ -0,0 +1,196 @@
+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}"
diff --git a/zhenxun/plugins/image_management/delete_image.py b/zhenxun/plugins/image_management/delete_image.py
new file mode 100644
index 00000000..6cabb8e9
--- /dev/null
+++ b/zhenxun/plugins/image_management/delete_image.py
@@ -0,0 +1,107 @@
+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()
diff --git a/zhenxun/plugins/image_management/image_management_log.py b/zhenxun/plugins/image_management/image_management_log.py
new file mode 100644
index 00000000..756e58f2
--- /dev/null
+++ b/zhenxun/plugins/image_management/image_management_log.py
@@ -0,0 +1,27 @@
+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 = "画廊操作记录"
diff --git a/zhenxun/plugins/image_management/move_image.py b/zhenxun/plugins/image_management/move_image.py
new file mode 100644
index 00000000..6c1ec5e9
--- /dev/null
+++ b/zhenxun/plugins/image_management/move_image.py
@@ -0,0 +1,137 @@
+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()
diff --git a/plugins/__init__.py b/zhenxun/plugins/image_management/send_image.py
old mode 100755
new mode 100644
similarity index 100%
rename from plugins/__init__.py
rename to zhenxun/plugins/image_management/send_image.py
diff --git a/zhenxun/plugins/image_management/upload_image.py b/zhenxun/plugins/image_management/upload_image.py
new file mode 100644
index 00000000..b69281a4
--- /dev/null
+++ b/zhenxun/plugins/image_management/upload_image.py
@@ -0,0 +1,195 @@
+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()
diff --git a/zhenxun/plugins/luxun.py b/zhenxun/plugins/luxun.py
new file mode 100644
index 00000000..1c1ab09c
--- /dev/null
+++ b/zhenxun/plugins/luxun.py
@@ -0,0 +1,74 @@
+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)
diff --git a/zhenxun/plugins/mute/__init__.py b/zhenxun/plugins/mute/__init__.py
new file mode 100644
index 00000000..eb35e275
--- /dev/null
+++ b/zhenxun/plugins/mute/__init__.py
@@ -0,0 +1,5 @@
+from pathlib import Path
+
+import nonebot
+
+nonebot.load_plugins(str(Path(__file__).parent.resolve()))
diff --git a/zhenxun/plugins/mute/_data_source.py b/zhenxun/plugins/mute/_data_source.py
new file mode 100644
index 00000000..206de400
--- /dev/null
+++ b/zhenxun/plugins/mute/_data_source.py
@@ -0,0 +1,124 @@
+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"),
+ time=base_config.get("MUTE_DEFAULT_TIME"),
+ duration=base_config.get("MUTE_DEFAULT_DURATION"),
+ )
+ 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()
diff --git a/zhenxun/plugins/mute/mute_message.py b/zhenxun/plugins/mute/mute_message.py
new file mode 100644
index 00000000..c2c476f0
--- /dev/null
+++ b/zhenxun/plugins/mute/mute_message.py
@@ -0,0 +1,53 @@
+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 NICKNAME
+from zhenxun.configs.utils import PluginExtraData
+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.HIDDEN,
+ ).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
+ 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"检测到恶意刷屏,{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)
diff --git a/zhenxun/plugins/mute/mute_setting.py b/zhenxun/plugins/mute/mute_setting.py
new file mode 100644
index 00000000..f723f075
--- /dev/null
+++ b/zhenxun/plugins/mute/mute_setting.py
@@ -0,0 +1,117 @@
+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 NICKNAME
+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="""
+ 刷屏禁言相关操作,需要 {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()
diff --git a/zhenxun/plugins/nbnhhsh.py b/zhenxun/plugins/nbnhhsh.py
new file mode 100644
index 00000000..7ab78aaa
--- /dev/null
+++ b/zhenxun/plugins/nbnhhsh.py
@@ -0,0 +1,62 @@
+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()
diff --git a/zhenxun/plugins/one_friend/__init__.py b/zhenxun/plugins/one_friend/__init__.py
new file mode 100644
index 00000000..0191084f
--- /dev/null
+++ b/zhenxun/plugins/one_friend/__init__.py
@@ -0,0 +1,79 @@
+import random
+from io import BytesIO
+
+from nonebot.adapters import Bot
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import Alconna, Args
+from nonebot_plugin_alconna import At as alcAt
+from nonebot_plugin_alconna import 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?", alcAt]), priority=5, block=True
+)
+
+_matcher.shortcut(
+ "^我.{0,4}朋友.{0,2}(?:想问问|说|让我问问|想问|让我问|想知道|让我帮他问问|让我帮他问|让我帮忙问|让我帮忙问问|问)(?P.{0,30})$",
+ command="one-friend",
+ arguments=["{content}"],
+ prefix=True,
+)
+
+
+@_matcher.handle()
+async def _(bot: Bot, text: str, at: Match[alcAt], 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()
diff --git a/zhenxun/plugins/open_cases/__init__.py b/zhenxun/plugins/open_cases/__init__.py
new file mode 100644
index 00000000..2798942e
--- /dev/null
+++ b/zhenxun/plugins/open_cases/__init__.py
@@ -0,0 +1,329 @@
+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",
+ )
diff --git a/zhenxun/plugins/open_cases/build_image.py b/zhenxun/plugins/open_cases/build_image.py
new file mode 100644
index 00000000..8b8db8e2
--- /dev/null
+++ b/zhenxun/plugins/open_cases/build_image.py
@@ -0,0 +1,155 @@
+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
diff --git a/zhenxun/plugins/open_cases/command.py b/zhenxun/plugins/open_cases/command.py
new file mode 100644
index 00000000..ea86c2fc
--- /dev/null
+++ b/zhenxun/plugins/open_cases/command.py
@@ -0,0 +1,75 @@
+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\d+)连开箱(?P.*?)",
+ 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,
+)
diff --git a/plugins/open_cases/config.py b/zhenxun/plugins/open_cases/config.py
old mode 100755
new mode 100644
similarity index 92%
rename from plugins/open_cases/config.py
rename to zhenxun/plugins/open_cases/config.py
index 43afbecb..cefa7384
--- a/plugins/open_cases/config.py
+++ b/zhenxun/plugins/open_cases/config.py
@@ -1,255 +1,253 @@
-import random
-from enum import Enum
-from typing import List, Tuple
-
-from configs.path_config import IMAGE_PATH
-from 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]:
- """获取皮肤品质及是否暗金
-
- Args:
- rand (float): 随机rand
-
- Returns:
- 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™) | 等高线
+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™) | 等高线
diff --git a/plugins/open_cases/models/__init__.py b/zhenxun/plugins/open_cases/models/__init__.py
old mode 100755
new mode 100644
similarity index 100%
rename from plugins/open_cases/models/__init__.py
rename to zhenxun/plugins/open_cases/models/__init__.py
diff --git a/plugins/open_cases/models/buff_prices.py b/zhenxun/plugins/open_cases/models/buff_prices.py
old mode 100755
new mode 100644
similarity index 91%
rename from plugins/open_cases/models/buff_prices.py
rename to zhenxun/plugins/open_cases/models/buff_prices.py
index 5402d5e0..9f53de0e
--- a/plugins/open_cases/models/buff_prices.py
+++ b/zhenxun/plugins/open_cases/models/buff_prices.py
@@ -1,7 +1,6 @@
-
from tortoise import fields
-from services.db_context import Model
+from zhenxun.services.db_context import Model
# 1.狂牙武器箱
diff --git a/plugins/open_cases/models/buff_skin.py b/zhenxun/plugins/open_cases/models/buff_skin.py
similarity index 81%
rename from plugins/open_cases/models/buff_skin.py
rename to zhenxun/plugins/open_cases/models/buff_skin.py
index 279f082e..7f51221a 100644
--- a/plugins/open_cases/models/buff_skin.py
+++ b/zhenxun/plugins/open_cases/models/buff_skin.py
@@ -1,10 +1,7 @@
-from datetime import datetime
-from typing import List, Optional
-
from tortoise import fields
from tortoise.contrib.postgres.functions import Random
-from services.db_context import Model
+from zhenxun.services.db_context import Model
class BuffSkin(Model):
@@ -28,24 +25,24 @@ class BuffSkin(Model):
img_url = fields.CharField(255)
"""图片url"""
- steam_price: float = fields.FloatField(default=0)
+ steam_price = fields.FloatField(default=0)
"""steam价格"""
weapon_type = fields.CharField(255)
"""枪械类型"""
- buy_max_price: float = fields.FloatField(default=0)
+ buy_max_price = fields.FloatField(default=0)
"""最大求购价格"""
- buy_num: int = fields.IntField(default=0)
+ buy_num = fields.IntField(default=0)
"""求购数量"""
- sell_min_price: float = fields.FloatField(default=0)
+ sell_min_price = fields.FloatField(default=0)
"""售卖最低价格"""
- sell_num: int = fields.IntField(default=0)
+ sell_num = fields.IntField(default=0)
"""出售个数"""
- sell_reference_price: float = fields.FloatField(default=0)
+ sell_reference_price = fields.FloatField(default=0)
"""参考价格"""
- create_time: datetime = fields.DatetimeField(auto_add_now=True)
+ create_time = fields.DatetimeField(auto_add_now=True)
"""创建日期"""
- update_time: datetime = fields.DatetimeField(auto_add=True)
+ update_time = fields.DatetimeField(auto_add=True)
"""更新日期"""
class Meta:
@@ -68,8 +65,20 @@ class BuffSkin(Model):
color: str,
abrasion: str,
is_stattrak: bool = False,
- case_name: Optional[str] = None,
- ) -> List["BuffSkin"]: # type: ignore
+ 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)
diff --git a/plugins/open_cases/models/buff_skin_log.py b/zhenxun/plugins/open_cases/models/buff_skin_log.py
similarity index 94%
rename from plugins/open_cases/models/buff_skin_log.py
rename to zhenxun/plugins/open_cases/models/buff_skin_log.py
index dd67e4c8..ac9fec95 100644
--- a/plugins/open_cases/models/buff_skin_log.py
+++ b/zhenxun/plugins/open_cases/models/buff_skin_log.py
@@ -1,7 +1,6 @@
from tortoise import fields
-from tortoise.contrib.postgres.functions import Random
-from services.db_context import Model
+from zhenxun.services.db_context import Model
class BuffSkinLog(Model):
diff --git a/plugins/open_cases/models/open_cases_log.py b/zhenxun/plugins/open_cases/models/open_cases_log.py
similarity index 94%
rename from plugins/open_cases/models/open_cases_log.py
rename to zhenxun/plugins/open_cases/models/open_cases_log.py
index ebcaf5bc..0c4f87bb 100644
--- a/plugins/open_cases/models/open_cases_log.py
+++ b/zhenxun/plugins/open_cases/models/open_cases_log.py
@@ -1,9 +1,7 @@
-from typing import List, Optional
-
from tortoise import fields
from tortoise.contrib.postgres.functions import Random
-from services.db_context import Model
+from zhenxun.services.db_context import Model
class OpenCasesLog(Model):
diff --git a/plugins/open_cases/models/open_cases_user.py b/zhenxun/plugins/open_cases/models/open_cases_user.py
old mode 100755
new mode 100644
similarity index 60%
rename from plugins/open_cases/models/open_cases_user.py
rename to zhenxun/plugins/open_cases/models/open_cases_user.py
index 3b488717..3ed43937
--- a/plugins/open_cases/models/open_cases_user.py
+++ b/zhenxun/plugins/open_cases/models/open_cases_user.py
@@ -1,8 +1,6 @@
-from datetime import datetime
-
from tortoise import fields
-from services.db_context import Model
+from zhenxun.services.db_context import Model
class OpenCasesUser(Model):
@@ -13,37 +11,37 @@ class OpenCasesUser(Model):
"""用户id"""
group_id = fields.CharField(255)
"""群聊id"""
- total_count: int = fields.IntField(default=0)
+ total_count = fields.IntField(default=0)
"""总开启次数"""
- blue_count: int = fields.IntField(default=0)
+ blue_count = fields.IntField(default=0)
"""蓝色"""
- blue_st_count: int = fields.IntField(default=0)
+ blue_st_count = fields.IntField(default=0)
"""蓝色暗金"""
- purple_count: int = fields.IntField(default=0)
+ purple_count = fields.IntField(default=0)
"""紫色"""
- purple_st_count: int = fields.IntField(default=0)
+ purple_st_count = fields.IntField(default=0)
"""紫色暗金"""
- pink_count: int = fields.IntField(default=0)
+ pink_count = fields.IntField(default=0)
"""粉色"""
- pink_st_count: int = fields.IntField(default=0)
+ pink_st_count = fields.IntField(default=0)
"""粉色暗金"""
- red_count: int = fields.IntField(default=0)
+ red_count = fields.IntField(default=0)
"""紫色"""
- red_st_count: int = fields.IntField(default=0)
+ red_st_count = fields.IntField(default=0)
"""紫色暗金"""
- knife_count: int = fields.IntField(default=0)
+ knife_count = fields.IntField(default=0)
"""金色"""
- knife_st_count: int = fields.IntField(default=0)
+ knife_st_count = fields.IntField(default=0)
"""金色暗金"""
- spend_money: float = fields.IntField(default=0)
+ spend_money = fields.IntField(default=0)
"""花费金币"""
- make_money: float = fields.FloatField(default=0)
+ make_money = fields.FloatField(default=0)
"""赚取金币"""
- today_open_total: int = fields.IntField(default=0)
+ today_open_total = fields.IntField(default=0)
"""今日开箱数量"""
- open_cases_time_last: datetime = fields.DatetimeField()
+ open_cases_time_last = fields.DatetimeField()
"""最后开箱日期"""
- knifes_name: str = fields.TextField(default="")
+ knifes_name = fields.TextField(default="")
"""已获取金色"""
class Meta:
diff --git a/plugins/open_cases/open_cases_c.py b/zhenxun/plugins/open_cases/open_cases_c.py
old mode 100755
new mode 100644
similarity index 70%
rename from plugins/open_cases/open_cases_c.py
rename to zhenxun/plugins/open_cases/open_cases_c.py
index 1f9409d3..f56aa9fe
--- a/plugins/open_cases/open_cases_c.py
+++ b/zhenxun/plugins/open_cases/open_cases_c.py
@@ -1,461 +1,481 @@
-import asyncio
-import random
-import re
-from datetime import datetime
-from typing import Union
-
-from nonebot.adapters.onebot.v11 import Message, MessageSegment
-
-from configs.config import Config
-from configs.path_config import IMAGE_PATH
-from models.sign_group_user import SignGroupUser
-from services.log import logger
-from utils.image_utils import BuildImage
-from utils.message_builder import image
-from 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 += 17 + case_price
-
-
-async def get_user_max_count(
- user_id: Union[int, str], group_id: Union[str, int]
-) -> int:
- """获取用户每日最大开箱次数
-
- Args:
- user_id (str): 用户id
- group_id (int): 群号
-
- Returns:
- int: 最大开箱次数
- """
- user, _ = await SignGroupUser.get_or_create(
- user_id=str(user_id), group_id=str(group_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: Union[int, str], group_id: Union[int, str], case_name: str
-) -> Union[str, Message]:
- """开箱
-
- Args:
- user_id (str): 用户id
- group_id (int): 群号
- case_name (str, optional): 武器箱名称. Defaults to "狂牙大行动".
-
- Returns:
- Union[str, Message]: 回复消息
- """
- user_id = str(user_id)
- group_id = str(group_id)
- if not CaseManager.CURRENT_CASES:
- return "未收录任何武器箱"
- 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}", "开箱", user_id, group_id)
- case = cn2py(case_name)
- 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, group_id)
- # 一天次数上限
- if user.today_open_total >= max_count:
- return _handle_is_MAX_COUNT()
- skin_list = await random_skin(1, case_name)
- if not skin_list:
- return "未抽取到任何皮肤..."
- 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}",
- "开箱",
- user_id,
- group_id,
- )
- 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 条开箱日志", "开箱", user_id, group_id)
- over_count = max_count - user.today_open_total
- img = await draw_card(skin, rand)
- return (
- f"开启{case_name}武器箱.\n剩余开箱次数:{over_count}.\n"
- + image(img)
- + f"\n箱子单价:{case_price}\n花费:{17 + case_price:.2f}\n:{ridicule_result}"
- )
-
-
-async def open_multiple_case(
- user_id: Union[int, str], group_id: Union[str, int], case_name: str, num: int = 10
-):
- """多连开箱
-
- Args:
- user_id (int): 用户id
- group_id (int): 群号
- case_name (str): 箱子名称
- num (int, optional): 数量. Defaults to 10.
-
- Returns:
- _type_: _description_
- """
- user_id = str(user_id)
- group_id = str(group_id)
- if not CaseManager.CURRENT_CASES:
- return "未收录任何武器箱"
- 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
- 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, group_id)
- if user.today_open_total >= max_count:
- return _handle_is_MAX_COUNT()
- if max_count - user.today_open_total < num:
- return (
- f"今天开箱次数不足{num}次噢,请单抽试试看(也许单抽运气更好?)"
- f"\n剩余开箱次数:{max_count - user.today_open_total}"
- )
- logger.debug(f"尝试开启武器箱: {case_name}", "开箱", user_id, group_id)
- case = cn2py(case_name)
- skin_count = {}
- img_list = []
- skin_list = await random_skin(num, case_name)
- if not skin_list:
- return "未抽取到任何皮肤..."
- 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}",
- "开箱",
- user_id,
- group_id,
- )
- 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)} 条开箱日志", "开箱", user_id, group_id)
- 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
- markImg = BuildImage(
- w - 10, h - 10, img_w - 10, img_h - 10, 10, color=(255, 255, 255)
- )
- for img in img_list:
- markImg.paste(img, alpha=True)
- over_count = max_count - user.today_open_total
- result = ""
- for color_name in skin_count:
- result += f"[{color_name}:{skin_count[color_name]}] "
- return (
- f"开启{case_name}武器箱\n剩余开箱次数:{over_count}\n"
- + image(markImg)
- + "\n"
- + result[:-1]
- + f"\n箱子单价:{case_price}\n总获取金额:{total_price:.2f}\n总花费:{(17 + case_price) * num:.2f}"
- )
-
-
-def _handle_is_MAX_COUNT() -> str:
- return f"今天已达开箱上限了喔,明天再来吧\n(提升好感度可以增加每日开箱数 #疯狂暗示)"
-
-
-async def total_open_statistics(
- user_id: Union[str, int], group_id: Union[str, int]
-) -> str:
- user, _ = await OpenCasesUser.get_or_create(
- user_id=str(user_id), group_id=str(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: Union[int, 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: Union[str, int], group_id: Union[str, int]
-) -> Union[str, MessageSegment]:
- """获取我的金色
-
- Args:
- user_id (str): 用户id
- group_id (str): 群号
-
- Returns:
- Union[str, MessageSegment]: 回复消息或图片
- """
- 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 "您木有开出金色级别的皮肤喔"
- 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, 540, 600)
- 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, 470, 470, font_size=20)
- await knife_img.apaste(
- BuildImage(470, 470, background=img_path if img_path.exists() else None),
- (0, 0),
- True,
- )
- await knife_img.atext(
- (5, 500), f"\t{skin.name}|{skin.skin_name}({skin.abrasion})"
- )
- await knife_img.atext((5, 530), f"\t磨损:{skin.abrasion_value}")
- await knife_img.atext((5, 560), f"\t价格:{skin.price}")
- await A.apaste(knife_img)
- return image(A)
-
-
-async def get_old_knife(user_id: str, group_id: str) -> List[OpenCasesLog]:
- """获取旧数据字段
-
- Args:
- user_id (str): 用户id
- group_id (str): 群号
-
- Returns:
- 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}", "我的金色", user_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)
+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)
diff --git a/plugins/open_cases/utils.py b/zhenxun/plugins/open_cases/utils.py
old mode 100755
new mode 100644
similarity index 80%
rename from plugins/open_cases/utils.py
rename to zhenxun/plugins/open_cases/utils.py
index d5e783e8..6fda2265
--- a/plugins/open_cases/utils.py
+++ b/zhenxun/plugins/open_cases/utils.py
@@ -1,643 +1,657 @@
-import asyncio
-import os
-import random
-import re
-import time
-from datetime import datetime, timedelta
-from typing import List, Optional, Tuple, Union
-
-import nonebot
-from tortoise.functions import Count
-
-from configs.config import Config
-from configs.path_config import IMAGE_PATH
-from services.log import logger
-from utils.http_utils import AsyncHttpx
-from utils.image_utils import BuildImage, BuildMat
-from utils.utils import broadcast_group, 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
-
-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:
- """更新箱子内皮肤数据
-
- Args:
- name (str): 箱子名称
- is_update_case_name (bool): 是否必定更新所属箱子
-
- Returns:
- _type_: _description_
- """
- 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"更新武器箱/皮肤: [{name}], 创建 {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"更新武器箱/皮肤: [{name}], 更新 {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"更新武器箱/皮肤: [{name}], 新增 {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[Union[List[BuffSkin], str], int]:
- """查询箱子皮肤
-
- Args:
- s_name (str): 箱子/皮肤名称
- page_index (int): 页数
-
- Returns:
- Union[List[BuffSkin], str]: BuffSkin
- """
- logger.debug(
- f"尝试访问武器箱/皮肤: [{s_name}] 页数: [{page_index}]", "开箱更新"
- )
- 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"武器箱: [{s_name}] 页数: [{page_index}] 正在收录皮肤: [{name}]...",
- "开箱更新",
- )
- 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"更新武器箱: [{s_name}] 皮肤: [{s_name}] 错误",
- e=e,
- )
- logger.debug(
- f"访问武器箱: [{s_name}] 页数: [{page_index}] 成功并收录完成",
- "开箱更新",
- )
- 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) -> Union[BuildImage, str]:
- """构造武器箱图片
-
- Args:
- case_name (str): 名称
-
- Returns:
- Union[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.afilter("GaussianBlur", 2)
- if case:
- case_img = await generate_skin(case, skin2count.get(f"{case_name}武器箱", 0))
- if case_img:
- A.paste(case_img, (25, 25), True)
- w = 25
- h = 230
- skin_image_list.reverse()
- for image in skin_image_list:
- A.paste(image, (w, h), True)
- w += image.w + 20
- if w + image.w - 25 > A.w:
- h += image.h + 10
- w = 25
- if h + img_h + 100 < A.h:
- await A.acrop((0, 0, A.w, 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.afilter("GaussianBlur", 2)
- bk_img = BuildImage(
- img_w, 120, color=(25, 25, 25, 100), font_size=60, font="CJGaoDeGuo.otf"
- )
- await bk_img.atext(
- (0, 0), f"已收录 {len(image_list)} 个武器箱", (255, 255, 255), center_type="center"
- )
- await A.apaste(bk_img, (10, 10), True, "by_width")
- for image in image_list:
- A.paste(image, (w, h), True)
- w += image.w + 20
- if w + image.w - 25 > A.w:
- h += image.h + 10
- w = 25
- if h + img_h + 100 < A.h:
- await A.acrop((0, 0, A.w, 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,
-):
- """获取所需背景大小且不改变图片长宽比
-
- Args:
- total_size (int): 总面积
- base_size (Tuple[int, int]): 初始背景大小
- img_size (Tuple[int, int]): 贴图大小
-
- Returns:
- _type_: 满足所有贴图大小
- """
- 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) -> Optional[List[str]]:
- """获取皮肤所在箱子
-
- Args:
- id_ (str): 皮肤id
-
- Returns:
- Optional[str]: 武器箱名称
- """
- 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('', 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
-) -> Optional[BuildMat]:
- 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)
- bar_graph = BuildMat(
- y=price_list,
- mat_type="line",
- title=f"{name}({skin})价格趋势({day})",
- x_index=date_list,
- x_min_spacing=90,
- display_num=True,
- x_rotate=30,
- background=[
- f"{IMAGE_PATH}/background/create_mat/{x}"
- for x in os.listdir(f"{IMAGE_PATH}/background/create_mat")
- ],
- bar_color=["*"],
- )
- await asyncio.get_event_loop().run_in_executor(None, bar_graph.gen_graph)
- return bar_graph
-
-
-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: Optional[str] = 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:
- 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()
+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"更新武器箱/皮肤: [{name}], 创建 {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"更新武器箱/皮肤: [{name}], 更新 {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"更新武器箱/皮肤: [{name}], 新增 {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"尝试访问武器箱/皮肤: [{s_name}] 页数: [{page_index}]",
+ "开箱更新",
+ )
+ 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"武器箱: [{s_name}] 页数: [{page_index}] 正在收录皮肤: [{name}]...",
+ "开箱更新",
+ )
+ 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"更新武器箱: [{s_name}] 皮肤: [{s_name}] 错误",
+ e=e,
+ )
+ logger.debug(
+ f"访问武器箱: [{s_name}] 页数: [{page_index}] 成功并收录完成",
+ "开箱更新",
+ )
+ 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('', 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()
diff --git a/zhenxun/plugins/parse_bilibili/__init__.py b/zhenxun/plugins/parse_bilibili/__init__.py
new file mode 100644
index 00000000..1d319093
--- /dev/null
+++ b/zhenxun/plugins/parse_bilibili/__init__.py
@@ -0,0 +1,177 @@
+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.HIDDEN,
+ 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:
+ # 将链接统一发送给处理函数
+ vd_info, live_info, vd_url, live_url, image_info, image_url = (
+ await parse_bili_url(get_url, information_container)
+ )
+ if vd_info:
+ # 判断一定时间内是否解析重复内容,或者是第一次解析
+ if (
+ vd_url in _tmp.keys() and time.time() - _tmp[vd_url] > repet_second
+ ) or vd_url not in _tmp.keys():
+ pic = vd_info.get("pic", "") # 封面
+ aid = vd_info.get("aid", "") # av号
+ title = vd_info.get("title", "") # 标题
+ author = vd_info.get("owner", {}).get("name", "") # UP主
+ reply = vd_info.get("stat", {}).get("reply", "") # 回复
+ favorite = vd_info.get("stat", {}).get("favorite", "") # 收藏
+ coin = vd_info.get("stat", {}).get("coin", "") # 投币
+ like = vd_info.get("stat", {}).get("like", "") # 点赞
+ danmuku = vd_info.get("stat", {}).get("danmaku", "") # 弹幕
+ ctime = vd_info["ctime"]
+ date = time.strftime("%Y-%m-%d", time.localtime(ctime))
+ logger.info(f"解析bilibili转发 {vd_url}", "b站解析", session=session)
+ _tmp[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{vd_url}",
+ ]
+ ).send()
+
+ elif live_info:
+ if (
+ live_url in _tmp.keys() and time.time() - _tmp[live_url] > repet_second
+ ) or live_url not in _tmp.keys():
+ uid = live_info.get("uid", "") # 主播uid
+ title = live_info.get("title", "") # 直播间标题
+ description = live_info.get("description", "") # 简介,可能会出现标签
+ user_cover = live_info.get("user_cover", "") # 封面
+ keyframe = live_info.get("keyframe", "") # 关键帧画面
+ live_time = live_info.get("live_time", "") # 开播时间
+ area_name = live_info.get("area_name", "") # 分区
+ parent_area_name = live_info.get("parent_area_name", "") # 父分区
+ logger.info(f"解析bilibili转发 {live_url}", "b站解析", session=session)
+ _tmp[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"{live_url}",
+ ]
+ ).send()
+ elif image_info:
+ if (
+ image_url in _tmp.keys()
+ and time.time() - _tmp[image_url] > repet_second
+ ) or image_url not in _tmp.keys():
+ logger.info(f"解析bilibili转发 {image_url}", "b站解析", session=session)
+ _tmp[image_url] = time.time()
+ await image_info.send()
diff --git a/zhenxun/plugins/parse_bilibili/get_image.py b/zhenxun/plugins/parse_bilibili/get_image.py
new file mode 100644
index 00000000..bbc005c5
--- /dev/null
+++ b/zhenxun/plugins/parse_bilibili/get_image.py
@@ -0,0 +1,108 @@
+import os
+import re
+
+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: str):
+ """调整图像大小的异步函数
+
+ 参数:
+ path (str): 图像文件路径
+ """
+ A = BuildImage(background=path)
+ await A.resize(0.5)
+ 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 = f"{TEMP_PATH}/bilibili_cv_{cv_number}.png"
+ elif opus_number:
+ screenshot_path = f"{TEMP_PATH}/bilibili_opus_{opus_number}.png"
+ elif t_opus_number:
+ screenshot_path = f"{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 os.path.exists(screenshot_path):
+ # 创建页面
+ # random.choice(),从列表中随机抽取一个对象
+ user_agent = get_user_agent_str()
+ 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
diff --git a/zhenxun/plugins/parse_bilibili/information_container.py b/zhenxun/plugins/parse_bilibili/information_container.py
new file mode 100644
index 00000000..1cbf651f
--- /dev/null
+++ b/zhenxun/plugins/parse_bilibili/information_container.py
@@ -0,0 +1,60 @@
+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.vd_info,
+ self.live_info,
+ self.vd_url,
+ self.live_url,
+ self.image_info,
+ self.image_url,
+ )
diff --git a/zhenxun/plugins/parse_bilibili/parse_url.py b/zhenxun/plugins/parse_bilibili/parse_url.py
new file mode 100644
index 00000000..b4e2a1fe
--- /dev/null
+++ b/zhenxun/plugins/parse_bilibili/parse_url.py
@@ -0,0 +1,65 @@
+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()
diff --git a/zhenxun/plugins/pid_search.py b/zhenxun/plugins/pid_search.py
new file mode 100644
index 00000000..97fc4d40
--- /dev/null
+++ b/zhenxun/plugins/pid_search.py
@@ -0,0 +1,125 @@
+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)
diff --git a/zhenxun/plugins/pix_gallery/__init__.py b/zhenxun/plugins/pix_gallery/__init__.py
new file mode 100644
index 00000000..d549a249
--- /dev/null
+++ b/zhenxun/plugins/pix_gallery/__init__.py
@@ -0,0 +1,62 @@
+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()))
diff --git a/plugins/pix_gallery/_data_source.py b/zhenxun/plugins/pix_gallery/_data_source.py
similarity index 77%
rename from plugins/pix_gallery/_data_source.py
rename to zhenxun/plugins/pix_gallery/_data_source.py
index 0b9f92d8..7e9db221 100644
--- a/plugins/pix_gallery/_data_source.py
+++ b/zhenxun/plugins/pix_gallery/_data_source.py
@@ -1,37 +1,27 @@
import asyncio
import math
-import platform
from asyncio.exceptions import TimeoutError
from asyncio.locks import Semaphore
from copy import deepcopy
-from typing import List, Optional, Tuple
+from pathlib import Path
import aiofiles
-from asyncpg.exceptions import UniqueViolationError
+from httpx import ConnectError
-from configs.config import Config
-from configs.path_config import TEMP_PATH
-from services.log import logger
-from utils.http_utils import AsyncHttpx
-from utils.image_utils import BuildImage
-from utils.utils import change_img_md5, change_pixiv_image_links
+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
-try:
- import ujson as json
-except ModuleNotFoundError:
- import json
-
-# if str(platform.system()).lower() == "windows":
-# policy = asyncio.WindowsSelectorEventLoopPolicy()
-# asyncio.set_event_loop_policy(policy)
-
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",
+ "Referer": "https://www.pixiv.net/",
}
HIBIAPI = Config.get_config("hibiapi", "HIBIAPI")
@@ -41,13 +31,17 @@ HIBIAPI = HIBIAPI[:-1] if HIBIAPI[-1] == "/" else HIBIAPI
async def start_update_image_url(
- current_keyword: List[str], black_pid: List[str]
-) -> "int, int":
- """
- 开始更新图片url
- :param current_keyword: 关键词
- :param black_pid: 黑名单pid
- :return: pid数量和图片数量
+ 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
@@ -69,7 +63,9 @@ async def start_update_image_url(
params = {"word": keyword, "page": page}
tasks.append(
asyncio.ensure_future(
- search_image(url, keyword, params, semaphore, page, black_pid)
+ search_image(
+ url, keyword, params, semaphore, page, black_pid, is_pid
+ )
)
)
if keyword.startswith("pid:"):
@@ -87,17 +83,22 @@ async def search_image(
params: dict,
semaphore: Semaphore,
page: int = 1,
- black: List[str] = None,
-) -> "int, int":
- """
- 搜索图片
- :param url: 搜索url
- :param keyword: 关键词
- :param params: params参数
- :param semaphore: semaphore
- :param page: 页面
- :param black: pid黑名单
- :return: pid数量和图片数量
+ 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
@@ -149,7 +150,7 @@ async def search_image(
)
and len(img_urls) < 10
and _check_black(img_urls, black)
- ):
+ ) or is_pid:
img_data[pid] = {
"pid": pid,
"title": title,
@@ -191,12 +192,15 @@ async def search_image(
return pid_count, pic_count
-async def get_image(img_url: str, user_id: int) -> Optional[str]:
- """
- 下载图片
- :param img_url:
- :param user_id:
- :return: 图片名称
+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]
@@ -249,14 +253,19 @@ async def get_image(img_url: str, user_id: int) -> Optional[str]:
return TEMP_PATH / f"pix_{user_id}_{img_url.split('/')[-1][:-4]}.jpg"
except TimeoutError:
logger.warning(f"PIX:{img_url} 图片下载超时...")
- pass
+ except ConnectError:
+ logger.warning(f"PIX:{img_url} 图片下载连接失败...")
return None
async def uid_pid_exists(id_: str) -> bool:
- """
- 检测 pid/uid 是否有效
- :param id_: pid/uid
+ """检测 pid/uid 是否有效
+
+ 参数:
+ id_: pid/uid
+
+ 返回:
+ bool: 是否有效
"""
if id_.startswith("uid:"):
url = f"{HIBIAPI}/api/pixiv/member"
@@ -271,10 +280,14 @@ async def uid_pid_exists(id_: str) -> bool:
return True
-async def get_keyword_num(keyword: str) -> Tuple[int, int, int, int, int]:
- """
- 查看图片相关 tag 数量
- :param keyword: 关键词tag
+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(
@@ -283,11 +296,12 @@ async def get_keyword_num(keyword: str) -> Tuple[int, int, int, int, int]:
return count, r18_count, count_, setu_count, r18_count_
-async def remove_image(pid: int, img_p: Optional[str]):
- """
- 删除置顶图片
- :param pid: pid
- :param img_p: 图片 p 如 p0,p1 等
+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:
@@ -298,14 +312,18 @@ async def remove_image(pid: int, img_p: Optional[str]):
await Pixiv.filter(pid=pid).delete()
-def gen_keyword_pic(
- _pass_keyword: List[str], not_pass_keyword: List[str], is_superuser: bool
-):
- """
- 已通过或未通过的所有关键词/uid/pid
- :param _pass_keyword: 通过列表
- :param not_pass_keyword: 未通过列表
- :param is_superuser: 是否超级用户
+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
@@ -362,7 +380,8 @@ def gen_keyword_pic(
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, 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
@@ -372,30 +391,33 @@ def gen_keyword_pic(
key_str = "\n".join(
[key for key in img_data[x]["data"][start_index:end_index]]
)
- tmp.text((10, 100), key_str)
+ await tmp.text((10, 100), key_str)
if x.find("_n") == -1:
- text_img.text((24, 24), "已收录")
+ await text_img.text((24, 24), "已收录")
else:
- text_img.text((24, 24), "待收录")
- tmp.paste(text_img, (0, 0))
+ 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")
- background_img.paste(tmp, (1, 1))
- img.paste(background_img)
- A.paste(img, (current_width, 0))
+ 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.pic2bs4()
+ return A
-def _check_black(img_urls: List[str], black: List[str]) -> bool:
- """
- 检测pid是否在黑名单中
- :param img_urls: 图片img列表
- :param black: 黑名单
- :return:
+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:
diff --git a/plugins/pix_gallery/_model/__init__.py b/zhenxun/plugins/pix_gallery/_model/__init__.py
similarity index 100%
rename from plugins/pix_gallery/_model/__init__.py
rename to zhenxun/plugins/pix_gallery/_model/__init__.py
diff --git a/plugins/pix_gallery/_model/omega_pixiv_illusts.py b/zhenxun/plugins/pix_gallery/_model/omega_pixiv_illusts.py
similarity index 72%
rename from plugins/pix_gallery/_model/omega_pixiv_illusts.py
rename to zhenxun/plugins/pix_gallery/_model/omega_pixiv_illusts.py
index c80c5f45..17e2156c 100644
--- a/plugins/pix_gallery/_model/omega_pixiv_illusts.py
+++ b/zhenxun/plugins/pix_gallery/_model/omega_pixiv_illusts.py
@@ -1,9 +1,8 @@
-from typing import List, Optional, Tuple
from tortoise import fields
from tortoise.contrib.postgres.functions import Random
-from services.db_context import Model
+from zhenxun.services.db_context import Model
class OmegaPixivIllusts(Model):
@@ -39,21 +38,20 @@ class OmegaPixivIllusts(Model):
@classmethod
async def query_images(
cls,
- keywords: Optional[List[str]] = None,
- uid: Optional[int] = None,
- pid: Optional[int] = None,
- nsfw_tag: Optional[int] = 0,
+ keywords: list[str] | None = None,
+ uid: int | None = None,
+ pid: int | None = None,
+ nsfw_tag: int | None = 0,
num: int = 100,
- ) -> List["OmegaPixivIllusts"]:
- """
- 说明:
- 查找符合条件的图片
+ ) -> list["OmegaPixivIllusts"]:
+ """查找符合条件的图片
+
参数:
- :param keywords: 关键词
- :param uid: 画师uid
- :param pid: 图片pid
- :param nsfw_tag: nsfw标签, 0=safe, 1=setu. 2=r18
- :param num: 获取图片数量
+ keywords: 关键词
+ uid: 画师uid
+ pid: 图片pid
+ nsfw_tag: nsfw标签, 0=safe, 1=setu. 2=r18
+ num: 获取图片数量
"""
if not num:
return []
@@ -72,13 +70,12 @@ class OmegaPixivIllusts(Model):
@classmethod
async def get_keyword_num(
- cls, tags: Optional[List[str]] = None
- ) -> Tuple[int, int, int]:
- """
- 说明:
- 获取相关关键词(keyword, tag)在图库中的数量
+ cls, tags: list[str] | None = None
+ ) -> tuple[int, int, int]:
+ """获取相关关键词(keyword, tag)在图库中的数量
+
参数:
- :param tags: 关键词/Tag
+ tags: 关键词/Tag
"""
query = cls
if tags:
diff --git a/plugins/pix_gallery/_model/pixiv.py b/zhenxun/plugins/pix_gallery/_model/pixiv.py
similarity index 71%
rename from plugins/pix_gallery/_model/pixiv.py
rename to zhenxun/plugins/pix_gallery/_model/pixiv.py
index 4af995a5..3451781d 100644
--- a/plugins/pix_gallery/_model/pixiv.py
+++ b/zhenxun/plugins/pix_gallery/_model/pixiv.py
@@ -1,9 +1,7 @@
-from typing import List, Optional, Tuple
-
from tortoise import fields
from tortoise.contrib.postgres.functions import Random
-from services.db_context import Model
+from zhenxun.services.db_context import Model
class Pixiv(Model):
@@ -43,21 +41,20 @@ class Pixiv(Model):
@classmethod
async def query_images(
cls,
- keywords: Optional[List[str]] = None,
- uid: Optional[int] = None,
- pid: Optional[int] = None,
- r18: Optional[int] = 0,
+ keywords: list[str] | None = None,
+ uid: int | None = None,
+ pid: int | None = None,
+ r18: int | None = 0,
num: int = 100,
- ) -> List[Optional["Pixiv"]]:
- """
- 说明:
- 查找符合条件的图片
+ ) -> list["Pixiv"]:
+ """查找符合条件的图片
+
参数:
- :param keywords: 关键词
- :param uid: 画师uid
- :param pid: 图片pid
- :param r18: 是否r18,0:非r18 1:r18 2:混合
- :param num: 查找图片的数量
+ keywords: 关键词
+ uid: 画师uid
+ pid: 图片pid
+ r18: 是否r18,0:非r18 1:r18 2:混合
+ num: 查找图片的数量
"""
if not num:
return []
@@ -77,12 +74,11 @@ class Pixiv(Model):
return await query.all() # type: ignore
@classmethod
- async def get_keyword_num(cls, tags: Optional[List[str]] = None) -> Tuple[int, int]:
- """
- 说明:
- 获取相关关键词(keyword, tag)在图库中的数量
+ async def get_keyword_num(cls, tags: list[str] | None = None) -> tuple[int, int]:
+ """获取相关关键词(keyword, tag)在图库中的数量
+
参数:
- :param tags: 关键词/Tag
+ tags: 关键词/Tag
"""
query = cls
if tags:
diff --git a/plugins/pix_gallery/_model/pixiv_keyword_user.py b/zhenxun/plugins/pix_gallery/_model/pixiv_keyword_user.py
similarity index 80%
rename from plugins/pix_gallery/_model/pixiv_keyword_user.py
rename to zhenxun/plugins/pix_gallery/_model/pixiv_keyword_user.py
index 829a1040..5de544a5 100644
--- a/plugins/pix_gallery/_model/pixiv_keyword_user.py
+++ b/zhenxun/plugins/pix_gallery/_model/pixiv_keyword_user.py
@@ -1,8 +1,6 @@
-from typing import List, Set, Tuple
-
from tortoise import fields
-from services.db_context import Model
+from zhenxun.services.db_context import Model
class PixivKeywordUser(Model):
@@ -23,11 +21,8 @@ class PixivKeywordUser(Model):
table_description = "pixiv关键词数据表"
@classmethod
- async def get_current_keyword(cls) -> Tuple[List[str], List[str]]:
- """
- 说明:
- 获取当前通过与未通过的关键词
- """
+ 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"):
@@ -38,11 +33,8 @@ class PixivKeywordUser(Model):
return pass_keyword, not_pass_keyword
@classmethod
- async def get_black_pid(cls) -> List[str]:
- """
- 说明:
- 获取黑名单PID
- """
+ async def get_black_pid(cls) -> list[str]:
+ """获取黑名单PID"""
black_pid = []
keyword_list = await cls.filter(user_id="114514").values_list(
"keyword", flat=True
diff --git a/zhenxun/plugins/pix_gallery/pix.py b/zhenxun/plugins/pix_gallery/pix.py
new file mode 100644
index 00000000..2f8d25c3
--- /dev/null
+++ b/zhenxun/plugins/pix_gallery/pix.py
@@ -0,0 +1,247 @@
+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,
+ )
diff --git a/zhenxun/plugins/pix_gallery/pix_add_keyword.py b/zhenxun/plugins/pix_gallery/pix_add_keyword.py
new file mode 100644
index 00000000..452213e3
--- /dev/null
+++ b/zhenxun/plugins/pix_gallery/pix_add_keyword.py
@@ -0,0 +1,135 @@
+from nonebot.adapters import Bot
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import (
+ Alconna,
+ Args,
+ Arparma,
+ Option,
+ on_alconna,
+ store_true,
+)
+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 uid_pid_exists
+from ._model.pixiv import Pixiv
+from ._model.pixiv_keyword_user import PixivKeywordUser
+
+__plugin_meta__ = PluginMetadata(
+ name="PIX添加",
+ description="PIX关键词/UID/PID添加管理",
+ usage="""
+ 指令:
+ 添加pix关键词 [Tag]: 添加一个pix搜索收录Tag
+ pix添加 uid [uid]: 添加一个pix搜索收录uid
+ pix添加 pid [pid]: 添加一个pix收录pid
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ ).dict(),
+)
+
+_add_matcher = on_alconna(
+ Alconna("添加pix关键词", Args["keyword", str]), priority=5, block=True
+)
+
+_uid_matcher = on_alconna(
+ Alconna(
+ "pix添加",
+ Args["add_type", ["uid", "pid"]]["id", str],
+ Option("-f", action=store_true, help_text="强制收录不检查是否存在"),
+ ),
+ priority=5,
+ block=True,
+)
+
+_black_matcher = on_alconna(
+ Alconna("添加pix黑名单", Args["pid", str]), priority=5, block=True
+)
+
+
+@_add_matcher.handle()
+async def _(bot: Bot, session: EventSession, keyword: str, arparma: Arparma):
+ group_id = session.id3 or session.id2 or -1
+ if not await PixivKeywordUser.exists(keyword=keyword):
+ await PixivKeywordUser.create(
+ user_id=str(session.id1),
+ group_id=str(group_id),
+ keyword=keyword,
+ is_pass=str(session.id1) in bot.config.superusers,
+ )
+ text = f"已成功添加pixiv搜图关键词:{keyword}"
+ if session.id1 not in bot.config.superusers:
+ text += ",请等待管理员通过该关键词!"
+ await MessageUtils.build_message(text).send(reply_to=True)
+ logger.info(
+ f"添加了pixiv搜图关键词: {keyword}", arparma.header_result, session=session
+ )
+ else:
+ await MessageUtils.build_message(f"该关键词 {keyword} 已存在...").send()
+
+
+@_uid_matcher.handle()
+async def _(bot: Bot, session: EventSession, arparma: Arparma, add_type: str, id: str):
+ group_id = session.id3 or session.id2 or -1
+ exists_flag = True
+ if arparma.find("f") and session.id1 in bot.config.superusers:
+ exists_flag = False
+ word = None
+ if add_type == "uid":
+ word = f"uid:{id}"
+ else:
+ word = f"pid:{id}"
+ if await Pixiv.get_or_none(pid=int(id), img_p="p0"):
+ await MessageUtils.build_message(f"该PID:{id}已存在...").finish(
+ reply_to=True
+ )
+ if not await uid_pid_exists(word) and exists_flag:
+ await MessageUtils.build_message(
+ "画师或作品不存在或搜索正在CD,请稍等..."
+ ).finish(reply_to=True)
+ if not await PixivKeywordUser.exists(keyword=word):
+ await PixivKeywordUser.create(
+ user_id=session.id1,
+ group_id=str(group_id),
+ keyword=word,
+ is_pass=session.id1 in bot.config.superusers,
+ )
+ text = f"已成功添加pixiv搜图UID/PID:{id}"
+ if session.id1 not in bot.config.superusers:
+ text += ",请等待管理员通过该关键词!"
+ await MessageUtils.build_message(text).send(reply_to=True)
+ else:
+ await MessageUtils.build_message(f"该UID/PID:{id} 已存在...").send()
+
+
+@_black_matcher.handle()
+async def _(bot: Bot, session: EventSession, arparma: Arparma, pid: str):
+ img_p = ""
+ if "p" in pid:
+ img_p = pid.split("p")[-1]
+ pid = pid.replace("_", "")
+ pid = pid[: pid.find("p")]
+ if not pid.isdigit:
+ await MessageUtils.build_message("PID必须全部是数字!").finish(reply_to=True)
+ if not await PixivKeywordUser.exists(
+ keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}"
+ ):
+ await PixivKeywordUser.create(
+ user_id=114514,
+ group_id=114514,
+ keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}",
+ is_pass=session.id1 in bot.config.superusers,
+ )
+ await MessageUtils.build_message(f"已添加PID:{pid} 至黑名单中...").send()
+ logger.info(
+ f" 添加了pixiv搜图黑名单 PID:{pid}", arparma.header_result, session=session
+ )
+ else:
+ await MessageUtils.build_message(
+ f"PID:{pid} 已添加黑名单中,添加失败..."
+ ).send()
diff --git a/zhenxun/plugins/pix_gallery/pix_pass_del_keyword.py b/zhenxun/plugins/pix_gallery/pix_pass_del_keyword.py
new file mode 100644
index 00000000..9a8f2ea7
--- /dev/null
+++ b/zhenxun/plugins/pix_gallery/pix_pass_del_keyword.py
@@ -0,0 +1,218 @@
+from nonebot.adapters import Bot
+from nonebot.permission import SUPERUSER
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import (
+ Alconna,
+ Args,
+ Arparma,
+ At,
+ Match,
+ Option,
+ on_alconna,
+ store_true,
+)
+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 zhenxun.utils.platform import PlatformUtils
+
+from ._data_source import remove_image
+from ._model.pixiv import Pixiv
+from ._model.pixiv_keyword_user import PixivKeywordUser
+
+__plugin_meta__ = PluginMetadata(
+ name="PIX删除",
+ description="PIX关键词/UID/PID添加管理",
+ usage="""
+ 指令:
+ pix关键词 [y/n] [关键词/pid/uid]
+ 删除pix关键词 ['pid'/'uid'/'keyword'] [关键词/pid/uid]
+ 删除pix图片 *[pid]
+ 示例:pix关键词 y 萝莉
+ 示例:pix关键词 y 12312312 uid
+ 示例:pix关键词 n 12312312 pid
+ 示例:删除pix关键词 keyword 萝莉
+ 示例:删除pix关键词 uid 123123123
+ 示例:删除pix关键词 pid 123123
+ 示例:删除pix图片 4223442
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier", version="0.1", plugin_type=PluginType.SUPERUSER
+ ).dict(),
+)
+
+
+_pass_matcher = on_alconna(
+ Alconna(
+ "pix关键词", Args["status", ["y", "n"]]["keyword", str]["type?", ["uid", "pid"]]
+ ),
+ permission=SUPERUSER,
+ priority=1,
+ block=True,
+)
+
+_del_matcher = on_alconna(
+ Alconna("删除pix关键词", Args["type", ["pid", "uid", "keyword"]]["keyword", str]),
+ permission=SUPERUSER,
+ priority=1,
+ block=True,
+)
+
+_del_pic_matcher = on_alconna(
+ Alconna(
+ "删除pix图片",
+ Args["pid", str],
+ Option("-b|--black", action=store_true, help_text=""),
+ ),
+ permission=SUPERUSER,
+ priority=1,
+ block=True,
+)
+
+
+@_pass_matcher.handle()
+async def _(
+ bot: Bot,
+ session: EventSession,
+ arparma: Arparma,
+ status: str,
+ keyword: str,
+ type: Match[str],
+):
+ tmp = {"group": {}, "private": {}}
+ flag = status == "y"
+ if type.available:
+ if type.result == "uid":
+ keyword = f"uid:{keyword}"
+ else:
+ keyword = f"pid:{keyword}"
+ if not keyword[4:].isdigit():
+ await MessageUtils.build_message(f"{keyword} 非全数字...").finish(
+ reply_to=True
+ )
+ data = await PixivKeywordUser.get_or_none(keyword=keyword)
+ user_id = 0
+ group_id = 0
+ if data:
+ data.is_pass = flag
+ await data.save(update_fields=["is_pass"])
+ user_id, group_id = data.user_id, data.group_id
+ if not user_id:
+ await MessageUtils.build_message(
+ f"未找到关键词/UID:{keyword},请检查关键词/UID是否存在..."
+ ).finish(reply_to=True)
+ if flag:
+ if group_id == -1:
+ if not tmp["private"].get(user_id):
+ tmp["private"][user_id] = {"keyword": [keyword]}
+ else:
+ tmp["private"][user_id]["keyword"].append(keyword)
+ else:
+ if not tmp["group"].get(group_id):
+ tmp["group"][group_id] = {}
+ if not tmp["group"][group_id].get(user_id):
+ tmp["group"][group_id][user_id] = {"keyword": [keyword]}
+ else:
+ tmp["group"][group_id][user_id]["keyword"].append(keyword)
+ await MessageUtils.build_message(
+ f"已成功{'通过' if flag else '拒绝'}搜图关键词:{keyword}..."
+ ).send()
+ for user in tmp["private"]:
+ text = ",".join(tmp["private"][user]["keyword"])
+ await PlatformUtils.send_message(
+ bot,
+ user,
+ None,
+ f"你的关键词/UID/PID {text} 已被管理员通过,将在下一次进行更新...",
+ )
+ # await bot.send_private_msg(
+ # user_id=user,
+ # message=f"你的关键词/UID/PID {x} 已被管理员通过,将在下一次进行更新...",
+ # )
+ for group in tmp["group"]:
+ for user in tmp["group"][group]:
+ text = ",".join(tmp["group"][group][user]["keyword"])
+ await PlatformUtils.send_message(
+ bot,
+ None,
+ group_id=group,
+ message=MessageUtils.build_message(
+ [
+ At(flag="user", target=user),
+ "你的关键词/UID/PID {x} 已被管理员通过,将在下一次进行更新...",
+ ]
+ ),
+ )
+ logger.info(
+ f" 通过了pixiv搜图关键词/UID: {keyword}", arparma.header_result, session=session
+ )
+
+
+@_del_matcher.handle()
+async def _(bot: Bot, session: EventSession, arparma: Arparma, type: str, keyword: str):
+ if type != "keyword":
+ keyword = f"{type}:{keyword}"
+ if data := await PixivKeywordUser.get_or_none(keyword=keyword):
+ await data.delete()
+ await MessageUtils.build_message(
+ f"删除搜图关键词/UID:{keyword} 成功..."
+ ).send()
+ logger.info(
+ f" 删除了pixiv搜图关键词: {keyword}", arparma.header_result, session=session
+ )
+ else:
+ await MessageUtils.build_message(
+ f"未查询到搜索关键词/UID/PID:{keyword},删除失败!"
+ ).send()
+
+
+@_del_pic_matcher.handle()
+async def _(bot: Bot, session: EventSession, arparma: Arparma, keyword: str):
+ msg = ""
+ black_pid = ""
+ flag = arparma.find("black")
+ img_p = None
+ if "p" in keyword:
+ img_p = keyword.split("p")[-1]
+ keyword = keyword.replace("_", "")
+ keyword = keyword[: keyword.find("p")]
+ elif "ugoira" in keyword:
+ img_p = keyword.split("ugoira")[-1]
+ keyword = keyword.replace("_", "")
+ keyword = keyword[: keyword.find("ugoira")]
+ if keyword.isdigit():
+ if await Pixiv.query_images(pid=int(keyword), r18=2):
+ if await remove_image(int(keyword), img_p):
+ msg += f'{keyword}{f"_p{img_p}" if img_p else ""},'
+ if flag:
+ if await PixivKeywordUser.exists(
+ keyword=f"black:{keyword}{f'_p{img_p}' if img_p else ''}"
+ ):
+ await PixivKeywordUser.create(
+ user_id="114514",
+ group_id="114514",
+ keyword=f"black:{keyword}{f'_p{img_p}' if img_p else ''}",
+ is_pass=False,
+ )
+ black_pid += f'{keyword}{f"_p{img_p}" if img_p else ""},'
+ logger.info(
+ f" 删除了PIX图片 PID:{keyword}{f'_p{img_p}' if img_p else ''}",
+ arparma.header_result,
+ session=session,
+ )
+ else:
+ await MessageUtils.build_message(
+ f"PIX:图片pix:{keyword}{f'_p{img_p}' if img_p else ''} 不存在...无法删除.."
+ ).send()
+ else:
+ await MessageUtils.build_message(f"PID必须为数字!pid:{keyword}").send(
+ reply_to=True
+ )
+ await MessageUtils.build_message(f"PIX:成功删除图片:{msg[:-1]}").send()
+ if flag:
+ await MessageUtils.build_message(
+ f"成功图片PID加入黑名单:{black_pid[:-1]}"
+ ).send()
diff --git a/zhenxun/plugins/pix_gallery/pix_show_info.py b/zhenxun/plugins/pix_gallery/pix_show_info.py
new file mode 100644
index 00000000..cb1cbf2a
--- /dev/null
+++ b/zhenxun/plugins/pix_gallery/pix_show_info.py
@@ -0,0 +1,85 @@
+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.utils import PluginExtraData
+from zhenxun.services.log import logger
+from zhenxun.utils.message import MessageUtils
+
+from ._data_source import gen_keyword_pic, get_keyword_num
+from ._model.pixiv_keyword_user import PixivKeywordUser
+
+__plugin_meta__ = PluginMetadata(
+ name="查看pix图库",
+ description="让我看看管理员私藏了多少货",
+ usage="""
+ 指令:
+ 我的pix关键词
+ 显示pix关键词
+ 查看pix图库 ?[tag]: 查看指定tag图片数量,为空时查看整个图库
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ ).dict(),
+)
+
+_my_matcher = on_alconna(Alconna("我的pix关键词"), priority=5, block=True)
+
+_show_matcher = on_alconna(Alconna("显示pix关键词"), priority=5, block=True)
+
+_pix_matcher = on_alconna(
+ Alconna("查看pix图库", Args["keyword?", str]), priority=5, block=True
+)
+
+
+@_my_matcher.handle()
+async def _(arparma: Arparma, session: EventSession):
+ data = await PixivKeywordUser.filter(user_id=session.id1).values_list(
+ "keyword", flat=True
+ )
+ if not data:
+ await MessageUtils.build_message("您目前没有提供任何Pixiv搜图关键字...").finish(
+ reply_to=True
+ )
+ await MessageUtils.build_message(f"您目前提供的如下关键字:\n\t" + ",".join(data)).send() # type: ignore
+ logger.info("查看我的pix关键词", arparma.header_result, session=session)
+
+
+@_show_matcher.handle()
+async def _(bot: Bot, arparma: Arparma, session: EventSession):
+ _pass_keyword, not_pass_keyword = await PixivKeywordUser.get_current_keyword()
+ if _pass_keyword or not_pass_keyword:
+ image = await gen_keyword_pic(
+ _pass_keyword, not_pass_keyword, session.id1 in bot.config.superusers
+ )
+ await MessageUtils.build_message(image).send() # type: ignore
+ else:
+ if session.id1 in bot.config.superusers:
+ await MessageUtils.build_message(
+ f"目前没有已收录或待收录的搜索关键词..."
+ ).send()
+ else:
+ await MessageUtils.build_message(f"目前没有已收录的搜索关键词...").send()
+
+
+@_pix_matcher.handle()
+async def _(bot: Bot, arparma: Arparma, session: EventSession, keyword: Match[str]):
+ _keyword = ""
+ if keyword.available:
+ _keyword = keyword.result
+ count, r18_count, count_, setu_count, r18_count_ = await get_keyword_num(_keyword)
+ await MessageUtils.build_message(
+ f"PIX图库:{_keyword}\n"
+ f"总数:{count + r18_count}\n"
+ f"美图:{count}\n"
+ f"R18:{r18_count}\n"
+ f"---------------\n"
+ f"Omega图库:{_keyword}\n"
+ f"总数:{count_ + setu_count + r18_count_}\n"
+ f"美图:{count_}\n"
+ f"色图:{setu_count}\n"
+ f"R18:{r18_count_}"
+ ).send()
+ logger.info("查看pix图库", arparma.header_result, session=session)
diff --git a/plugins/pix_gallery/pix_update.py b/zhenxun/plugins/pix_gallery/pix_update.py
old mode 100755
new mode 100644
similarity index 60%
rename from plugins/pix_gallery/pix_update.py
rename to zhenxun/plugins/pix_gallery/pix_update.py
index ce19604f..b0f209dc
--- a/plugins/pix_gallery/pix_update.py
+++ b/zhenxun/plugins/pix_gallery/pix_update.py
@@ -1,207 +1,225 @@
-import asyncio
-import os
-import re
-import time
-from pathlib import Path
-from typing import List
-
-from nonebot import on_command
-from nonebot.adapters.onebot.v11 import Message
-from nonebot.params import CommandArg
-from nonebot.permission import SUPERUSER
-
-from services.log import logger
-from utils.utils import is_number
-
-from ._data_source import start_update_image_url
-from ._model.omega_pixiv_illusts import OmegaPixivIllusts
-from ._model.pixiv import Pixiv
-from ._model.pixiv_keyword_user import PixivKeywordUser
-
-__zx_plugin_name__ = "pix检查更新 [Superuser]"
-__plugin_usage__ = """
-usage:
- 更新pix收录的所有或指定数量的 关键词/uid/pid
- 指令:
- 更新pix关键词 *[keyword/uid/pid] [num=max]: 更新仅keyword/uid/pid或全部
- pix检测更新:检测从未更新过的uid和pid
- 示例:更新pix关键词keyword
- 示例:更新pix关键词uid 10
-""".strip()
-__plugin_des__ = "pix图库收录数据检查更新"
-__plugin_cmd__ = ["更新pix关键词 *[keyword/uid/pid] [num=max]", "pix检测更新"]
-__plugin_version__ = 0.1
-__plugin_author__ = "HibiKier"
-
-start_update = on_command(
- "更新pix关键词", aliases={"更新pix关键字"}, permission=SUPERUSER, priority=1, block=True
-)
-
-check_not_update_uid_pid = on_command(
- "pix检测更新",
- aliases={"pix检查更新"},
- permission=SUPERUSER,
- priority=1,
- block=True,
-)
-
-check_omega = on_command("检测omega图库", permission=SUPERUSER, priority=1, block=True)
-
-
-@start_update.handle()
-async def _(arg: Message = CommandArg()):
- msg_sp = arg.extract_plain_text().strip().split()
- _pass_keyword, _ = await PixivKeywordUser.get_current_keyword()
- _pass_keyword.reverse()
- black_pid = await PixivKeywordUser.get_black_pid()
- _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:")]
- num = 9999
- msg = msg_sp[0] if len(msg_sp) else ""
- if len(msg_sp) == 2:
- if is_number(msg_sp[1]):
- num = int(msg_sp[1])
- else:
- await start_update.finish("参数错误...第二参数必须为数字")
- if num < 10000:
- keyword_str = ",".join(
- _keyword[: num if num < len(_keyword) else len(_keyword)]
- )
- uid_str = ",".join(_uid[: num if num < len(_uid) else len(_uid)])
- pid_str = ",".join(_pid[: num if num < len(_pid) else len(_pid)])
- if msg.lower() == "pid":
- update_lst = _pid
- info = f"开始更新Pixiv搜图PID:\n{pid_str}"
- elif msg.lower() == "uid":
- update_lst = _uid
- info = f"开始更新Pixiv搜图UID:\n{uid_str}"
- elif msg.lower() == "keyword":
- update_lst = _keyword
- info = f"开始更新Pixiv搜图关键词:\n{keyword_str}"
- else:
- update_lst = _pass_keyword
- info = f"开始更新Pixiv搜图关键词:\n{keyword_str}\n更新UID:{uid_str}\n更新PID:{pid_str}"
- num = num if num < len(update_lst) else len(update_lst)
- else:
- if msg.lower() == "pid":
- update_lst = [f"pid:{num}"]
- info = f"开始更新Pixiv搜图UID:\npid:{num}"
- else:
- update_lst = [f"uid:{num}"]
- info = f"开始更新Pixiv搜图UID:\nuid:{num}"
- await start_update.send(info)
- start_time = time.time()
- pid_count, pic_count = await start_update_image_url(update_lst[:num], black_pid)
- await start_update.send(
- f"Pixiv搜图关键词搜图更新完成...\n"
- f"累计更新PID {pid_count} 个\n"
- f"累计更新图片 {pic_count} 张" + "\n耗时:{:.2f}秒".format((time.time() - start_time))
- )
-
-
-@check_not_update_uid_pid.handle()
-async def _(arg: Message = CommandArg()):
- msg = arg.extract_plain_text().strip()
- flag = False
- if msg == "update":
- flag = True
- _pass_keyword, _ = await PixivKeywordUser.get_current_keyword()
- x_uid = []
- x_pid = []
- _uid = [int(x[4:]) for x in _pass_keyword if x.startswith("uid:")]
- _pid = [int(x[4:]) for x in _pass_keyword if x.startswith("pid:")]
- all_images = await Pixiv.query_images(r18=2)
- for img in all_images:
- if img.pid not in x_pid:
- x_pid.append(img.pid)
- if img.uid not in x_uid:
- x_uid.append(img.uid)
- await check_not_update_uid_pid.send(
- "从未更新过的UID:"
- + ",".join([f"uid:{x}" for x in _uid if x not in x_uid])
- + "\n"
- + "从未更新过的PID:"
- + ",".join([f"pid:{x}" for x in _pid if x not in x_pid])
- )
- if flag:
- await check_not_update_uid_pid.send("开始自动自动更新PID....")
- update_lst = [f"pid:{x}" for x in _uid if x not in x_uid]
- black_pid = await PixivKeywordUser.get_black_pid()
- start_time = time.time()
- pid_count, pic_count = await start_update_image_url(update_lst, black_pid)
- await check_not_update_uid_pid.send(
- f"Pixiv搜图关键词搜图更新完成...\n"
- f"累计更新PID {pid_count} 个\n"
- f"累计更新图片 {pic_count} 张" + "\n耗时:{:.2f}秒".format((time.time() - start_time))
- )
-
-
-@check_omega.handle()
-async def _():
- async def _tasks(line: str, all_pid: List[int], length: int, index: int):
- data = line.split("VALUES", maxsplit=1)[-1].strip()[1:-2]
- num_list = re.findall(r"(\d+)", data)
- pid = int(num_list[1])
- uid = int(num_list[2])
- id_ = 3
- while num_list[id_] not in ["0", "1"]:
- id_ += 1
- classified = int(num_list[id_])
- nsfw_tag = int(num_list[id_ + 1])
- width = int(num_list[id_ + 2])
- height = int(num_list[id_ + 3])
- str_list = re.findall(r"'(.*?)',", data)
- title = str_list[0]
- uname = str_list[1]
- tags = str_list[2]
- url = str_list[3]
- if pid in all_pid:
- logger.info(f"添加OmegaPixivIllusts图库数据已存在 ---> pid:{pid}")
- return
- _, is_create = await OmegaPixivIllusts.get_or_create(
- pid=pid,
- title=title,
- width=width,
- height=height,
- url=url,
- uid=uid,
- nsfw_tag=nsfw_tag,
- tags=tags,
- uname=uname,
- classified=classified,
- )
- if is_create:
- logger.info(
- f"成功添加OmegaPixivIllusts图库数据 pid:{pid} 本次预计存储 {length} 张,已更新第 {index} 张"
- )
- else:
- logger.info(f"添加OmegaPixivIllusts图库数据已存在 ---> pid:{pid}")
-
- omega_pixiv_illusts = None
- for file in os.listdir("."):
- if "omega_pixiv_artwork" in file and ".sql" in file:
- omega_pixiv_illusts = Path() / file
- if omega_pixiv_illusts:
- with open(omega_pixiv_illusts, "r", encoding="utf8") as f:
- lines = f.readlines()
- tasks = []
- length = len([x for x in lines if "INSERT INTO" in x.upper()])
- all_pid = await OmegaPixivIllusts.all().values_list("pid", flat=True)
- index = 0
- logger.info("检测到OmegaPixivIllusts数据库,准备开始更新....")
- for line in lines:
- if "INSERT INTO" in line.upper():
- index += 1
- logger.info(f"line: {line} 加入更新计划")
- tasks.append(
- asyncio.ensure_future(_tasks(line, all_pid, length, index))
- )
- await asyncio.gather(*tasks)
- omega_pixiv_illusts.unlink()
+import asyncio
+import os
+import re
+import time
+from pathlib import Path
+
+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,
+ store_true,
+)
+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 start_update_image_url
+from ._model.omega_pixiv_illusts import OmegaPixivIllusts
+from ._model.pixiv import Pixiv
+from ._model.pixiv_keyword_user import PixivKeywordUser
+
+__plugin_meta__ = PluginMetadata(
+ name="pix检查更新",
+ description="pix图库收录数据检查更新",
+ usage="""
+ 指令:
+ 更新pix关键词 *[keyword/uid/pid] [num=max]: 更新仅keyword/uid/pid或全部
+ pix检测更新:检测从未更新过的uid和pid
+ 示例:更新pix关键词keyword
+ 示例:更新pix关键词uid 10
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier", version="0.1", plugin_type=PluginType.SUPERUSER
+ ).dict(),
+)
+
+
+_update_matcher = on_alconna(
+ Alconna("更新pix关键词", Args["type", ["uid", "pid", "keyword"]]["num?", int]),
+ permission=SUPERUSER,
+ priority=1,
+ block=True,
+)
+
+_check_matcher = on_alconna(
+ Alconna(
+ "pix检测更新", Option("-u|--update", action=store_true, help_text="是否更新")
+ ),
+ permission=SUPERUSER,
+ priority=1,
+ block=True,
+)
+
+_omega_matcher = on_alconna(
+ Alconna("检测omega图库"), permission=SUPERUSER, priority=1, block=True
+)
+
+
+@_update_matcher.handle()
+async def _(arparma: Arparma, session: EventSession, type: str, num: Match[int]):
+ _pass_keyword, _ = await PixivKeywordUser.get_current_keyword()
+ _pass_keyword.reverse()
+ black_pid = await PixivKeywordUser.get_black_pid()
+ _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:")]
+ _num = num.result if num.available else 9999
+ if _num < 10000:
+ keyword_str = ",".join(
+ _keyword[: _num if _num < len(_keyword) else len(_keyword)]
+ )
+ uid_str = ",".join(_uid[: _num if _num < len(_uid) else len(_uid)])
+ pid_str = ",".join(_pid[: _num if _num < len(_pid) else len(_pid)])
+ if type == "pid":
+ update_lst = _pid
+ info = f"开始更新Pixiv搜图PID:\n{pid_str}"
+ elif type == "uid":
+ update_lst = _uid
+ info = f"开始更新Pixiv搜图UID:\n{uid_str}"
+ elif type == "keyword":
+ update_lst = _keyword
+ info = f"开始更新Pixiv搜图关键词:\n{keyword_str}"
+ else:
+ update_lst = _pass_keyword
+ info = f"开始更新Pixiv搜图关键词:\n{keyword_str}\n更新UID:{uid_str}\n更新PID:{pid_str}"
+ _num = _num if _num < len(update_lst) else len(update_lst)
+ else:
+ if type == "pid":
+ update_lst = [f"pid:{_num}"]
+ info = f"开始更新Pixiv搜图UID:\npid:{_num}"
+ else:
+ update_lst = [f"uid:{_num}"]
+ info = f"开始更新Pixiv搜图UID:\nuid:{_num}"
+ await MessageUtils.build_message(info).send()
+ start_time = time.time()
+ pid_count, pic_count = await start_update_image_url(
+ update_lst[:_num], black_pid, type == "pid"
+ )
+ await MessageUtils.build_message(
+ f"Pixiv搜图关键词搜图更新完成...\n"
+ f"累计更新PID {pid_count} 个\n"
+ f"累计更新图片 {pic_count} 张"
+ + "\n耗时:{:.2f}秒".format((time.time() - start_time))
+ ).send()
+ logger.info("更新pix关键词", arparma.header_result, session=session)
+
+
+@_check_matcher.handle()
+async def _(bot: Bot, arparma: Arparma, session: EventSession):
+ _pass_keyword, _ = await PixivKeywordUser.get_current_keyword()
+ x_uid = []
+ x_pid = []
+ _uid = [int(x[4:]) for x in _pass_keyword if x.startswith("uid:")]
+ _pid = [int(x[4:]) for x in _pass_keyword if x.startswith("pid:")]
+ all_images = await Pixiv.query_images(r18=2)
+ for img in all_images:
+ if img.pid not in x_pid:
+ x_pid.append(img.pid)
+ if img.uid not in x_uid:
+ x_uid.append(img.uid)
+ await MessageUtils.build_message(
+ "从未更新过的UID:"
+ + ",".join([f"uid:{x}" for x in _uid if x not in x_uid])
+ + "\n"
+ + "从未更新过的PID:"
+ + ",".join([f"pid:{x}" for x in _pid if x not in x_pid])
+ ).send()
+ if arparma.find("update"):
+ await MessageUtils.build_message("开始自动自动更新PID....").send()
+ update_lst = [f"pid:{x}" for x in _uid if x not in x_uid]
+ black_pid = await PixivKeywordUser.get_black_pid()
+ start_time = time.time()
+ pid_count, pic_count = await start_update_image_url(
+ update_lst, black_pid, False
+ )
+ await MessageUtils.build_message(
+ f"Pixiv搜图关键词搜图更新完成...\n"
+ f"累计更新PID {pid_count} 个\n"
+ f"累计更新图片 {pic_count} 张"
+ + "\n耗时:{:.2f}秒".format((time.time() - start_time))
+ ).send()
+ logger.info(
+ f"pix检测更新, 是否更新: {arparma.find('update')}",
+ arparma.header_result,
+ session=session,
+ )
+
+
+@_omega_matcher.handle()
+async def _():
+ async def _tasks(line: str, all_pid: list[int], length: int, index: int):
+ data = line.split("VALUES", maxsplit=1)[-1].strip()[1:-2]
+ num_list = re.findall(r"(\d+)", data)
+ pid = int(num_list[1])
+ uid = int(num_list[2])
+ id_ = 3
+ while num_list[id_] not in ["0", "1"]:
+ id_ += 1
+ classified = int(num_list[id_])
+ nsfw_tag = int(num_list[id_ + 1])
+ width = int(num_list[id_ + 2])
+ height = int(num_list[id_ + 3])
+ str_list = re.findall(r"'(.*?)',", data)
+ title = str_list[0]
+ uname = str_list[1]
+ tags = str_list[2]
+ url = str_list[3]
+ if pid in all_pid:
+ logger.info(f"添加OmegaPixivIllusts图库数据已存在 ---> pid:{pid}")
+ return
+ _, is_create = await OmegaPixivIllusts.get_or_create(
+ pid=pid,
+ title=title,
+ width=width,
+ height=height,
+ url=url,
+ uid=uid,
+ nsfw_tag=nsfw_tag,
+ tags=tags,
+ uname=uname,
+ classified=classified,
+ )
+ if is_create:
+ logger.info(
+ f"成功添加OmegaPixivIllusts图库数据 pid:{pid} 本次预计存储 {length} 张,已更新第 {index} 张"
+ )
+ else:
+ logger.info(f"添加OmegaPixivIllusts图库数据已存在 ---> pid:{pid}")
+
+ omega_pixiv_illusts = None
+ for file in os.listdir("."):
+ if "omega_pixiv_artwork" in file and ".sql" in file:
+ omega_pixiv_illusts = Path() / file
+ if omega_pixiv_illusts:
+ with open(omega_pixiv_illusts, "r", encoding="utf8") as f:
+ lines = f.readlines()
+ tasks = []
+ length = len([x for x in lines if "INSERT INTO" in x.upper()])
+ all_pid = await OmegaPixivIllusts.all().values_list("pid", flat=True)
+ index = 0
+ logger.info("检测到OmegaPixivIllusts数据库,准备开始更新....")
+ for line in lines:
+ if "INSERT INTO" in line.upper():
+ index += 1
+ logger.info(f"line: {line} 加入更新计划")
+ tasks.append(
+ asyncio.create_task(_tasks(line, all_pid, length, index)) # type: ignore
+ )
+ await asyncio.gather(*tasks)
+ omega_pixiv_illusts.unlink()
diff --git a/zhenxun/plugins/pixiv_rank_search/__init__.py b/zhenxun/plugins/pixiv_rank_search/__init__.py
new file mode 100644
index 00000000..01945cd8
--- /dev/null
+++ b/zhenxun/plugins/pixiv_rank_search/__init__.py
@@ -0,0 +1,225 @@
+from asyncio.exceptions import TimeoutError
+
+from httpx import NetworkError
+from nonebot.adapters import Bot
+from nonebot.plugin import PluginMetadata
+from nonebot.rule import to_me
+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.utils import is_valid_date
+
+from .data_source import download_pixiv_imgs, get_pixiv_urls, search_pixiv_urls
+
+__plugin_meta__ = PluginMetadata(
+ name="P站排行/搜图",
+ description="P站排行榜直接冲,P站搜图跟着冲",
+ usage="""
+ P站排行:
+ 可选参数:
+ 类型:
+ 1. 日排行
+ 2. 周排行
+ 3. 月排行
+ 4. 原创排行
+ 5. 新人排行
+ 6. R18日排行
+ 7. R18周排行
+ 8. R18受男性欢迎排行
+ 9. R18重口排行【慎重!】
+ 【使用时选择参数序号即可,R18仅可私聊】
+ p站排行 ?[参数] ?[数量] ?[日期]
+ 示例:
+ p站排行 [无参数默认为日榜]
+ p站排行 1
+ p站排行 1 5
+ p站排行 1 5 2018-4-25
+ 【注意空格!!】【在线搜索会较慢】
+ ---------------------------------
+ P站搜图:
+ 搜图 [关键词] ?[数量] ?[页数=1] ?[r18](不屏蔽R-18)
+ 示例:
+ 搜图 樱岛麻衣
+ 搜图 樱岛麻衣 5
+ 搜图 樱岛麻衣 5 r18
+ 搜图 樱岛麻衣#1000users 5
+ 【多个关键词用#分割】
+ 【默认为 热度排序】
+ 【注意空格!!】【在线搜索会较慢】【数量可能不符?可能该页数量不够,也可能被R-18屏蔽】
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ aliases={"P站排行", "搜图"},
+ menu_type="来点好康的",
+ limits=[BaseBlock(result="P站排行榜或搜图正在搜索,请不要重复触发命令...")],
+ configs=[
+ RegisterConfig(
+ key="TIMEOUT",
+ value=10,
+ help="图片下载超时限制",
+ default_value=10,
+ type=int,
+ ),
+ RegisterConfig(
+ key="MAX_PAGE_LIMIT",
+ value=20,
+ help="作品最大页数限制,超过的作品会被略过",
+ default_value=20,
+ type=int,
+ ),
+ RegisterConfig(
+ key="ALLOW_GROUP_R18",
+ value=False,
+ help="图允许群聊中使用 r18 参数",
+ default_value=False,
+ type=bool,
+ ),
+ RegisterConfig(
+ module="hibiapi",
+ key="HIBIAPI",
+ value="https://api.obfs.dev",
+ help="如果没有自建或其他hibiapi请不要修改",
+ default_value="https://api.obfs.dev",
+ ),
+ RegisterConfig(
+ module="pixiv",
+ key="PIXIV_NGINX_URL",
+ value="i.pixiv.re",
+ help="Pixiv反向代理",
+ ),
+ ],
+ ).dict(),
+)
+
+
+rank_dict = {
+ "1": "day",
+ "2": "week",
+ "3": "month",
+ "4": "week_original",
+ "5": "week_rookie",
+ "6": "day_r18",
+ "7": "week_r18",
+ "8": "day_male_r18",
+ "9": "week_r18g",
+}
+
+_rank_matcher = on_alconna(
+ Alconna("p站排行", Args["rank_type", int, 1]["num", int, 10]["datetime?", str]),
+ aliases={"p站排行榜"},
+ priority=5,
+ block=True,
+ rule=to_me(),
+)
+
+_keyword_matcher = on_alconna(
+ Alconna(
+ "搜图",
+ Args["keyword", str]["num", int, 10]["page", int, 1],
+ Option("-r", action=store_true, help_text="是否屏蔽r18"),
+ ),
+ priority=5,
+ block=True,
+ rule=to_me(),
+)
+
+
+@_rank_matcher.handle()
+async def _(
+ bot: Bot,
+ session: EventSession,
+ arparma: Arparma,
+ rank_type: int,
+ num: int,
+ datetime: Match[str],
+):
+ gid = session.id3 or session.id2
+ if not session.id1:
+ await MessageUtils.build_message("用户id为空...").finish()
+ code = 0
+ info_list = []
+ _datetime = None
+ if datetime.available:
+ _datetime = datetime.result
+ if not is_valid_date(_datetime):
+ await MessageUtils.build_message("日期不合法,示例: 2018-4-25").finish(
+ reply_to=True
+ )
+ if rank_type in [6, 7, 8, 9]:
+ if gid:
+ await MessageUtils.build_message("羞羞脸!私聊里自己看!").finish(
+ at_sender=True
+ )
+ info_list, code = await get_pixiv_urls(
+ rank_dict[str(rank_type)], num, date=_datetime
+ )
+ if code != 200 and info_list:
+ if isinstance(info_list[0], str):
+ await MessageUtils.build_message(info_list[0]).finish()
+ if not info_list:
+ await MessageUtils.build_message("没有找到啊,等等再试试吧~V").send(
+ at_sender=True
+ )
+ for title, author, urls in info_list:
+ try:
+ images = await download_pixiv_imgs(urls, session.id1) # type: ignore
+ await MessageUtils.build_message(
+ [f"title: {title}\nauthor: {author}\n"] + images # type: ignore
+ ).send()
+
+ except (NetworkError, TimeoutError):
+ await MessageUtils.build_message("这张图网络直接炸掉了!").send()
+ logger.info(
+ f" 查看了P站排行榜 rank_type{rank_type}", arparma.header_result, session=session
+ )
+
+
+@_keyword_matcher.handle()
+async def _(
+ bot: Bot, session: EventSession, arparma: Arparma, keyword: str, num: int, page: int
+):
+ gid = session.id3 or session.id2
+ if not session.id1:
+ await MessageUtils.build_message("用户id为空...").finish()
+ if gid:
+ if arparma.find("r") and not Config.get_config(
+ "pixiv_rank_search", "ALLOW_GROUP_R18"
+ ):
+ await MessageUtils.build_message("(脸红#) 你不会害羞的 八嘎!").finish(
+ at_sender=True
+ )
+ r18 = 0 if arparma.find("r") else 1
+ info_list = None
+ keyword = keyword.replace("#", " ")
+ info_list, code = await search_pixiv_urls(keyword, num, page, r18)
+ if code != 200 and isinstance(info_list[0], str):
+ await MessageUtils.build_message(info_list[0]).finish()
+ if not info_list:
+ await MessageUtils.build_message("没有找到啊,等等再试试吧~V").finish(
+ at_sender=True
+ )
+ for title, author, urls in info_list:
+ try:
+ images = await download_pixiv_imgs(urls, session.id1) # type: ignore
+ await MessageUtils.build_message(
+ [f"title: {title}\nauthor: {author}\n"] + images # type: ignore
+ ).send()
+
+ except (NetworkError, TimeoutError):
+ await MessageUtils.build_message("这张图网络直接炸掉了!").send()
+ logger.info(
+ f" 查看了搜索 {keyword} R18:{r18}", arparma.header_result, session=session
+ )
diff --git a/zhenxun/plugins/pixiv_rank_search/data_source.py b/zhenxun/plugins/pixiv_rank_search/data_source.py
new file mode 100644
index 00000000..761a93f2
--- /dev/null
+++ b/zhenxun/plugins/pixiv_rank_search/data_source.py
@@ -0,0 +1,171 @@
+from asyncio.exceptions import TimeoutError
+from pathlib import Path
+
+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.utils import change_img_md5
+
+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/",
+}
+
+
+async def get_pixiv_urls(
+ mode: str, num: int = 10, page: int = 1, date: str | None = None
+) -> tuple[list[tuple[str, str, list[str]] | str], int]:
+ """获取排行榜图片url
+
+ 参数:
+ mode: 模式类型
+ num: 数量.
+ page: 页数.
+ date: 日期.
+
+ 返回:
+ tuple[list[tuple[str, str, list[str]] | str], int]: 图片标题作者url数据,请求状态
+ """
+
+ params = {"mode": mode, "page": page}
+ if date:
+ params["date"] = date
+ hibiapi = Config.get_config("hibiapi", "HIBIAPI")
+ hibiapi = hibiapi[:-1] if hibiapi[-1] == "/" else hibiapi
+ rank_url = f"{hibiapi}/api/pixiv/rank"
+ return await parser_data(rank_url, num, params, "rank")
+
+
+async def search_pixiv_urls(
+ keyword: str, num: int, page: int, r18: int
+) -> tuple[list[tuple[str, str, list[str]] | str], int]:
+ """搜图图片url
+
+ 参数:
+ keyword: 关键词
+ num: 数量
+ page: 页数
+ r18: 是否r18
+
+ 返回:
+ tuple[list[tuple[str, str, list[str]] | str], int]: 图片标题作者url数据,请求状态
+ """
+ params = {"word": keyword, "page": page}
+ hibiapi = Config.get_config("hibiapi", "HIBIAPI")
+ hibiapi = hibiapi[:-1] if hibiapi[-1] == "/" else hibiapi
+ search_url = f"{hibiapi}/api/pixiv/search"
+ return await parser_data(search_url, num, params, "search", r18)
+
+
+async def parser_data(
+ url: str, num: int, params: dict, type_: str, r18: int = 0
+) -> tuple[list[tuple[str, str, list[str]] | str], int]:
+ """解析数据搜索
+
+ 参数:
+ url: 访问URL
+ num: 数量
+ params: 请求参数
+ type_: 类型,rank或search
+ r18: 是否r18.
+
+ 返回:
+ tuple[list[tuple[str, str, list[str]] | str], int]: 图片标题作者url数据,请求状态
+ """
+ info_list = []
+ for _ in range(3):
+ try:
+ response = await AsyncHttpx.get(
+ url,
+ params=params,
+ timeout=Config.get_config("pixiv_rank_search", "TIMEOUT"),
+ )
+ if response.status_code == 200:
+ data = response.json()
+ if data.get("illusts"):
+ data = data["illusts"]
+ break
+ except TimeoutError:
+ pass
+ except Exception as e:
+ logger.error(f"P站排行/搜图解析数据发生错误", e=e)
+ return ["发生了一些些错误..."], 995
+ else:
+ return ["网络不太好?没有该页数?也许过一会就好了..."], 998
+ num = num if num < 30 else 30
+ _data = []
+ for x in data:
+ if x["page_count"] < Config.get_config("pixiv_rank_search", "MAX_PAGE_LIMIT"):
+ if type_ == "search" and r18 == 1:
+ if "R-18" in str(x["tags"]):
+ continue
+ _data.append(x)
+ if len(_data) == num:
+ break
+ for x in _data:
+ title = x["title"]
+ author = x["user"]["name"]
+ urls = []
+ if x["page_count"] == 1:
+ urls.append(x["image_urls"]["large"])
+ else:
+ for j in x["meta_pages"]:
+ urls.append(j["image_urls"]["large"])
+ info_list.append((title, author, urls))
+ return info_list, 200
+
+
+async def download_pixiv_imgs(
+ urls: list[str], user_id: str, forward_msg_index: int | None = None
+) -> list[Path]:
+ """下载图片
+
+ 参数:
+ urls: 图片链接
+ user_id: 用户id
+ forward_msg_index: 转发消息中的图片排序.
+
+ 返回:
+ MessageFactory: 图片
+ """
+ result_list = []
+ index = 0
+ for url in urls:
+ ws_url = Config.get_config("pixiv", "PIXIV_NGINX_URL")
+ url = url.replace("_webp", "")
+ if ws_url:
+ url = url.replace("i.pximg.net", ws_url).replace("i.pixiv.cat", ws_url)
+ try:
+ file = (
+ TEMP_PATH / f"{user_id}_{forward_msg_index}_{index}_pixiv.jpg"
+ if forward_msg_index is not None
+ else TEMP_PATH / f"{user_id}_{index}_pixiv.jpg"
+ )
+ file = Path(file)
+ try:
+ if await AsyncHttpx.download_file(
+ url,
+ file,
+ timeout=Config.get_config("pixiv_rank_search", "TIMEOUT"),
+ headers=headers,
+ ):
+ change_img_md5(file)
+ image = None
+ if forward_msg_index is not None:
+ image = (
+ TEMP_PATH
+ / f"{user_id}_{forward_msg_index}_{index}_pixiv.jpg"
+ )
+ else:
+ image = TEMP_PATH / f"{user_id}_{index}_pixiv.jpg"
+ if image:
+ result_list.append(image)
+ index += 1
+ except OSError:
+ if file.exists():
+ file.unlink()
+ except Exception as e:
+ logger.error(f"P站排行/搜图下载图片错误", e=e)
+ return result_list
diff --git a/plugins/poke/__init__.py b/zhenxun/plugins/poke/__init__.py
old mode 100755
new mode 100644
similarity index 53%
rename from plugins/poke/__init__.py
rename to zhenxun/plugins/poke/__init__.py
index 180935c2..7f46be96
--- a/plugins/poke/__init__.py
+++ b/zhenxun/plugins/poke/__init__.py
@@ -1,86 +1,86 @@
-import os
-import random
-
-from nonebot import on_notice
-from nonebot.adapters.onebot.v11 import PokeNotifyEvent
-
-from configs.path_config import IMAGE_PATH, RECORD_PATH
-from models.ban_user import BanUser
-from services.log import logger
-from utils.message_builder import image, poke, record
-from utils.utils import CountLimiter
-
-__zx_plugin_name__ = "戳一戳"
-
-__plugin_usage__ = """
-usage:
- 戳一戳随机掉落语音或美图萝莉图
-""".strip()
-__plugin_des__ = "戳一戳发送语音美图萝莉图不美哉?"
-__plugin_type__ = ("其他",)
-__plugin_version__ = 0.1
-__plugin_author__ = "HibiKier"
-__plugin_settings__ = {
- "level": 5,
- "default_status": True,
- "limit_superuser": False,
- "cmd": ["戳一戳"],
-}
-
-poke__reply = [
- "lsp你再戳?",
- "连个可爱美少女都要戳的肥宅真恶心啊。",
- "你再戳!",
- "?再戳试试?",
- "别戳了别戳了再戳就坏了555",
- "我爪巴爪巴,球球别再戳了",
- "你戳你🐎呢?!",
- "那...那里...那里不能戳...绝对...",
- "(。´・ω・)ん?",
- "有事恁叫我,别天天一个劲戳戳戳!",
- "欸很烦欸!你戳🔨呢",
- "?",
- "再戳一下试试?",
- "???",
- "正在关闭对您的所有服务...关闭成功",
- "啊呜,太舒服刚刚竟然睡着了。什么事?",
- "正在定位您的真实地址...定位成功。轰炸机已起飞",
-]
-
-
-_clmt = CountLimiter(3)
-
-poke_ = on_notice(priority=5, block=False)
-
-
-@poke_.handle()
-async def _poke_event(event: PokeNotifyEvent):
- if event.self_id == event.target_id:
- _clmt.add(event.user_id)
- if _clmt.check(event.user_id) or random.random() < 0.3:
- rst = ""
- if random.random() < 0.15:
- await BanUser.ban(event.user_id, 1, 60)
- rst = "气死我了!"
- await poke_.finish(rst + random.choice(poke__reply), at_sender=True)
- rand = random.random()
- path = random.choice(["luoli", "meitu"])
- if rand <= 0.3 and len(os.listdir(IMAGE_PATH / "image_management" / path)) > 0:
- index = random.randint(
- 0, len(os.listdir(IMAGE_PATH / "image_management" / path)) - 1
- )
- result = f"id:{index}" + image(
- IMAGE_PATH / "image_management" / path / f"{index}.jpg"
- )
- await poke_.send(result)
- logger.info(f"USER {event.user_id} 戳了戳我 回复: {result} {result}")
- elif 0.3 < rand < 0.6:
- voice = random.choice(os.listdir(RECORD_PATH / "dinggong"))
- result = record(RECORD_PATH / "dinggong" / voice)
- await poke_.send(result)
- await poke_.send(voice.split("_")[1])
- logger.info(
- f'USER {event.user_id} 戳了戳我 回复: {result} \n {voice.split("_")[1]}'
- )
- else:
- await poke_.send(poke(event.user_id))
+import os
+import random
+
+from nonebot import on_notice
+from nonebot.adapters.onebot.v11 import PokeNotifyEvent
+from nonebot.adapters.onebot.v11.message import MessageSegment
+from nonebot.plugin import PluginMetadata
+
+from zhenxun.configs.path_config import IMAGE_PATH, RECORD_PATH
+from zhenxun.configs.utils import PluginExtraData
+from zhenxun.models.ban_console import BanConsole
+from zhenxun.services.log import logger
+from zhenxun.utils.message import MessageUtils
+from zhenxun.utils.utils import CountLimiter
+
+__plugin_meta__ = PluginMetadata(
+ name="戳一戳",
+ description="戳一戳发送语音美图萝莉图不美哉?",
+ usage="""
+ 戳一戳随机掉落语音或美图萝莉图
+ """.strip(),
+ extra=PluginExtraData(author="HibiKier", version="0.1", menu_type="其他").dict(),
+)
+
+REPLY_MESSAGE = [
+ "lsp你再戳?",
+ "连个可爱美少女都要戳的肥宅真恶心啊。",
+ "你再戳!",
+ "?再戳试试?",
+ "别戳了别戳了再戳就坏了555",
+ "我爪巴爪巴,球球别再戳了",
+ "你戳你🐎呢?!",
+ "那...那里...那里不能戳...绝对...",
+ "(。´・ω・)ん?",
+ "有事恁叫我,别天天一个劲戳戳戳!",
+ "欸很烦欸!你戳🔨呢",
+ "?",
+ "再戳一下试试?",
+ "???",
+ "正在关闭对您的所有服务...关闭成功",
+ "啊呜,太舒服刚刚竟然睡着了。什么事?",
+ "正在定位您的真实地址...定位成功。轰炸机已起飞",
+]
+
+
+_clmt = CountLimiter(3)
+
+poke_ = on_notice(priority=5, block=False)
+
+
+@poke_.handle()
+async def _(event: PokeNotifyEvent):
+ uid = str(event.user_id) if event.user_id else None
+ gid = str(event.group_id) if event.group_id else None
+ if event.self_id == event.target_id:
+ _clmt.increase(event.user_id)
+ if _clmt.check(event.user_id) or random.random() < 0.3:
+ rst = ""
+ if random.random() < 0.15:
+ await BanConsole.ban(uid, gid, 1, 60)
+ rst = "气死我了!"
+ await poke_.finish(rst + random.choice(REPLY_MESSAGE), at_sender=True)
+ rand = random.random()
+ path = random.choice(["luoli", "meitu"])
+ if rand <= 0.3 and len(os.listdir(IMAGE_PATH / "image_management" / path)) > 0:
+ index = random.randint(
+ 0, len(os.listdir(IMAGE_PATH / "image_management" / path)) - 1
+ )
+ await MessageUtils.build_message(
+ [
+ f"id: {index}",
+ IMAGE_PATH / "image_management" / path / f"{index}.jpg",
+ ]
+ ).send()
+ logger.info(f"USER {event.user_id} 戳了戳我")
+ elif 0.3 < rand < 0.6:
+ voice = random.choice(os.listdir(RECORD_PATH / "dinggong"))
+ result = MessageSegment.record(RECORD_PATH / "dinggong" / voice)
+ await poke_.send(result)
+ await poke_.send(voice.split("_")[1])
+ logger.info(
+ f'USER {event.user_id} 戳了戳我 回复: {result} \n {voice.split("_")[1]}',
+ "戳一戳",
+ )
+ else:
+ await poke_.send(MessageSegment("poke", {"qq": event.user_id}))
diff --git a/zhenxun/plugins/quotations.py b/zhenxun/plugins/quotations.py
new file mode 100644
index 00000000..e213ee04
--- /dev/null
+++ b/zhenxun/plugins/quotations.py
@@ -0,0 +1,32 @@
+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.http_utils import AsyncHttpx
+from zhenxun.utils.message import MessageUtils
+
+__plugin_meta__ = PluginMetadata(
+ name="一言二次元语录",
+ description="二次元语录给你力量",
+ usage="""
+ usage:
+ 一言二次元语录
+ 指令:
+ 语录/二次元
+ """.strip(),
+ extra=PluginExtraData(author="HibiKier", version="0.1").dict(),
+)
+
+URL = "https://international.v1.hitokoto.cn/?c=a"
+
+_matcher = on_alconna(Alconna("语录"), aliases={"二次元"}, priority=5, block=True)
+
+
+@_matcher.handle()
+async def _(session: EventSession, arparma: Arparma):
+ data = (await AsyncHttpx.get(URL, timeout=5)).json()
+ result = f'{data["hitokoto"]}\t——{data["from"]}'
+ await MessageUtils.build_message(result).send()
+ logger.info(f" 发送语录:" + result, arparma.header_result, session=session)
diff --git a/zhenxun/plugins/roll.py b/zhenxun/plugins/roll.py
new file mode 100644
index 00000000..7c953496
--- /dev/null
+++ b/zhenxun/plugins/roll.py
@@ -0,0 +1,66 @@
+import asyncio
+import random
+
+from nonebot import on_command
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import UniMsg
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.config import NICKNAME
+from zhenxun.configs.utils import PluginExtraData
+from zhenxun.services.log import logger
+from zhenxun.utils.depends import UserName
+from zhenxun.utils.message import MessageUtils
+
+__plugin_meta__ = PluginMetadata(
+ name="roll",
+ description="犹豫不决吗?那就让我帮你决定吧",
+ usage="""
+ usage:
+ 随机数字 或 随机选择事件
+ 指令:
+ roll: 随机 0-100 的数字
+ roll *[文本]: 随机事件
+ 示例:roll 吃饭 睡觉 打游戏
+ """.strip(),
+ extra=PluginExtraData(author="HibiKier", version="0.1").dict(),
+)
+
+
+_matcher = on_command("roll", priority=5, block=True)
+
+
+@_matcher.handle()
+async def _(
+ session: EventSession,
+ message: UniMsg,
+ user_name: str = UserName(),
+):
+ text = message.extract_plain_text().strip().replace("roll", "", 1).split()
+ if not text:
+ await MessageUtils.build_message(f"roll: {random.randint(0, 100)}").finish(
+ reply_to=True
+ )
+ await MessageUtils.build_message(
+ random.choice(
+ [
+ "转动命运的齿轮,拨开眼前迷雾...",
+ f"启动吧,命运的水晶球,为{user_name}指引方向!",
+ "嗯哼,在此刻转动吧!命运!",
+ f"在此祈愿,请为{user_name}降下指引...",
+ ]
+ )
+ ).send()
+ await asyncio.sleep(1)
+ random_text = random.choice(text)
+ await MessageUtils.build_message(
+ random.choice(
+ [
+ f"让{NICKNAME}看看是什么结果!答案是:‘{random_text}’",
+ f"根据命运的指引,接下来{user_name} ‘{random_text}’ 会比较好",
+ f"祈愿被回应了!是 ‘{random_text}’!",
+ f"结束了,{user_name},命运之轮停在了 ‘{random_text}’!",
+ ]
+ )
+ ).send(reply_to=True)
+ logger.info(f"发送roll:{text}", "roll", session=session)
diff --git a/zhenxun/plugins/russian/__init__.py b/zhenxun/plugins/russian/__init__.py
new file mode 100644
index 00000000..ee25fdfd
--- /dev/null
+++ b/zhenxun/plugins/russian/__init__.py
@@ -0,0 +1,201 @@
+from nonebot.adapters import Bot
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import Arparma
+from nonebot_plugin_alconna import At as alcAt
+from nonebot_plugin_alconna import Match, UniMsg
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.utils import PluginExtraData, RegisterConfig
+from zhenxun.services.log import logger
+from zhenxun.utils.depends import UserName
+from zhenxun.utils.message import MessageUtils
+
+from .command import (
+ _accept_matcher,
+ _rank_matcher,
+ _record_matcher,
+ _refuse_matcher,
+ _russian_matcher,
+ _settlement_matcher,
+ _shoot_matcher,
+)
+from .data_source import Russian, russian_manage
+from .model import RussianUser
+
+__plugin_meta__ = PluginMetadata(
+ name="俄罗斯轮盘",
+ description="虽然是运气游戏,但这可是战场啊少年",
+ usage="""
+ 又到了决斗时刻
+ 指令:
+ 装弹 [金额] [子弹数] ?[at]: 开启游戏,装填子弹,可选自定义金额,或邀请决斗对象
+ 接受对决: 接受当前存在的对决
+ 拒绝对决: 拒绝邀请的对决
+ 开枪: 开出未知的一枪
+ 结算: 强行结束当前比赛 (仅当一方未开枪超过30秒时可使用)
+ 我的战绩: 对,你的战绩
+ 轮盘胜场排行/轮盘败场排行/轮盘欧洲人排行/轮盘慈善家排行/轮盘最高连胜排行/轮盘最高连败排行: 各种排行榜
+ 示例:装弹 100 3 @sdd
+ * 注:同一时间群内只能有一场对决 *
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ menu_type="群内小游戏",
+ configs=[
+ RegisterConfig(
+ key="MAX_RUSSIAN_BET_GOLD",
+ value=1000,
+ help="俄罗斯轮盘最大赌注金额",
+ default_value=1000,
+ type=int,
+ )
+ ],
+ ).dict(),
+)
+
+
+@_russian_matcher.handle()
+async def _(money: int, num: Match[str], at_user: Match[alcAt]):
+ _russian_matcher.set_path_arg("money", money)
+ if num.available:
+ _russian_matcher.set_path_arg("num", num.result)
+ if at_user.available:
+ _russian_matcher.set_path_arg("at_user", at_user.result.target)
+
+
+@_russian_matcher.got_path(
+ "num", prompt="请输入装填子弹的数量!(最多6颗,输入取消来取消装弹)"
+)
+async def _(
+ bot: Bot,
+ session: EventSession,
+ message: UniMsg,
+ arparma: Arparma,
+ money: int,
+ num: str,
+ at_user: Match[alcAt],
+ uname: str = UserName(),
+):
+ gid = session.id2
+ if message.extract_plain_text() == "取消":
+ await MessageUtils.build_message("已取消装弹...").finish()
+ if not session.id1:
+ await MessageUtils.build_message("用户id为空...").finish()
+ if not gid:
+ await MessageUtils.build_message("群组id为空...").finish()
+ if money <= 0:
+ await MessageUtils.build_message("赌注金额必须大于0!").finish(reply_to=True)
+ if num in ["取消", "算了"]:
+ await MessageUtils.build_message("已取消装弹...").finish()
+ if not num.isdigit():
+ await MessageUtils.build_message("输入的子弹数必须是数字!").finish(
+ reply_to=True
+ )
+ b_num = int(num)
+ if b_num < 0 or b_num > 6:
+ await MessageUtils.build_message("子弹数量必须在1-6之间!").finish(reply_to=True)
+ _at_user = at_user.result.target if at_user.available else None
+ rus = Russian(
+ at_user=_at_user, player1=(session.id1, uname), money=money, bullet_num=b_num
+ )
+ result = await russian_manage.add_russian(bot, gid, rus)
+ await result.send()
+ logger.info(
+ f"添加俄罗斯轮盘 装弹: {b_num}, 金额: {money}",
+ arparma.header_result,
+ session=session,
+ )
+
+
+@_accept_matcher.handle()
+async def _(bot: Bot, session: EventSession, arparma: Arparma, uname: str = UserName()):
+ gid = session.id2
+ if not session.id1:
+ await MessageUtils.build_message("用户id为空...").finish()
+ if not gid:
+ await MessageUtils.build_message("群组id为空...").finish()
+ result = await russian_manage.accept(bot, gid, session.id1, uname)
+ await result.send()
+ logger.info(f"俄罗斯轮盘接受对决", arparma.header_result, session=session)
+
+
+@_refuse_matcher.handle()
+async def _(session: EventSession, arparma: Arparma, uname: str = UserName()):
+ gid = session.id2
+ if not session.id1:
+ await MessageUtils.build_message("用户id为空...").finish()
+ if not gid:
+ await MessageUtils.build_message("群组id为空...").finish()
+ result = russian_manage.refuse(gid, session.id1, uname)
+ await result.send()
+ logger.info(f"俄罗斯轮盘拒绝对决", arparma.header_result, session=session)
+
+
+@_settlement_matcher.handle()
+async def _(session: EventSession, arparma: Arparma):
+ gid = session.id2
+ if not session.id1:
+ await MessageUtils.build_message("用户id为空...").finish()
+ if not gid:
+ await MessageUtils.build_message("群组id为空...").finish()
+ result = await russian_manage.settlement(gid, session.id1, session.platform)
+ await result.send()
+ logger.info(f"俄罗斯轮盘结算", arparma.header_result, session=session)
+
+
+@_shoot_matcher.handle()
+async def _(bot: Bot, session: EventSession, arparma: Arparma, uname: str = UserName()):
+ gid = session.id2
+ if not session.id1:
+ await MessageUtils.build_message("用户id为空...").finish()
+ if not gid:
+ await MessageUtils.build_message("群组id为空...").finish()
+ result, settle = await russian_manage.shoot(
+ bot, gid, session.id1, uname, session.platform
+ )
+ await result.send()
+ if settle:
+ await settle.send()
+ logger.info(f"俄罗斯轮盘开枪", arparma.header_result, session=session)
+
+
+@_record_matcher.handle()
+async def _(session: EventSession, arparma: Arparma):
+ gid = session.id2
+ if not session.id1:
+ await MessageUtils.build_message("用户id为空...").finish()
+ if not gid:
+ await MessageUtils.build_message("群组id为空...").finish()
+ user, _ = await RussianUser.get_or_create(user_id=session.id1, group_id=gid)
+ await MessageUtils.build_message(
+ f"俄罗斯轮盘\n"
+ f"总胜利场次:{user.win_count}\n"
+ f"当前连胜:{user.winning_streak}\n"
+ f"最高连胜:{user.max_winning_streak}\n"
+ f"总失败场次:{user.fail_count}\n"
+ f"当前连败:{user.losing_streak}\n"
+ f"最高连败:{user.max_losing_streak}\n"
+ f"赚取金币:{user.make_money}\n"
+ f"输掉金币:{user.lose_money}",
+ ).send(reply_to=True)
+ logger.info(f"俄罗斯轮盘查看战绩", arparma.header_result, session=session)
+
+
+@_rank_matcher.handle()
+async def _(session: EventSession, arparma: Arparma, rank_type: str, num: int):
+ gid = session.id2
+ if not session.id1:
+ await MessageUtils.build_message("用户id为空...").finish()
+ if not gid:
+ await MessageUtils.build_message("群组id为空...").finish()
+ if 51 < num or num < 10:
+ num = 10
+ result = await russian_manage.rank(session.id1, gid, rank_type, num)
+ if isinstance(result, str):
+ await MessageUtils.build_message(result).finish(reply_to=True)
+ result.show()
+ await MessageUtils.build_message(result).send(reply_to=True)
+ logger.info(
+ f"查看轮盘排行: {rank_type} 数量: {num}", arparma.header_result, session=session
+ )
diff --git a/zhenxun/plugins/russian/command.py b/zhenxun/plugins/russian/command.py
new file mode 100644
index 00000000..a20dd2af
--- /dev/null
+++ b/zhenxun/plugins/russian/command.py
@@ -0,0 +1,108 @@
+from nonebot_plugin_alconna import Alconna, Args
+from nonebot_plugin_alconna import At as alcAt
+from nonebot_plugin_alconna import on_alconna
+
+from zhenxun.utils.rules import ensure_group
+
+_russian_matcher = on_alconna(
+ Alconna(
+ "俄罗斯轮盘",
+ Args["money", int]["num?", str]["at_user?", alcAt],
+ ),
+ aliases={"装弹", "俄罗斯转盘"},
+ rule=ensure_group,
+ priority=5,
+ block=True,
+)
+
+_accept_matcher = on_alconna(
+ Alconna("接受对决"),
+ aliases={"接受决斗", "接受挑战"},
+ rule=ensure_group,
+ priority=5,
+ block=True,
+)
+
+_refuse_matcher = on_alconna(
+ Alconna("拒绝对决"),
+ aliases={"拒绝决斗", "拒绝挑战"},
+ rule=ensure_group,
+ priority=5,
+ block=True,
+)
+
+_shoot_matcher = on_alconna(
+ Alconna("开枪"),
+ aliases={"咔", "嘭", "嘣"},
+ rule=ensure_group,
+ priority=5,
+ block=True,
+)
+
+_settlement_matcher = on_alconna(
+ Alconna("结算"),
+ rule=ensure_group,
+ priority=5,
+ block=True,
+)
+
+_record_matcher = on_alconna(
+ Alconna("我的战绩"),
+ rule=ensure_group,
+ priority=5,
+ block=True,
+)
+
+_rank_matcher = on_alconna(
+ Alconna(
+ "russian-rank",
+ Args["rank_type", ["win", "lose", "a", "b", "max_win", "max_lose"]][
+ "num?", int, 10
+ ],
+ ),
+ rule=ensure_group,
+ priority=5,
+ block=True,
+)
+
+_rank_matcher.shortcut(
+ r"轮盘胜场排行(?P\d*)",
+ command="russian-rank",
+ arguments=["win", "{num}"],
+ prefix=True,
+)
+
+_rank_matcher.shortcut(
+ r"轮盘败场排行(?P\d*)",
+ command="russian-rank",
+ arguments=["lose", "{num}"],
+ prefix=True,
+)
+
+_rank_matcher.shortcut(
+ r"轮盘欧洲人排行(?P\d*)",
+ command="russian-rank",
+ arguments=["a", "{num}"],
+ prefix=True,
+)
+
+_rank_matcher.shortcut(
+ r"轮盘慈善家排行(?P\d*)",
+ command="russian-rank",
+ arguments=["b", "{num}"],
+ prefix=True,
+)
+
+_rank_matcher.shortcut(
+ r"轮盘最高连胜排行(?P\d*)",
+ command="russian-rank",
+ arguments=["max_win", "{num}"],
+ prefix=True,
+)
+
+_rank_matcher.shortcut(
+ r"轮盘最高连败排行(?P\d*)",
+ command="russian-rank",
+ arguments=["max_lose", "{num}"],
+ prefix=True,
+)
diff --git a/zhenxun/plugins/russian/data_source.py b/zhenxun/plugins/russian/data_source.py
new file mode 100644
index 00000000..eb8381cb
--- /dev/null
+++ b/zhenxun/plugins/russian/data_source.py
@@ -0,0 +1,535 @@
+import random
+import time
+from datetime import datetime, timedelta
+
+from apscheduler.jobstores.base import JobLookupError
+from nonebot.adapters import Bot
+from nonebot_plugin_alconna import At, UniMessage
+from nonebot_plugin_apscheduler import scheduler
+from pydantic import BaseModel
+
+from zhenxun.configs.config import NICKNAME, Config
+from zhenxun.models.group_member_info import GroupInfoUser
+from zhenxun.models.user_console import UserConsole
+from zhenxun.utils.enum import GoldHandle
+from zhenxun.utils.exception import InsufficientGold
+from zhenxun.utils.image_utils import BuildImage, BuildMat, MatType, text2image
+from zhenxun.utils.message import MessageUtils
+from zhenxun.utils.platform import PlatformUtils
+
+from .model import RussianUser
+
+base_config = Config.get("russian")
+
+
+class Russian(BaseModel):
+
+ at_user: str | None
+ """指定决斗对象"""
+ player1: tuple[str, str]
+ """玩家1id, 昵称"""
+ player2: tuple[str, str] | None = None
+ """玩家2id, 昵称"""
+ money: int
+ """金额"""
+ bullet_num: int
+ """子弹数"""
+ bullet_arr: list[int] = []
+ """子弹排列"""
+ bullet_index: int = 0
+ """当前子弹下标"""
+ next_user: str = ""
+ """下一个开枪用户"""
+ time: float = time.time()
+ """创建时间"""
+ win_user: str | None = None
+ """胜利者"""
+
+
+class RussianManage:
+
+ def __init__(self) -> None:
+ self._data: dict[str, Russian] = {}
+
+ def __check_is_timeout(self, group_id: str) -> bool:
+ """检查决斗是否超时
+
+ 参数:
+ group_id: 群组id
+
+ 返回:
+ bool: 是否超时
+ """
+ if russian := self._data.get(group_id):
+ if russian.time + 30 < time.time():
+ return True
+ return False
+
+ def __random_bullet(self, num: int) -> list[int]:
+ """随机排列子弹
+
+ 参数:
+ num: 子弹数量
+
+ 返回:
+ list[int]: 子弹排列数组
+ """
+ bullet_list = [0, 0, 0, 0, 0, 0, 0]
+ for i in random.sample([0, 1, 2, 3, 4, 5, 6], num):
+ bullet_list[i] = 1
+ return bullet_list
+
+ def __remove_job(self, group_id: str):
+ """移除定时任务
+
+ 参数:
+ group_id: 群组id
+ """
+ try:
+ scheduler.remove_job(f"russian_job_{group_id}")
+ except JobLookupError:
+ pass
+
+ def __build_job(
+ self, bot: Bot, group_id: str, is_add: bool = False, platform: str | None = None
+ ):
+ """移除定时任务和构建新定时任务
+
+ 参数:
+ bot: Bot
+ group_id: 群组id
+ is_add: 是否添加新定时任务.
+ platform: 平台
+ """
+ self.__remove_job(group_id)
+ if is_add:
+ date = datetime.now() + timedelta(seconds=31)
+ scheduler.add_job(
+ self.__auto_end_game,
+ "date",
+ run_date=date.replace(microsecond=0),
+ id=f"russian_job_{group_id}",
+ args=[bot, group_id, platform],
+ )
+
+ async def __auto_end_game(self, bot: Bot, group_id: str, platform: str):
+ """自动结束对决
+
+ 参数:
+ bot: Bot
+ group_id: 群组id
+ platform: 平台
+ """
+ result = await self.settlement(group_id, None, platform)
+ if result:
+ await PlatformUtils.send_message(bot, None, group_id, result)
+
+ async def add_russian(self, bot: Bot, group_id: str, rus: Russian) -> UniMessage:
+ """添加决斗
+
+ 参数:
+ bot: Bot
+ group_id: 群组id
+ rus: Russian
+
+ 返回:
+ UniMessage: 返回消息
+ """
+ russian = self._data.get(group_id)
+ if russian:
+ if russian.time + 30 < time.time():
+ if not russian.player2:
+ return MessageUtils.build_message(
+ f"现在是 {russian.player1[1]} 发起的对决, 请接受对决或等待决斗超时..."
+ )
+ else:
+ return MessageUtils.build_message(
+ f"{russian.player1[1]} 和 {russian.player2[1]}的对决还未结束!"
+ )
+ return MessageUtils.build_message(
+ f"现在是 {russian.player1[1]} 发起的对决\n请等待比赛结束后再开始下一轮..."
+ )
+ max_money = base_config.get("MAX_RUSSIAN_BET_GOLD")
+ if rus.money > max_money:
+ return MessageUtils.build_message(f"太多了!单次金额不能超过{max_money}!")
+ user = await UserConsole.get_user(rus.player1[0])
+ if user.gold < rus.money:
+ return MessageUtils.build_message("你没有足够的钱支撑起这场挑战")
+ rus.bullet_arr = self.__random_bullet(rus.bullet_num)
+ self._data[group_id] = rus
+ message_list: list[str | At] = []
+ if rus.at_user:
+ user = await GroupInfoUser.get_or_none(
+ user_id=rus.at_user, group_id=group_id
+ )
+ message_list = [
+ f"{rus.player1[1]} 向",
+ At(flag="user", target=rus.at_user),
+ f"发起了决斗!请 {user.user_name if user else rus.at_user} 在30秒内回复‘接受对决’ or ‘拒绝对决’,超时此次决斗作废!",
+ ]
+ else:
+ message_list = [
+ "若30秒内无人接受挑战则此次对决作废【首次游玩请at我发送 ’帮助俄罗斯轮盘‘ 来查看命令】"
+ ]
+ result = (
+ "咔 " * rus.bullet_num
+ + f"装填完毕\n挑战金额:{rus.money}\n第一枪的概率为:{float(rus.bullet_num) / 7.0 * 100:.2f}%\n"
+ )
+
+ message_list.insert(0, result)
+ self.__build_job(bot, group_id, True)
+ return MessageUtils.build_message(message_list) # type: ignore
+
+ async def accept(
+ self, bot: Bot, group_id: str, user_id: str, uname: str
+ ) -> UniMessage:
+ """接受对决
+
+ 参数:
+ bot: Bot
+ group_id: 群组id
+ user_id: 用户id
+ uname: 用户名称
+
+ 返回:
+ Text | MessageFactory: 返回消息
+ """
+ if russian := self._data.get(group_id):
+ if russian.at_user and russian.at_user != user_id:
+ return MessageUtils.build_message("又不是找你决斗,你接受什么啊!气!")
+ if russian.player2:
+ return MessageUtils.build_message(
+ "当前决斗已被其他玩家接受!请等待下局对决!"
+ )
+ if russian.player1[0] == user_id:
+ return MessageUtils.build_message("你发起的对决,你接受什么啊!气!")
+ user = await UserConsole.get_user(user_id)
+ if user.gold < russian.money:
+ return MessageUtils.build_message("你没有足够的钱来接受这场挑战...")
+ russian.player2 = (user_id, uname)
+ russian.next_user = russian.player1[0]
+ self.__build_job(bot, group_id, True)
+ return MessageUtils.build_message(
+ [
+ "决斗已经开始!请",
+ At(flag="user", target=russian.player1[0]),
+ "先开枪!",
+ ]
+ )
+ return MessageUtils.build_message(
+ "目前没有进行的决斗,请发送 装弹 开启决斗吧!"
+ )
+
+ def refuse(self, group_id: str, user_id: str, uname: str) -> UniMessage:
+ """拒绝决斗
+
+ 参数:
+ group_id: 群组id
+ user_id: 用户id
+ uname: 用户名称
+
+ 返回:
+ Text | MessageFactory: 返回消息
+ """
+ if russian := self._data.get(group_id):
+ if russian.at_user:
+ if russian.at_user != user_id:
+ return MessageUtils.build_message(
+ "又不是找你决斗,你拒绝什么啊!气!"
+ )
+ del self._data[group_id]
+ self.__remove_job(group_id)
+ return MessageUtils.build_message(
+ [
+ At(flag="user", target=russian.player1[0]),
+ f"{uname}拒绝了你的对决!",
+ ]
+ )
+ return MessageUtils.build_message("当前决斗并没有指定对手,无法拒绝哦!")
+ return MessageUtils.build_message(
+ "目前没有进行的决斗,请发送 装弹 开启决斗吧!"
+ )
+
+ async def shoot(
+ self, bot: Bot, group_id: str, user_id: str, uname: str, platform: str
+ ) -> tuple[UniMessage, UniMessage | None]:
+ """开枪
+
+ 参数:
+ bot: Bot
+ group_id: 群组id
+ user_id: 用户id
+ uname: 用户名称
+ platform: 平台
+
+ 返回:
+ Text | MessageFactory: 返回消息
+ """
+ if russian := self._data.get(group_id):
+ if not russian.player2:
+ return (
+ MessageUtils.build_message("当前还没有玩家接受对决,无法开枪..."),
+ None,
+ )
+ if user_id not in [russian.player1[0], russian.player2[0]]:
+ """非玩家1和玩家2发送开枪"""
+ return (
+ MessageUtils.build_message(
+ random.choice(
+ [
+ f"不要打扰 {russian.player1[1]} 和 {russian.player2[1]} 的决斗啊!",
+ f"给我好好做好一个观众!不然{NICKNAME}就要生气了",
+ f"不要捣乱啊baka{uname}!",
+ ]
+ )
+ ),
+ None,
+ )
+ if user_id != russian.next_user:
+ """相同玩家连续开枪"""
+ return (
+ MessageUtils.build_message(
+ f"你的左轮不是连发的!该 {russian.player2[1]} 开枪了!"
+ ),
+ None,
+ )
+ if russian.bullet_arr[russian.bullet_index] == 1:
+ """去世"""
+ result = MessageUtils.build_message(
+ random.choice(
+ [
+ '"嘭!",你直接去世了',
+ "眼前一黑,你直接穿越到了异世界...(死亡)",
+ "终究还是你先走一步...",
+ ]
+ )
+ )
+ settle = await self.settlement(group_id, user_id, platform)
+ return result, settle
+ else:
+ """存活"""
+ p = (
+ (russian.bullet_index + russian.bullet_num + 1)
+ / len(russian.bullet_arr)
+ * 100
+ )
+ result = (
+ random.choice(
+ [
+ "呼呼,没有爆裂的声响,你活了下来",
+ "虽然黑洞洞的枪口很恐怖,但好在没有子弹射出来,你活下来了",
+ '"咔",你没死,看来运气不错',
+ ]
+ )
+ + f"\n下一枪中弹的概率: {p:.2f}%, 轮到 "
+ )
+ next_user = (
+ russian.player2[0]
+ if russian.next_user == russian.player1[0]
+ else russian.player1[0]
+ )
+ russian.next_user = next_user
+ russian.bullet_index += 1
+ self.__build_job(bot, group_id, True)
+ return (
+ MessageUtils.build_message(
+ [result, At(flag="user", target=next_user), " 了!"]
+ ),
+ None,
+ )
+ return (
+ MessageUtils.build_message("目前没有进行的决斗,请发送 装弹 开启决斗吧!"),
+ None,
+ )
+
+ async def settlement(
+ self, group_id: str, user_id: str | None, platform: str | None = None
+ ) -> UniMessage:
+ """结算
+
+ 参数:
+ group_id: 群组id
+ user_id: 用户id
+ platform: 平台
+
+ 返回:
+ Text | MessageFactory: 返回消息
+ """
+ if russian := self._data.get(group_id):
+ if not russian.player2:
+ if self.__check_is_timeout(group_id):
+ del self._data[group_id]
+ return MessageUtils.build_message(
+ "规定时间内还未有人接受决斗,当前决斗过期..."
+ )
+ return MessageUtils.build_message("决斗还未开始,,无法结算哦...")
+ if user_id and user_id not in [russian.player1[0], russian.player2[0]]:
+ return MessageUtils.build_message(f"吃瓜群众不要捣乱!黄牌警告!")
+ if not self.__check_is_timeout(group_id):
+ return MessageUtils.build_message(
+ f"{russian.player1[1]} 和 {russian.player2[1]} 比赛并未超时,请继续比赛..."
+ )
+ win_user = None
+ lose_user = None
+ if win_user:
+ russian.next_user = (
+ russian.player1[0]
+ if win_user == russian.player2[0]
+ else russian.player2[0]
+ )
+ if russian.next_user != russian.player1[0]:
+ win_user = russian.player1
+ lose_user = russian.player2
+ else:
+ win_user = russian.player2
+ lose_user = russian.player1
+ if win_user and lose_user:
+ rand = 0
+ if russian.money > 10:
+ rand = random.randint(0, 5)
+ fee = int(russian.money * float(rand) / 100)
+ fee = 1 if fee < 1 and rand != 0 else fee
+ else:
+ fee = 0
+ winner = await RussianUser.add_count(win_user[0], group_id, "win")
+ loser = await RussianUser.add_count(lose_user[0], group_id, "lose")
+ await RussianUser.money(
+ win_user[0], group_id, "win", russian.money - fee
+ )
+ await RussianUser.money(lose_user[0], group_id, "lose", russian.money)
+ await UserConsole.add_gold(
+ win_user[0], russian.money - fee, "russian", platform
+ )
+ try:
+ await UserConsole.reduce_gold(
+ lose_user[0],
+ russian.money,
+ GoldHandle.PLUGIN,
+ "russian",
+ platform,
+ )
+ except InsufficientGold:
+ if u := await UserConsole.get_user(lose_user[0]):
+ u.gold = 0
+ await u.save(update_fields=["gold"])
+ result = [
+ "这场决斗是 ",
+ At(flag="user", target=win_user[0]),
+ " 胜利了!",
+ ]
+ image = await text2image(
+ f"结算:\n"
+ f"\t胜者:{win_user[1]}\n"
+ f"\t赢取金币:{russian.money - fee}\n"
+ f"\t累计胜场:{winner.win_count}\n"
+ f"\t累计赚取金币:{winner.make_money}\n"
+ f"-------------------\n"
+ f"\t败者:{lose_user[1]}\n"
+ f"\t输掉金币:{russian.money}\n"
+ f"\t累计败场:{loser.fail_count}\n"
+ f"\t累计输掉金币:{loser.lose_money}\n"
+ f"-------------------\n"
+ f"哼哼,{NICKNAME}从中收取了 {float(rand)}%({fee}金币) 作为手续费!\n"
+ f"子弹排列:{russian.bullet_arr}",
+ padding=10,
+ color="#f9f6f2",
+ )
+ self.__remove_job(group_id)
+ result.append(image)
+ del self._data[group_id]
+ return MessageUtils.build_message(result)
+ return MessageUtils.build_message("赢家和输家获取错误...")
+ return MessageUtils.build_message("比赛并没有开始...无法结算...")
+
+ async def __get_x_index(self, users: list[RussianUser], group_id: str):
+ uid_list = [u.user_id for u in users]
+ group_user_list = await GroupInfoUser.filter(
+ user_id__in=uid_list, group_id=group_id
+ ).all()
+ group_user = {gu.user_id: gu.user_name for gu in group_user_list}
+ data = []
+ for uid in uid_list:
+ if uid in group_user:
+ data.append(group_user[uid])
+ else:
+ data.append(uid)
+ return data
+
+ async def rank(
+ self, user_id: str, group_id: str, rank_type: str, num: int
+ ) -> BuildImage | str:
+ x_index = []
+ data = []
+ title = ""
+ x_name = ""
+ if rank_type == "win":
+ users = (
+ await RussianUser.filter(group_id=group_id, win_count__not=0)
+ .order_by("win_count")
+ .limit(num)
+ )
+ x_index = await self.__get_x_index(users, group_id)
+ data = [u.win_count for u in users]
+ title = "胜场排行"
+ x_name = "场次"
+ if rank_type == "lose":
+ users = (
+ await RussianUser.filter(group_id=group_id, fail_count__not=0)
+ .order_by("fail_count")
+ .limit(num)
+ )
+ x_index = await self.__get_x_index(users, group_id)
+ data = [u.fail_count for u in users]
+ title = "败场排行"
+ x_name = "场次"
+ if rank_type == "a":
+ users = (
+ await RussianUser.filter(group_id=group_id, make_money__not=0)
+ .order_by("make_money")
+ .limit(num)
+ )
+ x_index = await self.__get_x_index(users, group_id)
+ data = [u.make_money for u in users]
+ title = "欧洲人排行"
+ x_name = "金币"
+ if rank_type == "b":
+ users = (
+ await RussianUser.filter(group_id=group_id, lose_money__not=0)
+ .order_by("lose_money")
+ .limit(num)
+ )
+ x_index = await self.__get_x_index(users, group_id)
+ data = [u.lose_money for u in users]
+ title = "慈善家排行"
+ x_name = "金币"
+ if rank_type == "max_win":
+ users = (
+ await RussianUser.filter(group_id=group_id, max_winning_streak__not=0)
+ .order_by("max_winning_streak")
+ .limit(num)
+ )
+ x_index = await self.__get_x_index(users, group_id)
+ data = [u.max_winning_streak for u in users]
+ title = "最高连胜排行"
+ x_name = "场次"
+ if rank_type == "max_lose":
+ users = (
+ await RussianUser.filter(group_id=group_id, max_losing_streak__not=0)
+ .order_by("max_losing_streak")
+ .limit(num)
+ )
+ x_index = await self.__get_x_index(users, group_id)
+ data = [u.max_losing_streak for u in users]
+ title = "最高连败排行"
+ x_name = "场次"
+ if not data:
+ return "当前数据为空..."
+ mat = BuildMat(MatType.BARH)
+ mat.x_index = x_index
+ mat.data = data # type: ignore
+ mat.title = title
+ mat.x_name = x_name
+ return await mat.build()
+
+
+russian_manage = RussianManage()
diff --git a/plugins/russian/model.py b/zhenxun/plugins/russian/model.py
old mode 100755
new mode 100644
similarity index 87%
rename from plugins/russian/model.py
rename to zhenxun/plugins/russian/model.py
index 4875f185..0fab9298
--- a/plugins/russian/model.py
+++ b/zhenxun/plugins/russian/model.py
@@ -1,6 +1,6 @@
from tortoise import fields
-from services.db_context import Model
+from zhenxun.services.db_context import Model
class RussianUser(Model):
@@ -35,13 +35,12 @@ class RussianUser(Model):
@classmethod
async def add_count(cls, user_id: str, group_id: str, itype: str):
- """
+ """添加用户输赢次数
+
说明:
- 添加用户输赢次数
- 说明:
- :param user_id: qq号
- :param group_id: 群号
- :param itype: 输或赢 'win' or 'lose'
+ user_id: 用户id
+ group_id: 群号
+ itype: 输或赢 'win' or 'lose'
"""
user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id)
if itype == "win":
@@ -80,17 +79,17 @@ class RussianUser(Model):
"max_losing_streak",
]
)
+ return user
@classmethod
- async def money(cls, user_id: str, group_id: str, itype: str, count: int) -> bool:
- """
- 说明:
- 添加用户输赢金钱
+ async def money(cls, user_id: str, group_id: str, itype: str, count: int):
+ """添加用户输赢金钱
+
参数:
- :param user_id: qq号
- :param group_id: 群号
- :param itype: 输或赢 'win' or 'lose'
- :param count: 金钱数量
+ user_id: 用户id
+ group_id: 群号
+ itype: 输或赢 'win' or 'lose'
+ count: 金钱数量
"""
user, _ = await cls.get_or_create(user_id=str(user_id), group_id=group_id)
if itype == "win":
diff --git a/zhenxun/plugins/search_anime/__init__.py b/zhenxun/plugins/search_anime/__init__.py
new file mode 100644
index 00000000..d12ad03e
--- /dev/null
+++ b/zhenxun/plugins/search_anime/__init__.py
@@ -0,0 +1,68 @@
+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.utils import BaseBlock, PluginExtraData, RegisterConfig
+from zhenxun.services.log import logger
+from zhenxun.utils.message import MessageUtils
+
+from .data_source import from_anime_get_info
+
+__plugin_meta__ = PluginMetadata(
+ name="搜番",
+ description="找不到想看的动漫吗?",
+ usage="""
+ 搜索动漫资源
+ 指令:
+ 搜番 [番剧名称或者关键词]
+ 示例:搜番 刀剑神域
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ menu_type="一些工具",
+ limits=[BaseBlock(result="搜索还未完成,不要重复触发!")],
+ configs=[
+ RegisterConfig(
+ key="SEARCH_ANIME_MAX_INFO",
+ value=20,
+ help="搜索动漫返回的最大数量",
+ default_value=20,
+ type=int,
+ )
+ ],
+ ).dict(),
+)
+
+_matcher = on_alconna(Alconna("搜番", Args["name?", str]), priority=5, block=True)
+
+
+@_matcher.handle()
+async def _(name: Match[str]):
+ if name.available:
+ _matcher.set_path_arg("name", name.result)
+
+
+@_matcher.got_path("name", prompt="是不是少了番名?")
+async def _(session: EventSession, arparma: Arparma, name: str):
+ gid = session.id3 or session.id2
+ await MessageUtils.build_message(f"开始搜番 {name}...").send()
+ anime_report = await from_anime_get_info(
+ name,
+ Config.get_config("search_anime", "SEARCH_ANIME_MAX_INFO"),
+ )
+ if anime_report:
+ if isinstance(anime_report, str):
+ await MessageUtils.build_message(anime_report).finish()
+ await MessageUtils.build_message("\n\n".join(anime_report)).send()
+ logger.info(
+ f"搜索番剧 {name} 成功: {anime_report}",
+ arparma.header_result,
+ session=session,
+ )
+ else:
+ logger.info(f"未找到番剧 {name}...")
+ await MessageUtils.build_message(
+ f"未找到番剧 {name}(也有可能是超时,再尝试一下?)"
+ ).send()
diff --git a/plugins/search_anime/data_source.py b/zhenxun/plugins/search_anime/data_source.py
old mode 100755
new mode 100644
similarity index 65%
rename from plugins/search_anime/data_source.py
rename to zhenxun/plugins/search_anime/data_source.py
index 7adb6836..59d0ac61
--- a/plugins/search_anime/data_source.py
+++ b/zhenxun/plugins/search_anime/data_source.py
@@ -1,52 +1,53 @@
-from lxml import etree
-import feedparser
-from urllib import parse
-from services.log import logger
-from utils.http_utils import AsyncHttpx
-from typing import List, Union
-import time
-
-
-async def from_anime_get_info(key_word: str, max_: int) -> Union[str, List[str]]:
- s_time = time.time()
- url = "https://share.dmhy.org/topics/rss/rss.xml?keyword=" + parse.quote(key_word)
- try:
- repass = await get_repass(url, max_)
- except Exception as e:
- logger.error(f"发生了一些错误 {type(e)}:{e}")
- return "发生了一些错误!"
- repass.insert(0, f"搜索 {key_word} 结果(耗时 {int(time.time() - s_time)} 秒):\n")
- return repass
-
-
-async def get_repass(url: str, max_: int) -> List[str]:
- put_line = []
- text = (await AsyncHttpx.get(url)).text
- d = feedparser.parse(text)
- max_ = max_ if max_ < len([e.link for e in d.entries]) else len([e.link for e in d.entries])
- url_list = [e.link for e in d.entries][:max_]
- for u in url_list:
- try:
- text = (await AsyncHttpx.get(u)).text
- html = etree.HTML(text)
- magent = html.xpath('.//a[@id="a_magnet"]/text()')[0]
- title = html.xpath(".//h3/text()")[0]
- item = html.xpath(
- '//div[@class="info resource-info right"]/ul/li'
- )
- class_a = (
- item[0]
- .xpath("string(.)")[5:]
- .strip()
- .replace("\xa0", "")
- .replace("\t", "")
- )
- size = item[3].xpath("string(.)")[5:].strip()
- put_line.append(
- "【{}】| {}\n【{}】| {}".format(class_a, title, size, magent)
- )
- except Exception as e:
- logger.error(f"搜番发生错误 {type(e)}:{e}")
- return put_line
-
-
+import time
+from urllib import parse
+
+import feedparser
+from lxml import etree
+
+from zhenxun.services.log import logger
+from zhenxun.utils.http_utils import AsyncHttpx
+
+
+async def from_anime_get_info(key_word: str, max_: int) -> str | list[str]:
+ s_time = time.time()
+ url = "https://share.dmhy.org/topics/rss/rss.xml?keyword=" + parse.quote(key_word)
+ try:
+ repass = await get_repass(url, max_)
+ except Exception as e:
+ logger.error(f"发生了一些错误 {type(e)}", e=e)
+ return "发生了一些错误!"
+ repass.insert(0, f"搜索 {key_word} 结果(耗时 {int(time.time() - s_time)} 秒):\n")
+ return repass
+
+
+async def get_repass(url: str, max_: int) -> list[str]:
+ put_line = []
+ text = (await AsyncHttpx.get(url)).text
+ d = feedparser.parse(text)
+ max_ = (
+ max_
+ if max_ < len([e.link for e in d.entries])
+ else len([e.link for e in d.entries])
+ )
+ url_list = [e.link for e in d.entries][:max_]
+ for u in url_list:
+ try:
+ text = (await AsyncHttpx.get(u)).text
+ html = etree.HTML(text) # type: ignore
+ magent = html.xpath('.//a[@id="a_magnet"]/text()')[0]
+ title = html.xpath(".//h3/text()")[0]
+ item = html.xpath('//div[@class="info resource-info right"]/ul/li')
+ class_a = (
+ item[0]
+ .xpath("string(.)")[5:]
+ .strip()
+ .replace("\xa0", "")
+ .replace("\t", "")
+ )
+ size = item[3].xpath("string(.)")[5:].strip()
+ put_line.append(
+ "【{}】| {}\n【{}】| {}".format(class_a, title, size, magent)
+ )
+ except Exception as e:
+ logger.error(f"搜番发生错误", e=e)
+ return put_line
diff --git a/zhenxun/plugins/search_buff_skin_price/__init__.py b/zhenxun/plugins/search_buff_skin_price/__init__.py
new file mode 100644
index 00000000..da224aaf
--- /dev/null
+++ b/zhenxun/plugins/search_buff_skin_price/__init__.py
@@ -0,0 +1,104 @@
+from nonebot.permission import SUPERUSER
+from nonebot.plugin import PluginMetadata
+from nonebot.rule import to_me
+from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.config import NICKNAME
+from zhenxun.configs.utils import BaseBlock, PluginExtraData, RegisterConfig
+from zhenxun.services.log import logger
+from zhenxun.utils.message import MessageUtils
+
+from .data_source import get_price, update_buff_cookie
+
+__plugin_meta__ = PluginMetadata(
+ name="BUFF查询皮肤",
+ description="BUFF皮肤底价查询",
+ usage="""
+ 在线实时获取BUFF指定皮肤所有磨损底价
+ 指令:
+ 查询皮肤 [枪械名] [皮肤名称]
+ 示例:查询皮肤 ak47 二西莫夫
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ menu_type="一些工具",
+ limits=[BaseBlock(result="您有皮肤正在搜索,请稍等...")],
+ configs=[
+ RegisterConfig(
+ key="BUFF_PROXY",
+ value=None,
+ help="BUFF代理,有些厂ip可能被屏蔽",
+ ),
+ RegisterConfig(
+ key="COOKIE",
+ value=None,
+ help="BUFF的账号cookie",
+ ),
+ ],
+ ).dict(),
+)
+
+
+_matcher = on_alconna(
+ Alconna("查询皮肤", Args["name", str]["skin", str]),
+ aliases={"皮肤查询"},
+ priority=5,
+ block=True,
+)
+
+_cookie_matcher = on_alconna(
+ Alconna("设置cookie", Args["cookie", str]),
+ rule=to_me(),
+ permission=SUPERUSER,
+ priority=1,
+)
+
+
+@_matcher.handle()
+async def _(name: Match[str], skin: Match[str]):
+ if name.available:
+ _matcher.set_path_arg("name", name.result)
+ if skin.available:
+ _matcher.set_path_arg("skin", skin.result)
+
+
+@_matcher.got_path("name", prompt="要查询什么武器呢?")
+@_matcher.got_path("skin", prompt="要查询该武器的什么皮肤呢?")
+async def arg_handle(
+ session: EventSession,
+ arparma: Arparma,
+ name: str,
+ skin: str,
+):
+ if name in ["算了", "取消"] or skin in ["算了", "取消"]:
+ await MessageUtils.build_message("已取消操作...").finish()
+ result = ""
+ if name in ["ak", "ak47"]:
+ name = "ak-47"
+ name = name + " | " + skin
+ status_code = -1
+ try:
+ result, status_code = await get_price(name)
+ except FileNotFoundError:
+ await MessageUtils.build_message(
+ f'请先对{NICKNAME}说"设置cookie"来设置cookie!'
+ ).send(at_sender=True)
+ if status_code in [996, 997, 998]:
+ await MessageUtils.build_message(result).finish()
+ if result:
+ logger.info(f"查询皮肤: {name}", arparma.header_result, session=session)
+ await MessageUtils.build_message(result).finish()
+ else:
+ logger.info(
+ f" 查询皮肤:{name} 没有查询到", arparma.header_result, session=session
+ )
+ await MessageUtils.build_message("没有查询到哦,请检查格式吧").send()
+
+
+@_cookie_matcher.handle()
+async def _(session: EventSession, arparma: Arparma, cookie: str):
+ result = update_buff_cookie(cookie)
+ await MessageUtils.build_message(result).send(at_sender=True)
+ logger.info("更新BUFF COOKIE", arparma.header_result, session=session)
diff --git a/plugins/search_buff_skin_price/data_source.py b/zhenxun/plugins/search_buff_skin_price/data_source.py
old mode 100755
new mode 100644
similarity index 84%
rename from plugins/search_buff_skin_price/data_source.py
rename to zhenxun/plugins/search_buff_skin_price/data_source.py
index 5ab86040..8dbe6a59
--- a/plugins/search_buff_skin_price/data_source.py
+++ b/zhenxun/plugins/search_buff_skin_price/data_source.py
@@ -1,58 +1,62 @@
-from asyncio.exceptions import TimeoutError
-from configs.config import Config
-from utils.http_utils import AsyncHttpx
-from services.log import logger
-
-
-url = "https://buff.163.com/api/market/goods"
-
-
-async def get_price(d_name: str) -> "str, int":
- """
- 查看皮肤价格
- :param d_name: 武器皮肤,如:awp 二西莫夫
- """
- cookie = {"session": Config.get_config("search_buff_skin_price", "COOKIE")}
- name_list = []
- price_list = []
- parameter = {"game": "csgo", "page_num": "1", "search": d_name}
- try:
- response = await AsyncHttpx.get(
- url,
- proxy=Config.get_config("search_buff_skin_price", "BUFF_PROXY"),
- params=parameter,
- cookies=cookie,
- )
- if response.status_code == 200:
- try:
- if response.text.find("Login Required") != -1:
- return "BUFF登录被重置,请联系管理员重新登入", 996
- data = response.json()["data"]
- total_page = data["total_page"]
- data = data["items"]
- for _ in range(total_page):
- for i in range(len(data)):
- name = data[i]["name"]
- price = data[i]["sell_reference_price"]
- name_list.append(name)
- price_list.append(price)
- except Exception as e:
- logger.error(f"BUFF查询皮肤发生错误 {type(e)}:{e}")
- return "没有查询到...", 998
- else:
- return "访问失败!", response.status_code
- except TimeoutError:
- return "访问超时! 请重试或稍后再试!", 997
- result = f"皮肤: {d_name}({len(name_list)})\n"
- for i in range(len(name_list)):
- result += name_list[i] + ": " + price_list[i] + "\n"
- return result[:-1], 999
-
-
-def update_buff_cookie(cookie: str) -> str:
- Config.set_config("search_buff_skin_price", "COOKIE", cookie)
- return "更新cookie成功"
-
-
-if __name__ == "__main__":
- print(get_price("awp 二西莫夫"))
+from asyncio.exceptions import TimeoutError
+
+from zhenxun.configs.config import Config
+from zhenxun.services.log import logger
+from zhenxun.utils.http_utils import AsyncHttpx
+
+url = "https://buff.163.com/api/market/goods"
+
+
+async def get_price(d_name: str) -> tuple[str, int]:
+ """查看皮肤价格
+
+ 参数:
+ d_name: 武器皮肤,如:awp 二西莫夫
+
+ 返回:
+ tuple[str, int]: 查询数据和状态
+ """
+ cookie = {"session": Config.get_config("search_buff_skin_price", "COOKIE")}
+ name_list = []
+ price_list = []
+ parameter = {"game": "csgo", "page_num": "1", "search": d_name}
+ try:
+ response = await AsyncHttpx.get(
+ url,
+ proxy=Config.get_config("search_buff_skin_price", "BUFF_PROXY"),
+ params=parameter,
+ cookies=cookie,
+ )
+ if response.status_code == 200:
+ try:
+ if response.text.find("Login Required") != -1:
+ return "BUFF登录被重置,请联系管理员重新登入", 996
+ data = response.json()["data"]
+ total_page = data["total_page"]
+ data = data["items"]
+ for _ in range(total_page):
+ for i in range(len(data)):
+ name = data[i]["name"]
+ price = data[i]["sell_reference_price"]
+ name_list.append(name)
+ price_list.append(price)
+ except Exception as e:
+ logger.error(f"BUFF查询皮肤发生错误 {type(e)}:{e}")
+ return "没有查询到...", 998
+ else:
+ return "访问失败!", response.status_code
+ except TimeoutError:
+ return "访问超时! 请重试或稍后再试!", 997
+ result = f"皮肤: {d_name}({len(name_list)})\n"
+ for i in range(len(name_list)):
+ result += name_list[i] + ": " + price_list[i] + "\n"
+ return result[:-1], 999
+
+
+def update_buff_cookie(cookie: str) -> str:
+ Config.set_config("search_buff_skin_price", "COOKIE", cookie)
+ return "更新cookie成功"
+
+
+if __name__ == "__main__":
+ print(get_price("awp 二西莫夫"))
diff --git a/zhenxun/plugins/search_image/__init__.py b/zhenxun/plugins/search_image/__init__.py
new file mode 100644
index 00000000..38e86de0
--- /dev/null
+++ b/zhenxun/plugins/search_image/__init__.py
@@ -0,0 +1,94 @@
+from pathlib import Path
+
+from nonebot.adapters import Bot
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import Alconna, Args, Arparma
+from nonebot_plugin_alconna import Image as alcImg
+from nonebot_plugin_alconna import 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.platform import PlatformUtils
+
+from .saucenao import get_saucenao_image
+
+__plugin_meta__ = PluginMetadata(
+ name="识图",
+ description="以图搜图,看破本源",
+ usage="""
+ 识别图片 [二次元图片]
+ 指令:
+ 识图 [图片]
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ menu_type="一些工具",
+ configs=[
+ RegisterConfig(
+ key="MAX_FIND_IMAGE_COUNT",
+ value=3,
+ help="搜索动漫返回的最大数量",
+ default_value=3,
+ type=int,
+ ),
+ RegisterConfig(
+ key="API_KEY",
+ value=None,
+ help="Saucenao的API_KEY,通过 https://saucenao.com/user.php?page=search-api 注册获取",
+ ),
+ ],
+ ).dict(),
+)
+
+
+_matcher = on_alconna(
+ Alconna("识图", Args["mode?", str]["image?", alcImg]), block=True, priority=5
+)
+
+
+async def get_image_info(mod: str, url: str) -> str | list[str | Path] | None:
+ if mod == "saucenao":
+ return await get_saucenao_image(url)
+
+
+@_matcher.handle()
+async def _(mode: Match[str], image: Match[alcImg]):
+ if mode.available:
+ _matcher.set_path_arg("mode", mode.result)
+ else:
+ _matcher.set_path_arg("mode", "saucenao")
+ if image.available:
+ _matcher.set_path_arg("image", image.result)
+
+
+@_matcher.got_path("image", prompt="图来!")
+async def _(
+ bot: Bot,
+ session: EventSession,
+ arparma: Arparma,
+ mode: str,
+ image: alcImg,
+):
+ gid = session.id3 or session.id2
+ if not image.url:
+ await MessageUtils.build_message("图片url为空...").finish()
+ await MessageUtils.build_message("开始处理图片...").send()
+ info_list = await get_image_info(mode, image.url)
+ if isinstance(info_list, str):
+ await MessageUtils.build_message(info_list).finish(at_sender=True)
+ if not info_list:
+ await MessageUtils.build_message("未查询到...").finish()
+ platform = PlatformUtils.get_platform(bot)
+ if "qq" == platform and gid:
+ forward = MessageUtils.template2forward(info_list[1:], bot.self_id) # type: ignore
+ await bot.send_group_forward_msg(
+ group_id=int(gid),
+ messages=forward, # type: ignore
+ )
+ else:
+ for info in info_list[1:]:
+ await MessageUtils.build_message(info).send()
+ logger.info(f" 识图: {image.url}", arparma.header_result, session=session)
diff --git a/plugins/search_image/saucenao.py b/zhenxun/plugins/search_image/saucenao.py
similarity index 73%
rename from plugins/search_image/saucenao.py
rename to zhenxun/plugins/search_image/saucenao.py
index dd0c395b..eab44fab 100644
--- a/plugins/search_image/saucenao.py
+++ b/zhenxun/plugins/search_image/saucenao.py
@@ -1,21 +1,28 @@
-from services import logger
-from utils.http_utils import AsyncHttpx
-from configs.config import Config
-from configs.path_config import TEMP_PATH
-from utils.message_builder import image
-from typing import Union, List
import random
+from pathlib import Path
+
+from zhenxun.configs.config import Config
+from zhenxun.configs.path_config import TEMP_PATH
+from zhenxun.services import logger
+from zhenxun.utils.http_utils import AsyncHttpx
API_URL_SAUCENAO = "https://saucenao.com/search.php"
API_URL_ASCII2D = "https://ascii2d.net/search/url/"
API_URL_IQDB = "https://iqdb.org/"
-async def get_saucenao_image(url: str) -> Union[str, List[str]]:
+async def get_saucenao_image(url: str) -> str | list[str | Path]:
+ """获取图片源
+
+ 参数:
+ url: 图片url
+
+ 返回:
+ str | list[Image | Text]: 识图数据
+ """
api_key = Config.get_config("search_image", "API_KEY")
if not api_key:
return "Saucenao 缺失API_KEY!"
-
params = {
"output_type": 2,
"api_key": api_key,
@@ -35,10 +42,8 @@ async def get_saucenao_image(url: str) -> Union[str, List[str]]:
)
msg_list = []
index = random.randint(0, 10000)
- if await AsyncHttpx.download_file(
- url, TEMP_PATH / f"saucenao_search_{index}.jpg"
- ):
- msg_list.append(image(TEMP_PATH / f"saucenao_search_{index}.jpg"))
+ if await AsyncHttpx.download_file(url, TEMP_PATH / f"saucenao_search_{index}.jpg"):
+ msg_list.append(TEMP_PATH / f"saucenao_search_{index}.jpg")
for info in data:
try:
similarity = info["header"]["similarity"]
@@ -53,5 +58,5 @@ async def get_saucenao_image(url: str) -> Union[str, List[str]]:
tmp += f'source:{info["header"]["thumbnail"]}\n'
msg_list.append(tmp[:-1])
except Exception as e:
- logger.warning(f"识图获取图片信息发生错误 {type(e)}:{e}")
+ logger.warning(f"识图获取图片信息发生错误", e=e)
return msg_list
diff --git a/zhenxun/plugins/send_setu_/__init__.py b/zhenxun/plugins/send_setu_/__init__.py
new file mode 100644
index 00000000..eb35e275
--- /dev/null
+++ b/zhenxun/plugins/send_setu_/__init__.py
@@ -0,0 +1,5 @@
+from pathlib import Path
+
+import nonebot
+
+nonebot.load_plugins(str(Path(__file__).parent.resolve()))
diff --git a/plugins/send_setu_/_model.py b/zhenxun/plugins/send_setu_/_model.py
similarity index 76%
rename from plugins/send_setu_/_model.py
rename to zhenxun/plugins/send_setu_/_model.py
index ba2920ec..865af7d1 100644
--- a/plugins/send_setu_/_model.py
+++ b/zhenxun/plugins/send_setu_/_model.py
@@ -1,10 +1,9 @@
-from typing import List, Optional
-
from tortoise import fields
from tortoise.contrib.postgres.functions import Random
from tortoise.expressions import Q
+from typing_extensions import Self
-from services.db_context import Model
+from zhenxun.services.db_context import Model
class Setu(Model):
@@ -19,7 +18,7 @@ class Setu(Model):
"""作者"""
pid = fields.BigIntField()
"""pid"""
- img_hash: str = fields.TextField()
+ img_hash = fields.TextField()
"""图片hash"""
img_url = fields.CharField(255)
"""pixiv url链接"""
@@ -36,19 +35,21 @@ class Setu(Model):
@classmethod
async def query_image(
cls,
- local_id: Optional[int] = None,
- tags: Optional[List[str]] = None,
+ local_id: int | None = None,
+ tags: list[str] | None = None,
r18: bool = False,
limit: int = 50,
- ):
- """
- 说明:
- 通过tag查找色图
+ ) -> list[Self] | Self | None:
+ """通过tag查找色图
+
参数:
- :param local_id: 本地色图 id
- :param tags: tags
- :param r18: 是否 r18,0:非r18 1:r18 2:混合
- :param limit: 获取数量
+ local_id: 本地色图 id
+ tags: tags
+ r18: 是否 r18,0:非r18 1:r18 2:混合
+ limit: 获取数量
+
+ 返回:
+ list[Self] | Self | None: 色图数据
"""
if local_id:
return await cls.filter(is_r18=r18, local_id=local_id).first()
@@ -65,11 +66,13 @@ class Setu(Model):
@classmethod
async def delete_image(cls, pid: int, img_url: str) -> int:
- """
- 说明:
- 删除图片并替换
+ """删除图片并替换
+
参数:
- :param pid: 图片pid
+ pid: 图片pid
+
+ 返回:
+ int: 删除返回的本地id
"""
print(pid)
return_id = -1
diff --git a/zhenxun/plugins/send_setu_/send_setu/__init__.py b/zhenxun/plugins/send_setu_/send_setu/__init__.py
new file mode 100644
index 00000000..3dab91f6
--- /dev/null
+++ b/zhenxun/plugins/send_setu_/send_setu/__init__.py
@@ -0,0 +1,243 @@
+import random
+from typing import Tuple
+
+from nonebot.adapters import Bot
+from nonebot.matcher import Matcher
+from nonebot.message import run_postprocessor
+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 NICKNAME
+from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig
+from zhenxun.models.sign_user import SignUser
+from zhenxun.models.user_console import UserConsole
+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 SetuManage, base_config
+
+__plugin_meta__ = PluginMetadata(
+ name="色图",
+ description="不要小看涩图啊混蛋!",
+ usage="""
+ 搜索 lolicon 图库,每日色图time...
+ 多个tag使用#连接
+ 指令:
+ 色图: 随机色图
+ 色图 -r: 随机在线r18涩图
+ 色图 -id [id]: 本地指定id色图
+ 色图 *[tags]: 在线搜索指定tag色图
+ 色图 *[tags] -r: 同上, r18色图
+ [1-9]张涩图: 本地随机色图连发
+ [1-9]张[tags]的涩图: 在线搜索指定tag色图连发
+ 示例:色图 萝莉|少女#白丝|黑丝
+ 示例:色图 萝莉#猫娘
+ 注:
+ tag至多取前20项,| 为或,萝莉|少女=萝莉或者少女
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ menu_type="来点好康的",
+ limits=[PluginCdBlock(result="您冲的太快了,请稍后再冲.")],
+ configs=[
+ RegisterConfig(
+ key="WITHDRAW_SETU_MESSAGE",
+ value=(0, 1),
+ help="自动撤回,参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)",
+ default_value=(0, 1),
+ type=Tuple[int, int],
+ ),
+ RegisterConfig(
+ key="ONLY_USE_LOCAL_SETU",
+ value=False,
+ help="仅仅使用本地色图,不在线搜索",
+ default_value=False,
+ type=bool,
+ ),
+ RegisterConfig(
+ key="INITIAL_SETU_PROBABILITY",
+ value=0.7,
+ help="初始色图概率,总概率 = 初始色图概率 + 好感度",
+ default_value=0.7,
+ type=float,
+ ),
+ RegisterConfig(
+ key="DOWNLOAD_SETU",
+ value=True,
+ help="是否存储下载的色图,使用本地色图可以加快图片发送速度",
+ default_value=True,
+ type=float,
+ ),
+ RegisterConfig(
+ key="TIMEOUT",
+ value=10,
+ help="色图下载超时限制(秒)",
+ default_value=10,
+ type=int,
+ ),
+ RegisterConfig(
+ key="SHOW_INFO",
+ value=True,
+ help="是否显示色图的基本信息,如PID等",
+ default_value=True,
+ type=bool,
+ ),
+ RegisterConfig(
+ key="ALLOW_GROUP_R18",
+ value=False,
+ help="在群聊中启用R18权限",
+ default_value=False,
+ type=bool,
+ ),
+ RegisterConfig(
+ key="MAX_ONCE_NUM2FORWARD",
+ value=None,
+ help="单次发送的图片数量达到指定值时转发为合并消息",
+ default_value=None,
+ type=int,
+ ),
+ RegisterConfig(
+ key="MAX_ONCE_NUM",
+ value=10,
+ help="单次发送图片数量限制",
+ default_value=10,
+ type=int,
+ ),
+ RegisterConfig(
+ module="pixiv",
+ key="PIXIV_NGINX_URL",
+ value="i.pixiv.re",
+ help="Pixiv反向代理",
+ default_value="i.pixiv.re",
+ ),
+ ],
+ ).dict(),
+)
+
+
+@run_postprocessor
+async def _(
+ matcher: Matcher,
+ exception: Exception | None,
+ session: EventSession,
+):
+ if matcher.plugin_name == "send_setu":
+ # 添加数据至数据库
+ try:
+ await SetuManage.save_to_database()
+ logger.info("色图数据自动存储数据库成功...")
+ except Exception:
+ pass
+
+
+_matcher = on_alconna(
+ Alconna(
+ "色图",
+ Args["tags?", str],
+ Option("-n", Args["num", int, 1], help_text="数量"),
+ Option("-id", Args["local_id", int], help_text="本地id"),
+ Option("-r", action=store_true, help_text="r18"),
+ ),
+ aliases={"涩图", "不够色", "来一发", "再来点"},
+ priority=5,
+ block=True,
+)
+
+_matcher.shortcut(
+ r".*?(?P\d*)[份|发|张|个|次|点](?P.*)[瑟|色|涩]图.*?",
+ command="色图",
+ arguments=["{tags}", "-n", "{num}"],
+ prefix=True,
+)
+
+
+@_matcher.handle()
+async def _(
+ bot: Bot,
+ session: EventSession,
+ arparma: Arparma,
+ num: Match[int],
+ tags: Match[str],
+ local_id: Match[int],
+):
+ _tags = tags.result.split("#") if tags.available else None
+ if _tags and NICKNAME in _tags:
+ await MessageUtils.build_message(
+ "咳咳咳,虽然我很可爱,但是我木有自己的色图~~~有的话记得发我一份呀"
+ ).finish()
+ if not session.id1:
+ await MessageUtils.build_message("用户id为空...").finish()
+ gid = session.id3 or session.id2
+ user_console = await UserConsole.get_user(session.id1, session.platform)
+ user, _ = await SignUser.get_or_create(
+ user_id=session.id1,
+ defaults={"user_console": user_console, "platform": session.platform},
+ )
+ if session.id1 not in bot.config.superusers:
+ """超级用户跳过罗翔"""
+ if result := SetuManage.get_luo(float(user.impression)):
+ await result.finish()
+ is_r18 = arparma.find("r")
+ _num = num.result if num.available else 1
+ if is_r18 and gid:
+ """群聊中禁止查看r18"""
+ if not base_config.get("ALLOW_GROUP_R18"):
+ await MessageUtils.build_message(
+ random.choice(
+ [
+ "这种不好意思的东西怎么可能给这么多人看啦",
+ "羞羞脸!给我滚出克私聊!",
+ "变态变态变态变态大变态!",
+ ]
+ )
+ ).finish()
+ if local_id.available:
+ """指定id"""
+ result = await SetuManage.get_setu(local_id=local_id.result)
+ if isinstance(result, str):
+ await MessageUtils.build_message(result).finish(reply_to=True)
+ await result[0].finish()
+ result_list = await SetuManage.get_setu(tags=_tags, num=_num, is_r18=is_r18)
+ if isinstance(result_list, str):
+ await MessageUtils.build_message(result_list).finish(reply_to=True)
+ max_once_num2forward = base_config.get("MAX_ONCE_NUM2FORWARD")
+ platform = PlatformUtils.get_platform(bot)
+ if (
+ "qq" == platform
+ and gid
+ and max_once_num2forward
+ and len(result_list) >= max_once_num2forward
+ ):
+ logger.debug("使用合并转发转发色图数据", arparma.header_result, session=session)
+ forward = MessageUtils.template2forward(result_list, bot.self_id) # type: ignore
+ await bot.send_group_forward_msg(
+ group_id=int(gid),
+ messages=forward, # type: ignore
+ )
+ else:
+ for result in result_list:
+ logger.info(f"发送色图 {result}", arparma.header_result, session=session)
+ receipt = await result.send()
+ if receipt:
+ message_id = receipt.msg_ids[0]["message_id"]
+ await WithdrawManager.withdraw_message(
+ bot,
+ message_id,
+ base_config.get("WITHDRAW_SETU_MESSAGE"),
+ session,
+ )
+ logger.info(
+ f"调用发送 {num}张 色图 tags: {_tags}", arparma.header_result, session=session
+ )
diff --git a/zhenxun/plugins/send_setu_/send_setu/_data_source.py b/zhenxun/plugins/send_setu_/send_setu/_data_source.py
new file mode 100644
index 00000000..6bac3d22
--- /dev/null
+++ b/zhenxun/plugins/send_setu_/send_setu/_data_source.py
@@ -0,0 +1,360 @@
+import os
+import random
+from pathlib import Path
+
+from asyncpg import UniqueViolationError
+from nonebot_plugin_alconna import UniMessage
+
+from zhenxun.configs.config import NICKNAME, Config
+from zhenxun.configs.path_config import IMAGE_PATH, TEMP_PATH
+from zhenxun.services.log import logger
+from zhenxun.utils.http_utils import AsyncHttpx
+from zhenxun.utils.image_utils import compressed_image
+from zhenxun.utils.message import MessageUtils
+from zhenxun.utils.utils import change_img_md5, change_pixiv_image_links
+
+from .._model import Setu
+
+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",
+}
+
+base_config = Config.get("send_setu")
+
+
+class SetuManage:
+
+ URL = "https://api.lolicon.app/setu/v2"
+ save_data = []
+
+ @classmethod
+ async def get_setu(
+ cls,
+ *,
+ local_id: int | None = None,
+ num: int = 10,
+ tags: list[str] | None = None,
+ is_r18: bool = False,
+ ) -> list[UniMessage] | str:
+ """获取色图
+
+ 参数:
+ local_id: 指定图片id
+ num: 数量
+ tags: 标签
+ is_r18: 是否r18
+
+ 返回:
+ list[MessageFactory] | str: 色图数据列表或消息
+
+ """
+ result_list = []
+ if local_id:
+ """本地id"""
+ data_list = await cls.get_setu_list(local_id=local_id)
+ if isinstance(data_list, str):
+ return data_list
+ file = await cls.get_image(data_list[0])
+ if isinstance(file, str):
+ return file
+ return [cls.init_image_message(file, data_list[0])]
+ if base_config.get("ONLY_USE_LOCAL_SETU"):
+ """仅使用本地色图"""
+ flag = False
+ data_list = await cls.get_setu_list(tags=tags, is_r18=is_r18)
+ if isinstance(data_list, str):
+ return data_list
+ cls.save_data = data_list
+ if num > len(data_list):
+ num = len(data_list)
+ flag = True
+ setu_list = random.sample(data_list, num)
+ for setu in setu_list:
+ base_path = None
+ if setu.is_r18:
+ base_path = IMAGE_PATH / "_r18"
+ else:
+ base_path = IMAGE_PATH / "_setu"
+ file_path = base_path / f"{setu.local_id}.jpg"
+ if not file_path.exists():
+ return f"本地色图Id: {setu.local_id} 不存在..."
+ result_list.append(cls.init_image_message(file_path, setu))
+ if flag:
+ result_list.append(
+ MessageUtils.build_message("坏了,已经没图了,被榨干了!")
+ )
+ return result_list
+ data_list = await cls.search_lolicon(tags, num, is_r18)
+ if isinstance(data_list, str):
+ """搜索失败, 从本地数据库中搜索"""
+ data_list = await cls.get_setu_list(tags=tags, is_r18=is_r18)
+ if isinstance(data_list, str):
+ return data_list
+ if not data_list:
+ return "没找到符合条件的色图..."
+ cls.save_data = data_list
+ flag = False
+ if num > len(data_list):
+ num = len(data_list)
+ flag = True
+ for setu in data_list:
+ file = await cls.get_image(setu)
+ if isinstance(file, str):
+ result_list.append(MessageUtils.build_message(file))
+ continue
+ result_list.append(cls.init_image_message(file, setu))
+ if not result_list:
+ return "没找到符合条件的色图..."
+ if flag:
+ result_list.append(
+ MessageUtils.build_message("坏了,已经没图了,被榨干了!")
+ )
+ return result_list
+
+ @classmethod
+ def init_image_message(cls, file: Path, setu: Setu) -> UniMessage:
+ """初始化图片发送消息
+
+ 参数:
+ file: 图片路径
+ setu: Setu
+
+ 返回:
+ UniMessage: 发送消息内容
+ """
+ data_list = []
+ if base_config.get("SHOW_INFO"):
+ data_list.append(
+ f"id:{setu.local_id or ''}\n"
+ f"title:{setu.title}\n"
+ f"author:{setu.author}\n"
+ f"PID:{setu.pid}\n"
+ )
+ data_list.append(file)
+ return MessageUtils.build_message(data_list)
+
+ @classmethod
+ async def get_setu_list(
+ cls,
+ *,
+ local_id: int | None = None,
+ tags: list[str] | None = None,
+ is_r18: bool = False,
+ ) -> list[Setu] | str:
+ """获取数据库中的色图数据
+
+ 参数:
+ local_id: 色图本地id.
+ tags: 标签.
+ is_r18: 是否r18.
+
+ 返回:
+ list[Setu] | str: 色图数据列表或消息
+ """
+ image_list: list[Setu] = []
+ if local_id:
+ image_count = await Setu.filter(is_r18=is_r18).count() - 1
+ if local_id < 0 or local_id > image_count:
+ return f"超过当前上下限!({image_count})"
+ image_list = [await Setu.query_image(local_id, r18=is_r18)] # type: ignore
+ elif tags:
+ image_list = await Setu.query_image(tags=tags, r18=is_r18) # type: ignore
+ else:
+ image_list = await Setu.query_image(r18=is_r18) # type: ignore
+ if not image_list:
+ return "没找到符合条件的色图..."
+ return image_list
+
+ @classmethod
+ def get_luo(cls, impression: float) -> UniMessage | None:
+ """罗翔
+
+ 参数:
+ impression: 好感度
+
+ 返回:
+ MessageFactory | None: 返回数据
+ """
+ if initial_setu_probability := base_config.get("INITIAL_SETU_PROBABILITY"):
+ probability = float(impression) + initial_setu_probability * 100
+ if probability < random.randint(1, 101):
+ return MessageUtils.build_message(
+ [
+ "我为什么要给你发这个?",
+ IMAGE_PATH
+ / "luoxiang"
+ / random.choice(os.listdir(IMAGE_PATH / "luoxiang")),
+ f"\n(快向{NICKNAME}签到提升好感度吧!)",
+ ]
+ )
+ return None
+
+ @classmethod
+ async def get_image(cls, setu: Setu) -> str | Path:
+ """下载图片
+
+ 参数:
+ setu: Setu
+
+ 返回:
+ str | Path: 图片路径或返回消息
+ """
+ url = change_pixiv_image_links(setu.img_url)
+ index = setu.local_id if setu.local_id else random.randint(1, 100000)
+ file_name = f"{index}_temp_setu.jpg"
+ base_path = TEMP_PATH
+ if setu.local_id:
+ """本地图片存在直接返回"""
+ file_name = f"{index}.jpg"
+ if setu.is_r18:
+ base_path = IMAGE_PATH / "_r18"
+ else:
+ base_path = IMAGE_PATH / "_setu"
+ local_file = base_path / file_name
+ if local_file.exists():
+ return local_file
+ file = base_path / file_name
+ download_success = False
+ for i in range(3):
+ logger.debug(f"尝试在线下载第 {i+1} 次", "色图")
+ try:
+ if await AsyncHttpx.download_file(
+ url,
+ file,
+ timeout=base_config.get("TIMEOUT"),
+ ):
+ download_success = True
+ if setu.local_id is not None:
+ if (
+ os.path.getsize(base_path / f"{index}.jpg")
+ > 1024 * 1024 * 1.5
+ ):
+ compressed_image(
+ base_path / f"{index}.jpg",
+ )
+ change_img_md5(file)
+ logger.info(f"下载 lolicon 图片 {url} 成功, id:{index}")
+ break
+ except TimeoutError as e:
+ logger.error(f"下载图片超时", "色图", e=e)
+ except Exception as e:
+ logger.error(f"下载图片错误", "色图", e=e)
+ return file if download_success else "图片被小怪兽恰掉啦..!QAQ"
+
+ @classmethod
+ async def search_lolicon(
+ cls, tags: list[str] | None, num: int, is_r18: bool
+ ) -> list[Setu] | str:
+ """搜索lolicon色图
+
+ 参数:
+ tags: 标签
+ num: 数量
+ is_r18: 是否r18
+
+ 返回:
+ list[Setu] | str: 色图数据或返回消息
+ """
+ params = {
+ "r18": 1 if is_r18 else 0, # 添加r18参数 0为否,1为是,2为混合
+ "tag": tags, # 若指定tag
+ "num": 20, # 一次返回的结果数量
+ "size": ["original"],
+ }
+ for count in range(3):
+ logger.debug(f"尝试获取图片URL第 {count+1} 次", "色图")
+ try:
+ response = await AsyncHttpx.get(
+ cls.URL, timeout=base_config.get("TIMEOUT"), params=params
+ )
+ if response.status_code == 200:
+ data = response.json()
+ if not data["error"]:
+ data = data["data"]
+ result_list = cls.__handle_data(data)
+ num = num if num < len(data) else len(data)
+ random_list = random.sample(result_list, num)
+ if not random_list:
+ return "没找到符合条件的色图..."
+ return random_list
+ else:
+ return "没找到符合条件的色图..."
+ except TimeoutError as e:
+ logger.error(f"获取图片URL超时", "色图", e=e)
+ except Exception as e:
+ logger.error(f"访问页面错误", "色图", e=e)
+ return "我网线被人拔了..QAQ"
+
+ @classmethod
+ def __handle_data(cls, data: dict) -> list[Setu]:
+ """lolicon数据处理
+
+ 参数:
+ data: lolicon数据
+
+ 返回:
+ list[Setu]: 整理的数据
+ """
+ result_list = []
+ for i in range(len(data)):
+ img_url = data[i]["urls"]["original"]
+ img_url = change_pixiv_image_links(img_url)
+ title = data[i]["title"]
+ author = data[i]["author"]
+ pid = data[i]["pid"]
+ tags = []
+ for j in range(len(data[i]["tags"])):
+ tags.append(data[i]["tags"][j])
+ # if command != "色图r":
+ # if "R-18" in tags:
+ # tags.remove("R-18")
+ setu = Setu(
+ title=title,
+ author=author,
+ pid=pid,
+ img_url=img_url,
+ tags=",".join(tags),
+ is_r18="R-18" in tags,
+ )
+ result_list.append(setu)
+ return result_list
+
+ @classmethod
+ async def save_to_database(cls):
+ """存储色图数据到数据库
+
+ 参数:
+ data_list: 色图数据列表
+ """
+ set_list = []
+ exists_list = []
+ for data in cls.save_data:
+ if f"{data.pid}:{data.img_url}" not in exists_list:
+ exists_list.append(f"{data.pid}:{data.img_url}")
+ set_list.append(data)
+ if set_list:
+ create_list = []
+ _cnt = 0
+ _r18_cnt = 0
+ for setu in set_list:
+ try:
+ if not await Setu.exists(pid=setu.pid, img_url=setu.img_url):
+ idx = await Setu.filter(is_r18=setu.is_r18).count()
+ setu.local_id = idx + (_r18_cnt if setu.is_r18 else _cnt)
+ setu.img_hash = ""
+ if setu.is_r18:
+ _r18_cnt += 1
+ else:
+ _cnt += 1
+ create_list.append(setu)
+ except UniqueViolationError:
+ pass
+ cls.save_data = []
+ if create_list:
+ try:
+ await Setu.bulk_create(create_list, 10)
+ logger.debug(f"成功保存 {len(create_list)} 条色图数据")
+ except Exception as e:
+ logger.error("存储色图数据错误...", e=e)
diff --git a/zhenxun/plugins/send_setu_/update_setu/__init__.py b/zhenxun/plugins/send_setu_/update_setu/__init__.py
new file mode 100644
index 00000000..2b5b6ae9
--- /dev/null
+++ b/zhenxun/plugins/send_setu_/update_setu/__init__.py
@@ -0,0 +1,59 @@
+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_apscheduler import scheduler
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.config import Config
+from zhenxun.configs.utils import BaseBlock, PluginExtraData
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import PluginType
+from zhenxun.utils.message import MessageUtils
+
+from .data_source import update_setu_img
+
+__plugin_meta__ = PluginMetadata(
+ name="更新色图",
+ description="更新数据库内存在的色图",
+ usage="""
+ 更新数据库内存在的色图
+ 指令:
+ 更新色图
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ plugin_type=PluginType.SUPERUSER,
+ limits=[BaseBlock(result="色图正在更新...")],
+ ).dict(),
+)
+
+_matcher = on_alconna(
+ Alconna("更新色图"), rule=to_me(), permission=SUPERUSER, priority=1, block=True
+)
+
+
+@_matcher.handle()
+async def _(session: EventSession, arparma: Arparma):
+ if Config.get_config("send_setu", "DOWNLOAD_SETU"):
+ await MessageUtils.build_message("开始更新色图...").send(reply_to=True)
+ result = await update_setu_img(True)
+ if result:
+ await MessageUtils.build_message(result).send()
+ logger.info("更新色图", arparma.header_result, session=session)
+ else:
+ await MessageUtils.build_message("更新色图配置未开启...").send()
+
+
+# 更新色图
+@scheduler.scheduled_job(
+ "cron",
+ hour=4,
+ minute=30,
+)
+async def _():
+ if Config.get_config("send_setu", "DOWNLOAD_SETU"):
+ result = await update_setu_img()
+ if result:
+ logger.info(result, "自动更新色图")
diff --git a/plugins/send_setu_/update_setu/data_source.py b/zhenxun/plugins/send_setu_/update_setu/data_source.py
old mode 100755
new mode 100644
similarity index 81%
rename from plugins/send_setu_/update_setu/data_source.py
rename to zhenxun/plugins/send_setu_/update_setu/data_source.py
index 52d548b3..07d217d6
--- a/plugins/send_setu_/update_setu/data_source.py
+++ b/zhenxun/plugins/send_setu_/update_setu/data_source.py
@@ -1,178 +1,187 @@
-import os
-import shutil
-from datetime import datetime
-
-import nonebot
-import ujson as json
-from asyncpg.exceptions import UniqueViolationError
-from nonebot import Driver
-from PIL import UnidentifiedImageError
-
-from configs.config import Config
-from configs.path_config import IMAGE_PATH, TEMP_PATH, TEXT_PATH
-from services.log import logger
-from utils.http_utils import AsyncHttpx
-from utils.image_utils import compressed_image, get_img_hash
-from utils.utils import change_pixiv_image_links, get_bot
-
-from .._model import Setu
-
-driver: Driver = nonebot.get_driver()
-
-_path = IMAGE_PATH
-
-
-# 替换旧色图数据,修复local_id一直是50的问题
-@driver.on_startup
-async def update_old_setu_data():
- path = TEXT_PATH
- setu_data_file = path / "setu_data.json"
- r18_data_file = path / "r18_setu_data.json"
- if setu_data_file.exists() or r18_data_file.exists():
- index = 0
- r18_index = 0
- count = 0
- fail_count = 0
- for file in [setu_data_file, r18_data_file]:
- if file.exists():
- data = json.load(open(file, "r", encoding="utf8"))
- for x in data:
- if file == setu_data_file:
- idx = index
- if "R-18" in data[x]["tags"]:
- data[x]["tags"].remove("R-18")
- else:
- idx = r18_index
- img_url = (
- data[x]["img_url"].replace("i.pixiv.cat", "i.pximg.net")
- if "i.pixiv.cat" in data[x]["img_url"]
- else data[x]["img_url"]
- )
- # idx = r18_index if 'R-18' in data[x]["tags"] else index
- try:
- if not await Setu.exists(pid=data[x]["pid"], url=img_url):
- await Setu.create(
- local_id=idx,
- title=data[x]["title"],
- author=data[x]["author"],
- pid=data[x]["pid"],
- img_hash=data[x]["img_hash"],
- img_url=img_url,
- is_r18="R-18" in data[x]["tags"],
- tags=",".join(data[x]["tags"]),
- )
- count += 1
- if "R-18" in data[x]["tags"]:
- r18_index += 1
- else:
- index += 1
- logger.info(f'添加旧色图数据成功 PID:{data[x]["pid"]} index:{idx}....')
- except UniqueViolationError:
- fail_count += 1
- logger.info(
- f'添加旧色图数据失败,色图重复 PID:{data[x]["pid"]} index:{idx}....'
- )
- file.unlink()
- setu_url_path = path / "setu_url.json"
- setu_r18_url_path = path / "setu_r18_url.json"
- if setu_url_path.exists():
- setu_url_path.unlink()
- if setu_r18_url_path.exists():
- setu_r18_url_path.unlink()
- logger.info(f"更新旧色图数据完成,成功更新数据:{count} 条,累计失败:{fail_count} 条")
-
-
-# 删除色图rar文件夹
-shutil.rmtree(IMAGE_PATH / "setu_rar", ignore_errors=True)
-shutil.rmtree(IMAGE_PATH / "r18_rar", ignore_errors=True)
-shutil.rmtree(IMAGE_PATH / "rar", ignore_errors=True)
-
-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",
-}
-
-
-async def update_setu_img(flag: bool = False):
- """
- 更新色图
- :param flag: 是否手动更新
- """
- image_list = await Setu.all().order_by("local_id")
- image_list.reverse()
- _success = 0
- error_info = []
- error_type = []
- count = 0
- for image in image_list:
- count += 1
- path = _path / "_r18" if image.is_r18 else _path / "_setu"
- local_image = path / f"{image.local_id}.jpg"
- path.mkdir(exist_ok=True, parents=True)
- TEMP_PATH.mkdir(exist_ok=True, parents=True)
- if not local_image.exists() or not image.img_hash:
- temp_file = TEMP_PATH / f"{image.local_id}.jpg"
- if temp_file.exists():
- temp_file.unlink()
- url_ = change_pixiv_image_links(image.img_url)
- try:
- if not await AsyncHttpx.download_file(
- url_, TEMP_PATH / f"{image.local_id}.jpg"
- ):
- continue
- _success += 1
- try:
- if (
- os.path.getsize(
- TEMP_PATH / f"{image.local_id}.jpg",
- )
- > 1024 * 1024 * 1.5
- ):
- compressed_image(
- TEMP_PATH / f"{image.local_id}.jpg",
- path / f"{image.local_id}.jpg",
- )
- else:
- logger.info(
- f"不需要压缩,移动图片{TEMP_PATH}/{image.local_id}.jpg "
- f"--> /{path}/{image.local_id}.jpg"
- )
- os.rename(
- TEMP_PATH / f"{image.local_id}.jpg",
- path / f"{image.local_id}.jpg",
- )
- except FileNotFoundError:
- logger.warning(f"文件 {image.local_id}.jpg 不存在,跳过...")
- continue
- img_hash = str(get_img_hash(f"{path}/{image.local_id}.jpg"))
- image.img_hash = img_hash
- await image.save(update_fields=["img_hash"])
- # await Setu.update_setu_data(image.pid, img_hash=img_hash)
- except UnidentifiedImageError:
- # 图片已删除
- unlink = False
- with open(local_image, "r") as f:
- if "404 Not Found" in f.read():
- unlink = True
- if unlink:
- local_image.unlink()
- max_num = await Setu.delete_image(image.pid, image.img_url)
- if (path / f"{max_num}.jpg").exists():
- os.rename(path / f"{max_num}.jpg", local_image)
- logger.warning(f"更新色图 PID:{image.pid} 404,已删除并替换")
- except Exception as e:
- _success -= 1
- logger.error(f"更新色图 {image.local_id}.jpg 错误 {type(e)}: {e}")
- if type(e) not in error_type:
- error_type.append(type(e))
- error_info.append(f"更新色图 {image.local_id}.jpg 错误 {type(e)}: {e}")
- else:
- logger.info(f"更新色图 {image.local_id}.jpg 已存在")
- if _success or error_info or flag:
- if bot := get_bot():
- await bot.send_private_msg(
- user_id=int(list(bot.config.superusers)[0]),
- message=f'{str(datetime.now()).split(".")[0]} 更新 色图 完成,本地存在 {count} 张,实际更新 {_success} 张,'
- f"以下为更新时未知错误:\n" + "\n".join(error_info),
- )
+import os
+import shutil
+from datetime import datetime
+
+import nonebot
+import ujson as json
+from asyncpg.exceptions import UniqueViolationError
+from nonebot.drivers import Driver
+from PIL import UnidentifiedImageError
+
+from zhenxun.configs.config import Config
+from zhenxun.configs.path_config import IMAGE_PATH, TEMP_PATH, TEXT_PATH
+from zhenxun.services.log import logger
+from zhenxun.utils.http_utils import AsyncHttpx
+from zhenxun.utils.image_utils import compressed_image
+from zhenxun.utils.utils import change_pixiv_image_links
+
+from .._model import Setu
+
+driver: Driver = nonebot.get_driver()
+
+_path = IMAGE_PATH
+
+
+# 替换旧色图数据,修复local_id一直是50的问题
+@driver.on_startup
+async def update_old_setu_data():
+ path = TEXT_PATH
+ setu_data_file = path / "setu_data.json"
+ r18_data_file = path / "r18_setu_data.json"
+ if setu_data_file.exists() or r18_data_file.exists():
+ index = 0
+ r18_index = 0
+ count = 0
+ fail_count = 0
+ for file in [setu_data_file, r18_data_file]:
+ if file.exists():
+ data = json.load(open(file, "r", encoding="utf8"))
+ for x in data:
+ if file == setu_data_file:
+ idx = index
+ if "R-18" in data[x]["tags"]:
+ data[x]["tags"].remove("R-18")
+ else:
+ idx = r18_index
+ img_url = (
+ data[x]["img_url"].replace("i.pixiv.cat", "i.pximg.net")
+ if "i.pixiv.cat" in data[x]["img_url"]
+ else data[x]["img_url"]
+ )
+ # idx = r18_index if 'R-18' in data[x]["tags"] else index
+ try:
+ if not await Setu.exists(pid=data[x]["pid"], url=img_url):
+ await Setu.create(
+ local_id=idx,
+ title=data[x]["title"],
+ author=data[x]["author"],
+ pid=data[x]["pid"],
+ img_hash=data[x]["img_hash"],
+ img_url=img_url,
+ is_r18="R-18" in data[x]["tags"],
+ tags=",".join(data[x]["tags"]),
+ )
+ count += 1
+ if "R-18" in data[x]["tags"]:
+ r18_index += 1
+ else:
+ index += 1
+ logger.info(
+ f'添加旧色图数据成功 PID:{data[x]["pid"]} index:{idx}....'
+ )
+ except UniqueViolationError:
+ fail_count += 1
+ logger.info(
+ f'添加旧色图数据失败,色图重复 PID:{data[x]["pid"]} index:{idx}....'
+ )
+ file.unlink()
+ setu_url_path = path / "setu_url.json"
+ setu_r18_url_path = path / "setu_r18_url.json"
+ if setu_url_path.exists():
+ setu_url_path.unlink()
+ if setu_r18_url_path.exists():
+ setu_r18_url_path.unlink()
+ logger.info(
+ f"更新旧色图数据完成,成功更新数据:{count} 条,累计失败:{fail_count} 条"
+ )
+
+
+# 删除色图rar文件夹
+shutil.rmtree(IMAGE_PATH / "setu_rar", ignore_errors=True)
+shutil.rmtree(IMAGE_PATH / "r18_rar", ignore_errors=True)
+shutil.rmtree(IMAGE_PATH / "rar", ignore_errors=True)
+
+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",
+}
+
+
+async def update_setu_img(flag: bool = False) -> str | None:
+ """更新色图
+
+ 参数:
+ flag: 是否手动更新.
+
+ 返回:
+ str | None: 更新信息
+ """
+ image_list = await Setu.all().order_by("local_id")
+ image_list.reverse()
+ _success = 0
+ error_info = []
+ error_type = []
+ count = 0
+ for image in image_list:
+ count += 1
+ path = _path / "_r18" if image.is_r18 else _path / "_setu"
+ local_image = path / f"{image.local_id}.jpg"
+ path.mkdir(exist_ok=True, parents=True)
+ TEMP_PATH.mkdir(exist_ok=True, parents=True)
+ if not local_image.exists() or not image.img_hash:
+ temp_file = TEMP_PATH / f"{image.local_id}.jpg"
+ if temp_file.exists():
+ temp_file.unlink()
+ url_ = change_pixiv_image_links(image.img_url)
+ try:
+ if not await AsyncHttpx.download_file(
+ url_, TEMP_PATH / f"{image.local_id}.jpg"
+ ):
+ continue
+ _success += 1
+ try:
+ if (
+ os.path.getsize(
+ TEMP_PATH / f"{image.local_id}.jpg",
+ )
+ > 1024 * 1024 * 1.5
+ ):
+ compressed_image(
+ TEMP_PATH / f"{image.local_id}.jpg",
+ path / f"{image.local_id}.jpg",
+ )
+ else:
+ logger.info(
+ f"不需要压缩,移动图片{TEMP_PATH}/{image.local_id}.jpg "
+ f"--> /{path}/{image.local_id}.jpg"
+ )
+ os.rename(
+ TEMP_PATH / f"{image.local_id}.jpg",
+ path / f"{image.local_id}.jpg",
+ )
+ except FileNotFoundError:
+ logger.warning(f"文件 {image.local_id}.jpg 不存在,跳过...")
+ continue
+ # img_hash = str(get_img_hash(f"{path}/{image.local_id}.jpg"))
+ image.img_hash = ""
+ await image.save(update_fields=["img_hash"])
+ # await Setu.update_setu_data(image.pid, img_hash=img_hash)
+ except UnidentifiedImageError:
+ # 图片已删除
+ unlink = False
+ with open(local_image, "r") as f:
+ if "404 Not Found" in f.read():
+ unlink = True
+ if unlink:
+ local_image.unlink()
+ max_num = await Setu.delete_image(image.pid, image.img_url)
+ if (path / f"{max_num}.jpg").exists():
+ os.rename(path / f"{max_num}.jpg", local_image)
+ logger.warning(f"更新色图 PID:{image.pid} 404,已删除并替换")
+ except Exception as e:
+ _success -= 1
+ logger.error(f"更新色图 {image.local_id}.jpg 错误 {type(e)}: {e}")
+ if type(e) not in error_type:
+ error_type.append(type(e))
+ error_info.append(
+ f"更新色图 {image.local_id}.jpg 错误 {type(e)}: {e}"
+ )
+ else:
+ logger.info(f"更新色图 {image.local_id}.jpg 已存在")
+ if _success or error_info or flag:
+ return (
+ f'{str(datetime.now()).split(".")[0]} 更新 色图 完成,本地存在 {count} 张,实际更新 {_success} 张,以下为更新时未知错误:\n'
+ + "\n".join(error_info),
+ )
+ return None
diff --git a/zhenxun/plugins/send_voice/__init__.py b/zhenxun/plugins/send_voice/__init__.py
new file mode 100644
index 00000000..eb35e275
--- /dev/null
+++ b/zhenxun/plugins/send_voice/__init__.py
@@ -0,0 +1,5 @@
+from pathlib import Path
+
+import nonebot
+
+nonebot.load_plugins(str(Path(__file__).parent.resolve()))
diff --git a/zhenxun/plugins/send_voice/dinggong.py b/zhenxun/plugins/send_voice/dinggong.py
new file mode 100644
index 00000000..a01129ca
--- /dev/null
+++ b/zhenxun/plugins/send_voice/dinggong.py
@@ -0,0 +1,51 @@
+import os
+import random
+
+from nonebot.plugin import PluginMetadata
+from nonebot.rule import to_me
+from nonebot_plugin_alconna import Alconna, Arparma, UniMessage, Voice, on_alconna
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.path_config import RECORD_PATH
+from zhenxun.configs.utils import PluginCdBlock, PluginExtraData
+from zhenxun.services.log import logger
+from zhenxun.utils.message import MessageUtils
+
+__plugin_meta__ = PluginMetadata(
+ name="钉宫骂我",
+ description="请狠狠的骂我一次!",
+ usage="""
+ 多骂我一点,球球了
+ 指令:
+ 骂老子
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ limits=[PluginCdBlock(cd=3, result="就...就算求我骂你也得慢慢来...")],
+ ).dict(),
+)
+
+_matcher = on_alconna(Alconna("ma-wo"), rule=to_me(), priority=5, block=True)
+
+_matcher.shortcut(
+ r".*?骂.*?我.*?",
+ command="ma-wo",
+ arguments=[],
+ prefix=True,
+)
+
+path = RECORD_PATH / "dinggong"
+
+
+@_matcher.handle()
+async def _(session: EventSession, arparma: Arparma):
+ if not path.exists():
+ await MessageUtils.build_message("钉宫语音文件夹不存在...").finish()
+ files = os.listdir(path)
+ if not files:
+ await MessageUtils.build_message("钉宫语音文件夹为空...").finish()
+ voice = random.choice(files)
+ await UniMessage([Voice(path=path / voice)]).send()
+ await MessageUtils.build_message(voice.split("_")[1]).send()
+ logger.info(f"发送钉宫骂人: {voice}", arparma.header_result, session=session)
diff --git a/plugins/statistics/__init__.py b/zhenxun/plugins/statistics/__init__.py
old mode 100755
new mode 100644
similarity index 94%
rename from plugins/statistics/__init__.py
rename to zhenxun/plugins/statistics/__init__.py
index 4ed43619..5cf30279
--- a/plugins/statistics/__init__.py
+++ b/zhenxun/plugins/statistics/__init__.py
@@ -1,127 +1,132 @@
-from configs.path_config import DATA_PATH
-import nonebot
-import os
-try:
- import ujson as json
-except ModuleNotFoundError:
- import json
-
-nonebot.load_plugins("plugins/statistics")
-
-old_file1 = DATA_PATH / "_prefix_count.json"
-old_file2 = DATA_PATH / "_prefix_user_count.json"
-new_path = DATA_PATH / "statistics"
-new_path.mkdir(parents=True, exist_ok=True)
-if old_file1.exists():
- os.rename(old_file1, new_path / "_prefix_count.json")
-if old_file2.exists():
- os.rename(old_file2, new_path / "_prefix_user_count.json")
-
-
-# 修改旧数据
-
-statistics_group_file = DATA_PATH / "statistics" / "_prefix_count.json"
-statistics_user_file = DATA_PATH / "statistics" / "_prefix_user_count.json"
-
-for file in [statistics_group_file, statistics_user_file]:
- if file.exists():
- with open(file, "r", encoding="utf8") as f:
- data = json.load(f)
- if not (statistics_group_file.parent / f"{file}.bak").exists():
- with open(f"{file}.bak", "w", encoding="utf8") as wf:
- json.dump(data, wf, ensure_ascii=False, indent=4)
- for x in ["total_statistics", "day_statistics"]:
- for key in data[x].keys():
- num = 0
- if data[x][key].get("ai") is not None:
- if data[x][key].get("Ai") is not None:
- data[x][key]["Ai"] += data[x][key]["ai"]
- else:
- data[x][key]["Ai"] = data[x][key]["ai"]
- del data[x][key]["ai"]
- if data[x][key].get("抽卡") is not None:
- if data[x][key].get("游戏抽卡") is not None:
- data[x][key]["游戏抽卡"] += data[x][key]["抽卡"]
- else:
- data[x][key]["游戏抽卡"] = data[x][key]["抽卡"]
- del data[x][key]["抽卡"]
- if data[x][key].get("我的道具") is not None:
- num += data[x][key]["我的道具"]
- del data[x][key]["我的道具"]
- if data[x][key].get("使用道具") is not None:
- num += data[x][key]["使用道具"]
- del data[x][key]["使用道具"]
- if data[x][key].get("我的金币") is not None:
- num += data[x][key]["我的金币"]
- del data[x][key]["我的金币"]
- if data[x][key].get("购买") is not None:
- num += data[x][key]["购买"]
- del data[x][key]["购买"]
- if data[x][key].get("商店") is not None:
- data[x][key]["商店"] += num
- else:
- data[x][key]["商店"] = num
- for x in ["week_statistics", "month_statistics"]:
- for key in data[x].keys():
- if key == "total":
- if data[x][key].get("ai") is not None:
- if data[x][key].get("Ai") is not None:
- data[x][key]["Ai"] += data[x][key]["ai"]
- else:
- data[x][key]["Ai"] = data[x][key]["ai"]
- del data[x][key]["ai"]
- if data[x][key].get("抽卡") is not None:
- if data[x][key].get("游戏抽卡") is not None:
- data[x][key]["游戏抽卡"] += data[x][key]["抽卡"]
- else:
- data[x][key]["游戏抽卡"] = data[x][key]["抽卡"]
- del data[x][key]["抽卡"]
- if data[x][key].get("我的道具") is not None:
- num += data[x][key]["我的道具"]
- del data[x][key]["我的道具"]
- if data[x][key].get("使用道具") is not None:
- num += data[x][key]["使用道具"]
- del data[x][key]["使用道具"]
- if data[x][key].get("我的金币") is not None:
- num += data[x][key]["我的金币"]
- del data[x][key]["我的金币"]
- if data[x][key].get("购买") is not None:
- num += data[x][key]["购买"]
- del data[x][key]["购买"]
- if data[x][key].get("商店") is not None:
- data[x][key]["商店"] += num
- else:
- data[x][key]["商店"] = num
- else:
- for day in data[x][key].keys():
- num = 0
- if data[x][key][day].get("ai") is not None:
- if data[x][key][day].get("Ai") is not None:
- data[x][key][day]["Ai"] += data[x][key][day]["ai"]
- else:
- data[x][key][day]["Ai"] = data[x][key][day]["ai"]
- del data[x][key][day]["ai"]
- if data[x][key][day].get("抽卡") is not None:
- if data[x][key][day].get("游戏抽卡") is not None:
- data[x][key][day]["游戏抽卡"] += data[x][key][day]["抽卡"]
- else:
- data[x][key][day]["游戏抽卡"] = data[x][key][day]["抽卡"]
- del data[x][key][day]["抽卡"]
- if data[x][key][day].get("我的道具") is not None:
- num += data[x][key][day]["我的道具"]
- del data[x][key][day]["我的道具"]
- if data[x][key][day].get("使用道具") is not None:
- num += data[x][key][day]["使用道具"]
- del data[x][key][day]["使用道具"]
- if data[x][key][day].get("我的金币") is not None:
- num += data[x][key][day]["我的金币"]
- del data[x][key][day]["我的金币"]
- if data[x][key][day].get("购买") is not None:
- num += data[x][key][day]["购买"]
- del data[x][key][day]["购买"]
- if data[x][key][day].get("商店") is not None:
- data[x][key][day]["商店"] += num
- else:
- data[x][key][day]["商店"] = num
- with open(file, "w", encoding="utf8") as f:
- json.dump(data, f, ensure_ascii=False, indent=4)
+import os
+from pathlib import Path
+
+import nonebot
+import ujson as json
+
+from zhenxun.configs.path_config import DATA_PATH
+
+nonebot.load_plugins(str(Path(__file__).parent.resolve()))
+
+old_file1 = DATA_PATH / "_prefix_count.json"
+old_file2 = DATA_PATH / "_prefix_user_count.json"
+new_path = DATA_PATH / "statistics"
+new_path.mkdir(parents=True, exist_ok=True)
+if old_file1.exists():
+ os.rename(old_file1, new_path / "_prefix_count.json")
+if old_file2.exists():
+ os.rename(old_file2, new_path / "_prefix_user_count.json")
+
+
+# 修改旧数据
+
+statistics_group_file = DATA_PATH / "statistics" / "_prefix_count.json"
+statistics_user_file = DATA_PATH / "statistics" / "_prefix_user_count.json"
+
+for file in [statistics_group_file, statistics_user_file]:
+ if file.exists():
+ with open(file, "r", encoding="utf8") as f:
+ data = json.load(f)
+ if not (statistics_group_file.parent / f"{file}.bak").exists():
+ with open(f"{file}.bak", "w", encoding="utf8") as wf:
+ json.dump(data, wf, ensure_ascii=False, indent=4)
+ for x in ["total_statistics", "day_statistics"]:
+ for key in data[x].keys():
+ num = 0
+ if data[x][key].get("ai") is not None:
+ if data[x][key].get("Ai") is not None:
+ data[x][key]["Ai"] += data[x][key]["ai"]
+ else:
+ data[x][key]["Ai"] = data[x][key]["ai"]
+ del data[x][key]["ai"]
+ if data[x][key].get("抽卡") is not None:
+ if data[x][key].get("游戏抽卡") is not None:
+ data[x][key]["游戏抽卡"] += data[x][key]["抽卡"]
+ else:
+ data[x][key]["游戏抽卡"] = data[x][key]["抽卡"]
+ del data[x][key]["抽卡"]
+ if data[x][key].get("我的道具") is not None:
+ num += data[x][key]["我的道具"]
+ del data[x][key]["我的道具"]
+ if data[x][key].get("使用道具") is not None:
+ num += data[x][key]["使用道具"]
+ del data[x][key]["使用道具"]
+ if data[x][key].get("我的金币") is not None:
+ num += data[x][key]["我的金币"]
+ del data[x][key]["我的金币"]
+ if data[x][key].get("购买") is not None:
+ num += data[x][key]["购买"]
+ del data[x][key]["购买"]
+ if data[x][key].get("商店") is not None:
+ data[x][key]["商店"] += num
+ else:
+ data[x][key]["商店"] = num
+ for x in ["week_statistics", "month_statistics"]:
+ for key in data[x].keys():
+ num = 0
+ if key == "total":
+ if data[x][key].get("ai") is not None:
+ if data[x][key].get("Ai") is not None:
+ data[x][key]["Ai"] += data[x][key]["ai"]
+ else:
+ data[x][key]["Ai"] = data[x][key]["ai"]
+ del data[x][key]["ai"]
+ if data[x][key].get("抽卡") is not None:
+ if data[x][key].get("游戏抽卡") is not None:
+ data[x][key]["游戏抽卡"] += data[x][key]["抽卡"]
+ else:
+ data[x][key]["游戏抽卡"] = data[x][key]["抽卡"]
+ del data[x][key]["抽卡"]
+ if data[x][key].get("我的道具") is not None:
+ num += data[x][key]["我的道具"]
+ del data[x][key]["我的道具"]
+ if data[x][key].get("使用道具") is not None:
+ num += data[x][key]["使用道具"]
+ del data[x][key]["使用道具"]
+ if data[x][key].get("我的金币") is not None:
+ num += data[x][key]["我的金币"]
+ del data[x][key]["我的金币"]
+ if data[x][key].get("购买") is not None:
+ num += data[x][key]["购买"]
+ del data[x][key]["购买"]
+ if data[x][key].get("商店") is not None:
+ data[x][key]["商店"] += num
+ else:
+ data[x][key]["商店"] = num
+ else:
+ for day in data[x][key].keys():
+ num = 0
+ if data[x][key][day].get("ai") is not None:
+ if data[x][key][day].get("Ai") is not None:
+ data[x][key][day]["Ai"] += data[x][key][day]["ai"]
+ else:
+ data[x][key][day]["Ai"] = data[x][key][day]["ai"]
+ del data[x][key][day]["ai"]
+ if data[x][key][day].get("抽卡") is not None:
+ if data[x][key][day].get("游戏抽卡") is not None:
+ data[x][key][day]["游戏抽卡"] += data[x][key][day][
+ "抽卡"
+ ]
+ else:
+ data[x][key][day]["游戏抽卡"] = data[x][key][day][
+ "抽卡"
+ ]
+ del data[x][key][day]["抽卡"]
+ if data[x][key][day].get("我的道具") is not None:
+ num += data[x][key][day]["我的道具"]
+ del data[x][key][day]["我的道具"]
+ if data[x][key][day].get("使用道具") is not None:
+ num += data[x][key][day]["使用道具"]
+ del data[x][key][day]["使用道具"]
+ if data[x][key][day].get("我的金币") is not None:
+ num += data[x][key][day]["我的金币"]
+ del data[x][key][day]["我的金币"]
+ if data[x][key][day].get("购买") is not None:
+ num += data[x][key][day]["购买"]
+ del data[x][key][day]["购买"]
+ if data[x][key][day].get("商店") is not None:
+ data[x][key][day]["商店"] += num
+ else:
+ data[x][key][day]["商店"] = num
+ with open(file, "w", encoding="utf8") as f:
+ json.dump(data, f, ensure_ascii=False, indent=4)
diff --git a/zhenxun/plugins/statistics/_data_source.py b/zhenxun/plugins/statistics/_data_source.py
new file mode 100644
index 00000000..e83707b1
--- /dev/null
+++ b/zhenxun/plugins/statistics/_data_source.py
@@ -0,0 +1,130 @@
+from datetime import datetime, timedelta
+
+from tortoise.functions import Count
+
+from zhenxun.models.group_console import GroupConsole
+from zhenxun.models.group_member_info import GroupInfoUser
+from zhenxun.models.plugin_info import PluginInfo
+from zhenxun.models.statistics import Statistics
+from zhenxun.utils.enum import PluginType
+from zhenxun.utils.image_utils import BuildImage, BuildMat, MatType
+
+
+class StatisticsManage:
+
+ @classmethod
+ async def get_statistics(
+ cls,
+ plugin_name: str | None,
+ is_global: bool,
+ search_type: str | None,
+ user_id: str | None = None,
+ group_id: str | None = None,
+ ):
+ day = None
+ day_type = ""
+ if search_type == "day":
+ day = 1
+ day_type = "日"
+ if search_type == "week":
+ day = 7
+ day_type = "周"
+ if search_type == "month":
+ day = 30
+ day_type = "月"
+ if day_type:
+ day_type += f"({day}天)"
+ title = ""
+ if user_id:
+ """查用户"""
+ query = GroupInfoUser.filter(user_id=user_id)
+ if group_id:
+ query = query.filter(group_id=group_id)
+ user = await query.first()
+ title = f"{user.user_name if user else user_id} {day_type}功能调用统计"
+ elif group_id:
+ """查群组"""
+ group = await GroupConsole.get_or_none(
+ group_id=group_id, channel_id__isnull=True
+ )
+ title = f"{group.group_name if group else group_id} {day_type}功能调用统计"
+ else:
+ title = "功能调用统计"
+ if is_global and not user_id:
+ title = "全局 " + title
+ return await cls.get_global_statistics(plugin_name, day, title)
+ if user_id:
+ return await cls.get_my_statistics(user_id, group_id, day, title)
+ if group_id:
+ return await cls.get_group_statistics(group_id, day, title)
+ return None
+
+ @classmethod
+ async def get_global_statistics(
+ cls, plugin_name: str | None, day: int | None, title: str
+ ) -> BuildImage | str:
+ query = Statistics
+ if plugin_name:
+ query = query.filter(plugin_name=plugin_name)
+ if day:
+ time = datetime.now() - timedelta(days=day)
+ query = query.filter(create_time__gte=time)
+ data_list = (
+ await query.annotate(count=Count("id"))
+ .group_by("plugin_name")
+ .values_list("plugin_name", "count")
+ )
+ if not data_list:
+ return "统计数据为空..."
+ return await cls.__build_image(data_list, title)
+
+ @classmethod
+ async def get_my_statistics(
+ cls, user_id: str, group_id: str | None, day: int | None, title: str
+ ):
+ query = Statistics.filter(user_id=user_id)
+ if group_id:
+ query = query.filter(group_id=group_id)
+ if day:
+ time = datetime.now() - timedelta(days=day)
+ query = query.filter(create_time__gte=time)
+ data_list = (
+ await query.annotate(count=Count("id"))
+ .group_by("plugin_name")
+ .values_list("plugin_name", "count")
+ )
+ if not data_list:
+ return "统计数据为空..."
+ return await cls.__build_image(data_list, title)
+
+ @classmethod
+ async def get_group_statistics(cls, group_id: str, day: int | None, title: str):
+ query = Statistics.filter(group_id=group_id)
+ if day:
+ time = datetime.now() - timedelta(days=day)
+ query = query.filter(create_time__gte=time)
+ data_list = (
+ await query.annotate(count=Count("id"))
+ .group_by("plugin_name")
+ .values_list("plugin_name", "count")
+ )
+ if not data_list:
+ return "统计数据为空..."
+ return await cls.__build_image(data_list, title)
+
+ @classmethod
+ async def __build_image(cls, data_list: list[tuple[str, int]], title: str):
+ mat = BuildMat(MatType.BARH)
+ module2count = {x[0]: x[1] for x in data_list}
+ plugin_info = await PluginInfo.filter(
+ module__in=module2count.keys(), plugin_type=PluginType.NORMAL
+ ).all()
+ x_index = []
+ data = []
+ for plugin in plugin_info:
+ x_index.append(plugin.name)
+ data.append(module2count.get(plugin.module, 0))
+ mat.x_index = x_index
+ mat.data = data
+ mat.title = title
+ return await mat.build()
diff --git a/zhenxun/plugins/statistics/statistics_handle.py b/zhenxun/plugins/statistics/statistics_handle.py
new file mode 100644
index 00000000..fc070e0c
--- /dev/null
+++ b/zhenxun/plugins/statistics/statistics_handle.py
@@ -0,0 +1,162 @@
+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.utils import PluginExtraData
+from zhenxun.utils.enum import PluginType
+from zhenxun.utils.message import MessageUtils
+
+from ._data_source import StatisticsManage
+
+__plugin_meta__ = PluginMetadata(
+ name="功能调用统计",
+ description="功能调用统计可视化",
+ usage="""
+ usage:
+ 功能调用统计可视化
+ 指令:
+ 功能调用统计
+ 日功能调用统计
+ 周功能调用统计
+ 月功能调用统计
+ 我的功能调用统计 : 当前群我的统计
+ 我的功能调用统计 -g: 我的全局统计
+ 我的日功能调用统计
+ 我的周功能调用统计
+ 我的月功能调用统计
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ plugin_type=PluginType.NORMAL,
+ menu_type="数据统计",
+ aliases={"功能调用统计"},
+ superuser_help="""
+ "全局功能调用统计",
+ "全局日功能调用统计",
+ "全局周功能调用统计",
+ "全局月功能调用统计",
+ """.strip(),
+ ).dict(),
+)
+
+
+_matcher = on_alconna(
+ Alconna(
+ "功能调用统计",
+ Args["name?", str],
+ Option("-g|--global", action=store_true, help_text="全局统计"),
+ Option("-my", action=store_true, help_text="我的"),
+ Option("-t|--type", Args["search_type", ["day", "week", "month"]]),
+ ),
+ priority=5,
+ block=True,
+)
+
+_matcher.shortcut(
+ "日功能调用统计(?P.*)",
+ command="功能调用统计",
+ arguments=["{name}", "-t", "day"],
+ prefix=True,
+)
+
+_matcher.shortcut(
+ "周功能调用统计(?P.*)",
+ command="功能调用统计",
+ arguments=["{name}", "-t", "week"],
+ prefix=True,
+)
+
+_matcher.shortcut(
+ "月功能调用统计(?P.*)",
+ command="功能调用统计",
+ arguments=["{name}", "-t", "month"],
+ prefix=True,
+)
+
+_matcher.shortcut(
+ "全局功能调用统计(?P.*)",
+ command="功能调用统计",
+ arguments=["{name}", "-g"],
+ prefix=True,
+)
+
+_matcher.shortcut(
+ "全局日功能调用统计(?P.*)",
+ command="功能调用统计",
+ arguments=["{name}", "-t", "day", "-g"],
+ prefix=True,
+)
+
+_matcher.shortcut(
+ "全局周功能调用统计(?P.*)",
+ command="功能调用统计",
+ arguments=["{name}", "-t", "week", "-g"],
+ prefix=True,
+)
+
+_matcher.shortcut(
+ "全局月功能调用统计(?P.*)",
+ command="功能调用统计",
+ arguments=["{name}", "-t", "month", "-g"],
+ prefix=True,
+)
+
+_matcher.shortcut(
+ "我的功能调用统计(?P.*)",
+ command="功能调用统计",
+ arguments=["{name}", "-my"],
+ prefix=True,
+)
+
+_matcher.shortcut(
+ "我的日功能调用统计(?P.*)",
+ command="功能调用统计",
+ arguments=["{name}", "-t", "day", "-my"],
+ prefix=True,
+)
+
+_matcher.shortcut(
+ "我的周功能调用统计(?P.*)",
+ command="功能调用统计",
+ arguments=["{name}", "-t", "week", "-my"],
+ prefix=True,
+)
+
+_matcher.shortcut(
+ "我的月功能调用统计(?P.*)",
+ command="功能调用统计",
+ arguments=["{name}", "-t", "month", "-my"],
+ prefix=True,
+)
+
+
+@_matcher.handle()
+async def _(
+ session: EventSession, arparma: Arparma, name: Match[str], search_type: Match[str]
+):
+ plugin_name = name.result if name.available else None
+ st = search_type.result if search_type.available else None
+ gid = session.id3 or session.id2
+ uid = session.id1 if (arparma.find("my") or not gid) else None
+ is_global = arparma.find("global")
+ if uid and is_global:
+ """个人全局"""
+ gid = None
+ if result := await StatisticsManage.get_statistics(
+ plugin_name, arparma.find("global"), st, uid, gid
+ ):
+ if isinstance(result, str):
+ await MessageUtils.build_message(result).finish(reply_to=True)
+ else:
+ await MessageUtils.build_message(result).send()
+ else:
+ await MessageUtils.build_message("获取数据失败...").send()
diff --git a/zhenxun/plugins/statistics/statistics_hook.py b/zhenxun/plugins/statistics/statistics_hook.py
new file mode 100644
index 00000000..cb1f4b1f
--- /dev/null
+++ b/zhenxun/plugins/statistics/statistics_hook.py
@@ -0,0 +1,40 @@
+from datetime import datetime
+
+from nonebot.adapters import Bot
+from nonebot.matcher import Matcher
+from nonebot.message import run_postprocessor
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.utils import PluginExtraData
+from zhenxun.models.plugin_info import PluginInfo
+from zhenxun.models.statistics import Statistics
+from zhenxun.utils.enum import PluginType
+
+__plugin_meta__ = PluginMetadata(
+ name="功能调用统计",
+ description="功能调用统计",
+ usage="""""".strip(),
+ extra=PluginExtraData(
+ author="HibiKier", version="0.1", plugin_type=PluginType.HIDDEN
+ ).dict(),
+)
+
+
+@run_postprocessor
+async def _(
+ matcher: Matcher, exception: Exception | None, bot: Bot, session: EventSession
+):
+ if session.id1:
+ plugin = await PluginInfo.get_or_none(module=matcher.plugin_name)
+ plugin_type = plugin.plugin_type if plugin else None
+ if plugin_type == PluginType.NORMAL and matcher.plugin_name not in [
+ "update_info",
+ "statistics_handle",
+ ]:
+ await Statistics.create(
+ user_id=session.id1,
+ group_id=session.id3 or session.id2,
+ plugin_name=matcher.plugin_name,
+ create_time=datetime.now(),
+ )
diff --git a/zhenxun/plugins/translate/__init__.py b/zhenxun/plugins/translate/__init__.py
new file mode 100644
index 00000000..372a5774
--- /dev/null
+++ b/zhenxun/plugins/translate/__init__.py
@@ -0,0 +1,91 @@
+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.utils import PluginExtraData, RegisterConfig
+from zhenxun.services.log import logger
+from zhenxun.utils.depends import CheckConfig
+from zhenxun.utils.image_utils import ImageTemplate
+from zhenxun.utils.message import MessageUtils
+
+from .data_source import language, translate_message
+
+__plugin_meta__ = PluginMetadata(
+ name="翻译",
+ description="出国旅游好助手",
+ usage="""
+ 指令:
+ 翻译语种: (查看soruce与to可用值,代码与中文都可)
+ 示例:
+ 翻译 你好: 将中文翻译为英文
+ 翻译 Hello: 将英文翻译为中文
+
+ 翻译 你好 -to 希腊语: 将"你好"翻译为希腊语
+ 翻译 你好: 允许form和to使用中文
+ 翻译 你好 -form:中文 to:日语 你好: 指定原语种并将"你好"翻译为日文
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ menu_type="一些工具",
+ configs=[
+ RegisterConfig(key="APPID", value=None, help="百度翻译APPID"),
+ RegisterConfig(key="SECRET_KEY", value=None, help="百度翻译秘钥"),
+ ],
+ ).dict(),
+)
+
+_matcher = on_alconna(
+ Alconna(
+ "翻译",
+ Args["text", str],
+ Option("-s|--source", Args["source_text", str, "auto"]),
+ Option("-t|--to", Args["to_text", str, "auto"]),
+ ),
+ priority=5,
+ block=True,
+)
+
+_language_matcher = on_alconna(Alconna("翻译语种"), priority=5, block=True)
+
+
+@_language_matcher.handle()
+async def _(session: EventSession, arparma: Arparma):
+ s = ""
+ column_list = ["语种", "代码"]
+ data_list = []
+ for key, value in language.items():
+ data_list.append([key, value])
+ image = await ImageTemplate.table_page("翻译语种", "", column_list, data_list)
+ await MessageUtils.build_message(image).send()
+ logger.info(f"查看翻译语种", arparma.header_result, session=session)
+
+
+@_matcher.handle(
+ parameterless=[
+ CheckConfig(config="APPID"),
+ CheckConfig(config="SECRET_KEY"),
+ ]
+)
+async def _(
+ session: EventSession,
+ arparma: Arparma,
+ text: str,
+ source_text: Match[str],
+ to_text: Match[str],
+):
+ source = source_text.result if source_text.available else "auto"
+ to = to_text.result if to_text.available else "auto"
+ values = language.values()
+ keys = language.keys()
+ if source not in values and source not in keys:
+ await MessageUtils.build_message("源语种不支持...").finish()
+ if to not in values and to not in keys:
+ await MessageUtils.build_message("目标语种不支持...").finish()
+ result = await translate_message(text, source, to)
+ await MessageUtils.build_message(result).send(reply_to=True)
+ logger.info(
+ f"source: {source}, to: {to}, 翻译: {text}",
+ arparma.header_result,
+ session=session,
+ )
diff --git a/plugins/translate/data_source.py b/zhenxun/plugins/translate/data_source.py
old mode 100755
new mode 100644
similarity index 57%
rename from plugins/translate/data_source.py
rename to zhenxun/plugins/translate/data_source.py
index 517938e2..a7a3018d
--- a/plugins/translate/data_source.py
+++ b/zhenxun/plugins/translate/data_source.py
@@ -1,119 +1,83 @@
-import time
-from hashlib import md5
-from typing import Any, Tuple
-
-from nonebot.internal.matcher import Matcher
-from nonebot.internal.params import Depends
-from nonebot.params import RegexGroup
-from nonebot.typing import T_State
-
-from configs.config import Config
-from utils.http_utils import AsyncHttpx
-
-URL = "http://api.fanyi.baidu.com/api/trans/vip/translate"
-
-
-language = {
- "自动": "auto",
- "粤语": "yue",
- "韩语": "kor",
- "泰语": "th",
- "葡萄牙语": "pt",
- "希腊语": "el",
- "保加利亚语": "bul",
- "芬兰语": "fin",
- "斯洛文尼亚语": "slo",
- "繁体中文": "cht",
- "中文": "zh",
- "文言文": "wyw",
- "法语": "fra",
- "阿拉伯语": "ara",
- "德语": "de",
- "荷兰语": "nl",
- "爱沙尼亚语": "est",
- "捷克语": "cs",
- "瑞典语": "swe",
- "越南语": "vie",
- "英语": "en",
- "日语": "jp",
- "西班牙语": "spa",
- "俄语": "ru",
- "意大利语": "it",
- "波兰语": "pl",
- "丹麦语": "dan",
- "罗马尼亚语": "rom",
- "匈牙利语": "hu",
-}
-
-
-def CheckParam():
- """
- 检查翻译内容是否在language中
- """
-
- async def dependency(
- matcher: Matcher,
- state: T_State,
- reg_group: Tuple[Any, ...] = RegexGroup(),
- ):
- form, to, _ = reg_group
- values = language.values()
- if form:
- form = form.split(":")[-1]
- if form not in language and form not in values:
- await matcher.finish("FORM选择的语种不存在")
- state["form"] = form
- else:
- state["form"] = "auto"
- if to:
- to = to.split(":")[-1]
- if to not in language and to not in values:
- await matcher.finish("TO选择的语种不存在")
- state["to"] = to
- else:
- state["to"] = "auto"
-
- return Depends(dependency)
-
-
-async def translate_msg(word: str, form: str, to: str) -> str:
- """翻译
-
- Args:
- word (str): 翻译文字
- form (str): 源语言
- to (str): 目标语言
-
- Returns:
- str: 翻译后的文字
- """
- if form in language:
- form = language[form]
- if to in language:
- to = language[to]
- salt = str(time.time())
- app_id = Config.get_config("translate", "APPID")
- secret_key = Config.get_config("translate", "SECRET_KEY")
- sign = app_id + word + salt + secret_key # type: ignore
- md5_ = md5()
- md5_.update(sign.encode("utf-8"))
- sign = md5_.hexdigest()
- params = {
- "q": word,
- "from": form,
- "to": to,
- "appid": app_id,
- "salt": salt,
- "sign": sign,
- }
- url = URL + "?"
- for key, value in params.items():
- url += f"{key}={value}&"
- url = url[:-1]
- resp = await AsyncHttpx.get(url)
- data = resp.json()
- if data.get("error_code"):
- return data.get("error_msg")
- if trans_result := data.get("trans_result"):
- return trans_result[0]["dst"]
- return "没有找到翻译捏"
+import time
+from hashlib import md5
+
+from zhenxun.configs.config import Config
+from zhenxun.utils.http_utils import AsyncHttpx
+
+URL = "http://api.fanyi.baidu.com/api/trans/vip/translate"
+
+
+language = {
+ "自动": "auto",
+ "粤语": "yue",
+ "韩语": "kor",
+ "泰语": "th",
+ "葡萄牙语": "pt",
+ "希腊语": "el",
+ "保加利亚语": "bul",
+ "芬兰语": "fin",
+ "斯洛文尼亚语": "slo",
+ "繁体中文": "cht",
+ "中文": "zh",
+ "文言文": "wyw",
+ "法语": "fra",
+ "阿拉伯语": "ara",
+ "德语": "de",
+ "荷兰语": "nl",
+ "爱沙尼亚语": "est",
+ "捷克语": "cs",
+ "瑞典语": "swe",
+ "越南语": "vie",
+ "英语": "en",
+ "日语": "jp",
+ "西班牙语": "spa",
+ "俄语": "ru",
+ "意大利语": "it",
+ "波兰语": "pl",
+ "丹麦语": "dan",
+ "罗马尼亚语": "rom",
+ "匈牙利语": "hu",
+}
+
+
+async def translate_message(word: str, form: str, to: str) -> str:
+ """翻译
+
+ 参数:
+ word (str): 翻译文字
+ form (str): 源语言
+ to (str): 目标语言
+
+ 返回:
+ str: 翻译后的文字
+ """
+ if form in language:
+ form = language[form]
+ if to in language:
+ to = language[to]
+ salt = str(time.time())
+ app_id = Config.get_config("translate", "APPID")
+ secret_key = Config.get_config("translate", "SECRET_KEY")
+ sign = app_id + word + salt + secret_key # type: ignore
+ md5_ = md5()
+ md5_.update(sign.encode("utf-8"))
+ sign = md5_.hexdigest()
+ params = {
+ "q": word,
+ "from": form,
+ "to": to,
+ "appid": app_id,
+ "salt": salt,
+ "sign": sign,
+ }
+ url = URL + "?"
+ for key, value in params.items():
+ url += f"{key}={value}&"
+ url = url[:-1]
+ resp = await AsyncHttpx.get(url)
+ data = resp.json()
+ if data.get("error_code"):
+ return data.get("error_msg")
+ if trans_result := data.get("trans_result"):
+ return trans_result[0]["dst"]
+ return "没有找到翻译捏..."
diff --git a/zhenxun/plugins/wbtop/__init__.py b/zhenxun/plugins/wbtop/__init__.py
new file mode 100644
index 00000000..fce1b995
--- /dev/null
+++ b/zhenxun/plugins/wbtop/__init__.py
@@ -0,0 +1,56 @@
+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 PluginExtraData
+from zhenxun.services.log import logger
+from zhenxun.utils.http_utils import AsyncPlaywright
+from zhenxun.utils.message import MessageUtils
+
+from .data_source import get_hot_image
+
+__plugin_meta__ = PluginMetadata(
+ name="微博热搜",
+ description="刚买完瓜,在吃瓜现场",
+ usage="""
+ 指令:
+ 微博热搜:发送实时热搜
+ 微博热搜 [id]:截图该热搜页面
+ 示例:微博热搜 5
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier & yajiwa",
+ version="0.1",
+ ).dict(),
+)
+
+
+_matcher = on_alconna(Alconna("微博热搜", Args["idx?", int]), priority=5, block=True)
+
+
+@_matcher.handle()
+async def _(session: EventSession, arparma: Arparma, idx: Match[int]):
+ result, data_list = await get_hot_image()
+ if isinstance(result, str):
+ await MessageUtils.build_message(result).finish(reply_to=True)
+ if idx.available:
+ _idx = idx.result
+ url = data_list[_idx - 1]["url"]
+ file = IMAGE_PATH / "temp" / f"wbtop_{session.id1}.png"
+ img = await AsyncPlaywright.screenshot(
+ url,
+ file,
+ "#pl_feed_main",
+ wait_time=12,
+ )
+ if img:
+ await MessageUtils.build_message(file).send()
+ logger.info(
+ f"查询微博热搜 Id: {_idx}", arparma.header_result, session=session
+ )
+ else:
+ await MessageUtils.build_message("获取图片失败...").send()
+ else:
+ await MessageUtils.build_message(result).send()
+ logger.info(f"查询微博热搜", arparma.header_result, session=session)
diff --git a/zhenxun/plugins/wbtop/data_source.py b/zhenxun/plugins/wbtop/data_source.py
new file mode 100644
index 00000000..e9c20627
--- /dev/null
+++ b/zhenxun/plugins/wbtop/data_source.py
@@ -0,0 +1,63 @@
+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
+
+URL = "https://weibo.com/ajax/side/hotSearch"
+
+
+async def get_data() -> list | str:
+ """获取数据
+
+ 返回:
+ list | str: 数据或消息
+ """
+ data_list = []
+ for _ in range(3):
+ try:
+ response = await AsyncHttpx.get(URL, timeout=20)
+ if response.status_code == 200:
+ data_json = response.json()["data"]["realtime"]
+ for item in data_json:
+ if "is_ad" in item:
+ """广告跳过"""
+ continue
+ data = {
+ "hot_word": item["note"],
+ "hot_word_num": str(item["num"]),
+ "url": "https://s.weibo.com/weibo?q=%23" + item["word"] + "%23",
+ }
+ data_list.append(data)
+ if not data:
+ return "没有搜索到..."
+ return data_list
+ except Exception as e:
+ logger.error("获取微博热搜错误", e=e)
+ return "获取失败,请十分钟后再试..."
+
+
+async def get_hot_image() -> tuple[BuildImage | str, list]:
+ """构造图片
+
+ 返回:
+ BuildImage | str: 热搜图片
+ """
+ data = await get_data()
+ if isinstance(data, str):
+ return data, []
+ bk = BuildImage(700, 32 * 50 + 280, color="#797979")
+ wbtop_bk = BuildImage(700, 280, background=f"{IMAGE_PATH}/other/webtop.png")
+ await bk.paste(wbtop_bk)
+ text_bk = BuildImage(700, 32 * 50, color="#797979")
+ image_list = []
+ for i, _data in enumerate(data):
+ title = f"{i + 1}. {_data['hot_word']}"
+ hot = str(_data["hot_word_num"])
+ img = BuildImage(700, 30, font_size=20)
+ _, h = img.getsize(title)
+ await img.text((10, int((30 - h) / 2)), title)
+ await img.text((580, int((30 - h) / 2)), hot)
+ image_list.append(img)
+ text_bk = await text_bk.auto_paste(image_list, 1, 2, 0)
+ await bk.paste(text_bk, (0, 280))
+ return bk, data
diff --git a/plugins/web_ui/__init__.py b/zhenxun/plugins/web_ui/__init__.py
similarity index 83%
rename from plugins/web_ui/__init__.py
rename to zhenxun/plugins/web_ui/__init__.py
index 00bcd2b5..37684372 100644
--- a/plugins/web_ui/__init__.py
+++ b/zhenxun/plugins/web_ui/__init__.py
@@ -8,9 +8,8 @@ from nonebot.matcher import Matcher
from nonebot.message import run_preprocessor
from nonebot.typing import T_State
-from configs.config import Config as gConfig
-from services.log import logger, logger_
-from utils.manager import plugins2settings_manager
+from zhenxun.configs.config import Config as gConfig
+from zhenxun.services.log import logger, logger_
from .api.logs import router as ws_log_routes
from .api.logs.log_manager import LOG_STORAGE
@@ -25,9 +24,11 @@ from .auth import router as auth_router
driver = nonebot.get_driver()
-gConfig.add_plugin_config("web-ui", "username", "admin", name="web-ui", help_="前端管理用户名")
+gConfig.add_plugin_config("web-ui", "username", "admin", help="前端管理用户名")
-gConfig.add_plugin_config("web-ui", "password", None, name="web-ui", help_="前端管理密码")
+gConfig.add_plugin_config("web-ui", "password", None, help="前端管理密码")
+
+gConfig.set_name("web-ui", "web-ui")
BaseApiRouter = APIRouter(prefix="/zhenxun/api")
@@ -51,13 +52,14 @@ WsApiRouter.include_router(chat_routes)
@driver.on_startup
def _():
try:
+
async def log_sink(message: str):
- loop = None
+ loop = None
if not loop:
try:
loop = asyncio.get_running_loop()
except Exception as e:
- logger.warning('Web Ui log_sink', e=e)
+ logger.warning("Web Ui log_sink", e=e)
if not loop:
loop = asyncio.new_event_loop()
loop.create_task(LOG_STORAGE.add(message.rstrip("\n")))
diff --git a/plugins/web_ui/api/__init__.py b/zhenxun/plugins/web_ui/api/__init__.py
similarity index 100%
rename from plugins/web_ui/api/__init__.py
rename to zhenxun/plugins/web_ui/api/__init__.py
diff --git a/plugins/web_ui/api/logs/__init__.py b/zhenxun/plugins/web_ui/api/logs/__init__.py
similarity index 100%
rename from plugins/web_ui/api/logs/__init__.py
rename to zhenxun/plugins/web_ui/api/logs/__init__.py
diff --git a/plugins/web_ui/api/logs/log_manager.py b/zhenxun/plugins/web_ui/api/logs/log_manager.py
similarity index 78%
rename from plugins/web_ui/api/logs/log_manager.py
rename to zhenxun/plugins/web_ui/api/logs/log_manager.py
index f375313d..71992c91 100644
--- a/plugins/web_ui/api/logs/log_manager.py
+++ b/zhenxun/plugins/web_ui/api/logs/log_manager.py
@@ -1,7 +1,5 @@
import asyncio
-import re
-from typing import Awaitable, Callable, Dict, Generic, List, Set, TypeVar
-from urllib.parse import urlparse
+from typing import Awaitable, Callable, Generic, TypeVar
PATTERN = r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))"
@@ -10,15 +8,14 @@ LogListener = Callable[[_T], Awaitable[None]]
class LogStorage(Generic[_T]):
-
"""
日志存储
"""
def __init__(self, rotation: float = 5 * 60):
self.count, self.rotation = 0, rotation
- self.logs: Dict[int, str] = {}
- self.listeners: Set[LogListener[str]] = set()
+ self.logs: dict[int, str] = {}
+ self.listeners: set[LogListener[str]] = set()
async def add(self, log: str):
seq = self.count = self.count + 1
@@ -36,4 +33,3 @@ class LogStorage(Generic[_T]):
LOG_STORAGE: LogStorage[str] = LogStorage[str]()
-
diff --git a/plugins/web_ui/api/logs/logs.py b/zhenxun/plugins/web_ui/api/logs/logs.py
similarity index 93%
rename from plugins/web_ui/api/logs/logs.py
rename to zhenxun/plugins/web_ui/api/logs/logs.py
index e6abebf3..01c78096 100644
--- a/plugins/web_ui/api/logs/logs.py
+++ b/zhenxun/plugins/web_ui/api/logs/logs.py
@@ -1,5 +1,3 @@
-from typing import List
-
from fastapi import APIRouter, WebSocket
from loguru import logger
from nonebot.utils import escape_tag
@@ -10,7 +8,7 @@ from .log_manager import LOG_STORAGE
router = APIRouter()
-@router.get("/logs", response_model=List[str])
+@router.get("/logs", response_model=list[str])
async def system_logs_history(reverse: bool = False):
"""历史日志
diff --git a/plugins/web_ui/api/tabs/__init__.py b/zhenxun/plugins/web_ui/api/tabs/__init__.py
similarity index 100%
rename from plugins/web_ui/api/tabs/__init__.py
rename to zhenxun/plugins/web_ui/api/tabs/__init__.py
diff --git a/plugins/web_ui/api/tabs/database/__init__.py b/zhenxun/plugins/web_ui/api/tabs/database/__init__.py
similarity index 62%
rename from plugins/web_ui/api/tabs/database/__init__.py
rename to zhenxun/plugins/web_ui/api/tabs/database/__init__.py
index f047d711..2fd77085 100644
--- a/plugins/web_ui/api/tabs/database/__init__.py
+++ b/zhenxun/plugins/web_ui/api/tabs/database/__init__.py
@@ -1,18 +1,13 @@
-from os import name
-from typing import Optional
-
import nonebot
from fastapi import APIRouter, Request
from nonebot.drivers import Driver
from tortoise import Tortoise
from tortoise.exceptions import OperationalError
-from configs.config import NICKNAME
-from services.db_context import TestSQL
-from utils.utils import get_matchers
+from zhenxun.models.plugin_info import PluginInfo
+from zhenxun.services.db_context import TestSQL
from ....base_model import BaseResultModel, QueryModel, Result
-from ....config import QueryDateType
from ....utils import authentication
from .models.model import SqlModel, SqlText
from .models.sql_log import SqlLog
@@ -38,38 +33,55 @@ FROM information_schema.columns
WHERE table_name = '{}';
"""
+
@driver.on_startup
async def _():
- for matcher in get_matchers(True):
- if _plugin := matcher.plugin:
- try:
- _module = _plugin.module
- except AttributeError:
- pass
+ for plugin in nonebot.get_loaded_plugins():
+ module = plugin.name
+ sql_list = []
+ if plugin.metadata and plugin.metadata.extra:
+ sql_list = plugin.metadata.extra.get("sql_list")
+ if module in SQL_DICT:
+ raise ValueError(f"{module} 常用SQL module 重复")
+ if sql_list:
+ SqlModel(
+ name="",
+ module=module,
+ sql_list=sql_list,
+ )
+ SQL_DICT[module] = SqlModel
+ if SQL_DICT:
+ result = await PluginInfo.filter(module__in=SQL_DICT.keys()).values_list(
+ "module", "name"
+ )
+ module2name = {r[0]: r[1] for r in result}
+ for s in SQL_DICT:
+ module = SQL_DICT[s].module
+ if module in module2name:
+ SQL_DICT[s].name = module2name[module]
else:
- plugin_name = matcher.plugin_name
- if plugin_name in SQL_DICT:
- raise ValueError(f"{plugin_name} 常用SQL plugin_name 重复")
- SqlModel(
- name=getattr(_module, "__plugin_name__", None) or plugin_name or "",
- plugin_name=plugin_name or "",
- sql_list=getattr(_module, "sql_list", []),
- )
- SQL_DICT[plugin_name] = SqlModel
+ SQL_DICT[s].name = module
-@router.get("/get_table_list", dependencies=[authentication()], description="获取数据库表")
-async def _() -> Result:
+
+@router.get(
+ "/get_table_list", dependencies=[authentication()], description="获取数据库表"
+)
+async def _() -> Result:
db = Tortoise.get_connection("default")
query = await db.execute_query_dict(SELECT_TABLE_SQL)
return Result.ok(query)
-@router.get("/get_table_column", dependencies=[authentication()], description="获取表字段")
-async def _(table_name: str) -> Result:
+
+@router.get(
+ "/get_table_column", dependencies=[authentication()], description="获取表字段"
+)
+async def _(table_name: str) -> Result:
db = Tortoise.get_connection("default")
print(SELECT_TABLE_COLUMN_SQL.format(table_name))
query = await db.execute_query_dict(SELECT_TABLE_COLUMN_SQL.format(table_name))
return Result.ok(query)
+
@router.post("/exec_sql", dependencies=[authentication()], description="执行sql")
async def _(sql: SqlText, request: Request) -> Result:
ip = request.client.host if request.client else "unknown"
@@ -91,14 +103,19 @@ async def _(sql: SqlText, request: Request) -> Result:
@router.post("/get_sql_log", dependencies=[authentication()], description="sql日志列表")
async def _(query: QueryModel) -> Result:
total = await SqlLog.all().count()
- if (total % query.size):
+ if total % query.size:
total += 1
- data = await SqlLog.all().order_by("-id").offset((query.index - 1) * query.size).limit(query.size)
+ data = (
+ await SqlLog.all()
+ .order_by("-id")
+ .offset((query.index - 1) * query.size)
+ .limit(query.size)
+ )
return Result.ok(BaseResultModel(total=total, data=data))
@router.get("/get_common_sql", dependencies=[authentication()], description="常用sql")
-async def _(plugin_name: Optional[str] = None) -> Result:
+async def _(plugin_name: str | None = None) -> Result:
if plugin_name:
return Result.ok(SQL_DICT.get(plugin_name))
return Result.ok(str(SQL_DICT))
diff --git a/plugins/web_ui/api/tabs/database/models/model.py b/zhenxun/plugins/web_ui/api/tabs/database/models/model.py
similarity index 69%
rename from plugins/web_ui/api/tabs/database/models/model.py
rename to zhenxun/plugins/web_ui/api/tabs/database/models/model.py
index 37c682a6..e18e4cfb 100644
--- a/plugins/web_ui/api/tabs/database/models/model.py
+++ b/zhenxun/plugins/web_ui/api/tabs/database/models/model.py
@@ -1,8 +1,6 @@
-from typing import List
-
from pydantic import BaseModel
-from utils.models import CommonSql
+from zhenxun.utils.plugin_models.base import CommonSql
class SqlText(BaseModel):
@@ -20,7 +18,7 @@ class SqlModel(BaseModel):
name: str
"""插件中文名称"""
- plugin_name: str
+ module: str
"""插件名称"""
- sql_list: List[CommonSql]
+ sql_list: list[CommonSql]
"""插件列表"""
diff --git a/plugins/web_ui/api/tabs/database/models/sql_log.py b/zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py
similarity index 65%
rename from plugins/web_ui/api/tabs/database/models/sql_log.py
rename to zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py
index d58ddd34..691f1b5a 100644
--- a/plugins/web_ui/api/tabs/database/models/sql_log.py
+++ b/zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py
@@ -1,8 +1,6 @@
-from typing import Optional, Union
-
from tortoise import fields
-from services.db_context import Model
+from zhenxun.services.db_context import Model
class SqlLog(Model):
@@ -26,15 +24,14 @@ class SqlLog(Model):
@classmethod
async def add(
- cls, ip: str, sql: str, result: Optional[str] = None, is_suc: bool = True
+ cls, ip: str, sql: str, result: str | None = None, is_suc: bool = True
):
- """
- 说明:
- 获取用户在群内的等级
+ """获取用户在群内的等级
+
参数:
- :param ip: ip
- :param sql: sql
- :param result: 返回结果
- :param is_suc: 是否成功
+ ip: ip
+ sql: sql
+ result: 返回结果
+ is_suc: 是否成功
"""
await cls.create(ip=ip, sql=sql, result=result, is_suc=is_suc)
diff --git a/plugins/web_ui/api/tabs/main/__init__.py b/zhenxun/plugins/web_ui/api/tabs/main/__init__.py
similarity index 75%
rename from plugins/web_ui/api/tabs/main/__init__.py
rename to zhenxun/plugins/web_ui/api/tabs/main/__init__.py
index ac892e65..ed8bb576 100644
--- a/plugins/web_ui/api/tabs/main/__init__.py
+++ b/zhenxun/plugins/web_ui/api/tabs/main/__init__.py
@@ -2,7 +2,6 @@ import asyncio
import time
from datetime import datetime, timedelta
from pathlib import Path
-from typing import List, Optional
import nonebot
from fastapi import APIRouter, WebSocket
@@ -10,12 +9,12 @@ from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState
from tortoise.functions import Count
from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
-from configs.config import NICKNAME
-from models.chat_history import ChatHistory
-from models.group_info import GroupInfo
-from models.statistics import Statistics
-from services.log import logger
-from utils.manager import plugin_data_manager, plugins2settings_manager, plugins_manager
+from zhenxun.models.chat_history import ChatHistory
+from zhenxun.models.group_info import GroupInfo
+from zhenxun.models.plugin_info import PluginInfo
+from zhenxun.models.statistics import Statistics
+from zhenxun.services.log import logger
+from zhenxun.utils.platform import PlatformUtils
from ....base_model import Result
from ....config import AVA_URL, GROUP_AVA_URL, QueryDateType
@@ -29,19 +28,17 @@ ws_router = APIRouter()
router = APIRouter(prefix="/main")
-
@router.get("/get_base_info", dependencies=[authentication()], description="基础信息")
-async def _(bot_id: Optional[str] = None) -> Result:
- """
- 获取Bot基础信息
+async def _(bot_id: str | None = None) -> Result:
+ """获取Bot基础信息
- Args:
+ 参数:
bot_id (Optional[str], optional): bot_id. Defaults to None.
- Returns:
+ 返回:
Result: 获取指定bot信息与bot列表
"""
- bot_list: List[BaseInfo] = []
+ bot_list: list[BaseInfo] = []
if bots := nonebot.get_bots():
select_bot: BaseInfo
for key, bot in bots.items():
@@ -74,9 +71,9 @@ async def _(bot_id: Optional[str] = None) -> Result:
for bot in bot_list:
bot.bot = None # type: ignore
# 插件加载数量
- select_bot.plugin_count = len(plugins2settings_manager)
- pm_data = plugins_manager.get_data()
- select_bot.fail_plugin_count = len([pd for pd in pm_data if pm_data[pd].error])
+ select_bot.plugin_count = await PluginInfo.all().count()
+ fail_count = await PluginInfo.filter(load_status=False).count()
+ select_bot.fail_plugin_count = fail_count
select_bot.success_plugin_count = (
select_bot.plugin_count - select_bot.fail_plugin_count
)
@@ -84,13 +81,18 @@ async def _(bot_id: Optional[str] = None) -> Result:
select_bot.connect_time = bot_live.get(select_bot.self_id) or 0
if select_bot.connect_time:
connect_date = datetime.fromtimestamp(select_bot.connect_time)
- select_bot.connect_date = connect_date.strftime("%Y-%m-%d %H:%M:%S")
+ connect_date_str = connect_date.strftime("%Y-%m-%d %H:%M:%S")
+ select_bot.connect_date = datetime.strptime(
+ connect_date_str, "%Y-%m-%d %H:%M:%S"
+ )
version_file = Path() / "__version__"
if version_file.exists():
if text := version_file.open().read():
if ver := text.replace("__version__: ", "").strip():
select_bot.version = ver
- day_call = await Statistics.filter(create_time__gte=now - timedelta(hours=now.hour)).count()
+ day_call = await Statistics.filter(
+ create_time__gte=now - timedelta(hours=now.hour)
+ ).count()
select_bot.day_call = day_call
return Result.ok(bot_list, "拿到信息啦!")
return Result.warning_("无Bot连接...")
@@ -125,8 +127,10 @@ async def _(bot_id: str) -> Result:
)
-@router.get("/get_ch_count", dependencies=[authentication()], description="获取接收消息数量")
-async def _(bot_id: str, query_type: Optional[QueryDateType] = None) -> Result:
+@router.get(
+ "/get_ch_count", dependencies=[authentication()], description="获取接收消息数量"
+)
+async def _(bot_id: str, query_type: QueryDateType | None = None) -> Result:
if bots := nonebot.get_bots():
if not query_type:
return Result.ok(await ChatHistory.filter(bot_id=bot_id).count())
@@ -158,27 +162,36 @@ async def _(bot_id: str, query_type: Optional[QueryDateType] = None) -> Result:
return Result.warning_("无Bot连接...")
-@router.get("get_fg_count", dependencies=[authentication()], description="好友/群组数量")
+@router.get(
+ "get_fg_count", dependencies=[authentication()], description="好友/群组数量"
+)
async def _(bot_id: str) -> Result:
if bots := nonebot.get_bots():
if bot_id not in bots:
return Result.warning_("指定Bot未连接...")
bot = bots[bot_id]
- data = {
- "friend_count": len(await bot.get_friend_list()),
- "group_count": len(await bot.get_group_list()),
- }
- return Result.ok(data)
+ platform = PlatformUtils.get_platform(bot)
+ if platform == "qq":
+ data = {
+ "friend_count": len(await bot.get_friend_list()),
+ "group_count": len(await bot.get_group_list()),
+ }
+ return Result.ok(data)
+ return Result.warning_("暂不支持该平台...")
return Result.warning_("无Bot连接...")
-@router.get("/get_run_time", dependencies=[authentication()], description="获取nb运行时间")
+@router.get(
+ "/get_run_time", dependencies=[authentication()], description="获取nb运行时间"
+)
async def _() -> Result:
return Result.ok(int(time.time() - run_time))
-@router.get("/get_active_group", dependencies=[authentication()], description="获取活跃群聊")
-async def _(date_type: Optional[QueryDateType] = None) -> Result:
+@router.get(
+ "/get_active_group", dependencies=[authentication()], description="获取活跃群聊"
+)
+async def _(date_type: QueryDateType | None = None) -> Result:
query = ChatHistory
now = datetime.now()
if date_type == QueryDateType.DAY:
@@ -190,14 +203,19 @@ async def _(date_type: Optional[QueryDateType] = None) -> Result:
if date_type == QueryDateType.YEAR:
query = ChatHistory.filter(create_time__gte=now - timedelta(days=365))
data_list = (
- await query.annotate(count=Count("id")).filter(group_id__not_isnull=True)
- .group_by("group_id").order_by("-count").limit(5)
+ await query.annotate(count=Count("id"))
+ .filter(group_id__not_isnull=True)
+ .group_by("group_id")
+ .order_by("-count")
+ .limit(5)
.values_list("group_id", "count")
)
active_group_list = []
id2name = {}
if data_list:
- if info_list := await GroupInfo.filter(group_id__in=[x[0] for x in data_list]).all():
+ if info_list := await GroupInfo.filter(
+ group_id__in=[x[0] for x in data_list]
+ ).all():
for group_info in info_list:
id2name[group_info.group_id] = group_info.group_name
for data in data_list:
@@ -217,8 +235,10 @@ async def _(date_type: Optional[QueryDateType] = None) -> Result:
return Result.ok(active_group_list)
-@router.get("/get_hot_plugin", dependencies=[authentication()], description="获取热门插件")
-async def _(date_type: Optional[QueryDateType] = None) -> Result:
+@router.get(
+ "/get_hot_plugin", dependencies=[authentication()], description="获取热门插件"
+)
+async def _(date_type: QueryDateType | None = None) -> Result:
query = Statistics
now = datetime.now()
if date_type == QueryDateType.DAY:
@@ -231,14 +251,18 @@ async def _(date_type: Optional[QueryDateType] = None) -> Result:
query = Statistics.filter(create_time__gte=now - timedelta(days=365))
data_list = (
await query.annotate(count=Count("id"))
- .group_by("plugin_name").order_by("-count").limit(5)
+ .group_by("plugin_name")
+ .order_by("-count")
+ .limit(5)
.values_list("plugin_name", "count")
)
hot_plugin_list = []
+ module_list = [x[0] for x in data_list]
+ plugins = await PluginInfo.filter(module__in=module_list).all()
+ module2name = {p.module: p.name for p in plugins}
for data in data_list:
- name = data[0]
- if plugin_data := plugin_data_manager.get(data[0]):
- name = plugin_data.name
+ module = data[0]
+ name = module2name.get(module) or module
hot_plugin_list.append(
HotPlugin(
module=data[0],
@@ -253,7 +277,7 @@ async def _(date_type: Optional[QueryDateType] = None) -> Result:
@ws_router.websocket("/system_status")
-async def system_logs_realtime(websocket: WebSocket, sleep: Optional[int] = 5):
+async def system_logs_realtime(websocket: WebSocket, sleep: int = 5):
await websocket.accept()
logger.debug("ws system_status is connect")
try:
diff --git a/plugins/web_ui/api/tabs/main/data_source.py b/zhenxun/plugins/web_ui/api/tabs/main/data_source.py
similarity index 84%
rename from plugins/web_ui/api/tabs/main/data_source.py
rename to zhenxun/plugins/web_ui/api/tabs/main/data_source.py
index 42a3df42..ca445016 100644
--- a/plugins/web_ui/api/tabs/main/data_source.py
+++ b/zhenxun/plugins/web_ui/api/tabs/main/data_source.py
@@ -1,9 +1,8 @@
import time
-from typing import Optional
import nonebot
-from nonebot import Driver
from nonebot.adapters.onebot.v11 import Bot
+from nonebot.drivers import Driver
driver: Driver = nonebot.get_driver()
@@ -15,7 +14,7 @@ class BotLive:
def add(self, bot_id: str):
self._data[bot_id] = time.time()
- def get(self, bot_id: str) -> Optional[int]:
+ def get(self, bot_id: str) -> int | None:
return self._data.get(bot_id)
def remove(self, bot_id: str):
diff --git a/plugins/web_ui/api/tabs/main/model.py b/zhenxun/plugins/web_ui/api/tabs/main/model.py
similarity index 89%
rename from plugins/web_ui/api/tabs/main/model.py
rename to zhenxun/plugins/web_ui/api/tabs/main/model.py
index 7dd770c6..c9d76706 100644
--- a/plugins/web_ui/api/tabs/main/model.py
+++ b/zhenxun/plugins/web_ui/api/tabs/main/model.py
@@ -1,7 +1,6 @@
from datetime import datetime
-from typing import Optional, Union
-from nonebot.adapters.onebot.v11 import Bot
+from nonebot.adapters import Bot
from nonebot.config import Config
from pydantic import BaseModel
@@ -37,7 +36,7 @@ class BaseInfo(BaseModel):
"""今日 累计接收消息"""
connect_time: int = 0
"""连接时间"""
- connect_date: Optional[datetime] = None
+ connect_date: datetime | None = None
"""连接日期"""
plugin_count: int = 0
@@ -50,13 +49,12 @@ class BaseInfo(BaseModel):
is_select: bool = False
"""当前选择"""
- config: Optional[Config] = None
+ config: Config | None = None
"""nb配置"""
day_call: int = 0
"""今日调用插件次数"""
version: str = "unknown"
"""真寻版本"""
-
class Config:
arbitrary_types_allowed = True
@@ -84,7 +82,7 @@ class ActiveGroup(BaseModel):
活跃群聊数据
"""
- group_id: Union[str, int]
+ group_id: str
"""群组id"""
name: str
"""群组名称"""
diff --git a/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py b/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py
new file mode 100644
index 00000000..1067cfb8
--- /dev/null
+++ b/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py
@@ -0,0 +1,546 @@
+import re
+from typing import Literal
+
+import nonebot
+from fastapi import APIRouter
+from nonebot.adapters.onebot.v11 import ActionFailed
+from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState
+from tortoise.functions import Count
+
+from zhenxun.configs.config import NICKNAME
+from zhenxun.models.ban_console import BanConsole
+from zhenxun.models.chat_history import ChatHistory
+from zhenxun.models.fg_request import FgRequest
+from zhenxun.models.friend_user import FriendUser
+from zhenxun.models.group_console import GroupConsole
+from zhenxun.models.group_member_info import GroupInfoUser
+from zhenxun.models.plugin_info import PluginInfo
+from zhenxun.models.statistics import Statistics
+from zhenxun.models.task_info import TaskInfo
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import RequestHandleType, RequestType
+from zhenxun.utils.exception import NotFoundError
+from zhenxun.utils.platform import PlatformUtils
+
+from ....base_model import Result
+from ....config import AVA_URL, GROUP_AVA_URL
+from ....utils import authentication
+from ...logs.log_manager import LOG_STORAGE
+from .model import (
+ DeleteFriend,
+ Friend,
+ FriendRequestResult,
+ GroupDetail,
+ GroupRequestResult,
+ GroupResult,
+ HandleRequest,
+ LeaveGroup,
+ Message,
+ MessageItem,
+ Plugin,
+ ReqResult,
+ SendMessage,
+ Task,
+ UpdateGroup,
+ UserDetail,
+)
+
+ws_router = APIRouter()
+router = APIRouter(prefix="/manage")
+
+SUB_PATTERN = r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))"
+
+GROUP_PATTERN = r".*?Message (-?\d*) from (\d*)@\[群:(\d*)] '(.*)'"
+
+PRIVATE_PATTERN = r".*?Message (-?\d*) from (\d*) '(.*)'"
+
+AT_PATTERN = r"\[CQ:at,qq=(.*)\]"
+
+IMAGE_PATTERN = r"\[image:file=.*,url=(.*);.*?\]"
+
+
+@router.get(
+ "/get_group_list", dependencies=[authentication()], description="获取群组列表"
+)
+async def _(bot_id: str) -> Result:
+ """
+ 获取群信息
+ """
+ if bots := nonebot.get_bots():
+ if bot_id not in bots:
+ return Result.warning_("指定Bot未连接...")
+ group_list_result = []
+ try:
+ group_info = {}
+ group_list = await bots[bot_id].get_group_list()
+ for g in group_list:
+ gid = g["group_id"]
+ g["ava_url"] = GROUP_AVA_URL.format(gid, gid)
+ group_list_result.append(GroupResult(**g))
+ except Exception as e:
+ logger.error("调用API错误", "/get_group_list", e=e)
+ return Result.fail(f"{type(e)}: {e}")
+ return Result.ok(group_list_result, "拿到了新鲜出炉的数据!")
+ return Result.warning_("无Bot连接...")
+
+
+@router.post(
+ "/update_group", dependencies=[authentication()], description="修改群组信息"
+)
+async def _(group: UpdateGroup) -> Result:
+ try:
+ group_id = group.group_id
+ if db_group := await GroupConsole.get_group(group_id):
+ task_list = await TaskInfo.all().values_list("module", flat=True)
+ db_group.level = group.level
+ db_group.status = group.status
+ if group.close_plugins:
+ db_group.block_plugin = ",".join(group.close_plugins) + ","
+ if group.task:
+ block_task = []
+ for t in task_list:
+ if t not in group.task:
+ block_task.append(t)
+ if block_task:
+ db_group.block_task = ",".join(block_task) + ","
+ await db_group.save(
+ update_fields=["level", "status", "block_plugin", "block_task"]
+ )
+ except Exception as e:
+ logger.error("调用API错误", "/get_group", e=e)
+ return Result.fail(f"{type(e)}: {e}")
+ return Result.ok(info="已完成记录!")
+
+
+@router.get(
+ "/get_friend_list", dependencies=[authentication()], description="获取好友列表"
+)
+async def _(bot_id: str) -> Result:
+ """
+ 获取群信息
+ """
+ if bots := nonebot.get_bots():
+ if bot_id not in bots:
+ return Result.warning_("指定Bot未连接...")
+ try:
+ platform = PlatformUtils.get_platform(bots[bot_id])
+ if platform != "qq":
+ return Result.warning_("该平台暂不支持该功能...")
+ friend_list = await bots[bot_id].get_friend_list()
+ for f in friend_list:
+ f["ava_url"] = AVA_URL.format(f["user_id"])
+ return Result.ok(
+ [Friend(**f) for f in friend_list if str(f["user_id"]) != bot_id],
+ "拿到了新鲜出炉的数据!",
+ )
+ except Exception as e:
+ logger.error("调用API错误", "/get_group_list", e=e)
+ return Result.fail(f"{type(e)}: {e}")
+ return Result.warning_("无Bot连接...")
+
+
+@router.get(
+ "/get_request_count", dependencies=[authentication()], description="获取请求数量"
+)
+async def _() -> Result:
+ f_count = await FgRequest.filter(request_type=RequestType.FRIEND).count()
+ g_count = await FgRequest.filter(request_type=RequestType.GROUP).count()
+ data = {
+ "friend_count": f_count,
+ "group_count": g_count,
+ }
+ return Result.ok(data, f"{NICKNAME}带来了最新的数据!")
+
+
+@router.get(
+ "/get_request_list", dependencies=[authentication()], description="获取请求列表"
+)
+async def _() -> Result:
+ try:
+ req_result = ReqResult()
+ data_list = await FgRequest.filter(handle_type__isnull=True).all()
+ for req in data_list:
+ if req.request_type == RequestType.FRIEND:
+ req_result.friend.append(
+ FriendRequestResult(
+ oid=req.id,
+ bot_id=req.bot_id,
+ id=req.user_id,
+ flag=req.flag,
+ nickname=req.nickname,
+ comment=req.comment,
+ ava_url=AVA_URL.format(req.user_id),
+ type=str(req.request_type).lower(),
+ )
+ )
+ else:
+ req_result.group.append(
+ GroupRequestResult(
+ oid=req.id,
+ bot_id=req.bot_id,
+ id=req.user_id,
+ flag=req.flag,
+ nickname=req.nickname,
+ comment=req.comment,
+ ava_url=GROUP_AVA_URL.format(req.group_id, req.group_id),
+ type=str(req.request_type).lower(),
+ invite_group=req.group_id,
+ group_name=None,
+ )
+ )
+ req_result.friend.reverse()
+ req_result.group.reverse()
+ except Exception as e:
+ logger.error("调用API错误", "/get_request", e=e)
+ return Result.fail(f"{type(e)}: {e}")
+ return Result.ok(req_result, f"{NICKNAME}带来了最新的数据!")
+
+
+@router.delete(
+ "/clear_request", dependencies=[authentication()], description="清空请求列表"
+)
+async def _(request_type: Literal["private", "group"]) -> Result:
+ await FgRequest.filter(handle_type__not_isnull=True).update(
+ handle_type=RequestHandleType.IGNORE
+ )
+ return Result.ok(info="成功清除了数据!")
+
+
+@router.post("/refuse_request", dependencies=[authentication()], description="拒绝请求")
+async def _(parma: HandleRequest) -> Result:
+ try:
+ if bots := nonebot.get_bots():
+ bot_id = parma.bot_id
+ if bot_id not in nonebot.get_bots():
+ return Result.warning_("指定Bot未连接...")
+ try:
+ await FgRequest.refused(bots[bot_id], parma.id)
+ except ActionFailed as e:
+ await FgRequest.expire(parma.id)
+ return Result.warning_("请求失败,可能该请求已失效或请求数据错误...")
+ except NotFoundError:
+ return Result.warning_("未找到此Id请求...")
+ return Result.ok(info="成功处理了请求!")
+ return Result.warning_("无Bot连接...")
+ except Exception as e:
+ logger.error("调用API错误", "/refuse_request", e=e)
+ return Result.fail(f"{type(e)}: {e}")
+
+
+@router.post("/delete_request", dependencies=[authentication()], description="忽略请求")
+async def _(parma: HandleRequest) -> Result:
+ await FgRequest.ignore(parma.id)
+ return Result.ok(info="成功处理了请求!")
+
+
+@router.post(
+ "/approve_request", dependencies=[authentication()], description="同意请求"
+)
+async def _(parma: HandleRequest) -> Result:
+ try:
+ if bots := nonebot.get_bots():
+ bot_id = parma.bot_id
+ if bot_id not in nonebot.get_bots():
+ return Result.warning_("指定Bot未连接...")
+ if req := await FgRequest.get_or_none(id=parma.id):
+ if group := await GroupConsole.get_group(group_id=req.group_id):
+ group.group_flag = 1
+ await group.save(update_fields=["group_flag"])
+ else:
+ group_info = await bots[bot_id].get_group_info(
+ group_id=req.group_id
+ )
+ await GroupConsole.update_or_create(
+ group_id=str(group_info["group_id"]),
+ defaults={
+ "group_name": group_info["group_name"],
+ "max_member_count": group_info["max_member_count"],
+ "member_count": group_info["member_count"],
+ "group_flag": 1,
+ },
+ )
+ else:
+ return Result.warning_("未找到此Id请求...")
+ try:
+ await FgRequest.approve(bots[bot_id], parma.id)
+ return Result.ok(info="成功处理了请求!")
+ except ActionFailed as e:
+ await FgRequest.expire(parma.id)
+ return Result.warning_("请求失败,可能该请求已失效或请求数据错误...")
+ return Result.warning_("无Bot连接...")
+ except Exception as e:
+ logger.error("调用API错误", "/approve_request", e=e)
+ return Result.fail(f"{type(e)}: {e}")
+
+
+@router.post("/leave_group", dependencies=[authentication()], description="退群")
+async def _(param: LeaveGroup) -> Result:
+ try:
+ if bots := nonebot.get_bots():
+ bot_id = param.bot_id
+ platform = PlatformUtils.get_platform(bots[bot_id])
+ if platform != "qq":
+ return Result.warning_("该平台不支持退群操作...")
+ group_list = await bots[bot_id].get_group_list()
+ if param.group_id not in [str(g["group_id"]) for g in group_list]:
+ return Result.warning_("Bot未在该群聊中...")
+ await bots[bot_id].set_group_leave(group_id=param.group_id)
+ return Result.ok(info="成功处理了请求!")
+ return Result.warning_("无Bot连接...")
+ except Exception as e:
+ logger.error("调用API错误", "/leave_group", e=e)
+ return Result.fail(f"{type(e)}: {e}")
+
+
+@router.post("/delete_friend", dependencies=[authentication()], description="删除好友")
+async def _(param: DeleteFriend) -> Result:
+ try:
+ if bots := nonebot.get_bots():
+ bot_id = param.bot_id
+ platform = PlatformUtils.get_platform(bots[bot_id])
+ if platform != "qq":
+ return Result.warning_("该平台不支持删除好友操作...")
+ friend_list = await bots[bot_id].get_friend_list()
+ if param.user_id not in [str(g["user_id"]) for g in friend_list]:
+ return Result.warning_("Bot未有其好友...")
+ await bots[bot_id].delete_friend(user_id=param.user_id)
+ return Result.ok(info="成功处理了请求!")
+ return Result.warning_("Bot未连接...")
+ except Exception as e:
+ logger.error("调用API错误", "/delete_friend", e=e)
+ return Result.fail(f"{type(e)}: {e}")
+
+
+@router.get(
+ "/get_friend_detail", dependencies=[authentication()], description="获取好友详情"
+)
+async def _(bot_id: str, user_id: str) -> Result:
+ if bots := nonebot.get_bots():
+ if bot_id in bots:
+ if fd := [
+ x
+ for x in await bots[bot_id].get_friend_list()
+ if str(x["user_id"]) == user_id
+ ]:
+ like_plugin_list = (
+ await Statistics.filter(user_id=user_id)
+ .annotate(count=Count("id"))
+ .group_by("plugin_name")
+ .order_by("-count")
+ .limit(5)
+ .values_list("plugin_name", "count")
+ )
+ like_plugin = {}
+ module_list = [x[0] for x in like_plugin_list]
+ plugins = await PluginInfo.filter(module__in=module_list).all()
+ module2name = {p.module: p.name for p in plugins}
+ for data in like_plugin_list:
+ name = module2name.get(data[0]) or data[0]
+ like_plugin[name] = data[1]
+ user = fd[0]
+ user_detail = UserDetail(
+ user_id=user_id,
+ ava_url=AVA_URL.format(user_id),
+ nickname=user["nickname"],
+ remark=user["remark"],
+ is_ban=await BanConsole.is_ban(user_id),
+ chat_count=await ChatHistory.filter(user_id=user_id).count(),
+ call_count=await Statistics.filter(user_id=user_id).count(),
+ like_plugin=like_plugin,
+ )
+ return Result.ok(user_detail)
+ else:
+ return Result.warning_("未添加指定好友...")
+ return Result.warning_("无Bot连接...")
+
+
+@router.get(
+ "/get_group_detail", dependencies=[authentication()], description="获取群组详情"
+)
+async def _(bot_id: str, group_id: str) -> Result:
+ if bots := nonebot.get_bots():
+ if bot_id in bots:
+ group = await GroupConsole.get_or_none(group_id=group_id)
+ if not group:
+ return Result.warning_("指定群组未被收录...")
+ like_plugin_list = (
+ await Statistics.filter(group_id=group_id)
+ .annotate(count=Count("id"))
+ .group_by("plugin_name")
+ .order_by("-count")
+ .limit(5)
+ .values_list("plugin_name", "count")
+ )
+ like_plugin = {}
+ plugins = await PluginInfo.all()
+ module2name = {p.module: p.name for p in plugins}
+ for data in like_plugin_list:
+ name = module2name.get(data[0]) or data[0]
+ like_plugin[name] = data[1]
+ close_plugins = []
+ if group.block_plugin:
+ for module in group.block_plugin.split(","):
+ module_ = module.replace(":super", "")
+ is_super_block = module.endswith(":super")
+ plugin = Plugin(
+ module=module_,
+ plugin_name=module,
+ is_super_block=is_super_block,
+ )
+ plugin.plugin_name = module2name.get(module) or module
+ close_plugins.append(plugin)
+ all_task = await TaskInfo.annotate().values_list("module", "name")
+ task_module2name = {x[0]: x[1] for x in all_task}
+ task_list = []
+ if group.block_task:
+ split_task = group.block_task.split(",")
+ for task in all_task:
+ task_list.append(
+ Task(
+ name=task[0],
+ zh_name=task_module2name.get(task[0]) or task[0],
+ status=task[0] not in split_task,
+ )
+ )
+ else:
+ for task in all_task:
+ task_list.append(
+ Task(
+ name=task[0],
+ zh_name=task_module2name.get(task[0]) or task[0],
+ status=True,
+ )
+ )
+ group_detail = GroupDetail(
+ group_id=group_id,
+ ava_url=GROUP_AVA_URL.format(group_id, group_id),
+ name=group.group_name,
+ member_count=group.member_count,
+ max_member_count=group.max_member_count,
+ chat_count=await ChatHistory.filter(group_id=group_id).count(),
+ call_count=await Statistics.filter(group_id=group_id).count(),
+ like_plugin=like_plugin,
+ level=group.level,
+ status=group.status,
+ close_plugins=close_plugins,
+ task=task_list,
+ )
+ return Result.ok(group_detail)
+ else:
+ return Result.warning_("未添加指定群组...")
+ return Result.warning_("无Bot连接...")
+
+
+@router.post(
+ "/send_message", dependencies=[authentication()], description="获取群组详情"
+)
+async def _(param: SendMessage) -> Result:
+ if bots := nonebot.get_bots():
+ if param.bot_id in bots:
+ platform = PlatformUtils.get_platform(bots[param.bot_id])
+ if platform != "qq":
+ return Result.warning_("暂不支持该平台...")
+ try:
+ if param.user_id:
+ await bots[param.bot_id].send_private_msg(
+ user_id=str(param.user_id), message=param.message
+ )
+ else:
+ await bots[param.bot_id].send_group_msg(
+ group_id=str(param.group_id), message=param.message
+ )
+ except Exception as e:
+ return Result.fail(str(e))
+ return Result.ok("发送成功!")
+ return Result.warning_("指定Bot未连接...")
+ return Result.warning_("无Bot连接...")
+
+
+MSG_LIST = []
+
+ID2NAME = {}
+
+
+async def message_handle(
+ sub_log: str, type: Literal["private", "group"]
+) -> Message | None:
+ global MSG_LIST, ID2NAME
+ pattern = PRIVATE_PATTERN if type == "private" else GROUP_PATTERN
+ msg_id = None
+ uid = None
+ gid = None
+ msg = None
+ img_list = re.findall(IMAGE_PATTERN, sub_log)
+ if r := re.search(pattern, sub_log):
+ if type == "private":
+ msg_id = r.group(1)
+ uid = r.group(2)
+ msg = r.group(3)
+ if uid not in ID2NAME:
+ if user := await FriendUser.get_or_none(user_id=uid):
+ ID2NAME[uid] = user.user_name or user.nickname
+ else:
+ msg_id = r.group(1)
+ uid = r.group(2)
+ gid = r.group(3)
+ msg = r.group(4)
+ if gid not in ID2NAME:
+ if user := await GroupInfoUser.get_or_none(user_id=uid, group_id=gid):
+ ID2NAME[uid] = user.user_name or user.nickname
+ if at_list := re.findall(AT_PATTERN, msg):
+ user_list = await GroupInfoUser.filter(
+ user_id__in=at_list, group_id=gid
+ ).all()
+ id2name = {u.user_id: (u.user_name or u.nickname) for u in user_list}
+ for qq in at_list:
+ msg = re.sub(rf"\[CQ:at,qq={qq}\]", f"@{id2name[qq] or ''}", msg)
+ if msg_id in MSG_LIST:
+ return
+ MSG_LIST.append(msg_id)
+ messages = []
+ if msg and uid:
+ rep = re.split(r"\[CQ:image.*\]", msg)
+ if img_list:
+ for i in range(len(rep)):
+ messages.append(MessageItem(type="text", msg=rep[i]))
+ if i < len(img_list):
+ messages.append(MessageItem(type="img", msg=img_list[i]))
+ else:
+ messages = [MessageItem(type="text", msg=x) for x in rep]
+ return Message(
+ object_id=uid if type == "private" else gid, # type: ignore
+ user_id=uid,
+ group_id=gid,
+ message=messages,
+ name=ID2NAME.get(uid) or "",
+ ava_url=AVA_URL.format(uid),
+ )
+ return None
+
+
+@ws_router.websocket("/chat")
+async def _(websocket: WebSocket):
+ await websocket.accept()
+
+ async def log_listener(log: str):
+ global MSG_LIST, ID2NAME
+ sub_log = re.sub(SUB_PATTERN, "", log)
+ if "message.private.friend" in log:
+ if message := await message_handle(sub_log, "private"):
+ await websocket.send_json(message.dict())
+ else:
+ if r := re.search(GROUP_PATTERN, sub_log):
+ if message := await message_handle(sub_log, "group"):
+ await websocket.send_json(message.dict())
+ if len(MSG_LIST) > 30:
+ MSG_LIST = MSG_LIST[-1:]
+
+ LOG_STORAGE.listeners.add(log_listener)
+ try:
+ while websocket.client_state == WebSocketState.CONNECTED:
+ recv = await websocket.receive()
+ except WebSocketDisconnect:
+ pass
+ finally:
+ LOG_STORAGE.listeners.remove(log_listener)
+ return
diff --git a/plugins/web_ui/api/tabs/manage/model.py b/zhenxun/plugins/web_ui/api/tabs/manage/model.py
similarity index 77%
rename from plugins/web_ui/api/tabs/manage/model.py
rename to zhenxun/plugins/web_ui/api/tabs/manage/model.py
index ab8b83f1..8df5a6cd 100644
--- a/plugins/web_ui/api/tabs/manage/model.py
+++ b/zhenxun/plugins/web_ui/api/tabs/manage/model.py
@@ -1,7 +1,5 @@
-from typing import Dict, List, Literal, Optional, Union
+from typing import Literal
-from matplotlib.dates import FR
-from nonebot.adapters.onebot.v11 import Bot
from pydantic import BaseModel
@@ -10,7 +8,7 @@ class Group(BaseModel):
群组信息
"""
- group_id: Union[str, int]
+ group_id: str
"""群组id"""
group_name: str
"""群组名称"""
@@ -32,11 +30,12 @@ class Task(BaseModel):
status: bool
"""状态"""
+
class Plugin(BaseModel):
"""
插件
"""
-
+
module: str
"""模块名"""
plugin_name: str
@@ -50,7 +49,7 @@ class GroupResult(BaseModel):
群组返回数据
"""
- group_id: Union[str, int]
+ group_id: str
"""群组id"""
group_name: str
"""群组名称"""
@@ -63,7 +62,7 @@ class Friend(BaseModel):
好友数据
"""
- user_id: Union[str, int]
+ user_id: str
"""用户id"""
nickname: str = ""
"""昵称"""
@@ -72,6 +71,7 @@ class Friend(BaseModel):
ava_url: str = ""
"""头像url"""
+
class UpdateGroup(BaseModel):
"""
更新群组信息
@@ -83,9 +83,9 @@ class UpdateGroup(BaseModel):
"""状态"""
level: int
"""群权限"""
- task: List[str]
+ task: list[str]
"""被动状态"""
- close_plugins: List[str]
+ close_plugins: list[str]
"""关闭插件"""
@@ -94,25 +94,17 @@ class FriendRequestResult(BaseModel):
好友/群组请求管理
"""
- bot_id: Union[str, int]
+ bot_id: str
"""bot_id"""
- oid: str
+ oid: int
"""排序"""
- id: int
+ id: str
"""id"""
flag: str
"""flag"""
- nickname: Optional[str]
+ nickname: str | None
"""昵称"""
- level: Optional[int]
- """等级"""
- sex: Optional[str]
- """性别"""
- age: Optional[int]
- """年龄"""
- from_: Optional[str]
- """来自"""
- comment: Optional[str]
+ comment: str | None
"""备注信息"""
ava_url: str
"""头像"""
@@ -125,9 +117,9 @@ class GroupRequestResult(FriendRequestResult):
群聊邀请请求
"""
- invite_group: Union[int, str]
+ invite_group: str
"""邀请群聊"""
- group_name: Optional[str]
+ group_name: str | None
"""群聊名称"""
@@ -136,12 +128,10 @@ class HandleRequest(BaseModel):
操作请求接收数据
"""
- bot_id: Optional[str] = None
+ bot_id: str | None = None
"""bot_id"""
- flag: str
- """flag"""
- request_type: Literal["private", "group"]
- """类型"""
+ id: int
+ """数据id"""
class LeaveGroup(BaseModel):
@@ -165,14 +155,15 @@ class DeleteFriend(BaseModel):
user_id: str
"""用户id"""
+
class ReqResult(BaseModel):
"""
好友/群组请求列表
"""
- friend: List[FriendRequestResult] = []
+ friend: list[FriendRequestResult] = []
"""好友请求列表"""
- group: List[GroupRequestResult] = []
+ group: list[GroupRequestResult] = []
"""群组请求列表"""
@@ -195,7 +186,7 @@ class UserDetail(BaseModel):
"""发言次数"""
call_count: int
"""功能调用次数"""
- like_plugin: Dict[str, int]
+ like_plugin: dict[str, int]
"""最喜爱的功能"""
@@ -218,17 +209,18 @@ class GroupDetail(BaseModel):
"""发言次数"""
call_count: int
"""功能调用次数"""
- like_plugin: Dict[str, int]
+ like_plugin: dict[str, int]
"""最喜爱的功能"""
level: int
"""群权限"""
status: bool
"""状态(睡眠)"""
- close_plugins: List[Plugin]
+ close_plugins: list[Plugin]
"""关闭的插件"""
- task: List[Task]
+ task: list[Task]
"""被动列表"""
+
class MessageItem(BaseModel):
type: str
@@ -236,6 +228,7 @@ class MessageItem(BaseModel):
msg: str
"""内容"""
+
class Message(BaseModel):
"""
消息
@@ -245,9 +238,9 @@ class Message(BaseModel):
"""主体id user_id 或 group_id"""
user_id: str
"""用户id"""
- group_id: Optional[str] = None
+ group_id: str | None = None
"""群组id"""
- message: List[MessageItem]
+ message: list[MessageItem]
"""消息"""
name: str
"""用户名称"""
@@ -255,16 +248,16 @@ class Message(BaseModel):
"""用户头像"""
-
class SendMessage(BaseModel):
"""
发送消息
"""
+
bot_id: str
"""bot id"""
- user_id: Optional[str] = None
+ user_id: str | None = None
"""用户id"""
- group_id: Optional[str] = None
+ group_id: str | None = None
"""群组id"""
message: str
"""消息"""
diff --git a/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py
new file mode 100644
index 00000000..b2a90577
--- /dev/null
+++ b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py
@@ -0,0 +1,190 @@
+import re
+
+import cattrs
+from fastapi import APIRouter, Query
+
+from zhenxun.configs.config import Config
+from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo
+from zhenxun.services.log import logger
+from zhenxun.utils.enum import BlockType, PluginType
+
+from ....base_model import Result
+from ....utils import authentication
+from .model import (
+ PluginConfig,
+ PluginCount,
+ PluginDetail,
+ PluginInfo,
+ PluginSwitch,
+ UpdatePlugin,
+)
+
+router = APIRouter(prefix="/plugin")
+
+
+@router.get(
+ "/get_plugin_list", dependencies=[authentication()], deprecated="获取插件列表" # type: ignore
+)
+async def _(
+ plugin_type: list[PluginType] = Query(None), menu_type: str | None = None
+) -> Result:
+ try:
+ plugin_list: list[PluginInfo] = []
+ query = DbPluginInfo
+ if plugin_type:
+ query = query.filter(plugin_type__in=plugin_type, load_status=True)
+ if menu_type:
+ query = query.filter(menu_type=menu_type)
+ plugins = await query.all()
+ for plugin in plugins:
+ plugin_info = PluginInfo(
+ module=plugin.module,
+ plugin_name=plugin.name,
+ default_status=plugin.default_status,
+ limit_superuser=plugin.limit_superuser,
+ cost_gold=plugin.cost_gold,
+ menu_type=plugin.menu_type,
+ version=plugin.version or "0",
+ level=plugin.level,
+ status=plugin.status,
+ author=plugin.author,
+ )
+ plugin_list.append(plugin_info)
+ except Exception as e:
+ logger.error("调用API错误", "/get_plugins", e=e)
+ return Result.fail(f"{type(e)}: {e}")
+ return Result.ok(plugin_list, "拿到了新鲜出炉的数据!")
+
+
+@router.get(
+ "/get_plugin_count", dependencies=[authentication()], deprecated="获取插件数量" # type: ignore
+)
+async def _() -> Result:
+ plugin_count = PluginCount()
+ plugin_count.normal = await DbPluginInfo.filter(
+ plugin_type=PluginType.NORMAL, load_status=True
+ ).count()
+ plugin_count.admin = await DbPluginInfo.filter(
+ plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN], load_status=True
+ ).count()
+ plugin_count.superuser = await DbPluginInfo.filter(
+ plugin_type__in=[PluginType.SUPERUSER, PluginType.SUPER_AND_ADMIN],
+ load_status=True,
+ ).count()
+ plugin_count.other = await DbPluginInfo.filter(
+ plugin_type=PluginType.HIDDEN, load_status=True
+ ).count()
+ return Result.ok(plugin_count)
+
+
+@router.post(
+ "/update_plugin", dependencies=[authentication()], description="更新插件参数"
+)
+async def _(plugin: UpdatePlugin) -> Result:
+ try:
+ db_plugin = await DbPluginInfo.get_or_none(
+ module=plugin.module, load_status=True
+ )
+ if not db_plugin:
+ return Result.fail("插件不存在...")
+ db_plugin.default_status = plugin.default_status
+ db_plugin.limit_superuser = plugin.limit_superuser
+ db_plugin.cost_gold = plugin.cost_gold
+ db_plugin.level = plugin.level
+ db_plugin.menu_type = plugin.menu_type
+ db_plugin.block_type = plugin.block_type
+ if plugin.block_type == BlockType.ALL:
+ db_plugin.status = False
+ else:
+ db_plugin.status = True
+ await db_plugin.save()
+ # 配置项
+ if plugin.configs and (configs := Config.get(plugin.module)):
+ for key in plugin.configs:
+ if c := configs.configs.get(key):
+ value = plugin.configs[key]
+ if c.type and value is not None:
+ value = cattrs.structure(value, c.type)
+ Config.set_config(plugin.module, key, value)
+ except Exception as e:
+ logger.error("调用API错误", "/update_plugins", e=e)
+ return Result.fail(f"{type(e)}: {e}")
+ return Result.ok(info="已经帮你写好啦!")
+
+
+@router.post("/change_switch", dependencies=[authentication()], description="开关插件")
+async def _(param: PluginSwitch) -> Result:
+ db_plugin = await DbPluginInfo.get_or_none(module=param.module, load_status=True)
+ if not db_plugin:
+ return Result.fail("插件不存在...")
+ if not param.status:
+ db_plugin.block_type = BlockType.ALL
+ db_plugin.status = False
+ else:
+ db_plugin.block_type = None
+ db_plugin.status = True
+ await db_plugin.save()
+ return Result.ok(info="成功改变了开关状态!")
+
+
+@router.get(
+ "/get_plugin_menu_type", dependencies=[authentication()], description="获取插件类型"
+)
+async def _() -> Result:
+ menu_type_list = []
+ result = await DbPluginInfo.annotate().values_list("menu_type", flat=True)
+ for r in result:
+ if r not in menu_type_list and r:
+ menu_type_list.append(r)
+ return Result.ok(menu_type_list)
+
+
+@router.get("/get_plugin", dependencies=[authentication()], description="获取插件详情")
+async def _(module: str) -> Result:
+ db_plugin = await DbPluginInfo.get_or_none(module=module, load_status=True)
+ if not db_plugin:
+ return Result.fail("插件不存在...")
+ config_list = []
+ if config := Config.get(module):
+ for cfg in config.configs:
+ type_str = ""
+ type_inner = None
+ x = str(config.configs[cfg].type)
+ r = re.search(r"", str(config.configs[cfg].type))
+ if r:
+ type_str = r.group(1)
+ else:
+ r = re.search(r"typing\.(.*)\[(.*)\]", str(config.configs[cfg].type))
+ if r:
+ type_str = r.group(1)
+ if type_str:
+ type_str = type_str.lower()
+ type_inner = r.group(2)
+ if type_inner:
+ type_inner = [x.strip() for x in type_inner.split(",")]
+ config_list.append(
+ PluginConfig(
+ module=module,
+ key=cfg,
+ value=config.configs[cfg].value,
+ help=config.configs[cfg].help,
+ default_value=config.configs[cfg].default_value,
+ type=type_str,
+ type_inner=type_inner, # type: ignore
+ )
+ )
+ plugin_info = PluginDetail(
+ module=module,
+ plugin_name=db_plugin.name,
+ default_status=db_plugin.default_status,
+ limit_superuser=db_plugin.limit_superuser,
+ cost_gold=db_plugin.cost_gold,
+ menu_type=db_plugin.menu_type,
+ version=db_plugin.version or "0",
+ level=db_plugin.level,
+ status=db_plugin.status,
+ author=db_plugin.author,
+ config_list=config_list,
+ block_type=db_plugin.block_type,
+ )
+ return Result.ok(plugin_info)
diff --git a/plugins/web_ui/api/tabs/plugin_manage/model.py b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py
similarity index 63%
rename from plugins/web_ui/api/tabs/plugin_manage/model.py
rename to zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py
index 31c1ae9a..e2952038 100644
--- a/plugins/web_ui/api/tabs/plugin_manage/model.py
+++ b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py
@@ -1,10 +1,8 @@
-from typing import Any, Dict, List, Optional, Union
+from typing import Any
from pydantic import BaseModel
-from utils.manager.models import Plugin as PluginManager
-from utils.manager.models import PluginBlock, PluginCd, PluginCount, PluginSetting
-from utils.typing import BLOCK_TYPE
+from zhenxun.utils.enum import BlockType
class PluginSwitch(BaseModel):
@@ -48,9 +46,9 @@ class UpdatePlugin(BaseModel):
"""插件菜单类型"""
level: int
"""插件所需群权限"""
- block_type: Optional[BLOCK_TYPE] = None
+ block_type: BlockType | None = None
"""禁用类型"""
- configs: Optional[Dict[str, Any]] = None
+ configs: dict[str, Any] | None = None
"""配置项"""
@@ -71,15 +69,15 @@ class PluginInfo(BaseModel):
"""花费金币"""
menu_type: str
"""插件菜单类型"""
- version: Union[int, str, float]
+ version: str
"""插件版本"""
level: int
"""群权限"""
status: bool
"""当前状态"""
- author: Optional[str] = None
+ author: str | None = None
"""作者"""
- block_type: BLOCK_TYPE = None
+ block_type: BlockType | None = None
"""禁用类型"""
@@ -94,37 +92,16 @@ class PluginConfig(BaseModel):
"""键"""
value: Any
"""值"""
- help: Optional[str] = None
+ help: str | None = None
"""帮助"""
default_value: Any
"""默认值"""
- type: Optional[Any] = None
+ type: Any = None
"""值类型"""
- type_inner: Optional[List[str]] = None
+ type_inner: list[str] | None = None
"""List Tuple等内部类型检验"""
-
-class Plugin(BaseModel):
- """
- 插件
- """
-
- module: str
- """模块名称"""
- plugin_settings: Optional[PluginSetting]
- """settings"""
- plugin_manager: Optional[PluginManager]
- """manager"""
- plugin_config: Optional[Dict[str, PluginConfig]]
- """配置项"""
- cd_limit: Optional[PluginCd]
- """cd限制"""
- block_limit: Optional[PluginBlock]
- """阻断限制"""
- count_limit: Optional[PluginCount]
- """次数限制"""
-
class PluginCount(BaseModel):
"""
插件数量
@@ -145,4 +122,4 @@ class PluginDetail(PluginInfo):
插件详情
"""
- config_list: List[PluginConfig]
\ No newline at end of file
+ config_list: list[PluginConfig]
diff --git a/plugins/web_ui/api/tabs/system/__init__.py b/zhenxun/plugins/web_ui/api/tabs/system/__init__.py
similarity index 100%
rename from plugins/web_ui/api/tabs/system/__init__.py
rename to zhenxun/plugins/web_ui/api/tabs/system/__init__.py
diff --git a/plugins/web_ui/api/tabs/system/model.py b/zhenxun/plugins/web_ui/api/tabs/system/model.py
similarity index 100%
rename from plugins/web_ui/api/tabs/system/model.py
rename to zhenxun/plugins/web_ui/api/tabs/system/model.py
diff --git a/plugins/web_ui/auth/__init__.py b/zhenxun/plugins/web_ui/auth/__init__.py
similarity index 78%
rename from plugins/web_ui/auth/__init__.py
rename to zhenxun/plugins/web_ui/auth/__init__.py
index d927977f..d5a4ead7 100644
--- a/plugins/web_ui/auth/__init__.py
+++ b/zhenxun/plugins/web_ui/auth/__init__.py
@@ -5,7 +5,7 @@ import nonebot
from fastapi import APIRouter, Depends
from fastapi.security import OAuth2PasswordRequestForm
-from configs.config import Config
+from zhenxun.configs.config import Config
from ..base_model import Result
from ..utils import (
@@ -28,10 +28,14 @@ async def login_get_token(form_data: OAuth2PasswordRequestForm = Depends()):
password = Config.get_config("web-ui", "password")
if not username or not password:
return Result.fail("你滴配置文件里用户名密码配置项为空", 998)
- if username != form_data.username or password != form_data.password:
+ if username != form_data.username or str(password) != form_data.password:
return Result.fail("真笨, 账号密码都能记错!", 999)
+ user = get_user(form_data.username)
+ if not user:
+ return Result.fail("用户不存在...", 997)
access_token = create_token(
- user=get_user(form_data.username), expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+ user=user,
+ expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
)
token_data["token"].append(access_token)
if len(token_data["token"]) > 3:
diff --git a/plugins/web_ui/base_model.py b/zhenxun/plugins/web_ui/base_model.py
similarity index 81%
rename from plugins/web_ui/base_model.py
rename to zhenxun/plugins/web_ui/base_model.py
index ee7a93e4..67bb280f 100644
--- a/plugins/web_ui/base_model.py
+++ b/zhenxun/plugins/web_ui/base_model.py
@@ -1,13 +1,8 @@
from datetime import datetime
-from logging import warning
-from typing import Any, Dict, Generic, List, Optional, TypeVar, Union
+from typing import Any, Generic, Optional, TypeVar
-from nonebot.adapters.onebot.v11 import Bot
from pydantic import BaseModel, validator
-
-from configs.utils import Config
-from utils.manager.models import Plugin as PluginManager
-from utils.manager.models import PluginBlock, PluginCd, PluginCount, PluginSetting
+from typing_extensions import Self
T = TypeVar("T")
@@ -39,15 +34,15 @@ class Result(BaseModel):
"""返回数据"""
@classmethod
- def warning_(cls, info: str, code: int = 200) -> "Result":
+ def warning_(cls, info: str, code: int = 200) -> Self:
return cls(suc=True, warning=info, code=code)
@classmethod
- def fail(cls, info: str = "异常错误", code: int = 500) -> "Result":
+ def fail(cls, info: str = "异常错误", code: int = 500) -> Self:
return cls(suc=False, info=info, code=code)
@classmethod
- def ok(cls, data: Any = None, info: str = "操作成功", code: int = 200) -> "Result":
+ def ok(cls, data: Any = None, info: str = "操作成功", code: int = 200) -> Self:
return cls(suc=True, info=info, code=code, data=data)
@@ -74,7 +69,7 @@ class QueryModel(BaseModel, Generic[T]):
if size < 1:
raise ValueError("每页数量小于1...")
return size
-
+
class BaseResultModel(BaseModel):
"""
@@ -98,8 +93,6 @@ class SystemStatus(BaseModel):
check_time: datetime
-
-
class SystemFolderSize(BaseModel):
"""
资源文件占比
@@ -113,5 +106,3 @@ class SystemFolderSize(BaseModel):
"""完整路径"""
is_dir: bool
"""是否为文件夹"""
-
-
diff --git a/zhenxun/plugins/web_ui/config.py b/zhenxun/plugins/web_ui/config.py
new file mode 100644
index 00000000..0f16949a
--- /dev/null
+++ b/zhenxun/plugins/web_ui/config.py
@@ -0,0 +1,36 @@
+import nonebot
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel
+from strenum import StrEnum
+
+app = nonebot.get_app()
+
+origins = ["*"]
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+AVA_URL = "http://q1.qlogo.cn/g?b=qq&nk={}&s=160"
+
+GROUP_AVA_URL = "http://p.qlogo.cn/gh/{}/{}/640/"
+
+
+class QueryDateType(StrEnum):
+ """
+ 查询日期类型
+ """
+
+ DAY = "day"
+ """日"""
+ WEEK = "week"
+ """周"""
+ MONTH = "month"
+ """月"""
+ YEAR = "year"
+ """年"""
diff --git a/plugins/web_ui/utils.py b/zhenxun/plugins/web_ui/utils.py
similarity index 60%
rename from plugins/web_ui/utils.py
rename to zhenxun/plugins/web_ui/utils.py
index eba64b63..f39f36ac 100644
--- a/plugins/web_ui/utils.py
+++ b/zhenxun/plugins/web_ui/utils.py
@@ -1,7 +1,6 @@
import os
from datetime import datetime, timedelta
from pathlib import Path
-from typing import Any, Dict, List, Optional, Union
import psutil
import ujson as json
@@ -10,16 +9,8 @@ from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from nonebot.utils import run_sync
-from configs.config import Config
-from configs.path_config import (
- DATA_PATH,
- FONT_PATH,
- IMAGE_PATH,
- LOG_PATH,
- RECORD_PATH,
- TEMP_PATH,
- TEXT_PATH,
-)
+from zhenxun.configs.config import Config
+from zhenxun.configs.path_config import DATA_PATH
from .base_model import SystemFolderSize, SystemStatus, User
@@ -39,7 +30,7 @@ if token_file.exists():
pass
-def get_user(uname: str) -> Optional[User]:
+def get_user(uname: str) -> User | None:
"""获取账号密码
参数:
@@ -54,7 +45,7 @@ def get_user(uname: str) -> Optional[User]:
return User(username=username, password=password)
-def create_token(user: User, expires_delta: Optional[timedelta] = None):
+def create_token(user: User, expires_delta: timedelta | None = None):
"""创建token
参数:
@@ -72,11 +63,11 @@ def create_token(user: User, expires_delta: Optional[timedelta] = None):
def authentication():
"""权限验证
-
异常:
JWTError: JWTError
HTTPException: HTTPException
"""
+
# if token not in token_data["token"]:
def inner(token: str = Depends(oauth2_scheme)):
try:
@@ -86,17 +77,18 @@ def authentication():
if user is None:
raise JWTError
except JWTError:
- raise HTTPException(status_code=400, detail="登录验证失败或已失效, 踢出房间!")
+ raise HTTPException(
+ status_code=400, detail="登录验证失败或已失效, 踢出房间!"
+ )
return Depends(inner)
def _get_dir_size(dir_path: Path) -> float:
- """
- 说明:
- 获取文件夹大小
+ """获取文件夹大小
+
参数:
- :param dir_path: 文件夹路径
+ dir_path: 文件夹路径
"""
size = 0
for root, dirs, files in os.walk(dir_path):
@@ -106,10 +98,7 @@ def _get_dir_size(dir_path: Path) -> float:
@run_sync
def get_system_status() -> SystemStatus:
- """
- 说明:
- 获取系统信息等
- """
+ """获取系统信息等"""
cpu = psutil.cpu_percent()
memory = psutil.virtual_memory().percent
disk = psutil.disk_usage("/").percent
@@ -123,12 +112,9 @@ def get_system_status() -> SystemStatus:
@run_sync
def get_system_disk(
- full_path: Optional[str],
-) -> List[SystemFolderSize]:
- """
- 说明:
- 获取资源文件大小等
- """
+ full_path: str | None,
+) -> list[SystemFolderSize]:
+ """获取资源文件大小等"""
base_path = Path(full_path) if full_path else Path()
other_size = 0
data_list = []
@@ -136,35 +122,15 @@ def get_system_disk(
f = base_path / file
if f.is_dir():
size = _get_dir_size(f) / 1024 / 1024
- data_list.append(SystemFolderSize(name=file, size=size, full_path=str(f), is_dir=True))
+ data_list.append(
+ SystemFolderSize(name=file, size=size, full_path=str(f), is_dir=True)
+ )
else:
other_size += f.stat().st_size / 1024 / 1024
if other_size:
- data_list.append(SystemFolderSize(name='other_file', size=other_size, full_path=full_path, is_dir=False))
+ data_list.append(
+ SystemFolderSize(
+ name="other_file", size=other_size, full_path=full_path, is_dir=False
+ )
+ )
return data_list
- # else:
- # if type_ == "image":
- # dir_path = IMAGE_PATH
- # elif type_ == "font":
- # dir_path = FONT_PATH
- # elif type_ == "text":
- # dir_path = TEXT_PATH
- # elif type_ == "record":
- # dir_path = RECORD_PATH
- # elif type_ == "data":
- # dir_path = DATA_PATH
- # elif type_ == "temp":
- # dir_path = TEMP_PATH
- # else:
- # dir_path = LOG_PATH
- # dir_map = {}
- # other_file_size = 0
- # for file in os.listdir(dir_path):
- # file = Path(dir_path / file)
- # if file.is_dir():
- # dir_map[file.name] = _get_dir_size(file) / 1024 / 1024
- # else:
- # other_file_size += os.path.getsize(file) / 1024 / 1024
- # dir_map["其他文件"] = other_file_size
- # dir_map["check_time"] = datetime.now().replace(microsecond=0)
- # return dir_map
diff --git a/zhenxun/plugins/what_anime/__init__.py b/zhenxun/plugins/what_anime/__init__.py
new file mode 100644
index 00000000..d3b91876
--- /dev/null
+++ b/zhenxun/plugins/what_anime/__init__.py
@@ -0,0 +1,60 @@
+from nonebot.plugin import PluginMetadata
+from nonebot_plugin_alconna import Alconna, Args, Arparma
+from nonebot_plugin_alconna import Image as alcImg
+from nonebot_plugin_alconna import Match, 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_anime
+
+__plugin_meta__ = PluginMetadata(
+ name="识番",
+ description="以图识番",
+ usage="""
+ usage:
+ api.trace.moe 以图识番
+ 指令:
+ 识番 [图片]
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier", version="0.1", menu_type="一些工具"
+ ).dict(),
+)
+
+
+_matcher = on_alconna(Alconna("识番", Args["image?", alcImg]), block=True, priority=5)
+
+
+@_matcher.handle()
+async def _(image: Match[alcImg]):
+ if image.available:
+ _matcher.set_path_arg("image", image.result)
+
+
+@_matcher.got_path("image", prompt="图来!")
+async def _(
+ session: EventSession,
+ arparma: Arparma,
+ image: alcImg,
+):
+ if not image.url:
+ await MessageUtils.build_message("图片url为空...").finish()
+ await MessageUtils.build_message("开始识别...").send()
+ anime_data_report = await get_anime(image.url)
+ if anime_data_report:
+ await MessageUtils.build_message(anime_data_report).send(reply_to=True)
+ logger.info(
+ f" 识番 {image.url} --> {anime_data_report}",
+ arparma.header_result,
+ session=session,
+ )
+ else:
+ logger.info(
+ f"识番 {image.url} 未找到...", arparma.header_result, session=session
+ )
+ await MessageUtils.build_message(f"没有寻找到该番剧,果咩..").send(
+ reply_to=True
+ )
diff --git a/plugins/what_anime/data_source.py b/zhenxun/plugins/what_anime/data_source.py
old mode 100755
new mode 100644
similarity index 86%
rename from plugins/what_anime/data_source.py
rename to zhenxun/plugins/what_anime/data_source.py
index 87fc071a..15801f62
--- a/plugins/what_anime/data_source.py
+++ b/zhenxun/plugins/what_anime/data_source.py
@@ -1,47 +1,47 @@
-import time
-from services.log import logger
-from utils.langconv import *
-from utils.http_utils import AsyncHttpx
-
-
-async def get_anime(anime: str) -> str:
- s_time = time.time()
- url = "https://api.trace.moe/search?anilistInfo&url={}".format(anime)
- logger.debug("[info]Now starting get the {}".format(url))
- try:
- anime_json = (await AsyncHttpx.get(url)).json()
- if not anime_json["error"]:
- if anime_json == "Error reading imagenull":
- return "图像源错误,注意必须是静态图片哦"
- repass = ""
- # 拿到动漫 中文名
- for anime in anime_json["result"][:5]:
- synonyms = anime["anilist"]["synonyms"]
- for x in synonyms:
- _count_ch = 0
- for word in x:
- if "\u4e00" <= word <= "\u9fff":
- _count_ch += 1
- if _count_ch > 3:
- anime_name = x
- break
- else:
- anime_name = anime["anilist"]["title"]["native"]
- episode = anime["episode"]
- from_ = int(anime["from"])
- m, s = divmod(from_, 60)
- similarity = anime["similarity"]
- putline = "[ {} ][{}][{}:{}] 相似度:{:.2%}".format(
- Converter("zh-hans").convert(anime_name),
- episode if episode else "?",
- m,
- s,
- similarity,
- )
- repass += putline + "\n"
- return f"耗时 {int(time.time() - s_time)} 秒\n" + repass[:-1]
- else:
- return f'访问错误 error:{anime_json["error"]}'
- except Exception as e:
- logger.error(f"识番发生错误 {type(e)}:{e}")
- return "发生了奇怪的错误,那就没办法了,再试一次?"
+import time
+
+from zhenxun.services.log import logger
+from zhenxun.utils.http_utils import AsyncHttpx
+
+
+async def get_anime(anime: str) -> str:
+ s_time = time.time()
+ url = "https://api.trace.moe/search?anilistInfo&url={}".format(anime)
+ logger.debug("[info]Now starting get the {}".format(url))
+ try:
+ anime_json = (await AsyncHttpx.get(url)).json()
+ if not anime_json["error"]:
+ if anime_json == "Error reading imagenull":
+ return "图像源错误,注意必须是静态图片哦"
+ repass = ""
+ # 拿到动漫 中文名
+ for anime in anime_json["result"][:5]:
+ synonyms = anime["anilist"]["synonyms"]
+ for x in synonyms:
+ _count_ch = 0
+ for word in x:
+ if "\u4e00" <= word <= "\u9fff":
+ _count_ch += 1
+ if _count_ch > 3:
+ anime_name = x
+ break
+ else:
+ anime_name = anime["anilist"]["title"]["native"]
+ episode = anime["episode"]
+ from_ = int(anime["from"])
+ m, s = divmod(from_, 60)
+ similarity = anime["similarity"]
+ putline = "[ {} ][{}][{}:{}] 相似度:{:.2%}".format(
+ anime_name,
+ episode if episode else "?",
+ m,
+ s,
+ similarity,
+ )
+ repass += putline + "\n"
+ return f"耗时 {int(time.time() - s_time)} 秒\n" + repass[:-1]
+ else:
+ return f'访问错误 error:{anime_json["error"]}'
+ except Exception as e:
+ logger.error(f"识番发生错误", e=e)
+ return "发生了奇怪的错误,那就没办法了,再试一次?"
diff --git a/zhenxun/plugins/word_bank/__init__.py b/zhenxun/plugins/word_bank/__init__.py
new file mode 100644
index 00000000..c3ef6546
--- /dev/null
+++ b/zhenxun/plugins/word_bank/__init__.py
@@ -0,0 +1,18 @@
+from pathlib import Path
+
+import nonebot
+
+from zhenxun.configs.config import Config
+
+Config.add_plugin_config(
+ "word_bank",
+ "WORD_BANK_LEVEL",
+ 5,
+ help="设置增删词库的权限等级",
+ default_value=5,
+ type=int,
+)
+Config.set_name("word_bank", "词库问答")
+
+
+nonebot.load_plugins(str(Path(__file__).parent.resolve()))
diff --git a/plugins/word_bank/_config.py b/zhenxun/plugins/word_bank/_config.py
similarity index 64%
rename from plugins/word_bank/_config.py
rename to zhenxun/plugins/word_bank/_config.py
index d1f6ab67..3d074c18 100644
--- a/plugins/word_bank/_config.py
+++ b/zhenxun/plugins/word_bank/_config.py
@@ -1,4 +1,7 @@
+from zhenxun.configs.path_config import DATA_PATH
+data_dir = DATA_PATH / "word_bank"
+data_dir.mkdir(parents=True, exist_ok=True)
scope2int = {
"全局": 0,
@@ -19,5 +22,3 @@ int2type = {
2: "正则",
3: "图片",
}
-
-
diff --git a/zhenxun/plugins/word_bank/_data_source.py b/zhenxun/plugins/word_bank/_data_source.py
new file mode 100644
index 00000000..c9a53a1b
--- /dev/null
+++ b/zhenxun/plugins/word_bank/_data_source.py
@@ -0,0 +1,287 @@
+from nonebot_plugin_alconna import At
+from nonebot_plugin_alconna import At as alcAt
+from nonebot_plugin_alconna import Image
+from nonebot_plugin_alconna import Image as alcImage
+from nonebot_plugin_alconna import Text as alcText
+from nonebot_plugin_alconna import UniMessage, UniMsg
+
+from zhenxun.utils.image_utils import ImageTemplate
+from zhenxun.utils.message import MessageUtils
+
+from ._model import WordBank
+
+
+def get_img_and_at_list(message: UniMsg) -> tuple[list[str], list[str]]:
+ """获取图片和at数据
+
+ 参数:
+ message: UniMsg
+
+ 返回:
+ tuple[list[str], list[str]]: 图片列表,at列表
+ """
+ img_list, at_list = [], []
+ for msg in message:
+ if isinstance(msg, alcImage):
+ img_list.append(msg.url)
+ elif isinstance(msg, alcAt):
+ at_list.append(msg.target)
+ return img_list, at_list
+
+
+def get_problem(message: UniMsg) -> str:
+ """获取问题内容
+
+ 参数:
+ message: UniMsg
+
+ 返回:
+ str: 问题文本
+ """
+ problem = ""
+ a, b = True, True
+ for msg in message:
+ if isinstance(msg, alcText) or isinstance(msg, str):
+ msg = str(msg)
+ if "问" in str(msg) and a:
+ a = False
+ split_text = msg.split("问")
+ if len(split_text) > 1:
+ problem += "问".join(split_text[1:])
+ if b:
+ if "答" in problem:
+ b = False
+ problem = problem.split("答")[0]
+ elif "答" in msg and b:
+ b = False
+ # problem += "答".join(msg.split("答")[:-1])
+ problem += msg.split("答")[0]
+ if not a and not b:
+ break
+ if isinstance(msg, alcAt):
+ problem += f"[at:{msg.target}]"
+ return problem
+
+
+def get_answer(message: UniMsg) -> UniMessage | None:
+ """获取at时回答
+
+ 参数:
+ message: UniMsg
+
+ 返回:
+ str: 回答内容
+ """
+ temp_message = None
+ answer = ""
+ index = 0
+ for msg in message:
+ index += 1
+ if isinstance(msg, alcText) or isinstance(msg, str):
+ msg = str(msg)
+ if "答" in msg:
+ answer += "答".join(msg.split("答")[1:])
+ break
+ if answer:
+ temp_message = message[index:]
+ temp_message.insert(0, alcText(answer))
+ return temp_message
+
+
+class WordBankManage:
+
+ @classmethod
+ async def update_word(
+ cls,
+ replace: str,
+ problem: str = "",
+ index: int | None = None,
+ group_id: str | None = None,
+ word_scope: int = 1,
+ ) -> tuple[str, str]:
+ """修改群词条
+
+ 参数:
+ params: 参数
+ group_id: 群号
+ word_scope: 词条范围
+
+ 返回:
+ tuple[str, str]: 处理消息,替换的旧词条
+ """
+ return await cls.__word_handle(
+ problem, group_id, "update", index, None, word_scope, replace
+ )
+
+ @classmethod
+ async def delete_word(
+ cls,
+ problem: str,
+ index: int | None = None,
+ aid: int | None = None,
+ group_id: str | None = None,
+ word_scope: int = 1,
+ ) -> tuple[str, str]:
+ """删除群词条
+
+ 参数:
+ params: 参数
+ index: 指定下标
+ aid: 指定回答下标
+ group_id: 群号
+ word_scope: 词条范围
+
+ 返回:
+ tuple[str, str]: 处理消息,空
+ """
+ return await cls.__word_handle(
+ problem, group_id, "delete", index, aid, word_scope
+ )
+
+ @classmethod
+ async def __word_handle(
+ cls,
+ problem: str,
+ group_id: str | None,
+ handle_type: str,
+ index: int | None = None,
+ aid: int | None = None,
+ word_scope: int = 0,
+ replace_problem: str = "",
+ ) -> tuple[str, str]:
+ """词条操作
+
+ 参数:
+ problem: 参数
+ group_id: 群号
+ handle_type: 类型
+ index: 指定回答下标
+ aid: 指定回答下标
+ word_scope: 词条范围
+ replace_problem: 替换问题内容
+
+ 返回:
+ tuple[str, str]: 处理消息,替换的旧词条
+ """
+ if index is not None:
+ problem, code = await cls.__get_problem_str(index, group_id, word_scope)
+ if code != 200:
+ return problem, ""
+ if handle_type == "delete":
+ if index:
+ problem, _problem_list = await WordBank.get_problem_all_answer(
+ problem, None, group_id, word_scope
+ )
+ if not _problem_list:
+ return problem, ""
+ if await WordBank.delete_group_problem(problem, group_id, aid, word_scope): # type: ignore
+ return "删除词条成功!", ""
+ return "词条不存在", ""
+ if handle_type == "update":
+ old_problem = await WordBank.update_group_problem(
+ problem, replace_problem, group_id, word_scope=word_scope
+ )
+ return f"修改词条成功!\n{old_problem} -> {replace_problem}", old_problem
+ return "类型错误", ""
+
+ @classmethod
+ async def __get_problem_str(
+ cls, idx: int, group_id: str | None = None, word_scope: int = 1
+ ) -> tuple[str, int]:
+ """通过id获取问题字符串
+
+ 参数:
+ idx: 下标
+ group_id: 群号
+ word_scope: 获取类型
+ """
+ if word_scope in [0, 2]:
+ all_problem = await WordBank.get_problem_by_scope(word_scope)
+ elif group_id:
+ all_problem = await WordBank.get_group_all_problem(group_id)
+ else:
+ raise Exception("词条类型与群组id不能为空")
+ if idx < 0 or idx >= len(all_problem):
+ return "问题下标id必须在范围内", 999
+ return all_problem[idx][0], 200
+
+ @classmethod
+ async def show_word(
+ cls,
+ problem: str | None,
+ index: int | None = None,
+ group_id: str | None = None,
+ word_scope: int | None = 1,
+ ) -> UniMessage:
+ """获取群词条
+
+ 参数:
+ problem: 问题
+ group_id: 群组id
+ word_scope: 词条范围
+ index: 指定回答下标
+ """
+ if problem or index != None:
+ msg_list = []
+ problem, _problem_list = await WordBank.get_problem_all_answer(
+ problem, # type: ignore
+ index,
+ group_id if group_id is None else None,
+ word_scope,
+ )
+ if not _problem_list:
+ return MessageUtils.build_message(problem)
+ for msg in _problem_list:
+ _text = str(msg)
+ if isinstance(msg, At):
+ _text = f"[at:{msg.target}]"
+ elif isinstance(msg, Image):
+ _text = msg.url or msg.path
+ elif isinstance(msg, list):
+ _text = []
+ for m in msg:
+ __text = str(m)
+ if isinstance(m, At):
+ __text = f"[at:{m.target}]"
+ elif isinstance(m, Image):
+ # TODO: 显示词条回答图片
+ # __text = (m.data["image"], 30, 30)
+ __text = "[图片]"
+ _text.append(__text)
+ msg_list.append("".join(str(_text)))
+ column_name = ["序号", "回答内容"]
+ data_list = []
+ for index, msg in enumerate(msg_list):
+ data_list.append([index, msg])
+ template_image = await ImageTemplate.table_page(
+ f"词条 {problem} 的回答", None, column_name, data_list
+ )
+ return MessageUtils.build_message(template_image)
+ else:
+ result = []
+ if group_id:
+ _problem_list = await WordBank.get_group_all_problem(group_id)
+ elif word_scope is not None:
+ _problem_list = await WordBank.get_problem_by_scope(word_scope)
+ else:
+ raise Exception("群组id和词条范围不能都为空")
+ global_problem_list = await WordBank.get_problem_by_scope(0)
+ if not _problem_list and not global_problem_list:
+ return MessageUtils.build_message("未收录任何词条...")
+ column_name = ["序号", "关键词", "匹配类型", "收录用户"]
+ data_list = [list(s) for s in _problem_list]
+ for i in range(len(data_list)):
+ data_list[i].insert(0, i)
+ group_image = await ImageTemplate.table_page(
+ "群组内词条" if group_id else "私聊词条", None, column_name, data_list
+ )
+ result.append(group_image)
+ if global_problem_list:
+ data_list = [list(s) for s in global_problem_list]
+ for i in range(len(data_list)):
+ data_list[i].insert(0, i)
+ global_image = await ImageTemplate.table_page(
+ "全局词条", None, column_name, data_list
+ )
+ result.append(global_image)
+ return MessageUtils.build_message(result)
diff --git a/plugins/word_bank/_model.py b/zhenxun/plugins/word_bank/_model.py
similarity index 52%
rename from plugins/word_bank/_model.py
rename to zhenxun/plugins/word_bank/_model.py
index cfd1c647..abf03779 100644
--- a/plugins/word_bank/_model.py
+++ b/zhenxun/plugins/word_bank/_model.py
@@ -3,24 +3,21 @@ import re
import time
import uuid
from datetime import datetime
-from typing import Any, List, Optional, Tuple, Union
+from typing import Any
-from nonebot.adapters.onebot.v11 import (
- GroupMessageEvent,
- Message,
- MessageEvent,
- MessageSegment,
-)
-from nonebot.internal.adapter.template import MessageTemplate
+from nonebot_plugin_alconna import At as alcAt
+from nonebot_plugin_alconna import Image as alcImage
+from nonebot_plugin_alconna import Text as alcText
+from nonebot_plugin_alconna import UniMessage
from tortoise import Tortoise, fields
from tortoise.expressions import Q
+from typing_extensions import Self
-from configs.path_config import DATA_PATH
-from services.db_context import Model
-from utils.http_utils import AsyncHttpx
-from utils.image_utils import get_img_hash
-from utils.message_builder import at, face, image
-from utils.utils import get_message_img
+from zhenxun.configs.path_config import DATA_PATH
+from zhenxun.services.db_context import Model
+from zhenxun.utils.http_utils import AsyncHttpx
+from zhenxun.utils.image_utils import get_img_hash
+from zhenxun.utils.message import MessageUtils
from ._config import int2type
@@ -55,6 +52,10 @@ class WordBank(Model):
"""创建时间"""
update_time = fields.DatetimeField(auto_now_add=True)
"""更新时间"""
+ platform = fields.CharField(255, default="qq")
+ """平台"""
+ author = fields.CharField(255, null=True, default="")
+ """收录人"""
class Meta:
table = "word_bank2"
@@ -63,23 +64,22 @@ class WordBank(Model):
@classmethod
async def exists(
cls,
- user_id: Optional[str],
- group_id: Optional[str],
+ user_id: str | None,
+ group_id: str | None,
problem: str,
- answer: Optional[str],
- word_scope: Optional[int] = None,
- word_type: Optional[int] = None,
+ answer: str | None,
+ word_scope: int | None = None,
+ word_type: int | None = None,
) -> bool:
- """
- 说明:
- 检测问题是否存在
+ """检测问题是否存在
+
参数:
- :param user_id: 用户id
- :param group_id: 群号
- :param problem: 问题
- :param answer: 回答
- :param word_scope: 词条范围
- :param word_type: 词条类型
+ user_id: 用户id
+ group_id: 群号
+ problem: 问题
+ answer: 回答
+ word_scope: 词条范围
+ word_type: 词条类型
"""
query = cls.filter(problem=problem)
if user_id:
@@ -92,45 +92,49 @@ class WordBank(Model):
query = query.filter(word_type=word_type)
if word_scope is not None:
query = query.filter(word_scope=word_scope)
- return bool(await query.first())
+ return await query.exists()
@classmethod
async def add_problem_answer(
cls,
user_id: str,
- group_id: Optional[str],
+ group_id: str | None,
word_scope: int,
word_type: int,
- problem: Union[str, Message],
- answer: Union[str, Message],
- to_me_nickname: Optional[str] = None,
+ problem: str,
+ answer: list[str | alcText | alcAt | alcImage],
+ to_me_nickname: str | None = None,
+ platform: str = "",
+ author: str = "",
):
- """
- 说明:
- 添加或新增一个问答
+ """添加或新增一个问答
+
参数:
- :param user_id: 用户id
- :param group_id: 群号
- :param word_scope: 词条范围,
- :param word_type: 词条类型,
- :param problem: 问题
- :param answer: 回答
- :param to_me_nickname: at真寻名称
+ user_id: 用户id
+ group_id: 群号
+ word_scope: 词条范围,
+ word_type: 词条类型,
+ problem: 问题, 为图片时是URl
+ answer: 回答
+ to_me_nickname: at真寻名称
+ platform: 所属平台
+ author: 收录人id
"""
# 对图片做额外处理
image_path = None
if word_type == 3:
- url = get_message_img(problem)[0]
_file = (
path / "problem" / f"{group_id}" / f"{user_id}_{int(time.time())}.jpg"
)
_file.parent.mkdir(exist_ok=True, parents=True)
- await AsyncHttpx.download_file(url, _file)
- problem = str(get_img_hash(_file))
+ await AsyncHttpx.download_file(problem, _file)
+ problem = get_img_hash(_file)
image_path = f"problem/{group_id}/{user_id}_{int(time.time())}.jpg"
- answer, _list = await cls._answer2format(answer, user_id, group_id)
+ new_answer, placeholder_list = await cls._answer2format(
+ answer, user_id, group_id
+ )
if not await cls.exists(
- user_id, group_id, str(problem), answer, word_scope, word_type
+ user_id, group_id, problem, new_answer, word_scope, word_type
):
await cls.create(
user_id=user_id,
@@ -139,44 +143,49 @@ class WordBank(Model):
word_type=word_type,
status=True,
problem=str(problem).strip(),
- answer=answer,
+ answer=new_answer,
image_path=image_path,
- placeholder=",".join(_list),
+ placeholder=",".join(placeholder_list),
create_time=datetime.now().replace(microsecond=0),
update_time=datetime.now().replace(microsecond=0),
to_me=to_me_nickname,
+ platform=platform,
+ author=author,
)
@classmethod
async def _answer2format(
- cls, answer: Union[str, Message], user_id: str, group_id: Optional[str]
- ) -> Tuple[str, List[Any]]:
- """
- 说明:
- 将CQ码转化为占位符
+ cls,
+ answer: list[str | alcText | alcAt | alcImage],
+ user_id: str,
+ group_id: str | None,
+ ) -> tuple[str, list[Any]]:
+ """将特殊字段转化为占位符,图片,at等
+
参数:
- :param answer: 回答内容
- :param user_id: 用户id
- :param group_id: 群号
+ answer: 回答内容
+ user_id: 用户id
+ group_id: 群号
+
+ 返回:
+ tuple[str, list[Any]]: 替换后的文本回答内容,占位符
"""
- if isinstance(answer, str):
- return answer, []
- _list = []
+ placeholder_list = []
text = ""
index = 0
for seg in answer:
placeholder = uuid.uuid1()
if isinstance(seg, str):
text += seg
- elif seg.type == "text":
- text += seg.data["text"]
- elif seg.type == "face":
+ elif isinstance(seg, alcText):
+ text += seg.text
+ elif seg.type == "face": # TODO: face貌似无用...
text += f"[face:placeholder_{placeholder}]"
- _list.append(seg.data["id"])
- elif seg.type == "at":
+ placeholder_list.append(seg.data["id"])
+ elif isinstance(seg, alcAt):
text += f"[at:placeholder_{placeholder}]"
- _list.append(seg.data["qq"])
- else:
+ placeholder_list.append(seg.target)
+ elif isinstance(seg, alcImage) and seg.url:
text += f"[image:placeholder_{placeholder}]"
index += 1
_file = (
@@ -186,30 +195,30 @@ class WordBank(Model):
/ f"{user_id}_{placeholder}.jpg"
)
_file.parent.mkdir(exist_ok=True, parents=True)
- await AsyncHttpx.download_file(seg.data["url"], _file)
- _list.append(
+ await AsyncHttpx.download_file(seg.url, _file)
+ placeholder_list.append(
f"answer/{group_id or user_id}/{user_id}_{placeholder}.jpg"
)
- return text, _list
+ return text, placeholder_list
@classmethod
async def _format2answer(
cls,
problem: str,
- answer: Union[str, Message],
+ answer: str,
user_id: int,
group_id: int,
- query: Optional["WordBank"] = None,
- ) -> Union[str, Message]:
- """
- 说明:
- 将占位符转换为CQ码
+ query: Self | None = None,
+ ) -> UniMessage:
+ """将占位符转换为实际内容
+
参数:
- :param problem: 问题内容
- :param answer: 回答内容
- :param user_id: 用户id
- :param group_id: 群号
+ problem: 问题内容
+ answer: 回答内容
+ user_id: 用户id
+ group_id: 群组id
"""
+ result_list = []
if not query:
query = await cls.get_or_none(
problem=problem,
@@ -218,44 +227,45 @@ class WordBank(Model):
answer=answer,
)
if not answer:
- answer = query.answer # type: ignore
+ answer = str(query.answer) # type: ignore
if query and query.placeholder:
- type_list = re.findall(rf"\[(.*?):placeholder_.*?]", str(answer))
- temp_answer = re.sub(rf"\[(.*?):placeholder_.*?]", "{}", str(answer))
- seg_list = []
- for t, p in zip(type_list, query.placeholder.split(",")):
- if t == "image":
- seg_list.append(image(path / p))
- elif t == "face":
- seg_list.append(face(int(p)))
- elif t == "at":
- seg_list.append(at(p))
- return MessageTemplate(temp_answer, Message).format(*seg_list) # type: ignore
- return answer
+ type_list = re.findall(rf"\[(.*?):placeholder_.*?]", answer)
+ answer_split = re.split(rf"\[.*:placeholder_.*?]", answer)
+ placeholder_split = query.placeholder.split(",")
+ for index, ans in enumerate(answer_split):
+ result_list.append(ans)
+ if index < len(type_list):
+ t = type_list[index]
+ p = placeholder_split[index]
+ if t == "image":
+ result_list.append(path / p)
+ elif t == "at":
+ result_list.append(alcAt(flag="user", target=p))
+ return MessageUtils.build_message(result_list)
+ return MessageUtils.build_message(answer)
@classmethod
async def check_problem(
cls,
- event: MessageEvent,
+ group_id: str | None,
problem: str,
- word_scope: Optional[int] = None,
- word_type: Optional[int] = None,
- ) -> Optional[Any]:
- """
- 说明:
- 检测是否包含该问题并获取所有回答
+ word_scope: int | None = None,
+ word_type: int | None = None,
+ ) -> Any:
+ """检测是否包含该问题并获取所有回答
+
参数:
- :param event: event
- :param problem: 问题内容
- :param word_scope: 词条范围
- :param word_type: 词条类型
+ group_id: 群组id
+ problem: 问题内容
+ word_scope: 词条范围
+ word_type: 词条类型
"""
query = cls
- if isinstance(event, GroupMessageEvent):
+ if group_id:
if word_scope:
query = query.filter(word_scope=word_scope)
else:
- query = query.filter(Q(group_id=event.group_id) | Q(word_scope=0))
+ query = query.filter(Q(group_id=group_id) | Q(word_scope=0))
else:
query = query.filter(Q(word_scope=2) | Q(word_scope=0))
if word_type:
@@ -283,24 +293,23 @@ class WordBank(Model):
@classmethod
async def get_answer(
cls,
- event: MessageEvent,
+ group_id: str | None,
problem: str,
- word_scope: Optional[int] = None,
- word_type: Optional[int] = None,
- ) -> Optional[Union[str, Message]]:
- """
- 说明:
- 根据问题内容获取随机回答
+ word_scope: int | None = None,
+ word_type: int | None = None,
+ ) -> UniMessage | None:
+ """根据问题内容获取随机回答
+
参数:
- :param event: event
- :param problem: 问题内容
- :param word_scope: 词条范围
- :param word_type: 词条类型
+ user_id: 用户id
+ group_id: 群组id
+ problem: 问题内容
+ word_scope: 词条范围
+ word_type: 词条类型
"""
- data_list = await cls.check_problem(event, problem, word_scope, word_type)
+ data_list = await cls.check_problem(group_id, problem, word_scope, word_type)
if data_list:
random_answer = random.choice(data_list)
- temp_answer = random_answer.answer
if random_answer.word_type == 2:
r = re.search(random_answer.problem, problem)
has_placeholder = re.search(rf"\$(\d)", random_answer.answer)
@@ -316,64 +325,93 @@ class WordBank(Model):
random_answer,
)
if random_answer.placeholder
- else random_answer.answer
+ else MessageUtils.build_message(random_answer.answer)
)
@classmethod
async def get_problem_all_answer(
cls,
problem: str,
- index: Optional[int] = None,
- group_id: Optional[str] = None,
- word_scope: Optional[int] = 0,
- ) -> List[Union[str, Message]]:
- """
- 说明:
- 获取指定问题所有回答
+ index: int | None = None,
+ group_id: str | None = None,
+ word_scope: int | None = 0,
+ ) -> tuple[str, list[UniMessage]]:
+ """获取指定问题所有回答
+
参数:
- :param problem: 问题
- :param index: 下标
- :param group_id: 群号
- :param word_scope: 词条范围
+ problem: 问题
+ index: 下标
+ group_id: 群号
+ word_scope: 词条范围
+
+ 返回:
+ tuple[str, list[UniMessage]]: 问题和所有回答
"""
if index is not None:
+ # TODO: group_by和order_by不能同时使用
if group_id:
- problem_ = (await cls.filter(group_id=group_id).all())[index]
+ _problem = (
+ await cls.filter(group_id=group_id).order_by("create_time")
+ # .group_by("problem")
+ .values_list("problem", flat=True)
+ )
else:
- problem_ = (await cls.filter(word_scope=(word_scope or 0)).all())[index]
- problem = problem_.problem
- answer = cls.filter(problem=problem)
+ _problem = (
+ await cls.filter(word_scope=(word_scope or 0)).order_by(
+ "create_time"
+ )
+ # .group_by("problem")
+ .values_list("problem", flat=True)
+ )
+ # if index is None and problem not in _problem:
+ # return "词条不存在...", []
+ sort_problem = []
+ for p in _problem:
+ if p not in sort_problem:
+ sort_problem.append(p)
+ if index > len(sort_problem) - 1:
+ return "下标错误,必须小于问题数量...", []
+ problem = sort_problem[index] # type: ignore
+ f = cls.filter(problem=problem, word_scope=(word_scope or 0))
if group_id:
- answer = answer.filter(group_id=group_id)
- return [await cls._format2answer("", "", 0, 0, x) for x in (await answer.all())]
+ f = f.filter(group_id=group_id)
+ answer_list = await f.all()
+ if not answer_list:
+ return "词条不存在...", []
+ return problem, [await cls._format2answer("", "", 0, 0, a) for a in answer_list]
@classmethod
async def delete_group_problem(
cls,
problem: str,
- group_id: Optional[str],
- index: Optional[int] = None,
+ group_id: str | None,
+ index: int | None = None,
word_scope: int = 1,
):
- """
- 说明:
- 删除指定问题全部或指定回答
+ """删除指定问题全部或指定回答
+
参数:
- :param problem: 问题文本
- :param group_id: 群号
- :param index: 回答下标
- :param word_scope: 词条范围
+ problem: 问题文本
+ group_id: 群号
+ index: 回答下标
+ word_scope: 词条范围
"""
if await cls.exists(None, group_id, problem, None, word_scope):
if index is not None:
if group_id:
- query = await cls.filter(group_id=group_id, problem=problem).all()
+ query = await cls.filter(
+ group_id=group_id, problem=problem, word_scope=word_scope
+ ).all()
else:
- query = await cls.filter(word_scope=0, problem=problem).all()
+ query = await cls.filter(
+ word_scope=word_scope, problem=problem
+ ).all()
await query[index].delete()
else:
if group_id:
- await WordBank.filter(group_id=group_id, problem=problem).delete()
+ await WordBank.filter(
+ group_id=group_id, problem=problem, word_scope=word_scope
+ ).delete()
else:
await WordBank.filter(
word_scope=word_scope, problem=problem
@@ -386,27 +424,31 @@ class WordBank(Model):
cls,
problem: str,
replace_str: str,
- group_id: Optional[str],
- index: Optional[int] = None,
+ group_id: str | None,
+ index: int | None = None,
word_scope: int = 1,
- ):
- """
- 说明:
- 修改词条问题
+ ) -> str:
+ """修改词条问题
+
参数:
- :param problem: 问题
- :param replace_str: 替换问题
- :param group_id: 群号
- :param index: 下标
- :param word_scope: 词条范围
+ problem: 问题
+ replace_str: 替换问题
+ group_id: 群号
+ index: 问题下标
+ word_scope: 词条范围
+
+ 返回:
+ str: 修改前的问题
"""
if index is not None:
if group_id:
query = await cls.filter(group_id=group_id, problem=problem).all()
else:
query = await cls.filter(word_scope=word_scope, problem=problem).all()
+ tmp = query[index].problem
query[index].problem = replace_str
await query[index].save(update_fields=["problem"])
+ return tmp
else:
if group_id:
await cls.filter(group_id=group_id, problem=problem).update(
@@ -416,85 +458,80 @@ class WordBank(Model):
await cls.filter(word_scope=word_scope, problem=problem).update(
problem=replace_str
)
+ return problem
@classmethod
- async def get_group_all_problem(
- cls, group_id: str
- ) -> List[Tuple[Any, Union[MessageSegment, str]]]:
- """
- 说明:
- 获取群聊所有词条
+ async def get_group_all_problem(cls, group_id: str) -> list[tuple[Any | str]]:
+ """获取群聊所有词条
+
参数:
- :param group_id: 群号
+ group_id: 群号
"""
return cls._handle_problem(
- await cls.filter(group_id=group_id).all() # type: ignore
+ await cls.filter(group_id=group_id).order_by("create_time").all() # type: ignore
)
@classmethod
async def get_problem_by_scope(cls, word_scope: int):
- """
- 说明:
- 通过词条范围获取词条
+ """通过词条范围获取词条
+
参数:
- :param word_scope: 词条范围
+ word_scope: 词条范围
"""
return cls._handle_problem(
- await cls.filter(word_scope=word_scope).all() # type: ignore
+ await cls.filter(word_scope=word_scope).order_by("create_time").all() # type: ignore
)
@classmethod
async def get_problem_by_type(cls, word_type: int):
- """
- 说明:
- 通过词条类型获取词条
+ """通过词条类型获取词条
+
参数:
- :param word_type: 词条类型
+ word_type: 词条类型
"""
return cls._handle_problem(
- await cls.filter(word_type=word_type).all() # type: ignore
+ await cls.filter(word_type=word_type).order_by("create_time").all() # type: ignore
)
@classmethod
- def _handle_problem(cls, msg_list: List["WordBank"]):
- """
- 说明:
- 格式化处理问题
+ def _handle_problem(cls, problem_list: list["WordBank"]):
+ """格式化处理问题
+
参数:
- :param msg_list: 消息列表
+ msg_list: 消息列表
"""
_tmp = []
- problem_list = []
- for q in msg_list:
+ result_list = []
+ for q in problem_list:
if q.problem not in _tmp:
+ # TODO: 获取收录人名称
problem = (
- q.problem,
- image(path / q.image_path)
- if q.image_path
- else f"[{int2type[q.word_type]}] " + q.problem,
+ (path / q.image_path, 30, 30) if q.image_path else q.problem,
+ int2type[q.word_type],
+ # q.author,
+ "-",
)
- problem_list.append(problem)
+ result_list.append(problem)
_tmp.append(q.problem)
- return problem_list
+ return result_list
@classmethod
async def _move(
cls,
user_id: str,
- group_id: Optional[str],
- problem: Union[str, Message],
- answer: Union[str, Message],
+ group_id: str | None,
+ problem: str,
+ answer: str,
placeholder: str,
):
- """
- 说明:
- 旧词条图片移动方法
+ """旧词条图片移动方法
+
参数:
- :param user_id: 用户id
- :param group_id: 群号
- :param problem: 问题
- :param answer: 回答
- :param placeholder: 占位符
+ user_id: 用户id
+ group_id: 群号
+ problem: 问题
+ answer: 回答
+ placeholder: 占位符
"""
word_scope = 0
word_type = 0
@@ -525,4 +562,6 @@ class WordBank(Model):
"ALTER TABLE word_bank2 RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id
"ALTER TABLE word_bank2 ALTER COLUMN user_id TYPE character varying(255);",
"ALTER TABLE word_bank2 ALTER COLUMN group_id TYPE character varying(255);",
+ "ALTER TABLE word_bank2 ADD platform varchar(255) DEFAULT 'qq';",
+ "ALTER TABLE word_bank2 ADD author varchar(255) DEFAULT '';",
]
diff --git a/zhenxun/plugins/word_bank/_rule.py b/zhenxun/plugins/word_bank/_rule.py
new file mode 100644
index 00000000..3ce032ca
--- /dev/null
+++ b/zhenxun/plugins/word_bank/_rule.py
@@ -0,0 +1,58 @@
+from io import BytesIO
+
+import imagehash
+from nonebot.adapters import Bot, Event
+from nonebot.typing import T_State
+from nonebot_plugin_alconna import At as alcAt
+from nonebot_plugin_alconna import Text as alcText
+from nonebot_plugin_alconna import UniMsg
+from nonebot_plugin_session import EventSession
+from PIL import Image
+
+from zhenxun.services.log import logger
+from zhenxun.utils.http_utils import AsyncHttpx
+
+from ._data_source import get_img_and_at_list
+from ._model import WordBank
+
+
+async def check(
+ bot: Bot,
+ event: Event,
+ message: UniMsg,
+ session: EventSession,
+ state: T_State,
+) -> bool:
+ text = message.extract_plain_text().strip()
+ img_list, at_list = get_img_and_at_list(message)
+ problem = text
+ if not text and len(img_list) == 1:
+ try:
+ r = await AsyncHttpx.get(img_list[0])
+ problem = str(imagehash.average_hash(Image.open(BytesIO(r.content))))
+ except Exception as e:
+ logger.warning(f"获取图片失败", "词条检测", session=session, e=e)
+ if at_list:
+ temp = ""
+ # TODO: 支持更多消息类型
+ for msg in message:
+ if isinstance(msg, alcAt):
+ temp += f"[at:{msg.target}]"
+ elif isinstance(msg, alcText):
+ temp += msg.text
+ problem = temp
+ if event.is_tome() and bot.config.nickname:
+ if isinstance(message[0], alcAt) and message[0].target == bot.self_id:
+ problem = f"[at:{bot.self_id}]" + problem
+ else:
+ if problem and bot.config.nickname:
+ nickname = [
+ nk for nk in bot.config.nickname if str(message).startswith(nk)
+ ]
+ problem = nickname[0] + problem if nickname else problem
+ if problem and (
+ await WordBank.check_problem(session.id3 or session.id2, problem) is not None
+ ):
+ state["problem"] = problem # type: ignore
+ return True
+ return False
diff --git a/zhenxun/plugins/word_bank/command.py b/zhenxun/plugins/word_bank/command.py
new file mode 100644
index 00000000..64049d30
--- /dev/null
+++ b/zhenxun/plugins/word_bank/command.py
@@ -0,0 +1,47 @@
+from nonebot import on_regex
+from nonebot_plugin_alconna import Alconna, Args, Option, on_alconna, store_true
+
+from zhenxun.utils.rules import admin_check, ensure_group
+
+_add_matcher = on_regex(
+ r"^(全局|私聊)?添加词条\s*?(模糊|正则|图片)?问\s*?(\S*\s?\S*)\s*?答\s?(\S*)",
+ priority=5,
+ block=True,
+ rule=admin_check("word_bank", "WORD_BANK_LEVEL"),
+)
+
+
+_del_matcher = on_alconna(
+ Alconna(
+ "删除词条",
+ Args["problem?", str],
+ Option("--all", action=store_true, help_text="所有词条"),
+ Option("--id", Args["index", int], help_text="下标id"),
+ Option("--aid", Args["answer_id", int], help_text="回答下标id"),
+ ),
+ priority=5,
+ block=True,
+)
+
+
+_update_matcher = on_alconna(
+ Alconna(
+ "修改词条",
+ Args["replace", str]["problem?", str],
+ Option("--id", Args["index", int], help_text="词条id"),
+ Option("--all", action=store_true, help_text="全局词条"),
+ )
+)
+
+_show_matcher = on_alconna(
+ Alconna(
+ "显示词条",
+ Args["problem?", str],
+ Option("-g|--group", Args["gid", str], help_text="群组id"),
+ Option("--id", Args["index", int], help_text="词条id"),
+ Option("--all", action=store_true, help_text="全局词条"),
+ ),
+ aliases={"查看词条"},
+ priority=5,
+ block=True,
+)
diff --git a/zhenxun/plugins/word_bank/message_handle.py b/zhenxun/plugins/word_bank/message_handle.py
new file mode 100644
index 00000000..a9bf624a
--- /dev/null
+++ b/zhenxun/plugins/word_bank/message_handle.py
@@ -0,0 +1,31 @@
+from nonebot import on_message
+from nonebot.plugin import PluginMetadata
+from nonebot.typing import T_State
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.utils import PluginExtraData
+from zhenxun.services import logger
+from zhenxun.utils.enum import PluginType
+
+from ._model import WordBank
+from ._rule import check
+
+__plugin_meta__ = PluginMetadata(
+ name="词库问答回复操作",
+ description="",
+ usage="""""",
+ extra=PluginExtraData(
+ author="HibiKier", version="0.1", plugin_type=PluginType.HIDDEN
+ ).dict(),
+)
+
+_matcher = on_message(priority=6, block=True, rule=check)
+
+
+@_matcher.handle()
+async def _(session: EventSession, state: T_State):
+ if problem := state.get("problem"):
+ gid = session.id3 or session.id2
+ if result := await WordBank.get_answer(gid, problem):
+ await result.send()
+ logger.info(f" 触发词条 {problem}", "词条检测", session=session)
diff --git a/zhenxun/plugins/word_bank/word_handle.py b/zhenxun/plugins/word_bank/word_handle.py
new file mode 100644
index 00000000..0f70a44a
--- /dev/null
+++ b/zhenxun/plugins/word_bank/word_handle.py
@@ -0,0 +1,325 @@
+import re
+from typing import Any
+
+from nonebot.adapters import Bot
+from nonebot.adapters.onebot.v11 import unescape
+from nonebot.exception import FinishedException
+from nonebot.internal.params import Arg, ArgStr
+from nonebot.params import RegexGroup
+from nonebot.plugin import PluginMetadata
+from nonebot.typing import T_State
+from nonebot_plugin_alconna import AlconnaQuery, Arparma
+from nonebot_plugin_alconna import Image
+from nonebot_plugin_alconna import Image as alcImage
+from nonebot_plugin_alconna import Match, Query, UniMsg
+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.message import MessageUtils
+
+from ._config import scope2int, type2int
+from ._data_source import WordBankManage, get_answer, get_img_and_at_list, get_problem
+from ._model import WordBank
+from .command import _add_matcher, _del_matcher, _show_matcher, _update_matcher
+
+base_config = Config.get("word_bank")
+
+
+__plugin_meta__ = PluginMetadata(
+ name="词库问答",
+ description="自定义词条内容随机回复",
+ usage="""
+ usage:
+ 对指定问题的随机回答,对相同问题可以设置多个不同回答
+ 删除词条后每个词条的id可能会变化,请查看后再删除
+ 更推荐使用id方式删除
+ 问题回答支持的类型:at, image
+ 查看词条命令:群聊时为 群词条+全局词条,私聊时为 私聊词条+全局词条
+ 添加词条正则:添加词条(模糊|正则|图片)?问\s*?(\S*\s?\S*)\s*?答\s?(\S*)
+ 正则问可以通过$1类推()捕获的组
+ 指令:
+ 添加词条 ?[模糊|正则|图片]问...答...:添加问答词条,可重复添加相同问题的不同回答
+ 示例:
+ 添加词条问你好答你也好
+ 添加图片词条问答看看涩图
+ 删除词条 ?[问题] ?[序号] ?[回答序号]:删除指定词条指定或全部回答
+ 示例:
+ 删除词条 谁是萝莉 : 删除文字是 谁是萝莉 的词条
+ 删除词条 --id 2 : 删除序号为2的词条
+ 删除词条 谁是萝莉 --aid 2 : 删除 谁是萝莉 词条的第2个回答
+ 删除词条 --id 2 --aid 2 : 删除序号为2词条的第2个回答
+ 修改词条 [替换文字] ?[旧词条文字] ?[序号]:修改词条问题
+ 示例:
+ 修改词条 谁是萝莉 谁是萝莉啊? : 将词条 谁是萝莉 修改为 谁是萝莉啊?
+ 修改词条 谁是萝莉 --id 2 : 将序号为2的词条修改为 谁是萝莉
+ 查看词条 ?[问题] ?[序号]:查看全部词条或对应词条回答
+ 示例:
+ 查看词条:
+ (在群组中使用时): 查看当前群组词条和全局词条
+ (在私聊中使用时): 查看当前私聊词条和全局词条
+ 查看词条 谁是萝莉 : 查看词条 谁是萝莉 的全部回答
+ 查看词条 --id 2 : 查看词条序号为2的全部回答
+ 查看词条 谁是萝莉 --all: 查看全局词条 谁是萝莉 的全部回答
+ 查看词条 --id 2 --all: 查看全局词条序号为2的全部回答
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier & yajiwa",
+ version="0.1",
+ superuser_help="""
+ 在私聊中超级用户额外设置
+ 指令:
+ (全局|私聊)?添加词条\s*?(模糊|正则|图片)?问\s*?(\S*\s?\S*)\s*?答\s?(\S*):添加问答词条,可重复添加相同问题的不同回答
+ 全局添加词条
+ 私聊添加词条
+ (私聊情况下)删除词条: 删除私聊词条
+ (私聊情况下)修改词条: 修改私聊词条
+ 通过添加参数 --all才指定全局词条
+ 示例:
+ 删除词条 --id 2 --all: 删除全局词条中序号为2的词条
+ 用法与普通用法相同
+ """,
+ admin_level=base_config.get("WORD_BANK_LEVEL"),
+ ).dict(),
+)
+
+
+@_add_matcher.handle()
+async def _(
+ bot: Bot,
+ session: EventSession,
+ state: T_State,
+ message: UniMsg,
+ reg_group: tuple[Any, ...] = RegexGroup(),
+):
+ img_list, at_list = get_img_and_at_list(message)
+ user_id = session.id1
+ group_id = session.id3 or session.id2
+ if not group_id and user_id not in bot.config.superusers:
+ await MessageUtils.build_message("权限不足捏...").finish(reply_to=True)
+ word_scope, word_type, problem, answer = reg_group
+ if not word_scope and not group_id:
+ word_scope = "私聊"
+ if (
+ word_scope
+ and word_scope in ["全局", "私聊"]
+ and user_id not in bot.config.superusers
+ ):
+ await MessageUtils.build_message("权限不足,无法添加该范围词条...").finish(
+ reply_to=True
+ )
+ if (not problem or not problem.strip()) and word_type != "图片":
+ await MessageUtils.build_message("词条问题不能为空!").finish(reply_to=True)
+ if (not answer or not answer.strip()) and not len(img_list) and not len(at_list):
+ await MessageUtils.build_message("词条回答不能为空!").finish(reply_to=True)
+ if word_type != "图片":
+ state["problem_image"] = "YES"
+ temp_problem = message.copy()
+ # answer = message.copy()
+ # 对at问题对额外处理
+ # if at_list:
+ answer = get_answer(message.copy())
+ # text = str(message.pop(0)).split("答", maxsplit=1)[-1].strip()
+ # temp_problem.insert(0, alcMessageUtils.build_message(text))
+ state["word_scope"] = word_scope
+ state["word_type"] = word_type
+ state["problem"] = get_problem(temp_problem)
+ state["answer"] = answer
+ logger.info(
+ f"添加词条 范围: {word_scope} 类型: {word_type} 问题: {problem} 回答: {answer}",
+ "添加词条",
+ session=session,
+ )
+
+
+@_add_matcher.got("problem_image", prompt="请发送该回答设置的问题图片")
+async def _(
+ bot: Bot,
+ session: EventSession,
+ message: UniMsg,
+ word_scope: str | None = ArgStr("word_scope"),
+ word_type: str | None = ArgStr("word_type"),
+ problem: str | None = ArgStr("problem"),
+ answer: Any = Arg("answer"),
+):
+ if not session.id1:
+ await MessageUtils.build_message("用户id不存在...").finish()
+ user_id = session.id1
+ group_id = session.id3 or session.id2
+ try:
+ if word_type == "图片":
+ problem = [m for m in message if isinstance(m, alcImage)][0].url
+ elif word_type == "正则" and problem:
+ problem = unescape(problem)
+ try:
+ re.compile(problem)
+ except re.error:
+ await MessageUtils.build_message(
+ f"添加词条失败,正则表达式 {problem} 非法!"
+ ).finish(reply_to=True)
+ # if str(event.user_id) in bot.config.superusers and isinstance(event, PrivateMessageEvent):
+ # word_scope = "私聊"
+ nickname = None
+ if problem and bot.config.nickname:
+ nickname = [nk for nk in bot.config.nickname if problem.startswith(nk)]
+ if not problem:
+ await MessageUtils.build_message("获取问题失败...").finish(reply_to=True)
+ await WordBank.add_problem_answer(
+ user_id,
+ (
+ group_id
+ if group_id and (not word_scope or word_scope == "私聊")
+ else "0"
+ ),
+ scope2int[word_scope] if word_scope else 1,
+ type2int[word_type] if word_type else 0,
+ problem,
+ answer,
+ nickname[0] if nickname else None,
+ session.platform,
+ session.id1,
+ )
+ except Exception as e:
+ if isinstance(e, FinishedException):
+ await _add_matcher.finish()
+ logger.error(
+ f"添加词条 {problem} 错误...",
+ "添加词条",
+ session=session,
+ e=e,
+ )
+ await MessageUtils.build_message(
+ f"添加词条 {problem if word_type != '图片' else '图片'} 发生错误!"
+ ).finish(reply_to=True)
+ if word_type == "图片":
+ result = MessageUtils.build_message(
+ ["添加词条 ", Image(url=problem), " 成功!"]
+ )
+ else:
+ result = MessageUtils.build_message(f"添加词条 {problem} 成功!")
+ await result.send()
+ logger.info(
+ f"添加词条 {problem} 成功!",
+ "添加词条",
+ session=session,
+ )
+
+
+@_del_matcher.handle()
+async def _(
+ bot: Bot,
+ session: EventSession,
+ problem: Match[str],
+ index: Match[int],
+ answer_id: Match[int],
+ arparma: Arparma,
+ all: Query[bool] = AlconnaQuery("all.value", False),
+):
+ if not problem.available and not index.available:
+ await MessageUtils.build_message(
+ "此命令之后需要跟随指定词条或id,通过“显示词条“查看"
+ ).finish(reply_to=True)
+ word_scope = 1 if session.id3 or session.id2 else 2
+ if all.result:
+ word_scope = 0
+ if gid := session.id3 or session.id2:
+ result, _ = await WordBankManage.delete_word(
+ problem.result,
+ index.result if index.available else None,
+ answer_id.result if answer_id.available else None,
+ gid,
+ word_scope,
+ )
+ else:
+ if session.id1 not in bot.config.superusers:
+ await MessageUtils.build_message("权限不足捏...").finish(reply_to=True)
+ result, _ = await WordBankManage.delete_word(
+ problem.result,
+ index.result if index.available else None,
+ answer_id.result if answer_id.available else None,
+ None,
+ word_scope,
+ )
+ await MessageUtils.build_message(result).send(reply_to=True)
+ logger.info(f"删除词条: {problem.result}", arparma.header_result, session=session)
+
+
+@_update_matcher.handle()
+async def _(
+ bot: Bot,
+ session: EventSession,
+ replace: str,
+ problem: Match[str],
+ index: Match[int],
+ arparma: Arparma,
+ all: Query[bool] = AlconnaQuery("all.value", False),
+):
+ if not problem.available and not index.available:
+ await MessageUtils.build_message(
+ "此命令之后需要跟随指定词条或id,通过“显示词条“查看"
+ ).finish(reply_to=True)
+ word_scope = 1 if session.id3 or session.id2 else 2
+ if all.result:
+ word_scope = 0
+ if gid := session.id3 or session.id2:
+ result, old_problem = await WordBankManage.update_word(
+ replace,
+ problem.result if problem.available else "",
+ index.result if index.available else None,
+ gid,
+ word_scope,
+ )
+ else:
+ if session.id1 not in bot.config.superusers:
+ await MessageUtils.build_message("权限不足捏...").finish(reply_to=True)
+ result, old_problem = await WordBankManage.update_word(
+ replace,
+ problem.result if problem.available else "",
+ index.result if index.available else None,
+ session.id3 or session.id2,
+ word_scope,
+ )
+ await MessageUtils.build_message(result).send(reply_to=True)
+ logger.info(
+ f"更新词条词条: {old_problem} -> {replace}",
+ arparma.header_result,
+ session=session,
+ )
+
+
+@_show_matcher.handle()
+async def _(
+ session: EventSession,
+ problem: Match[str],
+ index: Match[int],
+ gid: Match[str],
+ arparma: Arparma,
+ all: Query[bool] = AlconnaQuery("all.value", False),
+):
+ word_scope = 1 if session.id3 or session.id2 else 2
+ if all.result:
+ word_scope = 0
+ group_id = session.id3 or session.id2
+ if gid.available:
+ group_id = gid.result
+ if problem.available:
+ if index.available:
+ if index.result < 0 or index.result > len(
+ await WordBank.get_problem_by_scope(2)
+ ):
+ await MessageUtils.build_message("id必须在范围内...").finish(
+ reply_to=True
+ )
+ result = await WordBankManage.show_word(
+ problem.result,
+ index.result if index.available else None,
+ group_id,
+ word_scope,
+ )
+ else:
+ result = await WordBankManage.show_word(
+ None, index.result if index.available else None, group_id, word_scope
+ )
+ await result.send()
+ logger.info(f"查看词条回答: {problem}", arparma.header_result, session=session)
diff --git a/services/__init__.py b/zhenxun/services/__init__.py
old mode 100755
new mode 100644
similarity index 95%
rename from services/__init__.py
rename to zhenxun/services/__init__.py
index 8bbd77c0..aae2c093
--- a/services/__init__.py
+++ b/zhenxun/services/__init__.py
@@ -1,2 +1,2 @@
-from .db_context import *
-from .log import *
+from .db_context import *
+from .log import *
diff --git a/zhenxun/services/db_context.py b/zhenxun/services/db_context.py
new file mode 100644
index 00000000..2bf9c109
--- /dev/null
+++ b/zhenxun/services/db_context.py
@@ -0,0 +1,118 @@
+import ujson as json
+from nonebot.utils import is_coroutine_callable
+from tortoise import Tortoise, fields
+from tortoise.connection import connections
+from tortoise.models import Model as Model_
+
+from zhenxun.configs.config import (
+ address,
+ bind,
+ database,
+ password,
+ port,
+ sql_name,
+ user,
+)
+from zhenxun.configs.path_config import DATA_PATH
+
+from .log import logger
+
+MODELS: list[str] = []
+
+SCRIPT_METHOD = []
+
+DATABASE_SETTING_FILE = DATA_PATH / "database.json"
+
+
+class Model(Model_):
+ """
+ 自动添加模块
+
+ Args:
+ Model_ (_type_): _description_
+ """
+
+ def __init_subclass__(cls, **kwargs):
+ MODELS.append(cls.__module__)
+
+ if func := getattr(cls, "_run_script", None):
+ SCRIPT_METHOD.append((cls.__module__, func))
+
+
+class TestSQL(Model):
+ id = fields.IntField(pk=True, generated=True, auto_increment=True)
+ """自增id"""
+
+ class Meta:
+ abstract = True
+ table = "test_sql"
+ table_description = "执行SQL命令,不记录任何数据"
+
+
+async def init():
+ if DATABASE_SETTING_FILE.exists():
+ with open(DATABASE_SETTING_FILE, "r", encoding="utf-8") as f:
+ setting_data = json.load(f)
+ else:
+ i_bind = bind
+ if not i_bind and any([user, password, address, port, database]):
+ i_bind = f"{sql_name}://{user}:{password}@{address}:{port}/{database}"
+ setting_data = {
+ "bind": i_bind,
+ "sql_name": sql_name,
+ "user": user,
+ "password": password,
+ "address": address,
+ "port": port,
+ "database": database,
+ }
+ with open(DATABASE_SETTING_FILE, "w", encoding="utf-8") as f:
+ json.dump(setting_data, f, ensure_ascii=False, indent=4)
+ i_bind = setting_data.get("bind")
+ _sql_name = setting_data.get("sql_name")
+ _user = setting_data.get("user")
+ _password = setting_data.get("password")
+ _address = setting_data.get("address")
+ _port = setting_data.get("port")
+ _database = setting_data.get("database")
+ if not i_bind and not any([_user, _password, _address, _port, _database]):
+ raise ValueError("\n数据库配置未填写...")
+ if not i_bind:
+ i_bind = f"{_sql_name}://{_user}:{_password}@{_address}:{_port}/{_database}"
+ try:
+ await Tortoise.init(
+ db_url=i_bind, modules={"models": MODELS}, timezone="Asia/Shanghai"
+ )
+ if SCRIPT_METHOD:
+ db = Tortoise.get_connection("default")
+ logger.debug(
+ f"即将运行SCRIPT_METHOD方法, 合计 {len(SCRIPT_METHOD)} 个..."
+ )
+ sql_list = []
+ for module, func in SCRIPT_METHOD:
+ try:
+ if is_coroutine_callable(func):
+ sql = await func()
+ else:
+ sql = func()
+ if sql:
+ sql_list += sql
+ except Exception as e:
+ logger.debug(f"{module} 执行SCRIPT_METHOD方法出错...", e=e)
+ for sql in sql_list:
+ logger.debug(f"执行SQL: {sql}")
+ try:
+ await db.execute_query_dict(sql)
+ # await TestSQL.raw(sql)
+ except Exception as e:
+ logger.debug(f"执行SQL: {sql} 错误...", e=e)
+ if sql_list:
+ logger.debug("SCRIPT_METHOD方法执行完毕!")
+ await Tortoise.generate_schemas()
+ logger.info(f"Database loaded successfully!")
+ except Exception as e:
+ raise Exception(f"数据库连接错误... e:{e}")
+
+
+async def disconnect():
+ await connections.close_all()
diff --git a/zhenxun/services/log.py b/zhenxun/services/log.py
new file mode 100644
index 00000000..a2b5e07a
--- /dev/null
+++ b/zhenxun/services/log.py
@@ -0,0 +1,340 @@
+from datetime import datetime, timedelta
+from typing import Any, Dict, overload
+
+from nonebot import require
+
+require("nonebot_plugin_session")
+from loguru import logger as logger_
+from nonebot.log import default_filter, default_format
+from nonebot_plugin_session import Session
+
+from zhenxun.configs.path_config import LOG_PATH
+
+logger_.add(
+ LOG_PATH / f"{datetime.now().date()}.log",
+ level="INFO",
+ rotation="00:00",
+ format=default_format,
+ filter=default_filter,
+ retention=timedelta(days=30),
+)
+
+logger_.add(
+ LOG_PATH / f"error_{datetime.now().date()}.log",
+ level="ERROR",
+ rotation="00:00",
+ format=default_format,
+ filter=default_filter,
+ retention=timedelta(days=30),
+)
+
+
+class logger:
+ TEMPLATE_A = "Adapter[{}] {}"
+ TEMPLATE_B = "Adapter[{}] [{}]: {}"
+ TEMPLATE_C = "Adapter[{}] 用户[{}] 触发 [{}]: {}"
+ TEMPLATE_D = "Adapter[{}] 群聊[{}] 用户[{}] 触发 [{}]: {}"
+ TEMPLATE_E = "Adapter[{}] 群聊[{}] 用户[{}] 触发 [{}] [Target]({}): {}"
+
+ TEMPLATE_ADAPTER = "Adapter[{}] "
+ TEMPLATE_USER = "用户[{}] "
+ TEMPLATE_GROUP = "群聊[{}] "
+ TEMPLATE_COMMAND = "CMD[{}] "
+ TEMPLATE_PLATFORM = "平台[{}] "
+ TEMPLATE_TARGET = "[Target]([{}]) "
+
+ SUCCESS_TEMPLATE = "[{}]: {} | 参数[{}] 返回: [{}]"
+
+ WARNING_TEMPLATE = "[{}]: {}"
+
+ ERROR_TEMPLATE = "[{}]: {}"
+
+ @overload
+ @classmethod
+ def info(
+ cls,
+ info: str,
+ command: str | None = None,
+ *,
+ session: int | str | None = None,
+ group_id: int | str | None = None,
+ adapter: str | None = None,
+ target: Any = None,
+ platform: str | None = None,
+ ): ...
+
+ @overload
+ @classmethod
+ def info(
+ cls,
+ info: str,
+ command: str | None = None,
+ *,
+ session: Session | None = None,
+ target: Any = None,
+ platform: str | None = None,
+ ): ...
+
+ @classmethod
+ def info(
+ cls,
+ info: str,
+ command: str | None = None,
+ *,
+ session: int | str | Session | None = None,
+ group_id: int | str | None = None,
+ adapter: str | None = None,
+ target: Any = None,
+ platform: str | None = None,
+ ):
+ user_id: str | None = session # type: ignore
+ group_id = None
+ if type(session) == Session:
+ user_id = session.id1
+ adapter = session.bot_type
+ if session.id3:
+ group_id = f"{session.id3}:{session.id2}"
+ elif session.id2:
+ group_id = f"{session.id2}"
+ platform = platform or session.platform
+ template = cls.__parser_template(
+ info, command, user_id, group_id, adapter, target, platform
+ )
+ try:
+ logger_.opt(colors=True).info(template)
+ except Exception as e:
+ logger_.info(template)
+
+ @classmethod
+ def success(
+ cls,
+ info: str,
+ command: str,
+ param: Dict[str, Any] | None = None,
+ result: str = "",
+ ):
+ param_str = ""
+ if param:
+ param_str = ",".join([f"{k}:{v}" for k, v in param.items()])
+ logger_.opt(colors=True).success(
+ cls.SUCCESS_TEMPLATE.format(command, info, param_str, result)
+ )
+
+ @overload
+ @classmethod
+ def warning(
+ cls,
+ info: str,
+ command: str | None = None,
+ *,
+ session: int | str | None = None,
+ group_id: int | str | None = None,
+ adapter: str | None = None,
+ target: Any = None,
+ platform: str | None = None,
+ e: Exception | None = None,
+ ): ...
+
+ @overload
+ @classmethod
+ def warning(
+ cls,
+ info: str,
+ command: str | None = None,
+ *,
+ session: Session | None = None,
+ adapter: str | None = None,
+ target: Any = None,
+ platform: str | None = None,
+ e: Exception | None = None,
+ ): ...
+
+ @classmethod
+ def warning(
+ cls,
+ info: str,
+ command: str | None = None,
+ *,
+ session: int | str | Session | None = None,
+ group_id: int | str | None = None,
+ adapter: str | None = None,
+ target: Any = None,
+ platform: str | None = None,
+ e: Exception | None = None,
+ ):
+ user_id: str | None = session # type: ignore
+ group_id = None
+ if type(session) == Session:
+ user_id = session.id1
+ adapter = session.bot_type
+ if session.id3:
+ group_id = f"{session.id3}:{session.id2}"
+ elif session.id2:
+ group_id = f"{session.id2}"
+ platform = platform or session.platform
+ template = cls.__parser_template(
+ info, command, user_id, group_id, adapter, target, platform
+ )
+ if e:
+ template += f" || 错误{type(e)}: {e}"
+ try:
+ logger_.opt(colors=True).warning(template)
+ except Exception as e:
+ logger_.warning(template)
+
+ @overload
+ @classmethod
+ def error(
+ cls,
+ info: str,
+ command: str | None = None,
+ *,
+ session: int | str | None = None,
+ group_id: int | str | None = None,
+ adapter: str | None = None,
+ target: Any = None,
+ platform: str | None = None,
+ e: Exception | None = None,
+ ): ...
+
+ @overload
+ @classmethod
+ def error(
+ cls,
+ info: str,
+ command: str | None = None,
+ *,
+ session: Session | None = None,
+ target: Any = None,
+ platform: str | None = None,
+ e: Exception | None = None,
+ ): ...
+
+ @classmethod
+ def error(
+ cls,
+ info: str,
+ command: str | None = None,
+ *,
+ session: int | str | Session | None = None,
+ group_id: int | str | None = None,
+ adapter: str | None = None,
+ target: Any = None,
+ platform: str | None = None,
+ e: Exception | None = None,
+ ):
+ user_id: str | None = session # type: ignore
+ group_id = None
+ if type(session) == Session:
+ user_id = session.id1
+ adapter = session.bot_type
+ if session.id3:
+ group_id = f"{session.id3}:{session.id2}"
+ elif session.id2:
+ group_id = f"{session.id2}"
+ platform = platform or session.platform
+ template = cls.__parser_template(
+ info, command, user_id, group_id, adapter, target, platform
+ )
+ if e:
+ template += f" || 错误 {type(e)}: {e}"
+ try:
+ logger_.opt(colors=True).error(template)
+ except Exception as e:
+ logger_.error(template)
+
+ @overload
+ @classmethod
+ def debug(
+ cls,
+ info: str,
+ command: str | None = None,
+ *,
+ session: int | str | None = None,
+ group_id: int | str | None = None,
+ adapter: str | None = None,
+ target: Any = None,
+ platform: str | None = None,
+ e: Exception | None = None,
+ ): ...
+
+ @overload
+ @classmethod
+ def debug(
+ cls,
+ info: str,
+ command: str | None = None,
+ *,
+ session: Session | None = None,
+ target: Any = None,
+ platform: str | None = None,
+ e: Exception | None = None,
+ ): ...
+
+ @classmethod
+ def debug(
+ cls,
+ info: str,
+ command: str | None = None,
+ *,
+ session: int | str | Session | None = None,
+ group_id: int | str | None = None,
+ adapter: str | None = None,
+ target: Any = None,
+ platform: str | None = None,
+ e: Exception | None = None,
+ ):
+ user_id: str | None = session # type: ignore
+ group_id = None
+ if type(session) == Session:
+ user_id = session.id1
+ adapter = session.bot_type
+ if session.id3:
+ group_id = f"{session.id3}:{session.id2}"
+ elif session.id2:
+ group_id = f"{session.id2}"
+ platform = platform or session.platform
+ template = cls.__parser_template(
+ info, command, user_id, group_id, adapter, target, platform
+ )
+ if e:
+ template += f" || 错误 {type(e)}: {e}"
+ try:
+ logger_.opt(colors=True).debug(template)
+ except Exception as e:
+ logger_.debug(template)
+
+ @classmethod
+ def __parser_template(
+ cls,
+ info: str,
+ command: str | None = None,
+ user_id: int | str | None = None,
+ group_id: int | str | None = None,
+ adapter: str | None = None,
+ target: Any = None,
+ platform: str | None = None,
+ ) -> str:
+ arg_list = []
+ template = ""
+ if adapter is not None:
+ template += cls.TEMPLATE_ADAPTER
+ arg_list.append(adapter)
+ if platform is not None:
+ template += cls.TEMPLATE_PLATFORM
+ arg_list.append(platform)
+ if group_id is not None:
+ template += cls.TEMPLATE_GROUP
+ arg_list.append(group_id)
+ if user_id is not None:
+ template += cls.TEMPLATE_USER
+ arg_list.append(user_id)
+ if command is not None:
+ template += cls.TEMPLATE_COMMAND
+ arg_list.append(command)
+ if target is not None:
+ template += cls.TEMPLATE_TARGET
+ arg_list.append(target)
+ arg_list.append(info)
+ template += "{}"
+ return template.format(*arg_list)
diff --git a/zhenxun/utils/_build_image.py b/zhenxun/utils/_build_image.py
new file mode 100644
index 00000000..33666f7e
--- /dev/null
+++ b/zhenxun/utils/_build_image.py
@@ -0,0 +1,720 @@
+import base64
+import math
+import uuid
+from io import BytesIO
+from pathlib import Path
+from typing import Literal, Tuple, TypeAlias, overload
+
+from nonebot.utils import run_sync
+from PIL import Image, ImageDraw, ImageFilter, ImageFont
+from PIL.Image import Image as tImage
+from PIL.ImageFont import FreeTypeFont
+from typing_extensions import Self
+
+from zhenxun.configs.path_config import FONT_PATH
+
+ModeType = Literal[
+ "1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr"
+]
+"""图片类型"""
+
+ColorAlias: TypeAlias = str | Tuple[int, int, int] | Tuple[int, int, int, int] | None
+
+CenterType = Literal["center", "height", "width"]
+"""
+粘贴居中
+
+center: 水平垂直居中
+
+height: 垂直居中
+
+width: 水平居中
+"""
+
+
+class BuildImage:
+ """
+ 快捷生成图片与操作图片的工具类
+ """
+
+ def __init__(
+ self,
+ width: int = 0,
+ height: int = 0,
+ color: ColorAlias = (255, 255, 255),
+ mode: ModeType = "RGBA",
+ font: str | Path | FreeTypeFont = "HYWenHei-85W.ttf",
+ font_size: int = 20,
+ background: str | BytesIO | Path | None = None,
+ ) -> None:
+ self.uid = uuid.uuid1()
+ self.width = width
+ self.height = height
+ self.color = color
+ self.font = (
+ self.load_font(font, font_size)
+ if not isinstance(font, FreeTypeFont)
+ else font
+ )
+ if background:
+ self.markImg = Image.open(background)
+ if width and height:
+ self.markImg = self.markImg.resize((width, height), Image.LANCZOS)
+ else:
+ self.width = self.markImg.width
+ self.height = self.markImg.height
+ else:
+ if not width or not height:
+ raise ValueError("长度和宽度不能为空...")
+ self.markImg = Image.new(mode, (width, height), color) # type: ignore
+ self.draw = ImageDraw.Draw(self.markImg)
+
+ @property
+ def size(self) -> Tuple[int, int]:
+ return self.markImg.size
+
+ @classmethod
+ def open(cls, path: str | Path) -> Self:
+ """打开图片
+
+ 参数:
+ path: 图片路径
+
+ 返回:
+ Self: BuildImage
+ """
+ return cls(background=path)
+
+ @classmethod
+ async def build_text_image(
+ cls,
+ text: str,
+ font: str | FreeTypeFont | Path = "HYWenHei-85W.ttf",
+ size: int = 10,
+ font_color: str | Tuple[int, int, int] = (0, 0, 0),
+ color: ColorAlias = None,
+ padding: int | Tuple[int, int, int, int] | None = None,
+ ) -> Self:
+ """构建文本图片
+
+ 参数:
+ text: 文本
+ font: 字体路径
+ size: 字体大小
+ font_color: 字体颜色.
+ color: 背景颜色
+ padding: 外边距
+
+ 返回:
+ Self: Self
+ """
+ if not text.strip():
+ return cls(1, 1)
+ _font = None
+ if isinstance(font, FreeTypeFont):
+ _font = font
+ elif isinstance(font, (str, Path)):
+ _font = cls.load_font(font, size)
+ width, height = cls.get_text_size(text, _font)
+ if isinstance(padding, int):
+ width += padding * 2
+ height += padding * 2
+ elif isinstance(padding, tuple):
+ width += padding[1] + padding[3]
+ height += padding[0] + padding[2]
+ markImg = cls(width, height, color, font=_font)
+ await markImg.text(
+ (0, 0), text, fill=font_color, font=_font, center_type="center"
+ )
+ return markImg
+
+ @classmethod
+ async def auto_paste(
+ cls,
+ img_list: list[Self | tImage],
+ row: int,
+ space: int = 10,
+ padding: int = 50,
+ color: ColorAlias = (255, 255, 255),
+ background: str | BytesIO | Path | None = None,
+ ) -> Self:
+ """自动贴图
+
+ 参数:
+ img_list: 图片列表
+ row: 一行图片的数量
+ space: 图片之间的间距.
+ padding: 外边距.
+ color: 图片背景颜色.
+ background: 图片背景图片.
+
+ 返回:
+ Self: Self
+ """
+ if not img_list:
+ raise ValueError("贴图类别为空...")
+ width, height = img_list[0].size
+ background_width = width * row + space * (row - 1) + padding * 2
+ row_count = math.ceil(len(img_list) / row)
+ if row_count == 1:
+ background_width = (
+ sum([img.width for img in img_list]) + space * (row - 1) + padding * 2
+ )
+ background_height = height * row_count + space * (row_count - 1) + padding * 2
+ background_image = cls(
+ background_width, background_height, color=color, background=background
+ )
+ _cur_width, _cur_height = padding, padding
+ for img in img_list:
+ await background_image.paste(img, (_cur_width, _cur_height))
+ _cur_width += space + img.width
+ if _cur_width + padding >= background_image.width:
+ _cur_height += space + img.height
+ _cur_width = padding
+ return background_image
+
+ @classmethod
+ def load_font(
+ cls, font: str | Path = "HYWenHei-85W.ttf", font_size: int = 10
+ ) -> FreeTypeFont:
+ """加载字体
+
+ 参数:
+ font: 字体名称
+ font_size: 字体大小
+
+ 返回:
+ FreeTypeFont: 字体
+ """
+ path = FONT_PATH / font if type(font) == str else font
+ return ImageFont.truetype(str(path), font_size)
+
+ @overload
+ @classmethod
+ def get_text_size(
+ cls, text: str, font: FreeTypeFont | None = None
+ ) -> Tuple[int, int]: ...
+
+ @overload
+ @classmethod
+ def get_text_size(
+ cls, text: str, font: str | None = None, font_size: int = 10
+ ) -> Tuple[int, int]: ...
+
+ @classmethod
+ def get_text_size(
+ cls,
+ text: str,
+ font: str | FreeTypeFont | None = "HYWenHei-85W.ttf",
+ font_size: int = 10,
+ ) -> Tuple[int, int]:
+ """获取该字体下文本需要的长宽
+
+ 参数:
+ text: 文本内容
+ font: 字体名称或FreeTypeFont
+ font_size: 字体大小
+
+ 返回:
+ Tuple[int, int]: 长宽
+ """
+ _font = font
+ if font and type(font) == str:
+ _font = cls.load_font(font, font_size)
+ temp_image = Image.new("RGB", (1, 1), (255, 255, 255))
+ draw = ImageDraw.Draw(temp_image)
+ text_box = draw.textbbox((0, 0), str(text), font=_font) # type: ignore
+ text_width = text_box[2] - text_box[0]
+ text_height = text_box[3] - text_box[1]
+ return text_width, text_height + 10
+ # return _font.getsize(str(text)) # type: ignore
+
+ def getsize(self, msg: str) -> Tuple[int, int]:
+ """
+ 获取文字在该图片 font_size 下所需要的空间
+
+ 参数:
+ msg: 文本
+
+ 返回:
+ Tuple[int, int]: 长宽
+ """
+ temp_image = Image.new("RGB", (1, 1), (255, 255, 255))
+ draw = ImageDraw.Draw(temp_image)
+ text_box = draw.textbbox((0, 0), str(msg), font=self.font)
+ text_width = text_box[2] - text_box[0]
+ text_height = text_box[3] - text_box[1]
+ return text_width, text_height + 10
+ # return self.font.getsize(msg) # type: ignore
+
+ def __center_xy(
+ self,
+ pos: Tuple[int, int],
+ width: int,
+ height: int,
+ center_type: CenterType | None,
+ ) -> Tuple[int, int]:
+ """
+ 根据居中类型定位xy
+
+ 参数:
+ pos: 定位
+ image: image
+ center_type: 居中类型
+
+ 返回:
+ Tuple[int, int]: 定位
+ """
+ # _width, _height = pos
+ if self.width and self.height:
+ if center_type == "center":
+ width = int((self.width - width) / 2)
+ height = int((self.height - height) / 2)
+ elif center_type == "width":
+ width = int((self.width - width) / 2)
+ height = pos[1]
+ elif center_type == "height":
+ width = pos[0]
+ height = int((self.height - height) / 2)
+ return width, height
+
+ @run_sync
+ def paste(
+ self,
+ image: Self | tImage,
+ pos: Tuple[int, int] = (0, 0),
+ center_type: CenterType | None = None,
+ ) -> Self:
+ """贴图
+
+ 参数:
+ image: BuildImage 或 Image
+ pos: 定位.
+ center_type: 居中.
+
+ 返回:
+ BuildImage: Self
+
+ 异常:
+ ValueError: 居中类型错误
+ """
+ if center_type and center_type not in ["center", "height", "width"]:
+ raise ValueError("center_type must be 'center', 'width' or 'height'")
+ width, height = 0, 0
+ _image = image
+ if isinstance(image, BuildImage):
+ _image = image.markImg
+ if _image.width and _image.height and center_type:
+ pos = self.__center_xy(pos, _image.width, _image.height, center_type)
+ try:
+ self.markImg.paste(_image, pos, _image) # type: ignore
+ except ValueError:
+ self.markImg.paste(_image, pos) # type: ignore
+ return self
+
+ @run_sync
+ def point(
+ self, pos: Tuple[int, int], fill: Tuple[int, int, int] | None = None
+ ) -> Self:
+ """
+ 绘制多个或单独的像素
+
+ 参数:
+ pos: 坐标
+ fill: 填充颜色.
+
+ 返回:
+ BuildImage: Self
+ """
+ self.draw.point(pos, fill=fill)
+ return self
+
+ @run_sync
+ def ellipse(
+ self,
+ pos: Tuple[int, int, int, int],
+ fill: Tuple[int, int, int] | None = None,
+ outline: Tuple[int, int, int] | None = None,
+ width: int = 1,
+ ) -> Self:
+ """
+ 绘制圆
+
+ 参数:
+ pos: 坐标范围
+ fill: 填充颜色.
+ outline: 描线颜色.
+ width: 描线宽度.
+
+ 返回:
+ BuildImage: Self
+ """
+ self.draw.ellipse(pos, fill, outline, width)
+ return self
+
+ @run_sync
+ def text(
+ self,
+ pos: Tuple[int, int],
+ text: str,
+ fill: str | Tuple[int, int, int] = (0, 0, 0),
+ center_type: CenterType | None = None,
+ font: FreeTypeFont | str | Path | None = None,
+ font_size: int = 10,
+ ) -> Self:
+ """
+ 在图片上添加文字
+
+ 参数:
+ pos: 文字位置
+ text: 文字内容
+ fill: 文字颜色.
+ center_type: 居中类型.
+ font: 字体.
+ font_size: 字体大小.
+
+ 返回:
+ BuildImage: Self
+
+ 异常:
+ ValueError: 居中类型错误
+ """
+ text = str(text)
+ if center_type and center_type not in ["center", "height", "width"]:
+ raise ValueError("center_type must be 'center', 'width' or 'height'")
+ max_length_text = ""
+ sentence = text.split("\n")
+ for x in sentence:
+ max_length_text = x if len(x) > len(max_length_text) else max_length_text
+ if font:
+ if not isinstance(font, FreeTypeFont):
+ font = self.load_font(font, font_size)
+ else:
+ font = self.font
+ if center_type:
+ ttf_w, ttf_h = self.getsize(max_length_text) # type: ignore
+ # ttf_h = ttf_h * len(sentence)
+ pos = self.__center_xy(pos, ttf_w, ttf_h, center_type)
+ self.draw.text(pos, text, fill=fill, font=font)
+ return self
+
+ @run_sync
+ def save(self, path: str | Path):
+ """
+ 保存图片
+
+ 参数:
+ path: 图片路径
+ """
+ self.markImg.save(path) # type: ignore
+
+ def show(self):
+ """
+ 说明:
+ 显示图片
+ """
+ self.markImg.show()
+
+ @run_sync
+ def resize(self, ratio: float = 0, width: int = 0, height: int = 0) -> Self:
+ """
+ 压缩图片
+
+ 参数:
+ ratio: 压缩倍率.
+ width: 压缩图片宽度至 width.
+ height: 压缩图片高度至 height.
+
+ 返回:
+ BuildImage: Self
+
+ 异常:
+ ValueError: 缺少参数
+ """
+ if not width and not height and not ratio:
+ raise ValueError("缺少参数...")
+ if self.width and self.height:
+ if not width and not height and ratio:
+ width = int(self.width * ratio)
+ height = int(self.height * ratio)
+ self.markImg = self.markImg.resize((width, height), Image.LANCZOS) # type: ignore
+ self.width, self.height = self.markImg.size
+ self.draw = ImageDraw.Draw(self.markImg)
+ return self
+
+ @run_sync
+ def crop(self, box: Tuple[int, int, int, int]) -> Self:
+ """
+ 裁剪图片
+
+ 参数:
+ box: 左上角坐标,右下角坐标 (left, upper, right, lower)
+
+ 返回:
+ BuildImage: Self
+ """
+ self.markImg = self.markImg.crop(box)
+ self.width, self.height = self.markImg.size
+ self.draw = ImageDraw.Draw(self.markImg)
+ return self
+
+ @run_sync
+ def transparent(self, alpha_ratio: float = 1, n: int = 0) -> Self:
+ """
+ 图片透明化
+
+ 参数:
+ alpha_ratio: 透明化程度.
+ n: 透明化大小内边距.
+
+ 返回:
+ BuildImage: Self
+ """
+ self.markImg = self.markImg.convert("RGBA")
+ x, y = self.markImg.size
+ for i in range(n, x - n):
+ for k in range(n, y - n):
+ color = self.markImg.getpixel((i, k))
+ color = color[:-1] + (int(100 * alpha_ratio),)
+ self.markImg.putpixel((i, k), color)
+ self.draw = ImageDraw.Draw(self.markImg)
+ return self
+
+ def pic2bs4(self) -> str:
+ """BuildImage 转 base64
+
+ 返回:
+ str: base64
+ """
+ buf = BytesIO()
+ self.markImg.save(buf, format="PNG")
+ base64_str = base64.b64encode(buf.getvalue()).decode()
+ return "base64://" + base64_str
+
+ def pic2bytes(self) -> bytes:
+ """获取bytes
+
+ 返回:
+ bytes: bytes
+ """
+ buf = BytesIO()
+ self.markImg.save(buf, format="PNG")
+ return buf.getvalue()
+
+ def convert(self, type_: ModeType) -> Self:
+ """
+ 修改图片类型
+
+ 参数:
+ type_: ModeType
+
+ 返回:
+ BuildImage: Self
+ """
+ self.markImg = self.markImg.convert(type_)
+ return self
+
+ @run_sync
+ def rectangle(
+ self,
+ xy: Tuple[int, int, int, int],
+ fill: Tuple[int, int, int] | None = None,
+ outline: str | None = None,
+ width: int = 1,
+ ) -> Self:
+ """
+ 画框
+
+ 参数:
+ xy: 坐标
+ fill: 填充颜色.
+ outline: 轮廓颜色.
+ width: 线宽.
+
+ 返回:
+ BuildImage: Self
+ """
+ self.draw.rectangle(xy, fill, outline, width)
+ return self
+
+ @run_sync
+ def polygon(
+ self,
+ xy: list[Tuple[int, int]],
+ fill: Tuple[int, int, int] = (0, 0, 0),
+ outline: int = 1,
+ ) -> Self:
+ """
+ 画多边形
+
+ 参数:
+ xy: 坐标
+ fill: 颜色.
+ outline: 线宽.
+
+ 返回:
+ BuildImage: Self
+ """
+ self.draw.polygon(xy, fill, outline)
+ return self
+
+ @run_sync
+ def line(
+ self,
+ xy: Tuple[int, int, int, int],
+ fill: Tuple[int, int, int] | str = "#D8DEE4",
+ width: int = 1,
+ ) -> Self:
+ """
+ 画线
+
+ 参数:
+ xy: 坐标
+ fill: 填充.
+ width: 线宽.
+
+ 返回:
+ BuildImage: Self
+ """
+ self.draw.line(xy, fill, width)
+ return self
+
+ @run_sync
+ def circle(self) -> Self:
+ """
+ 图像变圆
+
+ 返回:
+ BuildImage: Self
+ """
+ self.markImg.convert("RGBA")
+ size = self.markImg.size
+ r2 = min(size[0], size[1])
+ if size[0] != size[1]:
+ self.markImg = self.markImg.resize((r2, r2), Image.LANCZOS) # type: ignore
+ width = 1
+ antialias = 4
+ ellipse_box = [0, 0, r2 - 2, r2 - 2]
+ mask = Image.new(
+ size=[int(dim * antialias) for dim in self.markImg.size], # type: ignore
+ mode="L",
+ color="black",
+ )
+ draw = ImageDraw.Draw(mask)
+ for offset, fill in (width / -2.0, "black"), (width / 2.0, "white"):
+ left, top = [(value + offset) * antialias for value in ellipse_box[:2]]
+ right, bottom = [(value - offset) * antialias for value in ellipse_box[2:]]
+ draw.ellipse([left, top, right, bottom], fill=fill)
+ mask = mask.resize(self.markImg.size, Image.LANCZOS)
+ try:
+ self.markImg.putalpha(mask)
+ except ValueError:
+ pass
+ return self
+
+ @run_sync
+ def circle_corner(
+ self,
+ radii: int = 30,
+ point_list: list[Literal["lt", "rt", "lb", "rb"]] = ["lt", "rt", "lb", "rb"],
+ ) -> Self:
+ """
+ 矩形四角变圆
+
+ 参数:
+ radii: 半径.
+ point_list: 需要变化的角.
+
+ 返回:
+ BuildImage: Self
+ """
+ # 画圆(用于分离4个角)
+ img = self.markImg.convert("RGBA")
+ alpha = img.split()[-1]
+ circle = Image.new("L", (radii * 2, radii * 2), 0)
+ draw = ImageDraw.Draw(circle)
+ draw.ellipse((0, 0, radii * 2, radii * 2), fill=255) # 黑色方形内切白色圆形
+ w, h = img.size
+ if "lt" in point_list:
+ alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0))
+ if "rt" in point_list:
+ alpha.paste(circle.crop((radii, 0, radii * 2, radii)), (w - radii, 0))
+ if "lb" in point_list:
+ alpha.paste(circle.crop((0, radii, radii, radii * 2)), (0, h - radii))
+ if "rb" in point_list:
+ alpha.paste(
+ circle.crop((radii, radii, radii * 2, radii * 2)),
+ (w - radii, h - radii),
+ )
+ img.putalpha(alpha)
+ self.markImg = img
+ self.draw = ImageDraw.Draw(self.markImg)
+ return self
+
+ @run_sync
+ def rotate(self, angle: int, expand: bool = False) -> Self:
+ """
+ 旋转图片
+
+ 参数:
+ angle: 角度
+ expand: 放大图片适应角度.
+
+ 返回:
+ BuildImage: Self
+ """
+ self.markImg = self.markImg.rotate(angle, expand=expand)
+ return self
+
+ @run_sync
+ def transpose(self, angle: Literal[0, 1, 2, 3, 4, 5, 6]) -> Self:
+ """
+ 旋转图片(包括边框)
+
+ 参数:
+ angle: 角度
+
+ 返回:
+ BuildImage: Self
+ """
+ self.markImg.transpose(angle)
+ return self
+
+ @run_sync
+ def filter(self, filter_: str, aud: int | None = None) -> Self:
+ """
+ 图片变化
+
+ 参数:
+ filter_: 变化效果
+ aud: 利率.
+
+ 返回:
+ BuildImage: Self
+ """
+ _type = None
+ if filter_ == "GaussianBlur": # 高斯模糊
+ _type = ImageFilter.GaussianBlur
+ elif filter_ == "EDGE_ENHANCE": # 锐化效果
+ _type = ImageFilter.EDGE_ENHANCE
+ elif filter_ == "BLUR": # 模糊效果
+ _type = ImageFilter.BLUR
+ elif filter_ == "CONTOUR": # 铅笔滤镜
+ _type = ImageFilter.CONTOUR
+ elif filter_ == "FIND_EDGES": # 边缘检测
+ _type = ImageFilter.FIND_EDGES
+ if _type:
+ if aud:
+ self.markImg = self.markImg.filter(_type(aud)) # type: ignore
+ else:
+ self.markImg = self.markImg.filter(_type)
+ self.draw = ImageDraw.Draw(self.markImg)
+ return self
+
+ def tobytes(self) -> bytes:
+ """转换为bytes
+
+ 返回:
+ bytes: bytes
+ """
+ return self.markImg.tobytes()
diff --git a/zhenxun/utils/_build_mat.py b/zhenxun/utils/_build_mat.py
new file mode 100644
index 00000000..6f30d301
--- /dev/null
+++ b/zhenxun/utils/_build_mat.py
@@ -0,0 +1,567 @@
+import random
+from io import BytesIO
+from pathlib import Path
+from re import S
+
+from pydantic import BaseModel
+from strenum import StrEnum
+
+from ._build_image import BuildImage
+
+
+class MatType(StrEnum):
+
+ LINE = "LINE"
+ """折线图"""
+ BAR = "BAR"
+ """柱状图"""
+ BARH = "BARH"
+ """横向柱状图"""
+
+
+class BuildMatData(BaseModel):
+
+ mat_type: MatType
+ """类型"""
+ data: list[int | float] = []
+ """数据"""
+ x_name: str | None = None
+ """X轴坐标名称"""
+ y_name: str | None = None
+ """Y轴坐标名称"""
+ x_index: list[str] = []
+ """显示轴坐标值"""
+ y_index: list[int | float] = []
+ """数据轴坐标值"""
+ space: tuple[int, int] = (20, 20)
+ """坐标值间隔(X, Y)"""
+ rotate: tuple[int, int] = (0, 0)
+ """坐标值旋转(X, Y)"""
+ title: str | None = None
+ """标题"""
+ font: str = "msyh.ttf"
+ """字体"""
+ font_size: int = 15
+ """字体大小"""
+ display_num: bool = True
+ """是否在点与柱状图顶部显示数值"""
+ is_grid: bool = False
+ """是否添加栅格"""
+ background_color: tuple[int, int, int] | str = (255, 255, 255)
+ """背景颜色"""
+ background: Path | bytes | None = None
+ """背景图片"""
+ bar_color: list[str] = ["*"]
+ """柱状图柱子颜色, 多个时随机, 使用 * 时七色随机"""
+ padding: tuple[int, int] = (50, 50)
+ """图表上下左右边距"""
+
+
+class BuildMat:
+ """
+ 针对 折线图/柱状图,基于 BuildImage 编写的 非常难用的 自定义画图工具
+ 目前仅支持 正整数
+ """
+
+ class InitGraph(BaseModel):
+
+ mark_image: BuildImage
+ """BuildImage"""
+ x_height: int
+ """横坐标开始高度"""
+ y_width: int
+ """纵坐标开始宽度"""
+ x_point: list[int]
+ """横坐标坐标"""
+ y_point: list[int]
+ """纵坐标坐标"""
+ graph_height: int
+ """坐标轴高度"""
+
+ class Config:
+ arbitrary_types_allowed = True
+
+ def __init__(self, mat_type: MatType) -> None:
+ self.line_length = 760
+ self._x_padding = 0
+ self._y_padding = 0
+ self.build_data = BuildMatData(mat_type=mat_type)
+
+ @property
+ def x_name(self) -> str | None:
+ return self.build_data.x_name
+
+ @x_name.setter
+ def x_name(self, data: str) -> str | None:
+ self.build_data.x_name = data
+
+ @property
+ def y_name(self) -> str | None:
+ return self.build_data.y_name
+
+ @y_name.setter
+ def y_name(self, data: str) -> str | None:
+ self.build_data.y_name = data
+
+ @property
+ def data(self) -> list[int | float]:
+ return self.build_data.data
+
+ @data.setter
+ def data(self, data: list[int | float]):
+ self._check_value(data, self.build_data.y_index)
+ self.build_data.data = data
+
+ @property
+ def x_index(self) -> list[str]:
+ return self.build_data.x_index
+
+ @x_index.setter
+ def x_index(self, data: list[str]):
+ self.build_data.x_index = data
+
+ @property
+ def y_index(self) -> list[int | float]:
+ return self.build_data.y_index
+
+ @y_index.setter
+ def y_index(self, data: list[int | float]):
+ # self._check_value(self.build_data.data, data)
+ data.sort()
+ self.build_data.y_index = data
+
+ @property
+ def space(self) -> tuple[int, int]:
+ return self.build_data.space
+
+ @space.setter
+ def space(self, data: tuple[int, int]):
+ self.build_data.space = data
+
+ @property
+ def rotate(self) -> tuple[int, int]:
+ return self.build_data.rotate
+
+ @rotate.setter
+ def rotate(self, data: tuple[int, int]):
+ self.build_data.rotate = data
+
+ @property
+ def title(self) -> str | None:
+ return self.build_data.title
+
+ @title.setter
+ def title(self, data: str):
+ self.build_data.title = data
+
+ @property
+ def font(self) -> str:
+ return self.build_data.font
+
+ @font.setter
+ def font(self, data: str):
+ self.build_data.font = data
+
+ # @property
+ # def font_size(self) -> int:
+ # return self.build_data.font_size
+
+ # @font_size.setter
+ # def font_size(self, data: int):
+ # self.build_data.font_size = data
+
+ @property
+ def display_num(self) -> bool:
+ return self.build_data.display_num
+
+ @display_num.setter
+ def display_num(self, data: bool):
+ self.build_data.display_num = data
+
+ @property
+ def is_grid(self) -> bool:
+ return self.build_data.is_grid
+
+ @is_grid.setter
+ def is_grid(self, data: bool):
+ self.build_data.is_grid = data
+
+ @property
+ def background_color(self) -> tuple[int, int, int] | str:
+ return self.build_data.background_color
+
+ @background_color.setter
+ def background_color(self, data: tuple[int, int, int] | str):
+ self.build_data.background_color = data
+
+ @property
+ def background(self) -> Path | bytes | None:
+ return self.build_data.background
+
+ @background.setter
+ def background(self, data: Path | bytes):
+ self.build_data.background = data
+
+ @property
+ def bar_color(self) -> list[str]:
+ return self.build_data.bar_color
+
+ @bar_color.setter
+ def bar_color(self, data: list[str]):
+ self.build_data.bar_color = data
+
+ def _check_value(
+ self,
+ y: list[int | float],
+ y_index: list[int | float] | None = None,
+ x_index: list[int | float] | None = None,
+ ):
+ """检查值合法性
+
+ 参数:
+ y: 坐标值
+ y_index: y轴坐标值
+ x_index: x轴坐标值
+ """
+ if y_index:
+ _value = x_index if self.build_data.mat_type == "barh" else y_index
+ if not isinstance(y[0], str):
+ __y = [float(t_y) for t_y in y]
+ _y_index = [float(t_y) for t_y in y_index]
+ if max(__y) > max(_y_index):
+ raise ValueError("坐标点的值必须小于y轴坐标的最大值...")
+ i = -9999999999
+ for _y in _y_index:
+ if _y > i:
+ i = _y
+ else:
+ raise ValueError("y轴坐标值必须有序...")
+
+ async def build(self) -> BuildImage:
+ """构造图片"""
+ A = BuildImage(1, 1)
+ bar_color = self.build_data.bar_color
+ if "*" in bar_color:
+ bar_color = [
+ "#FF0000",
+ "#FF7F00",
+ "#FFFF00",
+ "#00FF00",
+ "#00FFFF",
+ "#0000FF",
+ "#8B00FF",
+ ]
+ init_graph = await self._init_graph()
+ mark_image = None
+ if self.build_data.mat_type == MatType.LINE:
+ mark_image = await self._build_line_graph(init_graph, bar_color)
+ if self.build_data.mat_type == MatType.BAR:
+ mark_image = await self._build_bar_graph(init_graph, bar_color)
+ if self.build_data.mat_type == MatType.BARH:
+ mark_image = await self._build_barh_graph(init_graph, bar_color)
+ if mark_image:
+ padding_width, padding_height = self.build_data.padding
+ width = mark_image.width + padding_width
+ height = mark_image.height + padding_height * 2
+ if self.build_data.background:
+ if isinstance(self.build_data.background, bytes):
+ A = BuildImage(
+ width, height, background=BytesIO(self.build_data.background)
+ )
+ elif isinstance(self.build_data.background, Path):
+ A = BuildImage(width, height, background=self.build_data.background)
+ else:
+ A = BuildImage(width, height, self.build_data.background_color)
+ if A:
+ await A.paste(mark_image, (10, padding_height))
+ if self.build_data.title:
+ font = BuildImage.load_font(
+ self.build_data.font, self.build_data.font_size + 7
+ )
+ title_width, title_height = BuildImage.get_text_size(
+ self.build_data.title, font
+ )
+ pos = (
+ int(A.width / 2 - title_width / 2),
+ int(padding_height / 2 - title_height / 2),
+ )
+ await A.text(pos, self.build_data.title)
+ if self.build_data.x_name:
+ font = BuildImage.load_font(
+ self.build_data.font, self.build_data.font_size + 4
+ )
+ title_width, title_height = BuildImage.get_text_size(
+ self.build_data.x_name, font # type: ignore
+ )
+ pos = (
+ A.width - title_width - 20,
+ A.height - int(padding_height / 2 + title_height),
+ )
+ await A.text(pos, self.build_data.x_name)
+ return A
+
+ async def _init_graph(self) -> InitGraph:
+ """构造初始化图表
+
+ 返回:
+ InitGraph: InitGraph
+ """
+ padding_width = 0
+ padding_height = 0
+ font = BuildImage.load_font(self.build_data.font, self.build_data.font_size)
+ x_width_list = []
+ y_height_list = []
+ for x in self.build_data.x_index:
+ text_size = BuildImage.get_text_size(x, font)
+ if text_size[1] > padding_height:
+ padding_height = text_size[1]
+ x_width_list.append(text_size)
+ if not self.build_data.y_index:
+ """没有指定y_index时,使用data自动生成"""
+ max_num = max(self.build_data.data)
+ if max_num < 5:
+ max_num = 5
+ s = int(max_num / 5)
+ _y_index = [max_num]
+ for _n in range(4):
+ max_num -= s
+ _y_index.append(max_num)
+ _y_index.sort()
+ # if len(_y_index) > 1:
+ # if _y_index[0] == _y_index[-1]:
+ # _tmp = ["_" for _ in range(len(_y_index) - 1)]
+ # _tmp.append(str(_y_index[0]))
+ # _y_index = _tmp
+ self.build_data.y_index = _y_index # type: ignore
+ for item in self.build_data.y_index:
+ text_size = BuildImage.get_text_size(str(item), font)
+ if text_size[0] > padding_width:
+ padding_width = text_size[0]
+ y_height_list.append(text_size)
+ if self.build_data.mat_type == MatType.BARH:
+ _tmp = x_width_list
+ x_width_list = y_height_list
+ y_height_list = _tmp
+ old_space = self.build_data.space
+ width = padding_width * 2 + self.build_data.space[0] * 2 + 20
+ height = (
+ sum([h[1] + self.build_data.space[1] for h in y_height_list])
+ + self.build_data.space[1] * 2
+ + 30
+ )
+ _x_index = self.build_data.x_index
+ _y_index = self.build_data.y_index
+ _barh_max_text_width = 0
+ if self.build_data.mat_type == MatType.BARH:
+ """XY轴下标互换"""
+ _tmp = _y_index
+ _y_index = _x_index
+ _x_index = _tmp
+ """额外增加字体宽度"""
+ for s in self.build_data.x_index:
+ s_w, s_h = BuildImage.get_text_size(s, font)
+ if s_w > _barh_max_text_width:
+ _barh_max_text_width = s_w
+ width += _barh_max_text_width
+ width += self.build_data.space[0] * 2 - old_space[0] * 2
+ """X轴重新等均分配"""
+ x_length = width - padding_width * 2 - _barh_max_text_width
+ x_space = int((x_length - 20) / (len(_x_index) + 1))
+ if x_space < 50:
+ """加大间距更加美观"""
+ x_space = 50
+ self.build_data.space = (x_space, self.build_data.space[1])
+ width += self.build_data.space[0] * (len(_x_index) - 1)
+ else:
+ """非横向柱状图时加字体宽度"""
+ width += sum([w[0] + self.build_data.space[0] for w in x_width_list])
+
+ A = BuildImage(
+ width + 5,
+ (height + 10),
+ # color=(255, 255, 255),
+ color=(255, 255, 255, 0),
+ )
+ padding_height += 5
+ """高"""
+ await A.line(
+ (
+ padding_width + 5 + _barh_max_text_width,
+ padding_height,
+ padding_width + 5 + _barh_max_text_width,
+ height - padding_height,
+ ),
+ width=2,
+ )
+ """长"""
+ await A.line(
+ (
+ padding_width + 5 + _barh_max_text_width,
+ height - padding_height,
+ width - padding_width + 5,
+ height - padding_height,
+ ),
+ width=2,
+ )
+ x_cur_width = (
+ padding_width + _barh_max_text_width + self.build_data.space[0] + 5
+ )
+ if self.build_data.mat_type != MatType.BARH:
+ """添加字体宽度"""
+ x_cur_width += x_width_list[0][0]
+ x_cur_height = height - y_height_list[0][1] - 5
+ # await A.point((x_cur_width, x_cur_height), (0, 0, 0))
+ x_point = []
+ for i, _x in enumerate(_x_index):
+ """X轴数值"""
+ grid_height = x_cur_height
+ if self.build_data.is_grid:
+ grid_height = padding_height
+ await A.line(
+ (
+ x_cur_width,
+ x_cur_height - 1,
+ x_cur_width,
+ grid_height - 5,
+ )
+ )
+ x_point.append(x_cur_width - 1)
+ mid_point = x_cur_width - int(x_width_list[i][0] / 2)
+ await A.text((mid_point, x_cur_height), str(_x), font=font)
+ x_cur_width += self.build_data.space[0]
+ if self.build_data.mat_type != MatType.BARH:
+ """添加字体宽度"""
+ x_cur_width += x_width_list[i][0]
+ y_cur_width = padding_width + _barh_max_text_width
+ y_cur_height = height - self.build_data.padding[1] - 9
+ start_height = y_cur_height
+ # await A.point((y_cur_width, y_cur_height), (0, 0, 0))
+ y_point = []
+ for i, _y in enumerate(_y_index):
+ """Y轴数值"""
+ grid_width = y_cur_width
+ if self.build_data.is_grid:
+ grid_width = width - padding_width + 5
+ y_point.append(y_cur_height)
+ await A.line((y_cur_width + 5, y_cur_height, grid_width + 11, y_cur_height))
+ text_width = BuildImage.get_text_size(str(_y), font)[0]
+ await A.text(
+ (
+ y_cur_width - text_width,
+ y_cur_height - int(y_height_list[i][1] / 2) - 3,
+ ),
+ str(_y),
+ font=font,
+ )
+ y_cur_height -= y_height_list[i][1] + self.build_data.space[1]
+ graph_height = 0
+ if self.build_data.mat_type == MatType.BARH:
+ graph_height = (
+ x_cur_width
+ - self.build_data.space[0]
+ - _barh_max_text_width
+ - padding_width
+ - 5
+ )
+ else:
+ graph_height = start_height - y_cur_height + 7
+ return self.InitGraph(
+ mark_image=A,
+ x_height=height - y_height_list[0][1] - 5,
+ y_width=padding_width + 5 + _barh_max_text_width,
+ graph_height=graph_height,
+ x_point=x_point,
+ y_point=y_point,
+ )
+
+ async def _build_line_graph(
+ self, init_graph: InitGraph, bar_color: list[str]
+ ) -> BuildImage:
+ """构建折线图
+
+ 参数:
+ init_graph: InitGraph
+ bar_color: 颜色列表
+
+ 返回:
+ BuildImage: 折线图
+ """
+ font = BuildImage.load_font(self.build_data.font, self.build_data.font_size)
+ mark_image = init_graph.mark_image
+ x_height = init_graph.x_height
+ graph_height = init_graph.graph_height
+ random_color = random.choice(bar_color)
+ _black_point = BuildImage(11, 11, color=random_color)
+ await _black_point.circle()
+ max_num = max(self.y_index)
+ point_list = []
+ for x_p, y in zip(init_graph.x_point, self.build_data.data):
+ """折线图标点"""
+ y_height = int(y / max_num * graph_height)
+ await mark_image.paste(_black_point, (x_p - 3, x_height - y_height))
+ point_list.append((x_p + 1, x_height - y_height + 1))
+ for i in range(len(point_list) - 1):
+ """画线"""
+ a_x, a_y = point_list[i]
+ b_x, b_y = point_list[i + 1]
+ await mark_image.line((a_x, a_y, b_x, b_y), random_color)
+ if self.build_data.display_num:
+ """显示数值"""
+ value = self.build_data.data[i]
+ text_size = BuildImage.get_text_size(str(value), font)
+ await mark_image.text(
+ (a_x - int(text_size[0] / 2), a_y - text_size[1] - 5),
+ str(value),
+ font=font,
+ )
+ """最后一个数值显示"""
+ value = self.build_data.data[-1]
+ text_size = BuildImage.get_text_size(str(value), font)
+ await mark_image.text(
+ (
+ point_list[-1][0] - int(text_size[0] / 2),
+ point_list[-1][1] - text_size[1] - 5,
+ ),
+ str(value),
+ font=font,
+ )
+ return mark_image
+
+ async def _build_bar_graph(self, init_graph: InitGraph, bar_color: list[str]):
+ """构建折线图
+
+ 参数:
+ init_graph: InitGraph
+ bar_color: 颜色列表
+
+ 返回:
+ BuildImage: 折线图
+ """
+ pass
+
+ async def _build_barh_graph(self, init_graph: InitGraph, bar_color: list[str]):
+ """构建折线图
+
+ 参数:
+ init_graph: InitGraph
+ bar_color: 颜色列表
+
+ 返回:
+ BuildImage: 横向柱状图
+ """
+ font = BuildImage.load_font(self.build_data.font, self.build_data.font_size)
+ mark_image = init_graph.mark_image
+ y_width = init_graph.y_width
+ graph_height = init_graph.graph_height
+ random_color = random.choice(bar_color)
+ max_num = max(self.y_index)
+ for y_p, y in zip(init_graph.y_point, self.build_data.data):
+ bar_width = int(y / max_num * graph_height) or 1
+ bar = BuildImage(bar_width, 18, random_color)
+ await mark_image.paste(bar, (y_width + 1, y_p - 9))
+ if self.build_data.display_num:
+ """显示数值"""
+ await mark_image.text(
+ (y_width + bar_width + 5, y_p - 12), str(y), font=font
+ )
+ return mark_image
diff --git a/zhenxun/utils/_image_template.py b/zhenxun/utils/_image_template.py
new file mode 100644
index 00000000..399c054c
--- /dev/null
+++ b/zhenxun/utils/_image_template.py
@@ -0,0 +1,282 @@
+import random
+from io import BytesIO
+from pathlib import Path
+from typing import Any, Callable, Dict
+
+from fastapi import background
+from PIL.ImageFont import FreeTypeFont
+from pydantic import BaseModel
+
+from ._build_image import BuildImage
+
+
+class RowStyle(BaseModel):
+
+ font: FreeTypeFont | str | Path | None = "HYWenHei-85W.ttf"
+ """字体"""
+ font_size: int = 20
+ """字体大小"""
+ font_color: str | tuple[int, int, int] = (0, 0, 0)
+ """字体颜色"""
+
+ class Config:
+ arbitrary_types_allowed = True
+
+
+class ImageTemplate:
+
+ color_list = ["#C2CEFE", "#FFA94C", "#3FE6A0", "#D1D4F5"]
+
+ @classmethod
+ async def hl_page(
+ cls,
+ head_text: str,
+ items: Dict[str, str],
+ row_space: int = 10,
+ padding: int = 30,
+ ) -> BuildImage:
+ """列文档 (如插件帮助)
+
+ 参数:
+ head_text: 头标签文本
+ items: 列内容
+ row_space: 列间距.
+ padding: 间距.
+
+ 返回:
+ BuildImage: 图片
+ """
+ font = BuildImage.load_font("HYWenHei-85W.ttf", 20)
+ width, height = BuildImage.get_text_size(head_text, font)
+ for title, item in items.items():
+ title_width, title_height = await cls.__get_text_size(title, font)
+ it_width, it_height = await cls.__get_text_size(item, font)
+ width = max([width, title_width, it_width])
+ height += title_height + it_height
+ width = max([width + padding * 2 + 100, 300])
+ height = max([height + padding * 2 + 150, 100])
+ A = BuildImage(width + padding * 2, height + padding * 2, color="#FAF9FE")
+ top_head = BuildImage(width, 100, color="#FFFFFF", font_size=40)
+ await top_head.line((0, 1, width, 1), "#C2CEFE", 2)
+ await top_head.text((15, 20), head_text, "#9FA3B2", "center")
+ await top_head.circle_corner()
+ await A.paste(top_head, (0, 20), "width")
+ _min_width = top_head.width - 60
+ cur_h = top_head.height + 35 + row_space * len(items)
+ for title, item in items.items():
+ title_width, title_height = BuildImage.get_text_size(title, font)
+ title_background = BuildImage(
+ title_width + 6, title_height + 10, font=font, color="#C1CDFF"
+ )
+ await title_background.text((3, 5), title)
+ await title_background.circle_corner(5)
+ _text_width, _text_height = await cls.__get_text_size(item, font)
+ _width = max([title_background.width, _text_width, _min_width])
+ text_image = await cls.__build_text_image(
+ item, _width, _text_height, font, color="#FDFCFA"
+ )
+ B = BuildImage(_width + 20, title_height + text_image.height + 40)
+ await B.paste(title_background, (10, 10))
+ await B.paste(text_image, (10, 20 + title_background.height))
+ await B.line((0, 0, 0, B.height), random.choice(cls.color_list))
+ await A.paste(B, (0, cur_h), "width")
+ cur_h += B.height + row_space
+ return A
+
+ @classmethod
+ async def table_page(
+ cls,
+ head_text: str,
+ tip_text: str | None,
+ column_name: list[str],
+ data_list: list[list[str | tuple[Path | BuildImage, int, int]]],
+ row_space: int = 35,
+ column_space: int = 30,
+ padding: int = 5,
+ text_style: Callable[[str, str], RowStyle] | None = None,
+ ) -> BuildImage:
+ """表格页
+
+ 参数:
+ head_text: 标题文本.
+ tip_text: 标题注释.
+ column_name: 表头列表.
+ data_list: 数据列表.
+ row_space: 行间距.
+ column_space: 列间距.
+ padding: 文本内间距.
+ text_style: 文本样式.
+
+ 返回:
+ BuildImage: 表格图片
+ """
+ font = BuildImage.load_font(font_size=50)
+ min_width, _ = BuildImage.get_text_size(head_text, font)
+ table = await cls.table(
+ column_name,
+ data_list,
+ row_space,
+ column_space,
+ padding,
+ text_style,
+ )
+ await table.circle_corner()
+ table_bk = BuildImage(
+ max(table.width, min_width) + 100, table.height + 50, "#EAEDF2"
+ )
+ await table_bk.paste(table, center_type="center")
+ height = table_bk.height + 200
+ background = BuildImage(table_bk.width, height, (255, 255, 255), font_size=50)
+ await background.paste(table_bk, (0, 200))
+ await background.text((0, 50), head_text, "#334762", center_type="width")
+ if tip_text:
+ text_image = await BuildImage.build_text_image(tip_text, size=22)
+ await background.paste(text_image, (0, 110), center_type="width")
+ return background
+
+ @classmethod
+ async def table(
+ cls,
+ column_name: list[str],
+ data_list: list[list[str | tuple[Path | BuildImage, int, int]]],
+ row_space: int = 25,
+ column_space: int = 10,
+ padding: int = 5,
+ text_style: Callable[[str, str], RowStyle] | None = None,
+ ) -> BuildImage:
+ """表格
+
+ 参数:
+ column_name: 表头列表
+ data_list: 数据列表
+ row_space: 行间距.
+ column_space: 列间距.
+ padding: 文本内间距.
+ text_style: 文本样式.
+ min_width: 最低宽度
+
+ 返回:
+ BuildImage: 表格图片
+ """
+ font = BuildImage.load_font("HYWenHei-85W.ttf", 20)
+ column_data = []
+ for i in range(len(column_name)):
+ c = []
+ for l in data_list:
+ if len(l) > i:
+ c.append(l[i])
+ else:
+ c.append("")
+ column_data.append(c)
+ build_data_list = []
+ _, base_h = BuildImage.get_text_size("A", font)
+ for i, column_list in enumerate(column_data):
+ name_width, _ = BuildImage.get_text_size(column_name[i], font)
+ _temp = {"width": name_width, "data": column_list}
+ for s in column_list:
+ if isinstance(s, tuple):
+ w = s[1]
+ else:
+ w, _ = BuildImage.get_text_size(s, font)
+ if w > _temp["width"]:
+ _temp["width"] = w
+ build_data_list.append(_temp)
+ column_image_list = []
+ for i, data in enumerate(build_data_list):
+ width = data["width"] + padding * 2
+ height = (base_h + row_space) * (len(data["data"]) + 1) + padding * 2
+ background = BuildImage(width, height, (255, 255, 255))
+ column_name_image = await BuildImage.build_text_image(
+ column_name[i], font, 12, "#C8CCCF"
+ )
+ await background.paste(column_name_image, (0, 20), center_type="width")
+ cur_h = column_name_image.height + row_space + 20
+ for item in data["data"]:
+ style = RowStyle(font=font)
+ if text_style:
+ style = text_style(column_name[i], item)
+ if isinstance(item, tuple):
+ """图片"""
+ data, width, height = item
+ if isinstance(data, Path):
+ image_ = BuildImage(width, height, background=data)
+ elif isinstance(data, bytes):
+ image_ = BuildImage(width, height, background=BytesIO(data))
+ elif isinstance(data, BuildImage):
+ image_ = data
+ await background.paste(image_, (padding, cur_h))
+ else:
+ await background.text(
+ (padding, cur_h),
+ item if item is not None else "",
+ style.font_color,
+ font=style.font,
+ font_size=style.font_size,
+ )
+ cur_h += base_h + row_space
+ column_image_list.append(background)
+ # height = max([bk.height for bk in column_image_list])
+ # width = sum([bk.width for bk in column_image_list])
+ return await BuildImage.auto_paste(
+ column_image_list, len(column_image_list), column_space
+ )
+
+ @classmethod
+ async def __build_text_image(
+ cls,
+ text: str,
+ width: int,
+ height: int,
+ font: FreeTypeFont,
+ font_color: str | tuple[int, int, int] = (0, 0, 0),
+ color: str | tuple[int, int, int] = (255, 255, 255),
+ ) -> BuildImage:
+ """文本转图片
+
+ 参数:
+ text: 文本
+ width: 宽度
+ height: 长度
+ font: 字体
+ font_color: 文本颜色
+ color: 背景颜色
+
+ 返回:
+ BuildImage: 文本转图片
+ """
+ _, h = BuildImage.get_text_size("A", font)
+ A = BuildImage(width, height, color=color)
+ cur_h = 0
+ for s in text.split("\n"):
+ text_image = await BuildImage.build_text_image(
+ s, font, font_color=font_color
+ )
+ await A.paste(text_image, (0, cur_h))
+ cur_h += h
+ return A
+
+ @classmethod
+ async def __get_text_size(
+ cls,
+ text: str,
+ font: FreeTypeFont,
+ ) -> tuple[int, int]:
+ """获取文本所占大小
+
+ 参数:
+ text: 文本
+ font: 字体
+
+ 返回:
+ tuple[int, int]: 宽, 高
+ """
+ width = 0
+ height = 0
+ _, h = BuildImage.get_text_size("A", font)
+ image_list = []
+ for s in text.split("\n"):
+ s = s.strip() or "A"
+ w, _ = BuildImage.get_text_size(s, font)
+ width = width if width > w else w
+ height += h
+ return width, height
diff --git a/utils/browser.py b/zhenxun/utils/browser.py
old mode 100755
new mode 100644
similarity index 81%
rename from utils/browser.py
rename to zhenxun/utils/browser.py
index 66530008..c7d89727
--- a/utils/browser.py
+++ b/zhenxun/utils/browser.py
@@ -1,48 +1,45 @@
-import asyncio
-from typing import Optional
-
-from nonebot import get_driver
-from playwright.async_api import Browser, Playwright, async_playwright
-
-from services.log import logger
-
-driver = get_driver()
-
-_playwright: Optional[Playwright] = None
-_browser: Optional[Browser] = None
-
-
-@driver.on_startup
-async def start_browser():
- global _playwright
- global _browser
- _playwright = await async_playwright().start()
- _browser = await _playwright.chromium.launch()
-
-
-@driver.on_shutdown
-async def shutdown_browser():
- if _browser:
- await _browser.close()
- if _playwright:
- await _playwright.stop() # type: ignore
-
-
-def get_browser() -> Browser:
- if not _browser:
- raise RuntimeError("playwright is not initalized")
- return _browser
-
-
-def install():
- """自动安装、更新 Chromium"""
- logger.info("正在检查 Chromium 更新")
- import sys
-
- from playwright.__main__ import main
-
- sys.argv = ["", "install", "chromium"]
- try:
- main()
- except SystemExit:
- pass
+from nonebot import get_driver
+from playwright.async_api import Browser, Playwright, async_playwright
+
+from zhenxun.services.log import logger
+
+driver = get_driver()
+
+_playwright: Playwright | None = None
+_browser: Browser | None = None
+
+
+@driver.on_startup
+async def start_browser():
+ global _playwright
+ global _browser
+ _playwright = await async_playwright().start()
+ _browser = await _playwright.chromium.launch()
+
+
+@driver.on_shutdown
+async def shutdown_browser():
+ if _browser:
+ await _browser.close()
+ if _playwright:
+ await _playwright.stop() # type: ignore
+
+
+def get_browser() -> Browser:
+ if not _browser:
+ raise RuntimeError("playwright is not initalized")
+ return _browser
+
+
+def install():
+ """自动安装、更新 Chromium"""
+ logger.info("正在检查 Chromium 更新")
+ import sys
+
+ from playwright.__main__ import main
+
+ sys.argv = ["", "install", "chromium"]
+ try:
+ main()
+ except SystemExit:
+ pass
diff --git a/zhenxun/utils/decorator/shop.py b/zhenxun/utils/decorator/shop.py
new file mode 100644
index 00000000..3e46db6a
--- /dev/null
+++ b/zhenxun/utils/decorator/shop.py
@@ -0,0 +1,290 @@
+from typing import Any, Callable, Dict
+
+from nonebot.adapters.onebot.v11 import Message, MessageSegment
+from nonebot.plugin import require
+from pydantic import BaseModel
+
+from zhenxun.models.goods_info import GoodsInfo
+
+
+class Goods(BaseModel):
+
+ before_handle: list[Callable] = []
+ after_handle: list[Callable] = []
+ price: int
+ des: str = ""
+ discount: float
+ limit_time: int
+ daily_limit: int
+ icon: str | None = None
+ is_passive: bool
+ func: Callable
+ kwargs: Dict[str, str] = {}
+ send_success_msg: bool
+ max_num_limit: int
+
+
+class ShopRegister(dict):
+
+ def __init__(self, *args, **kwargs):
+ super(ShopRegister, self).__init__(*args, **kwargs)
+ self._data: Dict[str, Goods] = {}
+ self._flag = True
+
+ def before_handle(self, name: str | tuple[str, ...], load_status: bool = True):
+ """使用前检查方法
+
+ 参数:
+ name: 道具名称
+ load_status: 加载状态
+ """
+
+ def register_before_handle(name_list: tuple[str, ...], func: Callable):
+ if load_status:
+ for name_ in name_list:
+ if goods := self._data.get(name_):
+ self._data[name_].before_handle.append(func)
+
+ _name = (name,) if isinstance(name, str) else name
+ return lambda func: register_before_handle(_name, func)
+
+ def after_handle(self, name: str | tuple[str, ...], load_status: bool = True):
+ """使用后执行方法
+
+ 参数:
+ name: 道具名称
+ load_status: 加载状态
+ """
+
+ def register_after_handle(name_list: tuple[str, ...], func: Callable):
+ if load_status:
+ for name_ in name_list:
+ if goods := self._data.get(name_):
+ self._data[name_].after_handle.append(func)
+
+ _name = (name,) if isinstance(name, str) else name
+ return lambda func: register_after_handle(_name, func)
+
+ def register(
+ self,
+ name: tuple[str, ...],
+ price: tuple[float, ...],
+ des: tuple[str, ...],
+ discount: tuple[float, ...],
+ limit_time: tuple[int, ...],
+ load_status: tuple[bool, ...],
+ daily_limit: tuple[int, ...],
+ is_passive: tuple[bool, ...],
+ icon: tuple[str, ...],
+ send_success_msg: tuple[bool, ...],
+ max_num_limit: tuple[int, ...],
+ **kwargs,
+ ):
+ """注册商品
+
+ 参数:
+ name: 商品名称
+ price: 价格
+ des: 简介
+ discount: 折扣
+ limit_time: 售卖限时时间
+ load_status: 是否加载
+ daily_limit: 每日限购
+ is_passive: 是否被动道具
+ icon: 图标
+ send_success_msg: 成功时发送消息
+ max_num_limit: 单次最大使用次数
+ """
+
+ def add_register_item(func: Callable):
+ if name in self._data.keys():
+ raise ValueError("该商品已注册,请替换其他名称!")
+ for n, p, d, dd, l, s, dl, pa, i, ssm, mnl in zip(
+ name,
+ price,
+ des,
+ discount,
+ limit_time,
+ load_status,
+ daily_limit,
+ is_passive,
+ icon,
+ send_success_msg,
+ max_num_limit,
+ ):
+ if s:
+ _temp_kwargs = {}
+ for key, value in kwargs.items():
+ if key.startswith(f"{n}_"):
+ _temp_kwargs[key.split("_", maxsplit=1)[-1]] = value
+ else:
+ _temp_kwargs[key] = value
+ goods = self._data.get(n) or Goods(
+ price=p,
+ des=d,
+ discount=dd,
+ limit_time=l,
+ daily_limit=dl,
+ is_passive=pa,
+ func=func,
+ send_success_msg=ssm,
+ max_num_limit=mnl,
+ )
+ goods.price = p
+ goods.des = d
+ goods.discount = dd
+ goods.limit_time = l
+ goods.daily_limit = dl
+ goods.icon = i
+ goods.is_passive = pa
+ goods.func = func
+ goods.kwargs = _temp_kwargs
+ goods.send_success_msg = ssm
+ goods.max_num_limit = mnl
+ self._data[n] = goods
+ return func
+
+ return lambda func: add_register_item(func)
+
+ async def load_register(self):
+ require("shop")
+ from zhenxun.builtin_plugins.shop._data_source import ShopManage
+
+ # 统一进行注册
+ if self._flag:
+ # 只进行一次注册
+ self._flag = False
+ for name in self._data.keys():
+ if goods := self._data.get(name):
+ uuid = await GoodsInfo.add_goods(
+ name,
+ goods.price,
+ goods.des,
+ goods.discount,
+ goods.limit_time,
+ goods.daily_limit,
+ goods.is_passive,
+ goods.icon,
+ )
+ if uuid:
+ await ShopManage.register_use(
+ name,
+ uuid,
+ goods.func,
+ goods.send_success_msg,
+ goods.max_num_limit,
+ goods.before_handle,
+ goods.after_handle,
+ **self._data[name].kwargs,
+ )
+
+ def __call__(
+ self,
+ name: str | tuple[str, ...],
+ price: float | tuple[float, ...],
+ des: str | tuple[str, ...],
+ discount: float | tuple[float, ...] = 1,
+ limit_time: int | tuple[int, ...] = 0,
+ load_status: bool | tuple[bool, ...] = True,
+ daily_limit: int | tuple[int, ...] = 0,
+ is_passive: bool | tuple[bool, ...] = False,
+ icon: str | tuple[str, ...] = "",
+ send_success_msg: bool | tuple[bool, ...] = True,
+ max_num_limit: int | tuple[int, ...] = 1,
+ **kwargs,
+ ):
+ """注册商品
+
+ 参数:
+ name: 商品名称
+ price: 价格
+ des: 简介
+ discount: 折扣
+ limit_time: 售卖限时时间
+ load_status: 是否加载
+ daily_limit: 每日限购
+ is_passive: 是否被动道具
+ icon: 图标
+ send_success_msg: 成功时发送消息
+ max_num_limit: 单次最大使用次数
+ """
+ _tuple_list = []
+ _current_len = -1
+ for x in [name, price, des, discount, limit_time, load_status]:
+ if isinstance(x, tuple):
+ if _current_len == -1:
+ _current_len = len(x)
+ if _current_len != len(x):
+ raise ValueError(
+ f"注册商品 {name} 中 name,price,des,discount,limit_time,load_status,daily_limit 数量不符!"
+ )
+ _current_len = _current_len if _current_len > -1 else 1
+ _name = self.__get(name, _current_len)
+ _price = self.__get(price, _current_len)
+ _discount = self.__get(discount, _current_len)
+ _limit_time = self.__get(limit_time, _current_len)
+ _des = self.__get(des, _current_len)
+ _load_status = self.__get(load_status, _current_len)
+ _daily_limit = self.__get(daily_limit, _current_len)
+ _is_passive = self.__get(is_passive, _current_len)
+ _icon = self.__get(icon, _current_len)
+ _send_success_msg = self.__get(send_success_msg, _current_len)
+ _max_num_limit = self.__get(max_num_limit, _current_len)
+ return self.register(
+ _name,
+ _price,
+ _des,
+ _discount,
+ _limit_time,
+ _load_status,
+ _daily_limit,
+ _is_passive,
+ _icon,
+ _send_success_msg,
+ _max_num_limit,
+ **kwargs,
+ )
+
+ def __get(self, value, _current_len):
+ return (
+ value
+ if isinstance(value, tuple)
+ else tuple([value for _ in range(_current_len)])
+ )
+
+ def __setitem__(self, key, value):
+ self._data[key] = value
+
+ def __getitem__(self, key):
+ return self._data[key]
+
+ def __contains__(self, key):
+ return key in self._data
+
+ def __str__(self):
+ return str(self._data)
+
+ def keys(self):
+ return self._data.keys()
+
+ def values(self):
+ return self._data.values()
+
+ def items(self):
+ return self._data.items()
+
+
+class NotMeetUseConditionsException(Exception):
+ """
+ 不满足条件异常类
+ """
+
+ def __init__(self, info: str | MessageSegment | Message | None):
+ super().__init__(self)
+ self._info = info
+
+ def get_info(self):
+ return self._info
+
+
+shop_register = ShopRegister()
diff --git a/zhenxun/utils/depends/__init__.py b/zhenxun/utils/depends/__init__.py
new file mode 100644
index 00000000..ca6b1ba4
--- /dev/null
+++ b/zhenxun/utils/depends/__init__.py
@@ -0,0 +1,108 @@
+from typing import Any
+
+from nonebot.internal.params import Depends
+from nonebot.matcher import Matcher
+from nonebot.params import Command
+from nonebot_plugin_session import EventSession
+from nonebot_plugin_userinfo import EventUserInfo, UserInfo
+
+from zhenxun.configs.config import Config
+from zhenxun.utils.message import MessageUtils
+
+
+def CheckUg(check_user: bool = True, check_group: bool = True):
+ """检测群组id和用户id是否存在
+
+ 参数:
+ check_user: 检查用户id.
+ check_group: 检查群组id.
+ """
+
+ async def dependency(session: EventSession):
+ if check_user:
+ user_id = session.id1
+ if not user_id:
+ await MessageUtils.build_message("用户id为空").finish()
+ if check_group:
+ group_id = session.id3 or session.id2
+ if not group_id:
+ await MessageUtils.build_message("群组id为空").finish()
+
+ return Depends(dependency)
+
+
+def OneCommand():
+ """
+ 获取单个命令Command
+ """
+
+ async def dependency(
+ cmd: tuple[str, ...] = Command(),
+ ):
+ return cmd[0] if cmd else None
+
+ return Depends(dependency)
+
+
+def UserName():
+ """
+ 用户名称
+ """
+
+ async def dependency(user_info: UserInfo = EventUserInfo()):
+ return (
+ user_info.user_displayname or user_info.user_remark or user_info.user_name
+ ) or ""
+
+ return Depends(dependency)
+
+
+def GetConfig(
+ module: str | None = None,
+ config: str = "",
+ default_value: Any = None,
+ prompt: str | None = None,
+):
+ """获取配置项
+
+ 参数:
+ module: 模块名,为空时默认使用当前插件模块名
+ config: 配置项名称
+ default_value: 默认值
+ prompt: 为空时提示
+ """
+
+ async def dependency(matcher: Matcher):
+ module_ = module or matcher.plugin_name
+ if module_:
+ value = Config.get_config(module_, config, default_value)
+ if value is None and prompt:
+ # await matcher.finish(prompt or f"配置项 {config} 未填写!")
+ await matcher.finish(prompt)
+ return value
+
+ return Depends(dependency)
+
+
+def CheckConfig(
+ module: str | None = None,
+ config: str | list[str] = "",
+ prompt: str | None = None,
+):
+ """检测配置项在配置文件中是否填写
+
+ 参数:
+ module: 模块名,为空时默认使用当前插件模块名
+ config: 需要检查的配置项名称
+ prompt: 为空时提示
+ """
+
+ async def dependency(matcher: Matcher):
+ module_ = module or matcher.plugin_name
+ if module_:
+ config_list = [config] if isinstance(config, str) else config
+ for c in config_list:
+ if Config.get_config(module_, c) is None:
+ await matcher.finish(prompt or f"配置项 {c} 未填写!")
+
+ return Depends(dependency)
diff --git a/zhenxun/utils/enum.py b/zhenxun/utils/enum.py
new file mode 100644
index 00000000..bf6c5daa
--- /dev/null
+++ b/zhenxun/utils/enum.py
@@ -0,0 +1,101 @@
+from strenum import StrEnum
+
+
+class GoldHandle(StrEnum):
+ """
+ 金币处理
+ """
+
+ BUY = "BUY"
+ """购买"""
+ GET = "GET"
+ """获取"""
+ PLUGIN = "PLUGIN"
+ """插件花费"""
+
+
+class PropHandle(StrEnum):
+ """
+ 道具处理
+ """
+
+ BUY = "BUY"
+ """购买"""
+ USE = "USE"
+ """使用"""
+
+
+class PluginType(StrEnum):
+ """
+ 插件类型
+ """
+
+ SUPERUSER = "SUPERUSER"
+ ADMIN = "ADMIN"
+ SUPER_AND_ADMIN = "ADMIN_SUPER"
+ NORMAL = "NORMAL"
+ HIDDEN = "HIDDEN"
+
+
+class BlockType(StrEnum):
+ """
+ 禁用状态
+ """
+
+ PRIVATE = "PRIVATE"
+ GROUP = "GROUP"
+ ALL = "ALL"
+
+
+class PluginLimitType(StrEnum):
+ """
+ 插件限制类型
+ """
+
+ CD = "CD"
+ COUNT = "COUNT"
+ BLOCK = "BLOCK"
+
+
+class LimitCheckType(StrEnum):
+ """
+ 插件限制类型
+ """
+
+ PRIVATE = "PRIVATE"
+ GROUP = "GROUP"
+ ALL = "ALL"
+
+
+class LimitWatchType(StrEnum):
+ """
+ 插件限制监听对象
+ """
+
+ USER = "USER"
+ GROUP = "GROUP"
+ ALL = "ALL"
+
+
+class RequestType(StrEnum):
+ """
+ 请求类型
+ """
+
+ FRIEND = "FRIEND"
+ GROUP = "GROUP"
+
+
+class RequestHandleType(StrEnum):
+ """
+ 请求处理类型
+ """
+
+ APPROVE = "APPROVE"
+ """同意"""
+ REFUSED = "REFUSED"
+ """拒绝"""
+ IGNORE = "IGNORE"
+ """忽略"""
+ EXPIRE = "EXPIRE"
+ """过期或失效"""
diff --git a/zhenxun/utils/exception.py b/zhenxun/utils/exception.py
new file mode 100644
index 00000000..dbade4e4
--- /dev/null
+++ b/zhenxun/utils/exception.py
@@ -0,0 +1,46 @@
+class NotFoundError(Exception):
+ """
+ 未发现
+ """
+
+ pass
+
+
+class GroupInfoNotFound(Exception):
+ """
+ 群组未找到
+ """
+
+ pass
+
+
+class EmptyError(Exception):
+ """
+ 空错误
+ """
+
+ pass
+
+
+class UserAndGroupIsNone(Exception):
+ """
+ 用户和群组为空
+ """
+
+ pass
+
+
+class InsufficientGold(Exception):
+ """
+ 金币不足
+ """
+
+ pass
+
+
+class NotFindSuperuser(Exception):
+ """
+ 未找到超级用户
+ """
+
+ pass
diff --git a/utils/http_utils.py b/zhenxun/utils/http_utils.py
similarity index 63%
rename from utils/http_utils.py
rename to zhenxun/utils/http_utils.py
index ad542dcc..b751f9cd 100644
--- a/utils/http_utils.py
+++ b/zhenxun/utils/http_utils.py
@@ -2,27 +2,28 @@ import asyncio
from asyncio.exceptions import TimeoutError
from contextlib import asynccontextmanager
from pathlib import Path
-from typing import Any, AsyncGenerator, Dict, List, Literal, Optional, Union
+from typing import Any, AsyncGenerator, Dict, Literal
import aiofiles
import httpx
import rich
from httpx import ConnectTimeout, Response
-from nonebot.adapters.onebot.v11 import MessageSegment
-from playwright.async_api import BrowserContext, Page
+from nonebot import require
+from nonebot_plugin_alconna import UniMessage
+from playwright.async_api import Page
from retrying import retry
-from services.log import logger
-from utils.user_agent import get_user_agent, get_user_agent_str
+from zhenxun.configs.config import SYSTEM_PROXY
+from zhenxun.services.log import logger
+from zhenxun.utils.message import MessageUtils
+from zhenxun.utils.user_agent import get_user_agent
from .browser import get_browser
-from .message_builder import image
-from .utils import get_local_proxy
class AsyncHttpx:
- proxy = {"http://": get_local_proxy(), "https://": get_local_proxy()}
+ proxy = {"http://": SYSTEM_PROXY, "https://": SYSTEM_PROXY}
@classmethod
@retry(stop_max_attempt_number=3)
@@ -30,32 +31,31 @@ class AsyncHttpx:
cls,
url: str,
*,
- params: Optional[Dict[str, Any]] = None,
- headers: Optional[Dict[str, str]] = None,
- cookies: Optional[Dict[str, str]] = None,
+ params: Dict[str, Any] | None = None,
+ headers: Dict[str, str] | None = None,
+ cookies: Dict[str, str] | None = None,
verify: bool = True,
use_proxy: bool = True,
- proxy: Optional[Dict[str, str]] = None,
- timeout: Optional[int] = 30,
+ proxy: Dict[str, str] | None = None,
+ timeout: int = 30,
**kwargs,
) -> Response:
- """
- 说明:
- Get
+ """Get
+
参数:
- :param url: url
- :param params: params
- :param headers: 请求头
- :param cookies: cookies
- :param verify: verify
- :param use_proxy: 使用默认代理
- :param proxy: 指定代理
- :param timeout: 超时时间
+ url: url
+ params: params
+ headers: 请求头
+ cookies: cookies
+ verify: verify
+ use_proxy: 使用默认代理
+ proxy: 指定代理
+ timeout: 超时时间
"""
if not headers:
headers = get_user_agent()
- proxy_ = proxy if proxy else cls.proxy if use_proxy else None
- async with httpx.AsyncClient(proxies=proxy_, verify=verify) as client:
+ _proxy = proxy if proxy else cls.proxy if use_proxy else None
+ async with httpx.AsyncClient(proxies=_proxy, verify=verify) as client: # type: ignore
return await client.get(
url,
params=params,
@@ -70,39 +70,39 @@ class AsyncHttpx:
cls,
url: str,
*,
- data: Optional[Dict[str, str]] = None,
+ data: Dict[str, Any] | None = None,
content: Any = None,
files: Any = None,
verify: bool = True,
use_proxy: bool = True,
- proxy: Optional[Dict[str, str]] = None,
- json: Optional[Dict[str, Any]] = None,
- params: Optional[Dict[str, str]] = None,
- headers: Optional[Dict[str, str]] = None,
- cookies: Optional[Dict[str, str]] = None,
- timeout: Optional[int] = 30,
+ proxy: Dict[str, str] | None = None,
+ json: Dict[str, Any] | None = None,
+ params: Dict[str, str] | None = None,
+ headers: Dict[str, str] | None = None,
+ cookies: Dict[str, str] | None = None,
+ timeout: int = 30,
**kwargs,
) -> Response:
"""
说明:
Post
参数:
- :param url: url
- :param data: data
- :param content: content
- :param files: files
- :param use_proxy: 是否默认代理
- :param proxy: 指定代理
- :param json: json
- :param params: params
- :param headers: 请求头
- :param cookies: cookies
- :param timeout: 超时时间
+ url: url
+ data: data
+ content: content
+ files: files
+ use_proxy: 是否默认代理
+ proxy: 指定代理
+ json: json
+ params: params
+ headers: 请求头
+ cookies: cookies
+ timeout: 超时时间
"""
if not headers:
headers = get_user_agent()
- proxy_ = proxy if proxy else cls.proxy if use_proxy else None
- async with httpx.AsyncClient(proxies=proxy_, verify=verify) as client:
+ _proxy = proxy if proxy else cls.proxy if use_proxy else None
+ async with httpx.AsyncClient(proxies=_proxy, verify=verify) as client: # type: ignore
return await client.post(
url,
content=content,
@@ -120,32 +120,31 @@ class AsyncHttpx:
async def download_file(
cls,
url: str,
- path: Union[str, Path],
+ path: str | Path,
*,
- params: Optional[Dict[str, str]] = None,
+ params: Dict[str, str] | None = None,
verify: bool = True,
use_proxy: bool = True,
- proxy: Optional[Dict[str, str]] = None,
- headers: Optional[Dict[str, str]] = None,
- cookies: Optional[Dict[str, str]] = None,
- timeout: Optional[int] = 30,
+ proxy: Dict[str, str] | None = None,
+ headers: Dict[str, str] | None = None,
+ cookies: Dict[str, str] | None = None,
+ timeout: int = 30,
stream: bool = False,
**kwargs,
) -> bool:
- """
- 说明:
- 下载文件
+ """下载文件
+
参数:
- :param url: url
- :param path: 存储路径
- :param params: params
- :param verify: verify
- :param use_proxy: 使用代理
- :param proxy: 指定代理
- :param headers: 请求头
- :param cookies: cookies
- :param timeout: 超时时间
- :param stream: 是否使用流式下载(流式写入+进度条,适用于下载大文件)
+ url: url
+ path: 存储路径
+ params: params
+ verify: verify
+ use_proxy: 使用代理
+ proxy: 指定代理
+ headers: 请求头
+ cookies: cookies
+ timeout: 超时时间
+ stream: 是否使用流式下载(流式写入+进度条,适用于下载大文件)
"""
if isinstance(path, str):
path = Path(path)
@@ -175,10 +174,10 @@ class AsyncHttpx:
else:
if not headers:
headers = get_user_agent()
- proxy_ = proxy if proxy else cls.proxy if use_proxy else None
+ _proxy = proxy if proxy else cls.proxy if use_proxy else None
try:
async with httpx.AsyncClient(
- proxies=proxy_, verify=verify
+ proxies=_proxy, verify=verify # type: ignore
) as client:
async with client.stream(
"GET",
@@ -194,12 +193,12 @@ class AsyncHttpx:
)
async with aiofiles.open(path, "wb") as wf:
total = int(response.headers["Content-Length"])
- with rich.progress.Progress(
- rich.progress.TextColumn(path.name),
- "[progress.percentage]{task.percentage:>3.0f}%",
- rich.progress.BarColumn(bar_width=None),
- rich.progress.DownloadColumn(),
- rich.progress.TransferSpeedColumn(),
+ with rich.progress.Progress( # type: ignore
+ rich.progress.TextColumn(path.name), # type: ignore
+ "[progress.percentage]{task.percentage:>3.0f}%", # type: ignore
+ rich.progress.BarColumn(bar_width=None), # type: ignore
+ rich.progress.DownloadColumn(), # type: ignore
+ rich.progress.TransferSpeedColumn(), # type: ignore
) as progress:
download_task = progress.add_task(
"Download", total=total
@@ -211,7 +210,9 @@ class AsyncHttpx:
download_task,
completed=response.num_bytes_downloaded,
)
- logger.info(f"下载 {url} 成功.. Path:{path.absolute()}")
+ logger.info(
+ f"下载 {url} 成功.. Path:{path.absolute()}"
+ )
return True
except (TimeoutError, ConnectTimeout):
pass
@@ -224,31 +225,30 @@ class AsyncHttpx:
@classmethod
async def gather_download_file(
cls,
- url_list: List[str],
- path_list: List[Union[str, Path]],
+ url_list: list[str],
+ path_list: list[str | Path],
*,
- limit_async_number: Optional[int] = None,
- params: Optional[Dict[str, str]] = None,
+ limit_async_number: int | None = None,
+ params: Dict[str, str] | None = None,
use_proxy: bool = True,
- proxy: Optional[Dict[str, str]] = None,
- headers: Optional[Dict[str, str]] = None,
- cookies: Optional[Dict[str, str]] = None,
- timeout: Optional[int] = 30,
+ proxy: Dict[str, str] | None = None,
+ headers: Dict[str, str] | None = None,
+ cookies: Dict[str, str] | None = None,
+ timeout: int = 30,
**kwargs,
- ) -> List[bool]:
- """
- 说明:
- 分组同时下载文件
+ ) -> list[bool]:
+ """分组同时下载文件
+
参数:
- :param url_list: url列表
- :param path_list: 存储路径列表
- :param limit_async_number: 限制同时请求数量
- :param params: params
- :param use_proxy: 使用代理
- :param proxy: 指定代理
- :param headers: 请求头
- :param cookies: cookies
- :param timeout: 超时时间
+ url_list: url列表
+ path_list: 存储路径列表
+ limit_async_number: 限制同时请求数量
+ params: params
+ use_proxy: 使用代理
+ proxy: 指定代理
+ headers: 请求头
+ cookies: cookies
+ timeout: 超时时间
"""
if n := len(url_list) != len(path_list):
raise UrlPathNumberNotEqual(
@@ -300,11 +300,10 @@ class AsyncPlaywright:
@classmethod
@asynccontextmanager
async def new_page(cls, **kwargs) -> AsyncGenerator[Page, None]:
- """
- 说明:
- 获取一个新页面
+ """获取一个新页面
+
参数:
- :param user_agent: 请求头
+ user_agent: 请求头
"""
browser = get_browser()
ctx = await browser.new_context(**kwargs)
@@ -319,31 +318,30 @@ class AsyncPlaywright:
async def screenshot(
cls,
url: str,
- path: Union[Path, str],
- element: Union[str, List[str]],
+ path: Path | str,
+ element: str | list[str],
*,
- wait_time: Optional[int] = None,
- viewport_size: Optional[Dict[str, int]] = None,
- wait_until: Optional[
- Literal["domcontentloaded", "load", "networkidle"]
- ] = "networkidle",
- timeout: Optional[float] = None,
- type_: Optional[Literal["jpeg", "png"]] = None,
- user_agent: Optional[str] = None,
+ wait_time: int | None = None,
+ viewport_size: Dict[str, int] | None = None,
+ wait_until: (
+ Literal["domcontentloaded", "load", "networkidle"] | None
+ ) = "networkidle",
+ timeout: float | None = None,
+ type_: Literal["jpeg", "png"] | None = None,
+ user_agent: str | None = None,
**kwargs,
- ) -> Optional[MessageSegment]:
- """
- 说明:
- 截图,该方法仅用于简单快捷截图,复杂截图请操作 page
+ ) -> UniMessage | None:
+ """截图,该方法仅用于简单快捷截图,复杂截图请操作 page
+
参数:
- :param url: 网址
- :param path: 存储路径
- :param element: 元素选择
- :param wait_time: 等待截取超时时间
- :param viewport_size: 窗口大小
- :param wait_until: 等待类型
- :param timeout: 超时限制
- :param type_: 保存类型
+ url: 网址
+ path: 存储路径
+ element: 元素选择
+ wait_time: 等待截取超时时间
+ viewport_size: 窗口大小
+ wait_until: 等待类型
+ timeout: 超时限制
+ type_: 保存类型
"""
if viewport_size is None:
viewport_size = dict(width=2560, height=1080)
@@ -367,7 +365,7 @@ class AsyncPlaywright:
card = await card.wait_for_selector(e, timeout=wait_time)
if card:
await card.screenshot(path=path, timeout=timeout, type=type_)
- return image(path)
+ return MessageUtils.build_message(path)
return None
diff --git a/zhenxun/utils/image_utils.py b/zhenxun/utils/image_utils.py
new file mode 100644
index 00000000..c193a565
--- /dev/null
+++ b/zhenxun/utils/image_utils.py
@@ -0,0 +1,422 @@
+import os
+import random
+import re
+from io import BytesIO
+from pathlib import Path
+from typing import Awaitable, Callable
+
+import cv2
+import imagehash
+from imagehash import ImageHash
+from nonebot.utils import is_coroutine_callable
+from PIL import Image
+
+from zhenxun.configs.path_config import IMAGE_PATH, TEMP_PATH
+from zhenxun.services.log import logger
+from zhenxun.utils.http_utils import AsyncHttpx
+
+from ._build_image import BuildImage, ColorAlias
+from ._build_mat import BuildMat, MatType
+from ._image_template import ImageTemplate, RowStyle
+
+# TODO: text2image 长度错误
+
+
+async def text2image(
+ text: str,
+ auto_parse: bool = True,
+ font_size: int = 20,
+ color: str | tuple[int, int, int] = (255, 255, 255),
+ font: str = "HYWenHei-85W.ttf",
+ font_color: str | tuple[int, int, int] = (0, 0, 0),
+ padding: int | tuple[int, int, int, int] = 0,
+ _add_height: float = 0,
+) -> BuildImage:
+ """解析文本并转为图片
+ 使用标签
+
+ 可选配置项
+ font: str -> 特殊文本字体
+ fs / font_size: int -> 特殊文本大小
+ fc / font_color: Union[str, Tuple[int, int, int]] -> 特殊文本颜色
+ 示例
+ 在不在,HibiKi小姐,
+ 你最近还好吗,我非常想你,这段时间我非常不好过,
+ 抽卡抽不到金色,这让我很痛苦
+ 参数:
+ text: 文本
+ auto_parse: 是否自动解析,否则原样发送
+ font_size: 普通字体大小
+ color: 背景颜色
+ font: 普通字体
+ font_color: 普通字体颜色
+ padding: 文本外边距,元组类型时为 (上,左,下,右)
+ _add_height: 由于get_size无法返回正确的高度,采用手动方式额外添加高度
+ """
+ if not text:
+ raise ValueError("文本转图片 text 不能为空...")
+ pw = ph = top_padding = left_padding = 0
+ if padding:
+ if isinstance(padding, int):
+ pw = padding * 2
+ ph = padding * 2
+ top_padding = left_padding = padding
+ elif isinstance(padding, tuple):
+ pw = padding[0] + padding[2]
+ ph = padding[1] + padding[3]
+ top_padding = padding[0]
+ left_padding = padding[1]
+ _font = BuildImage.load_font(font, font_size)
+ if auto_parse and re.search(r"(.*)", text):
+ _data = []
+ new_text = ""
+ placeholder_index = 0
+ for s in text.split(""):
+ r = re.search(r"(.*)", s)
+ if r:
+ start, end = r.span()
+ if start != 0 and (t := s[:start]):
+ new_text += t
+ _data.append(
+ [
+ (start, end),
+ f"[placeholder_{placeholder_index}]",
+ r.group(1).strip(),
+ r.group(2),
+ ]
+ )
+ new_text += f"[placeholder_{placeholder_index}]"
+ placeholder_index += 1
+ new_text += text.split("")[-1]
+ image_list = []
+ current_placeholder_index = 0
+ # 切分换行,每行为单张图片
+ for s in new_text.split("\n"):
+ _tmp_text = s
+ img_width = 0
+ img_height = BuildImage.get_text_size("正", _font)[1]
+ _tmp_index = current_placeholder_index
+ for _ in range(s.count("[placeholder_")):
+ placeholder = _data[_tmp_index]
+ if "font_size" in placeholder[2]:
+ r = re.search(r"font_size=['\"]?(\d+)", placeholder[2])
+ if r:
+ w, h = BuildImage.get_text_size(
+ placeholder[3], font, int(r.group(1))
+ )
+ img_height = img_height if img_height > h else h
+ img_width += w
+ else:
+ img_width += BuildImage.get_text_size(placeholder[3], _font)[0]
+ _tmp_text = _tmp_text.replace(f"[placeholder_{_tmp_index}]", "")
+ _tmp_index += 1
+ img_width += BuildImage.get_text_size(_tmp_text, _font)[0]
+ # 开始画图
+ A = BuildImage(
+ img_width, img_height, color=color, font=font, font_size=font_size
+ )
+ basic_font_h = A.getsize("正")[1]
+ current_width = 0
+ # 遍历占位符
+ for _ in range(s.count("[placeholder_")):
+ if not s.startswith(f"[placeholder_{current_placeholder_index}]"):
+ slice_ = s.split(f"[placeholder_{current_placeholder_index}]")
+ await A.text(
+ (current_width, A.height - basic_font_h - 1),
+ slice_[0],
+ font_color,
+ )
+ current_width += A.getsize(slice_[0])[0]
+ placeholder = _data[current_placeholder_index]
+ # 解析配置
+ _font = font
+ _font_size = font_size
+ _font_color = font_color
+ for e in placeholder[2].split():
+ if e.startswith("font="):
+ _font = e.split("=")[-1]
+ if e.startswith("font_size=") or e.startswith("fs="):
+ _font_size = int(e.split("=")[-1])
+ if _font_size > 1000:
+ _font_size = 1000
+ if _font_size < 1:
+ _font_size = 1
+ if e.startswith("font_color") or e.startswith("fc="):
+ _font_color = e.split("=")[-1]
+ text_img = await BuildImage.build_text_image(
+ placeholder[3], font=_font, size=_font_size, font_color=_font_color
+ )
+ _img_h = (
+ int(A.height / 2 - text_img.height / 2)
+ if new_text == "[placeholder_0]"
+ else A.height - text_img.height
+ )
+ await A.paste(text_img, (current_width, _img_h - 1))
+ current_width += text_img.width
+ s = s[
+ s.index(f"[placeholder_{current_placeholder_index}]")
+ + len(f"[placeholder_{current_placeholder_index}]") :
+ ]
+ current_placeholder_index += 1
+ if s:
+ slice_ = s.split(f"[placeholder_{current_placeholder_index}]")
+ await A.text((current_width, A.height - basic_font_h), slice_[0])
+ current_width += A.getsize(slice_[0])[0]
+ await A.crop((0, 0, current_width, A.height))
+ # A.show()
+ image_list.append(A)
+ height = 0
+ width = 0
+ for img in image_list:
+ height += img.h
+ width = width if width > img.w else img.w
+ width += pw
+ height += ph
+ A = BuildImage(width + left_padding, height + top_padding, color=color)
+ current_height = top_padding
+ for img in image_list:
+ await A.paste(img, (left_padding, current_height))
+ current_height += img.h
+ else:
+ width = 0
+ height = 0
+ _, h = BuildImage.get_text_size("正", _font)
+ line_height = int(font_size / 3)
+ image_list = []
+ for s in text.split("\n"):
+ w, _ = BuildImage.get_text_size(s.strip() or "正", _font)
+ height += h + line_height
+ width = width if width > w else w
+ image_list.append(
+ await BuildImage.build_text_image(
+ s.strip(), font, font_size, font_color
+ )
+ )
+ width += pw
+ height += ph
+ A = BuildImage(
+ width + left_padding,
+ height + top_padding + 2,
+ color=color,
+ )
+ cur_h = ph
+ for img in image_list:
+ await A.paste(img, (pw, cur_h))
+ cur_h += img.height + line_height
+ return A
+
+
+def group_image(image_list: list[BuildImage]) -> tuple[list[list[BuildImage]], int]:
+ """
+ 说明:
+ 根据图片大小进行分组
+ 参数:
+ image_list: 排序图片列表
+ """
+ image_list.sort(key=lambda x: x.height, reverse=True)
+ max_image = max(image_list, key=lambda x: x.height)
+
+ image_list.remove(max_image)
+ max_h = max_image.height
+ total_w = 0
+
+ # 图片分组
+ image_group = [[max_image]]
+ is_use = []
+ surplus_list = image_list[:]
+
+ for image in image_list:
+ if image.uid not in is_use:
+ group = [image]
+ is_use.append(image.uid)
+ curr_h = image.height
+ while True:
+ surplus_list = [x for x in surplus_list if x.uid not in is_use]
+ for tmp in surplus_list:
+ temp_h = curr_h + tmp.height + 10
+ if temp_h < max_h or abs(max_h - temp_h) < 100:
+ curr_h += tmp.height + 15
+ is_use.append(tmp.uid)
+ group.append(tmp)
+ break
+ else:
+ break
+ total_w += max([x.width for x in group]) + 15
+ image_group.append(group)
+ while surplus_list:
+ surplus_list = [x for x in surplus_list if x.uid not in is_use]
+ if not surplus_list:
+ break
+ surplus_list.sort(key=lambda x: x.height, reverse=True)
+ for img in surplus_list:
+ if img.uid not in is_use:
+ _w = 0
+ index = -1
+ for i, ig in enumerate(image_group):
+ if s := sum([x.height for x in ig]) > _w:
+ _w = s
+ index = i
+ if index != -1:
+ image_group[index].append(img)
+ is_use.append(img.uid)
+
+ max_h = 0
+ max_w = 0
+ for ig in image_group:
+ if (_h := sum([x.height + 15 for x in ig])) > max_h:
+ max_h = _h
+ max_w += max([x.width for x in ig]) + 30
+ is_use.clear()
+ while abs(max_h - max_w) > 200 and len(image_group) - 1 >= len(image_group[-1]):
+ for img in image_group[-1]:
+ _min_h = 999999
+ _min_index = -1
+ for i, ig in enumerate(image_group):
+ # if i not in is_use and (_h := sum([x.h for x in ig]) + img.h) > _min_h:
+ if (_h := sum([x.height for x in ig]) + img.height) < _min_h:
+ _min_h = _h
+ _min_index = i
+ is_use.append(_min_index)
+ image_group[_min_index].append(img)
+ max_w -= max([x.width for x in image_group[-1]]) - 30
+ image_group.pop(-1)
+ max_h = max([sum([x.height + 15 for x in ig]) for ig in image_group])
+ return image_group, max(max_h + 250, max_w + 70)
+
+
+async def build_sort_image(
+ image_group: list[list[BuildImage]],
+ h: int | None = None,
+ padding_top: int = 200,
+ color: ColorAlias = (
+ 255,
+ 255,
+ 255,
+ ),
+ background_path: Path | None = None,
+ background_handle: Callable[[BuildImage], Awaitable] | None = None,
+) -> BuildImage:
+ """
+ 说明:
+ 对group_image的图片进行组装
+ 参数:
+ image_group: 分组图片列表
+ h: max(宽,高),一般为group_image的返回值,有值时,图片必定为正方形
+ padding_top: 图像列表与最顶层间距
+ color: 背景颜色
+ background_path: 背景图片文件夹路径(随机)
+ background_handle: 背景图额外操作
+ """
+ bk_file = None
+ if background_path:
+ random_bk = os.listdir(background_path)
+ if random_bk:
+ bk_file = random.choice(random_bk)
+ image_w = 0
+ image_h = 0
+ if not h:
+ for ig in image_group:
+ _w = max([x.width + 30 for x in ig])
+ image_w += _w + 30
+ _h = sum([x.height + 10 for x in ig])
+ if _h > image_h:
+ image_h = _h
+ image_h += padding_top
+ else:
+ image_w = h
+ image_h = h
+ A = BuildImage(
+ image_w,
+ image_h,
+ font_size=24,
+ font="CJGaoDeGuo.otf",
+ color=color,
+ background=(background_path / bk_file) if background_path and bk_file else None,
+ )
+ if background_handle:
+ if is_coroutine_callable(background_handle):
+ await background_handle(A)
+ else:
+ background_handle(A)
+ curr_w = 50
+ for ig in image_group:
+ curr_h = padding_top - 20
+ for img in ig:
+ await A.paste(img, (curr_w, curr_h))
+ curr_h += img.height + 10
+ curr_w += max([x.width for x in ig]) + 30
+ return A
+
+
+def compressed_image(
+ in_file: str | Path,
+ out_file: str | Path | None = None,
+ ratio: float = 0.9,
+):
+ """压缩图片
+
+ 参数:
+ in_file: 被压缩的文件路径
+ out_file: 压缩后输出的文件路径
+ ratio: 压缩率,宽高 * 压缩率
+ """
+ in_file = IMAGE_PATH / in_file if isinstance(in_file, str) else in_file
+ if out_file:
+ out_file = IMAGE_PATH / out_file if isinstance(out_file, str) else out_file
+ else:
+ out_file = in_file
+ h, w, d = cv2.imread(str(in_file.absolute())).shape
+ img = cv2.resize(
+ cv2.imread(str(in_file.absolute())), (int(w * ratio), int(h * ratio))
+ )
+ cv2.imwrite(str(out_file.absolute()), img)
+
+
+def get_img_hash(image_file: str | Path) -> str:
+ """获取图片的hash值
+
+ 参数:
+ image_file: 图片文件路径
+
+ 返回:
+ str: 哈希值
+ """
+ hash_value = ""
+ try:
+ with open(image_file, "rb") as fp:
+ hash_value = imagehash.average_hash(Image.open(fp))
+ except Exception as e:
+ logger.warning(f"获取图片Hash出错", "禁言检测", e=e)
+ return str(hash_value)
+
+
+async def get_download_image_hash(url: str, mark: str) -> str:
+ """下载图片获取哈希值
+
+ 参数:
+ url: 图片url
+ mark: 随机标志符
+
+ 返回:
+ str: 哈希值
+ """
+ try:
+ if await AsyncHttpx.download_file(
+ url, TEMP_PATH / f"compare_download_{mark}_img.jpg"
+ ):
+ img_hash = get_img_hash(TEMP_PATH / f"compare_download_{mark}_img.jpg")
+ return str(img_hash)
+ except Exception as e:
+ logger.warning(f"下载读取图片Hash出错", e=e)
+ return ""
+
+
+def pic2bytes(image) -> bytes:
+ """获取bytes
+
+ 返回:
+ bytes: bytes
+ """
+ buf = BytesIO()
+ image.save(buf, format="PNG")
+ return buf.getvalue()
diff --git a/zhenxun/utils/message.py b/zhenxun/utils/message.py
new file mode 100644
index 00000000..54e7f908
--- /dev/null
+++ b/zhenxun/utils/message.py
@@ -0,0 +1,136 @@
+from io import BytesIO
+from pathlib import Path
+
+import nonebot
+from nonebot.adapters.onebot.v11 import Message, MessageSegment
+from nonebot_plugin_alconna import At, Image, Text, UniMessage
+
+from zhenxun.configs.config import NICKNAME
+from zhenxun.services.log import logger
+from zhenxun.utils._build_image import BuildImage
+
+driver = nonebot.get_driver()
+
+MESSAGE_TYPE = (
+ str | int | float | Path | bytes | BytesIO | BuildImage | At | Image | Text
+)
+
+
+class MessageUtils:
+
+ @classmethod
+ def __build_message(cls, msg_list: list[MESSAGE_TYPE]) -> list[Text | Image]:
+ """构造消息
+
+ 参数:
+ msg_list: 消息列表
+
+ 返回:
+ list[Text | Text]: 构造完成的消息列表
+ """
+ is_bytes = False
+ try:
+ is_bytes = driver.config.image_to_bytes in ["True", "true"]
+ except AttributeError:
+ pass
+ message_list = []
+ for msg in msg_list:
+ if isinstance(msg, (Image, Text, At)):
+ message_list.append(msg)
+ elif isinstance(msg, (str, int, float)):
+ message_list.append(Text(str(msg)))
+ elif isinstance(msg, Path):
+ if msg.exists():
+ if is_bytes:
+ image = BuildImage.open(msg)
+ message_list.append(Image(raw=image.pic2bytes()))
+ else:
+ message_list.append(Image(path=msg))
+ else:
+ logger.warning(f"图片路径不存在: {msg}")
+ elif isinstance(msg, bytes):
+ message_list.append(Image(raw=msg))
+ elif isinstance(msg, BytesIO):
+ message_list.append(Image(raw=msg))
+ elif isinstance(msg, BuildImage):
+ message_list.append(Image(raw=msg.pic2bytes()))
+ return message_list
+
+ @classmethod
+ def build_message(
+ cls, msg_list: MESSAGE_TYPE | list[MESSAGE_TYPE | list[MESSAGE_TYPE]]
+ ) -> UniMessage:
+ """构造消息
+
+ 参数:
+ msg_list: 消息列表
+
+ 返回:
+ UniMessage: 构造完成的消息列表
+ """
+ message_list = []
+ if not isinstance(msg_list, list):
+ msg_list = [msg_list]
+ for m in msg_list:
+ _data = m if isinstance(m, list) else [m]
+ message_list += cls.__build_message(_data) # type: ignore
+ return UniMessage(message_list)
+
+ @classmethod
+ def custom_forward_msg(
+ cls,
+ msg_list: list[str | Message],
+ uin: str,
+ name: str = f"这里是{NICKNAME}",
+ ) -> list[dict]:
+ """生成自定义合并消息
+
+ 参数:
+ msg_list: 消息列表
+ uin: 发送者 QQ
+ name: 自定义名称
+
+ 返回:
+ list[dict]: 转发消息
+ """
+ mes_list = []
+ for _message in msg_list:
+ data = {
+ "type": "node",
+ "data": {
+ "name": name,
+ "uin": f"{uin}",
+ "content": _message,
+ },
+ }
+ mes_list.append(data)
+ return mes_list
+
+ @classmethod
+ def template2forward(cls, msg_list: list[UniMessage], uni: str) -> list[dict]:
+ """模板转转发消息
+
+ 参数:
+ msg_list: 消息列表
+ uni: 发送者qq
+
+ 返回:
+ list[dict]: 转发消息
+ """
+ forward_data = []
+ for r_list in msg_list:
+ s = ""
+ if isinstance(r_list, (UniMessage, list)):
+ for r in r_list:
+ if isinstance(r, Text):
+ s += str(r)
+ elif isinstance(r, Image):
+ if v := r.url or r.path:
+ s += MessageSegment.image(v)
+ elif isinstance(r_list, Image):
+ if v := r_list.url or r_list.path:
+ s = MessageSegment.image(v)
+ else:
+ s = str(r_list)
+ forward_data.append(s)
+ return cls.custom_forward_msg(forward_data, uni)
diff --git a/zhenxun/utils/platform.py b/zhenxun/utils/platform.py
new file mode 100644
index 00000000..07213280
--- /dev/null
+++ b/zhenxun/utils/platform.py
@@ -0,0 +1,631 @@
+import random
+from typing import Awaitable, Callable, Literal, Set
+
+import httpx
+import nonebot
+from nonebot.adapters import Bot
+from nonebot.adapters.discord import Bot as DiscordBot
+from nonebot.adapters.dodo import Bot as DodoBot
+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 nonebot.utils import is_coroutine_callable
+from nonebot_plugin_alconna.uniseg import Receipt, Target, UniMessage
+from pydantic import BaseModel
+
+from zhenxun.models.friend_user import FriendUser
+from zhenxun.models.group_console import GroupConsole
+from zhenxun.services.log import logger
+from zhenxun.utils.exception import NotFindSuperuser
+from zhenxun.utils.message import MessageUtils
+
+
+class UserData(BaseModel):
+
+ name: str
+ """昵称"""
+ card: str | None = None
+ """名片/备注"""
+ user_id: str
+ """用户id"""
+ group_id: str | None = None
+ """群组id"""
+ role: str | None = None
+ """角色"""
+ avatar_url: str | None = None
+ """头像url"""
+ join_time: int | None = None
+ """加入时间"""
+
+
+class PlatformUtils:
+
+ @classmethod
+ async def ban_user(cls, bot: Bot, user_id: str, group_id: str, duration: int):
+ """禁言
+
+ 参数:
+ bot: Bot
+ user_id: 用户id
+ group_id: 群组id
+ duration: 禁言时长(分钟)
+ """
+ if isinstance(bot, v11Bot):
+ await bot.set_group_ban(
+ group_id=int(group_id),
+ user_id=int(user_id),
+ duration=duration * 60,
+ )
+
+ @classmethod
+ async def send_superuser(
+ cls,
+ bot: Bot,
+ message: UniMessage,
+ superuser_id: str | None = None,
+ ) -> Receipt | None:
+ """发送消息给超级用户
+
+ 参数:
+ bot: Bot
+ message: 消息
+ superuser_id: 指定超级用户id.
+
+ 异常:
+ NotFindSuperuser: 未找到超级用户id
+
+ 返回:
+ Receipt | None: Receipt
+ """
+ if not superuser_id:
+ platform = cls.get_platform(bot)
+ platform_superusers = bot.config.PLATFORM_SUPERUSERS.get(platform) or []
+ if not platform_superusers:
+ raise NotFindSuperuser()
+ superuser_id = random.choice(platform_superusers)
+ return await cls.send_message(bot, superuser_id, None, message)
+
+ @classmethod
+ async def get_group_member_list(cls, bot: Bot, group_id: str) -> list[UserData]:
+ """获取群组/频道成员列表
+
+ 参数:
+ bot: Bot
+ group_id: 群组/频道id
+
+ 返回:
+ list[UserData]: 用户数据列表
+ """
+ if isinstance(bot, v11Bot):
+ if member_list := await bot.get_group_member_list(group_id=int(group_id)):
+ return [
+ UserData(
+ name=user["nickname"],
+ card=user["card"],
+ user_id=user["user_id"],
+ group_id=user["group_id"],
+ role=user["role"],
+ join_time=user["join_time"],
+ )
+ for user in member_list
+ ]
+ if isinstance(bot, v12Bot):
+ if member_list := await bot.get_group_member_list(group_id=group_id):
+ return [
+ UserData(
+ name=user["user_name"],
+ card=user["user_displayname"],
+ user_id=user["user_id"],
+ group_id=group_id,
+ )
+ for user in member_list
+ ]
+ if isinstance(bot, DodoBot):
+ if result_data := await bot.get_member_list(
+ island_source_id=group_id, page_size=100, max_id=0
+ ):
+ max_id = result_data.max_id
+ result_list = result_data.list
+ data_list = []
+ while max_id == 100:
+ result_data = await bot.get_member_list(
+ island_source_id=group_id, page_size=100, max_id=0
+ )
+ result_list += result_data.list
+ max_id = result_data.max_id
+ for user in result_list:
+ data_list.append(
+ UserData(
+ name=user.nick_name,
+ card=user.personal_nick_name,
+ avatar_url=user.avatar_url,
+ user_id=user.dodo_source_id,
+ group_id=user.island_source_id,
+ join_time=int(user.join_time.timestamp()),
+ )
+ )
+ return data_list
+ if isinstance(bot, KaiheilaBot):
+ if result_data := await bot.guild_userList(guild_id=group_id):
+ if result_data.users:
+ data_list = []
+ for user in result_data.users:
+ second = None
+ if user.joined_at:
+ second = int(user.joined_at / 1000)
+ data_list.append(
+ UserData(
+ name=user.nickname or "",
+ avatar_url=user.avatar,
+ user_id=user.id_, # type: ignore
+ group_id=group_id,
+ join_time=second,
+ )
+ )
+ return data_list
+ if isinstance(bot, DiscordBot):
+ # TODO: discord获取用户
+ pass
+ return []
+
+ @classmethod
+ async def get_user(
+ cls, bot: Bot, user_id: str, group_id: str | None = None
+ ) -> UserData | None:
+ """获取用户信息
+
+ 参数:
+ bot: Bot
+ user_id: 用户id
+ group_id: 群组/频道id.
+
+ 返回:
+ UserData | None: 用户数据
+ """
+ if isinstance(bot, v11Bot):
+ if group_id:
+ if user := await bot.get_group_member_info(
+ group_id=int(group_id), user_id=int(user_id)
+ ):
+ return UserData(
+ name=user["nickname"],
+ card=user["card"],
+ user_id=user["user_id"],
+ group_id=user["group_id"],
+ role=user["role"],
+ join_time=user["join_time"],
+ )
+ else:
+ if friend_list := await bot.get_friend_list():
+ for f in friend_list:
+ if f["user_id"] == int(user_id):
+ return UserData(
+ name=f["nickname"],
+ card=f["remark"],
+ user_id=f["user_id"],
+ )
+ if isinstance(bot, v12Bot):
+ if group_id:
+ if user := await bot.get_group_member_info(
+ group_id=group_id, user_id=user_id
+ ):
+ return UserData(
+ name=user["user_name"],
+ card=user["user_displayname"],
+ user_id=user["user_id"],
+ group_id=group_id,
+ )
+ else:
+ if friend_list := await bot.get_friend_list():
+ for f in friend_list:
+ if f["user_id"] == int(user_id):
+ return UserData(
+ name=f["user_name"],
+ card=f["user_remark"],
+ user_id=f["user_id"],
+ )
+ if isinstance(bot, DodoBot):
+ if group_id:
+ if user := await bot.get_member_info(
+ island_source_id=group_id, dodo_source_id=user_id
+ ):
+ return UserData(
+ name=user.nick_name,
+ card=user.personal_nick_name,
+ avatar_url=user.avatar_url,
+ user_id=user.dodo_source_id,
+ group_id=user.island_source_id,
+ join_time=int(user.join_time.timestamp()),
+ )
+ else:
+ # TODO: DoDo个人数据
+ pass
+ if isinstance(bot, KaiheilaBot):
+ if group_id:
+ if user := await bot.user_view(guild_id=group_id, user_id=user_id):
+ second = None
+ if user.joined_at:
+ second = int(user.joined_at / 1000)
+ return UserData(
+ name=user.nickname or "",
+ avatar_url=user.avatar,
+ user_id=user_id,
+ group_id=group_id,
+ join_time=second,
+ )
+ else:
+ # TODO: kaiheila用户详情
+ pass
+ if isinstance(bot, DiscordBot):
+ # TODO: discord获取用户
+ pass
+ return None
+
+ @classmethod
+ async def get_user_avatar(cls, user_id: str, platform: str) -> bytes | None:
+ """快捷获取用户头像
+
+ 参数:
+ user_id: 用户id
+ platform: 平台
+ """
+ if platform == "qq":
+ url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=160"
+ async with httpx.AsyncClient() as client:
+ for _ in range(3):
+ try:
+ return (await client.get(url)).content
+ except Exception as e:
+ logger.error(
+ "获取用户头像错误",
+ "Util",
+ target=user_id,
+ platform=platform,
+ )
+ else:
+ pass
+ return None
+
+ @classmethod
+ async def get_group_avatar(cls, gid: str, platform: str) -> bytes | None:
+ """快捷获取用群头像
+
+ 参数:
+ gid: 群组id
+ platform: 平台
+ """
+ if platform == "qq":
+ url = f"http://p.qlogo.cn/gh/{gid}/{gid}/640/"
+ async with httpx.AsyncClient() as client:
+ for _ in range(3):
+ try:
+ return (await client.get(url)).content
+ except Exception as e:
+ logger.error(
+ "获取群头像错误", "Util", target=gid, platform=platform
+ )
+ else:
+ pass
+ return None
+
+ @classmethod
+ async def send_message(
+ cls,
+ bot: Bot,
+ user_id: str | None,
+ group_id: str | None,
+ message: str | UniMessage,
+ ) -> Receipt | None:
+ """发送消息
+
+ 参数:
+ bot: Bot
+ user_id: 用户id
+ group_id: 群组id或频道id
+ message: 消息文本
+
+ 返回:
+ Receipt | None: 是否发送成功
+ """
+ if target := cls.get_target(bot, user_id, group_id):
+ send_message = (
+ MessageUtils.build_message(message)
+ if isinstance(message, str)
+ else message
+ )
+ return await send_message.send(target=target, bot=bot)
+ return None
+
+ @classmethod
+ async def update_group(cls, bot: Bot) -> int:
+ """更新群组信息
+
+ 参数:
+ bot: Bot
+
+ 返回:
+ int: 更新个数
+ """
+ create_list = []
+ group_list, platform = await cls.get_group_list(bot)
+ if group_list:
+ exists_group_list = await GroupConsole.all().values_list(
+ "group_id", "channel_id"
+ )
+ for group in group_list:
+ group.platform = platform
+ if (group.group_id, group.channel_id) not in exists_group_list:
+ create_list.append(group)
+ logger.debug(
+ "群聊信息更新成功",
+ "更新群信息",
+ target=f"{group.group_id}:{group.channel_id}",
+ )
+ if create_list:
+ await GroupConsole.bulk_create(create_list, 10)
+ return len(create_list)
+
+ @classmethod
+ def get_platform(cls, bot: Bot) -> str | None:
+ """获取平台
+
+ 参数:
+ bot: Bot
+
+ 返回:
+ str | None: 平台
+ """
+ if isinstance(bot, (v11Bot, v12Bot)):
+ return "qq"
+ # if isinstance(bot, DodoBot):
+ # return "dodo"
+ # if isinstance(bot, KaiheilaBot):
+ # return "kaiheila"
+ # if isinstance(bot, DiscordBot):
+ # return "discord"
+ return None
+
+ @classmethod
+ async def get_group_list(cls, bot: Bot) -> tuple[list[GroupConsole], str]:
+ """获取群组列表
+
+ 参数:
+ bot: Bot
+
+ 返回:
+ tuple[list[GroupConsole], str]: 群组列表, 平台
+ """
+ if isinstance(bot, v11Bot):
+ group_list = await bot.get_group_list()
+ return [
+ GroupConsole(
+ group_id=str(g["group_id"]),
+ group_name=g["group_name"],
+ max_member_count=g["max_member_count"],
+ member_count=g["member_count"],
+ )
+ for g in group_list
+ ], "qq"
+ if isinstance(bot, v12Bot):
+ group_list = await bot.get_group_list()
+ return [
+ GroupConsole(
+ group_id=g.group_id, # type: ignore
+ user_name=g.group_name, # type: ignore
+ )
+ for g in group_list
+ ], "qq"
+ if isinstance(bot, DodoBot):
+ island_list = await bot.get_island_list()
+ source_id_list = [
+ (g.island_source_id, g.island_name)
+ for g in island_list
+ if g.island_source_id
+ ]
+ group_list = []
+ for id, name in source_id_list:
+ channel_list = await bot.get_channel_list(island_source_id=id)
+ group_list.append(GroupConsole(group_id=id, group_name=name))
+ group_list += [
+ GroupConsole(
+ group_id=id, group_name=c.channel_name, channel_id=c.channel_id
+ )
+ for c in channel_list
+ ]
+ return group_list, "dodo"
+ if isinstance(bot, KaiheilaBot):
+ group_list = []
+ guilds = await bot.guild_list()
+ if guilds.guilds:
+ for guild_id, name in [(g.id_, g.name) for g in guilds.guilds if g.id_]:
+ view = await bot.guild_view(guild_id=guild_id)
+ group_list.append(GroupConsole(group_id=guild_id, group_name=name))
+ if view.channels:
+ group_list += [
+ GroupConsole(
+ group_id=guild_id, group_name=c.name, channel_id=c.id_
+ )
+ for c in view.channels
+ if c.type != 0
+ ]
+ return group_list, "kaiheila"
+ if isinstance(bot, DiscordBot):
+ # TODO: discord群组列表
+ pass
+ return [], ""
+
+ @classmethod
+ async def update_friend(cls, bot: Bot) -> int:
+ """更新好友信息
+
+ 参数:
+ bot: Bot
+
+ 返回:
+ int: 更新个数
+ """
+ create_list = []
+ friend_list, platform = await cls.get_friend_list(bot)
+ if friend_list:
+ user_id_list = await FriendUser.all().values_list("user_id", flat=True)
+ for friend in friend_list:
+ friend.platform = platform
+ if friend.user_id not in user_id_list:
+ create_list.append(friend)
+ if create_list:
+ await FriendUser.bulk_create(create_list, 10)
+ return len(create_list)
+
+ @classmethod
+ async def get_friend_list(cls, bot: Bot) -> tuple[list[FriendUser], str]:
+ """获取好友列表
+
+ 参数:
+ bot: Bot
+
+ 返回:
+ list[FriendUser]: 好友列表
+ """
+ if isinstance(bot, v11Bot):
+ friend_list = await bot.get_friend_list()
+ return [
+ FriendUser(user_id=str(f["user_id"]), user_name=f["nickname"])
+ for f in friend_list
+ ], "qq"
+ if isinstance(bot, v12Bot):
+ friend_list = await bot.get_friend_list()
+ return [
+ FriendUser(
+ user_id=f.user_id, # type: ignore
+ user_name=f.user_displayname or f.user_remark or f.user_name, # type: ignore
+ )
+ for f in friend_list
+ ], "qq"
+ if isinstance(bot, DodoBot):
+ # TODO: dodo好友列表
+ pass
+ if isinstance(bot, KaiheilaBot):
+ # TODO: kaiheila好友列表
+ pass
+ if isinstance(bot, DiscordBot):
+ # TODO: discord好友列表
+ pass
+ return [], ""
+
+ @classmethod
+ def get_target(
+ cls,
+ bot: Bot,
+ user_id: str | None = None,
+ group_id: str | None = None,
+ channel_id: str | None = None,
+ ):
+ """获取发生Target
+
+ 参数:
+ bot: Bot
+ user_id: 用户id
+ group_id: 频道id或群组id
+ channel_id: 频道id
+
+ 返回:
+ target: 对应平台Target
+ """
+ target = None
+ if isinstance(bot, (v11Bot, v12Bot)):
+ if group_id:
+ target = Target(group_id)
+ elif user_id:
+ target = Target(user_id, private=True)
+ elif isinstance(bot, (DodoBot, KaiheilaBot)):
+ if group_id and channel_id:
+ target = Target(channel_id, parent_id=group_id, channel=True)
+ elif user_id:
+ target = Target(user_id, private=True)
+ return target
+
+
+async def broadcast_group(
+ message: str | UniMessage,
+ bot: Bot | list[Bot] | None = None,
+ bot_id: str | Set[str] | None = None,
+ ignore_group: Set[int] | None = None,
+ check_func: Callable[[str], Awaitable] | None = None,
+ log_cmd: str | None = None,
+ platform: Literal["qq", "dodo", "kaiheila"] | None = None,
+):
+ """获取所有Bot或指定Bot对象广播群聊
+
+ 参数:
+ message: 广播消息内容
+ bot: 指定bot对象.
+ bot_id: 指定bot id.
+ ignore_group: 忽略群聊列表.
+ check_func: 发送前对群聊检测方法,判断是否发送.
+ log_cmd: 日志标记.
+ platform: 指定平台
+ """
+ if platform and platform not in ["qq", "dodo", "kaiheila"]:
+ raise ValueError("指定平台不支持")
+ if not message:
+ raise ValueError("群聊广播消息不能为空")
+ bot_dict = nonebot.get_bots()
+ bot_list: list[Bot] = []
+ if bot:
+ if isinstance(bot, list):
+ bot_list = bot
+ else:
+ bot_list.append(bot)
+ elif bot_id:
+ _bot_id_list = bot_id
+ if isinstance(bot_id, str):
+ _bot_id_list = [bot_id]
+ for id_ in _bot_id_list:
+ if bot_id in bot_dict:
+ bot_list.append(bot_dict[bot_id])
+ else:
+ logger.warning(f"Bot:{id_} 对象未连接或不存在")
+ else:
+ bot_list = list(bot_dict.values())
+ _used_group = []
+ for _bot in bot_list:
+ try:
+ if platform and platform != PlatformUtils.get_platform(_bot):
+ continue
+ group_list, _ = await PlatformUtils.get_group_list(_bot)
+ if group_list:
+ for group in group_list:
+ key = f"{group.group_id}:{group.channel_id}"
+ try:
+ if (
+ ignore_group
+ and (
+ group.group_id in ignore_group
+ or group.channel_id in ignore_group
+ )
+ ) or key in _used_group:
+ continue
+ is_run = False
+ if check_func:
+ if is_coroutine_callable(check_func):
+ is_run = await check_func(group.group_id)
+ else:
+ is_run = check_func(group.group_id)
+ if not is_run:
+ continue
+ target = PlatformUtils.get_target(
+ _bot, None, group.group_id, group.channel_id
+ )
+ if target:
+ _used_group.append(key)
+ message_list = message
+ await MessageUtils.build_message(message_list).send(
+ target, _bot
+ )
+ logger.debug("发送成功", log_cmd, target=key)
+ else:
+ logger.warning("target为空", log_cmd, target=key)
+ except Exception as e:
+ logger.error("发送失败", log_cmd, target=key, e=e)
+ except Exception as e:
+ logger.error(f"Bot: {_bot.self_id} 获取群聊列表失败", command=log_cmd, e=e)
diff --git a/zhenxun/utils/plugin_models/base.py b/zhenxun/utils/plugin_models/base.py
new file mode 100644
index 00000000..a50f6b4a
--- /dev/null
+++ b/zhenxun/utils/plugin_models/base.py
@@ -0,0 +1,9 @@
+from pydantic import BaseModel
+
+
+class CommonSql(BaseModel):
+
+ sql: str
+ """sql语句"""
+ remark: str
+ """备注"""
diff --git a/zhenxun/utils/rules.py b/zhenxun/utils/rules.py
new file mode 100644
index 00000000..f3381a48
--- /dev/null
+++ b/zhenxun/utils/rules.py
@@ -0,0 +1,61 @@
+from nonebot.adapters import Bot, Event
+from nonebot.internal.rule import Rule
+from nonebot.permission import SUPERUSER
+from nonebot_plugin_session import EventSession, SessionLevel
+
+from zhenxun.configs.config import Config
+from zhenxun.models.level_user import LevelUser
+
+
+def admin_check(a: int | str, key: str | None = None) -> Rule:
+ """
+ 管理员权限等级检查
+
+ 参数:
+ a: 权限等级或 配置项 module
+ key: 配置项 key.
+
+ 返回:
+ Rule: Rule
+ """
+
+ async def _rule(bot: Bot, event: Event, session: EventSession) -> bool:
+ if await SUPERUSER(bot, event):
+ return True
+ if session.id1 and session.id2:
+ level = a
+ if type(a) == str and key:
+ level = Config.get_config(a, key)
+ if level is not None:
+ return bool(
+ await LevelUser.check_level(session.id1, session.id2, int(level))
+ )
+ return False
+
+ return Rule(_rule)
+
+
+def ensure_group(session: EventSession) -> bool:
+ """
+ 是否在群聊中
+
+ 参数:
+ session: session
+
+ 返回:
+ bool: bool
+ """
+ return session.level in [SessionLevel.LEVEL2, SessionLevel.LEVEL3]
+
+
+def ensure_private(session: EventSession) -> bool:
+ """
+ 是否在私聊中
+
+ 参数:
+ session: session
+
+ 返回:
+ bool: bool
+ """
+ return not session.id3 and not session.id2
diff --git a/plugins/my_info/data_source.py b/zhenxun/utils/typing.py
similarity index 50%
rename from plugins/my_info/data_source.py
rename to zhenxun/utils/typing.py
index 6fb66a5e..b28b04f6 100644
--- a/plugins/my_info/data_source.py
+++ b/zhenxun/utils/typing.py
@@ -1,6 +1,3 @@
-
-
-
diff --git a/utils/user_agent.py b/zhenxun/utils/user_agent.py
old mode 100755
new mode 100644
similarity index 100%
rename from utils/user_agent.py
rename to zhenxun/utils/user_agent.py
diff --git a/zhenxun/utils/utils.py b/zhenxun/utils/utils.py
new file mode 100644
index 00000000..7ea698a9
--- /dev/null
+++ b/zhenxun/utils/utils.py
@@ -0,0 +1,232 @@
+import os
+import time
+from collections import defaultdict
+from datetime import datetime
+from pathlib import Path
+from typing import Any
+
+import httpx
+import pypinyin
+import pytz
+
+from zhenxun.configs.config import Config
+from zhenxun.services.log import logger
+
+
+class ResourceDirManager:
+ """
+ 临时文件管理器
+ """
+
+ temp_path = []
+
+ @classmethod
+ def __tree_append(cls, path: Path):
+ """递归添加文件夹
+
+ 参数:
+ path: 文件夹路径
+ """
+ for f in os.listdir(path):
+ file = path / f
+ if file.is_dir():
+ if file not in cls.temp_path:
+ cls.temp_path.append(file)
+ logger.debug(f"添加临时文件夹: {path}")
+ cls.__tree_append(file)
+
+ @classmethod
+ def add_temp_dir(cls, path: str | Path, tree: bool = False):
+ """添加临时清理文件夹,这些文件夹会被自动清理
+
+ 参数:
+ path: 文件夹路径
+ tree: 是否递归添加文件夹
+ """
+ if isinstance(path, str):
+ path = Path(path)
+ if path not in cls.temp_path:
+ cls.temp_path.append(path)
+ logger.debug(f"添加临时文件夹: {path}")
+ if tree:
+ cls.__tree_append(path)
+
+
+class CountLimiter:
+ """
+ 每日调用命令次数限制
+ """
+
+ tz = pytz.timezone("Asia/Shanghai")
+
+ def __init__(self, max_num):
+ self.today = -1
+ self.count = defaultdict(int)
+ self.max = max_num
+
+ def check(self, key) -> bool:
+ day = datetime.now(self.tz).day
+ if day != self.today:
+ self.today = day
+ self.count.clear()
+ return bool(self.count[key] < self.max)
+
+ def get_num(self, key):
+ return self.count[key]
+
+ def increase(self, key, num=1):
+ self.count[key] += num
+
+ def reset(self, key):
+ self.count[key] = 0
+
+
+class UserBlockLimiter:
+ """
+ 检测用户是否正在调用命令
+ """
+
+ def __init__(self):
+ self.flag_data = defaultdict(bool)
+ self.time = time.time()
+
+ def set_true(self, key: Any):
+ self.time = time.time()
+ self.flag_data[key] = True
+
+ def set_false(self, key: Any):
+ self.flag_data[key] = False
+
+ def check(self, key: Any) -> bool:
+ if time.time() - self.time > 30:
+ self.set_false(key)
+ return not self.flag_data[key]
+
+
+class FreqLimiter:
+ """
+ 命令冷却,检测用户是否处于冷却状态
+ """
+
+ def __init__(self, default_cd_seconds: int):
+ self.next_time = defaultdict(float)
+ self.default_cd = default_cd_seconds
+
+ def check(self, key: Any) -> bool:
+ return time.time() >= self.next_time[key]
+
+ def start_cd(self, key: Any, cd_time: int = 0):
+ self.next_time[key] = time.time() + (
+ cd_time if cd_time > 0 else self.default_cd
+ )
+
+ def left_time(self, key: Any) -> float:
+ return self.next_time[key] - time.time()
+
+
+def cn2py(word: str) -> str:
+ """将字符串转化为拼音
+
+ 参数:
+ word: 文本
+ """
+ temp = ""
+ for i in pypinyin.pinyin(word, style=pypinyin.NORMAL):
+ temp += "".join(i)
+ return temp
+
+
+async def get_user_avatar(uid: int | str) -> bytes | None:
+ """快捷获取用户头像
+
+ 参数:
+ uid: 用户id
+ """
+ url = f"http://q1.qlogo.cn/g?b=qq&nk={uid}&s=160"
+ async with httpx.AsyncClient() as client:
+ for _ in range(3):
+ try:
+ return (await client.get(url)).content
+ except Exception as e:
+ logger.error("获取用户头像错误", "Util", target=uid)
+ return None
+
+
+async def get_group_avatar(gid: int | str) -> bytes | None:
+ """快捷获取用群头像
+
+ 参数:
+ gid: 群号
+ """
+ url = f"http://p.qlogo.cn/gh/{gid}/{gid}/640/"
+ async with httpx.AsyncClient() as client:
+ for _ in range(3):
+ try:
+ return (await client.get(url)).content
+ except Exception as e:
+ logger.error("获取群头像错误", "Util", target=gid)
+ return None
+
+
+def change_pixiv_image_links(
+ url: str, size: str | None = None, nginx_url: str | None = None
+) -> str:
+ """根据配置改变图片大小和反代链接
+
+ 参数:
+ url: 图片原图链接
+ size: 模式
+ nginx_url: 反代
+
+ 返回:
+ str: url
+ """
+ if size == "master":
+ img_sp = url.rsplit(".", maxsplit=1)
+ url = img_sp[0]
+ img_type = img_sp[1]
+ url = url.replace("original", "master") + f"_master1200.{img_type}"
+ if not nginx_url:
+ nginx_url = Config.get_config("pixiv", "PIXIV_NGINX_URL")
+ if nginx_url:
+ url = (
+ url.replace("i.pximg.net", nginx_url)
+ .replace("i.pixiv.cat", nginx_url)
+ .replace("_webp", "")
+ )
+ return url
+
+
+def change_img_md5(path_file: str | Path) -> bool:
+ """改变图片MD5
+
+ 参数:
+ path_file: 图片路径
+
+ 返还:
+ bool: 是否修改成功
+ """
+ try:
+ with open(path_file, "a") as f:
+ f.write(str(int(time.time() * 1000)))
+ return True
+ except Exception as e:
+ logger.warning(f"改变图片MD5错误 Path:{path_file}", e=e)
+ return False
+
+
+def is_valid_date(date_text: str, separator: str = "-") -> bool:
+ """日期是否合法
+
+ 参数:
+ date_text: 日期
+ separator: 分隔符
+
+ 返回:
+ bool: 日期是否合法
+ """
+ try:
+ datetime.strptime(date_text, f"%Y{separator}%m{separator}%d")
+ return True
+ except ValueError:
+ return False
diff --git a/zhenxun/utils/withdraw_manage.py b/zhenxun/utils/withdraw_manage.py
new file mode 100644
index 00000000..d88b394f
--- /dev/null
+++ b/zhenxun/utils/withdraw_manage.py
@@ -0,0 +1,112 @@
+import asyncio
+
+from nonebot.adapters import Bot
+
+# from nonebot.adapters.discord import Bot as DiscordBot
+# from nonebot.adapters.dodo import Bot as DodoBot
+# 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 nonebot_plugin_session import EventSession
+from ruamel.yaml.comments import CommentedSeq
+
+from zhenxun.services.log import logger
+
+
+class WithdrawManager:
+
+ _data = {}
+ _index = 0
+
+ @classmethod
+ def check(cls, session: EventSession, withdraw_time: tuple[int, int]) -> bool:
+ """配置项检查
+
+ 参数:
+ session: Session
+ withdraw_time: 配置项数据, (0, 1)
+
+ 返回:
+ bool: 是否允许撤回
+ """
+ if withdraw_time[0] and withdraw_time[0] > 0:
+ if withdraw_time[1] == 2:
+ return True
+ if withdraw_time[1] == 1 and (session.id2 or session.id3):
+ return True
+ if withdraw_time[1] == 0 and not (session.id2 or session.id3):
+ return True
+ return False
+
+ @classmethod
+ def append(cls, bot: Bot, message_id: str | int, time: int):
+ """添加消息撤回
+
+ 参数:
+ bot: Bot
+ message_id: 消息Id
+ time: 延迟时间
+ """
+ cls._data[cls._index] = (
+ bot,
+ message_id,
+ time,
+ )
+ cls._index += 1
+
+ @classmethod
+ def remove(cls, index: int):
+ """移除
+
+ 参数:
+ index: index
+ """
+ if index in cls._data:
+ del cls._data[index]
+
+ @classmethod
+ async def withdraw_message(
+ cls,
+ bot: Bot,
+ message_id: str | int,
+ time: int | tuple[int, int] | None = None,
+ session: EventSession | None = None,
+ ):
+ """消息撤回
+
+ 参数:
+ bot: Bot
+ message_id: 消息Id
+ time: 延迟时间
+ """
+ if time:
+ gid = None
+ _time = 1
+ if isinstance(time, (tuple, CommentedSeq)):
+ if time[0] == 0:
+ return
+ if session:
+ gid = session.id3 or session.id2
+ if not gid and int(time[1]) not in [0, 2]:
+ return
+ if gid and int(time[1]) not in [1, 2]:
+ return
+ _time = time[0]
+ else:
+ _time = time
+ logger.debug(
+ f"将在 {_time}秒 内撤回消息ID: {message_id}", "WithdrawManager"
+ )
+ await asyncio.sleep(_time)
+ if isinstance(bot, v11Bot):
+ logger.debug(f"v11Bot 撤回消息ID: {message_id}", "WithdrawManager")
+ await bot.delete_msg(message_id=int(message_id))
+ elif isinstance(bot, v12Bot):
+ logger.debug(f"v12Bot 撤回消息ID: {message_id}", "WithdrawManager")
+ await bot.delete_message(message_id=str(message_id))
+ # elif isinstance(bot, KaiheilaBot):
+ # pass
+ # elif isinstance(bot, DodoBot):
+ # pass
+ # elif isinstance(bot, DiscordBot):
+ # pass