mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-02-21 14:05:17 +03:00
Stop auto-accepting ToS on sign_up, add get_tos instead
This commit is contained in:
parent
80d44cb75b
commit
0bc598c121
|
@ -1,3 +1,4 @@
|
||||||
|
import asyncio
|
||||||
import getpass
|
import getpass
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
|
@ -235,7 +236,7 @@ async def sign_in(
|
||||||
|
|
||||||
if isinstance(result, _tl.auth.AuthorizationSignUpRequired):
|
if isinstance(result, _tl.auth.AuthorizationSignUpRequired):
|
||||||
# The method must return the User but we don't have it, so raise instead (matches pre-layer 104 behaviour)
|
# The method must return the User but we don't have it, so raise instead (matches pre-layer 104 behaviour)
|
||||||
self._tos = result.terms_of_service
|
self._tos = (result.terms_of_service, None)
|
||||||
raise errors.SignUpRequired()
|
raise errors.SignUpRequired()
|
||||||
|
|
||||||
return await _update_session_state(self, result.user)
|
return await _update_session_state(self, result.user)
|
||||||
|
@ -258,16 +259,11 @@ async def sign_up(
|
||||||
# because the user already tried to sign in.
|
# because the user already tried to sign in.
|
||||||
#
|
#
|
||||||
# We're emulating pre-layer 104 behaviour so except the right error:
|
# We're emulating pre-layer 104 behaviour so except the right error:
|
||||||
if not self._tos:
|
|
||||||
try:
|
try:
|
||||||
return await self.sign_in(code=code)
|
return await self.sign_in(code=code)
|
||||||
except errors.SignUpRequired:
|
except errors.SignUpRequired:
|
||||||
pass # code is correct and was used, now need to sign in
|
pass # code is correct and was used, now need to sign in
|
||||||
|
|
||||||
if self._tos and self._tos.text:
|
|
||||||
sys.stderr.write("{}\n".format(self._tos.text))
|
|
||||||
sys.stderr.flush()
|
|
||||||
|
|
||||||
result = await self(_tl.fn.auth.SignUp(
|
result = await self(_tl.fn.auth.SignUp(
|
||||||
phone_number=phone,
|
phone_number=phone,
|
||||||
phone_code_hash=phone_code_hash,
|
phone_code_hash=phone_code_hash,
|
||||||
|
@ -275,12 +271,23 @@ async def sign_up(
|
||||||
last_name=last_name
|
last_name=last_name
|
||||||
))
|
))
|
||||||
|
|
||||||
if self._tos:
|
|
||||||
await self(_tl.fn.help.AcceptTermsOfService(self._tos.id))
|
|
||||||
|
|
||||||
return await _update_session_state(self, result.user)
|
return await _update_session_state(self, result.user)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_tos(self):
|
||||||
|
first_time = self._tos is None
|
||||||
|
no_tos = self._tos and self._tos[0] is None
|
||||||
|
tos_expired = self._tos and self._tos[1] is not None and asyncio.get_running_loop().time() >= self._tos[1]
|
||||||
|
|
||||||
|
if first_time or no_tos or tos_expired:
|
||||||
|
result = await self(_tl.fn.help.GetTermsOfServiceUpdate())
|
||||||
|
tos = getattr(result, 'terms_of_service', None)
|
||||||
|
self._tos = (tos, asyncio.get_running_loop().time() + result.expires)
|
||||||
|
|
||||||
|
# not stored in the client to prevent a cycle
|
||||||
|
return _custom.TermsOfService._new(self, *self._tos)
|
||||||
|
|
||||||
|
|
||||||
async def _update_session_state(self, user, save=True):
|
async def _update_session_state(self, user, save=True):
|
||||||
"""
|
"""
|
||||||
Callback called whenever the login or sign up process completes.
|
Callback called whenever the login or sign up process completes.
|
||||||
|
|
|
@ -142,6 +142,7 @@ def init(
|
||||||
self.flood_sleep_threshold = flood_sleep_threshold
|
self.flood_sleep_threshold = flood_sleep_threshold
|
||||||
self._flood_waited_requests = {} # prevent calls that would floodwait entirely
|
self._flood_waited_requests = {} # prevent calls that would floodwait entirely
|
||||||
self._phone_code_hash = None # used during login to prevent exposing the hash to end users
|
self._phone_code_hash = None # used during login to prevent exposing the hash to end users
|
||||||
|
self._tos = None # used during signup and when fetching tos (tos/expiry)
|
||||||
|
|
||||||
# Update handling.
|
# Update handling.
|
||||||
self._catch_up = catch_up
|
self._catch_up = catch_up
|
||||||
|
|
|
@ -455,10 +455,15 @@ class TelegramClient:
|
||||||
|
|
||||||
You must call `send_code_request` first.
|
You must call `send_code_request` first.
|
||||||
|
|
||||||
**By using this method you're agreeing to Telegram's
|
.. important::
|
||||||
Terms of Service. This is required and your account
|
|
||||||
will be banned otherwise.** See https://telegram.org/tos
|
When creating a new account, you must be sure to show the Terms of Service
|
||||||
and https://core.telegram.org/api/terms.
|
to the user, and only after they approve, the code can accept the Terms of
|
||||||
|
Service. If not, they must be declined, in which case the account **will be
|
||||||
|
deleted**.
|
||||||
|
|
||||||
|
Make sure to use `client.get_tos` to fetch the Terms of Service, and to
|
||||||
|
use `tos.accept()` or `tos.decline()` after the user selects an option.
|
||||||
|
|
||||||
Arguments
|
Arguments
|
||||||
first_name (`str`):
|
first_name (`str`):
|
||||||
|
@ -481,6 +486,16 @@ class TelegramClient:
|
||||||
|
|
||||||
code = input('enter code: ')
|
code = input('enter code: ')
|
||||||
await client.sign_up('Anna', 'Banana', code=code)
|
await client.sign_up('Anna', 'Banana', code=code)
|
||||||
|
|
||||||
|
# IMPORTANT: you MUST retrieve the Terms of Service and accept
|
||||||
|
# them, or Telegram has every right to delete the account.
|
||||||
|
tos = await client.get_tos()
|
||||||
|
print(tos.html)
|
||||||
|
|
||||||
|
if code('accept (y/n)?: ') == 'y':
|
||||||
|
await tos.accept()
|
||||||
|
else:
|
||||||
|
await tos.decline() # deletes the account!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@forward_call(auth.send_code_request)
|
@forward_call(auth.send_code_request)
|
||||||
|
@ -628,6 +643,42 @@ class TelegramClient:
|
||||||
await client.edit_2fa(current_password='I_<3_Telethon')
|
await client.edit_2fa(current_password='I_<3_Telethon')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@forward_call(auth.get_tos)
|
||||||
|
async def get_tos(self: 'TelegramClient') -> '_custom.TermsOfService':
|
||||||
|
"""
|
||||||
|
Fetch `Telegram's Terms of Service`_, which every user must accept in order to use
|
||||||
|
Telegram, or they must otherwise `delete their account`_.
|
||||||
|
|
||||||
|
This method **must** be called after sign up, and **should** be called again
|
||||||
|
after it expires (at the risk of having the account terminated otherwise).
|
||||||
|
|
||||||
|
See the documentation of `TermsOfService` for more information.
|
||||||
|
|
||||||
|
The library cannot automate this process because the user must read the Terms of Service.
|
||||||
|
Automating its usage without reading the terms would be done at the developer's own risk.
|
||||||
|
|
||||||
|
Example
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Fetch the ToS, forever (this could be a separate task, for example)
|
||||||
|
while True:
|
||||||
|
tos = await client.get_tos()
|
||||||
|
|
||||||
|
if tos:
|
||||||
|
# There's an update or they must be accepted (you could show a popup)
|
||||||
|
print(tos.html)
|
||||||
|
if code('accept (y/n)?: ') == 'y':
|
||||||
|
await tos.accept()
|
||||||
|
else:
|
||||||
|
await tos.decline() # deletes the account!
|
||||||
|
|
||||||
|
# after tos.timeout expires, the method should be called again!
|
||||||
|
await asyncio.sleep(tos.timeout)
|
||||||
|
|
||||||
|
_Telegram's Terms of Service: https://telegram.org/tos
|
||||||
|
_delete their account: https://core.telegram.org/api/config#terms-of-service
|
||||||
|
"""
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
await self.connect()
|
await self.connect()
|
||||||
return self
|
return self
|
||||||
|
|
|
@ -10,6 +10,7 @@ from ..errors._rpcbase import RpcError, ServerError, FloodError, InvalidDcError,
|
||||||
from .._misc import helpers, utils, hints
|
from .._misc import helpers, utils, hints
|
||||||
from .._sessions.types import Entity
|
from .._sessions.types import Entity
|
||||||
from .. import errors, _tl
|
from .. import errors, _tl
|
||||||
|
from ..types import _custom
|
||||||
from .account import ignore_takeout
|
from .account import ignore_takeout
|
||||||
|
|
||||||
_NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!')
|
_NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!')
|
||||||
|
@ -134,7 +135,7 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl
|
||||||
async def get_me(self: 'TelegramClient') \
|
async def get_me(self: 'TelegramClient') \
|
||||||
-> 'typing.Union[_tl.User, _tl.InputPeerUser]':
|
-> 'typing.Union[_tl.User, _tl.InputPeerUser]':
|
||||||
try:
|
try:
|
||||||
return (await self(_tl.fn.users.GetUsers([_tl.InputUserSelf()])))[0]
|
return _custom.User._new(self, (await self(_tl.fn.users.GetUsers([_tl.InputUserSelf()])))[0])
|
||||||
except UnauthorizedError:
|
except UnauthorizedError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -17,4 +17,5 @@ from ._custom import (
|
||||||
ParticipantPermissions,
|
ParticipantPermissions,
|
||||||
Chat,
|
Chat,
|
||||||
User,
|
User,
|
||||||
|
TermsOfService,
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,3 +14,4 @@ from .qrlogin import QRLogin
|
||||||
from .participantpermissions import ParticipantPermissions
|
from .participantpermissions import ParticipantPermissions
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
from .user import User
|
from .user import User
|
||||||
|
from .tos import TermsOfService
|
||||||
|
|
160
telethon/types/_custom/tos.py
Normal file
160
telethon/types/_custom/tos.py
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from typing import Optional, List, TYPE_CHECKING
|
||||||
|
from datetime import datetime
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import mimetypes
|
||||||
|
from .chatgetter import ChatGetter
|
||||||
|
from .sendergetter import SenderGetter
|
||||||
|
from .messagebutton import MessageButton
|
||||||
|
from .forward import Forward
|
||||||
|
from .file import File
|
||||||
|
from .inputfile import InputFile
|
||||||
|
from .inputmessage import InputMessage
|
||||||
|
from .button import build_reply_markup
|
||||||
|
from ..._misc import utils, helpers, tlobject, markdown, html
|
||||||
|
from ... import _tl, _misc
|
||||||
|
|
||||||
|
|
||||||
|
_DEFAULT_TIMEOUT = 24 * 60 * 60
|
||||||
|
|
||||||
|
|
||||||
|
class TermsOfService:
|
||||||
|
"""
|
||||||
|
Represents `Telegram's Terms of Service`_, which every user must accept in order to use
|
||||||
|
Telegram, or they must otherwise `delete their account`_.
|
||||||
|
|
||||||
|
This is not the same as the `API's Terms of Service`_, which every developer must accept
|
||||||
|
before creating applications for Telegram.
|
||||||
|
|
||||||
|
You must make sure to check for the terms text (or markdown, or HTML), as well as confirm
|
||||||
|
the user's age if required.
|
||||||
|
|
||||||
|
This class implements `__bool__`, meaning it will be truthy if there are terms to display,
|
||||||
|
and falsey otherwise.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
tos = await client.get_tos()
|
||||||
|
if tos:
|
||||||
|
print(tos.html) # there's something to read and accept or decline
|
||||||
|
...
|
||||||
|
else:
|
||||||
|
await asyncio.sleep(tos.timeout) # nothing to read, but still has tos.timeout
|
||||||
|
|
||||||
|
_Telegram's Terms of Service: https://telegram.org/tos
|
||||||
|
_delete their account: https://core.telegram.org/api/config#terms-of-service
|
||||||
|
_API's Terms of Service: https://core.telegram.org/api/terms
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self):
|
||||||
|
"""Plain-text version of the Terms of Service, or `None` if there is no ToS update."""
|
||||||
|
return self._tos and self._tos.text
|
||||||
|
|
||||||
|
@property
|
||||||
|
def markdown(self):
|
||||||
|
"""Markdown-formatted version of the Terms of Service, or `None` if there is no ToS update."""
|
||||||
|
return self._tos and markdown.unparse(self._tos.text, self._tos.entities)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def html(self):
|
||||||
|
"""HTML-formatted version of the Terms of Service, or `None` if there is no ToS update."""
|
||||||
|
return self._tos and html.unparse(self._tos.text, self._tos.entities)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def popup(self):
|
||||||
|
"""`True` a popup should be shown to the user."""
|
||||||
|
return self._tos and self._tos.popup
|
||||||
|
|
||||||
|
@property
|
||||||
|
def minimum_age(self):
|
||||||
|
"""The minimum age the user must be to accept the terms, or `None` if there's no requirement."""
|
||||||
|
return self._tos and self._tos.min_age_confirm
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timeout(self):
|
||||||
|
"""
|
||||||
|
How many seconds are left before `client.get_tos` should be used again.
|
||||||
|
|
||||||
|
This value is a positive floating point number, and is monotically decreasing.
|
||||||
|
The value will reach zero after enough seconds have elapsed. This lets you do some work
|
||||||
|
and call sleep on the value and still wait just long enough.
|
||||||
|
"""
|
||||||
|
return max(0.0, self._expiry - asyncio.get_running_loop().time())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expired(self):
|
||||||
|
"""
|
||||||
|
Returns `True` if this instance of the Terms of Service has expired and should be re-fetched.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
if tos.expired:
|
||||||
|
tos = await client.get_tos()
|
||||||
|
"""
|
||||||
|
return asyncio.get_running_loop() >= self._expiry
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
raise TypeError('You cannot create TermsOfService instances by hand!')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _new(cls, client, tos, expiry):
|
||||||
|
self = cls.__new__(cls)
|
||||||
|
self._client = client
|
||||||
|
self._tos = tos
|
||||||
|
self._expiry = expiry or asyncio.get_running_loop().time() + _DEFAULT_TIMEOUT
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def accept(self, *, age=None):
|
||||||
|
"""
|
||||||
|
Accept the Terms of Service.
|
||||||
|
|
||||||
|
Does nothing if there is nothing to accept.
|
||||||
|
|
||||||
|
If `minimum_age` is not `None`, the `age` parameter must be provided,
|
||||||
|
and be greater than or equal to `minimum_age`. Otherwise, the function will fail.
|
||||||
|
|
||||||
|
.. code-example:
|
||||||
|
|
||||||
|
if tos.minimum_age:
|
||||||
|
age = int(input('age: '))
|
||||||
|
else:
|
||||||
|
age = None
|
||||||
|
|
||||||
|
print(tos.html)
|
||||||
|
if input('accept (y/n)?: ') == 'y':
|
||||||
|
await tos.accept(age=age)
|
||||||
|
"""
|
||||||
|
if not self._tos:
|
||||||
|
return
|
||||||
|
|
||||||
|
if age < (self.minimum_age or 0):
|
||||||
|
raise ValueError('User is not old enough to accept the Terms of Service')
|
||||||
|
|
||||||
|
if age > 122:
|
||||||
|
# This easter egg may be out of date by 2025
|
||||||
|
print('Lying is done at your own risk!', file=sys.stderr)
|
||||||
|
|
||||||
|
await self._client(_tl.fn.help.AcceptTermsOfService(self._tos.id))
|
||||||
|
|
||||||
|
async def decline(self):
|
||||||
|
"""
|
||||||
|
Decline the Terms of Service.
|
||||||
|
|
||||||
|
Does nothing if there is nothing to decline.
|
||||||
|
|
||||||
|
.. danger::
|
||||||
|
|
||||||
|
Declining the Terms of Service will result in the `termination of your account`_.
|
||||||
|
**Your account will be deleted**.
|
||||||
|
|
||||||
|
_termination of your account: https://core.telegram.org/api/config#terms-of-service
|
||||||
|
"""
|
||||||
|
if not self._tos:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._client(_tl.fn.account.DeleteAccount('Decline ToS update'))
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return self._tos is not None
|
Loading…
Reference in New Issue
Block a user