diff --git a/.gitignore b/.gitignore index 1bfefd1..d92485e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ __pycache__/ **_test.html test_**.html logs/ +report/ /resources/*/*/test/ ### DotEnv ### @@ -47,4 +48,4 @@ plugins/private ### mtp ### paimon.session PaimonBot.session -PaimonBot.session-journal \ No newline at end of file +PaimonBot.session-journal diff --git a/modules/apihelper/hyperion.py b/modules/apihelper/hyperion.py index a0d5e74..3bd918a 100644 --- a/modules/apihelper/hyperion.py +++ b/modules/apihelper/hyperion.py @@ -249,3 +249,123 @@ class YuanShen: :return: """ await self.client.aclose() + + class SignIn: + LOGIN_URL = "https://webapi.account.mihoyo.com/Api/login_by_mobilecaptcha" + S_TOKEN_URL = "https://api-takumi.mihoyo.com/auth/api/getMultiTokenByLoginTicket?" \ + "login_ticket={0}&token_types=3&uid={1}" + BBS_URL = "https://api-takumi.mihoyo.com/account/auth/api/webLoginByMobile" + USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " \ + "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15" + HEADERS = { + "Host": "webapi.account.mihoyo.com", + "Connection": "keep-alive", + "sec-ch-ua": "\".Not/A)Brand\";v=\"99\", \"Microsoft Edge\";v=\"103\", \"Chromium\";v=\"103\"", + "DNT": "1", + "x-rpc-device_model": "OS X 10.15.7", + "sec-ch-ua-mobile": "?0", + "User-Agent": USER_AGENT, + 'x-rpc-device_id': get_device_id(USER_AGENT), + "Accept": "application/json, text/plain, */*", + "x-rpc-device_name": "Microsoft Edge 103.0.1264.62", + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + "x-rpc-client_type": "4", + "sec-ch-ua-platform": "\"macOS\"", + "Origin": "https://user.mihoyo.com", + "Sec-Fetch-Site": "same-site", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Dest": "empty", + "Referer": "https://user.mihoyo.com/", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6" + } + BBS_HEADERS = { + "Host": "api-takumi.mihoyo.com", + "Content-Type": "application/json;charset=utf-8", + "Origin": "https://bbs.mihoyo.com", + "Accept-Encoding": "gzip, deflate, br", + "Connection": "keep-alive", + "Accept": "application/json, text/plain, */*", + "User-Agent": USER_AGENT, + "Referer": "https://bbs.mihoyo.com/", + "Accept-Language": "zh-CN,zh-Hans;q=0.9" + } + + def __init__(self, phone: int): + self.phone = phone + self.client = AsyncClient() + self.uid = 0 + self.cookie = {} + + def parse_uid(self): + """ + 从cookie中获取uid + :param self: + :return: + """ + if "login_ticket" not in self.cookie: + return + for item in ["login_uid", "stuid", "ltuid", "account_id"]: + if item in self.cookie: + self.uid = self.cookie[item] + break + for item in ["login_uid", "stuid", "ltuid", "account_id"]: + self.cookie[item] = self.uid + + @staticmethod + def check_error(data: dict) -> bool: + """ + 检查是否有错误 + :param data: + :return: + """ + res_data = data.get("data", {}) + return res_data.get("msg") == "验证码错误" or res_data.get("info") == "Captcha not match Err" + + async def login(self, captcha: int) -> bool: + data = await self.client.post( + self.LOGIN_URL, + data={"mobile": str(self.phone), "mobile_captcha": str(captcha), "source": "user.mihoyo.com"}, + headers=self.HEADERS + ) + res_json = data.json() + if self.check_error(res_json): + return False + + for k, v in data.cookies.items(): + self.cookie[k] = v + + self.parse_uid() + return bool(self.uid) + + async def get_s_token(self): + data = await self.client.get( + self.S_TOKEN_URL.format(self.cookie["login_ticket"], self.uid), + headers={"User-Agent": self.USER_AGENT} + ) + res_json = data.json() + res_data = res_json.get("data", {}).get("list", []) + for i in res_data: + if i.get("name") and i.get("token"): + self.cookie[i.get("name")] = i.get("token") + + async def get_token(self, captcha: int) -> bool: + data = await self.client.post( + self.BBS_URL, + headers=self.BBS_HEADERS, + json={ + "is_bh2": False, + "mobile": str(self.phone), + "captcha": str(captcha), + "action_type": "login", + "token_type": 6 + } + ) + res_json = data.json() + if self.check_error(res_json): + return False + + for k, v in data.cookies.items(): + self.cookie[k] = v + + return "cookie_token" in self.cookie diff --git a/plugins/genshin/cookies.py b/plugins/genshin/cookies.py index d9a8a05..aeabbb3 100644 --- a/plugins/genshin/cookies.py +++ b/plugins/genshin/cookies.py @@ -15,6 +15,7 @@ from core.plugin import Plugin, handler, conversation from core.user.error import UserNotFoundError from core.user.models import User from core.user.services import UserService +from modules.apihelper.hyperion import YuanShen from utils.decorators.error import error_callable from utils.decorators.restricts import restricts from utils.log import logger @@ -27,9 +28,11 @@ class AddUserCommandData(TelegramObject): region: RegionEnum = RegionEnum.NULL cookies: dict = {} game_uid: int = 0 + phone: int = 0 + sign_in_client: Optional[YuanShen.SignIn] = None -CHECK_SERVER, CHECK_COOKIES, COMMAND_RESULT = range(10100, 10103) +CHECK_SERVER, CHECK_PHONE, CHECK_CAPTCHA, INPUT_COOKIES, COMMAND_RESULT = range(10100, 10106) class SetUserCookies(Plugin.Conversation, BasePlugin.Conversation): @@ -57,6 +60,22 @@ class SetUserCookies(Plugin.Conversation, BasePlugin.Conversation): await message.reply_markdown_v2(text, reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)) return CHECK_SERVER + @conversation.entry_point + @handler.command(command='mlogin', filters=filters.ChatType.PRIVATE, block=True) + @error_callable + async def choose_method(self, update: Update, context: CallbackContext) -> int: + user = update.effective_user + message = update.effective_message + logger.info(f"用户 {user.full_name}[{user.id}] 绑定账号命令请求") + add_user_command_data: AddUserCommandData = context.chat_data.get("add_user_command_data") + if add_user_command_data is None: + cookies_command_data = AddUserCommandData() + cookies_command_data.region = RegionEnum.HYPERION + context.chat_data["add_user_command_data"] = cookies_command_data + text = f'你好 {user.mention_markdown_v2()} {escape_markdown("!该绑定方法仅支持国服,请发送 11 位手机号码!或回复退出取消操作")}' + await message.reply_markdown_v2(text) + return CHECK_PHONE + @conversation.state(state=CHECK_SERVER) @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) @error_callable @@ -109,13 +128,83 @@ class SetUserCookies(Plugin.Conversation, BasePlugin.Conversation): f"2、复制下方的代码,并将其粘贴在地址栏中,点击右侧箭头\n" \ f"`{escape_markdown(javascript_android, version=2, entity_type='code')}`" await message.reply_markdown_v2(help_message, disable_web_page_preview=True) - return CHECK_COOKIES + return INPUT_COOKIES - @conversation.state(state=CHECK_COOKIES) + @conversation.state(state=CHECK_PHONE) @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) @error_callable - async def check_cookies(self, update: Update, context: CallbackContext) -> int: - user = update.effective_user + async def check_phone(self, update: Update, context: CallbackContext) -> int: + message = update.effective_message + if message.text == "退出": + await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + try: + if not message.text.startswith("1"): + raise ValueError + phone = int(message.text) + if len(str(phone)) != 11: + raise ValueError + except ValueError: + await message.reply_text("手机号码输入错误,请重新输入!或回复退出取消操作") + return CHECK_PHONE + add_user_command_data: AddUserCommandData = context.chat_data.get("add_user_command_data") + add_user_command_data.phone = phone + await message.reply_text("请打开 https://user.mihoyo.com/#/login/captcha ,输入手机号并获取验证码," + "然后将收到的验证码发送给我(请不要在网页上进行登录)") + return CHECK_CAPTCHA + + @conversation.state(state=CHECK_CAPTCHA) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) + @error_callable + async def check_captcha(self, update: Update, context: CallbackContext) -> int: + message = update.effective_message + if message.text == "退出": + await message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END + try: + captcha = int(message.text) + if len(str(captcha)) != 6: + raise ValueError + except ValueError: + await message.reply_text("验证码输入错误,请重新输入!或回复退出取消操作") + return CHECK_CAPTCHA + add_user_command_data: AddUserCommandData = context.chat_data.get("add_user_command_data") + if not add_user_command_data.sign_in_client: + phone = add_user_command_data.phone + client = YuanShen.SignIn(phone) + try: + success = await client.login(captcha) + if not success: + await message.reply_text( + "登录失败:可能是验证码错误,注意不要在登录页面使用掉验证码,如果验证码已经使用,请重新获取验证码!") + return ConversationHandler.END + await client.get_s_token() + except Exception: + await message.reply_text("登录失败:米游社返回了错误的数据,请稍后再试!") + return ConversationHandler.END + add_user_command_data.sign_in_client = client + await message.reply_text( + "请再次打开 https://user.mihoyo.com/#/login/captcha ,输入手机号并获取验证码(需要等待一分钟)," + "然后将收到的验证码发送给我(请不要在网页上进行登录)") + return CHECK_CAPTCHA + else: + client = add_user_command_data.sign_in_client + try: + success = await client.get_token(captcha) + if not success: + await message.reply_text( + "登录失败:可能是验证码错误,注意不要在登录页面使用掉验证码,如果验证码已经使用,请重新获取验证码!") + return ConversationHandler.END + except Exception: + await message.reply_text("登录失败:米游社返回了错误的数据,请稍后再试!") + return ConversationHandler.END + add_user_command_data.cookies = client.cookie + return await self.check_cookies(update, context) + + @conversation.state(state=INPUT_COOKIES) + @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) + @error_callable + async def input_cookies(self, update: Update, context: CallbackContext) -> int: message = update.effective_message add_user_command_data: AddUserCommandData = context.chat_data.get("add_user_command_data") if message.text == "退出": @@ -135,6 +224,15 @@ class SetUserCookies(Plugin.Conversation, BasePlugin.Conversation): if not cookies: await message.reply_text("Cookies格式有误,请检查", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END + add_user_command_data.cookies = cookies + return await self.check_cookies(update, context) + + @staticmethod + async def check_cookies(update: Update, context: CallbackContext) -> int: + user = update.effective_user + message = update.effective_message + add_user_command_data: AddUserCommandData = context.chat_data.get("add_user_command_data") + cookies = add_user_command_data.cookies if add_user_command_data.region == RegionEnum.HYPERION: client = genshin.ChineseClient(cookies=cookies) elif add_user_command_data.region == RegionEnum.HOYOLAB: @@ -157,7 +255,6 @@ class SetUserCookies(Plugin.Conversation, BasePlugin.Conversation): except (AttributeError, ValueError): await message.reply_text("Cookies错误,请检查是否正确", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END - add_user_command_data.cookies = cookies add_user_command_data.game_uid = user_info.uid reply_keyboard = [['确认', '退出']] await message.reply_text("获取角色基础信息成功,请检查是否正确!")