Stop auto-accepting ToS on sign_up, add get_tos instead

This commit is contained in:
Lonami Exo 2022-02-17 12:40:09 +01:00
parent 80d44cb75b
commit 0bc598c121
7 changed files with 240 additions and 18 deletions

View File

@ -1,3 +1,4 @@
import asyncio
import getpass
import inspect
import os
@ -235,7 +236,7 @@ async def sign_in(
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)
self._tos = result.terms_of_service
self._tos = (result.terms_of_service, None)
raise errors.SignUpRequired()
return await _update_session_state(self, result.user)
@ -258,15 +259,10 @@ async def sign_up(
# because the user already tried to sign in.
#
# We're emulating pre-layer 104 behaviour so except the right error:
if not self._tos:
try:
return await self.sign_in(code=code)
except errors.SignUpRequired:
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()
try:
return await self.sign_in(code=code)
except errors.SignUpRequired:
pass # code is correct and was used, now need to sign in
result = await self(_tl.fn.auth.SignUp(
phone_number=phone,
@ -275,12 +271,23 @@ async def sign_up(
last_name=last_name
))
if self._tos:
await self(_tl.fn.help.AcceptTermsOfService(self._tos.id))
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):
"""
Callback called whenever the login or sign up process completes.

View File

@ -142,6 +142,7 @@ def init(
self.flood_sleep_threshold = flood_sleep_threshold
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._tos = None # used during signup and when fetching tos (tos/expiry)
# Update handling.
self._catch_up = catch_up

View File

@ -455,10 +455,15 @@ class TelegramClient:
You must call `send_code_request` first.
**By using this method you're agreeing to Telegram's
Terms of Service. This is required and your account
will be banned otherwise.** See https://telegram.org/tos
and https://core.telegram.org/api/terms.
.. important::
When creating a new account, you must be sure to show the Terms of Service
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
first_name (`str`):
@ -481,6 +486,16 @@ class TelegramClient:
code = input('enter 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)
@ -628,6 +643,42 @@ class TelegramClient:
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):
await self.connect()
return self

View File

@ -10,6 +10,7 @@ from ..errors._rpcbase import RpcError, ServerError, FloodError, InvalidDcError,
from .._misc import helpers, utils, hints
from .._sessions.types import Entity
from .. import errors, _tl
from ..types import _custom
from .account import ignore_takeout
_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') \
-> 'typing.Union[_tl.User, _tl.InputPeerUser]':
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:
return None

View File

@ -17,4 +17,5 @@ from ._custom import (
ParticipantPermissions,
Chat,
User,
TermsOfService,
)

View File

@ -14,3 +14,4 @@ from .qrlogin import QRLogin
from .participantpermissions import ParticipantPermissions
from .chat import Chat
from .user import User
from .tos import TermsOfService

View 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