root.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. """Main bot page"""
  2. # Friendly Telegram (telegram userbot)
  3. # Copyright (C) 2018-2021 The Authors
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU Affero General Public License for more details.
  12. # You should have received a copy of the GNU Affero General Public License
  13. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. # █ █ ▀ █▄▀ ▄▀█ █▀█ ▀
  15. # █▀█ █ █ █ █▀█ █▀▄ █
  16. # © Copyright 2022
  17. # https://t.me/hikariatama
  18. #
  19. # 🔒 Licensed under the GNU AGPLv3
  20. # 🌐 https://www.gnu.org/licenses/agpl-3.0.html
  21. import asyncio
  22. import collections
  23. import os
  24. import string
  25. from aiohttp import web
  26. import aiohttp_jinja2
  27. import atexit
  28. import functools
  29. import logging
  30. import sys
  31. import re
  32. import requests
  33. import time
  34. from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
  35. import telethon
  36. from telethon.errors.rpcerrorlist import YouBlockedUserError, FloodWaitError
  37. from telethon.tl.functions.contacts import UnblockRequest
  38. from .. import utils, main, database, heroku
  39. from ..tl_cache import CustomTelegramClient
  40. DATA_DIR = (
  41. os.path.normpath(os.path.join(utils.get_base_dir(), ".."))
  42. if "OKTETO" not in os.environ and "DOCKER" not in os.environ
  43. else "/data"
  44. )
  45. def restart(*argv):
  46. os.execl(
  47. sys.executable,
  48. sys.executable,
  49. "-m",
  50. os.path.relpath(utils.get_base_dir()),
  51. *argv,
  52. )
  53. class Web:
  54. sign_in_clients = {}
  55. _pending_client = None
  56. _sessions = []
  57. _ratelimit = {}
  58. def __init__(self, **kwargs):
  59. self.api_token = kwargs.pop("api_token")
  60. self.data_root = kwargs.pop("data_root")
  61. self.connection = kwargs.pop("connection")
  62. self.proxy = kwargs.pop("proxy")
  63. super().__init__(**kwargs)
  64. self.app.router.add_get("/", self.root)
  65. self.app.router.add_put("/setApi", self.set_tg_api)
  66. self.app.router.add_post("/sendTgCode", self.send_tg_code)
  67. self.app.router.add_post("/check_session", self.check_session)
  68. self.app.router.add_post("/web_auth", self.web_auth)
  69. self.app.router.add_post("/okteto", self.okteto)
  70. self.app.router.add_post("/tgCode", self.tg_code)
  71. self.app.router.add_post("/finishLogin", self.finish_login)
  72. self.app.router.add_post("/custom_bot", self.custom_bot)
  73. self.api_set = asyncio.Event()
  74. self.clients_set = asyncio.Event()
  75. @aiohttp_jinja2.template("root.jinja2")
  76. async def root(self, _):
  77. return {
  78. "skip_creds": self.api_token is not None,
  79. "tg_done": bool(self.client_data),
  80. "okteto": "OKTETO" in os.environ,
  81. "lavhost": "LAVHOST" in os.environ,
  82. "heroku": "DYNO" in os.environ,
  83. }
  84. async def check_session(self, request):
  85. return web.Response(body=("1" if self._check_session(request) else "0"))
  86. def wait_for_api_token_setup(self):
  87. return self.api_set.wait()
  88. def wait_for_clients_setup(self):
  89. return self.clients_set.wait()
  90. def _check_session(self, request) -> bool:
  91. return (
  92. request.cookies.get("session", None) in self._sessions
  93. if main.hikka.clients
  94. else True
  95. )
  96. async def _check_bot(
  97. self,
  98. client: CustomTelegramClient,
  99. username: str,
  100. ) -> bool:
  101. async with client.conversation("@BotFather", exclusive=False) as conv:
  102. try:
  103. m = await conv.send_message("/token")
  104. except YouBlockedUserError:
  105. await client(UnblockRequest(id="@BotFather"))
  106. m = await conv.send_message("/token")
  107. r = await conv.get_response()
  108. await m.delete()
  109. await r.delete()
  110. if not hasattr(r, "reply_markup") or not hasattr(r.reply_markup, "rows"):
  111. return False
  112. for row in r.reply_markup.rows:
  113. for button in row.buttons:
  114. if username != button.text.strip("@"):
  115. continue
  116. m = await conv.send_message("/cancel")
  117. r = await conv.get_response()
  118. await m.delete()
  119. await r.delete()
  120. return True
  121. async def custom_bot(self, request):
  122. if not self._check_session(request):
  123. return web.Response(status=401)
  124. text = await request.text()
  125. client = self._pending_client
  126. db = database.Database(client)
  127. await db.init()
  128. text = text.strip("@")
  129. if any(
  130. litera not in (string.ascii_letters + string.digits + "_")
  131. for litera in text
  132. ) or not text.lower().endswith("bot"):
  133. return web.Response(body="OCCUPIED")
  134. try:
  135. await client.get_entity(f"@{text}")
  136. except ValueError:
  137. pass
  138. else:
  139. if not await self._check_bot(client, text):
  140. return web.Response(body="OCCUPIED")
  141. db.set("hikka.inline", "custom_bot", text)
  142. return web.Response(body="OK")
  143. async def set_tg_api(self, request):
  144. if not self._check_session(request):
  145. return web.Response(status=401, body="Authorization required")
  146. text = await request.text()
  147. if len(text) < 36:
  148. return web.Response(
  149. status=400,
  150. body="API ID and HASH pair has invalid length",
  151. )
  152. api_id = text[32:]
  153. api_hash = text[:32]
  154. if any(c not in string.hexdigits for c in api_hash) or any(
  155. c not in string.digits for c in api_id
  156. ):
  157. return web.Response(
  158. status=400,
  159. body="You specified invalid API ID and/or API HASH",
  160. )
  161. if "DYNO" not in os.environ:
  162. # On Heroku it'll be saved later
  163. with open(
  164. os.path.join(self.data_root or DATA_DIR, "api_token.txt"),
  165. "w",
  166. ) as f:
  167. f.write(api_id + "\n" + api_hash)
  168. self.api_token = collections.namedtuple("api_token", ("ID", "HASH"))(
  169. api_id,
  170. api_hash,
  171. )
  172. self.api_set.set()
  173. return web.Response(body="ok")
  174. async def send_tg_code(self, request):
  175. if not self._check_session(request):
  176. return web.Response(status=401, body="Authorization required")
  177. text = await request.text()
  178. phone = telethon.utils.parse_phone(text)
  179. if not phone:
  180. return web.Response(status=400, body="Invalid phone number")
  181. client = CustomTelegramClient(
  182. telethon.sessions.MemorySession(),
  183. self.api_token.ID,
  184. self.api_token.HASH,
  185. connection=self.connection,
  186. proxy=self.proxy,
  187. connection_retries=None,
  188. device_model="Hikka",
  189. )
  190. self._pending_client = client
  191. await client.connect()
  192. try:
  193. await client.send_code_request(phone)
  194. except FloodWaitError as e:
  195. return web.Response(
  196. status=429,
  197. body=(
  198. f"You got FloodWait of {e.seconds} seconds. Wait the specified"
  199. " amount of time and try again."
  200. ),
  201. )
  202. return web.Response(body="ok")
  203. async def okteto(self, request):
  204. if main.get_config_key("okteto_uri"):
  205. return web.Response(status=418)
  206. text = await request.text()
  207. main.save_config_key("okteto_uri", text)
  208. return web.Response(body="URI_SAVED")
  209. async def tg_code(self, request):
  210. if not self._check_session(request):
  211. return web.Response(status=401)
  212. text = await request.text()
  213. if len(text) < 6:
  214. return web.Response(status=400)
  215. split = text.split("\n", 2)
  216. if len(split) not in (2, 3):
  217. return web.Response(status=400)
  218. code = split[0]
  219. phone = telethon.utils.parse_phone(split[1])
  220. password = split[2]
  221. if (
  222. (len(code) != 5 and not password)
  223. or any(c not in string.digits for c in code)
  224. or not phone
  225. ):
  226. return web.Response(status=400)
  227. if not password:
  228. try:
  229. await self._pending_client.sign_in(phone, code=code)
  230. except telethon.errors.SessionPasswordNeededError:
  231. return web.Response(
  232. status=401,
  233. body="2FA Password required",
  234. ) # Requires 2FA login
  235. except telethon.errors.PhoneCodeExpiredError:
  236. return web.Response(status=404, body="Code expired")
  237. except telethon.errors.PhoneCodeInvalidError:
  238. return web.Response(status=403, body="Invalid code")
  239. except telethon.errors.FloodWaitError as e:
  240. return web.Response(
  241. status=421,
  242. body=(
  243. f"You got FloodWait of {e.seconds} seconds. Wait the specified"
  244. " amount of time and try again."
  245. ),
  246. )
  247. else:
  248. try:
  249. await self._pending_client.sign_in(phone, password=password)
  250. except telethon.errors.PasswordHashInvalidError:
  251. return web.Response(
  252. status=403,
  253. body="Invalid 2FA password",
  254. ) # Invalid 2FA password
  255. except telethon.errors.FloodWaitError as e:
  256. return web.Response(
  257. status=421,
  258. body=(
  259. f"You got FloodWait of {e.seconds} seconds. Wait the specified"
  260. " amount of time and try again."
  261. ),
  262. )
  263. # At this step we don't want `main.hikka` to "know" about our client
  264. # so it doesn't create bot immediately. That's why we only save its session
  265. # in case user closes web early. It will be handled on restart
  266. # If user finishes login further, client will be passed to main
  267. # To prevent Heroku from restarting too soon, we'll do it after setting bot
  268. if "DYNO" not in os.environ:
  269. await main.hikka.save_client_session(self._pending_client)
  270. return web.Response()
  271. async def finish_login(self, request):
  272. if not self._check_session(request):
  273. return web.Response(status=401)
  274. if not self._pending_client:
  275. return web.Response(status=400)
  276. if "DYNO" in os.environ:
  277. app, config = heroku.get_app()
  278. config["api_id"] = self.api_token.ID
  279. config["api_hash"] = self.api_token.HASH
  280. await main.hikka.save_client_session(
  281. self._pending_client,
  282. heroku_config=config,
  283. heroku_app=app,
  284. )
  285. # We don't care what happens next, bc Heroku will restart anyway
  286. return
  287. first_session = not bool(main.hikka.clients)
  288. # Client is ready to pass in to dispatcher
  289. main.hikka.clients = list(set(main.hikka.clients + [self._pending_client]))
  290. self._pending_client = None
  291. self.clients_set.set()
  292. if not first_session:
  293. atexit.register(functools.partial(restart, *sys.argv[1:]))
  294. handler = logging.getLogger().handlers[0]
  295. handler.setLevel(logging.CRITICAL)
  296. for client in main.hikka.clients:
  297. await client.disconnect()
  298. sys.exit(0)
  299. return web.Response()
  300. async def web_auth(self, request):
  301. if self._check_session(request):
  302. return web.Response(body=request.cookies.get("session", "unauthorized"))
  303. token = utils.rand(8)
  304. markup = InlineKeyboardMarkup()
  305. markup.add(
  306. InlineKeyboardButton(
  307. "🔓 Authorize user",
  308. callback_data=f"authorize_web_{token}",
  309. )
  310. )
  311. ips = request.headers.get("X-FORWARDED-FOR", None) or request.remote
  312. cities = []
  313. for ip in re.findall(r"[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}", ips):
  314. if ip not in self._ratelimit:
  315. self._ratelimit[ip] = []
  316. if (
  317. len(
  318. list(
  319. filter(lambda x: time.time() - x < 3 * 60, self._ratelimit[ip])
  320. )
  321. )
  322. >= 3
  323. ):
  324. return web.Response(status=429)
  325. self._ratelimit[ip] = list(
  326. filter(lambda x: time.time() - x < 3 * 60, self._ratelimit[ip])
  327. )
  328. self._ratelimit[ip] += [time.time()]
  329. try:
  330. res = (
  331. await utils.run_sync(
  332. requests.get,
  333. f"https://freegeoip.app/json/{ip}",
  334. )
  335. ).json()
  336. cities += [
  337. f"<i>{utils.get_lang_flag(res['country_code'])} {res['country_name']} {res['region_name']} {res['city']} {res['zip_code']}</i>"
  338. ]
  339. except Exception:
  340. pass
  341. cities = (
  342. ("<b>🏢 Possible cities:</b>\n\n" + "\n".join(cities) + "\n")
  343. if cities
  344. else ""
  345. )
  346. ops = []
  347. for user in self.client_data.values():
  348. try:
  349. bot = user[0].inline.bot
  350. msg = await bot.send_message(
  351. user[1].tg_id,
  352. "🌘🔐 <b>Click button below to confirm web application"
  353. f" ops</b>\n\n<b>Client IP</b>: {ips}\n{cities}\n<i>If you did not"
  354. " request any codes, simply ignore this message</i>",
  355. disable_web_page_preview=True,
  356. reply_markup=markup,
  357. )
  358. ops += [
  359. functools.partial(
  360. bot.delete_message,
  361. chat_id=msg.chat.id,
  362. message_id=msg.message_id,
  363. )
  364. ]
  365. except Exception:
  366. pass
  367. session = f"hikka_{utils.rand(16)}"
  368. if not ops:
  369. # If no auth message was sent, just leave it empty
  370. # probably, request was a bug and user doesn't have
  371. # inline bot or did not authorize any sessions
  372. return web.Response(body=session)
  373. if not await main.hikka.wait_for_web_auth(token):
  374. for op in ops:
  375. await op()
  376. return web.Response(body="TIMEOUT")
  377. for op in ops:
  378. await op()
  379. self._sessions += [session]
  380. return web.Response(body=session)