Skip to content

Commit 40cd7ba

Browse files
committed
pyrofork: Implement QrCode Login
Signed-off-by: wulan17 <wulan17@komodos.id>
1 parent c0c7634 commit 40cd7ba

6 files changed

Lines changed: 246 additions & 34 deletions

File tree

compiler/docs/compiler.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ def get_title_list(s: str) -> list:
421421
resend_code
422422
sign_in
423423
sign_in_bot
424+
sign_in_qrcode
424425
sign_up
425426
get_password_hint
426427
check_password
@@ -724,6 +725,7 @@ def get_title_list(s: str) -> list:
724725
Authorization
725726
ActiveSession
726727
ActiveSessions
728+
LoginToken
727729
SentCode
728730
TermsOfService
729731
"""

pyrogram/client.py

Lines changed: 64 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ class Client(Methods):
130130
Pass a session string to load the session in-memory.
131131
Implies ``in_memory=True``.
132132
133+
use_qrcode (``bool``, *optional*):
134+
Pass True to login using a QR code.
135+
133136
in_memory (``bool``, *optional*):
134137
Pass True to start an in-memory session that will be discarded as soon as the client stops.
135138
In order to reconnect again using an in-memory session without having to login again, you can use
@@ -254,6 +257,7 @@ def __init__(
254257
test_mode: Optional[bool] = False,
255258
bot_token: Optional[str] = None,
256259
session_string: Optional[str] = None,
260+
use_qrcode: Optional[bool] = False,
257261
in_memory: Optional[bool] = None,
258262
mongodb: Optional[dict] = None,
259263
storage: Optional[Storage] = None,
@@ -289,6 +293,7 @@ def __init__(
289293
self.test_mode = test_mode
290294
self.bot_token = bot_token
291295
self.session_string = session_string
296+
self.use_qrcode = use_qrcode
292297
self.in_memory = in_memory
293298
self.mongodb = mongodb
294299
self.phone_number = phone_number
@@ -397,59 +402,76 @@ async def updates_watchdog(self):
397402
if datetime.now() - self.last_update_time > timedelta(seconds=self.UPDATES_WATCHDOG_INTERVAL):
398403
await self.invoke(raw.functions.updates.GetState())
399404

405+
async def _wait_for_update_login_token(self):
406+
"""
407+
Wait for an UpdateLoginToken update from Telegram.
408+
"""
409+
while True:
410+
update, _, _ = await self.dispatcher.updates_queue.get()
411+
if isinstance(update, raw.types.UpdateLoginToken):
412+
break
413+
400414
async def authorize(self) -> User:
401415
if self.bot_token:
402416
return await self.sign_in_bot(self.bot_token)
403417

404418
print(f"Welcome to Pyrogram (version {__version__})")
405419
print(f"Pyrogram is free software and comes with ABSOLUTELY NO WARRANTY. Licensed\n"
406420
f"under the terms of the {__license__}.\n")
421+
if not self.use_qrcode:
422+
while True:
423+
try:
424+
if not self.phone_number:
425+
while True:
426+
print("Enter 'qrcode' if you want to login with qrcode.")
427+
value = await ainput("Enter phone number or bot token: ")
407428

408-
while True:
409-
try:
410-
if not self.phone_number:
411-
while True:
412-
value = await ainput("Enter phone number or bot token: ")
429+
if not value:
430+
continue
413431

414-
if not value:
415-
continue
432+
if value.lower() == "qrcode":
433+
self.use_qrcode = True
434+
break
416435

417-
confirm = (await ainput(f'Is "{value}" correct? (y/N): ')).lower()
436+
confirm = (await ainput(f'Is "{value}" correct? (y/N): ')).lower()
418437

419-
if confirm == "y":
420-
break
438+
if confirm == "y":
439+
break
421440

422-
if ":" in value:
423-
self.bot_token = value
424-
return await self.sign_in_bot(value)
425-
else:
426-
self.phone_number = value
441+
if ":" in value:
442+
self.bot_token = value
443+
return await self.sign_in_bot(value)
444+
else:
445+
self.phone_number = value
427446

428-
sent_code = await self.send_code(self.phone_number)
429-
except BadRequest as e:
430-
print(e.MESSAGE)
431-
self.phone_number = None
432-
self.bot_token = None
433-
else:
434-
break
447+
sent_code = await self.send_code(self.phone_number)
448+
except BadRequest as e:
449+
print(e.MESSAGE)
450+
self.phone_number = None
451+
self.bot_token = None
452+
else:
453+
break
435454

436-
sent_code_descriptions = {
437-
enums.SentCodeType.APP: "Telegram app",
438-
enums.SentCodeType.SMS: "SMS",
439-
enums.SentCodeType.CALL: "phone call",
440-
enums.SentCodeType.FLASH_CALL: "phone flash call",
441-
enums.SentCodeType.FRAGMENT_SMS: "Fragment SMS",
442-
enums.SentCodeType.EMAIL_CODE: "email code"
443-
}
455+
sent_code_descriptions = {
456+
enums.SentCodeType.APP: "Telegram app",
457+
enums.SentCodeType.SMS: "SMS",
458+
enums.SentCodeType.CALL: "phone call",
459+
enums.SentCodeType.FLASH_CALL: "phone flash call",
460+
enums.SentCodeType.FRAGMENT_SMS: "Fragment SMS",
461+
enums.SentCodeType.EMAIL_CODE: "email code"
462+
}
444463

445-
print(f"The confirmation code has been sent via {sent_code_descriptions[sent_code.type]}")
464+
print(f"The confirmation code has been sent via {sent_code_descriptions[sent_code.type]}")
446465

447466
while True:
448-
if not self.phone_code:
467+
if not self.use_qrcode and not self.phone_code:
449468
self.phone_code = await ainput("Enter confirmation code: ")
450469

451470
try:
452-
signed_in = await self.sign_in(self.phone_number, sent_code.phone_code_hash, self.phone_code)
471+
if self.use_qrcode:
472+
signed_in = await self.sign_in_qrcode()
473+
else:
474+
signed_in = await self.sign_in(self.phone_number, sent_code.phone_code_hash, self.phone_code)
453475
except BadRequest as e:
454476
print(e.MESSAGE)
455477
self.phone_code = None
@@ -488,7 +510,15 @@ async def authorize(self) -> User:
488510
print(e.MESSAGE)
489511
self.password = None
490512
else:
491-
break
513+
if self.use_qrcode and isinstance(signed_in, raw.types.auth.LoginToken):
514+
time_out = signed_in.expires - datetime.timestamp(datetime.now())
515+
try:
516+
await asyncio.wait_for(self._wait_for_update_login_token(), timeout=time_out)
517+
except asyncio.TimeoutError:
518+
print("QR code expired, Requesting new Qr code...")
519+
continue
520+
else:
521+
break
492522

493523
if isinstance(signed_in, User):
494524
return signed_in

pyrogram/methods/auth/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from .send_recovery_code import SendRecoveryCode
3232
from .sign_in import SignIn
3333
from .sign_in_bot import SignInBot
34+
from .sign_in_qrcode import SignInQrcode
3435
from .sign_up import SignUp
3536
from .terminate import Terminate
3637

@@ -50,6 +51,7 @@ class Auth(
5051
SendRecoveryCode,
5152
SignIn,
5253
SignInBot,
54+
SignInQrcode,
5355
SignUp,
5456
Terminate
5557
):
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Pyrofork - Telegram MTProto API Client Library for Python
2+
# Copyright (C) 2017-present Dan <https://github.com/delivrance>
3+
# Copyright (C) 2022-present Mayuri-Chan <https://github.com/Mayuri-Chan>
4+
#
5+
# This file is part of Pyrofork.
6+
#
7+
# Pyrofork is free software: you can redistribute it and/or modify
8+
# it under the terms of the GNU Lesser General Public License as published
9+
# by the Free Software Foundation, either version 3 of the License, or
10+
# (at your option) any later version.
11+
#
12+
# Pyrofork is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU Lesser General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU Lesser General Public License
18+
# along with Pyrofork. If not, see <http://www.gnu.org/licenses/>.
19+
20+
import logging
21+
from base64 import b64encode
22+
from typing import Union
23+
24+
import pyrogram
25+
from pyrogram import raw
26+
from pyrogram import types
27+
from pyrogram.session import Session, Auth
28+
29+
log = logging.getLogger(__name__)
30+
31+
QRCODE_AVAIL = False
32+
try:
33+
import qrcode
34+
QRCODE_AVAIL = True
35+
except ImportError:
36+
QRCODE_AVAIL = False
37+
38+
39+
class SignInQrcode:
40+
async def sign_in_qrcode(
41+
self: "pyrogram.Client"
42+
) -> Union["types.User", "types.LoginToken"]:
43+
"""Authorize a user in Telegram with a QR code.
44+
45+
.. include:: /_includes/usable-by/users.rst
46+
47+
Returns:
48+
:obj:`~pyrogram.types.User` | :obj:`pyrogram.types.LoginToken`, in case the
49+
authorization completed, the user is returned. In case the QR code is
50+
not scanned, a login token is returned.
51+
52+
Raises:
53+
ImportError: In case the qrcode library is not installed.
54+
SessionPasswordNeeded: In case a password is needed to sign in.
55+
"""
56+
57+
if not QRCODE_AVAIL:
58+
raise ImportError("qrcode is missing! "
59+
"Please install it with `pip install qrcode`")
60+
r = await self.session.invoke(
61+
raw.functions.auth.ExportLoginToken(
62+
api_id=self.api_id,
63+
api_hash=self.api_hash,
64+
except_ids=[]
65+
)
66+
)
67+
if isinstance(r, raw.types.auth.LoginToken):
68+
base64_token = b64encode(r.token).decode("utf-8")
69+
login_url = f"tg://login?token={base64_token}"
70+
qr = qrcode.QRCode(
71+
version=1,
72+
error_correction=qrcode.constants.ERROR_CORRECT_L,
73+
box_size=10,
74+
border=4,
75+
)
76+
qr.add_data(login_url)
77+
qr.make(fit=True)
78+
79+
print("Scan the QR code with your Telegram app.")
80+
qr.print_ascii()
81+
82+
return r
83+
elif isinstance(r, raw.types.auth.LoginTokenSuccess):
84+
await self.storage.user_id(r.authorization.user.id)
85+
await self.storage.is_bot(False)
86+
87+
return types.User._parse(self, r.authorization.user)
88+
elif isinstance(r, raw.types.auth.LoginTokenMigrateTo):
89+
# pylint: disable=access-member-before-definition
90+
await self.session.stop()
91+
92+
await self.storage.dc_id(r.dc_id)
93+
await self.storage.auth_key(
94+
await Auth(
95+
self, await self.storage.dc_id(),
96+
await self.storage.test_mode()
97+
).create()
98+
)
99+
self.session = Session(
100+
self, await self.storage.dc_id(),
101+
await self.storage.auth_key(), await self.storage.test_mode()
102+
)
103+
104+
await self.session.start()
105+
r = await self.session.invoke(
106+
raw.functions.auth.ImportLoginToken(
107+
token=r.token
108+
)
109+
)
110+
if isinstance(r, raw.types.auth.LoginToken):
111+
base64_token = b64encode(r.token).decode("utf-8")
112+
login_url = f"tg://login?token={base64_token}"
113+
qr = qrcode.QRCode(
114+
version=1,
115+
error_correction=qrcode.constants.ERROR_CORRECT_L,
116+
box_size=10,
117+
border=4,
118+
)
119+
qr.add_data(login_url)
120+
qr.make(fit=True)
121+
122+
print("Scan the QR code below with your Telegram app.")
123+
qr.print_ascii()
124+
125+
return types.LoginToken(
126+
self,
127+
r.token,
128+
r.expires
129+
)
130+
elif isinstance(r, raw.types.auth.LoginTokenSuccess):
131+
await self.storage.user_id(r.authorization.user.id)
132+
await self.storage.is_bot(False)
133+
134+
return types.User._parse(self, r.authorization.user)
135+
else:
136+
raise pyrogram.exceptions.RPCError(
137+
"Unknown response type from Telegram API"
138+
)
139+
return r

pyrogram/types/authorization/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919

2020
from .active_session import ActiveSession
2121
from .active_sessions import ActiveSessions
22+
from .login_token import LoginToken
2223
from .sent_code import SentCode
2324
from .terms_of_service import TermsOfService
2425

2526
__all__ = [
2627
"ActiveSession",
2728
"ActiveSessions",
29+
"LoginToken",
2830
"SentCode",
2931
"TermsOfService",
3032
]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Pyrofork - Telegram MTProto API Client Library for Python
2+
# Copyright (C) 2022-present Mayuri-Chan <https://github.com/Mayuri-Chan>
3+
#
4+
# This file is part of Pyrofork.
5+
#
6+
# Pyrofork is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU Lesser General Public License as published
8+
# by the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# Pyrofork is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with Pyrofork. If not, see <http://www.gnu.org/licenses/>.
18+
19+
from ..object import Object
20+
21+
22+
class LoginToken(Object):
23+
"""Contains info on a login token.
24+
25+
Parameters:
26+
token (``str``):
27+
The login token.
28+
29+
expires (``int``):
30+
The expiration date of the token in UNIX format.
31+
"""
32+
33+
def __init__(self, *, token: str, expires: int):
34+
super().__init__()
35+
36+
self.token = token
37+
self.expires = expires

0 commit comments

Comments
 (0)