From 8f424bf0d49afc5d165cc7fcaa6d06300a327594 Mon Sep 17 00:00:00 2001 From: Karako <70872201+karakoo@users.noreply.github.com> Date: Thu, 8 Sep 2022 09:08:37 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=20=E6=9B=B4=E6=96=B0V3=E7=89=88?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ 重构插件系统 ⚙️ 重写插件 🎨 改进代码结构 📝 完善文档 Co-authored-by: zhxy-CN Co-authored-by: 洛水居室 Co-authored-by: xtaodada Co-authored-by: Li Chuangbo --- .gitignore | 4 + README.md | 3 +- alembic/env.py | 46 +- config.py | 64 - core/README.md | 22 + core/admin/__init__.py | 14 +- core/admin/cache.py | 2 +- core/admin/repositories.py | 4 +- core/admin/services.py | 10 +- core/base/__init__.py | 0 core/base/aiobrowser.py | 41 + core/base/mysql.py | 30 + core/base/redisdb.py | 44 + core/baseplugin.py | 48 + core/bot.py | 256 +++ core/config.py | 88 + core/cookies/__init__.py | 16 +- core/cookies/cache.py | 34 +- core/cookies/error.py | 18 +- core/cookies/repositories.py | 27 +- core/cookies/services.py | 51 +- core/error.py | 8 + core/game/__init__.py | 10 +- core/game/cache.py | 2 +- core/game/services.py | 2 +- core/plugin.py | 423 ++++ core/quiz/__init__.py | 8 +- core/quiz/base.py | 3 +- core/quiz/cache.py | 4 +- core/quiz/models.py | 4 +- core/quiz/repositories.py | 4 +- core/quiz/services.py | 2 +- core/service.py | 30 + core/sign/__init__.py | 6 +- core/sign/repositories.py | 4 +- core/template/__init__.py | 6 +- core/template/services.py | 30 +- core/user/__init__.py | 6 +- core/user/error.py | 3 + core/user/models.py | 2 +- core/user/repositories.py | 13 +- core/wiki/__init__.py | 6 +- core/wiki/cache.py | 4 +- core/wiki/services.py | 26 +- jobs/README.md | 35 - jobs/base.py | 13 - jobs/public_cookies.py | 26 - logger.py | 61 - main.py | 97 - metadata/README.md | 6 +- models/apihelper/metadata/CharactersMap.json | 1725 ----------------- .../apihelper/metadata/NameTextMapHash.json | 387 ---- .../apihelper/metadata/ReliquaryNameMap.json | 31 - models/apihelper/playercards.py | 156 -- models/game/artifact.py | 51 - models/game/character.py | 123 -- models/game/fetter.py | 15 - models/game/skill.py | 21 - models/game/talent.py | 19 - models/game/weapon.py | 36 - models/types.py | 5 - {models => modules}/README.md | 8 +- {models => modules}/apihelper/artifact.py | 2 +- {models => modules}/apihelper/base.py | 0 {models => modules}/apihelper/gacha.py | 2 +- {models => modules}/apihelper/helpers.py | 2 +- {models => modules}/apihelper/hoyolab.py | 4 +- {models => modules}/apihelper/hyperion.py | 4 +- modules/playercards/models/talent.py | 8 + {models => modules}/wiki/base.py | 17 +- {models => modules}/wiki/character.py | 8 +- {models => modules}/wiki/material.py | 2 +- .../wiki/metadata/ascension.json | 0 {models => modules}/wiki/metadata/elite.json | 0 .../wiki/metadata/monster.json | 0 .../wiki/metadata/weapon_level.json | 0 {models => modules}/wiki/other.py | 2 +- {models => modules}/wiki/weapon.py | 4 +- plugins/README.md | 167 +- plugins/base.py | 97 - plugins/genshin/abyss.py | 41 +- plugins/genshin/adduser.py | 192 +- plugins/genshin/artifact_rate.py | 51 +- plugins/genshin/daily_note.py | 32 +- plugins/genshin/gacha/__init__.py | 2 - plugins/genshin/gacha/gacha.py | 31 +- plugins/genshin/help.py | 41 +- plugins/genshin/hilichurls.py | 26 +- plugins/genshin/ledger.py | 34 +- plugins/genshin/map/__init__.py | 2 - plugins/genshin/map/map.py | 25 +- plugins/genshin/material.py | 27 +- plugins/genshin/post.py | 116 +- plugins/genshin/quiz.py | 280 +-- plugins/genshin/set_uid.py | 178 ++ plugins/genshin/sign.py | 53 +- plugins/genshin/strategy.py | 36 +- plugins/genshin/userstats.py | 78 +- plugins/genshin/weapon.py | 41 +- plugins/genshin/wiki.py | 20 +- plugins/jobs/public_cookies.py | 19 + {jobs => plugins/jobs}/sign.py | 47 +- plugins/system/admin.py | 31 +- plugins/system/auth.py | 97 +- plugins/system/errorhandler.py | 130 +- plugins/system/inline.py | 14 +- plugins/system/new_member.py | 37 + plugins/system/set_quiz.py | 245 +++ plugins/system/start.py | 61 +- poetry.lock | 84 +- pyproject.toml | 2 + resources/genshin/abyss/abyss.html | 6 +- resources/genshin/abyss/example.html | 18 +- run.py | 9 + .../model/apihelper/test_artifact.py | 2 +- {test => tests}/model/wiki/test_wiki.py | 6 +- {test => tests}/run.py | 0 {test => tests}/service/test_game.py | 2 +- utils/aiobrowser.py | 50 - {models => utils}/baseobject.py | 2 +- utils/const.py | 14 + utils/decorators/admins.py | 29 +- utils/decorators/error.py | 79 +- utils/decorators/restricts.py | 25 +- utils/helpers.py | 56 +- utils/job/manager.py | 44 - utils/job/register.py | 20 - utils/log/__init__.py | 1 + utils/log/_file.py | 99 + utils/log/_logger.py | 503 +++++ utils/log/_style.py | 282 +++ utils/manager.py | 55 - {models => utils/models}/base.py | 2 +- utils/mysql.py | 30 - utils/plugins/manager.py | 44 - utils/plugins/register.py | 71 - utils/random.py | 9 +- utils/redisdb.py | 39 - utils/service/inject.py | 48 - utils/service/manager.py | 68 - utils/storage.py | 39 - utils/typedefs.py | 13 + 142 files changed, 3566 insertions(+), 4734 deletions(-) delete mode 100644 config.py create mode 100644 core/README.md create mode 100644 core/base/__init__.py create mode 100644 core/base/aiobrowser.py create mode 100644 core/base/mysql.py create mode 100644 core/base/redisdb.py create mode 100644 core/baseplugin.py create mode 100644 core/bot.py create mode 100644 core/config.py create mode 100644 core/error.py create mode 100644 core/plugin.py create mode 100644 core/service.py create mode 100644 core/user/error.py delete mode 100644 jobs/README.md delete mode 100644 jobs/base.py delete mode 100644 jobs/public_cookies.py delete mode 100644 logger.py delete mode 100644 main.py delete mode 100644 models/apihelper/metadata/CharactersMap.json delete mode 100644 models/apihelper/metadata/NameTextMapHash.json delete mode 100644 models/apihelper/metadata/ReliquaryNameMap.json delete mode 100644 models/apihelper/playercards.py delete mode 100644 models/game/artifact.py delete mode 100644 models/game/character.py delete mode 100644 models/game/fetter.py delete mode 100644 models/game/skill.py delete mode 100644 models/game/talent.py delete mode 100644 models/game/weapon.py delete mode 100644 models/types.py rename {models => modules}/README.md (79%) rename {models => modules}/apihelper/artifact.py (98%) rename {models => modules}/apihelper/base.py (100%) rename {models => modules}/apihelper/gacha.py (95%) rename {models => modules}/apihelper/helpers.py (97%) rename {models => modules}/apihelper/hoyolab.py (95%) rename {models => modules}/apihelper/hyperion.py (98%) create mode 100644 modules/playercards/models/talent.py rename {models => modules}/wiki/base.py (95%) rename {models => modules}/wiki/character.py (96%) rename {models => modules}/wiki/material.py (96%) rename {models => modules}/wiki/metadata/ascension.json (100%) rename {models => modules}/wiki/metadata/elite.json (100%) rename {models => modules}/wiki/metadata/monster.json (100%) rename {models => modules}/wiki/metadata/weapon_level.json (100%) rename {models => modules}/wiki/other.py (98%) rename {models => modules}/wiki/weapon.py (97%) delete mode 100644 plugins/base.py create mode 100644 plugins/genshin/set_uid.py create mode 100644 plugins/jobs/public_cookies.py rename {jobs => plugins/jobs}/sign.py (75%) create mode 100644 plugins/system/new_member.py create mode 100644 plugins/system/set_quiz.py create mode 100644 run.py rename {test => tests}/model/apihelper/test_artifact.py (94%) rename {test => tests}/model/wiki/test_wiki.py (96%) rename {test => tests}/run.py (100%) rename {test => tests}/service/test_game.py (96%) delete mode 100644 utils/aiobrowser.py rename {models => utils}/baseobject.py (98%) create mode 100644 utils/const.py delete mode 100644 utils/job/manager.py delete mode 100644 utils/job/register.py create mode 100644 utils/log/__init__.py create mode 100644 utils/log/_file.py create mode 100644 utils/log/_logger.py create mode 100644 utils/log/_style.py delete mode 100644 utils/manager.py rename {models => utils/models}/base.py (98%) delete mode 100644 utils/mysql.py delete mode 100644 utils/plugins/manager.py delete mode 100644 utils/plugins/register.py delete mode 100644 utils/redisdb.py delete mode 100644 utils/service/inject.py delete mode 100644 utils/service/manager.py delete mode 100644 utils/storage.py create mode 100644 utils/typedefs.py diff --git a/.gitignore b/.gitignore index 47acc88..dddf32f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ out/ env/ venv/ cache/ +temp/ # Byte-compiled / optimized / DLL files __pycache__/ @@ -35,3 +36,6 @@ logs/ ### DotEnv ### .env + +### private plugins ### +plugins/private \ No newline at end of file diff --git a/README.md b/README.md index eeaeee5..21fbe11 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ pip install --upgrade poetry ```bash poetry install +playwright install chromium ``` ### 3. 修改配置 @@ -50,7 +51,7 @@ alembic upgrade head ### 5. 运行 ```bash -python ./main.py +python ./run.py ``` ## 其他说明 diff --git a/alembic/env.py b/alembic/env.py index c69f2ca..4ff4e20 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -1,5 +1,8 @@ +import os import asyncio +from importlib import import_module from logging.config import fileConfig +from typing import Iterator from sqlalchemy import engine_from_config from sqlalchemy import pool @@ -10,6 +13,8 @@ from sqlmodel import SQLModel from alembic import context +from utils.const import PROJECT_ROOT +from utils.log import logger # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -20,15 +25,28 @@ config = context.config if config.config_file_name is not None: fileConfig(config.config_file_name) -# register our models for alembic to auto-generate migrations -from utils.manager import ModulesManager -manager = ModulesManager() -manager.refresh_list("core/*") -manager.refresh_list("jobs/*") -manager.refresh_list("plugins/genshin/*") -manager.refresh_list("plugins/system/*") -manager.import_module() +def scan_models() -> Iterator[str]: + """扫描所有 models.py 模块。 + 我们规定所有插件的 model 都需要放在名为 models.py 的文件里。""" + + for path in PROJECT_ROOT.glob("**/models.py"): + yield str(path.relative_to(PROJECT_ROOT).with_suffix("")).replace(os.sep, ".") + + +def import_models(): + """导入我们所有的 models,使 alembic 可以自动对比 db scheme 创建 migration revision""" + for pkg in scan_models(): + try: + import_module(pkg) # 导入 models + except Exception as e: # pylint: disable=W0703 + logger.error( + f'在导入文件 "{pkg}" 的过程中遇到了错误: \n[red bold]{type(e).__name__}: {e}[/]' + ) + + +# register our models for alembic to auto-generate migrations +import_models() target_metadata = SQLModel.metadata @@ -39,14 +57,14 @@ target_metadata = SQLModel.metadata # here we allow ourselves to pass interpolation vars to alembic.ini # from the application config module -from config import config as appConfig +from core.config import config as botConfig section = config.config_ini_section -config.set_section_option(section, "DB_HOST", appConfig.mysql["host"]) -config.set_section_option(section, "DB_PORT", str(appConfig.mysql["port"])) -config.set_section_option(section, "DB_USERNAME", appConfig.mysql["user"]) -config.set_section_option(section, "DB_PASSWORD", appConfig.mysql["password"]) -config.set_section_option(section, "DB_DATABASE", appConfig.mysql["database"]) +config.set_section_option(section, "DB_HOST", botConfig.mysql.host) +config.set_section_option(section, "DB_PORT", str(botConfig.mysql.port)) +config.set_section_option(section, "DB_USERNAME", botConfig.mysql.username) +config.set_section_option(section, "DB_PASSWORD", botConfig.mysql.password) +config.set_section_option(section, "DB_DATABASE", botConfig.mysql.database) def run_migrations_offline() -> None: diff --git a/config.py b/config.py deleted file mode 100644 index 912e1f3..0000000 --- a/config.py +++ /dev/null @@ -1,64 +0,0 @@ -import os -from typing import Any - -import ujson -from dotenv import load_dotenv - -from utils.storage import Storage - -# take environment variables from .env. -load_dotenv() - -env = os.getenv - - -def str_to_bool(value: Any) -> bool: - """Return whether the provided string (or any value really) represents true. Otherwise false. - Just like plugin server stringToBoolean. - """ - if not value: - return False - return str(value).lower() in ("y", "yes", "t", "true", "on", "1") - - -_config = { - "debug": str_to_bool(os.getenv('DEBUG', 'False')), - - "mysql": { - "host": env("DB_HOST", "127.0.0.1"), - "port": int(env("DB_PORT", "3306")), - "user": env("DB_USERNAME"), - "password": env("DB_PASSWORD"), - "database": env("DB_DATABASE"), - }, - - "redis": { - "host": env("REDIS_HOST", "127.0.0.1"), - "port": int(env("REDIS_PORT", "6369")), - "database": int(env("REDIS_DB", "0")), - }, - - # 联系 https://t.me/BotFather 使用 /newbot 命令创建机器人并获取 token - "bot_token": env("BOT_TOKEN"), - - # 记录错误并发送消息通知开发人员 - "error_notification_chat_id": env("ERROR_NOTIFICATION_CHAT_ID"), - - # 文章推送群组 - "channels": [ - # {"name": "", "chat_id": 1}, - # 在环境变量里的格式是 json: [{"name": "", "chat_id": 1}] - *ujson.loads(env('CHANNELS', '[]')) - ], - - # bot 管理员 - "admins": [ - # {"username": "", "user_id": 123}, - # 在环境变量里的格式是 json: [{"username": "", "user_id": 1}] - *ujson.loads(env('ADMINS', '[]')) - ], - - "joining_verification_groups": ujson.loads(env('JOINING_VERIFICATION_GROUPS', '[]')), -} - -config = Storage(_config) diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000..0f1200d --- /dev/null +++ b/core/README.md @@ -0,0 +1,22 @@ +# core 目录说明 + +## 关于 `Service` + +服务 `Service` 需定义在 `services` 文件夹下, 并继承 `core.service.Service` + +每个 `Service` 都应包含 `start` 和 `stop` 方法, 且这两个方法都为异步方法 + +```python +from core.service import Service + + +class TestService(Service): + def __init__(self): + """do something""" + + async def start(self, *args, **kwargs): + """do something""" + + async def stop(self, *args, **kwargs): + """do something""" +``` \ No newline at end of file diff --git a/core/admin/__init__.py b/core/admin/__init__.py index 55820fe..5b1d97a 100644 --- a/core/admin/__init__.py +++ b/core/admin/__init__.py @@ -1,12 +1,12 @@ -from utils.mysql import MySQL -from utils.redisdb import RedisDB -from utils.service.manager import listener_service -from .cache import BotAdminCache -from .repositories import BotAdminRepository -from .services import BotAdminService +from core.service import init_service +from core.base.mysql import MySQL +from core.base.redisdb import RedisDB +from core.admin.cache import BotAdminCache +from core.admin.repositories import BotAdminRepository +from core.admin.services import BotAdminService -@listener_service() +@init_service def create_bot_admin_service(mysql: MySQL, redis: RedisDB): _cache = BotAdminCache(redis) _repository = BotAdminRepository(mysql) diff --git a/core/admin/cache.py b/core/admin/cache.py index 42f7e58..ddce28e 100644 --- a/core/admin/cache.py +++ b/core/admin/cache.py @@ -1,6 +1,6 @@ from typing import List -from utils.redisdb import RedisDB +from core.base.redisdb import RedisDB class BotAdminCache: diff --git a/core/admin/repositories.py b/core/admin/repositories.py index b7ce235..e1001fe 100644 --- a/core/admin/repositories.py +++ b/core/admin/repositories.py @@ -3,8 +3,8 @@ from typing import List, cast from sqlalchemy import select from sqlmodel.ext.asyncio.session import AsyncSession -from utils.mysql import MySQL -from .models import Admin +from core.admin.models import Admin +from core.base.mysql import MySQL class BotAdminRepository: diff --git a/core/admin/services.py b/core/admin/services.py index 5ba2cad..4b8acca 100644 --- a/core/admin/services.py +++ b/core/admin/services.py @@ -3,10 +3,10 @@ from typing import List from pymysql import IntegrityError from telegram import Bot -from config import config -from logger import Log -from .cache import BotAdminCache, GroupAdminCache -from .repositories import BotAdminRepository +from core.admin.cache import BotAdminCache, GroupAdminCache +from core.admin.repositories import BotAdminRepository +from core.config import config +from utils.log import logger class BotAdminService: @@ -27,7 +27,7 @@ class BotAdminService: try: await self._repository.add_by_user_id(user_id) except IntegrityError as error: - Log.warning(f"{user_id} 已经存在数据库 \n", error) + logger.warning(f"{user_id} 已经存在数据库 \n", error) admin_list = await self._repository.get_all_user_id() for config_admin in config.admins: admin_list.append(config_admin["user_id"]) diff --git a/core/base/__init__.py b/core/base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/base/aiobrowser.py b/core/base/aiobrowser.py new file mode 100644 index 0000000..b159c17 --- /dev/null +++ b/core/base/aiobrowser.py @@ -0,0 +1,41 @@ +from typing import Optional + +from playwright.async_api import Browser, Playwright, async_playwright + +from core.service import Service +from utils.log import logger + + +class AioBrowser(Service): + + def __init__(self, loop=None): + self.browser: Optional[Browser] = None + self._playwright: Optional[Playwright] = None + self._loop = loop + + async def start(self): + if self._playwright is None: + logger.info("正在尝试启动 [blue]Playwright[/]") + self._playwright = await async_playwright().start() + logger.success("[blue]Playwright[/] 启动成功") + if self.browser is None: + logger.info("正在尝试启动 [blue]Browser[/]") + try: + self.browser = await self._playwright.chromium.launch(timeout=5000) + logger.success("[blue]Browser[/] 启动成功") + except TimeoutError as err: + logger.warning("[blue]Browser[/] 启动失败") + raise err + + return self.browser + + async def stop(self): + if self.browser is not None: + await self.browser.close() + if self._playwright is not None: + await self._playwright.stop() + + async def get_browser(self) -> Browser: + if self.browser is None: + await self.start() + return self.browser diff --git a/core/base/mysql.py b/core/base/mysql.py new file mode 100644 index 0000000..fd7d90e --- /dev/null +++ b/core/base/mysql.py @@ -0,0 +1,30 @@ +from sqlalchemy.ext.asyncio import create_async_engine +from sqlalchemy.orm import sessionmaker +from sqlmodel.ext.asyncio.session import AsyncSession +from typing_extensions import Self + +from core.config import BotConfig +from core.service import Service + + +class MySQL(Service): + @classmethod + def from_config(cls, config: BotConfig) -> Self: + return cls(**config.mysql.dict()) + + def __init__(self, host: str = "127.0.0.1", port: int = 3306, username: str = "root", # nosec B107 + password: str = "", database: str = ""): # nosec B107 + self.database = database + self.password = password + self.user = username + self.port = port + self.host = host + self.engine = create_async_engine( + f"mysql+asyncmy://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}" + ) + self.Session = sessionmaker(bind=self.engine, class_=AsyncSession) + + async def get_session(self): + """获取会话""" + async with self.Session() as session: + yield session diff --git a/core/base/redisdb.py b/core/base/redisdb.py new file mode 100644 index 0000000..0d41f23 --- /dev/null +++ b/core/base/redisdb.py @@ -0,0 +1,44 @@ +import asyncio + +import fakeredis.aioredis +from redis import asyncio as aioredis +from typing_extensions import Self + +from core.config import BotConfig +from core.service import Service +from utils.log import logger + + +class RedisDB(Service): + @classmethod + def from_config(cls, config: BotConfig) -> Self: + return cls(**config.redis.dict()) + + def __init__(self, host="127.0.0.1", port=6379, database=0, loop=None): + self.client = aioredis.Redis(host=host, port=port, db=database) + self.ttl = 600 + self.key_prefix = "paimon_bot" + self._loop = loop + + async def ping(self): + if await self.client.ping(): + logger.info("连接 [red]Redis[/] 成功") + else: + logger.info("连接 [red]Redis[/] 失败") + raise RuntimeError("连接 [red]Redis[/] 失败") + + async def start(self): # pylint: disable=W0221 + if self._loop is None: + self._loop = asyncio.get_running_loop() + logger.info("正在尝试建立与 [red]Redis[/] 连接") + try: + await self.ping() + except (KeyboardInterrupt, SystemExit): + pass + except BaseException as exc: + logger.warning("尝试连接 [red]Redis[/] 失败,使用 [red]fakeredis[/] 模拟", exc) + self.client = fakeredis.aioredis.FakeRedis() + await self.ping() + + async def stop(self): # pylint: disable=W0221 + await self.client.close() diff --git a/core/baseplugin.py b/core/baseplugin.py new file mode 100644 index 0000000..07e31bb --- /dev/null +++ b/core/baseplugin.py @@ -0,0 +1,48 @@ +from telegram import Update, ReplyKeyboardRemove +from telegram.error import BadRequest +from telegram.ext import CallbackContext, ConversationHandler + +from core.plugin import handler, conversation +from utils.log import logger + + +async def clean_message(context: CallbackContext): + job = context.job + logger.debug(f"删除消息 chat_id[{job.chat_id}] 的 message_id[{job.data}]") + try: + # noinspection PyTypeChecker + await context.bot.delete_message(chat_id=job.chat_id, message_id=job.data) + except BadRequest as error: + if "not found" in str(error): + logger.warning(f"Auth模块删除消息 chat_id[{job.chat_id}] message_id[{job.data}]失败 消息不存在") + elif "Message can't be deleted" in str(error): + logger.warning( + f"Auth模块删除消息 chat_id[{job.chat_id}] message_id[{job.data}]失败 消息无法删除 可能是没有授权") + else: + logger.error(f"Auth模块删除消息 chat_id[{job.chat_id}] message_id[{job.data}]失败", error) + + +def add_delete_message_job(context: CallbackContext, chat_id: int, message_id: int, delete_seconds: int): + context.job_queue.run_once(callback=clean_message, when=delete_seconds, data=message_id, + name=f"{chat_id}|{message_id}|clean_message", chat_id=chat_id, + job_kwargs={"replace_existing": True, + "id": f"{chat_id}|{message_id}|clean_message"}) + + +class _BasePlugin: + @staticmethod + def _add_delete_message_job(context: CallbackContext, chat_id: int, message_id: int, delete_seconds: int = 60): + return add_delete_message_job(context, chat_id, message_id, delete_seconds) + + +class _Conversation(_BasePlugin): + + @conversation.fallback + @handler.command(command='cancel', block=True) + async def cancel(self, update: Update, _: CallbackContext) -> int: + await update.effective_message.reply_text("退出命令", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + + +class BasePlugin(_BasePlugin): + Conversation = _Conversation diff --git a/core/bot.py b/core/bot.py new file mode 100644 index 0000000..c8ed1b4 --- /dev/null +++ b/core/bot.py @@ -0,0 +1,256 @@ +import asyncio +import inspect +import os +from importlib import import_module +from multiprocessing import RLock as Lock +from pathlib import Path +from typing import Any, Callable, ClassVar, Dict, Iterator, List, NoReturn, Optional, TYPE_CHECKING, Type, TypeVar + +import pytz +from telegram.error import NetworkError, TimedOut +from telegram.ext import AIORateLimiter, Application as TgApplication, Defaults, JobQueue, MessageHandler +from telegram.ext.filters import StatusUpdate + +from core.config import BotConfig, config # pylint: disable=W0611 +from core.error import ServiceNotFoundError +# noinspection PyProtectedMember +from core.plugin import Plugin, _Plugin +from core.service import Service +from utils.const import PLUGIN_DIR, PROJECT_ROOT +from utils.log import logger + +if TYPE_CHECKING: + from telegram import Update + from telegram.ext import CallbackContext + +__all__ = ['bot'] + +T = TypeVar('T') +PluginType = TypeVar('PluginType', bound=_Plugin) + + +class Bot: + _lock: ClassVar[Lock] = Lock() + _instance: ClassVar[Optional["Bot"]] = None + + def __new__(cls, *args, **kwargs) -> "Bot": + """实现单例""" + with cls._lock: # 使线程、进程安全 + if cls._instance is None: + cls._instance = object.__new__(cls) + return cls._instance + + app: Optional[TgApplication] = None + _config: BotConfig = config + _services: Dict[Type[T], T] = {} + _running: bool = False + + def init_inject(self, target: Callable[..., T]) -> T: + """用于实例化Plugin的方法。用于给插件传入一些必要组件,如 MySQL、Redis等""" + if isinstance(target, type): + signature = inspect.signature(target.__init__) + else: + signature = inspect.signature(target) + kwargs = {} + for name, parameter in signature.parameters.items(): + if name != 'self' and parameter.annotation != inspect.Parameter.empty: + if value := self._services.get(parameter.annotation): + kwargs[name] = value + return target(**kwargs) + + def _gen_pkg(self, root: Path) -> Iterator[str]: + """生成可以用于 import_module 导入的字符串""" + for path in root.iterdir(): + if not path.name.startswith('_'): + if path.is_dir(): + yield from self._gen_pkg(path) + elif path.suffix == '.py': + yield str(path.relative_to(PROJECT_ROOT).with_suffix('')).replace(os.sep, '.') + + async def install_plugins(self): + """安装插件""" + for pkg in self._gen_pkg(PLUGIN_DIR): + try: + import_module(pkg) # 导入插件 + except Exception as e: # pylint: disable=W0703 + logger.exception(f'在导入文件 "{pkg}" 的过程中遇到了错误: \n[red bold]{type(e).__name__}: {e}[/]') + continue # 如有错误则继续 + callback_dict: Dict[int, List[Callable]] = {} + for plugin_cls in {*Plugin.__subclasses__(), *Plugin.Conversation.__subclasses__()}: + path = f"{plugin_cls.__module__}.{plugin_cls.__name__}" + try: + plugin: PluginType = self.init_inject(plugin_cls) + if hasattr(plugin, '__async_init__'): + await plugin.__async_init__() + handlers = plugin.handlers + self.app.add_handlers(handlers) + if handlers: + logger.debug(f'插件 "{path}" 添加了 {len(handlers)} 个 handler ') + + # noinspection PyProtectedMember + for priority, callback in plugin._new_chat_members_handler_funcs(): # pylint: disable=W0212 + if not callback_dict.get(priority): + callback_dict[priority] = [] + callback_dict[priority].append(callback) + + error_handlers = plugin.error_handlers + for callback, block in error_handlers.items(): + self.app.add_error_handler(callback, block) + if error_handlers: + logger.debug(f"插件 \"{path}\" 添加了 {len(error_handlers)} 个 error handler") + + if jobs := plugin.jobs: + logger.debug(f'插件 "{path}" 添加了 {len(jobs)} 个任务') + logger.success(f'插件 "{path}" 载入成功') + except Exception as e: # pylint: disable=W0703 + logger.exception(f'在安装插件 \"{path}\" 的过程中遇到了错误: \n[red bold]{type(e).__name__}: {e}[/]') + if callback_dict: + num = sum(len(callback_dict[i]) for i in callback_dict) + + async def _new_chat_member_callback(update: 'Update', context: 'CallbackContext'): + nonlocal callback + for _, value in callback_dict.items(): + for callback in value: + await callback(update, context) + + self.app.add_handler(MessageHandler( + callback=_new_chat_member_callback, filters=StatusUpdate.NEW_CHAT_MEMBERS, block=False + )) + logger.success( + f'成功添加了 {num} 个针对 [blue]{StatusUpdate.NEW_CHAT_MEMBERS}[/] 的 [blue]MessageHandler[/]' + ) + + async def _start_base_services(self): + for pkg in self._gen_pkg(PROJECT_ROOT / 'core/base'): + try: + import_module(pkg) + except Exception as e: # pylint: disable=W0703 + logger.exception(f'在导入文件 "{pkg}" 的过程中遇到了错误: \n[red bold]{type(e).__name__}: {e}[/]') + continue + for base_service_cls in Service.__subclasses__(): + try: + if hasattr(base_service_cls, 'from_config'): + instance = base_service_cls.from_config(self._config) + else: + instance = self.init_inject(base_service_cls) + await instance.start() + logger.success(f'服务 "{base_service_cls.__name__}" 初始化成功') + self._services.update({base_service_cls: instance}) + except Exception as e: # pylint: disable=W0703 + logger.exception(f'服务 "{base_service_cls.__name__}" 初始化失败: {e}') + continue + + async def start_services(self): + """启动服务""" + await self._start_base_services() + for path in (PROJECT_ROOT / 'core').iterdir(): + if not path.name.startswith('_') and path.is_dir() and path.name != 'base': + pkg = str(path.relative_to(PROJECT_ROOT).with_suffix('')).replace(os.sep, '.') + try: + import_module(pkg) + except Exception as e: # pylint: disable=W0703 + logger.exception(f'在导入文件 "{pkg}" 的过程中遇到了错误: \n[red bold]{type(e).__name__}: {e}[/]') + continue + + async def stop_services(self): + """关闭服务""" + if not self._services: + return + logger.info('正在关闭服务') + for _, service in self._services.items(): + try: + if hasattr(service, 'stop'): + if inspect.iscoroutinefunction(service.stop): + await service.stop() + else: + service.stop() + logger.success(f'服务 "{service.__class__.__name__}" 关闭成功') + except Exception as e: # pylint: disable=W0703 + logger.exception(f"服务 \"{service.__class__.__name__}\" 关闭失败: \n{type(e).__name__}: {e}") + + async def _post_init(self, _) -> NoReturn: + logger.info('开始初始化服务') + await self.start_services() + logger.info('开始安装插件') + await self.install_plugins() + + def launch(self) -> NoReturn: + """启动机器人""" + self._running = True + logger.info('正在初始化BOT') + self.app = ( + TgApplication.builder() + .rate_limiter(AIORateLimiter()) + .defaults(Defaults(tzinfo=pytz.timezone("Asia/Shanghai"))) + .token(self._config.bot_token) + .post_init(self._post_init) + .build() + ) + logger.info('BOT 初始化成功') + try: + for _ in range(5): + try: + self.app.run_polling(close_loop=False) + break + except TimedOut: + logger.warning("连接至 [blue]telegram[/] 服务器失败,正在重试") + continue + except NetworkError as e: + logger.exception() + if 'SSLZeroReturnError' in str(e): + logger.error("代理服务出现异常, 请检查您的代理服务是否配置成功.") + else: + logger.error("网络连接出现问题, 请检查您的网络状况.") + break + except (SystemExit, KeyboardInterrupt): + pass + except Exception as e: # pylint: disable=W0703 + logger.exception(f"BOT 执行过程中出现错误: {e}") + finally: + loop = asyncio.get_event_loop() + loop.run_until_complete(self.stop_services()) + loop.close() + logger.info("BOT 已经关闭") + self._running = False + + def find_service(self, target: Type[T]) -> T: + """查找服务。若没找到则抛出 ServiceNotFoundError""" + if result := self._services.get(target) is None: + raise ServiceNotFoundError(target) + return result + + def add_service(self, service: T) -> NoReturn: + """添加服务。若已经有同类型的服务,则会抛出异常""" + if type(service) in self._services: + raise ValueError(f"Service \"{type(service)}\" is already existed.") + self.update_service(service) + + def update_service(self, service: T): + """更新服务。若服务不存在,则添加;若存在,则更新""" + self._services.update({type(service): service}) + + def contain_service(self, service: Any) -> bool: + """判断服务是否存在""" + if isinstance(service, type): + return service in self._services + else: + return service in self._services.values() + + @property + def job_queue(self) -> JobQueue: + return self.app.job_queue + + @property + def services(self) -> Dict[Type[T], T]: + return self._services + + @property + def config(self) -> BotConfig: + return self._config + + @property + def is_running(self) -> bool: + return self._running + + +bot = Bot() diff --git a/core/config.py b/core/config.py new file mode 100644 index 0000000..5eec427 --- /dev/null +++ b/core/config.py @@ -0,0 +1,88 @@ +from typing import ( + List, + Optional, + Union, +) + +import dotenv +import ujson as json +from pydantic import ( + BaseModel, + BaseSettings, +) + +__all__ = ['BotConfig', 'config'] + +dotenv.load_dotenv() + + +class BotConfig(BaseSettings): + debug: bool = False + + db_host: str + db_port: int + db_username: str + db_password: str + db_database: str + + redis_host: str + redis_port: int + redis_db: int + + bot_token: str + error_notification_chat_id: str + + channels: List['ConfigChannel'] = [] + admins: List['ConfigUser'] = [] + verify_groups: List[Union[int, str]] = [] + + class Config: + case_sensitive = False + json_loads = json.loads + json_dumps = json.dumps + + @property + def mysql(self) -> "MySqlConfig": + return MySqlConfig( + host=self.db_host, + port=self.db_port, + username=self.db_username, + password=self.db_password, + database=self.db_database, + ) + + @property + def redis(self) -> "RedisConfig": + return RedisConfig( + host=self.redis_host, + port=self.redis_port, + database=self.redis_db, + ) + + +class ConfigChannel(BaseModel): + name: str + chat_id: int + + +class ConfigUser(BaseModel): + username: Optional[str] + user_id: int + + +class MySqlConfig(BaseModel): + host: str = "127.0.0.1" + port: int = 3306 + username: str + password: str + database: str + + +class RedisConfig(BaseModel): + host: str = '127.0.0.1' + port: int + database: int = 0 + + +BotConfig.update_forward_refs() +config = BotConfig() diff --git a/core/cookies/__init__.py b/core/cookies/__init__.py index 80f1454..a3c15cd 100644 --- a/core/cookies/__init__.py +++ b/core/cookies/__init__.py @@ -1,19 +1,19 @@ -from utils.mysql import MySQL -from utils.redisdb import RedisDB -from utils.service.manager import listener_service -from .cache import PublicCookiesCache -from .repositories import CookiesRepository -from .services import CookiesService, PublicCookiesService +from core.base.mysql import MySQL +from core.base.redisdb import RedisDB +from core.cookies.cache import PublicCookiesCache +from core.cookies.repositories import CookiesRepository +from core.cookies.services import CookiesService, PublicCookiesService +from core.service import init_service -@listener_service() +@init_service def create_cookie_service(mysql: MySQL): _repository = CookiesRepository(mysql) _service = CookiesService(_repository) return _service -@listener_service() +@init_service def create_public_cookie_service(mysql: MySQL, redis: RedisDB): _repository = CookiesRepository(mysql) _cache = PublicCookiesCache(redis) diff --git a/core/cookies/cache.py b/core/cookies/cache.py index c956601..3c65b09 100644 --- a/core/cookies/cache.py +++ b/core/cookies/cache.py @@ -1,8 +1,8 @@ from typing import List, Union -from models.base import RegionEnum +from core.base.redisdb import RedisDB from utils.error import RegionNotFoundError -from utils.redisdb import RedisDB +from utils.models.base import RegionEnum from .error import CookiesCachePoolExhausted @@ -12,9 +12,11 @@ class PublicCookiesCache: def __init__(self, redis: RedisDB): self.client = redis.client self.score_qname = "cookie:public" + self.user_times_qname = "cookie:public:times" self.end = 20 + self.user_times_ttl = 60 * 60 * 24 - def get_queue_name(self, region: RegionEnum): + def get_public_cookies_queue_name(self, region: RegionEnum): if region == RegionEnum.HYPERION: return self.score_qname + ":yuanshen" elif region == RegionEnum.HOYOLAB: @@ -22,26 +24,26 @@ class PublicCookiesCache: else: raise RegionNotFoundError(region.name) - async def putback(self, uid: int, region: RegionEnum): + async def putback_public_cookies(self, uid: int, region: RegionEnum): """重新添加单个到缓存列表 :param uid: :param region: :return: """ - qname = self.get_queue_name(region) + qname = self.get_public_cookies_queue_name(region) score_maps = {f"{uid}": 0} result = await self.client.zrem(qname, f"{uid}") if result == 1: await self.client.zadd(qname, score_maps) return result - async def add(self, uid: Union[List[int], int], region: RegionEnum): + async def add_public_cookies(self, uid: Union[List[int], int], region: RegionEnum): """单个或批量添加到缓存列表 :param uid: :param region: :return: 成功返回列表大小 """ - qname = self.get_queue_name(region) + qname = self.get_public_cookies_queue_name(region) if isinstance(uid, int): score_maps = {f"{uid}": 0} elif isinstance(uid, list): @@ -57,16 +59,17 @@ class PublicCookiesCache: add, count = await pipe.execute() return int(add), count - async def get(self, region: RegionEnum): + async def get_public_cookies(self, region: RegionEnum): """从缓存列表获取 :param region: :return: """ - qname = self.get_queue_name(region) + qname = self.get_public_cookies_queue_name(region) scores = await self.client.zrevrange(qname, 0, self.end, withscores=True, score_cast_func=int) if len(scores) > 0: def take_score(elem): return elem[1] + scores.sort(key=take_score) key = scores[0][0] score = scores[0][1] @@ -77,16 +80,23 @@ class PublicCookiesCache: await pipe.execute() return int(key), score + 1 - async def delete(self, uid: int, region: RegionEnum): - qname = self.get_queue_name(region) + async def delete_public_cookies(self, uid: int, region: RegionEnum): + qname = self.get_public_cookies_queue_name(region) async with self.client.pipeline(transaction=True) as pipe: await pipe.zrem(qname, uid) return await pipe.execute() - async def count(self, limit: bool = True): + async def get_public_cookies_count(self, limit: bool = True): async with self.client.pipeline(transaction=True) as pipe: if limit: await pipe.zcount(0, self.end) else: await pipe.zcard(self.score_qname) return await pipe.execute() + + async def incr_by_user_times(self, user_id: Union[List[int], int]): + qname = self.user_times_qname + f":{user_id}" + times = await self.client.incrby(qname) + if times <= 1: + await self.client.expire(qname, self.user_times_ttl) + return times diff --git a/core/cookies/error.py b/core/cookies/error.py index 59ca3d4..5873e67 100644 --- a/core/cookies/error.py +++ b/core/cookies/error.py @@ -1,3 +1,17 @@ -class CookiesCachePoolExhausted(Exception): +class CookieServiceError(Exception): + pass + + +class CookiesCachePoolExhausted(CookieServiceError): def __init__(self): - super().__init__("Cookies cache pool is exhausted") \ No newline at end of file + super().__init__("Cookies cache pool is exhausted") + + +class CookiesNotFoundError(CookieServiceError): + def __init__(self, user_id): + super().__init__(f"{user_id} cookies not found") + + +class TooManyRequestPublicCookies(CookieServiceError): + def __init__(self, user_id): + super().__init__(f"{user_id} too many request public cookies") diff --git a/core/cookies/repositories.py b/core/cookies/repositories.py index 4505892..c8d1bec 100644 --- a/core/cookies/repositories.py +++ b/core/cookies/repositories.py @@ -3,9 +3,10 @@ from typing import cast, List from sqlalchemy import select from sqlmodel.ext.asyncio.session import AsyncSession -from models.base import RegionEnum -from utils.error import NotFoundError, RegionNotFoundError -from utils.mysql import MySQL +from core.base.mysql import MySQL +from utils.error import RegionNotFoundError +from utils.models.base import RegionEnum +from .error import CookiesNotFoundError from .models import HyperionCookie, HoyolabCookie, Cookies @@ -47,16 +48,11 @@ class CookiesRepository: async def update_cookies_ex(self, cookies: Cookies, region: RegionEnum): async with self.mysql.Session() as session: session = cast(AsyncSession, session) - if region == RegionEnum.HYPERION: - session.add(cookies) - await session.commit() - await session.refresh(cookies) - elif region == RegionEnum.HOYOLAB: - await session.add(cookies) - await session.commit() - await session.refresh(cookies) - else: + if region not in [RegionEnum.HYPERION, RegionEnum.HOYOLAB]: raise RegionNotFoundError(region.name) + session.add(cookies) + await session.commit() + await session.refresh(cookies) async def get_cookies(self, user_id, region: RegionEnum) -> Cookies: async with self.mysql.Session() as session: @@ -92,9 +88,4 @@ class CookiesRepository: db_cookies = results.all() return [cookies[0] for cookies in db_cookies] else: - raise RegionNotFoundError(region.name) - - -class CookiesNotFoundError(NotFoundError): - entity_name: str = "CookiesRepository" - entity_value_name: str = "user_id" + raise RegionNotFoundError(region.name) \ No newline at end of file diff --git a/core/cookies/services.py b/core/cookies/services.py index 6c184d2..e9e8fcb 100644 --- a/core/cookies/services.py +++ b/core/cookies/services.py @@ -1,13 +1,14 @@ from typing import List import genshin -from genshin import types, InvalidCookies, TooManyRequests, GenshinException +from genshin import GenshinException, InvalidCookies, TooManyRequests, types -from logger import Log -from models.base import RegionEnum +from utils.log import logger +from utils.models.base import RegionEnum from .cache import PublicCookiesCache +from .error import TooManyRequestPublicCookies, CookieServiceError from .models import CookiesStatusEnum -from .repositories import CookiesRepository, CookiesNotFoundError +from .repositories import CookiesNotFoundError, CookiesRepository class CookiesService: @@ -30,6 +31,7 @@ class PublicCookiesService: self._cache = public_cookies_cache self._repository: CookiesRepository = cookies_repository self.count: int = 0 + self.user_times_limiter = 3 * 3 async def refresh(self): """刷新公共Cookies 定时任务 @@ -41,14 +43,14 @@ class PublicCookiesService: if cookies.status is not None and cookies.status != CookiesStatusEnum.STATUS_SUCCESS: continue user_list.append(cookies.user_id) - add, count = await self._cache.add(user_list, RegionEnum.HYPERION) - Log.info(f"国服公共Cookies池已经添加[{add}]个 当前成员数为[{count}]") + add, count = await self._cache.add_public_cookies(user_list, RegionEnum.HYPERION) + logger.info(f"国服公共Cookies池已经添加[{add}]个 当前成员数为[{count}]") user_list.clear() cookies_list = await self._repository.get_all_cookies(RegionEnum.HOYOLAB) for cookies in cookies_list: user_list.append(cookies.user_id) - add, count = await self._cache.add(user_list, RegionEnum.HOYOLAB) - Log.info(f"国际服公共Cookies池已经添加[{add}]个 当前成员数为[{count}]") + add, count = await self._cache.add_public_cookies(user_list, RegionEnum.HOYOLAB) + logger.info(f"国际服公共Cookies池已经添加[{add}]个 当前成员数为[{count}]") async def get_cookies(self, user_id: int, region: RegionEnum = RegionEnum.NULL): """获取公共Cookies @@ -56,12 +58,15 @@ class PublicCookiesService: :param region: 注册的服务器 :return: """ + user_times = await self._cache.incr_by_user_times(user_id) + if int(user_times) > self.user_times_limiter: + raise TooManyRequestPublicCookies while True: - public_id, count = await self._cache.get(region) + public_id, count = await self._cache.get_public_cookies(region) try: cookies = await self._repository.get_cookies(public_id, region) except CookiesNotFoundError: - await self._cache.delete(public_id, region) + await self._cache.delete_public_cookies(public_id, region) continue if region == RegionEnum.HYPERION: client = genshin.Client(cookies=cookies.cookies, game=types.Game.GENSHIN, region=types.Region.CHINESE) @@ -69,31 +74,33 @@ class PublicCookiesService: client = genshin.Client(cookies=cookies.cookies, game=types.Game.GENSHIN, region=types.Region.OVERSEAS, lang="zh-cn") else: - return None + raise CookieServiceError try: await client.get_record_card() except InvalidCookies as exc: if "[10001]" in str(exc): - Log.warning(f"用户 [{public_id}] Cookies无效") + logger.warning(f"用户 [{public_id}] Cookies无效") elif "[-100]" in str(exc): - Log.warning(f"用户 [{public_id}] Cookies无效") + logger.warning(f"用户 [{public_id}] Cookies无效") elif "[10103]" in str(exc): - Log.warning(f"用户 [{public_id}] Cookie有效,但没有绑定到游戏帐户") + logger.warning(f"用户 [{public_id}] Cookie有效,但没有绑定到游戏帐户") else: - Log.warning("Cookies无效,具体原因未知", exc) + logger.warning("Cookies无效,具体原因未知") + logger.exception(exc) cookies.status = CookiesStatusEnum.INVALID_COOKIES await self._repository.update_cookies_ex(cookies, region) - await self._cache.delete(cookies.user_id, region) + await self._cache.delete_public_cookies(cookies.user_id, region) continue - except TooManyRequests as exc: - Log.warning(f"用户 [{public_id}] 查询次数太多(操作频繁)") + except TooManyRequests: + logger.warning(f"用户 [{public_id}] 查询次数太多或操作频繁") cookies.status = CookiesStatusEnum.TOO_MANY_REQUESTS await self._repository.update_cookies_ex(cookies, region) - await self._cache.delete(cookies.user_id, region) + await self._cache.delete_public_cookies(cookies.user_id, region) continue except GenshinException as exc: - Log.warning(f"用户 [{public_id}] 获取账号信息发生错误,错误信息为", exc) + logger.warning(f"用户 [{public_id}] 获取账号信息发生错误,错误信息为") + logger.exception(exc) continue - Log.info(f"用户 user_id[{user_id}] 请求" - f"用户 user_id[{public_id}] 的公共Cookies 该Cookie使用次数为[{count}]次 ") + logger.info(f"用户 user_id[{user_id}] 请求" + f"用户 user_id[{public_id}] 的公共Cookies 该Cookie使用次数为[{count}]次 ") return cookies diff --git a/core/error.py b/core/error.py new file mode 100644 index 0000000..8b2f25d --- /dev/null +++ b/core/error.py @@ -0,0 +1,8 @@ +"""此模块包含核心模块的错误的基类""" +from typing import Union + + +class ServiceNotFoundError(Exception): + + def __init__(self, name: Union[str, type]): + super().__init__(f"No service named '{name if isinstance(name, str) else name.__name__}'") diff --git a/core/game/__init__.py b/core/game/__init__.py index 3e45445..cb4e271 100644 --- a/core/game/__init__.py +++ b/core/game/__init__.py @@ -1,16 +1,16 @@ -from utils.redisdb import RedisDB -from utils.service.manager import listener_service +from core.base.redisdb import RedisDB +from core.service import init_service from .cache import GameCache -from .services import GameStrategyService, GameMaterialService +from .services import GameMaterialService, GameStrategyService -@listener_service() +@init_service def create_game_strategy_service(redis: RedisDB): _cache = GameCache(redis, "game:strategy") return GameStrategyService(_cache) -@listener_service() +@init_service def create_game_material_service(redis: RedisDB): _cache = GameCache(redis, "game:material") return GameMaterialService(_cache) diff --git a/core/game/cache.py b/core/game/cache.py index 0ccb3df..a03ed56 100644 --- a/core/game/cache.py +++ b/core/game/cache.py @@ -1,6 +1,6 @@ from typing import List -from utils.redisdb import RedisDB +from core.base.redisdb import RedisDB class GameCache: diff --git a/core/game/services.py b/core/game/services.py index 8834fcc..1c2f603 100644 --- a/core/game/services.py +++ b/core/game/services.py @@ -1,6 +1,6 @@ from typing import List, Optional -from models.apihelper.hyperion import Hyperion +from modules.apihelper.hyperion import Hyperion from .cache import GameCache diff --git a/core/plugin.py b/core/plugin.py new file mode 100644 index 0000000..d95ed50 --- /dev/null +++ b/core/plugin.py @@ -0,0 +1,423 @@ +import datetime +import re +from importlib import import_module +from re import Pattern +from types import MethodType +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar, Union + +# noinspection PyProtectedMember +from telegram._utils.defaultvalue import DEFAULT_TRUE +# noinspection PyProtectedMember +from telegram._utils.types import DVInput, JSONDict +from telegram.ext import BaseHandler, ConversationHandler, Job +# noinspection PyProtectedMember +from telegram.ext._utils.types import JobCallback +from telegram.ext.filters import BaseFilter +from typing_extensions import ParamSpec + +__all__ = [ + 'Plugin', 'handler', 'conversation', 'job', 'error_handler' +] + +P = ParamSpec('P') +T = TypeVar('T') +HandlerType = TypeVar('HandlerType', bound=BaseHandler) +TimeType = Union[float, datetime.timedelta, datetime.datetime, datetime.time] + +_Module = import_module('telegram.ext') + +_NORMAL_HANDLER_ATTR_NAME = "_handler_data" +_CONVERSATION_HANDLER_ATTR_NAME = "_conversation_data" +_JOB_ATTR_NAME = "_job_data" + +_EXCLUDE_ATTRS = ['handlers', 'jobs', 'error_handlers'] + + +class _Plugin: + + def _make_handler(self, data: Dict) -> HandlerType: + func = getattr(self, data.pop('func')) + return data.pop('type')(callback=func, **data.pop('kwargs')) + + @property + def handlers(self) -> List[HandlerType]: + result = [] + for attr in dir(self): + # noinspection PyUnboundLocalVariable + if ( + not (attr.startswith('_') or attr in _EXCLUDE_ATTRS) + and + isinstance(func := getattr(self, attr), MethodType) + and + (data := getattr(func, _NORMAL_HANDLER_ATTR_NAME, None)) + ): + if data['type'] not in ['error', 'new_chat_member']: + result.append(self._make_handler(data)) + + return result + + def _new_chat_members_handler_funcs(self) -> List[Tuple[int, Callable]]: + + result = [] + for attr in dir(self): + # noinspection PyUnboundLocalVariable + if ( + not (attr.startswith('_') or attr in _EXCLUDE_ATTRS) + and + isinstance(func := getattr(self, attr), MethodType) + and + (data := getattr(func, _NORMAL_HANDLER_ATTR_NAME, None)) + ): + if data['type'] == 'new_chat_member': + result.append((data['priority'], func)) + + return result + + @property + def error_handlers(self) -> Dict[Callable, bool]: + result = {} + for attr in dir(self): + # noinspection PyUnboundLocalVariable + if ( + not (attr.startswith('_') or attr in _EXCLUDE_ATTRS) + and + isinstance(func := getattr(self, attr), MethodType) + and + (data := getattr(func, _NORMAL_HANDLER_ATTR_NAME, None)) + ): + if data['type'] == 'error': + result.update({func: data['block']}) + return result + + @property + def jobs(self) -> List[Job]: + from core.bot import bot + result = [] + for attr in dir(self): + # noinspection PyUnboundLocalVariable + if ( + not (attr.startswith('_') or attr in _EXCLUDE_ATTRS) + and + isinstance(func := getattr(self, attr), MethodType) + and + (data := getattr(func, _JOB_ATTR_NAME, None)) + ): + _job = getattr(bot.job_queue, data.pop('type'))( + callback=func, **data.pop('kwargs'), + **{key: data.pop(key) for key in list(data.keys())} + ) + result.append(_job) + return result + + +class _Conversation(_Plugin): + _conversation_kwargs: Dict + + def __init_subclass__(cls, **kwargs): + cls._conversation_kwargs = kwargs + super(_Conversation, cls).__init_subclass__() + return cls + + @property + def handlers(self) -> List[HandlerType]: + result: List[HandlerType] = [] + + entry_points: List[HandlerType] = [] + states: Dict[Any, List[HandlerType]] = {} + fallbacks: List[HandlerType] = [] + for attr in dir(self): + # noinspection PyUnboundLocalVariable + if ( + not (attr.startswith('_') or attr == 'handlers') + and + isinstance(func := getattr(self, attr), Callable) + and + (handler_data := getattr(func, _NORMAL_HANDLER_ATTR_NAME, None)) + ): + _handler = self._make_handler(handler_data) + if conversation_data := getattr(func, _CONVERSATION_HANDLER_ATTR_NAME, None): + if (_type := conversation_data.pop('type')) == 'entry': + entry_points.append(_handler) + elif _type == 'state': + if (key := conversation_data.pop('state')) in states: + states[key].append(_handler) + else: + states[key] = [_handler] + elif _type == 'fallback': + fallbacks.append(_handler) + else: + result.append(_handler) + if entry_points or states or fallbacks: + result.append( + ConversationHandler( + entry_points, states, fallbacks, + **self.__class__._conversation_kwargs # pylint: disable=W0212 + ) + ) + return result + + +class Plugin(_Plugin): + Conversation = _Conversation + + +class _Handler: + def __init__(self, **kwargs): + self.kwargs = kwargs + + @property + def _type(self) -> Type[BaseHandler]: + return getattr(_Module, f"{self.__class__.__name__.strip('_')}Handler") + + def __call__(self, func: Callable[P, T]) -> Callable[P, T]: + setattr(func, _NORMAL_HANDLER_ATTR_NAME, {'type': self._type, 'func': func.__name__, 'kwargs': self.kwargs}) + return func + + +class _CallbackQuery(_Handler): + def __init__( + self, + pattern: Union[str, Pattern, type, Callable[[object], Optional[bool]]] = None, + block: DVInput[bool] = DEFAULT_TRUE, + ): + super(_CallbackQuery, self).__init__(pattern=pattern, block=block) + + +class _ChatJoinRequest(_Handler): + def __init__(self, block: DVInput[bool] = DEFAULT_TRUE): + super(_ChatJoinRequest, self).__init__(block=block) + + +class _ChatMember(_Handler): + def __init__(self, chat_member_types: int = -1): + super().__init__(chat_member_types=chat_member_types) + + +class _ChosenInlineResult(_Handler): + def __init__(self, block: DVInput[bool] = DEFAULT_TRUE, pattern: Union[str, Pattern] = None): + super().__init__(block=block, pattern=pattern) + + +class _Command(_Handler): + def __init__(self, command: str, filters: "BaseFilter" = None, block: DVInput[bool] = DEFAULT_TRUE): + super(_Command, self).__init__(command=command, filters=filters, block=block) + + +class _InlineQuery(_Handler): + def __init__( + self, + pattern: Union[str, Pattern] = None, + block: DVInput[bool] = DEFAULT_TRUE, + chat_types: List[str] = None + ): + super().__init__(pattern=pattern, block=block, chat_types=chat_types) + + +class _MessageNewChatMembers(_Handler): + def __init__(self, func: Callable[P, T] = None, *, priority: int = 5): + super().__init__() + self.func = func + self.priority = priority + + def __call__(self, func: Callable[P, T] = None) -> Callable[P, T]: + self.func = self.func or func + setattr( + self.func, _NORMAL_HANDLER_ATTR_NAME, + {'type': 'new_chat_member', 'priority': self.priority} + ) + return self.func + + +class _Message(_Handler): + def __init__(self, filters: "BaseFilter", block: DVInput[bool] = DEFAULT_TRUE, ): + super(_Message, self).__init__(filters=filters, block=block) + + new_chat_members = _MessageNewChatMembers + + +class _PollAnswer(_Handler): + def __init__(self, block: DVInput[bool] = DEFAULT_TRUE): + super(_PollAnswer, self).__init__(block=block) + + +class _Poll(_Handler): + def __init__(self, block: DVInput[bool] = DEFAULT_TRUE): + super(_Poll, self).__init__(block=block) + + +class _PreCheckoutQuery(_Handler): + def __init__(self, block: DVInput[bool] = DEFAULT_TRUE): + super(_PreCheckoutQuery, self).__init__(block=block) + + +class _Prefix(_Handler): + def __init__( + self, + prefix: str, + command: str, + filters: BaseFilter = None, + block: DVInput[bool] = DEFAULT_TRUE, + ): + super(_Prefix, self).__init__(prefix=prefix, command=command, filters=filters, block=block) + + +class _ShippingQuery(_Handler): + def __init__(self, block: DVInput[bool] = DEFAULT_TRUE): + super(_ShippingQuery, self).__init__(block=block) + + +class _StringCommand(_Handler): + def __init__(self, command: str): + super(_StringCommand, self).__init__(command=command) + + +class _StringRegex(_Handler): + def __init__(self, pattern: Union[str, Pattern], block: DVInput[bool] = DEFAULT_TRUE): + super(_StringRegex, self).__init__(pattern=pattern, block=block) + + +class _Type(_Handler): + # noinspection PyShadowingBuiltins + def __init__( + self, + type: Type, # pylint: disable=redefined-builtin + strict: bool = False, + block: DVInput[bool] = DEFAULT_TRUE + ): + super(_Type, self).__init__(type=type, strict=strict, block=block) + + +# noinspection PyPep8Naming +class handler(_Handler): + def __init__(self, handler_type: Callable[P, HandlerType], **kwargs: P.kwargs): + self._type_ = handler_type + super(handler, self).__init__(**kwargs) + + @property + def _type(self) -> Type[BaseHandler]: + # noinspection PyTypeChecker + return self._type_ + + callback_query = _CallbackQuery + chat_join_request = _ChatJoinRequest + chat_member = _ChatMember + chosen_inline_result = _ChosenInlineResult + command = _Command + inline_query = _InlineQuery + message = _Message + poll_answer = _PollAnswer + pool = _Poll + pre_checkout_query = _PreCheckoutQuery + prefix = _Prefix + shipping_query = _ShippingQuery + string_command = _StringCommand + string_regex = _StringRegex + type = _Type + + +# noinspection PyPep8Naming +class error_handler: + def __init__(self, func: Callable[P, T] = None, *, block: bool = DEFAULT_TRUE): + self._func = func + self._block = block + + def __call__(self, func: Callable[P, T] = None) -> Callable[P, T]: + self._func = func or self._func + setattr(self._func, _NORMAL_HANDLER_ATTR_NAME, {'type': 'error', 'block': self._block}) + return self._func + + +def _entry(func: Callable[P, T]) -> Callable[P, T]: + setattr(func, _CONVERSATION_HANDLER_ATTR_NAME, {'type': 'entry'}) + return func + + +class _State: + def __init__(self, state: Any): + self.state = state + + def __call__(self, func: Callable[P, T] = None) -> Callable[P, T]: + setattr(func, _CONVERSATION_HANDLER_ATTR_NAME, {'type': 'state', 'state': self.state}) + return func + + +def _fallback(func: Callable[P, T]) -> Callable[P, T]: + setattr(func, _CONVERSATION_HANDLER_ATTR_NAME, {'type': 'fallback'}) + return func + + +# noinspection PyPep8Naming +class conversation(_Handler): + entry_point = _entry + state = _State + fallback = _fallback + + +class _Job: + kwargs: Dict = {} + + def __init__( + self, name: str = None, data: object = None, chat_id: int = None, + user_id: int = None, job_kwargs: JSONDict = None, **kwargs + ): + self.name = name + self.data = data + self.chat_id = chat_id + self.user_id = user_id + self.job_kwargs = {} if job_kwargs is None else job_kwargs + self.kwargs = kwargs + + def __call__(self, func: JobCallback) -> JobCallback: + setattr(func, _JOB_ATTR_NAME, { + 'name': self.name, 'data': self.data, 'chat_id': self.chat_id, 'user_id': self.user_id, + 'job_kwargs': self.job_kwargs, 'kwargs': self.kwargs, + 'type': re.sub(r'([A-Z])', lambda x: '_' + x.group().lower(), self.__class__.__name__).lstrip('_') + }) + return func + + +class _RunOnce(_Job): + def __init__( + self, when: TimeType, + data: object = None, name: str = None, chat_id: int = None, user_id: int = None, job_kwargs: JSONDict = None + ): + super().__init__(name, data, chat_id, user_id, job_kwargs, when=when) + + +class _RunRepeating(_Job): + def __init__( + self, interval: Union[float, datetime.timedelta], first: TimeType = None, last: TimeType = None, + data: object = None, name: str = None, chat_id: int = None, user_id: int = None, job_kwargs: JSONDict = None + ): + super().__init__(name, data, chat_id, user_id, job_kwargs, interval=interval, first=first, last=last) + + +class _RunMonthly(_Job): + def __init__( + self, when: datetime.time, day: int, + data: object = None, name: str = None, chat_id: int = None, user_id: int = None, job_kwargs: JSONDict = None + ): + super().__init__(name, data, chat_id, user_id, job_kwargs, when=when, day=day) + + +class _RunDaily(_Job): + def __init__( + self, time: datetime.time, days: Tuple[int, ...] = tuple(range(7)), + data: object = None, name: str = None, chat_id: int = None, user_id: int = None, job_kwargs: JSONDict = None + ): + super().__init__(name, data, chat_id, user_id, job_kwargs, time=time, days=days) + + +class _RunCustom(_Job): + def __init__(self, data: object = None, name: str = None, chat_id: int = None, user_id: int = None, + job_kwargs: JSONDict = None): + super().__init__(name, data, chat_id, user_id, job_kwargs) + + +# noinspection PyPep8Naming +class job: + run_once = _RunOnce + run_repeating = _RunRepeating + run_monthly = _RunMonthly + run_daily = _RunDaily + run_custom = _RunCustom diff --git a/core/quiz/__init__.py b/core/quiz/__init__.py index e8725e7..f2eddf4 100644 --- a/core/quiz/__init__.py +++ b/core/quiz/__init__.py @@ -1,12 +1,12 @@ -from utils.mysql import MySQL -from utils.redisdb import RedisDB -from utils.service.manager import listener_service +from core.base.mysql import MySQL +from core.base.redisdb import RedisDB +from core.service import init_service from .cache import QuizCache from .repositories import QuizRepository from .services import QuizService -@listener_service() +@init_service def create_quiz_service(mysql: MySQL, redis: RedisDB): _repository = QuizRepository(mysql) _cache = QuizCache(redis) diff --git a/core/quiz/base.py b/core/quiz/base.py index 516379d..77bcca7 100644 --- a/core/quiz/base.py +++ b/core/quiz/base.py @@ -1,6 +1,6 @@ from typing import List -from .models import Question, Answer +from .models import Answer, Question def CreatQuestionFromSQLData(data: tuple) -> List[Question]: @@ -10,6 +10,7 @@ def CreatQuestionFromSQLData(data: tuple) -> List[Question]: temp_list.append(Question(question_id, text)) return temp_list + def CreatAnswerFromSQLData(data: tuple) -> List[Answer]: temp_list = [] for temp_data in data: diff --git a/core/quiz/cache.py b/core/quiz/cache.py index ff17175..e4cddf1 100644 --- a/core/quiz/cache.py +++ b/core/quiz/cache.py @@ -2,8 +2,8 @@ from typing import List import ujson -from utils.redisdb import RedisDB -from .models import Question, Answer +from core.base.redisdb import RedisDB +from .models import Answer, Question class QuizCache: diff --git a/core/quiz/models.py b/core/quiz/models.py index 75c48eb..982f7a4 100644 --- a/core/quiz/models.py +++ b/core/quiz/models.py @@ -2,8 +2,8 @@ from typing import List, Optional from sqlmodel import SQLModel, Field, Column, Integer, ForeignKey -from models.baseobject import BaseObject -from models.types import JSONDict +from utils.baseobject import BaseObject +from utils.typedefs import JSONDict class AnswerDB(SQLModel, table=True): diff --git a/core/quiz/repositories.py b/core/quiz/repositories.py index 09a20f5..5096b24 100644 --- a/core/quiz/repositories.py +++ b/core/quiz/repositories.py @@ -2,8 +2,8 @@ from typing import List from sqlmodel import select -from utils.mysql import MySQL -from .models import QuestionDB, AnswerDB +from core.base.mysql import MySQL +from .models import AnswerDB, QuestionDB class QuizRepository: diff --git a/core/quiz/services.py b/core/quiz/services.py index 471dcb3..a5f543f 100644 --- a/core/quiz/services.py +++ b/core/quiz/services.py @@ -2,7 +2,7 @@ import asyncio from typing import List from .cache import QuizCache -from .models import Question, Answer +from .models import Answer, Question from .repositories import QuizRepository diff --git a/core/service.py b/core/service.py new file mode 100644 index 0000000..1085a9f --- /dev/null +++ b/core/service.py @@ -0,0 +1,30 @@ +from abc import ABC, abstractmethod +from types import FunctionType + +from utils.log import logger + +__all__ = ['Service', 'init_service'] + + +class Service(ABC): + @abstractmethod + def __init__(self, *args, **kwargs): + """初始化""" + + async def start(self): + """启动 service""" + + async def stop(self): + """关闭 service""" + + +def init_service(func: FunctionType): + from core.bot import bot + if bot.is_running: + try: + service = bot.init_inject(func) + logger.success(f'服务 "{service.__class__.__name__}" 初始化成功') + bot.add_service(service) + except Exception as e: # pylint: disable=W0703 + logger.exception(f'来自{func.__module__}的服务初始化失败:{e}') + return func diff --git a/core/sign/__init__.py b/core/sign/__init__.py index 3b41879..e396a02 100644 --- a/core/sign/__init__.py +++ b/core/sign/__init__.py @@ -1,10 +1,10 @@ -from utils.mysql import MySQL -from utils.service.manager import listener_service +from core.base.mysql import MySQL +from core.service import init_service from .repositories import SignRepository from .services import SignServices -@listener_service() +@init_service def create_game_strategy_service(mysql: MySQL): _repository = SignRepository(mysql) _service = SignServices(_repository) diff --git a/core/sign/repositories.py b/core/sign/repositories.py index caf4c7f..995694f 100644 --- a/core/sign/repositories.py +++ b/core/sign/repositories.py @@ -1,9 +1,9 @@ -from typing import List, cast, Optional +from typing import List, Optional, cast from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from utils.mysql import MySQL +from core.base.mysql import MySQL from .models import Sign diff --git a/core/template/__init__.py b/core/template/__init__.py index 441825d..c2dca0a 100644 --- a/core/template/__init__.py +++ b/core/template/__init__.py @@ -1,9 +1,9 @@ -from utils.aiobrowser import AioBrowser -from utils.service.manager import listener_service +from core.base.aiobrowser import AioBrowser +from core.service import init_service from .services import TemplateService -@listener_service() +@init_service def create_template_service(browser: AioBrowser): _service = TemplateService(browser) return _service diff --git a/core/template/services.py b/core/template/services.py index 82b16ca..5db01c6 100644 --- a/core/template/services.py +++ b/core/template/services.py @@ -2,12 +2,12 @@ import os import time from typing import Optional -from jinja2 import PackageLoader, Environment, Template +from jinja2 import Environment, PackageLoader, Template from playwright.async_api import ViewportSize -from config import config -from logger import Log -from utils.aiobrowser import AioBrowser +from core.base.aiobrowser import AioBrowser +from core.bot import bot +from utils.log import logger class TemplateService: @@ -22,7 +22,7 @@ class TemplateService: self._jinja2_template = {} def get_template(self, package_path: str, template_name: str) -> Template: - if config.debug: + if bot.config.debug: # DEBUG下 禁止复用 方便查看和修改模板 loader = PackageLoader(self._template_package_name, package_path) jinja2_env = Environment(loader=loader, enable_async=True, autoescape=True) @@ -38,16 +38,26 @@ class TemplateService: self._jinja2_template[package_path + template_name] = jinja2_template return jinja2_template + async def render_async(self, template_path: str, template_name: str, template_data: dict): + """模板渲染 + :param template_path: 模板目录 + :param template_name: 模板文件名 + :param template_data: 模板数据 + """ + start_time = time.time() + template = self.get_template(template_path, template_name) + html = await template.render_async(**template_data) + logger.debug(f"{template_name} 模板渲染使用了 {str(time.time() - start_time)}") + return html + async def render(self, template_path: str, template_name: str, template_data: dict, viewport: ViewportSize, full_page: bool = True, evaluate: Optional[str] = None) -> bytes: - """ - 模板渲染成图片 + """模板渲染成图片 :param template_path: 模板目录 :param template_name: 模板文件名 :param template_data: 模板数据 :param viewport: 截图大小 :param full_page: 是否长截图 - :param auto_escape: 是否自动转义 :param evaluate: 页面加载后运行的 js :return: """ @@ -55,7 +65,7 @@ class TemplateService: template = self.get_template(template_path, template_name) template_data["res_path"] = f"file://{self._current_dir}" html = await template.render_async(**template_data) - Log.debug(f"{template_name} 模板渲染使用了 {str(time.time() - start_time)}") + logger.debug(f"{template_name} 模板渲染使用了 {str(time.time() - start_time)}") browser = await self._browser.get_browser() start_time = time.time() page = await browser.new_page(viewport=viewport) @@ -65,5 +75,5 @@ class TemplateService: await page.evaluate(evaluate) png_data = await page.screenshot(full_page=full_page) await page.close() - Log.debug(f"{template_name} 图片渲染使用了 {str(time.time() - start_time)}") + logger.debug(f"{template_name} 图片渲染使用了 {str(time.time() - start_time)}") return png_data diff --git a/core/user/__init__.py b/core/user/__init__.py index 1286bb2..2189d23 100644 --- a/core/user/__init__.py +++ b/core/user/__init__.py @@ -1,10 +1,10 @@ -from utils.mysql import MySQL -from utils.service.manager import listener_service +from core.base.mysql import MySQL +from core.service import init_service from .repositories import UserRepository from .services import UserService -@listener_service() +@init_service def create_user_service(mysql: MySQL): _repository = UserRepository(mysql) _service = UserService(_repository) diff --git a/core/user/error.py b/core/user/error.py new file mode 100644 index 0000000..13b8b9c --- /dev/null +++ b/core/user/error.py @@ -0,0 +1,3 @@ +class UserNotFoundError(Exception): + def __init__(self, user_id): + super().__init__(f"user not found, user_id: {user_id}") diff --git a/core/user/models.py b/core/user/models.py index dd75f6c..94d8157 100644 --- a/core/user/models.py +++ b/core/user/models.py @@ -2,7 +2,7 @@ from typing import Optional from sqlmodel import SQLModel, Field, Enum, Column -from models.base import RegionEnum +from utils.models.base import RegionEnum class User(SQLModel, table=True): diff --git a/core/user/repositories.py b/core/user/repositories.py index 728ca55..694775c 100644 --- a/core/user/repositories.py +++ b/core/user/repositories.py @@ -3,8 +3,8 @@ from typing import cast from sqlalchemy import select from sqlmodel.ext.asyncio.session import AsyncSession -from utils.error import NotFoundError -from utils.mysql import MySQL +from core.base.mysql import MySQL +from .error import UserNotFoundError from .models import User @@ -32,10 +32,5 @@ class UserRepository: async def add_user(self, user: User): async with self.mysql.Session() as session: session = cast(AsyncSession, session) - await session.add(user) - await session.commit() - - -class UserNotFoundError(NotFoundError): - entity_name: str = "User" - entity_value_name: str = "user_id" + session.add(user) + await session.commit() \ No newline at end of file diff --git a/core/wiki/__init__.py b/core/wiki/__init__.py index 078927b..9c97544 100644 --- a/core/wiki/__init__.py +++ b/core/wiki/__init__.py @@ -1,10 +1,10 @@ -from utils.redisdb import RedisDB -from utils.service.manager import listener_service +from core.base.redisdb import RedisDB +from core.service import init_service from .cache import WikiCache from .services import WikiService -@listener_service() +@init_service def create_wiki_service(redis: RedisDB): _cache = WikiCache(redis) _service = WikiService(_cache) diff --git a/core/wiki/cache.py b/core/wiki/cache.py index d9d3c6e..5de0050 100644 --- a/core/wiki/cache.py +++ b/core/wiki/cache.py @@ -1,7 +1,7 @@ import ujson as json -from models.wiki.base import Model -from utils.redisdb import RedisDB +from core.base.redisdb import RedisDB +from modules.wiki.base import Model class WikiCache: diff --git a/core/wiki/services.py b/core/wiki/services.py index 75de4ac..31c953d 100644 --- a/core/wiki/services.py +++ b/core/wiki/services.py @@ -1,9 +1,9 @@ from typing import List, NoReturn, Optional from core.wiki.cache import WikiCache -from logger import Log -from models.wiki.character import Character -from models.wiki.weapon import Weapon +from modules.wiki.character import Character +from modules.wiki.weapon import Weapon +from utils.log import logger class WikiService: @@ -19,7 +19,7 @@ class WikiService: async def refresh_weapon(self) -> NoReturn: weapon_name_list = await Weapon.get_name_list() - Log.info(f"一共找到 {len(weapon_name_list)} 把武器信息") + logger.info(f"一共找到 {len(weapon_name_list)} 把武器信息") weapon_list = [] num = 0 @@ -27,16 +27,16 @@ class WikiService: weapon_list.append(weapon) num += 1 if num % 10 == 0: - Log.info(f"现在已经获取到 {num} 把武器信息") + logger.info(f"现在已经获取到 {num} 把武器信息") - Log.info("写入武器信息到Redis") + logger.info("写入武器信息到Redis") self._weapon_list = weapon_list await self._cache.delete("weapon") await self._cache.set("weapon", [i.json() for i in weapon_list]) async def refresh_characters(self) -> NoReturn: character_name_list = await Character.get_name_list() - Log.info(f"一共找到 {len(character_name_list)} 个角色信息") + logger.info(f"一共找到 {len(character_name_list)} 个角色信息") character_list = [] num = 0 @@ -44,9 +44,9 @@ class WikiService: character_list.append(character) num += 1 if num % 10 == 0: - Log.info(f"现在已经获取到 {num} 个角色信息") + logger.info(f"现在已经获取到 {num} 个角色信息") - Log.info("写入角色信息到Redis") + logger.info("写入角色信息到Redis") self._character_list = character_list await self._cache.delete("characters") await self._cache.set("characters", [i.json() for i in character_list]) @@ -56,12 +56,12 @@ class WikiService: 用于把Redis的缓存全部加载进Python :return: """ - Log.info("正在重新获取Wiki") - Log.info("正在重新获取武器信息") + logger.info("正在重新获取Wiki") + logger.info("正在重新获取武器信息") await self.refresh_weapon() - Log.info("正在重新获取角色信息") + logger.info("正在重新获取角色信息") await self.refresh_characters() - Log.info("刷新成功") + logger.info("刷新成功") async def init(self) -> NoReturn: """ diff --git a/jobs/README.md b/jobs/README.md deleted file mode 100644 index 844e10b..0000000 --- a/jobs/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# jobs 目录 - -## 说明 - -改目录存放 BOT 的工作队列、注册和具体实现 - -## 基础代码 - -``` python -import datetime - -from telegram.ext import CallbackContext - -from logger import Log -from utils.job.manager import listener_jobs_class - -@listener_jobs_class() -class JobTest: - - @classmethod - def build_jobs(cls, job_queue: JobQueue): - test = cls() - # 注册每日执行任务 - # 执行时间为21点45分 - job_queue.run_daily(test.test, datetime.time(21, 45, 00), name="测试Job") - - async def test(self, context: CallbackContext): - Log.info("测试Job[OK]") -``` - -### 注意 - -jobs 模块下的类必须提供 `build_jobs` 类方法作为构建相应处理程序给 `handle.py` - -只需在构建的类前加上 `@listener_jobs_class()` 修饰器即可 diff --git a/jobs/base.py b/jobs/base.py deleted file mode 100644 index f8bb9cf..0000000 --- a/jobs/base.py +++ /dev/null @@ -1,13 +0,0 @@ -from telegram.ext import CallbackContext - - -class BaseJob: - - @staticmethod - def remove_job_if_exists(name: str, context: CallbackContext) -> bool: - current_jobs = context.job_queue.get_jobs_by_name(name) - if not current_jobs: - return False - for job in current_jobs: - job.schedule_removal() - return True diff --git a/jobs/public_cookies.py b/jobs/public_cookies.py deleted file mode 100644 index 136215c..0000000 --- a/jobs/public_cookies.py +++ /dev/null @@ -1,26 +0,0 @@ -import datetime - -from telegram.ext import CallbackContext, JobQueue - -from core.cookies.services import PublicCookiesService -from logger import Log -from utils.job.manager import listener_jobs_class -from utils.service.inject import inject - - -@listener_jobs_class() -class PublicCookies: - - @inject - def __init__(self, public_cookies_service: PublicCookiesService = None): - self.public_cookies_service = public_cookies_service - - @classmethod - def build_jobs(cls, job_queue: JobQueue): - jobs = cls() - job_queue.run_repeating(jobs.refresh, datetime.timedelta(hours=2)) - - async def refresh(self, _: CallbackContext): - Log.info("正在刷新公共Cookies池") - await self.public_cookies_service.refresh() - Log.info("刷新公共Cookies池成功") diff --git a/logger.py b/logger.py deleted file mode 100644 index 5283ca7..0000000 --- a/logger.py +++ /dev/null @@ -1,61 +0,0 @@ -import logging -import os -from logging.handlers import RotatingFileHandler - -import colorlog - -from config import config - -current_path = os.path.realpath(os.getcwd()) -log_path = os.path.join(current_path, "logs") -if not os.path.exists(log_path): - os.mkdir(log_path) -log_file_name = os.path.join(log_path, "log.log") - -log_colors_config = { - "DEBUG": "cyan", - "INFO": "green", - "WARNING": "yellow", - "ERROR": "red", - "CRITICAL": "red", -} - - -class Logger: - def __init__(self): - self.logger = logging.getLogger("TGPaimonBot") - root_logger = logging.getLogger() - root_logger.setLevel(logging.CRITICAL) - if config.debug: - self.logger.setLevel(logging.DEBUG) - else: - self.logger.setLevel(logging.INFO) - self.formatter = colorlog.ColoredFormatter( - "%(log_color)s[%(asctime)s] [%(levelname)s] - %(message)s", log_colors=log_colors_config) - self.formatter2 = logging.Formatter("[%(asctime)s] [%(levelname)s] - %(message)s") - fh = RotatingFileHandler(filename=log_file_name, maxBytes=1024 * 1024 * 5, backupCount=5, - encoding="utf-8") - fh.setFormatter(self.formatter2) - root_logger.addHandler(fh) - - ch = colorlog.StreamHandler() - ch.setFormatter(self.formatter) - root_logger.addHandler(ch) - - def getLogger(self): - return self.logger - - def debug(self, msg, exc_info=None): - self.logger.debug(msg=msg, exc_info=exc_info) - - def info(self, msg, exc_info=None): - self.logger.info(msg=msg, exc_info=exc_info) - - def warning(self, msg, exc_info=None): - self.logger.warning(msg=msg, exc_info=exc_info) - - def error(self, msg, exc_info=None): - self.logger.error(msg=msg, exc_info=exc_info) - - -Log = Logger() diff --git a/main.py b/main.py deleted file mode 100644 index a7f765b..0000000 --- a/main.py +++ /dev/null @@ -1,97 +0,0 @@ -import asyncio -from warnings import filterwarnings - -import pytz -from telegram.ext import Application, Defaults, AIORateLimiter -from telegram.warnings import PTBUserWarning - -from config import config -from logger import Log -from utils.aiobrowser import AioBrowser -from utils.job.register import register_job -from utils.mysql import MySQL -from utils.plugins.register import register_plugin_handlers -from utils.redisdb import RedisDB -from utils.service.manager import ServicesManager - -# 无视相关警告 -# 该警告说明在官方GITHUB的WIKI中Frequently Asked Questions里的What do the per_* settings in ConversationHandler do? -filterwarnings(action="ignore", message=r".*CallbackQueryHandler", category=PTBUserWarning) -filterwarnings(action="ignore", message=r".*Prior to v20.0 the `days` parameter", category=PTBUserWarning) - - -def main() -> None: - Log.info("正在启动项目") - - # 初始化数据库 - Log.info("初始化数据库") - mysql = MySQL(host=config.mysql["host"], user=config.mysql["user"], password=config.mysql["password"], - port=config.mysql["port"], database=config.mysql["database"]) - - # 初始化Redis缓存 - Log.info("初始化Redis缓存") - redis = RedisDB(host=config.redis["host"], port=config.redis["port"], db=config.redis["database"]) - - # 初始化Playwright - Log.info("初始化Playwright") - browser = AioBrowser() - - # 传入服务并启动 - Log.info("正在启动服务") - services = ServicesManager(mysql, redis, browser) - services.refresh_list("core/*") - services.import_module() - services.add_service() - - # 构建BOT - Log.info("构建BOT") - - defaults = Defaults(tzinfo=pytz.timezone("Asia/Shanghai")) - rate_limiter = AIORateLimiter() - - application = Application \ - .builder() \ - .token(config.bot_token) \ - .defaults(defaults) \ - .rate_limiter(rate_limiter) \ - .build() - - register_plugin_handlers(application) - - register_job(application) - - # 启动BOT - try: - Log.info("BOT已经启动 开始处理命令") - # BOT 在退出后默认关闭LOOP 这时候得让LOOP不要关闭 - application.run_polling(close_loop=False) - except (KeyboardInterrupt, SystemExit): - pass - except Exception as exc: - Log.info("BOT执行过程中出现错误") - raise exc - finally: - Log.info("项目收到退出命令 BOT停止处理并退出") - loop = asyncio.get_event_loop() - try: - # 需要关闭数据库连接 - Log.info("正在关闭数据库连接") - loop.run_until_complete(mysql.wait_closed()) - # 关闭Redis连接 - Log.info("正在关闭Redis连接") - loop.run_until_complete(redis.close()) - # 关闭playwright - Log.info("正在关闭Playwright") - loop.run_until_complete(browser.close()) - except (KeyboardInterrupt, SystemExit): - pass - except Exception as exc: - Log.error("关闭必要连接时出现错误 \n", exc) - Log.info("正在关闭loop") - # 关闭LOOP - loop.close() - Log.info("项目已经已结束") - - -if __name__ == '__main__': - main() diff --git a/metadata/README.md b/metadata/README.md index 46023a4..476e0fd 100644 --- a/metadata/README.md +++ b/metadata/README.md @@ -1,5 +1,5 @@ # metadata 目录说明 -| FileName | Introduce | -| :----------: | ------------- | -| shortname.py | 记录短名称MAP | \ No newline at end of file +| FileName | Introduce | +|:------------:|-----------| +| shortname.py | 记录短名称MAP | \ No newline at end of file diff --git a/models/apihelper/metadata/CharactersMap.json b/models/apihelper/metadata/CharactersMap.json deleted file mode 100644 index 40a5227..0000000 --- a/models/apihelper/metadata/CharactersMap.json +++ /dev/null @@ -1,1725 +0,0 @@ -{ - "10000002": { - "Element": "Ice", - "Consts": [ - "UI_Talent_S_Ayaka_01", - "UI_Talent_S_Ayaka_02", - "UI_Talent_U_Ayaka_02", - "UI_Talent_S_Ayaka_03", - "UI_Talent_U_Ayaka_01", - "UI_Talent_S_Ayaka_04" - ], - "SkillOrder": [ - 10024, - 10018, - 10019 - ], - "Skills": { - "10018": "Skill_S_Ayaka_01", - "10019": "Skill_E_Ayaka", - "10024": "Skill_A_01" - }, - "ProudMap": { - "10018": 232, - "10019": 239, - "10024": 231 - }, - "NameTextMapHash": 1006042610, - "SideIconName": "UI_AvatarIcon_Side_Ayaka", - "QualityType": "QUALITY_ORANGE" - }, - "10000003": { - "Element": "Wind", - "Consts": [ - "UI_Talent_S_Qin_01", - "UI_Talent_S_Qin_02", - "UI_Talent_U_Qin_02", - "UI_Talent_S_Qin_03", - "UI_Talent_U_Qin_01", - "UI_Talent_S_Qin_04" - ], - "SkillOrder": [ - 10031, - 10033, - 10034 - ], - "Skills": { - "10031": "Skill_A_01", - "10033": "Skill_S_Qin_02", - "10034": "Skill_E_Qin_01" - }, - "ProudMap": { - "10031": 331, - "10033": 332, - "10034": 339 - }, - "NameTextMapHash": 3221566250, - "SideIconName": "UI_AvatarIcon_Side_Qin", - "QualityType": "QUALITY_ORANGE", - "Costumes": { - "200301": { - "sideIconName": "UI_AvatarIcon_Side_QinCostumeSea", - "icon": "UI_AvatarIcon_QinCostumeSea", - "art": "UI_Costume_QinCostumeSea", - "avatarId": 10000003 - }, - "200302": { - "sideIconName": "UI_AvatarIcon_Side_QinCostumeWic", - "icon": "UI_AvatarIcon_QinCostumeWic", - "art": "UI_Costume_QinCostumeWic", - "avatarId": 10000003 - } - } - }, - "10000005": { - "Element": "Wind", - "Consts": [ - "UI_Talent_S_PlayerWind_01", - "UI_Talent_S_PlayerWind_02", - "UI_Talent_U_PlayerWind_02", - "UI_Talent_S_PlayerWind_03", - "UI_Talent_U_PlayerWind_01", - "UI_Talent_S_PlayerWind_04" - ], - "SkillOrder": [ - 100543, - 10067, - 10068 - ], - "Skills": { - "10067": "Skill_S_PlayerWind_01", - "10068": "Skill_E_PlayerWind_01", - "100543": "Skill_A_01" - }, - "NameTextMapHash": 1533656818, - "ProudMap": { - "10067": 732, - "10068": 739, - "100543": 730 - }, - "SideIconName": "UI_AvatarIcon_Side_PlayerBoy", - "QualityType": "QUALITY_ORANGE" - }, - "10000006": { - "Element": "Electric", - "Consts": [ - "UI_Talent_S_Lisa_01", - "UI_Talent_S_Lisa_02", - "UI_Talent_U_Lisa_02", - "UI_Talent_S_Lisa_03", - "UI_Talent_U_Lisa_01", - "UI_Talent_S_Lisa_04" - ], - "SkillOrder": [ - 10060, - 10061, - 10062 - ], - "Skills": { - "10060": "Skill_A_Catalyst_MD", - "10061": "Skill_S_Lisa_01", - "10062": "Skill_E_Lisa_01" - }, - "ProudMap": { - "10060": 431, - "10061": 432, - "10062": 439 - }, - "NameTextMapHash": 3344622722, - "SideIconName": "UI_AvatarIcon_Side_Lisa", - "QualityType": "QUALITY_PURPLE" - }, - "10000007": { - "Element": "Wind", - "Consts": [ - "UI_Talent_S_PlayerWind_01", - "UI_Talent_S_PlayerWind_02", - "UI_Talent_U_PlayerWind_02", - "UI_Talent_S_PlayerWind_03", - "UI_Talent_U_PlayerWind_01", - "UI_Talent_S_PlayerWind_04" - ], - "SkillOrder": [ - 100553, - 10067, - 10068 - ], - "Skills": { - "10067": "Skill_S_PlayerWind_01", - "10068": "Skill_E_PlayerWind_01", - "100553": "Skill_A_01" - }, - "NameTextMapHash": 3816664530, - "ProudMap": { - "10067": 732, - "10068": 739, - "100553": 731 - }, - "SideIconName": "UI_AvatarIcon_Side_PlayerGirl", - "QualityType": "QUALITY_ORANGE" - }, - "10000014": { - "Element": "Water", - "Consts": [ - "UI_Talent_S_Barbara_01", - "UI_Talent_S_Barbara_02", - "UI_Talent_U_Barbara_02", - "UI_Talent_S_Barbara_03", - "UI_Talent_U_Barbara_01", - "UI_Talent_S_Barbara_04" - ], - "SkillOrder": [ - 10070, - 10071, - 10072 - ], - "Skills": { - "10070": "Skill_A_Catalyst_MD", - "10071": "Skill_S_Barbara_01", - "10072": "Skill_E_Barbara_01" - }, - "ProudMap": { - "10070": 1431, - "10071": 1432, - "10072": 1439 - }, - "NameTextMapHash": 3775299170, - "SideIconName": "UI_AvatarIcon_Side_Barbara", - "QualityType": "QUALITY_PURPLE", - "Costumes": { - "201401": { - "sideIconName": "UI_AvatarIcon_Side_BarbaraCostumeSummertime", - "icon": "UI_AvatarIcon_BarbaraCostumeSummertime", - "art": "UI_Costume_BarbaraCostumeSummertime", - "avatarId": 10000014 - } - } - }, - "10000015": { - "Element": "Ice", - "Consts": [ - "UI_Talent_S_Kaeya_01", - "UI_Talent_S_Kaeya_02", - "UI_Talent_U_Kaeya_01", - "UI_Talent_S_Kaeya_03", - "UI_Talent_U_Kaeya_02", - "UI_Talent_S_Kaeya_04" - ], - "SkillOrder": [ - 10073, - 10074, - 10075 - ], - "Skills": { - "10073": "Skill_A_01", - "10074": "Skill_S_Kaeya_01", - "10075": "Skill_E_Kaeya_01" - }, - "ProudMap": { - "10073": 1531, - "10074": 1532, - "10075": 1539 - }, - "NameTextMapHash": 4119663210, - "SideIconName": "UI_AvatarIcon_Side_Kaeya", - "QualityType": "QUALITY_PURPLE" - }, - "10000016": { - "Element": "Fire", - "Consts": [ - "UI_Talent_S_Diluc_01", - "UI_Talent_S_Diluc_02", - "UI_Talent_U_Diluc_01", - "UI_Talent_S_Diluc_03", - "UI_Talent_U_Diluc_02", - "UI_Talent_S_Diluc_04" - ], - "SkillOrder": [ - 10160, - 10161, - 10165 - ], - "Skills": { - "10160": "Skill_A_04", - "10161": "Skill_S_Diluc_01_01", - "10165": "Skill_E_Diluc_01" - }, - "ProudMap": { - "10160": 1631, - "10161": 1632, - "10165": 1639 - }, - "NameTextMapHash": 3608180322, - "SideIconName": "UI_AvatarIcon_Side_Diluc", - "QualityType": "QUALITY_ORANGE" - }, - "10000020": { - "Element": "Electric", - "Consts": [ - "UI_Talent_S_Razor_01", - "UI_Talent_S_Razor_02", - "UI_Talent_U_Razor_02", - "UI_Talent_S_Razor_03", - "UI_Talent_U_Razor_01", - "UI_Talent_S_Razor_04" - ], - "SkillOrder": [ - 10201, - 10202, - 10203 - ], - "Skills": { - "10201": "Skill_A_04", - "10202": "Skill_S_Razor_01", - "10203": "Skill_E_Razor_01" - }, - "ProudMap": { - "10201": 2031, - "10202": 2032, - "10203": 2039 - }, - "NameTextMapHash": 4160147242, - "SideIconName": "UI_AvatarIcon_Side_Razor", - "QualityType": "QUALITY_PURPLE" - }, - "10000021": { - "Element": "Fire", - "Consts": [ - "UI_Talent_S_Ambor_01", - "UI_Talent_S_Ambor_02", - "UI_Talent_U_Ambor_02", - "UI_Talent_S_Ambor_03", - "UI_Talent_U_Ambor_01", - "UI_Talent_S_Ambor_04" - ], - "SkillOrder": [ - 10041, - 10032, - 10017 - ], - "Skills": { - "10017": "Skill_E_Ambor", - "10032": "Skill_S_Ambor_01", - "10041": "Skill_A_02" - }, - "ProudMap": { - "10017": 2139, - "10032": 2132, - "10041": 2131 - }, - "NameTextMapHash": 1966438658, - "SideIconName": "UI_AvatarIcon_Side_Ambor", - "QualityType": "QUALITY_PURPLE", - "Costumes": { - "202101": { - "sideIconName": "UI_AvatarIcon_Side_AmborCostumeWic", - "icon": "UI_AvatarIcon_AmborCostumeWic", - "art": "UI_Costume_AmborCostumeWic", - "avatarId": 10000021 - } - } - }, - "10000022": { - "Element": "Wind", - "Consts": [ - "UI_Talent_S_Venti_01", - "UI_Talent_S_Venti_02", - "UI_Talent_U_Venti_02", - "UI_Talent_S_Venti_03", - "UI_Talent_U_Venti_01", - "UI_Talent_S_Venti_04" - ], - "SkillOrder": [ - 10221, - 10224, - 10225 - ], - "Skills": { - "10221": "Skill_A_02", - "10224": "Skill_S_Venti_01", - "10225": "Skill_E_Venti_01" - }, - "ProudMap": { - "10221": 2231, - "10224": 2232, - "10225": 2239 - }, - "NameTextMapHash": 2466140362, - "SideIconName": "UI_AvatarIcon_Side_Venti", - "QualityType": "QUALITY_ORANGE" - }, - "10000023": { - "Element": "Fire", - "Consts": [ - "UI_Talent_S_Xiangling_01", - "UI_Talent_S_Xiangling_02", - "UI_Talent_U_Xiangling_02", - "UI_Talent_S_Xiangling_03", - "UI_Talent_U_Xiangling_01", - "UI_Talent_S_Xiangling_04" - ], - "SkillOrder": [ - 10231, - 10232, - 10235 - ], - "Skills": { - "10231": "Skill_A_03", - "10232": "Skill_S_Xiangling_01", - "10235": "Skill_E_Xiangling_01" - }, - "ProudMap": { - "10231": 2331, - "10232": 2332, - "10235": 2339 - }, - "NameTextMapHash": 1130996346, - "SideIconName": "UI_AvatarIcon_Side_Xiangling", - "QualityType": "QUALITY_PURPLE" - }, - "10000024": { - "Element": "Electric", - "Consts": [ - "UI_Talent_S_Beidou_02", - "UI_Talent_S_Beidou_01", - "UI_Talent_U_Beidou_01", - "UI_Talent_S_Beidou_03", - "UI_Talent_U_Beidou_02", - "UI_Talent_S_Beidou_04" - ], - "SkillOrder": [ - 10241, - 10242, - 10245 - ], - "Skills": { - "10241": "Skill_A_04", - "10242": "Skill_S_Beidou_01", - "10245": "Skill_E_Beidou_01" - }, - "ProudMap": { - "10241": 2431, - "10242": 2432, - "10245": 2439 - }, - "NameTextMapHash": 2646367730, - "SideIconName": "UI_AvatarIcon_Side_Beidou", - "QualityType": "QUALITY_PURPLE" - }, - "10000025": { - "Element": "Water", - "Consts": [ - "UI_Talent_S_Xingqiu_01", - "UI_Talent_S_Xingqiu_02", - "UI_Talent_U_Xingqiu_01", - "UI_Talent_S_Xingqiu_03", - "UI_Talent_U_Xingqiu_02", - "UI_Talent_S_Xingqiu_04" - ], - "SkillOrder": [ - 10381, - 10382, - 10385 - ], - "Skills": { - "10381": "Skill_A_01", - "10382": "Skill_S_Xingqiu_01", - "10385": "Skill_E_Xingqiu_01" - }, - "ProudMap": { - "10381": 2531, - "10382": 2532, - "10385": 2539 - }, - "NameTextMapHash": 4197635682, - "SideIconName": "UI_AvatarIcon_Side_Xingqiu", - "QualityType": "QUALITY_PURPLE" - }, - "10000026": { - "Element": "Wind", - "Consts": [ - "UI_Talent_S_Xiao_01", - "UI_Talent_S_Xiao_02", - "UI_Talent_U_Xiao_01", - "UI_Talent_S_Xiao_03", - "UI_Talent_U_Xiao_02", - "UI_Talent_S_Xiao_04" - ], - "SkillOrder": [ - 10261, - 10262, - 10265 - ], - "Skills": { - "10261": "Skill_A_03", - "10262": "Skill_S_Xiao_01", - "10265": "Skill_E_Xiao_01" - }, - "ProudMap": { - "10261": 2631, - "10262": 2632, - "10265": 2639 - }, - "NameTextMapHash": 1021947690, - "SideIconName": "UI_AvatarIcon_Side_Xiao", - "QualityType": "QUALITY_ORANGE" - }, - "10000027": { - "Element": "Rock", - "Consts": [ - "UI_Talent_S_Ningguang_01", - "UI_Talent_S_Ningguang_05", - "UI_Talent_U_Ningguang_02", - "UI_Talent_S_Ningguang_03", - "UI_Talent_U_Ningguang_01", - "UI_Talent_S_Ningguang_04" - ], - "SkillOrder": [ - 10271, - 10272, - 10274 - ], - "Skills": { - "10271": "Skill_A_Catalyst_MD", - "10272": "Skill_S_Ningguang_01", - "10274": "Skill_E_Ningguang_01" - }, - "ProudMap": { - "10271": 2731, - "10272": 2732, - "10274": 2739 - }, - "NameTextMapHash": 4127888970, - "SideIconName": "UI_AvatarIcon_Side_Ningguang", - "QualityType": "QUALITY_PURPLE", - "Costumes": { - "202701": { - "sideIconName": "UI_AvatarIcon_Side_NingguangCostumeFloral", - "icon": "UI_AvatarIcon_NingguangCostumeFloral", - "art": "UI_Costume_NingguangCostumeFloral", - "avatarId": 10000027 - } - } - }, - "10000029": { - "Element": "Fire", - "Consts": [ - "UI_Talent_S_Klee_01", - "UI_Talent_S_Klee_02", - "UI_Talent_U_Klee_01", - "UI_Talent_S_Klee_03", - "UI_Talent_U_Klee_02", - "UI_Talent_S_Klee_04" - ], - "SkillOrder": [ - 10291, - 10292, - 10295 - ], - "Skills": { - "10291": "Skill_A_Catalyst_MD", - "10292": "Skill_S_Klee_01", - "10295": "Skill_E_Klee_01" - }, - "ProudMap": { - "10291": 2931, - "10292": 2932, - "10295": 2939 - }, - "NameTextMapHash": 3339083250, - "SideIconName": "UI_AvatarIcon_Side_Klee", - "QualityType": "QUALITY_ORANGE" - }, - "10000030": { - "Element": "Rock", - "Consts": [ - "UI_Talent_S_Zhongli_01", - "UI_Talent_S_Zhongli_02", - "UI_Talent_U_Zhongli_01", - "UI_Talent_S_Zhongli_03", - "UI_Talent_U_Zhongli_02", - "UI_Talent_S_Zhongli_04" - ], - "SkillOrder": [ - 10301, - 10302, - 10303 - ], - "Skills": { - "10301": "Skill_A_03", - "10302": "Skill_S_Zhongli_01", - "10303": "Skill_E_Zhongli_01" - }, - "ProudMap": { - "10301": 3031, - "10302": 3032, - "10303": 3039 - }, - "NameTextMapHash": 3862787418, - "SideIconName": "UI_AvatarIcon_Side_Zhongli", - "QualityType": "QUALITY_ORANGE" - }, - "10000031": { - "Element": "Electric", - "Consts": [ - "UI_Talent_S_Fischl_01", - "UI_Talent_S_Fischl_02", - "UI_Talent_U_Fischl_01", - "UI_Talent_S_Fischl_03", - "UI_Talent_U_Fischl_02", - "UI_Talent_S_Fischl_04" - ], - "SkillOrder": [ - 10311, - 10312, - 10313 - ], - "Skills": { - "10311": "Skill_A_02", - "10312": "Skill_S_Fischl_01", - "10313": "Skill_E_Fischl_01" - }, - "ProudMap": { - "10311": 3131, - "10312": 3132, - "10313": 3139 - }, - "NameTextMapHash": 3277782506, - "SideIconName": "UI_AvatarIcon_Side_Fischl", - "QualityType": "QUALITY_PURPLE" - }, - "10000032": { - "Element": "Fire", - "Consts": [ - "UI_Talent_S_Bennett_01", - "UI_Talent_S_Bennett_02", - "UI_Talent_U_Bennett_01", - "UI_Talent_S_Bennett_03", - "UI_Talent_U_Bennett_02", - "UI_Talent_S_Bennett_04" - ], - "SkillOrder": [ - 10321, - 10322, - 10323 - ], - "Skills": { - "10321": "Skill_A_01", - "10322": "Skill_S_Bennett_01", - "10323": "Skill_E_Bennett_01" - }, - "ProudMap": { - "10321": 3231, - "10322": 3232, - "10323": 3239 - }, - "NameTextMapHash": 968893378, - "SideIconName": "UI_AvatarIcon_Side_Bennett", - "QualityType": "QUALITY_PURPLE" - }, - "10000033": { - "Element": "Water", - "Consts": [ - "UI_Talent_S_Tartaglia_01", - "UI_Talent_S_Tartaglia_02", - "UI_Talent_U_Tartaglia_01", - "UI_Talent_S_Tartaglia_05", - "UI_Talent_U_Tartaglia_02", - "UI_Talent_S_Tartaglia_04" - ], - "SkillOrder": [ - 10331, - 10332, - 10333 - ], - "Skills": { - "10331": "Skill_A_02", - "10332": "Skill_S_Tartaglia_01", - "10333": "Skill_E_Tartaglia_01" - }, - "ProudMap": { - "10331": 3331, - "10332": 3332, - "10333": 3339 - }, - "NameTextMapHash": 3847143266, - "SideIconName": "UI_AvatarIcon_Side_Tartaglia", - "QualityType": "QUALITY_ORANGE" - }, - "10000034": { - "Element": "Rock", - "Consts": [ - "UI_Talent_S_Noel_01", - "UI_Talent_S_Noel_02", - "UI_Talent_U_Noel_01", - "UI_Talent_S_Noel_03", - "UI_Talent_U_Noel_02", - "UI_Talent_S_Noel_04" - ], - "SkillOrder": [ - 10341, - 10342, - 10343 - ], - "Skills": { - "10341": "Skill_A_04", - "10342": "Skill_S_Noel_01", - "10343": "Skill_E_Noel_01" - }, - "ProudMap": { - "10341": 3431, - "10342": 3432, - "10343": 3439 - }, - "NameTextMapHash": 1921418842, - "SideIconName": "UI_AvatarIcon_Side_Noel", - "QualityType": "QUALITY_PURPLE" - }, - "10000035": { - "Element": "Ice", - "Consts": [ - "UI_Talent_S_Qiqi_01", - "UI_Talent_S_Qiqi_02", - "UI_Talent_U_Qiqi_01", - "UI_Talent_S_Qiqi_03", - "UI_Talent_U_Qiqi_02", - "UI_Talent_S_Qiqi_04" - ], - "SkillOrder": [ - 10351, - 10352, - 10353 - ], - "Skills": { - "10351": "Skill_A_01", - "10352": "Skill_S_Qiqi_01", - "10353": "Skill_E_Qiqi_01" - }, - "ProudMap": { - "10351": 3531, - "10352": 3532, - "10353": 3539 - }, - "NameTextMapHash": 168956722, - "SideIconName": "UI_AvatarIcon_Side_Qiqi", - "QualityType": "QUALITY_ORANGE" - }, - "10000036": { - "Element": "Ice", - "Consts": [ - "UI_Talent_S_Chongyun_01", - "UI_Talent_S_Chongyun_02", - "UI_Talent_U_Chongyun_01", - "UI_Talent_S_Chongyun_03", - "UI_Talent_U_Chongyun_02", - "UI_Talent_S_Chongyun_04" - ], - "SkillOrder": [ - 10401, - 10402, - 10403 - ], - "Skills": { - "10401": "Skill_A_04", - "10402": "Skill_S_Chongyun_01", - "10403": "Skill_E_Chongyun_01" - }, - "ProudMap": { - "10401": 3631, - "10402": 3632, - "10403": 3639 - }, - "NameTextMapHash": 2876340530, - "SideIconName": "UI_AvatarIcon_Side_Chongyun", - "QualityType": "QUALITY_PURPLE" - }, - "10000037": { - "Element": "Ice", - "Consts": [ - "UI_Talent_S_Ganyu_01", - "UI_Talent_S_Ganyu_02", - "UI_Talent_U_Ganyu_01", - "UI_Talent_S_Ganyu_03", - "UI_Talent_U_Ganyu_02", - "UI_Talent_S_Ganyu_04" - ], - "SkillOrder": [ - 10371, - 10372, - 10373 - ], - "Skills": { - "10371": "Skill_A_02", - "10372": "Skill_S_Ganyu_01", - "10373": "Skill_E_Ganyu_01" - }, - "ProudMap": { - "10371": 3731, - "10372": 3732, - "10373": 3739 - }, - "NameTextMapHash": 2679781122, - "SideIconName": "UI_AvatarIcon_Side_Ganyu", - "QualityType": "QUALITY_ORANGE" - }, - "10000038": { - "Element": "Rock", - "Consts": [ - "UI_Talent_S_Albedo_01", - "UI_Talent_S_Albedo_02", - "UI_Talent_U_Albedo_01", - "UI_Talent_S_Albedo_03", - "UI_Talent_U_Albedo_02", - "UI_Talent_S_Albedo_04" - ], - "SkillOrder": [ - 10386, - 10387, - 10388 - ], - "Skills": { - "10386": "Skill_A_01", - "10387": "Skill_S_Albedo_01", - "10388": "Skill_E_Albedo_01" - }, - "ProudMap": { - "10386": 3831, - "10387": 3832, - "10388": 3839 - }, - "NameTextMapHash": 4108620722, - "SideIconName": "UI_AvatarIcon_Side_Albedo", - "QualityType": "QUALITY_ORANGE" - }, - "10000039": { - "Element": "Ice", - "Consts": [ - "UI_Talent_S_Diona_01", - "UI_Talent_S_Diona_02", - "UI_Talent_U_Diona_01", - "UI_Talent_S_Diona_03", - "UI_Talent_U_Diona_02", - "UI_Talent_S_Diona_04" - ], - "SkillOrder": [ - 10391, - 10392, - 10395 - ], - "Skills": { - "10391": "Skill_A_02", - "10392": "Skill_S_Diona_01", - "10395": "Skill_E_Diona_01" - }, - "ProudMap": { - "10391": 3931, - "10392": 3932, - "10395": 3939 - }, - "NameTextMapHash": 1468367538, - "SideIconName": "UI_AvatarIcon_Side_Diona", - "QualityType": "QUALITY_PURPLE" - }, - "10000041": { - "Element": "Water", - "Consts": [ - "UI_Talent_S_Mona_01", - "UI_Talent_S_Mona_02", - "UI_Talent_U_Mona_01", - "UI_Talent_S_Mona_03", - "UI_Talent_U_Mona_02", - "UI_Talent_S_Mona_04" - ], - "SkillOrder": [ - 10411, - 10412, - 10415 - ], - "Skills": { - "10411": "Skill_A_Catalyst_MD", - "10412": "Skill_S_Mona_01", - "10415": "Skill_E_Mona_01" - }, - "ProudMap": { - "10411": 4131, - "10412": 4132, - "10415": 4139 - }, - "NameTextMapHash": 1113306282, - "SideIconName": "UI_AvatarIcon_Side_Mona", - "QualityType": "QUALITY_ORANGE", - "Costumes": { - "204101": { - "sideIconName": "UI_AvatarIcon_Side_MonaCostumeWic", - "icon": "UI_AvatarIcon_MonaCostumeWic", - "art": "UI_Costume_MonaCostumeWic", - "avatarId": 10000041 - } - } - }, - "10000042": { - "Element": "Electric", - "Consts": [ - "UI_Talent_S_Keqing_01", - "UI_Talent_S_Keqing_02", - "UI_Talent_U_Keqing_01", - "UI_Talent_S_Keqing_03", - "UI_Talent_U_Keqing_02", - "UI_Talent_S_Keqing_04" - ], - "SkillOrder": [ - 10421, - 10422, - 10425 - ], - "Skills": { - "10421": "Skill_A_01", - "10422": "Skill_S_Keqing_01", - "10425": "Skill_E_Keqing_01" - }, - "ProudMap": { - "10421": 4231, - "10422": 4232, - "10425": 4239 - }, - "NameTextMapHash": 1864015138, - "SideIconName": "UI_AvatarIcon_Side_Keqing", - "QualityType": "QUALITY_ORANGE", - "Costumes": { - "204201": { - "sideIconName": "UI_AvatarIcon_Side_KeqingCostumeFeather", - "icon": "UI_AvatarIcon_KeqingCostumeFeather", - "art": "UI_Costume_KeqingCostumeFeather", - "avatarId": 10000042 - } - } - }, - "10000043": { - "Element": "Wind", - "Consts": [ - "UI_Talent_S_Sucrose_01", - "UI_Talent_S_Sucrose_02", - "UI_Talent_U_Sucrose_01", - "UI_Talent_S_Sucrose_03", - "UI_Talent_U_Sucrose_02", - "UI_Talent_S_Sucrose_04" - ], - "SkillOrder": [ - 10431, - 10432, - 10435 - ], - "Skills": { - "10431": "Skill_A_Catalyst_MD", - "10432": "Skill_S_Sucrose_01", - "10435": "Skill_E_Sucrose_01" - }, - "ProudMap": { - "10431": 4331, - "10432": 4332, - "10435": 4339 - }, - "NameTextMapHash": 1053433018, - "SideIconName": "UI_AvatarIcon_Side_Sucrose", - "QualityType": "QUALITY_PURPLE" - }, - "10000044": { - "Element": "Fire", - "Consts": [ - "UI_Talent_S_Xinyan_01", - "UI_Talent_S_Xinyan_02", - "UI_Talent_U_Xinyan_01", - "UI_Talent_S_Xinyan_03", - "UI_Talent_U_Xinyan_02", - "UI_Talent_S_Xinyan_04" - ], - "SkillOrder": [ - 10441, - 10442, - 10443 - ], - "Skills": { - "10441": "Skill_A_04", - "10442": "Skill_S_Xinyan_01", - "10443": "Skill_E_Xinyan_01" - }, - "ProudMap": { - "10441": 4431, - "10442": 4432, - "10443": 4439 - }, - "NameTextMapHash": 4273845410, - "SideIconName": "UI_AvatarIcon_Side_Xinyan", - "QualityType": "QUALITY_PURPLE" - }, - "10000045": { - "Element": "Ice", - "Consts": [ - "UI_Talent_S_Rosaria_01", - "UI_Talent_S_Rosaria_02", - "UI_Talent_U_Rosaria_01", - "UI_Talent_S_Rosaria_03", - "UI_Talent_U_Rosaria_02", - "UI_Talent_S_Rosaria_04" - ], - "SkillOrder": [ - 10451, - 10452, - 10453 - ], - "Skills": { - "10451": "Skill_A_03", - "10452": "Skill_S_Rosaria_01", - "10453": "Skill_E_Rosaria_01" - }, - "ProudMap": { - "10451": 4531, - "10452": 4532, - "10453": 4539 - }, - "NameTextMapHash": 4260733330, - "SideIconName": "UI_AvatarIcon_Side_Rosaria", - "QualityType": "QUALITY_PURPLE", - "Costumes": { - "204501": { - "sideIconName": "UI_AvatarIcon_Side_RosariaCostumeWic", - "icon": "UI_AvatarIcon_RosariaCostumeWic", - "art": "UI_Costume_RosariaCostumeWic", - "avatarId": 10000045 - } - } - }, - "10000046": { - "Element": "Fire", - "Consts": [ - "UI_Talent_S_Hutao_03", - "UI_Talent_S_Hutao_01", - "UI_Talent_U_Hutao_01", - "UI_Talent_S_Hutao_02", - "UI_Talent_U_Hutao_02", - "UI_Talent_S_Hutao_04" - ], - "SkillOrder": [ - 10461, - 10462, - 10463 - ], - "Skills": { - "10461": "Skill_A_03", - "10462": "Skill_S_Hutao_01", - "10463": "Skill_E_Hutao_01" - }, - "ProudMap": { - "10461": 4631, - "10462": 4632, - "10463": 4639 - }, - "NameTextMapHash": 1940919994, - "SideIconName": "UI_AvatarIcon_Side_Hutao", - "QualityType": "QUALITY_ORANGE" - }, - "10000047": { - "Element": "Wind", - "Consts": [ - "UI_Talent_S_Kazuha_01", - "UI_Talent_S_Kazuha_02", - "UI_Talent_U_Kazuha_01", - "UI_Talent_S_Kazuha_03", - "UI_Talent_U_Kazuha_02", - "UI_Talent_S_Kazuha_04" - ], - "SkillOrder": [ - 10471, - 10472, - 10475 - ], - "Skills": { - "10471": "Skill_A_01", - "10472": "Skill_S_Kazuha_01", - "10475": "Skill_E_Kazuha_01" - }, - "ProudMap": { - "10471": 4731, - "10472": 4732, - "10475": 4739 - }, - "NameTextMapHash": 88505754, - "SideIconName": "UI_AvatarIcon_Side_Kazuha", - "QualityType": "QUALITY_ORANGE" - }, - "10000048": { - "Element": "Fire", - "Consts": [ - "UI_Talent_S_Feiyan_01", - "UI_Talent_S_Feiyan_02", - "UI_Talent_U_Feiyan_01", - "UI_Talent_S_Feiyan_03", - "UI_Talent_U_Feiyan_02", - "UI_Talent_S_Feiyan_04" - ], - "SkillOrder": [ - 10481, - 10482, - 10485 - ], - "Skills": { - "10481": "Skill_A_Catalyst_MD", - "10482": "Skill_S_Feiyan_01", - "10485": "Skill_E_Feiyan_01" - }, - "ProudMap": { - "10481": 4831, - "10482": 4832, - "10485": 4839 - }, - "NameTextMapHash": 697277554, - "SideIconName": "UI_AvatarIcon_Side_Feiyan", - "QualityType": "QUALITY_PURPLE" - }, - "10000049": { - "Element": "Fire", - "Consts": [ - "UI_Talent_S_Yoimiya_01", - "UI_Talent_S_Yoimiya_02", - "UI_Talent_U_Yoimiya_01", - "UI_Talent_S_Yoimiya_03", - "UI_Talent_U_Yoimiya_02", - "UI_Talent_S_Yoimiya_04" - ], - "SkillOrder": [ - 10491, - 10492, - 10495 - ], - "Skills": { - "10491": "Skill_A_02", - "10492": "Skill_S_Yoimiya_01", - "10495": "Skill_E_Yoimiya_01" - }, - "ProudMap": { - "10491": 4931, - "10492": 4932, - "10495": 4939 - }, - "NameTextMapHash": 2504399314, - "SideIconName": "UI_AvatarIcon_Side_Yoimiya", - "QualityType": "QUALITY_ORANGE" - }, - "10000050": { - "Element": "Fire", - "Consts": [ - "UI_Talent_S_Tohma_01", - "UI_Talent_S_Tohma_02", - "UI_Talent_U_Tohma_01", - "UI_Talent_S_Tohma_03", - "UI_Talent_U_Tohma_02", - "UI_Talent_S_Tohma_04" - ], - "SkillOrder": [ - 10501, - 10502, - 10505 - ], - "Skills": { - "10501": "Skill_A_03", - "10502": "Skill_S_Tohma_01", - "10505": "Skill_E_Tohma_01" - }, - "ProudMap": { - "10501": 5031, - "10502": 5032, - "10505": 5039 - }, - "NameTextMapHash": 3555115602, - "SideIconName": "UI_AvatarIcon_Side_Tohma", - "QualityType": "QUALITY_PURPLE" - }, - "10000051": { - "Element": "Ice", - "Consts": [ - "UI_Talent_S_Eula_02", - "UI_Talent_S_Eula_01", - "UI_Talent_U_Eula_01", - "UI_Talent_S_Eula_03", - "UI_Talent_U_Eula_02", - "UI_Talent_S_Eula_04" - ], - "SkillOrder": [ - 10511, - 10512, - 10515 - ], - "Skills": { - "10511": "Skill_A_04", - "10512": "Skill_S_Eula_01", - "10515": "Skill_E_Eula_01" - }, - "ProudMap": { - "10511": 5131, - "10512": 5132, - "10515": 5139 - }, - "NameTextMapHash": 3717667418, - "SideIconName": "UI_AvatarIcon_Side_Eula", - "QualityType": "QUALITY_ORANGE" - }, - "10000052": { - "Element": "Electric", - "Consts": [ - "UI_Talent_S_Shougun_01", - "UI_Talent_S_Shougun_03", - "UI_Talent_U_Shougun_02", - "UI_Talent_S_Shougun_02", - "UI_Talent_U_Shougun_01", - "UI_Talent_S_Shougun_04" - ], - "SkillOrder": [ - 10521, - 10522, - 10525 - ], - "Skills": { - "10521": "Skill_A_03", - "10522": "Skill_S_Shougun_01", - "10525": "Skill_E_Shougun_01" - }, - "ProudMap": { - "10521": 5231, - "10522": 5232, - "10525": 5239 - }, - "NameTextMapHash": 3024507506, - "SideIconName": "UI_AvatarIcon_Side_Shougun", - "QualityType": "QUALITY_ORANGE" - }, - "10000053": { - "Element": "Wind", - "Consts": [ - "UI_Talent_S_Sayu_01", - "UI_Talent_S_Sayu_02", - "UI_Talent_U_Sayu_02", - "UI_Talent_S_Sayu_03", - "UI_Talent_U_Sayu_01", - "UI_Talent_S_Sayu_04" - ], - "SkillOrder": [ - 10531, - 10532, - 10535 - ], - "Skills": { - "10531": "Skill_A_04", - "10532": "Skill_S_Sayu_01", - "10535": "Skill_E_Sayu_01" - }, - "ProudMap": { - "10531": 5331, - "10532": 5332, - "10535": 5339 - }, - "NameTextMapHash": 2388785242, - "SideIconName": "UI_AvatarIcon_Side_Sayu", - "QualityType": "QUALITY_PURPLE" - }, - "10000054": { - "Element": "Water", - "Consts": [ - "UI_Talent_S_Kokomi_01", - "UI_Talent_S_Kokomi_02", - "UI_Talent_U_Kokomi_02", - "UI_Talent_S_Kokomi_03", - "UI_Talent_U_Kokomi_01", - "UI_Talent_S_Kokomi_04" - ], - "SkillOrder": [ - 10541, - 10542, - 10545 - ], - "Skills": { - "10541": "Skill_A_Catalyst_MD", - "10542": "Skill_S_Kokomi_01", - "10545": "Skill_E_Kokomi_01" - }, - "ProudMap": { - "10541": 5431, - "10542": 5432, - "10545": 5439 - }, - "NameTextMapHash": 3914045794, - "SideIconName": "UI_AvatarIcon_Side_Kokomi", - "QualityType": "QUALITY_ORANGE" - }, - "10000055": { - "Element": "Rock", - "Consts": [ - "UI_Talent_S_Gorou_01", - "UI_Talent_S_Gorou_02", - "UI_Talent_U_Gorou_01", - "UI_Talent_S_Gorou_03", - "UI_Talent_U_Gorou_02", - "UI_Talent_S_Gorou_04" - ], - "SkillOrder": [ - 10551, - 10552, - 10555 - ], - "Skills": { - "10551": "Skill_A_02", - "10552": "Skill_S_Gorou_01", - "10555": "Skill_E_Gorou_01" - }, - "ProudMap": { - "10551": 5531, - "10552": 5532, - "10555": 5539 - }, - "NameTextMapHash": 3400133546, - "SideIconName": "UI_AvatarIcon_Side_Gorou", - "QualityType": "QUALITY_PURPLE" - }, - "10000056": { - "Element": "Electric", - "Consts": [ - "UI_Talent_S_Sara_05", - "UI_Talent_S_Sara_02", - "UI_Talent_U_Sara_02", - "UI_Talent_S_Sara_03", - "UI_Talent_U_Sara_01", - "UI_Talent_S_Sara_04" - ], - "SkillOrder": [ - 10561, - 10562, - 10565 - ], - "Skills": { - "10561": "Skill_A_02", - "10562": "Skill_S_Sara_01", - "10565": "Skill_E_Sara_01" - }, - "ProudMap": { - "10561": 5631, - "10562": 5632, - "10565": 5639 - }, - "NameTextMapHash": 1483922610, - "SideIconName": "UI_AvatarIcon_Side_Sara", - "QualityType": "QUALITY_PURPLE" - }, - "10000057": { - "Element": "Rock", - "Consts": [ - "UI_Talent_S_Itto_01", - "UI_Talent_S_Itto_02", - "UI_Talent_U_Itto_01", - "UI_Talent_S_Itto_03", - "UI_Talent_U_Itto_02", - "UI_Talent_S_Itto_04" - ], - "SkillOrder": [ - 10571, - 10572, - 10575 - ], - "Skills": { - "10571": "Skill_A_04", - "10572": "Skill_S_Itto_01", - "10575": "Skill_E_Itto_01" - }, - "ProudMap": { - "10571": 5731, - "10572": 5732, - "10575": 5739 - }, - "NameTextMapHash": 3068316954, - "SideIconName": "UI_AvatarIcon_Side_Itto", - "QualityType": "QUALITY_ORANGE" - }, - "10000058": { - "Element": "Electric", - "Consts": [ - "UI_Talent_S_Yae_01", - "UI_Talent_S_Yae_02", - "UI_Talent_U_Yae_01", - "UI_Talent_S_Yae_03", - "UI_Talent_U_Yae_02", - "UI_Talent_S_Yae_04" - ], - "SkillOrder": [ - 10581, - 10582, - 10585 - ], - "Skills": { - "10581": "Skill_A_Catalyst_MD", - "10582": "Skill_S_Yae_01", - "10585": "Skill_E_Yae_01" - }, - "ProudMap": { - "10581": 5831, - "10582": 5832, - "10585": 5839 - }, - "NameTextMapHash": 2713453234, - "SideIconName": "UI_AvatarIcon_Side_Yae", - "QualityType": "QUALITY_ORANGE" - }, - "10000060": { - "Element": "Water", - "Consts": [ - "UI_Talent_S_Yelan_01", - "UI_Talent_S_Yelan_02", - "UI_Talent_U_Yelan_01", - "UI_Talent_S_Yelan_03", - "UI_Talent_U_Yelan_02", - "UI_Talent_S_Yelan_04" - ], - "SkillOrder": [ - 10606, - 10607, - 10610 - ], - "Skills": { - "10606": "Skill_A_02", - "10607": "Skill_S_Yelan_01", - "10610": "Skill_E_Yelan_01" - }, - "ProudMap": { - "10606": 6031, - "10607": 6032, - "10610": 6039 - }, - "NameTextMapHash": 2848374378, - "SideIconName": "UI_AvatarIcon_Side_Yelan", - "QualityType": "QUALITY_ORANGE" - }, - "10000062": { - "Element": "Ice", - "Consts": [ - "UI_Talent_S_Aloy_Lock", - "UI_Talent_S_Aloy_Lock", - "UI_Talent_S_Aloy_Lock", - "UI_Talent_S_Aloy_Lock", - "UI_Talent_S_Aloy_Lock", - "UI_Talent_S_Aloy_Lock" - ], - "SkillOrder": [ - 10621, - 10622, - 10625 - ], - "Skills": { - "10621": "Skill_A_02", - "10622": "Skill_S_Aloy_01", - "10625": "Skill_E_Aloy_01" - }, - "ProudMap": { - "10621": 6231, - "10622": 6232, - "10625": 6239 - }, - "NameTextMapHash": 3689108098, - "SideIconName": "UI_AvatarIcon_Side_Aloy", - "QualityType": "QUALITY_ORANGE_SP" - }, - "10000063": { - "Element": "Ice", - "Consts": [ - "UI_Talent_S_Shenhe_02", - "UI_Talent_S_Shenhe_01", - "UI_Talent_U_Shenhe_01", - "UI_Talent_S_Shenhe_03", - "UI_Talent_U_Shenhe_02", - "UI_Talent_S_Shenhe_04" - ], - "SkillOrder": [ - 10631, - 10632, - 10635 - ], - "Skills": { - "10631": "Skill_A_03", - "10632": "Skill_S_Shenhe_01", - "10635": "Skill_E_Shenhe_01" - }, - "ProudMap": { - "10631": 6331, - "10632": 6332, - "10635": 6339 - }, - "NameTextMapHash": 334242634, - "SideIconName": "UI_AvatarIcon_Side_Shenhe", - "QualityType": "QUALITY_ORANGE" - }, - "10000064": { - "Element": "Rock", - "Consts": [ - "UI_Talent_S_Yunjin_01", - "UI_Talent_S_Yunjin_02", - "UI_Talent_U_Yunjin_01", - "UI_Talent_S_Yunjin_03", - "UI_Talent_U_Yunjin_02", - "UI_Talent_S_Yunjin_04" - ], - "SkillOrder": [ - 10641, - 10642, - 10643 - ], - "Skills": { - "10641": "Skill_A_03", - "10642": "Skill_S_Yunjin_01", - "10643": "Skill_E_Yunjin_01" - }, - "ProudMap": { - "10641": 6431, - "10642": 6432, - "10643": 6439 - }, - "NameTextMapHash": 655825874, - "SideIconName": "UI_AvatarIcon_Side_Yunjin", - "QualityType": "QUALITY_PURPLE" - }, - "10000065": { - "Element": "Electric", - "Consts": [ - "UI_Talent_S_Shinobu_01", - "UI_Talent_S_Shinobu_02", - "UI_Talent_U_Shinobu_01", - "UI_Talent_S_Shinobu_03", - "UI_Talent_U_Shinobu_02", - "UI_Talent_S_Shinobu_04" - ], - "SkillOrder": [ - 10651, - 10652, - 10655 - ], - "Skills": { - "10651": "Skill_A_01", - "10652": "Skill_S_Shinobu_01", - "10655": "Skill_E_Shinobu_01" - }, - "ProudMap": { - "10651": 6531, - "10652": 6532, - "10655": 6539 - }, - "NameTextMapHash": 1940821986, - "SideIconName": "UI_AvatarIcon_Side_Shinobu", - "QualityType": "QUALITY_PURPLE" - }, - "10000066": { - "Element": "Water", - "Consts": [ - "UI_Talent_S_Ayato_01", - "UI_Talent_S_Ayato_02", - "UI_Talent_U_Ayato_02", - "UI_Talent_S_Ayato_03", - "UI_Talent_U_Ayato_01", - "UI_Talent_S_Ayato_04" - ], - "SkillOrder": [ - 10661, - 10662, - 10665 - ], - "Skills": { - "10661": "Skill_A_01", - "10662": "Skill_S_Ayato_01", - "10665": "Skill_E_Ayato_01" - }, - "ProudMap": { - "10661": 6631, - "10662": 6632, - "10665": 6639 - }, - "NameTextMapHash": 1588620330, - "SideIconName": "UI_AvatarIcon_Side_Ayato", - "QualityType": "QUALITY_ORANGE" - }, - "10000005-501": {}, - "10000005-502": {}, - "10000005-503": {}, - "10000005-504": { - "Element": "Wind", - "Consts": [ - "UI_Talent_S_PlayerWind_01", - "UI_Talent_S_PlayerWind_02", - "UI_Talent_U_PlayerWind_02", - "UI_Talent_S_PlayerWind_03", - "UI_Talent_U_PlayerWind_01", - "UI_Talent_S_PlayerWind_04" - ], - "SkillOrder": [ - 100543, - 10067, - 10068 - ], - "Skills": { - "10067": "Skill_S_PlayerWind_01", - "10068": "Skill_E_PlayerWind_01", - "100543": "Skill_A_01" - }, - "NameTextMapHash": 1533656818, - "ProudMap": { - "10067": 732, - "10068": 739, - "100543": 730 - }, - "SideIconName": "UI_AvatarIcon_Side_PlayerBoy", - "QualityType": "QUALITY_ORANGE" - }, - "10000005-505": {}, - "10000005-506": { - "Element": "Rock", - "Consts": [ - "UI_Talent_S_PlayerRock_01", - "UI_Talent_S_PlayerRock_02", - "UI_Talent_U_PlayerRock_02", - "UI_Talent_S_PlayerRock_03", - "UI_Talent_U_PlayerRock_01", - "UI_Talent_S_PlayerRock_04" - ], - "SkillOrder": [ - 100545, - 10077, - 10078 - ], - "Skills": { - "10077": "Skill_S_PlayerRock_01", - "10078": "Skill_E_PlayerRock_01", - "100545": "Skill_A_01" - }, - "NameTextMapHash": 1533656818, - "ProudMap": { - "10077": 932, - "10078": 939, - "100545": 730 - }, - "SideIconName": "UI_AvatarIcon_Side_PlayerBoy", - "QualityType": "QUALITY_ORANGE" - }, - "10000005-507": { - "Element": "Electric", - "Consts": [ - "UI_Talent_S_PlayerElectric_01", - "UI_Talent_S_PlayerElectric_02", - "UI_Talent_U_PlayerElectric_01", - "UI_Talent_S_PlayerElectric_03", - "UI_Talent_U_PlayerElectric_02", - "UI_Talent_S_PlayerElectric_04" - ], - "SkillOrder": [ - 100546, - 10602, - 10605 - ], - "Skills": { - "10602": "Skill_S_PlayerElectric_01", - "10605": "Skill_E_PlayerElectric_01", - "100546": "Skill_A_01" - }, - "NameTextMapHash": 1533656818, - "ProudMap": { - "10602": 1032, - "10605": 1039, - "100546": 1030 - }, - "SideIconName": "UI_AvatarIcon_Side_PlayerBoy", - "QualityType": "QUALITY_ORANGE" - }, - "10000007-701": {}, - "10000007-702": {}, - "10000007-703": {}, - "10000007-704": { - "Element": "Wind", - "Consts": [ - "UI_Talent_S_PlayerWind_01", - "UI_Talent_S_PlayerWind_02", - "UI_Talent_U_PlayerWind_02", - "UI_Talent_S_PlayerWind_03", - "UI_Talent_U_PlayerWind_01", - "UI_Talent_S_PlayerWind_04" - ], - "SkillOrder": [ - 100553, - 10067, - 10068 - ], - "Skills": { - "10067": "Skill_S_PlayerWind_01", - "10068": "Skill_E_PlayerWind_01", - "100553": "Skill_A_01" - }, - "NameTextMapHash": 3816664530, - "ProudMap": { - "10067": 732, - "10068": 739, - "100553": 731 - }, - "SideIconName": "UI_AvatarIcon_Side_PlayerGirl", - "QualityType": "QUALITY_ORANGE" - }, - "10000007-705": {}, - "10000007-706": { - "Element": "Rock", - "Consts": [ - "UI_Talent_S_PlayerRock_01", - "UI_Talent_S_PlayerRock_02", - "UI_Talent_U_PlayerRock_02", - "UI_Talent_S_PlayerRock_03", - "UI_Talent_U_PlayerRock_01", - "UI_Talent_S_PlayerRock_04" - ], - "SkillOrder": [ - 100555, - 10077, - 10078 - ], - "Skills": { - "10077": "Skill_S_PlayerRock_01", - "10078": "Skill_E_PlayerRock_01", - "100555": "Skill_A_01" - }, - "NameTextMapHash": 3816664530, - "ProudMap": { - "10077": 932, - "10078": 939, - "100555": 731 - }, - "SideIconName": "UI_AvatarIcon_Side_PlayerGirl", - "QualityType": "QUALITY_ORANGE" - }, - "10000007-707": { - "Element": "Electric", - "Consts": [ - "UI_Talent_S_PlayerElectric_01", - "UI_Talent_S_PlayerElectric_02", - "UI_Talent_U_PlayerElectric_01", - "UI_Talent_S_PlayerElectric_03", - "UI_Talent_U_PlayerElectric_02", - "UI_Talent_S_PlayerElectric_04" - ], - "SkillOrder": [ - 100556, - 10602, - 10605 - ], - "Skills": { - "10602": "Skill_S_PlayerElectric_01", - "10605": "Skill_E_PlayerElectric_01", - "100556": "Skill_A_01" - }, - "NameTextMapHash": 3816664530, - "ProudMap": { - "10602": 1032, - "10605": 1039, - "100556": 1031 - }, - "SideIconName": "UI_AvatarIcon_Side_PlayerGirl", - "QualityType": "QUALITY_ORANGE" - } -} \ No newline at end of file diff --git a/models/apihelper/metadata/NameTextMapHash.json b/models/apihelper/metadata/NameTextMapHash.json deleted file mode 100644 index 21ba862..0000000 --- a/models/apihelper/metadata/NameTextMapHash.json +++ /dev/null @@ -1,387 +0,0 @@ -{ - "20848859": "黑岩斩刀", - "33330467": "元素熟练", - "37147251": "匣里日月", - "43015699": "待定", - "54857595": "止水息雷", - "83115355": "被怜爱的少女", - "85795635": "专注", - "88505754": "枫原万叶", - "135182203": "止水息雷", - "147298547": "流浪大地的乐团", - "156294403": "沉沦之心", - "160493219": "暗铁剑", - "168956722": "七七", - "197755235": "贯虹之槊", - "212557731": "祭雷之人", - "240385755": "破浪", - "246984427": "踏火息雷", - "262428003": "祭冰之人", - "270124867": "护国的无垢之心", - "287454963": "祭风之人", - "288666635": "无垢之心", - "302691299": "琥珀玥", - "303155515": "离簇不归", - "310247243": "神乐之真意", - "334242634": "申鹤", - "339931171": "乘胜追击", - "342097547": "辰砂之纺锤", - "346510395": "衔珠海皇", - "368014203": "斩裂晴空的龙脊", - "391273955": "斫断黑翼的利齿", - "411685275": "钢轮弓", - "479076483": "冷刃", - "481755219": "黑岩刺枪", - "486287579": "余热", - "500612819": "「旗杆」", - "500987603": "(test)穿模测试", - "506630267": "顺风而行", - "514784907": "踏火止水", - "521221323": "护国的无垢之心", - "540938627": "掠食者", - "566772267": "御伽大王御伽话", - "577103787": "能量沐浴", - "578575283": "流月针", - "597991835": "白夜皓月", - "613846163": "降世", - "618786571": "钺矛", - "623494555": "摧坚", - "623534363": "西风秘典", - "630452219": "樱之斋宫", - "646100491": "千岩诀·同心", - "650049651": "风花之颂", - "655825874": "云堇", - "656120259": "神射手之誓", - "680510411": "白影剑", - "688991243": "息灾", - "693354267": "尘世之锁", - "697277554": "烟绯", - "716252627": "千岩长枪", - "729851187": "冰之川与雪之砂", - "735056795": "西风大剑", - "807607555": "天空之卷", - "824949859": "嘟嘟!大冒险", - "828711395": "阿莫斯之弓", - "836208539": "炊金", - "850802171": "白铁大剑", - "855894507": "战狂", - "862591315": "苍白之火", - "877751435": "宗室大剑", - "902264035": "风鹰剑", - "902282051": "收割", - "909145139": "护国的无垢之心", - "930640955": "钟剑", - "933076627": "冰风迷途的勇士", - "942758755": "专注", - "944332883": "斫峰之刃", - "949506483": "海洋的胜利", - "968378595": "西风之鹰的抗争", - "968893378": "班尼特", - "991968139": "非时之梦·常世灶食", - "1006042610": "神里绫华", - "1021898539": "弹弓", - "1021947690": "魈", - "1028735635": "抗争的践行之歌", - "1053433018": "砂糖", - "1072884907": "万国诸海图谱", - "1075647299": "松籁响起之时", - "1082448331": "微光的海渊民", - "1089950259": "天空之傲", - "1097898243": "沉重", - "1103732675": "幸运儿", - "1113306282": "莫娜", - "1114777131": "和弦", - "1119368259": "旅程", - "1130996346": "香菱", - "1133599347": "矢志不忘", - "1148024603": "「渔获」", - "1154009435": "试作星镰", - "1163263227": "流浪乐章", - "1163616891": "霜葬", - "1182966603": "佣兵重剑", - "1186209435": "赌徒", - "1212345779": "角斗士的终幕礼", - "1217552947": "白刃流转", - "1240067179": "西风猎弓", - "1319974859": "激励", - "1321135667": "匣里龙吟", - "1337666507": "千岩牢固", - "1344953075": "顺风而行", - "1345343763": "磐岩结绿", - "1383639611": "奇迹", - "1388004931": "飞天御剑", - "1390797107": "白缨枪", - "1404688115": "别离的思念之歌", - "1406746947": "异世界行记", - "1414366819": "金璋皇极", - "1437658243": "螭骨剑", - "1438974835": "逆飞的流星", - "1455107995": "四风原典", - "1468367538": "迪奥娜", - "1479961579": "铁影阔剑", - "1483922610": "九条裟罗", - "1485303435": "注能之刺", - "1492752155": "气定神闲", - "1499235563": "乘胜追击", - "1499817443": "苍翠之风", - "1516554699": "石英大剑", - "1522029867": "踏火息雷", - "1524173875": "炽烈的炎之魔女", - "1533656818": "旅行者", - "1541919827": "染血的骑士道", - "1545992315": "「正义」", - "1558036915": "辰砂往生录", - "1562601179": "翠绿之影", - "1588620330": "神里绫人", - "1595734083": "(test)穿模测试", - "1600275315": "波乱月白经津", - "1608953539": "黎明神剑", - "1610242915": "传承", - "1628928163": "风花之愿", - "1632377563": "渡过烈火的贤人", - "1651985379": "极昼的先兆者", - "1660598451": "岩藏之胤", - "1675686363": "祭礼大剑", - "1706534267": "有话直说", - "1722706579": "止水融冰", - "1745286795": "名士振舞", - "1745712907": "驭浪的海祇民", - "1751039235": "昔日宗室之仪", - "1756609915": "海染砗磲", - "1771603299": "金璋皇极", - "1773425155": "降临之剑", - "1789612403": "回响", - "1820235315": "无矢之歌", - "1836628747": "叛逆的守护者", - "1860795787": "曚云之月", - "1864015138": "刻晴", - "1873342283": "平息鸣雷的尊者", - "1890163363": "不灭月华", - "1901973075": "冬极白星", - "1921418842": "诺艾尔", - "1932742643": "灭却之戒法", - "1934830979": "无尽的渴慕", - "1940821986": "久岐忍", - "1940919994": "胡桃", - "1966438658": "安柏", - "1982136171": "专注", - "1990641987": "祭礼剑", - "1990820123": "天目影打刀", - "1991707099": "试作斩岩", - "1997709467": "和璞鸢", - "2006422931": "千岩古剑", - "2009975571": "(test)穿模测试", - "2017033267": "气定神闲", - "2025598051": "顺风而行", - "2040573235": "悠古的磐岩", - "2060049099": "祭火之人", - "2108574027": "碎石", - "2109571443": "专注", - "2125206395": "银剑", - "2149411851": "金璋皇极", - "2172529947": "乘胜追击", - "2176134843": "专注", - "2190368347": "决", - "2191797987": "冒险家", - "2195665683": "祭礼残章", - "2242027395": "黑檀弓", - "2276480763": "绝缘之旗印", - "2279290283": "魔导绪论", - "2297485451": "速射弓斗", - "2312640651": "气定神闲", - "2317820211": "注能之针", - "2322648115": "粉碎", - "2324146259": "白辰之环", - "2340970067": "历练的猎弓", - "2359799475": "恶王丸", - "2364208851": "行者之心", - "2365025043": "街巷游侠", - "2375993851": "宗室长剑", - "2383998915": "驭浪的海祇民", - "2384519283": "弹弓", - "2388785242": "早柚", - "2400012995": "祭礼弓", - "2410593283": "无锋剑", - "2417717595": "暗巷猎手", - "2425414923": "落霞", - "2433755451": "揭旗的叛逆之歌", - "2440850563": "回响长天的诗歌", - "2466140362": "温迪", - "2469300579": "乘胜追击", - "2470306939": "飞雷御执", - "2474354867": "西风剑", - "2476346187": "踏火止水", - "2491797315": "喜多院十文字", - "2504399314": "宵宫", - "2512309395": "如雷的盛怒", - "2521338131": "试作金珀", - "2534304035": "雾切御腰物", - "2539208459": "证誓之明瞳", - "2546254811": "华馆梦醒形骸记", - "2556914683": "绝弦", - "2587614459": "忍冬之果", - "2614170427": "飞天大御剑", - "2646367730": "北斗", - "2664629131": "匣里灭辰", - "2666951267": "训练大剑", - "2673337443": "注能之矢", - "2679781122": "甘雨", - "2684365579": "登场乐", - "2705029563": "口袋魔导书", - "2713453234": "八重神子", - "2719832059": "(test)穿模测试", - "2743659331": "激流", - "2749508387": "金璋皇极", - "2749853923": "腐殖之剑", - "2753539619": "雪葬的星银", - "2764598579": "流放者", - "2792766467": "无工之剑", - "2796697027": "新手长枪", - "2832648187": "宗室长弓", - "2834803571": "金璋皇极", - "2848374378": "夜兰", - "2853296811": "穿刺高天的利齿", - "2871793795": "锐利", - "2876340530": "重云", - "2890909531": "武人", - "2893964243": "飞矢传书", - "2915865819": "渊中霞彩", - "2918525947": "飞雷之弦振", - "2935286715": "宗室猎枪", - "2947140987": "暗巷闪光", - "2949448555": "苍古自由之誓", - "2963220587": "翡玉法球", - "3001782875": "气定神闲", - "3018479371": "船歌", - "3024507506": "雷电将军", - "3063488107": "强力攻击", - "3068316954": "荒泷一斗", - "3070169307": "铁尖枪", - "3079462611": "驭浪的海祇民", - "3090373787": "暗巷的酒与诗", - "3097441915": "以理服人", - "3112448011": "决心", - "3112679155": "终末嗟叹之诗", - "3156385731": "昭心", - "3169209451": "弓藏", - "3192689683": "霜葬", - "3221566250": "琴", - "3235324891": "护摩之杖", - "3252085691": "顺风而行", - "3258658763": "嗜魔", - "3265161211": "注能之锋", - "3273999011": "黑岩绯玉", - "3277782506": "菲谢尔", - "3302787771": "霜葬", - "3305772819": "奔袭战术", - "3314157803": "克柔", - "3337185491": "浅濑之弭", - "3337249451": "过载", - "3339083250": "可莉", - "3344622722": "丽莎", - "3364338659": "无边际的眷顾", - "3371922315": "神樱神游神乐舞", - "3378007475": "黑岩长剑", - "3400133546": "五郎", - "3406113971": "顺风而行", - "3421967235": "吃虎鱼刀", - "3439749859": "苍翠猎弓", - "3443142923": "龙脊长枪", - "3447737235": "黑岩战弓", - "3456986819": "嘟嘟可故事集", - "3465493459": "精准", - "3500935003": "讨龙英杰谭", - "3535784755": "勇士之心", - "3541083923": "角斗士", - "3555115602": "托马", - "3584825427": "学徒笔记", - "3587062891": "千岩诀·同心", - "3587621259": "笛剑", - "3600623979": "猎弓", - "3608180322": "迪卢克", - "3618167299": "学士", - "3625393819": "试作澹月", - "3626268211": "来歆余响", - "3673792067": "旅行剑", - "3684723963": "雨裁", - "3689108098": "埃洛伊", - "3717667418": "优菈", - "3717849275": "薙草之稻光", - "3719372715": "甲级宝珏", - "3722933411": "试作古华", - "3755004051": "西风长枪", - "3762437019": "(test)穿模测试", - "3775299170": "芭芭拉", - "3782508715": "游医", - "3796702635": "变化万端", - "3796905611": "黑剑", - "3816664530": "旅行者", - "3827789435": "宗室秘法录", - "3832443723": "不屈", - "3836188467": "无羁的朱赤之蝶", - "3847143266": "达达利亚", - "3862787418": "钟离", - "3890292467": "教官", - "3898539027": "浮游四方的灵云", - "3914045794": "珊瑚宫心海", - "3914951691": "赤角石溃杵", - "3933622347": "天空之翼", - "3949653579": "幽夜华尔兹", - "3966753539": "洗濯诸类之形", - "3975746731": "鸦羽弓", - "3995710363": "狼的末路", - "3996017211": "收割", - "3999792907": "祭水之人", - "4000770243": "街巷伏击", - "4022012131": "乘胜追击", - "4049410651": "决斗之枪", - "4055003299": "天空之刃", - "4060235987": "日月辉", - "4080317355": "勇气", - "4082302819": "守护之心", - "4090429643": "沐浴龙血的剑", - "4103022435": "铁蜂刺", - "4103766499": "黑缨枪", - "4108620722": "阿贝多", - "4113638323": "昭理的鸢之枪", - "4119663210": "凯亚", - "4122509083": "断浪长鳍", - "4124851547": "雾切之回光", - "4127888970": "凝光", - "4137694339": "(test)竿测试", - "4139294531": "信使", - "4144069251": "追忆之注连", - "4158505619": "天空之脊", - "4160147242": "雷泽", - "4162981171": "(test)穿模测试", - "4186179883": "破魔之弓", - "4193089947": "桂木斩长正", - "4197635682": "行秋", - "4226083179": "名士振舞", - "4230231107": "若水", - "4245213187": "注能之卷", - "4258047555": "极夜二重奏", - "4260733330": "罗莎莉亚", - "4267718859": "反曲弓", - "4273845410": "辛焱", - "4275754179": "如狼般狩猎者", - "FIGHT_PROP_MAX_HP": "生命值上限", - "FIGHT_PROP_ATTACK": "攻击力", - "FIGHT_PROP_DEFENSE": "防御力", - "FIGHT_PROP_ELEMENT_MASTERY": "元素精通", - "FIGHT_PROP_CRITICAL": "暴击率", - "FIGHT_PROP_CRITICAL_HURT": "暴击伤害", - "FIGHT_PROP_HEAL_ADD": "治疗加成", - "FIGHT_PROP_HEALED_ADD": "受治疗加成", - "FIGHT_PROP_CHARGE_EFFICIENCY": "元素充能效率", - "FIGHT_PROP_SHIELD_COST_MINUS_RATIO": "护盾强效", - "FIGHT_PROP_FIRE_ADD_HURT": "火元素伤害加成", - "FIGHT_PROP_WATER_ADD_HURT": "水元素伤害加成", - "FIGHT_PROP_GRASS_ADD_HURT": "草元素伤害加成", - "FIGHT_PROP_ELEC_ADD_HURT": "雷元素伤害加成", - "FIGHT_PROP_WIND_ADD_HURT": "风元素伤害加成", - "FIGHT_PROP_ICE_ADD_HURT": "冰元素伤害加成", - "FIGHT_PROP_ROCK_ADD_HURT": "岩元素伤害加成", - "FIGHT_PROP_PHYSICAL_ADD_HURT": "物理伤害加成", - "level": "等级" -} \ No newline at end of file diff --git a/models/apihelper/metadata/ReliquaryNameMap.json b/models/apihelper/metadata/ReliquaryNameMap.json deleted file mode 100644 index ed504c5..0000000 --- a/models/apihelper/metadata/ReliquaryNameMap.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "FIGHT_PROP_BASE_ATTACK": "基础攻击力", - "FIGHT_PROP_BASE_DEFENSE": "基础防御力", - "FIGHT_PROP_BASE_HP": "基础血量", - "FIGHT_PROP_ATTACK": "攻击力", - "FIGHT_PROP_ATTACK_PERCENT": "百分比攻击力", - "FIGHT_PROP_HP": "血量", - "FIGHT_PROP_HP_PERCENT": "百分比血量", - "FIGHT_PROP_DEFENSE": "防御力", - "FIGHT_PROP_DEFENSE_PERCENT": "百分比防御力", - "FIGHT_PROP_ELEMENT_MASTERY": "元素精通", - "FIGHT_PROP_CRITICAL": "暴击率", - "FIGHT_PROP_CRITICAL_HURT": "暴击伤害", - "FIGHT_PROP_CHARGE_EFFICIENCY": "元素充能效率", - "FIGHT_PROP_FIRE_SUB_HURT": "火元素抗性", - "FIGHT_PROP_ELEC_SUB_HURT": "雷元素抗性", - "FIGHT_PROP_ICE_SUB_HURT": "冰元素抗性", - "FIGHT_PROP_WATER_SUB_HURT": "水元素抗性", - "FIGHT_PROP_WIND_SUB_HURT": "风元素抗性", - "FIGHT_PROP_ROCK_SUB_HURT": "岩元素抗性", - "FIGHT_PROP_GRASS_SUB_HURT": "草元素抗性", - "FIGHT_PROP_FIRE_ADD_HURT": "火元素伤害加成", - "FIGHT_PROP_ELEC_ADD_HURT": "雷元素伤害加成", - "FIGHT_PROP_ICE_ADD_HURT": "冰元素伤害加成", - "FIGHT_PROP_WATER_ADD_HURT": "水元素伤害加成", - "FIGHT_PROP_WIND_ADD_HURT": "风元素伤害加成", - "FIGHT_PROP_ROCK_ADD_HURT": "岩元素伤害加成", - "FIGHT_PROP_GRASS_ADD_HURT": "草元素伤害加成", - "FIGHT_PROP_PHYSICAL_ADD_HURT": "物理伤害加成", - "FIGHT_PROP_HEAL_ADD": "治疗加成" -} \ No newline at end of file diff --git a/models/apihelper/playercards.py b/models/apihelper/playercards.py deleted file mode 100644 index 27d3886..0000000 --- a/models/apihelper/playercards.py +++ /dev/null @@ -1,156 +0,0 @@ -import os -from typing import Union, Optional - -import httpx -import ujson - -from models.base import GameItem -from models.game.artifact import ArtifactInfo -from models.game.character import CharacterInfo, CharacterValueInfo -from models.game.fetter import FetterInfo -from models.game.skill import Skill -from models.game.talent import Talent -from models.game.weapon import WeaponInfo -from .helpers import get_headers - - -class PlayerCardsAPI: - UI_URL = "https://enka.shinshin.moe/ui/" - - def __init__(self): - self.client = httpx.AsyncClient(headers=get_headers()) - project_path = os.path.dirname(__file__) - characters_map_file = os.path.join(project_path, "metadata", "CharactersMap.json") - name_text_map_hash_file = os.path.join(project_path, "metadata", "NameTextMapHash.json") - reliquary_name_map_file = os.path.join(project_path, "metadata", "ReliquaryNameMap.json") - with open(characters_map_file, "r", encoding="utf-8") as f: - self._characters_map_json: dict = ujson.load(f) - with open(name_text_map_hash_file, "r", encoding="utf-8") as f: - self._name_text_map_hash_json: dict = ujson.load(f) - with open(reliquary_name_map_file, "r", encoding="utf-8") as f: - self._reliquary_name_map_json: dict = ujson.load(f) - - def get_characters_name(self, item_id: Union[int, str]) -> str: - if isinstance(item_id, int): - item_id = str(item_id) - characters = self.get_characters(item_id) - name_text_map_hash = characters.get("NameTextMapHash", "-1") - return self.get_text(str(name_text_map_hash)) - - def get_characters(self, item_id: Union[int, str]) -> dict: - if isinstance(item_id, int): - item_id = str(item_id) - return self._characters_map_json.get(item_id, {}) - - def get_text(self, hash_value: Union[int, str]) -> str: - if isinstance(hash_value, int): - hash_value = str(hash_value) - return self._name_text_map_hash_json.get(hash_value, "") - - def get_reliquary_name(self, reliquary: str) -> str: - return self._reliquary_name_map_json[reliquary] - - async def get_data(self, uid: Union[str, int]): - url = f"https://enka.shinshin.moe/u/{uid}/__data.json" - response = await self.client.get(url) - return response - - def data_handler(self, avatar_data: dict, avatar_id: int) -> CharacterInfo: - artifact_list = [] - - weapon_info: Optional[WeaponInfo] = None - - equip_list = avatar_data["equipList"] # 圣遗物和武器相关 - fetter_info = avatar_data["fetterInfo"] # 好感等级 - fight_prop_map = avatar_data["fightPropMap"] # 属性 - # inherent_proud_skill_list = avatar_data["inherentProudSkillList"] # 不知道 - prop_map = avatar_data["propMap"] # 角色等级 其他信息 - # proud_skill_extra_level_map = avatar_data["proudSkillExtraLevelMap"] # 不知道 - # skill_depot_id = avatar_data["skillDepotId"] # 不知道 - skill_level_map = avatar_data["skillLevelMap"] # 技能等级 - - # 角色等级 - character_level = prop_map['4001']['val'] - - # 角色姓名 - character_name = self.get_characters_name(avatar_id) - characters_data = self.get_characters(avatar_id) - - # 圣遗物和武器 - for equip in equip_list: - if "reliquary" in equip: # 圣遗物 - flat = equip["flat"] - reliquary = equip["reliquary"] - reliquary_main_stat = flat["reliquaryMainstat"] - reliquary_sub_stats = flat['reliquarySubstats'] - sub_item = [] - for reliquary_sub in reliquary_sub_stats: - sub_item.append(GameItem(name=self.get_reliquary_name(reliquary_sub["appendPropId"]), - item_type=reliquary_sub["appendPropId"], value=reliquary_sub["statValue"])) - main_item = GameItem(name=self.get_reliquary_name(reliquary_main_stat["mainPropId"]), - item_type=reliquary_main_stat["mainPropId"], - value=reliquary_main_stat["statValue"]) - name = self.get_text(flat["nameTextMapHash"]) - artifact_list.append(ArtifactInfo(item_id=equip["itemId"], name=name, star=flat["rankLevel"], - level=reliquary["level"] - 1, main_item=main_item, sub_item=sub_item)) - if "weapon" in equip: # 武器 - flat = equip["flat"] - weapon_data = equip["weapon"] - # 防止未精炼 - if 'promoteLevel' in weapon_data: - weapon_level = weapon_data['promoteLevel'] - 1 - else: - weapon_level = 0 - if 'affixMap' in weapon_data: - affix = list(weapon_data['affixMap'].values())[0] + 1 - else: - - affix = 1 - reliquary_main_stat = flat["weaponStats"][0] - reliquary_sub_stats = flat['weaponStats'][1] - sub_item = GameItem(name=self.get_reliquary_name(reliquary_main_stat["appendPropId"]), - item_type=reliquary_sub_stats["appendPropId"], - value=reliquary_sub_stats["statValue"]) - main_item = GameItem(name=self.get_reliquary_name(reliquary_main_stat["appendPropId"]), - item_type=reliquary_main_stat["appendPropId"], - value=reliquary_main_stat["statValue"]) - weapon_name = self.get_text(flat["nameTextMapHash"]) - weapon_info = WeaponInfo(item_id=equip["itemId"], name=weapon_name, star=flat["rankLevel"], - level=weapon_level, main_item=main_item, sub_item=sub_item, affix=affix) - - # 好感度 - fetter = FetterInfo(fetter_info["expLevel"]) - - # 基础数值处理 - for i in range(40, 47): - if fight_prop_map[str(i)] > 0: - dmg_bonus = fight_prop_map[str(i)] - break - else: - dmg_bonus = 0 - - base_value = CharacterValueInfo(fight_prop_map["2000"], fight_prop_map["1"], fight_prop_map["2001"], - fight_prop_map["4"], fight_prop_map["2002"], fight_prop_map["7"], - fight_prop_map["28"], fight_prop_map["20"], fight_prop_map["22"], - fight_prop_map["23"], fight_prop_map["26"], fight_prop_map["27"], - fight_prop_map["29"], fight_prop_map["30"], dmg_bonus) - - # 技能处理 - skill_list = [] - skills = characters_data["Skills"] - for skill_id in skill_level_map: - skill_list.append(Skill(skill_id, name=skill_level_map[skill_id], icon=skills[skill_id])) - - # 命座处理 - talent_list = [] - consts = characters_data["Consts"] - if 'talentIdList' in avatar_data: - talent_id_list = avatar_data["talentIdList"] - for index, _ in enumerate(talent_id_list): - talent_list.append(Talent(talent_id_list[index], icon=consts[index])) - - element = characters_data["Element"] - icon = characters_data["SideIconName"] - character_info = CharacterInfo(character_name, element, character_level, fetter, base_value, weapon_info, - artifact_list, skill_list, talent_list, icon) - return character_info diff --git a/models/game/artifact.py b/models/game/artifact.py deleted file mode 100644 index 3ebeacd..0000000 --- a/models/game/artifact.py +++ /dev/null @@ -1,51 +0,0 @@ -from enum import Enum -from typing import Union, Optional, List - -from models.base import GameItem -from models.baseobject import BaseObject -from models.types import JSONDict - - -class ArtifactInfo(BaseObject): - """ - 圣遗物信息 - """ - - def __init__(self, item_id: int = 0, name: str = "", level: int = 0, main_item: Optional[GameItem] = None, - pos: Union[Enum, str] = "", star: int = 1, sub_item: Optional[List[GameItem]] = None, icon: str = ""): - """ - :param item_id: item_id - :param name: 圣遗物名字 - :param level: 圣遗物等级 - :param main_item: 主词条 - :param pos: 圣遗物类型 - :param star: 星级 - :param sub_item: 副词条 - :param icon: 图片 - """ - self.icon = icon - self.item_id = item_id - self.level = level - self.main_item = main_item - self.name = name - self.pos = pos - self.star = star - self.sub_item: List[GameItem] = [] - if sub_item is not None: - self.sub_item = sub_item - - def to_dict(self) -> JSONDict: - data = super().to_dict() - if self.sub_item: - data["sub_item"] = [e.to_dict() for e in self.sub_item] - return data - - @classmethod - def de_json(cls, data: Optional[JSONDict]) -> Optional["ArtifactInfo"]: - data = cls._parse_data(data) - if not data: - return None - data["sub_item"] = GameItem.de_list(data.get("sub_item")) - return cls(**data) - - __slots__ = ("name", "type", "value", "pos", "star", "sub_item", "main_item", "level", "item_id", "icon") diff --git a/models/game/character.py b/models/game/character.py deleted file mode 100644 index 6d9ad68..0000000 --- a/models/game/character.py +++ /dev/null @@ -1,123 +0,0 @@ -from typing import Optional, List - -from models.baseobject import BaseObject -from models.game.artifact import ArtifactInfo -from models.game.fetter import FetterInfo -from models.game.skill import Skill -from models.game.talent import Talent -from models.game.weapon import WeaponInfo -from models.types import JSONDict - - -class CharacterValueInfo(BaseObject): - """角色数值信息 - """ - - def __init__(self, hp: float = 0, base_hp: float = 0, atk: float = 0, base_atk: float = 0, - def_value: float = 0, base_def: float = 0, elemental_mastery: float = 0, crit_rate: float = 0, - crit_dmg: float = 0, energy_recharge: float = 0, heal_bonus: float = 0, healed_bonus: float = 0, - physical_dmg_sub: float = 0, physical_dmg_bonus: float = 0, dmg_bonus: float = 0): - """ - :param hp: 生命值 - :param base_hp: 基础生命值 - :param atk: 攻击力 - :param base_atk: 基础攻击力 - :param def_value: 防御力 - :param base_def: 基础防御力 - :param elemental_mastery: 元素精通 - :param crit_rate: 暴击率 - :param crit_dmg: 暴击伤害 - :param energy_recharge: 充能效率 - :param heal_bonus: 治疗 - :param healed_bonus: 受治疗 - :param physical_dmg_sub: 物理伤害加成 - :param physical_dmg_bonus: 物理伤害抗性 - :param dmg_bonus: 伤害加成 - """ - self.dmg_bonus = dmg_bonus - self.physical_dmg_bonus = physical_dmg_bonus - self.physical_dmg_sub = physical_dmg_sub - self.healed_bonus = healed_bonus - self.heal_bonus = heal_bonus - self.energy_recharge = energy_recharge - self.crit_dmg = crit_dmg - self.crit_rate = crit_rate - self.elemental_mastery = elemental_mastery - self.base_def = base_def - self.def_value = def_value - self.base_atk = base_atk - self.atk = atk - self.base_hp = base_hp - self.hp = hp - - @property - def add_hp(self) -> float: - return self.hp - self.base_hp - - @property - def add_atk(self) -> float: - return self.atk - self.base_atk - - @property - def add_def(self) -> float: - return self.def_value - self.base_def - - __slots__ = ( - "hp", "base_hp", "atk", "base_atk", "def_value", "base_def", "elemental_mastery", "crit_rate", "crit_dmg", - "energy_recharge", "dmg_bonus", "physical_dmg_bonus", "physical_dmg_sub", "healed_bonus", - "heal_bonus") - - -class CharacterInfo(BaseObject): - """角色信息 - """ - - def __init__(self, name: str = "", elementl: str = 0, level: int = 0, fetter: Optional[FetterInfo] = None, - base_value: Optional[CharacterValueInfo] = None, weapon: Optional[WeaponInfo] = None, - artifact: Optional[List[ArtifactInfo]] = None, skill: Optional[List[Skill]] = None, - talent: Optional[List[Talent]] = None, icon: str = ""): - """ - :param name: 角色名字 - :param level: 角色等级 - :param elementl: 属性 - :param fetter: 好感度 - :param base_value: 基础数值 - :param weapon: 武器 - :param artifact: 圣遗物 - :param skill: 技能 - :param talent: 命座 - :param icon: 角色图片 - """ - self.icon = icon - self.elementl = elementl - self.talent = talent - self.skill = skill - self.artifact = artifact - self.weapon = weapon - self.base_value = base_value - self.fetter = fetter - self.level = level - self.name = name - - def to_dict(self) -> JSONDict: - data = super().to_dict() - if self.artifact: - data["artifact"] = [e.to_dict() for e in self.artifact] - if self.artifact: - data["skill"] = [e.to_dict() for e in self.skill] - if self.artifact: - data["talent"] = [e.to_dict() for e in self.talent] - return data - - @classmethod - def de_json(cls, data: Optional[JSONDict]) -> Optional["CharacterInfo"]: - data = cls._parse_data(data) - if not data: - return None - data["artifact"] = ArtifactInfo.de_list(data.get("sub_item")) - data["skill"] = Skill.de_list(data.get("sub_item")) - data["talent"] = Talent.de_list(data.get("sub_item")) - return cls(**data) - - __slots__ = ( - "name", "level", "level", "fetter", "base_value", "weapon", "artifact", "skill", "talent", "elementl", "icon") diff --git a/models/game/fetter.py b/models/game/fetter.py deleted file mode 100644 index 0b6c991..0000000 --- a/models/game/fetter.py +++ /dev/null @@ -1,15 +0,0 @@ -from models.baseobject import BaseObject - - -class FetterInfo(BaseObject): - """ - 好感度信息 - """ - - def __init__(self, level: int = 0): - """ - :param level: 等级 - """ - self.level = level - - __slots__ = ("level",) diff --git a/models/game/skill.py b/models/game/skill.py deleted file mode 100644 index 28674d3..0000000 --- a/models/game/skill.py +++ /dev/null @@ -1,21 +0,0 @@ -from models.baseobject import BaseObject - - -class Skill(BaseObject): - """ - 技能信息 - """ - - def __init__(self, skill_id: int = 0, name: str = "", level: int = 0, icon: str = ""): - """ - :param skill_id: 技能ID - :param name: 技能名称 - :param level: 技能等级 - :param icon: 技能图标 - """ - self.icon = icon - self.level = level - self.name = name - self.skill_id = skill_id - - __slots__ = ("skill_id", "name", "level", "icon") diff --git a/models/game/talent.py b/models/game/talent.py deleted file mode 100644 index c6b56be..0000000 --- a/models/game/talent.py +++ /dev/null @@ -1,19 +0,0 @@ -from models.baseobject import BaseObject - - -class Talent(BaseObject): - """ - 命座 - """ - - def __init__(self, talent_id: int = 0, name: str = "", icon: str = ""): - """ - :param talent_id: 命座ID - :param name: 命座名字 - :param icon: 图标 - """ - self.icon = icon - self.name = name - self.talent_id = talent_id - - __slots__ = ("talent_id", "name", "icon") diff --git a/models/game/weapon.py b/models/game/weapon.py deleted file mode 100644 index e330622..0000000 --- a/models/game/weapon.py +++ /dev/null @@ -1,36 +0,0 @@ -from enum import Enum -from typing import Union, Optional - -from models.base import GameItem -from models.baseobject import BaseObject - - -class WeaponInfo(BaseObject): - """武器信息 - """ - - def __init__(self, item_id: int = 0, name: str = "", level: int = 0, main_item: Optional[GameItem] = None, - affix: int = 0, pos: Union[Enum, str] = "", star: int = 1, sub_item: Optional[GameItem] = None, - icon: str = ""): - """ - :param item_id: item_id - :param name: 武器名字 - :param level: 武器等级 - :param main_item: 主词条 - :param affix: 精炼等级 - :param pos: 武器类型 - :param star: 星级 - :param sub_item: 副词条 - :param icon: 图片 - """ - self.affix = affix - self.icon = icon - self.item_id = item_id - self.level = level - self.main_item = main_item - self.name = name - self.pos = pos - self.star = star - self.sub_item = sub_item - - __slots__ = ("name", "type", "value", "pos", "star", "sub_item", "main_item", "level", "item_id", "icon", "affix") diff --git a/models/types.py b/models/types.py deleted file mode 100644 index d48981f..0000000 --- a/models/types.py +++ /dev/null @@ -1,5 +0,0 @@ -from typing import Dict, Any, Callable, TypeVar - -JSONDict = Dict[str, Any] - -Func = TypeVar("Func", bound=Callable[..., Any]) \ No newline at end of file diff --git a/models/README.md b/modules/README.md similarity index 79% rename from models/README.md rename to modules/README.md index cc4caad..94eec58 100644 --- a/models/README.md +++ b/modules/README.md @@ -1,4 +1,4 @@ -# model 目录说明 +# modules 目录说明 ## apihelpe 模块 @@ -22,6 +22,6 @@ ### 感谢 -| Nickname | Contribution | -| :--------------------------------------------------------: | -------------------- | -| [Crawler-ghhw](https://github.com/DGP-Studio/Crawler-ghhw) | 本项目参考的爬虫代码 | +| Nickname | Contribution | +|:----------------------------------------------------------:|--------------| +| [Crawler-ghhw](https://github.com/DGP-Studio/Crawler-ghhw) | 本项目参考的爬虫代码 | diff --git a/models/apihelper/artifact.py b/modules/apihelper/artifact.py similarity index 98% rename from models/apihelper/artifact.py rename to modules/apihelper/artifact.py index 38e189a..a5fe6d3 100644 --- a/models/apihelper/artifact.py +++ b/modules/apihelper/artifact.py @@ -38,7 +38,7 @@ class ArtifactOcrRate: HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36", - "Content-Type": "application/json; charset=utf-8", + "Content-Type": "bot/json; charset=utf-8", } def __init__(self): diff --git a/models/apihelper/base.py b/modules/apihelper/base.py similarity index 100% rename from models/apihelper/base.py rename to modules/apihelper/base.py diff --git a/models/apihelper/gacha.py b/modules/apihelper/gacha.py similarity index 95% rename from models/apihelper/gacha.py rename to modules/apihelper/gacha.py index c7a96c0..caab717 100644 --- a/models/apihelper/gacha.py +++ b/modules/apihelper/gacha.py @@ -1,6 +1,6 @@ import httpx -from .base import BaseResponseData +from modules.apihelper.base import BaseResponseData class GachaInfo: diff --git a/models/apihelper/helpers.py b/modules/apihelper/helpers.py similarity index 97% rename from models/apihelper/helpers.py rename to modules/apihelper/helpers.py index 4c0d71b..97bf71b 100644 --- a/models/apihelper/helpers.py +++ b/modules/apihelper/helpers.py @@ -20,7 +20,7 @@ def get_device_id(name: str) -> str: def md5(text: str) -> str: - _md5 = hashlib.md5() + _md5 = hashlib.md5() # nosec B303 _md5.update(text.encode()) return _md5.hexdigest() diff --git a/models/apihelper/hoyolab.py b/modules/apihelper/hoyolab.py similarity index 95% rename from models/apihelper/hoyolab.py rename to modules/apihelper/hoyolab.py index 8bb9bfb..0f64ed8 100644 --- a/models/apihelper/hoyolab.py +++ b/modules/apihelper/hoyolab.py @@ -1,7 +1,7 @@ from httpx import AsyncClient -from .base import BaseResponseData -from .helpers import get_ds, get_device_id, get_recognize_server +from modules.apihelper.base import BaseResponseData +from modules.apihelper.helpers import get_ds, get_device_id, get_recognize_server class Genshin: diff --git a/models/apihelper/hyperion.py b/modules/apihelper/hyperion.py similarity index 98% rename from models/apihelper/hyperion.py rename to modules/apihelper/hyperion.py index bd5168f..a0d5e74 100644 --- a/models/apihelper/hyperion.py +++ b/modules/apihelper/hyperion.py @@ -5,8 +5,8 @@ from typing import List import httpx from httpx import AsyncClient -from .base import HyperionResponse, ArtworkImage, BaseResponseData -from .helpers import get_ds, get_device_id +from modules.apihelper.base import HyperionResponse, ArtworkImage, BaseResponseData +from modules.apihelper.helpers import get_ds, get_device_id class Hyperion: diff --git a/modules/playercards/models/talent.py b/modules/playercards/models/talent.py new file mode 100644 index 0000000..48e74cb --- /dev/null +++ b/modules/playercards/models/talent.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel + + +class Talent(BaseModel): + """命座""" + talent_id: int = 0 + name: str = "" + icon: str = "" diff --git a/models/wiki/base.py b/modules/wiki/base.py similarity index 95% rename from models/wiki/base.py rename to modules/wiki/base.py index a8ac0d9..a0f2ef1 100644 --- a/models/wiki/base.py +++ b/modules/wiki/base.py @@ -35,15 +35,16 @@ class Model(PydanticBaseModel): class WikiModel(Model): + # noinspection PyUnresolvedReferences """wiki所用到的基类 - Attributes: - id (:obj:`int`): ID - name (:obj:`str`): 名称 - rarity (:obj:`int`): 星级 + Attributes: + id (:obj:`int`): ID + name (:obj:`str`): 名称 + rarity (:obj:`int`): 星级 - _client (:class:`httpx.AsyncClient`): 发起 http 请求的 client - """ + _client (:class:`httpx.AsyncClient`): 发起 http 请求的 client + """ _client: ClassVar[AsyncClient] = AsyncClient() id: str @@ -199,7 +200,7 @@ class WikiModel(Model): queue: Queue[Union[str, Tuple[str, URL]]] = Queue() # 存放 Model 的队列 signal = Value('i', len(urls)) # 一个用于异步任务同步的信号,初始值为存放所需要爬取的页面数 - async def task(page: URL, s: Value): + async def task(page: URL): """包装的爬虫任务""" response = await cls._client_get(page) # 从页面中获取对应的 chaos data (未处理的json格式字符串) @@ -215,7 +216,7 @@ class WikiModel(Model): signal.value = signal.value - 1 # 信号量减少 1 ,说明该爬虫任务已经完成 for url in urls: # 遍历需要爬出的页面 - asyncio.create_task(task(url, signal)) # 添加爬虫任务 + asyncio.create_task(task(url)) # 添加爬虫任务 while signal.value > 0 or not queue.empty(): # 当还有未完成的爬虫任务或存放数据的队列不为空时 yield await queue.get() # 取出并返回一个存放的 Model diff --git a/models/wiki/character.py b/modules/wiki/character.py similarity index 96% rename from models/wiki/character.py rename to modules/wiki/character.py index d4633dc..ced2ea2 100644 --- a/models/wiki/character.py +++ b/modules/wiki/character.py @@ -4,9 +4,9 @@ from typing import List, Optional from bs4 import BeautifulSoup from httpx import URL -from models.wiki.base import Model, SCRAPE_HOST -from models.wiki.base import WikiModel -from models.wiki.other import Association, Element, WeaponType +from modules.wiki.base import Model, SCRAPE_HOST +from modules.wiki.base import WikiModel +from modules.wiki.other import Association, Element, WeaponType class Birth(Model): @@ -163,7 +163,7 @@ class Character(WikiModel): async def get_url_by_name(cls, name: str) -> Optional[URL]: # 重写此函数的目的是处理主角名字的 ID _map = {'荧': "playergirl_007", '空': "playerboy_005"} - if (id_ := _map.get(name, None)) is not None: + if (id_ := _map.get(name)) is not None: return await cls.get_url_by_id(id_) return await super(Character, cls).get_url_by_name(name) diff --git a/models/wiki/material.py b/modules/wiki/material.py similarity index 96% rename from models/wiki/material.py rename to modules/wiki/material.py index 6cdb930..ef71f4a 100644 --- a/models/wiki/material.py +++ b/modules/wiki/material.py @@ -5,7 +5,7 @@ from bs4 import BeautifulSoup from httpx import URL from typing_extensions import Self -from models.wiki.base import SCRAPE_HOST, WikiModel +from modules.wiki.base import SCRAPE_HOST, WikiModel __all__ = ['Material'] diff --git a/models/wiki/metadata/ascension.json b/modules/wiki/metadata/ascension.json similarity index 100% rename from models/wiki/metadata/ascension.json rename to modules/wiki/metadata/ascension.json diff --git a/models/wiki/metadata/elite.json b/modules/wiki/metadata/elite.json similarity index 100% rename from models/wiki/metadata/elite.json rename to modules/wiki/metadata/elite.json diff --git a/models/wiki/metadata/monster.json b/modules/wiki/metadata/monster.json similarity index 100% rename from models/wiki/metadata/monster.json rename to modules/wiki/metadata/monster.json diff --git a/models/wiki/metadata/weapon_level.json b/modules/wiki/metadata/weapon_level.json similarity index 100% rename from models/wiki/metadata/weapon_level.json rename to modules/wiki/metadata/weapon_level.json diff --git a/models/wiki/other.py b/modules/wiki/other.py similarity index 98% rename from models/wiki/other.py rename to modules/wiki/other.py index 4c54035..7446de7 100644 --- a/models/wiki/other.py +++ b/modules/wiki/other.py @@ -3,7 +3,7 @@ from typing import Optional from typing_extensions import Self -from models.wiki.base import SCRAPE_HOST +from modules.wiki.base import SCRAPE_HOST __all__ = [ 'Element', diff --git a/models/wiki/weapon.py b/modules/wiki/weapon.py similarity index 97% rename from models/wiki/weapon.py rename to modules/wiki/weapon.py index 4a48858..e22173f 100644 --- a/models/wiki/weapon.py +++ b/modules/wiki/weapon.py @@ -5,8 +5,8 @@ from typing import List, Optional, Tuple, Union from bs4 import BeautifulSoup from httpx import URL -from models.wiki.base import Model, SCRAPE_HOST, WikiModel -from models.wiki.other import AttributeType, WeaponType +from modules.wiki.base import Model, SCRAPE_HOST, WikiModel +from modules.wiki.other import AttributeType, WeaponType __all__ = ['Weapon', 'WeaponAffix', 'WeaponAttribute'] diff --git a/plugins/README.md b/plugins/README.md index bc09bf7..2b96087 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -4,49 +4,160 @@ 该目录仅限处理交互层和业务层数据交换的任务 -如有任何新业务接口,请转到 `core` 目录添加 +如有任何核心接口,请转到 `core` 目录添加 如有任何API请求接口,请转到 `models` 目录添加 -## 基础代码 +## 新版插件 Plugin 的写法 -``` python -from telegram.ext import CommandHandler, CallbackContext +### 关于路径 -from logger import Log -from utils.decorators.error import error_callable -from utils.decorators.restricts import restricts -from utils.plugins.manager import listener_plugins_class +插件应该写在 `plugins` 文件夹下,可以是一个包或者是一个文件,但文件名、文件夹名中不能包含`_`字符 -@listener_plugins_class() -class Example: +### 关于类 - @classmethod - def create_handlers(cls): - example = cls() - return [CommandHandler('example', example.command_start)] +1. 除了要使用`ConversationHandler` 的插件外,都要继承 `core.plugin.Plugin` - @error_callable - @restricts() - async def command_start(self, update: Update, context: CallbackContext) -> None: - user = update.effective_user - Log.info(f"用户 {user.full_name}[{user.id}] 发出example命令") - await message.reply_text("Example") + ```python + from core.plugin import Plugin + + + class TestPlugin(Plugin): + pass + ``` +2. 针对要用 `ConversationHandler` 的插件,要继承 `core.plugin.Plugin.Conversation` + + ```python + from core.plugin import Plugin + + + class TestConversationPlugin(Plugin.Conversation): + pass + ``` + +3. 关于初始化方法以及依赖注入 + + 初始化类, 可写在 `__init__` 和 `__async_init__` 中, 其中 `__async_init__` 应该是异步方法, + 用于执行初始化时需要的异步操作. 这两个方法的执行顺序是 `__init__` 在前, `__async_init__` 在后 + + 若需要注入依赖, 直接在插件类的`__init__`方法中,提供相应的参数以及标注标注即可, 例如我需要注入一个 `MySQL` + + ```python + from service.mysql import MySQL + from core.plugin import Plugin + + class TestPlugin(Plugin): + def __init__(self, mysql: MySQL): + self.mysql = mysql + + async def __async_init__(self): + """do something""" + + ``` + +## 关于 `handler` + +给函数加上 `core.plugin.handler` 这一装饰器即可将这个函数注册为`handler` + +### 非 `ConversationHandler` 的 `handler` + +1. 直接使用 `core.plugin.handler` 装饰器 + + 第一个参数是 `handler` 的种类,后续参数为该 `handler` 除 `callback` 参数外的其余参数 + + ```python + from core.plugin import Plugin, handler + from telegram import Update + from telegram.ext import CommandHandler, CallbackContext + + + class TestPlugin(Plugin): + @handler(CommandHandler, command='start', block=False) + async def start(self, update: Update, context: CallbackContext): + await update.effective_chat.send_message('hello world!') + ``` + + 比如上面代码中的 `command='start', block=False` 就是 `CommandHandler` 的参数 + +2. 使用 `core.plugin.handler` 的子装饰器 + + 这种方式比第一种简单, 不需要声明 `handler` 的类型 + + ```python + from core.plugin import Plugin, handler + from telegram import Update + from telegram.ext import CallbackContext + + + class TestPlugin(Plugin): + @handler.command(command='start', block=False) + async def start(self, update: Update, context: CallbackContext): + await update.effective_chat.send_message('hello world!') + ``` + +### 对于 `ConversationHandler` + +由于 `ConversationHandler` 比较特殊,所以**一个 Plugin 类中只能存在一个 `ConversationHandler`** + +`conversation.entry_point` 、`conversation.state` 和 `conversation.fallback` 装饰器分别对应 +`ConversationHandler` 的 `entry_points`、`stats` 和 `fallbacks` 参数 + +```python +from telegram import Update +from telegram.ext import CallbackContext, filters + +from core.plugin import Plugin, conversation, handler + +STATE_A, STATE_B, STATE_C = range(3) + + +class TestConversation(Plugin.Conversation, allow_reentry=True, block=False): + + @conversation.entry_point # 标注这个handler是ConversationHandler的一个entry_point + @handler.command(command='entry') + async def entry_point(self, update: Update, context: CallbackContext): + """do something""" + + @conversation.state(state=STATE_A) + @handler.message(filters=filters.TEXT) + async def state(self, update: Update, context: CallbackContext): + """do something""" + + @conversation.fallback + @handler.message(filters=filters.TEXT) + async def fallback(self, update: Update, context: CallbackContext): + """do something""" + + @handler.inline_query() # 你可以在此 Plugin 下定义其它类型的 handler + async def inline_query(self, update: Update, context: CallbackContext): + """do something""" + +``` + +### 对于 `Job` + +1. 依然需要继承 `core.plugin.Plugin` +2. 直接使用 `core.plugin.job` 装饰器 参数都与官方 `JobQueue` 类对应 + +```python +from core.plugin import Plugin, job + +class TestJob(Plugin): + + @job.run_repeating(interval=datetime.timedelta(hours=2), name="TestJob") + async def refresh(self, _: CallbackContext): + logger.info("TestJob") ``` ### 注意 -plugins 模块下的类必须提供 `create_handlers` 类方法作为构建相应处理程序给 `handle.py` +被注册到 `handler` 的函数需要添加 `error_callable` 修饰器作为错误统一处理 -在函数注册为命令处理过程(如 `CommandHandler` )需要添加 `error_callable` 修饰器作为错误统一处理 +被注册到 `handler` 的函数必须使用 `@restricts()` 修饰器 **预防洪水攻击** 但 `ConversationHandler` 外只需要注册入口函数使用 -如果引用服务,参数需要声明需要引用服务的类型并设置默认传入为 `None` ,并且添加 `inject` 修饰器 +如果引用服务,参数需要声明需要引用服务的类型并设置默认传入为 `None` 必要的函数必须捕获异常后通知用户或者直接抛出异常 -入口函数必须使用 `@restricts()` 修饰器 预防洪水攻击 - -只需在构建的类前加上 `@listener_plugins_class()` 修饰器即可向程序注册插件 - -**注意:`@restricts()` 修饰器带参,必须带括号,否则会出现调用错误** +**部分修饰器为带参修饰器,必须带括号,否则会出现调用错误** \ No newline at end of file diff --git a/plugins/base.py b/plugins/base.py deleted file mode 100644 index c6f4431..0000000 --- a/plugins/base.py +++ /dev/null @@ -1,97 +0,0 @@ -import asyncio -import datetime -from typing import List, Tuple, Callable - -from telegram import Update, ReplyKeyboardRemove -from telegram.error import BadRequest -from telegram.ext import CallbackContext, ConversationHandler, filters - -from core.admin.services import BotAdminService -from logger import Log -from utils.service.inject import inject - - -async def clean_message(context: CallbackContext, chat_id: int, message_id: int) -> bool: - try: - await context.bot.delete_message(chat_id=chat_id, message_id=message_id) - return True - except BadRequest as error: - if "not found" in str(error): - Log.warning(f"定时删除消息 chat_id[{chat_id}] message_id[{message_id}]失败 消息不存在") - elif "Message can't be deleted" in str(error): - Log.warning(f"定时删除消息 chat_id[{chat_id}] message_id[{message_id}]失败 消息无法删除 可能是没有授权") - else: - Log.warning(f"定时删除消息 chat_id[{chat_id}] message_id[{message_id}]失败 \n", error) - return False - - -def add_delete_message_job(context: CallbackContext, chat_id: int, message_id: int, - delete_seconds: int = 60): - context.job_queue.scheduler.add_job(clean_message, "date", - id=f"{chat_id}|{message_id}|auto_clean_message", - name=f"{chat_id}|{message_id}|auto_clean_message", - args=[context, chat_id, message_id], - run_date=context.job_queue._tz_now() + datetime.timedelta( - seconds=delete_seconds), replace_existing=True) - - -class BasePlugins: - - @staticmethod - async def cancel(update: Update, _: CallbackContext) -> int: - await update.message.reply_text("退出命令", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - - @staticmethod - async def _clean(context: CallbackContext, chat_id: int, message_id: int) -> bool: - return await clean_message(context, chat_id, message_id) - - @staticmethod - def _add_delete_message_job(context: CallbackContext, chat_id: int, message_id: int, - delete_seconds: int = 60): - return add_delete_message_job(context, chat_id, message_id, delete_seconds) - - -class NewChatMembersHandler: - - @inject - def __init__(self, bot_admin_service: BotAdminService = None): - self.bot_admin_service = bot_admin_service - self.callback: List[Tuple[Callable, int]] = [] - - def add_callback(self, callback, chat_id: int): - if chat_id >= 0: - raise ValueError - self.callback.append((callback, chat_id)) - - async def new_member(self, update: Update, context: CallbackContext) -> None: - message = update.message - chat = message.chat - from_user = message.from_user - quit_status = False - if filters.ChatType.GROUPS.filter(message): - for user in message.new_chat_members: - if user.id == context.bot.id: - if from_user is not None: - Log.info(f"用户 {from_user.full_name}[{from_user.id}] 在群 {chat.title}[{chat.id}] 邀请BOT") - admin_list = await self.bot_admin_service.get_admin_list() - if from_user.id in admin_list: - await context.bot.send_message(message.chat_id, - '感谢邀请小派蒙到本群!请使用 /help 查看咱已经学会的功能。') - else: - quit_status = True - else: - Log.info(f"未知用户 在群 {chat.title}[{chat.id}] 邀请BOT") - quit_status = True - if quit_status: - Log.warning("不是管理员邀请!退出群聊。") - await context.bot.send_message(message.chat_id, "派蒙不想进去!不是旅行者的邀请!") - await context.bot.leave_chat(chat.id) - else: - tasks = [] - for callback, chat_id in self.callback: - if chat.id == chat_id: - task = asyncio.create_task(callback(update, context)) - tasks.append(task) - if len(tasks) >= 1: - await asyncio.gather(*tasks) diff --git a/plugins/genshin/abyss.py b/plugins/genshin/abyss.py index 95aee16..91bcba4 100644 --- a/plugins/genshin/abyss.py +++ b/plugins/genshin/abyss.py @@ -1,19 +1,21 @@ +from typing import Dict + from genshin import Client from telegram import Update from telegram.constants import ChatAction -from telegram.ext import CommandHandler, MessageHandler, filters, CallbackContext +from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters +from core.baseplugin import BasePlugin +from core.cookies.error import CookiesNotFoundError from core.cookies.services import CookiesService -from core.template.services import TemplateService +from core.plugin import Plugin, handler +from core.template import TemplateService from core.user import UserService -from core.user.repositories import UserNotFoundError -from logger import Log -from plugins.base import BasePlugins +from core.user.error import UserNotFoundError from utils.decorators.error import error_callable from utils.decorators.restricts import restricts from utils.helpers import get_genshin_client, url_to_file -from utils.plugins.manager import listener_plugins_class -from utils.service.inject import inject +from utils.log import logger class AbyssUnlocked(Exception): @@ -26,25 +28,15 @@ class NoMostKills(Exception): pass -@listener_plugins_class() -class Abyss(BasePlugins): +class Abyss(Plugin, BasePlugin): """深渊数据查询""" - @inject def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None, template_service: TemplateService = None): self.template_service = template_service self.cookies_service = cookies_service self.user_service = user_service - @classmethod - def create_handlers(cls) -> list: - abyss = cls() - return [ - CommandHandler("abyss", abyss.command_start, block=False), - MessageHandler(filters.Regex(r"^深渊数据查询(.*)"), abyss.command_start, block=True) - ] - @staticmethod def _get_role_star_bg(value: int): if value == 4: @@ -54,7 +46,7 @@ class Abyss(BasePlugins): else: raise ValueError("错误的数据") - async def _get_abyss_data(self, client: Client) -> dict: + async def _get_abyss_data(self, client: Client) -> Dict: uid = client.uid await client.get_record_cards() spiral_abyss_info = await client.get_spiral_abyss(uid) @@ -101,17 +93,19 @@ class Abyss(BasePlugins): abyss_data["most_played_list"].append(temp) return abyss_data + @handler(CommandHandler, command="abyss", block=False) + @handler(MessageHandler, filters=filters.Regex("^深渊数据查询(.*)"), block=False) @restricts() @error_callable async def command_start(self, update: Update, context: CallbackContext) -> None: user = update.effective_user - message = update.message - Log.info(f"用户 {user.full_name}[{user.id}] 查深渊挑战命令请求") + message = update.effective_message + logger.info(f"用户 {user.full_name}[{user.id}] 查深渊挑战命令请求") await message.reply_chat_action(ChatAction.TYPING) try: - client = await get_genshin_client(user.id, self.user_service, self.cookies_service) + client = await get_genshin_client(user.id) abyss_data = await self._get_abyss_data(client) - except UserNotFoundError: + except (UserNotFoundError, CookiesNotFoundError): reply_message = await message.reply_text("未查询到账号信息,请先私聊派蒙绑定账号") if filters.ChatType.GROUPS.filter(message): self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id, 10) @@ -128,4 +122,3 @@ class Abyss(BasePlugins): {"width": 690, "height": 504}, full_page=False) await message.reply_photo(png_data, filename=f"abyss_{user.id}.png", allow_sending_without_reply=True) - return diff --git a/plugins/genshin/adduser.py b/plugins/genshin/adduser.py index 3dcc6d9..d362c47 100644 --- a/plugins/genshin/adduser.py +++ b/plugins/genshin/adduser.py @@ -3,106 +3,96 @@ from typing import Optional import genshin from genshin import InvalidCookies, GenshinException, DataNotPublic -from sqlalchemy.exc import NoResultFound from telegram import Update, ReplyKeyboardRemove, ReplyKeyboardMarkup, TelegramObject -from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters, ConversationHandler +from telegram.ext import CallbackContext, filters, ConversationHandler from telegram.helpers import escape_markdown +from core.baseplugin import BasePlugin +from core.cookies.error import CookiesNotFoundError +from core.cookies.models import Cookies from core.cookies.services import CookiesService +from core.plugin import Plugin, handler, conversation +from core.user.error import UserNotFoundError from core.user.models import User -from core.user.repositories import UserNotFoundError from core.user.services import UserService -from logger import Log -from models.base import RegionEnum -from plugins.base import BasePlugins from utils.decorators.error import error_callable from utils.decorators.restricts import restricts -from utils.plugins.manager import listener_plugins_class -from utils.service.inject import inject +from utils.log import logger +from utils.models.base import RegionEnum class AddUserCommandData(TelegramObject): user: Optional[User] = None + cookies_database_data: Optional[Cookies] = None region: RegionEnum = RegionEnum.HYPERION cookies: dict = {} game_uid: int = 0 -@listener_plugins_class() -class AddUser(BasePlugins): +CHECK_SERVER, CHECK_COOKIES, COMMAND_RESULT = range(10100, 10103) + + +class AddUser(Plugin.Conversation, BasePlugin.Conversation): """用户绑定""" - CHECK_SERVER, CHECK_COOKIES, COMMAND_RESULT = range(10100, 10103) - - @inject def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None): self.cookies_service = cookies_service self.user_service = user_service - @classmethod - def create_handlers(cls): - cookies = cls() - cookies_handler = ConversationHandler( - entry_points=[CommandHandler('adduser', cookies.command_start, filters.ChatType.PRIVATE, block=True), - MessageHandler(filters.Regex(r"^绑定账号(.*)") & filters.ChatType.PRIVATE, - cookies.command_start, block=True)], - states={ - cookies.CHECK_SERVER: [MessageHandler(filters.TEXT & ~filters.COMMAND, - cookies.check_server, block=True)], - cookies.CHECK_COOKIES: [MessageHandler(filters.TEXT & ~filters.COMMAND, - cookies.check_cookies, block=True)], - cookies.COMMAND_RESULT: [MessageHandler(filters.TEXT & ~filters.COMMAND, - cookies.command_result, block=True)], - }, - fallbacks=[CommandHandler('cancel', cookies.cancel, block=True)], - ) - return [cookies_handler] - + @conversation.entry_point + @handler.command(command='adduser', filters=filters.ChatType.PRIVATE, block=True) @restricts() @error_callable async def command_start(self, update: Update, context: CallbackContext) -> int: user = update.effective_user - Log.info(f"用户 {user.full_name}[{user.id}] 绑定账号命令请求") + message = update.effective_message + logger.info(f"用户 {user.full_name}[{user.id}] 绑定账号命令请求") add_user_command_data: AddUserCommandData = context.chat_data.get("add_user_command_data") if add_user_command_data is None: cookies_command_data = AddUserCommandData() context.chat_data["add_user_command_data"] = cookies_command_data - message = f'你好 {user.mention_markdown_v2()} {escape_markdown("!请选择要绑定的服务器!或回复退出取消操作")}' + text = f'你好 {user.mention_markdown_v2()} {escape_markdown("!请选择要绑定的服务器!或回复退出取消操作")}' reply_keyboard = [['米游社', 'HoYoLab'], ["退出"]] - await update.message.reply_markdown_v2(message, - reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)) - - return self.CHECK_SERVER + await message.reply_markdown_v2(text, reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)) + return CHECK_SERVER + @conversation.state(state=CHECK_SERVER) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) @error_callable async def check_server(self, update: Update, context: CallbackContext) -> int: user = update.effective_user + message = update.effective_message add_user_command_data: AddUserCommandData = context.chat_data.get("add_user_command_data") + if message.text == "退出": + await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + elif message.text == "米游社": + region = RegionEnum.HYPERION + bbs_url = "https://bbs.mihoyo.com/ys/" + bbs_name = "米游社" + elif message.text == "HoYoLab": + bbs_url = "https://www.hoyolab.com/home" + bbs_name = "HoYoLab" + region = RegionEnum.HOYOLAB + else: + await message.reply_text("选择错误,请重新选择") + return CHECK_SERVER try: user_info = await self.user_service.get_user_by_id(user.id) except UserNotFoundError: user_info = None + if user_info is not None: + try: + cookies_database_data = await self.cookies_service.get_cookies(user.id, add_user_command_data.region) + add_user_command_data.cookies_database_data = cookies_database_data + except CookiesNotFoundError: + await message.reply_text("你已经绑定UID,如果继续操作会覆盖当前UID。") + else: + await message.reply_text("警告,你已经绑定Cookie,如果继续操作会覆盖当前Cookie。") add_user_command_data.user = user_info - if update.message.text == "退出": - await update.message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - elif update.message.text == "米游社": - add_user_command_data.region = RegionEnum.HYPERION - bbs_url = "https://bbs.mihoyo.com/ys/" - bbs_name = "米游社" - if user_info is not None: - await update.message.reply_text("警告,你已经绑定Cookie,如果继续操作会覆盖当前Cookie。") - elif update.message.text == "HoYoLab": - bbs_url = "https://www.hoyolab.com/home" - bbs_name = "HoYoLab" - add_user_command_data.region = RegionEnum.HOYOLAB - if user_info is not None: - await update.message.reply_text("警告,你已经绑定Cookie,如果继续操作会覆盖当前Cookie。") - else: - await update.message.reply_text("选择错误,请重新选择") - return self.CHECK_SERVER - await update.message.reply_text(f"请输入{bbs_name}的Cookies!或回复退出取消操作", reply_markup=ReplyKeyboardRemove()) + add_user_command_data.region = region + await message.reply_text(f"请输入{bbs_name}的Cookies!或回复退出取消操作", reply_markup=ReplyKeyboardRemove()) javascript = "javascript:(()=>{_=(n)=>{for(i in(r=document.cookie.split(';'))){var a=r[i].split('=');if(a[" \ "0].trim()==n)return a[1]}};c=_('account_id')||alert('无效的Cookie,请重新登录!');c&&confirm(" \ "'将Cookie复制到剪贴板?')&©(document.cookie)})(); " @@ -118,76 +108,79 @@ class AddUser(BasePlugins): f"[1、通过 Via 浏览器打开{bbs_name}并登录]({bbs_url})\n" \ f"2、复制下方的代码,并将其粘贴在地址栏中,点击右侧箭头\n" \ f"`{escape_markdown(javascript_android, version=2, entity_type='code')}`" - await update.message.reply_markdown_v2(help_message, disable_web_page_preview=True) - return self.CHECK_COOKIES + await message.reply_markdown_v2(help_message, disable_web_page_preview=True) + return CHECK_COOKIES + @conversation.state(state=CHECK_COOKIES) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) @error_callable async def check_cookies(self, update: Update, context: CallbackContext) -> int: user = update.effective_user + message = update.effective_message add_user_command_data: AddUserCommandData = context.chat_data.get("add_user_command_data") - if update.message.text == "退出": - await update.message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) + if message.text == "退出": + await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END - str_cookies = update.message.text + str_cookies = message.text cookie = SimpleCookie() try: cookie.load(str_cookies) except CookieError: - await update.message.reply_text("Cookies格式有误,请检查", reply_markup=ReplyKeyboardRemove()) + await message.reply_text("Cookies格式有误,请检查", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END if len(cookie) == 0: - await update.message.reply_text("Cookies格式有误,请检查", reply_markup=ReplyKeyboardRemove()) + await message.reply_text("Cookies格式有误,请检查", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END cookies = {key: morsel.value for key, morsel in cookie.items()} if not cookies: - await update.message.reply_text("Cookies格式有误,请检查", reply_markup=ReplyKeyboardRemove()) + await message.reply_text("Cookies格式有误,请检查", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END if add_user_command_data.region == RegionEnum.HYPERION: client = genshin.ChineseClient(cookies=cookies) elif add_user_command_data.region == RegionEnum.HOYOLAB: client = genshin.GenshinClient(cookies=cookies) else: - await update.message.reply_text("数据错误", reply_markup=ReplyKeyboardRemove()) + await message.reply_text("数据错误", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END try: user_info = await client.get_record_card() except DataNotPublic: - await update.message.reply_text("账号疑似被注销,请检查账号状态", reply_markup=ReplyKeyboardRemove()) + await message.reply_text("账号疑似被注销,请检查账号状态", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END except InvalidCookies: - await update.message.reply_text("Cookies已经过期,请检查是否正确", reply_markup=ReplyKeyboardRemove()) + await message.reply_text("Cookies已经过期,请检查是否正确", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END except GenshinException as error: - await update.message.reply_text(f"获取账号信息发生错误,错误信息为 {str(error)},请检查Cookie或者账号是否正常", - reply_markup=ReplyKeyboardRemove()) + await message.reply_text(f"获取账号信息发生错误,错误信息为 {str(error)},请检查Cookie或者账号是否正常", + reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END except (AttributeError, ValueError): - await update.message.reply_text("Cookies错误,请检查是否正确", reply_markup=ReplyKeyboardRemove()) + await message.reply_text("Cookies错误,请检查是否正确", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END add_user_command_data.cookies = cookies add_user_command_data.game_uid = user_info.uid reply_keyboard = [['确认', '退出']] - await update.message.reply_text("获取角色基础信息成功,请检查是否正确!") - Log.info(f"用户 {user.full_name}[{user.id}] 获取账号 {user_info.nickname}[{user_info.uid}] 信息成功") - message = f"*角色信息*\n" \ - f"角色名称:{escape_markdown(user_info.nickname, version=2)}\n" \ - f"角色等级:{user_info.level}\n" \ - f"UID:`{user_info.uid}`\n" \ - f"服务器名称:`{user_info.server_name}`\n" - await update.message.reply_markdown_v2( - message, - reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True) - ) - return self.COMMAND_RESULT + await message.reply_text("获取角色基础信息成功,请检查是否正确!") + logger.info(f"用户 {user.full_name}[{user.id}] 获取账号 {user_info.nickname}[{user_info.uid}] 信息成功") + text = f"*角色信息*\n" \ + f"角色名称:{escape_markdown(user_info.nickname, version=2)}\n" \ + f"角色等级:{user_info.level}\n" \ + f"UID:`{user_info.uid}`\n" \ + f"服务器名称:`{user_info.server_name}`\n" + await message.reply_markdown_v2(text, reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)) + return COMMAND_RESULT + @conversation.state(state=COMMAND_RESULT) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) @error_callable async def command_result(self, update: Update, context: CallbackContext) -> int: user = update.effective_user + message = update.effective_message add_user_command_data: AddUserCommandData = context.chat_data.get("add_user_command_data") - if update.message.text == "退出": - await update.message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) + if message.text == "退出": + await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END - elif update.message.text == "确认": + elif message.text == "确认": if add_user_command_data.user is None: if add_user_command_data.region == RegionEnum.HYPERION: user_db = User(user_id=user.id, yuanshen_uid=add_user_command_data.game_uid, @@ -196,11 +189,9 @@ class AddUser(BasePlugins): user_db = User(user_id=user.id, genshin_uid=add_user_command_data.game_uid, region=add_user_command_data.region) else: - await update.message.reply_text("数据错误") + await message.reply_text("数据错误") return ConversationHandler.END await self.user_service.add_user(user_db) - await self.cookies_service.add_cookies(user.id, add_user_command_data.cookies, - add_user_command_data.region) else: user_db = add_user_command_data.user user_db.region = add_user_command_data.region @@ -209,19 +200,18 @@ class AddUser(BasePlugins): elif add_user_command_data.region == RegionEnum.HOYOLAB: user_db.genshin_uid = add_user_command_data.game_uid else: - await update.message.reply_text("数据错误") + await message.reply_text("数据错误") return ConversationHandler.END await self.user_service.update_user(user_db) - # 临时解决错误 - try: - await self.cookies_service.update_cookies(user.id, add_user_command_data.cookies, - add_user_command_data.region) - except NoResultFound: - await self.cookies_service.add_cookies(user.id, add_user_command_data.cookies, - add_user_command_data.region) - Log.info(f"用户 {user.full_name}[{user.id}] 绑定账号成功") - await update.message.reply_text("保存成功", reply_markup=ReplyKeyboardRemove()) + if add_user_command_data.cookies_database_data is None: + await self.cookies_service.add_cookies(user.id, add_user_command_data.cookies, + add_user_command_data.region) + else: + await self.cookies_service.update_cookies(user.id, add_user_command_data.cookies, + add_user_command_data.region) + logger.info(f"用户 {user.full_name}[{user.id}] 绑定账号成功") + await message.reply_text("保存成功", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END else: - await update.message.reply_text("回复错误,请重新输入") - return self.COMMAND_RESULT + await message.reply_text("回复错误,请重新输入") + return COMMAND_RESULT diff --git a/plugins/genshin/artifact_rate.py b/plugins/genshin/artifact_rate.py index 3338907..8b15cfa 100644 --- a/plugins/genshin/artifact_rate.py +++ b/plugins/genshin/artifact_rate.py @@ -2,24 +2,22 @@ from typing import Optional from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, File from telegram.constants import ChatAction, ParseMode -from telegram.ext import CallbackContext, ConversationHandler, CommandHandler, CallbackQueryHandler, MessageHandler, \ - filters +from telegram.ext import CallbackContext, ConversationHandler, filters from telegram.helpers import escape_markdown -from logger import Log -from models.apihelper.artifact import ArtifactOcrRate, get_comment, get_format_sub_item -from plugins.base import BasePlugins +from core.baseplugin import BasePlugin +from core.plugin import Plugin, conversation, handler +from modules.apihelper.artifact import ArtifactOcrRate, get_comment, get_format_sub_item from utils.decorators.error import error_callable from utils.decorators.restricts import restricts -from utils.plugins.manager import listener_plugins_class +from utils.log import logger + +COMMAND_RESULT = 1 -@listener_plugins_class() -class ArtifactRate(BasePlugins): +class ArtifactRate(Plugin.Conversation, BasePlugin.Conversation): """圣遗物评分""" - COMMAND_RESULT = 1 - STAR_KEYBOARD = [[ InlineKeyboardButton( f"{i}", callback_data=f"artifact_ocr_rate_data|star|{i}") for i in range(1, 6) @@ -33,21 +31,6 @@ class ArtifactRate(BasePlugins): def __init__(self): self.artifact_rate = ArtifactOcrRate() - @classmethod - def create_handlers(cls) -> list: - artifact_rate = cls() - return [ - ConversationHandler( - entry_points=[CommandHandler('artifact_rate', artifact_rate.command_start), - MessageHandler(filters.Regex(r"^圣遗物评分(.*)"), artifact_rate.command_start), - MessageHandler(filters.CaptionRegex(r"^圣遗物评分(.*)"), artifact_rate.command_start)], - states={ - artifact_rate.COMMAND_RESULT: [CallbackQueryHandler(artifact_rate.command_result)] - }, - fallbacks=[CommandHandler('cancel', artifact_rate.cancel)] - ) - ] - async def get_rate(self, artifact_attr: dict) -> str: rate_result_req = await self.artifact_rate.rate_artifact(artifact_attr) if rate_result_req.status_code != 200: @@ -67,12 +50,16 @@ class ArtifactRate(BasePlugins): f"{escape_markdown(get_comment(rate_result['total_percent']), version=2)}\n" \ "_评分、识图均来自 genshin\\.pub_" + @conversation.entry_point + @handler.command(command='artifact_rate', filters=filters.ChatType.PRIVATE, block=True) + @handler.message(filters=filters.Regex(r"^圣遗物评分(.*)"), block=True) + @handler.message(filters=filters.CaptionRegex(r"^圣遗物评分(.*)"), block=True) @error_callable @restricts(return_data=ConversationHandler.END) async def command_start(self, update: Update, context: CallbackContext) -> int: - message = update.message + message = update.effective_message user = update.effective_user - Log.info(f"用户 {user.full_name}[{user.id}] 圣遗物评分命令请求") + logger.info(f"用户 {user.full_name}[{user.id}] 圣遗物评分命令请求") context.user_data["artifact_attr"] = None photo_file: Optional[File] = None if message is None: @@ -110,17 +97,19 @@ class ArtifactRate(BasePlugins): if artifact_attr.get("star") is None: await message.reply_text("无法识别圣遗物星级,请选择圣遗物星级", reply_markup=InlineKeyboardMarkup(self.STAR_KEYBOARD)) - return self.COMMAND_RESULT + return COMMAND_RESULT if artifact_attr.get("level") is None: await message.reply_text("无法识别圣遗物等级,请选择圣遗物等级", reply_markup=InlineKeyboardMarkup(self.LEVEL_KEYBOARD)) - return self.COMMAND_RESULT + return COMMAND_RESULT reply_message = await message.reply_text("识图成功!\n" "正在评分中...") rate_text = await self.get_rate(artifact_attr) await reply_message.edit_text(rate_text, parse_mode=ParseMode.MARKDOWN_V2) return ConversationHandler.END + @conversation.state(state=COMMAND_RESULT) + @handler.callback_query() @error_callable async def command_result(self, update: Update, context: CallbackContext) -> int: query = update.callback_query @@ -151,11 +140,11 @@ class ArtifactRate(BasePlugins): if artifact_attr.get("level") is None: await query.edit_message_text("无法识别圣遗物等级,请选择圣遗物等级", reply_markup=InlineKeyboardMarkup(self.LEVEL_KEYBOARD)) - return self.COMMAND_RESULT + return COMMAND_RESULT if artifact_attr.get("star") is None: await query.edit_message_text("无法识别圣遗物星级,请选择圣遗物星级", reply_markup=InlineKeyboardMarkup(self.STAR_KEYBOARD)) - return self.COMMAND_RESULT + return COMMAND_RESULT await query.edit_message_text("正在评分中...") rate_text = await self.get_rate(artifact_attr) await query.edit_message_text(rate_text, parse_mode=ParseMode.MARKDOWN_V2) diff --git a/plugins/genshin/daily_note.py b/plugins/genshin/daily_note.py index 668262c..dfb8dfd 100644 --- a/plugins/genshin/daily_note.py +++ b/plugins/genshin/daily_note.py @@ -8,26 +8,22 @@ from telegram.constants import ChatAction from telegram.ext import CommandHandler, MessageHandler, ConversationHandler, filters, \ CallbackContext +from core.baseplugin import BasePlugin +from core.cookies.error import CookiesNotFoundError from core.cookies.services import CookiesService +from core.plugin import Plugin, handler from core.template.services import TemplateService -from core.user.repositories import UserNotFoundError +from core.user.error import UserNotFoundError from core.user.services import UserService -from logger import Log -from plugins.base import BasePlugins from utils.decorators.error import error_callable from utils.decorators.restricts import restricts from utils.helpers import get_genshin_client -from utils.plugins.manager import listener_plugins_class -from utils.service.inject import inject +from utils.log import logger -@listener_plugins_class() -class DailyNote(BasePlugins): +class DailyNote(Plugin, BasePlugin): """每日便签""" - COMMAND_RESULT, = range(10200, 10201) - - @inject def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None, template_service: TemplateService = None): self.template_service = template_service @@ -35,12 +31,6 @@ class DailyNote(BasePlugins): self.user_service = user_service self.current_dir = os.getcwd() - @classmethod - def create_handlers(cls) -> list: - daily_note = cls() - return [CommandHandler('dailynote', daily_note.command_start, block=True), - MessageHandler(filters.Regex(r"^当前状态(.*)"), daily_note.command_start, block=True)] - async def _get_daily_note(self, client) -> bytes: daily_info = await client.get_genshin_notes(client.uid) day = datetime.datetime.now().strftime("%m-%d %H:%M") + " 星期" + "一二三四五六日"[datetime.datetime.now().weekday()] @@ -89,16 +79,18 @@ class DailyNote(BasePlugins): {"width": 600, "height": 548}, full_page=False) return png_data - @restricts() + @handler(CommandHandler, command="dailynote", block=False) + @handler(MessageHandler, filters=filters.Regex("^当前状态(.*)"), block=False) + @restricts(return_data=ConversationHandler.END) @error_callable async def command_start(self, update: Update, context: CallbackContext) -> Optional[int]: user = update.effective_user message = update.message - Log.info(f"用户 {user.full_name}[{user.id}] 查询游戏状态命令请求") + logger.info(f"用户 {user.full_name}[{user.id}] 查询游戏状态命令请求") try: - client = await get_genshin_client(user.id, self.user_service, self.cookies_service) + client = await get_genshin_client(user.id) png_data = await self._get_daily_note(client) - except UserNotFoundError: + except (UserNotFoundError, CookiesNotFoundError): reply_message = await message.reply_text("未查询到账号信息,请先私聊派蒙绑定账号") if filters.ChatType.GROUPS.filter(message): self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id, 30) diff --git a/plugins/genshin/gacha/__init__.py b/plugins/genshin/gacha/__init__.py index 997efa6..6ae1c8e 100644 --- a/plugins/genshin/gacha/__init__.py +++ b/plugins/genshin/gacha/__init__.py @@ -1,7 +1,5 @@ -from utils.plugins.manager import listener_plugins_class from .gacha import Gacha -@listener_plugins_class() class GachaPlugins(Gacha): pass diff --git a/plugins/genshin/gacha/gacha.py b/plugins/genshin/gacha/gacha.py index 11f5d31..ae7a7c6 100644 --- a/plugins/genshin/gacha/gacha.py +++ b/plugins/genshin/gacha/gacha.py @@ -7,34 +7,20 @@ from telegram import Update from telegram.constants import ChatAction from telegram.ext import filters, CommandHandler, MessageHandler, CallbackContext +from core.baseplugin import BasePlugin +from core.plugin import Plugin, handler from core.template import TemplateService -from logger import Log -from models.apihelper.gacha import GachaInfo -from plugins.base import BasePlugins +from modules.apihelper.gacha import GachaInfo from plugins.genshin.gacha.wish import WishCountInfo, get_one from utils.bot import get_all_args from utils.decorators.error import error_callable from utils.decorators.restricts import restricts -from utils.plugins.manager import listener_plugins_class -from utils.service.inject import inject +from utils.log import logger -@listener_plugins_class() -class Gacha(BasePlugins): +class Gacha(Plugin, BasePlugin): """抽卡模拟器(非首模拟器/减寿模拟器)""" - CHECK_SERVER, COMMAND_RESULT = range(10600, 10602) - - @classmethod - def create_handlers(cls) -> list: - gacha = cls() - return [ - CommandHandler("gacha", gacha.command_start, block=False), - MessageHandler(filters.Regex("^抽卡模拟器(.*)"), gacha.command_start, block=False), - MessageHandler(filters.Regex("^非首模拟器(.*)"), gacha.command_start, block=False), - ] - - @inject def __init__(self, template_service: TemplateService = None): self.gacha = GachaInfo() self.template_service = template_service @@ -59,6 +45,9 @@ class Gacha(BasePlugins): gacha_info["gacha_id"] = gacha_id return gacha_info + @handler(CommandHandler, command="gacha", block=False) + @handler(MessageHandler, filters=filters.Regex("^深渊数据查询(.*)"), block=False) + @handler(MessageHandler, filters=filters.Regex("^非首模拟器(.*)"), block=False) @restricts(filters.ChatType.GROUPS, restricts_time=20, try_delete_message=True) @restricts(filters.ChatType.PRIVATE) @error_callable @@ -80,7 +69,7 @@ class Gacha(BasePlugins): return else: gacha_info = await self.gacha_info(default=True) - Log.info(f"用户 {user.full_name}[{user.id}] 抽卡模拟器命令请求 || 参数 {gacha_name}") + logger.info(f"用户 {user.full_name}[{user.id}] 抽卡模拟器命令请求 || 参数 {gacha_name}") # 用户数据储存和处理 gacha_id: str = gacha_info["gacha_id"] user_gacha: dict[str, WishCountInfo] = context.user_data.get("gacha") @@ -95,7 +84,7 @@ class Gacha(BasePlugins): if re_color is None: title_html = BeautifulSoup(title, "lxml") pool_name = title_html.text - Log.warning(f"卡池信息 title 提取 color 失败 title[{title}]") + logger.warning(f"卡池信息 title 提取 color 失败 title[{title}]") else: color = re_color.group(1) title_html = BeautifulSoup(title, "lxml") diff --git a/plugins/genshin/help.py b/plugins/genshin/help.py index 46afacd..ad811b0 100644 --- a/plugins/genshin/help.py +++ b/plugins/genshin/help.py @@ -3,41 +3,32 @@ from telegram.constants import ChatAction from telegram.error import BadRequest from telegram.ext import CommandHandler, CallbackContext -from config import config -from core.template.services import TemplateService -from logger import Log +from core.bot import bot +from core.plugin import Plugin, handler +from core.template import TemplateService from utils.decorators.error import error_callable from utils.decorators.restricts import restricts -from utils.plugins.manager import listener_plugins_class -from utils.service.inject import inject +from utils.log import logger -@listener_plugins_class() -class Help: - """帮助菜单""" - - @inject +class HelpPlugin(Plugin): def __init__(self, template_service: TemplateService = None): - self.template_service = template_service - self.help_png = None self.file_id = None + self.help_png = None + if template_service is None: + raise ModuleNotFoundError + self.template_service = template_service - @classmethod - def create_handlers(cls) -> list: - _help = cls() - return [ - CommandHandler("help", _help.command_start, block=False), - ] - + @handler(CommandHandler, command="help", block=False) @error_callable @restricts() - async def command_start(self, update: Update, _: CallbackContext) -> None: - message = update.message + async def start(self, update: Update, _: CallbackContext): user = update.effective_user - Log.info(f"用户 {user.full_name}[{user.id}] 发出help命令") - if self.file_id is None or config.debug: + message = update.effective_message + logger.info(f"用户 {user.full_name}[{user.id}] 发出help命令") + if self.file_id is None or bot.config.debug: await message.reply_chat_action(ChatAction.TYPING) - help_png = await self.template_service.render('bot/help', "help.html", {}, {"width": 768, "height": 768}) + help_png = await self.template_service.render("bot/help", "help.html", {}, {"width": 768, "height": 768}) await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) reply_photo = await message.reply_photo(help_png, filename="help.png", allow_sending_without_reply=True) photo = reply_photo.photo[0] @@ -48,5 +39,5 @@ class Help: await message.reply_photo(self.file_id, allow_sending_without_reply=True) except BadRequest as error: self.file_id = None - Log.error("发送图片失败,尝试清空已经保存的file_id,错误信息为", error) + logger.error("发送图片失败,尝试清空已经保存的file_id,错误信息为", error) await message.reply_text("发送图片失败", allow_sending_without_reply=True) diff --git a/plugins/genshin/hilichurls.py b/plugins/genshin/hilichurls.py index 08177b4..c5c93d8 100644 --- a/plugins/genshin/hilichurls.py +++ b/plugins/genshin/hilichurls.py @@ -2,34 +2,30 @@ import json from os import sep from telegram import Update -from telegram.ext import CommandHandler, CallbackContext, filters +from telegram.ext import CommandHandler, CallbackContext +from telegram.ext import filters -from logger import Log -from plugins.base import BasePlugins +from core.baseplugin import BasePlugin +from core.plugin import Plugin, handler from utils.bot import get_all_args from utils.decorators.error import error_callable from utils.decorators.restricts import restricts -from utils.plugins.manager import listener_plugins_class +from utils.log import logger -@listener_plugins_class() -class Hilichurls(BasePlugins): +class HilichurlsPlugin(Plugin, BasePlugin): + """丘丘语字典.""" def __init__(self): """加载数据文件.数据整理自 https://wiki.biligame.com/ys By @zhxycn.""" with open(f"resources{sep}json{sep}hilichurls_dictionary.json", "r", encoding="utf8") as f: self.hilichurls_dictionary = json.load(f) - @classmethod - def create_handlers(cls): - hilichurls = cls() - return [CommandHandler('hilichurls', hilichurls.command_start)] - - @error_callable + @handler(CommandHandler, command="hilichurls", block=False) @restricts() + @error_callable async def command_start(self, update: Update, context: CallbackContext) -> None: - """丘丘语字典.""" - message = update.message + message = update.effective_message user = update.effective_user args = get_all_args(context) if len(args) >= 1: @@ -47,6 +43,6 @@ class Hilichurls(BasePlugins): self._add_delete_message_job(context, message.chat_id, message.message_id) self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id) return - Log.info(f"用户 {user.full_name}[{user.id}] 查询丘丘语字典命令请求 || 参数 {msg}") + logger.info(f"用户 {user.full_name}[{user.id}] 查询丘丘语字典命令请求 || 参数 {msg}") result = self.hilichurls_dictionary[f"{search}"] await message.reply_markdown_v2(f"丘丘语: `{search}`\n\n`{result}`") diff --git a/plugins/genshin/ledger.py b/plugins/genshin/ledger.py index 828e800..cfbc002 100644 --- a/plugins/genshin/ledger.py +++ b/plugins/genshin/ledger.py @@ -6,20 +6,20 @@ from datetime import datetime, timedelta from genshin import GenshinException, DataNotPublic from telegram import Update from telegram.constants import ChatAction -from telegram.ext import CallbackContext, CommandHandler, MessageHandler, ConversationHandler, filters +from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters +from core.baseplugin import BasePlugin +from core.cookies.error import CookiesNotFoundError from core.cookies.services import CookiesService +from core.plugin import Plugin, handler from core.template.services import TemplateService -from core.user.repositories import UserNotFoundError +from core.user.error import UserNotFoundError from core.user.services import UserService -from logger import Log -from plugins.base import BasePlugins from utils.bot import get_all_args from utils.decorators.error import error_callable from utils.decorators.restricts import restricts from utils.helpers import get_genshin_client -from utils.plugins.manager import listener_plugins_class -from utils.service.inject import inject +from utils.log import logger def check_ledger_month(context: CallbackContext) -> int: @@ -50,13 +50,9 @@ def check_ledger_month(context: CallbackContext) -> int: return now_time.month -@listener_plugins_class() -class Ledger(BasePlugins): +class Ledger(Plugin, BasePlugin): """旅行札记""" - COMMAND_RESULT, = range(10200, 10201) - - @inject def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None, template_service: TemplateService = None): self.template_service = template_service @@ -64,12 +60,6 @@ class Ledger(BasePlugins): self.user_service = user_service self.current_dir = os.getcwd() - @classmethod - def create_handlers(cls): - ledger = cls() - return [CommandHandler("ledger", ledger.command_start, block=True), - MessageHandler(filters.Regex(r"^旅行扎记(.*)"), ledger.command_start, block=True)] - async def _start_get_ledger(self, client, month=None) -> bytes: try: diary_info = await client.get_diary(client.uid, month=month) @@ -142,8 +132,10 @@ class Ledger(BasePlugins): evaluate=evaluate) return png_data + @handler(CommandHandler, command="ledger", block=False) + @handler(MessageHandler, filters=filters.Regex("^旅行扎记(.*)"), block=False) + @restricts() @error_callable - @restricts(return_data=ConversationHandler.END) async def command_start(self, update: Update, context: CallbackContext) -> None: user = update.effective_user message = update.message @@ -155,12 +147,12 @@ class Ledger(BasePlugins): self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id, 30) self._add_delete_message_job(context, message.chat_id, message.message_id, 30) return - Log.info(f"用户 {user.full_name}[{user.id}] 查询原石手扎") + logger.info(f"用户 {user.full_name}[{user.id}] 查询原石手扎") await update.message.reply_chat_action(ChatAction.TYPING) try: - client = await get_genshin_client(user.id, self.user_service, self.cookies_service) + client = await get_genshin_client(user.id) png_data = await self._start_get_ledger(client, month) - except UserNotFoundError: + except (UserNotFoundError, CookiesNotFoundError): reply_message = await message.reply_text("未查询到账号信息,请先私聊派蒙绑定账号") if filters.ChatType.GROUPS.filter(message): self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id, 30) diff --git a/plugins/genshin/map/__init__.py b/plugins/genshin/map/__init__.py index 62f58ea..cf3e8b3 100644 --- a/plugins/genshin/map/__init__.py +++ b/plugins/genshin/map/__init__.py @@ -1,7 +1,5 @@ -from utils.plugins.manager import listener_plugins_class from .map import Map -@listener_plugins_class() class MapPlugins(Map): pass diff --git a/plugins/genshin/map/map.py b/plugins/genshin/map/map.py index 1198deb..de8a4de 100644 --- a/plugins/genshin/map/map.py +++ b/plugins/genshin/map/map.py @@ -5,34 +5,29 @@ from telegram import Update from telegram.constants import ChatAction from telegram.ext import CommandHandler, MessageHandler, filters, CallbackContext -from logger import Log -from plugins.base import BasePlugins +from core.baseplugin import BasePlugin +from core.plugin import handler, Plugin from utils.decorators.error import error_callable from utils.decorators.restricts import restricts +from utils.log import logger from .model import MapHelper -class Map(BasePlugins): +class Map(Plugin, BasePlugin): """支持资源点查询""" def __init__(self): self.init_resource_map = False self.map_helper = MapHelper() - @classmethod - def create_handlers(cls) -> list: - map_res = cls() - return [ - CommandHandler("map", map_res.command_start, block=False), - MessageHandler(filters.Regex(r"^资源点查询(.*)"), map_res.command_start, block=True) - ] - async def init_point_list_and_map(self): - Log.info("正在初始化地图资源节点") + logger.info("正在初始化地图资源节点") if not self.init_resource_map: await self.map_helper.init_point_list_and_map() self.init_resource_map = True + @handler(CommandHandler, command="map", block=False) + @handler(MessageHandler, filters=filters.Regex("^资源点查询(.*)"), block=False) @error_callable @restricts(restricts_time=20) async def command_start(self, update: Update, context: CallbackContext): @@ -45,7 +40,7 @@ class Map(BasePlugins): if len(args) >= 1: resource_name = args[0] else: - Log.info(f"用户: {user.full_name} [{user.id}] 使用了 map 命令") + logger.info(f"用户: {user.full_name} [{user.id}] 使用了 map 命令") await message.reply_text("请输入要查找的资源,或私聊派蒙发送 `/map list` 查看资源列表", parse_mode="Markdown") return if resource_name in ("list", "列表"): @@ -54,11 +49,11 @@ class Map(BasePlugins): self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id, 300) self._add_delete_message_job(context, message.chat_id, message.message_id, 300) return - Log.info(f"用户: {user.full_name} [{user.id}] 使用 map 命令查询了 资源列表") + logger.info(f"用户: {user.full_name} [{user.id}] 使用 map 命令查询了 资源列表") text = self.map_helper.get_resource_list_mes() await message.reply_text(text) return - Log.info(f"用户: {user.full_name} [{user.id}] 使用 map 命令查询了 {resource_name}") + logger.info(f"用户: {user.full_name} [{user.id}] 使用 map 命令查询了 {resource_name}") text = await self.map_helper.get_resource_map_mes(resource_name) if "不知道" in text or "没有找到" in text: await message.reply_text(text, parse_mode="Markdown") diff --git a/plugins/genshin/material.py b/plugins/genshin/material.py index 4552439..5c4e6c0 100644 --- a/plugins/genshin/material.py +++ b/plugins/genshin/material.py @@ -2,41 +2,32 @@ from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.constants import ChatAction, ParseMode from telegram.ext import filters, ConversationHandler, CommandHandler, MessageHandler, CallbackContext +from core.baseplugin import BasePlugin from core.game.services import GameMaterialService -from logger import Log -from plugins.base import BasePlugins +from core.plugin import Plugin, handler from utils.bot import get_all_args from utils.decorators.error import error_callable from utils.decorators.restricts import restricts from utils.helpers import url_to_file -from utils.plugins.manager import listener_plugins_class -from utils.service.inject import inject +from utils.log import logger -@listener_plugins_class() -class Material(BasePlugins): +class Material(Plugin, BasePlugin): """角色培养素材查询""" KEYBOARD = [[InlineKeyboardButton( text="查看角色培养素材列表并查询", switch_inline_query_current_chat="查看角色培养素材列表并查询")]] - @inject def __init__(self, game_material_service: GameMaterialService = None): self.game_material_service = game_material_service - @classmethod - def create_handlers(cls) -> list: - material = cls() - return [ - CommandHandler("material", material.command_start, block=False), - MessageHandler(filters.Regex("^角色培养素材查询(.*)"), material.command_start, block=False), - ] - - @error_callable + @handler(CommandHandler, command="material", block=False) + @handler(MessageHandler, filters=filters.Regex("^角色培养素材查询(.*)"), block=False) @restricts(return_data=ConversationHandler.END) + @error_callable async def command_start(self, update: Update, context: CallbackContext) -> None: - message = update.message + message = update.effective_message user = update.effective_user args = get_all_args(context) if len(args) >= 1: @@ -56,7 +47,7 @@ class Material(BasePlugins): self._add_delete_message_job(context, message.chat_id, message.message_id) self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id) return - Log.info(f"用户 {user.full_name}[{user.id}] 查询角色培养素材命令请求 || 参数 {character_name}") + logger.info(f"用户 {user.full_name}[{user.id}] 查询角色培养素材命令请求 || 参数 {character_name}") await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) file_path = await url_to_file(url, "") caption = "From 米游社 " \ diff --git a/plugins/genshin/post.py b/plugins/genshin/post.py index 364a06a..e0be828 100644 --- a/plugins/genshin/post.py +++ b/plugins/genshin/post.py @@ -4,17 +4,18 @@ from bs4 import BeautifulSoup from telegram import Update, ReplyKeyboardMarkup, ReplyKeyboardRemove, InputMediaPhoto from telegram.constants import ParseMode, MessageLimit from telegram.error import BadRequest -from telegram.ext import CallbackContext, ConversationHandler, CommandHandler, MessageHandler, filters +from telegram.ext import CallbackContext, ConversationHandler, filters from telegram.helpers import escape_markdown -from config import config -from logger import Log -from models.apihelper.base import ArtworkImage -from models.apihelper.hyperion import Hyperion -from plugins.base import BasePlugins +from core.baseplugin import BasePlugin +from core.bot import bot +from core.plugin import Plugin, conversation, handler +from modules.apihelper.base import ArtworkImage +from modules.apihelper.hyperion import Hyperion from utils.decorators.admins import bot_admins_rights_check from utils.decorators.error import error_callable -from utils.plugins.manager import listener_plugins_class +from utils.decorators.restricts import restricts +from utils.log import logger class PostHandlerData: @@ -27,44 +28,27 @@ class PostHandlerData: self.tags: Optional[List[str]] = [] -@listener_plugins_class() -class Post(BasePlugins): - """文章推送""" +CHECK_POST, SEND_POST, CHECK_COMMAND, GTE_DELETE_PHOTO = range(10900, 10904) +GET_POST_CHANNEL, GET_TAGS, GET_TEXT = range(10904, 10907) - CHECK_POST, SEND_POST, CHECK_COMMAND, GTE_DELETE_PHOTO = range(10900, 10904) - GET_POST_CHANNEL, GET_TAGS, GET_TEXT = range(10904, 10907) + +class Post(Plugin.Conversation, BasePlugin): + """文章推送""" MENU_KEYBOARD = ReplyKeyboardMarkup([["推送频道", "添加TAG"], ["编辑文字", "删除图片"], ["退出"]], True, True) def __init__(self): self.bbs = Hyperion() - @classmethod - def create_handlers(cls): - post = cls() - post_handler = ConversationHandler( - entry_points=[CommandHandler('post', post.command_start, block=True)], - states={ - post.CHECK_POST: [MessageHandler(filters.TEXT & ~filters.COMMAND, post.check_post, block=True)], - post.SEND_POST: [MessageHandler(filters.TEXT & ~filters.COMMAND, post.send_post, block=True)], - post.CHECK_COMMAND: [MessageHandler(filters.TEXT & ~filters.COMMAND, post.check_command, block=True)], - post.GTE_DELETE_PHOTO: [ - MessageHandler(filters.TEXT & ~filters.COMMAND, post.get_delete_photo, block=True)], - post.GET_POST_CHANNEL: [ - MessageHandler(filters.TEXT & ~filters.COMMAND, post.get_post_channel, block=True)], - post.GET_TAGS: [MessageHandler(filters.TEXT & ~filters.COMMAND, post.get_tags, block=True)], - post.GET_TEXT: [MessageHandler(filters.TEXT & ~filters.COMMAND, post.get_edit_text, block=True)] - }, - fallbacks=[CommandHandler('cancel', post.cancel, block=True)] - ) - return [post_handler] - + @conversation.entry_point + @handler.command(command='post', filters=filters.ChatType.PRIVATE, block=True) + @restricts() @bot_admins_rights_check @error_callable async def command_start(self, update: Update, context: CallbackContext) -> int: user = update.effective_user - message = update.message - Log.info(f"用户 {user.full_name}[{user.id}] POST命令请求") + message = update.effective_message + logger.info(f"用户 {user.full_name}[{user.id}] POST命令请求") post_handler_data = context.chat_data.get("post_handler_data") if post_handler_data is None: post_handler_data = PostHandlerData() @@ -76,11 +60,13 @@ class Post(BasePlugins): await message.reply_text(text, reply_markup=ReplyKeyboardMarkup(reply_keyboard, True, True)) return self.CHECK_POST + @conversation.state(state=CHECK_POST) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) @error_callable async def check_post(self, update: Update, context: CallbackContext) -> int: post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data") - message = update.message - if update.message.text == "退出": + message = update.effective_message + if message.text == "退出": await message.reply_text("退出投稿", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END @@ -114,9 +100,10 @@ class Post(BasePlugins): else: await message.reply_text("图片获取错误", reply_markup=ReplyKeyboardRemove()) # excuse? return ConversationHandler.END - except (BadRequest, TypeError) as error: + except (BadRequest, TypeError) as exc: await message.reply_text("发送图片时发生错误,错误信息已经写到日记", reply_markup=ReplyKeyboardRemove()) - Log.error("Post模块发送图片时发生错误", error) + logger.error("Post模块发送图片时发生错误") + logger.exception(exc) return ConversationHandler.END post_handler_data.post_text = post_text post_handler_data.post_images = post_images @@ -126,9 +113,11 @@ class Post(BasePlugins): await message.reply_text("请选择你的操作", reply_markup=self.MENU_KEYBOARD) return self.CHECK_COMMAND + @conversation.state(state=CHECK_COMMAND) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) @error_callable async def check_command(self, update: Update, context: CallbackContext) -> int: - message = update.message + message = update.effective_message if message.text == "退出": await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END @@ -150,11 +139,13 @@ class Post(BasePlugins): f"当前一共有 {photo_len} 张图片") return self.GTE_DELETE_PHOTO + @conversation.state(state=GTE_DELETE_PHOTO) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) @error_callable async def get_delete_photo(self, update: Update, context: CallbackContext) -> int: post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data") photo_len = len(post_handler_data.post_images) - message = update.message + message = update.effective_message args = message.text.split(" ") index: List[int] = [] try: @@ -171,31 +162,34 @@ class Post(BasePlugins): return self.CHECK_COMMAND async def get_channel(self, update: Update, _: CallbackContext) -> int: - message = update.message + message = update.effective_message reply_keyboard = [] try: - for channel_info in config.channels: + for channel_info in bot.config.channels: name = channel_info["name"] reply_keyboard.append([f"{name}"]) except KeyError as error: - Log.error("从配置文件获取频道信息发生错误,退出任务", error) + logger.error("从配置文件获取频道信息发生错误,退出任务", error) await message.reply_text("从配置文件获取频道信息发生错误,退出任务", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END await message.reply_text("请选择你要推送的频道", reply_markup=ReplyKeyboardMarkup(reply_keyboard, True, True)) return self.GET_POST_CHANNEL + @conversation.state(state=GET_POST_CHANNEL) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) @error_callable async def get_post_channel(self, update: Update, context: CallbackContext) -> int: post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data") - message = update.message + message = update.effective_message channel_id = -1 try: - for channel_info in config.channels: + for channel_info in bot.config.channels: if message.text == channel_info["name"]: channel_id = channel_info["chat_id"] - except KeyError as error: - Log.error("从配置文件获取频道信息发生错误,退出任务", error) + except KeyError as exc: + logger.error("从配置文件获取频道信息发生错误,退出任务", exc) + logger.exception(exc) await message.reply_text("从配置文件获取频道信息发生错误,退出任务", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END if channel_id == -1: @@ -208,14 +202,16 @@ class Post(BasePlugins): return self.SEND_POST async def add_tags(self, update: Update, _: CallbackContext) -> int: - message = update.message + message = update.effective_message await message.reply_text("请回复添加的tag名称,如果要添加多个tag请以空格作为分隔符,不用添加 # 作为开头,推送时程序会自动添加") return self.GET_TAGS + @conversation.state(state=GET_TAGS) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) @error_callable async def get_tags(self, update: Update, context: CallbackContext) -> int: post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data") - message = update.message + message = update.effective_message args = message.text.split(" ") post_handler_data.tags = args await message.reply_text("添加成功") @@ -223,14 +219,16 @@ class Post(BasePlugins): return self.CHECK_COMMAND async def edit_text(self, update: Update, _: CallbackContext) -> int: - message = update.message + message = update.effective_message await message.reply_text("请回复替换的文本") return self.GET_TEXT + @conversation.state(state=GET_TEXT) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) @error_callable async def get_edit_text(self, update: Update, context: CallbackContext) -> int: post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data") - message = update.message + message = update.effective_message post_handler_data.post_text = message.text_markdown_v2 await message.reply_text("替换成功") await message.reply_text("请选择你的操作", reply_markup=self.MENU_KEYBOARD) @@ -240,19 +238,20 @@ class Post(BasePlugins): @error_callable async def send_post(update: Update, context: CallbackContext) -> int: post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data") - message = update.message - if update.message.text == "退出": + message = update.effective_message + if message.text == "退出": await message.reply_text(text="退出任务", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END await message.reply_text("正在推送", reply_markup=ReplyKeyboardRemove()) channel_id = post_handler_data.channel_id channel_name = None try: - for channel_info in config.channels: + for channel_info in bot.config.channels: if post_handler_data.channel_id == channel_info["chat_id"]: channel_name = channel_info["name"] - except KeyError as error: - Log.error("从配置文件获取频道信息发生错误,退出任务", error) + except KeyError as exc: + logger.error("从配置文件获取频道信息发生错误,退出任务") + logger.exception(exc) await message.reply_text("从配置文件获取频道信息发生错误,退出任务", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END post_text = post_handler_data.post_text @@ -277,9 +276,10 @@ class Post(BasePlugins): else: await message.reply_text("图片获取错误", reply_markup=ReplyKeyboardRemove()) # excuse? return ConversationHandler.END - except (BadRequest, TypeError) as error: + except (BadRequest, TypeError) as exc: await message.reply_text("发送图片时发生错误,错误信息已经写到日记", reply_markup=ReplyKeyboardRemove()) - Log.error("Post模块发送图片时发生错误", error) + logger.error("Post模块发送图片时发生错误") + logger.exception(exc) return ConversationHandler.END await message.reply_text("推送成功", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END diff --git a/plugins/genshin/quiz.py b/plugins/genshin/quiz.py index 88cf677..35d58db 100644 --- a/plugins/genshin/quiz.py +++ b/plugins/genshin/quiz.py @@ -1,81 +1,37 @@ import random -import re -from typing import List, Optional -from redis import DataError, ResponseError -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, Poll, \ - ReplyKeyboardRemove, Message +from telegram import Update, Poll from telegram.constants import ChatAction -from telegram.ext import CallbackContext, ConversationHandler, CommandHandler, MessageHandler, filters -from telegram.helpers import escape_markdown +from telegram.ext import CallbackContext, CommandHandler, filters from core.admin import BotAdminService +from core.baseplugin import BasePlugin +from core.plugin import Plugin, handler from core.quiz import QuizService -from core.quiz.models import Answer, Question -from logger import Log -from plugins.base import BasePlugins from utils.decorators.restricts import restricts -from utils.plugins.manager import listener_plugins_class -from utils.random import MT19937_Random -from utils.service.inject import inject +from utils.log import logger +from utils.random import MT19937Random -class QuizCommandData: - question_id: int = -1 - new_question: str = "" - new_correct_answer: str = "" - new_wrong_answer: List[str] = [] - status: int = 0 +class QuizPlugin(Plugin, BasePlugin): + """派蒙的十万个为什么""" - -@listener_plugins_class() -class QuizPlugin(BasePlugins): - """派蒙的十万个为什么 - 合并了问题修改/添加/删除 - """ - - CHECK_COMMAND, VIEW_COMMAND, CHECK_QUESTION, \ - GET_NEW_QUESTION, GET_NEW_CORRECT_ANSWER, GET_NEW_WRONG_ANSWER, \ - QUESTION_EDIT, SAVE_QUESTION = range(10300, 10308) - - @inject def __init__(self, quiz_service: QuizService = None, bot_admin_service: BotAdminService = None): self.bot_admin_service = bot_admin_service - self.user_time = {} self.quiz_service = quiz_service self.time_out = 120 - self.random = MT19937_Random() + self.random = MT19937Random() - @classmethod - def create_handlers(cls): - quiz = cls() - quiz_handler = ConversationHandler( - entry_points=[CommandHandler('quiz', quiz.command_start, block=True)], - states={ - quiz.CHECK_COMMAND: [MessageHandler(filters.TEXT & ~filters.COMMAND, - quiz.check_command, block=True)], - quiz.CHECK_QUESTION: [MessageHandler(filters.TEXT & ~filters.COMMAND, - quiz.check_question, block=True)], - quiz.GET_NEW_QUESTION: [MessageHandler(filters.TEXT & ~filters.COMMAND, - quiz.get_new_question, block=True)], - quiz.GET_NEW_CORRECT_ANSWER: [MessageHandler(filters.TEXT & ~filters.COMMAND, - quiz.get_new_correct_answer, block=True)], - quiz.GET_NEW_WRONG_ANSWER: [MessageHandler(filters.TEXT & ~filters.COMMAND, - quiz.get_new_wrong_answer, block=True), - CommandHandler("finish", quiz.finish_edit)], - quiz.SAVE_QUESTION: [MessageHandler(filters.TEXT & ~filters.COMMAND, - quiz.save_question, block=True)], - }, - fallbacks=[CommandHandler('cancel', quiz.cancel, block=True)] - ) - return [quiz_handler] - - async def send_poll(self, update: Update) -> Optional[Message]: - chat = update.message.chat + @handler(CommandHandler, command="quiz", block=False) + @restricts(restricts_time=20, try_delete_message=True) + async def command_start(self, update: Update, context: CallbackContext) -> None: user = update.effective_user + message = update.effective_message + chat = message.chat + await message.reply_chat_action(ChatAction.TYPING) question_id_list = await self.quiz_service.get_question_id_list() if filters.ChatType.GROUPS.filter(update.message): - Log.info(f"用户 {user.full_name}[{user.id}] 在群 {chat.title}[{chat.id}] 发送挑战问题命令请求") + logger.info(f"用户 {user.full_name}[{user.id}] 在群 {chat.title}[{chat.id}] 发送挑战问题命令请求") if len(question_id_list) == 0: return None if len(question_id_list) == 0: @@ -90,204 +46,12 @@ class QuizPlugin(BasePlugins): correct_option = answer.text if correct_option is None: question_id = question["question_id"] - Log.warning(f"Quiz模块 correct_option 异常 question_id[{question_id}] ") + logger.warning(f"Quiz模块 correct_option 异常 question_id[{question_id}] ") return None random.shuffle(_options) index = _options.index(correct_option) - return await update.effective_message.reply_poll(question.text, _options, - correct_option_id=index, is_anonymous=False, - open_period=self.time_out, type=Poll.QUIZ) - - @restricts(filters.ChatType.GROUPS, ConversationHandler.END, restricts_time=20, try_delete_message=True) - @restricts(filters.ChatType.PRIVATE, ConversationHandler.END) - async def command_start(self, update: Update, context: CallbackContext) -> int: - user = update.effective_user - message = update.message - if filters.ChatType.PRIVATE.filter(message): - Log.info(f"用户 {user.full_name}[{user.id}] quiz命令请求") - admin_list = await self.bot_admin_service.get_admin_list() - if user.id in admin_list: - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - if quiz_command_data is None: - quiz_command_data = QuizCommandData() - context.chat_data["quiz_command_data"] = quiz_command_data - text = f'你好 {user.mention_markdown_v2()} {escape_markdown("!请选择你的操作!")}' - reply_keyboard = [ - ["查看问题", "添加问题"], - ["重载问题"], - ["退出"] - ] - await message.reply_markdown_v2(text, reply_markup=ReplyKeyboardMarkup(reply_keyboard, - one_time_keyboard=True)) - return self.CHECK_COMMAND - else: - await self.send_poll(update) - elif filters.ChatType.GROUPS.filter(update.message): - await update.message.reply_chat_action(ChatAction.TYPING) - poll_message = await self.send_poll(update) - if poll_message is None: - return ConversationHandler.END - self._add_delete_message_job(context, update.message.chat_id, update.message.message_id, 300) - self._add_delete_message_job(context, poll_message.chat_id, poll_message.message_id, 300) - return ConversationHandler.END - - async def view_command(self, update: Update, _: CallbackContext) -> int: - keyboard = [ - [ - InlineKeyboardButton(text="选择问题", switch_inline_query_current_chat="查看问题 ") - ] - ] - await update.message.reply_text("请回复你要查看的问题", - reply_markup=InlineKeyboardMarkup(keyboard)) - return self.CHECK_COMMAND - - async def check_question(self, update: Update, _: CallbackContext) -> int: - reply_keyboard = [ - ["删除问题"], - ["退出"] - ] - await update.message.reply_text("请选择你的操作", reply_markup=ReplyKeyboardMarkup(reply_keyboard)) - return self.CHECK_COMMAND - - async def check_command(self, update: Update, context: CallbackContext) -> int: - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - if update.message.text == "退出": - await update.message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - elif update.message.text == "查看问题": - return await self.view_command(update, context) - elif update.message.text == "添加问题": - return await self.add_question(update, context) - elif update.message.text == "删除问题": - return await self.delete_question(update, context) - # elif update.message.text == "修改问题": - # return await self.edit_question(update, context) - elif update.message.text == "重载问题": - return await self.refresh_question(update, context) - else: - result = re.findall(r"问题ID (\d+)", update.message.text) - if len(result) == 1: - try: - question_id = int(result[0]) - except ValueError: - await update.message.reply_text("获取问题ID失败") - return ConversationHandler.END - quiz_command_data.question_id = question_id - await update.message.reply_text("获取问题ID成功") - return await self.check_question(update, context) - await update.message.reply_text("命令错误", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - - async def refresh_question(self, update: Update, _: CallbackContext) -> int: - try: - await self.quiz_service.refresh_quiz() - except DataError: - await update.message.reply_text("Redis数据错误,重载失败", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - except ResponseError as error: - Log.error("重载问题失败", error) - await update.message.reply_text("重载问题失败,异常抛出Redis请求错误异常,详情错误请看日记", - reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - await update.message.reply_text("重载成功", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - - async def add_question(self, update: Update, context: CallbackContext) -> int: - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - quiz_command_data.new_wrong_answer = [] - quiz_command_data.new_question = "" - quiz_command_data.new_correct_answer = "" - quiz_command_data.status = 1 - await update.message.reply_text("请回复你要添加的问题,或发送 /cancel 取消操作", reply_markup=ReplyKeyboardRemove()) - return self.GET_NEW_QUESTION - - async def get_new_question(self, update: Update, context: CallbackContext) -> int: - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - reply_text = f"问题:`{escape_markdown(update.message.text, version=2)}`\n" \ - f"请填写正确答案:" - quiz_command_data.new_question = update.message.text - await update.message.reply_markdown_v2(reply_text) - return self.GET_NEW_CORRECT_ANSWER - - async def get_new_correct_answer(self, update: Update, context: CallbackContext) -> int: - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - reply_text = f"正确答案:`{escape_markdown(update.message.text, version=2)}`\n" \ - f"请填写错误答案:" - await update.message.reply_markdown_v2(reply_text) - quiz_command_data.new_correct_answer = update.message.text - return self.GET_NEW_WRONG_ANSWER - - async def get_new_wrong_answer(self, update: Update, context: CallbackContext) -> int: - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - reply_text = f"错误答案:`{escape_markdown(update.message.text, version=2)}`\n" \ - f"可继续填写,并使用 {escape_markdown('/finish', version=2)} 结束。" - await update.message.reply_markdown_v2(reply_text) - quiz_command_data.new_wrong_answer.append(update.message.text) - return self.GET_NEW_WRONG_ANSWER - - async def finish_edit(self, update: Update, context: CallbackContext): - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - reply_text = f"问题:`{escape_markdown(quiz_command_data.new_question, version=2)}`\n" \ - f"正确答案:`{escape_markdown(quiz_command_data.new_correct_answer, version=2)}`\n" \ - f"错误答案:`{escape_markdown(' '.join(quiz_command_data.new_wrong_answer), version=2)}`" - await update.message.reply_markdown_v2(reply_text) - reply_keyboard = [["保存并重载配置", "抛弃修改并退出"]] - await update.message.reply_text("请核对问题,并选择下一步操作。", reply_markup=ReplyKeyboardMarkup(reply_keyboard)) - return self.SAVE_QUESTION - - async def save_question(self, update: Update, context: CallbackContext): - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - if update.message.text == "抛弃修改并退出": - await update.message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - elif update.message.text == "保存并重载配置": - if quiz_command_data.status == 1: - answer = [ - Answer(text=wrong_answer, is_correct=False) for wrong_answer in - quiz_command_data.new_wrong_answer - ] - answer.append(Answer(text=quiz_command_data.new_correct_answer, is_correct=True)) - await self.quiz_service.save_quiz( - Question(text=quiz_command_data.new_question)) - await update.message.reply_text("保存成功", reply_markup=ReplyKeyboardRemove()) - try: - await self.quiz_service.refresh_quiz() - except ResponseError as error: - Log.error("重载问题失败", error) - await update.message.reply_text("重载问题失败,异常抛出Redis请求错误异常,详情错误请看日记", - reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - await update.message.reply_text("重载配置成功", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - else: - await update.message.reply_text("回复错误,请重新选择") - return self.SAVE_QUESTION - - async def edit_question(self, update: Update, context: CallbackContext) -> int: - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - quiz_command_data.new_wrong_answer = [] - quiz_command_data.new_question = "" - quiz_command_data.new_correct_answer = "" - quiz_command_data.status = 2 - await update.message.reply_text("请回复你要修改的问题", reply_markup=ReplyKeyboardRemove()) - return self.GET_NEW_QUESTION - - async def delete_question(self, update: Update, context: CallbackContext) -> int: - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - # 再问题重载Redis 以免redis数据为空时出现奔溃 - try: - await self.quiz_service.refresh_quiz() - question = await self.quiz_service.get_question(quiz_command_data.question_id) - # 因为外键的存在,先删除答案 - for answer in question.answers: - await self.quiz_service.delete_question_by_id(answer.answer_id) - await self.quiz_service.delete_question_by_id(question.question_id) - await update.message.reply_text("删除问题成功", reply_markup=ReplyKeyboardRemove()) - await self.quiz_service.refresh_quiz() - except ResponseError as error: - Log.error("重载问题失败", error) - await update.message.reply_text("重载问题失败,异常抛出Redis请求错误异常,详情错误请看日记", - reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - await update.message.reply_text("重载配置成功", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END + poll_message = await update.effective_message.reply_poll(question.text, _options, + correct_option_id=index, is_anonymous=False, + open_period=self.time_out, type=Poll.QUIZ) + self._add_delete_message_job(context, update.message.chat_id, update.message.message_id, 300) + self._add_delete_message_job(context, poll_message.chat_id, poll_message.message_id, 300) diff --git a/plugins/genshin/set_uid.py b/plugins/genshin/set_uid.py new file mode 100644 index 0000000..37aed28 --- /dev/null +++ b/plugins/genshin/set_uid.py @@ -0,0 +1,178 @@ +from typing import Optional + +import genshin +from genshin import GenshinException, types +from telegram import Update, ReplyKeyboardRemove, ReplyKeyboardMarkup, TelegramObject +from telegram.ext import CallbackContext, filters, ConversationHandler +from telegram.helpers import escape_markdown + +from core.baseplugin import BasePlugin +from core.cookies.error import CookiesNotFoundError, TooManyRequestPublicCookies +from core.cookies.services import CookiesService, PublicCookiesService +from core.plugin import Plugin, handler, conversation +from core.user.error import UserNotFoundError +from core.user.models import User +from core.user.services import UserService +from utils.decorators.error import error_callable +from utils.decorators.restricts import restricts +from utils.log import logger +from utils.models.base import RegionEnum + + +class AddUserCommandData(TelegramObject): + user: Optional[User] = None + region: RegionEnum = RegionEnum.HYPERION + game_uid: int = 0 + + +CHECK_SERVER, CHECK_UID, COMMAND_RESULT = range(10100, 10103) + + +class SetUid(Plugin.Conversation, BasePlugin.Conversation): + """UID用户绑定""" + + def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None, + public_cookies_service: PublicCookiesService = None): + self.public_cookies_service = public_cookies_service + self.cookies_service = cookies_service + self.user_service = user_service + + @conversation.entry_point + @handler.command(command='set_uid', filters=filters.ChatType.PRIVATE, block=True) + @restricts() + @error_callable + async def command_start(self, update: Update, context: CallbackContext) -> int: + user = update.effective_user + message = update.effective_message + logger.info(f"用户 {user.full_name}[{user.id}] 绑定账号命令请求") + add_user_command_data: AddUserCommandData = context.chat_data.get("add_uid_command_data") + if add_user_command_data is None: + cookies_command_data = AddUserCommandData() + context.chat_data["add_uid_command_data"] = cookies_command_data + text = f'你好 {user.mention_markdown_v2()} {escape_markdown("!请选择要绑定的服务器!或回复退出取消操作")}' + reply_keyboard = [['米游社', 'HoYoLab'], ["退出"]] + await message.reply_markdown_v2(text, reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)) + return CHECK_SERVER + + @conversation.state(state=CHECK_SERVER) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) + @error_callable + async def check_server(self, update: Update, context: CallbackContext) -> int: + user = update.effective_user + message = update.effective_message + add_user_command_data: AddUserCommandData = context.chat_data.get("add_uid_command_data") + if message.text == "退出": + await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + elif message.text == "米游社": + region = add_user_command_data.region = RegionEnum.HYPERION + elif message.text == "HoYoLab": + region = add_user_command_data.region = RegionEnum.HOYOLAB + else: + await message.reply_text("选择错误,请重新选择") + return CHECK_SERVER + try: + user_info = await self.user_service.get_user_by_id(user.id) + add_user_command_data.user = user_info + except UserNotFoundError: + user_info = None + if user_info is not None: + try: + await self.cookies_service.get_cookies(user.id, region) + except CookiesNotFoundError: + pass + else: + await message.reply_text("你已经绑定Cookie,无法继续下一步") + return ConversationHandler.END + await message.reply_text("请输入你的UID", reply_markup=ReplyKeyboardRemove()) + return CHECK_UID + + @conversation.state(state=CHECK_UID) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) + @error_callable + async def check_cookies(self, update: Update, context: CallbackContext) -> int: + user = update.effective_user + message = update.effective_message + add_user_command_data: AddUserCommandData = context.chat_data.get("add_uid_command_data") + region = add_user_command_data.region + if message.text == "退出": + await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + try: + uid = int(message.text) + except ValueError: + await message.reply_text("Cookies格式有误,请检查", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + try: + cookies = await self.public_cookies_service.get_cookies(user.id, region) + except TooManyRequestPublicCookies: + await message.reply_text("Cookies公共池已经使用完,请稍后重试", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + if region == RegionEnum.HYPERION: + client = genshin.Client(cookies=cookies.cookies, game=types.Game.GENSHIN, region=types.Region.CHINESE) + elif region == RegionEnum.HOYOLAB: + client = genshin.Client(cookies=cookies.cookies, game=types.Game.GENSHIN, region=types.Region.OVERSEAS, + lang="zh-cn") + else: + return ConversationHandler.END + try: + user_info = await client.get_record_card(uid) + except GenshinException as exc: + await message.reply_text("获取账号信息发生错误", reply_markup=ReplyKeyboardRemove()) + logger.error("获取账号信息发生错误") + logger.exception(exc) + return ConversationHandler.END + add_user_command_data.game_uid = uid + reply_keyboard = [['确认', '退出']] + await message.reply_text("获取角色基础信息成功,请检查是否正确!") + logger.info(f"用户 {user.full_name}[{user.id}] 获取账号 {user_info.nickname}[{user_info.uid}] 信息成功") + text = f"*角色信息*\n" \ + f"角色名称:{escape_markdown(user_info.nickname, version=2)}\n" \ + f"角色等级:{user_info.level}\n" \ + f"UID:`{user_info.uid}`\n" \ + f"服务器名称:`{user_info.server_name}`\n" + await message.reply_markdown_v2( + text, + reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True) + ) + return COMMAND_RESULT + + @conversation.state(state=COMMAND_RESULT) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) + @error_callable + async def command_result(self, update: Update, context: CallbackContext) -> int: + user = update.effective_user + message = update.effective_message + add_user_command_data: AddUserCommandData = context.chat_data.get("add_uid_command_data") + if message.text == "退出": + await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + elif message.text == "确认": + if add_user_command_data.user is None: + if add_user_command_data.region == RegionEnum.HYPERION: + user_db = User(user_id=user.id, yuanshen_uid=add_user_command_data.game_uid, + region=add_user_command_data.region) + elif add_user_command_data.region == RegionEnum.HOYOLAB: + user_db = User(user_id=user.id, genshin_uid=add_user_command_data.game_uid, + region=add_user_command_data.region) + else: + await message.reply_text("数据错误") + return ConversationHandler.END + await self.user_service.add_user(user_db) + else: + user_db = add_user_command_data.user + user_db.region = add_user_command_data.region + if add_user_command_data.region == RegionEnum.HYPERION: + user_db.yuanshen_uid = add_user_command_data.game_uid + elif add_user_command_data.region == RegionEnum.HOYOLAB: + user_db.genshin_uid = add_user_command_data.game_uid + else: + await message.reply_text("数据错误") + return ConversationHandler.END + await self.user_service.update_user(user_db) + logger.info(f"用户 {user.full_name}[{user.id}] 绑定UID账号成功") + await message.reply_text("保存成功", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + else: + await message.reply_text("回复错误,请重新输入") + return COMMAND_RESULT diff --git a/plugins/genshin/sign.py b/plugins/genshin/sign.py index 96981bb..26f9f11 100644 --- a/plugins/genshin/sign.py +++ b/plugins/genshin/sign.py @@ -3,72 +3,65 @@ import time from genshin import Game, GenshinException, AlreadyClaimed, Client from telegram import Update -from telegram.ext import CommandHandler, MessageHandler, ConversationHandler, filters, CallbackContext +from telegram.ext import CommandHandler, CallbackContext +from telegram.ext import MessageHandler, filters +from core.baseplugin import BasePlugin +from core.cookies.error import CookiesNotFoundError from core.cookies.services import CookiesService +from core.plugin import Plugin, handler from core.sign.models import Sign as SignUser, SignStatusEnum from core.sign.services import SignServices -from core.user.repositories import UserNotFoundError +from core.user.error import UserNotFoundError from core.user.services import UserService -from logger import Log -from plugins.base import BasePlugins from utils.bot import get_all_args from utils.decorators.error import error_callable from utils.decorators.restricts import restricts from utils.helpers import get_genshin_client -from utils.plugins.manager import listener_plugins_class -from utils.service.inject import inject +from utils.log import logger -@listener_plugins_class() -class Sign(BasePlugins): +class Sign(Plugin, BasePlugin): """每日签到""" CHECK_SERVER, COMMAND_RESULT = range(10400, 10402) - @inject def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None, sign_service: SignServices = None): self.cookies_service = cookies_service self.user_service = user_service self.sign_service = sign_service - @classmethod - def create_handlers(cls): - sign = cls() - return [CommandHandler('sign', sign.command_start, block=True), - MessageHandler(filters.Regex(r"^每日签到(.*)"), sign.command_start, block=True)] - @staticmethod async def _start_sign(client: Client) -> str: try: rewards = await client.get_monthly_rewards(game=Game.GENSHIN, lang="zh-cn") except GenshinException as error: - Log.error(f"UID {client.uid} 获取签到信息失败,API返回信息为 {str(error)}") + logger.error(f"UID {client.uid} 获取签到信息失败,API返回信息为 {str(error)}") return f"获取签到信息失败,API返回信息为 {str(error)}" try: daily_reward_info = await client.get_reward_info(game=Game.GENSHIN, lang="zh-cn") # 获取签到信息失败 except GenshinException as error: - Log.error(f"UID {client.uid} 获取签到状态失败,API返回信息为 {str(error)}") + logger.error(f"UID {client.uid} 获取签到状态失败,API返回信息为 {str(error)}") return f"获取签到状态失败,API返回信息为 {str(error)}" if not daily_reward_info.signed_in: try: request_daily_reward = await client.request_daily_reward("sign", method="POST", game=Game.GENSHIN, lang="zh-cn") - Log.info(f"UID {client.uid} 签到请求 {request_daily_reward}") + logger.info(f"UID {client.uid} 签到请求 {request_daily_reward}") if request_daily_reward and request_daily_reward.get("success", 0) == 1: - Log.warning(f"UID {client.uid} 签到失败,触发验证码风控") + logger.warning(f"UID {client.uid} 签到失败,触发验证码风控") return f"UID {client.uid} 签到失败,触发验证码风控,请尝试重新签到。" except AlreadyClaimed: result = "今天旅行者已经签到过了~" except GenshinException as error: - Log.error(f"UID {client.uid} 签到失败,API返回信息为 {str(error)}") + logger.error(f"UID {client.uid} 签到失败,API返回信息为 {str(error)}") return f"获取签到状态失败,API返回信息为 {str(error)}" else: result = "OK" else: result = "今天旅行者已经签到过了~" - Log.info(f"UID {client.uid} 签到结果 {result}") + logger.info(f"UID {client.uid} 签到结果 {result}") reward = rewards[daily_reward_info.claimed_rewards - (1 if daily_reward_info.signed_in else 0)] today = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) cn_timezone = datetime.timezone(datetime.timedelta(hours=8)) @@ -85,8 +78,8 @@ class Sign(BasePlugins): async def _process_auto_sign(self, user_id: int, chat_id: int, method: str) -> str: try: - await get_genshin_client(user_id, self.user_service, self.cookies_service) - except UserNotFoundError: + await get_genshin_client(user_id) + except (UserNotFoundError, CookiesNotFoundError): return "未查询到账号信息,请先私聊派蒙绑定账号" user: SignUser = await self.sign_service.get_by_user_id(user_id) if user: @@ -107,11 +100,13 @@ class Sign(BasePlugins): await self.sign_service.add(user) return "开启自动签到成功" + @handler(CommandHandler, command="sign", block=False) + @handler(MessageHandler, filters=filters.Regex("^每日签到(.*)"), block=False) + @restricts() @error_callable - @restricts(return_data=ConversationHandler.END) async def command_start(self, update: Update, context: CallbackContext) -> None: user = update.effective_user - message = update.message + message = update.effective_message args = get_all_args(context) if len(args) >= 1: msg = None @@ -120,22 +115,22 @@ class Sign(BasePlugins): elif args[0] == "关闭自动签到": msg = await self._process_auto_sign(user.id, message.chat_id, "关闭") if msg: - Log.info(f"用户 {user.full_name}[{user.id}] 自动签到命令请求 || 参数 {args[0]}") + logger.info(f"用户 {user.full_name}[{user.id}] 自动签到命令请求 || 参数 {args[0]}") reply_message = await message.reply_text(msg) if filters.ChatType.GROUPS.filter(message): self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id, 30) self._add_delete_message_job(context, message.chat_id, message.message_id, 30) return - Log.info(f"用户 {user.full_name}[{user.id}] 每日签到命令请求") + logger.info(f"用户 {user.full_name}[{user.id}] 每日签到命令请求") if filters.ChatType.GROUPS.filter(message): self._add_delete_message_job(context, message.chat_id, message.message_id) try: - client = await get_genshin_client(user.id, self.user_service, self.cookies_service) + client = await get_genshin_client(user.id) sign_text = await self._start_sign(client) reply_message = await message.reply_text(sign_text, allow_sending_without_reply=True) if filters.ChatType.GROUPS.filter(reply_message): self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id) - except UserNotFoundError: + except (UserNotFoundError, CookiesNotFoundError): reply_message = await message.reply_text("未查询到账号信息,请先私聊派蒙绑定账号") if filters.ChatType.GROUPS.filter(message): self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id, 30) diff --git a/plugins/genshin/strategy.py b/plugins/genshin/strategy.py index 0dd1045..3447d70 100644 --- a/plugins/genshin/strategy.py +++ b/plugins/genshin/strategy.py @@ -1,40 +1,34 @@ -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup -from telegram.constants import ChatAction, ParseMode -from telegram.ext import filters, ConversationHandler, CommandHandler, MessageHandler, CallbackContext +from telegram import InlineKeyboardButton, InlineKeyboardMarkup +from telegram import Update +from telegram.constants import ChatAction +from telegram.constants import ParseMode +from telegram.ext import CommandHandler, CallbackContext +from telegram.ext import MessageHandler, filters +from core.baseplugin import BasePlugin from core.game.services import GameStrategyService -from logger import Log -from plugins.base import BasePlugins +from core.plugin import Plugin, handler from utils.bot import get_all_args from utils.decorators.error import error_callable from utils.decorators.restricts import restricts from utils.helpers import url_to_file -from utils.plugins.manager import listener_plugins_class -from utils.service.inject import inject +from utils.log import logger -@listener_plugins_class() -class Strategy(BasePlugins): +class StrategyPlugin(Plugin, BasePlugin): """角色攻略查询""" KEYBOARD = [[InlineKeyboardButton(text="查看角色攻略列表并查询", switch_inline_query_current_chat="查看角色攻略列表并查询")]] - @inject def __init__(self, game_strategy_service: GameStrategyService = None): self.game_strategy_service = game_strategy_service - @classmethod - def create_handlers(cls) -> list: - strategy = cls() - return [ - CommandHandler("strategy", strategy.command_start, block=False), - MessageHandler(filters.Regex("^角色攻略查询(.*)"), strategy.command_start, block=False), - ] - + @handler(CommandHandler, command="strategy", block=False) + @handler(MessageHandler, filters=filters.Regex("^角色攻略查询(.*)"), block=False) + @restricts() @error_callable - @restricts(return_data=ConversationHandler.END) async def command_start(self, update: Update, context: CallbackContext) -> None: - message = update.message + message = update.effective_message user = update.effective_user args = get_all_args(context) if len(args) >= 1: @@ -54,7 +48,7 @@ class Strategy(BasePlugins): self._add_delete_message_job(context, message.chat_id, message.message_id) self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id) return - Log.info(f"用户 {user.full_name}[{user.id}] 查询角色攻略命令请求 || 参数 {character_name}") + logger.info(f"用户 {user.full_name}[{user.id}] 查询角色攻略命令请求 || 参数 {character_name}") await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) file_path = await url_to_file(url, "") caption = "From 米游社 西风驿站 " \ diff --git a/plugins/genshin/userstats.py b/plugins/genshin/userstats.py index 0e723f8..2c5f9df 100644 --- a/plugins/genshin/userstats.py +++ b/plugins/genshin/userstats.py @@ -7,39 +7,28 @@ from telegram import Update from telegram.constants import ChatAction from telegram.ext import CallbackContext, CommandHandler, MessageHandler, ConversationHandler, filters -from core.cookies.services import CookiesService +from core.baseplugin import BasePlugin +from core.cookies.error import CookiesNotFoundError +from core.plugin import Plugin, handler from core.template.services import TemplateService -from core.user.repositories import UserNotFoundError -from core.user.services import UserService -from logger import Log -from plugins.base import BasePlugins +from core.user.error import UserNotFoundError from utils.decorators.error import error_callable from utils.decorators.restricts import restricts -from utils.helpers import url_to_file, get_genshin_client -from utils.plugins.manager import listener_plugins_class -from utils.service.inject import inject +from utils.helpers import url_to_file, get_genshin_client, get_public_genshin_client +from utils.log import logger -@listener_plugins_class() -class UserStats(BasePlugins): +class TeapotUnlocked(Exception): + """尘歌壶未解锁""" + + +class UserStatsPlugins(Plugin, BasePlugin): """玩家统计查询""" - COMMAND_RESULT, = range(10200, 10201) - - @inject - def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None, - template_service: TemplateService = None): + def __init__(self, template_service: TemplateService = None): self.template_service = template_service - self.cookies_service = cookies_service - self.user_service = user_service self.current_dir = os.getcwd() - @classmethod - def create_handlers(cls): - uid = cls() - return [CommandHandler('stats', uid.command_start, block=True), - MessageHandler(filters.Regex(r"^玩家统计查询(.*)"), uid.command_start, block=True)] - async def _start_get_user_info(self, client: Client, uid: int = -1) -> bytes: if uid == -1: _uid = client.uid @@ -47,24 +36,22 @@ class UserStats(BasePlugins): _uid = uid try: user_info = await client.get_genshin_user(_uid) - except GenshinException as error: - Log.warning("get_record_card请求失败", error) - raise error + except GenshinException as exc: + raise exc if user_info.teapot is None: - raise ValueError("洞庭湖未解锁") + raise TeapotUnlocked try: # 查询的UID如果是自己的,会返回DataNotPublic,自己查不了自己可还行...... if uid > 0: record_card_info = await client.get_record_card(uid) else: record_card_info = await client.get_record_card() - except DataNotPublic as error: - Log.warning("get_record_card请求失败 查询的用户数据未公开", error) + except DataNotPublic: + logger.warning("get_record_card请求失败 查询的用户数据未公开") nickname = _uid user_uid = "" - except GenshinException as error: - Log.warning("get_record_card请求失败", error) - raise error + except GenshinException as exc: + raise exc else: nickname = record_card_info.nickname user_uid = record_card_info.uid @@ -129,24 +116,31 @@ class UserStats(BasePlugins): {"width": 1024, "height": 1024}) return png_data - @error_callable + @handler(CommandHandler, command="stats", block=False) + @handler(MessageHandler, filters=filters.Regex("^玩家统计查询(.*)"), block=False) @restricts(return_data=ConversationHandler.END) + @error_callable async def command_start(self, update: Update, context: CallbackContext) -> Optional[int]: user = update.effective_user - message = update.message - Log.info(f"用户 {user.full_name}[{user.id}] 查询游戏用户命令请求") + message = update.effective_message + logger.info(f"用户 {user.full_name}[{user.id}] 查询游戏用户命令请求") uid: int = -1 try: args = context.args if args is not None and len(args) >= 1: uid = int(args[0]) - except ValueError as error: - Log.error("获取 uid 发生错误! 错误信息为", error) + except ValueError as exc: + logger.error("获取 uid 发生错误! 错误信息为") + logger.exception(exc) await message.reply_text("输入错误") return ConversationHandler.END try: - client = await get_genshin_client(user.id, self.user_service, self.cookies_service) - + try: + client = await get_genshin_client(user.id) + except CookiesNotFoundError: + client, _uid = await get_public_genshin_client(user.id) + if uid == -1: + uid = _uid png_data = await self._start_get_user_info(client, uid) except UserNotFoundError: reply_message = await message.reply_text("未查询到账号信息,请先私聊派蒙绑定账号") @@ -155,13 +149,11 @@ class UserStats(BasePlugins): self._add_delete_message_job(context, message.chat_id, message.message_id, 30) return - except ValueError as exc: - if "洞庭湖未解锁" not in str(exc): - raise exc + except TeapotUnlocked: await message.reply_text("角色尘歌壶未解锁 如果想要查看具体数据 嗯...... 咕咕咕~") return ConversationHandler.END except AttributeError as exc: - Log.warning("角色数据有误", exc) + logger.warning("角色数据有误", exc) await message.reply_text("角色数据有误 估计是派蒙晕了") return ConversationHandler.END await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) diff --git a/plugins/genshin/weapon.py b/plugins/genshin/weapon.py index 6c4c2fc..3a1d27c 100644 --- a/plugins/genshin/weapon.py +++ b/plugins/genshin/weapon.py @@ -1,47 +1,40 @@ -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram import InlineKeyboardButton, InlineKeyboardMarkup +from telegram import Update from telegram.constants import ChatAction -from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters +from telegram.ext import CommandHandler, CallbackContext +from telegram.ext import MessageHandler, filters -from core.template.services import TemplateService +from core.baseplugin import BasePlugin +from core.plugin import Plugin, handler +from core.template import TemplateService from core.wiki.services import WikiService -from logger import Log from metadata.shortname import weaponToName -from models.wiki.base import SCRAPE_HOST -from models.wiki.weapon import Weapon -from plugins.base import BasePlugins +from modules.wiki.base import SCRAPE_HOST +from modules.wiki.weapon import Weapon from utils.bot import get_all_args from utils.decorators.error import error_callable from utils.decorators.restricts import restricts from utils.helpers import url_to_file -from utils.plugins.manager import listener_plugins_class -from utils.service.inject import inject +from utils.log import logger -@listener_plugins_class() -class WeaponPlugin(BasePlugins): +class WeaponPlugin(Plugin, BasePlugin): """武器查询""" KEYBOARD = [[ InlineKeyboardButton(text="查看武器列表并查询", switch_inline_query_current_chat="查看武器列表并查询") ]] - @inject def __init__(self, template_service: TemplateService = None, wiki_service: WikiService = None): self.wiki_service = wiki_service self.template_service = template_service - @classmethod - def create_handlers(cls) -> list: - weapon = cls() - return [ - CommandHandler("weapon", weapon.command_start, block=False), - MessageHandler(filters.Regex("^武器查询(.*)"), weapon.command_start, block=False) - ] - + @handler(CommandHandler, command="weapon", block=False) + @handler(MessageHandler, filters=filters.Regex("^武器查询(.*)"), block=False) @error_callable @restricts() async def command_start(self, update: Update, context: CallbackContext) -> None: - message = update.message + message = update.effective_message user = update.effective_user args = get_all_args(context) if len(args) >= 1: @@ -66,7 +59,7 @@ class WeaponPlugin(BasePlugins): self._add_delete_message_job(context, message.chat_id, message.message_id) self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id) return - Log.info(f"用户 {user.full_name}[{user.id}] 查询武器命令请求 || 参数 {weapon_name}") + logger.info(f"用户 {user.full_name}[{user.id}] 查询武器命令请求 || 参数 {weapon_name}") await message.reply_chat_action(ChatAction.TYPING) async def input_template_data(_weapon_data: Weapon): @@ -85,7 +78,7 @@ class WeaponPlugin(BasePlugins): "weapon_info_max_level": _weapon_data.stats[-1].level, "progression_base_atk": round(_weapon_data.stats[-1].ATK), "weapon_info_source_list": [ - await url_to_file(str(SCRAPE_HOST.join(f'/img/{mid}.webp'))) + await url_to_file(str(SCRAPE_HOST.join(f'/img/{mid}.png'))) for mid in _weapon_data.ascension[-3:] ], "special_ability_name": _weapon_data.affix.name, @@ -101,7 +94,7 @@ class WeaponPlugin(BasePlugins): "weapon_info_max_level": _weapon_data.stats[-1].level, "progression_base_atk": round(_weapon_data.stats[-1].ATK), "weapon_info_source_list": [ - await url_to_file(str(SCRAPE_HOST.join(f'/img/{mid}.webp'))) + await url_to_file(str(SCRAPE_HOST.join(f'/img/{mid}.png'))) for mid in _weapon_data.ascension[-3:] ], "special_ability_name": '', diff --git a/plugins/genshin/wiki.py b/plugins/genshin/wiki.py index 1d65f28..b2703b3 100644 --- a/plugins/genshin/wiki.py +++ b/plugins/genshin/wiki.py @@ -1,33 +1,21 @@ from telegram import Update from telegram.ext import CommandHandler, CallbackContext +from core.plugin import Plugin, handler from core.wiki.services import WikiService -from plugins.base import BasePlugins from utils.decorators.admins import bot_admins_rights_check -from utils.decorators.error import error_callable -from utils.plugins.manager import listener_plugins_class -from utils.service.inject import inject -@listener_plugins_class() -class Wiki(BasePlugins): +class Wiki(Plugin): """有关WIKI操作""" - @inject def __init__(self, wiki_service: WikiService = None): self.wiki_service = wiki_service - @classmethod - def create_handlers(cls) -> list: - wiki = cls() - return [ - CommandHandler("refresh_wiki", wiki.refresh_wiki, block=False), - ] - + @handler(CommandHandler, command="refresh_wiki", block=False) @bot_admins_rights_check - @error_callable async def refresh_wiki(self, update: Update, _: CallbackContext): - message = update.message + message = update.effective_message await message.reply_text("正在刷新Wiki缓存,请稍等") await self.wiki_service.refresh_wiki() await message.reply_text("刷新Wiki缓存成功") diff --git a/plugins/jobs/public_cookies.py b/plugins/jobs/public_cookies.py new file mode 100644 index 0000000..8326434 --- /dev/null +++ b/plugins/jobs/public_cookies.py @@ -0,0 +1,19 @@ +import datetime + +from telegram.ext import CallbackContext + +from core.cookies.services import PublicCookiesService +from core.plugin import Plugin, job +from utils.log import logger + + +class PublicCookies(Plugin): + + def __init__(self, public_cookies_service: PublicCookiesService = None): + self.public_cookies_service = public_cookies_service + + @job.run_repeating(interval=datetime.timedelta(hours=2), name="PublicCookiesRefresh") + async def refresh(self, _: CallbackContext): + logger.info("正在刷新公共Cookies池") + await self.public_cookies_service.refresh() + logger.info("刷新公共Cookies池成功") diff --git a/jobs/sign.py b/plugins/jobs/sign.py similarity index 75% rename from jobs/sign.py rename to plugins/jobs/sign.py index 3691198..133d4a8 100644 --- a/jobs/sign.py +++ b/plugins/jobs/sign.py @@ -1,64 +1,53 @@ +import asyncio import datetime import time -import asyncio from aiohttp import ClientConnectorError from genshin import Game, GenshinException, AlreadyClaimed, InvalidCookies from telegram.constants import ParseMode from telegram.error import BadRequest, Forbidden -from telegram.ext import CallbackContext, JobQueue +from telegram.ext import CallbackContext -from config import config from core.cookies import CookiesService +from core.plugin import Plugin, job from core.sign.models import SignStatusEnum from core.sign.services import SignServices from core.user import UserService -from logger import Log from utils.helpers import get_genshin_client -from utils.job.manager import listener_jobs_class -from utils.service.inject import inject +from utils.log import logger class NeedChallenge(Exception): pass -@listener_jobs_class() -class SignJob: +class SignJob(Plugin): - @inject def __init__(self, sign_service: SignServices = None, user_service: UserService = None, cookies_service: CookiesService = None): self.sign_service = sign_service self.cookies_service = cookies_service self.user_service = user_service - @classmethod - def build_jobs(cls, job_queue: JobQueue): - sign = cls() - if config.debug: - job_queue.run_once(sign.sign, 3, name="SignJobTest") - # 每天凌晨一点执行 - job_queue.run_daily(sign.sign, datetime.time(hour=0, minute=1, second=0), name="SignJob") - + @job.run_daily(time=datetime.time(hour=0, minute=1, second=0), name="SignJob") async def sign(self, context: CallbackContext): - Log.info("正在执行自动签到") + logger.info("正在执行自动签到") sign_list = await self.sign_service.get_all() for sign_db in sign_list: if sign_db.status != SignStatusEnum.STATUS_SUCCESS: continue user_id = sign_db.user_id try: - client = await get_genshin_client(user_id, self.user_service, self.cookies_service) + client = await get_genshin_client(user_id) rewards = await client.get_monthly_rewards(game=Game.GENSHIN, lang="zh-cn") daily_reward_info = await client.get_reward_info(game=Game.GENSHIN) if not daily_reward_info.signed_in: request_daily_reward = await client.request_daily_reward("sign", method="POST", game=Game.GENSHIN) if request_daily_reward and request_daily_reward.get("success", 0) == 1: - Log.warning(f"UID {client.uid} 签到失败,触发验证码风控") + logger.warning(f"UID {client.uid} 签到失败,触发验证码风控") raise NeedChallenge else: - Log.info(f"UID {client.uid} 签到请求 {request_daily_reward}") + logger.info(f"UID {client.uid} 签到请求 {request_daily_reward}") result = "OK" else: result = "今天旅行者已经签到过了~" @@ -88,10 +77,11 @@ class SignJob: text = "签到失败了呜呜呜 ~ 服务器连接超时 服务器熟啦 ~ " sign_db.status = SignStatusEnum.TIMEOUT_ERROR except NeedChallenge: - text = f"签到失败,触发验证码风控,自动签到自动关闭" + text = "签到失败,触发验证码风控,自动签到自动关闭" sign_db.status = SignStatusEnum.NEED_CHALLENGE except BaseException as exc: - Log.error(f"执行自动签到时发生错误 用户UID[{user_id}]", exc) + logger.error(f"执行自动签到时发生错误 用户UID[{user_id}]") + logger.exception(exc) text = "签到失败了呜呜呜 ~ 执行自动签到时发生错误" if sign_db.chat_id < 0: text = f"NOTICE {sign_db.user_id}\n\n{text}" @@ -99,14 +89,17 @@ class SignJob: await context.bot.send_message(sign_db.chat_id, text, parse_mode=ParseMode.HTML) await asyncio.sleep(5) # 回复延迟5S避免触发洪水防御 except BadRequest as exc: - Log.error(f"执行自动签到时发生错误 用户UID[{user_id}]", exc) + logger.error(f"执行自动签到时发生错误 用户UID[{user_id}]") + logger.exception(exc) sign_db.status = SignStatusEnum.BAD_REQUEST except Forbidden as exc: - Log.error(f"执行自动签到时发生错误 用户UID[{user_id}]", exc) + logger.error(f"执行自动签到时发生错误 用户UID[{user_id}]") + logger.exception(exc) sign_db.status = SignStatusEnum.FORBIDDEN except BaseException as exc: - Log.error(f"执行自动签到时发生错误 用户UID[{user_id}]", exc) + logger.error(f"执行自动签到时发生错误 用户UID[{user_id}]") + logger.exception(exc) continue sign_db.time_updated = datetime.datetime.now() await self.sign_service.update(sign_db) - Log.info("执行自动签到完成") + logger.info("执行自动签到完成") diff --git a/plugins/system/admin.py b/plugins/system/admin.py index e5f4951..959bcc3 100644 --- a/plugins/system/admin.py +++ b/plugins/system/admin.py @@ -3,32 +3,21 @@ from telegram.error import BadRequest, Forbidden from telegram.ext import CallbackContext, CommandHandler from core.admin import BotAdminService -from logger import Log +from core.plugin import handler, Plugin from utils.decorators.admins import bot_admins_rights_check -from utils.plugins.manager import listener_plugins_class -from utils.service.inject import inject +from utils.log import logger -@listener_plugins_class() -class Admin: +class AdminPlugin(Plugin): """有关BOT ADMIN处理""" - @inject def __init__(self, bot_admin_service: BotAdminService = None): self.bot_admin_service = bot_admin_service - @classmethod - def create_handlers(cls) -> list: - admin = cls() - return [ - CommandHandler("add_admin", admin.add_admin, block=False), - CommandHandler("del_admin", admin.del_admin, block=False), - CommandHandler("leave_chat", admin.leave_chat, block=False), - ] - + @handler(CommandHandler, command="add_admin", block=False) @bot_admins_rights_check async def add_admin(self, update: Update, _: CallbackContext): - message = update.message + message = update.effective_message reply_to_message = message.reply_to_message if reply_to_message is None: await message.reply_text("请回复对应消息") @@ -40,9 +29,10 @@ class Admin: await self.bot_admin_service.add_admin(reply_to_message.from_user.id) await message.reply_text("添加成功") + @handler(CommandHandler, command="del_admin", block=False) @bot_admins_rights_check async def del_admin(self, update: Update, _: CallbackContext): - message = update.message + message = update.effective_message reply_to_message = message.reply_to_message admin_list = await self.bot_admin_service.get_admin_list() if reply_to_message is None: @@ -54,9 +44,10 @@ class Admin: else: await message.reply_text("该用户不存在管理员列表") + @handler(CommandHandler, command="leave_chat", block=False) @bot_admins_rights_check async def leave_chat(self, update: Update, context: CallbackContext): - message = update.message + message = update.effective_message try: args = message.text.split() if len(args) >= 2: @@ -65,7 +56,7 @@ class Admin: await message.reply_text("输入错误") return except ValueError as error: - Log.error("获取 chat_id 发生错误! 错误信息为 \n", error) + logger.error("获取 chat_id 发生错误! 错误信息为 \n", error) await message.reply_text("输入错误") return try: @@ -76,7 +67,7 @@ class Admin: pass await context.bot.leave_chat(chat_id) except (BadRequest, Forbidden) as error: - Log.error(f"退出 chat_id[{chat_id}] 发生错误! 错误信息为 \n", error) + logger.error(f"退出 chat_id[{chat_id}] 发生错误! 错误信息为 \n", error) await message.reply_text(f"退出 chat_id[{chat_id}] 发生错误! 错误信息为 {str(error)}") return await message.reply_text(f"退出 chat_id[{chat_id}] 成功!") diff --git a/plugins/system/auth.py b/plugins/system/auth.py index 4e55bdd..7a9c861 100644 --- a/plugins/system/auth.py +++ b/plugins/system/auth.py @@ -6,13 +6,14 @@ from typing import Tuple, Union, Dict, List from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ChatPermissions, ChatMember from telegram.constants import ParseMode from telegram.error import BadRequest -from telegram.ext import CallbackContext +from telegram.ext import CallbackContext, CallbackQueryHandler from telegram.helpers import escape_markdown +from core.bot import bot +from core.plugin import Plugin, handler from core.quiz import QuizService -from logger import Log -from utils.random import MT19937_Random -from utils.service.inject import inject +from utils.log import logger +from utils.random import MT19937Random FullChatPermissions = ChatPermissions( can_send_messages=True, @@ -26,15 +27,14 @@ FullChatPermissions = ChatPermissions( ) -class GroupJoiningVerification: +class GroupJoiningVerification(Plugin): """群验证模块""" - @inject def __init__(self, quiz_service: QuizService = None): self.quiz_service = quiz_service self.time_out = 120 self.kick_time = 120 - self.random = MT19937_Random() + self.random = MT19937Random() self.lock = asyncio.Lock() self.chat_administrators_cache: Dict[Union[str, int], Tuple[float, List[ChatMember]]] = {} self.is_refresh_quiz = False @@ -62,53 +62,57 @@ class GroupJoiningVerification: async def kick_member_job(self, context: CallbackContext): job = context.job - Log.info(f"踢出用户 user_id[{job.user_id}] 在 chat_id[{job.chat_id}]") + logger.info(f"踢出用户 user_id[{job.user_id}] 在 chat_id[{job.chat_id}]") try: await context.bot.ban_chat_member(chat_id=job.chat_id, user_id=job.user_id, until_date=int(time.time()) + self.kick_time) - except BadRequest as error: - Log.error(f"Auth模块在 chat_id[{job.chat_id}] user_id[{job.user_id}] 执行kick失败", error) + except BadRequest as exc: + logger.error(f"Auth模块在 chat_id[{job.chat_id}] user_id[{job.user_id}] 执行kick失败") + logger.exception(exc) @staticmethod async def clean_message_job(context: CallbackContext): job = context.job - Log.debug(f"删除消息 chat_id[{job.chat_id}] 的 message_id[{job.data}]") + logger.debug(f"删除消息 chat_id[{job.chat_id}] 的 message_id[{job.data}]") try: await context.bot.delete_message(chat_id=job.chat_id, message_id=job.data) - except BadRequest as error: - if "not found" in str(error): - Log.warning(f"Auth模块删除消息 chat_id[{job.chat_id}] message_id[{job.data}]失败 消息不存在") - elif "Message can't be deleted" in str(error): - Log.warning( + except BadRequest as exc: + if "not found" in str(exc): + logger.warning(f"Auth模块删除消息 chat_id[{job.chat_id}] message_id[{job.data}]失败 消息不存在") + elif "Message can't be deleted" in str(exc): + logger.warning( f"Auth模块删除消息 chat_id[{job.chat_id}] message_id[{job.data}]失败 消息无法删除 可能是没有授权") else: - Log.error(f"Auth模块删除消息 chat_id[{job.chat_id}] message_id[{job.data}]失败", error) + logger.error(f"Auth模块删除消息 chat_id[{job.chat_id}] message_id[{job.data}]失败") + logger.exception(exc) @staticmethod async def restore_member(context: CallbackContext, chat_id: int, user_id: int): - Log.debug(f"重置用户权限 user_id[{user_id}] 在 chat_id[{chat_id}]") + logger.debug(f"重置用户权限 user_id[{user_id}] 在 chat_id[{chat_id}]") try: await context.bot.restrict_chat_member(chat_id=chat_id, user_id=user_id, permissions=FullChatPermissions) - except BadRequest as error: - Log.error(f"Auth模块在 chat_id[{chat_id}] user_id[{user_id}] 执行restore失败", error) + except BadRequest as exc: + logger.error(f"Auth模块在 chat_id[{chat_id}] user_id[{user_id}] 执行restore失败") + logger.exception(exc) + @handler(CallbackQueryHandler, pattern=r"^auth_admin\|", block=False) async def admin(self, update: Update, context: CallbackContext) -> None: async def admin_callback(callback_query_data: str) -> Tuple[str, int]: _data = callback_query_data.split("|") _result = _data[1] _user_id = int(_data[2]) - Log.debug(f"admin_callback函数返回 result[{_result}] user_id[{_user_id}]") + logger.debug(f"admin_callback函数返回 result[{_result}] user_id[{_user_id}]") return _result, _user_id callback_query = update.callback_query user = callback_query.from_user message = callback_query.message chat = message.chat - Log.info(f"用户 {user.full_name}[{user.id}] 在群 {chat.title}[{chat.id}] 点击Auth管理员命令") + logger.info(f"用户 {user.full_name}[{user.id}] 在群 {chat.title}[{chat.id}] 点击Auth管理员命令") chat_administrators = await self.get_chat_administrators(context, chat_id=chat.id) if not self.is_admin(chat_administrators, user.id): - Log.debug(f"用户 {user.full_name}[{user.id}] 在群 {chat.title}[{chat.id}] 非群管理") + logger.debug(f"用户 {user.full_name}[{user.id}] 在群 {chat.title}[{chat.id}] 非群管理") await callback_query.answer(text="你不是管理!\n" "再乱点我叫西风骑士团、千岩军和天领奉行了!", show_alert=True) return @@ -116,7 +120,7 @@ class GroupJoiningVerification: try: member_info = await context.bot.get_chat_member(chat.id, user_id) except BadRequest as error: - Log.warning(f"获取用户 {user_id} 在群 {chat.title}[{chat.id}] 信息失败 \n", error) + logger.warning(f"获取用户 {user_id} 在群 {chat.title}[{chat.id}] 信息失败 \n", error) user_info = f"{user_id}" else: user_info = member_info.user.mention_markdown_v2() @@ -128,13 +132,13 @@ class GroupJoiningVerification: schedule.remove() await message.edit_text(f"{user_info} 被 {user.mention_markdown_v2()} 放行", parse_mode=ParseMode.MARKDOWN_V2) - Log.info(f"用户 user_id[{user_id}] 在群 {chat.title}[{chat.id}] 被管理放行") + logger.info(f"用户 user_id[{user_id}] 在群 {chat.title}[{chat.id}] 被管理放行") elif result == "kick": await callback_query.answer(text="驱离", show_alert=False) await context.bot.ban_chat_member(chat.id, user_id) await message.edit_text(f"{user_info} 被 {user.mention_markdown_v2()} 驱离", parse_mode=ParseMode.MARKDOWN_V2) - Log.info(f"用户 user_id[{user_id}] 在群 {chat.title}[{chat.id}] 被管理踢出") + logger.info(f"用户 user_id[{user_id}] 在群 {chat.title}[{chat.id}] 被管理踢出") elif result == "unban": await callback_query.answer(text="解除驱离", show_alert=False) await self.restore_member(context, chat.id, user_id) @@ -142,13 +146,14 @@ class GroupJoiningVerification: schedule.remove() await message.edit_text(f"{user_info} 被 {user.mention_markdown_v2()} 解除驱离", parse_mode=ParseMode.MARKDOWN_V2) - Log.info(f"用户 user_id[{user_id}] 在群 {chat.title}[{chat.id}] 被管理解除封禁") + logger.info(f"用户 user_id[{user_id}] 在群 {chat.title}[{chat.id}] 被管理解除封禁") else: - Log.warning(f"auth 模块 admin 函数 发现未知命令 result[{result}]") + logger.warning(f"auth 模块 admin 函数 发现未知命令 result[{result}]") await context.bot.send_message(chat.id, "派蒙这边收到了错误的消息!请检查详细日记!") if schedule := context.job_queue.scheduler.get_job(f"{chat.id}|{user_id}|auth_kick"): schedule.remove() + @handler(CallbackQueryHandler, pattern=r"^auth_challenge\|", block=False) async def query(self, update: Update, context: CallbackContext) -> None: async def query_callback(callback_query_data: str) -> Tuple[int, bool, str, str]: @@ -161,8 +166,8 @@ class GroupJoiningVerification: _result = _answer.is_correct _answer_encode = _answer.text _question_encode = _question.text - Log.debug(f"query_callback函数返回 user_id[{_user_id}] result[{_result}] \n" - f"question_encode[{_question_encode}] answer_encode[{_answer_encode}]") + logger.debug(f"query_callback函数返回 user_id[{_user_id}] result[{_result}] \n" + f"question_encode[{_question_encode}] answer_encode[{_answer_encode}]") return _user_id, _result, _question_encode, _answer_encode callback_query = update.callback_query @@ -170,12 +175,13 @@ class GroupJoiningVerification: message = callback_query.message chat = message.chat user_id, result, question, answer = await query_callback(callback_query.data) - Log.info(f"用户 {user.full_name}[{user.id}] 在群 {chat.title}[{chat.id}] 点击Auth认证命令 ") + logger.info(f"用户 {user.full_name}[{user.id}] 在群 {chat.title}[{chat.id}] 点击Auth认证命令 ") if user.id != user_id: await callback_query.answer(text="这不是你的验证!\n" "再乱点再按我叫西风骑士团、千岩军和天领奉行了!", show_alert=True) return - Log.info(f"用户 {user.full_name}[{user.id}] 在群 {chat.title}[{chat.id}] 认证结果为 {'通过' if result else '失败'}") + logger.info( + f"用户 {user.full_name}[{user.id}] 在群 {chat.title}[{chat.id}] 认证结果为 {'通过' if result else '失败'}") if result: buttons = [[InlineKeyboardButton("驱离", callback_data=f"auth_admin|kick|{user.id}")]] await callback_query.answer(text="验证成功", show_alert=False) @@ -185,7 +191,7 @@ class GroupJoiningVerification: text = f"{user.mention_markdown_v2()} 验证成功,向着星辰与深渊!\n" \ f"问题:{escape_markdown(question, version=2)} \n" \ f"回答:{escape_markdown(answer, version=2)}" - Log.info(f"用户 user_id[{user_id}] 在群 {chat.title}[{chat.id}] 验证成功") + logger.info(f"用户 user_id[{user_id}] 在群 {chat.title}[{chat.id}] 验证成功") else: buttons = [[InlineKeyboardButton("驱离", callback_data=f"auth_admin|kick|{user.id}"), InlineKeyboardButton("撤回驱离", callback_data=f"auth_admin|unban|{user.id}")]] @@ -196,25 +202,34 @@ class GroupJoiningVerification: text = f"{user.mention_markdown_v2()} 验证失败,已经赶出提瓦特大陆!\n" \ f"问题:{escape_markdown(question, version=2)} \n" \ f"回答:{escape_markdown(answer, version=2)}" - Log.info(f"用户 user_id[{user_id}] 在群 {chat.title}[{chat.id}] 验证失败") + logger.info(f"用户 user_id[{user_id}] 在群 {chat.title}[{chat.id}] 验证失败") try: await message.edit_text(text, reply_markup=InlineKeyboardMarkup(buttons), parse_mode=ParseMode.MARKDOWN_V2) except BadRequest as exc: if 'are exactly the same as ' in str(exc): - Log.warning("编辑消息发生异常,可能为用户点按多次键盘导致") + logger.warning("编辑消息发生异常,可能为用户点按多次键盘导致") else: raise exc if schedule := context.job_queue.scheduler.get_job(f"{chat.id}|{user.id}|auth_kick"): schedule.remove() + @handler.message.new_chat_members(priority=2) async def new_mem(self, update: Update, context: CallbackContext) -> None: - await self.refresh_quiz() - message = update.message + message = update.effective_message chat = message.chat + if len(bot.config.verify_groups) >= 1: + for verify_group in bot.config.verify_groups: + if verify_group == chat.id: + break + else: + return + else: + return + await self.refresh_quiz() for user in message.new_chat_members: if user.id == context.bot.id: return - Log.info(f"用户 {user.full_name}[{user.id}] 尝试加入群 {chat.title}[{chat.id}]") + logger.info(f"用户 {user.full_name}[{user.id}] 尝试加入群 {chat.title}[{chat.id}]") not_enough_rights = context.chat_data.get("not_enough_rights", False) if not_enough_rights: return @@ -234,7 +249,7 @@ class GroupJoiningVerification: permissions=ChatPermissions(can_send_messages=False)) except BadRequest as err: if "Not enough rights" in str(err): - Log.warning(f"权限不够 chat_id[{message.chat_id}]") + logger.warning(f"权限不够 chat_id[{message.chat_id}]") # reply_message = await message.reply_markdown_v2(f"派蒙无法修改 {user.mention_markdown_v2()} 的权限!" # f"请检查是否给派蒙授权管理了") context.chat_data["not_enough_rights"] = True @@ -269,8 +284,8 @@ class GroupJoiningVerification: reply_message = f"*欢迎来到「提瓦特」世界!* \n" \ f"问题: {escape_markdown(question.text, version=2)} \n" \ f"请在 {self.time_out}S 内回答问题" - Log.debug(f"发送入群验证问题 question_id[{question.question_id}] question[{question.text}] \n" - f"给{user.full_name}[{user.id}] 在 {chat.title}[{chat.id}]") + logger.debug(f"发送入群验证问题 question_id[{question.question_id}] question[{question.text}] \n" + f"给{user.full_name}[{user.id}] 在 {chat.title}[{chat.id}]") try: question_message = await message.reply_markdown_v2(reply_message, reply_markup=InlineKeyboardMarkup(buttons)) diff --git a/plugins/system/errorhandler.py b/plugins/system/errorhandler.py index f9dd7c1..06c3750 100644 --- a/plugins/system/errorhandler.py +++ b/plugins/system/errorhandler.py @@ -1,84 +1,78 @@ import html +import json import traceback -from typing import Optional -import ujson -from telegram import Update, ReplyKeyboardRemove, Message +from telegram import Update, ReplyKeyboardRemove from telegram.constants import ParseMode -from telegram.error import BadRequest +from telegram.error import BadRequest, Forbidden from telegram.ext import CallbackContext -from config import config -from logger import Log +from core.bot import bot +from core.plugin import error_handler, Plugin +from utils.log import logger -try: - notice_chat_id = config.error_notification_chat_id -except KeyError as error: - Log.warning("错误通知Chat_id获取失败或未配置,BOT发生致命错误时不会收到通知 错误信息为\n", error) - notice_chat_id = None +notice_chat_id = bot.config.error_notification_chat_id -async def error_handler(update: object, context: CallbackContext) -> None: - """记录错误并发送消息通知开发人员。 Log the error and send a telegram message to notify the developer.""" +class ErrorHandler(Plugin): - Log.error(msg="处理函数时发生异常:", exc_info=context.error) + @error_handler(block=False) # pylint: disable=E1123, E1120 + async def error_handler(self, update: object, context: CallbackContext) -> None: + """记录错误并发送消息通知开发人员。 logger the error and send a telegram message to notify the developer.""" - if notice_chat_id is None: - return + logger.error("处理函数时发生异常") + logger.exception(context.error) - tb_list = traceback.format_exception(None, context.error, context.error.__traceback__) - tb_string = ''.join(tb_list) - - update_str = update.to_dict() if isinstance(update, Update) else str(update) - text_1 = ( - f'处理函数时发生异常 \n' - f'Exception while handling an update \n' - f'
update = {html.escape(ujson.dumps(update_str, indent=2, ensure_ascii=False))}'
-        '
\n\n' - f'
context.chat_data = {html.escape(str(context.chat_data))}
\n\n' - f'
context.user_data = {html.escape(str(context.user_data))}
\n\n' - ) - text_2 = ( - f'
{html.escape(tb_string)}
' - ) - try: - if 'make sure that only one bot instance is running' in tb_string: - Log.error("其他机器人在运行,请停止!") + if notice_chat_id is None: return - await context.bot.send_message(notice_chat_id, text_1, parse_mode=ParseMode.HTML) - await context.bot.send_message(notice_chat_id, text_2, parse_mode=ParseMode.HTML) - except BadRequest as exc: - if 'too long' in str(exc): - text = ( - f'处理函数时发生异常,traceback太长导致无法发送,但已写入日志 \n' - f'{html.escape(str(context.error))}' - ) - try: - await context.bot.send_message(notice_chat_id, text, parse_mode=ParseMode.HTML) - except BadRequest: + + tb_list = traceback.format_exception(None, context.error, context.error.__traceback__) + tb_string = ''.join(tb_list) + + update_str = update.to_dict() if isinstance(update, Update) else str(update) + text_1 = ( + f'处理函数时发生异常 \n' + f'Exception while handling an update \n' + f'
update = {html.escape(json.dumps(update_str, indent=2, ensure_ascii=False))}'
+            '
\n\n' + f'
context.chat_data = {html.escape(str(context.chat_data))}
\n\n' + f'
context.user_data = {html.escape(str(context.user_data))}
\n\n' + ) + text_2 = ( + f'
{html.escape(tb_string)}
' + ) + try: + if 'make sure that only one bot instance is running' in tb_string: + logger.error("其他机器人在运行,请停止!") + return + await context.bot.send_message(notice_chat_id, text_1, parse_mode=ParseMode.HTML) + await context.bot.send_message(notice_chat_id, text_2, parse_mode=ParseMode.HTML) + except BadRequest as exc: + if 'too long' in str(exc): text = ( - '处理函数时发生异常,traceback太长导致无法发送,但已写入日志 \n') + f'处理函数时发生异常,traceback太长导致无法发送,但已写入日志 \n' + f'{html.escape(str(context.error))}' + ) try: await context.bot.send_message(notice_chat_id, text, parse_mode=ParseMode.HTML) - except BadRequest as exc_1: - Log.error("处理函数时发生异常", exc_1) - effective_user = update.effective_user - try: - message: Optional[Message] = None - if update.callback_query is not None: - message = update.callback_query.message - if update.message is not None: - message = update.message - if update.edited_message is not None: - message = update.edited_message - if message is not None: - chat = message.chat - Log.info(f"尝试通知用户 {effective_user.full_name}[{effective_user.id}] " - f"在 {chat.full_name}[{chat.id}]" - f"的 update_id[{update.update_id}] 错误信息") - text = f"出错了呜呜呜 ~ 派蒙这边发生了点问题无法处理!\n" \ - f"如果当前有对话请发送 /cancel 退出对话。\n" - await context.bot.send_message(message.chat_id, text, reply_markup=ReplyKeyboardRemove(), - parse_mode=ParseMode.HTML) - except BadRequest as exc: - Log.error(f"发送 update_id[{update.update_id}] 错误信息失败 错误信息为 {str(exc)}") + except BadRequest: + text = ( + '处理函数时发生异常,traceback太长导致无法发送,但已写入日志 \n') + try: + await context.bot.send_message(notice_chat_id, text, parse_mode=ParseMode.HTML) + except BadRequest as exc_1: + logger.error("处理函数时发生异常", exc_1) + effective_user = update.effective_user + effective_message = update.effective_message + try: + if effective_message is not None: + chat = effective_message.chat + logger.info(f"尝试通知用户 {effective_user.full_name}[{effective_user.id}] " + f"在 {chat.full_name}[{chat.id}]" + f"的 update_id[{update.update_id}] 错误信息") + text = "出错了呜呜呜 ~ 派蒙这边发生了点问题无法处理!" + await context.bot.send_message(effective_message.chat_id, text, reply_markup=ReplyKeyboardRemove(), + parse_mode=ParseMode.HTML) + except (BadRequest, Forbidden) as exc: + logger.error(f"发送 update_id[{update.update_id}] 错误信息失败 错误信息为") + logger.exception(exc) diff --git a/plugins/system/inline.py b/plugins/system/inline.py index 2d1a5c5..504a242 100644 --- a/plugins/system/inline.py +++ b/plugins/system/inline.py @@ -4,20 +4,20 @@ from uuid import uuid4 from telegram import InlineQueryResultArticle, InputTextMessageContent, Update, InlineQuery from telegram.constants import ParseMode from telegram.error import BadRequest -from telegram.ext import CallbackContext +from telegram.ext import CallbackContext, InlineQueryHandler +from core.plugin import handler, Plugin from core.wiki import WikiService -from logger import Log -from utils.service.inject import inject +from utils.log import logger -class Inline: +class Inline(Plugin): """Inline模块""" - @inject def __init__(self, wiki_service: WikiService = None): self.wiki_service = wiki_service + @handler(InlineQueryHandler, block=False) async def inline_query(self, update: Update, _: CallbackContext) -> None: user = update.effective_user ilq = cast(InlineQuery, update.inline_query) @@ -80,11 +80,11 @@ class Inline: ) except BadRequest as exc: if "Query is too old" in exc.message: # 过时请求全部忽略 - Log.warning(f"用户 {user.full_name}[{user.id}] inline_query请求过时") + logger.warning(f"用户 {user.full_name}[{user.id}] inline_query请求过时") return if "can't parse entities" not in exc.message: raise exc - Log.warning("inline_query发生BadRequest错误", exc_info=exc) + logger.warning("inline_query发生BadRequest错误", exc_info=exc) await ilq.answer( results=[], switch_pm_text="糟糕,发生错误了。", diff --git a/plugins/system/new_member.py b/plugins/system/new_member.py new file mode 100644 index 0000000..45c2f4c --- /dev/null +++ b/plugins/system/new_member.py @@ -0,0 +1,37 @@ +from telegram import Update +from telegram.ext import CallbackContext, filters + +from core.admin import BotAdminService +from core.plugin import Plugin, handler +from utils.log import logger + + +class BotJoiningGroupsVerification(Plugin): + + def __init__(self, bot_admin_service: BotAdminService = None): + self.bot_admin_service = bot_admin_service + + @handler.message.new_chat_members(priority=1) + async def new_member(self, update: Update, context: CallbackContext) -> None: + message = update.effective_message + chat = message.chat + from_user = message.from_user + quit_status = False + if filters.ChatType.GROUPS.filter(message): + for user in message.new_chat_members: + if user.id == context.bot.id: + if from_user is not None: + logger.info(f"用户 {from_user.full_name}[{from_user.id}] 在群 {chat.title}[{chat.id}] 邀请BOT") + admin_list = await self.bot_admin_service.get_admin_list() + if from_user.id in admin_list: + await context.bot.send_message(message.chat_id, + '感谢邀请小派蒙到本群!请使用 /help 查看咱已经学会的功能。') + else: + quit_status = True + else: + logger.info(f"未知用户 在群 {chat.title}[{chat.id}] 邀请BOT") + quit_status = True + if quit_status: + logger.warning("不是管理员邀请!退出群聊。") + await context.bot.send_message(message.chat_id, "派蒙不想进去!不是旅行者的邀请!") + await context.bot.leave_chat(chat.id) diff --git a/plugins/system/set_quiz.py b/plugins/system/set_quiz.py new file mode 100644 index 0000000..830e096 --- /dev/null +++ b/plugins/system/set_quiz.py @@ -0,0 +1,245 @@ +import re +from typing import List + +from redis import DataError, ResponseError +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, Update +from telegram.ext import CallbackContext, ConversationHandler, filters +from telegram.helpers import escape_markdown + +from core.baseplugin import BasePlugin +from core.plugin import Plugin, conversation, handler +from core.quiz import QuizService +from core.quiz.models import Answer, Question +from utils.decorators.admins import bot_admins_rights_check +from utils.decorators.error import error_callable +from utils.decorators.restricts import restricts +from utils.log import logger + +( + CHECK_COMMAND, + VIEW_COMMAND, + CHECK_QUESTION, + GET_NEW_QUESTION, + GET_NEW_CORRECT_ANSWER, + GET_NEW_WRONG_ANSWER, + QUESTION_EDIT, + SAVE_QUESTION +) = range(10300, 10308) + + +class QuizCommandData: + question_id: int = -1 + new_question: str = "" + new_correct_answer: str = "" + new_wrong_answer: List[str] = [] + status: int = 0 + + +class SetQuizPlugin(Plugin.Conversation, BasePlugin.Conversation): + """派蒙的十万个为什么问题修改/添加/删除""" + + def __init__(self, quiz_service: QuizService = None): + self.quiz_service = quiz_service + self.time_out = 120 + + @conversation.entry_point + @handler.command(command='set_quiz', filters=filters.ChatType.PRIVATE, block=True) + @restricts() + @bot_admins_rights_check + @error_callable + async def command_start(self, update: Update, context: CallbackContext) -> int: + user = update.effective_user + message = update.message + logger.info(f"用户 {user.full_name}[{user.id}] set_quiz命令请求") + quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") + if quiz_command_data is None: + quiz_command_data = QuizCommandData() + context.chat_data["quiz_command_data"] = quiz_command_data + text = f'你好 {user.mention_markdown_v2()} {escape_markdown("!请选择你的操作!")}' + reply_keyboard = [ + ["查看问题", "添加问题"], + ["重载问题"], + ["退出"] + ] + await message.reply_markdown_v2(text, reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)) + return CHECK_COMMAND + + async def view_command(self, update: Update, _: CallbackContext) -> int: + _ = self + keyboard = [ + [ + InlineKeyboardButton(text="选择问题", switch_inline_query_current_chat="查看问题 ") + ] + ] + await update.message.reply_text("请回复你要查看的问题", + reply_markup=InlineKeyboardMarkup(keyboard)) + return CHECK_COMMAND + + @conversation.state(state=CHECK_QUESTION) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) + async def check_question(self, update: Update, _: CallbackContext) -> int: + reply_keyboard = [ + ["删除问题"], + ["退出"] + ] + await update.message.reply_text("请选择你的操作", reply_markup=ReplyKeyboardMarkup(reply_keyboard)) + return CHECK_COMMAND + + @conversation.state(state=CHECK_COMMAND) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) + async def check_command(self, update: Update, context: CallbackContext) -> int: + quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") + if update.message.text == "退出": + await update.message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + elif update.message.text == "查看问题": + return await self.view_command(update, context) + elif update.message.text == "添加问题": + return await self.add_question(update, context) + elif update.message.text == "删除问题": + return await self.delete_question(update, context) + # elif update.message.text == "修改问题": + # return await self.edit_question(update, context) + elif update.message.text == "重载问题": + return await self.refresh_question(update, context) + else: + result = re.findall(r"问题ID (\d+)", update.message.text) + if len(result) == 1: + try: + question_id = int(result[0]) + except ValueError: + await update.message.reply_text("获取问题ID失败") + return ConversationHandler.END + quiz_command_data.question_id = question_id + await update.message.reply_text("获取问题ID成功") + return await self.check_question(update, context) + await update.message.reply_text("命令错误", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + + async def refresh_question(self, update: Update, _: CallbackContext) -> int: + try: + await self.quiz_service.refresh_quiz() + except DataError: + await update.message.reply_text("Redis数据错误,重载失败", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + except ResponseError as error: + logger.error("重载问题失败", error) + await update.message.reply_text("重载问题失败,异常抛出Redis请求错误异常,详情错误请看日记", + reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + await update.message.reply_text("重载成功", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + + async def add_question(self, update: Update, context: CallbackContext) -> int: + _ = self + quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") + quiz_command_data.new_wrong_answer = [] + quiz_command_data.new_question = "" + quiz_command_data.new_correct_answer = "" + quiz_command_data.status = 1 + await update.message.reply_text("请回复你要添加的问题,或发送 /cancel 取消操作", + reply_markup=ReplyKeyboardRemove()) + return GET_NEW_QUESTION + + @conversation.state(state=GET_NEW_QUESTION) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) + async def get_new_question(self, update: Update, context: CallbackContext) -> int: + quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") + reply_text = f"问题:`{escape_markdown(update.message.text, version=2)}`\n" \ + f"请填写正确答案:" + quiz_command_data.new_question = update.message.text + await update.message.reply_markdown_v2(reply_text) + return GET_NEW_CORRECT_ANSWER + + @conversation.state(state=GET_NEW_CORRECT_ANSWER) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) + async def get_new_correct_answer(self, update: Update, context: CallbackContext) -> int: + quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") + reply_text = f"正确答案:`{escape_markdown(update.message.text, version=2)}`\n" \ + f"请填写错误答案:" + await update.message.reply_markdown_v2(reply_text) + quiz_command_data.new_correct_answer = update.message.text + return GET_NEW_WRONG_ANSWER + + @conversation.state(state=GET_NEW_WRONG_ANSWER) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) + @handler.command(command='finish_edit', block=True) + async def get_new_wrong_answer(self, update: Update, context: CallbackContext) -> int: + quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") + reply_text = f"错误答案:`{escape_markdown(update.message.text, version=2)}`\n" \ + f"可继续填写,并使用 {escape_markdown('/finish', version=2)} 结束。" + await update.message.reply_markdown_v2(reply_text) + quiz_command_data.new_wrong_answer.append(update.message.text) + return GET_NEW_WRONG_ANSWER + + async def finish_edit(self, update: Update, context: CallbackContext): + _ = self + quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") + reply_text = f"问题:`{escape_markdown(quiz_command_data.new_question, version=2)}`\n" \ + f"正确答案:`{escape_markdown(quiz_command_data.new_correct_answer, version=2)}`\n" \ + f"错误答案:`{escape_markdown(' '.join(quiz_command_data.new_wrong_answer), version=2)}`" + await update.message.reply_markdown_v2(reply_text) + reply_keyboard = [["保存并重载配置", "抛弃修改并退出"]] + await update.message.reply_text("请核对问题,并选择下一步操作。", + reply_markup=ReplyKeyboardMarkup(reply_keyboard)) + return SAVE_QUESTION + + @conversation.state(state=SAVE_QUESTION) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) + async def save_question(self, update: Update, context: CallbackContext): + quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") + if update.message.text == "抛弃修改并退出": + await update.message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + elif update.message.text == "保存并重载配置": + if quiz_command_data.status == 1: + answer = [ + Answer(text=wrong_answer, is_correct=False) for wrong_answer in + quiz_command_data.new_wrong_answer + ] + answer.append(Answer(text=quiz_command_data.new_correct_answer, is_correct=True)) + await self.quiz_service.save_quiz( + Question(text=quiz_command_data.new_question)) + await update.message.reply_text("保存成功", reply_markup=ReplyKeyboardRemove()) + try: + await self.quiz_service.refresh_quiz() + except ResponseError as error: + logger.error("重载问题失败", error) + await update.message.reply_text("重载问题失败,异常抛出Redis请求错误异常,详情错误请看日记", + reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + await update.message.reply_text("重载配置成功", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + else: + await update.message.reply_text("回复错误,请重新选择") + return SAVE_QUESTION + + async def edit_question(self, update: Update, context: CallbackContext) -> int: + _ = self + quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") + quiz_command_data.new_wrong_answer = [] + quiz_command_data.new_question = "" + quiz_command_data.new_correct_answer = "" + quiz_command_data.status = 2 + await update.message.reply_text("请回复你要修改的问题", reply_markup=ReplyKeyboardRemove()) + return GET_NEW_QUESTION + + async def delete_question(self, update: Update, context: CallbackContext) -> int: + quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") + # 再问题重载Redis 以免redis数据为空时出现奔溃 + try: + await self.quiz_service.refresh_quiz() + question = await self.quiz_service.get_question(quiz_command_data.question_id) + # 因为外键的存在,先删除答案 + for answer in question.answers: + await self.quiz_service.delete_question_by_id(answer.answer_id) + await self.quiz_service.delete_question_by_id(question.question_id) + await update.message.reply_text("删除问题成功", reply_markup=ReplyKeyboardRemove()) + await self.quiz_service.refresh_quiz() + except ResponseError as error: + logger.error("重载问题失败", error) + await update.message.reply_text("重载问题失败,异常抛出Redis请求错误异常,详情错误请看日记", + reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + await update.message.reply_text("重载配置成功", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END diff --git a/plugins/system/start.py b/plugins/system/start.py index 3cf376b..1fe08b9 100644 --- a/plugins/system/start.py +++ b/plugins/system/start.py @@ -1,42 +1,41 @@ from telegram import Update, ReplyKeyboardRemove -from telegram.ext import CallbackContext +from telegram.ext import CallbackContext, CommandHandler from telegram.helpers import escape_markdown +from core.plugin import handler, Plugin from utils.decorators.restricts import restricts -@restricts() -async def start(update: Update, context: CallbackContext) -> None: - user = update.effective_user - message = update.message - args = context.args - if args is not None and len(args) >= 1 and args[0] == "inline_message": - await message.reply_markdown_v2(f"你好 {user.mention_markdown_v2()} {escape_markdown('!我是派蒙 !')}\n" - f"{escape_markdown('发送 /help 命令即可查看命令帮助')}") - return - await update.message.reply_markdown_v2(f"你好 {user.mention_markdown_v2()} {escape_markdown('!我是派蒙 !')}") +class StartPlugin(Plugin): + @handler(CommandHandler, command="start", block=False) + @restricts() + async def start(self, update: Update, context: CallbackContext) -> None: + user = update.effective_user + message = update.effective_message + args = context.args + if args is not None and len(args) >= 1 and args[0] == "inline_message": + await message.reply_markdown_v2(f"你好 {user.mention_markdown_v2()} {escape_markdown('!我是派蒙 !')}\n" + f"{escape_markdown('发送 /help 命令即可查看命令帮助')}") + return + await message.reply_markdown_v2(f"你好 {user.mention_markdown_v2()} {escape_markdown('!我是派蒙 !')}") -@restricts() -async def help_command(update: Update, _: CallbackContext) -> None: - await update.message.reply_text("前面的区域,以后再来探索吧!") + @staticmethod + @restricts() + async def unknown_command(update: Update, _: CallbackContext) -> None: + await update.effective_message.reply_text("前面的区域,以后再来探索吧!") + @staticmethod + @restricts() + async def emergency_food(update: Update, _: CallbackContext) -> None: + await update.effective_message.reply_text("派蒙才不是应急食品!") -@restricts() -async def unknown_command(update: Update, _: CallbackContext) -> None: - await update.message.reply_text("前面的区域,以后再来探索吧!") + @handler(CommandHandler, command="ping", block=False) + @restricts() + async def ping(self, update: Update, _: CallbackContext) -> None: + await update.effective_message.reply_text("online! ヾ(✿゚▽゚)ノ") - -@restricts() -async def emergency_food(update: Update, _: CallbackContext) -> None: - await update.message.reply_text("派蒙才不是应急食品!") - - -@restricts() -async def ping(update: Update, _: CallbackContext) -> None: - await update.message.reply_text("online! ヾ(✿゚▽゚)ノ") - - -@restricts() -async def reply_keyboard_remove(update: Update, _: CallbackContext) -> None: - await update.message.reply_text("移除远程键盘成功", reply_markup=ReplyKeyboardRemove()) + @handler(CommandHandler, command="reply_keyboard_remove", block=False) + @restricts() + async def reply_keyboard_remove(self, update: Update, _: CallbackContext) -> None: + await update.message.reply_text("移除远程键盘成功", reply_markup=ReplyKeyboardRemove()) diff --git a/poetry.lock b/poetry.lock index a9a3096..34c1cc3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -255,6 +255,17 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] development = ["black", "flake8", "mypy", "pytest", "types-colorama"] +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + [[package]] name = "Deprecated" version = "1.2.13" @@ -269,6 +280,25 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] +[[package]] +name = "enkanetwork.py" +version = "1.2.10" +description = "Library for fetching JSON data from site https://enka.network/" +category = "main" +optional = false +python-versions = ">=3.6" +develop = false + +[package.dependencies] +aiohttp = "*" +pydantic = "*" + +[package.source] +type = "git" +url = "https://github.com/mrwan200/EnkaNetwork.py" +reference = "HEAD" +resolved_reference = "d1079baff72a55da52b38451e950ded6a4af8dd9" + [[package]] name = "fakeredis" version = "1.9.0" @@ -297,7 +327,7 @@ python-versions = ">=3.7" [[package]] name = "genshin" version = "1.2.3" -description = "" +description = "An API wrapper for Genshin Impact." category = "main" optional = false python-versions = ">=3.8" @@ -557,6 +587,17 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "Pygments" +version = "2.13.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +plugins = ["importlib-metadata"] + [[package]] name = "PyMySQL" version = "1.0.2" @@ -680,6 +721,22 @@ idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} [package.extras] idna2008 = ["idna"] +[[package]] +name = "rich" +version = "12.5.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.6.3,<4.0.0" + +[package.dependencies] +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + [[package]] name = "setuptools" version = "65.3.0" @@ -799,7 +856,7 @@ python-versions = ">= 3.7" [[package]] name = "tqdm" -version = "4.64.0" +version = "4.64.1" description = "Fast, Extensible Progress Meter" category = "main" optional = false @@ -911,7 +968,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "5a1c1cb602c48797f132ae1c51c1a6afb023a3d6efb2b99348fca509269be118" +content-hash = "e9e557aad86a486e9d7a603dbcefe18269ad6414d89f9b98dbcad06fb200d2a1" [metadata.files] aiofiles = [ @@ -1110,10 +1167,15 @@ colorlog = [ {file = "colorlog-6.7.0-py2.py3-none-any.whl", hash = "sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662"}, {file = "colorlog-6.7.0.tar.gz", hash = "sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5"}, ] +commonmark = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] Deprecated = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] +"enkanetwork.py" = [] fakeredis = [ {file = "fakeredis-1.9.0-py3-none-any.whl", hash = "sha256:868467ff399520fc77e37ff002c60d1b2a1674742982e27338adaeebcc537648"}, {file = "fakeredis-1.9.0.tar.gz", hash = "sha256:60639946e3bb1274c30416f539f01f9d73b4ea68c244c1442f5524e45f51e882"}, @@ -1425,8 +1487,8 @@ Pillow = [ {file = "Pillow-9.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e"}, {file = "Pillow-9.2.0-cp310-cp310-win32.whl", hash = "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28"}, {file = "Pillow-9.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d"}, - {file = "Pillow-9.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:adabc0bce035467fb537ef3e5e74f2847c8af217ee0be0455d4fec8adc0462fc"}, - {file = "Pillow-9.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:336b9036127eab855beec9662ac3ea13a4544a523ae273cbf108b228ecac8437"}, + {file = "Pillow-9.2.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:408673ed75594933714482501fe97e055a42996087eeca7e5d06e33218d05aa8"}, + {file = "Pillow-9.2.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:727dd1389bc5cb9827cbd1f9d40d2c2a1a0c9b32dd2261db522d22a604a6eec9"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4"}, @@ -1529,6 +1591,10 @@ pyee = [ {file = "pyee-8.1.0-py2.py3-none-any.whl", hash = "sha256:383973b63ad7ed5e3c0311f8b179c52981f9e7b3eaea0e9a830d13ec34dde65f"}, {file = "pyee-8.1.0.tar.gz", hash = "sha256:92dacc5bd2bdb8f95aa8dd2585d47ca1c4840e2adb95ccf90034d64f725bfd31"}, ] +Pygments = [ + {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, + {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, +] PyMySQL = [ {file = "PyMySQL-1.0.2-py3-none-any.whl", hash = "sha256:41fc3a0c5013d5f039639442321185532e3e2c8924687abe6537de157d403641"}, {file = "PyMySQL-1.0.2.tar.gz", hash = "sha256:816927a350f38d56072aeca5dfb10221fe1dc653745853d30a216637f5d7ad36"}, @@ -1565,6 +1631,10 @@ rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] +rich = [ + {file = "rich-12.5.1-py3-none-any.whl", hash = "sha256:2eb4e6894cde1e017976d2975ac210ef515d7548bc595ba20e195fb9628acdeb"}, + {file = "rich-12.5.1.tar.gz", hash = "sha256:63a5c5ce3673d3d5fbbf23cd87e11ab84b6b451436f1b7f19ec54b6bc36ed7ca"}, +] setuptools = [ {file = "setuptools-65.3.0-py3-none-any.whl", hash = "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82"}, {file = "setuptools-65.3.0.tar.gz", hash = "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57"}, @@ -1649,8 +1719,8 @@ tornado = [ {file = "tornado-6.2.tar.gz", hash = "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13"}, ] tqdm = [ - {file = "tqdm-4.64.0-py2.py3-none-any.whl", hash = "sha256:74a2cdefe14d11442cedf3ba4e21a3b84ff9a2dbdc6cfae2c34addb2a14a5ea6"}, - {file = "tqdm-4.64.0.tar.gz", hash = "sha256:40be55d30e200777a307a7585aee69e4eabb46b4ec6a4b4a5f2d9f11e7d5408d"}, + {file = "tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1"}, + {file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"}, ] typing-extensions = [ {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, diff --git a/pyproject.toml b/pyproject.toml index ea99b20..96917a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,8 @@ python-dotenv = "^0.20.0" PyMySQL = "^1.0.2" alembic = "^1.8.1" black = "^22.8.0" +rich = "^12.5.1" +enkanetwork-py = {git = "https://github.com/mrwan200/EnkaNetwork.py"} [build-system] diff --git a/resources/genshin/abyss/abyss.html b/resources/genshin/abyss/abyss.html index 60ce075..305590a 100644 --- a/resources/genshin/abyss/abyss.html +++ b/resources/genshin/abyss/abyss.html @@ -34,8 +34,8 @@
出战次数
{% for most_played in most_played_list %} -
-
+
{{most_played.value}}次
@@ -45,7 +45,7 @@
-
出战次数
+
战斗数据榜
diff --git a/resources/genshin/abyss/example.html b/resources/genshin/abyss/example.html index 838c4f3..1a7376f 100644 --- a/resources/genshin/abyss/example.html +++ b/resources/genshin/abyss/example.html @@ -33,26 +33,26 @@
出战次数
-
-
+
12次
-
-
+
12次
-
-
+
12次
-
-
+
12次
@@ -61,7 +61,7 @@
-
出战次数
+
战斗数据榜
diff --git a/run.py b/run.py new file mode 100644 index 0000000..8d0f4a5 --- /dev/null +++ b/run.py @@ -0,0 +1,9 @@ +from core.bot import bot + + +def main(): + bot.launch() + + +if __name__ == '__main__': + main() diff --git a/test/model/apihelper/test_artifact.py b/tests/model/apihelper/test_artifact.py similarity index 94% rename from test/model/apihelper/test_artifact.py rename to tests/model/apihelper/test_artifact.py index 4b48b07..31c7838 100644 --- a/test/model/apihelper/test_artifact.py +++ b/tests/model/apihelper/test_artifact.py @@ -1,7 +1,7 @@ import unittest from unittest import IsolatedAsyncioTestCase -from models.apihelper.artifact import ArtifactOcrRate +from modules.apihelper.artifact import ArtifactOcrRate class TestArtifact(IsolatedAsyncioTestCase): diff --git a/test/model/wiki/test_wiki.py b/tests/model/wiki/test_wiki.py similarity index 96% rename from test/model/wiki/test_wiki.py rename to tests/model/wiki/test_wiki.py index cf72371..175d67f 100644 --- a/test/model/wiki/test_wiki.py +++ b/tests/model/wiki/test_wiki.py @@ -1,9 +1,9 @@ import unittest from unittest import IsolatedAsyncioTestCase -from models.wiki.character import Character -from models.wiki.material import Material -from models.wiki.weapon import Weapon +from modules.wiki.character import Character +from modules.wiki.material import Material +from modules.wiki.weapon import Weapon class TestWeapon(IsolatedAsyncioTestCase): diff --git a/test/run.py b/tests/run.py similarity index 100% rename from test/run.py rename to tests/run.py diff --git a/test/service/test_game.py b/tests/service/test_game.py similarity index 96% rename from test/service/test_game.py rename to tests/service/test_game.py index 8087773..bab29cc 100644 --- a/test/service/test_game.py +++ b/tests/service/test_game.py @@ -1,7 +1,7 @@ import unittest from unittest import IsolatedAsyncioTestCase -from models.apihelper.hyperion import Hyperion +from modules.apihelper.hyperion import Hyperion class TestGame(IsolatedAsyncioTestCase): diff --git a/utils/aiobrowser.py b/utils/aiobrowser.py deleted file mode 100644 index 972af5c..0000000 --- a/utils/aiobrowser.py +++ /dev/null @@ -1,50 +0,0 @@ -import asyncio -from typing import Optional - -from playwright.async_api import async_playwright, Browser, Playwright - -from logger import Log - - -class AioBrowser: - def __init__(self, loop=None): - self.browser: Optional[Browser] = None - self._playwright: Optional[Playwright] = None - self._loop = loop - if self._loop is None: - self._loop = asyncio.get_event_loop() - try: - Log.info("正在尝试启动Playwright") - self._loop.run_until_complete(self._browser_init()) - Log.info("启动Playwright成功") - except (KeyboardInterrupt, SystemExit): - pass - except Exception as exc: - Log.error("启动浏览器失败") - raise exc - - async def _browser_init(self) -> Browser: - if self._playwright is None: - self._playwright = await async_playwright().start() - try: - self.browser = await self._playwright.chromium.launch(timeout=5000) - except TimeoutError as err: - raise err - else: - if self.browser is None: - try: - self.browser = await self._playwright.chromium.launch(timeout=10000) - except TimeoutError as err: - raise err - return self.browser - - async def close(self): - if self.browser is not None: - await self.browser.close() - if self._playwright is not None: - await self._playwright.stop() - - async def get_browser(self) -> Browser: - if self.browser is None: - raise RuntimeError("browser is not None") - return self.browser diff --git a/models/baseobject.py b/utils/baseobject.py similarity index 98% rename from models/baseobject.py rename to utils/baseobject.py index fad7ae4..6343f5a 100644 --- a/models/baseobject.py +++ b/utils/baseobject.py @@ -2,7 +2,7 @@ import json from copy import deepcopy from typing import Dict, Union, Optional, List -from models.types import JSONDict +from utils.typedefs import JSONDict class BaseObject: diff --git a/utils/const.py b/utils/const.py new file mode 100644 index 0000000..eadd69d --- /dev/null +++ b/utils/const.py @@ -0,0 +1,14 @@ +"""常量""" +from pathlib import Path + +__all__ = [ + 'PROJECT_ROOT', 'PLUGIN_DIR', + 'NOT_SET', +] + +# 项目根目录 +PROJECT_ROOT = Path(__file__).joinpath('../..').resolve() +# 插件目录 +PLUGIN_DIR = PROJECT_ROOT / 'plugins' + +NOT_SET = object() diff --git a/utils/decorators/admins.py b/utils/decorators/admins.py index d777fd4..b3dfe5f 100644 --- a/utils/decorators/admins.py +++ b/utils/decorators/admins.py @@ -1,35 +1,38 @@ from functools import wraps -from typing import Callable +from typing import Callable, cast -from core.admin.services import BotAdminService -from utils.service.inject import inject +from telegram import Update + +from core.admin import BotAdminService +from core.bot import bot +from core.error import ServiceNotFoundError + +bot_admin_service = bot.services.get(BotAdminService) def bot_admins_rights_check(func: Callable) -> Callable: """BOT ADMIN 权限检查""" - @inject - def get_bot_admin_service(bot_admin_service: BotAdminService = None): - return bot_admin_service - @wraps(func) async def decorator(*args, **kwargs): if len(args) == 3: # self update context - _, update, context = args + _, update, _ = args elif len(args) == 2: # update context - update, context = args + update, _ = args else: return await func(*args, **kwargs) - bot_admin_service = get_bot_admin_service() if bot_admin_service is None: - raise RuntimeError("bot_admin_service is None") + raise ServiceNotFoundError("BotAdminService") admin_list = await bot_admin_service.get_admin_list() - if update.message.from_user.id in admin_list: + update = cast(Update, update) + message = update.effective_message + user = update.effective_user + if user.id in admin_list: return await func(*args, **kwargs) else: - await update.message.reply_text("权限不足") + await message.reply_text("权限不足") return None return decorator diff --git a/utils/decorators/error.py b/utils/decorators/error.py index 2d48a27..c331eae 100644 --- a/utils/decorators/error.py +++ b/utils/decorators/error.py @@ -1,43 +1,40 @@ +import json from functools import wraps -from typing import Callable, Optional +from typing import Callable, cast -import ujson from aiohttp import ClientConnectorError from genshin import InvalidCookies, GenshinException, TooManyRequests from httpx import ConnectTimeout -from telegram import Update, ReplyKeyboardRemove, Message +from telegram import Update, ReplyKeyboardRemove from telegram.error import BadRequest, TimedOut, Forbidden from telegram.ext import CallbackContext, ConversationHandler -from logger import Log from utils.error import UrlResourcesNotFoundError +from utils.log import logger async def send_user_notification(update: Update, _: CallbackContext, text: str): effective_user = update.effective_user - message: Optional[Message] = None - if update.callback_query is not None: - message = update.callback_query.message - if update.message is not None: - message = update.message - if update.edited_message is not None: - message = update.edited_message + message = update.effective_message if message is None: update_str = update.to_dict() if isinstance(update, Update) else str(update) - Log.warning("错误的消息类型\n" + ujson.dumps(update_str, indent=2, ensure_ascii=False)) + logger.warning("错误的消息类型\n" + json.dumps(update_str, indent=2, ensure_ascii=False)) return chat = message.chat - Log.info(f"尝试通知用户 {effective_user.full_name}[{effective_user.id}] " - f"在 {chat.full_name}[{chat.id}]" - f"的 错误信息[{text}]") + logger.info(f"尝试通知用户 {effective_user.full_name}[{effective_user.id}] " + f"在 {chat.full_name}[{chat.id}]" + f"的 错误信息[{text}]") try: await message.reply_text(text, reply_markup=ReplyKeyboardRemove(), allow_sending_without_reply=True) except BadRequest as exc: - Log.error(f"发送 update_id[{update.update_id}] 错误信息失败 错误信息为 {str(exc)}") + logger.error(f"发送 update_id[{update.update_id}] 错误信息失败 错误信息为") + logger.exception(exc) except Forbidden as exc: - Log.error(f"发送 update_id[{update.update_id}] 错误信息失败 错误信息为 {str(exc)}") + logger.error(f"发送 update_id[{update.update_id}] 错误信息失败 错误信息为") + logger.exception(exc) except BaseException as exc: - Log.error(f"发送 update_id[{update.update_id}] 错误信息失败 错误信息为 {str(exc)}") + logger.error(f"发送 update_id[{update.update_id}] 错误信息失败 错误信息为") + logger.exception(exc) finally: pass @@ -50,8 +47,6 @@ def error_callable(func: Callable) -> Callable: @wraps(func) async def decorator(*args, **kwargs): - update: Optional[Update] = None - context: Optional[CallbackContext] = None if len(args) == 3: # self update context _, update, context = args @@ -60,54 +55,60 @@ def error_callable(func: Callable) -> Callable: update, context = args else: return await func(*args, **kwargs) + update = cast(Update, update) + context = cast(CallbackContext, context) try: return await func(*args, **kwargs) except ClientConnectorError: - Log.error("aiohttp 模块连接服务器 ClientConnectorError") + logger.error("aiohttp 模块连接服务器 ClientConnectorError") await send_user_notification(update, context, "出错了呜呜呜 ~ 服务器连接超时 服务器熟啦 ~ ") return ConversationHandler.END except ConnectTimeout: - Log.error("httpx 模块连接服务器 ConnectTimeout") + logger.error("httpx 模块连接服务器 ConnectTimeout") await send_user_notification(update, context, "出错了呜呜呜 ~ 服务器连接超时 服务器熟啦 ~ ") return ConversationHandler.END except TimedOut: - Log.error("python-telegram-bot 模块连接服务器 TimedOut") + logger.error("python-telegram-bot 模块连接服务器 TimedOut") await send_user_notification(update, context, "出错了呜呜呜 ~ 服务器连接超时 服务器熟啦 ~ ") return ConversationHandler.END except UrlResourcesNotFoundError as exc: - Log.error("URL数据资源未找到", exc) + logger.error("URL数据资源未找到") + logger.exception(exc) await send_user_notification(update, context, "出错了呜呜呜 ~ 资源未找到 ~ ") return ConversationHandler.END except InvalidCookies as exc: if "[10001]" in str(exc): - await send_user_notification(update, context, "Cookies无效,请尝试重新绑定账户") + await send_user_notification(update, context, "出错了呜呜呜 ~ Cookies无效,请尝试重新绑定账户") elif "[-100]" in str(exc): - await send_user_notification(update, context, "Cookies无效,请尝试重新绑定账户") + await send_user_notification(update, context, "出错了呜呜呜 ~ Cookies无效,请尝试重新绑定账户") elif "[10103]" in str(exc): - await send_user_notification(update, context, "Cookie有效,但没有绑定到游戏帐户,请尝试重新绑定邮游戏账户") + await send_user_notification(update, context, "出错了呜呜呜 ~ Cookie有效,但没有绑定到游戏帐户," + "请尝试重新绑定邮游戏账户") else: - Log.warning("Cookie错误", exc) - await send_user_notification(update, context, "Cookies无效,具体原因未知") + logger.warning("Cookie错误") + logger.exception(exc) + await send_user_notification(update, context, "出错了呜呜呜 ~ Cookies无效,具体原因未知") return ConversationHandler.END except TooManyRequests as exc: - Log.warning("查询次数太多(操作频繁)", exc) - await send_user_notification(update, context, "当天查询次数已经超过30次,请次日再进行查询") + logger.warning("查询次数太多(操作频繁)", exc) + await send_user_notification(update, context, "出错了呜呜呜 ~ 当天查询次数已经超过30次,请次日再进行查询") return ConversationHandler.END except GenshinException as exc: if "[-130]" in str(exc): - await send_user_notification(update, context, "未设置默认角色,请尝试重新绑定默认角色") + await send_user_notification(update, context, "出错了呜呜呜 ~ 未设置默认角色,请尝试重新绑定默认角色") return ConversationHandler.END - Log.warning("GenshinException", exc) - await send_user_notification(update, context, - f"获取账号信息发生错误,错误信息为 {str(exc)}") + logger.warning("GenshinException", exc) + await send_user_notification(update, context, "出错了呜呜呜 ~ 获取账号信息发生错误") return ConversationHandler.END except BadRequest as exc: - Log.warning("python-telegram-bot 请求错误", exc) - await send_user_notification(update, context, f"telegram-bot-api请求错误 错误信息为 {str(exc)}") + logger.warning("python-telegram-bot 请求错误") + logger.exception(exc) + await send_user_notification(update, context, "出错了呜呜呜 ~ telegram-bot-api请求错误") return ConversationHandler.END except Forbidden as exc: - Log.warning("python-telegram-bot返回 Forbidden", exc) - await send_user_notification(update, context, "telegram-bot-api请求错误") + logger.warning("python-telegram-bot返回 Forbidden") + logger.exception(exc) + await send_user_notification(update, context, "出错了呜呜呜 ~ telegram-bot-api请求错误") return ConversationHandler.END return decorator diff --git a/utils/decorators/restricts.py b/utils/decorators/restricts.py index 302c242..c09af60 100644 --- a/utils/decorators/restricts.py +++ b/utils/decorators/restricts.py @@ -1,12 +1,12 @@ import time from functools import wraps -from typing import Callable +from typing import Callable, cast from telegram import Update from telegram.error import TelegramError from telegram.ext import filters, CallbackContext -from logger import Log +from utils.log import logger def restricts(filters_chat: filters = filters.ALL, return_data=None, try_delete_message: bool = False, @@ -39,14 +39,16 @@ def restricts(filters_chat: filters = filters.ALL, return_data=None, try_delete_ @wraps(func) async def restricts_func(*args, **kwargs): if len(args) == 3: - update: Update = args[1] - context: CallbackContext = args[2] + # self update context + _, update, context = args elif len(args) == 2: - update: Update = args[0] - context: CallbackContext = args[1] + # update context + update, context = args else: return await func(*args, **kwargs) - message = update.message + update = cast(Update, update) + context = cast(CallbackContext, context) + message = update.effective_message user = update.effective_user if filters_chat.filter(message): command_time = context.user_data.get("command_time", 0) @@ -62,8 +64,8 @@ def restricts(filters_chat: filters = filters.ALL, return_data=None, try_delete_ else: if count == 5: context.user_data["restrict_since"] = time.time() - await update.effective_message.reply_text("你已经触发洪水防御,请等待5分钟") - Log.warning(f"用户 {user.full_name}[{user.id}] 触发洪水限制 已被限制5分钟") + await message.reply_text("你已经触发洪水防御,请等待5分钟") + logger.warning(f"用户 {user.full_name}[{user.id}] 触发洪水限制 已被限制5分钟") return return_data # 单次使用限制 if command_time: @@ -73,8 +75,9 @@ def restricts(filters_chat: filters = filters.ALL, return_data=None, try_delete_ if try_delete_message: try: await message.delete() - except TelegramError as error: - Log.warning("删除消息失败", error) + except TelegramError as exc: + logger.warning("删除消息失败") + logger.exception(exc) return return_data else: if count >= 1: diff --git a/utils/helpers.py b/utils/helpers.py index 654c001..05d5f82 100644 --- a/utils/helpers.py +++ b/utils/helpers.py @@ -1,6 +1,6 @@ import hashlib import os -from typing import Union, Optional +from typing import Tuple, Union, Optional, cast import aiofiles import genshin @@ -8,11 +8,13 @@ import httpx from genshin import Client, types from httpx import UnsupportedProtocol -from core.cookies.services import CookiesService +from core.bot import bot +from core.cookies.services import CookiesService, PublicCookiesService +from core.error import ServiceNotFoundError from core.user.services import UserService -from logger import Log -from models.base import RegionEnum from utils.error import UrlResourcesNotFoundError +from utils.log import logger +from utils.models.base import RegionEnum USER_AGENT: str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " \ "Chrome/90.0.4430.72 Safari/537.36" @@ -22,6 +24,13 @@ cache_dir = os.path.join(current_dir, "cache") if not os.path.exists(cache_dir): os.mkdir(cache_dir) +cookies_service = bot.services.get(CookiesService) +cookies_service = cast(CookiesService, cookies_service) +user_service = bot.services.get(UserService) +user_service = cast(UserService, user_service) +public_cookies_service = bot.services.get(PublicCookiesService) +public_cookies_service = cast(PublicCookiesService, public_cookies_service) + REGION_MAP = { "1": RegionEnum.HYPERION, "2": RegionEnum.HYPERION, @@ -49,24 +58,26 @@ async def url_to_file(url: str, prefix: str = "file://") -> str: async with httpx.AsyncClient(headers=REQUEST_HEADERS) as client: try: data = await client.get(url) - except UnsupportedProtocol as error: - Log.error(f"连接不支持 url[{url}]") - Log.error("错误信息为", error) + except UnsupportedProtocol: + logger.error(f"连接不支持 url[{url}]") return "" if data.is_error: - Log.error(f"请求出现错误 url[{url}] status_code[{data.status_code}]") + logger.error(f"请求出现错误 url[{url}] status_code[{data.status_code}]") raise UrlResourcesNotFoundError(url) if data.status_code != 200: - Log.error(f"url_to_file 获取url[{url}] 错误 status_code[f{data.status_code}]") + logger.error(f"url_to_file 获取url[{url}] 错误 status_code[f{data.status_code}]") raise UrlResourcesNotFoundError(url) async with aiofiles.open(file_dir, mode='wb') as f: await f.write(data.content) - Log.debug(f"url_to_file 获取url[{url}] 并下载到 file_dir[{file_dir}]") + logger.debug(f"url_to_file 获取url[{url}] 并下载到 file_dir[{file_dir}]") return prefix + file_dir -async def get_genshin_client(user_id: int, user_service: UserService, cookies_service: CookiesService, - region: Optional[RegionEnum] = None) -> Client: +async def get_genshin_client(user_id: int, region: Optional[RegionEnum] = None) -> Client: + if user_service is None: + raise ServiceNotFoundError(UserService) + if cookies_service is None: + raise ServiceNotFoundError(CookiesService) user = await user_service.get_user_by_id(user_id) if region is None: region = user.region @@ -83,6 +94,26 @@ async def get_genshin_client(user_id: int, user_service: UserService, cookies_se return client +async def get_public_genshin_client(user_id: int) -> Tuple[Client, Optional[int]]: + if user_service is None: + raise ServiceNotFoundError(UserService) + if public_cookies_service is None: + raise ServiceNotFoundError(PublicCookiesService) + user = await user_service.get_user_by_id(user_id) + region = user.region + cookies = await public_cookies_service.get_cookies(user_id, region) + if region == RegionEnum.HYPERION: + uid = user.yuanshen_uid + client = genshin.Client(cookies=cookies.cookies, game=types.Game.GENSHIN, region=types.Region.CHINESE) + elif region == RegionEnum.HOYOLAB: + uid = user.genshin_uid + client = genshin.Client(cookies=cookies.cookies, + game=types.Game.GENSHIN, region=types.Region.OVERSEAS, lang="zh-cn") + else: + raise TypeError("region is not RegionEnum.NULL") + return client, uid + + def region_server(uid: Union[int, str]) -> RegionEnum: if isinstance(uid, int): region = REGION_MAP.get(str(uid)[0]) @@ -94,4 +125,3 @@ def region_server(uid: Union[int, str]) -> RegionEnum: return region else: raise TypeError(f"UID {uid} isn't associated with any region") - diff --git a/utils/job/manager.py b/utils/job/manager.py deleted file mode 100644 index c892684..0000000 --- a/utils/job/manager.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import List - -from telegram.ext import Application - -from logger import Log -from utils.manager import ModulesManager - -JobsClass: List[object] = [] - - -def listener_jobs_class(): - """监听JOB - :return: None - """ - - def decorator(func: object): - JobsClass.append(func) - return func - - return decorator - - -class JobsManager(ModulesManager): - def __init__(self): - super().__init__() - self.job_list: List[str] = [] # 用于存储文件名称 - self.exclude_list: List[str] = [] - self.manager_name = "定时任务管理器" - - @staticmethod - def add_handler(application: Application): - for func in JobsClass: - if callable(func): - try: - func.build_jobs(application.job_queue) - # Log.info(f"添加每日Job成功 Job名称[{handler.name}] Job每日执行时间[{handler.time.isoformat()}]") - except AttributeError as exc: - if "build_jobs" in str(exc): - Log.error("build_jobs 函数未找到", exc) - Log.error("初始化Class失败", exc) - except BaseException as exc: - Log.error("初始化Class失败", exc) - finally: - pass diff --git a/utils/job/register.py b/utils/job/register.py deleted file mode 100644 index a955845..0000000 --- a/utils/job/register.py +++ /dev/null @@ -1,20 +0,0 @@ -from telegram.ext import Application - -from logger import Log -from utils.job.manager import JobsManager - - -def register_job(application: Application): - Log.info("正在加载Job管理器") - jobs_manager = JobsManager() - - jobs_manager.refresh_list("jobs/*") - - # 忽略内置模块 - jobs_manager.add_exclude(["base"]) - - Log.info("Job管理器正在加载插件") - jobs_manager.import_module() - jobs_manager.add_handler(application) - - Log.info("Job加载成功") diff --git a/utils/log/__init__.py b/utils/log/__init__.py new file mode 100644 index 0000000..1ae7531 --- /dev/null +++ b/utils/log/__init__.py @@ -0,0 +1 @@ +from utils.log._logger import * diff --git a/utils/log/_file.py b/utils/log/_file.py new file mode 100644 index 0000000..68a7ea6 --- /dev/null +++ b/utils/log/_file.py @@ -0,0 +1,99 @@ +import os +from datetime import date +from pathlib import Path +from types import TracebackType +from typing import AnyStr, IO, Iterable, Iterator, List, Optional, Type + +__all__ = ['FileIO'] + + +# noinspection SpellCheckingInspection +class FileIO(IO[str]): + def __init__(self, path: Path): + self.path = path.parent.resolve() + self.file = path + self.file_stream: Optional[IO[str]] = None + + def _get_file(self) -> IO[str]: + today = date.today() + if self.file.exists(): + if not self.file.is_file(): + raise RuntimeError(f'日志文件冲突, 请删除文件夹 \"{str(self.file.resolve())}\"') + if self.file_stream is None or self.file_stream.closed: + self.file_stream = self.file.open(mode='a+', encoding='utf-8') + modify_date = date.fromtimestamp(os.stat(self.file).st_mtime) + else: + self.file_stream = self.file.open(mode='a+', encoding='utf-8') + modify_date = today + if modify_date < today: + if self.file_stream is not None and not self.file_stream.closed: + self.file_stream.close() + log_path = self.path.joinpath(f'{modify_date.strftime("%Y-%m-%d")}.log') + if log_path.exists(): + # 转存日志 + with open(log_path, mode='a+', encoding='utf-8') as file: + file.write('\n') + with open(self.file, mode='r+', encoding='utf-8') as f: + file.writelines(f.readlines()) + else: + self.file.rename(self.path.joinpath(f'{modify_date.strftime("%Y-%m-%d")}.log')) + self.file_stream = self.file.open(mode='a+', encoding='utf-8') + return self.file_stream + + def close(self) -> None: + return self._get_file().close() + + def fileno(self) -> int: + return self._get_file().fileno() + + def flush(self) -> None: + return self._get_file().flush() + + def isatty(self) -> bool: + return self._get_file().isatty() + + def read(self, __n: int = -1) -> AnyStr: + return self._get_file().read(__n) + + def readable(self) -> bool: + return self._get_file().readable() + + def readline(self, __limit: int = ...) -> AnyStr: + return self._get_file().readline() + + def readlines(self, __hint: int = ...) -> List[AnyStr]: + return self._get_file().readlines() + + def seek(self, __offset: int, __whence: int = 0) -> int: + return self._get_file().seek(__offset, __whence) + + def seekable(self) -> bool: + return self._get_file().seekable() + + def tell(self) -> int: + return self._get_file().tell() + + def truncate(self, __size: Optional[int] = None) -> int: + return self._get_file().truncate(__size) + + def writable(self) -> bool: + return self._get_file().writable() + + def write(self, __s: AnyStr) -> int: + return self._get_file().write(__s) + + def writelines(self, __lines: Iterable[AnyStr]) -> None: + return self._get_file().writelines(__lines) + + def __next__(self) -> AnyStr: + return self._get_file().__next__() + + def __iter__(self) -> Iterator[AnyStr]: + return self._get_file().__iter__() + + def __enter__(self) -> IO[AnyStr]: + return self._get_file().__enter__() + + def __exit__(self, __t: Optional[Type[BaseException]], __value: Optional[BaseException], + __traceback: Optional[TracebackType]) -> None: + return self._get_file().__exit__(__t, __value, __traceback) diff --git a/utils/log/_logger.py b/utils/log/_logger.py new file mode 100644 index 0000000..a5eee7a --- /dev/null +++ b/utils/log/_logger.py @@ -0,0 +1,503 @@ +import logging +import os +import sys +from datetime import datetime +from multiprocessing import RLock as Lock +from pathlib import Path +from typing import (Any, Callable, Dict, Iterable, List, Literal, Mapping, Optional, TYPE_CHECKING, Union) + +import ujson as json +from rich.columns import Columns +from rich.console import ( + Console, + RenderResult, + group, +) +from rich.logging import ( + LogRender as DefaultLogRender, + RichHandler as DefaultRichHandler, +) +from rich.syntax import ( + PygmentsSyntaxTheme, + Syntax, +) +from rich.text import ( + Text, + TextType, +) +from rich.theme import Theme +from rich.traceback import ( + Stack, + Traceback as BaseTraceback, +) +from ujson import JSONDecodeError + +from core.config import BotConfig +from utils.const import NOT_SET, PROJECT_ROOT +from utils.log._file import FileIO +from utils.log._style import ( + DEFAULT_STYLE, + MonokaiProStyle, +) +from utils.typedefs import ExceptionInfoType + +if TYPE_CHECKING: + from rich.table import Table # pylint: disable=unused-import + from rich.console import ( # pylint: disable=unused-import + ConsoleRenderable, + RenderableType, + ) + from rich.console import ( # pylint: disable=unused-import + ConsoleRenderable, + RenderableType, + ) + from rich.table import Table # pylint: disable=unused-import + from logging import LogRecord # pylint: disable=unused-import + +__all__ = ["logger"] + +_lock = Lock() +__initialized__ = False + +FormatTimeCallable = Callable[[datetime], Text] + +config = BotConfig() +logging.addLevelName(5, 'TRACE') +logging.addLevelName(25, 'SUCCESS') +color_system: Literal['windows', 'truecolor'] +if sys.platform == 'win32': + color_system = 'windows' +else: + color_system = 'truecolor' +# noinspection SpellCheckingInspection +log_console = Console( + color_system=color_system, theme=Theme(DEFAULT_STYLE), + width=180 if "PYCHARM_HOSTED" in os.environ else None # 针对 Pycharm +) + + +class Traceback(BaseTraceback): + def __init__(self, *args, **kwargs): + kwargs.update({'show_locals': True, 'max_frames': 10}) + super(Traceback, self).__init__(*args, **kwargs) + self.theme = PygmentsSyntaxTheme(MonokaiProStyle) + + @group() + def _render_stack(self, stack: Stack) -> RenderResult: + from rich.traceback import ( + PathHighlighter, + Frame, + ) + + path_highlighter = PathHighlighter() + theme = self.theme + code_cache: Dict[str, str] = {} + + # noinspection PyShadowingNames + def read_code(filename: str) -> str: + code = code_cache.get(filename) + if code is None: + with open( + filename, "rt", encoding="utf-8", errors="replace" + ) as code_file: + code = code_file.read() + code_cache[filename] = code + return code + + # noinspection PyShadowingNames + def render_locals(frame: Frame) -> Iterable["ConsoleRenderable"]: + if frame.locals: + from rich.scope import render_scope + yield render_scope( + frame.locals, + title="locals", + indent_guides=self.indent_guides, + max_length=self.locals_max_length, + max_string=self.locals_max_string, + ) + + exclude_frames: Optional[range] = None + if self.max_frames != 0: + exclude_frames = range( + self.max_frames // 2, + len(stack.frames) - self.max_frames // 2, + ) + + excluded = False + for frame_index, frame in enumerate(stack.frames): + + if exclude_frames and frame_index in exclude_frames: + excluded = True + continue + + if excluded: + if exclude_frames is None: + raise ValueError(exclude_frames) + yield Text( + f"\n... {len(exclude_frames)} frames hidden ...", + justify="center", + style="traceback.error", + ) + excluded = False + + first = frame_index == 0 + frame_filename = frame.filename + suppressed = any( + frame_filename.startswith(path) for path in self.suppress) + + text = Text.assemble( + path_highlighter(Text(frame.filename, style="pygments.string")), + (":", "pygments.text"), + (str(frame.lineno), "pygments.number"), + " in ", + (frame.name, "pygments.function"), + style="pygments.text", + ) + if not frame.filename.startswith("<") and not first: + yield "" + yield text + if frame.filename.startswith("<"): + yield from render_locals(frame) + continue + if not suppressed: + try: + if self.width is not None: + code_width = self.width - 5 + else: + code_width = 100 + code = read_code(frame.filename) + lexer_name = self._guess_lexer(frame.filename, code) + syntax = Syntax( + code, + lexer_name, + theme=theme, + line_numbers=True, + line_range=( + frame.lineno - self.extra_lines, + frame.lineno + self.extra_lines, + ), + highlight_lines={frame.lineno}, + word_wrap=self.word_wrap, + code_width=code_width, + indent_guides=self.indent_guides, + dedent=False, + ) + yield "" + except Exception as error: # pylint: disable=W0703 + yield Text.assemble( + (f"\n{error}", "traceback.error"), + ) + else: + yield ( + Columns( + [ + syntax, + *render_locals(frame), + ], + padding=1, + ) + if frame.locals + else syntax + ) + + +class LogRender(DefaultLogRender): + @property + def last_time(self): + return self._last_time + + @last_time.setter + def last_time(self, last_time): + self._last_time = last_time + + def __init__(self, *args, **kwargs): + super(LogRender, self).__init__(*args, **kwargs) + self.show_level = True + self.time_format = "[%Y-%m-%d %X]" + + def __call__( + self, + console: "Console", + renderables: Iterable["ConsoleRenderable"], + log_time: Optional[datetime] = None, + time_format: Optional[Union[str, FormatTimeCallable]] = None, + level: TextType = "", + path: Optional[str] = None, + line_no: Optional[int] = None, + link_path: Optional[str] = None, + ) -> "Table": + from rich.containers import Renderables + from rich.table import Table + + output = Table.grid(padding=(0, 1)) + output.expand = True + output.add_column(style="log.time") + output.add_column(style="log.level", width=self.level_width) + output.add_column(ratio=1, style="log.message", overflow="fold") + if path: + output.add_column(style="log.path") + if line_no: + output.add_column(style='log.line_no', width=4) + row: List["RenderableType"] = [] + if self.show_time: + log_time = log_time or log_console.get_datetime() + time_format = time_format or self.time_format + if callable(time_format): + log_time_display = time_format(log_time) + else: + log_time_display = Text(log_time.strftime(time_format)) + if log_time_display == self.last_time and self.omit_repeated_times: + row.append(Text(" " * len(log_time_display))) + else: + row.append(log_time_display) + self.last_time = log_time_display + if self.show_level: + row.append(level) + + row.append(Renderables(renderables)) + if path: + path_text = Text() + path_text.append( + path, style=f"link file://{link_path}" if link_path else "" + ) + row.append(path_text) + + line_no_text = Text() + line_no_text.append( + str(line_no), style=f"link file://{link_path}#{line_no}" if link_path else "" + ) + row.append(line_no_text) + + output.add_row(*row) + return output + + +class Handler(DefaultRichHandler): + def __init__(self, *args, **kwargs): + super(Handler, self).__init__(*args, **kwargs) + self._log_render = LogRender() + self.console = log_console + self.rich_tracebacks = True + self.tracebacks_show_locals = True + self.markup = True + self.keywords = [*self.KEYWORDS, 'BOT'] + + def render( + self, + *, + record: "LogRecord", + traceback: Optional[Traceback], + message_renderable: Optional["ConsoleRenderable"], + ) -> "ConsoleRenderable": + if record.pathname != '': + try: + path = str(Path(record.pathname).relative_to(PROJECT_ROOT)) + path = path.split('.')[0].replace(os.sep, '.') + except ValueError: + import site + path = None + for s in site.getsitepackages(): + try: + path = str(Path(record.pathname).relative_to(Path(s))) + break + except ValueError: + continue + if path is None: + path = "" + else: + path = path.split('.')[0].replace(os.sep, '.') + else: + path = '' + path = path.replace('lib.site-packages.', '') + _level = self.get_level_text(record) + time_format = None if self.formatter is None else self.formatter.datefmt + log_time = datetime.fromtimestamp(record.created) + + log_renderable = self._log_render( + self.console, + ( + [message_renderable] + if not traceback else + ( + [message_renderable, traceback] + if message_renderable is not None else + [traceback] + ) + ), + log_time=log_time, + time_format=time_format, + level=_level, + path=path, + link_path=record.pathname if self.enable_link_path else None, + line_no=record.lineno, + ) + return log_renderable + + def render_message( + self, record: "LogRecord", message: Any + ) -> "ConsoleRenderable": + use_markup = getattr(record, "markup", self.markup) + if isinstance(message, str): + message_text = ( + Text.from_markup(message) if use_markup else Text(message) + ) + highlighter = getattr(record, "highlighter", self.highlighter) + else: + from rich.highlighter import JSONHighlighter + from rich.json import JSON + highlighter = JSONHighlighter() + message_text = JSON.from_data(message, indent=4).text + + if highlighter is not None: + # noinspection PyCallingNonCallable + message_text = highlighter(message_text) + + if self.keywords is None: + self.keywords = self.KEYWORDS + + if self.keywords: + message_text.highlight_words(self.keywords, "logging.keyword") + + return message_text + + def emit(self, record: "LogRecord") -> None: + message = self.format(record) + traceback = None + if ( + self.rich_tracebacks + and record.exc_info + and record.exc_info != (None, None, None) + ): + exc_type, exc_value, exc_traceback = record.exc_info + if exc_type is None or exc_value is None: + raise ValueError(record) + traceback = Traceback.from_exception( + exc_type, + exc_value, + exc_traceback, + width=self.tracebacks_width, + extra_lines=self.tracebacks_extra_lines, + word_wrap=self.tracebacks_word_wrap, + show_locals=self.tracebacks_show_locals, + locals_max_length=self.locals_max_length, + locals_max_string=self.locals_max_string, + suppress=self.tracebacks_suppress, + ) + message = record.getMessage() + if self.formatter: + record.message = record.getMessage() + formatter = self.formatter + if hasattr(formatter, "usesTime") and formatter.usesTime(): + record.asctime = formatter.formatTime(record, formatter.datefmt) + message = formatter.formatMessage(record) + if message == str(exc_value): + message = None + + if message is not None: + try: + message_renderable = self.render_message(record, json.loads(message)) + except JSONDecodeError: + message_renderable = self.render_message(record, message) + else: + message_renderable = None + log_renderable = self.render(record=record, traceback=traceback, message_renderable=message_renderable) + # noinspection PyBroadException + try: + self.console.print(log_renderable) + except Exception: # pylint: disable=W0703 + self.handleError(record) + + +class DebugFileHandler(DefaultRichHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.level = 10 + path = PROJECT_ROOT.joinpath("logs/debug/debug.log") + while True: + try: + path.parent.mkdir(exist_ok=True) + break + except FileNotFoundError: + parent = path.parent + while True: + try: + parent.mkdir(exist_ok=True) + break + except FileNotFoundError: + parent = parent.parent + self.console = Console(color_system='auto', width=200, file=FileIO(path)) + + +class ErrorFileHandler(DefaultRichHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.level = 40 + path = PROJECT_ROOT.joinpath("logs/error/error.log") + while True: + try: + path.parent.mkdir(exist_ok=True) + break + except FileNotFoundError: + parent = path.parent + while True: + try: + parent.mkdir(exist_ok=True) + break + except FileNotFoundError: + parent = parent.parent + path.parent.mkdir(exist_ok=True) + self.console = Console(color_system='auto', width=200, file=FileIO(path)) + + +class Logger(logging.Logger): + def success( + self, + msg: Any, *args: Any, + exc_info: Optional[ExceptionInfoType] = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Optional[Mapping[str, Any]] = None + ) -> None: + return self.log(25, msg, *args, exc_info=exc_info, stack_info=stack_info, stacklevel=stacklevel, extra=extra) + + def exception( + self, + *args: Any, + msg: Any = NOT_SET, + exc_info: Optional[ExceptionInfoType] = True, + stack_info: bool = False, + stacklevel: int = 1, + extra: Optional[Mapping[str, Any]] = None, + **kwargs + ) -> None: + if args and msg is NOT_SET: + msg = args[0] + args = args[1:] + super(Logger, self).exception( + "" if msg is NOT_SET else msg, *args, + exc_info=exc_info, stack_info=stack_info, stacklevel=stacklevel, extra=extra + ) + + +with _lock: + if not __initialized__: + logging.captureWarnings(True) + handler, debug_handler, error_handler = Handler(), DebugFileHandler(), ErrorFileHandler() + + level_ = 10 if config.debug else 20 + logging.basicConfig( + level=10 if config.debug else 20, + format="%(message)s", + datefmt="[%Y-%m-%d %X]", + handlers=[handler, debug_handler, error_handler] + ) + warnings_logger = logging.getLogger("py.warnings") + warnings_logger.addHandler(handler) + warnings_logger.addHandler(debug_handler) + + logger = Logger('TGPaimon', level_) + logger.addHandler(handler) + logger.addHandler(debug_handler) + logger.addHandler(error_handler) + + __initialized__ = True diff --git a/utils/log/_style.py b/utils/log/_style.py new file mode 100644 index 0000000..05e3a75 --- /dev/null +++ b/utils/log/_style.py @@ -0,0 +1,282 @@ +from typing import Dict + +from pygments.style import Style as PyStyle +from pygments.token import ( + Comment, + Error, + Generic, + Keyword, + Literal, + Name, + Number, + Operator, + Punctuation, + String, + Text, +) +from rich.style import Style + +__all__ = [ + 'MonokaiProStyle', 'DEFAULT_STYLE', + 'BACKGROUND', 'FOREGROUND', + 'BLACK', 'DARK_GREY', 'LIGHT_GREY', 'GREY', 'RED', 'MAGENTA', 'GREEN', + 'YELLOW', 'ORANGE', 'PURPLE', 'BLUE', 'CYAN', 'WHITE' +] + +BACKGROUND = "#272822" +FOREGROUND = "#f8f8f2" + +BLACK = "#1A1A1A" +DARK_GREY = "#363537" +LIGHT_GREY = "#69676c" +GREY = "#595959" +RED = "#ff6188" +MAGENTA = "#FC61D3" +GREEN = "#7bd88f" +YELLOW = "#ffd866" +ORANGE = "#fc9867" +PURPLE = "#ab9df2" +BLUE = "#81a1c1" +CYAN = "#78dce8" +WHITE = "#e5e9f0" + + +class MonokaiProStyle(PyStyle): + background_color = DARK_GREY + highlight_color = "#49483e" + + styles = { + # No corresponding class for the following: + Text: WHITE, # class: '' + Error: "#fc618d bg:#1e0010", # class: 'err' + + Comment: LIGHT_GREY, # class: 'c' + Comment.Multiline: YELLOW, # class: 'cm' + + Keyword: RED, # class: 'k' + Keyword.Namespace: GREEN, # class: 'kn' + + Operator: RED, # class: 'o' + + Punctuation: WHITE, # class: 'p' + + Name: WHITE, # class: 'n' + Name.Attribute: GREEN, # class: 'na' - to be revised + Name.Builtin: CYAN, # class: 'nb' + Name.Builtin.Pseudo: ORANGE, # class: 'bp' + Name.Class: GREEN, # class: 'nc' - to be revised + Name.Decorator: PURPLE, # class: 'nd' - to be revised + Name.Exception: GREEN, # class: 'ne' + Name.Function: GREEN, # class: 'nf' + Name.Property: ORANGE, # class: 'py' + + Number: PURPLE, # class: 'm' + + Literal: PURPLE, # class: 'l' + Literal.Date: ORANGE, # class: 'ld' + + String: YELLOW, # class: 's' + String.Regex: ORANGE, # class: 'sr' + + Generic.Deleted: YELLOW, # class: 'gd', + Generic.Emph: "italic", # class: 'ge' + Generic.Inserted: GREEN, # class: 'gi' + Generic.Strong: "bold", # class: 'gs' + Generic.Subheading: LIGHT_GREY, # class: 'gu' + } + + +DEFAULT_STYLE: Dict[str, Style] = { + # base + "none": Style.null(), + "reset": Style( + color=FOREGROUND, + bgcolor=BACKGROUND, + dim=False, + bold=False, + italic=False, + underline=False, + blink=False, + blink2=False, + reverse=False, + conceal=False, + strike=False, + ), + "dim": Style(dim=True), + "bright": Style(dim=False), + "bold": Style(bold=True), + "strong": Style(bold=True), + "code": Style(reverse=True, bold=True), + "italic": Style(italic=True), + "emphasize": Style(italic=True), + "underline": Style(underline=True), + "blink": Style(blink=True), + "blink2": Style(blink2=True), + "reverse": Style(reverse=True), + "strike": Style(strike=True), + "black": Style(color=BLACK), + "red": Style(color=RED), + "green": Style(color=GREEN), + "yellow": Style(color=YELLOW), + "magenta": Style(color=MAGENTA), + "blue": Style(color=BLUE), + "cyan": Style(color=CYAN), + "white": Style(color=WHITE), + + # inspect + "inspect.attr": Style(color=YELLOW, italic=True), + "inspect.attr.dunder": Style(color=YELLOW, italic=True, dim=True), + "inspect.callable": Style(bold=True, color=RED), + "inspect.def": Style(italic=True, color="bright_cyan"), + "inspect.class": Style(italic=True, color="bright_cyan"), + "inspect.error": Style(bold=True, color=RED), + "inspect.equals": Style(), + "inspect.help": Style(color=CYAN), + "inspect.doc": Style(dim=True), + "inspect.value.border": Style(color=GREEN), + + # live + "live.ellipsis": Style(bold=True, color=RED), + + # layout + "layout.tree.row": Style(dim=False, color=RED), + "layout.tree.column": Style(dim=False, color=BLUE), + + # log + "logging.keyword": Style(bold=True, color=ORANGE), + "logging.level.notset": Style(color=DARK_GREY, dim=True), + "logging.level.trace": Style(color=GREY), + "logging.level.debug": Style(color=LIGHT_GREY, bold=True), + "logging.level.info": Style(color='white'), + "logging.level.plugin": Style(color='cyan'), + "logging.level.success": Style(color='green'), + "logging.level.warning": Style(color='yellow'), + "logging.level.error": Style(color='red'), + "logging.level.critical": Style(color='red', bgcolor='#1e0010', bold=True), + "log.level": Style.null(), + "log.time": Style(color=CYAN, dim=True), + "log.message": Style.null(), + "log.path": Style(dim=True), + "log.line_no": Style(color=CYAN, bold=True, italic=False, dim=True), + + # repr + "repr.ellipsis": Style(color=YELLOW), + "repr.indent": Style(color=GREEN, dim=True), + "repr.error": Style(color=RED, bold=True), + "repr.str": Style(color=GREEN, italic=False, bold=False), + "repr.brace": Style(bold=True), + "repr.comma": Style(bold=True), + "repr.ipv4": Style(bold=True, color="bright_green"), + "repr.ipv6": Style(bold=True, color="bright_green"), + "repr.eui48": Style(bold=True, color="bright_green"), + "repr.eui64": Style(bold=True, color="bright_green"), + "repr.tag_start": Style(bold=True), + "repr.tag_name": Style(color="bright_magenta", bold=True), + "repr.tag_contents": Style(color="default"), + "repr.tag_end": Style(bold=True), + "repr.attrib_name": Style(color=YELLOW, italic=False), + "repr.attrib_equal": Style(bold=True), + "repr.attrib_value": Style(color=MAGENTA, italic=False), + "repr.number": Style(color=CYAN, bold=True, italic=False), + "repr.number_complex": Style(color=CYAN, bold=True, italic=False), # same + "repr.bool_true": Style(color="bright_green", italic=True), + "repr.bool_false": Style(color="bright_red", italic=True), + "repr.none": Style(color=MAGENTA, italic=True), + "repr.url": Style( + underline=True, color="bright_blue", italic=False, bold=False + ), + "repr.uuid": Style(color="bright_yellow", bold=False), + "repr.call": Style(color=MAGENTA, bold=True), + "repr.path": Style(color=MAGENTA), + "repr.filename": Style(color="bright_magenta"), + "rule.line": Style(color="bright_green"), + "rule.text": Style.null(), + + # json + "json.brace": Style(bold=True), + "json.bool_true": Style(color="bright_green", italic=True), + "json.bool_false": Style(color="bright_red", italic=True), + "json.null": Style(color=MAGENTA, italic=True), + "json.number": Style(color=CYAN, bold=True, italic=False), + "json.str": Style(color=GREEN, italic=False, bold=False), + "json.key": Style(color=BLUE, bold=True), + + # prompt + "prompt": Style.null(), + "prompt.choices": Style(color=MAGENTA, bold=True), + "prompt.default": Style(color=CYAN, bold=True), + "prompt.invalid": Style(color=RED), + "prompt.invalid.choice": Style(color=RED), + + # pretty + "pretty": Style.null(), + + # scope + "scope.border": Style(color=BLUE), + "scope.key": Style(color=YELLOW, italic=True), + "scope.key.special": Style(color=YELLOW, italic=True, dim=True), + "scope.equals": Style(color=RED), + + # table + "table.header": Style(bold=True), + "table.footer": Style(bold=True), + "table.cell": Style.null(), + "table.title": Style(italic=True), + "table.caption": Style(italic=True, dim=True), + + # traceback + "traceback.error": Style(color=RED, italic=True), + "traceback.border.syntax_error": Style(color="bright_red"), + "traceback.border": Style(color=RED), + "traceback.text": Style.null(), + "traceback.title": Style(color=RED, bold=True), + "traceback.exc_type": Style(color="bright_red", bold=True), + "traceback.exc_value": Style.null(), + "traceback.offset": Style(color="bright_red", bold=True), + + # bar + "bar.back": Style(color="grey23"), + "bar.complete": Style(color="rgb(249,38,114)"), + "bar.finished": Style(color="rgb(114,156,31)"), + "bar.pulse": Style(color="rgb(249,38,114)"), + + # progress + "progress.description": Style.null(), + "progress.filesize": Style(color=GREEN), + "progress.filesize.total": Style(color=GREEN), + "progress.download": Style(color=GREEN), + "progress.elapsed": Style(color=YELLOW), + "progress.percentage": Style(color=MAGENTA), + "progress.remaining": Style(color=CYAN), + "progress.data.speed": Style(color=RED), + "progress.spinner": Style(color=GREEN), + "status.spinner": Style(color=GREEN), + + # tree + "tree": Style(), + "tree.line": Style(), + + # markdown + "markdown.paragraph": Style(), + "markdown.text": Style(), + "markdown.emph": Style(italic=True), + "markdown.strong": Style(bold=True), + "markdown.code": Style(bgcolor=BLACK, color="bright_white"), + "markdown.code_block": Style(dim=True, color=CYAN, bgcolor=BLACK), + "markdown.block_quote": Style(color=MAGENTA), + "markdown.list": Style(color=CYAN), + "markdown.item": Style(), + "markdown.item.bullet": Style(color=YELLOW, bold=True), + "markdown.item.number": Style(color=YELLOW, bold=True), + "markdown.hr": Style(color=YELLOW), + "markdown.h1.border": Style(), + "markdown.h1": Style(bold=True), + "markdown.h2": Style(bold=True, underline=True), + "markdown.h3": Style(bold=True), + "markdown.h4": Style(bold=True, dim=True), + "markdown.h5": Style(underline=True), + "markdown.h6": Style(italic=True), + "markdown.h7": Style(italic=True, dim=True), + "markdown.link": Style(color="bright_blue"), + "markdown.link_url": Style(color=BLUE), +} diff --git a/utils/manager.py b/utils/manager.py deleted file mode 100644 index 28afc0d..0000000 --- a/utils/manager.py +++ /dev/null @@ -1,55 +0,0 @@ -import os -from glob import glob -from importlib import import_module -from os import path -from typing import List, Union - -from logger import Log -from models.base import ModuleInfo - - -class ModulesManager: - def __init__(self): - self.manager_name: str = "模块管理器" - self.modules_list: List[ModuleInfo] = [] # 用于存储文件名称 - self.exclude_list: List[str] = [] - - def clear(self): - self.modules_list.clear() - - def refresh_list(self, pathname: str): - path_list = [i.replace(os.sep, "/") for i in glob(pathname)] - for temp_path in path_list: - if "__" in temp_path: - continue - if os.path.isdir(temp_path): - self.modules_list.append(ModuleInfo(relative_path=temp_path)) - else: - module_name = path.basename(path.normpath(temp_path)) - root, ext = os.path.splitext(module_name) - if ext == ".py": - self.modules_list.append(ModuleInfo(relative_path=temp_path)) - - def add_exclude(self, exclude: Union[str, List[str]]): - if isinstance(exclude, str): - self.exclude_list.append(exclude) - elif isinstance(exclude, list): - self.exclude_list.extend(exclude) - else: - raise TypeError - - def import_module(self): - module_name_list: List[str] = [] - for module_info in self.modules_list: - if module_info.module_name not in self.exclude_list: - try: - import_module(f"{module_info.package_path}") - except ImportError as exc: - Log.warning(f"{self.manager_name}加载 {module_info} 失败", exc) - except ImportWarning as exc: - Log.warning(f"{self.manager_name}加载 {module_info} 成功但有警告", exc) - except BaseException as exc: - Log.warning(f"{self.manager_name}加载 {module_info} 失败", exc) - else: - module_name_list.append(module_info.module_name) - Log.info(f"{self.manager_name}加载模块: " + ", ".join(module_name_list)) diff --git a/models/base.py b/utils/models/base.py similarity index 98% rename from models/base.py rename to utils/models/base.py index 635af3d..413a6a4 100644 --- a/models/base.py +++ b/utils/models/base.py @@ -3,7 +3,7 @@ import os from enum import Enum from typing import Union, Optional -from models.baseobject import BaseObject +from utils.baseobject import BaseObject class Stat: diff --git a/utils/mysql.py b/utils/mysql.py deleted file mode 100644 index ddfef2b..0000000 --- a/utils/mysql.py +++ /dev/null @@ -1,30 +0,0 @@ -from sqlalchemy.ext.asyncio import create_async_engine -from sqlalchemy.orm import sessionmaker -from sqlmodel.ext.asyncio.session import AsyncSession - -from logger import Log - - -class MySQL: - def __init__(self, host: str = "127.0.0.1", port: int = 3306, user: str = "root", - password: str = "", database: str = ""): - self.database = database - self.password = password - self.user = user - self.port = port - self.host = host - Log.debug(f'获取数据库配置 [host]: {self.host}') - Log.debug(f'获取数据库配置 [port]: {self.port}') - Log.debug(f'获取数据库配置 [user]: {self.user}') - Log.debug(f'获取数据库配置 [password][len]: {len(self.password)}') - Log.debug(f'获取数据库配置 [db]: {self.database}') - self.engine = create_async_engine(f"mysql+asyncmy://{user}:{password}@{host}:{port}/{database}") - self.Session = sessionmaker(bind=self.engine, class_=AsyncSession) - - async def get_session(self): - """获取会话""" - async with self.Session() as session: - yield session - - async def wait_closed(self): - pass diff --git a/utils/plugins/manager.py b/utils/plugins/manager.py deleted file mode 100644 index c8f3052..0000000 --- a/utils/plugins/manager.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import List - -from telegram.ext import Application - -from logger import Log -from utils.manager import ModulesManager - -PluginsClass: List[object] = [] - - -def listener_plugins_class(): - """监听插件 - :return: None - """ - - def decorator(func: object): - PluginsClass.append(func) - return func - - return decorator - - -class PluginsManager(ModulesManager): - - def __init__(self): - super().__init__() - self.manager_name = "插件管理器" - - @staticmethod - def add_handler(application: Application): - for func in PluginsClass: - if callable(func): - try: - handlers_list = func.create_handlers() - for handler in handlers_list: - application.add_handler(handler) - except AttributeError as exc: - if "create_handlers" in str(exc): - Log.error("创建 handlers 函数未找到", exc) - Log.error("初始化Class失败", exc) - except BaseException as exc: - Log.error("初始化Class失败", exc) - finally: - pass diff --git a/utils/plugins/register.py b/utils/plugins/register.py deleted file mode 100644 index 3c73865..0000000 --- a/utils/plugins/register.py +++ /dev/null @@ -1,71 +0,0 @@ -from importlib import import_module -from typing import Optional - -from telegram.ext import CommandHandler, MessageHandler, filters, CallbackQueryHandler, Application, InlineQueryHandler - -from config import config -from logger import Log -from plugins.base import NewChatMembersHandler -from plugins.system.auth import GroupJoiningVerification -from plugins.system.errorhandler import error_handler -from plugins.system.inline import Inline -from plugins.system.start import start, ping, reply_keyboard_remove, unknown_command -from utils.plugins.manager import PluginsManager - - -def register_plugin_handlers(application: Application): - """ - 注册插件相关处理程序 - :param application: - :param service: - :return: - """ - - # 添加相关命令处理过程 - def add_handler(handler, command: Optional[str] = None, regex: Optional[str] = None, query: Optional[str] = None, - block: bool = False) -> None: - if command: - application.add_handler(CommandHandler(command, handler, block=block)) - if regex: - application.add_handler(MessageHandler(filters.Regex(regex), handler, block=block)) - if query: - application.add_handler(CallbackQueryHandler(handler, pattern=query, block=block)) - - # 初始化 - Log.info("正在加载动态插件管理器") - plugins_manager = PluginsManager() - plugins_manager.add_exclude(["start", "auth", "inline", "errorhandler"]) # 忽略内置模块 - - Log.info("正在加载插件") - plugins_manager.refresh_list("plugins/genshin/*") - plugins_manager.refresh_list("plugins/system/*") - - Log.info("加载插件管理器正在加载插件") - plugins_manager.import_module() - plugins_manager.add_handler(application) - - Log.info("正在加载静态系统插件") - inline = Inline() - new_chat_members_handler = NewChatMembersHandler() - - if len(config.joining_verification_groups) >= 1: - auth = GroupJoiningVerification() - for chat_id in config.joining_verification_groups: - new_chat_members_handler.add_callback(auth.new_mem, chat_id) - add_handler(auth.query, query=r"^auth_challenge\|") - add_handler(auth.admin, query=r"^auth_admin\|") - - add_handler(start, command="start") - add_handler(ping, command="ping") - # 调试功能 - add_handler(reply_keyboard_remove, command="reply_keyboard_remove") - - application.add_handler(MessageHandler(filters.StatusUpdate.NEW_CHAT_MEMBERS, - new_chat_members_handler.new_member, block=False)) - application.add_handler(InlineQueryHandler(inline.inline_query, block=False)) - application.add_handler(MessageHandler(filters.COMMAND & filters.ChatType.PRIVATE, unknown_command)) - application.add_error_handler(error_handler, block=False) - - import_module("plugins.system.admin") - - Log.info("插件加载成功") diff --git a/utils/random.py b/utils/random.py index 0e11fb3..db36ce4 100644 --- a/utils/random.py +++ b/utils/random.py @@ -3,9 +3,8 @@ import time from numpy.random import Generator, MT19937 -class MT19937_Random: - """ - 基于 numpy 实现的动态删除时间设计 +class MT19937Random: + """基于 numpy 实现的动态删除时间设计 ——MXtao_dada | 小男孩赛高! 笑死,不然你猜猜为啥 requirements.txt 有 numpy ? ——洛水居室 @@ -14,9 +13,7 @@ class MT19937_Random: """ def __init__(self): - """ - 创建随机数生成器 - """ + """创建随机数生成器""" self.send_time = time.time() self.generator = Generator(MT19937(int(self.send_time))) diff --git a/utils/redisdb.py b/utils/redisdb.py deleted file mode 100644 index 4023af9..0000000 --- a/utils/redisdb.py +++ /dev/null @@ -1,39 +0,0 @@ -import asyncio - -import fakeredis.aioredis -from redis import asyncio as aioredis - -from logger import Log - - -class RedisDB: - - def __init__(self, host="127.0.0.1", port=6379, db=0, loop=None): - Log.debug(f'获取Redis配置 [host]: {host}') - Log.debug(f'获取Redis配置 [port]: {port}') - Log.debug(f'获取Redis配置 [db]: {db}') - self.client = aioredis.Redis(host=host, port=port, db=db) - self.ttl = 600 - self.key_prefix = "paimon_bot" - self._loop = loop - if self._loop is None: - self._loop = asyncio.get_event_loop() - try: - Log.info("正在尝试建立与Redis连接") - self._loop.run_until_complete(self.ping()) - except (KeyboardInterrupt, SystemExit): - pass - except BaseException as exc: - Log.warning("尝试连接Redis失败,使用 fakeredis 模拟", exc) - self.client = fakeredis.aioredis.FakeRedis() - self._loop.run_until_complete(self.ping()) - - async def ping(self): - if await self.client.ping(): - Log.info("连接Redis成功") - else: - Log.info("连接Redis失败") - raise RuntimeError("连接Redis失败") - - async def close(self): - await self.client.close() diff --git a/utils/service/inject.py b/utils/service/inject.py deleted file mode 100644 index 98d1ecf..0000000 --- a/utils/service/inject.py +++ /dev/null @@ -1,48 +0,0 @@ -import inspect -from functools import wraps - -from logger import Log -from models.types import Func -from utils.service.manager import ServicesDict - - -def get_injections(func: Func): - injections = {} - try: - signature = inspect.signature(func) - except ValueError as exception: - if "no signature found" in str(exception): - Log.warning("no signature found", exception) - elif "not supported by signature" in str(exception): - Log.warning("not supported by signature", exception) - else: - raise exception - else: - for parameter_name, parameter in signature.parameters.items(): - annotation = parameter.annotation - class_name = annotation.__name__ - param = ServicesDict.get(class_name) - if param is not None: - injections.setdefault(parameter_name, param) - return injections - - -def inject(func: Func) -> Func: - """依赖注入""" - - @wraps(func) - async def async_decorator(*args, **kwargs): - injections = get_injections(func) - kwargs.update(injections) - return await func(*args, **kwargs) - - @wraps(func) - def sync_decorator(*args, **kwargs): - injections = get_injections(func) - kwargs.update(injections) - return func(*args, **kwargs) - - if inspect.iscoroutinefunction(func): - return async_decorator - else: - return sync_decorator diff --git a/utils/service/manager.py b/utils/service/manager.py deleted file mode 100644 index 3bbb9b9..0000000 --- a/utils/service/manager.py +++ /dev/null @@ -1,68 +0,0 @@ -import inspect -from typing import List, Dict - -from logger import Log -from models.types import Func -from utils.aiobrowser import AioBrowser -from utils.manager import ModulesManager -from utils.mysql import MySQL -from utils.redisdb import RedisDB - -ServicesFunctions: List[Func] = [] -ServicesDict: Dict[str, Func] = {} - - -def listener_service(): - """监听服务""" - - def decorator(func: Func): - ServicesFunctions.append( - func - ) - return func - - return decorator - - -class ServicesManager(ModulesManager): - def __init__(self, mysql: MySQL, redis: RedisDB, browser: AioBrowser): - super().__init__() - self.browser = browser - self.redis = redis - self.mysql = mysql - self.services_list: List[str] = [] - self.exclude_list: List[str] = [] - self.manager_name = "核心服务管理器" - - def add_service(self): - for func in ServicesFunctions: - if callable(func): - kwargs = {} - try: - signature = inspect.signature(func) - except ValueError as exception: - if "no signature found" in str(exception): - Log.warning("no signature found", exception) - break - elif "not supported by signature" in str(exception): - Log.warning("not supported by signature", exception) - break - else: - raise exception - else: - for parameter_name, parameter in signature.parameters.items(): - annotation = parameter.annotation - if issubclass(annotation, MySQL): - kwargs[parameter_name] = self.mysql - if issubclass(annotation, RedisDB): - kwargs[parameter_name] = self.redis - if issubclass(annotation, AioBrowser): - kwargs[parameter_name] = self.browser - try: - handlers_list = func(**kwargs) - class_name = handlers_list.__class__.__name__ - ServicesDict.setdefault(class_name, handlers_list) - except BaseException as exc: - Log.error("初始化Service失败", exc) - finally: - pass diff --git a/utils/storage.py b/utils/storage.py deleted file mode 100644 index 38bd5ee..0000000 --- a/utils/storage.py +++ /dev/null @@ -1,39 +0,0 @@ -# Storage is from web.py utils -# https://github.com/webpy/webpy/blob/d69a49eb3c593be21fa4a5275ca9f028245678fd/web/utils.py#L81 - -class Storage(dict): - """ - A Storage object is like a dictionary except `obj.foo` can be used - in addition to `obj['foo']`. - >>> o = storage(a=1) - >>> o.a - 1 - >>> o['a'] - 1 - >>> o.a = 2 - >>> o['a'] - 2 - >>> del o.a - >>> o.a - Traceback (most recent call last): - ... - AttributeError: 'a' - """ - - def __getattr__(self, key): - try: - return self[key] - except KeyError as k: - raise AttributeError(k) - - def __setattr__(self, key, value): - self[key] = value - - def __delattr__(self, key): - try: - del self[key] - except KeyError as k: - raise AttributeError(k) - - def __repr__(self): - return "" diff --git a/utils/typedefs.py b/utils/typedefs.py new file mode 100644 index 0000000..cd4d67f --- /dev/null +++ b/utils/typedefs.py @@ -0,0 +1,13 @@ +from pathlib import Path +from types import TracebackType +from typing import Tuple, Type, Union, Dict, Any + +__all__ = [ + 'StrOrPath', + 'ExceptionInfoType', + 'JSONDict', +] + +StrOrPath = Union[str, Path] +ExceptionInfoType = Union[bool, Tuple[Type[BaseException], BaseException, TracebackType, None], Tuple[None, None, None]] +JSONDict = Dict[str, Any]