main.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912
  1. """Main script, where all the fun starts"""
  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. # ©️ Dan Gazizullin, 2021-2023
  15. # This file is a part of Hikka Userbot
  16. # 🌐 https://github.com/hikariatama/Hikka
  17. # You can redistribute it and/or modify it under the terms of the GNU AGPLv3
  18. # 🔑 https://www.gnu.org/licenses/agpl-3.0.html
  19. import argparse
  20. import asyncio
  21. import collections
  22. import contextlib
  23. import importlib
  24. import json
  25. import logging
  26. import os
  27. import random
  28. import socket
  29. import sqlite3
  30. import typing
  31. from getpass import getpass
  32. from pathlib import Path
  33. import hikkatl
  34. from hikkatl import events
  35. from hikkatl.errors import (
  36. ApiIdInvalidError,
  37. AuthKeyDuplicatedError,
  38. FloodWaitError,
  39. PasswordHashInvalidError,
  40. PhoneNumberInvalidError,
  41. SessionPasswordNeededError,
  42. )
  43. from hikkatl.network.connection import (
  44. ConnectionTcpFull,
  45. ConnectionTcpMTProxyRandomizedIntermediate,
  46. )
  47. from hikkatl.password import compute_check
  48. from hikkatl.sessions import MemorySession, SQLiteSession
  49. from hikkatl.tl.functions.account import GetPasswordRequest
  50. from hikkatl.tl.functions.auth import CheckPasswordRequest
  51. from . import database, loader, utils, version
  52. from ._internal import print_banner
  53. from .dispatcher import CommandDispatcher
  54. from .qr import QRCode
  55. from .tl_cache import CustomTelegramClient
  56. from .translations import Translator
  57. from .version import __version__
  58. try:
  59. from .web import core
  60. except ImportError:
  61. web_available = False
  62. logging.exception("Unable to import web")
  63. else:
  64. web_available = True
  65. BASE_DIR = (
  66. "/data"
  67. if "DOCKER" in os.environ
  68. else os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
  69. )
  70. BASE_PATH = Path(BASE_DIR)
  71. CONFIG_PATH = BASE_PATH / "config.json"
  72. IS_TERMUX = "com.termux" in os.environ.get("PREFIX", "")
  73. IS_CODESPACES = "CODESPACES" in os.environ
  74. IS_DOCKER = "DOCKER" in os.environ
  75. IS_RAILWAY = "RAILWAY" in os.environ
  76. IS_GOORM = "GOORM" in os.environ
  77. IS_LAVHOST = "LAVHOST" in os.environ
  78. IS_WSL = False
  79. with contextlib.suppress(Exception):
  80. from platform import uname
  81. if "microsoft-standard" in uname().release:
  82. IS_WSL = True
  83. # fmt: off
  84. LATIN_MOCK = [
  85. "Amor", "Arbor", "Astra", "Aurum", "Bellum", "Caelum",
  86. "Calor", "Candor", "Carpe", "Celer", "Certo", "Cibus",
  87. "Civis", "Clemens", "Coetus", "Cogito", "Conexus",
  88. "Consilium", "Cresco", "Cura", "Cursus", "Decus",
  89. "Deus", "Dies", "Digitus", "Discipulus", "Dominus",
  90. "Donum", "Dulcis", "Durus", "Elementum", "Emendo",
  91. "Ensis", "Equus", "Espero", "Fidelis", "Fides",
  92. "Finis", "Flamma", "Flos", "Fortis", "Frater", "Fuga",
  93. "Fulgeo", "Genius", "Gloria", "Gratia", "Gravis",
  94. "Habitus", "Honor", "Hora", "Ignis", "Imago",
  95. "Imperium", "Inceptum", "Infinitus", "Ingenium",
  96. "Initium", "Intra", "Iunctus", "Iustitia", "Labor",
  97. "Laurus", "Lectus", "Legio", "Liberi", "Libertas",
  98. "Lumen", "Lux", "Magister", "Magnus", "Manus",
  99. "Memoria", "Mens", "Mors", "Mundo", "Natura",
  100. "Nexus", "Nobilis", "Nomen", "Novus", "Nox",
  101. "Oculus", "Omnis", "Opus", "Orbis", "Ordo", "Os",
  102. "Pax", "Perpetuus", "Persona", "Petra", "Pietas",
  103. "Pons", "Populus", "Potentia", "Primus", "Proelium",
  104. "Pulcher", "Purus", "Quaero", "Quies", "Ratio",
  105. "Regnum", "Sanguis", "Sapientia", "Sensus", "Serenus",
  106. "Sermo", "Signum", "Sol", "Solus", "Sors", "Spes",
  107. "Spiritus", "Stella", "Summus", "Teneo", "Terra",
  108. "Tigris", "Trans", "Tribuo", "Tristis", "Ultimus",
  109. "Unitas", "Universus", "Uterque", "Valde", "Vates",
  110. "Veritas", "Verus", "Vester", "Via", "Victoria",
  111. "Vita", "Vox", "Vultus", "Zephyrus"
  112. ]
  113. # fmt: on
  114. def generate_app_name() -> str:
  115. """
  116. Generate random app name
  117. :return: Random app name
  118. :example: "Cresco Cibus Consilium"
  119. """
  120. return " ".join(random.choices(LATIN_MOCK, k=3))
  121. def get_app_name() -> str:
  122. """
  123. Generates random app name or gets the saved one of present
  124. :return: App name
  125. :example: "Cresco Cibus Consilium"
  126. """
  127. if not (app_name := get_config_key("app_name")):
  128. app_name = generate_app_name()
  129. save_config_key("app_name", app_name)
  130. return app_name
  131. try:
  132. import uvloop
  133. uvloop.install()
  134. except Exception:
  135. pass
  136. def run_config():
  137. """Load configurator.py"""
  138. from . import configurator
  139. return configurator.api_config(IS_TERMUX or None)
  140. def get_config_key(key: str) -> typing.Union[str, bool]:
  141. """
  142. Parse and return key from config
  143. :param key: Key name in config
  144. :return: Value of config key or `False`, if it doesn't exist
  145. """
  146. try:
  147. return json.loads(CONFIG_PATH.read_text()).get(key, False)
  148. except FileNotFoundError:
  149. return False
  150. def save_config_key(key: str, value: str) -> bool:
  151. """
  152. Save `key` with `value` to config
  153. :param key: Key name in config
  154. :param value: Desired value in config
  155. :return: `True` on success, otherwise `False`
  156. """
  157. try:
  158. # Try to open our newly created json config
  159. config = json.loads(CONFIG_PATH.read_text())
  160. except FileNotFoundError:
  161. # If it doesn't exist, just default config to none
  162. # It won't cause problems, bc after new save
  163. # we will create new one
  164. config = {}
  165. # Assign config value
  166. config[key] = value
  167. # And save config
  168. CONFIG_PATH.write_text(json.dumps(config, indent=4))
  169. return True
  170. def gen_port(cfg: str = "port", no8080: bool = False) -> int:
  171. """
  172. Generates random free port in case of VDS.
  173. In case of Docker, also return 8080, as it's already exposed by default.
  174. :returns: Integer value of generated port
  175. """
  176. if "DOCKER" in os.environ and not no8080:
  177. return 8080
  178. # But for own server we generate new free port, and assign to it
  179. if port := get_config_key(cfg):
  180. return port
  181. # If we didn't get port from config, generate new one
  182. # First, try to randomly get port
  183. while port := random.randint(1024, 65536):
  184. if socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect_ex(
  185. ("localhost", port)
  186. ):
  187. break
  188. return port
  189. def parse_arguments() -> dict:
  190. """
  191. Parses the arguments
  192. :returns: Dictionary with arguments
  193. """
  194. parser = argparse.ArgumentParser()
  195. parser.add_argument(
  196. "--port",
  197. dest="port",
  198. action="store",
  199. default=gen_port(),
  200. type=int,
  201. )
  202. parser.add_argument("--phone", "-p", action="append")
  203. parser.add_argument("--no-web", dest="disable_web", action="store_true")
  204. parser.add_argument(
  205. "--qr-login",
  206. dest="qr_login",
  207. action="store_true",
  208. help=(
  209. "Use QR code login instead of phone number (will only work if scanned from"
  210. " another device)"
  211. ),
  212. )
  213. parser.add_argument(
  214. "--data-root",
  215. dest="data_root",
  216. default="",
  217. help="Root path to store session files in",
  218. )
  219. parser.add_argument(
  220. "--no-auth",
  221. dest="no_auth",
  222. action="store_true",
  223. help="Disable authentication and API token input, exitting if needed",
  224. )
  225. parser.add_argument(
  226. "--proxy-host",
  227. dest="proxy_host",
  228. action="store",
  229. help="MTProto proxy host, without port",
  230. )
  231. parser.add_argument(
  232. "--proxy-port",
  233. dest="proxy_port",
  234. action="store",
  235. type=int,
  236. help="MTProto proxy port",
  237. )
  238. parser.add_argument(
  239. "--proxy-secret",
  240. dest="proxy_secret",
  241. action="store",
  242. help="MTProto proxy secret",
  243. )
  244. parser.add_argument(
  245. "--root",
  246. dest="disable_root_check",
  247. action="store_true",
  248. help="Disable `force_insecure` warning",
  249. )
  250. parser.add_argument(
  251. "--proxy-pass",
  252. dest="proxy_pass",
  253. action="store_true",
  254. help="Open proxy pass tunnel on start (not needed on setup)",
  255. )
  256. parser.add_argument(
  257. "--no-tty",
  258. dest="tty",
  259. action="store_false",
  260. default=True,
  261. help="Do not print colorful output using ANSI escapes",
  262. )
  263. arguments = parser.parse_args()
  264. logging.debug(arguments)
  265. return arguments
  266. class SuperList(list):
  267. """
  268. Makes able: await self.allclients.send_message("foo", "bar")
  269. """
  270. def __getattribute__(self, attr: str) -> typing.Any:
  271. if hasattr(list, attr):
  272. return list.__getattribute__(self, attr)
  273. for obj in self:
  274. attribute = getattr(obj, attr)
  275. if callable(attribute):
  276. if asyncio.iscoroutinefunction(attribute):
  277. async def foobar(*args, **kwargs):
  278. return [await getattr(_, attr)(*args, **kwargs) for _ in self]
  279. return foobar
  280. return lambda *args, **kwargs: [
  281. getattr(_, attr)(*args, **kwargs) for _ in self
  282. ]
  283. return [getattr(x, attr) for x in self]
  284. class InteractiveAuthRequired(Exception):
  285. """Is being rased by Telethon, if phone is required"""
  286. def raise_auth():
  287. """Raises `InteractiveAuthRequired`"""
  288. raise InteractiveAuthRequired()
  289. class Hikka:
  290. """Main userbot instance, which can handle multiple clients"""
  291. def __init__(self):
  292. global BASE_DIR, BASE_PATH, CONFIG_PATH
  293. self.omit_log = False
  294. self.arguments = parse_arguments()
  295. if self.arguments.data_root:
  296. BASE_DIR = self.arguments.data_root
  297. BASE_PATH = Path(BASE_DIR)
  298. CONFIG_PATH = BASE_PATH / "config.json"
  299. self.loop = asyncio.get_event_loop()
  300. self.clients = SuperList()
  301. self.ready = asyncio.Event()
  302. self._read_sessions()
  303. self._get_api_token()
  304. self._get_proxy()
  305. def _get_proxy(self):
  306. """
  307. Get proxy tuple from --proxy-host, --proxy-port and --proxy-secret
  308. and connection to use (depends on proxy - provided or not)
  309. """
  310. if (
  311. self.arguments.proxy_host is not None
  312. and self.arguments.proxy_port is not None
  313. and self.arguments.proxy_secret is not None
  314. ):
  315. logging.debug(
  316. "Using proxy: %s:%s",
  317. self.arguments.proxy_host,
  318. self.arguments.proxy_port,
  319. )
  320. self.proxy, self.conn = (
  321. (
  322. self.arguments.proxy_host,
  323. self.arguments.proxy_port,
  324. self.arguments.proxy_secret,
  325. ),
  326. ConnectionTcpMTProxyRandomizedIntermediate,
  327. )
  328. return
  329. self.proxy, self.conn = None, ConnectionTcpFull
  330. def _read_sessions(self):
  331. """Gets sessions from environment and data directory"""
  332. self.sessions = []
  333. self.sessions += [
  334. SQLiteSession(
  335. os.path.join(
  336. BASE_DIR,
  337. session.rsplit(".session", maxsplit=1)[0],
  338. )
  339. )
  340. for session in filter(
  341. lambda f: f.startswith("hikka-") and f.endswith(".session"),
  342. os.listdir(BASE_DIR),
  343. )
  344. ]
  345. def _get_api_token(self):
  346. """Get API Token from disk or environment"""
  347. api_token_type = collections.namedtuple("api_token", ("ID", "HASH"))
  348. # Try to retrieve credintials from config, or from env vars
  349. try:
  350. # Legacy migration
  351. if not get_config_key("api_id"):
  352. api_id, api_hash = (
  353. line.strip()
  354. for line in (Path(BASE_DIR) / "api_token.txt")
  355. .read_text()
  356. .splitlines()
  357. )
  358. save_config_key("api_id", int(api_id))
  359. save_config_key("api_hash", api_hash)
  360. (Path(BASE_DIR) / "api_token.txt").unlink()
  361. logging.debug("Migrated api_token.txt to config.json")
  362. api_token = api_token_type(
  363. get_config_key("api_id"),
  364. get_config_key("api_hash"),
  365. )
  366. except FileNotFoundError:
  367. try:
  368. from . import api_token
  369. except ImportError:
  370. try:
  371. api_token = api_token_type(
  372. os.environ["api_id"],
  373. os.environ["api_hash"],
  374. )
  375. except KeyError:
  376. api_token = None
  377. self.api_token = api_token
  378. def _init_web(self):
  379. """Initialize web"""
  380. if (
  381. not web_available
  382. or getattr(self.arguments, "disable_web", False)
  383. or IS_TERMUX
  384. ):
  385. self.web = None
  386. return
  387. self.web = core.Web(
  388. data_root=BASE_DIR,
  389. api_token=self.api_token,
  390. proxy=self.proxy,
  391. connection=self.conn,
  392. )
  393. async def _get_token(self):
  394. """Reads or waits for user to enter API credentials"""
  395. while self.api_token is None:
  396. if self.arguments.no_auth:
  397. return
  398. if self.web:
  399. await self.web.start(self.arguments.port, proxy_pass=True)
  400. await self._web_banner()
  401. await self.web.wait_for_api_token_setup()
  402. self.api_token = self.web.api_token
  403. else:
  404. run_config()
  405. importlib.invalidate_caches()
  406. self._get_api_token()
  407. async def save_client_session(self, client: CustomTelegramClient):
  408. if hasattr(client, "tg_id"):
  409. telegram_id = client.tg_id
  410. else:
  411. if not (me := await client.get_me()):
  412. raise RuntimeError("Attempted to save non-inited session")
  413. telegram_id = me.id
  414. client._tg_id = telegram_id
  415. client.tg_id = telegram_id
  416. client.hikka_me = me
  417. session = SQLiteSession(
  418. os.path.join(
  419. BASE_DIR,
  420. f"hikka-{telegram_id}",
  421. )
  422. )
  423. session.set_dc(
  424. client.session.dc_id,
  425. client.session.server_address,
  426. client.session.port,
  427. )
  428. session.auth_key = client.session.auth_key
  429. session.save()
  430. client.session = session
  431. # Set db attribute to this client in order to save
  432. # custom bot nickname from web
  433. client.hikka_db = database.Database(client)
  434. await client.hikka_db.init()
  435. async def _web_banner(self):
  436. """Shows web banner"""
  437. logging.info("✅ Web mode ready for configuration")
  438. logging.info("🌐 Please visit %s", self.web.url)
  439. async def wait_for_web_auth(self, token: str) -> bool:
  440. """
  441. Waits for web auth confirmation in Telegram
  442. :param token: Token to wait for
  443. :return: True if auth was successful, False otherwise
  444. """
  445. timeout = 5 * 60
  446. polling_interval = 1
  447. for _ in range(timeout * polling_interval):
  448. await asyncio.sleep(polling_interval)
  449. for client in self.clients:
  450. if client.loader.inline.pop_web_auth_token(token):
  451. return True
  452. return False
  453. async def _phone_login(self, client: CustomTelegramClient) -> bool:
  454. phone = input(
  455. "\033[0;96mEnter phone: \033[0m"
  456. if IS_TERMUX or self.arguments.tty
  457. else "Enter phone: "
  458. )
  459. await client.start(phone)
  460. await self.save_client_session(client)
  461. self.clients += [client]
  462. return True
  463. async def _initial_setup(self) -> bool:
  464. """Responsible for first start"""
  465. if self.arguments.no_auth:
  466. return False
  467. if not self.web:
  468. client = CustomTelegramClient(
  469. MemorySession(),
  470. self.api_token.ID,
  471. self.api_token.HASH,
  472. connection=self.conn,
  473. proxy=self.proxy,
  474. connection_retries=None,
  475. device_model=get_app_name(),
  476. system_version="Windows 10",
  477. app_version=".".join(map(str, __version__)) + " x64",
  478. lang_code="en",
  479. system_lang_code="en-US",
  480. )
  481. await client.connect()
  482. print(
  483. (
  484. "\033[0;96m{}\033[0m" if IS_TERMUX or self.arguments.tty else "{}"
  485. ).format(
  486. "You can use QR-code to login from another device (your friend's"
  487. " phone, for example)."
  488. )
  489. )
  490. if (
  491. input(
  492. "\033[0;96mUse QR code? [y/N]: \033[0m"
  493. if IS_TERMUX or self.arguments.tty
  494. else "Use QR code? [y/N]: "
  495. ).lower()
  496. != "y"
  497. ):
  498. return await self._phone_login(client)
  499. print("\033[0;96mLoading QR code...\033[0m")
  500. qr_login = await client.qr_login()
  501. def print_qr():
  502. qr = QRCode()
  503. qr.add_data(qr_login.url)
  504. print("\033[2J\033[3;1f")
  505. qr.print_ascii(invert=True)
  506. print("\033[0;96mScan the QR code above to log in.\033[0m")
  507. print("\033[0;96mPress Ctrl+C to cancel.\033[0m")
  508. async def qr_login_poll() -> bool:
  509. logged_in = False
  510. while not logged_in:
  511. try:
  512. logged_in = await qr_login.wait(10)
  513. except asyncio.TimeoutError:
  514. try:
  515. await qr_login.recreate()
  516. print_qr()
  517. except SessionPasswordNeededError:
  518. return True
  519. except SessionPasswordNeededError:
  520. return True
  521. except KeyboardInterrupt:
  522. print("\033[2J\033[3;1f")
  523. return None
  524. return False
  525. if (qr_logined := await qr_login_poll()) is None:
  526. return await self._phone_login(client)
  527. if qr_logined:
  528. print_banner("2fa.txt")
  529. password = await client(GetPasswordRequest())
  530. while True:
  531. _2fa = getpass(
  532. f"\033[0;96mEnter 2FA password ({password.hint}): \033[0m"
  533. if IS_TERMUX or self.arguments.tty
  534. else f"Enter 2FA password ({password.hint}): "
  535. )
  536. try:
  537. await client._on_login(
  538. (
  539. await client(
  540. CheckPasswordRequest(
  541. compute_check(password, _2fa.strip())
  542. )
  543. )
  544. ).user
  545. )
  546. except PasswordHashInvalidError:
  547. print("\033[0;91mInvalid 2FA password!\033[0m")
  548. except FloodWaitError as e:
  549. seconds, minutes, hours = (
  550. e.seconds % 3600 % 60,
  551. e.seconds % 3600 // 60,
  552. e.seconds // 3600,
  553. )
  554. seconds, minutes, hours = (
  555. f"{seconds} second(-s)",
  556. f"{minutes} minute(-s) " if minutes else "",
  557. f"{hours} hour(-s) " if hours else "",
  558. )
  559. print(
  560. "\033[0;91mYou got FloodWait error! Please wait"
  561. f" {hours}{minutes}{seconds}\033[0m"
  562. )
  563. return False
  564. else:
  565. break
  566. print_banner("success.txt")
  567. print("\033[0;92mLogged in successfully!\033[0m")
  568. await self.save_client_session(client)
  569. self.clients += [client]
  570. return True
  571. if not self.web.running.is_set():
  572. await self.web.start(
  573. self.arguments.port,
  574. proxy_pass=True,
  575. )
  576. await self._web_banner()
  577. await self.web.wait_for_clients_setup()
  578. return True
  579. async def _init_clients(self) -> bool:
  580. """
  581. Reads session from disk and inits them
  582. :returns: `True` if at least one client started successfully
  583. """
  584. for session in self.sessions.copy():
  585. try:
  586. client = CustomTelegramClient(
  587. session,
  588. self.api_token.ID,
  589. self.api_token.HASH,
  590. connection=self.conn,
  591. proxy=self.proxy,
  592. connection_retries=None,
  593. device_model=get_app_name(),
  594. system_version="Windows 10",
  595. app_version=".".join(map(str, __version__)) + " x64",
  596. lang_code="en",
  597. system_lang_code="en-US",
  598. )
  599. await client.start(
  600. phone=(
  601. raise_auth
  602. if self.web
  603. else lambda: input(
  604. "\033[0;96mEnter phone: \033[0m"
  605. if IS_TERMUX or self.arguments.tty
  606. else "Enter phone: "
  607. )
  608. )
  609. )
  610. client.phone = "never gonna give you up"
  611. self.clients += [client]
  612. except sqlite3.OperationalError:
  613. logging.error(
  614. (
  615. "Check that this is the only instance running. "
  616. "If that doesn't help, delete the file '%s'"
  617. ),
  618. session.filename,
  619. )
  620. continue
  621. except (TypeError, AuthKeyDuplicatedError):
  622. Path(session.filename).unlink(missing_ok=True)
  623. self.sessions.remove(session)
  624. except (ValueError, ApiIdInvalidError):
  625. # Bad API hash/ID
  626. run_config()
  627. return False
  628. except PhoneNumberInvalidError:
  629. logging.error(
  630. "Phone number is incorrect. Use international format (+XX...) "
  631. "and don't put spaces in it."
  632. )
  633. self.sessions.remove(session)
  634. except InteractiveAuthRequired:
  635. logging.error(
  636. "Session %s was terminated and re-auth is required",
  637. session.filename,
  638. )
  639. self.sessions.remove(session)
  640. return bool(self.sessions)
  641. async def amain_wrapper(self, client: CustomTelegramClient):
  642. """Wrapper around amain"""
  643. async with client:
  644. first = True
  645. me = await client.get_me()
  646. client._tg_id = me.id
  647. client.tg_id = me.id
  648. client.hikka_me = me
  649. while await self.amain(first, client):
  650. first = False
  651. async def _badge(self, client: CustomTelegramClient):
  652. """Call the badge in shell"""
  653. try:
  654. import git
  655. repo = git.Repo()
  656. build = utils.get_git_hash()
  657. diff = repo.git.log([f"HEAD..origin/{version.branch}", "--oneline"])
  658. upd = "Update required" if diff else "Up-to-date"
  659. logo = (
  660. "█ █ █ █▄▀ █▄▀ ▄▀█\n"
  661. "█▀█ █ █ █ █ █ █▀█\n\n"
  662. f"• Build: {build[:7]}\n"
  663. f"• Version: {'.'.join(list(map(str, list(__version__))))}\n"
  664. f"• {upd}\n"
  665. )
  666. if not self.omit_log:
  667. print(logo)
  668. web_url = (
  669. f"🌐 Web url: {self.web.url}"
  670. if self.web and hasattr(self.web, "url")
  671. else ""
  672. )
  673. logging.debug(
  674. "\n🌘 Hikka %s #%s (%s) started\n%s",
  675. ".".join(list(map(str, list(__version__)))),
  676. build[:7],
  677. upd,
  678. web_url,
  679. )
  680. self.omit_log = True
  681. await client.hikka_inline.bot.send_animation(
  682. logging.getLogger().handlers[0].get_logid_by_client(client.tg_id),
  683. "https://github.com/hikariatama/assets/raw/master/hikka_banner.mp4",
  684. caption=(
  685. "🌘 <b>Hikka {} started!</b>\n\n🌳 <b>GitHub commit SHA: <a"
  686. ' href="https://github.com/hikariatama/Hikka/commit/{}">{}</a></b>\n✊'
  687. " <b>Update status: {}</b>\n<b>{}</b>".format(
  688. ".".join(list(map(str, list(__version__)))),
  689. build,
  690. build[:7],
  691. upd,
  692. web_url,
  693. )
  694. ),
  695. )
  696. logging.debug(
  697. "· Started for %s · Prefix: «%s» ·",
  698. client.tg_id,
  699. client.hikka_db.get(__name__, "command_prefix", False) or ".",
  700. )
  701. except Exception:
  702. logging.exception("Badge error")
  703. async def _add_dispatcher(
  704. self,
  705. client: CustomTelegramClient,
  706. modules: loader.Modules,
  707. db: database.Database,
  708. ):
  709. """Inits and adds dispatcher instance to client"""
  710. dispatcher = CommandDispatcher(modules, client, db)
  711. client.dispatcher = dispatcher
  712. modules.check_security = dispatcher.check_security
  713. client.add_event_handler(
  714. dispatcher.handle_incoming,
  715. events.NewMessage,
  716. )
  717. client.add_event_handler(
  718. dispatcher.handle_incoming,
  719. events.ChatAction,
  720. )
  721. client.add_event_handler(
  722. dispatcher.handle_command,
  723. events.NewMessage(forwards=False),
  724. )
  725. client.add_event_handler(
  726. dispatcher.handle_command,
  727. events.MessageEdited(),
  728. )
  729. client.add_event_handler(
  730. dispatcher.handle_raw,
  731. events.Raw(),
  732. )
  733. async def amain(self, first: bool, client: CustomTelegramClient):
  734. """Entrypoint for async init, run once for each user"""
  735. client.parse_mode = "HTML"
  736. await client.start()
  737. db = database.Database(client)
  738. client.hikka_db = db
  739. await db.init()
  740. logging.debug("Got DB")
  741. logging.debug("Loading logging config...")
  742. translator = Translator(client, db)
  743. await translator.init()
  744. modules = loader.Modules(client, db, self.clients, translator)
  745. client.loader = modules
  746. client.pyro_proxy = None # Will be set later if needed
  747. if self.web:
  748. await self.web.add_loader(client, modules, db)
  749. await self.web.start_if_ready(
  750. len(self.clients),
  751. self.arguments.port,
  752. proxy_pass=self.arguments.proxy_pass,
  753. )
  754. await self._add_dispatcher(client, modules, db)
  755. await modules.register_all(None)
  756. modules.send_config()
  757. await modules.send_ready()
  758. if first:
  759. await self._badge(client)
  760. await client.run_until_disconnected()
  761. async def _main(self):
  762. """Main entrypoint"""
  763. self._init_web()
  764. save_config_key("port", self.arguments.port)
  765. await self._get_token()
  766. if (
  767. not self.clients and not self.sessions or not await self._init_clients()
  768. ) and not await self._initial_setup():
  769. return
  770. self.loop.set_exception_handler(
  771. lambda _, x: logging.error(
  772. "Exception on event loop! %s",
  773. x["message"],
  774. exc_info=x.get("exception", None),
  775. )
  776. )
  777. await asyncio.gather(*[self.amain_wrapper(client) for client in self.clients])
  778. def main(self):
  779. """Main entrypoint"""
  780. self.loop.run_until_complete(self._main())
  781. self.loop.close()
  782. hikkatl.extensions.html.CUSTOM_EMOJIS = not get_config_key("disable_custom_emojis")
  783. hikka = Hikka()