pyrofork: Implement QrCode Login

Signed-off-by: wulan17 <wulan17@komodos.id>
This commit is contained in:
wulan17 2025-05-13 23:05:36 +07:00
parent 716c6212b2
commit 4bcbfe7054
No known key found for this signature in database
GPG key ID: 737814D4B5FF0420
6 changed files with 239 additions and 42 deletions

View file

@ -421,6 +421,7 @@ def pyrogram_api():
resend_code resend_code
sign_in sign_in
sign_in_bot sign_in_bot
sign_in_qrcode
sign_up sign_up
get_password_hint get_password_hint
check_password check_password
@ -724,6 +725,7 @@ def pyrogram_api():
Authorization Authorization
ActiveSession ActiveSession
ActiveSessions ActiveSessions
LoginToken
SentCode SentCode
TermsOfService TermsOfService
""" """

View file

@ -26,6 +26,7 @@ import platform
import re import re
import shutil import shutil
import sys import sys
from base64 import b64encode
from concurrent.futures.thread import ThreadPoolExecutor from concurrent.futures.thread import ThreadPoolExecutor
from datetime import datetime, timedelta from datetime import datetime, timedelta
from hashlib import sha256 from hashlib import sha256
@ -130,6 +131,9 @@ class Client(Methods):
Pass a session string to load the session in-memory. Pass a session string to load the session in-memory.
Implies ``in_memory=True``. Implies ``in_memory=True``.
use_qrcode (``bool``, *optional*):
Pass True to login using a QR code.
in_memory (``bool``, *optional*): in_memory (``bool``, *optional*):
Pass True to start an in-memory session that will be discarded as soon as the client stops. Pass True to start an in-memory session that will be discarded as soon as the client stops.
In order to reconnect again using an in-memory session without having to login again, you can use In order to reconnect again using an in-memory session without having to login again, you can use
@ -254,6 +258,7 @@ class Client(Methods):
test_mode: Optional[bool] = False, test_mode: Optional[bool] = False,
bot_token: Optional[str] = None, bot_token: Optional[str] = None,
session_string: Optional[str] = None, session_string: Optional[str] = None,
use_qrcode: Optional[bool] = False,
in_memory: Optional[bool] = None, in_memory: Optional[bool] = None,
mongodb: Optional[dict] = None, mongodb: Optional[dict] = None,
storage: Optional[Storage] = None, storage: Optional[Storage] = None,
@ -289,6 +294,7 @@ class Client(Methods):
self.test_mode = test_mode self.test_mode = test_mode
self.bot_token = bot_token self.bot_token = bot_token
self.session_string = session_string self.session_string = session_string
self.use_qrcode = use_qrcode
self.in_memory = in_memory self.in_memory = in_memory
self.mongodb = mongodb self.mongodb = mongodb
self.phone_number = phone_number self.phone_number = phone_number
@ -404,52 +410,55 @@ class Client(Methods):
print(f"Welcome to Pyrogram (version {__version__})") print(f"Welcome to Pyrogram (version {__version__})")
print(f"Pyrogram is free software and comes with ABSOLUTELY NO WARRANTY. Licensed\n" print(f"Pyrogram is free software and comes with ABSOLUTELY NO WARRANTY. Licensed\n"
f"under the terms of the {__license__}.\n") f"under the terms of the {__license__}.\n")
if not self.use_qrcode:
while True:
try:
if not self.phone_number:
while True:
value = await ainput("Enter phone number or bot token: ")
if not value:
continue
confirm = (await ainput(f'Is "{value}" correct? (y/N): ')).lower()
if confirm == "y":
break
if ":" in value:
self.bot_token = value
return await self.sign_in_bot(value)
else:
self.phone_number = value
sent_code = await self.send_code(self.phone_number)
except BadRequest as e:
print(e.MESSAGE)
self.phone_number = None
self.bot_token = None
else:
break
sent_code_descriptions = {
enums.SentCodeType.APP: "Telegram app",
enums.SentCodeType.SMS: "SMS",
enums.SentCodeType.CALL: "phone call",
enums.SentCodeType.FLASH_CALL: "phone flash call",
enums.SentCodeType.FRAGMENT_SMS: "Fragment SMS",
enums.SentCodeType.EMAIL_CODE: "email code"
}
print(f"The confirmation code has been sent via {sent_code_descriptions[sent_code.type]}")
while True: while True:
try: if not self.use_qrcode and not self.phone_code:
if not self.phone_number:
while True:
value = await ainput("Enter phone number or bot token: ")
if not value:
continue
confirm = (await ainput(f'Is "{value}" correct? (y/N): ')).lower()
if confirm == "y":
break
if ":" in value:
self.bot_token = value
return await self.sign_in_bot(value)
else:
self.phone_number = value
sent_code = await self.send_code(self.phone_number)
except BadRequest as e:
print(e.MESSAGE)
self.phone_number = None
self.bot_token = None
else:
break
sent_code_descriptions = {
enums.SentCodeType.APP: "Telegram app",
enums.SentCodeType.SMS: "SMS",
enums.SentCodeType.CALL: "phone call",
enums.SentCodeType.FLASH_CALL: "phone flash call",
enums.SentCodeType.FRAGMENT_SMS: "Fragment SMS",
enums.SentCodeType.EMAIL_CODE: "email code"
}
print(f"The confirmation code has been sent via {sent_code_descriptions[sent_code.type]}")
while True:
if not self.phone_code:
self.phone_code = await ainput("Enter confirmation code: ") self.phone_code = await ainput("Enter confirmation code: ")
try: try:
signed_in = await self.sign_in(self.phone_number, sent_code.phone_code_hash, self.phone_code) if self.use_qrcode:
signed_in = await self.sign_in_qrcode()
else:
signed_in = await self.sign_in(self.phone_number, sent_code.phone_code_hash, self.phone_code)
except BadRequest as e: except BadRequest as e:
print(e.MESSAGE) print(e.MESSAGE)
self.phone_code = None self.phone_code = None
@ -488,7 +497,13 @@ class Client(Methods):
print(e.MESSAGE) print(e.MESSAGE)
self.password = None self.password = None
else: else:
break if self.use_qrcode and isinstance(signed_in, raw.types.auth.LoginToken):
# TODO: Handle pyrogram.raw.types.UpdateLoginToken
time_out = signed_in.expires - datetime.timestamp(datetime.now()) - 10 # 10 seconds buffer
await asyncio.sleep(time_out)
print("QR code expired, Requesting new Qr code...")
else:
break
if isinstance(signed_in, User): if isinstance(signed_in, User):
return signed_in return signed_in

View file

@ -31,6 +31,7 @@ from .send_code import SendCode
from .send_recovery_code import SendRecoveryCode from .send_recovery_code import SendRecoveryCode
from .sign_in import SignIn from .sign_in import SignIn
from .sign_in_bot import SignInBot from .sign_in_bot import SignInBot
from .sign_in_qrcode import SignInQrcode
from .sign_up import SignUp from .sign_up import SignUp
from .terminate import Terminate from .terminate import Terminate
@ -50,6 +51,7 @@ class Auth(
SendRecoveryCode, SendRecoveryCode,
SignIn, SignIn,
SignInBot, SignInBot,
SignInQrcode,
SignUp, SignUp,
Terminate Terminate
): ):

View file

@ -0,0 +1,139 @@
# Pyrofork - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-present Dan <https://github.com/delivrance>
# Copyright (C) 2022-present Mayuri-Chan <https://github.com/Mayuri-Chan>
#
# This file is part of Pyrofork.
#
# Pyrofork is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrofork is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrofork. If not, see <http://www.gnu.org/licenses/>.
import logging
from base64 import b64encode
from typing import Union
import pyrogram
from pyrogram import raw
from pyrogram import types
from pyrogram.session import Session, Auth
log = logging.getLogger(__name__)
QRCODE_AVAIL = False
try:
import qrcode
QRCODE_AVAIL = True
except ImportError:
QRCODE_AVAIL = False
class SignInQrcode:
async def sign_in_qrcode(
self: "pyrogram.Client"
) -> Union["types.User", "types.LoginToken"]:
"""Authorize a user in Telegram with a QR code.
.. include:: /_includes/usable-by/users.rst
Returns:
:obj:`~pyrogram.types.User` | :obj:`pyrogram.types.LoginToken`, in case the
authorization completed, the user is returned. In case the QR code is
not scanned, a login token is returned.
Raises:
ImportError: In case the qrcode library is not installed.
SessionPasswordNeeded: In case a password is needed to sign in.
"""
if not QRCODE_AVAIL:
raise ImportError("qrcode is missing! "
"Please install it with `pip install qrcode`")
r = await self.session.invoke(
raw.functions.auth.ExportLoginToken(
api_id=self.api_id,
api_hash=self.api_hash,
except_ids=[]
)
)
if isinstance(r, raw.types.auth.LoginToken):
base64_token = b64encode(r.token).decode("utf-8")
login_url = f"tg://login?token={base64_token}"
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(login_url)
qr.make(fit=True)
print("Scan the QR code with your Telegram app.")
qr.print_ascii()
return r
elif isinstance(r, raw.types.auth.LoginTokenSuccess):
await self.storage.user_id(r.authorization.user.id)
await self.storage.is_bot(False)
return types.User._parse(self, r.authorization.user)
elif isinstance(r, raw.types.auth.LoginTokenMigrateTo):
# pylint: disable=access-member-before-definition
await self.session.stop()
await self.storage.dc_id(r.dc_id)
await self.storage.auth_key(
await Auth(
self, await self.storage.dc_id(),
await self.storage.test_mode()
).create()
)
self.session = Session(
self, await self.storage.dc_id(),
await self.storage.auth_key(), await self.storage.test_mode()
)
await self.session.start()
r = await self.session.invoke(
raw.functions.auth.ImportLoginToken(
token=r.token
)
)
if isinstance(r, raw.types.auth.LoginToken):
base64_token = b64encode(r.token).decode("utf-8")
login_url = f"tg://login?token={base64_token}"
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(login_url)
qr.make(fit=True)
print("Scan the QR code below with your Telegram app.")
qr.print_ascii()
return types.LoginToken(
self,
r.token,
r.expires
)
elif isinstance(r, raw.types.auth.LoginTokenSuccess):
await self.storage.user_id(r.authorization.user.id)
await self.storage.is_bot(False)
return types.User._parse(self, r.authorization.user)
else:
raise pyrogram.exceptions.RPCError(
"Unknown response type from Telegram API"
)
return r

View file

@ -19,12 +19,14 @@
from .active_session import ActiveSession from .active_session import ActiveSession
from .active_sessions import ActiveSessions from .active_sessions import ActiveSessions
from .login_token import LoginToken
from .sent_code import SentCode from .sent_code import SentCode
from .terms_of_service import TermsOfService from .terms_of_service import TermsOfService
__all__ = [ __all__ = [
"ActiveSession", "ActiveSession",
"ActiveSessions", "ActiveSessions",
"LoginToken",
"SentCode", "SentCode",
"TermsOfService", "TermsOfService",
] ]

View file

@ -0,0 +1,37 @@
# Pyrofork - Telegram MTProto API Client Library for Python
# Copyright (C) 2022-present Mayuri-Chan <https://github.com/Mayuri-Chan>
#
# This file is part of Pyrofork.
#
# Pyrofork is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrofork is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrofork. If not, see <http://www.gnu.org/licenses/>.
from ..object import Object
class LoginToken(Object):
"""Contains info on a login token.
Parameters:
token (``str``):
The login token.
expires (``int``):
The expiration date of the token in UNIX format.
"""
def __init__(self, *, token: str, expires: int):
super().__init__()
self.token = token
self.expires = expires