♻ 更新V3版本

♻️ 重构插件系统
⚙️ 重写插件
🎨 改进代码结构
📝 完善文档

Co-authored-by: zhxy-CN <admin@owo.cab>
Co-authored-by: 洛水居室 <luoshuijs@outlook.com>
Co-authored-by: xtaodada <xtao@xtaolink.cn>
Co-authored-by: Li Chuangbo <im@chuangbo.li>
This commit is contained in:
Karako 2022-09-08 09:08:37 +08:00 committed by GitHub
parent 86503671ed
commit 8f424bf0d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
142 changed files with 3566 additions and 4734 deletions

4
.gitignore vendored
View File

@ -17,6 +17,7 @@ out/
env/ env/
venv/ venv/
cache/ cache/
temp/
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
@ -35,3 +36,6 @@ logs/
### DotEnv ### ### DotEnv ###
.env .env
### private plugins ###
plugins/private

View File

@ -31,6 +31,7 @@ pip install --upgrade poetry
```bash ```bash
poetry install poetry install
playwright install chromium
``` ```
### 3. 修改配置 ### 3. 修改配置
@ -50,7 +51,7 @@ alembic upgrade head
### 5. 运行 ### 5. 运行
```bash ```bash
python ./main.py python ./run.py
``` ```
## 其他说明 ## 其他说明

View File

@ -1,5 +1,8 @@
import os
import asyncio import asyncio
from importlib import import_module
from logging.config import fileConfig from logging.config import fileConfig
from typing import Iterator
from sqlalchemy import engine_from_config from sqlalchemy import engine_from_config
from sqlalchemy import pool from sqlalchemy import pool
@ -10,6 +13,8 @@ from sqlmodel import SQLModel
from alembic import context from alembic import context
from utils.const import PROJECT_ROOT
from utils.log import logger
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # 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: if config.config_file_name is not None:
fileConfig(config.config_file_name) fileConfig(config.config_file_name)
# register our models for alembic to auto-generate migrations
from utils.manager import ModulesManager
manager = ModulesManager() def scan_models() -> Iterator[str]:
manager.refresh_list("core/*") """扫描所有 models.py 模块。
manager.refresh_list("jobs/*") 我们规定所有插件的 model 都需要放在名为 models.py 的文件里"""
manager.refresh_list("plugins/genshin/*")
manager.refresh_list("plugins/system/*") for path in PROJECT_ROOT.glob("**/models.py"):
manager.import_module() 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 target_metadata = SQLModel.metadata
@ -39,14 +57,14 @@ target_metadata = SQLModel.metadata
# here we allow ourselves to pass interpolation vars to alembic.ini # here we allow ourselves to pass interpolation vars to alembic.ini
# from the application config module # from the application config module
from config import config as appConfig from core.config import config as botConfig
section = config.config_ini_section section = config.config_ini_section
config.set_section_option(section, "DB_HOST", appConfig.mysql["host"]) config.set_section_option(section, "DB_HOST", botConfig.mysql.host)
config.set_section_option(section, "DB_PORT", str(appConfig.mysql["port"])) config.set_section_option(section, "DB_PORT", str(botConfig.mysql.port))
config.set_section_option(section, "DB_USERNAME", appConfig.mysql["user"]) config.set_section_option(section, "DB_USERNAME", botConfig.mysql.username)
config.set_section_option(section, "DB_PASSWORD", appConfig.mysql["password"]) config.set_section_option(section, "DB_PASSWORD", botConfig.mysql.password)
config.set_section_option(section, "DB_DATABASE", appConfig.mysql["database"]) config.set_section_option(section, "DB_DATABASE", botConfig.mysql.database)
def run_migrations_offline() -> None: def run_migrations_offline() -> None:

View File

@ -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)

22
core/README.md Normal file
View File

@ -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"""
```

View File

@ -1,12 +1,12 @@
from utils.mysql import MySQL from core.service import init_service
from utils.redisdb import RedisDB from core.base.mysql import MySQL
from utils.service.manager import listener_service from core.base.redisdb import RedisDB
from .cache import BotAdminCache from core.admin.cache import BotAdminCache
from .repositories import BotAdminRepository from core.admin.repositories import BotAdminRepository
from .services import BotAdminService from core.admin.services import BotAdminService
@listener_service() @init_service
def create_bot_admin_service(mysql: MySQL, redis: RedisDB): def create_bot_admin_service(mysql: MySQL, redis: RedisDB):
_cache = BotAdminCache(redis) _cache = BotAdminCache(redis)
_repository = BotAdminRepository(mysql) _repository = BotAdminRepository(mysql)

View File

@ -1,6 +1,6 @@
from typing import List from typing import List
from utils.redisdb import RedisDB from core.base.redisdb import RedisDB
class BotAdminCache: class BotAdminCache:

View File

@ -3,8 +3,8 @@ from typing import List, cast
from sqlalchemy import select from sqlalchemy import select
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from utils.mysql import MySQL from core.admin.models import Admin
from .models import Admin from core.base.mysql import MySQL
class BotAdminRepository: class BotAdminRepository:

View File

@ -3,10 +3,10 @@ from typing import List
from pymysql import IntegrityError from pymysql import IntegrityError
from telegram import Bot from telegram import Bot
from config import config from core.admin.cache import BotAdminCache, GroupAdminCache
from logger import Log from core.admin.repositories import BotAdminRepository
from .cache import BotAdminCache, GroupAdminCache from core.config import config
from .repositories import BotAdminRepository from utils.log import logger
class BotAdminService: class BotAdminService:
@ -27,7 +27,7 @@ class BotAdminService:
try: try:
await self._repository.add_by_user_id(user_id) await self._repository.add_by_user_id(user_id)
except IntegrityError as error: 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() admin_list = await self._repository.get_all_user_id()
for config_admin in config.admins: for config_admin in config.admins:
admin_list.append(config_admin["user_id"]) admin_list.append(config_admin["user_id"])

0
core/base/__init__.py Normal file
View File

41
core/base/aiobrowser.py Normal file
View File

@ -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

30
core/base/mysql.py Normal file
View File

@ -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

44
core/base/redisdb.py Normal file
View File

@ -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()

48
core/baseplugin.py Normal file
View File

@ -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

256
core/bot.py Normal file
View File

@ -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()

88
core/config.py Normal file
View File

@ -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()

View File

@ -1,19 +1,19 @@
from utils.mysql import MySQL from core.base.mysql import MySQL
from utils.redisdb import RedisDB from core.base.redisdb import RedisDB
from utils.service.manager import listener_service from core.cookies.cache import PublicCookiesCache
from .cache import PublicCookiesCache from core.cookies.repositories import CookiesRepository
from .repositories import CookiesRepository from core.cookies.services import CookiesService, PublicCookiesService
from .services import CookiesService, PublicCookiesService from core.service import init_service
@listener_service() @init_service
def create_cookie_service(mysql: MySQL): def create_cookie_service(mysql: MySQL):
_repository = CookiesRepository(mysql) _repository = CookiesRepository(mysql)
_service = CookiesService(_repository) _service = CookiesService(_repository)
return _service return _service
@listener_service() @init_service
def create_public_cookie_service(mysql: MySQL, redis: RedisDB): def create_public_cookie_service(mysql: MySQL, redis: RedisDB):
_repository = CookiesRepository(mysql) _repository = CookiesRepository(mysql)
_cache = PublicCookiesCache(redis) _cache = PublicCookiesCache(redis)

View File

@ -1,8 +1,8 @@
from typing import List, Union from typing import List, Union
from models.base import RegionEnum from core.base.redisdb import RedisDB
from utils.error import RegionNotFoundError from utils.error import RegionNotFoundError
from utils.redisdb import RedisDB from utils.models.base import RegionEnum
from .error import CookiesCachePoolExhausted from .error import CookiesCachePoolExhausted
@ -12,9 +12,11 @@ class PublicCookiesCache:
def __init__(self, redis: RedisDB): def __init__(self, redis: RedisDB):
self.client = redis.client self.client = redis.client
self.score_qname = "cookie:public" self.score_qname = "cookie:public"
self.user_times_qname = "cookie:public:times"
self.end = 20 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: if region == RegionEnum.HYPERION:
return self.score_qname + ":yuanshen" return self.score_qname + ":yuanshen"
elif region == RegionEnum.HOYOLAB: elif region == RegionEnum.HOYOLAB:
@ -22,26 +24,26 @@ class PublicCookiesCache:
else: else:
raise RegionNotFoundError(region.name) 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 uid:
:param region: :param region:
:return: :return:
""" """
qname = self.get_queue_name(region) qname = self.get_public_cookies_queue_name(region)
score_maps = {f"{uid}": 0} score_maps = {f"{uid}": 0}
result = await self.client.zrem(qname, f"{uid}") result = await self.client.zrem(qname, f"{uid}")
if result == 1: if result == 1:
await self.client.zadd(qname, score_maps) await self.client.zadd(qname, score_maps)
return result 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 uid:
:param region: :param region:
:return: 成功返回列表大小 :return: 成功返回列表大小
""" """
qname = self.get_queue_name(region) qname = self.get_public_cookies_queue_name(region)
if isinstance(uid, int): if isinstance(uid, int):
score_maps = {f"{uid}": 0} score_maps = {f"{uid}": 0}
elif isinstance(uid, list): elif isinstance(uid, list):
@ -57,16 +59,17 @@ class PublicCookiesCache:
add, count = await pipe.execute() add, count = await pipe.execute()
return int(add), count return int(add), count
async def get(self, region: RegionEnum): async def get_public_cookies(self, region: RegionEnum):
"""从缓存列表获取 """从缓存列表获取
:param region: :param region:
:return: :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) scores = await self.client.zrevrange(qname, 0, self.end, withscores=True, score_cast_func=int)
if len(scores) > 0: if len(scores) > 0:
def take_score(elem): def take_score(elem):
return elem[1] return elem[1]
scores.sort(key=take_score) scores.sort(key=take_score)
key = scores[0][0] key = scores[0][0]
score = scores[0][1] score = scores[0][1]
@ -77,16 +80,23 @@ class PublicCookiesCache:
await pipe.execute() await pipe.execute()
return int(key), score + 1 return int(key), score + 1
async def delete(self, uid: int, region: RegionEnum): async def delete_public_cookies(self, uid: int, region: RegionEnum):
qname = self.get_queue_name(region) qname = self.get_public_cookies_queue_name(region)
async with self.client.pipeline(transaction=True) as pipe: async with self.client.pipeline(transaction=True) as pipe:
await pipe.zrem(qname, uid) await pipe.zrem(qname, uid)
return await pipe.execute() 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: async with self.client.pipeline(transaction=True) as pipe:
if limit: if limit:
await pipe.zcount(0, self.end) await pipe.zcount(0, self.end)
else: else:
await pipe.zcard(self.score_qname) await pipe.zcard(self.score_qname)
return await pipe.execute() 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

View File

@ -1,3 +1,17 @@
class CookiesCachePoolExhausted(Exception): class CookieServiceError(Exception):
pass
class CookiesCachePoolExhausted(CookieServiceError):
def __init__(self): def __init__(self):
super().__init__("Cookies cache pool is exhausted") 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")

View File

@ -3,9 +3,10 @@ from typing import cast, List
from sqlalchemy import select from sqlalchemy import select
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from models.base import RegionEnum from core.base.mysql import MySQL
from utils.error import NotFoundError, RegionNotFoundError from utils.error import RegionNotFoundError
from utils.mysql import MySQL from utils.models.base import RegionEnum
from .error import CookiesNotFoundError
from .models import HyperionCookie, HoyolabCookie, Cookies from .models import HyperionCookie, HoyolabCookie, Cookies
@ -47,16 +48,11 @@ class CookiesRepository:
async def update_cookies_ex(self, cookies: Cookies, region: RegionEnum): async def update_cookies_ex(self, cookies: Cookies, region: RegionEnum):
async with self.mysql.Session() as session: async with self.mysql.Session() as session:
session = cast(AsyncSession, session) session = cast(AsyncSession, session)
if region == RegionEnum.HYPERION: if region not in [RegionEnum.HYPERION, RegionEnum.HOYOLAB]:
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:
raise RegionNotFoundError(region.name) 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 def get_cookies(self, user_id, region: RegionEnum) -> Cookies:
async with self.mysql.Session() as session: async with self.mysql.Session() as session:
@ -93,8 +89,3 @@ class CookiesRepository:
return [cookies[0] for cookies in db_cookies] return [cookies[0] for cookies in db_cookies]
else: else:
raise RegionNotFoundError(region.name) raise RegionNotFoundError(region.name)
class CookiesNotFoundError(NotFoundError):
entity_name: str = "CookiesRepository"
entity_value_name: str = "user_id"

View File

@ -1,13 +1,14 @@
from typing import List from typing import List
import genshin import genshin
from genshin import types, InvalidCookies, TooManyRequests, GenshinException from genshin import GenshinException, InvalidCookies, TooManyRequests, types
from logger import Log from utils.log import logger
from models.base import RegionEnum from utils.models.base import RegionEnum
from .cache import PublicCookiesCache from .cache import PublicCookiesCache
from .error import TooManyRequestPublicCookies, CookieServiceError
from .models import CookiesStatusEnum from .models import CookiesStatusEnum
from .repositories import CookiesRepository, CookiesNotFoundError from .repositories import CookiesNotFoundError, CookiesRepository
class CookiesService: class CookiesService:
@ -30,6 +31,7 @@ class PublicCookiesService:
self._cache = public_cookies_cache self._cache = public_cookies_cache
self._repository: CookiesRepository = cookies_repository self._repository: CookiesRepository = cookies_repository
self.count: int = 0 self.count: int = 0
self.user_times_limiter = 3 * 3
async def refresh(self): async def refresh(self):
"""刷新公共Cookies 定时任务 """刷新公共Cookies 定时任务
@ -41,14 +43,14 @@ class PublicCookiesService:
if cookies.status is not None and cookies.status != CookiesStatusEnum.STATUS_SUCCESS: if cookies.status is not None and cookies.status != CookiesStatusEnum.STATUS_SUCCESS:
continue continue
user_list.append(cookies.user_id) user_list.append(cookies.user_id)
add, count = await self._cache.add(user_list, RegionEnum.HYPERION) add, count = await self._cache.add_public_cookies(user_list, RegionEnum.HYPERION)
Log.info(f"国服公共Cookies池已经添加[{add}]个 当前成员数为[{count}]") logger.info(f"国服公共Cookies池已经添加[{add}]个 当前成员数为[{count}]")
user_list.clear() user_list.clear()
cookies_list = await self._repository.get_all_cookies(RegionEnum.HOYOLAB) cookies_list = await self._repository.get_all_cookies(RegionEnum.HOYOLAB)
for cookies in cookies_list: for cookies in cookies_list:
user_list.append(cookies.user_id) user_list.append(cookies.user_id)
add, count = await self._cache.add(user_list, RegionEnum.HOYOLAB) add, count = await self._cache.add_public_cookies(user_list, RegionEnum.HOYOLAB)
Log.info(f"国际服公共Cookies池已经添加[{add}]个 当前成员数为[{count}]") logger.info(f"国际服公共Cookies池已经添加[{add}]个 当前成员数为[{count}]")
async def get_cookies(self, user_id: int, region: RegionEnum = RegionEnum.NULL): async def get_cookies(self, user_id: int, region: RegionEnum = RegionEnum.NULL):
"""获取公共Cookies """获取公共Cookies
@ -56,12 +58,15 @@ class PublicCookiesService:
:param region: 注册的服务器 :param region: 注册的服务器
:return: :return:
""" """
user_times = await self._cache.incr_by_user_times(user_id)
if int(user_times) > self.user_times_limiter:
raise TooManyRequestPublicCookies
while True: while True:
public_id, count = await self._cache.get(region) public_id, count = await self._cache.get_public_cookies(region)
try: try:
cookies = await self._repository.get_cookies(public_id, region) cookies = await self._repository.get_cookies(public_id, region)
except CookiesNotFoundError: except CookiesNotFoundError:
await self._cache.delete(public_id, region) await self._cache.delete_public_cookies(public_id, region)
continue continue
if region == RegionEnum.HYPERION: if region == RegionEnum.HYPERION:
client = genshin.Client(cookies=cookies.cookies, game=types.Game.GENSHIN, region=types.Region.CHINESE) 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, client = genshin.Client(cookies=cookies.cookies, game=types.Game.GENSHIN, region=types.Region.OVERSEAS,
lang="zh-cn") lang="zh-cn")
else: else:
return None raise CookieServiceError
try: try:
await client.get_record_card() await client.get_record_card()
except InvalidCookies as exc: except InvalidCookies as exc:
if "[10001]" in str(exc): if "[10001]" in str(exc):
Log.warning(f"用户 [{public_id}] Cookies无效") logger.warning(f"用户 [{public_id}] Cookies无效")
elif "[-100]" in str(exc): elif "[-100]" in str(exc):
Log.warning(f"用户 [{public_id}] Cookies无效") logger.warning(f"用户 [{public_id}] Cookies无效")
elif "[10103]" in str(exc): elif "[10103]" in str(exc):
Log.warning(f"用户 [{public_id}] Cookie有效但没有绑定到游戏帐户") logger.warning(f"用户 [{public_id}] Cookie有效但没有绑定到游戏帐户")
else: else:
Log.warning("Cookies无效具体原因未知", exc) logger.warning("Cookies无效具体原因未知")
logger.exception(exc)
cookies.status = CookiesStatusEnum.INVALID_COOKIES cookies.status = CookiesStatusEnum.INVALID_COOKIES
await self._repository.update_cookies_ex(cookies, region) 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 continue
except TooManyRequests as exc: except TooManyRequests:
Log.warning(f"用户 [{public_id}] 查询次数太多(操作频繁)") logger.warning(f"用户 [{public_id}] 查询次数太多或操作频繁")
cookies.status = CookiesStatusEnum.TOO_MANY_REQUESTS cookies.status = CookiesStatusEnum.TOO_MANY_REQUESTS
await self._repository.update_cookies_ex(cookies, region) 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 continue
except GenshinException as exc: except GenshinException as exc:
Log.warning(f"用户 [{public_id}] 获取账号信息发生错误,错误信息为", exc) logger.warning(f"用户 [{public_id}] 获取账号信息发生错误,错误信息为")
logger.exception(exc)
continue continue
Log.info(f"用户 user_id[{user_id}] 请求" logger.info(f"用户 user_id[{user_id}] 请求"
f"用户 user_id[{public_id}] 的公共Cookies 该Cookie使用次数为[{count}]次 ") f"用户 user_id[{public_id}] 的公共Cookies 该Cookie使用次数为[{count}]次 ")
return cookies return cookies

8
core/error.py Normal file
View File

@ -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__}'")

View File

@ -1,16 +1,16 @@
from utils.redisdb import RedisDB from core.base.redisdb import RedisDB
from utils.service.manager import listener_service from core.service import init_service
from .cache import GameCache 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): def create_game_strategy_service(redis: RedisDB):
_cache = GameCache(redis, "game:strategy") _cache = GameCache(redis, "game:strategy")
return GameStrategyService(_cache) return GameStrategyService(_cache)
@listener_service() @init_service
def create_game_material_service(redis: RedisDB): def create_game_material_service(redis: RedisDB):
_cache = GameCache(redis, "game:material") _cache = GameCache(redis, "game:material")
return GameMaterialService(_cache) return GameMaterialService(_cache)

View File

@ -1,6 +1,6 @@
from typing import List from typing import List
from utils.redisdb import RedisDB from core.base.redisdb import RedisDB
class GameCache: class GameCache:

View File

@ -1,6 +1,6 @@
from typing import List, Optional from typing import List, Optional
from models.apihelper.hyperion import Hyperion from modules.apihelper.hyperion import Hyperion
from .cache import GameCache from .cache import GameCache

423
core/plugin.py Normal file
View File

@ -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

View File

@ -1,12 +1,12 @@
from utils.mysql import MySQL from core.base.mysql import MySQL
from utils.redisdb import RedisDB from core.base.redisdb import RedisDB
from utils.service.manager import listener_service from core.service import init_service
from .cache import QuizCache from .cache import QuizCache
from .repositories import QuizRepository from .repositories import QuizRepository
from .services import QuizService from .services import QuizService
@listener_service() @init_service
def create_quiz_service(mysql: MySQL, redis: RedisDB): def create_quiz_service(mysql: MySQL, redis: RedisDB):
_repository = QuizRepository(mysql) _repository = QuizRepository(mysql)
_cache = QuizCache(redis) _cache = QuizCache(redis)

View File

@ -1,6 +1,6 @@
from typing import List from typing import List
from .models import Question, Answer from .models import Answer, Question
def CreatQuestionFromSQLData(data: tuple) -> List[Question]: def CreatQuestionFromSQLData(data: tuple) -> List[Question]:
@ -10,6 +10,7 @@ def CreatQuestionFromSQLData(data: tuple) -> List[Question]:
temp_list.append(Question(question_id, text)) temp_list.append(Question(question_id, text))
return temp_list return temp_list
def CreatAnswerFromSQLData(data: tuple) -> List[Answer]: def CreatAnswerFromSQLData(data: tuple) -> List[Answer]:
temp_list = [] temp_list = []
for temp_data in data: for temp_data in data:

View File

@ -2,8 +2,8 @@ from typing import List
import ujson import ujson
from utils.redisdb import RedisDB from core.base.redisdb import RedisDB
from .models import Question, Answer from .models import Answer, Question
class QuizCache: class QuizCache:

View File

@ -2,8 +2,8 @@ from typing import List, Optional
from sqlmodel import SQLModel, Field, Column, Integer, ForeignKey from sqlmodel import SQLModel, Field, Column, Integer, ForeignKey
from models.baseobject import BaseObject from utils.baseobject import BaseObject
from models.types import JSONDict from utils.typedefs import JSONDict
class AnswerDB(SQLModel, table=True): class AnswerDB(SQLModel, table=True):

View File

@ -2,8 +2,8 @@ from typing import List
from sqlmodel import select from sqlmodel import select
from utils.mysql import MySQL from core.base.mysql import MySQL
from .models import QuestionDB, AnswerDB from .models import AnswerDB, QuestionDB
class QuizRepository: class QuizRepository:

View File

@ -2,7 +2,7 @@ import asyncio
from typing import List from typing import List
from .cache import QuizCache from .cache import QuizCache
from .models import Question, Answer from .models import Answer, Question
from .repositories import QuizRepository from .repositories import QuizRepository

30
core/service.py Normal file
View File

@ -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

View File

@ -1,10 +1,10 @@
from utils.mysql import MySQL from core.base.mysql import MySQL
from utils.service.manager import listener_service from core.service import init_service
from .repositories import SignRepository from .repositories import SignRepository
from .services import SignServices from .services import SignServices
@listener_service() @init_service
def create_game_strategy_service(mysql: MySQL): def create_game_strategy_service(mysql: MySQL):
_repository = SignRepository(mysql) _repository = SignRepository(mysql)
_service = SignServices(_repository) _service = SignServices(_repository)

View File

@ -1,9 +1,9 @@
from typing import List, cast, Optional from typing import List, Optional, cast
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from utils.mysql import MySQL from core.base.mysql import MySQL
from .models import Sign from .models import Sign

View File

@ -1,9 +1,9 @@
from utils.aiobrowser import AioBrowser from core.base.aiobrowser import AioBrowser
from utils.service.manager import listener_service from core.service import init_service
from .services import TemplateService from .services import TemplateService
@listener_service() @init_service
def create_template_service(browser: AioBrowser): def create_template_service(browser: AioBrowser):
_service = TemplateService(browser) _service = TemplateService(browser)
return _service return _service

View File

@ -2,12 +2,12 @@ import os
import time import time
from typing import Optional from typing import Optional
from jinja2 import PackageLoader, Environment, Template from jinja2 import Environment, PackageLoader, Template
from playwright.async_api import ViewportSize from playwright.async_api import ViewportSize
from config import config from core.base.aiobrowser import AioBrowser
from logger import Log from core.bot import bot
from utils.aiobrowser import AioBrowser from utils.log import logger
class TemplateService: class TemplateService:
@ -22,7 +22,7 @@ class TemplateService:
self._jinja2_template = {} self._jinja2_template = {}
def get_template(self, package_path: str, template_name: str) -> Template: def get_template(self, package_path: str, template_name: str) -> Template:
if config.debug: if bot.config.debug:
# DEBUG下 禁止复用 方便查看和修改模板 # DEBUG下 禁止复用 方便查看和修改模板
loader = PackageLoader(self._template_package_name, package_path) loader = PackageLoader(self._template_package_name, package_path)
jinja2_env = Environment(loader=loader, enable_async=True, autoescape=True) 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 self._jinja2_template[package_path + template_name] = jinja2_template
return 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, async def render(self, template_path: str, template_name: str, template_data: dict,
viewport: ViewportSize, full_page: bool = True, evaluate: Optional[str] = None) -> bytes: viewport: ViewportSize, full_page: bool = True, evaluate: Optional[str] = None) -> bytes:
""" """模板渲染成图片
模板渲染成图片
:param template_path: 模板目录 :param template_path: 模板目录
:param template_name: 模板文件名 :param template_name: 模板文件名
:param template_data: 模板数据 :param template_data: 模板数据
:param viewport: 截图大小 :param viewport: 截图大小
:param full_page: 是否长截图 :param full_page: 是否长截图
:param auto_escape: 是否自动转义
:param evaluate: 页面加载后运行的 js :param evaluate: 页面加载后运行的 js
:return: :return:
""" """
@ -55,7 +65,7 @@ class TemplateService:
template = self.get_template(template_path, template_name) template = self.get_template(template_path, template_name)
template_data["res_path"] = f"file://{self._current_dir}" template_data["res_path"] = f"file://{self._current_dir}"
html = await template.render_async(**template_data) 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() browser = await self._browser.get_browser()
start_time = time.time() start_time = time.time()
page = await browser.new_page(viewport=viewport) page = await browser.new_page(viewport=viewport)
@ -65,5 +75,5 @@ class TemplateService:
await page.evaluate(evaluate) await page.evaluate(evaluate)
png_data = await page.screenshot(full_page=full_page) png_data = await page.screenshot(full_page=full_page)
await page.close() 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 return png_data

View File

@ -1,10 +1,10 @@
from utils.mysql import MySQL from core.base.mysql import MySQL
from utils.service.manager import listener_service from core.service import init_service
from .repositories import UserRepository from .repositories import UserRepository
from .services import UserService from .services import UserService
@listener_service() @init_service
def create_user_service(mysql: MySQL): def create_user_service(mysql: MySQL):
_repository = UserRepository(mysql) _repository = UserRepository(mysql)
_service = UserService(_repository) _service = UserService(_repository)

3
core/user/error.py Normal file
View File

@ -0,0 +1,3 @@
class UserNotFoundError(Exception):
def __init__(self, user_id):
super().__init__(f"user not found, user_id: {user_id}")

View File

@ -2,7 +2,7 @@ from typing import Optional
from sqlmodel import SQLModel, Field, Enum, Column from sqlmodel import SQLModel, Field, Enum, Column
from models.base import RegionEnum from utils.models.base import RegionEnum
class User(SQLModel, table=True): class User(SQLModel, table=True):

View File

@ -3,8 +3,8 @@ from typing import cast
from sqlalchemy import select from sqlalchemy import select
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from utils.error import NotFoundError from core.base.mysql import MySQL
from utils.mysql import MySQL from .error import UserNotFoundError
from .models import User from .models import User
@ -32,10 +32,5 @@ class UserRepository:
async def add_user(self, user: User): async def add_user(self, user: User):
async with self.mysql.Session() as session: async with self.mysql.Session() as session:
session = cast(AsyncSession, session) session = cast(AsyncSession, session)
await session.add(user) session.add(user)
await session.commit() await session.commit()
class UserNotFoundError(NotFoundError):
entity_name: str = "User"
entity_value_name: str = "user_id"

View File

@ -1,10 +1,10 @@
from utils.redisdb import RedisDB from core.base.redisdb import RedisDB
from utils.service.manager import listener_service from core.service import init_service
from .cache import WikiCache from .cache import WikiCache
from .services import WikiService from .services import WikiService
@listener_service() @init_service
def create_wiki_service(redis: RedisDB): def create_wiki_service(redis: RedisDB):
_cache = WikiCache(redis) _cache = WikiCache(redis)
_service = WikiService(_cache) _service = WikiService(_cache)

View File

@ -1,7 +1,7 @@
import ujson as json import ujson as json
from models.wiki.base import Model from core.base.redisdb import RedisDB
from utils.redisdb import RedisDB from modules.wiki.base import Model
class WikiCache: class WikiCache:

View File

@ -1,9 +1,9 @@
from typing import List, NoReturn, Optional from typing import List, NoReturn, Optional
from core.wiki.cache import WikiCache from core.wiki.cache import WikiCache
from logger import Log from modules.wiki.character import Character
from models.wiki.character import Character from modules.wiki.weapon import Weapon
from models.wiki.weapon import Weapon from utils.log import logger
class WikiService: class WikiService:
@ -19,7 +19,7 @@ class WikiService:
async def refresh_weapon(self) -> NoReturn: async def refresh_weapon(self) -> NoReturn:
weapon_name_list = await Weapon.get_name_list() weapon_name_list = await Weapon.get_name_list()
Log.info(f"一共找到 {len(weapon_name_list)} 把武器信息") logger.info(f"一共找到 {len(weapon_name_list)} 把武器信息")
weapon_list = [] weapon_list = []
num = 0 num = 0
@ -27,16 +27,16 @@ class WikiService:
weapon_list.append(weapon) weapon_list.append(weapon)
num += 1 num += 1
if num % 10 == 0: if num % 10 == 0:
Log.info(f"现在已经获取到 {num} 把武器信息") logger.info(f"现在已经获取到 {num} 把武器信息")
Log.info("写入武器信息到Redis") logger.info("写入武器信息到Redis")
self._weapon_list = weapon_list self._weapon_list = weapon_list
await self._cache.delete("weapon") await self._cache.delete("weapon")
await self._cache.set("weapon", [i.json() for i in weapon_list]) await self._cache.set("weapon", [i.json() for i in weapon_list])
async def refresh_characters(self) -> NoReturn: async def refresh_characters(self) -> NoReturn:
character_name_list = await Character.get_name_list() character_name_list = await Character.get_name_list()
Log.info(f"一共找到 {len(character_name_list)} 个角色信息") logger.info(f"一共找到 {len(character_name_list)} 个角色信息")
character_list = [] character_list = []
num = 0 num = 0
@ -44,9 +44,9 @@ class WikiService:
character_list.append(character) character_list.append(character)
num += 1 num += 1
if num % 10 == 0: if num % 10 == 0:
Log.info(f"现在已经获取到 {num} 个角色信息") logger.info(f"现在已经获取到 {num} 个角色信息")
Log.info("写入角色信息到Redis") logger.info("写入角色信息到Redis")
self._character_list = character_list self._character_list = character_list
await self._cache.delete("characters") await self._cache.delete("characters")
await self._cache.set("characters", [i.json() for i in character_list]) await self._cache.set("characters", [i.json() for i in character_list])
@ -56,12 +56,12 @@ class WikiService:
用于把Redis的缓存全部加载进Python 用于把Redis的缓存全部加载进Python
:return: :return:
""" """
Log.info("正在重新获取Wiki") logger.info("正在重新获取Wiki")
Log.info("正在重新获取武器信息") logger.info("正在重新获取武器信息")
await self.refresh_weapon() await self.refresh_weapon()
Log.info("正在重新获取角色信息") logger.info("正在重新获取角色信息")
await self.refresh_characters() await self.refresh_characters()
Log.info("刷新成功") logger.info("刷新成功")
async def init(self) -> NoReturn: async def init(self) -> NoReturn:
""" """

View File

@ -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()` 修饰器即可

View File

@ -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

View File

@ -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池成功")

View File

@ -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()

97
main.py
View File

@ -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()

View File

@ -1,5 +1,5 @@
# metadata 目录说明 # metadata 目录说明
| FileName | Introduce | | FileName | Introduce |
| :----------: | ------------- | |:------------:|-----------|
| shortname.py | 记录短名称MAP | | shortname.py | 记录短名称MAP |

File diff suppressed because it is too large Load Diff

View File

@ -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": "等级"
}

View File

@ -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": "治疗加成"
}

View File

@ -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

View File

@ -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")

View File

@ -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")

View File

@ -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",)

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -1,5 +0,0 @@
from typing import Dict, Any, Callable, TypeVar
JSONDict = Dict[str, Any]
Func = TypeVar("Func", bound=Callable[..., Any])

View File

@ -1,4 +1,4 @@
# model 目录说明 # modules 目录说明
## apihelpe 模块 ## apihelpe 模块
@ -22,6 +22,6 @@
### 感谢 ### 感谢
| Nickname | Contribution | | Nickname | Contribution |
| :--------------------------------------------------------: | -------------------- | |:----------------------------------------------------------:|--------------|
| [Crawler-ghhw](https://github.com/DGP-Studio/Crawler-ghhw) | 本项目参考的爬虫代码 | | [Crawler-ghhw](https://github.com/DGP-Studio/Crawler-ghhw) | 本项目参考的爬虫代码 |

View File

@ -38,7 +38,7 @@ class ArtifactOcrRate:
HEADERS = { HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "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", "(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): def __init__(self):

View File

@ -1,6 +1,6 @@
import httpx import httpx
from .base import BaseResponseData from modules.apihelper.base import BaseResponseData
class GachaInfo: class GachaInfo:

View File

@ -20,7 +20,7 @@ def get_device_id(name: str) -> str:
def md5(text: str) -> str: def md5(text: str) -> str:
_md5 = hashlib.md5() _md5 = hashlib.md5() # nosec B303
_md5.update(text.encode()) _md5.update(text.encode())
return _md5.hexdigest() return _md5.hexdigest()

View File

@ -1,7 +1,7 @@
from httpx import AsyncClient from httpx import AsyncClient
from .base import BaseResponseData from modules.apihelper.base import BaseResponseData
from .helpers import get_ds, get_device_id, get_recognize_server from modules.apihelper.helpers import get_ds, get_device_id, get_recognize_server
class Genshin: class Genshin:

View File

@ -5,8 +5,8 @@ from typing import List
import httpx import httpx
from httpx import AsyncClient from httpx import AsyncClient
from .base import HyperionResponse, ArtworkImage, BaseResponseData from modules.apihelper.base import HyperionResponse, ArtworkImage, BaseResponseData
from .helpers import get_ds, get_device_id from modules.apihelper.helpers import get_ds, get_device_id
class Hyperion: class Hyperion:

View File

@ -0,0 +1,8 @@
from pydantic import BaseModel
class Talent(BaseModel):
"""命座"""
talent_id: int = 0
name: str = ""
icon: str = ""

View File

@ -35,15 +35,16 @@ class Model(PydanticBaseModel):
class WikiModel(Model): class WikiModel(Model):
# noinspection PyUnresolvedReferences
"""wiki所用到的基类 """wiki所用到的基类
Attributes: Attributes:
id (:obj:`int`): ID id (:obj:`int`): ID
name (:obj:`str`): 名称 name (:obj:`str`): 名称
rarity (:obj:`int`): 星级 rarity (:obj:`int`): 星级
_client (:class:`httpx.AsyncClient`): 发起 http 请求的 client _client (:class:`httpx.AsyncClient`): 发起 http 请求的 client
""" """
_client: ClassVar[AsyncClient] = AsyncClient() _client: ClassVar[AsyncClient] = AsyncClient()
id: str id: str
@ -199,7 +200,7 @@ class WikiModel(Model):
queue: Queue[Union[str, Tuple[str, URL]]] = Queue() # 存放 Model 的队列 queue: Queue[Union[str, Tuple[str, URL]]] = Queue() # 存放 Model 的队列
signal = Value('i', len(urls)) # 一个用于异步任务同步的信号,初始值为存放所需要爬取的页面数 signal = Value('i', len(urls)) # 一个用于异步任务同步的信号,初始值为存放所需要爬取的页面数
async def task(page: URL, s: Value): async def task(page: URL):
"""包装的爬虫任务""" """包装的爬虫任务"""
response = await cls._client_get(page) response = await cls._client_get(page)
# 从页面中获取对应的 chaos data (未处理的json格式字符串) # 从页面中获取对应的 chaos data (未处理的json格式字符串)
@ -215,7 +216,7 @@ class WikiModel(Model):
signal.value = signal.value - 1 # 信号量减少 1 ,说明该爬虫任务已经完成 signal.value = signal.value - 1 # 信号量减少 1 ,说明该爬虫任务已经完成
for url in urls: # 遍历需要爬出的页面 for url in urls: # 遍历需要爬出的页面
asyncio.create_task(task(url, signal)) # 添加爬虫任务 asyncio.create_task(task(url)) # 添加爬虫任务
while signal.value > 0 or not queue.empty(): # 当还有未完成的爬虫任务或存放数据的队列不为空时 while signal.value > 0 or not queue.empty(): # 当还有未完成的爬虫任务或存放数据的队列不为空时
yield await queue.get() # 取出并返回一个存放的 Model yield await queue.get() # 取出并返回一个存放的 Model

View File

@ -4,9 +4,9 @@ from typing import List, Optional
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from httpx import URL from httpx import URL
from models.wiki.base import Model, SCRAPE_HOST from modules.wiki.base import Model, SCRAPE_HOST
from models.wiki.base import WikiModel from modules.wiki.base import WikiModel
from models.wiki.other import Association, Element, WeaponType from modules.wiki.other import Association, Element, WeaponType
class Birth(Model): class Birth(Model):
@ -163,7 +163,7 @@ class Character(WikiModel):
async def get_url_by_name(cls, name: str) -> Optional[URL]: async def get_url_by_name(cls, name: str) -> Optional[URL]:
# 重写此函数的目的是处理主角名字的 ID # 重写此函数的目的是处理主角名字的 ID
_map = {'': "playergirl_007", '': "playerboy_005"} _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 cls.get_url_by_id(id_)
return await super(Character, cls).get_url_by_name(name) return await super(Character, cls).get_url_by_name(name)

View File

@ -5,7 +5,7 @@ from bs4 import BeautifulSoup
from httpx import URL from httpx import URL
from typing_extensions import Self from typing_extensions import Self
from models.wiki.base import SCRAPE_HOST, WikiModel from modules.wiki.base import SCRAPE_HOST, WikiModel
__all__ = ['Material'] __all__ = ['Material']

View File

@ -3,7 +3,7 @@ from typing import Optional
from typing_extensions import Self from typing_extensions import Self
from models.wiki.base import SCRAPE_HOST from modules.wiki.base import SCRAPE_HOST
__all__ = [ __all__ = [
'Element', 'Element',

View File

@ -5,8 +5,8 @@ from typing import List, Optional, Tuple, Union
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from httpx import URL from httpx import URL
from models.wiki.base import Model, SCRAPE_HOST, WikiModel from modules.wiki.base import Model, SCRAPE_HOST, WikiModel
from models.wiki.other import AttributeType, WeaponType from modules.wiki.other import AttributeType, WeaponType
__all__ = ['Weapon', 'WeaponAffix', 'WeaponAttribute'] __all__ = ['Weapon', 'WeaponAffix', 'WeaponAttribute']

View File

@ -4,49 +4,160 @@
该目录仅限处理交互层和业务层数据交换的任务 该目录仅限处理交互层和业务层数据交换的任务
如有任何新业务接口,请转到 `core` 目录添加 如有任何核心接口,请转到 `core` 目录添加
如有任何API请求接口请转到 `models` 目录添加 如有任何API请求接口请转到 `models` 目录添加
## 基础代码 ## 新版插件 Plugin 的写法
``` python ### 关于路径
from telegram.ext import CommandHandler, CallbackContext
from logger import Log 插件应该写在 `plugins` 文件夹下,可以是一个包或者是一个文件,但文件名、文件夹名中不能包含`_`字符
from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts
from utils.plugins.manager import listener_plugins_class
@listener_plugins_class() ### 关于类
class Example:
@classmethod 1. 除了要使用`ConversationHandler` 的插件外,都要继承 `core.plugin.Plugin`
def create_handlers(cls):
example = cls()
return [CommandHandler('example', example.command_start)]
@error_callable ```python
@restricts() from core.plugin import Plugin
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")
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()` 修饰器带参,必须带括号,否则会出现调用错误**

View File

@ -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)

View File

@ -1,19 +1,21 @@
from typing import Dict
from genshin import Client from genshin import Client
from telegram import Update from telegram import Update
from telegram.constants import ChatAction 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.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 import UserService
from core.user.repositories import UserNotFoundError from core.user.error import UserNotFoundError
from logger import Log
from plugins.base import BasePlugins
from utils.decorators.error import error_callable from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts from utils.decorators.restricts import restricts
from utils.helpers import get_genshin_client, url_to_file from utils.helpers import get_genshin_client, url_to_file
from utils.plugins.manager import listener_plugins_class from utils.log import logger
from utils.service.inject import inject
class AbyssUnlocked(Exception): class AbyssUnlocked(Exception):
@ -26,25 +28,15 @@ class NoMostKills(Exception):
pass pass
@listener_plugins_class() class Abyss(Plugin, BasePlugin):
class Abyss(BasePlugins):
"""深渊数据查询""" """深渊数据查询"""
@inject
def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None, def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None,
template_service: TemplateService = None): template_service: TemplateService = None):
self.template_service = template_service self.template_service = template_service
self.cookies_service = cookies_service self.cookies_service = cookies_service
self.user_service = user_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 @staticmethod
def _get_role_star_bg(value: int): def _get_role_star_bg(value: int):
if value == 4: if value == 4:
@ -54,7 +46,7 @@ class Abyss(BasePlugins):
else: else:
raise ValueError("错误的数据") raise ValueError("错误的数据")
async def _get_abyss_data(self, client: Client) -> dict: async def _get_abyss_data(self, client: Client) -> Dict:
uid = client.uid uid = client.uid
await client.get_record_cards() await client.get_record_cards()
spiral_abyss_info = await client.get_spiral_abyss(uid) spiral_abyss_info = await client.get_spiral_abyss(uid)
@ -101,17 +93,19 @@ class Abyss(BasePlugins):
abyss_data["most_played_list"].append(temp) abyss_data["most_played_list"].append(temp)
return abyss_data return abyss_data
@handler(CommandHandler, command="abyss", block=False)
@handler(MessageHandler, filters=filters.Regex("^深渊数据查询(.*)"), block=False)
@restricts() @restricts()
@error_callable @error_callable
async def command_start(self, update: Update, context: CallbackContext) -> None: async def command_start(self, update: Update, context: CallbackContext) -> None:
user = update.effective_user user = update.effective_user
message = update.message message = update.effective_message
Log.info(f"用户 {user.full_name}[{user.id}] 查深渊挑战命令请求") logger.info(f"用户 {user.full_name}[{user.id}] 查深渊挑战命令请求")
await message.reply_chat_action(ChatAction.TYPING) await message.reply_chat_action(ChatAction.TYPING)
try: 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) abyss_data = await self._get_abyss_data(client)
except UserNotFoundError: except (UserNotFoundError, CookiesNotFoundError):
reply_message = await message.reply_text("未查询到账号信息,请先私聊派蒙绑定账号") reply_message = await message.reply_text("未查询到账号信息,请先私聊派蒙绑定账号")
if filters.ChatType.GROUPS.filter(message): if filters.ChatType.GROUPS.filter(message):
self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id, 10) 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) {"width": 690, "height": 504}, full_page=False)
await message.reply_photo(png_data, filename=f"abyss_{user.id}.png", await message.reply_photo(png_data, filename=f"abyss_{user.id}.png",
allow_sending_without_reply=True) allow_sending_without_reply=True)
return

View File

@ -3,106 +3,96 @@ from typing import Optional
import genshin import genshin
from genshin import InvalidCookies, GenshinException, DataNotPublic from genshin import InvalidCookies, GenshinException, DataNotPublic
from sqlalchemy.exc import NoResultFound
from telegram import Update, ReplyKeyboardRemove, ReplyKeyboardMarkup, TelegramObject 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 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.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.models import User
from core.user.repositories import UserNotFoundError
from core.user.services import UserService 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.error import error_callable
from utils.decorators.restricts import restricts from utils.decorators.restricts import restricts
from utils.plugins.manager import listener_plugins_class from utils.log import logger
from utils.service.inject import inject from utils.models.base import RegionEnum
class AddUserCommandData(TelegramObject): class AddUserCommandData(TelegramObject):
user: Optional[User] = None user: Optional[User] = None
cookies_database_data: Optional[Cookies] = None
region: RegionEnum = RegionEnum.HYPERION region: RegionEnum = RegionEnum.HYPERION
cookies: dict = {} cookies: dict = {}
game_uid: int = 0 game_uid: int = 0
@listener_plugins_class() CHECK_SERVER, CHECK_COOKIES, COMMAND_RESULT = range(10100, 10103)
class AddUser(BasePlugins):
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): def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None):
self.cookies_service = cookies_service self.cookies_service = cookies_service
self.user_service = user_service self.user_service = user_service
@classmethod @conversation.entry_point
def create_handlers(cls): @handler.command(command='adduser', filters=filters.ChatType.PRIVATE, block=True)
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]
@restricts() @restricts()
@error_callable @error_callable
async def command_start(self, update: Update, context: CallbackContext) -> int: async def command_start(self, update: Update, context: CallbackContext) -> int:
user = update.effective_user 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") add_user_command_data: AddUserCommandData = context.chat_data.get("add_user_command_data")
if add_user_command_data is None: if add_user_command_data is None:
cookies_command_data = AddUserCommandData() cookies_command_data = AddUserCommandData()
context.chat_data["add_user_command_data"] = cookies_command_data 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'], ["退出"]] reply_keyboard = [['米游社', 'HoYoLab'], ["退出"]]
await update.message.reply_markdown_v2(message, await message.reply_markdown_v2(text, reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True))
reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)) return CHECK_SERVER
return self.CHECK_SERVER
@conversation.state(state=CHECK_SERVER)
@handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True)
@error_callable @error_callable
async def check_server(self, update: Update, context: CallbackContext) -> int: async def check_server(self, update: Update, context: CallbackContext) -> int:
user = update.effective_user user = update.effective_user
message = update.effective_message
add_user_command_data: AddUserCommandData = context.chat_data.get("add_user_command_data") 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: try:
user_info = await self.user_service.get_user_by_id(user.id) user_info = await self.user_service.get_user_by_id(user.id)
except UserNotFoundError: except UserNotFoundError:
user_info = None 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 add_user_command_data.user = user_info
if update.message.text == "退出": add_user_command_data.region = region
await update.message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) await message.reply_text(f"请输入{bbs_name}的Cookies或回复退出取消操作", 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())
javascript = "javascript:(()=>{_=(n)=>{for(i in(r=document.cookie.split(';'))){var a=r[i].split('=');if(a[" \ 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(" \ "0].trim()==n)return a[1]}};c=_('account_id')||alert('无效的Cookie,请重新登录!');c&&confirm(" \
"'将Cookie复制到剪贴板?')&&copy(document.cookie)})(); " "'将Cookie复制到剪贴板?')&&copy(document.cookie)})(); "
@ -118,76 +108,79 @@ class AddUser(BasePlugins):
f"[1、通过 Via 浏览器打开{bbs_name}并登录]({bbs_url})\n" \ f"[1、通过 Via 浏览器打开{bbs_name}并登录]({bbs_url})\n" \
f"2、复制下方的代码并将其粘贴在地址栏中点击右侧箭头\n" \ f"2、复制下方的代码并将其粘贴在地址栏中点击右侧箭头\n" \
f"`{escape_markdown(javascript_android, version=2, entity_type='code')}`" f"`{escape_markdown(javascript_android, version=2, entity_type='code')}`"
await update.message.reply_markdown_v2(help_message, disable_web_page_preview=True) await message.reply_markdown_v2(help_message, disable_web_page_preview=True)
return self.CHECK_COOKIES return CHECK_COOKIES
@conversation.state(state=CHECK_COOKIES)
@handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True)
@error_callable @error_callable
async def check_cookies(self, update: Update, context: CallbackContext) -> int: async def check_cookies(self, update: Update, context: CallbackContext) -> int:
user = update.effective_user user = update.effective_user
message = update.effective_message
add_user_command_data: AddUserCommandData = context.chat_data.get("add_user_command_data") add_user_command_data: AddUserCommandData = context.chat_data.get("add_user_command_data")
if update.message.text == "退出": if message.text == "退出":
await update.message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
str_cookies = update.message.text str_cookies = message.text
cookie = SimpleCookie() cookie = SimpleCookie()
try: try:
cookie.load(str_cookies) cookie.load(str_cookies)
except CookieError: except CookieError:
await update.message.reply_text("Cookies格式有误请检查", reply_markup=ReplyKeyboardRemove()) await message.reply_text("Cookies格式有误请检查", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
if len(cookie) == 0: if len(cookie) == 0:
await update.message.reply_text("Cookies格式有误请检查", reply_markup=ReplyKeyboardRemove()) await message.reply_text("Cookies格式有误请检查", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
cookies = {key: morsel.value for key, morsel in cookie.items()} cookies = {key: morsel.value for key, morsel in cookie.items()}
if not cookies: if not cookies:
await update.message.reply_text("Cookies格式有误请检查", reply_markup=ReplyKeyboardRemove()) await message.reply_text("Cookies格式有误请检查", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
if add_user_command_data.region == RegionEnum.HYPERION: if add_user_command_data.region == RegionEnum.HYPERION:
client = genshin.ChineseClient(cookies=cookies) client = genshin.ChineseClient(cookies=cookies)
elif add_user_command_data.region == RegionEnum.HOYOLAB: elif add_user_command_data.region == RegionEnum.HOYOLAB:
client = genshin.GenshinClient(cookies=cookies) client = genshin.GenshinClient(cookies=cookies)
else: else:
await update.message.reply_text("数据错误", reply_markup=ReplyKeyboardRemove()) await message.reply_text("数据错误", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
try: try:
user_info = await client.get_record_card() user_info = await client.get_record_card()
except DataNotPublic: except DataNotPublic:
await update.message.reply_text("账号疑似被注销,请检查账号状态", reply_markup=ReplyKeyboardRemove()) await message.reply_text("账号疑似被注销,请检查账号状态", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
except InvalidCookies: except InvalidCookies:
await update.message.reply_text("Cookies已经过期请检查是否正确", reply_markup=ReplyKeyboardRemove()) await message.reply_text("Cookies已经过期请检查是否正确", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
except GenshinException as error: except GenshinException as error:
await update.message.reply_text(f"获取账号信息发生错误,错误信息为 {str(error)}请检查Cookie或者账号是否正常", await message.reply_text(f"获取账号信息发生错误,错误信息为 {str(error)}请检查Cookie或者账号是否正常",
reply_markup=ReplyKeyboardRemove()) reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
except (AttributeError, ValueError): except (AttributeError, ValueError):
await update.message.reply_text("Cookies错误请检查是否正确", reply_markup=ReplyKeyboardRemove()) await message.reply_text("Cookies错误请检查是否正确", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
add_user_command_data.cookies = cookies add_user_command_data.cookies = cookies
add_user_command_data.game_uid = user_info.uid add_user_command_data.game_uid = user_info.uid
reply_keyboard = [['确认', '退出']] reply_keyboard = [['确认', '退出']]
await update.message.reply_text("获取角色基础信息成功,请检查是否正确!") await message.reply_text("获取角色基础信息成功,请检查是否正确!")
Log.info(f"用户 {user.full_name}[{user.id}] 获取账号 {user_info.nickname}[{user_info.uid}] 信息成功") logger.info(f"用户 {user.full_name}[{user.id}] 获取账号 {user_info.nickname}[{user_info.uid}] 信息成功")
message = f"*角色信息*\n" \ text = f"*角色信息*\n" \
f"角色名称:{escape_markdown(user_info.nickname, version=2)}\n" \ f"角色名称:{escape_markdown(user_info.nickname, version=2)}\n" \
f"角色等级:{user_info.level}\n" \ f"角色等级:{user_info.level}\n" \
f"UID`{user_info.uid}`\n" \ f"UID`{user_info.uid}`\n" \
f"服务器名称:`{user_info.server_name}`\n" f"服务器名称:`{user_info.server_name}`\n"
await update.message.reply_markdown_v2( await message.reply_markdown_v2(text, reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True))
message, return COMMAND_RESULT
reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
)
return self.COMMAND_RESULT
@conversation.state(state=COMMAND_RESULT)
@handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True)
@error_callable @error_callable
async def command_result(self, update: Update, context: CallbackContext) -> int: async def command_result(self, update: Update, context: CallbackContext) -> int:
user = update.effective_user user = update.effective_user
message = update.effective_message
add_user_command_data: AddUserCommandData = context.chat_data.get("add_user_command_data") add_user_command_data: AddUserCommandData = context.chat_data.get("add_user_command_data")
if update.message.text == "退出": if message.text == "退出":
await update.message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
elif update.message.text == "确认": elif message.text == "确认":
if add_user_command_data.user is None: if add_user_command_data.user is None:
if add_user_command_data.region == RegionEnum.HYPERION: if add_user_command_data.region == RegionEnum.HYPERION:
user_db = User(user_id=user.id, yuanshen_uid=add_user_command_data.game_uid, 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, user_db = User(user_id=user.id, genshin_uid=add_user_command_data.game_uid,
region=add_user_command_data.region) region=add_user_command_data.region)
else: else:
await update.message.reply_text("数据错误") await message.reply_text("数据错误")
return ConversationHandler.END return ConversationHandler.END
await self.user_service.add_user(user_db) 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: else:
user_db = add_user_command_data.user user_db = add_user_command_data.user
user_db.region = add_user_command_data.region user_db.region = add_user_command_data.region
@ -209,19 +200,18 @@ class AddUser(BasePlugins):
elif add_user_command_data.region == RegionEnum.HOYOLAB: elif add_user_command_data.region == RegionEnum.HOYOLAB:
user_db.genshin_uid = add_user_command_data.game_uid user_db.genshin_uid = add_user_command_data.game_uid
else: else:
await update.message.reply_text("数据错误") await message.reply_text("数据错误")
return ConversationHandler.END return ConversationHandler.END
await self.user_service.update_user(user_db) await self.user_service.update_user(user_db)
# 临时解决错误 if add_user_command_data.cookies_database_data is None:
try: await self.cookies_service.add_cookies(user.id, add_user_command_data.cookies,
await self.cookies_service.update_cookies(user.id, add_user_command_data.cookies, add_user_command_data.region)
add_user_command_data.region) else:
except NoResultFound: await self.cookies_service.update_cookies(user.id, add_user_command_data.cookies,
await self.cookies_service.add_cookies(user.id, add_user_command_data.cookies, add_user_command_data.region)
add_user_command_data.region) logger.info(f"用户 {user.full_name}[{user.id}] 绑定账号成功")
Log.info(f"用户 {user.full_name}[{user.id}] 绑定账号成功") await message.reply_text("保存成功", reply_markup=ReplyKeyboardRemove())
await update.message.reply_text("保存成功", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
else: else:
await update.message.reply_text("回复错误,请重新输入") await message.reply_text("回复错误,请重新输入")
return self.COMMAND_RESULT return COMMAND_RESULT

View File

@ -2,24 +2,22 @@ from typing import Optional
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, File from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, File
from telegram.constants import ChatAction, ParseMode from telegram.constants import ChatAction, ParseMode
from telegram.ext import CallbackContext, ConversationHandler, CommandHandler, CallbackQueryHandler, MessageHandler, \ from telegram.ext import CallbackContext, ConversationHandler, filters
filters
from telegram.helpers import escape_markdown from telegram.helpers import escape_markdown
from logger import Log from core.baseplugin import BasePlugin
from models.apihelper.artifact import ArtifactOcrRate, get_comment, get_format_sub_item from core.plugin import Plugin, conversation, handler
from plugins.base import BasePlugins from modules.apihelper.artifact import ArtifactOcrRate, get_comment, get_format_sub_item
from utils.decorators.error import error_callable from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts 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(Plugin.Conversation, BasePlugin.Conversation):
class ArtifactRate(BasePlugins):
"""圣遗物评分""" """圣遗物评分"""
COMMAND_RESULT = 1
STAR_KEYBOARD = [[ STAR_KEYBOARD = [[
InlineKeyboardButton( InlineKeyboardButton(
f"{i}", callback_data=f"artifact_ocr_rate_data|star|{i}") for i in range(1, 6) 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): def __init__(self):
self.artifact_rate = ArtifactOcrRate() 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: async def get_rate(self, artifact_attr: dict) -> str:
rate_result_req = await self.artifact_rate.rate_artifact(artifact_attr) rate_result_req = await self.artifact_rate.rate_artifact(artifact_attr)
if rate_result_req.status_code != 200: 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" \ f"{escape_markdown(get_comment(rate_result['total_percent']), version=2)}\n" \
"_评分、识图均来自 genshin\\.pub_" "_评分、识图均来自 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 @error_callable
@restricts(return_data=ConversationHandler.END) @restricts(return_data=ConversationHandler.END)
async def command_start(self, update: Update, context: CallbackContext) -> int: async def command_start(self, update: Update, context: CallbackContext) -> int:
message = update.message message = update.effective_message
user = update.effective_user 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 context.user_data["artifact_attr"] = None
photo_file: Optional[File] = None photo_file: Optional[File] = None
if message is None: if message is None:
@ -110,17 +97,19 @@ class ArtifactRate(BasePlugins):
if artifact_attr.get("star") is None: if artifact_attr.get("star") is None:
await message.reply_text("无法识别圣遗物星级,请选择圣遗物星级", await message.reply_text("无法识别圣遗物星级,请选择圣遗物星级",
reply_markup=InlineKeyboardMarkup(self.STAR_KEYBOARD)) reply_markup=InlineKeyboardMarkup(self.STAR_KEYBOARD))
return self.COMMAND_RESULT return COMMAND_RESULT
if artifact_attr.get("level") is None: if artifact_attr.get("level") is None:
await message.reply_text("无法识别圣遗物等级,请选择圣遗物等级", await message.reply_text("无法识别圣遗物等级,请选择圣遗物等级",
reply_markup=InlineKeyboardMarkup(self.LEVEL_KEYBOARD)) reply_markup=InlineKeyboardMarkup(self.LEVEL_KEYBOARD))
return self.COMMAND_RESULT return COMMAND_RESULT
reply_message = await message.reply_text("识图成功!\n" reply_message = await message.reply_text("识图成功!\n"
"正在评分中...") "正在评分中...")
rate_text = await self.get_rate(artifact_attr) rate_text = await self.get_rate(artifact_attr)
await reply_message.edit_text(rate_text, parse_mode=ParseMode.MARKDOWN_V2) await reply_message.edit_text(rate_text, parse_mode=ParseMode.MARKDOWN_V2)
return ConversationHandler.END return ConversationHandler.END
@conversation.state(state=COMMAND_RESULT)
@handler.callback_query()
@error_callable @error_callable
async def command_result(self, update: Update, context: CallbackContext) -> int: async def command_result(self, update: Update, context: CallbackContext) -> int:
query = update.callback_query query = update.callback_query
@ -151,11 +140,11 @@ class ArtifactRate(BasePlugins):
if artifact_attr.get("level") is None: if artifact_attr.get("level") is None:
await query.edit_message_text("无法识别圣遗物等级,请选择圣遗物等级", await query.edit_message_text("无法识别圣遗物等级,请选择圣遗物等级",
reply_markup=InlineKeyboardMarkup(self.LEVEL_KEYBOARD)) reply_markup=InlineKeyboardMarkup(self.LEVEL_KEYBOARD))
return self.COMMAND_RESULT return COMMAND_RESULT
if artifact_attr.get("star") is None: if artifact_attr.get("star") is None:
await query.edit_message_text("无法识别圣遗物星级,请选择圣遗物星级", await query.edit_message_text("无法识别圣遗物星级,请选择圣遗物星级",
reply_markup=InlineKeyboardMarkup(self.STAR_KEYBOARD)) reply_markup=InlineKeyboardMarkup(self.STAR_KEYBOARD))
return self.COMMAND_RESULT return COMMAND_RESULT
await query.edit_message_text("正在评分中...") await query.edit_message_text("正在评分中...")
rate_text = await self.get_rate(artifact_attr) rate_text = await self.get_rate(artifact_attr)
await query.edit_message_text(rate_text, parse_mode=ParseMode.MARKDOWN_V2) await query.edit_message_text(rate_text, parse_mode=ParseMode.MARKDOWN_V2)

View File

@ -8,26 +8,22 @@ from telegram.constants import ChatAction
from telegram.ext import CommandHandler, MessageHandler, ConversationHandler, filters, \ from telegram.ext import CommandHandler, MessageHandler, ConversationHandler, filters, \
CallbackContext CallbackContext
from core.baseplugin import BasePlugin
from core.cookies.error import CookiesNotFoundError
from core.cookies.services import CookiesService from core.cookies.services import CookiesService
from core.plugin import Plugin, handler
from core.template.services import TemplateService 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 core.user.services import UserService
from logger import Log
from plugins.base import BasePlugins
from utils.decorators.error import error_callable from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts from utils.decorators.restricts import restricts
from utils.helpers import get_genshin_client from utils.helpers import get_genshin_client
from utils.plugins.manager import listener_plugins_class from utils.log import logger
from utils.service.inject import inject
@listener_plugins_class() class DailyNote(Plugin, BasePlugin):
class DailyNote(BasePlugins):
"""每日便签""" """每日便签"""
COMMAND_RESULT, = range(10200, 10201)
@inject
def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None, def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None,
template_service: TemplateService = None): template_service: TemplateService = None):
self.template_service = template_service self.template_service = template_service
@ -35,12 +31,6 @@ class DailyNote(BasePlugins):
self.user_service = user_service self.user_service = user_service
self.current_dir = os.getcwd() 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: async def _get_daily_note(self, client) -> bytes:
daily_info = await client.get_genshin_notes(client.uid) daily_info = await client.get_genshin_notes(client.uid)
day = datetime.datetime.now().strftime("%m-%d %H:%M") + " 星期" + "一二三四五六日"[datetime.datetime.now().weekday()] 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) {"width": 600, "height": 548}, full_page=False)
return png_data return png_data
@restricts() @handler(CommandHandler, command="dailynote", block=False)
@handler(MessageHandler, filters=filters.Regex("^当前状态(.*)"), block=False)
@restricts(return_data=ConversationHandler.END)
@error_callable @error_callable
async def command_start(self, update: Update, context: CallbackContext) -> Optional[int]: async def command_start(self, update: Update, context: CallbackContext) -> Optional[int]:
user = update.effective_user user = update.effective_user
message = update.message message = update.message
Log.info(f"用户 {user.full_name}[{user.id}] 查询游戏状态命令请求") logger.info(f"用户 {user.full_name}[{user.id}] 查询游戏状态命令请求")
try: 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) png_data = await self._get_daily_note(client)
except UserNotFoundError: except (UserNotFoundError, CookiesNotFoundError):
reply_message = await message.reply_text("未查询到账号信息,请先私聊派蒙绑定账号") reply_message = await message.reply_text("未查询到账号信息,请先私聊派蒙绑定账号")
if filters.ChatType.GROUPS.filter(message): 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, reply_message.chat_id, reply_message.message_id, 30)

View File

@ -1,7 +1,5 @@
from utils.plugins.manager import listener_plugins_class
from .gacha import Gacha from .gacha import Gacha
@listener_plugins_class()
class GachaPlugins(Gacha): class GachaPlugins(Gacha):
pass pass

View File

@ -7,34 +7,20 @@ from telegram import Update
from telegram.constants import ChatAction from telegram.constants import ChatAction
from telegram.ext import filters, CommandHandler, MessageHandler, CallbackContext 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 core.template import TemplateService
from logger import Log from modules.apihelper.gacha import GachaInfo
from models.apihelper.gacha import GachaInfo
from plugins.base import BasePlugins
from plugins.genshin.gacha.wish import WishCountInfo, get_one from plugins.genshin.gacha.wish import WishCountInfo, get_one
from utils.bot import get_all_args from utils.bot import get_all_args
from utils.decorators.error import error_callable from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts from utils.decorators.restricts import restricts
from utils.plugins.manager import listener_plugins_class from utils.log import logger
from utils.service.inject import inject
@listener_plugins_class() class Gacha(Plugin, BasePlugin):
class Gacha(BasePlugins):
"""抽卡模拟器(非首模拟器/减寿模拟器)""" """抽卡模拟器(非首模拟器/减寿模拟器)"""
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): def __init__(self, template_service: TemplateService = None):
self.gacha = GachaInfo() self.gacha = GachaInfo()
self.template_service = template_service self.template_service = template_service
@ -59,6 +45,9 @@ class Gacha(BasePlugins):
gacha_info["gacha_id"] = gacha_id gacha_info["gacha_id"] = gacha_id
return gacha_info 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.GROUPS, restricts_time=20, try_delete_message=True)
@restricts(filters.ChatType.PRIVATE) @restricts(filters.ChatType.PRIVATE)
@error_callable @error_callable
@ -80,7 +69,7 @@ class Gacha(BasePlugins):
return return
else: else:
gacha_info = await self.gacha_info(default=True) 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"] gacha_id: str = gacha_info["gacha_id"]
user_gacha: dict[str, WishCountInfo] = context.user_data.get("gacha") user_gacha: dict[str, WishCountInfo] = context.user_data.get("gacha")
@ -95,7 +84,7 @@ class Gacha(BasePlugins):
if re_color is None: if re_color is None:
title_html = BeautifulSoup(title, "lxml") title_html = BeautifulSoup(title, "lxml")
pool_name = title_html.text pool_name = title_html.text
Log.warning(f"卡池信息 title 提取 color 失败 title[{title}]") logger.warning(f"卡池信息 title 提取 color 失败 title[{title}]")
else: else:
color = re_color.group(1) color = re_color.group(1)
title_html = BeautifulSoup(title, "lxml") title_html = BeautifulSoup(title, "lxml")

View File

@ -3,41 +3,32 @@ from telegram.constants import ChatAction
from telegram.error import BadRequest from telegram.error import BadRequest
from telegram.ext import CommandHandler, CallbackContext from telegram.ext import CommandHandler, CallbackContext
from config import config from core.bot import bot
from core.template.services import TemplateService from core.plugin import Plugin, handler
from logger import Log from core.template import TemplateService
from utils.decorators.error import error_callable from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts from utils.decorators.restricts import restricts
from utils.plugins.manager import listener_plugins_class from utils.log import logger
from utils.service.inject import inject
@listener_plugins_class() class HelpPlugin(Plugin):
class Help:
"""帮助菜单"""
@inject
def __init__(self, template_service: TemplateService = None): def __init__(self, template_service: TemplateService = None):
self.template_service = template_service
self.help_png = None
self.file_id = None self.file_id = None
self.help_png = None
if template_service is None:
raise ModuleNotFoundError
self.template_service = template_service
@classmethod @handler(CommandHandler, command="help", block=False)
def create_handlers(cls) -> list:
_help = cls()
return [
CommandHandler("help", _help.command_start, block=False),
]
@error_callable @error_callable
@restricts() @restricts()
async def command_start(self, update: Update, _: CallbackContext) -> None: async def start(self, update: Update, _: CallbackContext):
message = update.message
user = update.effective_user user = update.effective_user
Log.info(f"用户 {user.full_name}[{user.id}] 发出help命令") message = update.effective_message
if self.file_id is None or config.debug: 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) 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) await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
reply_photo = await message.reply_photo(help_png, filename="help.png", allow_sending_without_reply=True) reply_photo = await message.reply_photo(help_png, filename="help.png", allow_sending_without_reply=True)
photo = reply_photo.photo[0] photo = reply_photo.photo[0]
@ -48,5 +39,5 @@ class Help:
await message.reply_photo(self.file_id, allow_sending_without_reply=True) await message.reply_photo(self.file_id, allow_sending_without_reply=True)
except BadRequest as error: except BadRequest as error:
self.file_id = None self.file_id = None
Log.error("发送图片失败尝试清空已经保存的file_id错误信息为", error) logger.error("发送图片失败尝试清空已经保存的file_id错误信息为", error)
await message.reply_text("发送图片失败", allow_sending_without_reply=True) await message.reply_text("发送图片失败", allow_sending_without_reply=True)

View File

@ -2,34 +2,30 @@ import json
from os import sep from os import sep
from telegram import Update 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 core.baseplugin import BasePlugin
from plugins.base import BasePlugins from core.plugin import Plugin, handler
from utils.bot import get_all_args from utils.bot import get_all_args
from utils.decorators.error import error_callable from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts from utils.decorators.restricts import restricts
from utils.plugins.manager import listener_plugins_class from utils.log import logger
@listener_plugins_class() class HilichurlsPlugin(Plugin, BasePlugin):
class Hilichurls(BasePlugins): """丘丘语字典."""
def __init__(self): def __init__(self):
"""加载数据文件.数据整理自 https://wiki.biligame.com/ys By @zhxycn.""" """加载数据文件.数据整理自 https://wiki.biligame.com/ys By @zhxycn."""
with open(f"resources{sep}json{sep}hilichurls_dictionary.json", "r", encoding="utf8") as f: with open(f"resources{sep}json{sep}hilichurls_dictionary.json", "r", encoding="utf8") as f:
self.hilichurls_dictionary = json.load(f) self.hilichurls_dictionary = json.load(f)
@classmethod @handler(CommandHandler, command="hilichurls", block=False)
def create_handlers(cls):
hilichurls = cls()
return [CommandHandler('hilichurls', hilichurls.command_start)]
@error_callable
@restricts() @restricts()
@error_callable
async def command_start(self, update: Update, context: CallbackContext) -> None: async def command_start(self, update: Update, context: CallbackContext) -> None:
"""丘丘语字典.""" message = update.effective_message
message = update.message
user = update.effective_user user = update.effective_user
args = get_all_args(context) args = get_all_args(context)
if len(args) >= 1: 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, message.chat_id, message.message_id)
self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id) self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id)
return 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}"] result = self.hilichurls_dictionary[f"{search}"]
await message.reply_markdown_v2(f"丘丘语: `{search}`\n\n`{result}`") await message.reply_markdown_v2(f"丘丘语: `{search}`\n\n`{result}`")

View File

@ -6,20 +6,20 @@ from datetime import datetime, timedelta
from genshin import GenshinException, DataNotPublic from genshin import GenshinException, DataNotPublic
from telegram import Update from telegram import Update
from telegram.constants import ChatAction 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.cookies.services import CookiesService
from core.plugin import Plugin, handler
from core.template.services import TemplateService 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 core.user.services import UserService
from logger import Log
from plugins.base import BasePlugins
from utils.bot import get_all_args from utils.bot import get_all_args
from utils.decorators.error import error_callable from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts from utils.decorators.restricts import restricts
from utils.helpers import get_genshin_client from utils.helpers import get_genshin_client
from utils.plugins.manager import listener_plugins_class from utils.log import logger
from utils.service.inject import inject
def check_ledger_month(context: CallbackContext) -> int: def check_ledger_month(context: CallbackContext) -> int:
@ -50,13 +50,9 @@ def check_ledger_month(context: CallbackContext) -> int:
return now_time.month return now_time.month
@listener_plugins_class() class Ledger(Plugin, BasePlugin):
class Ledger(BasePlugins):
"""旅行札记""" """旅行札记"""
COMMAND_RESULT, = range(10200, 10201)
@inject
def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None, def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None,
template_service: TemplateService = None): template_service: TemplateService = None):
self.template_service = template_service self.template_service = template_service
@ -64,12 +60,6 @@ class Ledger(BasePlugins):
self.user_service = user_service self.user_service = user_service
self.current_dir = os.getcwd() 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: async def _start_get_ledger(self, client, month=None) -> bytes:
try: try:
diary_info = await client.get_diary(client.uid, month=month) diary_info = await client.get_diary(client.uid, month=month)
@ -142,8 +132,10 @@ class Ledger(BasePlugins):
evaluate=evaluate) evaluate=evaluate)
return png_data return png_data
@handler(CommandHandler, command="ledger", block=False)
@handler(MessageHandler, filters=filters.Regex("^旅行扎记(.*)"), block=False)
@restricts()
@error_callable @error_callable
@restricts(return_data=ConversationHandler.END)
async def command_start(self, update: Update, context: CallbackContext) -> None: async def command_start(self, update: Update, context: CallbackContext) -> None:
user = update.effective_user user = update.effective_user
message = update.message 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, reply_message.chat_id, reply_message.message_id, 30)
self._add_delete_message_job(context, message.chat_id, message.message_id, 30) self._add_delete_message_job(context, message.chat_id, message.message_id, 30)
return 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) await update.message.reply_chat_action(ChatAction.TYPING)
try: 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) png_data = await self._start_get_ledger(client, month)
except UserNotFoundError: except (UserNotFoundError, CookiesNotFoundError):
reply_message = await message.reply_text("未查询到账号信息,请先私聊派蒙绑定账号") reply_message = await message.reply_text("未查询到账号信息,请先私聊派蒙绑定账号")
if filters.ChatType.GROUPS.filter(message): 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, reply_message.chat_id, reply_message.message_id, 30)

View File

@ -1,7 +1,5 @@
from utils.plugins.manager import listener_plugins_class
from .map import Map from .map import Map
@listener_plugins_class()
class MapPlugins(Map): class MapPlugins(Map):
pass pass

View File

@ -5,34 +5,29 @@ from telegram import Update
from telegram.constants import ChatAction from telegram.constants import ChatAction
from telegram.ext import CommandHandler, MessageHandler, filters, CallbackContext from telegram.ext import CommandHandler, MessageHandler, filters, CallbackContext
from logger import Log from core.baseplugin import BasePlugin
from plugins.base import BasePlugins from core.plugin import handler, Plugin
from utils.decorators.error import error_callable from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts from utils.decorators.restricts import restricts
from utils.log import logger
from .model import MapHelper from .model import MapHelper
class Map(BasePlugins): class Map(Plugin, BasePlugin):
"""支持资源点查询""" """支持资源点查询"""
def __init__(self): def __init__(self):
self.init_resource_map = False self.init_resource_map = False
self.map_helper = MapHelper() 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): async def init_point_list_and_map(self):
Log.info("正在初始化地图资源节点") logger.info("正在初始化地图资源节点")
if not self.init_resource_map: if not self.init_resource_map:
await self.map_helper.init_point_list_and_map() await self.map_helper.init_point_list_and_map()
self.init_resource_map = True self.init_resource_map = True
@handler(CommandHandler, command="map", block=False)
@handler(MessageHandler, filters=filters.Regex("^资源点查询(.*)"), block=False)
@error_callable @error_callable
@restricts(restricts_time=20) @restricts(restricts_time=20)
async def command_start(self, update: Update, context: CallbackContext): async def command_start(self, update: Update, context: CallbackContext):
@ -45,7 +40,7 @@ class Map(BasePlugins):
if len(args) >= 1: if len(args) >= 1:
resource_name = args[0] resource_name = args[0]
else: 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") await message.reply_text("请输入要查找的资源,或私聊派蒙发送 `/map list` 查看资源列表", parse_mode="Markdown")
return return
if resource_name in ("list", "列表"): 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, reply_message.chat_id, reply_message.message_id, 300)
self._add_delete_message_job(context, message.chat_id, message.message_id, 300) self._add_delete_message_job(context, message.chat_id, message.message_id, 300)
return 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() text = self.map_helper.get_resource_list_mes()
await message.reply_text(text) await message.reply_text(text)
return 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) text = await self.map_helper.get_resource_map_mes(resource_name)
if "不知道" in text or "没有找到" in text: if "不知道" in text or "没有找到" in text:
await message.reply_text(text, parse_mode="Markdown") await message.reply_text(text, parse_mode="Markdown")

View File

@ -2,41 +2,32 @@ from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.constants import ChatAction, ParseMode from telegram.constants import ChatAction, ParseMode
from telegram.ext import filters, ConversationHandler, CommandHandler, MessageHandler, CallbackContext from telegram.ext import filters, ConversationHandler, CommandHandler, MessageHandler, CallbackContext
from core.baseplugin import BasePlugin
from core.game.services import GameMaterialService from core.game.services import GameMaterialService
from logger import Log from core.plugin import Plugin, handler
from plugins.base import BasePlugins
from utils.bot import get_all_args from utils.bot import get_all_args
from utils.decorators.error import error_callable from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts from utils.decorators.restricts import restricts
from utils.helpers import url_to_file from utils.helpers import url_to_file
from utils.plugins.manager import listener_plugins_class from utils.log import logger
from utils.service.inject import inject
@listener_plugins_class() class Material(Plugin, BasePlugin):
class Material(BasePlugins):
"""角色培养素材查询""" """角色培养素材查询"""
KEYBOARD = [[InlineKeyboardButton( KEYBOARD = [[InlineKeyboardButton(
text="查看角色培养素材列表并查询", text="查看角色培养素材列表并查询",
switch_inline_query_current_chat="查看角色培养素材列表并查询")]] switch_inline_query_current_chat="查看角色培养素材列表并查询")]]
@inject
def __init__(self, game_material_service: GameMaterialService = None): def __init__(self, game_material_service: GameMaterialService = None):
self.game_material_service = game_material_service self.game_material_service = game_material_service
@classmethod @handler(CommandHandler, command="material", block=False)
def create_handlers(cls) -> list: @handler(MessageHandler, filters=filters.Regex("^角色培养素材查询(.*)"), block=False)
material = cls()
return [
CommandHandler("material", material.command_start, block=False),
MessageHandler(filters.Regex("^角色培养素材查询(.*)"), material.command_start, block=False),
]
@error_callable
@restricts(return_data=ConversationHandler.END) @restricts(return_data=ConversationHandler.END)
@error_callable
async def command_start(self, update: Update, context: CallbackContext) -> None: async def command_start(self, update: Update, context: CallbackContext) -> None:
message = update.message message = update.effective_message
user = update.effective_user user = update.effective_user
args = get_all_args(context) args = get_all_args(context)
if len(args) >= 1: 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, message.chat_id, message.message_id)
self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id) self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id)
return 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) await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
file_path = await url_to_file(url, "") file_path = await url_to_file(url, "")
caption = "From 米游社 " \ caption = "From 米游社 " \

View File

@ -4,17 +4,18 @@ from bs4 import BeautifulSoup
from telegram import Update, ReplyKeyboardMarkup, ReplyKeyboardRemove, InputMediaPhoto from telegram import Update, ReplyKeyboardMarkup, ReplyKeyboardRemove, InputMediaPhoto
from telegram.constants import ParseMode, MessageLimit from telegram.constants import ParseMode, MessageLimit
from telegram.error import BadRequest 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 telegram.helpers import escape_markdown
from config import config from core.baseplugin import BasePlugin
from logger import Log from core.bot import bot
from models.apihelper.base import ArtworkImage from core.plugin import Plugin, conversation, handler
from models.apihelper.hyperion import Hyperion from modules.apihelper.base import ArtworkImage
from plugins.base import BasePlugins from modules.apihelper.hyperion import Hyperion
from utils.decorators.admins import bot_admins_rights_check from utils.decorators.admins import bot_admins_rights_check
from utils.decorators.error import error_callable 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: class PostHandlerData:
@ -27,44 +28,27 @@ class PostHandlerData:
self.tags: Optional[List[str]] = [] self.tags: Optional[List[str]] = []
@listener_plugins_class() CHECK_POST, SEND_POST, CHECK_COMMAND, GTE_DELETE_PHOTO = range(10900, 10904)
class Post(BasePlugins): 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) MENU_KEYBOARD = ReplyKeyboardMarkup([["推送频道", "添加TAG"], ["编辑文字", "删除图片"], ["退出"]], True, True)
def __init__(self): def __init__(self):
self.bbs = Hyperion() self.bbs = Hyperion()
@classmethod @conversation.entry_point
def create_handlers(cls): @handler.command(command='post', filters=filters.ChatType.PRIVATE, block=True)
post = cls() @restricts()
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]
@bot_admins_rights_check @bot_admins_rights_check
@error_callable @error_callable
async def command_start(self, update: Update, context: CallbackContext) -> int: async def command_start(self, update: Update, context: CallbackContext) -> int:
user = update.effective_user user = update.effective_user
message = update.message message = update.effective_message
Log.info(f"用户 {user.full_name}[{user.id}] POST命令请求") logger.info(f"用户 {user.full_name}[{user.id}] POST命令请求")
post_handler_data = context.chat_data.get("post_handler_data") post_handler_data = context.chat_data.get("post_handler_data")
if post_handler_data is None: if post_handler_data is None:
post_handler_data = PostHandlerData() post_handler_data = PostHandlerData()
@ -76,11 +60,13 @@ class Post(BasePlugins):
await message.reply_text(text, reply_markup=ReplyKeyboardMarkup(reply_keyboard, True, True)) await message.reply_text(text, reply_markup=ReplyKeyboardMarkup(reply_keyboard, True, True))
return self.CHECK_POST return self.CHECK_POST
@conversation.state(state=CHECK_POST)
@handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True)
@error_callable @error_callable
async def check_post(self, update: Update, context: CallbackContext) -> int: async def check_post(self, update: Update, context: CallbackContext) -> int:
post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data") post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data")
message = update.message message = update.effective_message
if update.message.text == "退出": if message.text == "退出":
await message.reply_text("退出投稿", reply_markup=ReplyKeyboardRemove()) await message.reply_text("退出投稿", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
@ -114,9 +100,10 @@ class Post(BasePlugins):
else: else:
await message.reply_text("图片获取错误", reply_markup=ReplyKeyboardRemove()) # excuse? await message.reply_text("图片获取错误", reply_markup=ReplyKeyboardRemove()) # excuse?
return ConversationHandler.END return ConversationHandler.END
except (BadRequest, TypeError) as error: except (BadRequest, TypeError) as exc:
await message.reply_text("发送图片时发生错误,错误信息已经写到日记", reply_markup=ReplyKeyboardRemove()) await message.reply_text("发送图片时发生错误,错误信息已经写到日记", reply_markup=ReplyKeyboardRemove())
Log.error("Post模块发送图片时发生错误", error) logger.error("Post模块发送图片时发生错误")
logger.exception(exc)
return ConversationHandler.END return ConversationHandler.END
post_handler_data.post_text = post_text post_handler_data.post_text = post_text
post_handler_data.post_images = post_images post_handler_data.post_images = post_images
@ -126,9 +113,11 @@ class Post(BasePlugins):
await message.reply_text("请选择你的操作", reply_markup=self.MENU_KEYBOARD) await message.reply_text("请选择你的操作", reply_markup=self.MENU_KEYBOARD)
return self.CHECK_COMMAND return self.CHECK_COMMAND
@conversation.state(state=CHECK_COMMAND)
@handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True)
@error_callable @error_callable
async def check_command(self, update: Update, context: CallbackContext) -> int: async def check_command(self, update: Update, context: CallbackContext) -> int:
message = update.message message = update.effective_message
if message.text == "退出": if message.text == "退出":
await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
@ -150,11 +139,13 @@ class Post(BasePlugins):
f"当前一共有 {photo_len} 张图片") f"当前一共有 {photo_len} 张图片")
return self.GTE_DELETE_PHOTO return self.GTE_DELETE_PHOTO
@conversation.state(state=GTE_DELETE_PHOTO)
@handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True)
@error_callable @error_callable
async def get_delete_photo(self, update: Update, context: CallbackContext) -> int: async def get_delete_photo(self, update: Update, context: CallbackContext) -> int:
post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data") post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data")
photo_len = len(post_handler_data.post_images) photo_len = len(post_handler_data.post_images)
message = update.message message = update.effective_message
args = message.text.split(" ") args = message.text.split(" ")
index: List[int] = [] index: List[int] = []
try: try:
@ -171,31 +162,34 @@ class Post(BasePlugins):
return self.CHECK_COMMAND return self.CHECK_COMMAND
async def get_channel(self, update: Update, _: CallbackContext) -> int: async def get_channel(self, update: Update, _: CallbackContext) -> int:
message = update.message message = update.effective_message
reply_keyboard = [] reply_keyboard = []
try: try:
for channel_info in config.channels: for channel_info in bot.config.channels:
name = channel_info["name"] name = channel_info["name"]
reply_keyboard.append([f"{name}"]) reply_keyboard.append([f"{name}"])
except KeyError as error: except KeyError as error:
Log.error("从配置文件获取频道信息发生错误,退出任务", error) logger.error("从配置文件获取频道信息发生错误,退出任务", error)
await message.reply_text("从配置文件获取频道信息发生错误,退出任务", reply_markup=ReplyKeyboardRemove()) await message.reply_text("从配置文件获取频道信息发生错误,退出任务", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
await message.reply_text("请选择你要推送的频道", await message.reply_text("请选择你要推送的频道",
reply_markup=ReplyKeyboardMarkup(reply_keyboard, True, True)) reply_markup=ReplyKeyboardMarkup(reply_keyboard, True, True))
return self.GET_POST_CHANNEL return self.GET_POST_CHANNEL
@conversation.state(state=GET_POST_CHANNEL)
@handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True)
@error_callable @error_callable
async def get_post_channel(self, update: Update, context: CallbackContext) -> int: async def get_post_channel(self, update: Update, context: CallbackContext) -> int:
post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data") post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data")
message = update.message message = update.effective_message
channel_id = -1 channel_id = -1
try: try:
for channel_info in config.channels: for channel_info in bot.config.channels:
if message.text == channel_info["name"]: if message.text == channel_info["name"]:
channel_id = channel_info["chat_id"] channel_id = channel_info["chat_id"]
except KeyError as error: except KeyError as exc:
Log.error("从配置文件获取频道信息发生错误,退出任务", error) logger.error("从配置文件获取频道信息发生错误,退出任务", exc)
logger.exception(exc)
await message.reply_text("从配置文件获取频道信息发生错误,退出任务", reply_markup=ReplyKeyboardRemove()) await message.reply_text("从配置文件获取频道信息发生错误,退出任务", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
if channel_id == -1: if channel_id == -1:
@ -208,14 +202,16 @@ class Post(BasePlugins):
return self.SEND_POST return self.SEND_POST
async def add_tags(self, update: Update, _: CallbackContext) -> int: async def add_tags(self, update: Update, _: CallbackContext) -> int:
message = update.message message = update.effective_message
await message.reply_text("请回复添加的tag名称如果要添加多个tag请以空格作为分隔符不用添加 # 作为开头,推送时程序会自动添加") await message.reply_text("请回复添加的tag名称如果要添加多个tag请以空格作为分隔符不用添加 # 作为开头,推送时程序会自动添加")
return self.GET_TAGS return self.GET_TAGS
@conversation.state(state=GET_TAGS)
@handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True)
@error_callable @error_callable
async def get_tags(self, update: Update, context: CallbackContext) -> int: async def get_tags(self, update: Update, context: CallbackContext) -> int:
post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data") post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data")
message = update.message message = update.effective_message
args = message.text.split(" ") args = message.text.split(" ")
post_handler_data.tags = args post_handler_data.tags = args
await message.reply_text("添加成功") await message.reply_text("添加成功")
@ -223,14 +219,16 @@ class Post(BasePlugins):
return self.CHECK_COMMAND return self.CHECK_COMMAND
async def edit_text(self, update: Update, _: CallbackContext) -> int: async def edit_text(self, update: Update, _: CallbackContext) -> int:
message = update.message message = update.effective_message
await message.reply_text("请回复替换的文本") await message.reply_text("请回复替换的文本")
return self.GET_TEXT return self.GET_TEXT
@conversation.state(state=GET_TEXT)
@handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True)
@error_callable @error_callable
async def get_edit_text(self, update: Update, context: CallbackContext) -> int: async def get_edit_text(self, update: Update, context: CallbackContext) -> int:
post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data") 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 post_handler_data.post_text = message.text_markdown_v2
await message.reply_text("替换成功") await message.reply_text("替换成功")
await message.reply_text("请选择你的操作", reply_markup=self.MENU_KEYBOARD) await message.reply_text("请选择你的操作", reply_markup=self.MENU_KEYBOARD)
@ -240,19 +238,20 @@ class Post(BasePlugins):
@error_callable @error_callable
async def send_post(update: Update, context: CallbackContext) -> int: async def send_post(update: Update, context: CallbackContext) -> int:
post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data") post_handler_data: PostHandlerData = context.chat_data.get("post_handler_data")
message = update.message message = update.effective_message
if update.message.text == "退出": if message.text == "退出":
await message.reply_text(text="退出任务", reply_markup=ReplyKeyboardRemove()) await message.reply_text(text="退出任务", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
await message.reply_text("正在推送", reply_markup=ReplyKeyboardRemove()) await message.reply_text("正在推送", reply_markup=ReplyKeyboardRemove())
channel_id = post_handler_data.channel_id channel_id = post_handler_data.channel_id
channel_name = None channel_name = None
try: try:
for channel_info in config.channels: for channel_info in bot.config.channels:
if post_handler_data.channel_id == channel_info["chat_id"]: if post_handler_data.channel_id == channel_info["chat_id"]:
channel_name = channel_info["name"] channel_name = channel_info["name"]
except KeyError as error: except KeyError as exc:
Log.error("从配置文件获取频道信息发生错误,退出任务", error) logger.error("从配置文件获取频道信息发生错误,退出任务")
logger.exception(exc)
await message.reply_text("从配置文件获取频道信息发生错误,退出任务", reply_markup=ReplyKeyboardRemove()) await message.reply_text("从配置文件获取频道信息发生错误,退出任务", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END
post_text = post_handler_data.post_text post_text = post_handler_data.post_text
@ -277,9 +276,10 @@ class Post(BasePlugins):
else: else:
await message.reply_text("图片获取错误", reply_markup=ReplyKeyboardRemove()) # excuse? await message.reply_text("图片获取错误", reply_markup=ReplyKeyboardRemove()) # excuse?
return ConversationHandler.END return ConversationHandler.END
except (BadRequest, TypeError) as error: except (BadRequest, TypeError) as exc:
await message.reply_text("发送图片时发生错误,错误信息已经写到日记", reply_markup=ReplyKeyboardRemove()) await message.reply_text("发送图片时发生错误,错误信息已经写到日记", reply_markup=ReplyKeyboardRemove())
Log.error("Post模块发送图片时发生错误", error) logger.error("Post模块发送图片时发生错误")
logger.exception(exc)
return ConversationHandler.END return ConversationHandler.END
await message.reply_text("推送成功", reply_markup=ReplyKeyboardRemove()) await message.reply_text("推送成功", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END return ConversationHandler.END

View File

@ -1,81 +1,37 @@
import random import random
import re
from typing import List, Optional
from redis import DataError, ResponseError from telegram import Update, Poll
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, Poll, \
ReplyKeyboardRemove, Message
from telegram.constants import ChatAction from telegram.constants import ChatAction
from telegram.ext import CallbackContext, ConversationHandler, CommandHandler, MessageHandler, filters from telegram.ext import CallbackContext, CommandHandler, filters
from telegram.helpers import escape_markdown
from core.admin import BotAdminService from core.admin import BotAdminService
from core.baseplugin import BasePlugin
from core.plugin import Plugin, handler
from core.quiz import QuizService 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.decorators.restricts import restricts
from utils.plugins.manager import listener_plugins_class from utils.log import logger
from utils.random import MT19937_Random from utils.random import MT19937Random
from utils.service.inject import inject
class QuizCommandData: class QuizPlugin(Plugin, BasePlugin):
question_id: int = -1 """派蒙的十万个为什么"""
new_question: str = ""
new_correct_answer: str = ""
new_wrong_answer: List[str] = []
status: int = 0
@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): def __init__(self, quiz_service: QuizService = None, bot_admin_service: BotAdminService = None):
self.bot_admin_service = bot_admin_service self.bot_admin_service = bot_admin_service
self.user_time = {}
self.quiz_service = quiz_service self.quiz_service = quiz_service
self.time_out = 120 self.time_out = 120
self.random = MT19937_Random() self.random = MT19937Random()
@classmethod @handler(CommandHandler, command="quiz", block=False)
def create_handlers(cls): @restricts(restricts_time=20, try_delete_message=True)
quiz = cls() async def command_start(self, update: Update, context: CallbackContext) -> None:
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
user = update.effective_user 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() question_id_list = await self.quiz_service.get_question_id_list()
if filters.ChatType.GROUPS.filter(update.message): 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: if len(question_id_list) == 0:
return None return None
if len(question_id_list) == 0: if len(question_id_list) == 0:
@ -90,204 +46,12 @@ class QuizPlugin(BasePlugins):
correct_option = answer.text correct_option = answer.text
if correct_option is None: if correct_option is None:
question_id = question["question_id"] 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 return None
random.shuffle(_options) random.shuffle(_options)
index = _options.index(correct_option) index = _options.index(correct_option)
return await update.effective_message.reply_poll(question.text, _options, poll_message = await update.effective_message.reply_poll(question.text, _options,
correct_option_id=index, is_anonymous=False, correct_option_id=index, is_anonymous=False,
open_period=self.time_out, type=Poll.QUIZ) open_period=self.time_out, type=Poll.QUIZ)
self._add_delete_message_job(context, update.message.chat_id, update.message.message_id, 300)
@restricts(filters.ChatType.GROUPS, ConversationHandler.END, restricts_time=20, try_delete_message=True) self._add_delete_message_job(context, poll_message.chat_id, poll_message.message_id, 300)
@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

178
plugins/genshin/set_uid.py Normal file
View File

@ -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

View File

@ -3,72 +3,65 @@ import time
from genshin import Game, GenshinException, AlreadyClaimed, Client from genshin import Game, GenshinException, AlreadyClaimed, Client
from telegram import Update 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.cookies.services import CookiesService
from core.plugin import Plugin, handler
from core.sign.models import Sign as SignUser, SignStatusEnum from core.sign.models import Sign as SignUser, SignStatusEnum
from core.sign.services import SignServices 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 core.user.services import UserService
from logger import Log
from plugins.base import BasePlugins
from utils.bot import get_all_args from utils.bot import get_all_args
from utils.decorators.error import error_callable from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts from utils.decorators.restricts import restricts
from utils.helpers import get_genshin_client from utils.helpers import get_genshin_client
from utils.plugins.manager import listener_plugins_class from utils.log import logger
from utils.service.inject import inject
@listener_plugins_class() class Sign(Plugin, BasePlugin):
class Sign(BasePlugins):
"""每日签到""" """每日签到"""
CHECK_SERVER, COMMAND_RESULT = range(10400, 10402) CHECK_SERVER, COMMAND_RESULT = range(10400, 10402)
@inject
def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None, def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None,
sign_service: SignServices = None): sign_service: SignServices = None):
self.cookies_service = cookies_service self.cookies_service = cookies_service
self.user_service = user_service self.user_service = user_service
self.sign_service = sign_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 @staticmethod
async def _start_sign(client: Client) -> str: async def _start_sign(client: Client) -> str:
try: try:
rewards = await client.get_monthly_rewards(game=Game.GENSHIN, lang="zh-cn") rewards = await client.get_monthly_rewards(game=Game.GENSHIN, lang="zh-cn")
except GenshinException as error: 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)}" return f"获取签到信息失败API返回信息为 {str(error)}"
try: try:
daily_reward_info = await client.get_reward_info(game=Game.GENSHIN, lang="zh-cn") # 获取签到信息失败 daily_reward_info = await client.get_reward_info(game=Game.GENSHIN, lang="zh-cn") # 获取签到信息失败
except GenshinException as error: 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)}" return f"获取签到状态失败API返回信息为 {str(error)}"
if not daily_reward_info.signed_in: if not daily_reward_info.signed_in:
try: try:
request_daily_reward = await client.request_daily_reward("sign", method="POST", request_daily_reward = await client.request_daily_reward("sign", method="POST",
game=Game.GENSHIN, lang="zh-cn") 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: 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} 签到失败,触发验证码风控,请尝试重新签到。" return f"UID {client.uid} 签到失败,触发验证码风控,请尝试重新签到。"
except AlreadyClaimed: except AlreadyClaimed:
result = "今天旅行者已经签到过了~" result = "今天旅行者已经签到过了~"
except GenshinException as error: 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)}" return f"获取签到状态失败API返回信息为 {str(error)}"
else: else:
result = "OK" result = "OK"
else: else:
result = "今天旅行者已经签到过了~" 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)] 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()) today = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
cn_timezone = datetime.timezone(datetime.timedelta(hours=8)) 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: async def _process_auto_sign(self, user_id: int, chat_id: int, method: str) -> str:
try: try:
await get_genshin_client(user_id, self.user_service, self.cookies_service) await get_genshin_client(user_id)
except UserNotFoundError: except (UserNotFoundError, CookiesNotFoundError):
return "未查询到账号信息,请先私聊派蒙绑定账号" return "未查询到账号信息,请先私聊派蒙绑定账号"
user: SignUser = await self.sign_service.get_by_user_id(user_id) user: SignUser = await self.sign_service.get_by_user_id(user_id)
if user: if user:
@ -107,11 +100,13 @@ class Sign(BasePlugins):
await self.sign_service.add(user) await self.sign_service.add(user)
return "开启自动签到成功" return "开启自动签到成功"
@handler(CommandHandler, command="sign", block=False)
@handler(MessageHandler, filters=filters.Regex("^每日签到(.*)"), block=False)
@restricts()
@error_callable @error_callable
@restricts(return_data=ConversationHandler.END)
async def command_start(self, update: Update, context: CallbackContext) -> None: async def command_start(self, update: Update, context: CallbackContext) -> None:
user = update.effective_user user = update.effective_user
message = update.message message = update.effective_message
args = get_all_args(context) args = get_all_args(context)
if len(args) >= 1: if len(args) >= 1:
msg = None msg = None
@ -120,22 +115,22 @@ class Sign(BasePlugins):
elif args[0] == "关闭自动签到": elif args[0] == "关闭自动签到":
msg = await self._process_auto_sign(user.id, message.chat_id, "关闭") msg = await self._process_auto_sign(user.id, message.chat_id, "关闭")
if msg: 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) reply_message = await message.reply_text(msg)
if filters.ChatType.GROUPS.filter(message): 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, reply_message.chat_id, reply_message.message_id, 30)
self._add_delete_message_job(context, message.chat_id, message.message_id, 30) self._add_delete_message_job(context, message.chat_id, message.message_id, 30)
return return
Log.info(f"用户 {user.full_name}[{user.id}] 每日签到命令请求") logger.info(f"用户 {user.full_name}[{user.id}] 每日签到命令请求")
if filters.ChatType.GROUPS.filter(message): if filters.ChatType.GROUPS.filter(message):
self._add_delete_message_job(context, message.chat_id, message.message_id) self._add_delete_message_job(context, message.chat_id, message.message_id)
try: 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) sign_text = await self._start_sign(client)
reply_message = await message.reply_text(sign_text, allow_sending_without_reply=True) reply_message = await message.reply_text(sign_text, allow_sending_without_reply=True)
if filters.ChatType.GROUPS.filter(reply_message): if filters.ChatType.GROUPS.filter(reply_message):
self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id) 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("未查询到账号信息,请先私聊派蒙绑定账号") reply_message = await message.reply_text("未查询到账号信息,请先私聊派蒙绑定账号")
if filters.ChatType.GROUPS.filter(message): 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, reply_message.chat_id, reply_message.message_id, 30)

View File

@ -1,40 +1,34 @@
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.constants import ChatAction, ParseMode from telegram import Update
from telegram.ext import filters, ConversationHandler, CommandHandler, MessageHandler, CallbackContext 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 core.game.services import GameStrategyService
from logger import Log from core.plugin import Plugin, handler
from plugins.base import BasePlugins
from utils.bot import get_all_args from utils.bot import get_all_args
from utils.decorators.error import error_callable from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts from utils.decorators.restricts import restricts
from utils.helpers import url_to_file from utils.helpers import url_to_file
from utils.plugins.manager import listener_plugins_class from utils.log import logger
from utils.service.inject import inject
@listener_plugins_class() class StrategyPlugin(Plugin, BasePlugin):
class Strategy(BasePlugins):
"""角色攻略查询""" """角色攻略查询"""
KEYBOARD = [[InlineKeyboardButton(text="查看角色攻略列表并查询", switch_inline_query_current_chat="查看角色攻略列表并查询")]] KEYBOARD = [[InlineKeyboardButton(text="查看角色攻略列表并查询", switch_inline_query_current_chat="查看角色攻略列表并查询")]]
@inject
def __init__(self, game_strategy_service: GameStrategyService = None): def __init__(self, game_strategy_service: GameStrategyService = None):
self.game_strategy_service = game_strategy_service self.game_strategy_service = game_strategy_service
@classmethod @handler(CommandHandler, command="strategy", block=False)
def create_handlers(cls) -> list: @handler(MessageHandler, filters=filters.Regex("^角色攻略查询(.*)"), block=False)
strategy = cls() @restricts()
return [
CommandHandler("strategy", strategy.command_start, block=False),
MessageHandler(filters.Regex("^角色攻略查询(.*)"), strategy.command_start, block=False),
]
@error_callable @error_callable
@restricts(return_data=ConversationHandler.END)
async def command_start(self, update: Update, context: CallbackContext) -> None: async def command_start(self, update: Update, context: CallbackContext) -> None:
message = update.message message = update.effective_message
user = update.effective_user user = update.effective_user
args = get_all_args(context) args = get_all_args(context)
if len(args) >= 1: 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, message.chat_id, message.message_id)
self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id) self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id)
return 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) await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
file_path = await url_to_file(url, "") file_path = await url_to_file(url, "")
caption = "From 米游社 西风驿站 " \ caption = "From 米游社 西风驿站 " \

View File

@ -7,39 +7,28 @@ from telegram import Update
from telegram.constants import ChatAction from telegram.constants import ChatAction
from telegram.ext import CallbackContext, CommandHandler, MessageHandler, ConversationHandler, filters 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.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.error import error_callable
from utils.decorators.restricts import restricts from utils.decorators.restricts import restricts
from utils.helpers import url_to_file, get_genshin_client from utils.helpers import url_to_file, get_genshin_client, get_public_genshin_client
from utils.plugins.manager import listener_plugins_class from utils.log import logger
from utils.service.inject import inject
@listener_plugins_class() class TeapotUnlocked(Exception):
class UserStats(BasePlugins): """尘歌壶未解锁"""
class UserStatsPlugins(Plugin, BasePlugin):
"""玩家统计查询""" """玩家统计查询"""
COMMAND_RESULT, = range(10200, 10201) def __init__(self, template_service: TemplateService = None):
@inject
def __init__(self, user_service: UserService = None, cookies_service: CookiesService = None,
template_service: TemplateService = None):
self.template_service = template_service self.template_service = template_service
self.cookies_service = cookies_service
self.user_service = user_service
self.current_dir = os.getcwd() 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: async def _start_get_user_info(self, client: Client, uid: int = -1) -> bytes:
if uid == -1: if uid == -1:
_uid = client.uid _uid = client.uid
@ -47,24 +36,22 @@ class UserStats(BasePlugins):
_uid = uid _uid = uid
try: try:
user_info = await client.get_genshin_user(_uid) user_info = await client.get_genshin_user(_uid)
except GenshinException as error: except GenshinException as exc:
Log.warning("get_record_card请求失败", error) raise exc
raise error
if user_info.teapot is None: if user_info.teapot is None:
raise ValueError("洞庭湖未解锁") raise TeapotUnlocked
try: try:
# 查询的UID如果是自己的会返回DataNotPublic自己查不了自己可还行...... # 查询的UID如果是自己的会返回DataNotPublic自己查不了自己可还行......
if uid > 0: if uid > 0:
record_card_info = await client.get_record_card(uid) record_card_info = await client.get_record_card(uid)
else: else:
record_card_info = await client.get_record_card() record_card_info = await client.get_record_card()
except DataNotPublic as error: except DataNotPublic:
Log.warning("get_record_card请求失败 查询的用户数据未公开", error) logger.warning("get_record_card请求失败 查询的用户数据未公开")
nickname = _uid nickname = _uid
user_uid = "" user_uid = ""
except GenshinException as error: except GenshinException as exc:
Log.warning("get_record_card请求失败", error) raise exc
raise error
else: else:
nickname = record_card_info.nickname nickname = record_card_info.nickname
user_uid = record_card_info.uid user_uid = record_card_info.uid
@ -129,24 +116,31 @@ class UserStats(BasePlugins):
{"width": 1024, "height": 1024}) {"width": 1024, "height": 1024})
return png_data return png_data
@error_callable @handler(CommandHandler, command="stats", block=False)
@handler(MessageHandler, filters=filters.Regex("^玩家统计查询(.*)"), block=False)
@restricts(return_data=ConversationHandler.END) @restricts(return_data=ConversationHandler.END)
@error_callable
async def command_start(self, update: Update, context: CallbackContext) -> Optional[int]: async def command_start(self, update: Update, context: CallbackContext) -> Optional[int]:
user = update.effective_user user = update.effective_user
message = update.message message = update.effective_message
Log.info(f"用户 {user.full_name}[{user.id}] 查询游戏用户命令请求") logger.info(f"用户 {user.full_name}[{user.id}] 查询游戏用户命令请求")
uid: int = -1 uid: int = -1
try: try:
args = context.args args = context.args
if args is not None and len(args) >= 1: if args is not None and len(args) >= 1:
uid = int(args[0]) uid = int(args[0])
except ValueError as error: except ValueError as exc:
Log.error("获取 uid 发生错误! 错误信息为", error) logger.error("获取 uid 发生错误! 错误信息为")
logger.exception(exc)
await message.reply_text("输入错误") await message.reply_text("输入错误")
return ConversationHandler.END return ConversationHandler.END
try: 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) png_data = await self._start_get_user_info(client, uid)
except UserNotFoundError: except UserNotFoundError:
reply_message = await message.reply_text("未查询到账号信息,请先私聊派蒙绑定账号") 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) self._add_delete_message_job(context, message.chat_id, message.message_id, 30)
return return
except ValueError as exc: except TeapotUnlocked:
if "洞庭湖未解锁" not in str(exc):
raise exc
await message.reply_text("角色尘歌壶未解锁 如果想要查看具体数据 嗯...... 咕咕咕~") await message.reply_text("角色尘歌壶未解锁 如果想要查看具体数据 嗯...... 咕咕咕~")
return ConversationHandler.END return ConversationHandler.END
except AttributeError as exc: except AttributeError as exc:
Log.warning("角色数据有误", exc) logger.warning("角色数据有误", exc)
await message.reply_text("角色数据有误 估计是派蒙晕了") await message.reply_text("角色数据有误 估计是派蒙晕了")
return ConversationHandler.END return ConversationHandler.END
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)

View File

@ -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.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 core.wiki.services import WikiService
from logger import Log
from metadata.shortname import weaponToName from metadata.shortname import weaponToName
from models.wiki.base import SCRAPE_HOST from modules.wiki.base import SCRAPE_HOST
from models.wiki.weapon import Weapon from modules.wiki.weapon import Weapon
from plugins.base import BasePlugins
from utils.bot import get_all_args from utils.bot import get_all_args
from utils.decorators.error import error_callable from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts from utils.decorators.restricts import restricts
from utils.helpers import url_to_file from utils.helpers import url_to_file
from utils.plugins.manager import listener_plugins_class from utils.log import logger
from utils.service.inject import inject
@listener_plugins_class() class WeaponPlugin(Plugin, BasePlugin):
class WeaponPlugin(BasePlugins):
"""武器查询""" """武器查询"""
KEYBOARD = [[ KEYBOARD = [[
InlineKeyboardButton(text="查看武器列表并查询", switch_inline_query_current_chat="查看武器列表并查询") InlineKeyboardButton(text="查看武器列表并查询", switch_inline_query_current_chat="查看武器列表并查询")
]] ]]
@inject
def __init__(self, template_service: TemplateService = None, wiki_service: WikiService = None): def __init__(self, template_service: TemplateService = None, wiki_service: WikiService = None):
self.wiki_service = wiki_service self.wiki_service = wiki_service
self.template_service = template_service self.template_service = template_service
@classmethod @handler(CommandHandler, command="weapon", block=False)
def create_handlers(cls) -> list: @handler(MessageHandler, filters=filters.Regex("^武器查询(.*)"), block=False)
weapon = cls()
return [
CommandHandler("weapon", weapon.command_start, block=False),
MessageHandler(filters.Regex("^武器查询(.*)"), weapon.command_start, block=False)
]
@error_callable @error_callable
@restricts() @restricts()
async def command_start(self, update: Update, context: CallbackContext) -> None: async def command_start(self, update: Update, context: CallbackContext) -> None:
message = update.message message = update.effective_message
user = update.effective_user user = update.effective_user
args = get_all_args(context) args = get_all_args(context)
if len(args) >= 1: 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, message.chat_id, message.message_id)
self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id) self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id)
return 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) await message.reply_chat_action(ChatAction.TYPING)
async def input_template_data(_weapon_data: Weapon): async def input_template_data(_weapon_data: Weapon):
@ -85,7 +78,7 @@ class WeaponPlugin(BasePlugins):
"weapon_info_max_level": _weapon_data.stats[-1].level, "weapon_info_max_level": _weapon_data.stats[-1].level,
"progression_base_atk": round(_weapon_data.stats[-1].ATK), "progression_base_atk": round(_weapon_data.stats[-1].ATK),
"weapon_info_source_list": [ "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:] for mid in _weapon_data.ascension[-3:]
], ],
"special_ability_name": _weapon_data.affix.name, "special_ability_name": _weapon_data.affix.name,
@ -101,7 +94,7 @@ class WeaponPlugin(BasePlugins):
"weapon_info_max_level": _weapon_data.stats[-1].level, "weapon_info_max_level": _weapon_data.stats[-1].level,
"progression_base_atk": round(_weapon_data.stats[-1].ATK), "progression_base_atk": round(_weapon_data.stats[-1].ATK),
"weapon_info_source_list": [ "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:] for mid in _weapon_data.ascension[-3:]
], ],
"special_ability_name": '', "special_ability_name": '',

View File

@ -1,33 +1,21 @@
from telegram import Update from telegram import Update
from telegram.ext import CommandHandler, CallbackContext from telegram.ext import CommandHandler, CallbackContext
from core.plugin import Plugin, handler
from core.wiki.services import WikiService from core.wiki.services import WikiService
from plugins.base import BasePlugins
from utils.decorators.admins import bot_admins_rights_check 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(Plugin):
class Wiki(BasePlugins):
"""有关WIKI操作""" """有关WIKI操作"""
@inject
def __init__(self, wiki_service: WikiService = None): def __init__(self, wiki_service: WikiService = None):
self.wiki_service = wiki_service self.wiki_service = wiki_service
@classmethod @handler(CommandHandler, command="refresh_wiki", block=False)
def create_handlers(cls) -> list:
wiki = cls()
return [
CommandHandler("refresh_wiki", wiki.refresh_wiki, block=False),
]
@bot_admins_rights_check @bot_admins_rights_check
@error_callable
async def refresh_wiki(self, update: Update, _: CallbackContext): async def refresh_wiki(self, update: Update, _: CallbackContext):
message = update.message message = update.effective_message
await message.reply_text("正在刷新Wiki缓存请稍等") await message.reply_text("正在刷新Wiki缓存请稍等")
await self.wiki_service.refresh_wiki() await self.wiki_service.refresh_wiki()
await message.reply_text("刷新Wiki缓存成功") await message.reply_text("刷新Wiki缓存成功")

Some files were not shown because too many files have changed in this diff Show More