diff --git a/apps/sign/__init__.py b/apps/sign/__init__.py new file mode 100644 index 0000000..9ac44b8 --- /dev/null +++ b/apps/sign/__init__.py @@ -0,0 +1,11 @@ +from utils.apps.manager import listener_service +from utils.mysql import MySQL +from .repositories import SignRepository +from .services import SignServices + + +@listener_service() +def create_game_strategy_service(mysql: MySQL): + _repository = SignRepository(mysql) + _service = SignServices(_repository) + return _service diff --git a/apps/sign/models.py b/apps/sign/models.py new file mode 100644 index 0000000..7d48640 --- /dev/null +++ b/apps/sign/models.py @@ -0,0 +1,25 @@ +import enum +from datetime import datetime +from typing import Optional + +from sqlalchemy import func +from sqlmodel import SQLModel, Field, Enum, Column, DateTime + + +class SignStatusEnum(int, enum.Enum): + STATUS_SUCCESS = 0 # 签到成功 + INVALID_COOKIES = 1 # Cookie无效 + ALREADY_CLAIMED = 2 # 已经获取奖励 + GENSHIN_EXCEPTION = 3 # API异常 + TIMEOUT_ERROR = 4 # 请求超时 + BAD_REQUEST = 5 # 请求失败 + FORBIDDEN = 6 # 这错误一般为通知失败 机器人被用户BAN + + +class Sign(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + user_id: int = Field() + chat_id: int = Field() + time_created: Optional[datetime] = Field(sa_column=Column(DateTime(timezone=True), server_default=func.now())) + time_updated: Optional[datetime] = Field(sa_column=Column(DateTime(timezone=True), onupdate=func.now())) + status: Optional[SignStatusEnum] = Field(sa_column=Column(Enum(SignStatusEnum))) diff --git a/apps/sign/repositories.py b/apps/sign/repositories.py new file mode 100644 index 0000000..04fd3c1 --- /dev/null +++ b/apps/sign/repositories.py @@ -0,0 +1,41 @@ +from typing import List, cast, Optional + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from utils.mysql import MySQL +from .models import Sign + + +class SignRepository: + def __init__(self, mysql: MySQL): + self.mysql = mysql + + async def add(self, sign: Sign): + async with self.mysql.Session() as session: + session = cast(AsyncSession, session) + session.add(sign) + await session.commit() + + async def update(self, sign: Sign): + async with self.mysql.Session() as session: + session = cast(AsyncSession, session) + session.add(sign) + await session.commit() + await session.refresh(sign) + + async def get_by_user_id(self, user_id: int) -> Optional[Sign]: + async with self.mysql.Session() as session: + session = cast(AsyncSession, session) + statement = select(Sign).where(Sign.user_id == user_id) + results = await session.exec(statement) + if sign := results.first(): + return sign[0] + return None + + async def get_all(self) -> List[Sign]: + async with self.mysql.Session() as session: + query = select(Sign) + results = await session.exec(query) + signs = results.all() + return [sign[0] for sign in signs] diff --git a/apps/sign/services.py b/apps/sign/services.py new file mode 100644 index 0000000..855f9b3 --- /dev/null +++ b/apps/sign/services.py @@ -0,0 +1,19 @@ +from .models import Sign +from .repositories import SignRepository + + +class SignServices: + 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 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) diff --git a/jobs/sign.py b/jobs/sign.py new file mode 100644 index 0000000..827e8dc --- /dev/null +++ b/jobs/sign.py @@ -0,0 +1,96 @@ +import datetime +import time + +from aiohttp import ClientConnectorError +from genshin import Game, GenshinException, AlreadyClaimed, InvalidCookies +from telegram.error import BadRequest, Forbidden +from telegram.ext import CallbackContext, JobQueue + +from apps.cookies import CookiesService +from apps.sign.models import SignStatusEnum +from apps.sign.services import SignServices +from apps.user import UserService +from config import config +from logger import Log +from utils.apps.inject import inject +from utils.helpers import get_genshin_client +from utils.job.manager import listener_jobs_class + + +@listener_jobs_class() +class SignJob: + + @inject + def __init__(self, sign_service: SignServices = None, user_service: UserService = None, + cookies_service: CookiesService = None): + self.sign_service = sign_service + self.cookies_service = cookies_service + self.user_service = user_service + + @classmethod + def build_jobs(cls, job_queue: JobQueue): + sign = cls() + if config.DEBUG: + job_queue.run_once(sign.sign, 3, name="SignJobTest") + # 每天凌晨执行 + job_queue.run_daily(sign.sign, datetime.time(hour=0, minute=0, second=0), name="SignJob") + + async def sign(self, context: CallbackContext): + Log.info("正在执行自动签到") + sign_list = await self.sign_service.get_all() + for sign_db in sign_list: + if sign_db.status != SignStatusEnum.STATUS_SUCCESS: + continue + user_id = sign_db.user_id + try: + client = await get_genshin_client(user_id, self.user_service, self.cookies_service) + rewards = await client.get_monthly_rewards(game=Game.GENSHIN, lang="zh-cn") + daily_reward_info = await client.get_reward_info(game=Game.GENSHIN) + if not daily_reward_info.signed_in: + request_daily_reward = await client.request_daily_reward("sign", method="POST", game=Game.GENSHIN) + Log.info(f"UID {client.uid} 签到请求 {request_daily_reward}") + result = "OK" + else: + result = "今天旅行者已经签到过了~" + reward = rewards[daily_reward_info.claimed_rewards - (1 if daily_reward_info.signed_in else 0)] + today = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + cn_timezone = datetime.timezone(datetime.timedelta(hours=8)) + now = datetime.datetime.now(cn_timezone) + missed_days = now.day - daily_reward_info.claimed_rewards + if not daily_reward_info.signed_in: + missed_days -= 1 + text = f"########### 定时签到 ###########\n" \ + f"#### {today} (UTC+8) ####\n" \ + f"UID: {client.uid}\n" \ + f"今日奖励: {reward.name} × {reward.amount}\n" \ + f"本月漏签次数:{missed_days}\n" \ + f"签到结果: {result}" + except InvalidCookies: + text = "自动签到执行失败,Cookie无效" + sign_db.status = SignStatusEnum.INVALID_COOKIES + except AlreadyClaimed: + text = "今天旅行者已经签到过了~" + sign_db.status = SignStatusEnum.ALREADY_CLAIMED + except GenshinException as exc: + text = f"自动签到执行失败,API返回信息为 {str(exc)}" + sign_db.status = SignStatusEnum.GENSHIN_EXCEPTION + except ClientConnectorError: + text = "签到失败了呜呜呜 ~ 服务器连接超时 服务器熟啦 ~ " + sign_db.status = SignStatusEnum.TIMEOUT_ERROR + except BaseException as exc: + Log.error(f"执行自动签到时发生错误", exc) + continue + try: + await context.bot.send_message(sign_db.chat_id, text) + except BadRequest as exc: + Log.error(f"执行自动签到时发生错误", exc) + sign_db.status = SignStatusEnum.BAD_REQUEST + except Forbidden as exc: + Log.error(f"执行自动签到时发生错误", exc) + sign_db.status = SignStatusEnum.FORBIDDEN + except BaseException as exc: + Log.error(f"执行自动签到时发生错误", exc) + continue + sign_db.time_updated = datetime.datetime.now() + await self.sign_service.update(sign_db) + Log.info("执行自动签到完成")