test.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. # █ █ ▀ █▄▀ ▄▀█ █▀█ ▀
  2. # █▀█ █ █ █ █▀█ █▀▄ █
  3. # © Copyright 2022
  4. # https://t.me/hikariatama
  5. #
  6. # 🔒 Licensed under the GNU AGPLv3
  7. # 🌐 https://www.gnu.org/licenses/agpl-3.0.html
  8. # scope: inline
  9. import inspect
  10. import logging
  11. import os
  12. import random
  13. import time
  14. from io import BytesIO
  15. from typing import Union
  16. from telethon.tl.functions.channels import EditAdminRequest, InviteToChannelRequest
  17. from telethon.tl.types import ChatAdminRights, Message
  18. from .. import loader, main, utils
  19. from ..inline.types import InlineCall
  20. logger = logging.getLogger(__name__)
  21. if "DYNO" not in os.environ:
  22. DEBUG_MODS_DIR = os.path.join(utils.get_base_dir(), "debug_modules")
  23. if not os.path.isdir(DEBUG_MODS_DIR):
  24. os.mkdir(DEBUG_MODS_DIR, mode=0o755)
  25. for mod in os.scandir(DEBUG_MODS_DIR):
  26. os.remove(mod.path)
  27. @loader.tds
  28. class TestMod(loader.Module):
  29. """Perform operations based on userbot self-testing"""
  30. _memory = {}
  31. strings = {
  32. "name": "Tester",
  33. "set_loglevel": "🚫 <b>Please specify verbosity as an integer or string</b>",
  34. "no_logs": "ℹ️ <b>You don't have any logs at verbosity {}.</b>",
  35. "logs_filename": "hikka-logs.txt",
  36. "logs_caption": (
  37. "<emoji document_id='6318565919471699564'>🌌</emoji> <b>Hikka logs with"
  38. " verbosity </b><code>{}</code>\n\n<emoji"
  39. " document_id='5454390891466726015'>👋</emoji> <b>Hikka version:"
  40. " {}.{}.{}</b>{}\n<emoji document_id='6321050180095313397'>⏱</emoji>"
  41. " <b>Uptime: {}</b>\n<b>{}</b>\n\n<b>{} NoNick</b>\n<b>{} Grep</b>\n<b>{}"
  42. " InlineLogs</b>"
  43. ),
  44. "suspend_invalid_time": (
  45. "<emoji document_id='5416024721705673488'>💀</emoji> <b>Invalid time to"
  46. " suspend</b>"
  47. ),
  48. "suspended": (
  49. "<emoji document_id='5452023368054216810'>🥶</emoji> <b>Bot suspended"
  50. " for</b> <code>{}</code> <b>seconds</b>"
  51. ),
  52. "results_ping": (
  53. "<emoji document_id='6321050180095313397'>⏱</emoji> <b>Telegram ping:</b>"
  54. " <code>{}</code> <b>ms</b>\n<emoji"
  55. " document_id='5377371691078916778'>😎</emoji> <b>Uptime: {}</b>"
  56. ),
  57. "ping_hint": (
  58. "<emoji document_id='6318918617891080008'>💡</emoji> <i>Telegram ping mostly"
  59. " depends on Telegram servers latency and other external factors and has"
  60. " nothing to do with the parameters of server on which userbot is"
  61. " installed</i>"
  62. ),
  63. "confidential": (
  64. "⚠️ <b>Log level </b><code>{}</code><b> may reveal your confidential info,"
  65. " be careful</b>"
  66. ),
  67. "confidential_text": (
  68. "⚠️ <b>Log level </b><code>{0}</code><b> may reveal your confidential info,"
  69. " be careful</b>\n<b>Type </b><code>.logs {0} force_insecure</code><b> to"
  70. " ignore this warning</b>"
  71. ),
  72. "choose_loglevel": "💁‍♂️ <b>Choose log level</b>",
  73. "bad_module": "🚫 <b>Module not found</b>",
  74. "debugging_enabled": (
  75. "🧑‍💻 <b>Debugging mode enabled for module </b><code>{0}</code>\n<i>Go to"
  76. " directory named `debug_modules`, edit file named `{0}.py` and see changes"
  77. " in real time</i>"
  78. ),
  79. "debugging_disabled": "✅ <b>Debugging disabled</b>",
  80. "heroku_debug": "🚫 <b>Debugging is not available on Heroku</b>",
  81. }
  82. strings_ru = {
  83. "set_loglevel": "🚫 <b>Укажи уровень логов числом или строкой</b>",
  84. "no_logs": "ℹ️ <b>У тебя нет логов уровня {}.</b>",
  85. "logs_filename": "hikka-logs.txt",
  86. "logs_caption": (
  87. "<emoji document_id='6318565919471699564'>🌌</emoji> <b>Логи Hikka уровня"
  88. " </b><code>{}</code>\n\n<emoji document_id='5454390891466726015'>👋</emoji>"
  89. " <b>Версия Hikka: {}.{}.{}</b>{}\n<emoji"
  90. " document_id='6321050180095313397'>⏱</emoji> <b>Uptime:"
  91. " {}</b>\n<b>{}</b>\n\n<b>{} NoNick</b>\n<b>{} Grep</b>\n<b>{}"
  92. " InlineLogs</b>"
  93. ),
  94. "bad_module": "🚫 <b>Модуль не найден</b>",
  95. "debugging_enabled": (
  96. "🧑‍💻 <b>Режим разработчика включен для модуля"
  97. " </b><code>{0}</code>\n<i>Отправляйся в директорию `debug_modules`,"
  98. " изменяй файл `{0}.py`, и смотри изменения в режиме реального времени</i>"
  99. ),
  100. "debugging_disabled": "✅ <b>Режим разработчика выключен</b>",
  101. "suspend_invalid_time": (
  102. "<emoji document_id='5416024721705673488'>💀</emoji> <b>Неверное время"
  103. " заморозки</b>"
  104. ),
  105. "suspended": (
  106. "<emoji document_id='5452023368054216810'>🥶</emoji> <b>Бот заморожен на</b>"
  107. " <code>{}</code> <b>секунд</b>"
  108. ),
  109. "results_ping": (
  110. "<emoji document_id='6321050180095313397'>⏱</emoji> <b>Скорость отклика"
  111. " Telegram:</b> <code>{}</code> <b>ms</b>\n<emoji"
  112. " document_id='5377371691078916778'>😎</emoji> <b>Прошло с последней"
  113. " перезагрузки: {}</b>"
  114. ),
  115. "ping_hint": (
  116. "<emoji document_id='6318918617891080008'>💡</emoji> <i>Скорость отклика"
  117. " Telegram в большей степени зависит от загруженности серверов Telegram и"
  118. " других внешних факторов и никак не связана с параметрами сервера, на"
  119. " который установлен юзербот</i>"
  120. ),
  121. "confidential": (
  122. "⚠️ <b>Уровень логов </b><code>{}</code><b> может содержать личную"
  123. " информацию, будь осторожен</b>"
  124. ),
  125. "confidential_text": (
  126. "⚠️ <b>Уровень логов </b><code>{0}</code><b> может содержать личную"
  127. " информацию, будь осторожен</b>\n<b>Напиши </b><code>.logs {0}"
  128. " force_insecure</code><b>, чтобы отправить логи игнорируя"
  129. " предупреждение</b>"
  130. ),
  131. "choose_loglevel": "💁‍♂️ <b>Выбери уровень логов</b>",
  132. "_cmd_doc_dump": "Показать информацию о сообщении",
  133. "_cmd_doc_logs": (
  134. "<уровень> - Отправляет лог-файл. Уровни ниже WARNING могут содержать"
  135. " личную инфомрацию."
  136. ),
  137. "_cmd_doc_suspend": "<время> - Заморозить бота на некоторое время",
  138. "_cmd_doc_ping": "Проверяет скорость отклика юзербота",
  139. "_cls_doc": "Операции, связанные с самотестированием",
  140. "heroku_debug": "🚫 <b>Режим разработчика не доступен на Heroku</b>",
  141. }
  142. def __init__(self):
  143. self.config = loader.ModuleConfig(
  144. loader.ConfigValue(
  145. "force_send_all",
  146. False,
  147. "⚠️ Do not touch, if you don't know what it does!\nBy default, Hikka"
  148. " will try to determine, which client caused logs. E.g. there is a"
  149. " module TestModule installed on Client1 and TestModule2 on Client2. By"
  150. " default, Client2 will get logs from TestModule2, and Client1 will get"
  151. " logs from TestModule. If this option is enabled, Hikka will send all"
  152. " logs to Client1 and Client2, even if it is not the one that caused"
  153. " the log.",
  154. validator=loader.validators.Boolean(),
  155. on_change=self._pass_config_to_logger,
  156. ),
  157. loader.ConfigValue(
  158. "tglog_level",
  159. "INFO",
  160. "⚠️ Do not touch, if you don't know what it does!\n"
  161. "Minimal loglevel for records to be sent in Telegram.",
  162. validator=loader.validators.Choice(
  163. ["INFO", "WARNING", "ERROR", "CRITICAL"]
  164. ),
  165. on_change=self._pass_config_to_logger,
  166. ),
  167. )
  168. def _pass_config_to_logger(self):
  169. logging.getLogger().handlers[0].force_send_all = self.config["force_send_all"]
  170. logging.getLogger().handlers[0].tg_level = self.config["tglog_level"]
  171. @loader.command(ru_doc="Ответь на сообщение, чтобы показать его дамп")
  172. async def dump(self, message: Message):
  173. """Use in reply to get a dump of a message"""
  174. if not message.is_reply:
  175. return
  176. await utils.answer(
  177. message,
  178. "<code>"
  179. + utils.escape_html((await message.get_reply_message()).stringify())
  180. + "</code>",
  181. )
  182. @loader.loop(interval=1)
  183. async def watchdog(self):
  184. if not os.path.isdir(DEBUG_MODS_DIR):
  185. return
  186. try:
  187. for module in os.scandir(DEBUG_MODS_DIR):
  188. last_modified = os.stat(module.path).st_mtime
  189. cls_ = module.path.split("/")[-1].split(".py")[0]
  190. if cls_ not in self._memory:
  191. self._memory[cls_] = last_modified
  192. continue
  193. if self._memory[cls_] == last_modified:
  194. continue
  195. self._memory[cls_] = last_modified
  196. logger.debug(f"Reloading debug module {cls_}")
  197. with open(module.path, "r") as f:
  198. try:
  199. await next(
  200. module
  201. for module in self.allmodules.modules
  202. if module.__class__.__name__ == "LoaderMod"
  203. ).load_module(
  204. f.read(),
  205. None,
  206. save_fs=False,
  207. )
  208. except Exception:
  209. logger.exception("Failed to reload module in watchdog")
  210. except Exception:
  211. logger.exception("Failed debugging watchdog")
  212. return
  213. @loader.command(
  214. ru_doc=(
  215. "[модуль] - Для разработчиков: открыть модуль в режиме дебага и применять"
  216. " изменения из него в режиме реального времени"
  217. )
  218. )
  219. async def debugmod(self, message: Message):
  220. """[module] - For developers: Open module for debugging
  221. You will be able to track changes in real-time"""
  222. if "DYNO" in os.environ:
  223. await utils.answer(message, self.strings("heroku_debug"))
  224. return
  225. args = utils.get_args_raw(message)
  226. instance = None
  227. for module in self.allmodules.modules:
  228. if (
  229. module.__class__.__name__.lower() == args.lower()
  230. or module.strings["name"].lower() == args.lower()
  231. ):
  232. if os.path.isfile(
  233. os.path.join(
  234. DEBUG_MODS_DIR,
  235. f"{module.__class__.__name__}.py",
  236. )
  237. ):
  238. os.remove(
  239. os.path.join(
  240. DEBUG_MODS_DIR,
  241. f"{module.__class__.__name__}.py",
  242. )
  243. )
  244. try:
  245. delattr(module, "hikka_debug")
  246. except AttributeError:
  247. pass
  248. await utils.answer(message, self.strings("debugging_disabled"))
  249. return
  250. module.hikka_debug = True
  251. instance = module
  252. break
  253. if not instance:
  254. await utils.answer(message, self.strings("bad_module"))
  255. return
  256. with open(
  257. os.path.join(
  258. DEBUG_MODS_DIR,
  259. f"{instance.__class__.__name__}.py",
  260. ),
  261. "wb",
  262. ) as f:
  263. f.write(inspect.getmodule(instance).__loader__.data)
  264. await utils.answer(
  265. message,
  266. self.strings("debugging_enabled").format(instance.__class__.__name__),
  267. )
  268. @loader.command(ru_doc="<уровень> - Показать логи")
  269. async def logs(
  270. self,
  271. message: Union[Message, InlineCall],
  272. force: bool = False,
  273. lvl: Union[int, None] = None,
  274. ):
  275. """<level> - Dump logs"""
  276. if not isinstance(lvl, int):
  277. args = utils.get_args_raw(message)
  278. try:
  279. try:
  280. lvl = int(args.split()[0])
  281. except ValueError:
  282. lvl = getattr(logging, args.split()[0].upper(), None)
  283. except IndexError:
  284. lvl = None
  285. if not isinstance(lvl, int):
  286. try:
  287. if not self.inline.init_complete or not await self.inline.form(
  288. text=self.strings("choose_loglevel"),
  289. reply_markup=[
  290. [
  291. {
  292. "text": "🚨 Critical",
  293. "callback": self.logs,
  294. "args": (False, 50),
  295. },
  296. {
  297. "text": "🚫 Error",
  298. "callback": self.logs,
  299. "args": (False, 40),
  300. },
  301. ],
  302. [
  303. {
  304. "text": "⚠️ Warning",
  305. "callback": self.logs,
  306. "args": (False, 30),
  307. },
  308. {
  309. "text": "ℹ️ Info",
  310. "callback": self.logs,
  311. "args": (False, 20),
  312. },
  313. ],
  314. [
  315. {
  316. "text": "🧑‍💻 Debug",
  317. "callback": self.logs,
  318. "args": (False, 10),
  319. },
  320. {
  321. "text": "👁 All",
  322. "callback": self.logs,
  323. "args": (False, 0),
  324. },
  325. ],
  326. [{"text": "🚫 Cancel", "action": "close"}],
  327. ],
  328. message=message,
  329. ):
  330. raise
  331. except Exception:
  332. await utils.answer(message, self.strings("set_loglevel"))
  333. return
  334. logs = "\n\n".join(
  335. [
  336. "\n".join(
  337. handler.dumps(lvl, client_id=self._client.tg_id)
  338. if "client_id" in inspect.signature(handler.dumps).parameters
  339. else handler.dumps(lvl)
  340. )
  341. for handler in logging.getLogger().handlers
  342. ]
  343. )
  344. named_lvl = (
  345. lvl
  346. if lvl not in logging._levelToName
  347. else logging._levelToName[lvl] # skipcq: PYL-W0212
  348. )
  349. if (
  350. lvl < logging.WARNING
  351. and not force
  352. and (
  353. not isinstance(message, Message)
  354. or "force_insecure" not in message.raw_text.lower()
  355. )
  356. ):
  357. try:
  358. if not self.inline.init_complete:
  359. raise
  360. cfg = {
  361. "text": self.strings("confidential").format(named_lvl),
  362. "reply_markup": [
  363. {
  364. "text": "📤 Send anyway",
  365. "callback": self.logs,
  366. "args": [True, lvl],
  367. },
  368. {"text": "🚫 Cancel", "action": "close"},
  369. ],
  370. }
  371. if isinstance(message, Message):
  372. if not await self.inline.form(**cfg, message=message):
  373. raise
  374. else:
  375. await message.edit(**cfg)
  376. except Exception:
  377. await utils.answer(
  378. message,
  379. self.strings("confidential_text").format(named_lvl),
  380. )
  381. return
  382. if len(logs) <= 2:
  383. if isinstance(message, Message):
  384. await utils.answer(message, self.strings("no_logs").format(named_lvl))
  385. else:
  386. await message.edit(self.strings("no_logs").format(named_lvl))
  387. await message.unload()
  388. return
  389. if btoken := self._db.get("hikka.inline", "bot_token", False):
  390. logs = logs.replace(
  391. btoken,
  392. f'{btoken.split(":")[0]}:***************************',
  393. )
  394. if hikka_token := self._db.get("HikkaDL", "token", False):
  395. logs = logs.replace(
  396. hikka_token,
  397. f'{hikka_token.split("_")[0]}_********************************',
  398. )
  399. if hikka_token := self._db.get("Kirito", "token", False):
  400. logs = logs.replace(
  401. hikka_token,
  402. f'{hikka_token.split("_")[0]}_********************************',
  403. )
  404. if os.environ.get("DATABASE_URL"):
  405. logs = logs.replace(
  406. os.environ.get("DATABASE_URL"),
  407. "postgre://**************************",
  408. )
  409. if os.environ.get("REDIS_URL"):
  410. logs = logs.replace(
  411. os.environ.get("REDIS_URL"),
  412. "postgre://**************************",
  413. )
  414. if os.environ.get("hikka_session"):
  415. logs = logs.replace(
  416. os.environ.get("hikka_session"),
  417. "StringSession(**************************)",
  418. )
  419. logs = BytesIO(logs.encode("utf-16"))
  420. logs.name = self.strings("logs_filename")
  421. ghash = utils.get_git_hash()
  422. other = (
  423. *main.__version__,
  424. " <i><a"
  425. f' href="https://github.com/hikariatama/Hikka/commit/{ghash}">({ghash[:8]})</a></i>'
  426. if ghash
  427. else "",
  428. utils.formatted_uptime(),
  429. utils.get_named_platform(),
  430. "✅" if self._db.get(main.__name__, "no_nickname", False) else "🚫",
  431. "✅" if self._db.get(main.__name__, "grep", False) else "🚫",
  432. "✅" if self._db.get(main.__name__, "inlinelogs", False) else "🚫",
  433. )
  434. if getattr(message, "out", True):
  435. await message.delete()
  436. if isinstance(message, Message):
  437. await utils.answer(
  438. message,
  439. logs,
  440. caption=self.strings("logs_caption").format(named_lvl, *other),
  441. )
  442. else:
  443. await self._client.send_file(
  444. message.form["chat"],
  445. logs,
  446. caption=self.strings("logs_caption").format(named_lvl, *other),
  447. )
  448. @loader.owner
  449. @loader.command(ru_doc="<время> - Заморозить бота на N секунд")
  450. async def suspend(self, message: Message):
  451. """<time> - Suspends the bot for N seconds"""
  452. try:
  453. time_sleep = float(utils.get_args_raw(message))
  454. await utils.answer(
  455. message,
  456. self.strings("suspended").format(time_sleep),
  457. )
  458. time.sleep(time_sleep)
  459. except ValueError:
  460. await utils.answer(message, self.strings("suspend_invalid_time"))
  461. @loader.command(ru_doc="Проверить скорость отклика юзербота")
  462. async def ping(self, message: Message):
  463. """Test your userbot ping"""
  464. start = time.perf_counter_ns()
  465. message = await utils.answer(message, "<code>🐻 Nofin...</code>")
  466. await utils.answer(
  467. message,
  468. self.strings("results_ping").format(
  469. round((time.perf_counter_ns() - start) / 10**6, 3),
  470. utils.formatted_uptime(),
  471. )
  472. + (
  473. ("\n\n" + self.strings("ping_hint"))
  474. if random.choice([0, 0, 1]) == 1
  475. else ""
  476. ),
  477. )
  478. async def client_ready(self):
  479. chat, is_new = await utils.asset_channel(
  480. self._client,
  481. "hikka-logs",
  482. "🌘 Your Hikka logs will appear in this chat",
  483. silent=True,
  484. avatar="https://github.com/hikariatama/assets/raw/master/hikka-logs.png",
  485. )
  486. self._logchat = int(f"-100{chat.id}")
  487. if "DYNO" not in os.environ:
  488. self.watchdog.start()
  489. if not is_new and any(
  490. participant.id == self.inline.bot_id
  491. for participant in (await self._client.get_participants(chat, limit=3))
  492. ):
  493. logging.getLogger().handlers[0].install_tg_log(self)
  494. logger.debug(f"Bot logging installed for {self._logchat}")
  495. return
  496. logger.debug("New logging chat created, init setup...")
  497. try:
  498. await self._client(InviteToChannelRequest(chat, [self.inline.bot_username]))
  499. except Exception:
  500. logger.warning("Unable to invite logger to chat")
  501. try:
  502. await self._client(
  503. EditAdminRequest(
  504. channel=chat,
  505. user_id=self.inline.bot_username,
  506. admin_rights=ChatAdminRights(ban_users=True),
  507. rank="Logger",
  508. )
  509. )
  510. except Exception:
  511. pass
  512. logging.getLogger().handlers[0].install_tg_log(self)
  513. logger.debug(f"Bot logging installed for {self._logchat}")