# Friendly Telegram (telegram userbot) # Copyright (C) 2018-2019 The Authors # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program 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 Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. from aiohttp import web import aiohttp_jinja2 import asyncio import collections import os import re import secrets import string import telethon from .. import utils class Web: def __init__(self, **kwargs): self.heroku_api_token = os.environ.get("heroku_api_token") self.api_token = kwargs.pop("api_token") self.redirect_url = None super().__init__(**kwargs) self.app.router.add_get("/initialSetup", self.initial_setup) self.app.router.add_put("/setApi", self.set_tg_api) self.app.router.add_post("/sendTgCode", self.send_tg_code) self.app.router.add_post("/tgCode", self.tg_code) self.app.router.add_post("/finishLogin", self.finish_login) self.api_set = asyncio.Event() self.sign_in_clients = {} self.clients = [] self.clients_set = asyncio.Event() self.root_redirected = asyncio.Event() self._pending_secret_to_uid = {} async def root(self, request): if self.clients_set.is_set(): await self.ready.wait() if self.redirect_url: self.root_redirected.set() return web.Response(status=302, headers={"Location": self.redirect_url}) if self.client_data: return await super().root(request) return await self.initial_setup(request) @aiohttp_jinja2.template("initial_root.jinja2") async def initial_setup(self, request): if self.client_data and await self.check_user(request) is None: return web.Response(status=302, headers={"Location": "/"}) # They gotta sign in. return {"api_done": self.api_token is not None, "tg_done": bool(self.client_data), "heroku_token": self.heroku_api_token} def wait_for_api_token_setup(self): return self.api_set.wait() def wait_for_clients_setup(self): return self.clients_set.wait() async def set_tg_api(self, request): if self.client_data and await self.check_user(request) is None: return web.Response(status=302, headers={"Location": "/"}) # They gotta sign in. text = await request.text() if len(text) < 36: return web.Response(status=400) api_id = text[32:] api_hash = text[:32] if any(c not in string.hexdigits for c in api_hash) or any(c not in string.digits for c in api_id): return web.Response(status=400) with open(os.path.join(utils.get_base_dir(), "api_token.py"), "w") as f: f.write("HASH = \"" + api_hash + "\"\nID = \"" + api_id + "\"\n") self.api_token = collections.namedtuple("api_token", ("ID", "HASH"))(api_id, api_hash) self.api_set.set() return web.Response() async def send_tg_code(self, request): if self.client_data and await self.check_user(request) is None: return web.Response(status=302, headers={"Location": "/"}) # They gotta sign in. text = await request.text() phone = telethon.utils.parse_phone(text) if not phone: return web.Response(status=400) client = telethon.TelegramClient(telethon.sessions.MemorySession(), self.api_token.ID, self.api_token.HASH, connection_retries=None) await client.connect() await client.send_code_request(phone) self.sign_in_clients[phone] = client return web.Response() async def tg_code(self, request): if self.client_data and await self.check_user(request) is None: return web.Response(status=302, headers={"Location": "/"}) # They gotta sign in. text = await request.text() if len(text) < 6: return web.Response(status=400) split = text.split("\n", 2) if len(split) not in (2, 3): return web.Response(status=400) code = split[0] phone = telethon.utils.parse_phone(split[1]) password = split[2] if (len(code) != 5 and not password) or any(c not in string.digits for c in code) or not phone: return web.Response(status=400) client = self.sign_in_clients[phone] if not password: try: user = await client.sign_in(phone, code=code) except telethon.errors.SessionPasswordNeededError: return web.Response(status=401) # Requires 2FA login except telethon.errors.PhoneCodeExpiredError: return web.Response(status=404) except telethon.errors.PhoneCodeInvalidError: return web.Response(status=403) except telethon.errors.FloodWaitError: return web.Response(status=421) else: try: user = await client.sign_in(phone, password=password) except telethon.errors.PasswordHashInvalidError: return web.Response(status=403) # Invalid 2FA password except telethon.errors.FloodWaitError: return web.Response(status=421) del self.sign_in_clients[phone] client.phone = "+" + user.phone self.clients.append(client) secret = secrets.token_urlsafe() self._pending_secret_to_uid[secret] = user.id return web.Response(text=secret) async def finish_login(self, request): if not self.clients: return web.Response(status=400) text = await request.text() if text: if not re.fullmatch(r"[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}", text): return web.Response(status=400) self.heroku_api_token = text else: self.heroku_api_token = None self._secret_to_uid.update(self._pending_secret_to_uid) self.clients_set.set() return web.Response()