root.py 13 KB

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