diff --git a/alembic/versions/1df05b897d3f_tasks.py b/alembic/versions/1df05b897d3f_tasks.py new file mode 100644 index 0000000..118aed3 --- /dev/null +++ b/alembic/versions/1df05b897d3f_tasks.py @@ -0,0 +1,134 @@ +"""tasks + +Revision ID: 1df05b897d3f +Revises: a1c10da5704b +Create Date: 2023-07-23 14:44:59.592519 + +""" +import logging + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import text +from sqlalchemy.dialects import mysql +from sqlalchemy.exc import NoSuchTableError + +# revision identifiers, used by Alembic. +revision = "1df05b897d3f" +down_revision = "a1c10da5704b" +branch_labels = None +depends_on = None + +logger = logging.getLogger(__name__) + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + connection = op.get_bind() + task_table = op.create_table( + "task", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=True), + sa.Column("chat_id", sa.BigInteger(), nullable=True), + sa.Column( + "time_created", + sa.DateTime(), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("time_updated", sa.DateTime(), nullable=True), + sa.Column( + "type", + sa.Enum( + "SIGN", + "RESIN", + "REALM", + "EXPEDITION", + "TRANSFORMER", + "CARD", + name="tasktypeenum", + ), + nullable=True, + ), + sa.Column( + "status", + sa.Enum( + "STATUS_SUCCESS", + "INVALID_COOKIES", + "ALREADY_CLAIMED", + "NEED_CHALLENGE", + "GENSHIN_EXCEPTION", + "TIMEOUT_ERROR", + "BAD_REQUEST", + "FORBIDDEN", + name="taskstatusenum", + ), + nullable=True, + ), + sa.Column("data", sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint("id"), + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", + ) + op.create_index("task_1", "task", ["user_id"], unique=False) + try: + statement = "SELECT * FROM sign;" + old_sign_table_data = connection.execute(text(statement)) + except NoSuchTableError: + logger.warning("Table 'sign' doesn't exist") + return # should not happen + if old_sign_table_data is not None: + for row in old_sign_table_data: + try: + user_id = row["user_id"] + chat_id = row["chat_id"] + time_created = row["time_created"] + time_updated = row["time_updated"] + status = row["status"] + task_type = "SIGN" + insert = task_table.insert().values( + user_id=int(user_id), + chat_id=int(chat_id), + time_created=time_created, + time_updated=time_updated, + type=task_type, + status=status, + ) + with op.get_context().autocommit_block(): + connection.execute(insert) + except Exception as exc: # pylint: disable=W0703 + logger.error("Process sign->task Exception", exc_info=exc) # pylint: disable=W0703 + op.drop_table("sign") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "sign", + sa.Column("id", mysql.INTEGER(), autoincrement=False, nullable=False), + sa.Column("user_id", mysql.BIGINT(), autoincrement=False, nullable=False), + sa.Column("chat_id", mysql.BIGINT(), autoincrement=False, nullable=True), + sa.Column("time_created", mysql.DATETIME(), nullable=True), + sa.Column("time_updated", mysql.DATETIME(), nullable=True), + sa.Column( + "status", + mysql.ENUM( + "STATUS_SUCCESS", + "INVALID_COOKIES", + "ALREADY_CLAIMED", + "GENSHIN_EXCEPTION", + "TIMEOUT_ERROR", + "BAD_REQUEST", + "FORBIDDEN", + ), + nullable=True, + ), + sa.PrimaryKeyConstraint("id", "user_id"), + mysql_collate="utf8mb4_general_ci", + mysql_default_charset="utf8mb4", + mysql_engine="InnoDB", + ) + op.drop_index("task_1", table_name="task") + op.drop_table("task") + # ### end Alembic commands ### diff --git a/core/services/sign/__init__.py b/core/services/sign/__init__.py deleted file mode 100644 index 9b51e2f..0000000 --- a/core/services/sign/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""SignService""" diff --git a/core/services/sign/services.py b/core/services/sign/services.py deleted file mode 100644 index 74de7aa..0000000 --- a/core/services/sign/services.py +++ /dev/null @@ -1,28 +0,0 @@ -from core.base_service import BaseService -from core.services.sign.models import Sign -from core.services.sign.repositories import SignRepository - -__all__ = ["SignServices"] - - -class SignServices(BaseService): - def __init__(self, sign_repository: SignRepository) -> None: - self._repository: SignRepository = sign_repository - - async def get_all(self): - return await self._repository.get_all() - - async def add(self, sign: Sign): - return await self._repository.add(sign) - - async def remove(self, sign: Sign): - return await self._repository.remove(sign) - - async def update(self, sign: Sign): - return await self._repository.update(sign) - - async def get_by_user_id(self, user_id: int): - return await self._repository.get_by_user_id(user_id) - - async def get_by_chat_id(self, chat_id: int): - return await self._repository.get_by_chat_id(chat_id) diff --git a/core/services/task/__init__.py b/core/services/task/__init__.py new file mode 100644 index 0000000..0835fdf --- /dev/null +++ b/core/services/task/__init__.py @@ -0,0 +1 @@ +"""TaskService""" diff --git a/core/services/sign/models.py b/core/services/task/models.py similarity index 56% rename from core/services/sign/models.py rename to core/services/task/models.py index dd62239..d8a3cd0 100644 --- a/core/services/sign/models.py +++ b/core/services/task/models.py @@ -1,15 +1,15 @@ import enum from datetime import datetime -from typing import Optional +from typing import Optional, Dict, Any -from sqlalchemy import func, BigInteger +from sqlalchemy import func, BigInteger, JSON from sqlmodel import Column, DateTime, Enum, Field, SQLModel, Integer -__all__ = ("SignStatusEnum", "Sign") +__all__ = ("Task", "TaskStatusEnum", "TaskTypeEnum") -class SignStatusEnum(int, enum.Enum): - STATUS_SUCCESS = 0 # 签到成功 +class TaskStatusEnum(int, enum.Enum): + STATUS_SUCCESS = 0 # 任务执行成功 INVALID_COOKIES = 1 # Cookie无效 ALREADY_CLAIMED = 2 # 已经获取奖励 NEED_CHALLENGE = 3 # 需要验证码 @@ -19,15 +19,26 @@ class SignStatusEnum(int, enum.Enum): FORBIDDEN = 7 # 这错误一般为通知失败 机器人被用户BAN -class Sign(SQLModel, table=True): +class TaskTypeEnum(int, enum.Enum): + SIGN = 0 # 签到 + RESIN = 1 # 开拓力 + REALM = 2 # 洞天宝钱 + EXPEDITION = 3 # 委托 + TRANSFORMER = 4 # 参量质变仪 + CARD = 5 # 生日画片 + + +class Task(SQLModel, table=True): __table_args__ = dict(mysql_charset="utf8mb4", mysql_collate="utf8mb4_general_ci") id: Optional[int] = Field( default=None, primary_key=True, sa_column=Column(Integer(), primary_key=True, autoincrement=True) ) user_id: int = Field(primary_key=True, sa_column=Column(BigInteger(), index=True)) - chat_id: Optional[int] = Field(default=None) + chat_id: Optional[int] = Field(default=None, sa_column=Column(BigInteger())) time_created: Optional[datetime] = Field( sa_column=Column(DateTime, server_default=func.now()) # pylint: disable=E1102 ) time_updated: Optional[datetime] = Field(sa_column=Column(DateTime, onupdate=func.now())) # pylint: disable=E1102 - status: Optional[SignStatusEnum] = Field(sa_column=Column(Enum(SignStatusEnum))) + type: TaskTypeEnum = Field(primary_key=True, sa_column=Column(Enum(TaskTypeEnum))) + status: Optional[TaskStatusEnum] = Field(sa_column=Column(Enum(TaskStatusEnum))) + data: Optional[Dict[str, Any]] = Field(sa_column=Column(JSON)) diff --git a/core/services/sign/repositories.py b/core/services/task/repositories.py similarity index 51% rename from core/services/sign/repositories.py rename to core/services/task/repositories.py index 9eccbc1..c509836 100644 --- a/core/services/sign/repositories.py +++ b/core/services/task/repositories.py @@ -4,47 +4,47 @@ from sqlmodel import select from core.base_service import BaseService from core.dependence.database import Database -from core.services.sign.models import Sign +from core.services.task.models import Task, TaskTypeEnum from core.sqlmodel.session import AsyncSession -__all__ = ("SignRepository",) +__all__ = ("TaskRepository",) -class SignRepository(BaseService.Component): +class TaskRepository(BaseService.Component): def __init__(self, database: Database): self.engine = database.engine - async def add(self, sign: Sign): + async def add(self, task: Task): async with AsyncSession(self.engine) as session: - session.add(sign) + session.add(task) await session.commit() - async def remove(self, sign: Sign): + async def remove(self, task: Task): async with AsyncSession(self.engine) as session: - await session.delete(sign) + await session.delete(task) await session.commit() - async def update(self, sign: Sign) -> Sign: + async def update(self, task: Task) -> Task: async with AsyncSession(self.engine) as session: - session.add(sign) + session.add(task) await session.commit() - await session.refresh(sign) - return sign + await session.refresh(task) + return task - async def get_by_user_id(self, user_id: int) -> Optional[Sign]: + async def get_by_user_id(self, user_id: int, task_type: TaskTypeEnum) -> Optional[Task]: async with AsyncSession(self.engine) as session: - statement = select(Sign).where(Sign.user_id == user_id) + statement = select(Task).where(Task.user_id == user_id).where(Task.type == task_type) results = await session.exec(statement) return results.first() - async def get_by_chat_id(self, chat_id: int) -> Optional[List[Sign]]: + async def get_by_chat_id(self, chat_id: int, task_type: TaskTypeEnum) -> Optional[List[Task]]: async with AsyncSession(self.engine) as session: - statement = select(Sign).where(Sign.chat_id == chat_id) + statement = select(Task).where(Task.chat_id == chat_id).where(Task.type == task_type) results = await session.exec(statement) return results.all() - async def get_all(self) -> List[Sign]: + async def get_all(self, task_type: TaskTypeEnum) -> List[Task]: async with AsyncSession(self.engine) as session: - query = select(Sign) + query = select(Task).where(Task.type == task_type) results = await session.exec(query) return results.all() diff --git a/core/services/task/services.py b/core/services/task/services.py new file mode 100644 index 0000000..2f19307 --- /dev/null +++ b/core/services/task/services.py @@ -0,0 +1,179 @@ +import datetime +from typing import Optional, Dict, Any + +from core.base_service import BaseService +from core.services.task.models import Task, TaskTypeEnum +from core.services.task.repositories import TaskRepository + +__all__ = [ + "TaskServices", + "SignServices", + "TaskCardServices", + "TaskResinServices", + "TaskExpeditionServices", +] + + +class TaskServices(BaseService): + TASK_TYPE: TaskTypeEnum + + def __init__(self, task_repository: TaskRepository) -> None: + self._repository: TaskRepository = task_repository + + async def add(self, task: Task): + return await self._repository.add(task) + + async def remove(self, task: Task): + return await self._repository.remove(task) + + async def update(self, task: Task): + task.time_updated = datetime.datetime.now() + return await self._repository.update(task) + + async def get_by_user_id(self, user_id: int): + return await self._repository.get_by_user_id(user_id, self.TASK_TYPE) + + async def get_all(self): + return await self._repository.get_all(self.TASK_TYPE) + + def create(self, user_id: int, chat_id: int, status: int, data: Optional[Dict[str, Any]] = None): + return Task( + user_id=user_id, + chat_id=chat_id, + time_created=datetime.datetime.now(), + status=status, + type=self.TASK_TYPE, + data=data, + ) + + +class SignServices(BaseService): + TASK_TYPE = TaskTypeEnum.SIGN + + def __init__(self, task_repository: TaskRepository) -> None: + self._repository: TaskRepository = task_repository + + async def add(self, task: Task): + return await self._repository.add(task) + + async def remove(self, task: Task): + return await self._repository.remove(task) + + async def update(self, task: Task): + task.time_updated = datetime.datetime.now() + return await self._repository.update(task) + + async def get_by_user_id(self, user_id: int): + return await self._repository.get_by_user_id(user_id, self.TASK_TYPE) + + async def get_all(self): + return await self._repository.get_all(self.TASK_TYPE) + + def create(self, user_id: int, chat_id: int, status: int, data: Optional[Dict[str, Any]] = None): + return Task( + user_id=user_id, + chat_id=chat_id, + time_created=datetime.datetime.now(), + status=status, + type=self.TASK_TYPE, + data=data, + ) + + +class TaskCardServices(BaseService): + TASK_TYPE = TaskTypeEnum.CARD + + def __init__(self, task_repository: TaskRepository) -> None: + self._repository: TaskRepository = task_repository + + async def add(self, task: Task): + return await self._repository.add(task) + + async def remove(self, task: Task): + return await self._repository.remove(task) + + async def update(self, task: Task): + task.time_updated = datetime.datetime.now() + return await self._repository.update(task) + + async def get_by_user_id(self, user_id: int): + return await self._repository.get_by_user_id(user_id, self.TASK_TYPE) + + async def get_all(self): + return await self._repository.get_all(self.TASK_TYPE) + + def create(self, user_id: int, chat_id: int, status: int, data: Optional[Dict[str, Any]] = None): + return Task( + user_id=user_id, + chat_id=chat_id, + time_created=datetime.datetime.now(), + status=status, + type=self.TASK_TYPE, + data=data, + ) + + +class TaskResinServices(BaseService): + TASK_TYPE = TaskTypeEnum.RESIN + + def __init__(self, task_repository: TaskRepository) -> None: + self._repository: TaskRepository = task_repository + + async def add(self, task: Task): + return await self._repository.add(task) + + async def remove(self, task: Task): + return await self._repository.remove(task) + + async def update(self, task: Task): + task.time_updated = datetime.datetime.now() + return await self._repository.update(task) + + async def get_by_user_id(self, user_id: int): + return await self._repository.get_by_user_id(user_id, self.TASK_TYPE) + + async def get_all(self): + return await self._repository.get_all(self.TASK_TYPE) + + def create(self, user_id: int, chat_id: int, status: int, data: Optional[Dict[str, Any]] = None): + return Task( + user_id=user_id, + chat_id=chat_id, + time_created=datetime.datetime.now(), + status=status, + type=self.TASK_TYPE, + data=data, + ) + + +class TaskExpeditionServices(BaseService): + TASK_TYPE = TaskTypeEnum.EXPEDITION + + def __init__(self, task_repository: TaskRepository) -> None: + self._repository: TaskRepository = task_repository + + async def add(self, task: Task): + return await self._repository.add(task) + + async def remove(self, task: Task): + return await self._repository.remove(task) + + async def update(self, task: Task): + task.time_updated = datetime.datetime.now() + return await self._repository.update(task) + + async def get_by_user_id(self, user_id: int): + return await self._repository.get_by_user_id(user_id, self.TASK_TYPE) + + async def get_all(self): + return await self._repository.get_all(self.TASK_TYPE) + + def create(self, user_id: int, chat_id: int, status: int, data: Optional[Dict[str, Any]] = None): + return Task( + user_id=user_id, + chat_id=chat_id, + time_created=datetime.datetime.now(), + status=status, + type=self.TASK_TYPE, + data=data, + ) diff --git a/plugins/admin/sign_all.py b/plugins/admin/sign_all.py index 006ded9..5706403 100644 --- a/plugins/admin/sign_all.py +++ b/plugins/admin/sign_all.py @@ -1,5 +1,5 @@ from telegram import Update -from telegram.ext import CallbackContext, CommandHandler +from telegram.ext import CallbackContext from core.plugin import Plugin, handler from plugins.tools.sign import SignSystem, SignJobType @@ -10,7 +10,7 @@ class SignAll(Plugin): def __init__(self, sign_system: SignSystem): self.sign_system = sign_system - @handler(CommandHandler, command="sign_all", block=False, admin=True) + @handler.command(command="sign_all", block=False, admin=True) async def sign_all(self, update: Update, context: CallbackContext): user = update.effective_user logger.info("用户 %s[%s] sign_all 命令请求", user.full_name, user.id) diff --git a/plugins/admin/sign_status.py b/plugins/admin/sign_status.py index 8dcfea1..ca1be95 100644 --- a/plugins/admin/sign_status.py +++ b/plugins/admin/sign_status.py @@ -2,7 +2,7 @@ from telegram import Update from telegram.ext import CallbackContext, CommandHandler from core.plugin import Plugin, handler -from core.services.sign.services import SignServices +from core.services.task.services import SignServices from utils.log import logger diff --git a/plugins/jobs/daily_note.py b/plugins/jobs/daily_note.py new file mode 100644 index 0000000..4e2b7ee --- /dev/null +++ b/plugins/jobs/daily_note.py @@ -0,0 +1,20 @@ +import datetime +from typing import TYPE_CHECKING + +from core.plugin import Plugin, job +from plugins.tools.daily_note import DailyNoteSystem +from utils.log import logger + +if TYPE_CHECKING: + from telegram.ext import ContextTypes + + +class NotesJob(Plugin): + def __init__(self, daily_note_system: DailyNoteSystem): + self.daily_note_system = daily_note_system + + @job.run_repeating(interval=datetime.timedelta(minutes=20), name="NotesJob") + async def card(self, context: "ContextTypes.DEFAULT_TYPE"): + logger.info("正在执行自动便签提醒") + await self.daily_note_system.do_get_notes_job(context) + logger.success("执行自动便签提醒完成") diff --git a/plugins/starrail/daily_note_tasks.py b/plugins/starrail/daily_note_tasks.py new file mode 100644 index 0000000..d6e9d53 --- /dev/null +++ b/plugins/starrail/daily_note_tasks.py @@ -0,0 +1,130 @@ +from pydantic import ValidationError +from typing import TYPE_CHECKING + +from simnet import Region +from simnet.errors import DataNotPublic, BadRequest as SimnetBadRequest +from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update, KeyboardButton, WebAppInfo +from telegram.ext import CallbackContext, ConversationHandler, filters +from telegram.helpers import escape_markdown + +from core.config import config +from core.plugin import Plugin, conversation, handler +from core.services.cookies.services import CookiesService +from core.services.players.services import PlayersService, PlayerInfoService +from plugins.app.webapp import WebApp +from plugins.tools.daily_note import DailyNoteSystem, WebAppData +from plugins.tools.genshin import GenshinHelper, CookiesNotFoundError, PlayerNotFoundError +from utils.log import logger + +if TYPE_CHECKING: + from simnet import StarRailClient + +__all__ = ("DailyNoteTasksPlugin",) + + +SET_BY_WEB = 10100 + + +class DailyNoteTasksPlugin(Plugin.Conversation): + """自动便签提醒任务""" + + def __init__( + self, + players_service: PlayersService, + cookies_service: CookiesService, + player_info_service: PlayerInfoService, + helper: GenshinHelper, + note_system: DailyNoteSystem, + ): + self.cookies_service = cookies_service + self.players_service = players_service + self.player_info_service = player_info_service + self.helper = helper + self.note_system = note_system + + @conversation.entry_point + @handler.command(command="daily_note_tasks", filters=filters.ChatType.PRIVATE, block=False) + async def command_start(self, update: Update, _: CallbackContext) -> int: + user = update.effective_user + message = update.effective_message + logger.info("用户 %s[%s] 设置自动便签提醒命令请求", user.full_name, user.id) + text = await self.check_genshin_user(user.id, False) + if text != "ok": + await message.reply_text(text, reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + note_user = await self.note_system.get_single_task_user(user.id) + url = f"{config.pass_challenge_user_web}/tasks2?command=tasks&bot_data={note_user.web_config}" + text = f'你好 {user.mention_markdown_v2()} {escape_markdown("!请点击下方按钮,开始设置,或者回复退出取消操作")}' + await message.reply_markdown_v2( + text, + reply_markup=ReplyKeyboardMarkup.from_button( + KeyboardButton( + text="点我开始设置", + web_app=WebAppInfo(url=url), + ) + ), + ) + return SET_BY_WEB + + async def check_genshin_user(self, user_id: int, request_note: bool) -> str: + try: + async with self.helper.genshin(user_id) as client: + client: "StarRailClient" + if request_note: + if client.region == Region.CHINESE: + await client.get_starrail_notes_by_stoken() + else: + await client.get_starrail_notes() + return "ok" + except ValueError: + return "Cookies 缺少 stoken ,请尝试重新绑定账号。" + except DataNotPublic: + return "查询失败惹,可能是便签功能被禁用了?请尝试通过米游社或者 hoyolab 获取一次便签信息后重试。" + except SimnetBadRequest as e: + return f"获取便签失败,可能遇到验证码风控,请尝试重新绑定账号。{e}" + except (CookiesNotFoundError, PlayerNotFoundError): + return "未查询到您所绑定的账号信息,请先私聊派蒙绑定账号" + + @conversation.state(state=SET_BY_WEB) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False) + async def set_by_web_text(self, update: Update, _: CallbackContext) -> int: + message = update.effective_message + if message.text == "退出": + await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + else: + await message.reply_text("输入错误,请重新输入") + return SET_BY_WEB + + @conversation.state(state=SET_BY_WEB) + @handler.message(filters=filters.StatusUpdate.WEB_APP_DATA, block=False) + async def set_by_web(self, update: Update, _: CallbackContext) -> int: + user = update.effective_user + message = update.effective_message + web_app_data = message.web_app_data + if web_app_data: + result = WebApp.de_web_app_data(web_app_data.data) + if result.code == 0: + if result.path == "tasks": + try: + validate = WebAppData(**result.data) + except ValidationError: + await message.reply_text( + "数据错误\n开拓力提醒数值必须在 100 ~ 180 之间", + reply_markup=ReplyKeyboardRemove(), + ) + return ConversationHandler.END + need_note = await self.check_genshin_user(user.id, True) + if need_note != "ok": + await message.reply_text(need_note, reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + await self.note_system.import_web_config(user.id, validate) + await message.reply_text("修改设置成功", reply_markup=ReplyKeyboardRemove()) + else: + logger.warning( + "用户 %s[%s] WEB_APP_DATA 请求错误 [%s]%s", user.full_name, user.id, result.code, result.message + ) + await message.reply_text(f"WebApp返回错误 {result.message}", reply_markup=ReplyKeyboardRemove()) + else: + logger.warning("用户 %s[%s] WEB_APP_DATA 非法数据", user.full_name, user.id) + return ConversationHandler.END diff --git a/plugins/starrail/sign.py b/plugins/starrail/sign.py index bc96508..0b113d5 100644 --- a/plugins/starrail/sign.py +++ b/plugins/starrail/sign.py @@ -1,4 +1,3 @@ -import datetime from typing import Optional, Tuple from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup @@ -10,8 +9,8 @@ from telegram.helpers import create_deep_linked_url from core.config import config from core.handler.callbackqueryhandler import CallbackQueryHandler from core.plugin import Plugin, handler -from core.services.sign.models import Sign as SignUser, SignStatusEnum -from core.services.sign.services import SignServices +from core.services.task.models import Task as SignUser, TaskStatusEnum +from core.services.task.services import SignServices from core.services.users.services import UserAdminService from plugins.tools.genshin import GenshinHelper, CookiesNotFoundError, PlayerNotFoundError from plugins.tools.sign import SignSystem, NeedChallenge @@ -49,18 +48,13 @@ class Sign(Plugin): if user.chat_id == chat_id: return "自动签到已经开启过了" user.chat_id = chat_id - user.status = SignStatusEnum.STATUS_SUCCESS + user.status = TaskStatusEnum.STATUS_SUCCESS await self.sign_service.update(user) return "修改自动签到通知对话成功" elif method == "关闭": return "您还没有开启自动签到" elif method == "开启": - user = SignUser( - user_id=user_id, - chat_id=chat_id, - time_created=datetime.datetime.now(), - status=SignStatusEnum.STATUS_SUCCESS, - ) + user = self.sign_service.create(user_id, chat_id, TaskStatusEnum.STATUS_SUCCESS) await self.sign_service.add(user) return "开启自动签到成功" diff --git a/plugins/tools/daily_note.py b/plugins/tools/daily_note.py new file mode 100644 index 0000000..f839742 --- /dev/null +++ b/plugins/tools/daily_note.py @@ -0,0 +1,299 @@ +import base64 +from datetime import datetime +from typing import TYPE_CHECKING, List, Optional + +from pydantic import BaseModel, validator +from simnet.errors import BadRequest as SimnetBadRequest, InvalidCookies, TimedOut as SimnetTimedOut +from telegram.constants import ParseMode +from telegram.error import BadRequest, Forbidden + +from core.basemodel import RegionEnum +from core.plugin import Plugin +from core.services.task.models import Task as TaskUser, TaskStatusEnum +from core.services.task.services import TaskResinServices, TaskExpeditionServices +from plugins.tools.genshin import GenshinHelper, PlayerNotFoundError, CookiesNotFoundError +from utils.log import logger + +if TYPE_CHECKING: + from simnet import StarRailClient + from telegram.ext import ContextTypes + + +class TaskDataBase(BaseModel): + noticed: Optional[bool] = False + + +class ResinData(TaskDataBase): + notice_num: Optional[int] = 140 + + @validator("notice_num") + def notice_num_validator(cls, v): + if v < 100 or v > 180: + raise ValueError("开拓力提醒数值必须在 100 ~ 180 之间") + return v + + +class ExpeditionData(TaskDataBase): + pass + + +class WebAppData(BaseModel): + resin: Optional[ResinData] + expedition: Optional[ExpeditionData] + + +class DailyNoteTaskUser: + def __init__( + self, + user_id: int, + resin_db: Optional[TaskUser] = None, + expedition_db: Optional[TaskUser] = None, + ): + self.user_id = user_id + self.resin_db = resin_db + self.expedition_db = expedition_db + self.resin = ResinData(**self.resin_db.data) if self.resin_db else None + self.expedition = ExpeditionData(**self.expedition_db.data) if self.expedition_db else None + + @property + def status(self) -> TaskStatusEnum: + return max( + [ + self.resin_db.status if self.resin_db else TaskStatusEnum.STATUS_SUCCESS, + self.expedition_db.status if self.expedition_db else TaskStatusEnum.STATUS_SUCCESS, + ] + ) + + @status.setter + def status(self, value: TaskStatusEnum): + if self.resin_db: + self.resin_db.status = value + if self.expedition_db: + self.expedition_db.status = value + + @staticmethod + def js_bool(value: bool) -> str: + return "true" if value else "false" + + @staticmethod + def set_model_noticed(model: TaskDataBase): + data = model.copy(deep=True) + data.noticed = True + return data + + @property + def web_config(self) -> str: + return base64.b64encode( + ( + WebAppData( + resin=self.set_model_noticed(self.resin) if self.resin else None, + expedition=self.set_model_noticed(self.expedition) if self.expedition else None, + ).json() + ).encode() + ).decode() + + def save(self): + if self.resin_db: + self.resin_db.data = self.resin.dict() + if self.expedition_db: + self.expedition_db.data = self.expedition.dict() + + +class DailyNoteSystem(Plugin): + def __init__( + self, + genshin_helper: GenshinHelper, + resin_service: TaskResinServices, + expedition_service: TaskExpeditionServices, + ): + self.genshin_helper = genshin_helper + self.resin_service = resin_service + self.expedition_service = expedition_service + + async def get_single_task_user(self, user_id: int) -> DailyNoteTaskUser: + resin_db = await self.resin_service.get_by_user_id(user_id) + expedition_db = await self.expedition_service.get_by_user_id(user_id) + return DailyNoteTaskUser( + user_id=user_id, + resin_db=resin_db, + expedition_db=expedition_db, + ) + + @staticmethod + async def start_get_notes( + client: "StarRailClient", + user: DailyNoteTaskUser = None, + ) -> List[str]: + if client.region == RegionEnum.HOYOLAB: + notes = await client.get_starrail_notes() + else: + notes = await client.get_starrail_notes_by_stoken() + if not user: + return [] + notices = [] + notice = None + if user.resin_db and notes.max_stamina > 0: + if notes.current_stamina >= user.resin.notice_num: + if not user.resin.noticed: + rec_time = datetime.now().astimezone() + notes.stamina_recover_time + notice = ( + f"### 开拓力提示 ####\n\n当前开拓力为 {notes.current_stamina} / {notes.max_stamina} ,记得使用哦~\n" + f"预计全部恢复完成:{rec_time.strftime('%Y-%m-%d %H:%M')}" + ) + user.resin.noticed = True + else: + user.resin.noticed = False + notices.append(notice) + notice = None + if user.expedition_db and len(notes.expeditions) > 0: + all_finished = all(i.status == "Finished" for i in notes.expeditions) + if all_finished: + if not user.expedition.noticed: + notice = "### 探索派遣提示 ####\n\n所有探索派遣已完成,记得重新派遣哦~" + user.expedition.noticed = True + else: + user.expedition.noticed = False + notices.append(notice) + user.save() + return notices + + async def get_all_task_users(self) -> List[DailyNoteTaskUser]: + resin_list = await self.resin_service.get_all() + expedition_list = await self.expedition_service.get_all() + user_list = set() + for i in resin_list: + user_list.add(i.user_id) + for i in expedition_list: + user_list.add(i.user_id) + return [ + DailyNoteTaskUser( + user_id=i, + resin_db=next((x for x in resin_list if x.user_id == i), None), + expedition_db=next((x for x in expedition_list if x.user_id == i), None), + ) + for i in user_list + ] + + async def remove_task_user(self, user: DailyNoteTaskUser): + if user.resin_db: + await self.resin_service.remove(user.resin_db) + if user.expedition_db: + await self.expedition_service.remove(user.expedition_db) + + async def update_task_user(self, user: DailyNoteTaskUser): + if user.resin_db: + await self.resin_service.update(user.resin_db) + if user.expedition_db: + await self.expedition_service.update(user.expedition_db) + + @staticmethod + async def check_need_note(web_config: WebAppData) -> bool: + need_verify = False + if web_config.resin and web_config.resin.noticed: + need_verify = True + if web_config.expedition and web_config.expedition.noticed: + need_verify = True + return need_verify + + async def import_web_config(self, user_id: int, web_config: WebAppData): + user = await self.get_single_task_user(user_id) + if web_config.resin: + if web_config.resin.noticed: + if not user.resin_db: + resin = self.resin_service.create( + user_id, + user_id, + status=TaskStatusEnum.STATUS_SUCCESS, + data=ResinData(notice_num=web_config.resin.notice_num).dict(), + ) + await self.resin_service.add(resin) + else: + user.resin.notice_num = web_config.resin.notice_num + user.resin.noticed = False + else: + if user.resin_db: + await self.resin_service.remove(user.resin_db) + user.resin_db = None + user.resin = None + if web_config.expedition: + if web_config.expedition.noticed: + if not user.expedition_db: + expedition = self.expedition_service.create( + user_id, + user_id, + status=TaskStatusEnum.STATUS_SUCCESS, + data=ExpeditionData().dict(), + ) + await self.expedition_service.add(expedition) + else: + user.expedition.noticed = False + else: + if user.expedition_db: + await self.expedition_service.remove(user.expedition_db) + user.expedition_db = None + user.expedition = None + user.save() + await self.update_task_user(user) + + async def do_get_notes_job(self, context: "ContextTypes.DEFAULT_TYPE"): + include_status: List[TaskStatusEnum] = [ + TaskStatusEnum.STATUS_SUCCESS, + TaskStatusEnum.TIMEOUT_ERROR, + ] + task_list = await self.get_all_task_users() + for task_db in task_list: + if task_db.status not in include_status: + continue + user_id = task_db.user_id + try: + async with self.genshin_helper.genshin(user_id) as client: + text = await self.start_get_notes(client, task_db) + if all(not i for i in text): + continue + except InvalidCookies: + text = "自动便签提醒执行失败,Cookie无效" + task_db.status = TaskStatusEnum.INVALID_COOKIES + except SimnetBadRequest as exc: + text = f"自动便签提醒执行失败,API返回信息为 {str(exc)}" + task_db.status = TaskStatusEnum.GENSHIN_EXCEPTION + except SimnetTimedOut: + text = "便签获取失败了呜呜呜 ~ 服务器连接超时 服务器熟啦 ~ " + task_db.status = TaskStatusEnum.TIMEOUT_ERROR + except PlayerNotFoundError: + logger.info("用户 user_id[%s] 玩家不存在 关闭并移除自动便签提醒", user_id) + await self.remove_task_user(task_db) + continue + except CookiesNotFoundError: + logger.info("用户 user_id[%s] cookie 不存在 关闭并移除自动便签提醒", user_id) + await self.remove_task_user(task_db) + continue + except Exception as exc: + logger.error("执行自动便签提醒时发生错误 user_id[%s]", user_id, exc_info=exc) + text = "获取便签失败了呜呜呜 ~ 执行自动便签提醒时发生错误" + else: + task_db.status = TaskStatusEnum.STATUS_SUCCESS + for idx, task_user_db in enumerate([task_db.resin_db, task_db.expedition_db]): + if task_user_db is None: + continue + notice_text = text[idx] if isinstance(text, list) else text + if not notice_text: + continue + if task_user_db.chat_id < 0: + notice_text = ( + f'' + f"NOTICE {task_user_db.user_id}\n\n{notice_text}" + ) + try: + await context.bot.send_message(task_user_db.chat_id, notice_text, parse_mode=ParseMode.HTML) + except BadRequest as exc: + logger.error("执行自动便签提醒时发生错误 user_id[%s] Message[%s]", user_id, exc.message) + task_user_db.status = TaskStatusEnum.BAD_REQUEST + except Forbidden as exc: + logger.error("执行自动便签提醒时发生错误 user_id[%s] message[%s]", user_id, exc.message) + task_user_db.status = TaskStatusEnum.FORBIDDEN + except Exception as exc: + logger.error("执行自动便签提醒时发生错误 user_id[%s]", user_id, exc_info=exc) + continue + else: + task_user_db.status = TaskStatusEnum.STATUS_SUCCESS + await self.update_task_user(task_db) diff --git a/plugins/tools/sign.py b/plugins/tools/sign.py index 37967a8..a1a0931 100644 --- a/plugins/tools/sign.py +++ b/plugins/tools/sign.py @@ -17,8 +17,8 @@ from core.config import config from core.dependence.redisdb import RedisDB from core.plugin import Plugin from core.services.cookies import CookiesService -from core.services.sign.models import SignStatusEnum -from core.services.sign.services import SignServices +from core.services.task.models import TaskStatusEnum +from core.services.task.services import SignServices from core.services.users.services import UserService from modules.apihelper.client.components.verify import Verify from plugins.tools.genshin import PlayerNotFoundError, CookiesNotFoundError, GenshinHelper @@ -269,16 +269,16 @@ class SignSystem(Plugin): return message async def do_sign_job(self, context: "ContextTypes.DEFAULT_TYPE", job_type: SignJobType): - include_status: List[SignStatusEnum] = [ - SignStatusEnum.STATUS_SUCCESS, - SignStatusEnum.TIMEOUT_ERROR, - SignStatusEnum.NEED_CHALLENGE, + include_status: List[TaskStatusEnum] = [ + TaskStatusEnum.STATUS_SUCCESS, + TaskStatusEnum.TIMEOUT_ERROR, + TaskStatusEnum.NEED_CHALLENGE, ] if job_type == SignJobType.START: title = "自动签到" elif job_type == SignJobType.REDO: title = "自动重新签到" - include_status.remove(SignStatusEnum.STATUS_SUCCESS) + include_status.remove(TaskStatusEnum.STATUS_SUCCESS) else: raise ValueError sign_list = await self.sign_service.get_all() @@ -291,19 +291,19 @@ class SignSystem(Plugin): text = await self.start_sign(client, is_sleep=True, is_raise=True, title=title) except InvalidCookies: text = "自动签到执行失败,Cookie无效" - sign_db.status = SignStatusEnum.INVALID_COOKIES + sign_db.status = TaskStatusEnum.INVALID_COOKIES except AlreadyClaimed: text = "今天开拓者已经签到过了~" - sign_db.status = SignStatusEnum.ALREADY_CLAIMED + sign_db.status = TaskStatusEnum.ALREADY_CLAIMED except SimnetBadRequest as exc: text = f"自动签到执行失败,API返回信息为 {str(exc)}" - sign_db.status = SignStatusEnum.GENSHIN_EXCEPTION + sign_db.status = TaskStatusEnum.GENSHIN_EXCEPTION except SimnetTimedOut: text = "签到失败了呜呜呜 ~ 服务器连接超时 服务器熟啦 ~ " - sign_db.status = SignStatusEnum.TIMEOUT_ERROR + sign_db.status = TaskStatusEnum.TIMEOUT_ERROR except NeedChallenge: text = "签到失败,触发验证码风控" - sign_db.status = SignStatusEnum.NEED_CHALLENGE + sign_db.status = TaskStatusEnum.NEED_CHALLENGE except PlayerNotFoundError: logger.info("用户 user_id[%s] 玩家不存在 关闭并移除自动签到", user_id) await self.sign_service.remove(sign_db) @@ -316,21 +316,20 @@ class SignSystem(Plugin): logger.error("执行自动签到时发生错误 user_id[%s]", user_id, exc_info=exc) text = "签到失败了呜呜呜 ~ 执行自动签到时发生错误" else: - sign_db.status = SignStatusEnum.STATUS_SUCCESS + sign_db.status = TaskStatusEnum.STATUS_SUCCESS if sign_db.chat_id < 0: text = f'NOTICE {sign_db.user_id}\n\n{text}' try: await context.bot.send_message(sign_db.chat_id, text, parse_mode=ParseMode.HTML) except BadRequest as exc: logger.error("执行自动签到时发生错误 user_id[%s] Message[%s]", user_id, exc.message) - sign_db.status = SignStatusEnum.BAD_REQUEST + sign_db.status = TaskStatusEnum.BAD_REQUEST except Forbidden as exc: logger.error("执行自动签到时发生错误 user_id[%s] message[%s]", user_id, exc.message) - sign_db.status = SignStatusEnum.FORBIDDEN + sign_db.status = TaskStatusEnum.FORBIDDEN except Exception as exc: logger.error("执行自动签到时发生错误 user_id[%s]", user_id, exc_info=exc) continue else: - sign_db.status = SignStatusEnum.STATUS_SUCCESS - sign_db.time_updated = datetime.datetime.now() + sign_db.status = TaskStatusEnum.STATUS_SUCCESS await self.sign_service.update(sign_db)