diff --git a/modules/gacha_log/log.py b/modules/gacha_log/log.py index cc41aa33..d4ed4eb9 100644 --- a/modules/gacha_log/log.py +++ b/modules/gacha_log/log.py @@ -11,6 +11,7 @@ import aiofiles from openpyxl import load_workbook from simnet import GenshinClient, Region from simnet.errors import AuthkeyTimeout, InvalidAuthkey +from simnet.models.base import add_timezone from simnet.models.genshin.wish import BannerType from simnet.utils.player import recognize_genshin_server @@ -248,7 +249,7 @@ class GachaLog(GachaLogOnlineView, GachaLogRanks): # 检查导入后的数据是否合法 await self.verify_data(i) i.sort(key=lambda x: (x.time, x.id)) - gacha_log.update_time = datetime.datetime.now() + gacha_log.update_time = add_timezone(datetime.datetime.now()) gacha_log.import_type = import_type.value await self.save_gacha_log_info(str(user_id), uid, gacha_log) return new_num @@ -265,11 +266,12 @@ class GachaLog(GachaLogOnlineView, GachaLogRanks): return GenshinClient(player_id=player_id, region=Region.CHINESE, lang="zh-cn") return GenshinClient(player_id=player_id, region=Region.OVERSEAS, lang="zh-cn") - async def get_gacha_log_data(self, user_id: int, player_id: int, authkey: str) -> int: + async def get_gacha_log_data(self, user_id: int, player_id: int, authkey: str, is_lazy: bool) -> int: """使用authkey获取抽卡记录数据,并合并旧数据 :param user_id: 用户id :param player_id: 玩家id :param authkey: authkey + :param is_lazy: 是否快速导入 :return: 更新结果 """ new_num = 0 @@ -281,7 +283,23 @@ class GachaLog(GachaLogOnlineView, GachaLogRanks): client = self.get_game_client(player_id) try: for pool_id, pool_name in GACHA_TYPE_LIST.items(): - wish_history = await client.wish_history(pool_id.value, authkey=authkey) + if pool_name not in temp_id_data: + temp_id_data[pool_name] = [] + if pool_name not in gacha_log.item_list: + gacha_log.item_list[pool_name] = [] + min_id = 0 + if is_lazy and gacha_log.item_list[pool_name]: + with contextlib.suppress(ValueError): + min_id = int(gacha_log.item_list[pool_name][-1].id) + + wish_history = await client.wish_history(pool_id.value, authkey=authkey, min_id=min_id) + + if not is_lazy: + min_id = wish_history[0].id if wish_history else min_id + if min_id: + gacha_log.item_list[pool_name][:] = filter( + lambda i: int(i.id) < min_id, gacha_log.item_list[pool_name] + ) for data in wish_history: item = GachaItem( id=str(data.id), @@ -289,24 +307,15 @@ class GachaLog(GachaLogOnlineView, GachaLogRanks): gacha_type=str(data.banner_type.value), item_type=data.type, rank_type=str(data.rarity), - time=datetime.datetime( - data.time.year, - data.time.month, - data.time.day, - data.time.hour, - data.time.minute, - data.time.second, - ), + time=data.time, ) - if pool_name not in temp_id_data: - temp_id_data[pool_name] = [] - if pool_name not in gacha_log.item_list: - gacha_log.item_list[pool_name] = [] - if item.id not in temp_id_data[pool_name]: + if item.id not in temp_id_data[pool_name] or (not is_lazy and min_id): gacha_log.item_list[pool_name].append(item) temp_id_data[pool_name].append(item.id) new_num += 1 + + await asyncio.sleep(1) except AuthkeyTimeout as exc: raise GachaLogAuthkeyTimeout from exc except InvalidAuthkey as exc: @@ -315,24 +324,28 @@ class GachaLog(GachaLogOnlineView, GachaLogRanks): await client.shutdown() for i in gacha_log.item_list.values(): i.sort(key=lambda x: (x.time, x.id)) - gacha_log.update_time = datetime.datetime.now() + gacha_log.update_time = add_timezone(datetime.datetime.now()) gacha_log.import_type = ImportType.UIGF.value await self.save_gacha_log_info(str(user_id), str(player_id), gacha_log) await self.recount_one_from_uid(user_id, player_id) return new_num + @staticmethod + def format_time(time: str) -> datetime.datetime: + return add_timezone(datetime.datetime.strptime(time, "%Y-%m-%d %H:%M:%S")) + @staticmethod def check_avatar_up(name: str, gacha_time: datetime.datetime) -> bool: if name in {"莫娜", "七七", "迪卢克", "琴", "迪希雅"}: return False if name == "刻晴": - start_time = datetime.datetime.strptime("2021-02-17 18:00:00", "%Y-%m-%d %H:%M:%S") - end_time = datetime.datetime.strptime("2021-03-02 15:59:59", "%Y-%m-%d %H:%M:%S") + start_time = GachaLog.format_time("2021-02-17 18:00:00") + end_time = GachaLog.format_time("2021-03-02 15:59:59") if not start_time < gacha_time < end_time: return False elif name == "提纳里": - start_time = datetime.datetime.strptime("2022-08-24 06:00:00", "%Y-%m-%d %H:%M:%S") - end_time = datetime.datetime.strptime("2022-09-09 17:59:59", "%Y-%m-%d %H:%M:%S") + start_time = GachaLog.format_time("2022-08-24 06:00:00") + end_time = GachaLog.format_time("2022-09-09 17:59:59") if not start_time < gacha_time < end_time: return False return True diff --git a/modules/gacha_log/models.py b/modules/gacha_log/models.py index 91160f8a..3588d131 100644 --- a/modules/gacha_log/models.py +++ b/modules/gacha_log/models.py @@ -2,7 +2,9 @@ import datetime from enum import Enum from typing import Any, Dict, List, Union -from pydantic import field_validator, BaseModel +from pydantic import field_validator + +from simnet.models.base import APIModel as BaseModel, DateTimeField, add_timezone from metadata.shortname import not_real_roles, roleToId, weaponToId from modules.gacha_log.const import UIGF_VERSION @@ -23,7 +25,7 @@ class FiveStarItem(BaseModel): type: str isUp: bool isBig: bool - time: datetime.datetime + time: DateTimeField class FourStarItem(BaseModel): @@ -31,7 +33,7 @@ class FourStarItem(BaseModel): icon: str count: int type: str - time: datetime.datetime + time: DateTimeField class GachaItem(BaseModel): @@ -40,7 +42,7 @@ class GachaItem(BaseModel): gacha_type: str item_type: str rank_type: str - time: datetime.datetime + time: DateTimeField @field_validator("name") @classmethod @@ -75,7 +77,7 @@ class GachaItem(BaseModel): class GachaLogInfo(BaseModel): user_id: str uid: str - update_time: datetime.datetime + update_time: DateTimeField import_type: str = "" item_list: Dict[str, List[GachaItem]] = { "角色祈愿": [], @@ -101,8 +103,8 @@ class Pool: self.four = four self.from_ = kwargs.get("from") self.to = to - self.from_time = datetime.datetime.strptime(self.from_, "%Y-%m-%d %H:%M:%S") - self.to_time = datetime.datetime.strptime(self.to, "%Y-%m-%d %H:%M:%S") + self.from_time = add_timezone(datetime.datetime.strptime(self.from_, "%Y-%m-%d %H:%M:%S")) + self.to_time = add_timezone(datetime.datetime.strptime(self.to, "%Y-%m-%d %H:%M:%S")) self.start = self.from_time self.start_init = False self.end = self.to_time diff --git a/plugins/genshin/wish_log.py b/plugins/genshin/wish_log.py index fd7e825b..641260f7 100644 --- a/plugins/genshin/wish_log.py +++ b/plugins/genshin/wish_log.py @@ -6,7 +6,13 @@ from urllib.parse import urlencode from aiofiles import open as async_open from simnet import GenshinClient, Region from simnet.models.genshin.wish import BannerType -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove +from telegram import ( + InlineKeyboardButton, + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + TelegramObject, +) from telegram.constants import ChatAction from telegram.ext import ConversationHandler, filters from telegram.helpers import create_deep_linked_url @@ -54,7 +60,7 @@ if TYPE_CHECKING: from gram_core.services.players.models import Player from gram_core.services.template.models import RenderResult -INPUT_URL, INPUT_FILE, CONFIRM_DELETE = range(10100, 10103) +INPUT_URL, INPUT_LAZY, CONFIRM_DELETE = range(10100, 10103) WAITING = f"小{config.notice.bot_name}正在从服务器获取数据,请稍后" WISHLOG_NOT_FOUND = f"{config.notice.bot_name}没有找到你的抽卡记录,快来私聊{config.notice.bot_name}导入吧~" WISHLOG_WEB = """抽卡记录详细信息查询 @@ -64,6 +70,15 @@ WISHLOG_WEB = """抽卡记录详细信息查询 有效期为 1 小时,过期需重新申请。如怀疑泄漏请立即重新申请。""" +class WishLogPluginData(TelegramObject): + player_id: int = 0 + authkey: str = "" + + def reset_data(self): + self.player_id = 0 + self.authkey = "" + + class WishLogPlugin(Plugin.Conversation): """抽卡记录导入/导出/分析""" @@ -108,7 +123,13 @@ class WishLogPlugin(Plugin.Conversation): return player.player_id async def _refresh_user_data( - self, user: "User", player_id: int, data: dict = None, authkey: str = None, verify_uid: bool = True + self, + user: "User", + player_id: int, + data: dict = None, + authkey: str = None, + verify_uid: bool = True, + is_lazy: bool = True, ) -> str: """刷新用户数据 :param user: 用户 @@ -119,7 +140,7 @@ class WishLogPlugin(Plugin.Conversation): try: logger.debug("尝试获取已绑定的原神账号") if authkey: - new_num = await self.gacha_log.get_gacha_log_data(user.id, player_id, authkey) + new_num = await self.gacha_log.get_gacha_log_data(user.id, player_id, authkey, is_lazy) return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条抽卡记录" if data: new_num = await self.gacha_log.import_gacha_log_data(user.id, player_id, data, verify_uid) @@ -229,7 +250,13 @@ class WishLogPlugin(Plugin.Conversation): message = update.effective_message user = update.effective_user player_id = await self.get_player_id(user.id, uid, offset) - context.chat_data["uid"] = player_id + wish_log_plugin_data: WishLogPluginData = context.chat_data.get("wish_log_plugin_data") + if wish_log_plugin_data is None: + wish_log_plugin_data = WishLogPluginData() + context.chat_data["wish_log_plugin_data"] = wish_log_plugin_data + else: + wish_log_plugin_data.reset_data() + wish_log_plugin_data.player_id = player_id logger.info("用户 %s[%s] 导入抽卡记录命令请求", user.full_name, user.id) keyboard = None if await self.can_gen_authkey(user.id, player_id): @@ -242,8 +269,10 @@ class WishLogPlugin(Plugin.Conversation): async def import_data_from_message(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int: message = update.effective_message user = update.effective_user - player_id = context.chat_data["uid"] + wish_log_plugin_data: WishLogPluginData = context.chat_data.get("wish_log_plugin_data") + player_id = wish_log_plugin_data.player_id if message.document: + logger.info("用户 %s[%s] 从文件导入抽卡记录", user.full_name, user.id) await self.import_from_file(user, player_id, message) return ConversationHandler.END if not message.text: @@ -261,9 +290,29 @@ class WishLogPlugin(Plugin.Conversation): return ConversationHandler.END else: authkey = from_url_get_authkey(message.text) + wish_log_plugin_data.authkey = authkey + keyboard = ReplyKeyboardMarkup([["快速导入(推荐)"], ["全量刷新"], ["退出"]], one_time_keyboard=True) + await message.reply_text("请选择导入方式", parse_mode="html", reply_markup=keyboard) + return INPUT_LAZY + + @conversation.state(state=INPUT_LAZY) + @handler.message(filters=~filters.COMMAND, block=False) + async def get_lazy_from_message(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int: + message = update.effective_message + user = update.effective_user + wish_log_plugin_data: WishLogPluginData = context.chat_data.get("wish_log_plugin_data") + player_id = wish_log_plugin_data.player_id + authkey = wish_log_plugin_data.authkey + is_lazy = True + if message.text == "全量刷新": + is_lazy = False + elif message.text == "退出": + await message.reply_text("取消导入抽卡记录", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + logger.info("用户 %s[%s] 从 authkey 导入抽卡记录 is_lazy[%s]", user.full_name, user.id, is_lazy) reply = await message.reply_text(WAITING, reply_markup=ReplyKeyboardRemove()) await message.reply_chat_action(ChatAction.TYPING) - text = await self._refresh_user_data(user, player_id, authkey=authkey) + text = await self._refresh_user_data(user, player_id, authkey=authkey, is_lazy=is_lazy) self.add_delete_message_job(reply, delay=1) await message.reply_text(text, reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END @@ -276,10 +325,16 @@ class WishLogPlugin(Plugin.Conversation): uid, offset = self.get_real_uid_or_offset(update) message = update.effective_message user = update.effective_user + wish_log_plugin_data: WishLogPluginData = context.chat_data.get("wish_log_plugin_data") + if wish_log_plugin_data is None: + wish_log_plugin_data = WishLogPluginData() + context.chat_data["wish_log_plugin_data"] = wish_log_plugin_data + else: + wish_log_plugin_data.reset_data() logger.info("用户 %s[%s] 删除抽卡记录命令请求", user.full_name, user.id) try: player_id = await self.get_player_id(user.id, uid, offset) - context.chat_data["uid"] = player_id + wish_log_plugin_data.player_id = player_id except PlayerNotFoundError: logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) await message.reply_text(config.notice.user_not_found) @@ -298,8 +353,9 @@ class WishLogPlugin(Plugin.Conversation): async def command_confirm_delete(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int: message = update.effective_message user = update.effective_user + wish_log_plugin_data: WishLogPluginData = context.chat_data.get("wish_log_plugin_data") if message.text == "确定": - status = await self.gacha_log.remove_history_info(str(user.id), str(context.chat_data["uid"])) + status = await self.gacha_log.remove_history_info(str(user.id), str(wish_log_plugin_data.player_id)) await message.reply_text("抽卡记录已删除" if status else "抽卡记录删除失败") return ConversationHandler.END await message.reply_text("已取消") diff --git a/requirements.txt b/requirements.txt index c90aabd2..7d55243a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,9 +2,9 @@ # uv export -o .\requirements.txt --no-hashes --all-extras aiocsv==1.3.2 aiofiles==24.1.0 -aiohappyeyeballs==2.4.3 +aiohappyeyeballs==2.4.4 aiohttp==3.11.8 -aiolimiter==1.1.0 +aiolimiter==1.1.1 aiosignal==1.3.1 aiosqlite==0.20.0 alembic==1.14.0 @@ -84,7 +84,7 @@ redis==5.2.0 rich==13.9.4 sentry-sdk==2.19.0 setuptools==75.6.0 -simnet @ git+https://github.com/PaiGramTeam/SIMNet@d7756addb558356adc65e7e14dc86e0a3cb5d8bd +simnet @ git+https://github.com/PaiGramTeam/SIMNet@97053ad235a354b15f6c1fd577455c2d53590ebd six==1.16.0 smmap==5.0.1 sniffio==1.3.1 diff --git a/uv.lock b/uv.lock index 19c85fe6..b065c782 100644 --- a/uv.lock +++ b/uv.lock @@ -39,11 +39,11 @@ wheels = [ [[package]] name = "aiohappyeyeballs" -version = "2.4.3" +version = "2.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/69/2f6d5a019bd02e920a3417689a89887b39ad1e350b562f9955693d900c40/aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586", size = 21809 } +sdist = { url = "https://files.pythonhosted.org/packages/7f/55/e4373e888fdacb15563ef6fa9fa8c8252476ea071e96fb46defac9f18bf2/aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745", size = 21977 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/d8/120cd0fe3e8530df0539e71ba9683eade12cae103dd7543e50d15f737917/aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572", size = 14742 }, + { url = "https://files.pythonhosted.org/packages/b9/74/fbb6559de3607b3300b9be3cc64e97548d55678e44623db17820dbd20002/aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8", size = 14756 }, ] [[package]] @@ -141,11 +141,11 @@ wheels = [ [[package]] name = "aiolimiter" -version = "1.1.0" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/62/6de944a6839a68f7d69e552e26d12234d9c556472e4c277a3a563013640a/aiolimiter-1.1.0.tar.gz", hash = "sha256:461cf02f82a29347340d031626c92853645c099cb5ff85577b831a7bd21132b5", size = 6229 } +sdist = { url = "https://files.pythonhosted.org/packages/ec/93/fcb0673940fd8843e73082265e5b5e0e078367b6525797487d3f50263ab8/aiolimiter-1.1.1.tar.gz", hash = "sha256:4b5740c96ecf022d978379130514a26c18001e7450ba38adf19515cd0970f68f", size = 6097 } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/69/4b7dea755fafa10b248928da836a2cc8b5cff0762f363234e24218040f8e/aiolimiter-1.1.0-py3-none-any.whl", hash = "sha256:0b4997961fc58b8df40279e739f9cf0d3e255e63e9a44f64df567a8c17241e24", size = 7212 }, + { url = "https://files.pythonhosted.org/packages/d2/cc/8b6f2ef4c821928a22368bc14935087ae2687085059604448887920dec3d/aiolimiter-1.1.1-py3-none-any.whl", hash = "sha256:bf23dafbd1370e0816792fbcfb8fb95d5138c26e05f839fe058f5440bea006f5", size = 5771 }, ] [[package]] @@ -2080,7 +2080,7 @@ wheels = [ [[package]] name = "simnet" version = "0.2.0" -source = { git = "https://github.com/PaiGramTeam/SIMNet#d7756addb558356adc65e7e14dc86e0a3cb5d8bd" } +source = { git = "https://github.com/PaiGramTeam/SIMNet#97053ad235a354b15f6c1fd577455c2d53590ebd" } dependencies = [ { name = "httpx" }, { name = "pydantic" },