diff --git a/README.md b/README.md index ac347592..7bf4fc20 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ 基于 [Nonebot2](https://github.com/nonebot/nonebot2) 的 qq 机器人 ![GitHub](https://img.shields.io/github/license/Ailitonia/omega-miya) -![Python](https://img.shields.io/badge/Python-3.8+-blue) +![Python](https://img.shields.io/badge/Python-3.9+-blue) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/Ailitonia/omega-miya?include_prereleases) ![GitHub (Pre-)Release Date](https://img.shields.io/github/release-date-pre/Ailitonia/omega-miya) @@ -13,17 +13,19 @@ ## 当前适配nonebot2版本 -[Nonebot2 Pre Release v2.0.0a13.post1](https://github.com/nonebot/nonebot2/releases/tag/v2.0.0a13.post1) +[Nonebot2 Pre Release v2.0.0a16](https://github.com/nonebot/nonebot2/releases/tag/v2.0.0a16) ## 功能 & 特点 - 基于异步 SQLAlchemy / MySQL 的数据存储 +- 插件管理系统 - 权限控制及管理系统 - 针对不同群组可选启用通知权限、命令权限、权限等级控制 - 针对不同好友可选启用 Bot 功能 - 针对不同群组、好友独立配置插件权限节点 - 支持多协议端连接, 各协议端权限及订阅配置相互独立 - 命令冷却系统 +- 速率控制系统 - HTTP 代理功能 - 自动处理加好友和被邀请进群 - 插件帮助功能 (支持群聊 / 私聊) @@ -36,6 +38,7 @@ - 签到 (仅支持群聊) - 求签 (仅支持群聊) - 抽卡 (仅支持群聊) +- 塔罗牌 (仅支持群聊) - 能不能好好说话 (lab.magiconch.com API) (支持群聊 / 私聊) - Pixiv助手 (需要 HTTP 代理, 除非部署在外网) (需要 go-cqhttp v0.9.40 及以上版本) (仅支持群聊) - Pixiv订阅 (需要 HTTP 代理, 除非部署在外网) (仅支持群聊) @@ -59,7 +62,7 @@ ## 关于图片数据 如果你不想自己收集图片数据, 可以将 -[这个图片数据库](https://github.com/Ailitonia/omega-miya/raw/main/archive_data/db_pixiv.7z) +[这个图片数据库](https://github.com/Ailitonia/omega-miya/raw/master/archive_data/db_pixiv.7z) 导入, 基本都是按我自己口味收集的图片 Update 2021.8.10: 最新发布图片数据库共 9w7 条图片数据 (包含已失效或画师已删除作品) diff --git a/bot.py b/bot.py index 652033bb..2421d179 100644 --- a/bot.py +++ b/bot.py @@ -11,7 +11,7 @@ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # File path -bot_root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'omega_miya')) +bot_root_path = os.path.abspath(os.path.dirname(__file__)) bot_tmp_path = os.path.abspath(os.path.join(bot_root_path, 'tmp')) if not os.path.exists(bot_tmp_path): os.makedirs(bot_tmp_path) @@ -20,6 +20,11 @@ if not os.path.exists(bot_log_path): os.makedirs(bot_log_path) +# Static resources path +bot_resources_path = os.path.abspath(os.path.join(bot_root_path, 'omega_miya', 'resources')) +if not os.path.exists(bot_resources_path): + os.makedirs(bot_resources_path) + # Custom logger log_info_name = f"{datetime.today().strftime('%Y%m%d-%H%M%S')}-INFO.log" log_error_name = f"{datetime.today().strftime('%Y%m%d-%H%M%S')}-ERROR.log" @@ -41,6 +46,7 @@ config = nonebot.get_driver().config config.root_path_ = bot_root_path config.tmp_path_ = bot_tmp_path +config.resources_path_ = bot_resources_path # 注册 cqhttp adapter driver = nonebot.get_driver() diff --git a/docs/img/pillow-text-anchors-reference.png b/docs/img/pillow-text-anchors-reference.png new file mode 100644 index 00000000..9a00df78 Binary files /dev/null and b/docs/img/pillow-text-anchors-reference.png differ diff --git a/omega_miya/utils/Omega_Base/__init__.py b/omega_miya/database/__init__.py similarity index 84% rename from omega_miya/utils/Omega_Base/__init__.py rename to omega_miya/database/__init__.py index d4c23743..2717b474 100644 --- a/omega_miya/utils/Omega_Base/__init__.py +++ b/omega_miya/database/__init__.py @@ -6,7 +6,8 @@ from .class_result import Result from .model import ( DBUser, DBFriend, DBBot, DBBotGroup, DBGroup, DBSkill, DBSubscription, DBDynamic, - DBPixivUserArtwork, DBPixivillust, DBPixivision, DBEmail, DBEmailBox, DBHistory, DBAuth, DBCoolDownEvent, DBStatus) + DBPixivUserArtwork, DBPixivillust, DBPixivision, DBEmail, DBEmailBox, DBHistory, + DBAuth, DBCoolDownEvent, DBStatus, DBStatistic, DBPlugin) __all__ = [ @@ -27,5 +28,7 @@ 'DBAuth', 'DBCoolDownEvent', 'DBStatus', + 'DBStatistic', + 'DBPlugin', 'Result' ] diff --git a/omega_miya/utils/Omega_Base/class_result.py b/omega_miya/database/class_result.py similarity index 100% rename from omega_miya/utils/Omega_Base/class_result.py rename to omega_miya/database/class_result.py diff --git a/omega_miya/utils/Omega_Base/database.py b/omega_miya/database/database.py similarity index 84% rename from omega_miya/utils/Omega_Base/database.py rename to omega_miya/database/database.py index 96603c62..b264790c 100644 --- a/omega_miya/utils/Omega_Base/database.py +++ b/omega_miya/database/database.py @@ -1,4 +1,5 @@ import nonebot +from urllib.parse import quote from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.future import select @@ -18,7 +19,7 @@ __DB_NAME = global_config.db_name # 格式化数据库引擎链接 -__DB_ENGINE = f'{__DATABASE}+{__DB_DRIVER}://{__DB_USER}:{__DB_PASSWORD}@{__DB_HOST}:{__DB_PORT}/{__DB_NAME}' +__DB_ENGINE = f'{__DATABASE}+{__DB_DRIVER}://{__DB_USER}:{quote(__DB_PASSWORD)}@{__DB_HOST}:{__DB_PORT}/{__DB_NAME}' # 创建数据库连接 @@ -28,10 +29,10 @@ connect_args={"use_unicode": True, "charset": "utf8mb4"}, pool_recycle=3600, pool_pre_ping=True, echo=False ) -except Exception as exp: +except Exception as e: import sys - nonebot.logger.opt(colors=True).critical(f'创建数据库连接失败, error: {repr(exp)}') - sys.exit('创建数据库连接失败') + nonebot.logger.opt(colors=True).critical(f'创建数据库连接失败, error: {repr(e)}') + sys.exit(f'创建数据库连接失败, {e}') # 初始化化数据库 @@ -48,14 +49,14 @@ async def database_init(): # where synchronous IO calls will be transparently translated for # await. await conn.run_sync(Base.metadata.create_all) - nonebot.logger.opt(colors=True).info(f'数据库初始化已完成.') - except Exception as e: + nonebot.logger.opt(colors=True).success(f'数据库初始化已完成.') + except Exception as e_: import sys - nonebot.logger.opt(colors=True).critical(f'数据库初始化失败, error: {repr(e)}') - sys.exit('数据库初始化失败') + nonebot.logger.opt(colors=True).critical(f'数据库初始化失败, error: {repr(e_)}') + sys.exit(f'数据库初始化失败, {e_}') -class NBdb(object): +class BaseDB(object): def __init__(self): # expire_on_commit=False will prevent attributes from being expired # after commit. @@ -90,7 +91,7 @@ async def list_col(self, col_name) -> Result.ListResult: if not self.__table: result = Result.ListResult(error=True, info='Table not exist', result=res) else: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -99,8 +100,8 @@ async def list_col(self, col_name) -> Result.ListResult: for item in session_result.scalars().all(): res.append(item) result = Result.ListResult(error=False, info='Success', result=res) - except Exception as e: - result = Result.ListResult(error=True, info=repr(e), result=res) + except Exception as e_: + result = Result.ListResult(error=True, info=repr(e_), result=res) return result async def list_col_with_condition(self, col_name, condition_col_name, condition) -> Result.ListResult: @@ -108,7 +109,7 @@ async def list_col_with_condition(self, col_name, condition_col_name, condition) if not self.__table: result = Result.ListResult(error=True, info='Table not exist', result=res) else: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -118,11 +119,11 @@ async def list_col_with_condition(self, col_name, condition_col_name, condition) for item in session_result.scalars().all(): res.append(item) result = Result.ListResult(error=False, info='Success', result=res) - except Exception as e: - result = Result.ListResult(error=True, info=repr(e), result=res) + except Exception as e_: + result = Result.ListResult(error=True, info=repr(e_), result=res) return result __all__ = [ - 'NBdb' + 'BaseDB' ] diff --git a/omega_miya/utils/Omega_Base/model/__init__.py b/omega_miya/database/model/__init__.py similarity index 86% rename from omega_miya/utils/Omega_Base/model/__init__.py rename to omega_miya/database/model/__init__.py index ee17e7bb..3662830e 100644 --- a/omega_miya/utils/Omega_Base/model/__init__.py +++ b/omega_miya/database/model/__init__.py @@ -10,10 +10,13 @@ from .pixiv_user_artwork import DBPixivUserArtwork from .pixivillust import DBPixivillust from .pixivision import DBPixivision +from .plugins import DBPlugin from .skill import DBSkill +from .statistic import DBStatistic +from .status import DBStatus from .subscription import DBSubscription from .user import DBUser -from .status import DBStatus + __all__ = [ 'DBAuth', @@ -29,8 +32,10 @@ 'DBPixivUserArtwork', 'DBPixivillust', 'DBPixivision', + 'DBPlugin', 'DBSkill', + 'DBStatistic', + 'DBStatus', 'DBSubscription', - 'DBUser', - 'DBStatus' + 'DBUser' ] diff --git a/omega_miya/utils/Omega_Base/model/auth.py b/omega_miya/database/model/auth.py similarity index 97% rename from omega_miya/utils/Omega_Base/model/auth.py rename to omega_miya/database/model/auth.py index 485dde5e..4bf35c44 100644 --- a/omega_miya/utils/Omega_Base/model/auth.py +++ b/omega_miya/database/model/auth.py @@ -1,6 +1,6 @@ -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import AuthUser, AuthGroup, User, Friends, Group, BotGroup +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import AuthUser, AuthGroup, User, Friends, Group, BotGroup from .friend import DBFriend from .bot_group import DBBotGroup from .bot_self import DBBot @@ -29,7 +29,7 @@ async def id(self) -> Result.IntResult: if self_bot_id_result.error: return Result.IntResult(error=True, info='Bot not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -76,7 +76,7 @@ async def set(self, allow_tag: int, deny_tag: int, auth_info: str = None) -> Res if self_bot_id_result.error: return Result.IntResult(error=True, info='Bot not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -154,7 +154,7 @@ async def allow_tag(self) -> Result.IntResult: if self_bot_id_result.error: return Result.IntResult(error=True, info='Bot not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -197,7 +197,7 @@ async def deny_tag(self) -> Result.IntResult: if self_bot_id_result.error: return Result.IntResult(error=True, info='Bot not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -240,7 +240,7 @@ async def tags_info(self) -> Result.IntTupleResult: if self_bot_id_result.error: return Result.IntTupleResult(error=True, info='Bot not exist', result=(-1, -1)) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -283,7 +283,7 @@ async def delete(self) -> Result.IntResult: if self_bot_id_result.error: return Result.IntResult(error=True, info='Bot not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -333,7 +333,7 @@ async def list(cls, auth_type: str, auth_id: int, self_bot: DBBot) -> Result.Lis if self_bot_id_result.error: return Result.ListResult(error=True, info='Bot not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: diff --git a/omega_miya/utils/Omega_Base/model/bilidynamic.py b/omega_miya/database/model/bilidynamic.py similarity index 92% rename from omega_miya/utils/Omega_Base/model/bilidynamic.py rename to omega_miya/database/model/bilidynamic.py index a32439d8..c2d55433 100644 --- a/omega_miya/utils/Omega_Base/model/bilidynamic.py +++ b/omega_miya/database/model/bilidynamic.py @@ -1,6 +1,6 @@ -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import Bilidynamic +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import Bilidynamic from datetime import datetime from sqlalchemy.future import select from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound @@ -12,7 +12,7 @@ def __init__(self, uid: int, dynamic_id: int): self.dynamic_id = dynamic_id async def id(self) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -36,7 +36,7 @@ async def exist(self) -> bool: return result.success() async def add(self, dynamic_type: int, content: str) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -67,7 +67,7 @@ async def add(self, dynamic_type: int, content: str) -> Result.IntResult: @classmethod async def list_all_dynamic(cls) -> Result.IntListResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -82,7 +82,7 @@ async def list_all_dynamic(cls) -> Result.IntListResult: @classmethod async def list_dynamic_by_uid(cls, uid: int) -> Result.IntListResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: diff --git a/omega_miya/utils/Omega_Base/model/bot_group.py b/omega_miya/database/model/bot_group.py similarity index 96% rename from omega_miya/utils/Omega_Base/model/bot_group.py rename to omega_miya/database/model/bot_group.py index 21d3916c..82a64a5d 100644 --- a/omega_miya/utils/Omega_Base/model/bot_group.py +++ b/omega_miya/database/model/bot_group.py @@ -1,6 +1,6 @@ -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import \ +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import \ User, Group, BotGroup, UserGroup, Vacation, Skill, UserSkill, \ Subscription, GroupSub, GroupSetting, EmailBox, GroupEmailBox from .user import DBUser @@ -25,7 +25,7 @@ async def list_exist_bot_groups(cls, self_bot: DBBot) -> Result.ListResult: if self_bot_id_result.error: return Result.ListResult(error=True, info='Bot not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -52,7 +52,7 @@ async def list_exist_bot_groups_by_notice_permissions( if self_bot_id_result.error: return Result.ListResult(error=True, info='Bot not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -80,7 +80,7 @@ async def list_exist_bot_groups_by_command_permissions( if self_bot_id_result.error: return Result.ListResult(error=True, info='Bot not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -108,7 +108,7 @@ async def list_exist_bot_groups_by_permission_level( if self_bot_id_result.error: return Result.ListResult(error=True, info='Bot not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -138,7 +138,7 @@ async def bot_group_id(self) -> Result.IntResult: if self_bot_id_result.error: return Result.IntResult(error=True, info='Bot not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -170,7 +170,7 @@ async def memo(self) -> Result.TextResult: if self_bot_id_result.error: return Result.TextResult(error=True, info='Bot not exist', result='') - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -204,7 +204,7 @@ async def set_bot_group(self, group_memo: str = None) -> Result.IntResult: elif len(group_memo) > 64: group_memo = group_memo[:63] - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -239,7 +239,7 @@ async def del_bot_group(self) -> Result.IntResult: if bot_group_id_result.error: return Result.IntResult(error=True, info='BotGroup not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -268,7 +268,7 @@ async def member_list(self) -> Result.TupleListResult: if bot_group_id_result.error: return Result.TupleListResult(error=True, info='BotGroup not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -293,7 +293,7 @@ async def member_add(self, user: DBUser, user_group_nickname: str) -> Result.Int if user_id_result.error: return Result.IntResult(error=True, info='User not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -333,7 +333,7 @@ async def member_del(self, user: DBUser) -> Result.IntResult: if user_id_result.error: return Result.IntResult(error=True, info='User not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -362,7 +362,7 @@ async def member_clear(self) -> Result.IntResult: if bot_group_id_result.error: return Result.IntResult(error=True, info='BotGroup not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -387,7 +387,7 @@ async def permission_reset(self) -> Result.IntResult: if self_bot_id_result.error: return Result.IntResult(error=True, info='Bot not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -423,7 +423,7 @@ async def permission_set(self, notice: int = 0, command: int = 0, level: int = 0 if self_bot_id_result.error: return Result.IntResult(error=True, info='Bot not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -462,7 +462,7 @@ async def permission_info(self) -> Result.IntTupleResult: if self_bot_id_result.error: return Result.IntTupleResult(error=True, info='Bot not exist', result=(-1, -1, -1)) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -490,7 +490,7 @@ async def permission_notice(self) -> Result.IntResult: if self_bot_id_result.error: return Result.IntResult(error=True, info='Bot not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -514,7 +514,7 @@ async def permission_command(self) -> Result.IntResult: if self_bot_id_result.error: return Result.IntResult(error=True, info='Bot not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -538,7 +538,7 @@ async def permission_level(self) -> Result.IntResult: if self_bot_id_result.error: return Result.IntResult(error=True, info='Bot not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -559,7 +559,7 @@ async def idle_member_list(self) -> Result.ListResult: return Result.ListResult(error=True, info='BotGroup not exist', result=[]) res = [] - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -602,7 +602,7 @@ async def idle_skill_list(self, skill: DBSkill) -> Result.TupleListResult: return Result.TupleListResult(error=True, info='Skill not exist', result=[]) res = [] - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -634,7 +634,7 @@ async def vacation_member_list(self) -> Result.TupleListResult: if bot_group_id_result.error: return Result.TupleListResult(error=True, info='BotGroup not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -670,7 +670,7 @@ async def subscription_list(self) -> Result.TupleListResult: if bot_group_id_result.error: return Result.TupleListResult(error=True, info='BotGroup not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -695,7 +695,7 @@ async def subscription_list_by_type(self, sub_type: int) -> Result.TupleListResu if bot_group_id_result.error: return Result.TupleListResult(error=True, info='BotGroup not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -721,7 +721,7 @@ async def subscription_add(self, sub: DBSubscription, group_sub_info: str = None if sub_id_result.error: return Result.IntResult(error=True, info='Subscription not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -759,7 +759,7 @@ async def subscription_del(self, sub: DBSubscription) -> Result.IntResult: if sub_id_result.error: return Result.IntResult(error=True, info='Subscription not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -788,7 +788,7 @@ async def subscription_clear(self) -> Result.IntResult: if bot_group_id_result.error: return Result.IntResult(error=True, info='BotGroup not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -809,7 +809,7 @@ async def subscription_clear_by_type(self, sub_type: int) -> Result.IntResult: if bot_group_id_result.error: return Result.IntResult(error=True, info='BotGroup not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -833,7 +833,7 @@ async def mailbox_list(self) -> Result.ListResult: if bot_group_id_result.error: return Result.ListResult(error=True, info='BotGroup not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -858,7 +858,7 @@ async def mailbox_add(self, mailbox: DBEmailBox, mailbox_info: str = None) -> Re if mailbox_id_result.error: return Result.IntResult(error=True, info='Mailbox not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -897,7 +897,7 @@ async def mailbox_del(self, mailbox: DBEmailBox) -> Result.IntResult: if mailbox_id_result.error: return Result.IntResult(error=True, info='Mailbox not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -926,7 +926,7 @@ async def mailbox_clear(self) -> Result.IntResult: if bot_group_id_result.error: return Result.IntResult(error=True, info='BotGroup not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -950,7 +950,7 @@ async def setting_list(self) -> Result.TupleListResult: if bot_group_id_result.error: return Result.TupleListResult(error=True, info='BotGroup not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -974,7 +974,7 @@ async def setting_get(self, setting_name: str) -> Result.TextTupleResult: if bot_group_id_result.error: return Result.TextTupleResult(error=True, info='BotGroup not exist', result=('', '', '')) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -1006,7 +1006,7 @@ async def setting_set( if bot_group_id_result.error: return Result.IntResult(error=True, info='BotGroup not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -1045,7 +1045,7 @@ async def setting_del(self, setting_name: str) -> Result.IntResult: if bot_group_id_result.error: return Result.IntResult(error=True, info='BotGroup not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): diff --git a/omega_miya/utils/Omega_Base/model/bot_self.py b/omega_miya/database/model/bot_self.py similarity index 91% rename from omega_miya/utils/Omega_Base/model/bot_self.py rename to omega_miya/database/model/bot_self.py index 9d41aa39..77742fa0 100644 --- a/omega_miya/utils/Omega_Base/model/bot_self.py +++ b/omega_miya/database/model/bot_self.py @@ -8,9 +8,9 @@ @Software : PyCharm """ -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import BotSelf +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import BotSelf from datetime import datetime from sqlalchemy.future import select from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound @@ -21,7 +21,7 @@ def __init__(self, self_qq: int): self.self_qq = self_qq async def id(self) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -43,7 +43,7 @@ async def exist(self) -> bool: return result.success() async def upgrade(self, status: int = 0, info: str = None) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): diff --git a/omega_miya/utils/Omega_Base/model/cooldown.py b/omega_miya/database/model/cooldown.py similarity index 80% rename from omega_miya/utils/Omega_Base/model/cooldown.py rename to omega_miya/database/model/cooldown.py index 687210f2..2b96ced1 100644 --- a/omega_miya/utils/Omega_Base/model/cooldown.py +++ b/omega_miya/database/model/cooldown.py @@ -1,27 +1,34 @@ -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import CoolDownEvent +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import CoolDownEvent from datetime import datetime from sqlalchemy.future import select from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound class DBCoolDownEvent(object): + global_group_type: str = 'global_group' + global_user_type: str = 'global_user' + group_type: str = 'group' + user_type: str = 'user' + @classmethod - async def add_global_cool_down_event(cls, stop_at: datetime, description: str = None) -> Result.IntResult: + async def add_global_group_cool_down_event( + cls, group_id: int, stop_at: datetime, description: str = None) -> Result.IntResult: """ :return: result = 0: Success result = -1: Error """ - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): try: session_result = await session.execute( select(CoolDownEvent). - where(CoolDownEvent.event_type == 'global') + where(CoolDownEvent.event_type == cls.global_group_type). + where(CoolDownEvent.group_id == group_id) ) exist_event = session_result.scalar_one() exist_event.stop_at = stop_at @@ -30,7 +37,8 @@ async def add_global_cool_down_event(cls, stop_at: datetime, description: str = result = Result.IntResult(error=False, info='Success upgraded', result=0) except NoResultFound: new_event = CoolDownEvent( - event_type='global', stop_at=stop_at, description=description, created_at=datetime.now()) + event_type=cls.global_group_type, group_id=group_id, stop_at=stop_at, + description=description, created_at=datetime.now()) session.add(new_event) result = Result.IntResult(error=False, info='Success added', result=0) await session.commit() @@ -43,7 +51,7 @@ async def add_global_cool_down_event(cls, stop_at: datetime, description: str = return result @classmethod - async def check_global_cool_down_event(cls) -> Result.IntResult: + async def check_global_group_cool_down_event(cls, group_id: int) -> Result.IntResult: """ :return: result = 2: Success with CoolDown Event expired @@ -51,13 +59,14 @@ async def check_global_cool_down_event(cls) -> Result.IntResult: result = 0: Success with CoolDown Event not found result = -1: Error """ - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: session_result = await session.execute( select(CoolDownEvent). - where(CoolDownEvent.event_type == 'global') + where(CoolDownEvent.event_type == cls.global_group_type). + where(CoolDownEvent.group_id == group_id) ) event = session_result.scalar_one() stop_at = event.stop_at @@ -74,22 +83,22 @@ async def check_global_cool_down_event(cls) -> Result.IntResult: return result @classmethod - async def add_plugin_cool_down_event( - cls, plugin: str, stop_at: datetime, description: str = None) -> Result.IntResult: + async def add_global_user_cool_down_event( + cls, user_id: int, stop_at: datetime, description: str = None) -> Result.IntResult: """ :return: result = 0: Success result = -1: Error """ - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): try: session_result = await session.execute( select(CoolDownEvent). - where(CoolDownEvent.event_type == 'plugin'). - where(CoolDownEvent.plugin == plugin) + where(CoolDownEvent.event_type == cls.global_user_type). + where(CoolDownEvent.user_id == user_id) ) exist_event = session_result.scalar_one() exist_event.stop_at = stop_at @@ -98,8 +107,8 @@ async def add_plugin_cool_down_event( result = Result.IntResult(error=False, info='Success upgraded', result=0) except NoResultFound: new_event = CoolDownEvent( - event_type='plugin', plugin=plugin, stop_at=stop_at, description=description, - created_at=datetime.now()) + event_type=cls.global_user_type, user_id=user_id, stop_at=stop_at, + description=description, created_at=datetime.now()) session.add(new_event) result = Result.IntResult(error=False, info='Success added', result=0) await session.commit() @@ -112,7 +121,7 @@ async def add_plugin_cool_down_event( return result @classmethod - async def check_plugin_cool_down_event(cls, plugin: str) -> Result.IntResult: + async def check_global_user_cool_down_event(cls, user_id: int) -> Result.IntResult: """ :return: result = 2: Success with CoolDown Event expired @@ -120,14 +129,14 @@ async def check_plugin_cool_down_event(cls, plugin: str) -> Result.IntResult: result = 0: Success with CoolDown Event not found result = -1: Error """ - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: session_result = await session.execute( select(CoolDownEvent). - where(CoolDownEvent.event_type == 'plugin'). - where(CoolDownEvent.plugin == plugin) + where(CoolDownEvent.event_type == cls.global_user_type). + where(CoolDownEvent.user_id == user_id) ) event = session_result.scalar_one() stop_at = event.stop_at @@ -151,14 +160,14 @@ async def add_group_cool_down_event( result = 0: Success result = -1: Error """ - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): try: session_result = await session.execute( select(CoolDownEvent). - where(CoolDownEvent.event_type == 'group'). + where(CoolDownEvent.event_type == cls.group_type). where(CoolDownEvent.plugin == plugin). where(CoolDownEvent.group_id == group_id) ) @@ -169,7 +178,7 @@ async def add_group_cool_down_event( result = Result.IntResult(error=False, info='Success upgraded', result=0) except NoResultFound: new_event = CoolDownEvent( - event_type='group', plugin=plugin, group_id=group_id, stop_at=stop_at, + event_type=cls.group_type, plugin=plugin, group_id=group_id, stop_at=stop_at, description=description, created_at=datetime.now()) session.add(new_event) result = Result.IntResult(error=False, info='Success added', result=0) @@ -191,13 +200,13 @@ async def check_group_cool_down_event(cls, plugin: str, group_id: int) -> Result result = 0: Success with CoolDown Event not found result = -1: Error """ - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: session_result = await session.execute( select(CoolDownEvent). - where(CoolDownEvent.event_type == 'group'). + where(CoolDownEvent.event_type == cls.group_type). where(CoolDownEvent.plugin == plugin). where(CoolDownEvent.group_id == group_id) ) @@ -223,14 +232,14 @@ async def add_user_cool_down_event( result = 0: Success result = -1: Error """ - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): try: session_result = await session.execute( select(CoolDownEvent). - where(CoolDownEvent.event_type == 'user'). + where(CoolDownEvent.event_type == cls.user_type). where(CoolDownEvent.plugin == plugin). where(CoolDownEvent.user_id == user_id) ) @@ -241,8 +250,8 @@ async def add_user_cool_down_event( result = Result.IntResult(error=False, info='Success upgraded', result=0) except NoResultFound: new_event = CoolDownEvent( - event_type='user', plugin=plugin, user_id=user_id, stop_at=stop_at, description=description, - created_at=datetime.now()) + event_type=cls.user_type, plugin=plugin, user_id=user_id, stop_at=stop_at, + description=description, created_at=datetime.now()) session.add(new_event) result = Result.IntResult(error=False, info='Success added', result=0) await session.commit() @@ -263,13 +272,13 @@ async def check_user_cool_down_event(cls, plugin: str, user_id: int) -> Result.I result = 0: Success with CoolDown Event not found result = -1: Error """ - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: session_result = await session.execute( select(CoolDownEvent). - where(CoolDownEvent.event_type == 'user'). + where(CoolDownEvent.event_type == cls.user_type). where(CoolDownEvent.plugin == plugin). where(CoolDownEvent.user_id == user_id) ) @@ -288,19 +297,22 @@ async def check_user_cool_down_event(cls, plugin: str, user_id: int) -> Result.I return result @classmethod - async def clear_time_out_event(cls): - async_session = NBdb().get_async_session() + async def clear_time_out_event(cls) -> Result.DictResult: + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): session_result = await session.execute( select(CoolDownEvent).order_by(CoolDownEvent.id) ) events = session_result.scalars().all() + failed_events = [] for event in events: try: if datetime.now() >= event.stop_at: await session.delete(event) await session.commit() - except Exception: + except Exception as e: await session.rollback() + failed_events.append((event, e)) continue + return Result.DictResult(error=False, info='Tasks completed', result={'all': events, 'failed': failed_events}) diff --git a/omega_miya/utils/Omega_Base/model/friend.py b/omega_miya/database/model/friend.py similarity index 95% rename from omega_miya/utils/Omega_Base/model/friend.py rename to omega_miya/database/model/friend.py index f1815140..3a78d1c9 100644 --- a/omega_miya/utils/Omega_Base/model/friend.py +++ b/omega_miya/database/model/friend.py @@ -1,8 +1,8 @@ from typing import Optional from datetime import datetime -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import Friends, User, Subscription, UserSub +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import Friends, User, Subscription, UserSub from .user import DBUser from .bot_self import DBBot from .subscription import DBSubscription @@ -21,7 +21,7 @@ async def list_exist_friends(cls, self_bot: DBBot) -> Result.ListResult: if self_bot_id_result.error: return Result.ListResult(error=True, info='Bot not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -48,7 +48,7 @@ async def list_exist_friends_by_private_permission( if self_bot_id_result.error: return Result.ListResult(error=True, info='Bot not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -78,7 +78,7 @@ async def friend_id(self) -> Result.IntResult: if self_bot_id_result.error: return Result.IntResult(error=True, info='Bot not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -113,7 +113,7 @@ async def set_friend( if self_bot_id_result.error: return Result.IntResult(error=True, info='Bot not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -155,7 +155,7 @@ async def del_friend(self) -> Result.IntResult: if friend_id_result.error: return Result.IntResult(error=True, info='Friend not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -188,7 +188,7 @@ async def set_private_permission(self, private_permissions: int) -> Result.IntRe if self_bot_id_result.error: return Result.IntResult(error=True, info='Bot not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -222,7 +222,7 @@ async def get_private_permission(self) -> Result.IntResult: if self_bot_id_result.error: return Result.IntResult(error=True, info='Bot not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -249,7 +249,7 @@ async def subscription_list(self) -> Result.TupleListResult: if friend_id_result.error: return Result.TupleListResult(error=True, info='Friend not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -274,7 +274,7 @@ async def subscription_list_by_type(self, sub_type: int) -> Result.TupleListResu if friend_id_result.error: return Result.TupleListResult(error=True, info='Friend not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -300,7 +300,7 @@ async def subscription_add(self, sub: DBSubscription, user_sub_info: str = None) if sub_id_result.error: return Result.IntResult(error=True, info='Subscription not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -338,7 +338,7 @@ async def subscription_del(self, sub: DBSubscription) -> Result.IntResult: if sub_id_result.error: return Result.IntResult(error=True, info='Subscription not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -367,7 +367,7 @@ async def subscription_clear(self) -> Result.IntResult: if friend_id_result.error: return Result.IntResult(error=True, info='Friend not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -388,7 +388,7 @@ async def subscription_clear_by_type(self, sub_type: int) -> Result.IntResult: if friend_id_result.error: return Result.IntResult(error=True, info='Friend not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): diff --git a/omega_miya/utils/Omega_Base/model/group.py b/omega_miya/database/model/group.py similarity index 92% rename from omega_miya/utils/Omega_Base/model/group.py rename to omega_miya/database/model/group.py index b07b7d83..88bf2374 100644 --- a/omega_miya/utils/Omega_Base/model/group.py +++ b/omega_miya/database/model/group.py @@ -1,6 +1,6 @@ -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import Group +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import Group from datetime import datetime from sqlalchemy.future import select from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound @@ -11,7 +11,7 @@ def __init__(self, group_id: int): self.group_id = group_id async def id(self) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -33,7 +33,7 @@ async def exist(self) -> bool: return result.success() async def name(self) -> Result.TextResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -51,7 +51,7 @@ async def name(self) -> Result.TextResult: return result async def add(self, name: str) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -81,7 +81,7 @@ async def delete(self) -> Result.IntResult: if id_result.error: return Result.IntResult(error=True, info='Group not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -106,7 +106,7 @@ async def delete(self) -> Result.IntResult: @classmethod async def list_all_group(cls) -> Result.IntListResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: diff --git a/omega_miya/utils/Omega_Base/model/history.py b/omega_miya/database/model/history.py similarity index 94% rename from omega_miya/utils/Omega_Base/model/history.py rename to omega_miya/database/model/history.py index 77aa1cfc..e3c8e7af 100644 --- a/omega_miya/utils/Omega_Base/model/history.py +++ b/omega_miya/database/model/history.py @@ -1,6 +1,6 @@ -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import History +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import History from sqlalchemy.future import select from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound from datetime import datetime @@ -15,7 +15,7 @@ def __init__(self, time: int, self_id: int, post_type: str, detail_type: str): async def add(self, sub_type: str = 'Undefined', event_id: int = 0, group_id: int = -1, user_id: int = -1, user_name: str = None, raw_data: str = None, msg_data: str = None) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -42,7 +42,7 @@ async def search_unique_msg( group_id: int, user_id: int ) -> Result.AnyResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -77,7 +77,7 @@ async def search_msgs( group_id: int = -1, user_id: int = -1 ) -> Result.ListResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -111,7 +111,7 @@ async def search_msgs_data( group_id: int = -1, user_id: int = -1 ) -> Result.TextListResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): diff --git a/omega_miya/utils/Omega_Base/model/mail.py b/omega_miya/database/model/mail.py similarity index 93% rename from omega_miya/utils/Omega_Base/model/mail.py rename to omega_miya/database/model/mail.py index 0d150085..eec355ba 100644 --- a/omega_miya/utils/Omega_Base/model/mail.py +++ b/omega_miya/database/model/mail.py @@ -1,6 +1,6 @@ -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import Email, EmailBox, GroupEmailBox +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import Email, EmailBox, GroupEmailBox from datetime import datetime from sqlalchemy.future import select from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound @@ -11,7 +11,7 @@ def __init__(self, address: str): self.address = address async def id(self) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -35,7 +35,7 @@ async def exist(self) -> bool: @classmethod async def list(cls) -> Result.ListResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -49,7 +49,7 @@ async def list(cls) -> Result.ListResult: return result async def get_info(self) -> Result.DictResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -72,7 +72,7 @@ async def get_info(self) -> Result.DictResult: return result async def add(self, server_host: str, password: str, port: int = 993) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -110,7 +110,7 @@ async def delete(self) -> Result.IntResult: # 清空持已绑定这个邮箱的群组 await self.mailbox_group_clear() - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -138,7 +138,7 @@ async def mailbox_group_clear(self) -> Result.IntResult: if id_result.error: return Result.IntResult(error=True, info='EmailBox not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -162,7 +162,7 @@ def __init__(self, mail_hash: str): async def add( self, date: str, header: str, sender: str, to: str, body: str = None, html: str = None ) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): diff --git a/omega_miya/utils/Omega_Base/model/pixiv_user_artwork.py b/omega_miya/database/model/pixiv_user_artwork.py similarity index 92% rename from omega_miya/utils/Omega_Base/model/pixiv_user_artwork.py rename to omega_miya/database/model/pixiv_user_artwork.py index 328800fe..2a218611 100644 --- a/omega_miya/utils/Omega_Base/model/pixiv_user_artwork.py +++ b/omega_miya/database/model/pixiv_user_artwork.py @@ -8,9 +8,9 @@ @Software : PyCharm """ -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import PixivUserArtwork +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import PixivUserArtwork from datetime import datetime from sqlalchemy.future import select from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound @@ -22,7 +22,7 @@ def __init__(self, pid: int, uid: int): self.uid = uid async def id(self) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -46,7 +46,7 @@ async def exist(self) -> bool: return result.success() async def add(self, uname: str, title: str) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -77,7 +77,7 @@ async def add(self, uname: str, title: str) -> Result.IntResult: @classmethod async def list_all_artwork(cls) -> Result.IntListResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -92,7 +92,7 @@ async def list_all_artwork(cls) -> Result.IntListResult: @classmethod async def list_artwork_by_uid(cls, uid: int) -> Result.IntListResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: diff --git a/omega_miya/utils/Omega_Base/model/pixivillust.py b/omega_miya/database/model/pixivillust.py similarity index 87% rename from omega_miya/utils/Omega_Base/model/pixivillust.py rename to omega_miya/database/model/pixivillust.py index 94c65352..8665f585 100644 --- a/omega_miya/utils/Omega_Base/model/pixivillust.py +++ b/omega_miya/database/model/pixivillust.py @@ -1,7 +1,7 @@ -from typing import List -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import Pixiv, PixivPage +from typing import List, Optional +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import Pixiv, PixivPage from datetime import datetime from sqlalchemy.future import select from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound @@ -14,7 +14,7 @@ def __init__(self, pid: int): self.pid = pid async def id(self) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -43,7 +43,7 @@ async def add( ) -> Result.IntResult: tag_text = ','.join(tags) # 将作品信息写入pixiv_illust表 - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -89,7 +89,7 @@ async def upgrade_page( if pixiv_id_result.error: return Result.IntResult(error=True, info='PixivIllust not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -131,7 +131,7 @@ async def get_page(self, page: int = 0) -> Result.TextTupleResult: if pixiv_id_result.error: return Result.TextTupleResult(error=True, info='PixivIllust not exist', result=('', '', '', '')) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -159,7 +159,7 @@ async def get_all_page(self) -> Result.ListResult: if pixiv_id_result.error: return Result.ListResult(error=True, info='PixivIllust not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -175,16 +175,36 @@ async def get_all_page(self) -> Result.ListResult: return result @classmethod - async def rand_illust(cls, num: int, nsfw_tag: int) -> Result.ListResult: - async_session = NBdb().get_async_session() + async def rand_illust(cls, num: int, nsfw_tag: int, *, ratio: Optional[int] = None) -> Result.ListResult: + """ + 随机抽取图库中的作品 + :param num: 抽取数量 + :param nsfw_tag: nsfw标签 + :param ratio: 图片长宽, 1: 横图, -1: 纵图, 0: 正方形图 + :return: ListResult, pid列表 + """ + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: - session_result = await session.execute( - select(Pixiv.pid). - where(Pixiv.nsfw_tag == nsfw_tag). - order_by(func.random()).limit(num) - ) + if ratio is None: + query = select(Pixiv.pid).where(Pixiv.nsfw_tag == nsfw_tag).order_by(func.random()).limit(num) + elif ratio < 0: + query = (select(Pixiv.pid). + where(Pixiv.nsfw_tag == nsfw_tag). + where(Pixiv.width < Pixiv.height). + order_by(func.random()).limit(num)) + elif ratio > 0: + query = (select(Pixiv.pid). + where(Pixiv.nsfw_tag == nsfw_tag). + where(Pixiv.width > Pixiv.height). + order_by(func.random()).limit(num)) + else: + query = (select(Pixiv.pid). + where(Pixiv.nsfw_tag == nsfw_tag). + where(Pixiv.width == Pixiv.height). + order_by(func.random()).limit(num)) + session_result = await session.execute(query) res = [x for x in session_result.scalars().all()] result = Result.ListResult(error=False, info='Success', result=res) except Exception as e: @@ -193,7 +213,7 @@ async def rand_illust(cls, num: int, nsfw_tag: int) -> Result.ListResult: @classmethod async def status(cls) -> Result.DictResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -214,7 +234,7 @@ async def status(cls) -> Result.DictResult: @classmethod async def count_keywords(cls, keywords: List[str]) -> Result.DictResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -248,7 +268,7 @@ async def count_keywords(cls, keywords: List[str]) -> Result.DictResult: @classmethod async def list_illust( cls, keywords: List[str], num: int, nsfw_tag: int, acc_mode: bool = False) -> Result.ListResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -285,7 +305,7 @@ async def list_illust( @classmethod async def list_all_illust(cls) -> Result.IntListResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -298,7 +318,7 @@ async def list_all_illust(cls) -> Result.IntListResult: @classmethod async def list_all_illust_by_nsfw_tag(cls, nsfw_tag: int) -> Result.IntListResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -311,7 +331,7 @@ async def list_all_illust_by_nsfw_tag(cls, nsfw_tag: int) -> Result.IntListResul @classmethod async def reset_all_nsfw_tag(cls) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -337,7 +357,7 @@ async def set_nsfw_tag(cls, tags: dict) -> Result.IntResult: :param tags: Dict[pid: int, nsfw_tag: int] :return: """ - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): diff --git a/omega_miya/utils/Omega_Base/model/pixivision.py b/omega_miya/database/model/pixivision.py similarity index 91% rename from omega_miya/utils/Omega_Base/model/pixivision.py rename to omega_miya/database/model/pixivision.py index 489b5324..1eff207d 100644 --- a/omega_miya/utils/Omega_Base/model/pixivision.py +++ b/omega_miya/database/model/pixivision.py @@ -1,6 +1,6 @@ -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import Pixivision +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import Pixivision from datetime import datetime from sqlalchemy.future import select from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound @@ -11,7 +11,7 @@ def __init__(self, aid: int): self.aid = aid async def id(self) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -33,7 +33,7 @@ async def exist(self) -> bool: return result.success() async def add(self, title: str, description: str, tags: str, illust_id: str, url: str) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -66,7 +66,7 @@ async def add(self, title: str, description: str, tags: str, illust_id: str, url @classmethod async def list_article_id(cls) -> Result.IntListResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: diff --git a/omega_miya/database/model/plugins.py b/omega_miya/database/model/plugins.py new file mode 100644 index 00000000..563168c0 --- /dev/null +++ b/omega_miya/database/model/plugins.py @@ -0,0 +1,109 @@ +""" +@Author : Ailitonia +@Date : 2021/09/12 12:19 +@FileName : plugins.py +@Project : nonebot2_miya +@Description : 数据库 plugins 表 module +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +from typing import Optional +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import OmegaPlugins +from datetime import datetime +from sqlalchemy.future import select +from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound + + +class DBPlugin(object): + def __init__(self, plugin_name: str): + self.plugin_name = plugin_name + + async def update( + self, + enabled: Optional[int] = None, + *, + matchers: Optional[int] = None, + info: Optional[str] = None + ) -> Result.IntResult: + async_session = BaseDB().get_async_session() + async with async_session() as session: + try: + async with session.begin(): + try: + # 已存在则更新 + session_result = await session.execute( + select(OmegaPlugins).where(OmegaPlugins.plugin_name == self.plugin_name) + ) + exist_plugin = session_result.scalar_one() + if enabled is not None: + exist_plugin.enabled = enabled + if matchers is not None: + exist_plugin.matchers = matchers + if info is not None: + exist_plugin.info = info + exist_plugin.updated_at = datetime.now() + result = Result.IntResult(error=False, info='Success upgraded', result=0) + except NoResultFound: + # 不存在则添加信息 + if enabled is None: + enabled = 1 + new_plugin = OmegaPlugins( + plugin_name=self.plugin_name, + enabled=enabled, + matchers=matchers, + info=info, + created_at=datetime.now()) + session.add(new_plugin) + result = Result.IntResult(error=False, info='Success added', result=0) + await session.commit() + except MultipleResultsFound: + await session.rollback() + result = Result.IntResult(error=True, info=f'{self.plugin_name} MultipleResultsFound', result=-1) + except Exception as e: + await session.rollback() + result = Result.IntResult(error=True, info=f'{self.plugin_name} update failed, {repr(e)}', result=-1) + return result + + async def get_enabled_status(self) -> Result.IntResult: + async_session = BaseDB().get_async_session() + async with async_session() as session: + async with session.begin(): + try: + session_result = await session.execute( + select(OmegaPlugins.enabled).where(OmegaPlugins.plugin_name == self.plugin_name) + ) + enabled = session_result.scalar_one() + result = Result.IntResult(error=False, info='Success', result=enabled) + except NoResultFound: + result = Result.IntResult(error=True, info='NoResultFound', result=-1) + except MultipleResultsFound: + result = Result.IntResult(error=True, info='MultipleResultsFound', result=-1) + except Exception as e: + result = Result.IntResult(error=True, info=repr(e), result=-1) + return result + + @classmethod + async def list_plugins(cls, *, enabled: Optional[int] = None) -> Result.TextListResult: + async_session = BaseDB().get_async_session() + async with async_session() as session: + async with session.begin(): + try: + if enabled is None: + session_result = await session.execute(select(OmegaPlugins.plugin_name)) + else: + session_result = await session.execute( + select(OmegaPlugins.plugin_name).where(OmegaPlugins.enabled == enabled) + ) + res = [x for x in session_result.scalars().all()] + result = Result.TextListResult(error=False, info='Success', result=res) + except Exception as e: + result = Result.TextListResult(error=True, info=repr(e), result=[]) + return result + + +__all__ = [ + 'DBPlugin' +] diff --git a/omega_miya/utils/Omega_Base/model/skill.py b/omega_miya/database/model/skill.py similarity index 92% rename from omega_miya/utils/Omega_Base/model/skill.py rename to omega_miya/database/model/skill.py index 9dde7c43..472edf52 100644 --- a/omega_miya/utils/Omega_Base/model/skill.py +++ b/omega_miya/database/model/skill.py @@ -1,6 +1,6 @@ -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import Skill, User, UserSkill +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import Skill, User, UserSkill from datetime import datetime from sqlalchemy.future import select from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound @@ -11,7 +11,7 @@ def __init__(self, name: str): self.name = name async def id(self) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -33,7 +33,7 @@ async def exist(self) -> bool: return result.success() async def add(self, description: str) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -66,7 +66,7 @@ async def delete(self) -> Result.IntResult: # 清空持有这个技能人的技能 await self.able_member_clear() - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -93,7 +93,7 @@ async def able_member_list(self) -> Result.ListResult: if id_result.error: return Result.ListResult(error=True, info='Skill not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -113,7 +113,7 @@ async def able_member_clear(self) -> Result.IntResult: if id_result.error: return Result.IntResult(error=True, info='Skill not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -132,7 +132,7 @@ async def able_member_clear(self) -> Result.IntResult: @classmethod async def list_available_skill(cls) -> Result.TextListResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: diff --git a/omega_miya/database/model/statistic.py b/omega_miya/database/model/statistic.py new file mode 100644 index 00000000..518b45d1 --- /dev/null +++ b/omega_miya/database/model/statistic.py @@ -0,0 +1,145 @@ +""" +@Author : Ailitonia +@Date : 2021/08/14 18:14 +@FileName : statistic.py +@Project : nonebot2_miya +@Description : 数据库统计表model +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +from typing import Optional +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import OmegaStatistics +from datetime import datetime +from sqlalchemy.future import select +from sqlalchemy.sql.expression import func + + +class DBStatistic(object): + def __init__(self, self_bot_id: int): + self.self_bot_id = self_bot_id + + async def add(self, + module_name: str, + plugin_name: str, + group_id: int, + user_id: int, + using_datetime: datetime, + *, + raw_message: Optional[str] = None, + info: Optional[str] = None) -> Result.IntResult: + async_session = BaseDB().get_async_session() + async with async_session() as session: + try: + async with session.begin(): + new_statistic = OmegaStatistics(module_name=module_name, plugin_name=plugin_name, + self_bot_id=self.self_bot_id, group_id=group_id, user_id=user_id, + using_datetime=using_datetime, raw_message=raw_message, info=info, + created_at=datetime.now()) + session.add(new_statistic) + await session.commit() + result = Result.IntResult(error=False, info='Success add', result=0) + except Exception as e: + await session.rollback() + result = Result.IntResult(error=True, info=repr(e), result=-1) + return result + + @classmethod + async def get_total_statistic(cls, *, start_time: Optional[datetime] = None) -> Result.TupleListResult: + """ + :param start_time: 统计起始时间, 为空则返回全部 + :return: List[Tuple[plugin_name, count]] + """ + async_session = BaseDB().get_async_session() + async with async_session() as session: + async with session.begin(): + try: + # 构造查询 + query = select(func.count(OmegaStatistics.module_name), OmegaStatistics.plugin_name) + if start_time: + query = query.where(OmegaStatistics.using_datetime >= start_time) + query = query.group_by(OmegaStatistics.plugin_name) + # 执行查询 + session_result = await session.execute(query) + res = [(plugin_name, count) for count, plugin_name in session_result.all()] + result = Result.TupleListResult(error=False, info='Success', result=res) + except Exception as e: + result = Result.TupleListResult(error=True, info=repr(e), result=[]) + return result + + async def get_bot_statistic(self, *, start_time: Optional[datetime] = None) -> Result.TupleListResult: + """ + :param start_time: 统计起始时间, 为空则返回全部 + :return: List[Tuple[plugin_name, count]] + """ + async_session = BaseDB().get_async_session() + async with async_session() as session: + async with session.begin(): + try: + # 构造查询 + query = select(func.count(OmegaStatistics.module_name), OmegaStatistics.plugin_name) + if start_time: + query = query.where(OmegaStatistics.using_datetime >= start_time) + query = query.where( + OmegaStatistics.self_bot_id == self.self_bot_id).group_by(OmegaStatistics.plugin_name) + # 执行查询 + session_result = await session.execute(query) + res = [(plugin_name, count) for count, plugin_name in session_result.all()] + result = Result.TupleListResult(error=False, info='Success', result=res) + except Exception as e: + result = Result.TupleListResult(error=True, info=repr(e), result=[]) + return result + + async def get_user_statistic( + self, user_id: int, *, start_time: Optional[datetime] = None) -> Result.TupleListResult: + """ + :param user_id: 用户qq号 + :param start_time: 统计起始时间, 为空则返回全部 + :return: List[Tuple[plugin_name, count]] + """ + async_session = BaseDB().get_async_session() + async with async_session() as session: + async with session.begin(): + try: + # 构造查询 + query = select(func.count(OmegaStatistics.module_name), OmegaStatistics.plugin_name) + if start_time: + query = query.where(OmegaStatistics.using_datetime >= start_time) + query = query.where( + OmegaStatistics.self_bot_id == self.self_bot_id).where(OmegaStatistics.user_id == user_id) + query = query.group_by(OmegaStatistics.plugin_name) + # 执行查询 + session_result = await session.execute(query) + res = [(plugin_name, count) for count, plugin_name in session_result.all()] + result = Result.TupleListResult(error=False, info='Success', result=res) + except Exception as e: + result = Result.TupleListResult(error=True, info=repr(e), result=[]) + return result + + async def get_group_statistic( + self, group_id: int, *, start_time: Optional[datetime] = None) -> Result.TupleListResult: + """ + :param group_id: 群号 + :param start_time: 统计起始时间, 为空则返回全部 + :return: List[Tuple[plugin_name, count]] + """ + async_session = BaseDB().get_async_session() + async with async_session() as session: + async with session.begin(): + try: + # 构造查询 + query = select(func.count(OmegaStatistics.module_name), OmegaStatistics.plugin_name) + if start_time: + query = query.where(OmegaStatistics.using_datetime >= start_time) + query = query.where( + OmegaStatistics.self_bot_id == self.self_bot_id).where(OmegaStatistics.group_id == group_id) + query = query.group_by(OmegaStatistics.plugin_name) + # 执行查询 + session_result = await session.execute(query) + res = [(plugin_name, count) for count, plugin_name in session_result.all()] + result = Result.TupleListResult(error=False, info='Success', result=res) + except Exception as e: + result = Result.TupleListResult(error=True, info=repr(e), result=[]) + return result diff --git a/omega_miya/utils/Omega_Base/model/status.py b/omega_miya/database/model/status.py similarity index 90% rename from omega_miya/utils/Omega_Base/model/status.py rename to omega_miya/database/model/status.py index 26567948..f6c74fa3 100644 --- a/omega_miya/utils/Omega_Base/model/status.py +++ b/omega_miya/database/model/status.py @@ -1,6 +1,6 @@ -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import OmegaStatus +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import OmegaStatus from datetime import datetime from sqlalchemy.future import select from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound @@ -11,7 +11,7 @@ def __init__(self, name: str): self.name = name async def get_status(self) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -29,7 +29,7 @@ async def get_status(self) -> Result.IntResult: return result async def set_status(self, status: int, info: str = None) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): diff --git a/omega_miya/utils/Omega_Base/model/subscription.py b/omega_miya/database/model/subscription.py similarity index 80% rename from omega_miya/utils/Omega_Base/model/subscription.py rename to omega_miya/database/model/subscription.py index 1ec38d89..d7889358 100644 --- a/omega_miya/utils/Omega_Base/model/subscription.py +++ b/omega_miya/database/model/subscription.py @@ -1,6 +1,6 @@ -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import Subscription, Group, BotGroup, GroupSub, User, Friends, UserSub +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import Subscription, Group, BotGroup, GroupSub, User, Friends, UserSub from .bot_self import DBBot from datetime import datetime from sqlalchemy.future import select @@ -8,12 +8,12 @@ class DBSubscription(object): - def __init__(self, sub_type: int, sub_id: int): + def __init__(self, sub_type: int, sub_id: str): self.sub_type = sub_type self.sub_id = sub_id async def id(self) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -37,7 +37,7 @@ async def exist(self) -> bool: return result.success() async def add(self, up_name: str, live_info: str = None) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -74,7 +74,7 @@ async def delete(self) -> Result.IntResult: # 清空持已订阅这个sub的群组 await self.sub_group_clear() - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -99,7 +99,7 @@ async def delete(self) -> Result.IntResult: return result async def get_name(self) -> Result.TextResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -118,16 +118,16 @@ async def get_name(self) -> Result.TextResult: result = Result.TextResult(error=True, info=repr(e), result='') return result - async def sub_group_list(self, self_bot: DBBot) -> Result.ListResult: + async def sub_group_list(self, self_bot: DBBot) -> Result.IntListResult: id_result = await self.id() if id_result.error: - return Result.ListResult(error=True, info='Subscription not exist', result=[]) + return Result.IntListResult(error=True, info='Subscription not exist', result=[]) self_bot_id_result = await self_bot.id() if self_bot_id_result.error: - return Result.ListResult(error=True, info='Bot not exist', result=[]) + return Result.IntListResult(error=True, info='Bot not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -140,21 +140,22 @@ async def sub_group_list(self, self_bot: DBBot) -> Result.ListResult: where(GroupSub.sub_id == id_result.result) ) res = [x for x in session_result.scalars().all()] - result = Result.ListResult(error=False, info='Success', result=res) + result = Result.IntListResult(error=False, info='Success', result=res) except Exception as e: - result = Result.ListResult(error=True, info=repr(e), result=[]) + result = Result.IntListResult(error=True, info=repr(e), result=[]) return result - async def sub_group_list_by_notice_permission(self, self_bot: DBBot, notice_permission: int) -> Result.ListResult: + async def sub_group_list_by_notice_permission( + self, self_bot: DBBot, notice_permission: int) -> Result.IntListResult: id_result = await self.id() if id_result.error: - return Result.ListResult(error=True, info='Subscription not exist', result=[]) + return Result.IntListResult(error=True, info='Subscription not exist', result=[]) self_bot_id_result = await self_bot.id() if self_bot_id_result.error: - return Result.ListResult(error=True, info='Bot not exist', result=[]) + return Result.IntListResult(error=True, info='Bot not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -168,9 +169,9 @@ async def sub_group_list_by_notice_permission(self, self_bot: DBBot, notice_perm where(GroupSub.sub_id == id_result.result) ) res = [x for x in session_result.scalars().all()] - result = Result.ListResult(error=False, info='Success', result=res) + result = Result.IntListResult(error=False, info='Success', result=res) except Exception as e: - result = Result.ListResult(error=True, info=repr(e), result=[]) + result = Result.IntListResult(error=True, info=repr(e), result=[]) return result async def sub_group_clear(self) -> Result.IntResult: @@ -178,7 +179,7 @@ async def sub_group_clear(self) -> Result.IntResult: if id_result.error: return Result.IntResult(error=True, info='Subscription not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -194,16 +195,16 @@ async def sub_group_clear(self) -> Result.IntResult: result = Result.IntResult(error=True, info=repr(e), result=-1) return result - async def sub_user_list(self, self_bot: DBBot) -> Result.ListResult: + async def sub_user_list(self, self_bot: DBBot) -> Result.IntListResult: id_result = await self.id() if id_result.error: - return Result.ListResult(error=True, info='Subscription not exist', result=[]) + return Result.IntListResult(error=True, info='Subscription not exist', result=[]) self_bot_id_result = await self_bot.id() if self_bot_id_result.error: - return Result.ListResult(error=True, info='Bot not exist', result=[]) + return Result.IntListResult(error=True, info='Bot not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -216,21 +217,22 @@ async def sub_user_list(self, self_bot: DBBot) -> Result.ListResult: where(UserSub.sub_id == id_result.result) ) res = [x for x in session_result.scalars().all()] - result = Result.ListResult(error=False, info='Success', result=res) + result = Result.IntListResult(error=False, info='Success', result=res) except Exception as e: - result = Result.ListResult(error=True, info=repr(e), result=[]) + result = Result.IntListResult(error=True, info=repr(e), result=[]) return result - async def sub_user_list_by_private_permission(self, self_bot: DBBot, private_permission: int) -> Result.ListResult: + async def sub_user_list_by_private_permission( + self, self_bot: DBBot, private_permission: int) -> Result.IntListResult: id_result = await self.id() if id_result.error: - return Result.ListResult(error=True, info='Subscription not exist', result=[]) + return Result.IntListResult(error=True, info='Subscription not exist', result=[]) self_bot_id_result = await self_bot.id() if self_bot_id_result.error: - return Result.ListResult(error=True, info='Bot not exist', result=[]) + return Result.IntListResult(error=True, info='Bot not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -244,9 +246,9 @@ async def sub_user_list_by_private_permission(self, self_bot: DBBot, private_per where(UserSub.sub_id == id_result.result) ) res = [x for x in session_result.scalars().all()] - result = Result.ListResult(error=False, info='Success', result=res) + result = Result.IntListResult(error=False, info='Success', result=res) except Exception as e: - result = Result.ListResult(error=True, info=repr(e), result=[]) + result = Result.IntListResult(error=True, info=repr(e), result=[]) return result async def sub_user_clear(self) -> Result.IntResult: @@ -254,7 +256,7 @@ async def sub_user_clear(self) -> Result.IntResult: if id_result.error: return Result.IntResult(error=True, info='Subscription not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -271,8 +273,8 @@ async def sub_user_clear(self) -> Result.IntResult: return result @classmethod - async def list_all_sub(cls) -> Result.IntListResult: - async_session = NBdb().get_async_session() + async def list_all_sub(cls) -> Result.TextListResult: + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -280,14 +282,14 @@ async def list_all_sub(cls) -> Result.IntListResult: select(Subscription.sub_id).order_by(Subscription.sub_id) ) res = [x for x in session_result.scalars().all()] - result = Result.IntListResult(error=False, info='Success', result=res) + result = Result.TextListResult(error=False, info='Success', result=res) except Exception as e: - result = Result.IntListResult(error=True, info=repr(e), result=[]) + result = Result.TextListResult(error=True, info=repr(e), result=[]) return result @classmethod - async def list_sub_by_type(cls, sub_type: int) -> Result.IntListResult: - async_session = NBdb().get_async_session() + async def list_sub_by_type(cls, sub_type: int) -> Result.TextListResult: + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -297,7 +299,7 @@ async def list_sub_by_type(cls, sub_type: int) -> Result.IntListResult: order_by(Subscription.sub_id) ) res = [x for x in session_result.scalars().all()] - result = Result.IntListResult(error=False, info='Success', result=res) + result = Result.TextListResult(error=False, info='Success', result=res) except Exception as e: - result = Result.IntListResult(error=True, info=repr(e), result=[]) + result = Result.TextListResult(error=True, info=repr(e), result=[]) return result diff --git a/omega_miya/utils/Omega_Base/model/user.py b/omega_miya/database/model/user.py similarity index 91% rename from omega_miya/utils/Omega_Base/model/user.py rename to omega_miya/database/model/user.py index 8c5d9b72..dcb9f787 100644 --- a/omega_miya/utils/Omega_Base/model/user.py +++ b/omega_miya/database/model/user.py @@ -1,9 +1,9 @@ from typing import List, Optional from datetime import date, datetime from dataclasses import dataclass -from omega_miya.utils.Omega_Base.database import NBdb -from omega_miya.utils.Omega_Base.class_result import Result -from omega_miya.utils.Omega_Base.tables import User, UserFavorability, UserSignIn, Skill, UserSkill, Vacation +from omega_miya.database.database import BaseDB +from omega_miya.database.class_result import Result +from omega_miya.database.tables import User, UserFavorability, UserSignIn, Skill, UserSkill, Vacation from .skill import DBSkill from sqlalchemy.future import select from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound @@ -21,7 +21,7 @@ def __repr__(self): return f'' async def id(self) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -43,7 +43,7 @@ async def exist(self) -> bool: return result.success() async def nickname(self) -> Result.TextResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -61,7 +61,7 @@ async def nickname(self) -> Result.TextResult: return result async def add(self, nickname: str, aliasname: str = None) -> Result.IntResult: - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -97,7 +97,7 @@ async def delete(self) -> Result.IntResult: if id_result.error: return Result.IntResult(error=True, info='User not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -125,7 +125,7 @@ async def skill_list(self) -> Result.ListResult: if id_result.error: return Result.ListResult(error=True, info='User not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -150,7 +150,7 @@ async def skill_add(self, skill: DBSkill, skill_level: int) -> Result.IntResult: if skill_id_result.error: return Result.IntResult(error=True, info='Skill not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -189,7 +189,7 @@ async def skill_del(self, skill: DBSkill) -> Result.IntResult: if skill_id_result.error: return Result.IntResult(error=True, info='Skill not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -218,7 +218,7 @@ async def skill_clear(self) -> Result.IntResult: if user_id_result.error: return Result.IntResult(error=True, info='User not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -239,7 +239,7 @@ async def status(self) -> Result.IntResult: if user_id_result.error: return Result.IntResult(error=True, info='User not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -257,7 +257,7 @@ async def vacation_status(self) -> Result.ListResult: if user_id_result.error: return Result.ListResult(error=True, info='User not exist', result=[-1, None]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -276,7 +276,7 @@ async def status_set(self, status: int) -> Result.IntResult: if user_id_result.error: return Result.IntResult(error=True, info='User not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -308,7 +308,7 @@ async def vacation_set(self, stop_time: datetime, reason: str = None) -> Result. if user_id_result.error: return Result.IntResult(error=True, info='User not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -341,7 +341,7 @@ async def status_del(self) -> Result.IntResult: if user_id_result.error: return Result.IntResult(error=True, info='User not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -373,7 +373,7 @@ async def sign_in(self, *, sign_in_info: Optional[str] = 'Normal sign in') -> Re datetime_now = datetime.now() date_now = datetime_now.date() - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -411,7 +411,7 @@ async def sign_in_statistics(self) -> DateListResult: if user_id_result.error: return self.DateListResult(error=True, info='User not exist', result=[]) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -458,6 +458,36 @@ async def sign_in_continuous_days(self) -> Result.IntResult: # 如果全部遍历完了那就说明全部没有断签 return Result.IntResult(error=False, info='Success with all continuous', result=len(all_sign_in_list)) + async def sign_in_check_today(self) -> Result.IntResult: + """ + 检查今天是否已经签到 + :return: IntResult + 1: 已签到 + 0: 未签到 + -1: 错误 + """ + user_id_result = await self.id() + if user_id_result.error: + return Result.IntResult(error=True, info='User not exist', result=-1) + + today = date.today() + async_session = BaseDB().get_async_session() + async with async_session() as session: + async with session.begin(): + try: + session_result = await session.execute( + select(UserSignIn.sign_in_date). + where(UserSignIn.sign_in_date == today). + where(UserSignIn.user_id == user_id_result.result) + ) + res = session_result.scalar_one() + result = Result.IntResult(error=False, info=f'Checked today: {res}', result=1) + except NoResultFound: + result = Result.IntResult(error=False, info='Not Sign in today', result=0) + except Exception as e: + result = Result.IntResult(error=True, info=repr(e), result=-1) + return result + async def favorability_status(self) -> Result.TupleResult: """ 查询好感度记录 @@ -468,7 +498,7 @@ async def favorability_status(self) -> Result.TupleResult: if user_id_result.error: return Result.TupleResult(error=True, info='User not exist', result=()) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: async with session.begin(): try: @@ -505,7 +535,7 @@ async def favorability_reset( if user_id_result.error: return Result.IntResult(error=True, info='User not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): @@ -554,7 +584,7 @@ async def favorability_add( if user_id_result.error: return Result.IntResult(error=True, info='User not exist', result=-1) - async_session = NBdb().get_async_session() + async_session = BaseDB().get_async_session() async with async_session() as session: try: async with session.begin(): diff --git a/omega_miya/utils/Omega_Base/tables.py b/omega_miya/database/tables.py similarity index 92% rename from omega_miya/utils/Omega_Base/tables.py rename to omega_miya/database/tables.py index 87dcf34a..48c60412 100644 --- a/omega_miya/utils/Omega_Base/tables.py +++ b/omega_miya/database/tables.py @@ -45,6 +45,89 @@ def __repr__(self): f"created_at='{self.created_at}', updated_at='{self.updated_at}')>" +# 插件表, 存放插件信息 +class OmegaPlugins(Base): + __tablename__ = f'{TABLE_PREFIX}plugins' + __table_args__ = {'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8mb4'} + + # 表结构 + id = Column(Integer, Sequence('omega_plugins_id_seq'), primary_key=True, nullable=False, index=True, unique=True) + plugin_name = Column(String(64), nullable=False, index=True, unique=True, comment='插件名称') + enabled = Column(Integer, nullable=False, index=True, comment='启用状态, 1: 启用, 0: 禁用, -1: 失效或未安装') + matchers = Column(Integer, nullable=True, comment='插件的matcher数') + info = Column(String(256), nullable=True, comment='附加说明') + created_at = Column(DateTime, nullable=True) + updated_at = Column(DateTime, nullable=True) + + def __init__(self, + plugin_name: str, + enabled: int, + *, + matchers: int = 0, + info: Optional[str] = None, + created_at: Optional[datetime] = None, + updated_at: Optional[datetime] = None): + self.plugin_name = plugin_name + self.enabled = enabled + self.matchers = matchers + self.info = info + self.created_at = created_at + self.updated_at = updated_at + + def __repr__(self): + return f"" + + +# 统计信息表, 存放插件运行统计 +class OmegaStatistics(Base): + __tablename__ = f'{TABLE_PREFIX}statistics' + __table_args__ = {'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8mb4'} + + # 表结构 + id = Column(Integer, Sequence('omega_statistics_id_seq'), primary_key=True, nullable=False, index=True, unique=True) + module_name = Column(String(64), nullable=False, index=True, comment='插件模块名称') + plugin_name = Column(String(64), nullable=False, index=True, comment='插件显示名称') + self_bot_id = Column(BigInteger, nullable=False, index=True, comment='调用Bot的QQ号') + group_id = Column(BigInteger, nullable=False, index=True, comment='调用群组群号') + user_id = Column(BigInteger, nullable=False, index=True, comment='调用用户群号') + using_datetime = Column(DateTime, nullable=False, index=True, comment='调用插件时间') + raw_message = Column(String(4096), nullable=True, comment='调用原始消息') + info = Column(String(128), nullable=True, comment='调用结果说明') + created_at = Column(DateTime, nullable=True) + updated_at = Column(DateTime, nullable=True) + + def __init__(self, + module_name: str, + plugin_name: str, + self_bot_id: int, + group_id: int, + user_id: int, + using_datetime: datetime, + *, + raw_message: Optional[str] = None, + info: Optional[str] = None, + created_at: Optional[datetime] = None, + updated_at: Optional[datetime] = None): + self.module_name = module_name + self.plugin_name = plugin_name + self.self_bot_id = self_bot_id + self.group_id = group_id + self.user_id = user_id + self.using_datetime = using_datetime + self.raw_message = raw_message + self.info = info + self.created_at = created_at + self.updated_at = updated_at + + def __repr__(self): + return f"" + + # Bot表 对应不同机器人协议端 class BotSelf(Base): __tablename__ = f'{TABLE_PREFIX}bots' @@ -700,7 +783,7 @@ class History(Base): __table_args__ = {'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8mb4'} # 表结构 - id = Column(Integer, Sequence('history_id_seq'), primary_key=True, nullable=False, index=True, unique=True) + id = Column(BigInteger, Sequence('history_id_seq'), primary_key=True, nullable=False, index=True, unique=True) time = Column(BigInteger, nullable=False, comment='事件发生的时间戳') self_id = Column(BigInteger, nullable=False, index=True, comment='收到事件的机器人QQ号') post_type = Column(String(64), nullable=False, index=True, comment='事件类型') @@ -766,7 +849,7 @@ class Subscription(Base): id = Column(Integer, Sequence('subscription_id_seq'), primary_key=True, nullable=False, index=True, unique=True) sub_type = Column(Integer, nullable=False, comment='订阅类型') - sub_id = Column(Integer, nullable=False, index=True, comment='订阅id,直播为直播间房间号,动态为用户uid') + sub_id = Column(String(64), nullable=False, index=True, comment='订阅id,直播为直播间房间号,动态为用户uid') up_name = Column(String(64), nullable=False, comment='up名称') live_info = Column(String(64), nullable=True, comment='相关信息,暂空备用') created_at = Column(DateTime, nullable=True) @@ -780,7 +863,7 @@ class Subscription(Base): def __init__(self, sub_type: int, - sub_id: int, + sub_id: str, up_name: str, *, live_info: Optional[str] = None, diff --git a/omega_miya/plugins/Omega_auto_manager/__init__.py b/omega_miya/plugins/Omega_auto_manager/__init__.py deleted file mode 100644 index 80a07114..00000000 --- a/omega_miya/plugins/Omega_auto_manager/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -@Author : Ailitonia -@Date : 2021/06/09 19:10 -@FileName : __init__.py.py -@Project : nonebot2_miya -@Description : Omega 自动化综合/群管/好友管理插件 -@GitHub : https://github.com/Ailitonia -@Software : PyCharm -""" - -from nonebot import export -from omega_miya.utils.Omega_plugin_utils import init_export -from .group_welcome_message import * -from .invite_request import * - - -# Custom plugin usage text -__plugin_name__ = 'OmegaAutoManager' -__plugin_usage__ = r'''【Omega 自动化综合/群管/好友管理插件】 -Omega机器人自动化综合/群管/好友管理 - -以下命令均需要@机器人 -**Usage** -**GroupAdmin and SuperUser Only** -/设置欢迎消息 -/清空欢迎消息 -''' - -# Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__) - - -__all__ = [ - 'WelcomeMsg', - 'group_increase', - 'add_and_invite_request' -] diff --git a/omega_miya/plugins/Omega_auto_manager/invite_request.py b/omega_miya/plugins/Omega_auto_manager/invite_request.py deleted file mode 100644 index a10a655c..00000000 --- a/omega_miya/plugins/Omega_auto_manager/invite_request.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -@Author : Ailitonia -@Date : 2021/06/12 0:28 -@FileName : invite_request.py -@Project : nonebot2_miya -@Description : -@GitHub : https://github.com/Ailitonia -@Software : PyCharm -""" - -from nonebot import on_request, on_notice, logger -from nonebot.typing import T_State -from nonebot.adapters.cqhttp.bot import Bot -from nonebot.adapters.cqhttp.message import MessageSegment, Message -from nonebot.adapters.cqhttp.event import FriendRequestEvent, GroupRequestEvent, GroupIncreaseNoticeEvent -from omega_miya.utils.Omega_Base import DBBot, DBBotGroup - -# 注册事件响应器 -add_and_invite_request = on_request(priority=100) - - -# 处理加好友申请 -@add_and_invite_request.handle() -async def handle_friend_request(bot: Bot, event: FriendRequestEvent, state: T_State): - user_id = event.user_id - detail_type = event.request_type - comment = event.comment - - # 加好友验证消息 - auth_str = f'Miya好萌好可爱{int(user_id) % 9}' - - if detail_type == 'friend' and comment == auth_str: - await bot.call_api('set_friend_add_request', flag=event.flag, approve=True) - logger.info(f'已同意用户: {user_id} 的好友申请') - elif detail_type == 'friend': - await bot.call_api('set_friend_add_request', flag=event.flag, approve=False) - logger.info(f'已拒绝用户: {user_id} 的好友申请') - - -# 处理被邀请进群 -@add_and_invite_request.handle() -async def handle_group_invite(bot: Bot, event: GroupRequestEvent, state: T_State): - user_id = event.user_id - group_id = event.group_id - sub_type = event.sub_type - detail_type = event.request_type - if detail_type == 'group' and sub_type == 'invite': - await bot.call_api('set_group_add_request', flag=event.flag, sub_type='invite', approve=True) - logger.info(f'已处理群组请求, 被用户: {user_id} 邀请加入群组: {group_id}.') - - -__all__ = [ - 'add_and_invite_request' -] diff --git a/omega_miya/plugins/Omega_sign_in/__init__.py b/omega_miya/plugins/Omega_sign_in/__init__.py deleted file mode 100644 index fb0b6044..00000000 --- a/omega_miya/plugins/Omega_sign_in/__init__.py +++ /dev/null @@ -1,134 +0,0 @@ -""" -@Author : Ailitonia -@Date : 2021/07/17 1:29 -@FileName : __init__.py.py -@Project : nonebot2_miya -@Description : 轻量化签到插件 -@GitHub : https://github.com/Ailitonia -@Software : PyCharm -""" - -from nonebot import CommandGroup, logger, export, get_driver -from nonebot.typing import T_State -from nonebot.adapters.cqhttp.bot import Bot -from nonebot.adapters.cqhttp.event import GroupMessageEvent -from nonebot.adapters.cqhttp.permission import GROUP -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state -from omega_miya.utils.Omega_Base import DBUser -from .config import Config - - -__global_config = get_driver().config -plugin_config = Config(**__global_config.dict()) -FAVORABILITY_ALIAS = plugin_config.favorability_alias -ENERGY_ALIAS = plugin_config.energy_alias -CURRENCY_ALIAS = plugin_config.currency_alias - - -class SignInException(Exception): - pass - - -class DuplicateException(SignInException): - pass - - -class FailedException(SignInException): - pass - - -# Custom plugin usage text -__plugin_name__ = '签到' -__plugin_usage__ = r'''【Omega 签到插件】 -轻量化签到插件 -好感度系统基础支持 -仅限群聊使用 - -**Permission** -Command & Lv.10 -or AuthNode - -**AuthNode** -basic - -**Usage** -/签到''' - -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] - -# Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) - - -SignIn = CommandGroup( - 'SignIn', - # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( - name='sign_in', - command=True, - level=10, - auth_node='basic'), - permission=GROUP, - priority=10, - block=True) - -sign_in = SignIn.command('sign_in', aliases={'签到'}) - - -@sign_in.handle() -async def handle_first_receive(bot: Bot, event: GroupMessageEvent, state: T_State): - user = DBUser(user_id=event.user_id) - - try: - # 尝试签到 - sign_in_result = await user.sign_in() - if sign_in_result.error: - raise FailedException(f'签到失败, {sign_in_result.info}') - elif sign_in_result.result == 1: - raise DuplicateException('重复签到') - - # 查询连续签到时间 - sign_in_c_d_result = await user.sign_in_continuous_days() - if sign_in_c_d_result.error: - raise FailedException(f'查询连续签到时间失败, {sign_in_c_d_result.info}') - continuous_days = sign_in_c_d_result.result - - # 尝试为用户增加好感度 - if continuous_days < 7: - favorability_inc = 10 - currency_inc = 1 - elif continuous_days < 30: - favorability_inc = 30 - currency_inc = 2 - else: - favorability_inc = 50 - currency_inc = 5 - - favorability_result = await user.favorability_add(favorability=favorability_inc, currency=currency_inc) - if favorability_result.error and favorability_result.info == 'NoResultFound': - favorability_result = await user.favorability_reset(favorability=favorability_inc, currency=currency_inc) - if favorability_result.error: - raise FailedException(f'增加好感度失败, {favorability_result.info}') - - # 获取当前好感度信息 - favorability_status_result = await user.favorability_status() - if favorability_status_result.error: - raise FailedException(f'获取好感度信息失败, {favorability_status_result}') - - status, mood, favorability, energy, currency, response_threshold = favorability_status_result.result - - msg = f'签到成功! {FAVORABILITY_ALIAS}+{favorability_inc}, {CURRENCY_ALIAS}+{currency_inc}!\n\n' \ - f'你已连续签到{continuous_days}天\n' \ - f'当前{FAVORABILITY_ALIAS}: {favorability}\n' \ - f'当前{CURRENCY_ALIAS}: {currency}' - logger.info(f'{event.group_id}/{event.user_id} 签到成功') - await sign_in.finish(msg) - except DuplicateException as e: - logger.info(f'{event.group_id}/{event.user_id} 重复签到, {str(e)}') - await sign_in.finish('你今天已经签到过了, 请明天再来吧~') - except FailedException as e: - logger.error(f'{event.group_id}/{event.user_id} 签到失败, {str(e)}') - await sign_in.finish('签到失败了QAQ, 请稍后再试或联系管理员处理') diff --git a/omega_miya/plugins/Omega_sign_in/config.py b/omega_miya/plugins/Omega_sign_in/config.py deleted file mode 100644 index e218d0cd..00000000 --- a/omega_miya/plugins/Omega_sign_in/config.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -@Author : Ailitonia -@Date : 2021/07/17 2:04 -@FileName : config.py -@Project : nonebot2_miya -@Description : -@GitHub : https://github.com/Ailitonia -@Software : PyCharm -""" - -from pydantic import BaseSettings - - -class Config(BaseSettings): - - # plugin custom config - favorability_alias: str = '好感度' - energy_alias: str = '能量值' - currency_alias: str = '金币' - - class Config: - extra = "ignore" diff --git a/omega_miya/plugins/announce/__init__.py b/omega_miya/plugins/announce/__init__.py index 81305ab9..6dbf5b35 100644 --- a/omega_miya/plugins/announce/__init__.py +++ b/omega_miya/plugins/announce/__init__.py @@ -1,11 +1,29 @@ import re from nonebot import on_command, logger +from nonebot.plugin.export import export from nonebot.rule import to_me from nonebot.permission import SUPERUSER from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import PrivateMessageEvent -from omega_miya.utils.Omega_Base import DBBot, DBBotGroup +from omega_miya.database import DBBot, DBBotGroup +from omega_miya.utils.omega_plugin_utils import init_export + + +# Custom plugin usage text +__plugin_custom_name__ = '公告' +__plugin_usage__ = r'''【公告插件】 +让管理员快速批量发送通知公告 + +**Permission** +SuperUser Only + +**Usage** +/公告''' + + +# Init plugin export +init_export(export(), __plugin_custom_name__, __plugin_usage__) # 注册事件响应器 diff --git a/omega_miya/plugins/bilibili_dynamic_monitor/__init__.py b/omega_miya/plugins/bilibili_dynamic_monitor/__init__.py index f111822e..940bc30c 100644 --- a/omega_miya/plugins/bilibili_dynamic_monitor/__init__.py +++ b/omega_miya/plugins/bilibili_dynamic_monitor/__init__.py @@ -1,18 +1,19 @@ import re -from nonebot import on_command, export, logger +from nonebot import on_command, logger +from nonebot.plugin.export import export from nonebot.permission import SUPERUSER from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent from nonebot.adapters.cqhttp.permission import GROUP_ADMIN, GROUP_OWNER, PRIVATE_FRIEND -from omega_miya.utils.Omega_Base import DBBot, DBBotGroup, DBFriend, DBSubscription, Result -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state +from omega_miya.database import DBBot, DBBotGroup, DBFriend, DBSubscription, Result +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state from omega_miya.utils.bilibili_utils import BiliUser from .monitor import init_user_dynamic, scheduler # Custom plugin usage text -__plugin_name__ = 'B站动态订阅' +__plugin_custom_name__ = 'B站动态订阅' __plugin_usage__ = r'''【B站动态订阅】 随时更新up动态 群组/私聊可用 @@ -32,13 +33,9 @@ /B站动态 清空订阅 /B站动态 订阅列表''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) # 注册事件响应器 @@ -46,11 +43,10 @@ 'B站动态', aliases={'b站动态'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='bilibili_dynamic', command=True, - level=20, - auth_node='basic'), + level=20), permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER | PRIVATE_FRIEND, priority=20, block=True) diff --git a/omega_miya/plugins/bilibili_dynamic_monitor/monitor.py b/omega_miya/plugins/bilibili_dynamic_monitor/monitor.py index a9a5ab52..0a86cf1b 100644 --- a/omega_miya/plugins/bilibili_dynamic_monitor/monitor.py +++ b/omega_miya/plugins/bilibili_dynamic_monitor/monitor.py @@ -4,9 +4,9 @@ from nonebot import logger, require, get_bots, get_driver from nonebot.adapters.cqhttp import MessageSegment from nonebot.adapters.cqhttp.bot import Bot -from omega_miya.utils.Omega_Base import DBSubscription, DBDynamic +from omega_miya.database import DBSubscription, DBDynamic from omega_miya.utils.bilibili_utils import BiliUser, BiliDynamic, BiliRequestUtils -from omega_miya.utils.Omega_plugin_utils import MsgSender +from omega_miya.utils.omega_plugin_utils import MsgSender from .config import Config @@ -45,7 +45,7 @@ async def dynamic_db_upgrade(): sub_res = await DBSubscription.list_sub_by_type(sub_type=2) for sub_id in sub_res.result: sub = DBSubscription(sub_type=2, sub_id=sub_id) - user_info_result = await BiliUser(user_id=sub_id).get_info() + user_info_result = await BiliUser(user_id=int(sub_id)).get_info() if user_info_result.error: logger.error(f'dynamic_db_upgrade: 更新用户信息失败, uid: {sub_id}, error: {user_info_result.info}') continue @@ -95,7 +95,7 @@ async def dynamic_checker(user_id: int, bots: List[Bot]): new_dynamic_data = [data for data in dynamics_data if data.result.dynamic_id not in user_dynamic_list] - subscription = DBSubscription(sub_type=2, sub_id=user_id) + subscription = DBSubscription(sub_type=2, sub_id=str(user_id)) for data in new_dynamic_data: dynamic_info = data.result diff --git a/omega_miya/plugins/bilibili_live_monitor/__init__.py b/omega_miya/plugins/bilibili_live_monitor/__init__.py index ac0ae1fe..b7f3a4e5 100644 --- a/omega_miya/plugins/bilibili_live_monitor/__init__.py +++ b/omega_miya/plugins/bilibili_live_monitor/__init__.py @@ -1,19 +1,20 @@ import re -from nonebot import on_command, export, logger +from nonebot import on_command, logger +from nonebot.plugin.export import export from nonebot.permission import SUPERUSER from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent from nonebot.adapters.cqhttp.permission import GROUP_ADMIN, GROUP_OWNER, PRIVATE_FRIEND -from omega_miya.utils.Omega_Base import DBBot, DBBotGroup, DBFriend, DBSubscription, Result -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state +from omega_miya.database import DBBot, DBBotGroup, DBFriend, DBSubscription, Result +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state from omega_miya.utils.bilibili_utils import BiliLiveRoom from .data_source import BiliLiveChecker from .monitor import scheduler # Custom plugin usage text -__plugin_name__ = 'B站直播间订阅' +__plugin_custom_name__ = 'B站直播间订阅' __plugin_usage__ = r'''【B站直播间订阅】 监控直播间状态 开播、下播、直播间换标题提醒 @@ -34,13 +35,9 @@ /B站直播间 清空订阅 /B站直播间 订阅列表''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) # 注册事件响应器 @@ -48,11 +45,10 @@ 'B站直播间', aliases={'b站直播间'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='bilibili_live', command=True, - level=20, - auth_node='basic'), + level=20), permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER | PRIVATE_FRIEND, priority=20, block=True) diff --git a/omega_miya/plugins/bilibili_live_monitor/data_source.py b/omega_miya/plugins/bilibili_live_monitor/data_source.py index ec69c2b8..909a9fd5 100644 --- a/omega_miya/plugins/bilibili_live_monitor/data_source.py +++ b/omega_miya/plugins/bilibili_live_monitor/data_source.py @@ -5,9 +5,9 @@ from nonebot import logger from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp import MessageSegment -from omega_miya.utils.Omega_Base import DBSubscription, DBHistory, Result +from omega_miya.database import DBSubscription, DBHistory, Result from omega_miya.utils.bilibili_utils import BiliLiveRoom, BiliUser, BiliRequestUtils, BiliInfo -from omega_miya.utils.Omega_plugin_utils import MsgSender +from omega_miya.utils.omega_plugin_utils import MsgSender # 初始化直播间标题, 状态 @@ -77,7 +77,7 @@ async def init_global_live_info(cls): tasks = [] sub_res = await DBSubscription.list_sub_by_type(sub_type=1) for sub_id in sub_res.result: - tasks.append(BiliLiveChecker(room_id=sub_id).init_live_info()) + tasks.append(BiliLiveChecker(room_id=int(sub_id)).init_live_info()) try: await asyncio.gather(*tasks) except Exception as e: @@ -296,7 +296,7 @@ async def broadcaster(self, live_info: BiliInfo.LiveRoomInfo, bots: List[Bot]): if global_check_result.error: return - subscription = DBSubscription(sub_type=1, sub_id=self.room_id) + subscription = DBSubscription(sub_type=1, sub_id=str(self.room_id)) # 标题变更检测 title_checker_result = await self.title_change_checker(live_info=live_info) diff --git a/omega_miya/plugins/bilibili_live_monitor/monitor.py b/omega_miya/plugins/bilibili_live_monitor/monitor.py index 3aa33632..881e37b6 100644 --- a/omega_miya/plugins/bilibili_live_monitor/monitor.py +++ b/omega_miya/plugins/bilibili_live_monitor/monitor.py @@ -1,7 +1,7 @@ import asyncio import random from nonebot import logger, require, get_driver, get_bots -from omega_miya.utils.Omega_Base import DBSubscription +from omega_miya.database import DBSubscription from omega_miya.utils.bilibili_utils import BiliLiveRoom from .data_source import BiliLiveChecker from .config import Config @@ -45,7 +45,7 @@ async def live_db_upgrade(): sub_res = await DBSubscription.list_sub_by_type(sub_type=1) for sub_id in sub_res.result: sub = DBSubscription(sub_type=1, sub_id=sub_id) - live_user_info_result = await BiliLiveRoom(room_id=sub_id).get_user_info() + live_user_info_result = await BiliLiveRoom(room_id=int(sub_id)).get_user_info() if live_user_info_result.error: logger.error(f'live_db_upgrade: 更新直播间信息失败, room_id: {sub_id}, error: {live_user_info_result.info}') continue diff --git a/omega_miya/plugins/calculator/__init__.py b/omega_miya/plugins/calculator/__init__.py index 10ff53b3..714ced1f 100644 --- a/omega_miya/plugins/calculator/__init__.py +++ b/omega_miya/plugins/calculator/__init__.py @@ -8,18 +8,19 @@ @Software : PyCharm """ -from nonebot import on_command, export, logger +from nonebot import on_command, logger +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent from nonebot.adapters.cqhttp.permission import GROUP, PRIVATE_FRIEND -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state from omega_miya.utils.dice_utils import BaseCalculator from omega_miya.utils.dice_utils.exception import CalculateException # Custom plugin usage text -__plugin_name__ = '计算器' +__plugin_custom_name__ = '计算器' __plugin_usage__ = r'''【简易计算器】 只能计算加减乘除和乘方! @@ -33,24 +34,19 @@ **Usage** /计算器 [算式]''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) # 注册事件响应器 calculator = on_command( 'Calculator', # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='calculator', command=True, - level=10, - auth_node='basic'), + level=10), aliases={'calculator', '计算器', '计算'}, permission=GROUP | PRIVATE_FRIEND, priority=20, diff --git a/omega_miya/plugins/draw/__init__.py b/omega_miya/plugins/draw/__init__.py index e28d4e13..d0529327 100644 --- a/omega_miya/plugins/draw/__init__.py +++ b/omega_miya/plugins/draw/__init__.py @@ -1,15 +1,16 @@ import re import random -from nonebot import CommandGroup, export, logger +from nonebot import CommandGroup, logger +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import GroupMessageEvent from nonebot.adapters.cqhttp.permission import GROUP -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state, PluginCoolDown +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state, PluginCoolDown from .data_source import deck_list, draw_deck # Custom plugin usage text -__plugin_name__ = '抽卡' +__plugin_custom_name__ = '抽卡' __plugin_usage__ = r'''【抽卡】 模拟各种抽卡 没有保底的啦! @@ -32,29 +33,19 @@ /抽卡 [卡组] /抽奖 [人数]''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - PluginCoolDown.skip_auth_node, - 'basic' -] - -# 声明本插件的冷却时间配置 -__plugin_cool_down__ = [ - PluginCoolDown(PluginCoolDown.user_type, 1) -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__, __plugin_cool_down__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) # 注册事件响应器 Draw = CommandGroup( 'Draw', # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='draw', command=True, level=10, - auth_node='basic'), + cool_down=[PluginCoolDown(PluginCoolDown.user_type, 60)]), permission=GROUP, priority=10, block=True) @@ -108,6 +99,7 @@ async def handle_deck(bot: Bot, event: GroupMessageEvent, state: T_State): # 向用户发送结果 msg = f"{draw_user}抽卡【{_draw}】!!\n{'='*12}\n{draw_result}" + logger.info(f'{event.group_id}/{event.user_id} 进行了一次抽卡: {_draw}') await deck.finish(msg) @@ -160,6 +152,8 @@ async def handle_lottery(bot: Bot, event: GroupMessageEvent, state: T_State): lottery_result = random.sample(group_user_name_list, k=people_num) msg = '【' + str.join('】\n【', lottery_result) + '】' + logger.info(f'{event.group_id}/{event.user_id} 进行了一次抽奖') await lottery.finish(f"抽奖人数: 【{people_num}】\n以下是中奖名单:\n{msg}") else: + logger.info(f'{event.group_id}/{event.user_id} 抽奖被取消') await lottery.finish(f'格式不对呢, 人数应该是数字') diff --git a/omega_miya/plugins/draw/data_source.py b/omega_miya/plugins/draw/data_source.py index 54569d19..ca019e01 100644 --- a/omega_miya/plugins/draw/data_source.py +++ b/omega_miya/plugins/draw/data_source.py @@ -6,7 +6,6 @@ # Deck事件 deck_list: Dict[str, T_DrawDeck] = { - '单张塔罗牌': one_tarot, '超能力': superpower, '程序员修行': course, '明日方舟单抽': draw_one_arknights, diff --git a/omega_miya/plugins/draw/deck/__init__.py b/omega_miya/plugins/draw/deck/__init__.py index 92887dbd..55cdf8ed 100644 --- a/omega_miya/plugins/draw/deck/__init__.py +++ b/omega_miya/plugins/draw/deck/__init__.py @@ -1,10 +1,8 @@ -from .tarot import one_tarot from .superpower import superpower from .course import course from .arknights import draw_one_arknights, draw_ten_arknights __all__ = [ - 'one_tarot', 'superpower', 'course', 'draw_one_arknights', diff --git a/omega_miya/plugins/draw/deck/arknights.py b/omega_miya/plugins/draw/deck/arknights.py index 34925690..4b04716e 100644 --- a/omega_miya/plugins/draw/deck/arknights.py +++ b/omega_miya/plugins/draw/deck/arknights.py @@ -29,8 +29,7 @@ class UpEvent: UpEvent( star=6, operator=[ - Operator(name='早露/Роса', star=6, limited=False, recruit_only=False, event_only=False, special_only=False), - Operator(name='安洁莉娜/Angelina', star=6, limited=False, recruit_only=False, event_only=False, + Operator(name='琴柳/Saileach', star=6, limited=False, recruit_only=False, event_only=False, special_only=False) ], zoom=0.5 @@ -38,17 +37,32 @@ class UpEvent: UpEvent( star=5, operator=[ - Operator(name='普罗旺斯/Provence', star=5, limited=False, recruit_only=False, event_only=False, + Operator(name='桑葚/Mulberry', star=5, limited=False, recruit_only=False, event_only=False, special_only=False), - Operator(name='梅尔/Mayer', star=5, limited=False, recruit_only=False, event_only=False, special_only=False), - Operator(name='乌有/Mr.Nothing', star=5, limited=False, recruit_only=False, event_only=False, + Operator(name='雷蛇/Liskarm', star=5, limited=False, recruit_only=False, event_only=False, special_only=False) ], zoom=0.5 + ), + UpEvent( + star=4, + operator=[ + Operator(name='罗比菈塔/Roberta', star=4, limited=False, recruit_only=False, event_only=False, + special_only=False) + ], + zoom=0.2 ) ] ALL_OPERATOR: List[Operator] = [ + Operator(name='琴柳/Saileach', star=6, limited=False, recruit_only=False, event_only=False, special_only=False), + Operator(name='桑葚/Mulberry', star=5, limited=False, recruit_only=False, event_only=False, special_only=False), + Operator(name='罗比菈塔/Roberta', star=4, limited=False, recruit_only=False, event_only=False, special_only=False), + Operator(name="假日威龙陈/Ch'en the Holungday", star=6, limited=True, recruit_only=False, event_only=False, + special_only=False), + Operator(name='水月/Mizuki', star=6, limited=False, recruit_only=False, event_only=False, special_only=False), + Operator(name='羽毛笔/La Pluma', star=5, limited=False, recruit_only=False, event_only=False, special_only=False), + Operator(name='龙舌兰/Tequila', star=5, limited=False, recruit_only=False, event_only=True, special_only=False), Operator(name='帕拉斯/Pallas', star=6, limited=False, recruit_only=False, event_only=False, special_only=False), Operator(name='卡涅利安/Carnelian', star=6, limited=False, recruit_only=False, event_only=False, special_only=False), Operator(name='绮良/Kirara', star=5, limited=False, recruit_only=False, event_only=False, special_only=False), diff --git a/omega_miya/plugins/draw/deck/course.py b/omega_miya/plugins/draw/deck/course.py index 27205054..f74e8ab9 100644 --- a/omega_miya/plugins/draw/deck/course.py +++ b/omega_miya/plugins/draw/deck/course.py @@ -33,7 +33,7 @@ '编译原理', '操作系统', '计算机网络', - '数据库原理 ', + '数据库原理', '软件工程', '软件系统设计', 'Python', diff --git a/omega_miya/plugins/draw/deck/superpower.py b/omega_miya/plugins/draw/deck/superpower.py index 390ebc4a..d499f5c4 100644 --- a/omega_miya/plugins/draw/deck/superpower.py +++ b/omega_miya/plugins/draw/deck/superpower.py @@ -118,7 +118,7 @@ '会给自己套上幸运-90%的DEBUFF', '必须穿上女装才能用', '你还没学过《三年施法五年模拟》所以你并不会用', - '使用时会在身边用全操场都能听到的环绕音播放《ねこみみスイッチ》' + '使用时会在身边用全操场都能听到的环绕音播放《ねこみみスイッチ》', '你单身时间决定了你技能的效果', '得先给自己来一发才能对其他目标施放', '只有在学校里面才能用', diff --git a/omega_miya/plugins/draw/deck/tarot.py b/omega_miya/plugins/draw/deck/tarot.py deleted file mode 100644 index 6c02c452..00000000 --- a/omega_miya/plugins/draw/deck/tarot.py +++ /dev/null @@ -1,91 +0,0 @@ -import datetime -import random -import hashlib - - -major_arcana = [ - {'name': '【0】愚者(The Fool,0)', - 'positive': '盲目的、有勇气的、超越世俗的、展开新的阶段、有新的机会、追求自我的理想、展开一段旅行、超乎常人的勇气、漠视道德舆论的', - 'negative': '过于盲目、不顾现实的、横冲直撞的、拒绝负担责任的、违背常理的、逃避的心态、一段危险的旅程、想法如孩童般天真幼稚的'}, - {'name': '【1】魔术师(The Magician,I)', - 'positive': '成功的、有实力的、聪明能干的、擅长沟通的、机智过人的、唯我独尊的、企划能力强的、透过更好的沟通而获得智慧、运用智慧影响他人、学习能力强的、有教育和学术上的能力、表达技巧良好的', - 'negative': '变魔术耍花招的、瞒骗的、失败的、狡猾的、善于谎言的、能力不足的、丧失信心的、以不正当手段获取认同的'}, - {'name': '【2】女祭司(The High Priestess,II)', - 'positive': '纯真无邪的、拥有直觉判断能力的、揭发真相的、运用潜意识的力量、掌握知识的、正确的判断、理性的思考、单恋的、精神上的恋爱、对爱情严苛的、回避爱情的、对学业有助益的', - 'negative': '冷酷无情的、无法正确思考的、错误的方向、迷信的、无理取闹的、情绪不安的、缺乏前瞻性的、严厉拒绝爱情的'}, - {'name': '【3】女皇(The Empress,III)', - 'positive': '温柔顺从的、高贵美丽的、享受生活的、丰收的、生产的、温柔多情的、维护爱情的、充满女性魅力的、具有母爱的、有创造力的女性、沈浸爱情的、财运充裕的、快乐愉悦的', - 'negative': '骄傲放纵的、过度享乐的、浪费的、充满嫉妒心的、母性的独裁、占有欲、败家的女人、挥霍无度的、骄纵的、纵欲的、为爱颓废的、不正当的爱情、不伦之恋、美丽的诱惑'}, - {'name': '【4】皇帝(The Emperor,IV)', - 'positive': '事业成功、物质丰厚、掌控爱情运的、有手段的、有方法的、阳刚的、独立自主的、有男性魅力的、大男人主义的、有处理事情的能力、有点独断的、想要实现野心与梦想的', - 'negative': '失败的、过于刚硬的、不利爱情运的、自以为是的、权威过度的、力量减弱的、丧失理智的、错误的判断、没有能力的、过于在乎世俗的、权力欲望过重的、权力使人腐败的、徒劳无功的'}, - {'name': '【5】教皇(The Hierophant,or the Pope,V)', - 'positive': '有智慧的、擅沟通的、适时的帮助、找到真理、有精神上的援助、得到贵人帮助、一个有影响力的导师、找到正确的方向、学业出现援助、爱情上出现长辈的干涉、媒人的帮助', - 'negative': '过于依赖的、错误的指导、盲目的安慰、无效的帮助、独裁的、疲劳轰炸的、精神洗脑的、以不正当手段取得认同的、毫无能力的、爱情遭破坏、第三者的介入'}, - {'name': '【6】恋人(The Lovers,VI)', - 'positive': '爱情甜蜜的、被祝福的关系、刚萌芽的爱情、顺利交往的、美满的结合、面临工作学业的选择、面对爱情的抉择、下决定的时刻、合作顺利的', - 'negative': '遭遇分离、有第三者介入、感情不合、外力干涉、面临分手状况、爱情已远去、无法结合的、遭受破坏的关系、爱错了人、不被祝福的恋情、因一时的寂寞而结合'}, - {'name': '【7】战车(The Chariot,VII)', - 'positive': '胜利的、凯旋而归的、不断的征服、有收获的、快速的解决、交通顺利的、充满信心的、不顾危险的、方向确定的、坚持向前的、冲劲十足的', - 'negative': '不易驾驭的、严重失败、交通意外、遭遇挫折的、遇到障碍的、挣扎的、意外冲击的、失去方向的、丧失理智的、鲁莽冲撞的'}, - {'name': '【8】力量(Strength,VIII)', - 'positive': '内在的力量使成功的、正确的信心、坦然的态度、以柔克刚的力量、有魅力的、精神力旺盛、有领导能力的、理性的处理态度、头脑清晰的', - 'negative': '丧失信心的、失去生命力的、沮丧的、失败的、失去魅力的、无助的、情绪化的、任性而为的、退缩的、没有能力处理问题的、充满负面情绪的'}, - {'name': '【9】隐者(The Hermit,IX)', - 'positive': '有骨气的、清高的、有智慧的、有法力的、自我修养的,生命的智慧情境、用智慧排除困难的、给予正确的指导方向、有鉴赏力的、三思而后行的、谨慎行动的', - 'negative': '假清高的、假道德的、没骨气、没有能力的、内心孤独寂寞的、缺乏支持的、错误的判断、被排挤的、没有足够智慧的、退缩的、自以为是的、与环境不合的'}, - {'name': '【10】命运之轮(The Wheel of Fortune,X)', - 'positive': '忽然而来的幸运、即将转变的局势、顺应局势带来成功、把握命运给予的机会、意外的发展、不可预测的未来、突如其来的爱情运变动', - 'negative': '突如其来的厄运、无法抵抗局势的变化、事情的发展失去了掌控、错失良机、无法掌握命运的关键时刻而导致失败、不利的突发状况、没有答案、被人摆布、有人暗中操作'}, - {'name': '【11】正义(Justice,XI)', - 'positive': '明智的决定、看清了真相、正确的判断与选择、得到公平的待遇、走向正确的道路、理智与正义战胜一切、维持平衡的、诉讼得到正义与公平、重新调整使之平衡、不留情面的', - 'negative': '错误的决定、不公平的待遇、没有原则的、缺乏理想的、失去方向的、不合理的、存有偏见的、冥顽不灵的、小心眼、过于冷漠的、不懂感情的'}, - {'name': '【12】倒吊人(The Hanged Man,XII)', - 'positive': '心甘情愿的牺牲奉献、以修练的方式来求道、不按常理的、反其道而行的、金钱上的损失、正专注于某个理想的、有坚定信仰的、长时间沈思的、需要沈淀的、成功之前的必经之道', - 'negative': '精神上的虐待、心不甘情不愿的牺牲、损失惨重的、受到亏待的、严重漏财的、不满足的、冷淡的、自私自利的、要求回报的付出、逃离綑绑和束缚、以错误的方式看世界'}, - {'name': '【13】死神(Death,XIII)', - 'positive': '必须结束旧有的现状、面临重新开始的时刻到了、将不好的过去清除掉、专注于心的开始、挥别过去的历史、展开心的旅程、在心里做个了结、激烈的变化', - 'negative': '已经历经了重生阶段了、革命已经完成、挥别了过去、失去了、结束了、失败了、病了、走出阴霾的时刻到了、没有转圜余地了'}, - {'name': '【14】节制(Temperance,XIV)', - 'positive': '良好的疏导、希望与承诺、得到调和、有节制的、平衡的、沟通良好的、健康的、成熟与均衡的个性、以机智处理问题、从过去的错误中学习、避免重蹈覆辙、净化的、有技巧的、有艺术才能的', - 'negative': '缺乏能力的、技术不佳的、不懂事的、需反省的、失去平衡状态、沟通不良的、缺乏自我控制力、不确定的、重复犯错的、挫败的、受阻碍的、暂时的分离、希望与承诺遥遥无期'}, - {'name': '【15】恶魔(The Devil ,XV)', - 'positive': '不伦之恋、不正当的欲望、受诱惑的、违反世俗约定的、不道德的、有特殊的艺术才能、沉浸在消极里、沉溺在恐惧之中的、充满愤怒和怨恨、因恐惧而阻碍了自己、错误的方向、不忠诚的、秘密恋情', - 'negative': '解脱了不伦之恋、挣脱了世俗的枷锁、不顾道德的、逃避的、伤害自己的、欲望的化解、被诅咒的、欲望强大的、不利的环境、盲目做判断、被唾弃的'}, - {'name': '【16】塔(The Tower,XVI)', - 'positive': '双方关系破裂、难以挽救的局面、组织瓦解了、损失惨重的、惨烈的破坏、毁灭性的事件、混乱的影响力、意外的发展、震惊扰人的问题、悲伤的、离别的、失望的、需要协助的、生活需要重建的', - 'negative': '全盘覆没、一切都已破坏殆尽、毫无转圜余地的、失去了、不安的、暴力的、已经遭逢厄运了、急需重建的'}, - {'name': '【17】星星(The Star,XVII)', - 'positive': '未来充满希望的、新的诞生、无限的希望、情感面精神面的希望、达成目标的、健康纯洁的、美好的未来、好运即将到来、美丽的身心、光明的时机、平静的生活、和平的处境', - 'negative': '希望遥遥无期的、失去信心的、没有寄托的未来、失去目标的、感伤的、放弃希望的、好运远离的、毫无进展的、过于虚幻、假想的爱情运、偏执于理想、希望破灭的'}, - {'name': '【18】月亮(The Moon,XVIII)', - 'positive': '负面的情绪、不安和恐惧、充满恐惧感、阴森恐怖的感觉、黑暗的环境、景气低落、白日梦、忽略现实的、未知的危险、无法预料的威胁、胡思乱想的、不脚踏实地的、沉溺的、固执的', - 'negative': '度过低潮阶段、心情平复、黑暗即将过去、曙光乍现、景气复甦、挥别恐惧、从忧伤中甦醒、恢复理智的、看清现实的、摆脱欲望的、脚踏实地的、走出谎言欺骗'}, - {'name': '【19】太阳(The Sun,XIX)', - 'positive': '前景看好的、运势如日中天的、成功的未来、光明正大的恋情、热恋的、美满的婚姻、丰收的、事件进行顺畅的、物质上的快乐、有成就的、满足的生活、旺盛', - 'negative': '热情消退的、逐渐黯淡的、遭遇失败的、分离的、傲慢的、失去目标的、没有远景的、失去活力的、没有未来的、物质的贫乏、不快乐的人生阶段'}, - {'name': '【20】审判(Judgement,XX)', - 'positive': '死而复生、调整心态重新来过、内心的觉醒、观念的翻新、超脱了束缚的、满意的结果、苦难的结束、重新检视过去而得到新的启发、一个新的开始、一段新的关系', - 'negative': '不公平的审判、无法度过考验的、旧事重演的、固执不改变的、自以为是的、对生命的看法狭隘的、后悔莫及的、自责的、不满意的结果、被击垮的'}, - {'name': '【21】世界(The World,XXI)', - 'positive': '完美的结局、重新开始的、生活上的完美境界、获得成功的、心理上的自由、完成成功的旅程、心灵的融合、自信十足带来成功、生活将有重大改变、获得完满的结果', - 'negative': '无法完美的、一段过往的结束、缺乏自尊的、感觉难受的、态度悲观的、丑恶的感情、无法挽回的局势、不完美的结局、无法再继续的、残缺的'} -] - - -def one_tarot(user_id: int) -> str: - # 用qq、日期生成随机种子 - # random_seed_str = str([user_id, datetime.date.today()]) - # md5 = hashlib.md5() - # md5.update(random_seed_str.encode('utf-8')) - # random_seed = md5.hexdigest() - # random.seed(random_seed) - tarot = random.sample(major_arcana, k=1)[0] - position = random.sample(['positive', 'negative'], k=1)[0] - if position == 'positive': - position_text = '正位' - else: - position_text = '逆位' - - result = f"【{position_text}】/{tarot['name']}\n{tarot[position]}" - return result diff --git a/omega_miya/plugins/http_cat/__init__.py b/omega_miya/plugins/http_cat/__init__.py index bf017c61..34d77ab3 100644 --- a/omega_miya/plugins/http_cat/__init__.py +++ b/omega_miya/plugins/http_cat/__init__.py @@ -9,18 +9,19 @@ """ import re -from nonebot import on_command, export, logger +from nonebot import on_command, logger +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent from nonebot.adapters.cqhttp.permission import GROUP, PRIVATE_FRIEND from nonebot.adapters.cqhttp.message import MessageSegment -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state from .data_source import get_http_cat # Custom plugin usage text -__plugin_name__ = 'HttpCat' +__plugin_custom_name__ = 'HttpCat' __plugin_usage__ = r'''【Http Cat】 就是喵喵喵 @@ -35,24 +36,19 @@ **Usage** /HttpCat ''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) # 注册事件响应器 httpcat = on_command( 'HttpCat', # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='httpcat', command=True, - level=30, - auth_node='basic'), + level=30), aliases={'httpcat', 'HTTPCAT'}, permission=GROUP | PRIVATE_FRIEND, priority=20, @@ -89,6 +85,8 @@ async def handle_httpcat(bot: Bot, event: MessageEvent, state: T_State): res = await get_http_cat(http_code=code) if res.success() and res.result: img_seg = MessageSegment.image(res.result) + logger.info(f'{event.user_id} 进获取了HttpCat: {code}') await httpcat.finish(img_seg) else: + logger.warning(f'{event.user_id} 进获取HttpCat失败: {repr(res)}') await httpcat.finish('^QAQ^') diff --git a/omega_miya/plugins/http_cat/data_source.py b/omega_miya/plugins/http_cat/data_source.py index c1b2379d..aee77c4c 100644 --- a/omega_miya/plugins/http_cat/data_source.py +++ b/omega_miya/plugins/http_cat/data_source.py @@ -11,8 +11,8 @@ import os import pathlib from nonebot import get_driver -from omega_miya.utils.Omega_plugin_utils import HttpFetcher -from omega_miya.utils.Omega_Base import Result +from omega_miya.utils.omega_plugin_utils import HttpFetcher +from omega_miya.database import Result global_config = get_driver().config TMP_PATH = global_config.tmp_path_ diff --git a/omega_miya/plugins/maybe/__init__.py b/omega_miya/plugins/maybe/__init__.py index 9b6411b7..a25c34a0 100644 --- a/omega_miya/plugins/maybe/__init__.py +++ b/omega_miya/plugins/maybe/__init__.py @@ -1,16 +1,19 @@ import datetime -from nonebot import CommandGroup, export, logger +import random +from nonebot import CommandGroup, logger +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import GroupMessageEvent from nonebot.adapters.cqhttp.permission import GROUP -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state +from nonebot.adapters.cqhttp.message import Message, MessageSegment +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state from .utils import maybe, sp, sp_event -from .oldalmanac import old_almanac +# from ._oldalmanac import old_almanac # Custom plugin usage text -__plugin_name__ = '求签' +__plugin_custom_name__ = '求签' __plugin_usage__ = r'''【求签】 求签, 求运势, 包括且不限于抽卡、吃饭、睡懒觉、DD 每个人每天求同一个东西的结果是一样的啦! @@ -26,25 +29,21 @@ **Usage** /求签 [所求之事] -/DD老黄历''' +/帮我选 [选项1 选项2 ...]''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) + # 注册事件响应器 Maybe = CommandGroup( 'maybe', # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='maybe', command=True, - level=10, - auth_node='basic'), + level=10), permission=GROUP, priority=10, block=True) @@ -55,7 +54,7 @@ # 修改默认参数处理 @luck.args_parser async def parse(bot: Bot, event: GroupMessageEvent, state: T_State): - args = str(event.get_plaintext()).strip().lower().split() + args = str(event.get_plaintext()).strip().split() if not args: await luck.reject('你似乎没有发送有效的参数呢QAQ, 请重新发送:') state[state["_current_key"]] = args[0] @@ -65,7 +64,7 @@ async def parse(bot: Bot, event: GroupMessageEvent, state: T_State): @luck.handle() async def handle_first_receive(bot: Bot, event: GroupMessageEvent, state: T_State): - args = str(event.get_plaintext()).strip().lower().split() + args = str(event.get_plaintext()).strip().split() if not args: pass elif args and len(args) == 1: @@ -92,30 +91,64 @@ async def handle_luck(bot: Bot, event: GroupMessageEvent, state: T_State): # 向用户发送结果 today = datetime.date.today().strftime('%Y年%m月%d日') msg = f'今天是{today}\n{draw_user}{draw_result}' + logger.info(f'{event.group_id}/{event.user_id} 进行了一次求签') await luck.finish(msg) -almanac = Maybe.command('almanac', aliases={'DD老黄历', 'dd老黄历'}) +help_choice = Maybe.command('choice', aliases={'帮我选', '选择困难症'}) -@almanac.handle() +# 修改默认参数处理 +@help_choice.args_parser +async def parse(bot: Bot, event: GroupMessageEvent, state: T_State): + args = str(event.get_plaintext()).strip() + if not args: + await help_choice.reject('你似乎没有发送有效的参数呢QAQ, 请重新发送:') + state[state["_current_key"]] = args + if state[state["_current_key"]] == '取消': + await help_choice.finish('操作已取消') + + +@help_choice.handle() async def handle_first_receive(bot: Bot, event: GroupMessageEvent, state: T_State): - args = str(event.get_plaintext()).strip().lower().split() + args = str(event.get_plaintext()).strip() if not args: pass else: - await almanac.finish('参数错误QAQ') - - user_id = event.user_id - - # 求签者昵称, 优先使用群昵称 - draw_user = event.sender.card - if not draw_user: - draw_user = event.sender.nickname - - draw_result = old_almanac(user_id=user_id) - - # 向用户发送结果 - today = datetime.date.today().strftime('%Y年%m月%d日') - msg = f"今天是{today}\n{draw_user}今日:\n{'='*12}\n{draw_result}" - await almanac.finish(msg) + state['choices'] = args + + +@help_choice.got('choices', prompt='有啥选项, 发来我帮你选~') +async def handle_help_choice(bot: Bot, event: GroupMessageEvent, state: T_State): + choices = state['choices'] + result = random.choice(str(choices).split()) + result_text = f'''帮你从“{'”,“'.join(str(choices).split())}”中选择了:\n\n“{result}”''' + msg = Message(MessageSegment.at(user_id=event.user_id)).append(result_text) + await help_choice.finish(msg) + + +# almanac = Maybe.command('almanac', aliases={'DD老黄历', 'dd老黄历'}) +# +# +# @almanac.handle() +# async def handle_first_receive(bot: Bot, event: GroupMessageEvent, state: T_State): +# args = str(event.get_plaintext()).strip().lower().split() +# if not args: +# pass +# else: +# await almanac.finish('参数错误QAQ') +# +# user_id = event.user_id +# +# # 求签者昵称, 优先使用群昵称 +# draw_user = event.sender.card +# if not draw_user: +# draw_user = event.sender.nickname +# +# draw_result = old_almanac(user_id=user_id) +# +# # 向用户发送结果 +# today = datetime.date.today().strftime('%Y年%m月%d日') +# msg = f"今天是{today}\n{draw_user}今日:\n{'='*12}\n{draw_result}" +# logger.info(f'{event.group_id}/{event.user_id} 进行了一次dd老黄历查询') +# await almanac.finish(msg) diff --git a/omega_miya/plugins/maybe/oldalmanac.py b/omega_miya/plugins/maybe/_oldalmanac.py similarity index 99% rename from omega_miya/plugins/maybe/oldalmanac.py rename to omega_miya/plugins/maybe/_oldalmanac.py index 2cfae74d..ea51ef91 100644 --- a/omega_miya/plugins/maybe/oldalmanac.py +++ b/omega_miya/plugins/maybe/_oldalmanac.py @@ -203,4 +203,7 @@ def old_almanac(user_id: int) -> str: f"【忌】\n{dd_do_and_not[1]['name']} —— {dd_do_and_not[1]['bad']}\n\n" \ f"今日宜D:{dd_vtb[0]}\n\n“{dd_saying[0]}”" + # 重置随机种子 + random.seed() + return result diff --git a/omega_miya/plugins/maybe/utils.py b/omega_miya/plugins/maybe/utils.py index c2093319..d54b3849 100644 --- a/omega_miya/plugins/maybe/utils.py +++ b/omega_miya/plugins/maybe/utils.py @@ -11,7 +11,7 @@ def maybe(draw: str, user_id: int) -> str: md5.update(random_seed_str.encode('utf-8')) random_seed = md5.hexdigest() random.seed(random_seed) - # 生成求签种子, 9分一级 + # 生成求签随机数, 9分一级 divination_result = random.randint(1, 109) # 大吉・中吉・小吉・吉・半吉・末吉・末小吉・凶・小凶・半凶・末凶・大凶 if divination_result < 9: @@ -38,7 +38,12 @@ def maybe(draw: str, user_id: int) -> str: result_text = '中吉' else: result_text = '大吉' + msg = f'所求事项: 【{draw}】\n\n结果: 【{result_text}】' + + # 重置随机种子 + random.seed() + return msg diff --git a/omega_miya/plugins/miya_button/__init__.py b/omega_miya/plugins/miya_button/__init__.py index cf227853..1407d20d 100644 --- a/omega_miya/plugins/miya_button/__init__.py +++ b/omega_miya/plugins/miya_button/__init__.py @@ -2,31 +2,47 @@ import os import pathlib from nonebot import MatcherGroup, logger +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.rule import to_me from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.message import MessageSegment from nonebot.adapters.cqhttp.event import GroupMessageEvent from nonebot.adapters.cqhttp.permission import GROUP -from omega_miya.utils.Omega_plugin_utils import init_permission_state +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state from .resources import miya_voices -""" -miya按钮bot实现版本 -测试中 -""" + +# Custom plugin usage text +__plugin_custom_name__ = '猫按钮' +__plugin_usage__ = r'''【猫按钮】 +发出可爱的猫叫 + +**Permission** +Command & Lv.10 +or AuthNode + +**AuthNode** +basic + +**Usage** +@bot 喵一个''' + + +# Init plugin export +init_export(export(), __plugin_custom_name__, __plugin_usage__) button = MatcherGroup( type='message', rule=to_me(), # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='button', command=True, level=10), permission=GROUP, - priority=100, + priority=50, block=False) @@ -46,3 +62,4 @@ async def handle_miya_button(bot: Bot, event: GroupMessageEvent, state: T_State) file_url = pathlib.Path(voice_file).as_uri() msg = MessageSegment.record(file=file_url) await miya_button.finish(msg) + logger.info(f'User: {event.user_id} 让 Omega 喵了一下~') diff --git a/omega_miya/plugins/nbnhhsh/__init__.py b/omega_miya/plugins/nbnhhsh/__init__.py index 163bd2e5..15b1dd0f 100644 --- a/omega_miya/plugins/nbnhhsh/__init__.py +++ b/omega_miya/plugins/nbnhhsh/__init__.py @@ -1,14 +1,15 @@ -from nonebot import on_command, export, logger +from nonebot import on_command, logger +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent from nonebot.adapters.cqhttp.permission import GROUP, PRIVATE_FRIEND -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state from .utils import get_guess # Custom plugin usage text -__plugin_name__ = '好好说话' +__plugin_custom_name__ = '好好说话' __plugin_usage__ = r'''【能不能好好说话?】 拼音首字母缩写释义 群组/私聊可用 @@ -24,24 +25,19 @@ **Usage** /好好说话 [缩写]''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) # 注册事件响应器 nbnhhsh = on_command( '好好说话', # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='nbnhhsh', command=True, - level=30, - auth_node='basic'), + level=30), aliases={'hhsh', 'nbnhhsh'}, permission=GROUP | PRIVATE_FRIEND, priority=20, diff --git a/omega_miya/plugins/nbnhhsh/utils.py b/omega_miya/plugins/nbnhhsh/utils.py index a98f3730..ff2f3858 100644 --- a/omega_miya/plugins/nbnhhsh/utils.py +++ b/omega_miya/plugins/nbnhhsh/utils.py @@ -1,4 +1,4 @@ -from omega_miya.utils.Omega_plugin_utils import HttpFetcher +from omega_miya.utils.omega_plugin_utils import HttpFetcher API_URL = 'https://lab.magiconch.com/api/nbnhhsh/guess/' diff --git a/omega_miya/plugins/nhentai/__init__.py b/omega_miya/plugins/nhentai/__init__.py index 65d0fc3b..63070e31 100644 --- a/omega_miya/plugins/nhentai/__init__.py +++ b/omega_miya/plugins/nhentai/__init__.py @@ -4,17 +4,18 @@ """ import re import os -from nonebot import on_command, export, logger +from nonebot import on_command, logger +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import GroupMessageEvent from nonebot.adapters.cqhttp.permission import GROUP -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state from omega_miya.utils.nhentai_utils import NhentaiGallery # Custom plugin usage text -__plugin_name__ = 'nh' +__plugin_custom_name__ = 'nh' __plugin_usage__ = r'''【nh】 神秘的插件 仅限群聊使用 @@ -30,13 +31,9 @@ /nh search [tag] /nh download [id]''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) # 注册事件响应器 @@ -44,10 +41,9 @@ 'nh', aliases={'NH'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='nhentai', - command=True, - auth_node='basic'), + command=True), permission=GROUP, priority=20, block=True) diff --git a/omega_miya/plugins/Omega_anti_flash/__init__.py b/omega_miya/plugins/omega_anti_flash/__init__.py similarity index 81% rename from omega_miya/plugins/Omega_anti_flash/__init__.py rename to omega_miya/plugins/omega_anti_flash/__init__.py index e4aea253..04981ed2 100644 --- a/omega_miya/plugins/Omega_anti_flash/__init__.py +++ b/omega_miya/plugins/omega_anti_flash/__init__.py @@ -1,17 +1,18 @@ -from nonebot import MatcherGroup, export, logger +from nonebot import MatcherGroup, logger +from nonebot.plugin.export import export from nonebot.permission import SUPERUSER from nonebot.typing import T_State -from nonebot.adapters.cqhttp import Message +from nonebot.adapters.cqhttp import Message, MessageSegment from nonebot.adapters.cqhttp.bot import Bot -from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent +from nonebot.adapters.cqhttp.event import GroupMessageEvent from nonebot.adapters.cqhttp.permission import GROUP, GROUP_ADMIN, GROUP_OWNER -from omega_miya.utils.Omega_Base import DBBot, DBBotGroup, DBAuth, Result -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state, OmegaRules +from omega_miya.database import DBBot, DBBotGroup, DBAuth, Result +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state, OmegaRules # Custom plugin usage text __plugin_raw_name__ = __name__.split('.')[-1] -__plugin_name__ = 'AntiFlash' +__plugin_custom_name__ = 'AntiFlash' __plugin_usage__ = r'''【AntiFlash 反闪照】 检测闪照并提取原图 @@ -26,13 +27,9 @@ **GroupAdmin and SuperUser Only** /AntiFlash ''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) # 注册事件响应器 @@ -42,7 +39,7 @@ 'AntiFlash', aliases={'antiflash', '反闪照'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='anti_flash', command=True, level=10), @@ -102,7 +99,8 @@ async def anti_flash_on(bot: Bot, event: GroupMessageEvent, state: T_State) -> R if not group_exist: return Result.IntResult(error=False, info='Group not exist', result=-1) - auth_node = DBAuth(self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=f'{__plugin_raw_name__}.basic') + auth_node = DBAuth( + self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=OmegaRules.basic_node(__plugin_raw_name__)) result = await auth_node.set(allow_tag=1, deny_tag=0, auth_info='启用反闪照') return result @@ -115,12 +113,13 @@ async def anti_flash_off(bot: Bot, event: GroupMessageEvent, state: T_State) -> if not group_exist: return Result.IntResult(error=False, info='Group not exist', result=-1) - auth_node = DBAuth(self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=f'{__plugin_raw_name__}.basic') + auth_node = DBAuth( + self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=OmegaRules.basic_node(__plugin_raw_name__)) result = await auth_node.set(allow_tag=0, deny_tag=1, auth_info='禁用反闪照') return result -anti_flash_handler = AntiFlash.on_message(rule=OmegaRules.has_auth_node(__plugin_raw_name__, 'basic')) +anti_flash_handler = AntiFlash.on_message(rule=OmegaRules.has_auth_node(OmegaRules.basic_node(__plugin_raw_name__))) @anti_flash_handler.handle() @@ -128,6 +127,8 @@ async def check_flash_img(bot: Bot, event: GroupMessageEvent, state: T_State): for msg_seg in event.message: if msg_seg.type == 'image': if msg_seg.data.get('type') == 'flash': - msg = Message('AntiFlash 已检测到闪照:\n').append(str(msg_seg).replace(',type=flash', '')) + img_file = msg_seg.data.get('file') + img_seq = MessageSegment.image(file=img_file) + msg = Message('AntiFlash 已检测到闪照:\n').append(img_seq) logger.info(f'AntiFlash 已处理闪照, message_id: {event.message_id}') await anti_flash_handler.finish(msg) diff --git a/omega_miya/plugins/Omega_anti_recall/__init__.py b/omega_miya/plugins/omega_anti_recall/__init__.py similarity index 85% rename from omega_miya/plugins/Omega_anti_recall/__init__.py rename to omega_miya/plugins/omega_anti_recall/__init__.py index 2f516653..931af68e 100644 --- a/omega_miya/plugins/Omega_anti_recall/__init__.py +++ b/omega_miya/plugins/omega_anti_recall/__init__.py @@ -1,17 +1,18 @@ -from nonebot import on_command, on_notice, export, logger +from nonebot import on_command, on_notice, logger +from nonebot.plugin.export import export from nonebot.permission import SUPERUSER from nonebot.typing import T_State from nonebot.adapters.cqhttp import Message from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import GroupMessageEvent, GroupRecallNoticeEvent from nonebot.adapters.cqhttp.permission import GROUP_ADMIN, GROUP_OWNER -from omega_miya.utils.Omega_Base import DBBot, DBBotGroup, DBAuth, DBHistory, Result -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state, OmegaRules +from omega_miya.database import DBBot, DBBotGroup, DBAuth, DBHistory, Result +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state, OmegaRules # Custom plugin usage text __plugin_raw_name__ = __name__.split('.')[-1] -__plugin_name__ = 'AntiRecall' +__plugin_custom_name__ = 'AntiRecall' __plugin_usage__ = r'''【AntiRecall 反撤回】 检测消息撤回并提取原消息 @@ -26,13 +27,9 @@ **GroupAdmin and SuperUser Only** /AntiRecall ''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) # 注册事件响应器 @@ -40,7 +37,7 @@ 'AntiRecall', aliases={'antirecall', '反撤回'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='anti_recall', command=True, level=10), @@ -100,7 +97,8 @@ async def anti_recall_on(bot: Bot, event: GroupMessageEvent, state: T_State) -> if not group_exist: return Result.IntResult(error=False, info='Group not exist', result=-1) - auth_node = DBAuth(self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=f'{__plugin_raw_name__}.basic') + auth_node = DBAuth( + self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=OmegaRules.basic_node(__plugin_raw_name__)) result = await auth_node.set(allow_tag=1, deny_tag=0, auth_info='启用反撤回') return result @@ -113,12 +111,16 @@ async def anti_recall_off(bot: Bot, event: GroupMessageEvent, state: T_State) -> if not group_exist: return Result.IntResult(error=False, info='Group not exist', result=-1) - auth_node = DBAuth(self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=f'{__plugin_raw_name__}.basic') + auth_node = DBAuth( + self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=OmegaRules.basic_node(__plugin_raw_name__)) result = await auth_node.set(allow_tag=0, deny_tag=1, auth_info='禁用反撤回') return result -anti_recall_handler = on_notice(rule=OmegaRules.has_auth_node(__plugin_raw_name__, 'basic'), priority=100, block=False) +anti_recall_handler = on_notice( + rule=OmegaRules.has_auth_node(OmegaRules.basic_node(__plugin_raw_name__)), + priority=100, + block=False) @anti_recall_handler.handle() diff --git a/omega_miya/plugins/Omega_auth_manager/__init__.py b/omega_miya/plugins/omega_auth_manager/__init__.py similarity index 95% rename from omega_miya/plugins/Omega_auth_manager/__init__.py rename to omega_miya/plugins/omega_auth_manager/__init__.py index 562e7b0f..2290809e 100644 --- a/omega_miya/plugins/Omega_auth_manager/__init__.py +++ b/omega_miya/plugins/omega_auth_manager/__init__.py @@ -1,16 +1,17 @@ import re -from nonebot import on_command, export, get_plugin, get_loaded_plugins, logger +from nonebot import on_command, get_plugin, get_loaded_plugins, logger +from nonebot.plugin.export import export from nonebot.rule import to_me from nonebot.permission import SUPERUSER from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent -from omega_miya.utils.Omega_Base import DBBot, DBFriend, DBBotGroup, DBAuth -from omega_miya.utils.Omega_plugin_utils import init_export +from omega_miya.database import DBBot, DBFriend, DBBotGroup, DBAuth +from omega_miya.utils.omega_plugin_utils import init_export # Custom plugin usage text -__plugin_name__ = 'OmegaAuth' +__plugin_custom_name__ = 'OmegaAuth' __plugin_usage__ = r'''【OmegaAuth 授权管理插件】 插件特殊权限授权管理 仅限管理员使用 @@ -19,8 +20,10 @@ **SuperUser Only** /OmegaAuth''' + # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) + # 注册事件响应器 omegaauth = on_command('OmegaAuth', rule=to_me(), aliases={'omegaauth', 'oauth'}, @@ -127,7 +130,7 @@ async def handle_auth_id(bot: Bot, event: MessageEvent, state: T_State): for plugin in plugins: if plugin.export.get('auth_node'): auth_node_plugin.append(plugin.name) - + auth_node_plugin.sort() state["auth_node_plugin"] = auth_node_plugin p_list = '\n'.join(auth_node_plugin) await omegaauth.send(f'可配置权限节点的插件有:\n\n{p_list}') diff --git a/omega_miya/plugins/Omega_email/__init__.py b/omega_miya/plugins/omega_email/__init__.py similarity index 93% rename from omega_miya/plugins/Omega_email/__init__.py rename to omega_miya/plugins/omega_email/__init__.py index 5ef0a42c..04da5122 100644 --- a/omega_miya/plugins/Omega_email/__init__.py +++ b/omega_miya/plugins/omega_email/__init__.py @@ -1,21 +1,21 @@ import re import pathlib -from nonebot import MatcherGroup, export, logger +from nonebot import MatcherGroup, logger +from nonebot.plugin.export import export from nonebot.rule import to_me from nonebot.permission import SUPERUSER from nonebot.typing import T_State -from nonebot.adapters.cqhttp import MessageSegment, Message +from nonebot.adapters.cqhttp import MessageSegment from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent from nonebot.adapters.cqhttp.permission import GROUP -from omega_miya.utils.Omega_Base import DBEmailBox, DBBot, DBBotGroup -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state -from omega_miya.utils.text_to_img import text_to_img +from omega_miya.database import DBEmailBox, DBBot, DBBotGroup +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state, TextUtils from .utils import check_mailbox, get_unseen_mail_info, encrypt_password, decrypt_password # Custom plugin usage text -__plugin_name__ = 'OmegaEmail' +__plugin_custom_name__ = 'OmegaEmail' __plugin_usage__ = r'''【OmegaEmail 邮箱插件】 主要是用来收验证码OvO 仅限群聊使用 @@ -32,13 +32,9 @@ /绑定邮箱 /清空绑定邮箱''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) # 注册事件响应器 @@ -170,10 +166,9 @@ async def handle_first_receive(bot: Bot, event: GroupMessageEvent, state: T_Stat OmegaEmail_group = MatcherGroup( type='message', # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='OmegaEmail_group', - command=True, - auth_node='basic'), + command=True), permission=GROUP, priority=20, block=True) @@ -231,7 +226,7 @@ async def handle_first_receive(bot: Bot, event: GroupMessageEvent, state: T_Stat content = re.sub(r'\s', '', content) content = content.replace(' ', '').replace('\n', '').replace(' ', '') msg = f"【{mail.header}】\n时间: {mail.date}\n发件人: {mail.sender}\n{'=' * 16}\n{content}" - text_img_result = await text_to_img(text=msg) + text_img_result = await TextUtils(text=msg).text_to_img() if text_img_result.error: raise Exception(f'Text to img failed, {text_img_result.info}') file_url = pathlib.Path(text_img_result.result).as_uri() diff --git a/omega_miya/plugins/Omega_email/imap.py b/omega_miya/plugins/omega_email/imap.py similarity index 100% rename from omega_miya/plugins/Omega_email/imap.py rename to omega_miya/plugins/omega_email/imap.py diff --git a/omega_miya/plugins/Omega_email/utils.py b/omega_miya/plugins/omega_email/utils.py similarity index 95% rename from omega_miya/plugins/Omega_email/utils.py rename to omega_miya/plugins/omega_email/utils.py index 777d2240..f2a532ec 100644 --- a/omega_miya/plugins/Omega_email/utils.py +++ b/omega_miya/plugins/omega_email/utils.py @@ -1,8 +1,8 @@ import asyncio import nonebot import json -from omega_miya.utils.Omega_Base import DBEmail, Result -from omega_miya.utils.Omega_plugin_utils import AESEncryptStr +from omega_miya.database import Result +from omega_miya.utils.omega_plugin_utils import AESEncryptStr from .imap import EmailImap diff --git a/omega_miya/plugins/Omega_help/__init__.py b/omega_miya/plugins/omega_help/__init__.py similarity index 75% rename from omega_miya/plugins/Omega_help/__init__.py rename to omega_miya/plugins/omega_help/__init__.py index 28316ffe..3aca10d7 100644 --- a/omega_miya/plugins/Omega_help/__init__.py +++ b/omega_miya/plugins/omega_help/__init__.py @@ -1,43 +1,47 @@ -from nonebot import on_command, export +from nonebot import on_command +from nonebot.plugin.export import export from nonebot.plugin import get_loaded_plugins from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot -from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent +from nonebot.adapters.cqhttp.event import MessageEvent from nonebot.adapters.cqhttp.permission import GROUP, PRIVATE_FRIEND -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state, PluginCoolDown +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state, PluginCoolDown # Custom plugin usage text -__plugin_name__ = '帮助' +__plugin_custom_name__ = '帮助' __plugin_usage__ = r'''【帮助】 +一个简单的帮助插件 -一个简单的帮助插件''' +**Permission** +Friend Private +Command & Lv.10 +or AuthNode -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - PluginCoolDown.skip_auth_node, - 'basic' -] +**AuthNode** +basic + +**Usage** +/帮助 [插件名]''' -# 声明本插件的冷却时间配置 -__plugin_cool_down__ = [ - PluginCoolDown(PluginCoolDown.user_type, 5), - PluginCoolDown(PluginCoolDown.group_type, 1) -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__, __plugin_cool_down__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) + # 注册事件响应器 bot_help = on_command( 'help', aliases={'Help', '帮助'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='help', command=True, level=10, - auth_node='basic'), + cool_down=[ + PluginCoolDown(PluginCoolDown.user_type, 300), + PluginCoolDown(PluginCoolDown.group_type, 60) + ]), permission=GROUP | PRIVATE_FRIEND, priority=10, block=True) diff --git a/omega_miya/plugins/omega_invite_manager/__init__.py b/omega_miya/plugins/omega_invite_manager/__init__.py new file mode 100644 index 00000000..91f22f06 --- /dev/null +++ b/omega_miya/plugins/omega_invite_manager/__init__.py @@ -0,0 +1,134 @@ +""" +@Author : Ailitonia +@Date : 2021/06/09 19:10 +@FileName : __init__.py +@Project : nonebot2_miya +@Description : 好友邀请与群组邀请管理 +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +import random +import string +from typing import Dict +from nonebot import on_request, on_command, logger +from nonebot.plugin.export import export +from nonebot.typing import T_State +from nonebot.rule import to_me +from nonebot.permission import SUPERUSER +from nonebot.adapters.cqhttp.bot import Bot +from nonebot.adapters.cqhttp.event import MessageEvent, FriendRequestEvent, GroupRequestEvent +from omega_miya.database import DBBot +from omega_miya.utils.omega_plugin_utils import init_export, MessageDecoder, PermissionChecker + + +# 为好友请求分配验证码 +FRIEND_ADD_VERIFY_CODE: Dict[int, str] = {} + + +# Custom plugin usage text +__plugin_custom_name__ = '好友和群组请求管理' +__plugin_usage__ = r'''【Omega 好友和群组请求管理】 +处理加好友请求和加群、退群请求 + +以下命令均需要@机器人 +**Usage** +**SuperUser Only** +/好友验证''' + + +# 声明本插件额外可配置的权限节点 +__plugin_auth_node__ = [ + 'allow_invite_group' +] + + +# Init plugin export +init_export(export(), __plugin_custom_name__, __plugin_usage__, __plugin_auth_node__) + + +# 注册事件响应器 +generate_verify_code = on_command('FriendVerifyCode', rule=to_me(), aliases={'加好友', '好友验证'}, + permission=SUPERUSER, priority=10, block=True) + + +# 修改默认参数处理 +@generate_verify_code.args_parser +async def parse(bot: Bot, event: MessageEvent, state: T_State): + args = str(event.get_message()).lower().strip() + if not args: + await generate_verify_code.reject('你似乎没有发送有效的参数呢QAQ, 请重新发送:') + state[state["_current_key"]] = args + if state[state["_current_key"]] == '取消': + await generate_verify_code.finish('操作已取消') + + +@generate_verify_code.handle() +async def handle_first_receive(bot: Bot, event: MessageEvent, state: T_State): + all_at_qq = MessageDecoder(message=event.get_message()).get_all_at_qq() + if len(all_at_qq) == 1: + state['user_id'] = all_at_qq[0] + elif len(all_at_qq) > 1: + await generate_verify_code.finish('每次只能为一个用户生成验证码QAQ') + + +@generate_verify_code.got('user_id', prompt='请发送你想要生成验证码的用户qq号:') +async def handle_verify_code(bot: Bot, event: MessageEvent, state: T_State): + try: + user_id = int(state['user_id']) + except ValueError: + await generate_verify_code.finish('输入的不是合法的qq号QAQ') + return + + global FRIEND_ADD_VERIFY_CODE + verify_code = ''.join(random.sample(string.ascii_letters, k=6)) + FRIEND_ADD_VERIFY_CODE.update({user_id: verify_code}) + await generate_verify_code.finish(f'已为用户: {user_id} 生成好友验证码: {verify_code}') + + +# 注册事件响应器 +add_and_invite_request = on_request(priority=100) + + +# 处理加好友申请 +@add_and_invite_request.handle() +async def handle_friend_request(bot: Bot, event: FriendRequestEvent, state: T_State): + global FRIEND_ADD_VERIFY_CODE + user_id = event.user_id + comment = event.comment + + # 加好友验证消息 + try: + verify_code = FRIEND_ADD_VERIFY_CODE.pop(user_id) + except KeyError: + await bot.set_friend_add_request(flag=event.flag, approve=False) + logger.warning(f'已拒绝用户: {user_id} 的好友申请, 该用户没有生成有效的验证码') + return + + if verify_code == comment: + await bot.set_friend_add_request(flag=event.flag, approve=True) + logger.info(f'已同意用户: {user_id} 的好友申请, 验证通过') + else: + await bot.set_friend_add_request(flag=event.flag, approve=False) + logger.warning(f'已拒绝用户: {user_id} 的好友申请, 验证码验证失败') + + +# 处理被邀请进群 +@add_and_invite_request.handle() +async def handle_group_invite(bot: Bot, event: GroupRequestEvent, state: T_State): + self_id = event.self_id + user_id = event.user_id + group_id = event.group_id + sub_type = event.sub_type + detail_type = event.request_type + if detail_type == 'group' and sub_type == 'invite': + # 只有具备相应权限节点的用户才可以邀请bot进入群组 + permission_check_result = await PermissionChecker(self_bot=DBBot(self_qq=self_id)).check_auth_node( + auth_id=user_id, auth_type='user', auth_node='omega_invite_manager.allow_invite_group' + ) + if permission_check_result == 1: + await bot.set_group_add_request(flag=event.flag, sub_type='invite', approve=True) + logger.info(f'已处理邀请进群请求, 被用户: {user_id} 邀请加入群组: {group_id}.') + else: + await bot.set_group_add_request(flag=event.flag, sub_type='invite', approve=False, reason='没有邀请权限') + logger.warning(f'已拒绝邀请进群请求, 非授权用户: {user_id} 试图邀请加入群组: {group_id}.') diff --git a/omega_miya/plugins/Omega_manager/__init__.py b/omega_miya/plugins/omega_manager/__init__.py similarity index 92% rename from omega_miya/plugins/Omega_manager/__init__.py rename to omega_miya/plugins/omega_manager/__init__.py index 3e487eb0..6892d35b 100644 --- a/omega_miya/plugins/Omega_manager/__init__.py +++ b/omega_miya/plugins/omega_manager/__init__.py @@ -6,20 +6,23 @@ - /Omega Notice - 为本群组配置通知权限(订阅类插件是否通知) - /Omega Command - 为本群组配置命令权限(是否允许使用命令) - /Omega SetLevel - 为本群组配置命令等级(对于低于命令要求等级的群组, 该命令不会被响应) +- /Omega QuitGroup - 命令 Bot 立即退群 """ from dataclasses import dataclass -from nonebot import on_command, export, logger +from nonebot import on_command, logger +from nonebot.exception import FinishedException +from nonebot.plugin.export import export from nonebot.permission import SUPERUSER from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent from nonebot.adapters.cqhttp.permission import GROUP_ADMIN, GROUP_OWNER, PRIVATE_FRIEND -from omega_miya.utils.Omega_Base import DBBot, DBBotGroup, DBUser, DBAuth, DBFriend, Result -from omega_miya.utils.Omega_plugin_utils import init_export -from .sys_background_scheduled import scheduler +from omega_miya.database import DBBot, DBBotGroup, DBUser, DBAuth, DBFriend, Result +from omega_miya.utils.omega_plugin_utils import init_export +from .background_tasks import scheduler # Custom plugin usage text -__plugin_name__ = 'Omega' +__plugin_custom_name__ = 'Omega' __plugin_usage__ = r'''【Omega 管理插件】 Omega机器人管理 @@ -32,6 +35,7 @@ /Omega SetLevel /Omega ShowPermission /Omega ResetPermission +/Omega QuitGroup **Friend Private Only** /Omega Init @@ -39,7 +43,7 @@ /Omega Disable''' # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) # 注册事件响应器 omega = on_command('Omega', rule=None, aliases={'omega'}, @@ -70,7 +74,8 @@ async def handle_first_receive(bot: Bot, event: MessageEvent, state: T_State): await omega.finish('你好呀~ 我是Omega Miya~ 请问您今天要来点喵娘吗?') -@omega.got('sub_command', prompt='执行操作?\n【Init/Upgrade/Notice/Command/SetLevel/ShowPermission/ResetPermission】') +@omega.got('sub_command', + prompt='执行操作?\n【Init/Upgrade/Notice/Command/SetLevel/ShowPermission/ResetPermission/QuitGroup】') async def handle_sub_command(bot: Bot, event: GroupMessageEvent, state: T_State): if not isinstance(event, GroupMessageEvent): return @@ -82,7 +87,8 @@ async def handle_sub_command(bot: Bot, event: GroupMessageEvent, state: T_State) 'command': set_group_command, 'setlevel': set_group_level, 'showpermission': show_group_permission, - 'resetpermission': reset_group_permission + 'resetpermission': reset_group_permission, + 'quitgroup': quit_group } # 需要回复信息的命令列表 need_reply = [ @@ -397,6 +403,18 @@ async def reset_group_permission(bot: Bot, event: GroupMessageEvent, state: T_St return result +async def quit_group(bot: Bot, event: GroupMessageEvent, state: T_State) -> None: + """退群命令, 直接抛 FinishedException 不返回""" + user_id = event.user_id + group_id = event.group_id + self_bot = DBBot(self_qq=int(bot.self_id)) + group = DBBotGroup(group_id=group_id, self_bot=self_bot) + await group.permission_set(notice=-1, command=-1, level=-1) + logger.warning(f"Bot will to leave the Group: {event.group_id} immediately, Operator: {user_id}") + await bot.set_group_leave(group_id=group_id, is_dismiss=False) + raise FinishedException + + async def init_group_auth_node(group_id: int, self_bot: DBBot): """ 为群组配置权限节点默认值 @@ -409,7 +427,7 @@ class AuthNode: auth_info: str default_auth_nodes = [ - AuthNode(node='Omega_help.skip_cd', allow_tag=1, deny_tag=0, auth_info='默认规则: help免cd'), + AuthNode(node='omega_help.skip_cd', allow_tag=1, deny_tag=0, auth_info='默认规则: help免cd'), AuthNode(node='nhentai.basic', allow_tag=0, deny_tag=1, auth_info='默认规则: 禁用nhentai'), AuthNode(node='setu.setu', allow_tag=0, deny_tag=1, auth_info='默认规则: 禁用setu'), AuthNode(node='setu.allow_r18', allow_tag=0, deny_tag=1, auth_info='默认规则: 禁用setu r18'), @@ -435,7 +453,7 @@ class AuthNode: auth_info: str default_auth_nodes = [ - AuthNode(node='Omega_help.skip_cd', allow_tag=1, deny_tag=0, auth_info='默认规则: 好友help免cd') + AuthNode(node='omega_help.skip_cd', allow_tag=1, deny_tag=0, auth_info='默认规则: 好友help免cd') ] for auth_node in default_auth_nodes: diff --git a/omega_miya/plugins/Omega_manager/sys_background_scheduled.py b/omega_miya/plugins/omega_manager/background_tasks.py similarity index 96% rename from omega_miya/plugins/Omega_manager/sys_background_scheduled.py rename to omega_miya/plugins/omega_manager/background_tasks.py index 18026515..ac9d48c7 100644 --- a/omega_miya/plugins/Omega_manager/sys_background_scheduled.py +++ b/omega_miya/plugins/omega_manager/background_tasks.py @@ -3,8 +3,8 @@ """ import nonebot from nonebot import logger, require -from omega_miya.utils.Omega_Base import DBBot, DBBotGroup, DBUser, DBFriend, DBStatus, DBCoolDownEvent -from omega_miya.utils.Omega_plugin_utils import HttpFetcher +from omega_miya.database import DBBot, DBBotGroup, DBUser, DBFriend, DBStatus, DBCoolDownEvent +from omega_miya.utils.omega_plugin_utils import HttpFetcher global_config = nonebot.get_driver().config ENABLE_PROXY = global_config.enable_proxy @@ -191,7 +191,8 @@ async def refresh_friends_info(): misfire_grace_time=10 ) async def cool_down_refresh(): - await DBCoolDownEvent.clear_time_out_event() + result = await DBCoolDownEvent.clear_time_out_event() + logger.debug(f'Cool down refresh | Task result, {", ".join([f"{k}: {v}" for (k,v) in result.result.items()])}') logger.opt(colors=True).info('Cool down refresh | Cleaned all expired event') diff --git a/omega_miya/plugins/omega_plugins_manager/__init__.py b/omega_miya/plugins/omega_plugins_manager/__init__.py new file mode 100644 index 00000000..887503b8 --- /dev/null +++ b/omega_miya/plugins/omega_plugins_manager/__init__.py @@ -0,0 +1,159 @@ +""" +@Author : Ailitonia +@Date : 2021/09/12 14:02 +@FileName : __init__.py.py +@Project : nonebot2_miya +@Description : 插件管理器 +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +import pathlib +from nonebot import CommandGroup, get_loaded_plugins, logger +from nonebot.plugin.export import export +from nonebot.rule import to_me +from nonebot.permission import SUPERUSER +from nonebot.typing import T_State +from nonebot.adapters.cqhttp.bot import Bot +from nonebot.adapters.cqhttp.event import MessageEvent +from nonebot.adapters.cqhttp.message import MessageSegment +from omega_miya.database import DBPlugin +from omega_miya.utils.omega_plugin_utils import init_export, TextUtils + + +# Custom plugin usage text +__plugin_custom_name__ = 'Omega Plugins Manager' +__plugin_usage__ = r'''【OmegaPluginsManager 插件管理器】 +插件管理器 +仅限超级管理员使用 + +**Usage** +**SuperUser Only** +/OPM''' + + +# Init plugin export +init_export(export(), __plugin_custom_name__, __plugin_usage__) + + +# 注册事件响应器 +OmegaPluginsManager = CommandGroup( + 'OPM', + rule=to_me(), + permission=SUPERUSER, + priority=10, + block=True +) + +enable_plugin = OmegaPluginsManager.command('enable', aliases={'启用插件'}) +disable_plugin = OmegaPluginsManager.command('disable', aliases={'禁用插件'}) +list_plugins = OmegaPluginsManager.command('list', aliases={'插件列表'}) + + +# 修改默认参数处理 +@enable_plugin.args_parser +async def parse(bot: Bot, event: MessageEvent, state: T_State): + args = event.get_plaintext().strip() + if not args: + await enable_plugin.reject('你似乎没有发送有效的参数呢QAQ, 请重新发送:') + state[state["_current_key"]] = args + if state[state["_current_key"]] == '取消': + await enable_plugin.finish('操作已取消') + + +@enable_plugin.handle() +async def handle_first_receive(bot: Bot, event: MessageEvent, state: T_State): + args = event.get_plaintext().strip() + if args: + state['plugin_name'] = args + else: + plugin_list = get_loaded_plugins() + # 提示的时候仅显示有 matcher 的插件 + plugin_msg = '\n'.join([x.name for x in plugin_list if len(x.matcher) > 0]) + msg = f'当前已加载的插件有:\n\n{plugin_msg}' + await enable_plugin.send(msg) + + +@enable_plugin.got('plugin_name', prompt='请输入需要启用的插件名称:') +async def handle_enable_plugin(bot: Bot, event: MessageEvent, state: T_State): + plugin_name = state['plugin_name'] + plugin_list = [x.name for x in get_loaded_plugins()] + if plugin_name not in plugin_list: + await enable_plugin.reject('没有这个插件, 请重新输入需要启用的插件名称:\n取消操作请发送【取消】') + + result = await DBPlugin(plugin_name=plugin_name).update(enabled=1) + if result.success(): + logger.info(f'OPM | Success enabled plugin {plugin_name} by user {event.user_id}') + await enable_plugin.finish(f'启用插件 {plugin_name} 成功') + else: + logger.error(f'OPM | Failed to enable {plugin_name}, {result.info}') + await enable_plugin.finish(f'启用插件 {plugin_name} 失败, 详细信息请参见日志') + + +# 修改默认参数处理 +@disable_plugin.args_parser +async def parse(bot: Bot, event: MessageEvent, state: T_State): + args = event.get_plaintext().strip() + if not args: + await disable_plugin.reject('你似乎没有发送有效的参数呢QAQ, 请重新发送:') + state[state["_current_key"]] = args + if state[state["_current_key"]] == '取消': + await disable_plugin.finish('操作已取消') + + +@disable_plugin.handle() +async def handle_first_receive(bot: Bot, event: MessageEvent, state: T_State): + args = event.get_plaintext().strip() + if args: + state['plugin_name'] = args + else: + plugin_list = get_loaded_plugins() + # 提示的时候仅显示有 matcher 的插件 + plugin_msg = '\n'.join([x.name for x in plugin_list if len(x.matcher) > 0]) + msg = f'当前已加载的插件有:\n\n{plugin_msg}' + await disable_plugin.send(msg) + + +@disable_plugin.got('plugin_name', prompt='请输入需要禁用的插件名称:') +async def handle_disable_plugin(bot: Bot, event: MessageEvent, state: T_State): + plugin_name = state['plugin_name'] + plugin_list = [x.name for x in get_loaded_plugins()] + if plugin_name not in plugin_list: + await disable_plugin.reject('没有这个插件, 请重新输入需要禁用的插件名称:\n取消操作请发送【取消】') + + result = await DBPlugin(plugin_name=plugin_name).update(enabled=0) + if result.success(): + logger.info(f'OPM | Success enabled plugin {plugin_name} by user {event.user_id}') + await disable_plugin.finish(f'禁用插件 {plugin_name} 成功') + else: + logger.error(f'OPM | Failed to enable {plugin_name}, {result.info}') + await disable_plugin.finish(f'禁用插件 {plugin_name} 失败, 详细信息请参见日志') + + +# 显示所有插件状态 +@list_plugins.handle() +async def handle_list_plugins(bot: Bot, event: MessageEvent, state: T_State): + # 只显示有 matcher 的插件信息 + plugin_list = [plugin.name for plugin in get_loaded_plugins() if len(plugin.matcher) > 0] + + # 获取插件启用状态 + enabled_plugins_result = await DBPlugin.list_plugins(enabled=1) + disabled_plugins_result = await DBPlugin.list_plugins(enabled=0) + + if enabled_plugins_result.error or disabled_plugins_result.error: + logger.error(f'OPM | Getting plugins info failed, ' + f'enabled result: {enabled_plugins_result}, disabled result: {disabled_plugins_result}') + await list_plugins.finish('获取插件信息失败, 详细信息请参见日志') + + enabled_plugins = '\n'.join(x for x in enabled_plugins_result.result if x in plugin_list) + disabled_plugins = '\n'.join(x for x in disabled_plugins_result.result if x in plugin_list) + + text = f'已启用的插件:\n{"="*12}\n{enabled_plugins}\n\n\n被禁用的插件:\n{"="*12}\n{disabled_plugins}' + text_img_result = await TextUtils(text=text).text_to_img() + + if text_img_result.error: + logger.error(f'OPM | Generate plugins list image failed, {text_img_result.info}') + await list_plugins.finish('生成插件信息失败, 详细信息请参见日志') + + img_seg = MessageSegment.image(file=pathlib.Path(text_img_result.result).as_uri()) + await list_plugins.finish(img_seg) diff --git a/omega_miya/plugins/omega_rate_limiting/__init__.py b/omega_miya/plugins/omega_rate_limiting/__init__.py new file mode 100644 index 00000000..821e3e24 --- /dev/null +++ b/omega_miya/plugins/omega_rate_limiting/__init__.py @@ -0,0 +1,296 @@ +""" +@Author : Ailitonia +@Date : 2021/09/24 22:03 +@FileName : __init__.py.py +@Project : nonebot2_miya +@Description : 速率限制及控制工具 +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +import re +from datetime import datetime, timedelta +from nonebot import logger +from nonebot.plugin.export import export +from nonebot.plugin import on_notice, CommandGroup +from nonebot.typing import T_State +from nonebot.rule import to_me +from nonebot.permission import SUPERUSER +from nonebot.adapters.cqhttp.permission import GROUP_ADMIN, GROUP_OWNER +from nonebot.adapters.cqhttp.bot import Bot +from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, GroupBanNoticeEvent +from omega_miya.database import DBBot, DBBotGroup, DBCoolDownEvent +from omega_miya.utils.omega_plugin_utils import init_export, MessageDecoder + + +GROUP_GLOBAL_CD_SETTING_NAME: str = 'group_global_cd' # 与 utils.omega_processor.rate_limiting 中配置保持一致 + + +# Custom plugin usage text +__plugin_custom_name__ = '流控限制' +__plugin_usage__ = r'''【Omega 流控限制插件】 +控制群组全局冷却及命令频率 + +以下命令均需要@机器人 +**Usage** +**SuperUser Only** +/设置全局冷却时间 +/删除全局冷却时间 +/移除全局冷却 +/Ban +/GBan +''' + + +# Init plugin export +init_export(export(), __plugin_custom_name__, __plugin_usage__) + + +# 注册事件响应器 +RateLimiting = CommandGroup( + 'RateLimiting', + rule=to_me(), + permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER, + priority=10, + block=True +) + +set_group_gcd = RateLimiting.command('set_gcd', aliases={'设置全局冷却时间'}) + + +# 修改默认参数处理 +@set_group_gcd.args_parser +async def parse(bot: Bot, event: MessageEvent, state: T_State): + args = str(event.get_message()).strip() + if not args: + await set_group_gcd.reject('你似乎没有发送有效的参数呢QAQ, 请重新发送:') + state[state["_current_key"]] = args + if state[state["_current_key"]] == '取消': + await set_group_gcd.finish('操作已取消') + + +@set_group_gcd.handle() +async def handle_first_receive(bot: Bot, event: MessageEvent, state: T_State): + if isinstance(event, GroupMessageEvent): + state['group_id'] = event.group_id + + +@set_group_gcd.got('group_id', prompt='请发送你要设置的群号:') +async def handle_group_id(bot: Bot, event: MessageEvent, state: T_State): + group_id = state['group_id'] + self_bot = DBBot(self_qq=int(bot.self_id)) + group = DBBotGroup(group_id=group_id, self_bot=self_bot) + if not re.match(r'^\d+$', str(group_id)) or not (await group.exist()): + await set_group_gcd.finish('群组不存在或群号不正确QAQ') + + +@set_group_gcd.got('time', prompt='请发送你要设置的冷却时间, 单位秒:') +async def handle_time(bot: Bot, event: MessageEvent, state: T_State): + group_id = state['group_id'] + time = state['time'] + if not re.match(r'^\d+$', str(time)): + await set_group_gcd.finish('时间格式错误QAQ, 应当为纯数字') + + self_bot = DBBot(self_qq=int(bot.self_id)) + group = DBBotGroup(group_id=group_id, self_bot=self_bot) + setting_result = await group.setting_set(setting_name=GROUP_GLOBAL_CD_SETTING_NAME, + main_config=time, + setting_info='限流配置群组全局冷却时间') + if setting_result.success(): + logger.info(f'已为群组: {group_id} 配置限流群组全局冷却时间: {time}') + await set_group_gcd.finish(f'已为群组: {group_id} 配置限流群组全局冷却时间: {time}') + else: + logger.error(f'为群组: {group_id} 配置限流群组全局冷却时间失败, error info: {setting_result.info}') + await set_group_gcd.finish(f'为群组: {group_id} 配置限流群组全局冷却时间失败了QAQ, 详情请见日志') + + +remove_group_gcd = RateLimiting.command('remove_gcd', aliases={'删除全局冷却时间'}) + + +@remove_group_gcd.args_parser +async def parse(bot: Bot, event: MessageEvent, state: T_State): + args = str(event.get_message()).strip() + if not args: + await remove_group_gcd.reject('你似乎没有发送有效的参数呢QAQ, 请重新发送:') + state[state["_current_key"]] = args + if state[state["_current_key"]] == '取消': + await remove_group_gcd.finish('操作已取消') + + +@remove_group_gcd.handle() +async def handle_first_receive(bot: Bot, event: MessageEvent, state: T_State): + if isinstance(event, GroupMessageEvent): + state['group_id'] = event.group_id + + +@remove_group_gcd.got('group_id', prompt='请发送你要配置的群号:') +async def handle_group_id(bot: Bot, event: MessageEvent, state: T_State): + group_id = state['group_id'] + self_bot = DBBot(self_qq=int(bot.self_id)) + group = DBBotGroup(group_id=group_id, self_bot=self_bot) + if not re.match(r'^\d+$', str(group_id)) or not (await group.exist()): + await remove_group_gcd.finish('群组不存在或群号不正确QAQ') + + setting_result = await group.setting_del(setting_name=GROUP_GLOBAL_CD_SETTING_NAME) + if setting_result.success(): + logger.info(f'已为群组: {group_id} 删除全局冷却时间') + await remove_group_gcd.finish(f'已删除了群组: {group_id} 的全局冷却时间!') + else: + logger.error(f'删除群组: {group_id} 的全局冷却时间失败, error info: {setting_result.info}') + await remove_group_gcd.finish(f'删除群组: {group_id} 的全局冷却时间失败失败了QAQ, 详情请见日志') + + +clear_group_exist_gcd = RateLimiting.command('clear_gcd', aliases={'移除全局冷却'}) + + +@clear_group_exist_gcd.args_parser +async def parse(bot: Bot, event: MessageEvent, state: T_State): + args = str(event.get_message()).strip() + if not args: + await clear_group_exist_gcd.reject('你似乎没有发送有效的参数呢QAQ, 请重新发送:') + state[state["_current_key"]] = args + if state[state["_current_key"]] == '取消': + await clear_group_exist_gcd.finish('操作已取消') + + +@clear_group_exist_gcd.handle() +async def handle_first_receive(bot: Bot, event: MessageEvent, state: T_State): + if isinstance(event, GroupMessageEvent): + state['group_id'] = event.group_id + + +@clear_group_exist_gcd.got('group_id', prompt='请发送你要移除全局冷却的群号:') +async def handle_first_receive(bot: Bot, event: MessageEvent, state: T_State): + group_id = state['group_id'] + self_bot = DBBot(self_qq=int(bot.self_id)) + group = DBBotGroup(group_id=group_id, self_bot=self_bot) + if not re.match(r'^\d+$', str(group_id)) or not (await group.exist()): + await remove_group_gcd.finish('群组不存在或群号不正确QAQ') + + result = await DBCoolDownEvent.add_global_group_cool_down_event( + group_id=group_id, + stop_at=datetime.now(), + description='Remove Group Global CoolDown') + + if result.success(): + logger.success(f'已移除群组: {group_id} 全局冷却') + else: + logger.error(f'移除群组: {group_id} 全局冷却失败, error: {result.info}') + + +ban_group = RateLimiting.command('ban_group', aliases={'GBan', 'GroupBan', 'gban'}) + + +# 修改默认参数处理 +@ban_group.args_parser +async def parse(bot: Bot, event: MessageEvent, state: T_State): + args = str(event.get_message()).strip() + if not args: + await ban_group.reject('你似乎没有发送有效的参数呢QAQ, 请重新发送:') + state[state["_current_key"]] = args + if state[state["_current_key"]] == '取消': + await ban_group.finish('操作已取消') + + +@ban_group.handle() +async def handle_first_receive(bot: Bot, event: MessageEvent, state: T_State): + if isinstance(event, GroupMessageEvent): + state['group_id'] = event.group_id + + +@ban_group.got('group_id', prompt='请发送你要封禁的群号:') +async def handle_group_id(bot: Bot, event: MessageEvent, state: T_State): + group_id = state['group_id'] + if not re.match(r'^\d+$', str(group_id)): + await ban_group.finish('群号格式不正确QAQ') + + +@ban_group.got('time', prompt='请发送需要封禁的时间, 单位秒:') +async def handle_time(bot: Bot, event: MessageEvent, state: T_State): + group_id = state['group_id'] + time = state['time'] + if not re.match(r'^\d+$', str(time)): + await ban_group.finish('时间格式错误QAQ, 应当为纯数字') + + result = await DBCoolDownEvent.add_global_group_cool_down_event( + group_id=group_id, + stop_at=datetime.now() + timedelta(seconds=int(time)), + description='Ban Group CoolDown') + + if result.success(): + logger.info(f'已封禁群组: {group_id}, {time} 秒') + await ban_group.finish(f'已封禁群组: {group_id}, {time} 秒') + else: + logger.error(f'封禁群组: {group_id} 失败, error info: {result.info}') + await ban_group.finish(f'封禁群组: {group_id} 失败了QAQ, 详情请见日志') + + +ban_user = RateLimiting.command('ban_user', aliases={'Ban', 'ban'}) + + +# 修改默认参数处理 +@ban_user.args_parser +async def parse(bot: Bot, event: MessageEvent, state: T_State): + args = str(event.get_message()).strip() + if not args: + await ban_user.reject('你似乎没有发送有效的参数呢QAQ, 请重新发送:') + state[state["_current_key"]] = args + if state[state["_current_key"]] == '取消': + await ban_user.finish('操作已取消') + + +@ban_user.handle() +async def handle_first_receive(bot: Bot, event: MessageEvent, state: T_State): + # 处理@人 + at_qq_list = MessageDecoder(message=event.message).get_all_at_qq() + if at_qq_list: + # 取at列表中第一个 + state['user_id'] = at_qq_list[0] + + +@ban_user.got('user_id', prompt='请发送你要封禁的用户qq:') +async def handle_group_id(bot: Bot, event: MessageEvent, state: T_State): + user_id = state['user_id'] + if not re.match(r'^\d+$', str(user_id)): + await ban_user.finish('qq号格式不正确QAQ') + + +@ban_user.got('time', prompt='请发送需要封禁的时间, 单位秒:') +async def handle_time(bot: Bot, event: MessageEvent, state: T_State): + user_id = state['user_id'] + time = state['time'] + if not re.match(r'^\d+$', str(time)): + await ban_user.finish('时间格式错误QAQ, 应当为纯数字') + + result = await DBCoolDownEvent.add_global_user_cool_down_event( + user_id=user_id, + stop_at=datetime.now() + timedelta(seconds=int(time)), + description='Ban User CoolDown') + + if result.success(): + logger.info(f'已封禁用户: {user_id}, {time} 秒') + await ban_user.finish(f'已封禁用户: {user_id}, {time} 秒') + else: + logger.error(f'封禁用户: {user_id} 失败, error info: {result.info}') + await ban_user.finish(f'封禁用户: {user_id} 失败了QAQ, 详情请见日志') + + +# 注册事件响应器, 被禁言时触发 +group_ban_cd = on_notice(priority=100, rule=to_me(), block=False) + + +@group_ban_cd.handle() +async def handle_group_ban_cd(bot: Bot, event: GroupBanNoticeEvent, state: T_State): + group_id = event.group_id + operator_id = event.operator_id + duration = event.duration + + result = await DBCoolDownEvent.add_global_group_cool_down_event( + group_id=group_id, + stop_at=datetime.now() + timedelta(seconds=(duration * 10)), + description=f'Group Ban CoolDown for 10 times duration: {duration}') + + if result.success(): + logger.success(f'被群组/管理员: {group_id}/{operator_id} 禁言, 时间 {duration} 秒, 已设置全局冷却') + else: + logger.error(f'被群组/管理员: {group_id}/{operator_id} 禁言, 时间 {duration} 秒, 设置全局冷却失败: {result.info}') diff --git a/omega_miya/plugins/Omega_recaller/__init__.py b/omega_miya/plugins/omega_recaller/__init__.py similarity index 62% rename from omega_miya/plugins/Omega_recaller/__init__.py rename to omega_miya/plugins/omega_recaller/__init__.py index edef843d..e0d5baad 100644 --- a/omega_miya/plugins/Omega_recaller/__init__.py +++ b/omega_miya/plugins/omega_recaller/__init__.py @@ -1,7 +1,7 @@ """ @Author : Ailitonia @Date : 2021/07/17 22:36 -@FileName : Omega_recaller.py +@FileName : omega_recaller.py @Project : nonebot2_miya @Description : 自助撤回群内消息 需bot为管理员 @GitHub : https://github.com/Ailitonia @@ -10,7 +10,8 @@ from typing import Dict from datetime import datetime -from nonebot import export, logger +from nonebot import logger +from nonebot.plugin.export import export from nonebot.plugin import CommandGroup from nonebot.typing import T_State from nonebot.exception import FinishedException @@ -19,13 +20,13 @@ from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import GroupMessageEvent from nonebot.adapters.cqhttp.message import Message, MessageSegment -from omega_miya.utils.Omega_Base import DBBot, DBBotGroup, DBAuth, DBHistory -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state, PermissionChecker +from omega_miya.database import DBBot, DBBotGroup, DBAuth, DBHistory +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state, PermissionChecker, MessageDecoder # Custom plugin usage text __plugin_raw_name__ = __name__.split('.')[-1] -__plugin_name__ = '自助撤回' +__plugin_custom_name__ = '自助撤回' __plugin_usage__ = r'''【自助撤回】 让非管理员自助撤回群消息 Bot得是管理员才行 @@ -45,30 +46,27 @@ /禁用撤回 [@用户] ''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) + # 存放bot在群组的身份 BOT_ROLE: Dict[int, str] = {} - # 存放bot群组信息过期时间 BOT_ROLE_EXPIRED: Dict[int, datetime] = {} +# 缓存的bot群组信息过期时间, 单位秒, 默认为 1800 秒 (半小时) +BOT_ROLE_EXPIRED_TIME: int = 1800 # 注册事件响应器 SelfHelpRecall = CommandGroup( 'SelfHelpRecall', # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='search_anime', command=True, - level=10, - auth_node='basic'), + level=10), permission=SUPERUSER | GROUP, priority=10, block=True @@ -117,8 +115,8 @@ async def handle_first_receive(bot: Bot, event: GroupMessageEvent, state: T_Stat if not bot_role_expired: bot_role_expired = datetime.now() BOT_ROLE_EXPIRED.update({event.group_id: bot_role_expired}) - # 默认过期时间为 21600 秒 (6 小时) - is_role_expired = (datetime.now() - bot_role_expired).total_seconds() > 21600 + # 缓存身份过期时间 + is_role_expired = (datetime.now() - bot_role_expired).total_seconds() > BOT_ROLE_EXPIRED_TIME if is_role_expired or not bot_role: bot_role = (await bot.get_group_member_info(group_id=event.group_id, user_id=event.self_id)).get('role') BOT_ROLE.update({event.group_id: bot_role}) @@ -184,30 +182,40 @@ async def handle_first_receive(bot: Bot, event: GroupMessageEvent, state: T_Stat else: await recall_allow.finish('参数错误QAQ, 请在 “/启用撤回” 命令后直接@对应用户') - # 处理@人 qq在at别人时后面会自动加空格 - if len(event.message) in [1, 2]: - if event.message[0].type == 'at': - at_qq = event.message[0].data.get('qq') - if at_qq: - self_bot = DBBot(self_qq=event.self_id) - group = DBBotGroup(group_id=event.group_id, self_bot=self_bot) - group_exist = await group.exist() - if not group_exist: - logger.error(f'Self-help Recall | 启用用户撤回失败, 数据库没有对应群组: {event.group_id}') - await recall_allow.finish('发生了意外的错误QAQ, 请联系管理员处理') - - auth_node = DBAuth(self_bot=self_bot, auth_id=event.group_id, auth_type='group', - auth_node=f'{__plugin_raw_name__}.basic.{at_qq}') - result = await auth_node.set(allow_tag=1, deny_tag=0, auth_info='启用自助撤回') - if result.success(): - logger.info(f'Self-help Recall | {event.group_id}/{event.user_id} 已启用用户 {at_qq} 撤回权限') - await recall_allow.finish(f'已启用用户{at_qq}撤回权限') - else: - logger.error(f'Self-help Recall | {event.group_id}/{event.user_id} 启用用户 {at_qq} 撤回权限失败, ' - f'error: {result.info}') - await recall_allow.finish(f'启用用户撤回权限失败QAQ, 请联系管理员处理') - - await recall_allow.finish('没有指定用户QAQ, 请在 “/启用撤回” 命令后直接@对应用户') + # 检查群组 + self_bot = DBBot(self_qq=event.self_id) + group = DBBotGroup(group_id=event.group_id, self_bot=self_bot) + group_exist = await group.exist() + if not group_exist: + logger.error(f'Self-help Recall | 启用用户撤回失败, 数据库没有对应群组: {event.group_id}') + await recall_allow.finish('发生了意外的错误QAQ, 请联系管理员处理') + + # 处理@人 + at_qq_list = MessageDecoder(message=event.message).get_all_at_qq() + if not at_qq_list: + await recall_allow.finish('没有指定用户QAQ, 请在 “/启用撤回” 命令后直接@对应用户') + + success_list = [] + failed_list = [] + for at_qq in at_qq_list: + auth_node = DBAuth(self_bot=self_bot, auth_id=event.group_id, auth_type='group', + auth_node=f'{__plugin_raw_name__}.basic.{at_qq}') + result = await auth_node.set(allow_tag=1, deny_tag=0, auth_info='启用自助撤回') + if result.success(): + success_list.append(str(at_qq)) + else: + logger.error(f'Self-help Recall | {event.group_id}/{event.user_id} 启用用户 {at_qq} 撤回权限失败, ' + f'error: {result.info}') + failed_list.append(str(at_qq)) + + if not failed_list: + logger.info(f'Self-help Recall | {event.group_id}/{event.user_id} 已启用用户 {", ".join(success_list)} 撤回权限') + await recall_allow.finish(f'已启用用户 {", ".join(success_list)} 撤回权限') + else: + logger.warning(f'Self-help Recall | {event.group_id}/{event.user_id} ' + f'已启用用户 {", ".join(success_list)} 撤回权限, 配置用户 {", ".join(failed_list)} 权限失败') + await recall_allow.finish(f'启用用户 {", ".join(success_list)} 撤回权限成功, ' + f'{", ".join(failed_list)} 失败QAQ, 请联系管理员处理') recall_deny = SelfHelpRecall.command( @@ -222,27 +230,37 @@ async def handle_first_receive(bot: Bot, event: GroupMessageEvent, state: T_Stat else: await recall_deny.finish('参数错误QAQ, 请在 “/禁用撤回” 命令后直接@对应用户') - # 处理@人 qq在at别人时后面会自动加空格 - if len(event.message) in [1, 2]: - if event.message[0].type == 'at': - at_qq = event.message[0].data.get('qq') - if at_qq: - self_bot = DBBot(self_qq=event.self_id) - group = DBBotGroup(group_id=event.group_id, self_bot=self_bot) - group_exist = await group.exist() - if not group_exist: - logger.error(f'Self-help Recall | 禁用用户撤回失败, 数据库没有对应群组: {event.group_id}') - await recall_deny.finish('发生了意外的错误QAQ, 请联系管理员处理') - - auth_node = DBAuth(self_bot=self_bot, auth_id=event.group_id, auth_type='group', - auth_node=f'{__plugin_raw_name__}.basic.{at_qq}') - result = await auth_node.set(allow_tag=0, deny_tag=1, auth_info='禁用自助撤回') - if result.success(): - logger.info(f'Self-help Recall | {event.group_id}/{event.user_id} 已禁用用户 {at_qq} 撤回权限') - await recall_deny.finish(f'已禁用用户{at_qq}撤回权限') - else: - logger.error(f'Self-help Recall | {event.group_id}/{event.user_id} 禁用用户 {at_qq} 撤回权限失败, ' - f'error: {result.info}') - await recall_deny.finish(f'禁用用户撤回权限失败QAQ, 请联系管理员处理') - - await recall_deny.finish('没有指定用户QAQ, 请在 “/禁用撤回” 命令后直接@对应用户') + # 检查群组 + self_bot = DBBot(self_qq=event.self_id) + group = DBBotGroup(group_id=event.group_id, self_bot=self_bot) + group_exist = await group.exist() + if not group_exist: + logger.error(f'Self-help Recall | 禁用用户撤回失败, 数据库没有对应群组: {event.group_id}') + await recall_deny.finish('发生了意外的错误QAQ, 请联系管理员处理') + + # 处理@人 + at_qq_list = MessageDecoder(message=event.message).get_all_at_qq() + if not at_qq_list: + await recall_deny.finish('没有指定用户QAQ, 请在 “/禁用撤回” 命令后直接@对应用户') + + success_list = [] + failed_list = [] + for at_qq in at_qq_list: + auth_node = DBAuth(self_bot=self_bot, auth_id=event.group_id, auth_type='group', + auth_node=f'{__plugin_raw_name__}.basic.{at_qq}') + result = await auth_node.set(allow_tag=0, deny_tag=1, auth_info='禁用自助撤回') + if result.success(): + success_list.append(str(at_qq)) + else: + logger.error(f'Self-help Recall | {event.group_id}/{event.user_id} 禁用用户 {at_qq} 撤回权限失败, ' + f'error: {result.info}') + failed_list.append(str(at_qq)) + + if not failed_list: + logger.info(f'Self-help Recall | {event.group_id}/{event.user_id} 已禁用用户 {", ".join(success_list)} 撤回权限') + await recall_deny.finish(f'已禁用用户 {", ".join(success_list)} 撤回权限') + else: + logger.warning(f'Self-help Recall | {event.group_id}/{event.user_id} ' + f'已禁用用户 {", ".join(success_list)} 撤回权限, 配置用户 {", ".join(failed_list)} 权限失败') + await recall_deny.finish(f'禁用用户 {", ".join(success_list)} 撤回权限成功, ' + f'{", ".join(failed_list)} 失败QAQ, 请联系管理员处理') diff --git a/omega_miya/plugins/omega_sign_in/__init__.py b/omega_miya/plugins/omega_sign_in/__init__.py new file mode 100644 index 00000000..d699af92 --- /dev/null +++ b/omega_miya/plugins/omega_sign_in/__init__.py @@ -0,0 +1,305 @@ +""" +@Author : Ailitonia +@Date : 2021/07/17 1:29 +@FileName : __init__.py.py +@Project : nonebot2_miya +@Description : 轻量化签到插件 +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +import random +import pathlib +from typing import Union +from nonebot import MatcherGroup, on_notice, logger, get_driver +from nonebot.plugin.export import export +from nonebot.typing import T_State +from nonebot.rule import to_me +from nonebot.adapters.cqhttp.bot import Bot +from nonebot.adapters.cqhttp.event import GroupMessageEvent, PokeNotifyEvent +from nonebot.adapters.cqhttp.permission import GROUP +from nonebot.adapters.cqhttp.message import Message, MessageSegment +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state, OmegaRules +from omega_miya.database import DBUser +from .config import Config +from .utils import scheduler, get_hitokoto, generate_sign_in_card + + +__global_config = get_driver().config +plugin_config = Config(**__global_config.dict()) +ENABLE_REGEX_MATCHER = plugin_config.enable_regex_matcher +FAVORABILITY_ALIAS = plugin_config.favorability_alias +ENERGY_ALIAS = plugin_config.energy_alias +CURRENCY_ALIAS = plugin_config.currency_alias +EF_EXCHANGE_RATE = plugin_config.ef_exchange_rate + + +class SignInException(Exception): + pass + + +class DuplicateException(SignInException): + pass + + +class FailedException(SignInException): + pass + + +# Custom plugin usage text +__plugin_custom_name__ = '签到' +__plugin_usage__ = r'''【Omega 签到插件】 +轻量化签到插件 +好感度系统基础支持 +仅限群聊使用 + +**Permission** +Command & Lv.20 +or AuthNode + +**AuthNode** +basic + +**Usage** +/签到 +/今日运势(今日人品) +/好感度(我的好感) +/一言 + +可使用戳一戳触发''' + + +# Init plugin export +init_export(export(), __plugin_custom_name__, __plugin_usage__) + + +SignIn = MatcherGroup( + type='message', + # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 + state=init_processor_state( + name='sign_in', + command=True, + level=20), + permission=GROUP, + priority=20, + block=True) + + +command_sign_in = SignIn.on_command('sign_in', aliases={'签到'}) +command_fortune_today = SignIn.on_command('fortune_today', aliases={'今日运势', '今日人品', '一言', '好感度', '我的好感'}) + + +poke_sign_in = on_notice( + rule=to_me() & OmegaRules.has_group_command_permission() & OmegaRules.has_level_or_node(20, 'omega_sign_in.basic'), + priority=50, + block=False +) + + +@poke_sign_in.handle() +async def handle_command_sign_in(bot: Bot, event: PokeNotifyEvent, state: T_State): + # 获取戳一戳用户身份 + sender_ = await bot.get_group_member_info(group_id=event.group_id, user_id=event.user_id) + sender = { + 'user_id': event.user_id, + 'nickname': sender_.get('nickname', ''), + 'sex': sender_.get('sex', ''), + 'age': sender_.get('age', 0), + 'card': sender_.get('card', ''), + 'area': sender_.get('area', ''), + 'level': sender_.get('level', ''), + 'role': sender_.get('role', ''), + 'title': sender_.get('title', '') + } + # 从 PokeNotifyEvent 构造一个 GroupMessageEvent + event_ = GroupMessageEvent(**{ + 'time': event.time, + 'self_id': event.self_id, + 'post_type': 'message', + 'sub_type': 'normal', + 'user_id': event.user_id, + 'group_id': event.group_id, + 'message_type': 'group', + 'message_id': hash(repr(event)), + 'message': Message('签到'), + 'raw_message': '签到', + 'font': 0, + 'sender': sender + }) + user = DBUser(user_id=event.user_id) + # 先检查签到状态 + check_result = await user.sign_in_check_today() + if check_result.result == 1: + # 已签到 + # 设置一个状态指示生成卡片中添加文字 + state.update({'_checked_sign_in_text': '今天你已经签到过了哦~'}) + msg = await handle_fortune(bot=bot, event=event_, state=state) + logger.info(f'{event.group_id}/{event.user_id}, 重复签到, 通过戳一戳获取了运势卡片') + await poke_sign_in.finish(msg) + else: + # 未签到及异常交由签到函数处理 + msg = await handle_sign_in(bot=bot, event=event_, state=state) + logger.info(f'{event.group_id}/{event.user_id}, 通过戳一戳进行了签到') + await poke_sign_in.finish(msg) + + +@command_sign_in.handle() +async def handle_command_sign_in(bot: Bot, event: GroupMessageEvent, state: T_State): + msg = await handle_sign_in(bot=bot, event=event, state=state) + await command_sign_in.finish(msg) + + +@command_fortune_today.handle() +async def handle_command_fortune_today(bot: Bot, event: GroupMessageEvent, state: T_State): + msg = await handle_fortune(bot=bot, event=event, state=state) + await command_fortune_today.finish(msg) + + +if ENABLE_REGEX_MATCHER: + regex_sign_in = SignIn.on_regex(r'^签到$') + regex_fortune_today = SignIn.on_regex(r'^(今日(运势|人品)|一言|好感度|我的好感)$') + + @regex_sign_in.handle() + async def handle_regex_sign_in(bot: Bot, event: GroupMessageEvent, state: T_State): + msg = await handle_sign_in(bot=bot, event=event, state=state) + await regex_sign_in.finish(msg) + + @regex_fortune_today.handle() + async def handle_regex_fortune_today(bot: Bot, event: GroupMessageEvent, state: T_State): + msg = await handle_fortune(bot=bot, event=event, state=state) + await regex_fortune_today.finish(msg) + + +async def handle_sign_in(bot: Bot, event: GroupMessageEvent, state: T_State) -> Union[Message, MessageSegment, str]: + user = DBUser(user_id=event.user_id) + try: + # 获取当前好感度信息 + favorability_status_result = await user.favorability_status() + if favorability_status_result.error and favorability_status_result.info == 'NoResultFound': + # 没有好感度记录的要重置 + reset_favorability_result = await user.favorability_reset() + if reset_favorability_result.error: + raise FailedException(f'Sign-in add User {event.user_id} Failed, ' + f'init user favorability status failed, {reset_favorability_result.info}') + favorability_status_result = await user.favorability_status() + elif favorability_status_result.error and favorability_status_result.info == 'User not exist': + # 没有用户的要先新增用户 + user_add_result = await user.add(nickname=event.sender.nickname) + if user_add_result.error: + raise FailedException(f'Sign-in add User {event.user_id} Failed, ' + f'add user to database failed, {user_add_result.info}') + # 新增了用户后同样要重置好感度记录 + reset_favorability_result = await user.favorability_reset() + if reset_favorability_result.error: + raise FailedException(f'Sign-in add User {event.user_id} Failed, ' + f'init user favorability status failed, {reset_favorability_result.info}') + favorability_status_result = await user.favorability_status() + if favorability_status_result.error: + raise FailedException(f'获取好感度信息失败, {favorability_status_result}') + status, mood, favorability, energy, currency, response_threshold = favorability_status_result.result + + # 尝试签到 + sign_in_result = await user.sign_in() + if sign_in_result.error: + raise FailedException(f'签到失败, {sign_in_result.info}') + elif sign_in_result.result == 1: + raise DuplicateException('重复签到') + + # 查询连续签到时间 + sign_in_c_d_result = await user.sign_in_continuous_days() + if sign_in_c_d_result.error: + raise FailedException(f'查询连续签到时间失败, {sign_in_c_d_result.info}') + continuous_days = sign_in_c_d_result.result + + # 尝试为用户增加好感度 + # 根据连签日期设置不同增幅 + if continuous_days < 7: + favorability_inc_ = int(10 * (1 + random.gauss(0.25, 0.25))) + currency_inc = 1 + elif continuous_days < 30: + favorability_inc_ = int(30 * (1 + random.gauss(0.35, 0.2))) + currency_inc = 3 + else: + favorability_inc_ = int(50 * (1 + random.gauss(0.45, 0.15))) + currency_inc = 5 + # 将能量值兑换为好感度 + favorability_inc = energy * EF_EXCHANGE_RATE + favorability_inc_ + # 增加后的好感度及硬币 + favorability_ = favorability + favorability_inc + currency_ = currency + currency_inc + + favorability_result = await user.favorability_add(favorability=favorability_inc, currency=currency_inc, + energy=(- energy)) + if favorability_result.error: + raise FailedException(f'增加好感度失败, {favorability_result.info}') + + nick_name = event.sender.card if event.sender.card else event.sender.nickname + user_text = f'@{nick_name} {FAVORABILITY_ALIAS}+{int(favorability_inc_)} ' \ + f'{CURRENCY_ALIAS}+{int(currency_inc)}\n' \ + f'已连续签到{continuous_days}天\n' \ + f'已将{int(energy)}{ENERGY_ALIAS}兑换为{int(energy * EF_EXCHANGE_RATE)}{FAVORABILITY_ALIAS}\n' \ + f'当前{FAVORABILITY_ALIAS}: {int(favorability_)}\n' \ + f'当前{CURRENCY_ALIAS}: {int(currency_)}' + + sign_in_card_result = await generate_sign_in_card(user_id=event.user_id, user_text=user_text, fav=favorability_) + if sign_in_card_result.error: + raise FailedException(f'生成签到卡片失败, {sign_in_card_result.info}') + + msg = MessageSegment.image(pathlib.Path(sign_in_card_result.result).as_uri()) + logger.info(f'{event.group_id}/{event.user_id} 签到成功') + return msg + except DuplicateException as e: + # 已签到 + # 设置一个状态指示生成卡片中添加文字 + state.update({'_checked_sign_in_text': '今天你已经签到过了哦~'}) + msg = await handle_fortune(bot=bot, event=event, state=state) + logger.info(f'{event.group_id}/{event.user_id} 重复签到, 生成运势卡片, {str(e)}') + return msg + except FailedException as e: + msg = Message(MessageSegment.at(event.user_id)).append('签到失败了QAQ, 请稍后再试或联系管理员处理') + logger.error(f'{event.group_id}/{event.user_id} 签到失败, {str(e)}') + return msg + except Exception as e: + msg = Message(MessageSegment.at(event.user_id)).append('签到失败了QAQ, 请稍后再试或联系管理员处理') + logger.error(f'{event.group_id}/{event.user_id} 签到失败, 发生了预期外的错误, {str(e)}') + return msg + + +async def handle_fortune(bot: Bot, event: GroupMessageEvent, state: T_State) -> Union[Message, MessageSegment, str]: + user = DBUser(user_id=event.user_id) + try: + # 获取当前好感度信息 + favorability_status_result = await user.favorability_status() + if favorability_status_result.error or not favorability_status_result.result: + logger.info(f'{event.group_id}/{event.user_id} 尚未签到或无好感度信息, {favorability_status_result}') + status, mood, favorability, energy, currency, response_threshold = ('', 0, 0, 0, 0, 0) + else: + status, mood, favorability, energy, currency, response_threshold = favorability_status_result.result + + nick_name = event.sender.card if event.sender.card else event.sender.nickname + + # 获取一言 + hitokoto_result = await get_hitokoto() + if hitokoto_result.error: + raise FailedException(f'获取一言失败, {hitokoto_result}') + + # 插入poke签到特殊文本 + pock_text = state.get('_checked_sign_in_text', None) + user_line = f'@{nick_name}\n' if not pock_text else f'@{nick_name} {pock_text}\n' + user_text = f'{hitokoto_result.result}\n\n' \ + f'{user_line}' \ + f'当前{FAVORABILITY_ALIAS}: {int(favorability)}\n' \ + f'当前{CURRENCY_ALIAS}: {int(currency)}' + + sign_in_card_result = await generate_sign_in_card( + user_id=event.user_id, user_text=user_text, fav=favorability, fortune_do=False) + if sign_in_card_result.error: + raise FailedException(f'生成运势卡片失败, {sign_in_card_result.info}') + + msg = MessageSegment.image(pathlib.Path(sign_in_card_result.result).as_uri()) + logger.info(f'{event.group_id}/{event.user_id} 获取运势卡片成功') + return msg + except Exception as e: + msg = Message(MessageSegment.at(event.user_id)).append('获取今日运势失败了QAQ, 请稍后再试或联系管理员处理') + logger.error(f'{event.group_id}/{event.user_id} 获取运势卡片失败, 发生了预期外的错误, {str(e)}') + return msg diff --git a/omega_miya/plugins/omega_sign_in/config.py b/omega_miya/plugins/omega_sign_in/config.py new file mode 100644 index 00000000..15684dfe --- /dev/null +++ b/omega_miya/plugins/omega_sign_in/config.py @@ -0,0 +1,34 @@ +""" +@Author : Ailitonia +@Date : 2021/07/17 2:04 +@FileName : config.py +@Project : nonebot2_miya +@Description : +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +from pydantic import BaseSettings + + +class Config(BaseSettings): + # 是否启用正则匹配matcher + # 如果 bot 配置了命令前缀, 但需要额外响应无前缀的 "签到" 等消息, 请将本选项设置为 True + # 如果 bot 没有配置命令前缀或空白前缀, 请将本选项设置为 False, 避免重复响应 + enable_regex_matcher: bool = True + + # 是否启用自动下载签到头图的定时任务 + enable_pic_preparing_scheduler: bool = True + # 缓存的签到头图的数量限制 + cache_pic_limit: int = 2000 + + # 相关数值显示命令 + favorability_alias: str = '好感度' + energy_alias: str = '能量值' + currency_alias: str = '硬币' + + # 能量值与好感度的兑换比例 公式为(能量值 * 兑换比 = 好感度) + ef_exchange_rate: float = 0.25 + + class Config: + extra = "ignore" diff --git a/omega_miya/plugins/omega_sign_in/fortune.py b/omega_miya/plugins/omega_sign_in/fortune.py new file mode 100644 index 00000000..f113d738 --- /dev/null +++ b/omega_miya/plugins/omega_sign_in/fortune.py @@ -0,0 +1,176 @@ +import datetime +import random +import hashlib + + +do_something = [ + {'name': '看直播', 'good': '喜欢的V开歌回啦', 'bad': '喜欢的V咕了一整天'}, + {'name': '打轴', 'good': '一次性过', 'bad': '"谁说话这么难懂"'}, + {'name': '剪辑', 'good': '灵感爆发', 'bad': '一团乱麻'}, + {'name': '校对', 'good': '变成无情的审轴机器', 'bad': '被闪轴闪瞎眼'}, + {'name': '浏览Pixiv', 'good': '发现符合xp的涩图', 'bad': '找不到想要的涩图'}, + {'name': '打SC', 'good': '享受石油佬的乐趣', 'bad': '吃土中'}, + {'name': '吃人', 'good': '你面前这位有成为神龙的潜质', 'bad': '这人会用Aegisub吗?'}, + {'name': '背单词', 'good': '这次六级肯定过', 'bad': '背完50个忘了45个'}, + {'name': '翘课', 'good': '老师不会点名', 'bad': '老师准会抽到你来回答问题'}, + {'name': '做作业', 'good': '做的每个都对', 'bad': '做一个做错一个'}, + {'name': '锻炼一下身体', 'good': '身体健康, 更加性福', 'bad': '能量没消耗多少, 吃得却更多'}, + {'name': '浏览成人网站', 'good': '重拾对生活的信心', 'bad': '你会心神不宁'}, + {'name': '修复BUG', 'good': '你今天对BUG的嗅觉大大提高', 'bad': '新产生的BUG将比修复的更多'}, + {'name': '上AB站', 'good': '还需要理由吗?', 'bad': '满屏兄贵亮瞎你的眼'}, + {'name': '打LOL', 'good': '你将有如神助', 'bad': '你会被虐的很惨'}, + {'name': '打DOTA', 'good': '天梯5000分不是梦', 'bad': '你会遇到猪一样的队友'}, + {'name': '打DOTA2', 'good': 'Godlike', 'bad': '不怕神一样的对手,就怕猪一样的队友'}, + {'name': '穿女装', 'good': '你会得到很多炙热的目光', 'bad': '被父母看到'}, + {'name': '组模型', 'good': '今天的喷漆会很完美', 'bad': '精神不集中板件被剪断了'}, + {'name': '熬夜', 'good': '夜间的效率更高', 'bad': '明天有很重要的事'}, + {'name': '抚摸猫咪', 'good': '才不是特意蹭你的呢', 'bad': '死开! 愚蠢的人类'}, + {'name': '烹饪', 'good': '黑暗料理界就由我来打败', 'bad': '难道这就是……仰望星空派?'}, + {'name': '告白', 'good': '其实我也喜欢你好久了', 'bad': '对不起, 你是一个好人'}, + {'name': '追新番', 'good': '完结之前我绝不会死', 'bad': '会被剧透'}, + {'name': '日麻', 'good': '立直一发自摸!', 'bad': '碰喵吃喵杠喵荣喵!'}, + {'name': '音游', 'good': 'FCACFRPR不过如此', 'bad': '又双叒叕LOST了...'}, + {'name': '向大佬请教', 'good': '太棒了,学到许多', 'bad': '太棒了,什么都没学到'}, + {'name': '早起', 'good': '迎接第一缕阳光', 'bad': '才4点,再睡一会'}, + {'name': '早睡', 'good': '第二天精神饱满', 'bad': '失眠数羊画圈圈'}, + {'name': '入正版游戏', 'good': '买了痛三天,不买悔三年', 'bad': 'emmmm,汇率还是……'}, + {'name': '补旧作', 'good': '意外地对胃口', 'bad': '会踩雷'}, + {'name': '晾晒老婆(抱枕套)', 'good': '天気も晴れココロも晴れ', 'bad': '引发路人围观'}, + {'name': '不按攻略打', 'good': '居然是HAPPY END', 'bad': '碰到BAD END'}, + {'name': '观赏CG包', 'good': '社保。', 'bad': '还不去如看游戏剧情'}, + {'name': '研究黄油创作理论', 'good': '增进鉴赏水平', 'bad': '闲适玩家不需要这些'}, + {'name': '暴露性癖', 'good': '会引来很多趣味相同的变态', 'bad': '四斋蒸鹅心'}, + {'name': '施法', 'good': '传统手艺精进了', 'bad': '房间门关好了吗'}, + {'name': '刷新作动态', 'good': '喜欢的画师发了新图', 'bad': '发现游戏跳票'}, + {'name': '回味玩过的作品', 'good': '重温感动', 'bad': '还是先看看新作'}, + {'name': '出门走走', 'good': '宅久了要发霉', 'bad': '太陽が眩しすぎる'}, + {'name': '思考人生', 'good': '自己的幸福呢?', 'bad': '喵喵……喵?'}, + {'name': '撸猫', 'good': '啊……好爽', 'bad': '家里没有猫的洗洗睡吧'}, + {'name': '抽卡', 'good': '单抽出货', 'bad': '到井前一发出货'}, + {'name': '拼乐高', 'good': '顺利完工', 'bad': '发现少了一块零件'}, + {'name': '跳槽', 'good': '新工作待遇大幅提升', 'bad': '待遇还不如之前的'}, + {'name': '和女神聊天', 'good': '今天天气不错', 'bad': '我去洗澡了,呵呵'}, + {'name': '写开源库', 'good': '今天北斗七星汇聚,裤子造的又快又好', 'bad': '写好会发现github上已经有了更好的'}, + {'name': '给测试妹子埋个bug', 'good': '下辈子的幸福就靠这个bug了', 'bad': '妹子会认为你活和代码一样差'}, + {'name': '写单元测试', 'good': '写单元测试将减少出错', 'bad': '写单元测试会降低你的开发效率'}, + {'name': '洗澡', 'good': '你几天没洗澡了?', 'bad': '会把设计方面的灵感洗掉'}, + {'name': '白天上线', 'good': '今天白天上线是安全的', 'bad': '可能导致灾难性后果'}, + {'name': '重构', 'good': '代码质量得到提高', 'bad': '你很有可能会陷入泥潭'}, + {'name': '招人', 'good': '你面前这位有成为牛人的潜质', 'bad': '这人会写程序吗?'}, + {'name': '面试', 'good': '面试官今天心情很好', 'bad': '面试官不爽,会拿你出气'}, + {'name': '申请加薪', 'good': '老板今天心情很好', 'bad': '公司正在考虑裁员'}, + {'name': '提交代码', 'good': '遇到冲突的几率是最低的', 'bad': '会遇到的一大堆冲突'}, + {'name': '代码复审', 'good': '发现重要问题的几率大大增加', 'bad': '你什么问题都发现不了,白白浪费时间'}, + {'name': '晚上上线', 'good': '晚上是程序员精神最好的时候', 'bad': '你白天已经筋疲力尽了'}, + {'name': '乘电梯', 'good': '正好赶上打卡截止时间', 'bad': '电梯超载'}, + {'name': '复读', 'good': '有时候,人云亦云也是一种生存方式', 'bad': '你的对手是鸽子'}, + {'name': '肝爆', 'good': '努力使人进步,肝爆让人快乐', 'bad': '醒醒,限时活动没了'}, + {'name': '氪金', 'good': '早买早享受,晚买哭着求', 'bad': '第二天就 50% off'}, + {'name': '卖弱', 'good': '楚楚动人更容易打动群友', 'bad': 'Boy♂next♂door'}, + {'name': '唱脑力', 'good': '唱一次提神醒脑,唱两次精神百倍', 'bad': '会与复读机一起对群聊造成毁灭性打击'}, + {'name': '看手元', 'good': '从手元中获得一点音游经验', 'bad': '会被大佬闪瞎'}, + {'name': '录手元', 'good': '音游届的未来新星UP主就是你', 'bad': '打完歌才发现忘记开录像'}, + {'name': '挑战魔王曲', 'good': '一上来就是一个新纪录', 'bad': '有这点时间还不如干点别的'}, + {'name': '咕咕咕', 'good': '一时咕一时爽', 'bad': '会被抓起来,被群友强迫穿上女装'}, + {'name': '与群友水聊', 'good': '扶我起来我还能打字', 'bad': '一不小心就被大佬闪瞎'}, + {'name': '迫害大佬', 'good': '迫害是大佬进步的阶梯', 'bad': '亲爱的,你号没了'}, + {'name': '算命', 'good': '算啥都准', 'bad': '诸事不宜'}, + {'name': '成为魔法少女', 'good': '勇敢的烧酒啊快去拯救世界吧!', 'bad': '会掉头'}, + {'name': '沟通克苏鲁', 'good': '奇怪的知识增加了', 'bad': '&▓▓▓◆▓▓▓¥#▓@■.◆'}, + {'name': '看新番', 'good': '你看的这部新番有成为本季度霸权的可能', 'bad': '这周更新的是总集篇'}, + {'name': '看旧番', 'good': '在宅的道路上又前进了一步', 'bad': '被剧情喂屎'}, + {'name': '看里番', 'good': '传统手艺精进了', 'bad': '房间门关好了吗?'}, + {'name': '看漫画', 'good': '正在追的作品十话连发', 'bad': '刷到正在追的作品的腰斩停更通知'}, + {'name': '看轻小说', 'good': '插画很好舔,孩子很满意', 'bad': '买插画送的厕纸有啥好看的'}, + {'name': '看本子', 'good': '被精准戳中性癖', 'bad': '更新的全是你不喜欢的类型'}, + {'name': '前往女仆咖啡厅', 'good': '感受身心上的治愈', 'bad': '终究是虚假的情景,只会让你内心更加空虚'}, + {'name': '女装Cosplay', 'good': '好评如潮', 'bad': '照片传到班级群还被认出来'}, + {'name': '修仙', 'good': '能突破到下一个境界', 'bad': '会在进阶中遭受心魔侵蚀'}, + {'name': '渡劫', 'good': '万事俱备,只待飞升', 'bad': '没能扛过去,寿元终'}, + {'name': '在妹子面前吹牛', 'good': '改善你矮穷挫的形象', 'bad': '会被识破'}, + {'name': '发超过10条的状态', 'good': '显得你很高产', 'bad': '会被人直接拉黑'}, + {'name': '在B站上传视频', 'good': '播放量爆炸', 'bad': '没人看'}, + {'name': '搬运视频', 'good': '会被硬币砸得很爽', 'bad': '不会有人看的'}, + {'name': '上微博', 'good': '今天的瓜不能错过', 'bad': '被智障发言糊一脸'}, + {'name': '作死', 'good': '节目效果一流', 'bad': '吾之旧友弔似汝,如今坟头草丈五'}, + {'name': '看老黄历', 'good': '反正你已经看了', 'bad': '反正你已经看了'}, + {'name': '学习一门新技能', 'good': '有会成为大神的资质', 'bad': '可能会误入歧途'}, + {'name': '睡懒觉', 'good': '避免内存不足', 'bad': '早上很早醒来睡不着了'}, + {'name': '睡懒觉', 'good': '你今天会更有精神', 'bad': '会错过重要的事情'}, + {'name': '上课玩手机', 'good': '会发现好玩的事情', 'bad': '会被老师教训'}, + {'name': '抄作业', 'good': '没有作业抄的学生生活是罪恶的!', 'bad': '老师会认真批改,你懂的……'}, + {'name': '学习', 'good': '你已经几天(月、年)没学习了?', 'bad': '会睡着'}, + {'name': '出门带伞', 'good': '今天下雨你信不信', 'bad': '好运气都被遮住了'}, + {'name': '走夜路', 'good': '偶尔也要一个人静一静', 'bad': '有坏人'}, + {'name': '补番', 'good': '你会后悔没早点看这部番', 'bad': '你会后悔看了这部番'}, + {'name': '修图', 'good': '原片直出毫无压力', 'bad': 'Photoshop未响应'}, + {'name': '赶稿', 'good': '完美守住deadline', 'bad': '终究还是超期了'}, + {'name': '摸鱼', 'good': '摸鱼一时爽,一直摸鱼一直爽', 'bad': '被老板当场抓获'}, + {'name': '入手新游戏', 'good': '你会玩的很开心', 'bad': '这游戏明天就99%off'}, + {'name': '出门', 'good': '今天会是个好天气', 'bad': '中途突降暴雨'} +] + + +def get_fortune(user_id: int) -> dict: + # 用qq、日期生成随机种子 + random_seed_str = str([user_id, datetime.date.today()]) + md5 = hashlib.md5() + md5.update(random_seed_str.encode('utf-8')) + random_seed = md5.hexdigest() + random.seed(random_seed) + # 今日运势 + # 生成运势随机数 + fortune_result = random.randint(1, 108) + # 大吉・中吉・小吉・吉・半吉・末吉・末小吉・凶・小凶・半凶・末凶・大凶 + if fortune_result < 4: + fortune_star = '☆' * 11 + fortune_text = '大凶' + elif fortune_result < 9: + fortune_star = '★' * 1 + '☆' * 10 + fortune_text = '末凶' + elif fortune_result < 16: + fortune_star = '★' * 2 + '☆' * 9 + fortune_text = '半凶' + elif fortune_result < 25: + fortune_star = '★' * 3 + '☆' * 8 + fortune_text = '小凶' + elif fortune_result < 36: + fortune_star = '★' * 4 + '☆' * 7 + fortune_text = '凶' + elif fortune_result < 48: + fortune_star = '★' * 5 + '☆' * 6 + fortune_text = '末小吉' + elif fortune_result < 60: + fortune_star = '★' * 6 + '☆' * 5 + fortune_text = '末吉' + elif fortune_result < 72: + fortune_star = '★' * 7 + '☆' * 4 + fortune_text = '半吉' + elif fortune_result < 84: + fortune_star = '★' * 8 + '☆' * 3 + fortune_text = '吉' + elif fortune_result < 96: + fortune_star = '★' * 9 + '☆' * 2 + fortune_text = '小吉' + elif fortune_result < 102: + fortune_star = '★' * 10 + '☆' * 1 + fortune_text = '中吉' + else: + fortune_star = '★' * 11 + fortune_text = '大吉' + # 宜做和不宜做 + do_and_not = random.sample(do_something, k=4) + + result = { + 'fortune_star': fortune_star, + 'fortune_text': fortune_text, + 'do_1': f"{do_and_not[0]['name']} —— {do_and_not[0]['good']}", + 'do_2': f"{do_and_not[2]['name']} —— {do_and_not[2]['good']}", + 'not_do_1': f"{do_and_not[1]['name']} —— {do_and_not[1]['bad']}", + 'not_do_2': f"{do_and_not[3]['name']} —— {do_and_not[3]['bad']}" + } + + # 重置随机种子 + random.seed() + + return result diff --git a/omega_miya/plugins/omega_sign_in/utils.py b/omega_miya/plugins/omega_sign_in/utils.py new file mode 100644 index 00000000..6a34c28a --- /dev/null +++ b/omega_miya/plugins/omega_sign_in/utils.py @@ -0,0 +1,405 @@ +""" +@Author : Ailitonia +@Date : 2021/08/27 0:48 +@FileName : utils.py +@Project : nonebot2_miya +@Description : 签到素材合成工具 +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +import os +import random +import asyncio +import aiofiles.os +from typing import Optional +from datetime import datetime +from PIL import Image, ImageDraw, ImageFont +from nonebot import get_driver, require, logger +from omega_miya.database import DBPixivillust, Result +from omega_miya.utils.pixiv_utils import PixivIllust +from omega_miya.utils.omega_plugin_utils import HttpFetcher, ProcessUtils, TextUtils +from .config import Config +from .fortune import get_fortune + + +global_config = get_driver().config +plugin_config = Config(**global_config.dict()) +TMP_PATH = global_config.tmp_path_ +RESOURCES_PATH = global_config.resources_path_ +SIGN_IN_PIC_PATH = os.path.abspath(os.path.join(TMP_PATH, 'sign_in_pic')) +SIGN_IN_CARD_PATH = os.path.abspath(os.path.join(TMP_PATH, 'sign_in_card')) +ENABLE_PIC_PREPARING_SCHEDULER = plugin_config.enable_pic_preparing_scheduler +CACHE_PIC_LIMIT = plugin_config.cache_pic_limit + + +async def __pre_download_sign_in_pic(pid: int, *, pic_size: str = 'regular') -> Result.IntResult: + illust_info_result = await PixivIllust(pid=pid).get_illust_data() + if illust_info_result.error: + return Result.IntResult(error=True, info=illust_info_result.info, result=-1) + pic_url = illust_info_result.result.get('illust_pages', {}).get(0, {}).get(pic_size) + if not pic_url: + return Result.IntResult(error=True, info='Small illust pages url not found', result=-1) + fetcher = HttpFetcher(timeout=30, attempt_limit=2, flag='pre_download_sign_in_pic', headers=PixivIllust.HEADERS) + download_result = await fetcher.download_file(url=pic_url, path=SIGN_IN_PIC_PATH) + if download_result.error: + return Result.IntResult(error=True, info=download_result.info, result=-1) + else: + return Result.IntResult(error=False, info='Success', result=0) + + +async def __prepare_sign_in_pic() -> Result.TextResult: + # 检查当前缓存目录里面的图片是不是超出数量限制 是的话就删除超出的部分 + if not os.path.exists(SIGN_IN_PIC_PATH): + os.makedirs(SIGN_IN_PIC_PATH) + pic_file_list = os.listdir(SIGN_IN_PIC_PATH) + if len(pic_file_list) > CACHE_PIC_LIMIT: + del_pic_file_list = random.sample(pic_file_list, k=(len(pic_file_list) - CACHE_PIC_LIMIT)) + for pic_file in del_pic_file_list: + await aiofiles.os.remove(os.path.abspath(os.path.join(SIGN_IN_PIC_PATH, pic_file))) + logger.info(f'Preparing sign in pic processing, ' + f'removed pic "{"/".join(del_pic_file_list)}" exceed the limit of cache') + + # 获取图片信息并下载图片 + pic_list_result = await DBPixivillust.rand_illust(num=100, nsfw_tag=0, ratio=1) + if pic_list_result.error or not pic_list_result.result: + logger.error(f'Preparing sign in pic failed, DB Error or not result, result: {pic_list_result}') + return Result.TextResult(error=True, info=pic_list_result.info, result='DB Error or not result') + + tasks = [__pre_download_sign_in_pic(pid=pid) for pid in pic_list_result.result] + pre_download_result = await ProcessUtils.fragment_process( + tasks=tasks, fragment_size=20, log_flag='pre_download_sign_in_pic') + + success_count = 0 + failed_count = 0 + for result in pre_download_result: + if result.success(): + success_count += 1 + else: + failed_count += 1 + result_text = f'Completed with {success_count} Success, {failed_count} Failed' + logger.info(f'Preparing sign in pic completed, {result_text}') + return Result.TextResult(error=True, info=f'Completed', result=result_text) + + +# 下载签到图片的定时任务 +if ENABLE_PIC_PREPARING_SCHEDULER: + scheduler = require("nonebot_plugin_apscheduler").scheduler + scheduler.add_job( + __prepare_sign_in_pic, + 'cron', + # year=None, + # month=None, + # day='*/1', + # week=None, + # day_of_week=None, + hour='*/6', + # minute=None, + # second=None, + # start_date=None, + # end_date=None, + # timezone=None, + id='prepare_sign_in_pic', + coalesce=True, + misfire_grace_time=120 + ) + + +async def __get_reand_sign_in_pic() -> Result.TextResult: + try_count = 0 + if not os.path.exists(SIGN_IN_PIC_PATH): + os.makedirs(SIGN_IN_PIC_PATH) + pic_file_list = os.listdir(SIGN_IN_PIC_PATH) + while not pic_file_list and try_count < 2: + await __prepare_sign_in_pic() + pic_file_list = os.listdir(SIGN_IN_PIC_PATH) + try_count += 1 + if not pic_file_list: + return Result.TextResult(error=True, info='Can not pre-download sign in pic', result='') + + # 重置随机种子 + random.seed() + + rand_file = random.choice(pic_file_list) + file_path = os.path.abspath(os.path.join(SIGN_IN_PIC_PATH, rand_file)) + return Result.TextResult(error=False, info='Success', result=file_path) + + +def __get_level(favorability: float) -> tuple[int, int, int]: + """ + 根据好感度获取等级及当前等级好感度 + :param favorability: 总好感度 + :return: (等级, 当前等级好感度, 当前等级好感度上限) + """ + if favorability <= 0: + return 0, 0, 1 + elif favorability < 10000: + return 1, int(favorability), 10000 + elif favorability < 36000: + return 2, int(favorability - 10000), 26000 + elif favorability < 78000: + return 3, int(favorability - 36000), 42000 + elif favorability < 136000: + return 4, int(favorability - 78000), 58000 + elif favorability < 210000: + return 5, int(favorability - 136000), 74000 + elif favorability < 300000: + return 6, int(favorability - 210000), 90000 + elif favorability < 406000: + return 7, int(favorability - 300000), 106000 + else: + return 8, int(favorability - 406000), 122000 + + +def __get_level_color(level: int) -> tuple[int, int, int]: + """ + 根据等级获取相应等级颜色 + :param level: 等级 + :return: (int, int, int): RGB 颜色 + """ + level_color: dict[int, tuple[int, int, int]] = { + 0: (136, 136, 136), + 1: (102, 102, 102), + 2: (153, 204, 153), + 3: (221, 204, 136), + 4: (255, 204, 51), + 5: (255, 204, 204), + 6: (247, 119, 127), + 7: (102, 204, 255), + 8: (175, 136, 250), + } + return level_color.get(level, (136, 136, 136)) + + +async def get_hitokoto(*, c: Optional[str] = None) -> Result.TextResult: + """获取一言""" + url = 'https://v1.hitokoto.cn' + params = { + 'encode': 'json', + 'charset': 'utf-8' + } + if c is not None: + params.update({'c': c}) + + headers = HttpFetcher.DEFAULT_HEADERS.update({'accept': 'application/json'}) + hitokoto_result = await HttpFetcher(flag='sign_hitokoto', headers=headers).get_json(url=url, params=params) + if hitokoto_result.error: + return Result.TextResult(error=True, info=hitokoto_result.info, result='') + + text = f'{hitokoto_result.result.get("hitokoto")}\n——《{hitokoto_result.result.get("from")}》' + if hitokoto_result.result.get("from_who"): + text += f' {hitokoto_result.result.get("from_who")}' + return Result.TextResult(error=False, info='Success', result=text) + + +async def generate_sign_in_card( + user_id: int, user_text: str, fav: float, *, width: int = 1024, fortune_do: bool = True) -> Result.TextResult: + """ + 生成卡片 + :param user_id: 用户id + :param user_text: 头部自定义文本 + :param fav: 用户好感度 用户计算等级 + :param width: 生成图片宽度 自适应排版 + :param fortune_do: 是否绘制老黄历当日宜与不宜 + :return: 生成图片地址 + """ + # 获取头图 + sign_pic_path_result = await __get_reand_sign_in_pic() + if sign_pic_path_result.error: + return Result.TextResult(error=True, info=sign_pic_path_result.info, result='') + sign_pic_path = sign_pic_path_result.result + + def __handle(): + # 生成用户当天老黄历 + user_fortune = get_fortune(user_id=user_id) + fortune_star = user_fortune.get('fortune_star') + fortune_text = user_fortune.get('fortune_text') + fortune_do_1 = user_fortune.get('do_1') + fortune_do_2 = user_fortune.get('do_2') + fortune_not_do_1 = user_fortune.get('not_do_1') + fortune_not_do_2 = user_fortune.get('not_do_2') + + # 加载头图 + draw_top_img: Image.Image = Image.open(sign_pic_path) + # 调整头图宽度 + top_img_height = int(width * draw_top_img.height / draw_top_img.width) + draw_top_img = draw_top_img.resize((width, top_img_height)) + + # 字体 + bd_font_path = os.path.abspath(os.path.join(RESOURCES_PATH, 'fonts', 'SourceHanSans_Heavy.otf')) + bd_font = ImageFont.truetype(bd_font_path, width // 10) + bd_title_font = ImageFont.truetype(bd_font_path, width // 12) + bd_text_font = ImageFont.truetype(bd_font_path, width // 18) + + main_font_path = os.path.abspath(os.path.join(RESOURCES_PATH, 'fonts', 'SourceHanSans_Regular.otf')) + text_font = ImageFont.truetype(main_font_path, width // 28) + + level_font_path = os.path.abspath(os.path.join(RESOURCES_PATH, 'fonts', 'pixel.ttf')) + level_font = ImageFont.truetype(level_font_path, width // 20) + + bottom_font_path = os.path.abspath(os.path.join(RESOURCES_PATH, 'fonts', 'fzzxhk.ttf')) + bottom_text_font = ImageFont.truetype(bottom_font_path, width // 40) + + # 打招呼 + if 4 <= datetime.now().hour < 11: + top_text = '早上好' + elif 11 <= datetime.now().hour < 14: + top_text = '中午好' + elif 14 <= datetime.now().hour < 19: + top_text = '下午好' + elif 19 <= datetime.now().hour < 22: + top_text = '晚上好' + else: + top_text = '晚安' + top_text_width, top_text_height = bd_font.getsize(top_text) + + # 计算好感度等级条 + level = __get_level(favorability=fav) + level_text = f'Level {level[0]}' + level_text_width, level_text_height = level_font.getsize(level_text) + fav_text = f'{level[1]}/{level[2]}' + fav_rat = level[1] / level[2] if level[1] < level[2] else 1 + fav_text_width, fav_text_height = text_font.getsize(fav_text) + + # 日期 + date_text = datetime.now().strftime('%m/%d') + # 昵称、好感度、积分 + # 首先要对文本进行分割 + user_text_ = TextUtils(text=user_text).split_multiline(width=(width - int(width * 0.125)), font=text_font) + user_text_width, user_text_height = text_font.getsize_multiline(user_text_) + # 今日运势 + fortune_text_width, fortune_text_height = bd_text_font.getsize(fortune_text) + fortune_star_width, fortune_star_height = text_font.getsize(fortune_star) + # 底部文字 + bottom_text_width, bottom_text_height = bottom_text_font.getsize(f'{"@@##" * 4}\n' * 4) + + # 总高度 + if fortune_do: + height = (top_img_height + top_text_height + user_text_height + level_text_height + + fortune_text_height * 3 + fortune_star_height * 6 + bottom_text_height * 4 + + int(0.25 * width)) + else: + height = (top_img_height + top_text_height + user_text_height + level_text_height + + fortune_text_height * 1 + fortune_star_height * 2 + bottom_text_height * 4 + + int(0.1875 * width)) + + # 生成背景 + background = Image.new( + mode="RGB", + size=(width, height), + color=(255, 255, 255)) + + # 开始往背景上绘制各个元素 + # 以下排列从上到下绘制 请勿变换顺序 否则导致位置错乱 + background.paste(draw_top_img, box=(0, 0)) # 背景 + + this_height = top_img_height + int(0.0625 * width) + ImageDraw.Draw(background).text(xy=(int(width * 0.0625), this_height), + text=top_text, font=bd_font, align='left', anchor='lt', + fill=(0, 0, 0)) # 打招呼 + + ImageDraw.Draw(background).text(xy=(width - int(width * 0.0625), this_height), + text=date_text, font=bd_title_font, align='right', anchor='rt', + fill=__get_level_color(level=level[0])) # 日期 + + this_height += top_text_height + ImageDraw.Draw(background).multiline_text(xy=(int(width * 0.0625), this_height), + text=user_text_, font=text_font, align='left', + fill=(128, 128, 128)) # 昵称、好感度、积分 + + this_height += user_text_height + int(0.046875 * width) + ImageDraw.Draw(background).text(xy=(int(width * 0.065), this_height), + text=level_text, font=level_font, align='left', anchor='lt', + fill=__get_level_color(level=level[0])) # 等级 + + this_height += level_text_height + int(0.03125 * width) + ImageDraw.Draw(background).text(xy=(width - int(width * 0.0625), this_height), + text=fav_text, font=text_font, align='right', anchor='rm', + fill=(208, 208, 208)) # 经验条数值 + + ImageDraw.Draw(background).line(xy=[(int(width * 0.0625), this_height), + (width - int(width * 0.09375 + fav_text_width), this_height)], + fill=(224, 224, 224), width=int(0.03125 * width)) # 经验条底 + + ImageDraw.Draw(background).line( + xy=[(int(width * 0.0625), this_height), + (int(width * 0.0625 + (width * 0.84375 - fav_text_width) * fav_rat), this_height)], + fill=__get_level_color(level=level[0]), width=int(0.03125 * width)) # 经验条内 + + this_height += fortune_star_height + int(0.015625 * width) + ImageDraw.Draw(background).text(xy=(int(width * 0.0625), this_height), + text=f'今日运势: {fortune_text}', font=bd_text_font, + align='left', anchor='lt', fill=(0, 0, 0)) # 今日运势 + + this_height += fortune_text_height + ImageDraw.Draw(background).text(xy=(int(width * 0.0625), this_height), + text=fortune_star, font=text_font, align='left', anchor='lt', + fill=(128, 128, 128)) # 运势星星 + + if fortune_do: + this_height += fortune_star_height + int(0.03125 * width) + ImageDraw.Draw(background).text(xy=(int(width * 0.0625), this_height), + text=f'宜', font=bd_text_font, align='left', anchor='lt', + fill=(0, 0, 0)) # 宜 + + this_height += fortune_text_height + ImageDraw.Draw(background).text(xy=(int(width * 0.0625), this_height), + text=fortune_do_1, font=text_font, align='left', anchor='lt', + fill=(128, 128, 128)) # 今日宜1 + + this_height += fortune_star_height # 反正这两字体都一样大 + ImageDraw.Draw(background).text(xy=(int(width * 0.0625), this_height), + text=fortune_do_2, font=text_font, align='left', anchor='lt', + fill=(128, 128, 128)) # 今日宜2 + + this_height += fortune_star_height + int(0.03125 * width) + ImageDraw.Draw(background).text(xy=(int(width * 0.0625), this_height), + text=f'不宜', font=bd_text_font, align='left', anchor='lt', + fill=(0, 0, 0)) # 不宜 + + this_height += fortune_text_height + ImageDraw.Draw(background).text(xy=(int(width * 0.0625), this_height), + text=fortune_not_do_1, font=text_font, align='left', anchor='lt', + fill=(128, 128, 128)) # 今日不宜1 + + this_height += fortune_star_height + ImageDraw.Draw(background).text(xy=(int(width * 0.0625), this_height), + text=fortune_not_do_2, font=text_font, align='left', anchor='lt', + fill=(128, 128, 128)) # 今日不宜2 + + this_height += fortune_star_height + bottom_text_height * 2 + ImageDraw.Draw(background).text(xy=(width - int(width * 0.0625), this_height), + text='随机生成 请勿迷信', font=bottom_text_font, align='right', anchor='rt', + fill=(128, 128, 128)) + + this_height += bottom_text_height + ImageDraw.Draw(background).text(xy=(width - int(width * 0.0625), this_height), + text=f'Omega Miya @ {datetime.now().year}', + font=bottom_text_font, align='right', anchor='rt', + fill=(128, 128, 128)) + + if not os.path.exists(SIGN_IN_CARD_PATH): + os.makedirs(SIGN_IN_CARD_PATH) + + if fortune_do: + name_prefix = 'fortune_sign_in' + else: + name_prefix = 'fortune' + save_path = os.path.abspath(os.path.join( + SIGN_IN_CARD_PATH, f"{name_prefix}_card_{user_id}_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.jpg")) + background.save(save_path, 'JPEG') + return save_path + + try: + loop = asyncio.get_running_loop() + result = await loop.run_in_executor(None, __handle) + return Result.TextResult(error=False, info='Success', result=result) + except Exception as e: + return Result.TextResult(error=True, info=repr(e), result='') + + +__all__ = [ + 'scheduler', + 'get_hitokoto', + 'generate_sign_in_card' +] diff --git a/omega_miya/plugins/Omega_skill/__init__.py b/omega_miya/plugins/omega_skill/__init__.py similarity index 95% rename from omega_miya/plugins/Omega_skill/__init__.py rename to omega_miya/plugins/omega_skill/__init__.py index 85d420c6..e1650c0f 100644 --- a/omega_miya/plugins/Omega_skill/__init__.py +++ b/omega_miya/plugins/omega_skill/__init__.py @@ -1,14 +1,15 @@ -from nonebot import on_command, export, logger +from nonebot import on_command, logger +from nonebot.plugin.export import export from nonebot.permission import SUPERUSER from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent from nonebot.adapters.cqhttp.permission import GROUP -from omega_miya.utils.Omega_Base import DBSkill, DBUser, Result -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state +from omega_miya.database import DBSkill, DBUser, Result +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state # Custom plugin usage text -__plugin_name__ = '技能' +__plugin_custom_name__ = '技能' __plugin_usage__ = r'''【Omega 技能插件】 用来设置/查询自己的技能 仅限群聊使用 @@ -31,13 +32,10 @@ /Skill add [SkillName] [SkillDescription] /Skill del [SkillName]''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) + # 注册事件响应器 skill_admin = on_command('Skill', aliases={'skill'}, permission=SUPERUSER, priority=10, block=True) @@ -121,10 +119,9 @@ async def skill_del(bot: Bot, event: MessageEvent, state: T_State) -> Result.Int '技能', aliases={'我的技能'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='skill', - command=True, - auth_node='basic'), + command=True), permission=GROUP, priority=10, block=True) diff --git a/omega_miya/plugins/omega_statistic/__init__.py b/omega_miya/plugins/omega_statistic/__init__.py new file mode 100644 index 00000000..25cb14b7 --- /dev/null +++ b/omega_miya/plugins/omega_statistic/__init__.py @@ -0,0 +1,151 @@ +""" +@Author : Ailitonia +@Date : 2021/08/15 1:19 +@FileName : __init__.py.py +@Project : nonebot2_miya +@Description : +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +from datetime import datetime +from nonebot import on_command, logger +from nonebot.plugin.export import export +from nonebot.typing import T_State +from nonebot.rule import to_me +from nonebot.permission import SUPERUSER +from nonebot.adapters.cqhttp.bot import Bot +from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent +from nonebot.adapters.cqhttp.permission import GROUP_ADMIN, GROUP_OWNER, PRIVATE_FRIEND +from nonebot.adapters.cqhttp.message import MessageSegment +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state, PicEncoder +from omega_miya.database import DBStatistic +from .utils import draw_statistics + + +# Custom plugin usage text +__plugin_custom_name__ = '统计信息' +__plugin_usage__ = r'''【Omega 插件使用统计】 +查询插件使用统计信息 + +**Permission** +Friend Private +Command & Lv.10 +or AuthNode + +**AuthNode** +basic + +**Usage** +/统计信息 [条件]''' + + +# Init plugin export +init_export(export(), __plugin_custom_name__, __plugin_usage__) + + +# 注册事件响应器 +statistic = on_command( + '统计信息', + # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 + state=init_processor_state( + name='statistic', + command=True, + level=10), + aliases={'插件统计', 'statistic'}, + permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER | PRIVATE_FRIEND, + priority=10, + block=True) + + +# 修改默认参数处理 +@statistic.args_parser +async def parse(bot: Bot, event: MessageEvent, state: T_State): + args = str(event.get_plaintext()).strip().lower().split() + if not args: + await statistic.reject('你似乎没有发送有效的参数呢QAQ, 请重新发送:') + state[state["_current_key"]] = args[0] + if state[state["_current_key"]] == '取消': + await statistic.finish('操作已取消') + + +@statistic.handle() +async def handle_first_receive(bot: Bot, event: MessageEvent, state: T_State): + args = str(event.get_plaintext()).strip().lower().split() + if not args: + state['condition'] = '本月' + elif args and len(args) == 1: + state['condition'] = args[0] + else: + await statistic.finish('参数错误QAQ') + + +@statistic.got('condition', prompt='请输入查询条件:\n【全部/本月/本年】') +async def handle_statistic(bot: Bot, event: MessageEvent, state: T_State): + condition = state['condition'] + self_id = event.self_id + now = datetime.now() + if condition == '本月': + start_time = datetime(year=now.year, month=now.month, day=1) + elif condition == '本年': + start_time = datetime(year=now.year, month=1, day=1) + else: + condition = '全部' + start_time = None + + if isinstance(event, GroupMessageEvent): + title = f'本群{condition}插件使用统计' + group_id = event.group_id + statistic_result = await DBStatistic( + self_bot_id=self_id).get_group_statistic(group_id=group_id, start_time=start_time) + elif isinstance(event, PrivateMessageEvent): + title = f'用户{condition}插件使用统计' + user_id = event.user_id + statistic_result = await DBStatistic( + self_bot_id=self_id).get_user_statistic(user_id=user_id, start_time=start_time) + else: + return + + if statistic_result.error: + logger.error(f'查询统计信息失败, error: {statistic_result.info}') + await statistic.finish('查询统计信息失败QAQ') + + draw_bytes = await draw_statistics(data=statistic_result.result, title=title) + img_result = await PicEncoder.bytes_to_file(image=draw_bytes, folder_flag='statistic') + if img_result.error: + logger.error(f'生成统计图表失败, error: {img_result.info}') + await statistic.finish('生成统计图表失败QAQ') + + await statistic.finish(MessageSegment.image(img_result.result)) + + +admin_statistic = on_command( + '全局统计信息', + # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 + rule=to_me(), + state=init_processor_state( + name='admin_statistic', + command=True, + level=10), + aliases={'全局插件统计', 'total_stat'}, + permission=SUPERUSER, + priority=10, + block=True) + + +@admin_statistic.handle() +async def handle_admin_statistic(bot: Bot, event: MessageEvent, state: T_State): + self_id = event.self_id + statistic_result = await DBStatistic(self_bot_id=self_id).get_bot_statistic() + if statistic_result.error: + logger.error(f'查询全局统计信息失败, error: {statistic_result.info}') + await statistic.finish('查询全局统计信息失败QAQ') + + title = f'Bot:{self_id} 全局插件使用统计' + draw_bytes = await draw_statistics(data=statistic_result.result, title=title) + img_result = await PicEncoder.bytes_to_file(image=draw_bytes, folder_flag='statistic') + if img_result.error: + logger.error(f'生成全局统计图表失败, error: {img_result.info}') + await statistic.finish('生成全局统计图表失败QAQ') + + await statistic.finish(MessageSegment.image(img_result.result)) diff --git a/omega_miya/plugins/omega_statistic/utils.py b/omega_miya/plugins/omega_statistic/utils.py new file mode 100644 index 00000000..c4f80f61 --- /dev/null +++ b/omega_miya/plugins/omega_statistic/utils.py @@ -0,0 +1,40 @@ +""" +@Author : Ailitonia +@Date : 2021/08/15 1:20 +@FileName : utils.py +@Project : nonebot2_miya +@Description : 统计做图工具 +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +import sys +import asyncio +from io import BytesIO +from typing import Tuple, List +from matplotlib import pyplot as plt + + +async def draw_statistics(data: List[Tuple[str, int]], *, title: str = '插件使用统计') -> bytes: + """ + :param data: DBStatistic 对象 get statistic 返回数据 + :param title: 图表标题 + :return: + """ + def __handle() -> bytes: + name = [x[0] for x in data] + count = [x[1] for x in data] + plt.switch_backend('agg') # Fix RuntimeError caused by GUI needed + if sys.platform.startswith('win'): + plt.rcParams['font.sans-serif'] = ['SimHei'] + plt.rcParams['axes.unicode_minus'] = False + plt.barh(name, count) + plt.title(title) + with BytesIO() as bf: + plt.savefig(bf, dpi=300) + img_bytes = bf.getvalue() + return img_bytes + + loop = asyncio.get_running_loop() + result = await loop.run_in_executor(None, __handle) + return result diff --git a/omega_miya/plugins/Omega_vacation/__init__.py b/omega_miya/plugins/omega_vacation/__init__.py similarity index 95% rename from omega_miya/plugins/Omega_vacation/__init__.py rename to omega_miya/plugins/omega_vacation/__init__.py index 4906036a..66ef4650 100644 --- a/omega_miya/plugins/Omega_vacation/__init__.py +++ b/omega_miya/plugins/omega_vacation/__init__.py @@ -1,15 +1,16 @@ import re from datetime import datetime, timedelta -from nonebot import MatcherGroup, export, logger, require +from nonebot import MatcherGroup, logger, require +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import GroupMessageEvent from nonebot.adapters.cqhttp.permission import GROUP -from omega_miya.utils.Omega_Base import DBSkill, DBUser, DBBot, DBBotGroup -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state, PermissionChecker +from omega_miya.database import DBSkill, DBUser, DBBot, DBBotGroup +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state, PermissionChecker # Custom plugin usage text -__plugin_name__ = '请假' +__plugin_custom_name__ = '请假' __plugin_usage__ = r'''【Omega 请假插件】 用来设置/查询自己以及群员的状态和假期 仅限群聊使用 @@ -30,22 +31,18 @@ /谁有空 [技能名称] /谁在休假''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) + # 注册事件响应器 vacation = MatcherGroup( type='message', # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='vacation', - command=True, - auth_node='basic'), + command=True), permission=GROUP, priority=10, block=True) @@ -306,7 +303,7 @@ async def member_vacations_monitor(): # 跳过不具备权限的组 self_bot = DBBot(self_qq=int(bot.self_id)) auth_check_res = await PermissionChecker(self_bot=self_bot).check_auth_node( - auth_id=group_id, auth_type='group', auth_node='Omega_vacation.basic') + auth_id=group_id, auth_type='group', auth_node='omega_vacation.basic') if auth_check_res != 1: continue logger.debug(f"member_vacations_monitor: checking group: {group_id}") diff --git a/omega_miya/plugins/Omega_auto_manager/group_welcome_message.py b/omega_miya/plugins/omega_welcome_msg/__init__.py similarity index 87% rename from omega_miya/plugins/Omega_auto_manager/group_welcome_message.py rename to omega_miya/plugins/omega_welcome_msg/__init__.py index 4049a2cd..7867cf92 100644 --- a/omega_miya/plugins/Omega_auto_manager/group_welcome_message.py +++ b/omega_miya/plugins/omega_welcome_msg/__init__.py @@ -1,14 +1,16 @@ """ @Author : Ailitonia -@Date : 2021/06/11 23:42 -@FileName : group_welcome_message.py +@Date : 2021/08/14 19:09 +@FileName : __init__.py.py @Project : nonebot2_miya @Description : 群自定义欢迎信息 @GitHub : https://github.com/Ailitonia @Software : PyCharm """ + from nonebot import logger +from nonebot.plugin.export import export from nonebot.plugin import on_notice, CommandGroup from nonebot.typing import T_State from nonebot.rule import to_me @@ -17,14 +19,31 @@ from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.message import Message, MessageSegment from nonebot.adapters.cqhttp.event import GroupMessageEvent, GroupIncreaseNoticeEvent -from omega_miya.utils.Omega_Base import DBBot, DBBotGroup -from omega_miya.utils.Omega_plugin_utils import OmegaRules +from omega_miya.database import DBBot, DBBotGroup +from omega_miya.utils.omega_plugin_utils import init_export, OmegaRules SETTING_NAME: str = 'group_welcome_message' DEFAULT_WELCOME_MSG: str = '欢迎新朋友~\n进群请先看群公告~\n一起愉快地聊天吧!' +# Custom plugin usage text +__plugin_custom_name__ = '群欢迎消息' +__plugin_usage__ = r'''【群自定义欢迎信息插件】 +向新入群的成员发送欢迎消息 + +以下命令均需要@机器人 +**Usage** +**GroupAdmin and SuperUser Only** +/设置欢迎消息 +/清空欢迎消息 +''' + + +# Init plugin export +init_export(export(), __plugin_custom_name__, __plugin_usage__) + + # 注册事件响应器 WelcomeMsg = CommandGroup( 'WelcomeMsg', @@ -104,9 +123,3 @@ async def handle_group_increase(bot: Bot, event: GroupIncreaseNoticeEvent, state at_seg = MessageSegment.at(user_id=user_id) await bot.send(event=event, message=Message(at_seg).append(msg)) logger.info(f'群组: {group_id}, 有新用户: {user_id} 进群') - - -__all__ = [ - 'WelcomeMsg', - 'group_increase' -] diff --git a/omega_miya/plugins/pixiv/__init__.py b/omega_miya/plugins/pixiv/__init__.py index dab21572..669a1d9d 100644 --- a/omega_miya/plugins/pixiv/__init__.py +++ b/omega_miya/plugins/pixiv/__init__.py @@ -1,26 +1,34 @@ import re +import os import asyncio +import pathlib +from math import ceil from typing import Optional -from nonebot import on_command, export, logger, get_driver +from datetime import datetime +from nonebot import on_command, logger, get_driver +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot -from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent +from nonebot.adapters.cqhttp.event import Event, MessageEvent, GroupMessageEvent, PrivateMessageEvent from nonebot.adapters.cqhttp.permission import GROUP, PRIVATE_FRIEND from nonebot.adapters.cqhttp import MessageSegment, Message -from omega_miya.utils.Omega_Base import DBBot -from omega_miya.utils.Omega_plugin_utils import \ - init_export, init_permission_state, PluginCoolDown, PermissionChecker, MsgSender +from omega_miya.database import DBBot, Result +from omega_miya.utils.omega_plugin_utils import \ + init_export, init_processor_state, PluginCoolDown, PermissionChecker, PicEncoder, MsgSender, ProcessUtils from omega_miya.utils.pixiv_utils import PixivIllust +from PIL import Image, ImageDraw, ImageFont from .config import Config __global_config = get_driver().config +TMP_PATH = __global_config.tmp_path_ +RESOURCES_PATH = __global_config.resources_path_ plugin_config = Config(**__global_config.dict()) ENABLE_NODE_CUSTOM = plugin_config.enable_node_custom # Custom plugin usage text -__plugin_name__ = 'Pixiv' +__plugin_custom_name__ = 'Pixiv' __plugin_usage__ = r'''【Pixiv助手】 查看Pixiv插画, 以及日榜、周榜、月榜 仅限群聊使用 @@ -44,25 +52,18 @@ /pixiv 日榜 /pixiv 周榜 /pixiv 月榜 +/pixiv [搜索关键词] **Need AuthNode** /pixivdl [页码]''' -# 声明本插件可配置的权限节点 +# 声明本插件额外可配置的权限节点 __plugin_auth_node__ = [ - PluginCoolDown.skip_auth_node, - 'basic', 'allow_r18', 'download' ] -# 声明本插件的冷却时间配置 -__plugin_cool_down__ = [ - PluginCoolDown(PluginCoolDown.user_type, 1), - PluginCoolDown(PluginCoolDown.group_type, 1) -] - # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__, __plugin_cool_down__) +init_export(export(), __plugin_custom_name__, __plugin_usage__, __plugin_auth_node__) # 注册事件响应器 @@ -70,11 +71,14 @@ 'pixiv', aliases={'Pixiv'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='pixiv', command=True, level=50, - auth_node='basic'), + cool_down=[ + PluginCoolDown(PluginCoolDown.user_type, 120), + PluginCoolDown(PluginCoolDown.group_type, 60) + ]), permission=GROUP | PRIVATE_FRIEND, priority=20, block=True) @@ -83,55 +87,36 @@ # 修改默认参数处理 @pixiv.args_parser async def parse(bot: Bot, event: MessageEvent, state: T_State): - args = str(event.get_plaintext()).strip().lower().split() + args = str(event.get_plaintext()).strip() if not args: await pixiv.reject('你似乎没有发送有效的参数呢QAQ, 请重新发送:') - state[state["_current_key"]] = args[0] + state[state["_current_key"]] = args if state[state["_current_key"]] == '取消': await pixiv.finish('操作已取消') @pixiv.handle() async def handle_first_receive(bot: Bot, event: MessageEvent, state: T_State): - args = str(event.get_plaintext()).strip().lower().split() + args = str(event.get_plaintext()).strip() if not args: pass - elif args and len(args) == 1: - state['mode'] = args[0] else: - await pixiv.finish('参数错误QAQ') + state['mode'] = args -@pixiv.got('mode', prompt='你是想看日榜, 周榜, 月榜, 还是作品呢? 想看特定作品的话请输入PixivID~') +@pixiv.got('mode', prompt='你是想看日榜, 周榜, 月榜, 还是作品呢? 想看特定作品的话请输入PixivID或关键词搜索~') async def handle_pixiv(bot: Bot, event: MessageEvent, state: T_State): mode = state['mode'] - if mode == '日榜': + if mode in ['日榜', '周榜', '月榜']: await pixiv.send('稍等, 正在下载图片~') - rank_result = await PixivIllust.get_ranking(mode='daily') - if rank_result.error: - logger.warning(f"User: {event.user_id} 获取Pixiv Rank失败, {rank_result.info}") - await pixiv.finish('加载失败, 网络超时QAQ') - - tasks = [] - for rank, illust_data in dict(rank_result.result).items(): - if rank >= 10: - break - tasks.append(__handle_ranking_msg(rank=rank, illust_data=illust_data)) - ranking_msg_result = list(await asyncio.gather(*tasks)) - - # 根据ENABLE_NODE_CUSTOM处理消息发送 - if ENABLE_NODE_CUSTOM and isinstance(event, GroupMessageEvent): - msg_sender = MsgSender(bot=bot, log_flag='PixivDailyRanking') - await msg_sender.safe_send_group_node_custom(group_id=event.group_id, message_list=ranking_msg_result) + if mode == '日榜': + rank_result = await PixivIllust.get_ranking(mode='daily') + elif mode == '周榜': + rank_result = await PixivIllust.get_ranking(mode='weekly') + elif mode == '月榜': + rank_result = await PixivIllust.get_ranking(mode='monthly') else: - for msg_seg in ranking_msg_result: - try: - await pixiv.send(msg_seg) - except Exception as e: - logger.warning(f'图片发送失败, user: {event.user_id}. error: {repr(e)}') - elif mode == '周榜': - await pixiv.send('稍等, 正在下载图片~') - rank_result = await PixivIllust.get_ranking(mode='weekly') + rank_result = await PixivIllust.get_ranking(mode='daily') if rank_result.error: logger.warning(f"User: {event.user_id} 获取Pixiv Rank失败, {rank_result.info}") await pixiv.finish('加载失败, 网络超时QAQ') @@ -145,31 +130,7 @@ async def handle_pixiv(bot: Bot, event: MessageEvent, state: T_State): # 根据ENABLE_NODE_CUSTOM处理消息发送 if ENABLE_NODE_CUSTOM and isinstance(event, GroupMessageEvent): - msg_sender = MsgSender(bot=bot, log_flag='PixivWeeklyRanking') - await msg_sender.safe_send_group_node_custom(group_id=event.group_id, message_list=ranking_msg_result) - else: - for msg_seg in ranking_msg_result: - try: - await pixiv.send(msg_seg) - except Exception as e: - logger.warning(f'图片发送失败, user: {event.user_id}. error: {repr(e)}') - elif mode == '月榜': - await pixiv.send('稍等, 正在下载图片~') - rank_result = await PixivIllust.get_ranking(mode='monthly') - if rank_result.error: - logger.warning(f"User: {event.user_id} 获取Pixiv Rank失败, {rank_result.info}") - await pixiv.finish('加载失败, 网络超时QAQ') - - tasks = [] - for rank, illust_data in dict(rank_result.result).items(): - if rank >= 10: - break - tasks.append(__handle_ranking_msg(rank=rank, illust_data=illust_data)) - ranking_msg_result = list(await asyncio.gather(*tasks)) - - # 根据ENABLE_NODE_CUSTOM处理消息发送 - if ENABLE_NODE_CUSTOM and isinstance(event, GroupMessageEvent): - msg_sender = MsgSender(bot=bot, log_flag='PixivMonthlyRanking') + msg_sender = MsgSender(bot=bot, log_flag='PixivRanking') await msg_sender.safe_send_group_node_custom(group_id=event.group_id, message_list=ranking_msg_result) else: for msg_seg in ranking_msg_result: @@ -189,17 +150,7 @@ async def handle_pixiv(bot: Bot, event: MessageEvent, state: T_State): # 处理r18权限 if illust_data_result.result.get('is_r18'): - if isinstance(event, PrivateMessageEvent): - user_id = event.user_id - auth_checker = await PermissionChecker(self_bot=DBBot(self_qq=int(bot.self_id))).\ - check_auth_node(auth_id=user_id, auth_type='user', auth_node='pixiv.allow_r18') - elif isinstance(event, GroupMessageEvent): - group_id = event.group_id - auth_checker = await PermissionChecker(self_bot=DBBot(self_qq=int(bot.self_id))).\ - check_auth_node(auth_id=group_id, auth_type='group', auth_node='pixiv.allow_r18') - else: - auth_checker = 0 - + auth_checker = await __handle_r18_perm(bot=bot, event=event) if auth_checker != 1: logger.warning(f"User: {event.user_id} 获取Pixiv资源 {pid} 被拒绝, 没有 allow_r18 权限") await pixiv.finish('R18禁止! 不准开车车!') @@ -217,13 +168,58 @@ async def handle_pixiv(bot: Bot, event: MessageEvent, state: T_State): msg = illust_info_result.result img_seg = MessageSegment.image(illust_result.result) # 发送图片和图片信息 + logger.info(f"User: {event.user_id} 获取了Pixiv作品: pid: {pid}") await pixiv.send(Message(img_seg).append(msg)) else: logger.warning(f"User: {event.user_id} 获取Pixiv资源失败, 网络超时或 {pid} 不存在, " f"{illust_info_result.info} // {illust_result.info}") await pixiv.send('加载失败, 网络超时或没有这张图QAQ') else: - await pixiv.reject('你输入的命令好像不对呢……请输入"月榜"、"周榜"、"日榜"或者PixivID, 取消命令请发送【取消】:') + text_ = mode + popular_order_ = True + near_year_ = True + nsfw_ = False + page_ = 1 + if filter_ := re.search(r'^(#(.+?)#)', mode): + text_ = re.sub(r'^(#(.+?)#)', '', mode).strip() + filter_text = filter_.groups()[1] + # 处理r18权限 + auth_checker = await __handle_r18_perm(bot=bot, event=event) + + if 'nsfw' in filter_text: + if auth_checker != 1: + logger.warning(f"User: {event.user_id} 搜索Pixiv nsfw资源 {mode} 被拒绝, 没有 allow_r18 权限") + await pixiv.finish('NSFW禁止! 不准开车车!') + return + else: + nsfw_ = True + + if '时间不限' in filter_text: + near_year_ = False + + if '最新' in filter_text: + popular_order_ = False + + if page_text := re.search(r'第(\d+?)页', filter_text): + page_ = int(page_text.groups()[0]) + + logger.debug(f'搜索Pixiv作品: {text_}') + search_result = await PixivIllust.search_artwork( + word=text_, popular_order=popular_order_, near_year=near_year_, nsfw=nsfw_, page=page_) + + if search_result.error or not search_result.result: + logger.warning(f'搜索Pixiv时没有找到相关作品, 或发生了意外的错误, result: {repr(search_result)}') + await pixiv.finish('没有找到相关作品QAQ, 也可能是发生了意外的错误, 请稍后再试~') + await pixiv.send(f'搜索Pixiv作品: {text_}\n图片下载中, 请稍等~') + + preview_result = await __preview_search_illust(search_result=search_result, title=f'Pixiv - {text_}') + if preview_result.error: + logger.error(f'生成Pixiv搜索预览图时发生了意外的错误, error: {repr(search_result)}') + await pixiv.finish('生成Pixiv搜索预览图时发生了意外的错误QAQ, 请稍后再试~') + + img_path = pathlib.Path(preview_result.result).as_uri() + logger.info(f"User: {event.user_id} 搜索了Pixiv作品: {mode}") + await pixiv.finish(MessageSegment.image(img_path)) # 注册事件响应器 @@ -231,7 +227,7 @@ async def handle_pixiv(bot: Bot, event: MessageEvent, state: T_State): 'pixivdl', aliases={'Pixivdl'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='pixivdl', command=True, auth_node='download'), @@ -297,6 +293,21 @@ async def handle_pixiv_dl(bot: Bot, event: GroupMessageEvent, state: T_State): await pixiv_dl.finish('参数错误, pid应为纯数字') +# 处理 pixiv 插件 r18 权限 +async def __handle_r18_perm(bot: Bot, event: Event) -> int: + if isinstance(event, PrivateMessageEvent): + user_id = event.user_id + auth_checker = await PermissionChecker(self_bot=DBBot(self_qq=int(bot.self_id))). \ + check_auth_node(auth_id=user_id, auth_type='user', auth_node='pixiv.allow_r18') + elif isinstance(event, GroupMessageEvent): + group_id = event.group_id + auth_checker = await PermissionChecker(self_bot=DBBot(self_qq=int(bot.self_id))). \ + check_auth_node(auth_id=group_id, auth_type='group', auth_node='pixiv.allow_r18') + else: + auth_checker = 0 + return auth_checker + + # 处理Pixiv.__ranking榜单消息 async def __handle_ranking_msg(rank: int, illust_data: dict) -> Optional[Message]: rank += 1 @@ -313,3 +324,75 @@ async def __handle_ranking_msg(rank: int, illust_data: dict) -> Optional[Message else: logger.warning(f"下载图片失败, pid: {illust_id}, {image_result.info}") return None + + +async def __preview_search_illust( + search_result: Result.DictListResult, + title: str, + *, + line_num: int = 6) -> Result.TextResult: + """ + 拼接pixiv作品预览图, 固定缩略图分辨率250*250 + :param search_result: 搜索结果 + :param title: 生成图片标题 + :param line_num: 单行作品数 + :return: 拼接后图片位置 + """ + illust_list = search_result.result + # 加载图片 + tasks = [PicEncoder(pic_url=x.get('thumb_url'), headers=PixivIllust.HEADERS + ).get_file(folder_flag='pixiv_search_thumb') for x in illust_list] + thumb_img_result = await ProcessUtils.fragment_process(tasks=tasks, fragment_size=20, log_flag='pixiv_search_thumb') + if not thumb_img_result: + return Result.TextResult(error=True, info='Not result', result='') + + def __handle() -> str: + size = (250, 250) + thumb_w, thumb_h = size + font_path = os.path.abspath(os.path.join(RESOURCES_PATH, 'fonts', 'fzzxhk.ttf')) + font_main = ImageFont.truetype(font_path, thumb_w // 15) + background = Image.new( + mode="RGB", + size=(thumb_w * line_num, (thumb_h + 100) * ceil(len(thumb_img_result) / line_num) + 100), + color=(255, 255, 255)) + # 写标题 + ImageDraw.Draw(background).text( + xy=((thumb_w * line_num) // 2, 20), text=title, font=ImageFont.truetype(font_path, thumb_w // 5), + spacing=8, align='center', anchor='ma', fill=(0, 0, 0)) + + # 处理拼图 + line = 0 + for index, img_result in enumerate(thumb_img_result): + # 处理单个缩略图 + draw_: Image.Image = Image.open(re.sub(r'^file:///', '', img_result.result)) + if draw_.size != size: + draw_.resize(size) + + # 确认缩略图单行位置 + seq = index % line_num + # 能被整除说明在行首要换行 + if seq == 0: + line += 1 + # 按位置粘贴单个缩略图 + background.paste(draw_, box=(seq * thumb_w, (thumb_h + 100) * (line - 1) + 100)) + pid_text = f"Pid: {illust_list[index].get('pid')}" + title_text = f"{illust_list[index].get('title')}" + title_text = f"{title_text[:13]}..." if len(title_text) > 13 else title_text + author_text = f"Author: {illust_list[index].get('author')}" + author_text = f"{author_text[:13]}..." if len(author_text) > 13 else author_text + text = f'{pid_text}\n{title_text}\n{author_text}' + ImageDraw.Draw(background).multiline_text( + xy=(seq * thumb_w + thumb_w // 2, (thumb_h + 100) * line + 10), text=text, font=font_main, + spacing=8, align='center', anchor='ma', fill=(0, 0, 0)) + + save_path = os.path.abspath(os.path.join( + TMP_PATH, 'pixiv_search_thumb', f"preview_search_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.jpg")) + background.save(save_path, 'JPEG') + return save_path + + try: + loop = asyncio.get_running_loop() + result = await loop.run_in_executor(None, __handle) + return Result.TextResult(error=False, info='Success', result=result) + except Exception as e: + return Result.TextResult(error=True, info=repr(e), result='') diff --git a/omega_miya/plugins/pixiv/config.py b/omega_miya/plugins/pixiv/config.py index bfdedbad..6c744e40 100644 --- a/omega_miya/plugins/pixiv/config.py +++ b/omega_miya/plugins/pixiv/config.py @@ -15,7 +15,7 @@ class Config(BaseSettings): # plugin custom config # 启用使用群组转发自定义消息节点的模式发送信息 # 发送速度受限于网络上传带宽, 有可能导致超时或发送失败, 请酌情启用 - enable_node_custom: bool = False + enable_node_custom: bool = True class Config: extra = "ignore" diff --git a/omega_miya/plugins/pixiv_monitor/__init__.py b/omega_miya/plugins/pixiv_monitor/__init__.py index 29fe4966..8681fe23 100644 --- a/omega_miya/plugins/pixiv_monitor/__init__.py +++ b/omega_miya/plugins/pixiv_monitor/__init__.py @@ -9,20 +9,21 @@ """ import re -from nonebot import on_command, export, logger +from nonebot import on_command, logger +from nonebot.plugin.export import export from nonebot.permission import SUPERUSER from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent -from nonebot.adapters.cqhttp.permission import GROUP_ADMIN, GROUP_OWNER, PRIVATE_FRIEND -from omega_miya.utils.Omega_Base import DBBot, DBBotGroup, DBFriend, DBSubscription, Result -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state +from nonebot.adapters.cqhttp.permission import GROUP_ADMIN, GROUP_OWNER +from omega_miya.database import DBBot, DBBotGroup, DBFriend, DBSubscription, Result +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state from omega_miya.utils.pixiv_utils import PixivUser from .monitor import scheduler, init_new_add_sub # Custom plugin usage text -__plugin_name__ = 'Pixiv画师订阅' +__plugin_custom_name__ = 'Pixiv画师订阅' __plugin_usage__ = r'''【Pixiv画师订阅】 随时更新Pixiv画师作品 仅限群聊使用 @@ -41,24 +42,20 @@ /Pixiv画师 清空订阅 /Pixiv画师 订阅列表''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) + # 注册事件响应器 pixiv_user_artwork = on_command( 'Pixiv画师', aliases={'pixiv画师', 'p站画师', 'P站画师'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='pixiv_user_artwork', command=True, - level=50, - auth_node='basic'), + level=50), permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER, priority=20, block=True) @@ -164,7 +161,7 @@ async def handle_check(bot: Bot, event: MessageEvent, state: T_State): await pixiv_user_artwork.finish(f'{sub_command}失败了QAQ, 可能并未订阅该用户, 或请稍后再试~') -async def sub_list(bot: Bot, event: MessageEvent, state: T_State) -> Result.ListResult: +async def sub_list(bot: Bot, event: MessageEvent, state: T_State) -> Result.TupleListResult: self_bot = DBBot(self_qq=int(bot.self_id)) if isinstance(event, GroupMessageEvent): group_id = event.group_id @@ -177,7 +174,7 @@ async def sub_list(bot: Bot, event: MessageEvent, state: T_State) -> Result.List result = await friend.subscription_list_by_type(sub_type=9) return result else: - return Result.ListResult(error=True, info='Illegal event', result=[]) + return Result.TupleListResult(error=True, info='Illegal event', result=[]) async def sub_add(bot: Bot, event: MessageEvent, state: T_State) -> Result.IntResult: diff --git a/omega_miya/plugins/pixiv_monitor/monitor.py b/omega_miya/plugins/pixiv_monitor/monitor.py index 7180c799..04d14310 100644 --- a/omega_miya/plugins/pixiv_monitor/monitor.py +++ b/omega_miya/plugins/pixiv_monitor/monitor.py @@ -12,9 +12,9 @@ import random from nonebot import logger, require, get_bots, get_driver from nonebot.adapters.cqhttp import MessageSegment, Message -from omega_miya.utils.Omega_Base import DBSubscription, DBPixivUserArtwork +from omega_miya.database import DBSubscription, DBPixivUserArtwork from omega_miya.utils.pixiv_utils import PixivUser, PixivIllust -from omega_miya.utils.Omega_plugin_utils import MsgSender, PicEffector, PicEncoder, ProcessUtils +from omega_miya.utils.omega_plugin_utils import MsgSender, PicEffector, PicEncoder, ProcessUtils from .config import Config @@ -54,7 +54,7 @@ async def dynamic_db_upgrade(): sub_res = await DBSubscription.list_sub_by_type(sub_type=9) for sub_id in sub_res.result: sub = DBSubscription(sub_type=9, sub_id=sub_id) - user_info_result = await PixivUser(uid=sub_id).get_info() + user_info_result = await PixivUser(uid=int(sub_id)).get_info() if user_info_result.error: logger.error(f'pixiv_user_db_upgrade: 获取用户信息失败, uid: {sub_id}, error: {user_info_result.info}') continue @@ -118,7 +118,7 @@ async def check_user_artwork(user_id: int): new_artwork = [pid for pid in all_artwork_list if pid not in exist_artwork_list] - subscription = DBSubscription(sub_type=9, sub_id=user_id) + subscription = DBSubscription(sub_type=9, sub_id=str(user_id)) for pid in new_artwork: illust = PixivIllust(pid=pid) diff --git a/omega_miya/plugins/pixivsion_monitor/__init__.py b/omega_miya/plugins/pixivsion_monitor/__init__.py index bc706237..de6aacb4 100644 --- a/omega_miya/plugins/pixivsion_monitor/__init__.py +++ b/omega_miya/plugins/pixivsion_monitor/__init__.py @@ -1,16 +1,17 @@ -from nonebot import on_command, export, logger +from nonebot import on_command, logger +from nonebot.plugin.export import export from nonebot.permission import SUPERUSER from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import GroupMessageEvent from nonebot.adapters.cqhttp.permission import GROUP_ADMIN, GROUP_OWNER -from omega_miya.utils.Omega_Base import DBBot, DBBotGroup, DBSubscription, Result -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state -from .monitor import scheduler, init_pixivsion_article +from omega_miya.database import DBBot, DBBotGroup, DBSubscription, Result +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state +from .monitor import scheduler, init_pixivsion_article, PIXIVISION_SUB_ID # Custom plugin usage text -__plugin_name__ = 'Pixivision' +__plugin_custom_name__ = 'Pixivision' __plugin_usage__ = r'''【Pixivision订阅】 推送最新的Pixivision特辑 仅限群聊使用 @@ -27,24 +28,20 @@ /Pixivision 订阅 /Pixivision 取消订阅''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) + # 注册事件响应器 pixivision = on_command( 'pixivision', aliases={'Pixivision'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='pixivision', command=True, - level=30, - auth_node='basic'), + level=30), permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER, priority=20, block=True) @@ -96,8 +93,7 @@ async def sub_add(bot: Bot, event: GroupMessageEvent, state: T_State) -> Result. group_id = event.group_id self_bot = DBBot(self_qq=int(bot.self_id)) group = DBBotGroup(group_id=group_id, self_bot=self_bot) - sub_id = -1 - sub = DBSubscription(sub_type=8, sub_id=sub_id) + sub = DBSubscription(sub_type=8, sub_id=PIXIVISION_SUB_ID) need_init = not (await sub.exist()) _res = await sub.add(up_name='Pixivision', live_info='Pixivision订阅') if not _res.success(): @@ -117,8 +113,7 @@ async def sub_del(bot: Bot, event: GroupMessageEvent, state: T_State) -> Result. group_id = event.group_id self_bot = DBBot(self_qq=int(bot.self_id)) group = DBBotGroup(group_id=group_id, self_bot=self_bot) - sub_id = -1 - _res = await group.subscription_del(sub=DBSubscription(sub_type=8, sub_id=sub_id)) + _res = await group.subscription_del(sub=DBSubscription(sub_type=8, sub_id=PIXIVISION_SUB_ID)) if not _res.success(): return _res result = Result.IntResult(error=False, info='Success', result=0) diff --git a/omega_miya/plugins/pixivsion_monitor/block_tag.py b/omega_miya/plugins/pixivsion_monitor/block_tag.py deleted file mode 100644 index 03ba9423..00000000 --- a/omega_miya/plugins/pixivsion_monitor/block_tag.py +++ /dev/null @@ -1,13 +0,0 @@ -TAG_BLOCK_LIST = [ - {'id': 206, 'name': '时尚男子'}, - {'id': 10, 'name': '有趣的'}, - {'id': 18, 'name': '恶搞'}, - {'id': 217, 'name': '恐怖'}, - {'id': 32, 'name': '男孩子'}, - {'id': 88, 'name': '帅哥'}, - {'id': 89, 'name': '大叔'}, - {'id': 328, 'name': '男子的眼睛'}, - {'id': 344, 'name': '日本画'}, - {'id': 321, 'name': '绝望'}, - {'id': 472, 'name': '我的英雄学院'} -] diff --git a/omega_miya/plugins/pixivsion_monitor/config.py b/omega_miya/plugins/pixivsion_monitor/config.py index a18b6499..f2a2cc30 100644 --- a/omega_miya/plugins/pixivsion_monitor/config.py +++ b/omega_miya/plugins/pixivsion_monitor/config.py @@ -8,6 +8,7 @@ @Software : PyCharm """ +from typing import List, Dict, Union from pydantic import BaseSettings @@ -16,6 +17,22 @@ class Config(BaseSettings): # 启用使用群组转发自定义消息节点的模式发送信息 # 发送速度受限于网络上传带宽, 有可能导致超时或发送失败, 请酌情启用 enable_node_custom: bool = False + sub_id: str = 'pixivision' + + # 不推送包含以下 tag 的 article + tag_block_list: List[Dict[str, Union[int, str]]] = [ + {'id': 206, 'name': '时尚男子'}, + {'id': 10, 'name': '有趣的'}, + {'id': 18, 'name': '恶搞'}, + {'id': 217, 'name': '恐怖'}, + # {'id': 32, 'name': '男孩子'}, + {'id': 88, 'name': '帅哥'}, + {'id': 89, 'name': '大叔'}, + {'id': 328, 'name': '男子的眼睛'}, + {'id': 344, 'name': '日本画'}, + {'id': 321, 'name': '绝望'}, + {'id': 472, 'name': '我的英雄学院'} + ] class Config: extra = "ignore" diff --git a/omega_miya/plugins/pixivsion_monitor/monitor.py b/omega_miya/plugins/pixivsion_monitor/monitor.py index 55eee764..0582f63a 100644 --- a/omega_miya/plugins/pixivsion_monitor/monitor.py +++ b/omega_miya/plugins/pixivsion_monitor/monitor.py @@ -1,17 +1,18 @@ import asyncio from nonebot import logger, require, get_bots, get_driver from nonebot.adapters.cqhttp import MessageSegment -from omega_miya.utils.Omega_Base import DBSubscription, DBPixivision -from omega_miya.utils.Omega_plugin_utils import MsgSender +from omega_miya.database import DBSubscription, DBPixivision +from omega_miya.utils.omega_plugin_utils import MsgSender from omega_miya.utils.pixiv_utils import PixivIllust, PixivisionArticle from .utils import pixivsion_article_parse -from .block_tag import TAG_BLOCK_LIST from .config import Config __global_config = get_driver().config plugin_config = Config(**__global_config.dict()) ENABLE_NODE_CUSTOM = plugin_config.enable_node_custom +PIXIVISION_SUB_ID = plugin_config.sub_id +TAG_BLOCK_LIST = plugin_config.tag_block_list # 启用检查动态状态的定时任务 @@ -33,7 +34,7 @@ # timezone=None, id='pixivision_monitor', coalesce=True, - misfire_grace_time=45 + misfire_grace_time=120 ) async def pixivision_monitor(): logger.debug(f"pixivision_monitor: checking started") @@ -82,7 +83,7 @@ async def pixivision_monitor(): logger.info(f'pixivision_monitor: checking completed, 没有新的article') return - subscription = DBSubscription(sub_type=8, sub_id=-1) + subscription = DBSubscription(sub_type=8, sub_id=PIXIVISION_SUB_ID) # 处理新的aritcle for article in new_article: @@ -155,7 +156,8 @@ async def init_pixivsion_article(): article_tags_id.append(int(tag['tag_id'])) article_tags_name.append(str(tag['tag_name'])) new_article.append({'aid': int(article['id']), 'tags': article_tags_name}) - except Exception: + except Exception as e: + logger.debug(f'解析pixivsion article失败, error: {repr(e)}, article data: {article}') continue # 处理新的aritcle for article in new_article: @@ -171,5 +173,6 @@ async def init_pixivsion_article(): __all__ = [ 'scheduler', - 'init_pixivsion_article' + 'init_pixivsion_article', + 'PIXIVISION_SUB_ID' ] diff --git a/omega_miya/plugins/pixivsion_monitor/utils.py b/omega_miya/plugins/pixivsion_monitor/utils.py index 95f97643..db5a4a8b 100644 --- a/omega_miya/plugins/pixivsion_monitor/utils.py +++ b/omega_miya/plugins/pixivsion_monitor/utils.py @@ -1,4 +1,4 @@ -from omega_miya.utils.Omega_Base import DBPixivision, Result +from omega_miya.database import DBPixivision, Result from omega_miya.utils.pixiv_utils import PixivisionArticle diff --git a/omega_miya/plugins/repeater/__init__.py b/omega_miya/plugins/repeater/__init__.py index 7a0041c4..800ece7a 100644 --- a/omega_miya/plugins/repeater/__init__.py +++ b/omega_miya/plugins/repeater/__init__.py @@ -1,14 +1,28 @@ from typing import Dict from nonebot import on_message +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.exception import FinishedException from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import GroupMessageEvent from nonebot.adapters.cqhttp.permission import GROUP -from omega_miya.utils.Omega_plugin_utils import OmegaRules +from omega_miya.utils.omega_plugin_utils import init_export, OmegaRules from .data_source import REPLY_RULES +# Custom plugin usage text +__plugin_custom_name__ = '复读姬' +__plugin_usage__ = r'''【复读姬】 +自动回复与复读 + +**Permission** +Command Group''' + + +# Init plugin export +init_export(export(), __plugin_custom_name__, __plugin_usage__) + + LAST_MSG: Dict[int, str] = {} LAST_REPEAT_MSG: Dict[int, str] = {} REPEAT_COUNT: Dict[int, int] = {} @@ -63,7 +77,7 @@ async def handle_repeater(bot: Bot, event: GroupMessageEvent, state: T_State): LAST_REPEAT_MSG[group_id] = '' # 当复读计数等于2时说明已经有连续三条同样的消息了, 此时触发复读, 更新上次服务消息LAST_REPEAT_MSG, 并重置复读计数 if REPEAT_COUNT[group_id] >= 2: - await repeater.send(message) REPEAT_COUNT[group_id] = 0 LAST_MSG[group_id] = '' LAST_REPEAT_MSG[group_id] = raw_msg + await repeater.send(message) diff --git a/omega_miya/plugins/repeater/data_source.py b/omega_miya/plugins/repeater/data_source.py index 42071a2f..764bc561 100644 --- a/omega_miya/plugins/repeater/data_source.py +++ b/omega_miya/plugins/repeater/data_source.py @@ -13,6 +13,7 @@ import pathlib from dataclasses import dataclass from typing import Dict, List, Tuple, Union +from nonebot import logger from nonebot.adapters.cqhttp.message import Message, MessageSegment RESOURCE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), 'resources')) @@ -58,7 +59,8 @@ def check_rule(self, group_id: int, message: str) -> Tuple[bool, Union[str, Mess reply_msg = reply.reply_msg # 按顺序匹配中立即返回, 忽略后续规则 return True, reply_msg - except Exception: + except Exception as e: + logger.debug(f'Checking reply: "{reply}" with rules: "{regular}" failed, error: {repr(e)}') continue return False, '' diff --git a/omega_miya/plugins/roll/__init__.py b/omega_miya/plugins/roll/__init__.py index d112776d..e4578379 100644 --- a/omega_miya/plugins/roll/__init__.py +++ b/omega_miya/plugins/roll/__init__.py @@ -1,15 +1,16 @@ import re import random -from nonebot import CommandGroup, export +from nonebot import CommandGroup +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import GroupMessageEvent from nonebot.adapters.cqhttp.permission import GROUP -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state # Custom plugin usage text -__plugin_name__ = 'roll' +__plugin_custom_name__ = 'Roll' __plugin_usage__ = r'''【Roll & 抽奖】 一个整合了各种roll机制的插件 更多功能待加入 @@ -26,28 +27,23 @@ /roll d /抽奖 <人数>''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) Roll = CommandGroup( - 'R0ll', + 'Roll', # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='roll', command=True, - level=10, - auth_node='basic'), + level=10), permission=GROUP, priority=10, block=True) -roll = Roll.command('rand', aliases={'Roll', 'roll'}) +roll = Roll.command('rand', aliases={'roll'}) # 修改默认参数处理 diff --git a/omega_miya/plugins/schedule_message/__init__.py b/omega_miya/plugins/schedule_message/__init__.py index 0da97877..a4d78206 100644 --- a/omega_miya/plugins/schedule_message/__init__.py +++ b/omega_miya/plugins/schedule_message/__init__.py @@ -12,19 +12,20 @@ import re from datetime import datetime from apscheduler.schedulers.asyncio import AsyncIOScheduler -from nonebot import MatcherGroup, export, logger, require +from nonebot import MatcherGroup, logger, require +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.permission import SUPERUSER from nonebot.adapters.cqhttp.permission import GROUP_ADMIN, GROUP_OWNER from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.message import Message from nonebot.adapters.cqhttp.event import GroupMessageEvent -from omega_miya.utils.Omega_Base import DBBot, DBBotGroup, Result -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state +from omega_miya.database import DBBot, DBBotGroup, Result +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state # Custom plugin usage text -__plugin_name__ = '定时消息' +__plugin_custom_name__ = '定时消息' __plugin_usage__ = r'''【定时消息】 设置群组定时通知消息 仅限群聊使用 @@ -42,13 +43,10 @@ /查看定时消息 /删除定时消息''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) + driver = nonebot.get_driver() scheduler: AsyncIOScheduler = require("nonebot_plugin_apscheduler").scheduler @@ -57,11 +55,10 @@ ScheduleMsg = MatcherGroup( type='message', # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='schedule_message', command=True, - level=10, - auth_node='basic'), + level=10), permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER, priority=10, block=True) diff --git a/omega_miya/plugins/search_anime/__init__.py b/omega_miya/plugins/search_anime/__init__.py index 96c83289..0294799e 100644 --- a/omega_miya/plugins/search_anime/__init__.py +++ b/omega_miya/plugins/search_anime/__init__.py @@ -1,15 +1,16 @@ -from nonebot import on_command, export, logger +from nonebot import on_command, logger +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent from nonebot.adapters.cqhttp.permission import GROUP, PRIVATE_FRIEND from nonebot.adapters.cqhttp import MessageSegment, Message -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state, PicEncoder +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state, PicEncoder from .utils import get_identify_result # Custom plugin usage text -__plugin_name__ = '识番' +__plugin_custom_name__ = '识番' __plugin_usage__ = r'''【识番助手】 使用 trace.moe API 识别番剧 群组/私聊可用 @@ -25,13 +26,9 @@ **Usage** /识番''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) # 注册事件响应器 @@ -39,11 +36,10 @@ '识番', aliases={'搜番', '番剧识别'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='search_anime', command=True, - level=50, - auth_node='basic'), + level=50), permission=GROUP | PRIVATE_FRIEND, priority=20, block=True) diff --git a/omega_miya/plugins/search_anime/utils.py b/omega_miya/plugins/search_anime/utils.py index 437a30e1..983ce751 100644 --- a/omega_miya/plugins/search_anime/utils.py +++ b/omega_miya/plugins/search_anime/utils.py @@ -1,6 +1,6 @@ from nonebot import logger -from omega_miya.utils.Omega_plugin_utils import HttpFetcher -from omega_miya.utils.Omega_Base import Result +from omega_miya.utils.omega_plugin_utils import HttpFetcher +from omega_miya.database import Result API_URL = 'https://api.trace.moe/search' @@ -33,7 +33,7 @@ # 获取识别结果 -async def get_identify_result(img_url: str, *, sensitivity: float = 0.8) -> Result.ListResult: +async def get_identify_result(img_url: str, *, sensitivity: float = 0.85) -> Result.ListResult: fetcher = HttpFetcher(timeout=10, flag='search_anime', headers=HEADERS) payload = {'url': img_url} diff --git a/omega_miya/plugins/search_image/__init__.py b/omega_miya/plugins/search_image/__init__.py index f07e6fcd..01516245 100644 --- a/omega_miya/plugins/search_image/__init__.py +++ b/omega_miya/plugins/search_image/__init__.py @@ -1,13 +1,15 @@ import random import asyncio -from nonebot import on_command, export, logger, get_driver +from nonebot import on_command, logger, get_driver +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent from nonebot.adapters.cqhttp.permission import GROUP, PRIVATE_FRIEND from nonebot.adapters.cqhttp import MessageSegment, Message -from omega_miya.utils.Omega_Base import DBBot -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state, PicEncoder, PermissionChecker +from omega_miya.database import DBBot +from omega_miya.utils.omega_plugin_utils import (init_export, init_processor_state, + PicEncoder, PermissionChecker, PluginCoolDown) from omega_miya.utils.pixiv_utils import PixivIllust from .utils import SEARCH_ENGINE, HEADERS from .config import Config @@ -19,7 +21,7 @@ ENABLE_ASCII2D = plugin_config.enable_ascii2d # Custom plugin usage text -__plugin_name__ = '识图' +__plugin_custom_name__ = '识图' __plugin_usage__ = r'''【识图助手】 使用SauceNAO/ascii2d识别各类图片、插画 群组/私聊可用 @@ -38,26 +40,25 @@ **Hidden Command** /再来点''' -# 声明本插件可配置的权限节点 +# 声明本插件额外可配置的权限节点 __plugin_auth_node__ = [ - 'basic', 'recommend_image', 'allow_recommend_r18' ] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__, __plugin_auth_node__) + # 注册事件响应器 search_image = on_command( '识图', aliases={'搜图'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='search_image', command=True, - level=50, - auth_node='basic'), + level=50), permission=GROUP | PRIVATE_FRIEND, priority=20, block=True) @@ -210,10 +211,12 @@ async def handle_result(bot: Bot, event: MessageEvent, state: T_State): '再来点', aliases={'多来点', '相似作品', '类似作品'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='search_image_recommend_image', command=True, - auth_node='recommend_image'), + auth_node='recommend_image', + cool_down=[PluginCoolDown(PluginCoolDown.group_type, 90)] + ), permission=GROUP | PRIVATE_FRIEND, priority=20, block=True) @@ -234,7 +237,7 @@ async def handle_first_receive(bot: Bot, event: GroupMessageEvent, state: T_Stat return # 若消息被分片可能导致链接被拆分 - raw_text = event.reply.dict().get('raw_message') + raw_text = getattr(event.reply, 'raw_message', None) if pid := PixivIllust.parse_pid_from_url(text=raw_text): state['pid'] = pid logger.debug(f"Recommend image | 已从消息 raw 文本匹配到 pixiv url, pid: {pid}") diff --git a/omega_miya/plugins/search_image/utils.py b/omega_miya/plugins/search_image/utils.py index 9a0b7b65..f9ee3d37 100644 --- a/omega_miya/plugins/search_image/utils.py +++ b/omega_miya/plugins/search_image/utils.py @@ -3,8 +3,8 @@ import nonebot from bs4 import BeautifulSoup from nonebot import logger -from omega_miya.utils.Omega_plugin_utils import HttpFetcher -from omega_miya.utils.Omega_Base import Result +from omega_miya.utils.omega_plugin_utils import HttpFetcher +from omega_miya.database import Result global_config = nonebot.get_driver().config diff --git a/omega_miya/plugins/setu/__init__.py b/omega_miya/plugins/setu/__init__.py index 362d352a..afc20a3a 100644 --- a/omega_miya/plugins/setu/__init__.py +++ b/omega_miya/plugins/setu/__init__.py @@ -2,17 +2,18 @@ import re import asyncio import aiofiles -from nonebot import CommandGroup, on_command, export, get_driver, logger +from nonebot import CommandGroup, on_command, get_driver, logger +from nonebot.plugin.export import export from nonebot.rule import to_me from nonebot.permission import SUPERUSER from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot -from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent +from nonebot.adapters.cqhttp.event import Event, MessageEvent, GroupMessageEvent, PrivateMessageEvent from nonebot.adapters.cqhttp.permission import GROUP, PRIVATE_FRIEND from nonebot.adapters.cqhttp import MessageSegment -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state, PluginCoolDown, PermissionChecker -from omega_miya.utils.Omega_plugin_utils import PicEncoder, PicEffector, MsgSender, ProcessUtils -from omega_miya.utils.Omega_Base import DBBot, DBPixivillust +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state, PluginCoolDown, PermissionChecker +from omega_miya.utils.omega_plugin_utils import PicEncoder, PicEffector, MsgSender, ProcessUtils +from omega_miya.database import DBBot, DBPixivillust from omega_miya.utils.pixiv_utils import PixivIllust from .utils import add_illust from .config import Config @@ -20,6 +21,7 @@ __global_config = get_driver().config plugin_config = Config(**__global_config.dict()) +ACC_MODE = plugin_config.acc_mode IMAGE_NUM_LIMIT = plugin_config.image_num_limit ENABLE_NODE_CUSTOM = plugin_config.enable_node_custom ENABLE_MOE_FLASH = plugin_config.enable_moe_flash @@ -32,7 +34,7 @@ # Custom plugin usage text -__plugin_name__ = '来点萌图' +__plugin_custom_name__ = '来点萌图' __plugin_usage__ = r'''【来点萌图】 测试群友LSP成分 群组/私聊可用 @@ -62,22 +64,15 @@ /图库查询 [*keywords] /导入图库''' -# 声明本插件可配置的权限节点 +# 声明本插件额外可配置的权限节点 __plugin_auth_node__ = [ - PluginCoolDown.skip_auth_node, 'setu', 'allow_r18', 'moepic' ] -# 声明本插件的冷却时间配置 -__plugin_cool_down__ = [ - PluginCoolDown(PluginCoolDown.user_type, 2), - PluginCoolDown(PluginCoolDown.group_type, 1) -] - # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__, __plugin_cool_down__) +init_export(export(), __plugin_custom_name__, __plugin_usage__, __plugin_auth_node__) # 注册事件响应器 @@ -87,11 +82,15 @@ 'setu', aliases={'来点涩图'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='setu', command=True, level=50, - auth_node='setu')) + auth_node='setu', + cool_down=[ + PluginCoolDown(PluginCoolDown.user_type, 180), + PluginCoolDown(PluginCoolDown.group_type, 120) + ])) @setu.handle() @@ -118,23 +117,14 @@ async def handle_setu(bot: Bot, event: MessageEvent, state: T_State): # 处理R18权限 if nsfw_tag > 1: - if isinstance(event, PrivateMessageEvent): - user_id = event.user_id - auth_checker = await PermissionChecker(self_bot=DBBot(self_qq=int(bot.self_id))). \ - check_auth_node(auth_id=user_id, auth_type='user', auth_node='setu.allow_r18') - elif isinstance(event, GroupMessageEvent): - group_id = event.group_id - auth_checker = await PermissionChecker(self_bot=DBBot(self_qq=int(bot.self_id))). \ - check_auth_node(auth_id=group_id, auth_type='group', auth_node='setu.allow_r18') - else: - auth_checker = 0 - + auth_checker = await __handle_r18_perm(bot=bot, event=event) if auth_checker != 1: logger.warning(f"User: {event.user_id} 请求涩图被拒绝, 没有 allow_r18 权限") await setu.finish('R18禁止! 不准开车车!') if tags: - pid_res = await DBPixivillust.list_illust(keywords=tags, num=IMAGE_NUM_LIMIT, nsfw_tag=nsfw_tag) + pid_res = await DBPixivillust.list_illust( + keywords=tags, num=IMAGE_NUM_LIMIT, nsfw_tag=nsfw_tag, acc_mode=ACC_MODE) pid_list = pid_res.result else: # 没有tag则随机获取 @@ -145,11 +135,10 @@ async def handle_setu(bot: Bot, event: MessageEvent, state: T_State): logger.info(f"{group_id} / {event.user_id} 没有找到他/她想要的涩图") await setu.finish('找不到涩图QAQ') await setu.send('稍等, 正在下载图片~') - # 处理article中图片内容 - tasks = [] - for pid in pid_list: - tasks.append(PixivIllust(pid=pid).load_illust_pic()) - p_res = await asyncio.gather(*tasks) + + # 处理下载图片 + tasks = [PixivIllust(pid=pid).load_illust_pic() for pid in pid_list] + p_res = await ProcessUtils.fragment_process(tasks=tasks, log_flag='load_setu') # 处理图片消息段, 之后再根据ENABLE_NODE_CUSTOM确定消息发送方式 fault_count = 0 @@ -223,11 +212,15 @@ async def handle_setu(bot: Bot, event: MessageEvent, state: T_State): 'moepic', aliases={'来点萌图'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='moepic', command=True, level=50, - auth_node='moepic')) + auth_node='moepic', + cool_down=[ + PluginCoolDown(PluginCoolDown.user_type, 120), + PluginCoolDown(PluginCoolDown.group_type, 60) + ])) @moepic.handle() @@ -250,7 +243,7 @@ async def handle_moepic(bot: Bot, event: MessageEvent, state: T_State): tags = state['tags'] if tags: - pid_res = await DBPixivillust.list_illust(keywords=tags, num=IMAGE_NUM_LIMIT, nsfw_tag=0) + pid_res = await DBPixivillust.list_illust(keywords=tags, num=IMAGE_NUM_LIMIT, nsfw_tag=0, acc_mode=ACC_MODE) pid_list = pid_res.result else: # 没有tag则随机获取 @@ -262,11 +255,10 @@ async def handle_moepic(bot: Bot, event: MessageEvent, state: T_State): await moepic.finish('找不到萌图QAQ') await moepic.send('稍等, 正在下载图片~') - # 处理article中图片内容 - tasks = [] - for pid in pid_list: - tasks.append(PixivIllust(pid=pid).load_illust_pic()) - p_res = await asyncio.gather(*tasks) + + # 处理下载图片 + tasks = [PixivIllust(pid=pid).load_illust_pic() for pid in pid_list] + p_res = await ProcessUtils.fragment_process(tasks=tasks, log_flag='load_moepic') # 处理图片消息段, 之后再根据ENABLE_NODE_CUSTOM确定消息发送方式 fault_count = 0 @@ -439,3 +431,18 @@ async def handle_setu_import(bot: Bot, event: MessageEvent, state: T_State): logger.info(f'setu_import: 导入操作已完成, 成功: {success_count}, 总计: {all_count}') await setu_import.send(f'导入操作已完成, 成功: {success_count}, 总计: {all_count}') + + +# 处理 setu 插件 r18 权限 +async def __handle_r18_perm(bot: Bot, event: Event) -> int: + if isinstance(event, PrivateMessageEvent): + user_id = event.user_id + auth_checker = await PermissionChecker(self_bot=DBBot(self_qq=int(bot.self_id))). \ + check_auth_node(auth_id=user_id, auth_type='user', auth_node='setu.allow_r18') + elif isinstance(event, GroupMessageEvent): + group_id = event.group_id + auth_checker = await PermissionChecker(self_bot=DBBot(self_qq=int(bot.self_id))). \ + check_auth_node(auth_id=group_id, auth_type='group', auth_node='setu.allow_r18') + else: + auth_checker = 0 + return auth_checker diff --git a/omega_miya/plugins/setu/config.py b/omega_miya/plugins/setu/config.py index 5005c9ee..c11561b6 100644 --- a/omega_miya/plugins/setu/config.py +++ b/omega_miya/plugins/setu/config.py @@ -13,6 +13,9 @@ class Config(BaseSettings): # plugin custom config + # 查询图片模式 + # 启用精确匹配可能导致结果减少 + acc_mode: bool = False # 每次查询的图片数量限制 image_num_limit: int = 3 # 启用使用群组转发自定义消息节点的模式发送信息, 仅在群组消息中生效 diff --git a/omega_miya/plugins/setu/utils.py b/omega_miya/plugins/setu/utils.py index 14a811ad..20ee3643 100644 --- a/omega_miya/plugins/setu/utils.py +++ b/omega_miya/plugins/setu/utils.py @@ -1,5 +1,5 @@ from nonebot import logger -from omega_miya.utils.Omega_Base import DBPixivillust, Result +from omega_miya.database import DBPixivillust, Result from omega_miya.utils.pixiv_utils import PixivIllust diff --git a/omega_miya/plugins/shindan_maker/__init__.py b/omega_miya/plugins/shindan_maker/__init__.py index 5a4de643..59b412ac 100644 --- a/omega_miya/plugins/shindan_maker/__init__.py +++ b/omega_miya/plugins/shindan_maker/__init__.py @@ -11,17 +11,18 @@ import re import datetime from typing import Dict -from nonebot import MatcherGroup, export, logger +from nonebot import MatcherGroup, logger +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent from nonebot.adapters.cqhttp.permission import GROUP -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state, PluginCoolDown, OmegaRules +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state, PluginCoolDown, OmegaRules from .data_source import ShindanMaker # Custom plugin usage text -__plugin_name__ = 'ShindanMaker' +__plugin_custom_name__ = 'ShindanMaker' __plugin_usage__ = r'''【ShindanMaker 占卜】 使用ShindanMaker进行各种奇怪的占卜 只能在群里使用 @@ -37,20 +38,9 @@ **Usage** /ShindanMaker [占卜名称] [占卜对象名称]''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - PluginCoolDown.skip_auth_node, - 'basic' -] - -# # 声明本插件的冷却时间配置 -# __plugin_cool_down__ = [ -# PluginCoolDown(PluginCoolDown.user_type, 1), -# PluginCoolDown(PluginCoolDown.group_type, 1) -# ] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) # 缓存占卜名称与对应id @@ -61,11 +51,10 @@ shindan_maker = MatcherGroup( type='message', # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='shindan_maker', command=True, - level=30, - auth_node='basic'), + level=30), permission=GROUP, priority=20, block=True) diff --git a/omega_miya/plugins/shindan_maker/data_source.py b/omega_miya/plugins/shindan_maker/data_source.py index 6fd7da06..43821f23 100644 --- a/omega_miya/plugins/shindan_maker/data_source.py +++ b/omega_miya/plugins/shindan_maker/data_source.py @@ -11,8 +11,8 @@ from nonebot import logger from bs4 import BeautifulSoup -from omega_miya.utils.Omega_Base import Result -from omega_miya.utils.Omega_plugin_utils import HttpFetcher +from omega_miya.database import Result +from omega_miya.utils.omega_plugin_utils import HttpFetcher class ShindanMaker(object): diff --git a/omega_miya/plugins/sticker_maker/__init__.py b/omega_miya/plugins/sticker_maker/__init__.py index d78f1887..1547dc36 100644 --- a/omega_miya/plugins/sticker_maker/__init__.py +++ b/omega_miya/plugins/sticker_maker/__init__.py @@ -1,17 +1,18 @@ import re import pathlib -from nonebot import on_command, export, logger +from nonebot import on_command, logger +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent from nonebot.adapters.cqhttp.permission import GROUP, PRIVATE_FRIEND from nonebot.adapters.cqhttp import MessageSegment -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state, PicEncoder +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state, PicEncoder from .utils import sticker_maker_main # Custom plugin usage text -__plugin_name__ = '表情包' +__plugin_custom_name__ = '表情包' __plugin_usage__ = r'''【表情包助手】 使用模板快速制作表情包 群组/私聊可用 @@ -27,24 +28,19 @@ **Usage** /表情包 [模板名]''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) sticker = on_command( '表情包', aliases={'Sticker', 'sticker'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='sticker', command=True, - level=10, - auth_node='basic'), + level=10), permission=GROUP | PRIVATE_FRIEND, priority=10, block=True) @@ -83,6 +79,8 @@ async def handle_sticker(bot: Bot, event: MessageEvent, state: T_State): '生草日语': {'name': 'grassja', 'type': 'default', 'text_part': 1, 'help_msg': '该模板不支持gif'}, '小天使': {'name': 'littleangel', 'type': 'default', 'text_part': 1, 'help_msg': '该模板不支持gif'}, '有内鬼': {'name': 'traitor', 'type': 'static', 'text_part': 1, 'help_msg': '该模板字数限制100(x)'}, + '鲁迅说': {'name': 'luxunshuo', 'type': 'static', 'text_part': 1, 'help_msg': '该模板字数限制100(x)'}, + '鲁迅写': {'name': 'luxunxie', 'type': 'static', 'text_part': 1, 'help_msg': '该模板字数限制100(x)'}, '记仇': {'name': 'jichou', 'type': 'static', 'text_part': 1, 'help_msg': '该模板字数限制100(x)'}, 'ph': {'name': 'phlogo', 'type': 'static', 'text_part': 1, 'help_msg': '两部分文字中间请用空格隔开'}, 'petpet': {'name': 'petpet', 'type': 'gif', 'text_part': 0, 'help_msg': '最好使用长宽比接近正方形的图片'} diff --git a/omega_miya/plugins/sticker_maker/utils/__init__.py b/omega_miya/plugins/sticker_maker/utils/__init__.py index 4f8d1b1b..ebc487d8 100644 --- a/omega_miya/plugins/sticker_maker/utils/__init__.py +++ b/omega_miya/plugins/sticker_maker/utils/__init__.py @@ -3,7 +3,7 @@ from typing import Optional from io import BytesIO from datetime import datetime -from omega_miya.utils.Omega_plugin_utils import HttpFetcher +from omega_miya.utils.omega_plugin_utils import HttpFetcher from nonebot import logger, get_driver from PIL import Image from .default_render import * @@ -12,6 +12,7 @@ global_config = get_driver().config TMP_PATH = global_config.tmp_path_ +RESOURCES_PATH = global_config.resources_path_ HEADERS = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' 'Chrome/89.0.4389.114 Safari/537.36'} @@ -27,6 +28,8 @@ async def sticker_maker_main(url: str, temp: str, text: str, sticker_temp_type: 'grassja': stick_maker_temp_grass_ja, 'littleangel': stick_maker_temp_littleangel, 'traitor': stick_maker_static_traitor, + 'luxunshuo': stick_maker_static_luxun, + 'luxunxie': stick_maker_static_luxun, 'jichou': stick_maker_static_jichou, 'phlogo': stick_maker_static_phlogo, 'petpet': stick_maker_temp_petpet @@ -41,6 +44,8 @@ async def sticker_maker_main(url: str, temp: str, text: str, sticker_temp_type: 'grassja': 'fzzxhk.ttf', 'littleangel': 'msyhbd.ttc', 'traitor': 'pixel.ttf', + 'luxunshuo': 'SourceHanSans_Regular.otf', + 'luxunxie': 'SourceHanSans_Regular.otf', 'jichou': 'SourceHanSans_Regular.otf', 'phlogo': 'SourceHanSans_Heavy.otf', 'petpet': 'SourceHanSans_Regular.otf' @@ -55,6 +60,8 @@ async def sticker_maker_main(url: str, temp: str, text: str, sticker_temp_type: 'grassja': 800, 'littleangel': 512, 'traitor': 512, + 'luxunshuo': 512, + 'luxunxie': 512, 'jichou': 512, 'phlogo': 512, 'petpet': 512 @@ -69,7 +76,7 @@ async def sticker_maker_main(url: str, temp: str, text: str, sticker_temp_type: plugin_src_path = os.path.abspath(os.path.dirname(__file__)) # 字体路径 - font_path = os.path.join(plugin_src_path, 'fonts', sticker_default_font.get(temp)) + font_path = os.path.abspath(os.path.join(RESOURCES_PATH, 'fonts', sticker_default_font.get(temp))) # 检查预置字体 if not os.path.exists(font_path): logger.error(f"Stick_maker: 模板预置文件错误, 字体{sticker_default_font.get(temp)}不存在") diff --git a/omega_miya/plugins/sticker_maker/utils/default_render.py b/omega_miya/plugins/sticker_maker/utils/default_render.py index 54804cd8..73fe27b4 100644 --- a/omega_miya/plugins/sticker_maker/utils/default_render.py +++ b/omega_miya/plugins/sticker_maker/utils/default_render.py @@ -4,42 +4,31 @@ async def stick_maker_temp_default( - text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int) -> Image.Image: + text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int, + *args, **kwargs) -> Image.Image: """ 默认加字表情包模板 """ def __handle() -> Image.Image: # 处理图片 draw = ImageDraw.Draw(image_file) - font_size = 72 + font_size = image_wight // 8 + text_stroke_width = int(font_size / 20) font = ImageFont.truetype(font_path, font_size) - text_w, text_h = font.getsize_multiline(text) - while text_w >= image_wight: - font_size = font_size * 3 // 4 + + text_w, text_h = font.getsize_multiline(text, stroke_width=text_stroke_width) + while text_w >= int(image_wight * 0.95): + font_size = font_size * 7 // 8 font = ImageFont.truetype(font_path, font_size) - text_w, text_h = font.getsize_multiline(text) + text_w, text_h = font.getsize_multiline(text, stroke_width=text_stroke_width) # 计算居中文字位置 - text_coordinate = (((image_wight - text_w) // 2), 9 * (image_height - text_h) // 10) - # 为文字设置黑边 - text_b_resize = 4 - if font_size >= 72: - text_b_resize = 4 - elif font_size >= 36: - text_b_resize = 3 - elif font_size >= 24: - text_b_resize = 2 - elif font_size < 12: - text_b_resize = 1 - text_coordinate_b1 = (text_coordinate[0] + text_b_resize, text_coordinate[1]) - text_coordinate_b2 = (text_coordinate[0] - text_b_resize, text_coordinate[1]) - text_coordinate_b3 = (text_coordinate[0], text_coordinate[1] + text_b_resize) - text_coordinate_b4 = (text_coordinate[0], text_coordinate[1] - text_b_resize) - draw.multiline_text(text_coordinate_b1, text, font=font, fill=(0, 0, 0)) - draw.multiline_text(text_coordinate_b2, text, font=font, fill=(0, 0, 0)) - draw.multiline_text(text_coordinate_b3, text, font=font, fill=(0, 0, 0)) - draw.multiline_text(text_coordinate_b4, text, font=font, fill=(0, 0, 0)) - # 白字要后画,后画的在上层,不然就是黑滋在上面挡住了 - draw.multiline_text(text_coordinate, text, font=font, fill=(255, 255, 255)) + text_coordinate = (image_wight // 2, 9 * (image_height - text_h) // 10) + draw.multiline_text(xy=text_coordinate, + text=text, + font=font, fill=(255, 255, 255), + align='center', anchor='ma', + stroke_width=text_stroke_width, + stroke_fill=(0, 0, 0)) return image_file loop = asyncio.get_running_loop() @@ -48,38 +37,39 @@ def __handle() -> Image.Image: async def stick_maker_temp_littleangel( - text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int) -> Image.Image: + text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int, + *args, **kwargs) -> Image.Image: """ 小天使表情包模板 """ def __handle() -> Image.Image: # 处理图片 background_w = image_wight + 100 - background_h = image_height + 230 + background_h = image_height + 240 background = Image.new(mode="RGB", size=(background_w, background_h), color=(255, 255, 255)) - # 处理粘贴位置 上留100像素,下留130像素 - image_coordinate = (((background_w - image_wight) // 2), 100) + # 处理粘贴位置 上留110像素,下留130像素 + image_coordinate = (((background_w - image_wight) // 2), 110) background.paste(image_file, image_coordinate) draw = ImageDraw.Draw(background) font_down_1 = ImageFont.truetype(font_path, 48) text_down_1 = r'非常可爱!简直就是小天使' text_down_1_w, text_down_1_h = font_down_1.getsize(text_down_1) - text_down_1_coordinate = (((background_w - text_down_1_w) // 2), background_h - 120) + text_down_1_coordinate = (((background_w - text_down_1_w) // 2), background_h - 110) draw.text(text_down_1_coordinate, text_down_1, font=font_down_1, fill=(0, 0, 0)) font_down_2 = ImageFont.truetype(font_path, 26) text_down_2 = r'她没失踪也没怎么样 我只是觉得你们都该看一下' text_down_2_w, text_down_2_h = font_down_2.getsize(text_down_2) - text_down_2_coordinate = (((background_w - text_down_2_w) // 2), background_h - 60) + text_down_2_coordinate = (((background_w - text_down_2_w) // 2), background_h - 50) draw.text(text_down_2_coordinate, text_down_2, font=font_down_2, fill=(0, 0, 0)) font_size_up = 72 font_up = ImageFont.truetype(font_path, font_size_up) text_up = f'请问你们看到{text}了吗?' text_up_w, text_up_h = font_up.getsize(text_up) - while text_up_w >= background_w: - font_size_up = font_size_up * 5 // 6 + while text_up_w >= int(background_w * 0.95): + font_size_up = font_size_up * 9 // 10 font_up = ImageFont.truetype(font_path, font_size_up) text_up_w, text_up_h = font_up.getsize(text_up) # 计算居中文字位置 @@ -93,7 +83,8 @@ def __handle() -> Image.Image: async def stick_maker_temp_whitebg( - text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int) -> Image.Image: + text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int, + *args, **kwargs) -> Image.Image: """ 白底加字表情包模板 """ @@ -131,7 +122,8 @@ def __handle() -> Image.Image: async def stick_maker_temp_blackbg( - text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int) -> Image.Image: + text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int, + *args, **kwargs) -> Image.Image: """ 黑边加底字表情包模板 """ @@ -178,7 +170,8 @@ def __handle() -> Image.Image: async def stick_maker_temp_decolorize( - text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int) -> Image.Image: + text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int, + *args, **kwargs) -> Image.Image: """ 去色加字表情包模板 """ @@ -193,7 +186,8 @@ def __handle() -> Image.Image: async def stick_maker_temp_grass_ja( - text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int) -> Image.Image: + text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int, + *args, **kwargs) -> Image.Image: """ 生草日语表情包模板 """ diff --git a/omega_miya/plugins/sticker_maker/utils/gif_render.py b/omega_miya/plugins/sticker_maker/utils/gif_render.py index 3414cf60..4cb6acde 100644 --- a/omega_miya/plugins/sticker_maker/utils/gif_render.py +++ b/omega_miya/plugins/sticker_maker/utils/gif_render.py @@ -22,7 +22,9 @@ async def stick_maker_temp_petpet( font_path: str, image_wight: int, image_height: int, - temp_path: str) -> Optional[bytes]: + temp_path: str, + *args, + **kwargs) -> Optional[bytes]: """ petpet 表情包模板 """ diff --git a/omega_miya/plugins/sticker_maker/utils/static/luxunshuo/default_bg.png b/omega_miya/plugins/sticker_maker/utils/static/luxunshuo/default_bg.png new file mode 100644 index 00000000..3973e338 Binary files /dev/null and b/omega_miya/plugins/sticker_maker/utils/static/luxunshuo/default_bg.png differ diff --git a/omega_miya/plugins/sticker_maker/utils/static/luxunxie/default_bg.png b/omega_miya/plugins/sticker_maker/utils/static/luxunxie/default_bg.png new file mode 100644 index 00000000..c3d5f8e6 Binary files /dev/null and b/omega_miya/plugins/sticker_maker/utils/static/luxunxie/default_bg.png differ diff --git a/omega_miya/plugins/sticker_maker/utils/static_render.py b/omega_miya/plugins/sticker_maker/utils/static_render.py index cfdb77a0..4247a7d4 100644 --- a/omega_miya/plugins/sticker_maker/utils/static_render.py +++ b/omega_miya/plugins/sticker_maker/utils/static_render.py @@ -1,10 +1,12 @@ import asyncio from PIL import Image, ImageDraw, ImageFont from datetime import date +from omega_miya.utils.omega_plugin_utils import TextUtils async def stick_maker_static_traitor( - text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int) -> Image.Image: + text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int, + *args, **kwargs) -> Image.Image: """ 有内鬼表情包模板 """ @@ -20,21 +22,10 @@ def __handle() -> Image.Image: # 处理文字层 主体部分 text_main_img = Image.new(mode="RGBA", size=(image_wight, image_height), color=(0, 0, 0, 0)) - font_main_size = 54 + font_main_size = 52 font_main = ImageFont.truetype(font_path, font_main_size) # 按长度切分文本 - spl_num = 0 - spl_list = [] - for num in range(len(text)): - text_w = font_main.getsize_multiline(text[spl_num:num])[0] - if text_w >= 415: - spl_list.append(text[spl_num:num]) - spl_num = num - else: - spl_list.append(text[spl_num:]) - test_main_fin = '' - for item in spl_list: - test_main_fin += item + '\n' + test_main_fin = TextUtils(text=text).split_multiline(width=410, font=font_main) ImageDraw.Draw(text_main_img).multiline_text(xy=(0, 0), text=test_main_fin, font=font_main, spacing=8, fill=(0, 0, 0)) @@ -54,26 +45,18 @@ def __handle() -> Image.Image: async def stick_maker_static_jichou( - text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int) -> Image.Image: + text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int, + *args, **kwargs) -> Image.Image: """ 记仇表情包模板 """ def __handle() -> Image.Image: # 处理文本主体 - text_ = f"今天是{date.today().strftime('%Y年%m月%d日')}{text}, 这个仇我先记下了" + text_ = f"今天是{date.today().strftime('%Y年%m月%d日')}, {text}, 这个仇我先记下了" font_main_size = 42 font_main = ImageFont.truetype(font_path, font_main_size) # 按长度切分文本 - spl_num = 0 - spl_list = [] - for num in range(len(text_)): - text_w = font_main.getsize_multiline(text_[spl_num:num])[0] - if text_w >= (image_wight * 7 // 8): - spl_list.append(text_[spl_num:num]) - spl_num = num - else: - spl_list.append(text_[spl_num:]) - text_main_fin = '\n'.join(spl_list) + text_main_fin = TextUtils(text=text_).split_multiline(width=(image_wight * 7 // 8), font=font_main) font = ImageFont.truetype(font_path, font_main_size) text_w, text_h = font.getsize_multiline(text_main_fin) @@ -99,7 +82,8 @@ def __handle() -> Image.Image: async def stick_maker_static_phlogo( - text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int) -> Image.Image: + text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int, + *args, **kwargs) -> Image.Image: """ ph表情包模板 """ @@ -111,44 +95,105 @@ def __handle() -> Image.Image: font_size = 640 font = ImageFont.truetype(font_path, font_size) - text_w, text_h = font.getsize(text) - - y_text_w, y_text_h = font.getsize(yellow_text) - bg_y_text = Image.new(mode="RGB", size=(round(y_text_w * 1.1), round(text_h * 1.3)), color=(254, 154, 0)) - draw_y_text = ImageDraw.Draw(bg_y_text) - draw_y_text.text((round(y_text_w * 1.1) // 2, round(text_h * 1.3) // 2), - yellow_text, anchor='mm', font=font, fill=(0, 0, 0)) - radii = 64 - # 画圆(用于分离4个角) - circle = Image.new('L', (radii * 2, radii * 2), 0) # 创建黑色方形 - draw_circle = ImageDraw.Draw(circle) - draw_circle.ellipse((0, 0, radii * 2, radii * 2), fill=255) # 黑色方形内切白色圆形 - # 原图转为带有alpha通道(表示透明程度) - bg_y_text = bg_y_text.convert("RGBA") - y_weight, y_height = bg_y_text.size - # 画4个角(将整圆分离为4个部分) - alpha = Image.new('L', bg_y_text.size, 255) # 与img同大小的白色矩形,L 表示黑白图 - alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0)) # 左上角 - alpha.paste(circle.crop((radii, 0, radii * 2, radii)), (y_weight - radii, 0)) # 右上角 - alpha.paste(circle.crop((radii, radii, radii * 2, radii * 2)), (y_weight - radii, y_height - radii)) # 右下角 - alpha.paste(circle.crop((0, radii, radii, radii * 2)), (0, y_height - radii)) # 左下角 - bg_y_text.putalpha(alpha) # 白色区域透明可见,黑色区域不可见 - - w_text_w, w_text_h = font.getsize(white_text) - bg_w_text = Image.new(mode="RGB", size=(round(w_text_w * 1.05), round(text_h * 1.3)), color=(0, 0, 0)) - w_weight, w_height = bg_w_text.size - draw_w_text = ImageDraw.Draw(bg_w_text) - draw_w_text.text((round(w_text_w * 1.025) // 2, round(text_h * 1.3) // 2), - white_text, anchor='mm', font=font, fill=(255, 255, 255)) - - text_bg = Image.new(mode="RGB", size=(w_weight + y_weight, y_height), color=(0, 0, 0)) - text_bg.paste(bg_w_text, (0, 0)) - text_bg.paste(bg_y_text, (round(w_text_w * 1.05), 0), mask=alpha) - t_weight, t_height = text_bg.size - - background = Image.new(mode="RGB", size=(round(t_weight * 1.2), round(t_height * 1.75)), color=(0, 0, 0)) - b_weight, b_height = background.size - background.paste(text_bg, ((b_weight - t_weight) // 2, (b_height - t_height) // 2)) + + # 分别确定两个边文字的大小 + w_text_wight, w_text_height = font.getsize(white_text) + y_text_wight, y_text_height = font.getsize(yellow_text) + + # 生成图片定长 两部分文字之间间隔及两侧留空为固定值三个空格大小 + split_wight, split_height = font.getsize(' ' * 1) + image_wight_ = w_text_wight + y_text_wight + int(split_wight * 5.5) + image_height_ = w_text_height + int(split_height * 1.25) + + # 计算黄色圆角矩形所在位置 + y_r_rectangle_x0 = w_text_wight + int(split_wight * 2.5) + y_r_rectangle_y0 = split_height // 2 + y_r_rectangle_x1 = image_wight_ - int(split_wight * 2) + y_r_rectangle_y1 = image_height_ - split_height // 2 + + # 生成背景层 + background = Image.new(mode="RGB", size=(image_wight_, image_height_), color=(0, 0, 0)) + background_draw = ImageDraw.Draw(background) + # 生成圆角矩形 + background_draw.rounded_rectangle( + xy=((y_r_rectangle_x0, y_r_rectangle_y0), (y_r_rectangle_x1, y_r_rectangle_y1)), + radius=(image_height_ // 20), + fill=(254, 154, 0) + ) + + # 绘制白色文字部分 + background_draw.text( + xy=(split_wight * 2, image_height_ // 2), # 左对齐前间距 上下居中 + text=white_text, + anchor='lm', + font=font, + fill=(255, 255, 255) + ) + # 绘制黄框黑字部分 + background_draw.text( + xy=(w_text_wight + int(split_wight * 3), image_height_ // 2), # 左对齐白字加间距 上下居中 + text=yellow_text, + anchor='lm', + font=font, + fill=(0, 0, 0) + ) + + return background + + loop = asyncio.get_running_loop() + result = await loop.run_in_executor(None, __handle) + return result + + +async def stick_maker_static_luxun( + text: str, image_file: Image.Image, font_path: str, image_wight: int, image_height: int, + *args, **kwargs) -> Image.Image: + """ + 鲁迅说/鲁迅写表情包模板 + """ + def __handle() -> Image.Image: + # 处理文本主体 + font_size = image_wight // 15 + text_stroke_width = int(font_size / 15) + font = ImageFont.truetype(font_path, font_size) + text_width_limit = image_wight - int(image_wight * 0.1875) + + sign_text = '—— 鲁迅' + + # 分割文本 + text_ = TextUtils(text=text).split_multiline(width=text_width_limit, font=font, stroke_width=text_stroke_width) + + # 文本大小 + main_text_width, main_text_height = font.getsize_multiline(text_, stroke_width=text_stroke_width) + sign_text_width, sign_text_height = font.getsize(sign_text, stroke_width=text_stroke_width) + + # 创建背景图层 + # 定位主体文字到图片下侧往上 1/4 处, 落款与主体文字间隔半行, 底部间隔一行, 超出部分为所有文字高度减去图片 1/4 高度 + bg_height_inc_ = int(main_text_height + sign_text_height * 2.5 - image_height * 0.25) + bg_height_inc = bg_height_inc_ if bg_height_inc_ > 0 else 0 + background = Image.new( + mode="RGB", + size=(image_wight, image_height + bg_height_inc), + color=(32, 32, 32)) + + # 先把鲁迅图贴上去 + background.paste(image_file, box=(0, 0)) + + # 再贴主体文本 + ImageDraw.Draw(background).multiline_text( + xy=(image_wight // 2, int(image_height * 0.75)), + text=text_, font=font, align='left', anchor='ma', + fill=(255, 255, 255), + stroke_width=text_stroke_width, + stroke_fill=(0, 0, 0)) + + ImageDraw.Draw(background).text( + xy=(int(image_wight * 0.85), int(main_text_height + sign_text_height / 2 + image_height * 0.75)), + text=sign_text, font=font, align='right', anchor='ra', + fill=(255, 255, 255), + stroke_width=text_stroke_width, + stroke_fill=(0, 0, 0)) + return background loop = asyncio.get_running_loop() @@ -159,5 +204,6 @@ def __handle() -> Image.Image: __all__ = [ 'stick_maker_static_traitor', 'stick_maker_static_jichou', - 'stick_maker_static_phlogo' + 'stick_maker_static_phlogo', + 'stick_maker_static_luxun' ] diff --git a/omega_miya/plugins/su_self_sent/__init__.py b/omega_miya/plugins/su_self_sent/__init__.py index cd666779..c6793c6a 100644 --- a/omega_miya/plugins/su_self_sent/__init__.py +++ b/omega_miya/plugins/su_self_sent/__init__.py @@ -13,6 +13,7 @@ import re from datetime import datetime from nonebot import logger +from nonebot.plugin.export import export from nonebot.plugin import on, CommandGroup from nonebot.typing import T_State from nonebot.message import handle_event @@ -21,15 +22,35 @@ from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.message import Message from nonebot.adapters.cqhttp.event import Event, MessageEvent, GroupMessageEvent +from omega_miya.utils.omega_plugin_utils import init_export SU_TAG: bool = False + +# Custom plugin usage text +__plugin_custom_name__ = '自调用消息' +__plugin_usage__ = r'''【Omega 自调用消息】 +让人工登陆机器人账号时可以通过特殊命令来自己调用自己 + +**Usage** +**SuperUser Only** +/Su.on +/Su.off + +**Bot SelfSent Only** +!SU [command]''' + + +# Init plugin export +init_export(export(), __plugin_custom_name__, __plugin_usage__) + + # 注册事件响应器 Su = CommandGroup('Su', rule=to_me(), permission=SUPERUSER, priority=10, block=True) -su_on = Su.command('on') -su_off = Su.command('off') +su_on = Su.command('on', aliases={'EnableSu'}) +su_off = Su.command('off', aliases={'DisableSu'}) @su_on.handle() @@ -50,17 +71,17 @@ async def handle_first_receive(bot: Bot, event: MessageEvent, state: T_State): self_sent_msg_convertor = on( type='message_sent', - priority=10, + priority=100, block=False ) @self_sent_msg_convertor.handle() async def _handle(bot: Bot, event: Event, state: T_State): - self_id = event.dict().get('self_id', -1) - user_id = event.dict().get('user_id', -1) + self_id = event.self_id + user_id = getattr(event, 'user_id', -1) if self_id == user_id and str(self_id) == bot.self_id and str(self_id) not in bot.config.superusers: - raw_message = event.dict().get('raw_message', '') + raw_message = getattr(event, 'raw_message', '') if str(raw_message).startswith('!SU'): global SU_TAG try: @@ -68,13 +89,13 @@ async def _handle(bot: Bot, event: Event, state: T_State): user_id = int(list(bot.config.superusers)[0]) raw_message = re.sub(r'^!SU', '', str(raw_message)).strip() message = Message(raw_message) - time = event.dict().get('time', int(datetime.now().timestamp())) - sub_type = event.dict().get('sub_type', 'normal') - group_id = event.dict().get('group_id', -1) - message_type = event.dict().get('message_type', 'group') - message_id = event.dict().get('message_id', -1) - font = event.dict().get('font', 0) - sender = event.dict().get('sender', {'user_id': user_id}) + time = getattr(event, 'time', int(datetime.now().timestamp())) + sub_type = getattr(event, 'sub_type', 'normal') + group_id = getattr(event, 'group_id', -1) + message_type = getattr(event, 'message_type', 'group') + message_id = getattr(event, 'message_id', -1) + font = getattr(event, 'font', 0) + sender = getattr(event, 'sender', {'user_id': user_id}) new_event = GroupMessageEvent(**{ 'time': time, diff --git a/omega_miya/plugins/tarot/__init__.py b/omega_miya/plugins/tarot/__init__.py new file mode 100644 index 00000000..d897a5e3 --- /dev/null +++ b/omega_miya/plugins/tarot/__init__.py @@ -0,0 +1,91 @@ +""" +@Author : Ailitonia +@Date : 2021/08/31 21:05 +@FileName : __init__.py.py +@Project : nonebot2_miya +@Description : 塔罗插件 +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +import random +import pathlib +from nonebot import get_driver, MatcherGroup, logger +from nonebot.plugin.export import export +from nonebot.typing import T_State +from nonebot.adapters.cqhttp.bot import Bot +from nonebot.adapters.cqhttp.event import GroupMessageEvent +from nonebot.adapters.cqhttp.permission import GROUP +from nonebot.adapters.cqhttp.message import MessageSegment +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state +from .config import Config +from .utils import generate_tarot_card + + +global_config = get_driver().config +plugin_config = Config(**global_config.dict()) + + +# Custom plugin usage text +__plugin_custom_name__ = '塔罗牌' +__plugin_usage__ = r'''【塔罗牌插件】 +塔罗牌插件 + +**Permission** +Command & Lv.10 +or AuthNode + +**AuthNode** +basic + +**Usage** +/单张塔罗牌''' + + +# Init plugin export +init_export(export(), __plugin_custom_name__, __plugin_usage__) + + +Tarot = MatcherGroup( + type='message', + # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 + state=init_processor_state( + name='tarot', + command=True, + level=10), + permission=GROUP, + priority=10, + block=True) + +single_tarot = Tarot.on_command('单张塔罗牌') + + +@single_tarot.handle() +async def handle_single_tarot(bot: Bot, event: GroupMessageEvent, state: T_State): + group_resources = plugin_config.get_group_resources(group_id=event.group_id) + # 随机一张出来 + card = random.choice(group_resources.pack.cards) + # 再随机正逆 + direction = random.choice([-1, 1]) + if direction == 1: + need_upright = True + need_reversed = False + else: + need_upright = False + need_reversed = True + # 绘制卡图 + card_result = await generate_tarot_card( + id_=card.id, + resources=group_resources, + direction=direction, + need_desc=False, + need_upright=need_upright, + need_reversed=need_reversed) + + if card_result.error: + logger.error(f'{event.group_id}/{event.user_id} 生成塔罗牌图片失败, {card_result.info}') + await single_tarot.finish('生成塔罗牌图片失败了QAQ, 请稍后再试或联系管理员处理') + else: + msg = MessageSegment.image(pathlib.Path(card_result.result).as_uri()) + logger.info(f'{event.group_id}/{event.user_id} 生成塔罗牌图片成功') + await single_tarot.finish(msg) diff --git a/omega_miya/plugins/tarot/config.py b/omega_miya/plugins/tarot/config.py new file mode 100644 index 00000000..5861df99 --- /dev/null +++ b/omega_miya/plugins/tarot/config.py @@ -0,0 +1,28 @@ +""" +@Author : Ailitonia +@Date : 2021/09/01 1:12 +@FileName : config.py +@Project : nonebot2_miya +@Description : 配置文件 +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +from typing import Dict +from pydantic import BaseSettings +from .tarot_resources import BaseTarotResource, TarotResources + + +class Config(BaseSettings): + + default_resources: BaseTarotResource = TarotResources.BiliTarotResources + # 为某些特殊定制要求 为不同群组分配不同资源文件 + group_resources: Dict[int, BaseTarotResource] = { + 0: default_resources + } + + class Config: + extra = "ignore" + + def get_group_resources(self, group_id: int) -> BaseTarotResource: + return self.group_resources.get(group_id, self.default_resources) diff --git a/omega_miya/plugins/tarot/tarot_data.py b/omega_miya/plugins/tarot/tarot_data.py new file mode 100644 index 00000000..7b91efd6 --- /dev/null +++ b/omega_miya/plugins/tarot/tarot_data.py @@ -0,0 +1,801 @@ +""" +@Author : Ailitonia +@Date : 2021/08/31 21:24 +@FileName : tarot_data.py +@Project : nonebot2_miya +@Description : 塔罗卡牌及卡组数据 虽然这里看起来使用 json 会更好 但还是用 dataclass 硬编码了:( +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +from typing import List +from dataclasses import dataclass, field, fields +from .tarot_typing import Element, Constellation, TarotCard, TarotPack + + +@dataclass +class Elements: + earth: Element = field(default=Element(id=0, orig_name='Earth', name='土元素'), init=False) + water: Element = field(default=Element(id=0, orig_name='Water', name='水元素'), init=False) + air: Element = field(default=Element(id=0, orig_name='Air', name='风元素'), init=False) + fire: Element = field(default=Element(id=0, orig_name='Fire', name='火元素'), init=False) + aether: Element = field(default=Element(id=0, orig_name='Aether', name='以太'), init=False) + + +@dataclass +class Constellations: + pluto: Constellation = field(default=Element(id=-9, orig_name='Pluto', name='冥王星'), init=False) + neptunus: Constellation = field(default=Element(id=-8, orig_name='Neptunus', name='海王星'), init=False) + uranus: Constellation = field(default=Element(id=-7, orig_name='Uranus', name='天王星'), init=False) + saturn: Constellation = field(default=Element(id=-6, orig_name='Saturn', name='土星'), init=False) + jupiter: Constellation = field(default=Element(id=-5, orig_name='Jupiter', name='木星'), init=False) + mars: Constellation = field(default=Element(id=-4, orig_name='Mars', name='火星'), init=False) + earth: Constellation = field(default=Element(id=-3, orig_name='Earth', name='地球'), init=False) + moon: Constellation = field(default=Element(id=-10, orig_name='Moon', name='月亮'), init=False) + venus: Constellation = field(default=Element(id=-2, orig_name='Venus', name='金星'), init=False) + mercury: Constellation = field(default=Element(id=-1, orig_name='Mercury', name='水星'), init=False) + sun: Constellation = field(default=Element(id=0, orig_name='Sun', name='太阳'), init=False) + aries: Constellation = field(default=Element(id=1, orig_name='Aries', name='白羊座'), init=False) + taurus: Constellation = field(default=Element(id=2, orig_name='Taurus', name='金牛座'), init=False) + gemini: Constellation = field(default=Element(id=3, orig_name='Gemini', name='双子座'), init=False) + cancer: Constellation = field(default=Element(id=4, orig_name='Cancer', name='巨蟹座'), init=False) + leo: Constellation = field(default=Element(id=5, orig_name='Leo', name='狮子座'), init=False) + virgo: Constellation = field(default=Element(id=6, orig_name='Virgo', name='室女座'), init=False) + libra: Constellation = field(default=Element(id=7, orig_name='Libra', name='天秤座'), init=False) + scorpio: Constellation = field(default=Element(id=8, orig_name='Scorpio', name='天蝎座'), init=False) + sagittarius: Constellation = field(default=Element(id=9, orig_name='Sagittarius', name='人马座'), init=False) + capricorn: Constellation = field(default=Element(id=10, orig_name='Capricorn', name='摩羯座'), init=False) + aquarius: Constellation = field(default=Element(id=11, orig_name='Aquarius', name='宝瓶座'), init=False) + pisces: Constellation = field(default=Element(id=12, orig_name='Pisces', name='双鱼座'), init=False) + + +@dataclass +class TarotCards: + """ + 所有卡牌 + 每个属性都是一张牌 + """ + @classmethod + def get_all_cards(cls) -> List[TarotCard]: + """ + 获取所有塔罗牌的列表 + :return: List[TarotCard] + """ + return [field_.default for field_ in fields(cls) if field_.type == TarotCard] + + blank: TarotCard = field(default=TarotCard( + id=-1, index='blank', type='special', orig_name='Blank', name='空白', + intro='空白的卡面,似乎可以用来作为卡牌背面图案使用', + words='', + desc='', upright='', reversed='' + ), init=False) + + the_fool: TarotCard = field(default=TarotCard( + id=0, index='the_fool', type='major_arcana', orig_name='The Fool', name='愚者', + intro='愚人穿着色彩斑斓的服装,头上戴顶象征成功的桂冠,无视于前方的悬崖,昂首阔步向前行。\n\n他左手拿着一朵白玫瑰,白色象征纯洁,玫瑰象征热情。他的右手则轻轻握着一根杖,象征经验的包袱即系于其上。那根杖可不是普通的杖,它是一根权杖,象征力量。愚人脚边有只小白狗正狂吠着,似乎在提醒他要悬崖勒马,又好像随他一同起舞。无论如何,愚人仍旧保持着欢欣的神色,望向遥远的天空而非眼前的悬崖,好像悬崖下会有个天使扥住他似的,他就这样昂首阔步地向前走。远方的山脉象征他前方未知的旅程,白色的太阳自始至终都目睹着愚人的一举一动──他从哪里来?他往何处去?他又如何回来?', + words='流浪', + desc='愚人牌暗示着你现在不顾风险而有所行动。\n\n愚人是一张代表自发性行为的牌,一段跳脱某种状态的日子,或尽情享受眼前日子的一段时光。对旅游而言,这是一张积极的牌,暗示你将会活在当下,并且会有和生命紧密结合的感觉。“每天都充实,乐趣便在其中”是一句很适合这张牌的古谚语。当你周遭的人都对某事提防戒慎,你却打算去冒这个险时,愚人牌可能就会出现。\n\n愚人暗示通往满足之路是经由自发的行动,而长期的计划则是将来的事。', + upright='盲目的、有勇气的、超越世俗的、展开新的阶段、有新的机会、追求自我的理想、展开一段旅行、超乎常人的勇气、漠视道德舆论的。', + reversed='过于盲目、不顾现实的、横冲直撞的、拒绝负担责任的、违背常理的、逃避的心态、一段危险的旅程、想法如孩童般天真幼稚的。' + ), init=False) + + the_magician: TarotCard = field(default=TarotCard( + id=1, index='the_magician', type='major_arcana', orig_name='The Magician (I)', name='魔术师', + intro='魔术师高举拿着权杖的右手指向天,左手食指指向地,他本人就是沟通上天与地面的桥梁。他身前的桌上放着象征四要素的权杖、圣杯、宝剑与星币,同时也代表塔罗牌的四个牌组。他身穿的大红袍子象征热情与主动,白色内衫表示纯洁与智慧的内在。缠绕他腰间的是一条青蛇,蛇虽然经常象征邪恶,但在这里代表的是智慧与启发。魔术师头顶上有个倒8符号,代表无限。画面前方和上方的红玫瑰象征热情,白百合象征智慧。此时,万事齐备,魔术师可以开始进行他的新计划了。和愚人牌同样鲜黄色的背景,预示未来成功的可能。', + words='创造', + desc='魔术牌意味着:现在是展开新计划的好时机。\n\n魔术师这张牌意味这是个着手新事物的适当时机。对的时间、对的机会、对的动机,使你的努力值回票价。对于展开行动、实现计划而言,这正是一个良好时机。由于你已为实现计划扎下良好基础,所以新的冒险很可能会实现。清楚的方向感和意志力的贯彻,大大的提升了成功的可能性。', + upright='成功的、有实力的、聪明能干的、擅长沟通的、机智过人的、唯我独尊的、企划能力强的、透过更好的沟通而获得智慧、运用智慧影响他人、学习能力强的、有教育和学术上的能力、表达技巧良好的。', + reversed='变魔术耍花招的、瞒骗的、失败的、狡猾的、善于谎言的、能力不足的、丧失信心的、以不正当手段获取认同的。' + ), init=False) + + the_high_priestess: TarotCard = field(default=TarotCard( + id=2, index='the_high_priestess', type='major_arcana', orig_name='The High Priestess (II)', name='女祭司', + intro='相较于上一张魔术师纯粹阳性的力量,女祭司表现的则是纯粹阴性的力量。她身穿代表纯洁的白色内袍,与圣母的蓝色外袍,静默端坐。胸前挂个十字架,象征阴阳平衡、与神合一。\n\n她头戴的帽子是由上弦月、下弦月和一轮满月所构成的,象征所有的处女神祇。手上拿着滚动条,象征深奥的智慧,其上的TORA字样,意为“神圣律法”,而滚动条卷起并半遮着,暗示此律法不为人所知。在她脚边的一轮新月,为她的内袍衣角所固定住,袍子并延伸到图面之外。女祭司两侧一黑一白的柱子,存在于圣经故事中所罗门王在耶路撒冷所建的圣殿中,黑白柱上的B与J字样,分别是Boas和Jachin的缩写,黑柱是阴而白柱是阳,两柱象征二元性,坐在中间的女祭司则不偏不倚,统合两者的力量。柱子上面的喇叭造型,代表女祭司敏锐的感受性,上面的百合花纹则象征纯洁与和平。两柱之间有帷幕遮着,帷幕上的石榴代表“阴”,棕榈代表“阳”。帷幕把后方的景色遮住了,仔细一看,依稀可见由水、山丘与的蓝天构成的背景。水象征情感与潜意识,这一片水平静无波,但其静止的表面下蕴藏深沉的秘密。整个图面呈现象征智慧的蓝色调,双柱的意象在后面的牌中重复出现。', + words='智慧', + desc='女祭司意味着:这是向内探索、沉思,或按兵不动的时刻。\n\n女祭司代表去思考可以导致实际结果的构想。这并不是一张代表具体事物的牌,而是一张代表可能性的牌。我们每个人都在我们的人生当中持续的耕耘和收获,而女祭司就是散播那些种子或理念的行动。\n\n女祭司暗示你应该要相信你的直觉,因为在这一点上,有些东西你可能看不见。高位的女祭司是一张代表精神和心灵发展的牌。它代表了向内心探索的一段时期,以便为你人生的下一个阶段播种,或者去消化你在肉体的层次上所处理的事情。', + upright='纯真无邪的、拥有直觉判断能力的、揭发真相的、运用潜意识的力量、掌握知识的、正确的判断、理性的思考、单恋的、精神上的恋爱、对爱情严苛的、回避爱情的、对学业有助益的。', + reversed='冷酷无情的、无法正确思考的、错误的方向、迷信的、无理取闹的、情绪不安的、缺乏前瞻性的、严厉拒绝爱情的。' + ), init=False) + + the_empress: TarotCard = field(default=TarotCard( + id=3, index='the_empress', type='major_arcana', orig_name='The Empress (III)', name='女皇', + intro='体态丰腴的皇后坐在宝座上,手持象征地球的圆形手杖,戴着由九颗珍珠组成的项链,象征九颗行星,也代表金星维纳斯。皇后头冠由十二个六角星组成,象征十二星座与一年的十二个月。更进一步,六角星本身是由一个正三角形和倒三角形组成,分别代表火要素和水要素。除了头冠之外,她还戴着香桃木叶作成的头环,象征金星维纳斯。她身穿的宽松袍子上面画满象征多产的石榴,宝座下方则是个绘有金星符号的心形枕头。她前方的麦田已经成熟,代表丰饶与多产;后方则是茂密的丝柏森林,与象征生命力的瀑布河流。', + words='丰收', + desc='女皇牌暗示家庭和谐及稳定。\n\n简单言之,女皇可能意味着实现计划,或朝向计划的下一个自然步骤迈进,亦即你又向目标靠近了一步。女皇牌也可能暗示一趟乡野之旅,或是休息一阵子并重返大自然的怀抱,因为她四周围绕着自然的产物。透过亲近自然,现在是你重新平衡自己的时候。这张牌意味家庭状态的稳定与和谐,而这通常是透过把爱从思考当中,带往内心来达成的。', + upright='温柔顺从的、高贵美丽的、享受生活的、丰收的、生产的、温柔多情的、维护爱情的、充满女性魅力的、具有母爱的、有创造力的女性、沈浸爱情的、财运充裕的、快乐愉悦的。', + reversed='骄傲放纵的、过度享乐的、浪费的、充满嫉妒心的、母性的独裁、占有欲、败家的女人、挥霍无度的、骄纵的、纵欲的、为爱颓废的、不正当的爱情、不伦之恋、美丽的诱惑。' + ), init=False) + + the_emperor: TarotCard = field(default=TarotCard( + id=4, index='the_emperor', type='major_arcana', orig_name='The Emperor (IV)', name='皇帝', + intro='一国之尊的皇帝头戴皇冠,身着红袍,脚穿象征严格纪律的盔甲,左手拿着一颗球,右手持的是象征生命的古埃及十字架,自信满满的坐在王位上。\n\n王位上有四个牡羊头作为装饰,如图所示,皇帝牌正是代表牡羊座的牌。牡羊座是十二星座的头一个,具有勇敢、积极、有野心、有自信的特质。红袍加上橙色的背景,呈现红色的主色调,与牡羊座的特性不谋而合。背景严峻的山象征前方险峻的路途。我们可以比较皇帝与皇后的背景,一个是严峻山川,一个是丰饶大地,形成互补的局面。', + words='支配', + desc='皇帝表示一种训练和实际致力于生活。\n\n皇帝意味透过自律和实际的努力而达到成功。它可以代表你生活中一段相当稳定,且井然有序的时光。这张牌可以暗示遭遇到法律上的问题,或是碰到某个地位、权利都在你之上的人,例如法官、警员、父亲,或具有父亲形象的人。\n\n为了成功,现在正是你采取务实态度来面对人生的时候。你被周遭的人设下种种限制,但只要你能在这些限制之内努力的话,你还是可以达成你的目标。', + upright='事业成功、物质丰厚、掌控爱情运的、有手段的、有方法的、阳刚的、独立自主的、有男性魅力的、大男人主义的、有处理事情的能力、有点独断的、想要实现野心与梦想的。', + reversed='失败的、过于刚硬的、不利爱情运的、自以为是的、权威过度的、力量减弱的、丧失理智的、错误的判断、没有能力的、过于在乎世俗的、权力欲望过重的、权力使人腐败的、徒劳无功的。' + ), init=False) + + the_hierophant: TarotCard = field(default=TarotCard( + id=5, index='the_hierophant', type='major_arcana', orig_name='The Hierophant (V)', name='教皇', + intro='教皇身穿大红袍子,端坐在信众前。他头戴象征权力的三层皇冠,分别代表身心灵三种层次的世界。\n\n他的右手食中指指向天,象征祝福﹔左手持着主字形的权杖,象征神圣与权力。他耳朵旁边垂挂的白色小物,代表内心的声音。教皇前方放着两把交叉的钥匙,在很多版本的塔罗牌里,钥匙是金色银色各一把,象征阳与阴,日与月,外在与内在,我们的课题就是要学会如何结合两者,而钥匙本身可用以开启智慧与神秘之门。教皇前方的两位信众,左边的身穿象征热情的红玫瑰花纹衣裳,右边则穿象征性灵成长的白百合衣裳(红玫瑰与白百合在魔术师也曾出现过)。教皇与信众三人的衣服都有牛轭形(Y字形)装饰,牛轭的用途是促使受过训练的动物去工作的,出现在教皇牌的道理值得深思。教皇后方则是曾经在女祭司中出现的两根柱子,不过在这里它们是灰色的,灰色象征由经验而来的智慧﹔另一说则是教皇后方虽无女祭司的帷幕将潜意识隔离,但暗沉的灰色代表通往潜意识之路仍未开启。柱子上的图案象征肉体结合。', + words='援助', + desc='教皇代表需要为你的心灵成长,及人生方向付起责任。\n\n教皇暗示你向某人或某个团体的人屈服了。或许这正是你为自己,及心灵上的需求负起责任的时刻了。你目前的行事作风并非应付事情的唯一方式,假设你愿意加以探索的话,或许你就会找到新的可能。', + upright='有智慧的、擅沟通的、适时的帮助、找到真理、有精神上的援助、得到贵人帮助、一个有影响力的导师、找到正确的方向、学业出现援助、爱情上出现长辈的干涉、媒人的帮助。', + reversed='过于依赖的、错误的指导、盲目的安慰、无效的帮助、独裁的、疲劳轰炸的、精神洗脑的、以不正当手段取得认同的、毫无能力的、爱情遭破坏、第三者的介入。' + ), init=False) + + the_lovers: TarotCard = field(default=TarotCard( + id=6, index='the_lovers', type='major_arcana', orig_name='The Lovers (VI)', name='恋人', + intro='恋人牌背景在伊甸园,亚当与夏娃分站两边,两者皆裸身,代表他们没什么需要隐藏的。两人所踩的土地相当肥沃,生机盎然。\n\n夏娃的背后是知识之树,生有五颗苹果,象征五种感官,有条蛇缠绕树上。蛇在世界文化中的象征丰富多元,此处可能象征智慧,也象征欲望与诱惑。牠由下往上缠绕在树上,暗示诱惑经常来自潜意识。亚当背后是生命之树,树上有十二团火焰,象征十二星座,也象征欲望之火。伟特说:“亚当与夏娃年轻诱人的躯体,象征未受有形物质污染之前的青春、童贞、纯真和爱”。两人背后的人物是风之天使拉斐尔(Raphael),风代表沟通,祂身穿的紫袍则是忠贞的象征,显示这个沟通的重要性。亚当看着夏娃,夏娃则望着天使,象征“意识─潜意识─超意识”与“身─心─灵”或是“理性─感性”之间的传导。天使之下,亚当夏娃中间有一座山,象征意义解读众多,主要有三种:一说是山代表阳性,水代表阴性,两者表现阴阳平衡,意味我们必须把阴与阳、理性与感性的能量调和。一说认为这座山象征正当思想的丰饶果实。另一说则认为它代表高峰经验与极乐。', + words='结合', + desc='恋人牌意味,为了爱的关系而做的某些决定。\n\n恋人是一张代表决定的牌,而且除非问的是某个特定的问题,否则它通常是指有关两性关系的决定。它可能是在描述沉浸在爱恋之中的过程,因为它可以意指一段两性关系中的最初,或者是罗曼蒂克的阶级。恋人牌也可以形容在决定到底要保留就有的关系,或转进新关系当中。它暗示你已经由过去经验而得到成长了,因此你可以安全的迈向一个新的阶段。', + upright='爱情甜蜜的、被祝福的关系、刚萌芽的爱情、顺利交往的、美满的结合、面临工作学业的选择、面对爱情的抉择、下决定的时刻、合作顺利的。', + reversed='遭遇分离、有第三者介入、感情不合、外力干涉、面临分手状况、爱情已远去、无法结合的、遭受破坏的关系、爱错了人、不被祝福的恋情、因一时的寂寞而结合。' + ), init=False) + + the_chariot: TarotCard = field(default=TarotCard( + id=7, index='the_chariot', type='major_arcana', orig_name='The Chariot (VII)', name='战车', + intro='一位英勇的战士驾着一座由两只人面狮身兽拉着的战车。人面狮身兽一只是黑的,代表严厉,另一只是白的,代表慈悲。两兽同时来看,也是阴阳平衡的象征。\n\n战车上有四根柱子(四个代表上帝的希伯来字母YHWH或火水风土四要素)支撑着蓝色车棚,车棚上饰以六角星花纹,象征天体对战士成功的影响。英勇的战士手持象征意志与力量的矛形权杖,头戴象征统治的八角星头冠和象征胜利的桂冠,身穿盔甲。盔甲上的肩章呈现弦月形,显示战车牌与属月亮的巨蟹座之关联。斜挂的腰带上有占星学符号,裙上有各种炼金术的符号。胸前的四方形图案代表土要素,象征意志的力量。战车前方的翅膀图案是古埃及的图腾,代表灵感。翅膀下面是一个小盾牌,其上的红色的图案是一种印度图腾,为男性与女性生殖器结合的象征,也是二元性与一元性,类似中国的阴与阳,可能暗示编号七的战车牌走过愚人之旅的三分之一,已达性成熟的阶段。战士身后的河流就是圣经创世纪中四条伊甸园之河其中的一条,与皇后、皇帝和、死神牌中的河是同一条。再后面就是一座高墙耸立的城市。战士背对城市,暗示他把物质置于身后,向前开展心灵上的旅程。他手上没有缰绳,表示他不是用肉体来控制那两头朝不同方向行进的人面狮身兽,而完全凭借他旺盛过人的意志力。值得注意的一点是他站在城墙外守御,而非进攻,所以这位战士是位守护者、防御者,而不是侵略者。他是尽他的本分,并努力做到最好。', + words='胜利', + desc='战车牌意味训练有素的心智。\n\n战车可以代表一部车,或是坐车旅行。当这张牌出现时,它可能意味着你需要控制生命中互相对抗的力量。目前的情况可能会出现某些矛盾,而你正以理智在控制着它们。\n\n这是一张代表由于坚持而取得成功的牌。如果用来形容一个人的话,战车是暗示这个人(通常是指男人),掌控着她自己和周遭的事物。正立的战车也可能意指一桩重要的生意,或意义重大的成功。', + upright='胜利的、凯旋而归的、不断的征服、有收获的、快速的解决、交通顺利的、充满信心的、不顾危险的、方向确定的、坚持向前的、冲劲十足的。', + reversed='不易驾驭的、严重失败、交通意外、遭遇挫折的、遇到障碍的、挣扎的、意外冲击的、失去方向的、丧失理智的、鲁莽冲撞的。' + ), init=False) + + strength: TarotCard = field(default=TarotCard( + id=8, index='strength', type='major_arcana', orig_name='Strength (VIII)', name='力量', + intro='代表力量的女人轻柔地合上狮子的嘴。女人头上有魔术师牌中出现的倒8符号,象征她的力量是无穷尽的。她头上戴着花环,腰间也系着花环,而且腰间花环还连系在狮子颈间,形成第二个倒8符号。狮子身体微倾,尾巴轻垂,表现出彻底的顺服,还伸出舌头来舔着女人的手。', + words='意志', + desc='力量牌暗示你拥有足够的内在力量去面对人生。\n\n这张力量牌意味你有能力面对生活和困难的环境,或者有能力以希望、内在力量及勇气去做改变。勇气并不代表你没有恐惧,而是虽然你有恐惧,你还是愿意对某人或某事有所承诺。\n\n这张牌象征你拥有内在的力量来面对你内在的恐惧和欲望,而非让它们屈服于你的意志。在健康的分析方面,这张牌可能是有关心脏或脊椎方面的毛病,不过这些毛病也可以透过内在能量来克服,而且这张牌也暗示你本身拥有这种能量。', + upright='内在的力量使成功的、正确的信心、坦然的态度、以柔克刚的力量、有魅力的、精神力旺盛、有领导能力的、理性的处理态度、头脑清晰的。', + reversed='丧失信心的、失去生命力的、沮丧的、失败的、失去魅力的、无助的、情绪化的、任性而为的、退缩的、没有能力处理问题的、充满负面情绪的。' + ), init=False) + + the_hermit: TarotCard = field(default=TarotCard( + id=9, index='the_hermit', type='major_arcana', orig_name='The Hermit (IX)', name='隐者', + intro='身穿灰色斗篷和帽子的老人站在冰天雪地的山巅上,低头沉思,四周渺无人烟。他右手高高举着一盏灯,这是真理之灯,灯里是颗发亮的六角星,名称是所罗门的封印,散发出潜意识之光。老人左手拄着一根族长之杖,这跟杖在愚人、魔术师、战车都曾经出现过。愚人太过天真,不知杖的魔力,拿它来系包袱;魔术师用代表意识的右手运用杖的法力;战车把杖化为矛,也用右手紧握着;隐士则杖交左手,用以在启蒙之路上做前导。', + words='探索', + desc='隐士牌暗示着:省思的一段时间。\n\n隐士牌暗示一段反省的时间。它代表着一段想要让你的过去、现在,以及未来成为有意义的时间。这张牌代表去看咨商辅导员、持续一段梦想之旅,或为了开发你自己的沉思。它也代表成熟,以及你已经知道生命中真正重要的是什么。\n\n它可能意味着得到身体或心灵上的协助及智因;或是你帮助其他人发现人生理解及事件的导因。它也代表一段时间内,你会问自己如下的问题:我从何处来?我现在位于何处?又将往何处去?', + upright='有骨气的、清高的、有智慧的、有法力的、自我修养的,生命的智慧情境、用智慧排除困难的、给予正确的指导方向、有鉴赏力的、三思而后行的、谨慎行动的。', + reversed='假清高的、假道德的、没骨气、没有能力的、内心孤独寂寞的、缺乏支持的、错误的判断、被排挤的、没有足够智慧的、退缩的、自以为是的、与环境不合的。' + ), init=False) + + wheel_of_fortune: TarotCard = field(default=TarotCard( + id=10, index='wheel_of_fortune', type='major_arcana', orig_name='Wheel of Fortune (X)', name='命运之轮', + intro='所有的大牌都有人物,命运之轮是唯一的例外,可见这张牌独树一格。深蓝色的天空悬着一个轮子,轮盘由三个圆圈构成(教宗的头冠也是),最里面的小圈代表创造力,中间是形成力,最外层是物质世界。小圈里头没有任何符号,因为创造力潜能无限;中间圆圈里有四个符号,从上方顺时针依序是炼金术中的汞风、硫、水,分别与风火水土四要素相关联,是形成物质世界的基本要素﹔最外层就是物质世界,上右下左四方位分别是TARO四个字母,这四个字母可以组成Rota(轮)、Orat(说)、Tora(律法)、Ator(哈扥尔女神),形成一个完整的句子“塔罗之轮述说哈扥尔女神的律法”,其余四个符号是希伯来字母YHVH,是上帝最古老的名字。轮盘从中心放射出八道直线,代表宇宙辐射能量。\n\n在轮盘左方有一条往下行进的蛇,是埃及神话中的邪恶之神Typhon,牠的向下沉沦带着轮子进入分崩离析的黑暗世界。相反的,背负轮盘的胡狼头动物渴求上升,牠是埃及神话中的阿努比神(Anubis)。而上方的人面狮身兽是智慧的象征,均衡持中,在变动中保持不变。牠拿着的宝剑代表风要素,表示心智慧力、思考力和智慧。\n\n四个角落的四只动物,从右上方顺时针看分别是老鹰、狮子、牛、人,而且他们都有翅膀。这四个动物出自圣经启示录第四章“宝座周围有四个活物,前后遍体都满了眼睛。第一个活物像狮子,第二个像牛犊,第三个脸面像人,第四个像飞鹰”,耶路撒冷圣经提到四活物象征四位福音书的作者(马太、马可、路加和约翰)。在占卜上这四个动物与占星学产生关联,分别代表四个固定星座和四要素,老鹰是天蝎座(水),狮子是狮子座(火),牛是金牛座(土),人是水瓶座(风)。牠们都在看书,汲取智慧,而翅膀赋予牠们在变动中保持稳定的能力。', + words='轮回', + desc='命运之轮意味着你境遇的改变。观察这个改变,并留意它的模式。\n\n生命是变化无常的,当牌面上的命运之轮是正立时,改变似乎是有利的;而当它倒立时,改变又似乎是有害的。它只是改变,而似乎有害的改变,事实上可能会是一种祝福。你必须超越现状,将眼光放远,来观察生命的消长。\n\n通常命运之轮象征你生命境遇的改变。或许你并不了解这些改变的原因,不过在这里,你如何因应改变是比较重要的。你要迎接生命所提供给你的机会,还是要抗拒改变呢?此牌正立时就是在告诉你,要去适应这些改变。', + upright='忽然而来的幸运、即将转变的局势、顺应局势带来成功、把握命运给予的机会、意外的发展、不可预测的未来、突如其来的爱情运变动。', + reversed='突如其来的厄运、无法抵抗局势的变化、事情的发展失去了掌控、错失良机、无法掌握命运的关键时刻而导致失败、不利的突发状况、没有答案、被人摆布、有人暗中操作。' + ), init=False) + + justice: TarotCard = field(default=TarotCard( + id=11, index='justice', type='major_arcana', orig_name='Justice (XI)', name='正义', + intro='一个女人端坐在石凳上,右手持剑高高举起,左手在下拿着天秤。身穿红袍,头戴金冠,绿色披肩用一个方形扣子扣起。她的右脚微微往外踏出,似乎想站起来,而左脚仍隐藏在袍子里面。她高举宝剑,象征她的决心。宝剑不偏不倚,象征公正,且智慧可以戳破任何虚伪与幻象。宝剑两面都有刃,可行善可行恶,端看个人选择。左手的金色天秤和披肩的绿色都是天秤座的象征。手持天秤表示她正在评估,正要下某个决定,同时追求平衡。胸前的方形扣子中间是个圆形,象征四要素的调和。头上的金冠中心有个四方形宝石,加上金冠的三个方顶,加起来得到数字七,代表金星,也就是天秤座的守护星。后方是个紫色帷幕,象征隐藏的智慧。两边柱子象征正面和负面的力量。', + words='均衡', + desc='正义意味,这是一段你为你的人生决定负起责任的时光。\n\n正义意味事情已经达成它应有的使命。也就是说,你过往的决定或行为已经引导你走到了目前的境遇。你已经得到你应得的了,如果你对自己是够诚实的话,你肯定知道这点。它代表你应该对自己,以及周遭的人绝对的诚实。你应该自己,以及使你成为今天这个样子的种种决定负起责任。你的未来可能会因为你目前的决定、行为或理解而改变。', + upright='明智的决定、看清了真相、正确的判断与选择、得到公平的待遇、走向正确的道路、理智与正义战胜一切、维持平衡的、诉讼得到正义与公平、重新调整使之平衡、不留情面的。', + reversed='错误的决定、不公平的待遇、没有原则的、缺乏理想的、失去方向的、不合理的、存有偏见的、冥顽不灵的、小心眼、过于冷漠的、不懂感情的。' + ), init=False) + + the_hanged_man: TarotCard = field(default=TarotCard( + id=12, index='the_hanged_man', type='major_arcana', orig_name='The Hanged Man (XII)', name='倒吊人', + intro='倒吊人图案简单,涵义却深远。我们看到一个男人在一棵T字形树上倒吊着。他两手背在背后,形成一个三角形。两腿交叉形成十字。十字和三角形结合在一起,就是一个炼金符号,象征伟大志业的完成,也象征低层次的欲望转化到高层次的灵魂(炼成黄金)。\n\n红裤子象征身心灵中的“身”,也就是人类的欲望和肉体。蓝上衣即身心灵中的“心”,象征知识。他的金发和光环象征智慧和心灵的进化,也就是“灵”。金色的鞋子则象征倒吊人崇高的理想。在某些版本的塔罗牌中,倒吊人就是神话中的奥丁(Odin),他身后的树就是北欧神话中的义格卓席尔巨树(Yggdrasil),也称作世界之树,由地狱(潜意识)开始生长,经过地面(意识),直达天庭(超意识)。还记得皇帝右手拿着一根象征生命的古埃及十字架吗?古埃及十字架代表希伯来的第十九个字母Tau,是属于世间的一个字母,而倒吊人倒吊的T字树,正是它的下半部,表示倒吊人仍然是入世的。', + words='牺牲', + desc='“以将有更美好的事物降临于你身上的信念,顺从于人生”是倒吊人这张牌所传达的讯息。\n\n倒吊人是一张代表投降的牌。它暗示,当你在这段期间内,透过对生命的顺从,并让它引领你到你需要去的地方,那么你便可以获益良多。\n\n倒吊人还是一张代表独立的牌。这段期间内,你应该顺着感觉走,或是接受自己,即使别人都认为你的方式很奇怪也不打紧。它也可能象征,经历了生命中一段艰难的时光后的心灵平静。\n\n现在不是挣扎的时候,静下来好好思考你过去的行为,以及未来的计划。这只是一个暂时的状态,只要你妥善的运用这段时间,对你应该是有好处的。让生命中的事物自然而然的发生,或许你会对结果感到惊喜。带着“会有更美好的事情临降,来取代你所捐弃的事物”的信念,顺从于人生。花点时间来观察潜伏于事件底下的生命潮流。生命会给你一段宁静的时光,远离世界的纷纷扰扰,所以善用这段时光将是明智之举。', + upright='心甘情愿的牺牲奉献、以修练的方式来求道、不按常理的、反其道而行的、金钱上的损失、正专注于某个理想的、有坚定信仰的、长时间沈思的、需要沈淀的、成功之前的必经之道。', + reversed='精神上的虐待、心不甘情不愿的牺牲、损失惨重的、受到亏待的、严重漏财的、不满足的、冷淡的、自私自利的、要求回报的付出、逃离綑绑和束缚、以错误的方式看世界。' + ), init=False) + + death: TarotCard = field(default=TarotCard( + id=13, index='death', type='major_arcana', orig_name='Death (XIII)', name='死神', + intro='传统的死神牌,通常是由骷髅人拿着镰刀来代表,而伟特将死神的意象提升到更深一层的境界。\n\n最显眼的就是那位骑着白马的骷髅骑士。他身边有四个人,国王、主教、女人、小孩,象征无论是世俗或出世、男或女、老或少,都逃不过死亡这个自然现象。国王抗拒死亡,被骷髅骑士践踏过去﹔主教的权杖掉在地上,双手合十崇敬死亡﹔女人跪下,别过脸不忍看﹔小孩不懂死亡,好奇的望着骷髅骑士。其中主教可能就是编号五的教宗牌,他掉落在地上的权杖象征世俗权力遇到死亡时毫无用处,仔细一看权杖顶似乎有三层圆圈,和教宗牌戴在头上的权冠相同,而主教头上戴的帽子状似尖尖的鱼头,代表双鱼世纪的结束,也可能暗示死神牌关联的希伯来文Nun,意思是鱼。跪着的女人可能是力量牌中的那位女性,她们的衣着与头冠都极为相似。再回到骷髅骑士,他头上那根红羽毛和愚人所戴的是同一根,他的旗帜是黑色背景,象征光芒的不存,上面五瓣蔷薇的图案是蔷薇十字会的图腾,关于此图腾的说法众多,可能是代表随着死亡而来的新生,另一说是象征火星与生命力,还有一说是象征美丽纯洁与不朽。远方的河流就是流经伊甸园的四条河流之一,称为冥河(Styx),象征川流不息的生命循环。河上有艘船,船的上方有个类似洞穴的地方,右方有个箭头(在死神的脚跟处)指向洞穴,这个洞穴可能是“神曲”一书中但丁前往阴间的通道,而牌中右方一条小径通往两座塔中(月亮和节制都有相同背景,这两座塔也可能是女祭司背后的柱子),代表通往新耶路撒冷的神秘旅程。象征永生的朝阳在两座塔间升起,似乎在告诉我们死亡并不是一切的终点。', + words='结束', + desc='死亡牌意味某种状况的结束。\n\n死亡为旧事物画上休止符,并让路给新事物。死亡牌代表改变的一段其间。我们可以这样说,生命中的某个章节就要结束了,而你对这份改变的接纳,将是变化自然而然地发生。\n\n抱持着“生命将会带来某些比它从你身上拿走的更美好的东西”的信念。在潜意识中,你或许也在渴望改变的发生,死亡牌即意味着改变正要出现。不要抗拒这份改变,试着去接纳它吧。', + upright='必须结束旧有的现状、面临重新开始的时刻到了、将不好的过去清除掉、专注于心的开始、挥别过去的历史、展开心的旅程、在心里做个了结、激烈的变化。', + reversed='已经历经了重生阶段了、革命已经完成、挥别了过去、失去了、结束了、失败了、病了、走出阴霾的时刻到了、没有转圜余地了。' + ), init=False) + + temperance: TarotCard = field(default=TarotCard( + id=14, index='temperance', type='major_arcana', orig_name='Temperance (XIV)', name='节制', + intro='十四号的节制牌,出现在死神牌之后。大天使麦可手持两个金杯,把左手杯中的水倒入右手杯中。\n\n金发的天使身着白袍,背长红翅膀,胸前有个方形图案(地元素),中间是个橘色的三角形(火元素),同样的图案在正义牌中也可看到。天使头上则戴个饼图案,中间有一个小点,是炼金术中代表黄金的符号,也就是终极目标。天使脸上闪耀着和谐的光辉,怡然自在,他/她的右脚踏入象征潜意识的池塘中,左脚站在象征显意识的岸边石头上,代表两者之间的融合。塘边生长一丛爱丽斯花。远方有一条小径通往淡蓝色的两座山间,双山顶间闪耀着王冠般的金色光芒,类似如此的图像也曾出现于前一张死神牌中的小径、双塔与朝阳。恋人与审判牌中也有天使的出现。另外,大天使对应希腊神话中的彩虹之神,暴风雨后的彩虹,意味着节制牌已经从死神带给我们的恐惧中超脱出来了。整张牌带给人宁静祥和的感受,让人们明白死亡之后终获新生。', + words='净化', + desc='节制代表行动及感情的融合,带来内心的平静感觉。\n\n节制是一张代表行为,而非观念的牌。它代表对某种特定状况的适当行为。显示一种因为行为及情绪的结合,而带来内在平静的感觉。节制意味着结合自发性及知识的能力,运用精神的知识及理解力来调节行为的能力。它是指知道每种状况来临时,应该采取什么适当的反映或行为。\n\n节制牌暗示你较高层次的自我,和较低层次的自我可以和谐共存。你带着一种方向感行动,不管那是精神上或实质上的行动。它代表尽力而为,以达到你可以达到的境界。', + upright='良好的疏导、希望与承诺、得到调和、有节制的、平衡的、沟通良好的、健康的、成熟与均衡的个性、以机智处理问题、从过去的错误中学习、避免重蹈覆辙、净化的、有技巧的、有艺术才能的。', + reversed='缺乏能力的、技术不佳的、不懂事的、需反省的、失去平衡状态、沟通不良的、缺乏自我控制力、不确定的、重复犯错的、挫败的、受阻碍的、暂时的分离、希望与承诺遥遥无期。' + ), init=False) + + the_devil: TarotCard = field(default=TarotCard( + id=15, index='the_devil', type='major_arcana', orig_name='The Devil (XV)', name='恶魔', + intro='在恶魔牌上,我们看到和恋人相似的构图,只是恋人牌的天使在这里换成了恶魔,而亚当夏娃已然沉沦,上天的祝福变成了诅咒。\n\n牌中的恶魔有蝙蝠翅膀、羊角、羊腿和鸟足,象征动物的本能与天性。牠的驴耳则代表固执。恶魔头上的倒立星币,顶端指向地面,代表物质世界。恶魔右手向上摆出黑魔法的手势,与教宗的祝福手势形成对比。手心的符号代表土星,限制与惰性之星,也是魔羯座的守护星。恶魔左手则持着火炬,同样向下导引到物质世界,似乎在煽动亚当的欲望。注意恶魔坐的地方并不是三度空间的立方体,而是二度空间的长方形,象征人们只看见感官所见的现实,却非全部的真实,好比瞎子摸象。前方的亚当夏娃同样长出角和尾巴,显露出野兽本能。亚当的尾巴尖端是朵火焰,夏娃则是葡萄,都是恋人牌树上结的果实,表示她们误用了天赋。两个人被铁链锁住,乍看无处可逃,但仔细一看,其实系在她们脖子上的链子非常的松,只要愿意,随时可以挣脱,但她们却没有,表示这个枷锁是他们自己套在自己身上的。恶魔牌背景全黑,光芒不存,代表精神上的黑暗。', + words='诱惑', + desc='魔鬼牌代表错以为别无选择。\n\n魔鬼牌代表一种错误的概念,认为事情别无选择。觉得“我所拥有的就是这些”或“这是我唯一的选择”。在宗教的前因后果当中,魔鬼引诱男人使它遗忘掉精神的探索,以及他的神圣目的。在一般性的占卜中,魔鬼代表一种控制生命的需求,你对与自己的可能性缺乏完整的关照。\n\n魔鬼牌描述的是一种对生命物质化的观点,或像王尔德(OscarWilde)所说的:“知道所有东西的价格,却不知道任何东西的价值。”它可能暗示在某种状况内受到限制,却不愿意去改变。它是一种“偷鸡摸狗胜过杀人放火”的态度。', + upright='不伦之恋、不正当的欲望、受诱惑的、违反世俗约定的、不道德的、有特殊的艺术才能、沉浸在消极里、沉溺在恐惧之中的、充满愤怒和怨恨、因恐惧而阻碍了自己、错误的方向、不忠诚的、秘密恋情。', + reversed='解脱了不伦之恋、挣脱了世俗的枷锁、不顾道德的、逃避的、伤害自己的、欲望的化解、被诅咒的、欲望强大的、不利的环境、盲目做判断、被唾弃的。' + ), init=False) + + the_tower: TarotCard = field(default=TarotCard( + id=16, index='the_tower', type='major_arcana', orig_name='The Tower (XVI)', name='塔', + intro='一座位于山巅上的高塔,被雷击中而毁坏,塔中两人头上脚下的坠落。塔顶有个王冠受雷殛而即将坠落。塔象征物质,王冠象征统治和成就,也代表物质与财富,受雷一殛,便荡然无存。天上的落雷是直接来自上帝的语言,两旁的火花有二十二个,象征塔罗二十二张大牌。灰色的云降下灾难之雨,不分性别阶级,平等的落向每一个人。背景全黑,这是一段黑暗的时期。', + words='毁灭', + desc='高塔象征生命中无可避免的改变。\n\n这种改变是是从根基到顶端的完全崩解与毁灭,是一种无可挽救的崩溃。这种改变是突然而来的,有时候激烈无比,这是一种易于顺从而难以抗拒的改变。当高塔牌出现时,便是到了改变的时刻。现在再来为改变做准备,或选择如何改变都已太迟,现在你需要做的就是丢掉旧东西。', + upright='双方关系破裂、难以挽救的局面、组织瓦解了、损失惨重的、惨烈的破坏、毁灭性的事件、混乱的影响力、意外的发展、震惊扰人的问题、悲伤的、离别的、失望的、需要协助的、生活需要重建的。', + reversed='全盘覆没、一切都已破坏殆尽、毫无转圜余地的、失去了、不安的、暴力的、已经遭逢厄运了、急需重建的。' + ), init=False) + + the_star: TarotCard = field(default=TarotCard( + id=17, index='the_star', type='major_arcana', orig_name='The Star (XVII)', name='星星', + intro='一位赤裸的金发女子,左膝跪在象征显意识的地面上,右脚踏在象征潜意识的池水里。她左右手各持一个水壶,壶中装的是生命之水,她右手壶的水倾倒入池,激起阵阵涟漪,左手壶的水则倒在青翠的草地上,分成象征人类五种感官的五道水流,其中一道又流回池塘,再度充实潜意识之泉。她身后有棵树,树上有只象征智慧的朱鹭,同时也代表埃及神话中的托特之神,是所有艺术的创造者。女子的后方则是一大片广阔开满花的草原,和一座山脉,天空一颗巨大的金色八角星,七颗白色的小八角星则环绕在四周。', + words='希望', + desc='星星牌意味创造力和对生命的可能性的信心。\n\n星星是一张代表重新点燃希望的牌。它代表相信明天会更好的内在信心。你可以直接体验潜意识,而不是它的种种符号或意象。你可以体验这种强而有力的能量,并将它导入你的生命中。例如,艺术家利用这种能量来工作,以创作某些足以触动观赏者心情和灵魂的作品。它是一张代表信心、希望和内在平静的牌。', + upright='未来充满希望的、新的诞生、无限的希望、情感面精神面的希望、达成目标的、健康纯洁的、美好的未来、好运即将到来、美丽的身心、光明的时机、平静的生活、和平的处境。', + reversed='希望遥遥无期的、失去信心的、没有寄托的未来、失去目标的、感伤的、放弃希望的、好运远离的、毫无进展的、过于虚幻、假想的爱情运、偏执于理想、希望破灭的。' + ), init=False) + + the_moon: TarotCard = field(default=TarotCard( + id=18, index='the_moon', type='major_arcana', orig_name='The Moon (XVIII)', name='月亮', + intro='相较于其它的牌,月亮整体呈现的图面经常令人感到诡异。近景是一只龙虾爬出池塘的景象,龙虾象征比恐惧和兽性更深的情绪,伟特说牠总是爬到一半又缩回去。中景处有频频吠叫的一只狗和一匹狼,分位于左右两边,分别象征人类内心中已驯化和未驯化的兽性。中间有一条通往两塔之间,延伸向远处山脉的小径上,这条小径是通往未知的出口,只有微弱的月光映照着。一轮月亮高挂空中,总共有三个层次,最右边的是新月,最左边的是满月,而中间的女人脸孔则是伟特所谓的“慈悲面”,从新月渐渐延伸向满月,越来越大。月亮的外围则有十六道大光芒,和十六道小光芒,其下有十五滴象征思想之露珠。', + words='不安', + desc='月亮象征倾听你的梦,以找到内心世界的平静。\n\n想象是相当强而有力的,它可以让内心很快的产生和平、和谐和欢乐;它也可以以同样快的速度产生痛苦、惊惧、悲伤和愤怒。月亮是一张代表梦和想象的牌。梦是转化为意象的潜意识能量。当这股能量强烈到无法被吸收或理解时,可能会导致狂野的梦、噩梦,甚至疯狂。月亮牌所代表的潜意识恐惧,必须由我们单独去面对。\n\n月亮代表强烈的梦想和经由梦传达到你意识思想中的直觉。强而有力的梦企图告诉你某些事情。倾听你的梦,你将会发现你所要找寻的答案。', + upright='负面的情绪、不安和恐惧、充满恐惧感、阴森恐怖的感觉、黑暗的环境、景气低落、白日梦、忽略现实的、未知的危险、无法预料的威胁、胡思乱想的、不脚踏实地的、沉溺的、固执的。', + reversed='度过低潮阶段、心情平复、黑暗即将过去、曙光乍现、景气复甦、挥别恐惧、从忧伤中甦醒、恢复理智的、看清现实的、摆脱欲望的、脚踏实地的、走出谎言欺骗。' + ), init=False) + + the_sun: TarotCard = field(default=TarotCard( + id=19, index='the_sun', type='major_arcana', orig_name='The Sun (XIX)', name='太阳', + intro='可爱的裸体孩童骑在马背上,跨越灰色的围墙,脸上带着微笑。\n\n孩童头上戴着雏菊花环,以及一根红色的羽毛。这根羽毛就是在愚人与死神出现的同一根,象征太阳牌已经跨越了死亡的界限,而重获新生。围墙后面种满向日葵,里头是一座人造的花园,而孩童跃离了花园,代表他不需要这些人工的产物,他是最纯真、自然、不需隐藏的,如同他一丝不挂的身体。向日葵共有四朵,象征四要素昂与小阿尔克那的四个牌组。有趣的是,四朵向日葵是向着孩童,而不是太阳,表示这位快乐的孩童已经拥有足够的能量。马匹背上没有马鞍,孩童不用缰绳控制牠,甚至连双手也不用,显示马匹象征的能量已经受到充分控制。孩童左手持着红色旗帜,左手象征潜意识,红色旗帜象征行动,表示他已经不用像战车那样用象征意识的右手来掌控,他可以轻而易举、自然的控制一切。背景的太阳是生命的源头,万物赖以维生之源,总共有21道光芒,代表愚人以外的21张大阿尔克那,仔细一看在上方罗马数字的旁边有一道黑色的曲线光芒,代表愚人(另有一说是太阳中心圆形的部分是愚人)。这样的更改是为了避免原本的暧昧。', + words='生命', + desc='太阳象征欢乐、内在的平和,以及表达自我的需求。\n\n它也代表理解到幸福是一种选择。太阳代表一种令人愉悦的解脱。它表示觉醒的力量足以驱逐黑暗。它代表一种表达内在无意识和潜意识力量的天赋趋力。它是充满希望、理想主义,以天真率直的。\n\n太阳象征欢乐和内在平静,而且感觉宇宙是一个充满乐趣和创造性的地方。太阳是自由的充分显现。它从意识层心智的日常限制中彻底解放,转为一种开放、觉醒及自由状态。它是一种可以带来肉体自由的内心自由。太阳显示出欢乐、和平、幸福及有创意的生活态度,并且深深体会到生命之美。', + upright='前景看好的、运势如日中天的、成功的未来、光明正大的恋情、热恋的、美满的婚姻、丰收的、事件进行顺畅的、物质上的快乐、有成就的、满足的生活、旺盛。', + reversed='热情消退的、逐渐黯淡的、遭遇失败的、分离的、傲慢的、失去目标的、没有远景的、失去活力的、没有未来的、物质的贫乏、不快乐的人生阶段。' + ), init=False) + + judgement: TarotCard = field(default=TarotCard( + id=20, index='judgement', type='major_arcana', orig_name='Judgement (XX)', name='审判', + intro='天使加百列(Gabriel)在空中居高临下吹号角,号角口处有七条放射状的线,象征七个音阶,能够将人类从物质世界的限制解放出来,并且疗愈人们的身心。\n\n喇叭绑着一张正方形红十字旗帜,象征业力的平衡。天使下方是个象征潜意识的海洋,在女祭司帘幕后面就曾出现过,如今已接近终点。海洋上漂浮着许多载着人的棺材,棺材象征物质世界的旧模式。棺材中人全都是灰色的,其中最显眼的是一位象征显意识的男性,含蓄地仰望天使;一位象征潜意识的女性伸出双手,大方迎接天使的呼唤;以及象征重生人格的小孩,背对着我们。远处则是白雪霭霭的高山,伟特说这是抽象思考的顶峰。', + words='复活', + desc='审判象征清晰的判断力。\n\n审判牌意指你对人生的老旧观念已经死亡,你正在接受内心的召唤,去过一种更有意义的生活。审判牌代表此时你有清晰的判断力。作为问题的答案,这牌暗示你拥有清晰的判断力。此时你理解了你由生命所展示的试炼及挑战中学习到了什么。\n\n审判牌也可能是在形容你了解你的精神目的,也知道要达成它的必要步骤。它代表你能清楚地看到自己,以及生命的时光。这会使你对如何开始又有何收获,产生莫大的喜悦或惊慌。收成十分就近了,你可以用你的正直和诚实来面对你的报偿。现在你审判你自己,如果你没有得到所希望的,实在也没有藉口可推诿了,因为你收割的正是你努力的产物。', + upright='死而复生、调整心态重新来过、内心的觉醒、观念的翻新、超脱了束缚的、满意的结果、苦难的结束、重新检视过去而得到新的启发、一个新的开始、一段新的关系。', + reversed='不公平的审判、无法度过考验的、旧事重演的、固执不改变的、自以为是的、对生命的看法狭隘的、后悔莫及的、自责的、不满意的结果、被击垮的。' + ), init=False) + + the_world: TarotCard = field(default=TarotCard( + id=21, index='the_world', type='major_arcana', orig_name='The World (XXI)', name='世界', + intro='终于来到愚人旅程的终点。一位赤裸的舞者自由地在空中跳舞,她外貌看起来虽是女的,但在许多版本的塔罗牌中,她是雌雄同体,象征愚人终于成功将阴阳两股力量融合。\n\n舞者身体缠绕着象征高贵与神圣的紫色丝巾,象征神性其实就在每个人身上。舞者轻柔随意地手持两根权杖,象征进化与退化的力量,她同时具备两者。舞者身旁环绕着一个椭圆桂冠,桂冠象征成功,而它围绕成的椭圆形就像愚人的0号形状,愚人无限的潜力,在世界牌中发挥得淋漓尽致。桂冠上下各有一条红巾缠绕,形成倒8符号,象征无限与永恒,这在魔术师与力量牌都曾出现过。在图中四角有人、老鹰、狮子、牛,这些符号曾经在命运之轮出现过,牠们在命运之轮中还拿著书汲取知识,最后在世界牌中完成使命。', + words='达成', + desc='世界描述一种来自内心的快乐,它也可能暗示持久的成功。这是一张象征永久和持续成功的牌。你已经到达了成功之门的前方,成功女神让你耐心等待,她会让你进入成功之门的,只不过是时间问题罢了。成功之门周围是你经历过的幸福与哀伤,成功与失败,在到达乐土之前回忆一下过去的时光是很有必要的。这张牌暗示只要你拥有一颗感恩的心,就必能在你为自己打造的美丽世界中,寻找到幸福与快乐。\n\n牌的本意是“达成”,它告诉我们所有的事情都可以达成,所有的梦想都可以成为现实,没有不可能得到的事物。只要有耕耘,就能有相应的收获。', + upright='完美的结局、重新开始的、生活上的完美境界、获得成功的、心理上的自由、完成成功的旅程、心灵的融合、自信十足带来成功、生活将有重大改变、获得完满的结果。', + reversed='无法完美的、一段过往的结束、缺乏自尊的、感觉难受的、态度悲观的、丑恶的感情、无法挽回的局势、不完美的结局、无法再继续的、残缺的。' + ), init=False) + + ace_of_wands: TarotCard = field(default=TarotCard( + id=22, index='ace_of_wands', type='minor_arcana', orig_name='Ace of Wands', name='权杖首牌', + intro='一只手从云中伸出,强而有力,握住一根长满绿叶的权杖。那根权杖是如此茂盛,以致鲜嫩的绿叶几乎从杖上“爆”开,有八片叶子脱离权杖,在空中飞舞。遍地青草溪流。远方的城堡似乎暗示着未来成功的可能。', + words='行动', + desc='权杖首牌暗示这是一个好的开始,放开手脚勇敢做。\n\n权杖首牌表示实践计划的能量和欲望。权杖首牌象征一个计划强而有力的开始,代表着手新计划的渴望、力量与勇气。这张牌推出已经开始的行动,而且一定会产生具体的结果,与纸上谈兵完全不同。首牌出现在采取行动的时候,他们不是代表任何的计划于决定,而是发动新事物的具体行为。', + upright='Creation, Willpower, Inspiration, Desire, Creative spark, New initiative, New passion, Enthusiasm, Energy', + reversed='Lack of energy, Lack of passion, Boredom, Delays, Blocks, Hesitancy, Creative blocks' + ), init=False) + + two_of_wands: TarotCard = field(default=TarotCard( + id=23, index='two_of_wands', type='minor_arcana', orig_name='Two of Wands', name='权杖二', + intro='一位身穿领主服装的男子,站在他的城墙上,俯视他的辽阔领土,遥望远方海洋。他右手拿着一颗类似地球仪的球体,左手扶着一根权杖。右边的权杖则是被铁环系在墙上。城墙上有个白百合与红玫瑰交叉的图案,白百合象征纯洁的思想,红玫瑰象征热情,暗示两者之间必须取得平衡。', + words='决定', + desc='权杖二意味着一个决定。\n\n权杖二并不代表具体的行动,而是决定本身,通常是身体上的决定。行动是由权杖一所代表。在决定行动之前,权杖二代表对选择的评估,它是你所习惯的东西与你所想拥有的东西之间的一个抉择。\n\n权杖二暗示因成长而不满当前环境,需要决定未来行动方向的时机。他表示你目前所拥有的事实是不够的,你将决定下一步要怎么做。', + upright='Planning, Making decisions, Leaving home, First steps, Leaving comfort, Taking risks', + reversed='Fear of change, Playing safe, Bad planning, Overanalyzing, Not taking action, Playing it safe, Avoiding risk' + ), init=False) + + three_of_wands: TarotCard = field(default=TarotCard( + id=24, index='three_of_wands', type='minor_arcana', orig_name='Three of Wands', name='权杖三', + intro='山巅上站着一个成功的商人,三根权杖笔直地竖立在地面上,商人右手握着其中一根,目送自己的贸易船出海。天空是鲜明的黄色,海映着天,也是黄色。', + words='远见', + desc='权杖三意味着面向远方,你的未来在你的眼光里。\n\n权杖三可以表示旅行或将计划付诸实行。可以代表当你寻求自我内在意义的时候,你仍可保持相对的沉静;表示你一边在扩展自身内在于外的新大道与利益,一边在维持一种平衡的状态。权杖三同时也暗示你正在考虑你最近的状况,并且寻找你内在与外在的意义。', + upright='Looking ahead, Expansion, Rapid growth, Momentum, Confidence, Growth, Foresight', + reversed='Obstacles, Delays, Frustration, Restriction, Limitations, Lack of progress' + ), init=False) + + four_of_wands: TarotCard = field(default=TarotCard( + id=25, index='four_of_wands', type='minor_arcana', orig_name='Four of Wands', name='权杖四', + intro='四根巨大的权杖耸立在前方,其上挂着象征胜利的花环。两位女子手持花束高举头顶欢庆舞蹈着,远方隐约可见庆祝的人群,呈现一幅和谐且繁荣的景象。右边有护城河上有座桥,通往远方的表示稳固庄园城堡。', + words='稳定', + desc='权杖四意味着坚定牢固的合作。\n\n权杖四描出一个坚固的家庭或工作环境,欢乐与分享是每天生活的一部分。权杖四代表坚固,将权杖三中所决定的计划变得稳固或实在的行为。它经常暗示搬入新家或换工作,也表示你在目前的环境中安定下来。', + upright='Community, Home, Celebration, Celebrations, Reunions, Parties, Gatherings, Stability, Belonging', + reversed='Lack of support, Transience, Home conflicts, Instability, Feeling unwelcome, Lack of roots, Home conflict' + ), init=False) + + five_of_wands: TarotCard = field(default=TarotCard( + id=26, index='five_of_wands', type='minor_arcana', orig_name='Five of Wands', name='权杖五', + intro='迥异于权杖四的和谐稳定局面,权杖五呈现一群年轻人混战的场面。每个人手上都拿着一根杖,彼此僵持不下,谁也不让谁。伟特说:这是一场模仿的战役。', + words='冲突', + desc='权杖五暗示缺乏和谐或者内在的冲突。\n\n权杖五是一张代表冲突的牌,虽然冲突不至于伤害任何人,但却是所有人全盘卷入。只是权杖类型的天性,总是把生活看成战争,因为如果没有障碍,就没有冒险了。而从另外一方面来看,这张牌比较可以形容成比较,较量,竞争。', + upright='Competition, Rivalry, Conflict, Arguments, Aggression, Tension, Rivals, Clashes of ego', + reversed='Avoiding conflict, Respecting differences. end of conflict, Cooperation, Agreements, Truces, Harmony, Peace' + ), init=False) + + six_of_wands: TarotCard = field(default=TarotCard( + id=27, index='six_of_wands', type='minor_arcana', orig_name='Six of Wands', name='权杖六', + intro='一位年轻男子,戴着胜利的桂冠,骑着白马凯旋而归。四周都是围绕簇拥着他的群众。白色代表纯洁,马象征力量。红色的外衣象征积极主动与热忱。男子手持的权杖饰以胜利花环。艰辛奋斗已然过去,他现在抬头挺胸,享受属于他的荣耀时刻。', + words='自信', + desc='权杖六暗示着对人生充满自信的态度。\n\n在这张牌中,火的乐观主义使其欲求和期望得到成功。这不是错误的乐观主义或虚无的期待,而是来自过去的成功及自信的一种真正的信仰。权杖六也表示工作的升迁、证实达成目标,或仅是一种自信的生活态度。', + upright='Victory, Success, Public reward, Triumph, Rewards, Recognition, Praise, Acclaim, Pride', + reversed='Excess pride, Lack of recognition, Punishment, Failure, No rewards, Lack of achievement' + ), init=False) + + seven_of_wands: TarotCard = field(default=TarotCard( + id=28, index='seven_of_wands', type='minor_arcana', orig_name='Seven of Wands', name='权杖七', + intro='绿衣男子站在青葱的山顶上,手持权杖,奋力迎击敌人从山下攻上的六根权杖。他高举右手,表情坚毅。', + words='挑战', + desc='权杖七暗示经由坚韧不拔而获得的成功。\n\n权杖七表示你需要更大的挑战。权杖七的讯息是“不要放弃”。继续努力前进,你将得到成功的回报。你投注于完成目标的体力与行动,将是值得的。', + upright='Perseverance, Defensive, Maintaining control, Protectiveness, Standing up for yourself, Defending yourself, Protecting territory', + reversed='Give up, Destroyed confidence, Overwhelmed, Giving up, Admitting defeat, Yielding, Lack of self belief, Surrender' + ), init=False) + + eight_of_wands: TarotCard = field(default=TarotCard( + id=29, index='eight_of_wands', type='minor_arcana', orig_name='Eight of Wands', name='权杖八', + intro='八根权杖整齐划一的在空中航行,背景是蔚蓝的天空与青翠的山丘平原,还有一条宁静的小溪流过。', + words='自由', + desc='权杖八意味旅行及自由流动的能量。\n\n权杖八代表了海外旅行、自由流动的能量,以及达成目标的清晰路径。过去的努力就是在为现在的人生可以自由的旅行而铺路。权杖八表示你的目标清楚可见,而且正轻松的向它们迈进。这点可以从八根权杖自由而无约束的掠过天际看出来。权杖八没有拘束的本性反映了这是很少阻碍的时机。它表示你是自由的、可投注热情、直接追求目标。', + upright='Rapid action, Movement, Quick decisions, Speed, Progress, Sudden changes, Excitement', + reversed='Panic, Waiting, Slowdown, Slowness, Chaos, Delays, Losing momentum, Hastiness, Being unprepared' + ), init=False) + + nine_of_wands: TarotCard = field(default=TarotCard( + id=30, index='nine_of_wands', type='minor_arcana', orig_name='Nine of Wands', name='权杖九', + intro='一个壮汉靠着长杖,似乎在等待着什么。他的头上扎绷带,显示他在过去战役中曾经受伤,尚未复原。但他并不畏惧,仍然紧锣密鼓等待着敌人的下一波来袭。他身后竖立八根权杖,井井有条,像是栅栏,包围着壮汉所守护的家园。', + words='谨慎', + desc='权杖九暗示重新评估目前承诺的时候。\n\n对于既存的问题纵是期待将来能够解决,现在这个人开始回顾过去的作为,以便看清他是怎么走到今天的。他已经渐渐知道所有行为都会产生结果,就好比他目前的生活就是过去作为的结果,而将来的生活则是由现在的决定和作为来引导的。\n\n这张牌代表逐渐意识到聚焦于承诺和目的是多么重要的事了。与其栽种五百颗混合的种子来期待有好的结果,不如仔细评估只耕耘一种特殊的品种,并且悉心照料它们,以享受耕耘后的收获。', + upright='Resilience, Grit, Last stand, Persistence, Perseverance, Close to success, Fatigue', + reversed='Exhaustion, Fatigue, Questioning motivations, Stubbornness, Rigidity, Defensiveness, Refusing compromise, Giving up' + ), init=False) + + ten_of_wands: TarotCard = field(default=TarotCard( + id=31, index='ten_of_wands', type='minor_arcana', orig_name='Ten of Wands', name='权杖十', + intro='一个男人奋力的扛着十根沉重的权杖,朝着远方的房子前进。他被权杖的重量压得喘不过气,疲累万分,但他仍不愿放弃,为了生活,一步一脚印的往前走。', + words='责任', + desc='权杖十暗示一个委任某些责任的时机。\n\n权杖十描绘暗示一个委任某些责任的时机。他被这些权杖给压的沉下去,而且它们也遮住了他的方向(即远方的房子)。他急切地想要涉入这么多的情况当中,结果,因为种种承诺和问题而不胜负荷。权杖十通常伴随着一种态度:“如果你想妥适的完成它,你就要自己做。你觉得身负重任,所以不能去信任别人也能完成这件工作。\n\n尽管负担重重,然而权杖十代表你在付出极大努力后所获得的成功。或许你会因为交付出去某些责任而受惠,因为那会减轻你的压力,并且用时间去深思长期以来的憧憬。当你实现目标时。你有充分的理由为你的成就感到骄傲,因为权杖是证实了,要梦想成真就需要坚持和努力。', + upright='Accomplishment, Responsibility, Burden, Duty, Stress, Obligation, Burning out, Struggles', + reversed='Inability to delegate, Overstressed, Burnt out, Failure to delegate, Shouldering too much responsibility, Collapse, Breakdown' + ), init=False) + + page_of_wands: TarotCard = field(default=TarotCard( + id=32, index='page_of_wands', type='minor_arcana', orig_name='Page of Wands', name='权杖侍从', + intro='权杖侍从把权杖拄在地上,好奇地看着杖顶,好像在研究什么东西。他的服装是明亮的鲜黄色,外衣上有权杖家族图腾火蜥蜴,有些蜥蜴的嘴没有真正咬到尾巴,形成不完整循环,但有些却有。牌的背景是沙漠和三个金字塔。', + words='开始', + desc='权杖侍从象征新的挑战,新的消息,跃跃欲试的梦想。\n\n权杖侍从意指该是开始某些新事物的时候了。它是展开一项新方案或旅行(如果有其他旅行牌出现在牌局中)的行动,且将指引你一个新方向。权杖侍从牌描述当开始一项新的事业时,一种可以感觉到年轻活力的行动。虽然对于行动会感到紧张,但是他仍然充满激情和热心,热衷于探索有用的经验以及展开新的冒险。', + upright='Exploration, Excitement, Freedom, Adventure, Fresh ideas, Cheerfulness, Energetic, Fearless, Extroverted', + reversed='Lack of direction, Procrastination, Creating conflict, Hasty, Impatient, Lacking ideas, Tantrums, Laziness, Boring, Unreliable, Distracted' + ), init=False) + + knight_of_wands: TarotCard = field(default=TarotCard( + id=33, index='knight_of_wands', type='minor_arcana', orig_name='Knight of Wands', name='权杖骑士', + intro='权杖骑士骑着健马,高举权杖,表情自信地看着远方。他穿着明亮黄色服装,上面同样有权杖的家族象征火蜥蜴,但蜥蜴的嘴没有触碰到尾巴,形成一个不完整的循环。骑士的头盔顶端和背後都饰着红色的长穗,还戴着红手套,他以左手拉着缰绳,健马的前蹄高高举起。远方背景中出现三座金字塔,金字塔呈现在马脚的下方。', + words='改变', + desc='权杖骑士象征充满活力,信心满满的迎接改变。\n\n充满活力,信心满满的迎接改变。权杖骑士所代表的是火元素当中的火元素。这张牌可以象征行动、旅行、改变以及为了自身缘故的活动。看得出来权杖骑士正在思考未来的行动,骑士正全神贯注于对向往目标的积极追求。这张牌经常代表一种态度——完成某件事情唯一的办法就是自己动手做。瞄一眼这张牌就会得到火、活动、热情及活力的印象。权杖骑士暗示需要挑战、爱好旅游和学习,并有教学的能力。', + upright='Action, Adventure, Fearlessness, Courageous, Energetic, Charming, Hero, Rebellious, Hot tempered, Free spirit', + reversed='Anger, Impulsiveness, Recklessness, Arrogant, Reckless, Impatient, Lack of self control, Passive, Volatile, Domineering' + ), init=False) + + queen_of_wands: TarotCard = field(default=TarotCard( + id=34, index='queen_of_wands', type='minor_arcana', orig_name='Queen of Wands', name='权杖皇后', + intro='权杖皇后戴着盛开绿叶的王冠,穿着阳光般金黄服饰,坐在宝座上。她的体态强健。她的左手拿着一朵向日葵,她的右手持权杖,眼光向左望。宝座的扶手是两只狮子,后面悬吊的帷幕上,再度出现火象的狮子图腾和向日葵。她前方有一只黑猫守护,这里的黑猫似乎也在保护权杖皇后,使她免于受伤害。远方有三座金字塔,天空则是一片既明亮又祥和的浅蓝色。', + words='决心', + desc='权杖皇后代表心灵的强大,透过内在力量而达到成功。\n\n权杖皇后牌可以说是透过内在的力量和自信而获得成功的。当你面对逆境时勇气会帮助你达成目标。相信你所做的事,以及做你所相信的事,可以帮助你了解你的目标。', + upright='Courage, Determination, Joy, Confident, Self-assured, Passionate, Determined, Social, Charismatic, Vivacious, Optimistic', + reversed='Selfishness, Jealousy, Insecurities, Demanding, Vengeful, Low confidence, Jealous, Selfish, Temperamental, Bully' + ), init=False) + + king_of_wands: TarotCard = field(default=TarotCard( + id=35, index='king_of_wands', type='minor_arcana', orig_name='King of Wands', name='权杖国王', + intro='权杖国王坐在宝座上,身躯稍微向前倾,好像随时准备出发。他右手持权杖,杖上长有新鲜的绿叶。宝座和披风饰以狮子和火蜥蜴,地上还有一只火蜥蜴陪伴着他。', + words='稳重', + desc='权张国王代表经由自律而成功。\n\n权杖国王代表热忱坚定,魄力十足,经由自律而成功。他为人诚实、积极而坦率,而且经常愿意接受新挑战。他认为过程比结果还重要,而且拒绝任何拖泥带水的挑战。权杖国王描绘一个强壮的人,能够透过他的意志力来领导及统御别人。他对自己有坚强的信念,因为他的信心是建立在自身的经验上。他知道他的方法有效,因为他尝试过也试验过这种方法。自律可以让你超越自己,因此逆就会有充分的时间和体力来掌握更好的机会,让你完成已着手之事。', + upright='Big picture, Leader, Overcoming challenges, Leadership, Vision, Taking control, Daring decisions, Boldness, Optimism', + reversed='Impulsive, Overbearing, Unachievable expectations, Forceful, Domineering, Tyrant, Vicious, Powerless, Ineffective, Weak leader' + ), init=False) + + ace_of_cups: TarotCard = field(default=TarotCard( + id=36, index='ace_of_cups', type='minor_arcana', orig_name='Ace of Cups', name='圣杯首牌', + intro='圣杯首牌是所有小牌的一号牌中最富象征意义的。图中的圣杯就是耶稣在最后晚餐中使用的杯子,杯上有个倒立的M字母。据说,在耶稣死后,他的鲜血就是由这个圣杯所承装着。\n\n白鸽是天主教中圣灵的象征,牠衔着象征耶稣身体的圣饼,自上而下彷佛要进入杯中。杯中有五道水涌出,下方的水面平静,只有少许涟漪,睡莲处处,睡莲茎长,向上伸展至水面。二十五滴水珠从四面落下,飘浮在空中。一只手从云中伸出,这只手和权杖一与宝剑一中的手截然不同,它是轻轻的捧着圣杯,而非用力抓住圣杯。', + words='情感', + desc='圣杯首牌意味情感的连接和满足。\n\n圣杯首牌正位是人际关系最好的开始,经常代表新感情的开端,对于人际关系是非常好的征兆。相对于权张首牌所代表的肉体上、体力上的开始,它暗示你已打开心扉接受新机会。它可能是一段新的两性关系,或既存关系的新阶段,或一种新层次的满足。此时正是你感觉情感满足的时刻。首牌描述的是透过感情和生活产生连接。你可能正经验着正立首牌的满足感或满意感。或许你正展开一项你全心期待的计划,或是一次旅行。', + upright='New feelings, Spirituality, Intuition, Love, Emotional awakening, Creativity', + reversed='Emotional loss, Blocked creativity, Emptiness, Coldness, Feeling unloved, Gloominess' + ), init=False) + + two_of_cups: TarotCard = field(default=TarotCard( + id=37, index='two_of_cups', type='minor_arcana', orig_name='Two of Cups', name='圣杯二', + intro='一男一女面对彼此,向对方持杯致意。两人头上都戴着花环,男人身躯微微向前,左脚踏出,右手也伸向女人,而女人站姿端凝如山。他们中间浮着一根两条蛇缠绕的杖,称为“赫米斯之杖”,是治疗的象征。杖上的狮子头象征沟通,而两片翅膀象征圣灵,使人联想到恋人牌中的天使。远方是一座城镇。', + words='平等', + desc='圣杯二意指一种平等的伙伴关系或两性关系。\n\n圣杯二意指一种心灵上的契合。它形容一种既丰富又有创意的友谊或两性关系。其实,圣杯二讲的就是这两种力量的结合,若能同时拥有两种力量,且融合良好的话,会比单一力量更强大。当牌局中出现此牌时,它意味着连结你和对方的特质,那么你可能会获得某些比你单打独斗的成就还要来得大的东西。', + upright='Unity, Partnership, Connection, Attraction, Close bonds, Joining forces, Mutual respect', + reversed='Imbalance, Broken communication, Tension, Separation, Rejection, Division, Bad communication, Withdrawal' + ), init=False) + + three_of_cups: TarotCard = field(default=TarotCard( + id=38, index='three_of_cups', type='minor_arcana', orig_name='Three of Cups', name='圣杯三', + intro='三个女子紧靠彼此,围成圆圈,高举圣杯互相庆贺。她们头上都戴着象征丰收的花圈,穿着色彩艳丽的袍子,脸上幸福洋溢。四周有藤蔓、葫芦及南瓜,一位女子手上提着一串葡萄,这些植物很容易让人联想到丰收的时节。这三位女子分别有不同颜色的头发与眼珠,穿戴的衣服花环也都各有不同,代表她们都是独立的个体,有独立的个性,但是,在这个团体中,她们都能尊重彼此,敬爱彼此。三人围成圆圈的型态,表示她们之间没有尊卑之分,在这个欢庆的场合里,每个人都是如此平等。', + words='团聚', + desc='圣杯三意味庆贺或重聚。\n\n圣杯三意指欢乐、分享或庆贺。圣杯三是一张代表庆祝、团圆或当所有参与者带来欢乐的一场聚会。这杖牌可一暗示由三人或更多的人来分享成功。圣杯三意味着一段庆祝的时光,一群志同道合的人们相聚,或代表这是个重大隆盛的晚宴。\n\n圣杯三也经常代表欢庆的场合,举凡各种宴会、聚餐、婚礼、弥月、尾牙、庆功宴等都算在内。其丰收的涵义表示事情有了好的结果,不管过程曾经有多艰辛。因此,圣杯三象征丰收的时节,长久的辛苦终于开花结果,获得成功。', + upright='Friendship, Community, Happiness, Gatherings, Celebrations, Group events, Social events', + reversed='Overindulgence, Gossip, Isolation, Scandal, Excess, Loneliness, Solitude, Imbalanced social life' + ), init=False) + + four_of_cups: TarotCard = field(default=TarotCard( + id=39, index='four_of_cups', type='minor_arcana', orig_name='Four of Cups', name='圣杯四', + intro='一个男人百无聊赖地坐在树下,双眼紧闭,双手双脚合在一起,形成防御的姿态。他前方三个杯子象征他过去的经验。云中伸出一只手给他第四个杯子,他却视而不见,独自沉浸在自己的世界中。', + words='不满', + desc='圣杯四暗示要留意目前感情上的机会。\n\n圣杯四在告诉我们,应该睁开我们的双眼,在那些机会自眼前溜走之前好好的把握住它们。当你内心感到越充实时,你对外在的需求则越少。你越深思熟虑或将焦点放到内心,你就需要越稳定的基础(或与土地有更强的连结)来平衡你自己。\n\n这张牌带有一种沉闷及不悦的感觉,可能是求问者的生活日日如是,一成不变。其实生活未如想像般单调乏味的,只要求问者肯开阔视野,有些意料不到的事情便会发生。', + upright='Apathy, Contemplation, Disconnectedness, Feeling disconnected, Melancholy, Boredom, Indifference, Discontent', + reversed='Sudden awareness, Choosing happiness, Acceptance, Clarity, Awareness, Depression, Negativity' + ), init=False) + + five_of_cups: TarotCard = field(default=TarotCard( + id=40, index='five_of_cups', type='minor_arcana', orig_name='Five of Cups', name='圣杯五', + intro='在灰暗的天空底下,有一个人身着黑色斗篷,低头哀悼地上三个倾倒的杯子,里头五颜六色的酒流了出来。他的前方是一条河,象征悲伤之流,但河上有座象征意识与决心的桥,通往远处的房子。灰暗的天色反映牌中人的沮丧的内心世界。从图面上无法分辨出这人是男是女,显示悲伤的情绪无论男女皆能体验。', + words='悲观', + desc='圣杯五代表在痛苦中回转身,寻找新的机会。\n\n圣杯五形容失落和悲伤。它可能是张代表分离的牌,或者有种和人生疏离的感觉。这段期间内,那些平稳而熟悉的事物似乎都逃离你了。在新机会现身前,你必须经历这段失落或孤立期。这张牌和所有的“五”(包括隐士牌)一样,在正立时都代表心胸窄狭,而倒立时,则有心胸宽大的意味。', + upright='Loss, Grief, Self-pity, Disappointment, Sadness, Mourning, Discontent, Feeling let down', + reversed='Acceptance, Moving on, Finding peace, Contentment, Seeing positives' + ), init=False) + + six_of_cups: TarotCard = field(default=TarotCard( + id=41, index='six_of_cups', type='minor_arcana', orig_name='Six of Cups', name='圣杯六', + intro='在一座宁静安详的庄园里,有六个盛装星币花朵的圣杯。一个小男孩捧着圣杯,似乎在嗅着花香,又好像把圣杯献给小女孩。背景充斥代表快乐的鲜黄色,而天气晴和。让人彷佛有置身童话世界的感受。', + words='安全', + desc='圣杯六代表童真环境下的保障和安全。\n\n圣杯六描绘的是一种温柔而隐秘的情景,其中有某种程度的保障和安全,它带有一种可预知性。保障和安全倍受珍惜,不过这是以极高的代价换来的。因为没有什么冒险,所以通常没什么成长。\n\n圣杯六暗示以成长为代价而得到保障、安全和亲密。它可以意指你的居家或家庭状态的稳定。也可能是过去的事物或人们又出现了,等着你去处理。他也可以代表一种舒适的状态,让你有时间静下来,重新关注活力或安顿下来。', + upright='Familiarity, Happy memories, Healing, Nostalgia, Memories, Comfort, Sentimentality, Pleasure', + reversed='Moving forward, Leaving home, Independence, Stuck in past' + ), init=False) + + seven_of_cups: TarotCard = field(default=TarotCard( + id=42, index='seven_of_cups', type='minor_arcana', orig_name='Seven of Cups', name='圣杯七', + intro='七个圣杯飘浮在云雾弥漫的半空中,杯中分别装着城堡(象征冒险)、珠宝(财富)、桂冠(胜利)、龙(恐惧,另一说是诱惑)、人头、盖着布发光的人(自己)以及蛇(智慧,另一说是嫉妒)。请注意桂冠的下方有颗不显眼的骷髅头,成功与死亡并存,似乎在给人什么警惕。有个人面对着这些圣杯,不知该如何选择,他的身体姿态似乎流露出些微恐惧。', + words='梦想', + desc='圣杯七代表应该认知你内在需求。\n\n圣杯七代表的是生活中的非现实层面,包括我们的梦境、幻想与白日梦,或是偶而异想天开的点子。这种想像通常只是空中楼阁,一般人不会真的把这些幻想付诸行动,因此圣杯七不是一张代表行动的牌,而只是一种个人想像的心理状态而已。这张牌描述的是:该去想想什么是你生活重要的部分。它显示出检视环境来确认你正走在通往满足之路的过程中。圣杯七意味着深思内在生活,已进行精神或情感的回顾。\n\n圣杯七是一张代表自我发现、心灵成长以及认识内在需求的牌。提醒你,充分了解自己与自己的行动,你需要行动,也需要思考。对行动有所思考能帮助你将直接的经验转变为知识,并更向智慧与理解靠近。没有思考,行动很快就会变得重复,而没有行动与经验,思考则可能变的索然无味,且毫无意义。这张圣杯七代表你需要向内探索自己,以追求所有爱的来源。你应该确认你所真正需要的是什么,并发现什么东西足以添满你的感情。', + upright='Searching for purpose, Choices, Daydreaming, Illusion, Fantasy, Wishful thinking, Indecision', + reversed='Lack of purpose, Diversion, Confusion, Disarray, Distractions, Clarity, Making choices' + ), init=False) + + eight_of_cups: TarotCard = field(default=TarotCard( + id=43, index='eight_of_cups', type='minor_arcana', orig_name='Eight of Cups', name='圣杯八', + intro='身穿红衣红鞋的男子在暮色中,手持长杖,离开他先前辛苦建立的的八个杯子,越过河川,转身而去。四周沼泽密布,象征淤塞的情感,如同一滩死水。', + words='突破', + desc='圣杯八意味你已经突破某种状况,并显示你要追寻更多的东西。\n\n这张牌代表为了追寻一种新的满足,而放弃既有的满足方式。或许你正打算离职去找一个更有价值的工作,或者你正从你的爱的关系中撤退去寻找更深层的幸福。\n\n圣杯八意味着你正超越某人,或突破某特定状况。它表示一个人光理解还不够,还包括离开一种稳定的状态(圣杯六),去发现圣杯十所提供的满足感。没有任何人事物强迫你放弃目前的状态,除了你内心想达到更强烈满足的需求。要圆满的挑战成功,需要内在的力量,当八出现时,你就会拥有相对的勇气和力量。在大阿尔克纳牌中,第八张是力量牌。而所有塔罗牌的八也都和力量有关。', + upright='Walking away, Disillusionment, Leaving behind, Abandonment, Letting go, Searching for truth', + reversed='Avoidance, Fear of change, Fear of loss, Stagnation, Monotony, Accepting less, Staying in bad situation' + ), init=False) + + nine_of_cups: TarotCard = field(default=TarotCard( + id=44, index='nine_of_cups', type='minor_arcana', orig_name='Nine of Cups', name='圣杯九', + intro='一个财主装扮的的男子坐在小凳上,双手抱胸,神情怡然自得。他身后的高桌上,覆盖蓝色桌布,九个圣杯排排站。背景则是一片光明的鲜黄色。', + words='满足', + desc='圣杯九意味对自己的满意和荣耀感。\n\n圣杯九的昵称叫做美梦成真,代表当事人的愿望极有可能实现,无论是精神或是物质方面。这张牌表示你了解自己真正的价值,而且就是你的价值造就了今天的你。\n\n圣杯九形容一种对能圆满达成工作而感到的骄傲和满足。你内心所拥有幸福和喜悦的感觉,可能是来自于你的工作环境、人际关系,或是来自一种心灵上的成就感。现在你内在的需求已经得到满足了,而你也能思考你所赢得的成功。在这张九牌当中有着从你对自己的爱里头所滋长出来的快乐、满足和平静。', + upright='Satisfaction, Emotional stability, Luxury, Wishes coming true, Contentment, Success, Achievements, Recognition, Pleasure', + reversed='Lack of inner joy, Smugness, Dissatisfaction, Unhappiness, Lack of fulfilment, Disappointment, Underachievement, Arrogance, Snobbery' + ), init=False) + + ten_of_cups: TarotCard = field(default=TarotCard( + id=45, index='ten_of_cups', type='minor_arcana', orig_name='Ten of Cups', name='圣杯十', + intro='在卡面中我们看到一家四口和乐融融,父母亲搂抱对方,各举一只手迎向圣杯彩虹,两个孩子快乐的手牵手跳舞,背景是清翠的树木河流,和一栋房屋。', + words='家庭', + desc='圣杯十意味一个互利的团体或家庭状态。\n\n圣杯十是一张表示欢乐和分享的牌。它通常是在描述一个团队或家庭,他们在身体及精神上都能相互奉献及合作,并且共享所有的利益。圣杯十形容一个家庭或团体,而其中的每个人均能受益。因为每个人都坦然的付出和接受,因而团体的气氛和谐,大家也乐于付出。它暗示对家庭或工作环境(包括团队合作和分享)有所付出。这张是意味一个成功的家庭状态或聚合,其中每位参与者都充分的感受到对这个团体的归属感。', + upright='Inner happiness, Fulfillment, Dreams coming true, Happiness, Homecomings, Emotional stability, Security, Domestic harmony', + reversed='Shattered dreams, Broken family, Domestic disharmony, Unhappy home, Separation, Domestic conflict, Disharmony, Isolation​' + ), init=False) + + page_of_cups: TarotCard = field(default=TarotCard( + id=46, index='page_of_cups', type='minor_arcana', orig_name='Page of Cups', name='圣杯侍从', + intro='圣杯侍从穿着花朵图案的衣服,身体很轻松地站着,左手叉腰,面带微笑,用好奇的眼光,没有任何压力地看着圣杯中蹦出的一条鱼。', + words='奉献', + desc='圣杯侍从意味有益于情感的奉献。\n\n圣杯侍从是想像力最丰富的孩子。他天真无邪,敏感细心,直觉性强,爱好幻想,好奇心重,甜美可人,喜欢作梦,常常问一些让人想都想不到的问题。他很随和,合作性高,可靠,关心别人的威受,也乐意为他人服务。这样的性格形成一位善解人意、敏感,多愁善感,强调感情交流互动的人。他认真对待他人,对於所爱的人更是忠诚。他也是一位勤勉好学和专心致志的人,自动自发地提供服务朝向特定目标努力,他热心助人,是值得信赖的好帮手,更是良好的工作伙伴。\n\n塔罗牌中的侍从牌都和学习有关,而且由于圣杯组牌涉及情感和直觉,所以这张牌可能意味着透过冥想,或其他任何类似的被动方式来进行心灵上的学习或发展。圣杯侍从代表一段新关系或圣以合伙关系的到来。一个让情感得到满足的机会。', + upright='Happy surprise, Dreamer, Sensitivity, Idealism, Naivete, Innocence, Inner child, Head in the clouds', + reversed='Emotional immaturity, Insecurity, Disappointment, Emotional vulnerability, Immaturity, Neglecting inner child, Escapism' + ), init=False) + + knight_of_cups: TarotCard = field(default=TarotCard( + id=47, index='knight_of_cups', type='minor_arcana', orig_name='Knight of Cups', name='圣杯骑士', + intro='不同于权杖骑士或宝剑骑士的迅捷骑马姿态,圣杯骑士的白马很有绅士风度,优雅地行进,跟主人一样。圣杯骑士平举着圣杯,他的眼光有些梦幻,深深注视着圣杯。', + words='选择', + desc='圣杯骑士意味在感情和行动之间做出决定。\n\n圣杯骑士暗示来自某人的供给。它可能是指情感上的奉献,或某种更为实际的事物。它可能是指情感上的付出,或某种更为实际的事物。骑士也意味着一段决定是否等待或行动,让事情充分发展或找寻新机会的时期。为了发现满足,或许现在是随着心意(河流的象征)而为的时候了。', + upright='Following the heart, Idealist, Romantic, Charming, Artistic, Graceful, Tactful, Diplomatic, Mediator, Negotiator', + reversed='Moodiness, Disappointment, Tantrums, Turmoil, Avoiding conflict, Vanity' + ), init=False) + + queen_of_cups: TarotCard = field(default=TarotCard( + id=48, index='queen_of_cups', type='minor_arcana', orig_name='Queen of Cups', name='圣杯皇后', + intro='圣杯皇后双手捧着圣杯,眼神直直的注视着圣杯。那圣杯是教堂形状,两臂各有一位天使,顶端是十字架,象征圣杯皇后的虔诚。她坐在海边的宝座上,宝座基部有个小美人鱼抓鱼的图案,顶部是两个小美人鱼共同抱着一个大蚌壳。', + words='倾听', + desc='圣杯皇后意味透过倾听直觉而成功。\n\n圣杯皇后意味透过倾听感觉,以及利用富创意的想象力而获得成功。她从经验得知,杂乱无章的想象所产生的结果通常是有限的,因此它可以将精力用在对身体、情感、精神及心灵上都相当有价值的行动上。虽然她可能显得温柔又细心,但眼神却意味着一种坚强的意志。爱调和她的意志,并增加个性上的深度。她带着爱心和怜悯行事,而且常常展现出浓浓的家庭感情。如果发生问题,她可能不会说出她的感觉,但仍然会对周遭的人给于支持,把自己的感情的困扰放在一边。', + upright='Compassion, Calm, Comfort, Warmth, Kindness, Intuition, Healer, Counsellor, Supportive', + reversed='Martyrdom, Insecurity, Dependence, Giving too much, Overly-sensitive, Needy, Fragile' + ), init=False) + + king_of_cups: TarotCard = field(default=TarotCard( + id=49, index='king_of_cups', type='minor_arcana', orig_name='King of Cups', name='圣杯国王', + intro='国王坐在波涛汹涌海中央的宝座上,左边有条鱼跳出海面,右边有一艘帆船。他的内袍是代表水要素的蓝色,胸前还挂著鱼形项链。他左手拿著象征权力的杖,右手持圣杯,他却是圣杯家族中唯一不注视圣杯的人。', + words='创作', + desc='圣杯国王暗示透过创造和情感上的训练而成功。\n\n圣杯国王展现深度和理解力,他适合一个以满足他人的需求为主的位置。他感情已经成熟到能够清楚的考虑别人和自己的需求,而且常常以家庭及环境中的共同参与感为荣。\n\n圣杯国王暗示透过情感和创作上的训练而成功,经由落实精力在有创作的目标上,可以达到所追寻的成功。一种成熟、有创意的方法带来琛功,尤其是在创造和艺术的努力上。这张国王牌暗示你应该信赖你本能——别放弃。它暗示一种坚强又冷静的方式。想象加灵感,再加上实际的努力就会得到回报。', + upright='Compassion, Control, Balance, Wise, Diplomatic, Balance between head and heart, Devoted, Advisor, Counsellor', + reversed='Coldness, Moodiness, Bad advice, Overwhelmed, Anxious, Cold, Repressed, Withdrawn, Manipulative, Selfish' + ), init=False) + + ace_of_swords: TarotCard = field(default=TarotCard( + id=50, index='ace_of_swords', type='minor_arcana', orig_name='Ace of Swords', name='宝剑首牌', + intro='一只手从云中伸出,紧紧握住宝剑,宝剑穿过皇冠与桂冠,而远方是毫无绿意的尖锐山头,以及灰白空旷的天际。', + words='思想', + desc='宝剑首牌代表毅然决然的行动,开始计划一项新的冒险。\n\n宝剑首牌代表的是一个开始,时涉及以相信冒险或方案的行动。权杖首牌描述身体上的行动,杯子牌的首牌则是情感上的行动,而这张首牌叙述一个意念的形成,或是为未来的行动所准备的计划。这张牌代表清晰的思考,或明确的了解到完成一项计划所需要的是什么。\n\n同时这把双面的宝剑强调着现实、成就与成功所必须负担的责任和应得的报酬。宝剑一只是一个开端,一种可能。未来究竟要如何发展,掌握在持剑者的手中。', + upright='Breakthrough, Clarity, Sharp mind, New idea, Concentration, Vision, Force, Focus, Truth', + reversed='Confusion, Brutality, Chaos, Miscommunication, Hostility, Arguments, Destruction' + ), init=False) + + two_of_swords: TarotCard = field(default=TarotCard( + id=51, index='two_of_swords', type='minor_arcana', orig_name='Two of Swords', name='宝剑二', + intro='身穿浅灰长袍的女人坐在灰石凳上,背对着澎湃汹涌、暗礁满布的海洋。她眼蒙白布,双手持剑,在胸前交叉不动。天际高挂一轮新月。', + words='抉择', + desc='宝剑二意味着做出一个决断,无论对与错,不要试图逃避。\n\n宝剑意味为你需要作决定或在两个选择当中择其一。这是二则一的抉择,或许在目前这个阶段,你对于所做的选择会产生怎样的结果,洞察力还不够。你在做决定的时候,并没有对你的环境做通盘的考虑,或者是,你没有考虑到你的抉择会带来怎样的结果。\n\n正视你所恐惧的,如此你才能明了你周遭事物对你有什么意义。一个正确决定的报偿正等着你,它的第一个回报是解脱感,这解脱感来自于你能够锁定一个方向。', + upright='Difficult choices, Indecision, Stalemate, Stuck in the middle, Denial, Hidden information', + reversed='Lesser of two evils, No right choice, Confusion, Indecision, Hesitancy, Anxiety, Too much information, Truth revealed' + ), init=False) + + three_of_swords: TarotCard = field(default=TarotCard( + id=52, index='three_of_swords', type='minor_arcana', orig_name='Three of Swords', name='宝剑三', + intro='映入眼帘的是一幅令人痛苦的画面。即使是完全没有接触过塔罗牌的朋友,也可以轻易道出宝剑三的涵义──伤心。三把剑合力刺进一颗鲜红的心,背景是灰暗的雨和云。某些版本的塔罗牌给这张牌一个更直接的名称,叫做“悲伤”。', + words='悲伤', + desc='宝剑三意味着伤心在所难免,请接受你的痛苦和悲伤。\n\n宝剑三代表的是,你正强烈的经验这悲伤和失落的一段时间。当出现这张牌时,内心的困惑、悲痛和沉重是很明显的,它表示强烈的失望。但你要知道:去体验你的悲伤是很重要的,因为在这么做的同时,你也扫除了障碍,让即将到来的机会可以接近你。记住,悲伤是会过去的。\n\n虽然痛苦,但我们要看破痛苦的假象。宝剑三凌空的心,告诉我们需要再去深入思考,以获得解脱和更深的觉醒,三把宝剑只是一种试炼,这颗心也可以是一种假托,而不是我们真正的心灵。以承受和接纳的态度,来化解宝剑成为优美的思考认知。', + upright='Heartbreak, Suffering, Grief, Separation, Sadness, Sorrow, Upset, Loss, Trauma, Tears', + reversed='Recovery, Forgiveness, Moving on. healing, Reconciliation, Repressing emotions' + ), init=False) + + four_of_swords: TarotCard = field(default=TarotCard( + id=53, index='four_of_swords', type='minor_arcana', orig_name='Four of Swords', name='宝剑四', + intro='图中的男人在类似修道院的建筑物内休息,双手合抱胸前,呈现安详的状态。彩绘玻璃表现一个祈祷者跪在圣母面前的画面,好像在寻求什么建议,以获得内心的宁静。三把宝剑挂在墙上不用,但他身旁仍保有一把宝剑,当他醒来,随时可以拿起宝剑来采取行动。', + words='沉思', + desc='宝剑四暗示在危机中安静的思考,退隐中的深思熟虑。\n\n\n宝剑四这张牌可能象征自生活中撤离:身体上退隐到自家当中,或在精神上退隐到梦想和幻想当中。这是一张反省过去行为和计划未来的牌。他说明精神层面的巩固:采取让过去行为有意义的行动,以及排除那些已经被证实为不正确、或没有建设性的想法和信念。如此一来就有可能运用过去的经验来帮助你获得未来的成功。在经历了宝剑三的痛苦之后,随之而来的是对你自己和你的人生有更深层的了解。', + upright='Rest, Restoration, Contemplation, Relaxation, Peace, Sanctuary, Recuperation, Self-protection, Rejuvenation', + reversed='Restlessness, Burnout, Stress, Recovery, Awakening, Re-entering world, Release from isolation' + ), init=False) + + five_of_swords: TarotCard = field(default=TarotCard( + id=54, index='five_of_swords', type='minor_arcana', orig_name='Five of Swords', name='宝剑五', + intro='红发的男子右手抱着两把剑,左手拄着另一把,回头注视远方两个失败者,嘴角似乎带着微笑。很明显的,他们刚结束一场争执,也许暴力相向。地上还散落着两把剑。另外两人中,一人怅然离去,一人用手摀着脸,似乎难以接受,或者感到伤心羞辱。天空中被风吹散的云彷佛也在说着他们争执的故事,看来很不宁静。', + words='纷争', + desc='宝剑五意味误会加深,争吵和紧张,解决的机会十分渺茫。\n\n宝剑五这张牌代表争吵、紧张和冲突,这可能使指你与自己内在的交战,或和你周遭人的不协调。假如这个冲突是指你和别人的,则其前提很有可能来自你的思想。在这种冲突的情况下,每个人对于事情的解决方法都各有见地,却没有人愿意聆听他人的心声。', + upright='Unbridled ambition, Win at all costs, Sneakiness, Arguments, Disputes, Aggression, Bullying, Intimidation, Conflict, Hostility, Stress', + reversed='Lingering resentment, Desire to reconcile, Forgiveness, Reconciliation, Resolution, Compromise, Revenge, Regret, Remorse, Cutting losses' + ), init=False) + + six_of_swords: TarotCard = field(default=TarotCard( + id=55, index='six_of_swords', type='minor_arcana', orig_name='Six of Swords', name='宝剑六', + intro='一艘小船上插着六把宝剑,船上有一个女人、一个小孩与一位船夫。\n\n船缓缓的朝远方的岸边前进,而此端的水汹涌,彼方的水平静。象征伤害的六把剑插在船身上,以及三个主角哀伤的背影,构成宝剑六缓慢低回的基调。沉重的剑身让船夫只能缓行,疗伤的过程亦同。但是我们不能把宝剑抽起,否则船会沉,正如我们不能把过去的哀伤连根拔起,只能轻轻的抚平。也许你该庆幸,这些宝剑并不能使船沉没。', + words='平静', + desc='宝剑六暗示远离是非,在混乱之后,逐渐回复平静。\n\n这是受伤后康复的过程,不管伤多重,总是能痊愈。水象征情绪,这端汹涌的水是你烦扰的过去,前方大片平静的水面,预示未来安详的情绪。船夫手持黑色长篙,黑色象征潜质,将来什么都还是可能发生,不要将自己困死了。宝剑六是一个信道,领你向未来的幸福快乐前进,光明的日子就在前方。\n\n这张牌暗示你正带着你的剑(问题),从过去走向未来。或许你根本没注意到它们,然而它们却是与你紧紧相随。这是一个从艰困时刻过渡到一个较为平衡状态的过程。即使现时的问题及困难如何复杂,最终都会得到解决,求问者届时心情自然轻松不少。宝剑六可能是在说明当你转移向新的经验时,你也正慢慢的远离困境,情绪从过去释放出来。', + upright='Transition, Leaving behind, Moving on, Departure, Distance, Accepting lessons', + reversed='Emotional baggage, Unresolved issues, Resisting transition, Stuck in past, Returning to trouble, Running away from problems, Trapped' + ), init=False) + + seven_of_swords: TarotCard = field(default=TarotCard( + id=56, index='seven_of_swords', type='minor_arcana', orig_name='Seven of Swords', name='宝剑七', + intro='图中的男子身处军营中,趁着远方敌人炊饭没有防备时,悄悄偷走五把剑,还留着两把在原处。', + words='逃避', + desc='宝剑七意味另辟蹊径,若要成功的话,需要一种新的方法。\n\n宝剑七所传达的讯息是:不要放弃。去找寻另一种可以达成目标的方法吧。坐下来,检查一下你所有的选择,以便发现先前未曾预见的可能性。你当然还有时间来完成你的愿望,然而在方法上需要更有弹性,各种行动的不同组合方式,就有可能会带来不同的结果。\n\n宝剑七暗示经由审慎评估各种可能,你就能找到有效的解决之道。透过详细的规划和不放弃的决心,你就能得到更多。比如你目前正汲汲营营于某件重要的事,理智所提供的解决方案会让你不需要如此费劲。\n\n宝剑七同时也是一张秘密、隐藏动机和不坦诚的牌。牌中暗示求问者欲逃避一段令他不愉快的事情,这件事可能会令他有金钱损失或与面子有关,求问者若肯勇敢面对,并应用智慧及交际手段去补救。', + upright='Deception, Trickery, Tactics and strategy, Lies, Scheming, Strategy, Resourcefulness, Sneakiness, Cunning', + reversed='Coming clean, Rethinking approach, Deception, Confession, Conscience, Regret, Maliciousness, Truth revealed' + ), init=False) + + eight_of_swords: TarotCard = field(default=TarotCard( + id=57, index='eight_of_swords', type='minor_arcana', orig_name='Eight of Swords', name='宝剑八', + intro='一个女人眼睛被布蒙住,上半身被捆绑着,身处八把宝剑群中。地上满是象征羞辱的泥泞,而远方是座矗立于峭壁之上的城堡,象征绑住她的权威。', + words='限制', + desc='宝剑八暗示限制及丧失个人的能力。\n\n宝剑八代表的是你被限制住的一段时间,或是在某种情况下你失去个人的能力。你觉得动弹不得,受到限制,而且没有办法看清楚你前面的路。\n\n塔罗牌的“八”是代表力量的牌。而对于宝剑八里面的女人,这份力量源自于倾听她内在的声音的能力。双眼被蒙蔽让她无法透过视觉来做判断,她显得那么的无能为力,然而第一眼看上去这是个阻碍,但其实却是助力。阻碍那个女人控制自己所处环境的力量,却使得她能够走进自己的内心世界倾听内在的声音,并且留心它所发出的指令。如果你想做出有效率的决定,现在是留心你的自我精神层次的时候了。\n\n去探索那等待着你的道路吧,利用你内在的力量和个人的能力,将自己从目前的情况中释放出来,并且把那些曾经屈服于他人的个人能量重新召唤回来。你的信念其实才是你最大的限制。好好自省并检视这些信念,因为事实上目前的“眼罩”是在帮助你,因为它可以让你不会分心。', + upright='Imprisonment, Entrapment, Self-victimization, Trapped, Restricted, Victimised, Paralysed, Helpless, Powerless', + reversed='Self acceptance, New perspective, Freedom, Release, Taking control, Survivor, Facing fears, Empowered, Surrender' + ), init=False) + + nine_of_swords: TarotCard = field(default=TarotCard( + id=58, index='nine_of_swords', type='minor_arcana', orig_name='Nine of Swords', name='宝剑九', + intro='午夜梦回,一个女子从睡梦中惊醒,把脸埋在双手中。墙上横挂着九把剑,看起来彷佛从后面刺穿那女子,带给人极大的压迫感。棉被图案是由象征热情的玫瑰,以及星座符号组成的。床侧则雕刻着一人击败另一人的画面。', + words='梦魇', + desc='宝剑九暗示由梦境传达的直觉,或对问题的担心。\n\n宝剑九代表的是强烈的梦。或许你的潜意识正努力教导你某些事情,去倾听你的梦境。宝剑九是一张代表担心和情绪骚动的牌。这种担心可能是对自己或周遭的一切。也可以代表鲜明的梦境或梦魇,而梦魇则可能是在传达一种强烈的讯息,即你生命当中某些不对劲的事物,已由潜意识而浮现在你的意识层面了。\n\n假设你将你的梦境写成日志,或许会发现一个共同的线索或是明显的讯息。那么你的梦就可以变成一项接近你潜意识的有效工具了。', + upright='Anxiety, Hopelessness, Trauma, Fear, Negativity, Breaking point, Despair, Nightmares, Isolation', + reversed='Hope, Reaching out, Despair, Recovery, Learning to cope, Facing life, Finding help, Shame, Guilt, Mental health issues' + ), init=False) + + ten_of_swords: TarotCard = field(default=TarotCard( + id=59, index='ten_of_swords', type='minor_arcana', orig_name='Ten of Swords', name='宝剑十', + intro='一个俯卧的男人,背上插着十把剑,有一把甚至从插进耳朵里去。这画面实在令人怵目惊心。牌面中有一半被黑色的天空和乌云所占去,多少暗示宝剑十这张牌是大家避之唯恐不及的所谓的“坏牌”。', + words='失败', + desc='宝剑十意味着痛苦挥之不去,在另一个开始之前某种状况的结束。\n\n这张牌暗示在某种情况下,你已到了最低潮的时刻,你也可能是被一些无用的事物,或对生命具破坏性的信念给绊住了。但远方微弱的阳光暗示着,尾随这艰困时刻的将会是新的以及更好的事物。你对人生的思想或信念导致你此刻的境遇,从这里,你的思想将会带领你到任何你认为能够去的地方。\n\n宝剑十代表一种情况的结束。可能指两性关系的结束,或某关系中的一个阶段的结束,或一项事业的失败。你生命中的某些事物已经结束了,虽然这毫无疑问的会是一段艰难的时期,不过好消息是,它终究会过去,接受这个事实有助于新事物来取代旧的的。', + upright='Failure, Collapse, Defeat, Ruin, Bitterness, Exhaustion, Dead end, Victimization, Betrayal', + reversed="Can't get worse, Only upwards, Inevitable end, Survival, Improvement, Healing, Lessons learned, Despair, Relapse" + ), init=False) + + page_of_swords: TarotCard = field(default=TarotCard( + id=60, index='page_of_swords', type='minor_arcana', orig_name='Page of Swords', name='宝剑侍从', + intro='宝剑侍从两手握著宝剑,眼光却朝著远方。他的头发和背景中的树都被风吹得飞扬。远方天空中有十只小鸟成群飞舞。背景灰云带来些许混乱的气氛', + words='幻想', + desc='宝剑侍从象征太多的梦想,而行动却不够。\n\n你可以发现到这个侍从双脚离地甚远,这个思考敏捷的年轻人喜欢说话、有很多点子和创新的概念,而这些成双出现的点子却无法搭在一起。这表示一种生活的态度,这种态度要求你透过梦境和思想让自己从现实抽离出来。\n\n宝剑侍从可能代表有关你目前所拥有的一个构想或计划的消息。但却没有付诸行动。对那些依赖创意和思考维生的人而言,这可说是一张正面的牌,但是也可能暗示脚踏实地是必要的,假设你想生产实际或有形的东西。', + upright='Curiosity, Restlessness, Mental energy, Curious, Witty, Chatty, Communicative, Inspired, Vigilant, Alert, Mental agility', + reversed='Deception, Manipulation, All talk, Scatterbrained, Cynical, Sarcastic, Gossipy, Insulting, Rude, Lack of planning' + ), init=False) + + knight_of_swords: TarotCard = field(default=TarotCard( + id=61, index='knight_of_swords', type='minor_arcana', orig_name='Knight of Swords', name='宝剑骑士', + intro='宝剑骑士和圣杯骑士同样骑着白马,但宝剑骑士这匹马在狂风中极速奔驰,与圣杯骑士平缓前进的马形成强烈对比。宝剑骑士将宝剑高举过头,表情狰狞,向前冲杀。马鞍上饰以蝴蝶和鸟,象征风要素。他穿着铁甲,外袍也有鸟的图案,而靴子前后都带着尖刺,在战场上毫不留情。云和树都被狂风吹得七零八落。空中飞翔的鸟,队形也略显散乱。', + words='急躁', + desc='宝剑骑士暗示要达成愿望需要有敏捷的行动。\n\n宝剑骑士代表的是迅速的行动:跃进或跳出某种情景。作为某个问题的答案,它暗示着一个快速的动作或出其不意的行为是有需要的。已经没有时间去想该做何选择了——去做就对了。\n\n这张牌通常是代表一个年轻人,他不按牌理出牌、缺少耐心、思考敏捷。这是属于年轻人的力量,他要走往自己的道路。是一种英勇的行径或者说英雄气概的展现。当然这种冲撞的行动,也可能极具破坏力,能造成摧毁的状况。他的意志力坚强,专注而犀利,有着清明的勇气和专一凝聚的心志。', + upright='Action, Impulsiveness, Defending beliefs, Assertive, Direct, Impatient, Intellectual, Daring, Focused, Perfectionist, Ambitious', + reversed='No direction, Disregard for consequences, Unpredictability, Rude, Tactless, Forceful, Bully, Aggressive, Vicious, Ruthless, Arrogant' + ), init=False) + + queen_of_swords: TarotCard = field(default=TarotCard( + id=62, index='queen_of_swords', type='minor_arcana', orig_name='Queen of Swords', name='宝剑皇后', + intro='宝剑皇后戴著蝴蝶花纹的王冠,象征灵魂,也象征风要素。她穿着灰色内袍,和蓝天灰云花纹的披风。她的表情坚毅,似乎皱著眉头,左手却对世界敞开。她右手高举宝剑,剑尖笔直向上。她的宝座扶手之下有个人头花纹,那是风之精灵,宝座的底部又有蝴蝶花纹。宝剑皇后的头顶上有只高飞的鸟。背景天空是深蓝色的,还有大片的灰云。', + words='理智', + desc='宝剑皇后代表淡定冷静,经过深思熟虑所得到的成就。\n\n宝剑皇后是一张思索感情的牌。它可能意味运用心智到情感中的行动,好让感觉有意义。作为某个问题的答案,宝剑皇后暗示透过清晰思考而获致成功。\n\n现在正是你反省过去的行为或目前情况的时刻了。密切的观察那些接近你的事物,以确认你不会再重陷困境中。你可能会想从生活当中撤退,好好的思考你自己,以及未来的方向。', + upright='Complexity, Perceptiveness, Clear mindedness, Honest, Independent, Principled, Fair, Constructive criticism, Objective, Perceptive', + reversed='Cold hearted, Cruel, Bitterness, Pessimistic, Malicious, Manipulative, Harsh, Bitter, Spiteful, Deceitful, Unforgiving' + ), init=False) + + king_of_swords: TarotCard = field(default=TarotCard( + id=63, index='king_of_swords', type='minor_arcana', orig_name='King of Swords', name='宝剑国王', + intro='宝剑国王是四张国王牌中唯一以正面出现的。他穿著蓝色内袍和红色披风,他的右手持剑,剑尖偏右,偏向行动的那一边。左手戴着象征权力的戒指,轻松的放在腿上。他后方帷幕上饰有象征灵魂和风要素的蝴蝶花纹。天空中的鸟的数量有两只,象征在智慧与行动之间的选择,对宝剑国王而言,智慧必须用行动来实现。', + words='公正', + desc='宝剑国王暗示将梦想化为现实,用构想去做一些真实的事。\n\n宝剑国王是客观理性,凡事讲求合理和公正,具有坚定而一贯的信念和完整的思想体系,很难被他人所影响。他凭借事实和原则而下决定,不会情感用事或主观成见,并且会考虑得十分周到,显出谨慎和深沉的特色。\n\n宝剑象征着人的思想和决心,这位国王手执宝剑,自然具有着掌握思考的能力,并且很重视理念和原则,在意的是合理与正义。宝剑国王代表对清楚的思想的追求、诚实,以及将只是倒入现实的需求。作为某个问题的答案,这张国王牌可以说是透过清楚而有效之计划而达到成功。', + upright='Head over heart, Discipline, Truth, Reason, Authority, Integrity, Morality, Serious, High standards, Strict', + reversed='Manipulative, Cruel, Weakness, Irrational, Dictator, Oppressive, Inhumane, Controlling, Cold, Ruthless, Dishonest' + ), init=False) + + ace_of_pentacles: TarotCard = field(default=TarotCard( + id=64, index='ace_of_pentacles', type='minor_arcana', orig_name='Ace of Pentacles', name='星币首牌', + intro='云中伸出一只手,捧着一枚星币。背景是花草满布的繁盛庭园,绿树拱门外的远方有座白色的山,暗示星币一不只有关物质,也可以延伸到精神层面。', + words='物质', + desc='星币首牌暗示,你有足够的钱好执行你的计划。\n\n星币首牌是张将梦想化为实质的牌。圣杯牌组中,我们有梦﹔星币牌组中,我们筑梦,梦想不再只是空中楼阁。星币首牌让我们稳健,踏实,有安全感。星币首牌和务实的开始有关。它意味你有足够的金钱、精力,或充分的条件,来开始一项新计划。它暗示你可以平衡掉花费。不论目前花掉了多少钱,赚回来的绝对够本。', + upright='Opportunity, Prosperity, New venture, New opportunities, Resources, Abundance, Security, Stability, Manifestation', + reversed='Lost opportunity, Missed chance, Bad investment, Missed chances, Scarcity, Deficiency, Instability, Stinginess, Bad investments' + ), init=False) + + two_of_pentacles: TarotCard = field(default=TarotCard( + id=65, index='two_of_pentacles', type='minor_arcana', orig_name='Two of Pentacles', name='星币二', + intro='一个红衣装扮,头戴高帽,类似街头艺人的男子,正在耍弄两个星币,星币外围的带子形成8自形无限符号,魔术师和力量牌中也有这个符号。他背后的海面起伏剧烈,两艘船正在其上行驶。', + words='两难', + desc='星币二暗示与金钱有关的决定。\n\n星币二显示一个专注于钱财的人。此时他并没有重大的财务压力,只是要决定那张账单要先付而已。保持弹性,是星币二带给我们的另一个课题。除了随机应变的弹性,星币二也求取平衡。\n\n星币二描述着权衡各种机会的轻重,而这次它们是属于身体或物质的层面上。这象征着介于两个选择之间的决定。你有没有办法现在就抉择,或是再等一会儿会不会比较好呢?', + upright='Balancing decisions, Priorities, Adapting to change, Balancing resources, Adaptation, Resourcefulness, Flexibility, Stretching resources', + reversed='Loss of balance, Disorganized, Overwhelmed, Imbalance, Unorganized, Messiness, Chaos, Overextending' + ), init=False) + + three_of_pentacles: TarotCard = field(default=TarotCard( + id=66, index='three_of_pentacles', type='minor_arcana', orig_name='Three of Pentacles', name='星币三', + intro='在一座修道院里头,有位雕刻师正在工作,旁边两位修道人拿着草图,似乎正在和雕刻师讨论工作的进度。', + words='学习', + desc='星币三暗示透过研究、学习,或者将构想付诸实现,而改善自身的境遇。\n\n这张牌代表扎根于稳固的基础上,建立某些具有持久价值的东西。也许你是在建造一栋房子,开始学习一个对你有助益的课程,或为稳固的两性关系或生意打基础。星币三对自我发展而言是张正面的牌。星币三表示去作某些将可以改善你环境事情的一段时间。它可能是开始一个课程、阅读书籍,或如果它是出现在事业的分析中,那就是你在工作当中学习拥有一个机会去建立某种具有永久价值的东西。\n\n星币三是一个鼓励,鼓励当事人不管在进行什么样的工作,都可以仔细计划,然后放手去做,因为他具备完成工作所需要的专业能力,他有充足的才干来达成手边任何任务。星币三的成功不是偶然,他不仅有专业能力,还实实在在的工作。', + upright='Teamwork, Collaboration, Building, Shared goals, Apprenticeship, Effort, Pooling energy', + reversed='Lack of teamwork, Disorganized, Group conflict, Lack of cohesion, Apathy, Poor motivation, Conflict, Ego, Competition' + ), init=False) + + four_of_pentacles: TarotCard = field(default=TarotCard( + id=67, index='four_of_pentacles', type='minor_arcana', orig_name='Four of Pentacles', name='星币四', + intro='图中的男人戴着皇冠,身穿象征统治威权的红色袍子,下摆饰以蓝边,显示出崇高的领主身分。他坐在一个箱子上,头顶一枚星币,双手紧抓着另一枚,双脚又各踩着两枚,紧张的神情似乎深怕他失去任何一丁点财产。这个人虽有钱,却孤绝于城市人群之外。', + words='节约', + desc='星币四意味厚积薄发,节省你的金钱或体能以迎接更大的挑战。\n\n星币四正位置常代表物质上的获利与稳定,获利的来源可能是工作,也可能是接受赠与或遗产。然而,星币四代表物质上的稳定,却不保证心灵上的成长。星币四意味你正在节约金钱、节省精力,或是节制。它也可能意味经由节约金钱、偿还债务及量入为出,而是你的财务状况日趋稳定。或许你在设计增加收入或减少指出,以确保自己进来的钱比出去的多。', + upright='Conservation, Frugality, Security, Possessiveness, Insecurity, Hoarding, Stinginess, Stability, Savings, Materialism, Wealth, Boundaries, Guardedness', + reversed='Greediness, Stinginess, Possessiveness, Generosity, Giving, Spending, Openness, Financial insecurity, Reckless spending' + ), init=False) + + five_of_pentacles: TarotCard = field(default=TarotCard( + id=68, index='five_of_pentacles', type='minor_arcana', orig_name='Five of Pentacles', name='星币五', + intro='冰天雪地中,两个乞丐蹒跚前行,又瘸又驼背,身上的衣服破烂不堪。他们经过一间象征物质与精神庇护的教堂,却视而不见,挺着饥饿且疲惫的身躯,径自赶路。', + words='困难', + desc='星币五意味对那些充实你的事物的疏离感。\n\n卡面上的两个人本可以选择如何去发现、跟随及落实精神之路。教堂其实只是他们的一种选择。它代表把精神价值介绍给那些无意去追求的人。在五这张牌中,这些人没有看见它,因此丧失了一个改变的机会。外在悲惨是内在悲惨的一种反映,所以当星币五出现时,你需要接受生命提供给你的改变机会。“如果你想改变这个世界,请先改变你自己”是这张牌的答案。\n\n就整体观点来看,星币五说的是财务上的困难、贫穷、疾病和内在的寂寞。在不断的挣扎当中,你很容易窄化你对问题的焦点,而忽略了你的机会。当这张五出现时,深度的心灵改变是有其需要的,否则虽然有外在的助力,可能还是解决不了你的问题。你目前的人生观并非你的支柱,而现在你必须问自己,是否仍愿意保有这些信念。', + upright='Need, Poverty, Insecurity, Hardship, Loss, Isolation, Feeling abandoned, Adversity, Struggle, Unemployment, Alienation, Disgrace', + reversed='Recovery, Charity, Improvement, Positive changes, Recovery from loss, Overcoming adversity, Forgiveness, Feeling welcomed' + ), init=False) + + six_of_pentacles: TarotCard = field(default=TarotCard( + id=69, index='six_of_pentacles', type='minor_arcana', orig_name='Six of Pentacles', name='星币六', + intro='一个商人装扮的男子,脚边跪着两个乞丐。商人右手施舍其中一名乞丐,左手拿着象征平衡的天秤。', + words='施舍', + desc='星币六暗示没有绝对的公平,其中一人比另一人更有控制力。\n\n星币六是在形容一种结构性的关系,其中一人比另一人更有控制力。是一张有很多层面的牌,而它的意义又会随着问题或周遭的牌而改变,在这张牌中,看似公平和正当,不过,请注意,两个乞丐是跪在富翁的面前。在这个关系里,他是处于有权力的地位。星币六是在形容一种关系:一个人支配另外一个人。\n\n跪在地上的人事实上是受制于他的,暗示着局面是由他所控制,而他是透过他的财富来掌控这一切。这个站着的人深谙拥有金钱就是拥有权力。他就越能选择自己的人生。施与受中间不只是金钱,知识、经验、技术的传授也算。所以星币六也代表知识、经验、技术的传授或是学习。', + upright='Charity, Generosity, Sharing, Community, Material help, Support, Giving and receiving, Gratitude', + reversed='Strings attached, Stinginess, Power and domination, Power dynamics, Abuse of generosity, Strings attached gifts, Inequality, Extortion' + ), init=False) + + seven_of_pentacles: TarotCard = field(default=TarotCard( + id=70, index='seven_of_pentacles', type='minor_arcana', orig_name='Seven of Pentacles', name='星币七', + intro='一位农夫把下巴架在杖上,低头看着他长久辛勤得来的收成。这丛农作物在他的耕耘下,已经可以自己成长茁壮了。农夫的表情似乎很满足,又彷佛在思考下一步该怎么做。', + words='规划', + desc='星币七意味着思考未来的财务或物质状况。\n\n星币七暗示目前工作即将完结,只剩下一点尾巴要收而已。经历过去长时间段孜孜不倦的努力,现在可以暂停一下,看看自己目前的成就,想想下一步的行止。星币七是一种实际面上的投资与等待,并且具有时间性,能解释出过去和未来的现象。代表过去曾经付出努力,投注了资源和精神,如今正在等待成果,未来也将有机运得到这些回收。处于一种回顾和期待的状态。\n\n星币七代表思考和计划未来的一段时间。你的生活或目前的状况尚称平稳,所以你有时间可以安静的计划未来的步骤。这可能包括进一步的学习、强调休闲、谨慎地经营现有财物,甚至再创另一种事业,以补充现有的事业。花些时间多做思考吧,因为你的决定有可能对将来产生很大的影响。', + upright='Hard work, Perseverance, Diligence, Harvest, Rewards, Results, Growth, Progress, Patience, Planning', + reversed='Work without results, Distractions, Lack of rewards, Unfinished work, Procrastination, Low effort, Waste, Lack of growth, Setbacks, Impatience, Lack of reward' + ), init=False) + + eight_of_pentacles: TarotCard = field(default=TarotCard( + id=71, index='eight_of_pentacles', type='minor_arcana', orig_name='Eight of Pentacles', name='星币八', + intro='一位雕刻匠坐在长板凳上,专注而勤劳地刻着星币星币,他前面已经完成六个了,脚边还有一个未完成。有一条黄色的道路连接远方的市镇与雕刻匠,连接工作与社会,无论什么工作,目的都是服务人群,雕刻匠并未忘记这一点。', + words='上进', + desc='星币八暗示对某人或某种状况的承诺。\n\n星币八是代表工作赚钱的一张牌,也表示能够累积财富,集中心力在赚取金钱上。这是一张代表承诺并专注于眼前工作的牌,而意念当中这乃是为了较长的目标而努力。\n\n星币八暗示对一个人或一种状况的深度承诺。现在你则着重于你的技巧以及如何变得更精炼。可以透过不懈的努力,或进一步的学习让技艺更上层楼。这张牌时说你已经在群体当中找到了自己的位置,并且在做适合你做的事情。你明白工作不应该是沉闷无味的,而是一种自我完成的机会。工作不仅只是为了填满你时间、胃或口袋,更重要的是让你的人生完整。', + upright='Apprenticeship, Passion, High standards, Skill, Talent, Craftsmanship, Quality, Expertise, Mastery, Commitment, Dedication, Accomplishment', + reversed='Lack of passion, Uninspired, No motivation, Lack of quality, Rushed job, Bad reputation, Lack of motivation, Mediocrity, Laziness, Low skill, Dead-end job' + ), init=False) + + nine_of_pentacles: TarotCard = field(default=TarotCard( + id=72, index='nine_of_pentacles', type='minor_arcana', orig_name='Nine of Pentacles', name='星币九', + intro='一位衣着华丽的女子站在她的庄园中,四周葡萄茂盛,正是收成时节。她右手扶在星币上,大拇指还扣着一根葡萄藤,左手则戴着白手套,让她的小鸟站在上面,小鸟的头部却被红布遮住了。', + words='自律', + desc='星币九代表收获与安逸,丰富的物质生活与相对应的束缚。\n\n星币九是一张代表自信或自我依赖的牌,那可说是要达到超凡成就的必要条件。你的自信如果在搭配上自律的话,那将使你在许多层面上获益。\n\n大体上来说,星币九形容由于过去的努力而带来的一种舒适的生活。星币九代表财富的成功与富足,显示对于生活实际投入的层面,并表达了物质与精神层面的相互关系。', + upright='Fruits of labor, Rewards, Luxury, Rewarded efforts, Success, Achievement, Independence, Leisure, Material security, Self-sufficiency', + reversed='Reckless spending, Living beyond means, False success, Being guarded, Material instability, Superficiality' + ), init=False) + + ten_of_pentacles: TarotCard = field(default=TarotCard( + id=73, index='ten_of_pentacles', type='minor_arcana', orig_name='Ten of Pentacles', name='星币十', + intro='星币十的近景是一位老年人,他舒服的坐着,身旁围绕着两只狗。拱门外的市镇中有一对青年男女,似乎在讨论什么,还有一个小孩子。十个星币排列成生命之树的符号。', + words='富裕', + desc='星币十意味归于平静的物质上的成功。\n\n星币十意味物质上的成功,星币十画的是一个安稳而舒适的居家环境。从墙上盾形家徽看得出这是一个富裕而巩固的环境,这个家庭拥有能提供舒适物质环境的一切条件。那么,为什么每个人都没有面对着别人呢?这老人是坐着的,他的注意力放在动物们的身上,年轻人背对我们,而女人也没有面对他,却稍稍侧着脸继续和他谈话。小孩子被忽略了,这些人彼此之间也没有真正的关联。它们得到别人所渴望的物质世界,不过很显然这也使他们感到沉闷,并陷入公式化的生活中,一旦这种公式消失,将无所适从。\n\n星币十是整组牌可能性的充分显示。他缺乏权杖的热情、宝剑的理想以及圣杯牌的情感。在这里可以找到物质上的安全感和稳定,但也付出了代价。', + upright='Legacy, Culmination, Inheritance, Roots, Family, Ancestry, Windfall, Foundations, Privilege, Affluence, Stability, Tradition', + reversed='Fleeting success, Lack of stability, Lack of resources, Family disputes, Bankruptcy, Debt, Conflict over money, Instability, Breaking traditions' + ), init=False) + + page_of_pentacles: TarotCard = field(default=TarotCard( + id=74, index='page_of_pentacles', type='minor_arcana', orig_name='Page of Pentacles', name='星币侍从', + intro='星币待从双脚坚稳的站立在地面上,高高捧着星币,他所着迷的东西,在眼前仔细地观察着。他头戴红色软帽头饰,带子围着肩颈。身上的穿着是以棕色为底,套着绿色的外衣,鞋子和腰带也是棕色的。他站在青葱且长满花朵的草地上,远方有茂密的树丛,画面的右下还有一座山。', + words='勤奋', + desc='星币侍从意味着为理想而努力学习。\n\n星币侍从象征有关金钱、新工作或学习一门课程的消息。它可以表示去学习某些将会产生实质效益的事物。这个侍从通常代表学生的勤奋向学。透过学习一门课程,或于工作中学习,发挥了自己的能力。有时候这个侍从可能暗示你对于正在学习的科目,变得更专注,甚至更重视学习的成果。', + upright='Ambition, Desire, Diligence, Ambitious, Diligent, Goal oriented, Planner, Consistent, Star student, Studious, Grounded, Loyal, Faithful, Dependable', + reversed='Lack of commitment, Greediness, Laziness, Foolish, Immature, Irresponsible, Lazy, Underachiever, Procrastinator, Missed chances, Poor prospects' + ), init=False) + + knight_of_pentacles: TarotCard = field(default=TarotCard( + id=75, index='knight_of_pentacles', type='minor_arcana', orig_name='Knight of Pentacles', name='星币骑士', + intro='星币骑士笔直地坐在黑马背上,仔细打量手上的星币。黑色的强壮马匹配着厚实的红色马鞍和缰绳,侧面垂着红色的软坐垫,牢牢地站在地面,是四张骑士牌中唯一不动的座骑。骑士戴着头盔,头盔顶端饰有穗状的绿叶,黑马的头顶也有相同的叶穗。他身着厚重盔甲,外披一件暗红色战袍,也戴着红色的手套。星币骑士处于空旷的大地上,眼前应是一望无际。远方的地面是一片经过细心耕耘的田地,背景是一片鲜黄色。', + words='稳健', + desc='星币骑士代表稳健而认真的计划。\n\n星币骑士通常指的是强化你的计划,并朝确定的目标迈进。它意味着为了实现一个目标而努力工作。就一个人而言,这个人对于承诺非常的认真,不论是对事业、个人雄心或两性关系。通常,他相信这个格言:“如果你想做好一件事,那就自己动手吧。”', + upright='Efficiency, Hard work, Responsibility, Practical, Reliable, Efficient, Stoic, Slow and steady, Hard-working, Committed, Patient, Conservative', + reversed='Laziness, Obsessiveness, Work without reward, Workaholic, Dull, Boring, No initiative, Cheap, Irresponsible, Gambler, Risky investments' + ), init=False) + + queen_of_pentacles: TarotCard = field(default=TarotCard( + id=76, index='queen_of_pentacles', type='minor_arcana', orig_name='Queen of Pentacles', name='星币皇后', + intro='星币皇后的面貌端庄而正直,双手捧着星币,并低头凝望着星币,神情若有所思。她的后冠是圆顶的,中间有插着两根红色羽毛,星币皇后的后袍是红色的,内衫露出的袖子是白色的,是红白对立的组合,绿色的披风由头上往下延伸到椅上。皇后的宝座处在长满丰盛植物的平原上,在茂密的林荫中,玫瑰花围绕成的拱门之下,所在的草地之上盛开着多株玫瑰花,。座椅是精工雕琢的,刻满了纹饰。有许多植物和天使的图案,很像圣杯皇后的座椅,扶前端有羊头的浮雕,椅侧有小孩的浮雕,椅背刻满了藤蔓瓜叶。宝座旁的近景是一片肥沃的土地,满是绿草和花朵。', + words='安定', + desc='星币皇后意味着喜爱大自然,又有良好的商业眼光。\n\n从一般观点来看,星币皇后是一张代表信任自己能力的牌。她意味经由深思熟虑而带来成功。作为一个人,星币皇后通常有着敏锐的生意眼光,而且总是喜欢存点钱在身边,好让自己有安全感。在有需要的时候她会很节俭,而且不会任意炫耀财富。她是一个可靠、实际的人,知道应该在那里下功夫可以得到最大的成功。\n\n这张皇后牌是指务实、可靠,并擅长喂养植物和动物。她也喜欢经常到乡间旅行,或漫步于大自然中,因为她需要和自然保持接触,让生命有完整而踏实的感觉。', + upright='Practicality, Creature comforts, Financial security, Generous, Caring, Nurturing, Homebody, Good business sense, Practical, Comforting, Welcoming, Sensible, Luxurious', + reversed='Self-centeredness, Jealousy, Smothering, Selfish, Unkempt, Jealous, Insecure, Greedy, Materialistic, Gold digger, Intolerant, Self-absorbed, Envious' + ), init=False) + + king_of_pentacles: TarotCard = field(default=TarotCard( + id=77, index='king_of_pentacles', type='minor_arcana', orig_name='King of Pentacles', name='星币国王', + intro='星币国王悠然自得的坐在他的花园里。他的左手拿着星币,右手拿着权杖,姿态轻松。花围中长满象征丰收成果的葡萄和各种植物,他的服装也满是葡萄图案,整个人似乎与大自然融成一体。宝座上有牛头图案,是星币的家族图腾。国王的右手靠在座椅的扶手上,掌中握着宝球权柄。左手持拥五芒星金币,并垫起左脚让这枚大金币更稳定确实地置于膝上。国王慵懒地靠在椅背上,低眼安然地端详着他的金币。', + words='坚定', + desc='星币国王表示务实而坚定的态度可以带来成功。\n\n星币国王暗示透过身体力行而达到成功。它也可以说是务实的努力带来物质上的成功。星币国王代表的是一个脚踏实地而又成熟的人。他的个性稳健、可靠且保守,并能努力履行其承诺,谨慎的负起他应负的责任。他不像权杖国王般富冒险精神,或像圣杯国王那么有创意,但他可凭藉着慢慢而稳定的步伐,以及认真的实践来达到成功。', + upright='Abundance, Prosperity, Security, Ambitious, Safe, Kind, Patriarchal, Protective, Businessman, Provider, Sensual, Reliable', + reversed='Greed, Indulgence, Sensuality, Materialistic, Wasteful, Chauvanist, Poor financial decisions, Gambler, Exploitative, Possessive' + ), init=False) + + +class TarotPacks(object): + """ + 定义套牌 + """ + SpecialCard: TarotPack = TarotPack( + name='special', + cards=[card for card in TarotCards.get_all_cards() if card.type == 'special']) + + MajorArcana: TarotPack = TarotPack( + name='major_arcana', + cards=[card for card in TarotCards.get_all_cards() if card.type == 'major_arcana']) + + MinorArcana: TarotPack = TarotPack( + name='minor_arcana', + cards=[card for card in TarotCards.get_all_cards() if card.type == 'minor_arcana']) + + RiderWaite: TarotPack = TarotPack( + name='rider_waite', + cards=[card for card in TarotCards.get_all_cards() if ( + card.type == 'major_arcana' or card.type == 'minor_arcana')]) + + +__all__ = [ + 'TarotCards', + 'TarotPacks' +] diff --git a/omega_miya/plugins/tarot/tarot_resources.py b/omega_miya/plugins/tarot/tarot_resources.py new file mode 100644 index 00000000..26927939 --- /dev/null +++ b/omega_miya/plugins/tarot/tarot_resources.py @@ -0,0 +1,84 @@ +""" +@Author : Ailitonia +@Date : 2021/09/01 0:22 +@FileName : tarot_resources.py +@Project : nonebot2_miya +@Description : 卡片资源 同样是硬编码在这里了:( +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +import os +from typing import List +from nonebot import get_driver +from .tarot_typing import TarotPack, TarotResourceFile, TarotResource +from .tarot_data import TarotPacks + + +global_config = get_driver().config +TMP_PATH = global_config.tmp_path_ +RESOURCES_PATH = global_config.resources_path_ +TAROT_RESOURCES_PATH = os.path.abspath(os.path.join(RESOURCES_PATH, 'images', 'tarot')) + + +class BaseTarotResource(object): + """ + 资源基类 + """ + def __init__(self, pack: TarotPack, source_name: str, file_format: str, source_folder_name: str): + self.pack: TarotPack = pack + + self.files: List[TarotResourceFile] = [ + TarotResourceFile(id=card.id, index=card.index) for card in self.pack.cards] + + self.resources: TarotResource = TarotResource( + source_name=source_name, + file_format=file_format, + file_path=os.path.abspath(os.path.join(TAROT_RESOURCES_PATH, source_folder_name)), + files=self.files + ) + + self.resources.check_source() + + +class TarotResources(object): + # 内置资源 BiliBili幻星集 + BiliTarotResources = BaseTarotResource( + pack=TarotPacks.RiderWaite, + source_name='BiliBili幻星集', + file_format='png', + source_folder_name='bilibili') + + # 内置资源 莱德韦特塔罗 + RWSTarotResources = BaseTarotResource( + pack=TarotPacks.RiderWaite, + source_name='莱德韦特塔罗', + file_format='jpg', + source_folder_name='RWS') + + # 内置资源 莱德韦特塔罗 大阿卡那 + RWSMTarotResources = BaseTarotResource( + pack=TarotPacks.MajorArcana, + source_name='莱德韦特塔罗_大阿卡那', + file_format='jpg', + source_folder_name='RWS_M') + + # 内置资源 通用塔罗 + UWTTarotResources = BaseTarotResource( + pack=TarotPacks.RiderWaite, + source_name='Universal Waite Tarot', + file_format='jpg', + source_folder_name='UWT') + + # 在这里自定义你的资源文件 + # CustomTarotResources = BaseTarotResource( + # pack=TarotPacks.MajorArcana, + # source_name='Custom', + # file_format='png', + # source_folder_name='Custom') + + +__all__ = [ + 'BaseTarotResource', + 'TarotResources' +] diff --git a/omega_miya/plugins/tarot/tarot_typing.py b/omega_miya/plugins/tarot/tarot_typing.py new file mode 100644 index 00000000..b1f1f8f6 --- /dev/null +++ b/omega_miya/plugins/tarot/tarot_typing.py @@ -0,0 +1,129 @@ +""" +@Author : Ailitonia +@Date : 2021/08/31 21:08 +@FileName : tarot_typing.py +@Project : nonebot2_miya +@Description : 类型 +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +import os +from typing import List +from dataclasses import dataclass + + +@dataclass +class Element: + """ + 元素 + """ + id: int + orig_name: str # 原始名称 + name: str # 名称 + + +@dataclass +class Constellation: + """ + 星与星座 + """ + id: int + orig_name: str # 原始名称 + name: str # 名称 + + +@dataclass +class TarotCard: + """ + 塔罗牌 + """ + id: int # 塔罗牌的序号 + index: str # 内部名称 + type: str # 卡片类型 + orig_name: str # 原始名称 + name: str # 显示名称 + intro: str # 卡面描述 + words: str # 相关词/关键词 + desc: str # 卡片描述 + upright: str # 正位释义 + reversed: str # 逆位释义 + + +@dataclass +class TarotPack: + """ + 套牌 + """ + name: str # 套牌名称 + cards: List[TarotCard] # 套牌内容 + + @property + def num(self) -> int: + return len(self.cards) + + def get_card_by_id(self, id_: int) -> TarotCard: + if card := [card for card in self.cards if card.id == id_]: + if len(card) == 1: + return card[0] + raise ValueError('Card not found or multi-card error') + + def get_card_by_index(self, index_: str) -> TarotCard: + if card := [card for card in self.cards if card.index == index_]: + if len(card) == 1: + return card[0] + raise ValueError('Card not found or multi-card error') + + def get_card_by_name(self, name: str) -> TarotCard: + if card := [card for card in self.cards if card.name == name]: + if len(card) == 1: + return card[0] + raise ValueError('Card not found or multi-card error') + + +@dataclass +class TarotResourceFile: + """ + 卡片资源文件 + """ + id: int # 塔罗牌的序号 同时也是存放文件名 + index: str # 内部名称 + + +@dataclass +class TarotResource: + """ + 卡片资源信息 + """ + source_name: str # 资源名称 + file_format: str # 文件格式 + file_path: str # 资源文件夹 + files: List[TarotResourceFile] # 卡牌名称列表 + + def check_source(self): + for file in self.files: + if not os.path.exists(os.path.abspath(os.path.join(self.file_path, f'{file.id}.{self.file_format}'))): + raise ValueError(f'Tarot | Tarot source: "{self.source_name}", file: "{file}" missing, ' + f'please check your "{self.file_path}" folder') + + def get_file_by_id(self, id_: int) -> str: + if file := [file for file in self.files if file.id == id_]: + if len(file) == 1: + return os.path.abspath(os.path.join(self.file_path, f'{file[0].id}.{self.file_format}')) + raise ValueError('File not found or multi-file error') + + def get_file_by_index(self, index_: str) -> str: + if file := [file for file in self.files if file.index == index_]: + if len(file) == 1: + return os.path.abspath(os.path.join(self.file_path, f'{file[0].id}.{self.file_format}')) + raise ValueError('File not found or multi-file error') + + +__all__ = [ + 'Element', + 'Constellation', + 'TarotCard', + 'TarotPack', + 'TarotResourceFile', + 'TarotResource' +] diff --git a/omega_miya/plugins/tarot/utils.py b/omega_miya/plugins/tarot/utils.py new file mode 100644 index 00000000..10400145 --- /dev/null +++ b/omega_miya/plugins/tarot/utils.py @@ -0,0 +1,164 @@ +""" +@Author : Ailitonia +@Date : 2021/09/01 1:20 +@FileName : utils.py +@Project : nonebot2_miya +@Description : 塔罗牌图片生成相关模块 +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +import os +import asyncio +from datetime import datetime +from nonebot import get_driver +from PIL import Image, ImageDraw, ImageFont +from omega_miya.database import Result +from omega_miya.utils.omega_plugin_utils import TextUtils +from .tarot_resources import BaseTarotResource + + +global_config = get_driver().config +TMP_PATH = global_config.tmp_path_ +RESOURCES_PATH = global_config.resources_path_ +TAROT_CARD_PATH = os.path.abspath(os.path.join(TMP_PATH, 'tarot_card')) + + +async def generate_tarot_card( + id_: int, + resources: BaseTarotResource, + direction: int = 1, + *, + need_desc: bool = True, + need_upright: bool = True, + need_reversed: bool = True, + width: int = 1024) -> Result.TextResult: + """ + 绘制塔罗卡片 + :param id_: 牌id + :param resources: 卡片资源 + :param direction: 方向, 1: 正, -1: 逆 + :param need_desc: 是否绘制描述 + :param need_upright: 是否绘制正位描述 + :param need_reversed: 是否绘制逆位描述 + :param width: 绘制图片宽度 + :return: + """ + # 获取这张卡牌 + tarot_card = resources.pack.get_card_by_id(id_=id_) + + def __handle(): + # 获取卡片图片 + draw_tarot_img: Image.Image = Image.open(resources.resources.get_file_by_id(id_=id_)) + # 正逆 + if direction < 0: + draw_tarot_img = draw_tarot_img.rotate(180) + + # 调整头图宽度 + tarot_img_height = int(width * draw_tarot_img.height / draw_tarot_img.width) + draw_tarot_img = draw_tarot_img.resize((width, tarot_img_height)) + + # 字体 + font_path = os.path.abspath(os.path.join(RESOURCES_PATH, 'fonts', 'fzzxhk.ttf')) + title_font = ImageFont.truetype(font_path, width // 10) + m_title_font = ImageFont.truetype(font_path, width // 20) + text_font = ImageFont.truetype(font_path, width // 25) + + # 标题 + title_width, title_height = title_font.getsize(tarot_card.name) + m_title_width, m_title_height = m_title_font.getsize(tarot_card.name) + + # 描述 + desc_text = TextUtils(text=tarot_card.desc).split_multiline(width=(width - int(width * 0.125)), font=text_font) + desc_text_width, desc_text_height = text_font.getsize_multiline(desc_text) + + # 正位描述 + upright_text = TextUtils( + text=tarot_card.upright).split_multiline(width=(width - int(width * 0.125)), font=text_font) + upright_text_width, upright_text_height = text_font.getsize_multiline(upright_text) + + # 逆位描述 + reversed_text = TextUtils( + text=tarot_card.reversed).split_multiline(width=(width - int(width * 0.125)), font=text_font) + reversed_text_width, reversed_text_height = text_font.getsize_multiline(reversed_text) + + # 计算高度 + background_height = title_height + m_title_height + tarot_img_height + int(0.09375 * width) + if need_desc: + background_height += desc_text_height + int(0.125 * width) + if need_upright: + background_height += m_title_height + upright_text_height + int(0.125 * width) + if need_reversed: + background_height += m_title_height + reversed_text_height + int(0.125 * width) + + # 生成背景 + background = Image.new( + mode="RGB", + size=(width, background_height), + color=(255, 255, 255)) + + # 开始往背景上绘制各个元素 + # 以下排列从上到下绘制 请勿变换顺序 否则导致位置错乱 + this_height = int(0.0625 * width) + ImageDraw.Draw(background).text(xy=(width // 2, this_height), + text=tarot_card.name, font=title_font, align='center', anchor='mt', + fill=(0, 0, 0)) # 中文名称 + + this_height += title_height + ImageDraw.Draw(background).text(xy=(width // 2, this_height), + text=tarot_card.orig_name, font=m_title_font, align='center', anchor='ma', + fill=(0, 0, 0)) # 英文名称 + + this_height += m_title_height + int(0.03125 * width) + background.paste(draw_tarot_img, box=(0, this_height)) # 卡面 + + this_height += tarot_img_height + if need_desc: + this_height += int(0.0625 * width) + ImageDraw.Draw(background).multiline_text(xy=(width // 2, this_height), + text=desc_text, font=text_font, align='center', anchor='ma', + fill=(0, 0, 0)) # 描述 + this_height += desc_text_height + + if need_upright: + this_height += int(0.0625 * width) + ImageDraw.Draw(background).text(xy=(width // 2, this_height), + text='【正位】', font=m_title_font, align='center', anchor='ma', + fill=(0, 0, 0)) # 正位 + + this_height += m_title_height + int(0.03125 * width) + ImageDraw.Draw(background).multiline_text(xy=(width // 2, this_height), + text=upright_text, font=text_font, align='center', anchor='ma', + fill=(0, 0, 0)) # 正位描述 + this_height += upright_text_height + + if need_reversed: + this_height += int(0.0625 * width) + ImageDraw.Draw(background).text(xy=(width // 2, this_height), + text='【逆位】', font=m_title_font, align='center', anchor='ma', + fill=(0, 0, 0)) # 逆位 + + this_height += m_title_height + int(0.03125 * width) + ImageDraw.Draw(background).multiline_text(xy=(width // 2, this_height), + text=reversed_text, font=text_font, align='center', anchor='ma', + fill=(0, 0, 0)) # 逆位描述 + + if not os.path.exists(TAROT_CARD_PATH): + os.makedirs(TAROT_CARD_PATH) + + save_path = os.path.abspath(os.path.join( + TAROT_CARD_PATH, f"tarot_card_{tarot_card.index}_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.jpg")) + background.save(save_path, 'JPEG') + return save_path + + try: + loop = asyncio.get_running_loop() + result = await loop.run_in_executor(None, __handle) + return Result.TextResult(error=False, info='Success', result=result) + except Exception as e: + return Result.TextResult(error=True, info=repr(e), result='') + + +__all__ = [ + 'generate_tarot_card' +] diff --git a/omega_miya/plugins/tencent_cloud/__init__.py b/omega_miya/plugins/tencent_cloud/__init__.py index 3ddff66a..82bc06bb 100644 --- a/omega_miya/plugins/tencent_cloud/__init__.py +++ b/omega_miya/plugins/tencent_cloud/__init__.py @@ -1,21 +1,24 @@ import re -from nonebot import MatcherGroup, logger, export +from nonebot import MatcherGroup, logger +from nonebot.plugin.export import export from nonebot.typing import T_State +from nonebot.rule import to_me from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import GroupMessageEvent from nonebot.adapters.cqhttp.permission import GROUP -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state, OmegaRules +from nonebot.adapters.cqhttp.message import Message, MessageSegment +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state, OmegaRules from omega_miya.utils.tencent_cloud_api import TencentNLP, TencentTMT # Custom plugin usage text -__plugin_name__ = 'TencentCloudCore' +__plugin_custom_name__ = '腾讯云Core' __plugin_usage__ = r'''【TencentCloud API Support】 腾讯云API插件 测试中 **Permission** -Command & Lv.50 +Command & Lv.30 or AuthNode **AuthNode** @@ -24,20 +27,20 @@ **Usage** /翻译''' -# 声明本插件可配置的权限节点 +# 声明本插件额外可配置的权限节点 __plugin_auth_node__ = [ 'tmt', 'nlp' ] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__, __plugin_auth_node__) tencent_cloud = MatcherGroup( type='message', permission=GROUP, - priority=100, + priority=50, block=False) @@ -45,12 +48,11 @@ '翻译', aliases={'translate'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='translate', command=True, level=30, auth_node='tmt'), - priority=30, block=True) @@ -84,12 +86,14 @@ async def handle_roll(bot: Bot, event: GroupMessageEvent, state: T_State): await translate.finish(f"翻译结果:\n\n{translate_result.result.get('targettext')}") -nlp = tencent_cloud.on_message( - rule=OmegaRules.has_group_command_permission() & OmegaRules.has_level_or_node(30, 'tencent_cloud.nlp')) +entity = tencent_cloud.on_message( + rule=OmegaRules.has_group_command_permission() & OmegaRules.has_level_or_node(30, 'tencent_cloud.nlp'), + priority=55 +) -@nlp.handle() -async def handle_nlp(bot: Bot, event: GroupMessageEvent, state: T_State): +@entity.handle() +async def handle_entity(bot: Bot, event: GroupMessageEvent, state: T_State): arg = str(event.get_plaintext()).strip().lower() # 排除列表 @@ -100,20 +104,51 @@ async def handle_nlp(bot: Bot, event: GroupMessageEvent, state: T_State): ] for pattern in ignore_pattern: if re.search(pattern, arg): - await nlp.finish() + await entity.finish() # describe_entity实体查询 if re.match(r'^(你?知道)?(.{1,32}?)的(.{1,32}?)是(什么|谁|啥)吗?[??]?$', arg): item, attr = re.findall(r'^(你?知道)?(.{1,32}?)的(.{1,32}?)是(什么|谁|啥)吗?[??]?$', arg)[0][1:3] res = await TencentNLP().describe_entity(entity_name=item, attr=attr) if not res.error and res.result: - await nlp.finish(f'{item}的{attr}是{res.result}') + await entity.finish(f'{item}的{attr}是{res.result}') else: logger.warning(f'nlp handling describe entity failed: {res.info}') elif re.match(r'^(你?知道)?(.{1,32}?)是(什么|谁|啥)吗?[??]?$', arg): item = re.findall(r'^(你?知道)?(.{1,32}?)是(什么|谁|啥)吗?[??]?$', arg)[0][1] res = await TencentNLP().describe_entity(entity_name=item) if not res.error and res.result: - await nlp.finish(str(res.result)) + await entity.finish(str(res.result)) else: logger.warning(f'nlp handling describe entity failed: {res.info}') + + +chat = tencent_cloud.on_message( + rule=to_me() & OmegaRules.has_group_command_permission() & OmegaRules.has_level_or_node(30, 'tencent_cloud.nlp'), + block=True +) + + +@chat.handle() +async def handle_nlp(bot: Bot, event: GroupMessageEvent, state: T_State): + chat_text = str(event.get_message()).strip() + + # 排除列表 + ignore_pattern = [ + re.compile(r'喵一个'), + re.compile(r'^今天'), + re.compile(r'[这那谁你我他她它]个?是[(什么)谁啥]') + ] + for pattern in ignore_pattern: + if re.search(pattern, chat_text): + await chat.finish() + + # 获得闲聊内容 + chat_result = await TencentNLP().chat_bot(text=chat_text) + if chat_result.error: + logger.error(f'Chat | Getting chat result failed, error: {chat_result.info}') + await chat.finish('我没听懂你在说什么QAQ') + else: + msg = Message(MessageSegment.at(user_id=event.user_id)).append(chat_result.result) + logger.debug(f'Chat | Chatting with user: {event.user_id}') + await chat.finish(msg) diff --git a/omega_miya/plugins/zhoushen_hime/__init__.py b/omega_miya/plugins/zhoushen_hime/__init__.py index 9174b0c3..7778b659 100644 --- a/omega_miya/plugins/zhoushen_hime/__init__.py +++ b/omega_miya/plugins/zhoushen_hime/__init__.py @@ -2,21 +2,22 @@ 要求go-cqhttp v0.9.40以上 """ import os -from nonebot import on_command, on_notice, export, logger +from nonebot import on_command, on_notice, logger +from nonebot.plugin.export import export from nonebot.typing import T_State from nonebot.permission import SUPERUSER from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.permission import GROUP_ADMIN, GROUP_OWNER from nonebot.adapters.cqhttp.message import MessageSegment, Message from nonebot.adapters.cqhttp.event import GroupMessageEvent, GroupUploadNoticeEvent -from omega_miya.utils.Omega_plugin_utils import init_export, init_permission_state, OmegaRules -from omega_miya.utils.Omega_Base import DBBot, DBBotGroup, DBAuth, Result +from omega_miya.utils.omega_plugin_utils import init_export, init_processor_state, OmegaRules +from omega_miya.database import DBBot, DBBotGroup, DBAuth, Result from .utils import ZhouChecker, download_file # Custom plugin usage text __plugin_raw_name__ = __name__.split('.')[-1] -__plugin_name__ = '自动审轴姬' +__plugin_custom_name__ = '自动审轴姬' __plugin_usage__ = r'''【自动审轴姬】 检测群内上传文件并自动锤轴 仅限群聊使用 @@ -32,20 +33,16 @@ **GroupAdmin and SuperUser Only** /ZhouShenHime ''' -# 声明本插件可配置的权限节点 -__plugin_auth_node__ = [ - 'basic' -] # Init plugin export -init_export(export(), __plugin_name__, __plugin_usage__, __plugin_auth_node__) +init_export(export(), __plugin_custom_name__, __plugin_usage__) # 注册事件响应器 zhoushen_hime_admin = on_command( 'ZhouShenHime', aliases={'zhoushenhime', '审轴姬', '审轴机'}, # 使用run_preprocessor拦截权限管理, 在default_state初始化所需权限 - state=init_permission_state( + state=init_processor_state( name='zhoushen_hime', command=True, level=10), @@ -105,7 +102,8 @@ async def zhoushen_hime_on(bot: Bot, event: GroupMessageEvent, state: T_State) - if not group_exist: return Result.IntResult(error=False, info='Group not exist', result=-1) - auth_node = DBAuth(self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=f'{__plugin_raw_name__}.basic') + auth_node = DBAuth( + self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=OmegaRules.basic_node(__plugin_raw_name__)) result = await auth_node.set(allow_tag=1, deny_tag=0, auth_info='启用自动审轴姬') return result @@ -118,18 +116,22 @@ async def zhoushen_hime_off(bot: Bot, event: GroupMessageEvent, state: T_State) if not group_exist: return Result.IntResult(error=False, info='Group not exist', result=-1) - auth_node = DBAuth(self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=f'{__plugin_raw_name__}.basic') + auth_node = DBAuth( + self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=OmegaRules.basic_node(__plugin_raw_name__)) result = await auth_node.set(allow_tag=0, deny_tag=1, auth_info='禁用自动审轴姬') return result -zhoushen_hime = on_notice(rule=OmegaRules.has_auth_node(__plugin_raw_name__, 'basic'), priority=100, block=False) +zhoushen_hime = on_notice( + rule=OmegaRules.has_auth_node(OmegaRules.basic_node(__plugin_raw_name__)), + priority=100, + block=False) @zhoushen_hime.handle() async def hime_handle(bot: Bot, event: GroupUploadNoticeEvent, state: T_State): file_name = event.file.name - file_url = event.file.dict().get('url') + file_url = getattr(event.file, 'url', None) user_id = event.user_id # 不响应自己上传的文件 diff --git a/omega_miya/plugins/zhoushen_hime/utils.py b/omega_miya/plugins/zhoushen_hime/utils.py index 1f831062..8071305c 100644 --- a/omega_miya/plugins/zhoushen_hime/utils.py +++ b/omega_miya/plugins/zhoushen_hime/utils.py @@ -2,8 +2,8 @@ import datetime from typing import Tuple, List from nonebot import logger, get_driver -from omega_miya.utils.Omega_plugin_utils import HttpFetcher -from omega_miya.utils.Omega_Base import Result +from omega_miya.utils.omega_plugin_utils import HttpFetcher +from omega_miya.database import Result global_config = get_driver().config TMP_PATH = global_config.tmp_path_ @@ -397,8 +397,8 @@ class ZhouChecker(AssScriptLineTool): '’': '」', '・・・': '...', '···': '...', - '“': '「', - '”': '」', + '“': '『', + '”': '』', '、': ' ', '~': '~', '!': '!', @@ -485,33 +485,34 @@ def handle(self) -> Result.DictResult: style_mode = self.__style_mode # 用于写入txt的内容 - out_log = '--- 自动审轴姬 v1.0 by:Ailitonia ---\n\n' + out_log = '--- 自动审轴姬 v1.1 by:Ailitonia ---\n\n' if style_mode: out_log += '--- 样式模式已启用: 样式不同的行之间不会相互进行比较 ---\n\n' else: out_log += '--- 样式模式已禁用: 所有行之间都相互进行比较 ---\n\n' - out_log += '字符检查部分:\n' + out_log += '--- 字符检查部分 ---\n' # 开始字符检查 character_count = 0 for line in self.__event_lines: # 检查需要校对的关键词 - if any(key in line.text for key in ZhouChecker.__proofreading_words): - out_log += f'第{line.event_line_num}行可能翻译没听懂, 校对请注意一下————{line.text}\n' + if any(key in line.text for key in self.__proofreading_words): + out_log += f'第{line.event_line_num}行可能翻译没听懂, 校对请注意一下: "{line.text}"\n' character_count += 1 # 检查标点符号 - if any(punctuation in line.text for punctuation in ZhouChecker.__punctuation_ignore): - out_log += f'第{line.event_line_num}行轴标点有问题(不确定怎么改), 请注意一下————{line.text}\n' + if any(punctuation in line.text for punctuation in self.__punctuation_ignore): + out_log += f'第{line.event_line_num}行轴标点有问题(不确定怎么改), 请注意一下: "{line.text}"\n' character_count += 1 - elif any(punctuation in line.text for punctuation in ZhouChecker.__punctuation_replace.keys()): - for key, value in ZhouChecker.__punctuation_replace.items(): + elif any(punctuation in line.text for punctuation in self.__punctuation_replace.keys()): + for key, value in self.__punctuation_replace.items(): if key in line.text: line.text = line.text.replace(key, value) out_log += f'第{line.event_line_num}行轴的{key}标点有问题, 但是我给你换成了{value}\n' character_count += 1 + out_log += '--- 字符检查部分结束 ---\n\n' # 开始锤轴部分 - out_log += '\n锤轴部分:\n' + out_log += '--- 锤轴部分 ---\n' overlap_count = 0 flash_count = 0 @@ -624,6 +625,13 @@ def handle(self) -> Result.DictResult: out_log += f"第{start_line.event_line_num}行轴和第{end_line.event_line_num}行轴之间是闪轴" \ f"({multi_flash_lines_duration.microseconds / 1000}ms), 不过我给你连上了({after_change_time})\n" break + out_log += '--- 锤轴部分结束 ---\n\n' \ + '--- 审轴信息 ---\n' \ + f'原始文件: {os.path.basename(self.__file_path)}\n' \ + f'报告生成时间: {datetime.datetime.now()}\n' \ + f'符号及疑问文本问题: {character_count}\n' \ + f'叠轴问题: {overlap_count}\n' \ + f'闪轴问题: {flash_count}' # 刷新数据 self.__event_lines.clear() diff --git a/omega_miya/plugins/sticker_maker/utils/fonts/K_Gothic.ttf b/omega_miya/resources/fonts/K_Gothic.ttf similarity index 100% rename from omega_miya/plugins/sticker_maker/utils/fonts/K_Gothic.ttf rename to omega_miya/resources/fonts/K_Gothic.ttf diff --git a/omega_miya/resources/fonts/NotoSansMonoCJKsc-Regular.otf b/omega_miya/resources/fonts/NotoSansMonoCJKsc-Regular.otf new file mode 100644 index 00000000..999c45aa Binary files /dev/null and b/omega_miya/resources/fonts/NotoSansMonoCJKsc-Regular.otf differ diff --git a/omega_miya/plugins/sticker_maker/utils/fonts/SourceHanSans_Heavy.otf b/omega_miya/resources/fonts/SourceHanSans_Heavy.otf similarity index 100% rename from omega_miya/plugins/sticker_maker/utils/fonts/SourceHanSans_Heavy.otf rename to omega_miya/resources/fonts/SourceHanSans_Heavy.otf diff --git a/omega_miya/plugins/sticker_maker/utils/fonts/SourceHanSans_Regular.otf b/omega_miya/resources/fonts/SourceHanSans_Regular.otf similarity index 100% rename from omega_miya/plugins/sticker_maker/utils/fonts/SourceHanSans_Regular.otf rename to omega_miya/resources/fonts/SourceHanSans_Regular.otf diff --git a/omega_miya/plugins/sticker_maker/utils/fonts/fzzxhk.ttf b/omega_miya/resources/fonts/fzzxhk.ttf similarity index 100% rename from omega_miya/plugins/sticker_maker/utils/fonts/fzzxhk.ttf rename to omega_miya/resources/fonts/fzzxhk.ttf diff --git a/omega_miya/plugins/sticker_maker/utils/fonts/msyh.ttc b/omega_miya/resources/fonts/msyh.ttc similarity index 100% rename from omega_miya/plugins/sticker_maker/utils/fonts/msyh.ttc rename to omega_miya/resources/fonts/msyh.ttc diff --git a/omega_miya/plugins/sticker_maker/utils/fonts/msyhbd.ttc b/omega_miya/resources/fonts/msyhbd.ttc similarity index 100% rename from omega_miya/plugins/sticker_maker/utils/fonts/msyhbd.ttc rename to omega_miya/resources/fonts/msyhbd.ttc diff --git a/omega_miya/plugins/sticker_maker/utils/fonts/pixel.ttf b/omega_miya/resources/fonts/pixel.ttf similarity index 100% rename from omega_miya/plugins/sticker_maker/utils/fonts/pixel.ttf rename to omega_miya/resources/fonts/pixel.ttf diff --git a/omega_miya/resources/images/tarot/RWS/0.jpg b/omega_miya/resources/images/tarot/RWS/0.jpg new file mode 100644 index 00000000..179278ac Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/0.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/1.jpg b/omega_miya/resources/images/tarot/RWS/1.jpg new file mode 100644 index 00000000..8f9efca2 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/1.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/10.jpg b/omega_miya/resources/images/tarot/RWS/10.jpg new file mode 100644 index 00000000..17f1ab47 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/10.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/11.jpg b/omega_miya/resources/images/tarot/RWS/11.jpg new file mode 100644 index 00000000..215a053f Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/11.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/12.jpg b/omega_miya/resources/images/tarot/RWS/12.jpg new file mode 100644 index 00000000..20f41ed4 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/12.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/13.jpg b/omega_miya/resources/images/tarot/RWS/13.jpg new file mode 100644 index 00000000..ae27064b Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/13.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/14.jpg b/omega_miya/resources/images/tarot/RWS/14.jpg new file mode 100644 index 00000000..33abdf35 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/14.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/15.jpg b/omega_miya/resources/images/tarot/RWS/15.jpg new file mode 100644 index 00000000..e87bb6fd Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/15.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/16.jpg b/omega_miya/resources/images/tarot/RWS/16.jpg new file mode 100644 index 00000000..70e1cec8 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/16.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/17.jpg b/omega_miya/resources/images/tarot/RWS/17.jpg new file mode 100644 index 00000000..e48cc01b Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/17.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/18.jpg b/omega_miya/resources/images/tarot/RWS/18.jpg new file mode 100644 index 00000000..4132bde8 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/18.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/19.jpg b/omega_miya/resources/images/tarot/RWS/19.jpg new file mode 100644 index 00000000..61f33a0d Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/19.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/2.jpg b/omega_miya/resources/images/tarot/RWS/2.jpg new file mode 100644 index 00000000..91eac8fe Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/2.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/20.jpg b/omega_miya/resources/images/tarot/RWS/20.jpg new file mode 100644 index 00000000..faf218a2 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/20.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/21.jpg b/omega_miya/resources/images/tarot/RWS/21.jpg new file mode 100644 index 00000000..508e4ad4 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/21.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/22.jpg b/omega_miya/resources/images/tarot/RWS/22.jpg new file mode 100644 index 00000000..12ea6ed1 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/22.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/23.jpg b/omega_miya/resources/images/tarot/RWS/23.jpg new file mode 100644 index 00000000..6242fd6e Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/23.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/24.jpg b/omega_miya/resources/images/tarot/RWS/24.jpg new file mode 100644 index 00000000..325825af Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/24.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/25.jpg b/omega_miya/resources/images/tarot/RWS/25.jpg new file mode 100644 index 00000000..663d54ea Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/25.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/26.jpg b/omega_miya/resources/images/tarot/RWS/26.jpg new file mode 100644 index 00000000..8b229d7a Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/26.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/27.jpg b/omega_miya/resources/images/tarot/RWS/27.jpg new file mode 100644 index 00000000..df45d306 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/27.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/28.jpg b/omega_miya/resources/images/tarot/RWS/28.jpg new file mode 100644 index 00000000..3ce9eedb Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/28.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/29.jpg b/omega_miya/resources/images/tarot/RWS/29.jpg new file mode 100644 index 00000000..6a5eb873 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/29.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/3.jpg b/omega_miya/resources/images/tarot/RWS/3.jpg new file mode 100644 index 00000000..f8ab4b69 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/3.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/30.jpg b/omega_miya/resources/images/tarot/RWS/30.jpg new file mode 100644 index 00000000..4fb9cf49 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/30.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/31.jpg b/omega_miya/resources/images/tarot/RWS/31.jpg new file mode 100644 index 00000000..91407b0d Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/31.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/32.jpg b/omega_miya/resources/images/tarot/RWS/32.jpg new file mode 100644 index 00000000..a08295c9 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/32.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/33.jpg b/omega_miya/resources/images/tarot/RWS/33.jpg new file mode 100644 index 00000000..c0040e3e Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/33.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/34.jpg b/omega_miya/resources/images/tarot/RWS/34.jpg new file mode 100644 index 00000000..9665f5fa Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/34.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/35.jpg b/omega_miya/resources/images/tarot/RWS/35.jpg new file mode 100644 index 00000000..aa83d3fe Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/35.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/36.jpg b/omega_miya/resources/images/tarot/RWS/36.jpg new file mode 100644 index 00000000..d8843d4e Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/36.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/37.jpg b/omega_miya/resources/images/tarot/RWS/37.jpg new file mode 100644 index 00000000..d4bbb613 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/37.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/38.jpg b/omega_miya/resources/images/tarot/RWS/38.jpg new file mode 100644 index 00000000..08464432 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/38.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/39.jpg b/omega_miya/resources/images/tarot/RWS/39.jpg new file mode 100644 index 00000000..b72621ee Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/39.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/4.jpg b/omega_miya/resources/images/tarot/RWS/4.jpg new file mode 100644 index 00000000..79a03683 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/4.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/40.jpg b/omega_miya/resources/images/tarot/RWS/40.jpg new file mode 100644 index 00000000..5173fb42 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/40.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/41.jpg b/omega_miya/resources/images/tarot/RWS/41.jpg new file mode 100644 index 00000000..25fb481b Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/41.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/42.jpg b/omega_miya/resources/images/tarot/RWS/42.jpg new file mode 100644 index 00000000..cb6de6be Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/42.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/43.jpg b/omega_miya/resources/images/tarot/RWS/43.jpg new file mode 100644 index 00000000..a2de6ef6 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/43.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/44.jpg b/omega_miya/resources/images/tarot/RWS/44.jpg new file mode 100644 index 00000000..ca80c32c Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/44.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/45.jpg b/omega_miya/resources/images/tarot/RWS/45.jpg new file mode 100644 index 00000000..ba73e005 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/45.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/46.jpg b/omega_miya/resources/images/tarot/RWS/46.jpg new file mode 100644 index 00000000..4156016f Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/46.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/47.jpg b/omega_miya/resources/images/tarot/RWS/47.jpg new file mode 100644 index 00000000..7ac8b256 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/47.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/48.jpg b/omega_miya/resources/images/tarot/RWS/48.jpg new file mode 100644 index 00000000..34efa435 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/48.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/49.jpg b/omega_miya/resources/images/tarot/RWS/49.jpg new file mode 100644 index 00000000..787f8209 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/49.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/5.jpg b/omega_miya/resources/images/tarot/RWS/5.jpg new file mode 100644 index 00000000..16186ef8 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/5.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/50.jpg b/omega_miya/resources/images/tarot/RWS/50.jpg new file mode 100644 index 00000000..39c06119 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/50.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/51.jpg b/omega_miya/resources/images/tarot/RWS/51.jpg new file mode 100644 index 00000000..78e09139 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/51.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/52.jpg b/omega_miya/resources/images/tarot/RWS/52.jpg new file mode 100644 index 00000000..4217dc4f Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/52.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/53.jpg b/omega_miya/resources/images/tarot/RWS/53.jpg new file mode 100644 index 00000000..c76adb09 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/53.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/54.jpg b/omega_miya/resources/images/tarot/RWS/54.jpg new file mode 100644 index 00000000..50aaaf9e Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/54.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/55.jpg b/omega_miya/resources/images/tarot/RWS/55.jpg new file mode 100644 index 00000000..721864ae Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/55.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/56.jpg b/omega_miya/resources/images/tarot/RWS/56.jpg new file mode 100644 index 00000000..70b79ef4 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/56.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/57.jpg b/omega_miya/resources/images/tarot/RWS/57.jpg new file mode 100644 index 00000000..1e486601 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/57.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/58.jpg b/omega_miya/resources/images/tarot/RWS/58.jpg new file mode 100644 index 00000000..1f93e5ff Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/58.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/59.jpg b/omega_miya/resources/images/tarot/RWS/59.jpg new file mode 100644 index 00000000..85d3b0af Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/59.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/6.jpg b/omega_miya/resources/images/tarot/RWS/6.jpg new file mode 100644 index 00000000..f24d4318 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/6.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/60.jpg b/omega_miya/resources/images/tarot/RWS/60.jpg new file mode 100644 index 00000000..84464349 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/60.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/61.jpg b/omega_miya/resources/images/tarot/RWS/61.jpg new file mode 100644 index 00000000..49689d93 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/61.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/62.jpg b/omega_miya/resources/images/tarot/RWS/62.jpg new file mode 100644 index 00000000..3b7b21c0 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/62.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/63.jpg b/omega_miya/resources/images/tarot/RWS/63.jpg new file mode 100644 index 00000000..c503693c Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/63.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/64.jpg b/omega_miya/resources/images/tarot/RWS/64.jpg new file mode 100644 index 00000000..2ac04d5a Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/64.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/65.jpg b/omega_miya/resources/images/tarot/RWS/65.jpg new file mode 100644 index 00000000..243d8602 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/65.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/66.jpg b/omega_miya/resources/images/tarot/RWS/66.jpg new file mode 100644 index 00000000..a6b84192 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/66.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/67.jpg b/omega_miya/resources/images/tarot/RWS/67.jpg new file mode 100644 index 00000000..9a920ab3 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/67.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/68.jpg b/omega_miya/resources/images/tarot/RWS/68.jpg new file mode 100644 index 00000000..89e85efb Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/68.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/69.jpg b/omega_miya/resources/images/tarot/RWS/69.jpg new file mode 100644 index 00000000..a0d746f1 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/69.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/7.jpg b/omega_miya/resources/images/tarot/RWS/7.jpg new file mode 100644 index 00000000..c9e08e2c Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/7.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/70.jpg b/omega_miya/resources/images/tarot/RWS/70.jpg new file mode 100644 index 00000000..ea1e35c0 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/70.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/71.jpg b/omega_miya/resources/images/tarot/RWS/71.jpg new file mode 100644 index 00000000..71c3d6f7 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/71.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/72.jpg b/omega_miya/resources/images/tarot/RWS/72.jpg new file mode 100644 index 00000000..e99851c3 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/72.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/73.jpg b/omega_miya/resources/images/tarot/RWS/73.jpg new file mode 100644 index 00000000..0224a6b9 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/73.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/74.jpg b/omega_miya/resources/images/tarot/RWS/74.jpg new file mode 100644 index 00000000..07fae1c8 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/74.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/75.jpg b/omega_miya/resources/images/tarot/RWS/75.jpg new file mode 100644 index 00000000..5e3ba7ea Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/75.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/76.jpg b/omega_miya/resources/images/tarot/RWS/76.jpg new file mode 100644 index 00000000..3538b1b5 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/76.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/77.jpg b/omega_miya/resources/images/tarot/RWS/77.jpg new file mode 100644 index 00000000..8b2dab82 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/77.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/8.jpg b/omega_miya/resources/images/tarot/RWS/8.jpg new file mode 100644 index 00000000..aeacc273 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/8.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS/9.jpg b/omega_miya/resources/images/tarot/RWS/9.jpg new file mode 100644 index 00000000..f605de2c Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS/9.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/0.jpg b/omega_miya/resources/images/tarot/RWS_M/0.jpg new file mode 100644 index 00000000..872aa3af Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/0.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/1.jpg b/omega_miya/resources/images/tarot/RWS_M/1.jpg new file mode 100644 index 00000000..afa64ace Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/1.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/10.jpg b/omega_miya/resources/images/tarot/RWS_M/10.jpg new file mode 100644 index 00000000..9f2e4350 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/10.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/11.jpg b/omega_miya/resources/images/tarot/RWS_M/11.jpg new file mode 100644 index 00000000..6179a4cf Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/11.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/12.jpg b/omega_miya/resources/images/tarot/RWS_M/12.jpg new file mode 100644 index 00000000..b2372189 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/12.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/13.jpg b/omega_miya/resources/images/tarot/RWS_M/13.jpg new file mode 100644 index 00000000..1ab49dc0 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/13.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/14.jpg b/omega_miya/resources/images/tarot/RWS_M/14.jpg new file mode 100644 index 00000000..e8392f00 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/14.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/15.jpg b/omega_miya/resources/images/tarot/RWS_M/15.jpg new file mode 100644 index 00000000..96b696cf Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/15.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/16.jpg b/omega_miya/resources/images/tarot/RWS_M/16.jpg new file mode 100644 index 00000000..392da421 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/16.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/17.jpg b/omega_miya/resources/images/tarot/RWS_M/17.jpg new file mode 100644 index 00000000..44e1641f Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/17.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/18.jpg b/omega_miya/resources/images/tarot/RWS_M/18.jpg new file mode 100644 index 00000000..b04f129b Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/18.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/19.jpg b/omega_miya/resources/images/tarot/RWS_M/19.jpg new file mode 100644 index 00000000..2d8f0d7d Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/19.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/2.jpg b/omega_miya/resources/images/tarot/RWS_M/2.jpg new file mode 100644 index 00000000..ef3b12ee Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/2.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/20.jpg b/omega_miya/resources/images/tarot/RWS_M/20.jpg new file mode 100644 index 00000000..87e65db0 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/20.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/21.jpg b/omega_miya/resources/images/tarot/RWS_M/21.jpg new file mode 100644 index 00000000..eca5f937 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/21.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/3.jpg b/omega_miya/resources/images/tarot/RWS_M/3.jpg new file mode 100644 index 00000000..ed9543c2 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/3.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/4.jpg b/omega_miya/resources/images/tarot/RWS_M/4.jpg new file mode 100644 index 00000000..da7f80fb Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/4.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/5.jpg b/omega_miya/resources/images/tarot/RWS_M/5.jpg new file mode 100644 index 00000000..b65e3f1b Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/5.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/6.jpg b/omega_miya/resources/images/tarot/RWS_M/6.jpg new file mode 100644 index 00000000..8df34e40 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/6.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/7.jpg b/omega_miya/resources/images/tarot/RWS_M/7.jpg new file mode 100644 index 00000000..b508ca85 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/7.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/8.jpg b/omega_miya/resources/images/tarot/RWS_M/8.jpg new file mode 100644 index 00000000..a3b0ea34 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/8.jpg differ diff --git a/omega_miya/resources/images/tarot/RWS_M/9.jpg b/omega_miya/resources/images/tarot/RWS_M/9.jpg new file mode 100644 index 00000000..978d1c91 Binary files /dev/null and b/omega_miya/resources/images/tarot/RWS_M/9.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/-1.jpg b/omega_miya/resources/images/tarot/UWT/-1.jpg new file mode 100644 index 00000000..5b5d5793 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/-1.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/0.jpg b/omega_miya/resources/images/tarot/UWT/0.jpg new file mode 100644 index 00000000..17d5d05a Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/0.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/1.jpg b/omega_miya/resources/images/tarot/UWT/1.jpg new file mode 100644 index 00000000..9c777ce4 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/1.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/10.jpg b/omega_miya/resources/images/tarot/UWT/10.jpg new file mode 100644 index 00000000..6cd41f34 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/10.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/11.jpg b/omega_miya/resources/images/tarot/UWT/11.jpg new file mode 100644 index 00000000..8d92a5c5 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/11.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/12.jpg b/omega_miya/resources/images/tarot/UWT/12.jpg new file mode 100644 index 00000000..0591e629 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/12.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/13.jpg b/omega_miya/resources/images/tarot/UWT/13.jpg new file mode 100644 index 00000000..e3243479 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/13.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/14.jpg b/omega_miya/resources/images/tarot/UWT/14.jpg new file mode 100644 index 00000000..57255e85 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/14.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/15.jpg b/omega_miya/resources/images/tarot/UWT/15.jpg new file mode 100644 index 00000000..ebdb4ebf Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/15.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/16.jpg b/omega_miya/resources/images/tarot/UWT/16.jpg new file mode 100644 index 00000000..59376a4a Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/16.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/17.jpg b/omega_miya/resources/images/tarot/UWT/17.jpg new file mode 100644 index 00000000..dbc1899f Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/17.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/18.jpg b/omega_miya/resources/images/tarot/UWT/18.jpg new file mode 100644 index 00000000..437d6050 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/18.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/19.jpg b/omega_miya/resources/images/tarot/UWT/19.jpg new file mode 100644 index 00000000..666edb7b Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/19.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/2.jpg b/omega_miya/resources/images/tarot/UWT/2.jpg new file mode 100644 index 00000000..8d40c2c1 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/2.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/20.jpg b/omega_miya/resources/images/tarot/UWT/20.jpg new file mode 100644 index 00000000..fdd8a35d Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/20.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/21.jpg b/omega_miya/resources/images/tarot/UWT/21.jpg new file mode 100644 index 00000000..89f44fa9 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/21.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/22.jpg b/omega_miya/resources/images/tarot/UWT/22.jpg new file mode 100644 index 00000000..cbbf1ac6 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/22.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/23.jpg b/omega_miya/resources/images/tarot/UWT/23.jpg new file mode 100644 index 00000000..e84ac267 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/23.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/24.jpg b/omega_miya/resources/images/tarot/UWT/24.jpg new file mode 100644 index 00000000..4ca2ecaa Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/24.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/25.jpg b/omega_miya/resources/images/tarot/UWT/25.jpg new file mode 100644 index 00000000..f7c13543 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/25.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/26.jpg b/omega_miya/resources/images/tarot/UWT/26.jpg new file mode 100644 index 00000000..6b134022 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/26.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/27.jpg b/omega_miya/resources/images/tarot/UWT/27.jpg new file mode 100644 index 00000000..7a4ff2bd Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/27.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/28.jpg b/omega_miya/resources/images/tarot/UWT/28.jpg new file mode 100644 index 00000000..3baaba9c Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/28.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/29.jpg b/omega_miya/resources/images/tarot/UWT/29.jpg new file mode 100644 index 00000000..2de704ee Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/29.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/3.jpg b/omega_miya/resources/images/tarot/UWT/3.jpg new file mode 100644 index 00000000..a5b964d4 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/3.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/30.jpg b/omega_miya/resources/images/tarot/UWT/30.jpg new file mode 100644 index 00000000..c451840a Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/30.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/31.jpg b/omega_miya/resources/images/tarot/UWT/31.jpg new file mode 100644 index 00000000..471a94d1 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/31.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/32.jpg b/omega_miya/resources/images/tarot/UWT/32.jpg new file mode 100644 index 00000000..80d8e05a Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/32.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/33.jpg b/omega_miya/resources/images/tarot/UWT/33.jpg new file mode 100644 index 00000000..d9e0b1ec Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/33.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/34.jpg b/omega_miya/resources/images/tarot/UWT/34.jpg new file mode 100644 index 00000000..5c997329 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/34.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/35.jpg b/omega_miya/resources/images/tarot/UWT/35.jpg new file mode 100644 index 00000000..efc2ecca Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/35.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/36.jpg b/omega_miya/resources/images/tarot/UWT/36.jpg new file mode 100644 index 00000000..832ac0d8 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/36.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/37.jpg b/omega_miya/resources/images/tarot/UWT/37.jpg new file mode 100644 index 00000000..7b6d8df8 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/37.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/38.jpg b/omega_miya/resources/images/tarot/UWT/38.jpg new file mode 100644 index 00000000..3519f83d Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/38.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/39.jpg b/omega_miya/resources/images/tarot/UWT/39.jpg new file mode 100644 index 00000000..808d9135 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/39.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/4.jpg b/omega_miya/resources/images/tarot/UWT/4.jpg new file mode 100644 index 00000000..4aa08de9 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/4.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/40.jpg b/omega_miya/resources/images/tarot/UWT/40.jpg new file mode 100644 index 00000000..0fa195bd Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/40.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/41.jpg b/omega_miya/resources/images/tarot/UWT/41.jpg new file mode 100644 index 00000000..b07a80a2 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/41.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/42.jpg b/omega_miya/resources/images/tarot/UWT/42.jpg new file mode 100644 index 00000000..6c469cdb Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/42.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/43.jpg b/omega_miya/resources/images/tarot/UWT/43.jpg new file mode 100644 index 00000000..912b8366 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/43.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/44.jpg b/omega_miya/resources/images/tarot/UWT/44.jpg new file mode 100644 index 00000000..88e4c4ea Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/44.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/45.jpg b/omega_miya/resources/images/tarot/UWT/45.jpg new file mode 100644 index 00000000..60601a6c Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/45.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/46.jpg b/omega_miya/resources/images/tarot/UWT/46.jpg new file mode 100644 index 00000000..55b5b661 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/46.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/47.jpg b/omega_miya/resources/images/tarot/UWT/47.jpg new file mode 100644 index 00000000..943de468 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/47.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/48.jpg b/omega_miya/resources/images/tarot/UWT/48.jpg new file mode 100644 index 00000000..fecdf3cf Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/48.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/49.jpg b/omega_miya/resources/images/tarot/UWT/49.jpg new file mode 100644 index 00000000..7b6dec20 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/49.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/5.jpg b/omega_miya/resources/images/tarot/UWT/5.jpg new file mode 100644 index 00000000..8062ebbb Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/5.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/50.jpg b/omega_miya/resources/images/tarot/UWT/50.jpg new file mode 100644 index 00000000..687bb24e Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/50.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/51.jpg b/omega_miya/resources/images/tarot/UWT/51.jpg new file mode 100644 index 00000000..833dbd85 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/51.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/52.jpg b/omega_miya/resources/images/tarot/UWT/52.jpg new file mode 100644 index 00000000..86fa7da0 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/52.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/53.jpg b/omega_miya/resources/images/tarot/UWT/53.jpg new file mode 100644 index 00000000..79616b25 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/53.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/54.jpg b/omega_miya/resources/images/tarot/UWT/54.jpg new file mode 100644 index 00000000..9ffc6106 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/54.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/55.jpg b/omega_miya/resources/images/tarot/UWT/55.jpg new file mode 100644 index 00000000..e48491f2 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/55.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/56.jpg b/omega_miya/resources/images/tarot/UWT/56.jpg new file mode 100644 index 00000000..bffd2d7c Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/56.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/57.jpg b/omega_miya/resources/images/tarot/UWT/57.jpg new file mode 100644 index 00000000..f0d29469 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/57.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/58.jpg b/omega_miya/resources/images/tarot/UWT/58.jpg new file mode 100644 index 00000000..8f4d4a0c Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/58.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/59.jpg b/omega_miya/resources/images/tarot/UWT/59.jpg new file mode 100644 index 00000000..420772f1 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/59.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/6.jpg b/omega_miya/resources/images/tarot/UWT/6.jpg new file mode 100644 index 00000000..d44447fc Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/6.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/60.jpg b/omega_miya/resources/images/tarot/UWT/60.jpg new file mode 100644 index 00000000..67cfd4ad Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/60.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/61.jpg b/omega_miya/resources/images/tarot/UWT/61.jpg new file mode 100644 index 00000000..a3b59bc8 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/61.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/62.jpg b/omega_miya/resources/images/tarot/UWT/62.jpg new file mode 100644 index 00000000..cdd20745 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/62.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/63.jpg b/omega_miya/resources/images/tarot/UWT/63.jpg new file mode 100644 index 00000000..f14b1ea7 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/63.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/64.jpg b/omega_miya/resources/images/tarot/UWT/64.jpg new file mode 100644 index 00000000..41b2a30b Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/64.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/65.jpg b/omega_miya/resources/images/tarot/UWT/65.jpg new file mode 100644 index 00000000..2da6e93f Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/65.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/66.jpg b/omega_miya/resources/images/tarot/UWT/66.jpg new file mode 100644 index 00000000..96d2a5d4 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/66.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/67.jpg b/omega_miya/resources/images/tarot/UWT/67.jpg new file mode 100644 index 00000000..d1207b5e Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/67.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/68.jpg b/omega_miya/resources/images/tarot/UWT/68.jpg new file mode 100644 index 00000000..061c3661 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/68.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/69.jpg b/omega_miya/resources/images/tarot/UWT/69.jpg new file mode 100644 index 00000000..0eb57a04 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/69.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/7.jpg b/omega_miya/resources/images/tarot/UWT/7.jpg new file mode 100644 index 00000000..df74fcd6 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/7.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/70.jpg b/omega_miya/resources/images/tarot/UWT/70.jpg new file mode 100644 index 00000000..1ab78e2e Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/70.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/71.jpg b/omega_miya/resources/images/tarot/UWT/71.jpg new file mode 100644 index 00000000..7b732b36 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/71.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/72.jpg b/omega_miya/resources/images/tarot/UWT/72.jpg new file mode 100644 index 00000000..138997e3 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/72.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/73.jpg b/omega_miya/resources/images/tarot/UWT/73.jpg new file mode 100644 index 00000000..7ad2fcc5 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/73.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/74.jpg b/omega_miya/resources/images/tarot/UWT/74.jpg new file mode 100644 index 00000000..ddb39ba3 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/74.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/75.jpg b/omega_miya/resources/images/tarot/UWT/75.jpg new file mode 100644 index 00000000..4f8c31b9 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/75.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/76.jpg b/omega_miya/resources/images/tarot/UWT/76.jpg new file mode 100644 index 00000000..233a88a9 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/76.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/77.jpg b/omega_miya/resources/images/tarot/UWT/77.jpg new file mode 100644 index 00000000..2eca2021 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/77.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/8.jpg b/omega_miya/resources/images/tarot/UWT/8.jpg new file mode 100644 index 00000000..62859e09 Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/8.jpg differ diff --git a/omega_miya/resources/images/tarot/UWT/9.jpg b/omega_miya/resources/images/tarot/UWT/9.jpg new file mode 100644 index 00000000..de49583d Binary files /dev/null and b/omega_miya/resources/images/tarot/UWT/9.jpg differ diff --git a/omega_miya/resources/images/tarot/bilibili/-1.png b/omega_miya/resources/images/tarot/bilibili/-1.png new file mode 100644 index 00000000..82dbe00d Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/-1.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/0.png b/omega_miya/resources/images/tarot/bilibili/0.png new file mode 100644 index 00000000..69614e23 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/0.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/1.png b/omega_miya/resources/images/tarot/bilibili/1.png new file mode 100644 index 00000000..f6c76f27 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/1.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/10.png b/omega_miya/resources/images/tarot/bilibili/10.png new file mode 100644 index 00000000..bb1d9d26 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/10.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/11.png b/omega_miya/resources/images/tarot/bilibili/11.png new file mode 100644 index 00000000..37f2f9cc Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/11.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/12.png b/omega_miya/resources/images/tarot/bilibili/12.png new file mode 100644 index 00000000..51bdcf67 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/12.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/13.png b/omega_miya/resources/images/tarot/bilibili/13.png new file mode 100644 index 00000000..b28e7e81 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/13.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/14.png b/omega_miya/resources/images/tarot/bilibili/14.png new file mode 100644 index 00000000..6b3fc081 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/14.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/15.png b/omega_miya/resources/images/tarot/bilibili/15.png new file mode 100644 index 00000000..c42dd90f Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/15.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/16.png b/omega_miya/resources/images/tarot/bilibili/16.png new file mode 100644 index 00000000..15a11b64 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/16.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/17.png b/omega_miya/resources/images/tarot/bilibili/17.png new file mode 100644 index 00000000..34cd8520 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/17.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/18.png b/omega_miya/resources/images/tarot/bilibili/18.png new file mode 100644 index 00000000..caee0359 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/18.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/19.png b/omega_miya/resources/images/tarot/bilibili/19.png new file mode 100644 index 00000000..2f73cb4e Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/19.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/2.png b/omega_miya/resources/images/tarot/bilibili/2.png new file mode 100644 index 00000000..70dc0663 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/2.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/20.png b/omega_miya/resources/images/tarot/bilibili/20.png new file mode 100644 index 00000000..0780c186 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/20.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/21.png b/omega_miya/resources/images/tarot/bilibili/21.png new file mode 100644 index 00000000..3b91759d Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/21.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/22.png b/omega_miya/resources/images/tarot/bilibili/22.png new file mode 100644 index 00000000..5bfff2ae Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/22.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/23.png b/omega_miya/resources/images/tarot/bilibili/23.png new file mode 100644 index 00000000..220329ac Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/23.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/24.png b/omega_miya/resources/images/tarot/bilibili/24.png new file mode 100644 index 00000000..d1607aa4 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/24.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/25.png b/omega_miya/resources/images/tarot/bilibili/25.png new file mode 100644 index 00000000..77040520 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/25.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/26.png b/omega_miya/resources/images/tarot/bilibili/26.png new file mode 100644 index 00000000..7c6cc068 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/26.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/27.png b/omega_miya/resources/images/tarot/bilibili/27.png new file mode 100644 index 00000000..2f25db98 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/27.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/28.png b/omega_miya/resources/images/tarot/bilibili/28.png new file mode 100644 index 00000000..208de02b Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/28.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/29.png b/omega_miya/resources/images/tarot/bilibili/29.png new file mode 100644 index 00000000..abc3aa59 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/29.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/3.png b/omega_miya/resources/images/tarot/bilibili/3.png new file mode 100644 index 00000000..158bba78 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/3.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/30.png b/omega_miya/resources/images/tarot/bilibili/30.png new file mode 100644 index 00000000..d816505d Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/30.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/31.png b/omega_miya/resources/images/tarot/bilibili/31.png new file mode 100644 index 00000000..9edce4eb Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/31.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/32.png b/omega_miya/resources/images/tarot/bilibili/32.png new file mode 100644 index 00000000..d48de5c5 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/32.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/33.png b/omega_miya/resources/images/tarot/bilibili/33.png new file mode 100644 index 00000000..8cbf1890 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/33.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/34.png b/omega_miya/resources/images/tarot/bilibili/34.png new file mode 100644 index 00000000..2b782c35 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/34.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/35.png b/omega_miya/resources/images/tarot/bilibili/35.png new file mode 100644 index 00000000..3f89bbb6 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/35.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/36.png b/omega_miya/resources/images/tarot/bilibili/36.png new file mode 100644 index 00000000..dd9f623c Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/36.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/37.png b/omega_miya/resources/images/tarot/bilibili/37.png new file mode 100644 index 00000000..7431d4af Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/37.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/38.png b/omega_miya/resources/images/tarot/bilibili/38.png new file mode 100644 index 00000000..5771d0db Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/38.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/39.png b/omega_miya/resources/images/tarot/bilibili/39.png new file mode 100644 index 00000000..4bff18ec Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/39.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/4.png b/omega_miya/resources/images/tarot/bilibili/4.png new file mode 100644 index 00000000..1cbc5e96 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/4.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/40.png b/omega_miya/resources/images/tarot/bilibili/40.png new file mode 100644 index 00000000..fc65a03f Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/40.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/41.png b/omega_miya/resources/images/tarot/bilibili/41.png new file mode 100644 index 00000000..166337f5 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/41.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/42.png b/omega_miya/resources/images/tarot/bilibili/42.png new file mode 100644 index 00000000..9c661295 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/42.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/43.png b/omega_miya/resources/images/tarot/bilibili/43.png new file mode 100644 index 00000000..b5f6c57a Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/43.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/44.png b/omega_miya/resources/images/tarot/bilibili/44.png new file mode 100644 index 00000000..cbb3b870 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/44.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/45.png b/omega_miya/resources/images/tarot/bilibili/45.png new file mode 100644 index 00000000..99d4b5fe Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/45.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/46.png b/omega_miya/resources/images/tarot/bilibili/46.png new file mode 100644 index 00000000..720c565d Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/46.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/47.png b/omega_miya/resources/images/tarot/bilibili/47.png new file mode 100644 index 00000000..f3e06f56 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/47.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/48.png b/omega_miya/resources/images/tarot/bilibili/48.png new file mode 100644 index 00000000..08d8e26e Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/48.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/49.png b/omega_miya/resources/images/tarot/bilibili/49.png new file mode 100644 index 00000000..00f9a137 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/49.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/5.png b/omega_miya/resources/images/tarot/bilibili/5.png new file mode 100644 index 00000000..0d8eb943 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/5.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/50.png b/omega_miya/resources/images/tarot/bilibili/50.png new file mode 100644 index 00000000..2ba19899 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/50.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/51.png b/omega_miya/resources/images/tarot/bilibili/51.png new file mode 100644 index 00000000..979220f3 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/51.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/52.png b/omega_miya/resources/images/tarot/bilibili/52.png new file mode 100644 index 00000000..2090c58c Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/52.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/53.png b/omega_miya/resources/images/tarot/bilibili/53.png new file mode 100644 index 00000000..29ef2719 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/53.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/54.png b/omega_miya/resources/images/tarot/bilibili/54.png new file mode 100644 index 00000000..f3f0d2d5 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/54.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/55.png b/omega_miya/resources/images/tarot/bilibili/55.png new file mode 100644 index 00000000..56aec055 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/55.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/56.png b/omega_miya/resources/images/tarot/bilibili/56.png new file mode 100644 index 00000000..5519f9e4 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/56.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/57.png b/omega_miya/resources/images/tarot/bilibili/57.png new file mode 100644 index 00000000..51457425 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/57.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/58.png b/omega_miya/resources/images/tarot/bilibili/58.png new file mode 100644 index 00000000..12e9ea7d Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/58.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/59.png b/omega_miya/resources/images/tarot/bilibili/59.png new file mode 100644 index 00000000..ec5921d1 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/59.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/6.png b/omega_miya/resources/images/tarot/bilibili/6.png new file mode 100644 index 00000000..958b43ab Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/6.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/60.png b/omega_miya/resources/images/tarot/bilibili/60.png new file mode 100644 index 00000000..62ff2dab Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/60.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/61.png b/omega_miya/resources/images/tarot/bilibili/61.png new file mode 100644 index 00000000..514d69c7 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/61.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/62.png b/omega_miya/resources/images/tarot/bilibili/62.png new file mode 100644 index 00000000..0268e589 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/62.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/63.png b/omega_miya/resources/images/tarot/bilibili/63.png new file mode 100644 index 00000000..4bca7aec Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/63.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/64.png b/omega_miya/resources/images/tarot/bilibili/64.png new file mode 100644 index 00000000..066fb640 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/64.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/65.png b/omega_miya/resources/images/tarot/bilibili/65.png new file mode 100644 index 00000000..dcacd1df Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/65.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/66.png b/omega_miya/resources/images/tarot/bilibili/66.png new file mode 100644 index 00000000..da94b7db Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/66.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/67.png b/omega_miya/resources/images/tarot/bilibili/67.png new file mode 100644 index 00000000..5b63b402 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/67.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/68.png b/omega_miya/resources/images/tarot/bilibili/68.png new file mode 100644 index 00000000..0326b9c0 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/68.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/69.png b/omega_miya/resources/images/tarot/bilibili/69.png new file mode 100644 index 00000000..1a96be14 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/69.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/7.png b/omega_miya/resources/images/tarot/bilibili/7.png new file mode 100644 index 00000000..27ae181a Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/7.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/70.png b/omega_miya/resources/images/tarot/bilibili/70.png new file mode 100644 index 00000000..6332decf Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/70.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/71.png b/omega_miya/resources/images/tarot/bilibili/71.png new file mode 100644 index 00000000..67e94f1b Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/71.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/72.png b/omega_miya/resources/images/tarot/bilibili/72.png new file mode 100644 index 00000000..ecaf2f42 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/72.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/73.png b/omega_miya/resources/images/tarot/bilibili/73.png new file mode 100644 index 00000000..ddfc1217 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/73.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/74.png b/omega_miya/resources/images/tarot/bilibili/74.png new file mode 100644 index 00000000..4bf6f39b Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/74.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/75.png b/omega_miya/resources/images/tarot/bilibili/75.png new file mode 100644 index 00000000..e1f4d240 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/75.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/76.png b/omega_miya/resources/images/tarot/bilibili/76.png new file mode 100644 index 00000000..ae681af4 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/76.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/77.png b/omega_miya/resources/images/tarot/bilibili/77.png new file mode 100644 index 00000000..9ac6b78c Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/77.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/8.png b/omega_miya/resources/images/tarot/bilibili/8.png new file mode 100644 index 00000000..47c3f447 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/8.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/9.png b/omega_miya/resources/images/tarot/bilibili/9.png new file mode 100644 index 00000000..c8dd68b1 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/9.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/ACE_OF_COINS.png b/omega_miya/resources/images/tarot/bilibili/ACE_OF_COINS.png new file mode 100644 index 00000000..74cdb361 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/ACE_OF_COINS.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/ACE_OF_CUPS.png b/omega_miya/resources/images/tarot/bilibili/ACE_OF_CUPS.png new file mode 100644 index 00000000..7cc7f58a Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/ACE_OF_CUPS.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/ACE_OF_SWORDS.png b/omega_miya/resources/images/tarot/bilibili/ACE_OF_SWORDS.png new file mode 100644 index 00000000..205b65d9 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/ACE_OF_SWORDS.png differ diff --git a/omega_miya/resources/images/tarot/bilibili/ACE_OF_WANDS.png b/omega_miya/resources/images/tarot/bilibili/ACE_OF_WANDS.png new file mode 100644 index 00000000..6ea00276 Binary files /dev/null and b/omega_miya/resources/images/tarot/bilibili/ACE_OF_WANDS.png differ diff --git a/omega_miya/utils/Omega_plugin_utils/__init__.py b/omega_miya/utils/Omega_plugin_utils/__init__.py deleted file mode 100644 index 1dcf2dbe..00000000 --- a/omega_miya/utils/Omega_plugin_utils/__init__.py +++ /dev/null @@ -1,61 +0,0 @@ -from typing import Optional -from nonebot.plugin import Export -from nonebot.typing import T_State -from .rules import OmegaRules -from .encrypt import AESEncryptStr -from .cooldown import PluginCoolDown -from .permission import PermissionChecker -from .http_fetcher import HttpFetcher -from .message_sender import MsgSender -from .picture_encoder import PicEncoder -from .picture_effector import PicEffector -from .process_utils import ProcessUtils -from .zip_utils import create_zip_file, create_7z_file - - -def init_export( - plugin_export: Export, - custom_name: str, - usage: str, - auth_node: list = None, - cool_down: list = None, - **kwargs: str) -> Export: - setattr(plugin_export, 'custom_name', custom_name) - setattr(plugin_export, 'usage', usage) - setattr(plugin_export, 'auth_node', auth_node) - setattr(plugin_export, 'cool_down', cool_down) - for key, value in kwargs.items(): - setattr(plugin_export, key, value) - return plugin_export - - -def init_permission_state( - name: str, - notice: Optional[bool] = None, - command: Optional[bool] = None, - level: Optional[int] = None, - auth_node: Optional[str] = None) -> T_State: - return { - '_matcher': name, - '_notice_permission': notice, - '_command_permission': command, - '_permission_level': level, - '_auth_node': auth_node - } - - -__all__ = [ - 'init_export', - 'init_permission_state', - 'OmegaRules', - 'AESEncryptStr', - 'PluginCoolDown', - 'PermissionChecker', - 'HttpFetcher', - 'MsgSender', - 'PicEncoder', - 'PicEffector', - 'ProcessUtils', - 'create_zip_file', - 'create_7z_file' -] diff --git a/omega_miya/utils/Omega_plugin_utils/cooldown.py b/omega_miya/utils/Omega_plugin_utils/cooldown.py deleted file mode 100644 index a756ccea..00000000 --- a/omega_miya/utils/Omega_plugin_utils/cooldown.py +++ /dev/null @@ -1,75 +0,0 @@ -import datetime -from omega_miya.utils.Omega_Base import DBCoolDownEvent, Result -from dataclasses import dataclass, field - - -@dataclass -class PluginCoolDown: - type: str - cool_down_time: int - global_type: str = field(default='global', init=False) - plugin_type: str = field(default='plugin', init=False) - group_type: str = field(default='group', init=False) - user_type: str = field(default='user', init=False) - skip_auth_node: str = field(default='skip_cd', init=False) - - @classmethod - async def check_and_set_global_cool_down(cls, minutes: int) -> Result.IntResult: - check = await DBCoolDownEvent.check_global_cool_down_event() - if check.result == 1: - return check - elif check.result in [0, 2]: - if minutes <= 0: - return check - await DBCoolDownEvent.add_global_cool_down_event( - stop_at=datetime.datetime.now() + datetime.timedelta(minutes=minutes)) - return check - else: - return check - - @classmethod - async def check_and_set_plugin_cool_down(cls, minutes: int, plugin: str) -> Result.IntResult: - check = await DBCoolDownEvent.check_plugin_cool_down_event(plugin=plugin) - if check.result == 1: - return check - elif check.result in [0, 2]: - if minutes <= 0: - return check - await DBCoolDownEvent.add_plugin_cool_down_event( - stop_at=datetime.datetime.now() + datetime.timedelta(minutes=minutes), plugin=plugin) - return check - else: - return check - - @classmethod - async def check_and_set_group_cool_down(cls, minutes: int, plugin: str, group_id: int) -> Result.IntResult: - check = await DBCoolDownEvent.check_group_cool_down_event(plugin=plugin, group_id=group_id) - if check.result == 1: - return check - elif check.result in [0, 2]: - if minutes <= 0: - return check - await DBCoolDownEvent.add_group_cool_down_event( - stop_at=datetime.datetime.now() + datetime.timedelta(minutes=minutes), plugin=plugin, group_id=group_id) - return check - else: - return check - - @classmethod - async def check_and_set_user_cool_down(cls, minutes: int, plugin: str, user_id: int) -> Result.IntResult: - check = await DBCoolDownEvent.check_user_cool_down_event(plugin=plugin, user_id=user_id) - if check.result == 1: - return check - elif check.result in [0, 2]: - if minutes <= 0: - return check - await DBCoolDownEvent.add_user_cool_down_event( - stop_at=datetime.datetime.now() + datetime.timedelta(minutes=minutes), plugin=plugin, user_id=user_id) - return check - else: - return check - - -__all__ = [ - 'PluginCoolDown' -] diff --git a/omega_miya/utils/Omega_processor/cooldown.py b/omega_miya/utils/Omega_processor/cooldown.py deleted file mode 100644 index 3b540fe3..00000000 --- a/omega_miya/utils/Omega_processor/cooldown.py +++ /dev/null @@ -1,150 +0,0 @@ -""" -插件命令冷却系统 -使用参考例: -plugins/setu -plugin/draw -""" -from nonebot import get_plugin, get_driver, logger -from nonebot.adapters.cqhttp import MessageSegment, Message -from nonebot.exception import IgnoredException -from nonebot.typing import T_State -from nonebot.matcher import Matcher -from nonebot.adapters.cqhttp.bot import Bot -from nonebot.adapters.cqhttp.event import MessageEvent -from omega_miya.utils.Omega_plugin_utils import PluginCoolDown -from omega_miya.utils.Omega_Base import DBCoolDownEvent, DBAuth, DBBot - - -global_config = get_driver().config -SUPERUSERS = global_config.superusers - - -async def preprocessor_cooldown(matcher: Matcher, bot: Bot, event: MessageEvent, state: T_State): - """ - 冷却处理 T_RunPreProcessor - """ - - group_id = event.dict().get('group_id') - user_id = event.dict().get('user_id') - - # 忽略超级用户 - if user_id in [int(x) for x in SUPERUSERS]: - return - - # 只处理message事件 - if matcher.type != 'message': - return - - # 处理插件冷却 - # 冷却处理优先级: 全局>插件>群组>用户 - # 冷却限制优先级: 用户>群组>插件>全局 - plugin_name = matcher.module - plugin = get_plugin(plugin_name) - plugin_cool_down_list = plugin.export.get('cool_down') - - # 只处理声明了__plugin_cool_down__的插件 - if not plugin_cool_down_list: - return - - # 跳过由 got 等事件处理函数创建临时 matcher 避免冷却在命令交互中被不正常触发 - if matcher.temp: - return - - # 处理不同bot权限 - self_bot = DBBot(self_qq=int(bot.self_id)) - - # 检查用户或群组是否有skip_cd权限, 跳过冷却检查 - skip_cd_auth_node = f'{plugin_name}.{PluginCoolDown.skip_auth_node}' - user_auth = DBAuth(self_bot=self_bot, auth_id=user_id, auth_type='user', auth_node=skip_cd_auth_node) - user_tag_res = await user_auth.tags_info() - if user_tag_res.result[0] == 1 and user_tag_res.result[1] == 0: - return - - group_auth = DBAuth(self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=skip_cd_auth_node) - group_tag_res = await group_auth.tags_info() - if group_tag_res.result[0] == 1 and group_tag_res.result[1] == 0: - return - - # 检查冷却情况 - global_check = await DBCoolDownEvent.check_global_cool_down_event() - plugin_check = await DBCoolDownEvent.check_plugin_cool_down_event(plugin=plugin_name) - group_check = await DBCoolDownEvent.check_group_cool_down_event(plugin=plugin_name, group_id=group_id) - user_check = await DBCoolDownEvent.check_user_cool_down_event(plugin=plugin_name, user_id=user_id) - - # 处理全局冷却 - # 先检查是否已有全局冷却 - if plugin_check.result == 1 or group_check.result == 1 or user_check.result == 1: - pass - elif global_check.result == 1: - await bot.send(event=event, message=Message(f'{MessageSegment.at(user_id=user_id)}命令冷却中!\n{global_check.info}')) - raise IgnoredException('全局命令冷却中') - elif global_check.result in [0, 2]: - pass - else: - logger.error(f'全局冷却事件异常! group: {group_id}, user: {user_id}, error: {global_check.info}') - # 然后再处理命令中存在的全局冷却 - for time in [x.cool_down_time for x in plugin_cool_down_list if x.type == PluginCoolDown.global_type]: - # 若有插件、群组或用户冷却则交由其处理 - if plugin_check.result == 1 or group_check.result == 1 or user_check.result == 1: - break - - res = await PluginCoolDown.check_and_set_global_cool_down(minutes=time) - if res.result == 1: - await bot.send(event=event, message=Message(f'{MessageSegment.at(user_id=user_id)}命令冷却中!\n{res.info}')) - raise IgnoredException('全局命令冷却中') - elif res.result in [0, 2]: - pass - else: - logger.error(f'全局冷却事件异常! group: {group_id}, user: {user_id}, error: {res.info}') - - # 处理插件冷却 - for time in [x.cool_down_time for x in plugin_cool_down_list if x.type == PluginCoolDown.plugin_type]: - # 若有群组或用户冷却则交由其处理 - if group_check.result == 1 or user_check.result == 1: - break - - res = await PluginCoolDown.check_and_set_plugin_cool_down(minutes=time, plugin=plugin_name) - if res.result == 1: - await bot.send(event=event, message=Message(f'{MessageSegment.at(user_id=user_id)}命令冷却中!\n{res.info}')) - raise IgnoredException('插件命令冷却中') - elif res.result in [0, 2]: - pass - else: - logger.error(f'插件冷却事件异常! group: {group_id}, user: {user_id}, plugin: {plugin_name}, error: {res.info}') - - # 处理群组冷却 - for time in [x.cool_down_time for x in plugin_cool_down_list if x.type == PluginCoolDown.group_type]: - if not group_id: - break - - # 若有用户冷却则交由其处理 - if user_check.result == 1: - break - - res = await PluginCoolDown.check_and_set_group_cool_down(minutes=time, plugin=plugin_name, group_id=group_id) - if res.result == 1: - await bot.send(event=event, message=Message(f'{MessageSegment.at(user_id=user_id)}命令冷却中!\n{res.info}')) - raise IgnoredException('群组命令冷却中') - elif res.result in [0, 2]: - pass - else: - logger.error(f'群组冷却事件异常! group: {group_id}, user: {user_id}, plugin: {plugin_name}, error: {res.info}') - - # 处理用户冷却 - for time in [x.cool_down_time for x in plugin_cool_down_list if x.type == PluginCoolDown.user_type]: - if not user_id: - break - - res = await PluginCoolDown.check_and_set_user_cool_down(minutes=time, plugin=plugin_name, user_id=user_id) - if res.result == 1: - await bot.send(event=event, message=Message(f'{MessageSegment.at(user_id=user_id)}命令冷却中!\n{res.info}')) - raise IgnoredException('用户命令冷却中') - elif res.result in [0, 2]: - pass - else: - logger.error(f'用户冷却事件异常! group: {group_id}, user: {user_id}, plugin: {plugin_name}, error: {res.info}') - - -__all__ = [ - 'preprocessor_cooldown' -] diff --git a/omega_miya/utils/bilibili_utils/data_classes.py b/omega_miya/utils/bilibili_utils/data_classes.py index 7d627c64..fb5fd76d 100644 --- a/omega_miya/utils/bilibili_utils/data_classes.py +++ b/omega_miya/utils/bilibili_utils/data_classes.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from typing import Optional, Dict, List -from omega_miya.utils.Omega_Base import Result +from omega_miya.database import Result class BiliInfo(object): diff --git a/omega_miya/utils/bilibili_utils/dynamic.py b/omega_miya/utils/bilibili_utils/dynamic.py index 0374fd66..1bfce15d 100644 --- a/omega_miya/utils/bilibili_utils/dynamic.py +++ b/omega_miya/utils/bilibili_utils/dynamic.py @@ -1,7 +1,7 @@ import json from nonebot import logger -from omega_miya.utils.Omega_plugin_utils import HttpFetcher -from omega_miya.utils.Omega_Base import Result +from omega_miya.utils.omega_plugin_utils import HttpFetcher +from omega_miya.database import Result from .request_utils import BiliRequestUtils from .data_classes import BiliInfo, BiliResult @@ -174,7 +174,7 @@ def data_parser(cls, dynamic_data: dict) -> BiliResult.DynamicInfoResult: desc = '发布了一条活动相关动态' content = dynamic_card['vest']['content'] title = dynamic_card['sketch']['title'] - description = dynamic_card['sketch']['desc_text'] + description = dynamic_card['sketch'].get('desc_text') # type=4200, 直播间动态(疑似) elif type_ == 4200: desc = '发布了一条直播间动态' diff --git a/omega_miya/utils/bilibili_utils/live.py b/omega_miya/utils/bilibili_utils/live.py index 3fda052c..6cc8d131 100644 --- a/omega_miya/utils/bilibili_utils/live.py +++ b/omega_miya/utils/bilibili_utils/live.py @@ -1,7 +1,7 @@ from typing import List, Union import datetime from nonebot import logger -from omega_miya.utils.Omega_plugin_utils import HttpFetcher +from omega_miya.utils.omega_plugin_utils import HttpFetcher from .request_utils import BiliRequestUtils from .data_classes import BiliInfo, BiliResult diff --git a/omega_miya/utils/bilibili_utils/request_utils.py b/omega_miya/utils/bilibili_utils/request_utils.py index da74a0b9..6ccef1ad 100644 --- a/omega_miya/utils/bilibili_utils/request_utils.py +++ b/omega_miya/utils/bilibili_utils/request_utils.py @@ -1,6 +1,6 @@ from nonebot import get_driver -from omega_miya.utils.Omega_plugin_utils import HttpFetcher, PicEncoder -from omega_miya.utils.Omega_Base import Result +from omega_miya.utils.omega_plugin_utils import HttpFetcher, PicEncoder +from omega_miya.database import Result __GLOBAL_CONFIG = get_driver().config diff --git a/omega_miya/utils/bilibili_utils/user.py b/omega_miya/utils/bilibili_utils/user.py index 22a51ccb..6f632198 100644 --- a/omega_miya/utils/bilibili_utils/user.py +++ b/omega_miya/utils/bilibili_utils/user.py @@ -1,5 +1,5 @@ -from omega_miya.utils.Omega_plugin_utils import HttpFetcher -from omega_miya.utils.Omega_Base import Result +from omega_miya.utils.omega_plugin_utils import HttpFetcher +from omega_miya.database import Result from .request_utils import BiliRequestUtils from .data_classes import BiliInfo, BiliResult diff --git a/omega_miya/utils/dice_utils/calculator.py b/omega_miya/utils/dice_utils/calculator.py index 6b254968..09ede395 100644 --- a/omega_miya/utils/dice_utils/calculator.py +++ b/omega_miya/utils/dice_utils/calculator.py @@ -164,7 +164,11 @@ def std_calculate(self) -> float: # 从最内层括号开始依次执行运算 while inner_par := re.search(r'(\([+\-*/^.\d]+?\))', _expression): inner_cal_result = self.__base_calculate(inner_par.group()) - _expression = f'{_expression[:inner_par.start()]}{inner_cal_result}{_expression[inner_par.end():]}' + # 处理括号前 a(x+y) 的简写形式 + if re.match(r'.*\d$', _expression[:inner_par.start()]): + _expression = f'{_expression[:inner_par.start()]}*{inner_cal_result}{_expression[inner_par.end():]}' + else: + _expression = f'{_expression[:inner_par.start()]}{inner_cal_result}{_expression[inner_par.end():]}' # 括号遍历完后执行最外层运算 return self.__base_calculate(_expression) diff --git a/omega_miya/utils/dice_utils/dice.py b/omega_miya/utils/dice_utils/dice.py index 8cc69c1d..3fca303b 100644 --- a/omega_miya/utils/dice_utils/dice.py +++ b/omega_miya/utils/dice_utils/dice.py @@ -12,6 +12,7 @@ import random import asyncio from typing import List, Dict +from dataclasses import dataclass class BaseDice(object): @@ -71,6 +72,36 @@ async def aio_dice(self) -> int: return result +class ParseDice(object): + """ + 掷骰表达式解析与执行 + """ + @dataclass + class ParsedExpression: + num: int # 骰子个数 + side: int # 骰子面数 + check: bool # 是否检定 + hidden: bool # 是否暗骰 + + @dataclass + class DiceResult: + num: int # 骰子个数 + side: int # 骰子面数 + hidden: bool # 是否暗骰 + __result_L: List[int] # 本次掷骰全部骰子点数的列表 + + @property + def sum(self) -> int: + return sum(self.__result_L) # 本次掷骰全部骰子点数之和 + + def __init__(self, expression: str): + self.__raw_expression = expression + self.__is_parsed: bool = False + + def __parse(self): + pass + + __all__ = [ 'BaseDice' ] diff --git a/omega_miya/utils/nhentai_utils/__init__.py b/omega_miya/utils/nhentai_utils/__init__.py index 378c7a09..36b37694 100644 --- a/omega_miya/utils/nhentai_utils/__init__.py +++ b/omega_miya/utils/nhentai_utils/__init__.py @@ -8,8 +8,8 @@ from dataclasses import dataclass from bs4 import BeautifulSoup from nonebot import logger, get_driver -from omega_miya.utils.Omega_plugin_utils import HttpFetcher, create_7z_file -from omega_miya.utils.Omega_Base import Result +from omega_miya.utils.omega_plugin_utils import HttpFetcher, create_7z_file +from omega_miya.database import Result global_config = get_driver().config TMP_PATH = global_config.tmp_path_ diff --git a/omega_miya/utils/nonebot_plugin_apscheduler/__init__.py b/omega_miya/utils/nonebot_plugin_apscheduler/__init__.py index 70287127..6f8596ae 100644 --- a/omega_miya/utils/nonebot_plugin_apscheduler/__init__.py +++ b/omega_miya/utils/nonebot_plugin_apscheduler/__init__.py @@ -1,6 +1,7 @@ import logging -from nonebot import get_driver, export +from nonebot import get_driver +from nonebot.plugin.export import export from nonebot.log import logger, LoguruHandler from apscheduler.schedulers.asyncio import AsyncIOScheduler diff --git a/omega_miya/utils/Omega_multibot_support/__init__.py b/omega_miya/utils/omega_multibot_support/__init__.py similarity index 66% rename from omega_miya/utils/Omega_multibot_support/__init__.py rename to omega_miya/utils/omega_multibot_support/__init__.py index 334500ef..48858390 100644 --- a/omega_miya/utils/Omega_multibot_support/__init__.py +++ b/omega_miya/utils/omega_multibot_support/__init__.py @@ -12,11 +12,12 @@ from nonebot import get_driver, logger from nonebot.typing import T_State from nonebot.matcher import Matcher -from nonebot.adapters.cqhttp.event import Event +from nonebot.adapters.cqhttp.event import Event, MessageEvent, NoticeEvent, RequestEvent from nonebot.message import run_preprocessor from nonebot.exception import IgnoredException from nonebot.adapters.cqhttp.bot import Bot -from omega_miya.utils.Omega_Base import DBBot +from omega_miya.database import DBBot +from .multi_bot_utils import MultiBotUtils driver = get_driver() ONLINE_BOTS: Dict[str, Bot] = {} @@ -31,8 +32,8 @@ async def upgrade_connected_bot(bot: Bot): info = '||'.join([f'{k}:{v}' for (k, v) in bot_info.items()]) bot_result = await DBBot(self_qq=int(bot.self_id)).upgrade(status=1, info=info) if bot_result.success(): - logger.opt(colors=True).info(f'Bot: {bot.self_id} 已连接, ' - f'Database upgrade Success: {bot_result.info}') + logger.opt(colors=True).success(f'Bot: {bot.self_id} 已连接, ' + f'Database upgrade Success: {bot_result.info}') else: logger.opt(colors=True).error(f'Bot: {bot.self_id} 已连接, ' f'Database upgrade Failed: {bot_result.info}') @@ -59,7 +60,23 @@ async def unique_bot_responding_limit(matcher: Matcher, bot: Bot, event: Event, raise IgnoredException(f'Bot {bot.self_id} ignored event which not match self_id with {event.self_id}.') # 对于多协议端同时接入, 各个bot之间不能相互响应, 避免形成死循环 - event_user_id = str(event.dict().get('user_id')) + event_user_id = str(getattr(event, 'user_id', -1)) if event_user_id in [x for x in ONLINE_BOTS.keys() if x != bot.self_id]: logger.debug(f'Bot {bot.self_id} ignored responding self-relation event with Bot {event_user_id}.') raise IgnoredException(f'Bot {bot.self_id} ignored responding self-relation event with Bot {event_user_id}.') + + # 对于多协议端同时接入, 需要使用permission_updater限制bot id避免响应混乱 + # 在matcher首次运行时在statue中写入首次执行matcher的bot id + if isinstance(event, (MessageEvent, NoticeEvent, RequestEvent)) and not matcher.temp: + try: + state.update({ + '_first_response_bot': bot.self_id, + '_original_session_id': event.get_session_id(), + '_original_permission': matcher.permission, + }) + matcher.permission_updater(MultiBotUtils.first_response_bot_permission_updater) + except ValueError: + logger.warning(f'Unique bot responding limit preprocessor running failed with ValueError. ' + f'The high probability is event had no session_id. Unsupported event: {repr(event)}.') + except Exception as e: + logger.error(f'Unique bot responding limit preprocessor running failed with error: {repr(e)}.') diff --git a/omega_miya/utils/omega_multibot_support/multi_bot_utils.py b/omega_miya/utils/omega_multibot_support/multi_bot_utils.py new file mode 100644 index 00000000..11ace7cc --- /dev/null +++ b/omega_miya/utils/omega_multibot_support/multi_bot_utils.py @@ -0,0 +1,42 @@ +""" +@Author : Ailitonia +@Date : 2021/08/10 22:53 +@FileName : multi_bot_utils.py +@Project : nonebot2_miya +@Description : Multi Bot Utils +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +from nonebot.typing import T_State +from nonebot.permission import Permission +from nonebot.adapters.cqhttp.event import Event +from nonebot.adapters.cqhttp.bot import Bot + + +class MultiBotUtils(object): + @classmethod + async def first_response_bot_permission_updater( + cls, bot: Bot, event: Event, state: T_State, permission: Permission) -> Permission: + """ + T_PermissionUpdater + :param bot: 事件响应 bot + :param event: 事件 + :param state: matcher 运行时 state + :param permission: 上次 reject 或 pause 前的 permission + """ + # stats默认执行初始化于omega_miya.utils.Omega_bot_manager.set_first_response_bot_state + _bot_self_id = state.get('_first_response_bot') + _original_permission = state.get('_original_permission') + _original_session_id = state.get('_original_session_id') + + async def _first_response_bot(_bot: Bot, _event: Event) -> bool: + return _bot.self_id == _bot_self_id and str(event.self_id) == _bot_self_id and _event.get_session_id( + ) == _original_session_id and await _original_permission(bot, event) + + return Permission(_first_response_bot) + + +__all__ = [ + 'MultiBotUtils' +] diff --git a/omega_miya/utils/omega_plugin_utils/__init__.py b/omega_miya/utils/omega_plugin_utils/__init__.py new file mode 100644 index 00000000..4e9d25b1 --- /dev/null +++ b/omega_miya/utils/omega_plugin_utils/__init__.py @@ -0,0 +1,100 @@ +from typing import Optional, List +from nonebot.plugin.export import Export +from nonebot.typing import T_State +from .rules import OmegaRules +from .encrypt import AESEncryptStr +from .cooldown import PluginCoolDown +from .permission import PermissionChecker +from .http_fetcher import HttpFetcher +from .message_decoder import MessageDecoder +from .message_sender import MsgSender +from .picture_encoder import PicEncoder +from .picture_effector import PicEffector +from .process_utils import ProcessUtils +from .text_utils import TextUtils +from .zip_utils import create_zip_file, create_7z_file + + +def init_export( + plugin_export: Export, + custom_name: str, + usage: str, + auth_node: Optional[List[str]] = None, + **kwargs: str) -> Export: + """ + 初始化 export, 用于声明当前插件配置等信息, 用于 processor 等集中处理 + :param plugin_export: 当前插件 export + :param custom_name: 插件自定义名称 + :param usage: 插件自定义用法说明 + :param auth_node: 插件所有可配置的权限节点 + :param kwargs: 其他自定义参数 + :return: + """ + setattr(plugin_export, 'custom_name', custom_name) + setattr(plugin_export, 'usage', usage) + + # 初始化默认权限节点 + if auth_node is not None: + auth_node_ = set(auth_node) + auth_node_.add(OmegaRules.basic_auth_node) + auth_node_.add(PluginCoolDown.skip_auth_node) + auth_node_ = list(auth_node_) + auth_node_.sort() + else: + auth_node_ = [OmegaRules.basic_auth_node, + PluginCoolDown.skip_auth_node] + setattr(plugin_export, 'auth_node', auth_node_) + + for key, value in kwargs.items(): + setattr(plugin_export, key, value) + return plugin_export + + +def init_processor_state( + name: str, + notice: bool = False, + command: bool = False, + level: Optional[int] = None, + auth_node: str = OmegaRules.basic_auth_node, + cool_down: Optional[List[PluginCoolDown]] = None, + enable_cool_down_check: bool = True +) -> T_State: + """ + matcher state 初始化函数, 用于声明当前 matcher 权限及冷却等信息, 用于 processor 集中处理 + :param name: 自定义名称, 用于识别 matcher + :param notice: 是否需要通知权限 + :param command: 是否需要命令权限 + :param level: 需要等级权限 + :param auth_node: 需要的权限节点名称 + :param cool_down: 需要的冷却时间配置 + :param enable_cool_down_check: 是否启用冷却检测, 部分无响应或被动响应的 matcher 应视情况将本选项设置为 False + :return: + """ + return { + '_matcher_name': name, + '_notice_permission': notice, + '_command_permission': command, + '_permission_level': level, + '_auth_node': auth_node, + '_cool_down': cool_down, + '_enable_cool_down_check': enable_cool_down_check + } + + +__all__ = [ + 'init_export', + 'init_processor_state', + 'OmegaRules', + 'AESEncryptStr', + 'PluginCoolDown', + 'PermissionChecker', + 'HttpFetcher', + 'MessageDecoder', + 'MsgSender', + 'PicEncoder', + 'PicEffector', + 'ProcessUtils', + 'TextUtils', + 'create_zip_file', + 'create_7z_file' +] diff --git a/omega_miya/utils/omega_plugin_utils/cooldown.py b/omega_miya/utils/omega_plugin_utils/cooldown.py new file mode 100644 index 00000000..f75515c0 --- /dev/null +++ b/omega_miya/utils/omega_plugin_utils/cooldown.py @@ -0,0 +1,114 @@ +import datetime +from omega_miya.database import DBCoolDownEvent, Result +from dataclasses import dataclass, field + + +@dataclass +class PluginCoolDown: + """ + 插件冷却工具类, 用于声明当前 matcher 权限及冷却等信息, 并负责处理具体冷却事件 + - type: 冷却类型 + - cool_down_time: 冷却时间, 单位秒 + """ + type: str + cool_down_time: int + global_group_type: str = field(default=DBCoolDownEvent.global_group_type, init=False) + global_user_type: str = field(default=DBCoolDownEvent.global_user_type, init=False) + group_type: str = field(default=DBCoolDownEvent.group_type, init=False) + user_type: str = field(default=DBCoolDownEvent.user_type, init=False) + skip_auth_node: str = field(default='skip_cd', init=False) + + @classmethod + async def check_and_set_global_group_cool_down(cls, group_id, seconds: int) -> Result.IntResult: + """ + :return: + result = 1: Success with still CoolDown with duration in info + result = 0: Success with not in CoolDown and set a new CoolDown event + result = -1: Error + """ + check = await DBCoolDownEvent.check_global_group_cool_down_event(group_id=group_id) + if check.success() and check.result == 1: + return check + elif check.success() and check.result in [0, 2]: + result = await DBCoolDownEvent.add_global_group_cool_down_event( + group_id=group_id, + stop_at=datetime.datetime.now() + datetime.timedelta(seconds=seconds)) + if result.error: + return Result.IntResult(error=True, info=f'Set CoolDown Event failed, {result.info}', result=-1) + else: + return Result.IntResult(error=False, info='Success set CoolDown Event', result=0) + else: + return check + + @classmethod + async def check_and_set_global_user_cool_down(cls, user_id: int, seconds: int) -> Result.IntResult: + """ + :return: + result = 1: Success with still CoolDown with duration in info + result = 0: Success with not in CoolDown and set a new CoolDown event + result = -1: Error + """ + check = await DBCoolDownEvent.check_global_user_cool_down_event(user_id=user_id) + if check.success() and check.result == 1: + return check + elif check.success() and check.result in [0, 2]: + result = await DBCoolDownEvent.add_global_user_cool_down_event( + user_id=user_id, + stop_at=datetime.datetime.now() + datetime.timedelta(seconds=seconds)) + if result.error: + return Result.IntResult(error=True, info=f'Set CoolDown Event failed, {result.info}', result=-1) + else: + return Result.IntResult(error=False, info='Success set CoolDown Event', result=0) + else: + return check + + @classmethod + async def check_and_set_group_cool_down(cls, plugin: str, group_id: int, seconds: int) -> Result.IntResult: + """ + :return: + result = 1: Success with still CoolDown with duration in info + result = 0: Success with not in CoolDown and set a new CoolDown event + result = -1: Error + """ + check = await DBCoolDownEvent.check_group_cool_down_event(plugin=plugin, group_id=group_id) + if check.success() and check.result == 1: + return check + elif check.success() and check.result in [0, 2]: + result = await DBCoolDownEvent.add_group_cool_down_event( + plugin=plugin, + group_id=group_id, + stop_at=datetime.datetime.now() + datetime.timedelta(seconds=seconds)) + if result.error: + return Result.IntResult(error=True, info=f'Set CoolDown Event failed, {result.info}', result=-1) + else: + return Result.IntResult(error=False, info='Success set CoolDown Event', result=0) + else: + return check + + @classmethod + async def check_and_set_user_cool_down(cls, plugin: str, user_id: int, seconds: int) -> Result.IntResult: + """ + :return: + result = 1: Success with still CoolDown with duration in info + result = 0: Success with not in CoolDown and set a new CoolDown event + result = -1: Error + """ + check = await DBCoolDownEvent.check_user_cool_down_event(plugin=plugin, user_id=user_id) + if check.success() and check.result == 1: + return check + elif check.success() and check.result in [0, 2]: + result = await DBCoolDownEvent.add_user_cool_down_event( + plugin=plugin, + user_id=user_id, + stop_at=datetime.datetime.now() + datetime.timedelta(seconds=seconds)) + if result.error: + return Result.IntResult(error=True, info=f'Set CoolDown Event failed, {result.info}', result=-1) + else: + return Result.IntResult(error=False, info='Success set CoolDown Event', result=0) + else: + return check + + +__all__ = [ + 'PluginCoolDown' +] diff --git a/omega_miya/utils/Omega_plugin_utils/encrypt.py b/omega_miya/utils/omega_plugin_utils/encrypt.py similarity index 100% rename from omega_miya/utils/Omega_plugin_utils/encrypt.py rename to omega_miya/utils/omega_plugin_utils/encrypt.py diff --git a/omega_miya/utils/Omega_plugin_utils/http_fetcher.py b/omega_miya/utils/omega_plugin_utils/http_fetcher.py similarity index 96% rename from omega_miya/utils/Omega_plugin_utils/http_fetcher.py rename to omega_miya/utils/omega_plugin_utils/http_fetcher.py index 204aac8e..5d6b3919 100644 --- a/omega_miya/utils/Omega_plugin_utils/http_fetcher.py +++ b/omega_miya/utils/omega_plugin_utils/http_fetcher.py @@ -8,8 +8,7 @@ from dataclasses import dataclass from typing import Dict, List, Union, Iterable, Optional, Any from nonebot import logger -from omega_miya.utils.Omega_Base import DBStatus - +from omega_miya.database import DBStatus global_config = nonebot.get_driver().config ENABLE_PROXY = global_config.enable_proxy @@ -19,6 +18,19 @@ class HttpFetcher(object): + DEFAULT_HEADERS = {'accept': '*/*', + 'accept-encoding': 'gzip, deflate', + 'accept-language': 'zh-CN,zh;q=0.9', + 'dnt': '1', + 'sec-ch-ua': '"Google Chrome";v="89", "Chromium";v="89", ";Not A Brand";v="99"', + 'sec-ch-ua-mobile': '?0', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'sec-gpc': '1', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/89.0.4389.114 Safari/537.36'} + @dataclass class __FetcherResult: error: bool diff --git a/omega_miya/utils/omega_plugin_utils/message_decoder.py b/omega_miya/utils/omega_plugin_utils/message_decoder.py new file mode 100644 index 00000000..d8e23fe7 --- /dev/null +++ b/omega_miya/utils/omega_plugin_utils/message_decoder.py @@ -0,0 +1,28 @@ +""" +@Author : Ailitonia +@Date : 2021/08/14 19:42 +@FileName : message_decoder.py +@Project : nonebot2_miya +@Description : 消息解析工具 +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +from typing import List +from nonebot.adapters.cqhttp.message import Message + + +class MessageDecoder(object): + def __init__(self, message: Message): + self.__message = message + + def get_all_img_url(self) -> List[str]: + return [msg_seg.data.get('url') for msg_seg in self.__message if msg_seg.type == 'image'] + + def get_all_at_qq(self) -> List[int]: + return [msg_seg.data.get('qq') for msg_seg in self.__message if msg_seg.type == 'at'] + + +__all__ = [ + 'MessageDecoder' +] diff --git a/omega_miya/utils/Omega_plugin_utils/message_sender.py b/omega_miya/utils/omega_plugin_utils/message_sender.py similarity index 99% rename from omega_miya/utils/Omega_plugin_utils/message_sender.py rename to omega_miya/utils/omega_plugin_utils/message_sender.py index 679eeebf..c6dd0ca9 100644 --- a/omega_miya/utils/Omega_plugin_utils/message_sender.py +++ b/omega_miya/utils/omega_plugin_utils/message_sender.py @@ -12,7 +12,7 @@ from typing import Optional, List, Union from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.message import Message, MessageSegment -from omega_miya.utils.Omega_Base import DBBot, DBBotGroup, DBFriend, DBSubscription +from omega_miya.database import DBBot, DBBotGroup, DBFriend, DBSubscription class MsgSender(object): @@ -77,6 +77,7 @@ async def safe_broadcast_groups_subscription_node_custom( "type": "node", "data": { "name": custom_nickname, + "user_id": custom_user_id, "uin": custom_user_id, "content": msg } diff --git a/omega_miya/utils/Omega_plugin_utils/permission.py b/omega_miya/utils/omega_plugin_utils/permission.py similarity index 95% rename from omega_miya/utils/Omega_plugin_utils/permission.py rename to omega_miya/utils/omega_plugin_utils/permission.py index b8f0e851..b8073dc4 100644 --- a/omega_miya/utils/Omega_plugin_utils/permission.py +++ b/omega_miya/utils/omega_plugin_utils/permission.py @@ -1,4 +1,4 @@ -from omega_miya.utils.Omega_Base import DBBot, DBFriend, DBBotGroup, DBAuth +from omega_miya.database import DBBot, DBFriend, DBBotGroup, DBAuth class PermissionChecker(object): diff --git a/omega_miya/utils/Omega_plugin_utils/picture_effector.py b/omega_miya/utils/omega_plugin_utils/picture_effector.py similarity index 98% rename from omega_miya/utils/Omega_plugin_utils/picture_effector.py rename to omega_miya/utils/omega_plugin_utils/picture_effector.py index 0e6c4fe1..2526ea24 100644 --- a/omega_miya/utils/Omega_plugin_utils/picture_effector.py +++ b/omega_miya/utils/omega_plugin_utils/picture_effector.py @@ -13,7 +13,7 @@ from typing import Optional from io import BytesIO from PIL import Image, ImageFilter, ImageEnhance -from omega_miya.utils.Omega_Base import Result +from omega_miya.database import Result class PicEffector(object): diff --git a/omega_miya/utils/Omega_plugin_utils/picture_encoder.py b/omega_miya/utils/omega_plugin_utils/picture_encoder.py similarity index 95% rename from omega_miya/utils/Omega_plugin_utils/picture_encoder.py rename to omega_miya/utils/omega_plugin_utils/picture_encoder.py index 9bf30feb..6a4ff16a 100644 --- a/omega_miya/utils/Omega_plugin_utils/picture_encoder.py +++ b/omega_miya/utils/omega_plugin_utils/picture_encoder.py @@ -3,8 +3,8 @@ import aiofiles import pathlib from nonebot import get_driver, logger -from omega_miya.utils.Omega_Base import Result -from omega_miya.utils.Omega_plugin_utils import HttpFetcher +from omega_miya.database import Result +from omega_miya.utils.omega_plugin_utils import HttpFetcher driver = get_driver() @@ -76,7 +76,7 @@ async def get_base64(self) -> Result.TextResult: return encode_result async def get_file(self, *, folder_flag: str = 'PicEncoder') -> Result.TextResult: - fetcher = HttpFetcher(timeout=30, attempt_limit=2, flag='PicEncoder_get_base64', headers=self.__headers) + fetcher = HttpFetcher(timeout=30, attempt_limit=2, flag='PicEncoder_get_file', headers=self.__headers) bytes_result = await fetcher.get_bytes(url=self.__pic_url) if bytes_result.error: return Result.TextResult(error=True, info='Image download failed', result='') diff --git a/omega_miya/utils/Omega_plugin_utils/process_utils.py b/omega_miya/utils/omega_plugin_utils/process_utils.py similarity index 100% rename from omega_miya/utils/Omega_plugin_utils/process_utils.py rename to omega_miya/utils/omega_plugin_utils/process_utils.py index 4e86e142..016e07e2 100644 --- a/omega_miya/utils/Omega_plugin_utils/process_utils.py +++ b/omega_miya/utils/omega_plugin_utils/process_utils.py @@ -30,10 +30,10 @@ async def fragment_process( all_count = len(tasks) if all_count <= 0: raise ValueError('Param "tasks" must not be null') - elif not isinstance(fragment_size, int): - raise ValueError('Param "fragment_size" must be int') elif not fragment_size: fragment_size = all_count + elif not isinstance(fragment_size, int): + raise ValueError('Param "fragment_size" must be int') elif fragment_size > all_count: fragment_size = all_count elif fragment_size <= 0: diff --git a/omega_miya/utils/Omega_plugin_utils/rules.py b/omega_miya/utils/omega_plugin_utils/rules.py similarity index 74% rename from omega_miya/utils/Omega_plugin_utils/rules.py rename to omega_miya/utils/omega_plugin_utils/rules.py index 58698c19..740895ce 100644 --- a/omega_miya/utils/Omega_plugin_utils/rules.py +++ b/omega_miya/utils/omega_plugin_utils/rules.py @@ -1,21 +1,30 @@ from nonebot.rule import Rule from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot -from nonebot.adapters.cqhttp.event import Event -from omega_miya.utils.Omega_Base import DBBot, DBFriend, DBBotGroup, DBAuth +from nonebot.adapters.cqhttp.event import Event, MessageEvent, PrivateMessageEvent, NoticeEvent, RequestEvent +from omega_miya.database import DBBot, DBFriend, DBBotGroup, DBAuth class OmegaRules(object): + # Default basic auth mode name + basic_auth_node: str = 'basic' + + @classmethod + def basic_node(cls, t: str) -> str: + """传入任意字符串组装 basic auth node""" + return f'{t}.{cls.basic_auth_node}' + # Plugin permission rule # Only using for group @classmethod def has_group_notice_permission(cls) -> Rule: async def _has_group_notice_permission(bot: Bot, event: Event, state: T_State) -> bool: - detail_type = event.dict().get(f'{event.get_type()}_type') - group_id = event.dict().get('group_id') + if isinstance(event, PrivateMessageEvent): + return False + group_id = getattr(event, 'group_id', None) self_bot = DBBot(self_qq=int(bot.self_id)) # 检查当前消息类型 - if not str(detail_type).startswith('group'): + if not group_id: return False else: res = await DBBotGroup(group_id=group_id, self_bot=self_bot).permission_notice() @@ -28,11 +37,12 @@ async def _has_group_notice_permission(bot: Bot, event: Event, state: T_State) - @classmethod def has_group_command_permission(cls) -> Rule: async def _has_group_command_permission(bot: Bot, event: Event, state: T_State) -> bool: - detail_type = event.dict().get(f'{event.get_type()}_type') - group_id = event.dict().get('group_id') + if isinstance(event, PrivateMessageEvent): + return False + group_id = getattr(event, 'group_id', None) self_bot = DBBot(self_qq=int(bot.self_id)) # 检查当前消息类型 - if not str(detail_type).startswith('group'): + if not group_id: return False else: res = await DBBotGroup(group_id=group_id, self_bot=self_bot).permission_command() @@ -45,11 +55,12 @@ async def _has_group_command_permission(bot: Bot, event: Event, state: T_State) @classmethod def has_group_permission_level(cls, level: int) -> Rule: async def _has_group_permission_level(bot: Bot, event: Event, state: T_State) -> bool: - detail_type = event.dict().get(f'{event.get_type()}_type') - group_id = event.dict().get('group_id') + if isinstance(event, PrivateMessageEvent): + return False + group_id = getattr(event, 'group_id', None) self_bot = DBBot(self_qq=int(bot.self_id)) # 检查当前消息类型 - if not str(detail_type).startswith('group'): + if not group_id: return False else: res = await DBBotGroup(group_id=group_id, self_bot=self_bot).permission_level() @@ -61,20 +72,18 @@ async def _has_group_permission_level(bot: Bot, event: Event, state: T_State) -> # 权限节点检查 @classmethod - def has_auth_node(cls, *auth_nodes: str) -> Rule: + def has_auth_node(cls, auth_node: str) -> Rule: async def _has_auth_node(bot: Bot, event: Event, state: T_State) -> bool: - auth_node = '.'.join(auth_nodes) - detail_type = event.dict().get(f'{event.get_type()}_type') - group_id = event.dict().get('group_id') - user_id = event.dict().get('user_id') + group_id = getattr(event, 'group_id', None) + user_id = getattr(event, 'user_id', None) self_bot = DBBot(self_qq=int(bot.self_id)) # 检查当前消息类型 - if detail_type == 'private': + if isinstance(event, PrivateMessageEvent): user_auth = DBAuth(self_bot=self_bot, auth_id=user_id, auth_type='user', auth_node=auth_node) user_tag_res = await user_auth.tags_info() allow_tag = user_tag_res.result[0] deny_tag = user_tag_res.result[1] - elif str(detail_type).startswith('group'): + elif group_id is not None: group_auth = DBAuth(self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=auth_node) group_tag_res = await group_auth.tags_info() allow_tag = group_tag_res.result[0] @@ -98,13 +107,12 @@ def has_level_or_node(cls, level: int, auth_node: str) -> Rule: :return: 群组权限等级大于要求等级或者具备权限节点, 权限节点为deny则拒绝 """ async def _has_level_or_node(bot: Bot, event: Event, state: T_State) -> bool: - detail_type = event.dict().get(f'{event.get_type()}_type') - group_id = event.dict().get('group_id') - user_id = event.dict().get('user_id') + group_id = getattr(event, 'group_id', None) + user_id = getattr(event, 'user_id', None) self_bot = DBBot(self_qq=int(bot.self_id)) # level检查部分 - if detail_type != 'group': + if not group_id: level_checker = False else: level_res = await DBBotGroup(group_id=group_id, self_bot=self_bot).permission_level() @@ -114,12 +122,12 @@ async def _has_level_or_node(bot: Bot, event: Event, state: T_State) -> bool: level_checker = False # node检查部分 - if detail_type == 'private': + if isinstance(event, PrivateMessageEvent): user_auth = DBAuth(self_bot=self_bot, auth_id=user_id, auth_type='user', auth_node=auth_node) user_tag_res = await user_auth.tags_info() allow_tag = user_tag_res.result[0] deny_tag = user_tag_res.result[1] - elif str(detail_type).startswith('group'): + elif group_id is not None: group_auth = DBAuth(self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=auth_node) group_tag_res = await group_auth.tags_info() allow_tag = group_tag_res.result[0] @@ -139,8 +147,15 @@ async def _has_level_or_node(bot: Bot, event: Event, state: T_State) -> bool: @classmethod def has_friend_private_permission(cls) -> Rule: async def _has_friend_private_permission(bot: Bot, event: Event, state: T_State) -> bool: - detail_type = event.dict().get(f'{event.get_type()}_type') - user_id = event.dict().get('user_id') + if isinstance(event, MessageEvent): + detail_type = event.message_type + elif isinstance(event, NoticeEvent): + detail_type = event.notice_type + elif isinstance(event, RequestEvent): + detail_type = event.request_type + else: + detail_type = None + user_id = getattr(event, 'user_id', None) self_bot = DBBot(self_qq=int(bot.self_id)) # 检查当前消息类型 if detail_type != 'private': diff --git a/omega_miya/utils/omega_plugin_utils/text_utils.py b/omega_miya/utils/omega_plugin_utils/text_utils.py new file mode 100644 index 00000000..e72c0d74 --- /dev/null +++ b/omega_miya/utils/omega_plugin_utils/text_utils.py @@ -0,0 +1,92 @@ +import os +import asyncio +from datetime import datetime +from nonebot import logger, get_driver +from PIL import Image, ImageDraw, ImageFont +from omega_miya.database import Result + +global_config = get_driver().config +TMP_PATH = global_config.tmp_path_ +RESOURCES_PATH = global_config.resources_path_ + + +class TextUtils(object): + def __init__(self, text: str): + self.text = text + + def split_multiline(self, width: int, font: ImageFont.FreeTypeFont, *, stroke_width=0) -> str: + """ + 按长度切分换行文本 + :return: 切分换行后的文本 + """ + spl_num = 0 + spl_list = [] + for num in range(len(self.text)): + text_width, text_height = font.getsize_multiline(self.text[spl_num:num], stroke_width=stroke_width) + if text_width > width: + spl_list.append(self.text[spl_num:num]) + spl_num = num + else: + spl_list.append(self.text[spl_num:]) + return '\n'.join(spl_list) + + def __text_to_img( + self, + *, + image_wight: int = 512, + font_name: str = 'SourceHanSans_Regular.otf' + ) -> Image: + font_path = os.path.abspath(os.path.join(RESOURCES_PATH, 'fonts', font_name)) + if not os.path.exists(font_path): + raise ValueError('Font not found') + + # 处理文字层 主体部分 + font_size = image_wight // 25 + font = ImageFont.truetype(font_path, font_size) + # 按长度切分文本 + text_ = self.split_multiline(width=int(image_wight * 0.75), font=font) + text_w, text_h = font.getsize_multiline(text_) + + # 初始化背景图层 + image_height = text_h + 100 + background = Image.new(mode="RGB", size=(image_wight, image_height), color=(255, 255, 255)) + # 绘制文字 + ImageDraw.Draw(background).multiline_text( + xy=(int(image_wight * 0.12), 50), + text=text_, + font=font, + fill=(0, 0, 0)) + + return background + + async def text_to_img( + self, + *, + image_wight: int = 512, + font_name: str = 'SourceHanSans_Regular.otf' + ) -> Result.TextResult: + def __handle(): + img_ = self.__text_to_img(image_wight=image_wight, font_name=font_name) + # 检查生成图片路径 + img_folder_path = os.path.abspath(os.path.join(TMP_PATH, 'text_to_img')) + if not os.path.exists(img_folder_path): + os.makedirs(img_folder_path) + img_path = os.path.abspath( + os.path.join(img_folder_path, f"{hash(self.text)}_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.jpg")) + # 保存图片 + img_.save(img_path, 'JPEG') + return img_path + + loop = asyncio.get_running_loop() + try: + path_result = await loop.run_in_executor(None, __handle) + path = os.path.abspath(path_result) + return Result.TextResult(error=False, info='Success', result=path) + except Exception as e: + logger.error(f'TextUtils | text_to_img failed, error: {repr(e)}') + return Result.TextResult(error=True, info=repr(e), result='') + + +__all__ = [ + 'TextUtils' +] diff --git a/omega_miya/utils/Omega_plugin_utils/zip_utils.py b/omega_miya/utils/omega_plugin_utils/zip_utils.py similarity index 98% rename from omega_miya/utils/Omega_plugin_utils/zip_utils.py rename to omega_miya/utils/omega_plugin_utils/zip_utils.py index bf51f02d..6a09b3a0 100644 --- a/omega_miya/utils/Omega_plugin_utils/zip_utils.py +++ b/omega_miya/utils/omega_plugin_utils/zip_utils.py @@ -4,7 +4,7 @@ import asyncio from typing import List from nonebot.log import logger -from omega_miya.utils.Omega_Base import Result +from omega_miya.database import Result def __create_zip_file(files: List[str], file_path: str, file_name: str) -> Result.TextResult: diff --git a/omega_miya/utils/Omega_processor/__init__.py b/omega_miya/utils/omega_processor/__init__.py similarity index 55% rename from omega_miya/utils/Omega_processor/__init__.py rename to omega_miya/utils/omega_processor/__init__.py index 2ecd6145..b0d29366 100644 --- a/omega_miya/utils/Omega_processor/__init__.py +++ b/omega_miya/utils/omega_processor/__init__.py @@ -9,31 +9,50 @@ """ from typing import Optional +from nonebot import get_driver from nonebot.message import event_preprocessor, event_postprocessor, run_preprocessor, run_postprocessor from nonebot.typing import T_State from nonebot.matcher import Matcher from nonebot.adapters.cqhttp.event import Event, MessageEvent from nonebot.adapters.cqhttp.bot import Bot +from .plugins import startup_init_plugins, preprocessor_plugins_manager from .permission import preprocessor_permission from .cooldown import preprocessor_cooldown +from .favorability import postprocessor_favorability from .history import postprocessor_history +from .statistic import postprocessor_statistic +from .rate_limiting import preprocessor_rate_limiting, postprocessor_rate_limiting + + +driver = get_driver() + + +# 启动时预处理 +@driver.on_startup +async def handle_on_startup(): + # 初始化插件信息 + await startup_init_plugins() # 事件预处理 @event_preprocessor async def handle_event_preprocessor(bot: Bot, event: Event, state: T_State): - pass + # 针对消息事件的处理 + if isinstance(event, MessageEvent): + # 处理速率控制 + await preprocessor_rate_limiting(bot=bot, event=event, state=state) # 运行预处理 @run_preprocessor async def handle_run_preprocessor(matcher: Matcher, bot: Bot, event: Event, state: T_State): - # 处理权限 + # 处理插件管理 + await preprocessor_plugins_manager(matcher=matcher, bot=bot, event=event, state=state) + # 针对消息事件的处理 if isinstance(event, MessageEvent): + # 处理权限 await preprocessor_permission(matcher=matcher, bot=bot, event=event, state=state) - - # 处理冷却 - if isinstance(event, MessageEvent): + # 处理冷却 await preprocessor_cooldown(matcher=matcher, bot=bot, event=event, state=state) @@ -41,8 +60,10 @@ async def handle_run_preprocessor(matcher: Matcher, bot: Bot, event: Event, stat @run_postprocessor async def handle_run_postprocessor( matcher: Matcher, exception: Optional[Exception], bot: Bot, event: Event, state: T_State): - # 处理插件统计 - pass + # 针对消息事件的处理 + if isinstance(event, MessageEvent): + # 处理插件统计 + await postprocessor_statistic(matcher=matcher, exception=exception, bot=bot, event=event, state=state) # 事件后处理 @@ -50,3 +71,9 @@ async def handle_run_postprocessor( async def handle_event_postprocessor(bot: Bot, event: Event, state: T_State): # 处理历史记录 await postprocessor_history(bot=bot, event=event, state=state) + # 针对消息事件的处理 + if isinstance(event, MessageEvent): + # 处理好感度 + await postprocessor_favorability(bot=bot, event=event, state=state) + # 处理事件后流控 + await postprocessor_rate_limiting(bot=bot, event=event, state=state) diff --git a/omega_miya/utils/omega_processor/cooldown.py b/omega_miya/utils/omega_processor/cooldown.py new file mode 100644 index 00000000..287734cd --- /dev/null +++ b/omega_miya/utils/omega_processor/cooldown.py @@ -0,0 +1,155 @@ +""" +插件命令冷却系统 +使用参考例: +plugins/setu +plugin/draw +""" +from typing import Optional, List +from nonebot import get_driver, logger +from nonebot.exception import IgnoredException +from nonebot.typing import T_State +from nonebot.matcher import Matcher +from nonebot.adapters.cqhttp.bot import Bot +from nonebot.adapters.cqhttp.event import MessageEvent +from omega_miya.utils.omega_plugin_utils import PluginCoolDown +from omega_miya.database import DBCoolDownEvent, DBAuth, DBBot + + +global_config = get_driver().config +SUPERUSERS = global_config.superusers + +# 是否在用户交互中提示全局冷却状态 +# 强烈建议将此选项保持为 False, 以避免非交互类插件触发 +NOTICE_GLOBAL_COOLDOWN: bool = False + + +async def preprocessor_cooldown(matcher: Matcher, bot: Bot, event: MessageEvent, state: T_State): + """ + 冷却处理 T_RunPreProcessor + 处理冷却优先级: 全局用户 > 全局群组 > 插件用户 > 插件群组 + """ + + group_id = getattr(event, 'group_id', None) + user_id = event.user_id + + # 忽略超级用户 + if user_id in [int(x) for x in SUPERUSERS]: + return + + # 获取插件及 matcher 信息 + plugin_name: str = matcher.plugin_name + matcher_default_state = matcher.state + matcher_cool_down: Optional[List[PluginCoolDown]] = matcher_default_state.get('_cool_down', None) + enable_cool_down_check: bool = matcher_default_state.get('_enable_cool_down_check', True) + + # 跳过声明了不处理冷却的 matcher + if not enable_cool_down_check: + return + + # 处理全局冷却 + # 设计上只有专用的管理员命令能设置全局冷却, 单插件不允许设置全局冷却, 因此这里只做检验不做更新 + # skip_cd 权限针对插件, 全局冷却优先级高于该权限, 优先处理 + # 检查全局用户冷却 + global_user_check = await DBCoolDownEvent.check_global_user_cool_down_event(user_id=user_id) + if global_user_check.success() and global_user_check.result == 1: + if NOTICE_GLOBAL_COOLDOWN: + await bot.send(event=event, message=f'用户冷却中!\n{global_user_check.info}', at_sender=True) + logger.info(f'CoolDown | 全局用户冷却中, user: {user_id}, {global_user_check.info}') + raise IgnoredException('全局用户冷却中') + elif global_user_check.success() and global_user_check.result in [0, 2]: + pass + else: + logger.error(f'CoolDown | 全局用户冷却事件异常, user: {user_id}, {global_user_check}') + + # 检查全局群组冷却 + if group_id is not None: + global_group_check = await DBCoolDownEvent.check_global_group_cool_down_event(group_id=group_id) + if global_group_check.success() and global_group_check.result == 1: + if NOTICE_GLOBAL_COOLDOWN: + await bot.send(event=event, message=f'群组冷却中!\n{global_group_check.info}', at_sender=True) + logger.info(f'CoolDown | 全局群组冷却中, group: {group_id}, {global_group_check.info}') + raise IgnoredException('全局用户冷却中') + elif global_group_check.success() and global_group_check.result in [0, 2]: + pass + else: + logger.error(f'CoolDown | 全局群组冷却事件异常, user: {user_id}, {global_group_check}') + + # 处理插件冷却 + # 跳过由 got 等事件处理函数创建临时 matcher 避免冷却在命令交互中被不正常触发 + if matcher.temp: + return + + # 跳过未声明冷却的 matcher + if not matcher_cool_down: + return + + # 检查用户或群组是否有skip_cd权限, 跳过冷却检查, 处理不同bot权限 + self_bot = DBBot(self_qq=int(bot.self_id)) + skip_cd_auth_node = f'{plugin_name}.{PluginCoolDown.skip_auth_node}' + + # 检查用户 skip_cd 权限 + user_auth = DBAuth(self_bot=self_bot, auth_id=user_id, auth_type='user', auth_node=skip_cd_auth_node) + user_tag_res = await user_auth.tags_info() + if user_tag_res.result[0] == 1 and user_tag_res.result[1] == 0: + return + + # 检查群组 skip_cd 权限 + group_auth = DBAuth(self_bot=self_bot, auth_id=group_id, auth_type='group', auth_node=skip_cd_auth_node) + group_tag_res = await group_auth.tags_info() + if group_tag_res.result[0] == 1 and group_tag_res.result[1] == 0: + return + + # 所有处理的冷却以该类型声明的第一个为准 + user_cool_down_l = [x for x in matcher_cool_down if x.type == PluginCoolDown.user_type] + user_cool_down = user_cool_down_l[0] if user_cool_down_l else None + + group_cool_down_l = [x for x in matcher_cool_down if x.type == PluginCoolDown.group_type] + group_cool_down = group_cool_down_l[0] if group_cool_down_l else None + + # 先过一个群组 cd 检查, 避免用户无 cd 而群组有 cd 时还会再给用户加 cd 的情况 + if (group_cool_down is not None) and (group_id is not None): + check_group_cd = True + group_cool_down_check = await DBCoolDownEvent.check_group_cool_down_event(plugin=plugin_name, group_id=group_id) + if group_cool_down_check.success() and group_cool_down_check.result == 1: + # 群组现在有 cd, 跳过用户的 cd 检查 + check_user_cd = False + else: + check_user_cd = True + else: + # 无 group_id 或未声明群组冷却则不更新群组冷却 + check_group_cd = False + check_user_cd = True + if user_cool_down is None: + # 没有声明用户冷却的情况 + check_user_cd = False + + # 检查并处理插件用户冷却 + if check_user_cd: + user_cool_down_result = await PluginCoolDown.check_and_set_user_cool_down( + plugin=plugin_name, user_id=user_id, seconds=user_cool_down.cool_down_time) + if user_cool_down_result.success() and user_cool_down_result.result == 1: + await bot.send(event=event, message=f'该命令正在用户冷却中!\n{user_cool_down_result.info}', at_sender=True) + raise IgnoredException('插件用户冷却中') + elif user_cool_down_result.success() and user_cool_down_result.result == 0: + pass + else: + logger.error(f'CoolDown | 插件用户冷却事件异常, user: {user_id}, {user_cool_down_result}') + pass + + # 检查并处理插件群组冷却 + if check_group_cd: + group_cool_down_result = await PluginCoolDown.check_and_set_group_cool_down( + plugin=plugin_name, group_id=group_id, seconds=group_cool_down.cool_down_time) + if group_cool_down_result.success() and group_cool_down_result.result == 1: + await bot.send(event=event, message=f'该命令正在群组冷却中!\n{group_cool_down_result.info}', at_sender=True) + raise IgnoredException('插件群组冷却中') + elif group_cool_down_result.success() and group_cool_down_result.result == 0: + pass + else: + logger.error(f'CoolDown | 插件群组冷却事件异常, group:{group_id}, {group_cool_down_result}') + pass + + +__all__ = [ + 'preprocessor_cooldown' +] diff --git a/omega_miya/utils/omega_processor/favorability.py b/omega_miya/utils/omega_processor/favorability.py new file mode 100644 index 00000000..527af110 --- /dev/null +++ b/omega_miya/utils/omega_processor/favorability.py @@ -0,0 +1,54 @@ +""" +@Author : Ailitonia +@Date : 2021/08/28 20:33 +@FileName : favorability.py +@Project : nonebot2_miya +@Description : 好感度处理模块 +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +from nonebot import logger +from nonebot.typing import T_State +from nonebot.adapters.cqhttp.bot import Bot +from nonebot.adapters.cqhttp.event import MessageEvent, PrivateMessageEvent, GroupMessageEvent +from omega_miya.database import DBUser + + +async def postprocessor_favorability(bot: Bot, event: MessageEvent, state: T_State): + """ + 用户能量值及好感度处理 T_EventPostProcessor + """ + user = DBUser(user_id=event.user_id) + if isinstance(event, GroupMessageEvent): + result = await user.favorability_add(energy=1) + elif isinstance(event, PrivateMessageEvent): + result = await user.favorability_add(energy=0.01) + else: + return + + if result.error and result.info == 'NoResultFound': + if isinstance(event, GroupMessageEvent): + result = await user.favorability_reset(energy=1) + elif isinstance(event, PrivateMessageEvent): + result = await user.favorability_reset(energy=0.01) + elif result.error and result.info == 'User not exist': + user_add_result = await user.add(nickname=event.sender.nickname) + if user_add_result.error: + logger.error(f'Favorability | Add User {event.user_id} favorability-energy Failed, ' + f'add user to database failed, {user_add_result.info}') + return + if isinstance(event, GroupMessageEvent): + result = await user.favorability_reset(energy=1) + elif isinstance(event, PrivateMessageEvent): + result = await user.favorability_reset(energy=0.01) + + if result.error: + logger.error(f'Favorability | Add User {event.user_id} favorability-energy Failed, {result.info}') + else: + logger.debug(f'Favorability | Add User {event.user_id} favorability-energy Success, energy increased') + + +__all__ = [ + 'postprocessor_favorability' +] diff --git a/omega_miya/utils/Omega_processor/history.py b/omega_miya/utils/omega_processor/history.py similarity index 59% rename from omega_miya/utils/Omega_processor/history.py rename to omega_miya/utils/omega_processor/history.py index 4cefdf1c..84049f23 100644 --- a/omega_miya/utils/Omega_processor/history.py +++ b/omega_miya/utils/omega_processor/history.py @@ -13,10 +13,13 @@ from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import (Event, MessageEvent, PrivateMessageEvent, GroupMessageEvent, NoticeEvent, RequestEvent, MetaEvent) -from omega_miya.utils.Omega_Base import DBHistory +from omega_miya.database import DBHistory async def postprocessor_history(bot: Bot, event: Event, state: T_State): + """ + 历史记录处理 T_EventPostProcessor + """ try: time = event.time self_id = event.self_id @@ -40,40 +43,52 @@ async def postprocessor_history(bot: Bot, event: Event, state: T_State): msg_data = str(event.message) elif isinstance(event, NoticeEvent): detail_type = event.notice_type - sub_type = event.dict().get('sub_type', 'Undefined') + sub_type = getattr(event, 'sub_type', 'Undefined') event_id = -1 - group_id = event.dict().get('group_id', -1) - user_id = event.dict().get('user_id', -1) + group_id = getattr(event, 'group_id', -1) + user_id = getattr(event, 'user_id', -1) user_name = '' raw_data = repr(event) msg_data = '' elif isinstance(event, RequestEvent): detail_type = event.request_type - sub_type = event.dict().get('sub_type', 'Undefined') + sub_type = getattr(event, 'sub_type', 'Undefined') event_id = -1 - group_id = event.dict().get('group_id', -1) - user_id = event.dict().get('user_id', -1) + group_id = getattr(event, 'group_id', -1) + user_id = getattr(event, 'user_id', -1) user_name = '' raw_data = repr(event) msg_data = '' else: detail_type = event.get_event_name() - sub_type = event.dict().get('sub_type', 'Undefined') + sub_type = getattr(event, 'sub_type', 'Undefined') event_id = -1 - group_id = event.dict().get('group_id', -1) - user_id = event.dict().get('user_id', -1) + group_id = getattr(event, 'group_id', -1) + user_id = getattr(event, 'user_id', -1) user_name = '' raw_data = repr(event) - msg_data = str(event.dict().get('message')) + msg_data = getattr(event, 'message', None) + + raw_data = str(raw_data) if not isinstance(raw_data, str) else raw_data + msg_data = str(msg_data) if not isinstance(msg_data, str) else msg_data + if len(raw_data) > 4096: + logger.warning(f'History | Raw data is longer than field limited and it will be reduce, <{raw_data}>') + raw_data = raw_data[:4096] + if len(msg_data) > 4096: + logger.warning(f'History | Raw data is longer than field limited and it will be reduce, <{msg_data}>') + msg_data = msg_data[:4096] new_history = DBHistory(time=time, self_id=self_id, post_type=post_type, detail_type=detail_type) + # 处理event中可能出现部分字段存在但为None的情况 + group_id = -1 if group_id is None else group_id + user_id = -1 if user_id is None else user_id add_result = await new_history.add( sub_type=sub_type, event_id=event_id, group_id=group_id, user_id=user_id, user_name=user_name, raw_data=raw_data, msg_data=msg_data) if add_result.error: - logger.error(f'History recording failed with database error: {add_result.info}, event: {repr(event)}') + logger.error(f'History | Recording failed with database error: {add_result.info}, event: {repr(event)}') except Exception as e: - logger.error(f'History recording Failed, error: {repr(e)}, event: {repr(event)}') + logger.error(f'History | Recording Failed, error: {repr(e)}, event: {repr(event)}') __all__ = [ diff --git a/omega_miya/utils/Omega_processor/permission.py b/omega_miya/utils/omega_processor/permission.py similarity index 93% rename from omega_miya/utils/Omega_processor/permission.py rename to omega_miya/utils/omega_processor/permission.py index 4045d13a..07de7eff 100644 --- a/omega_miya/utils/Omega_processor/permission.py +++ b/omega_miya/utils/omega_processor/permission.py @@ -4,8 +4,8 @@ from nonebot.matcher import Matcher from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent, PrivateMessageEvent -from omega_miya.utils.Omega_plugin_utils import PermissionChecker -from omega_miya.utils.Omega_Base import DBBot +from omega_miya.utils.omega_plugin_utils import PermissionChecker +from omega_miya.database import DBBot global_config = get_driver().config @@ -17,6 +17,9 @@ async def preprocessor_permission(matcher: Matcher, bot: Bot, event: MessageEven 权限处理 T_RunPreProcessor """ + group_id = getattr(event, 'group_id', None) + user_id = event.user_id + if isinstance(event, PrivateMessageEvent): private_mode = True elif isinstance(event, GroupMessageEvent): @@ -24,9 +27,6 @@ async def preprocessor_permission(matcher: Matcher, bot: Bot, event: MessageEven else: private_mode = False - group_id = event.dict().get('group_id') - user_id = event.dict().get('user_id') - # 忽略超级用户 if user_id in [int(x) for x in SUPERUSERS]: return @@ -70,7 +70,7 @@ async def preprocessor_permission(matcher: Matcher, bot: Bot, event: MessageEven # 检查权限节点 if matcher_auth_node: - auth_node = '.'.join([matcher.module, matcher_auth_node]) + auth_node = '.'.join([matcher.module_name, matcher_auth_node]) # 分别检查用户及群组权限节点 user_auth_checker = await permission_checker.check_auth_node(auth_id=user_id, auth_type='user', diff --git a/omega_miya/utils/omega_processor/plugins.py b/omega_miya/utils/omega_processor/plugins.py new file mode 100644 index 00000000..d7c8197c --- /dev/null +++ b/omega_miya/utils/omega_processor/plugins.py @@ -0,0 +1,66 @@ +""" +@Author : Ailitonia +@Date : 2021/09/12 12:36 +@FileName : plugins.py +@Project : nonebot2_miya +@Description : 插件管理器相关组件 +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +from nonebot import get_driver, get_loaded_plugins, logger +from nonebot.exception import IgnoredException +from nonebot.typing import T_State +from nonebot.matcher import Matcher +from nonebot.adapters.cqhttp.bot import Bot +from nonebot.adapters.cqhttp.event import Event +from omega_miya.database import DBPlugin +from omega_miya.utils.omega_plugin_utils import ProcessUtils + + +global_config = get_driver().config +SUPERUSERS = global_config.superusers + + +async def startup_init_plugins(): + tasks = [DBPlugin(plugin_name=plugin.name).update( + matchers=len(plugin.matcher), info=plugin.export.get('custom_name') + ) for plugin in get_loaded_plugins()] + + plugins_init_result = await ProcessUtils.fragment_process(tasks=tasks, log_flag='Startup Init Plugins') + + for result in plugins_init_result: + if result.error: + import sys + logger.opt(colors=True).critical(f'初始化插件信息失败, {result.info}') + sys.exit(f'初始化插件信息失败, {result.info}') + + logger.opt(colors=True).success(f'插件信息初始化已完成.') + + +async def preprocessor_plugins_manager(matcher: Matcher, bot: Bot, event: Event, state: T_State): + """ + 插件管理处理 T_RunPreProcessor + """ + user_id = getattr(event, 'user_id', -1) + + # 忽略超级用户 + if user_id in [int(x) for x in SUPERUSERS]: + return + + plugin_name = matcher.plugin_name + plugin_enable_result = await DBPlugin(plugin_name=plugin_name).get_enabled_status() + if plugin_enable_result.success() and plugin_enable_result.result == 1: + pass + elif plugin_enable_result.success() and plugin_enable_result.result != 1: + logger.warning(f'Plugins Manager | User: {user_id}, 尝试使用未启用的插件: {plugin_name}') + raise IgnoredException('插件未启用') + else: + logger.error(f'Plugins Manager | 获取插件: {plugin_name} 启用状态失败, 插件状态异常, {plugin_enable_result.info}') + raise IgnoredException('插件状态异常') + + +__all__ = [ + 'startup_init_plugins', + 'preprocessor_plugins_manager' +] diff --git a/omega_miya/utils/omega_processor/rate_limiting.py b/omega_miya/utils/omega_processor/rate_limiting.py new file mode 100644 index 00000000..4fd131e0 --- /dev/null +++ b/omega_miya/utils/omega_processor/rate_limiting.py @@ -0,0 +1,148 @@ +""" +@Author : Ailitonia +@Date : 2021/09/12 0:09 +@FileName : rate_limiting.py +@Project : nonebot2_miya +@Description : 速率限制 +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +import time +from datetime import datetime, timedelta +from typing import Union, Dict +from nonebot import get_driver, logger +from nonebot.exception import IgnoredException +from nonebot.typing import T_State +from nonebot.adapters.cqhttp.bot import Bot +from nonebot.adapters.cqhttp.event import MessageEvent, GroupMessageEvent +from omega_miya.database import DBBot, DBBotGroup, DBCoolDownEvent + + +global_config = get_driver().config +SUPERUSERS = global_config.superusers + +# 速率限制次数阈值, 触发超过该次数后启用限制 +RATE_LIMITING_THRESHOLD: int = 5 +# 速率限制时间阈值, 判断连续消息触发的时间间隔小于该值, 单位为秒, 判断依据时间戳为标准 +RATE_LIMITING_TIME: float = 1.0 +# 启用后判断依据将以处理时的系统时间戳为准, 而不是上报 event 的时间戳 +USING_SYSTEM_TIMESTAMP: bool = False +# 记录用户上次消息的时间戳, 作为对比依据 +USER_LAST_MSG_TIME: Dict[int, Union[int, float]] = {} +# 记录用户消息在速率限制时间阈值内触发的次数 +RATE_LIMITING_COUNT: Dict[int, int] = {} +# 触发速率限制时为用户设置的全局冷却时间, 单位秒 +RATE_LIMITING_COOL_DOWN: int = 1800 +# 缓存的已被限制的用户id及到期时间, 避免多次查数据库 +RATE_LIMITING_USER_TEMP: Dict[int, datetime] = {} +# 群组流控配置冷却时间, 与 plugins.omega_rate_limiting 中配置保持一致 +GROUP_GLOBAL_CD_SETTING_NAME: str = 'group_global_cd' + + +async def preprocessor_rate_limiting(bot: Bot, event: MessageEvent, state: T_State): + """ + 速率限制处理 T_EventPreProcessor + """ + global USER_LAST_MSG_TIME + global RATE_LIMITING_COUNT + global RATE_LIMITING_USER_TEMP + + user_id = event.user_id + # 忽略超级用户 + if user_id in [int(x) for x in SUPERUSERS]: + return + + # 检测该用户是否已经被速率限制 + if RATE_LIMITING_USER_TEMP.get(user_id, datetime.now()) > datetime.now(): + logger.info(f'Rate Limiting | User: {user_id} 仍在速率限制中, 到期时间 {RATE_LIMITING_USER_TEMP.get(user_id)}') + raise IgnoredException('速率限制中') + + # 获取上条消息的时间戳 + last_msg_timestamp = USER_LAST_MSG_TIME.get(user_id, None) + + # 获取当前消息时间戳 + if USING_SYSTEM_TIMESTAMP: + this_msg_timestamp = time.time() + else: + this_msg_timestamp = event.time + + # 更新上次消息时间戳为本次消息时间戳 + if last_msg_timestamp is None: + USER_LAST_MSG_TIME.update({user_id: this_msg_timestamp}) + # 上次消息时间戳为空则这是第一条消息 + return + else: + USER_LAST_MSG_TIME.update({user_id: this_msg_timestamp}) + + # 获取速录限制触发计数 + over_limiting_count = RATE_LIMITING_COUNT.get(user_id, 0) + + # 进行速率判断 + if this_msg_timestamp - last_msg_timestamp < RATE_LIMITING_TIME: + # 小于时间阈值则计数 +1 + over_limiting_count += 1 + logger.info(f'Rate Limiting | User: {user_id} over rate limiting, counting {over_limiting_count}') + else: + # 否则重置计数 + over_limiting_count = 0 + + # 更新计数 + RATE_LIMITING_COUNT.update({user_id: over_limiting_count}) + + # 判断计数大于阈值则触发限制, 为用户设置一个全局冷却并重置计数 + if over_limiting_count > RATE_LIMITING_THRESHOLD: + RATE_LIMITING_USER_TEMP.update({user_id: datetime.now() + timedelta(seconds=RATE_LIMITING_COOL_DOWN)}) + logger.success(f'Rate Limiting | User: {user_id} 触发速率限制, Upgrade RATE_LIMITING_USER_TEMP completed') + + result = await DBCoolDownEvent.add_global_user_cool_down_event( + user_id=user_id, + stop_at=datetime.now() + timedelta(seconds=RATE_LIMITING_COOL_DOWN), + description='Rate Limiting CoolDown') + RATE_LIMITING_COUNT.update({user_id: 0}) + + if result.error: + logger.error(f'Rate Limiting | Set rate limiting cool down failed: {result.info}') + else: + logger.success(f'Rate Limiting | User: {user_id} 触发速率限制, 已设置 {RATE_LIMITING_COOL_DOWN} 秒全局冷却限制') + raise IgnoredException('触发速率限制') + + +async def postprocessor_rate_limiting(bot: Bot, event: MessageEvent, state: T_State): + """ + 速率限制事件后处理 T_EventPostProcessor + """ + if not state.get('_prefix', {}).get('raw_command') and not state.get('_suffix', {}).get('raw_command'): + # 限流仅能由命令触发 + logger.debug(f'Rate Limiting | Not command event, ignore global group cool down') + return + + self_bot = DBBot(self_qq=int(bot.self_id)) + if isinstance(event, GroupMessageEvent): + group_id = event.group_id + group = DBBotGroup(group_id=group_id, self_bot=self_bot) + setting_result = await group.setting_get(setting_name=GROUP_GLOBAL_CD_SETTING_NAME) + if setting_result.success(): + main, second, extra = setting_result.result + if main: + # 配置有效, 为群组设置一个全局冷却 + result = await DBCoolDownEvent.add_global_group_cool_down_event( + group_id=group_id, + stop_at=datetime.now() + timedelta(seconds=int(main)), + description='Rate Limiting Global Group CoolDown') + if result.error: + logger.error(f'Rate Limiting | Set global group cool down failed: {result.info}') + else: + logger.debug(f'Rate Limiting | Set Group: {group_id} global group cool down, times: {main}') + else: + logger.error(f'Rate Limiting | Group: {group_id} global group cool down setting not found') + elif setting_result.error and setting_result.info == 'NoResultFound': + logger.debug(f'Rate Limiting | Group: {group_id} not set global group cool down, pass') + else: + logger.error(f'Rate Limiting | Set global group cool down getting setting failed: {setting_result.info}') + + +__all__ = [ + 'preprocessor_rate_limiting', + 'postprocessor_rate_limiting' +] diff --git a/omega_miya/utils/omega_processor/statistic.py b/omega_miya/utils/omega_processor/statistic.py new file mode 100644 index 00000000..9cf0f3c9 --- /dev/null +++ b/omega_miya/utils/omega_processor/statistic.py @@ -0,0 +1,63 @@ +""" +@Author : Ailitonia +@Date : 2021/08/14 18:26 +@FileName : statistic.py +@Project : nonebot2_miya +@Description : 插件调用统计 +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +from datetime import datetime +from typing import Optional +from nonebot import logger, get_plugin +from nonebot.typing import T_State +from nonebot.matcher import Matcher +from nonebot.adapters.cqhttp.event import Event +from nonebot.adapters.cqhttp.bot import Bot +from omega_miya.database import DBStatistic + + +async def postprocessor_statistic( + matcher: Matcher, exception: Optional[Exception], bot: Bot, event: Event, state: T_State): + """ + 插件统计处理 T_RunPostProcessor + """ + if matcher.temp: + logger.debug('Postprocessor Statistic | Temp matcher, ignore') + return + elif matcher.priority > 50: + logger.debug('Postprocessor Statistic | Non-command matcher, ignore') + return + + plugin_name = get_plugin(matcher.plugin_name).export.get('custom_name', None) + if not plugin_name: + logger.debug('Postprocessor Statistic | Not custom plugin name, ignore') + return + + module_name = matcher.module_name + self_bot_id = int(bot.self_id) + group_id = getattr(event, 'group_id', -1) + user_id = getattr(event, 'user_id', -1) + if exception: + info = repr(exception) + else: + info = 'Success' + raw_message = str(getattr(event, 'message', '')) + if len(raw_message) >= 4096: + raw_message = raw_message[:4096] + + add_statistic_result = await DBStatistic(self_bot_id=self_bot_id).add( + module_name=module_name, plugin_name=plugin_name, group_id=group_id, user_id=user_id, + using_datetime=datetime.now(), raw_message=raw_message, info=info + ) + + if add_statistic_result.error: + logger.error(f'Postprocessor Statistic | Adding statistic failed, error: {add_statistic_result.info}') + else: + logger.debug('Postprocessor Statistic | Adding succeed') + + +__all__ = [ + 'postprocessor_statistic' +] diff --git a/omega_miya/utils/pixiv_utils/pixiv.py b/omega_miya/utils/pixiv_utils/pixiv.py index 589f73ff..4c23f4f4 100644 --- a/omega_miya/utils/pixiv_utils/pixiv.py +++ b/omega_miya/utils/pixiv_utils/pixiv.py @@ -6,11 +6,13 @@ import aiofiles import zipfile import imageio +from datetime import datetime, timedelta from io import BytesIO from typing import Dict, Optional +from urllib.parse import quote from nonebot import logger, get_driver -from omega_miya.utils.Omega_plugin_utils import HttpFetcher, PicEncoder, create_zip_file -from omega_miya.utils.Omega_Base import Result +from omega_miya.utils.omega_plugin_utils import HttpFetcher, PicEncoder, create_zip_file +from omega_miya.database import Result global_config = get_driver().config TMP_PATH = global_config.tmp_path_ @@ -23,9 +25,11 @@ class Pixiv(object): - ILLUST_DATA_URL = 'https://www.pixiv.net/ajax/illust/' + PIXIV_API_URL = 'https://www.pixiv.net/ajax/' + SEARCH_URL = f'{PIXIV_API_URL}search/' + ILLUST_DATA_URL = f'{PIXIV_API_URL}illust/' ILLUST_ARTWORK_URL = 'https://www.pixiv.net/artworks/' - RANKING_URL = 'http://www.pixiv.net/ranking.php' + RANKING_URL = 'https://www.pixiv.net/ranking.php' HEADERS = {'accept': '*/*', 'accept-encoding': 'gzip, deflate', @@ -106,6 +110,138 @@ async def get_ranking( continue return Result.DictResult(error=False, info='Success', result=result) + @classmethod + async def search_raw( + cls, + word: str, + mode: str = 'artworks', + *, + order: str = 'date_d', + page: int = 1, + mode_: str = 'all', + s_mode_: str = 's_tag', + type_: str = 'illust_and_ugoira', + ratio_: Optional[float] = None, + scd_: Optional[datetime] = None, + blt_: Optional[int] = None, + bgt_: Optional[int] = None, + lang_: str = 'zh' + ) -> Result.DictResult: + """ + :param word: 搜索内容 + :param mode: 作品类型, artworks: 插画·漫画, top: 全部作品, illustrations: 插画, manga: 漫画, novels: 小说 + :param page: 解析搜索结果页码 + :param order: 排序模式(部分参数仅限pixiv高级会员可用), date_d: 按最新排序, date: 按旧排序, popular_d: 受全站欢迎, popular_male_d: 受男性欢迎, popular_female_d: 受女性欢迎 + :param mode_: 搜索参数(部分参数可能仅限pixiv高级会员可用), all: 全部, safe: 全年龄, r18: R-18, 最好不要动这个 + :param s_mode_: 检索标签模式(部分参数可能仅限pixiv高级会员可用), s_tag: 标签(部分一致), s_tag_full: 标签(完全一致), s_tc: 标题、说明文字, 最好不要动这个 + :param type_: 筛选检索范围(部分参数可能仅限pixiv高级会员可用), all: 插画、漫画、动图(动态插画), illust_and_ugoira: 插画、动图, illust: 插画, manga: 漫画. ugoira: 动图, 最好不要动这个 + :param ratio_: 筛选纵横比(部分参数可能仅限pixiv高级会员可用), 0.5: 横图, -0.5: 纵图, 0: 正方形图, 最好不要动这个 + :param scd_: 筛选时间(参数仅限pixiv高级会员可用), 从什么时候开始, 最好不要动这个 + :param blt_: 筛选收藏数下限(参数仅限pixiv高级会员可用), 最好不要动这个 + :param bgt_: 筛选收藏数上限(参数仅限pixiv高级会员可用), 最好不要动这个 + :param lang_: 搜索语言, 不要动这个 + :return: dict, 原始返回数据 + """ + if not COOKIES: + return Result.DictResult( + error=True, info='Cookies not configured, some order modes not supported in searching', result={}) + else: + word = quote(word, encoding='utf-8') + params = { + 'word': word, + 'order': order, + 'mode': mode_, + 'p': page, + 's_mode': s_mode_, + 'type': type_, + 'lang': lang_ + } + if ratio_: + params.update({ + 'ratio': ratio_ + }) + if scd_: + scd_str = scd_.strftime('%Y-%m-%d') + params.update({ + 'scd': scd_str + }) + if blt_: + params.update({ + 'blt': blt_ + }) + if bgt_: + params.update({ + 'bgt': bgt_ + }) + url = f'{cls.SEARCH_URL}{mode}/{word}' + fetcher = HttpFetcher(timeout=10, flag='pixiv_search_raw', headers=cls.HEADERS, cookies=COOKIES) + search_search_result = await fetcher.get_json(url=url, params=params) + + if search_search_result.error: + return Result.DictResult( + error=True, info=f'Getting searching data failed, {search_search_result.info}', result={}) + + # 检查返回状态 + if search_search_result.result.get('error') or not isinstance(search_search_result.result.get('body'), dict): + return Result.DictResult(error=True, info=f'PixivApiError: {search_search_result.result}', result={}) + + return Result.DictResult(error=False, info='Success', result=search_search_result.result.get('body')) + + @classmethod + async def search_artwork( + cls, + word: str, + popular_order: bool = True, + *, + near_year: bool = False, + nsfw: bool = False, + page: int = 1 + ) -> Result.DictListResult: + """ + :param word: 搜索内容 + :param popular_order: 是否按热度排序 + :param near_year: 是否筛选近一年的作品 + :param nsfw: 是否允许nsfw内容 + :param page: 解析搜索结果页码 + :return: List[dict], 作品信息列表 + """ + kwarg = { + 'word': word, + 'page': page, + } + if popular_order: + kwarg.update({ + 'order': 'popular_d' + }) + if near_year: + last_year_today = datetime.now() - timedelta(days=365) + kwarg.update({ + 'scd_': last_year_today + }) + if nsfw: + kwarg.update({ + 'mode_': 'all' + }) + else: + kwarg.update({ + 'mode_': 'safe' + }) + search_result = await cls.search_raw(**kwarg) + if search_result.error: + return Result.DictListResult(error=True, info=search_result.info, result=[]) + + try: + result = [{ + 'pid': x.get('id'), + 'title': x.get('title'), + 'author': x.get('userName'), + 'thumb_url': x.get('url'), + } for x in search_result.result['illustManga']['data']] + except Exception as e: + return Result.DictListResult(error=True, info=f'Parse search result failed, error: {repr(e)}', result=[]) + + return Result.DictListResult(error=False, info='Success', result=result) + class PixivIllust(Pixiv): def __init__(self, pid: int): diff --git a/omega_miya/utils/pixiv_utils/pixivision.py b/omega_miya/utils/pixiv_utils/pixivision.py index 3815a8a1..c96ad4ed 100644 --- a/omega_miya/utils/pixiv_utils/pixivision.py +++ b/omega_miya/utils/pixiv_utils/pixivision.py @@ -1,8 +1,8 @@ import re from bs4 import BeautifulSoup from nonebot import logger -from omega_miya.utils.Omega_plugin_utils import HttpFetcher -from omega_miya.utils.Omega_Base import Result +from omega_miya.utils.omega_plugin_utils import HttpFetcher +from omega_miya.database import Result class Pixivision(object): diff --git a/omega_miya/utils/tencent_cloud_api/cloud_api.py b/omega_miya/utils/tencent_cloud_api/cloud_api.py index 6d4c156d..8e4ffa23 100644 --- a/omega_miya/utils/tencent_cloud_api/cloud_api.py +++ b/omega_miya/utils/tencent_cloud_api/cloud_api.py @@ -4,8 +4,8 @@ import datetime from typing import Dict, Any import nonebot -from omega_miya.utils.Omega_Base import Result -from omega_miya.utils.Omega_plugin_utils import HttpFetcher +from omega_miya.database import Result +from omega_miya.utils.omega_plugin_utils import HttpFetcher global_config = nonebot.get_driver().config SECRET_ID = global_config.secret_id diff --git a/omega_miya/utils/tencent_cloud_api/nlp.py b/omega_miya/utils/tencent_cloud_api/nlp.py index 09ca8643..20db7fad 100644 --- a/omega_miya/utils/tencent_cloud_api/nlp.py +++ b/omega_miya/utils/tencent_cloud_api/nlp.py @@ -1,7 +1,7 @@ import re import json from typing import Optional -from omega_miya.utils.Omega_Base import Result +from omega_miya.database import Result from .cloud_api import SECRET_ID, SECRET_KEY, TencentCloudApi @@ -201,6 +201,64 @@ async def participle_and_tagging( else: return Result.TupleListResult(error=True, info=result.info, result=[]) + async def text_correction_(self, text: str) -> Result.DictResult: + """ + 文本纠错 + :param text: 待纠错的文本(仅支持UTF-8格式, 不超过2000字符) + :return: DictResult + CCITokens: Array of CCIToken, 纠错详情, 注意:此字段可能返回 null, 表示取不到有效值。 + Word: str, 错别字内容 + BeginOffset: int, 错别字的起始位置,从0开始 + CorrectWord: str, 错别字纠错结果 + ResultText: str, 纠错后的文本 + RequestId: str, 唯一请求 ID, 每次请求都会返回。定位问题时需要提供该次请求的 RequestId。 + """ + payload = {'Text': text} + api = TencentCloudApi( + secret_id=self.__secret_id, + secret_key=self.__secret_key, + host='nlp.tencentcloudapi.com') + result = await api.post_request( + action='TextCorrection', version='2019-04-08', region='ap-guangzhou', payload=payload) + + if result.success(): + if result.result['Response'].get('Error'): + return Result.DictResult( + error=True, info=f"API error: {result.result['Response'].get('Error')}", result={}) + else: + response = dict(result.result['Response']) + return Result.DictResult(error=False, info='Success', result=response) + else: + return Result.DictResult(error=True, info=result.info, result={}) + + async def text_correction(self, text: str) -> Result.DictResult: + """ + :param text: 待纠错的文本(仅支持UTF-8格式, 不超过2000字符) + :return: DictResult + HasCorrection: bool, 是否存在错误 + ResultText: str, 纠错后的文本 + Correction: List[Tuple[int, str, str]], (错别字的起始位置, 错别字内容, 错别字纠错结果) + """ + text_correction_result = await self.text_correction_(text=text) + if text_correction_result.success(): + corrections = text_correction_result.result.get('CCITokens') + if not corrections: + result = { + 'HasCorrection': False, + 'ResultText': text, + 'Correction': [] + } + else: + correction_list = [(x.get('BeginOffset'), x.get('Word'), x.get('CorrectWord')) for x in corrections] + result = { + 'HasCorrection': True, + 'ResultText': text_correction_result.result.get('ResultText'), + 'Correction': correction_list + } + return Result.DictResult(error=False, info='Success', result=result) + else: + return Result.DictResult(error=True, info=text_correction_result.info, result={}) + __all__ = [ 'TencentNLP' diff --git a/omega_miya/utils/tencent_cloud_api/tmt.py b/omega_miya/utils/tencent_cloud_api/tmt.py index 67476588..a87d0825 100644 --- a/omega_miya/utils/tencent_cloud_api/tmt.py +++ b/omega_miya/utils/tencent_cloud_api/tmt.py @@ -8,7 +8,7 @@ @Software : PyCharm """ -from omega_miya.utils.Omega_Base import Result +from omega_miya.database import Result from .cloud_api import SECRET_ID, SECRET_KEY, TencentCloudApi diff --git a/omega_miya/utils/text_to_img/__init__.py b/omega_miya/utils/text_to_img/__init__.py deleted file mode 100644 index 85a82d85..00000000 --- a/omega_miya/utils/text_to_img/__init__.py +++ /dev/null @@ -1,73 +0,0 @@ -import os -import asyncio -from datetime import datetime -from nonebot import logger, get_driver -from PIL import Image, ImageDraw, ImageFont -from omega_miya.utils.Omega_Base import Result - -global_config = get_driver().config -TMP_PATH = global_config.tmp_path_ -FOLDER_PATH = os.path.abspath(os.path.dirname(__file__)) - - -def __text_to_img(text: str, image_wight: int = 512) -> Image: - font_path = os.path.abspath(os.path.join(FOLDER_PATH, 'default_font.otf')) - if not os.path.exists(font_path): - raise ValueError('Font not found') - - # 处理文字层 主体部分 - font_main_size = image_wight // 25 - font_main = ImageFont.truetype(font_path, font_main_size) - # 按长度切分文本 - spl_num = 0 - spl_list = [] - for num in range(len(text)): - text_w = font_main.getsize_multiline(text[spl_num:num])[0] - if text_w >= image_wight * 0.78: - spl_list.append(text[spl_num:num]) - spl_num = num - else: - spl_list.append(text[spl_num:]) - test_main_fin = '\n' + '\n'.join(spl_list) + '\n' - - # 绘制文字图层 - text_w, text_h = font_main.getsize_multiline(test_main_fin) - text_main_img = Image.new(mode="RGBA", size=(text_w, text_h), color=(0, 0, 0, 0)) - ImageDraw.Draw(text_main_img).multiline_text(xy=(0, 0), text=test_main_fin, font=font_main, fill=(0, 0, 0)) - - # 初始化背景图层 - image_height = text_h + 100 - background = Image.new(mode="RGB", size=(image_wight, image_height), color=(255, 255, 255)) - - # 向背景图层中置入文字图层 - background.paste(im=text_main_img, box=(image_wight // 10, 50), mask=text_main_img) - - return background - - -async def text_to_img(text: str, image_wight: int = 512) -> Result.TextResult: - def __handle(): - byte_img = __text_to_img(text, image_wight) - # 检查生成图片路径 - img_folder_path = os.path.abspath(os.path.join(TMP_PATH, 'text_to_img')) - if not os.path.exists(img_folder_path): - os.makedirs(img_folder_path) - img_path = os.path.abspath( - os.path.join(img_folder_path, f"{hash(text)}_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.jpg")) - # 保存图片 - byte_img.save(img_path, 'JPEG') - return img_path - - loop = asyncio.get_running_loop() - try: - path_result = await loop.run_in_executor(None, __handle) - path = os.path.abspath(path_result) - return Result.TextResult(error=False, info='Success', result=path) - except Exception as e: - logger.error(f'text_to_img failed, error: {repr(e)}') - return Result.TextResult(error=True, info=repr(e), result='') - - -__all__ = [ - 'text_to_img' -] diff --git a/omega_miya/utils/text_to_img/default_font.otf b/omega_miya/utils/text_to_img/default_font.otf deleted file mode 100644 index 886f82f8..00000000 Binary files a/omega_miya/utils/text_to_img/default_font.otf and /dev/null differ diff --git a/requirements.txt b/requirements.txt index 71e3407d..edd087f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,22 @@ -nonebot2==2.0.0a13.post1 -nonebot-adapter-cqhttp==2.0.0a13 -SQLAlchemy~=1.4.22 +nonebot2==2.0.0a16 +nonebot-adapter-cqhttp==2.0.0a16 +SQLAlchemy~=1.4.25 mysqlclient~=2.0.3 aiomysql~=0.0.21 -aiocqhttp~=1.4.1 -aiofiles==0.6.0 +aiocqhttp~=1.4.2 +aiofiles==0.7.0 bs4~=0.0.1 lxml~=4.6.3 -numpy~=1.21.1 -Pillow~=8.3.1 +numpy~=1.21.2 +matplotlib~=3.4.3 +Pillow~=8.3.2 imageio~=2.9.0 -beautifulsoup4~=4.9.3 +beautifulsoup4~=4.10.0 aiohttp~=3.7.4.post0 xlwt~=1.3.0 -ujson~=4.0.2 +ujson~=4.2.0 msgpack~=1.0.2 pydantic~=1.8.2 -APScheduler~=3.7.0 -pycryptodome~=3.10.1 +APScheduler~=3.8.0 +pycryptodome~=3.10.4 py7zr==0.16.1 \ No newline at end of file diff --git a/test/Bilibili_utils_test.py b/test/Bilibili_utils_test.py new file mode 100644 index 00000000..b30926b6 --- /dev/null +++ b/test/Bilibili_utils_test.py @@ -0,0 +1,25 @@ +""" +@Author : Ailitonia +@Date : 2021/05/29 0:05 +@FileName : Bilibili_utils_test.py +@Project : nonebot2_miya +@Description : Bilibili Utils test +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +from omega_miya.utils.bilibili_utils import BiliDynamic, BiliUser +import nonebot + + +async def test(): + bu = BiliUser(user_id=1617739681) + # dy_his = await bu.get_dynamic_history() + bd_info = await BiliDynamic(dynamic_id=529075172698301430).get_info() + print(BiliDynamic.data_parser(dynamic_data=bd_info.result)) + + bd_info = await BiliDynamic(dynamic_id=529074691660911614).get_info() + print(BiliDynamic.data_parser(dynamic_data=bd_info.result)) + + +nonebot.get_driver().on_startup(test) diff --git a/test/PicEffector_test.py b/test/PicEffector_test.py new file mode 100644 index 00000000..ebbcf3d4 --- /dev/null +++ b/test/PicEffector_test.py @@ -0,0 +1,24 @@ +""" +@Author : Ailitonia +@Date : 2021/06/03 21:14 +@FileName : PicEffector_test.py +@Project : nonebot2_miya +@Description : +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +import aiofiles +import nonebot +from omega_miya.utils.omega_plugin_utils import PicEffector + + +async def test(): + async with aiofiles.open('D:\\78486250_p0.png', 'rb') as af: + image = await af.read() + effector = PicEffector(image=image) + async with aiofiles.open('D:\\out_put.png', 'wb+') as af2: + await af2.write((await effector.gaussian_blur()).result) + + +nonebot.get_driver().on_startup(test) diff --git a/test/get_pixiv_recommend.py b/test/get_pixiv_recommend.py new file mode 100644 index 00000000..3524e69d --- /dev/null +++ b/test/get_pixiv_recommend.py @@ -0,0 +1,96 @@ +""" +@Author : Ailitonia +@Date : 2021/07/30 20:35 +@FileName : get_pixiv_recommend.py +@Project : nonebot2_miya +@Description : +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +import re +from typing import Optional +from nonebot import get_driver, logger +from omega_miya.database import DBPixivillust, Result +from omega_miya.utils.pixiv_utils import PixivIllust +from omega_miya.utils.omega_plugin_utils import ProcessUtils + + +driver = get_driver() + + +async def add_illust( + pid: int, nsfw_tag: int, + *, + force_tag: bool = False, illust_data: Optional[Result.DictResult] = None) -> Result.IntResult: + if not illust_data: + illust_result = await PixivIllust(pid=pid).get_illust_data() + else: + illust_result = illust_data + + if illust_result.success(): + illust_data = illust_result.result + title = illust_data.get('title') + uid = illust_data.get('uid') + uname = illust_data.get('uname') + url = illust_data.get('url') + width = illust_data.get('width') + height = illust_data.get('height') + tags = illust_data.get('tags') + is_r18 = illust_data.get('is_r18') + illust_pages = illust_data.get('illust_pages') + + if is_r18: + nsfw_tag = 2 + + illust = DBPixivillust(pid=pid) + illust_add_result = await illust.add(uid=uid, title=title, uname=uname, nsfw_tag=nsfw_tag, + width=width, height=height, tags=tags, url=url, force_tag=force_tag) + if illust_add_result.error: + logger.error(f'Setu | add_illust failed: {illust_add_result.info}') + return illust_add_result + + for page, urls in illust_pages.items(): + original = urls.get('original') + regular = urls.get('regular') + small = urls.get('small') + thumb_mini = urls.get('thumb_mini') + page_upgrade_result = await illust.upgrade_page( + page=page, original=original, regular=regular, small=small, thumb_mini=thumb_mini) + if page_upgrade_result.error: + logger.warning(f'Setu | upgrade illust page {page} failed: {page_upgrade_result.info}') + return illust_add_result + else: + return Result.IntResult(error=True, info=illust_result.info, result=-1) + + +async def test_init(): + with open(r'C:\Users\ailitonia\Desktop\pids.txt', 'r') as f: + lines = f.readlines() + pids = list(set([int(x.strip()) for x in lines if re.match(r'^[0-9]+$', x)])) + pids.sort() + tasks = [add_illust(pid, nsfw_tag=0) for pid in pids] + await ProcessUtils.fragment_process(tasks=tasks, fragment_size=50) + + +async def add_recommend(pid: int): + recommend_result = await PixivIllust(pid=pid).get_recommend(init_limit=180) + pid_list = [x.get('id') for x in recommend_result.result.get('illusts') if x.get('illustType') == 0] + tasks = [PixivIllust(pid=x).get_illust_data() for x in pid_list] + recommend_illust_data_result = await ProcessUtils.fragment_process(tasks=tasks, fragment_size=50) + filtered_illust_pids = [x for x in recommend_illust_data_result if ( + x.success() and + 6666 <= x.result.get('bookmark_count') <= 2 * x.result.get('like_count') and + x.result.get('view_count') <= 20 * x.result.get('like_count') + )] + tasks = [add_illust(x.result.get('pid'), nsfw_tag=0, illust_data=x) for x in filtered_illust_pids] + await ProcessUtils.fragment_process(tasks=tasks, fragment_size=50) + + +async def test(): + all_illust_result = await DBPixivillust.list_all_illust() + tasks = [add_recommend(pid) for pid in all_illust_result.result] + await ProcessUtils.fragment_process(tasks=tasks, fragment_size=2) + + +# driver.on_startup(test) diff --git a/test/init_all_auth_node.py b/test/init_all_auth_node.py index 4130be7f..6e13c1b6 100644 --- a/test/init_all_auth_node.py +++ b/test/init_all_auth_node.py @@ -1,5 +1,5 @@ -from omega_miya.plugins.Omega_manager import init_group_auth_node, init_user_auth_node -from omega_miya.utils.Omega_Base import DBBot, DBFriend, DBBotGroup +from omega_miya.plugins.omega_manager import init_group_auth_node, init_user_auth_node +from omega_miya.database import DBBot, DBFriend, DBBotGroup from nonebot.adapters.cqhttp.bot import Bot import nonebot @@ -13,7 +13,7 @@ async def init_all_auth_node(bot: Bot): for user_id in all_friends.result: if user_id in [123456789]: continue - await init_user_auth_node(user_id=int(user_id)) + await init_user_auth_node(user_id=int(user_id), self_bot=self_bot) print(f'Init_user_auth_node completed, user: {user_id}') group_res = await DBBotGroup.list_exist_bot_groups(self_bot=self_bot) @@ -21,6 +21,6 @@ async def init_all_auth_node(bot: Bot): for group_id in all_groups: if group_id in [987654321]: continue - await init_group_auth_node(group_id=int(group_id)) + await init_group_auth_node(group_id=int(group_id), self_bot=self_bot) print(f'Init_group_auth_node completed, group: {group_id}') diff --git a/test/multi_bot_test.py b/test/multi_bot_test.py new file mode 100644 index 00000000..9570ba3c --- /dev/null +++ b/test/multi_bot_test.py @@ -0,0 +1,51 @@ +""" +@Author : Ailitonia +@Date : 2021/05/29 11:27 +@FileName : multi_bot_test.py +@Project : nonebot2_miya +@Description : +@GitHub : https://github.com/Ailitonia +@Software : PyCharm +""" + +from nonebot import on_command +from nonebot.typing import T_State +from nonebot.matcher import Matcher +from nonebot.message import run_preprocessor +from nonebot.permission import Permission +from nonebot.permission import SUPERUSER +from nonebot.adapters.cqhttp.bot import Bot +from nonebot.adapters.cqhttp.event import Event, MessageEvent + + +permission_count = 0 + + +@run_preprocessor +async def _permission_checker(matcher: Matcher, bot: Bot, event: MessageEvent, state: T_State): + print('==>> _permission_checker progressing, permission.checkers now is: ', matcher.permission.checkers) + + +test = on_command('test', permission=SUPERUSER, priority=10, block=True) + + +@test.permission_updater +async def _permission_updater(bot: Bot, event: Event, state: T_State, permission: Permission): + global permission_count + print('==>> _permission_updater progressing, permission.checkers now is: ', permission.checkers) + print('==>> permission_count is: ', permission_count) + permission_count += 1 + + async def _new_permission(_bot: Bot, _event: Event) -> bool: + if permission_count > 3: + return False and await permission(bot, event) + else: + return True and await permission(bot, event) + return Permission(_new_permission) + + +@test.got('sub_command', prompt='sub_command?') +async def _handle(bot: Bot, event: MessageEvent, state: T_State): + sub_command = state['sub_command'] + msg = f'Last msg: {sub_command}, sending anything to continue...' + await test.reject(msg) diff --git a/test/pixiv_illust_updater.py b/test/pixiv_illust_updater.py index 693c7df9..4689e394 100644 --- a/test/pixiv_illust_updater.py +++ b/test/pixiv_illust_updater.py @@ -16,7 +16,7 @@ from nonebot.typing import T_State from nonebot.adapters.cqhttp.bot import Bot from nonebot.adapters.cqhttp.event import MessageEvent -from omega_miya.utils.Omega_Base import DBPixivillust, Result +from omega_miya.database import DBPixivillust, Result from omega_miya.utils.pixiv_utils import PixivIllust @@ -32,6 +32,8 @@ async def add_illust(pid: int, nsfw_tag: int) -> Result.IntResult: uid = illust_data.get('uid') uname = illust_data.get('uname') url = illust_data.get('url') + width = illust_data.get('width') + height = illust_data.get('height') tags = illust_data.get('tags') is_r18 = illust_data.get('is_r18') illust_pages = illust_data.get('illust_pages') @@ -40,7 +42,8 @@ async def add_illust(pid: int, nsfw_tag: int) -> Result.IntResult: nsfw_tag = 2 illust = DBPixivillust(pid=pid) - illust_add_result = await illust.add(uid=uid, title=title, uname=uname, nsfw_tag=nsfw_tag, tags=tags, url=url) + illust_add_result = await illust.add(uid=uid, title=title, uname=uname, nsfw_tag=nsfw_tag, + width=width, height=height, tags=tags, url=url) if illust_add_result.error: logger.error(f'Add illust failed: {illust_add_result.info}') return Result.IntResult(error=True, info=illust_add_result.info, result=pid)