From c927b7b86b9c01f557e932200142ab0c6f67c32e Mon Sep 17 00:00:00 2001 From: omg-xtao <100690902+omg-xtao@users.noreply.github.com> Date: Fri, 23 Dec 2022 21:06:08 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Use=20qrcode=20to=20add=20cookies?= =?UTF-8?q?=20only=20CN=20users?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/apihelper/client/components/signin.py | 187 +++++++++--------- plugins/genshin/cookies.py | 119 ++++------- poetry.lock | 55 ++++-- pyproject.toml | 1 + 4 files changed, 166 insertions(+), 196 deletions(-) diff --git a/modules/apihelper/client/components/signin.py b/modules/apihelper/client/components/signin.py index eecffe17..694a10d3 100644 --- a/modules/apihelper/client/components/signin.py +++ b/modules/apihelper/client/components/signin.py @@ -1,5 +1,12 @@ +import json +import random +from asyncio import sleep +from io import BytesIO +from string import ascii_letters, digits from typing import Dict +import qrcode +from genshin.utility import generate_cn_dynamic_secret from httpx import AsyncClient from ...utility.helpers import get_device_id @@ -8,69 +15,25 @@ __all__ = ("SignIn",) 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", - } - AUTHKEY_API = "https://api-takumi.mihoyo.com/binding/api/genAuthKey" - USER_INFO_API = "https://bbs-api.mihoyo.com/user/wapi/getUserFullInfo" - GACHA_HEADERS = { - "User-Agent": "okhttp/4.8.0", - "x-rpc-app_version": "2.28.1", - "x-rpc-sys_version": "12", - "x-rpc-client_type": "5", - "x-rpc-channel": "mihoyo", - "x-rpc-device_id": get_device_id(USER_AGENT), - "x-rpc-device_name": "Mi 10", - "x-rpc-device_model": "Mi 10", - "Referer": "https://app.mihoyo.com", - "Host": "api-takumi.mihoyo.com", - } + QRCODE_GEN_API = "https://hk4e-sdk.mihoyo.com/hk4e_cn/combo/panda/qrcode/fetch" + QRCODE_GET_API = "https://hk4e-sdk.mihoyo.com/hk4e_cn/combo/panda/qrcode/query" + GAME_TOKEN_API = "https://api-takumi.mihoyo.com/auth/api/getCookieAccountInfoByGameToken" + GAME_LTOKEN_API = "https://passport-api.mihoyo.com/account/ma-cn-session/app/getTokenByGameToken" - def __init__(self, phone: int = 0, uid: int = 0, cookie: Dict = None): - self.phone = phone + def __init__(self, uid: int = 0, cookie: Dict = None): self.client = AsyncClient() self.uid = uid self.cookie = cookie if cookie is not None else {} self.parse_uid() + self.ticket = None + self.device_id = None def parse_uid(self): """ @@ -87,34 +50,6 @@ class SignIn: 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 - - if "login_ticket" not in self.cookie: - return False - self.parse_uid() - return bool(self.uid) - async def get_s_token(self): if not self.cookie.get("login_ticket") or not self.uid: return @@ -127,23 +62,85 @@ class SignIn: 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, - }, + async def get_ltoken_by_game_token(self, game_token: str): + data = {"account_id": self.uid, "game_token": game_token} + headers = { + "x-rpc-app_version": "2.41.2", + "DS": generate_cn_dynamic_secret(body=data, salt="osgT0DljLarYxgebPPHJFjdaxPfoiHGt"), + "x-rpc-aigis": "", + "Content-Type": "application/json", + "Accept": "application/json", + "x-rpc-game_biz": "bbs_cn", + "x-rpc-sys_version": "11", + "x-rpc-device_id": get_device_id(self.USER_AGENT), + "x-rpc-device_fp": "".join(random.choices((ascii_letters + digits), k=13)), + "x-rpc-device_name": "Chrome 108.0.0.0", + "x-rpc-device_model": "Windows 10 64-bit", + "x-rpc-app_id": "bll8iq97cem8", + "x-rpc-client_type": "4", + "User-Agent": "okhttp/4.8.0", + } + res = await self.client.post( + self.GAME_LTOKEN_API, + headers=headers, + json={"account_id": self.uid, "game_token": game_token}, ) - res_json = data.json() - if self.check_error(res_json): + return res.json() + + async def create_login_data(self) -> str: + self.device_id = get_device_id("".join(random.choices((ascii_letters + digits), k=64))) + data = {"app_id": "4", "device": self.device_id} + res = await self.client.post(self.QRCODE_GEN_API, json=data) + res_json = res.json() + url = res_json.get("data", {}).get("url", "") + if not url: + return "" + self.ticket = url.split("ticket=")[1] + return url + + async def get_cookie_token_data(self, game_token: str) -> Dict: + res = await self.client.get( + self.GAME_TOKEN_API, + params={"game_token": game_token, "account_id": self.uid}, + ) + return res.json() + + async def set_cookie(self, data: Dict) -> bool: + self.cookie = {} + game_token = json.loads(data.get("payload", {}).get("raw", "{}")) + if not game_token: return False + self.uid = int(game_token["uid"]) + for item in ["login_uid", "stuid", "ltuid", "account_id"]: + self.cookie[item] = str(self.uid) + cookie_token_data = await self.get_cookie_token_data(game_token["token"]) + ltoken_data = await self.get_ltoken_by_game_token(game_token["token"]) + self.cookie["cookie_token"] = cookie_token_data["data"]["cookie_token"] + for item in ["account_mid_v2", "ltmid_v2"]: + self.cookie[item] = ltoken_data["data"]["user_info"]["mid"] + self.cookie["ltoken_v2"] = ltoken_data["data"]["token"]["token"] + return True - for k, v in data.cookies.items(): - self.cookie[k] = v + async def check_login(self): + data = {"app_id": "4", "ticket": self.ticket, "device": self.device_id} + for _ in range(20): + await sleep(10) + res = await self.client.post(self.QRCODE_GET_API, json=data) + res_json = res.json() + ret_code = res_json.get("retcode", 1) + if ret_code != 0: + print(res_json) + return False + data = res_json.get("data", {}) + if data.get("stat", "") == "Confirmed": + return await self.set_cookie(res_json.get("data", {})) - return "cookie_token" in self.cookie or "cookie_token_v2" in self.cookie + @staticmethod + def generate_qrcode(url: str) -> bytes: + qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=4) + qr.add_data(url) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + bio = BytesIO() + img.save(bio, format="PNG") + return bio.getvalue() diff --git a/plugins/genshin/cookies.py b/plugins/genshin/cookies.py index b05168d7..03271258 100644 --- a/plugins/genshin/cookies.py +++ b/plugins/genshin/cookies.py @@ -6,6 +6,7 @@ from arkowrapper import ArkoWrapper from genshin import DataNotPublic, GenshinException, InvalidCookies, types from genshin.models import GenshinAccount from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, TelegramObject, Update +from telegram.constants import ParseMode from telegram.ext import CallbackContext, ConversationHandler, filters from telegram.helpers import escape_markdown @@ -34,7 +35,7 @@ class AddUserCommandData(TelegramObject): sign_in_client: Optional[SignIn] = None -CHECK_SERVER, CHECK_PHONE, CHECK_CAPTCHA, INPUT_COOKIES, COMMAND_RESULT = range(10100, 10105) +CHECK_SERVER, INPUT_COOKIES, COMMAND_RESULT = range(10100, 10103) class SetUserCookies(Plugin.Conversation, BasePlugin.Conversation): @@ -85,20 +86,41 @@ class SetUserCookies(Plugin.Conversation, BasePlugin.Conversation): return CHECK_SERVER @conversation.entry_point - @handler.command(command="mlogin", filters=filters.ChatType.PRIVATE, block=True) + @handler.command("qlogin", filters=filters.ChatType.PRIVATE, block=True) @error_callable - async def choose_method(self, update: Update, context: CallbackContext) -> int: + async def qrcode_login(self, update: Update, context: CallbackContext): user = update.effective_user message = update.effective_message - logger.info(f"用户 {user.full_name}[{user.id}] 绑定账号命令请求") + logger.info("用户 %s[%s] 绑定账号命令请求", 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 + add_user_command_data = AddUserCommandData() + add_user_command_data.region = RegionEnum.HYPERION + context.chat_data["add_user_command_data"] = add_user_command_data + try: + user_info = await self.user_service.get_user_by_id(user.id) + except UserNotFoundError: + user_info = None + if user_info is not None: + try: + cookies_database_data = await self.cookies_service.get_cookies(user.id, RegionEnum.HYPERION) + add_user_command_data.cookies_database_data = cookies_database_data + except CookiesNotFoundError: + await message.reply_text("你已经绑定UID,如果继续操作会覆盖当前UID。") + else: + await message.reply_text("警告,你已经绑定Cookie,如果继续操作会覆盖当前Cookie。") + add_user_command_data.user = user_info + sign_in_client = SignIn() + url = await sign_in_client.create_login_data() + data = sign_in_client.generate_qrcode(url) + text = f"你好 {user.mention_html()} !该绑定方法仅支持国服,请在3分钟内使用米游社扫码并确认进行绑定。" + await message.reply_photo(data, caption=text, parse_mode=ParseMode.HTML) + if await sign_in_client.check_login(): + add_user_command_data.cookies = sign_in_client.cookie + return await self.check_cookies(update, context) + else: + await message.reply_markdown_v2("可能是验证码已过期或者你没有同意授权,请重新发送命令进行绑定。") + return ConversationHandler.END @conversation.state(state=CHECK_SERVER) @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) @@ -177,79 +199,6 @@ class SetUserCookies(Plugin.Conversation, BasePlugin.Conversation): await message.reply_html(help_message, disable_web_page_preview=True) return INPUT_COOKIES - @conversation.state(state=CHECK_PHONE) - @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=True) - @error_callable - 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: - user = update.effective_user - 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 = 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 as exc: # pylint: disable=W0703 - logger.error(f"用户 {user.full_name}[{user.id}] 登录失败 {repr(exc)}") - 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 as exc: # pylint: disable=W0703 - logger.error(f"用户 {user.full_name}[{user.id}] 登录失败 {repr(exc)}") - 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 @@ -325,7 +274,7 @@ class SetUserCookies(Plugin.Conversation, BasePlugin.Conversation): return ConversationHandler.END except (AttributeError, ValueError) as exc: logger.warning("用户 %s[%s] Cookies错误", user.full_name, user.id) - logger.debug("用户 %s[%s] Cookies错误" % (user.full_name, user.id), exc_info=exc) + logger.debug("用户 %s[%s] Cookies错误", user.full_name, user.id, exc_info=exc) await message.reply_text("Cookies错误,请检查是否正确", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END with contextlib.suppress(Exception): @@ -347,7 +296,7 @@ class SetUserCookies(Plugin.Conversation, BasePlugin.Conversation): add_user_command_data.game_uid = user_info.uid reply_keyboard = [["确认", "退出"]] await message.reply_text("获取角色基础信息成功,请检查是否正确!") - logger.info(f"用户 {user.full_name}[{user.id}] 获取账号 {user_info.nickname}[{user_info.uid}] 信息成功") + logger.info("用户 %s[%s] 获取账号 {%s}[%s] 信息成功", user.full_name, user.id, user_info.nickname, user_info.uid) text = ( f"*角色信息*\n" f"角色名称:{escape_markdown(user_info.nickname, version=2)}\n" diff --git a/poetry.lock b/poetry.lock index 63a9b12d..119ab7b9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -167,7 +167,7 @@ python-versions = ">=3.5" dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "backports.zoneinfo" @@ -242,7 +242,7 @@ optional = false python-versions = ">=3.6.0" [package.extras] -unicode-backport = ["unicodedata2"] +unicode_backport = ["unicodedata2"] [[package]] name = "click" @@ -304,7 +304,7 @@ dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version [[package]] name = "enkanetwork.py" -version = "1.3.0" +version = "1.2.10" description = "Library for fetching JSON data from site https://enka.network/" category = "main" optional = false @@ -392,8 +392,8 @@ python-versions = ">=3.7" [[package]] name = "genshin" -version = "1.4.0" -description = "" +version = "1.3.0" +description = "An API wrapper for Genshin Impact." category = "main" optional = false python-versions = ">=3.8" @@ -413,7 +413,7 @@ geetest = ["rsa"] type = "git" url = "https://github.com/thesadru/genshin.py" reference = "HEAD" -resolved_reference = "072c7c101b648f7a847ab27a4b1ae47c943ee316" +resolved_reference = "6685886c4651e489c0fc145af4a61e826c7750ac" [[package]] name = "gitdb" @@ -890,6 +890,24 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "qrcode" +version = "7.3.1" +description = "QR Code image generator" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +all = ["pillow", "pytest", "pytest", "pytest-cov", "tox", "zest.releaser[recommended]"] +dev = ["pytest", "tox"] +maintainer = ["zest.releaser[recommended]"] +pil = ["pillow"] +test = ["pytest", "pytest-cov"] + [[package]] name = "redis" version = "4.3.4" @@ -960,7 +978,7 @@ falcon = ["falcon (>=1.4)"] fastapi = ["fastapi (>=0.79.0)"] flask = ["blinker (>=1.1)", "flask (>=0.11)"] httpx = ["httpx (>=0.16.0)"] -pure-eval = ["asttokens", "executing", "pure-eval"] +pure_eval = ["asttokens", "executing", "pure-eval"] pymongo = ["pymongo (>=3.1)"] pyspark = ["pyspark (>=2.4.4)"] quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] @@ -1039,19 +1057,19 @@ aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] +mariadb_connector = ["mariadb (>=1.0.1,!=1.1.2)"] mssql = ["pyodbc"] -mssql-pymssql = ["pymssql"] -mssql-pyodbc = ["pyodbc"] +mssql_pymssql = ["pymssql"] +mssql_pyodbc = ["pyodbc"] mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] -mysql-connector = ["mysql-connector-python"] +mysql_connector = ["mysql-connector-python"] oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"] postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] -postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] -postgresql-psycopg2binary = ["psycopg2-binary"] -postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql_asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] +postgresql_psycopg2binary = ["psycopg2-binary"] +postgresql_psycopg2cffi = ["psycopg2cffi"] pymysql = ["pymysql", "pymysql (<1)"] sqlcipher = ["sqlcipher3_binary"] @@ -1294,7 +1312,7 @@ test = ["pytest", "pytest-asyncio", "flaky"] [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "762203f9de83ba0b8003e9e7ba233477e701d234ac0c782635c4870537a4fe93" +content-hash = "e1cf967bf3a56d440f3caadfa5bc93bf5b58a218c0a010d15f0fc3db1d8635f7" [metadata.files] aiofiles = [ @@ -1761,6 +1779,7 @@ lxml = [ {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0"}, {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e"}, {file = "lxml-4.9.2-cp35-cp35m-win32.whl", hash = "sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df"}, + {file = "lxml-4.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5"}, {file = "lxml-4.9.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53"}, {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7"}, {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe"}, @@ -1770,6 +1789,7 @@ lxml = [ {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74"}, {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38"}, {file = "lxml-4.9.2-cp36-cp36m-win32.whl", hash = "sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5"}, + {file = "lxml-4.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3"}, {file = "lxml-4.9.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03"}, {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941"}, {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726"}, @@ -2147,6 +2167,9 @@ PyYAML = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] +qrcode = [ + {file = "qrcode-7.3.1.tar.gz", hash = "sha256:375a6ff240ca9bd41adc070428b5dfc1dcfbb0f2507f1ac848f6cded38956578"}, +] redis = [ {file = "redis-4.3.4-py3-none-any.whl", hash = "sha256:a52d5694c9eb4292770084fa8c863f79367ca19884b329ab574d5cb2036b3e54"}, {file = "redis-4.3.4.tar.gz", hash = "sha256:ddf27071df4adf3821c4f2ca59d67525c3a82e5f268bed97b813cb4fabf87880"}, diff --git a/pyproject.toml b/pyproject.toml index 722f3983..2ec58501 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ GitPython = "^3.1.29" openpyxl = "^3.0.10" async-lru = "^1.0.3" thefuzz = "^0.19.0" +qrcode = "^7.3.1" [tool.poetry.extras] pyro = ["Pyrogram", "TgCrypto"]