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)