test.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  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. "🌘 <b>Hikka logs with verbosity </b><code>{}</code>\n\n👩‍🎤 <b>Hikka"
  38. " version: {}.{}.{}</b>{}\n⏱ <b>Uptime:"
  39. " {}</b>\n<b>{}</b>\n\n<b>{}</b>\n\n<b>{} NoNick</b>\n<b>{} Grep</b>\n<b>{}"
  40. " InlineLogs</b>"
  41. ),
  42. "suspend_invalid_time": "🚫 <b>Invalid time to suspend</b>",
  43. "suspended": "🥶 <b>Bot suspended for</b> <code>{}</code> <b>seconds</b>",
  44. "results_ping": (
  45. "⏱ <b>Telegram ping:</b> <code>{}</code> <b>ms</b>\n👩‍💼 <b>Uptime: {}</b>"
  46. ),
  47. "ping_hint": (
  48. "💡 <i>Telegram ping mostly depends on Telegram servers latency and other"
  49. " external factors and has nothing to do with the parameters of server on"
  50. " which userbot is installed</i>"
  51. ),
  52. "confidential": (
  53. "⚠️ <b>Log level </b><code>{}</code><b> may reveal your confidential info,"
  54. " be careful</b>"
  55. ),
  56. "confidential_text": (
  57. "⚠️ <b>Log level </b><code>{0}</code><b> may reveal your confidential info,"
  58. " be careful</b>\n<b>Type </b><code>.logs {0} force_insecure</code><b> to"
  59. " ignore this warning</b>"
  60. ),
  61. "choose_loglevel": "💁‍♂️ <b>Choose log level</b>",
  62. "database_unlocked": "🚫 DB eval unlocked",
  63. "database_locked": "✅ DB eval locked",
  64. "bad_module": "🚫 <b>Module not found</b>",
  65. "debugging_enabled": (
  66. "🧑‍💻 <b>Debugging mode enabled for module </b><code>{0}</code>\n<i>Go to"
  67. " directory named `debug_modules`, edit file named `{0}.py` and see changes"
  68. " in real time</i>"
  69. ),
  70. "debugging_disabled": "✅ <b>Debugging disabled</b>",
  71. "heroku_debug": "🚫 <b>Debugging is not available on Heroku</b>",
  72. }
  73. strings_ru = {
  74. "set_loglevel": "🚫 <b>Укажи уровень логов числом или строкой</b>",
  75. "no_logs": "ℹ️ <b>У тебя нет логов уровня {}.</b>",
  76. "logs_filename": "hikka-logs.txt",
  77. "logs_caption": (
  78. "🌘 <b>Логи Hikka уровня </b><code>{}</code>\n\n👩‍🎤 <b>Версия Hikka:"
  79. " {}.{}.{}</b>{}\n⏱ <b>Uptime: {}</b>\n<b>{}</b>\n\n<b>{}</b>\n\n<b>{}"
  80. " NoNick</b>\n<b>{} Grep</b>\n<b>{} InlineLogs</b>"
  81. ),
  82. "database_unlocked": "🚫 База скомпрометирована",
  83. "database_locked": "✅ База защищена",
  84. "bad_module": "🚫 <b>Модуль не найден</b>",
  85. "debugging_enabled": (
  86. "🧑‍💻 <b>Режим разработчика включен для модуля"
  87. " </b><code>{0}</code>\n<i>Отправляйся в директорию `debug_modules`,"
  88. " изменяй файл `{0}.py`, и смотри изменения в режиме реального времени</i>"
  89. ),
  90. "debugging_disabled": "✅ <b>Режим разработчика выключен</b>",
  91. "suspend_invalid_time": "🚫 <b>Неверное время заморозки</b>",
  92. "suspended": "🥶 <b>Бот заморожен на</b> <code>{}</code> <b>секунд</b>",
  93. "results_ping": (
  94. "⏱ <b>Скорость отклика Telegram:</b> <code>{}</code> <b>ms</b>\n👩‍💼"
  95. " <b>Прошло с последней перезагрузки: {}</b>"
  96. ),
  97. "ping_hint": (
  98. "💡 <i>Скорость отклика Telegram в большей степени зависит от загруженности"
  99. " серверов Telegram и других внешних факторов и никак не связана с"
  100. " параметрами сервера, на который установлен юзербот</i>"
  101. ),
  102. "confidential": (
  103. "⚠️ <b>Уровень логов </b><code>{}</code><b> может содержать личную"
  104. " информацию, будь осторожен</b>"
  105. ),
  106. "confidential_text": (
  107. "⚠️ <b>Уровень логов </b><code>{0}</code><b> может содержать личную"
  108. " информацию, будь осторожен</b>\n<b>Напиши </b><code>.logs {0}"
  109. " force_insecure</code><b>, чтобы отправить логи игнорируя"
  110. " предупреждение</b>"
  111. ),
  112. "choose_loglevel": "💁‍♂️ <b>Выбери уровень логов</b>",
  113. "_cmd_doc_dump": "Показать информацию о сообщении",
  114. "_cmd_doc_logs": (
  115. "<уровень> - Отправляет лог-файл. Уровни ниже WARNING могут содержать"
  116. " личную инфомрацию."
  117. ),
  118. "_cmd_doc_suspend": "<время> - Заморозить бота на некоторое время",
  119. "_cmd_doc_ping": "Проверяет скорость отклика юзербота",
  120. "_cls_doc": "Операции, связанные с самотестированием",
  121. "heroku_debug": "🚫 <b>Режим разработчика не доступен на Heroku</b>",
  122. }
  123. def __init__(self):
  124. self.config = loader.ModuleConfig(
  125. loader.ConfigValue(
  126. "force_send_all",
  127. False,
  128. "Forcefully send logs to all clients, aka do not split logs "
  129. "to <mine> and <not-mine>. Restart required after setting",
  130. validator=loader.validators.Boolean(),
  131. )
  132. )
  133. logging.getLogger().handlers[0].force_send_all = self.config["force_send_all"]
  134. async def dumpcmd(self, message: Message):
  135. """Use in reply to get a dump of a message"""
  136. if not message.is_reply:
  137. return
  138. await utils.answer(
  139. message,
  140. "<code>"
  141. + utils.escape_html((await message.get_reply_message()).stringify())
  142. + "</code>",
  143. )
  144. @loader.loop(interval=1)
  145. async def watchdog(self):
  146. try:
  147. for module in os.scandir(DEBUG_MODS_DIR):
  148. last_modified = os.stat(module.path).st_mtime
  149. cls_ = module.path.split("/")[-1].split(".py")[0]
  150. if cls_ not in self._memory:
  151. self._memory[cls_] = last_modified
  152. continue
  153. if self._memory[cls_] == last_modified:
  154. continue
  155. self._memory[cls_] = last_modified
  156. logger.debug(f"Reloading debug module {cls_}")
  157. with open(module.path, "r") as f:
  158. try:
  159. await next(
  160. module
  161. for module in self.allmodules.modules
  162. if module.__class__.__name__ == "LoaderMod"
  163. ).load_module(
  164. f.read(),
  165. None,
  166. save_fs=False,
  167. )
  168. except Exception:
  169. logger.exception("Failed to reload module in watchdog")
  170. except Exception:
  171. logger.exception("Failed debugging watchdog")
  172. return
  173. async def debugmodcmd(self, message: Message):
  174. """[module] - For developers: Open module for debugging
  175. You will be able to track changes in real-time"""
  176. if "DYNO" in os.environ:
  177. await utils.answer(message, self.strings("heroku_debug"))
  178. return
  179. args = utils.get_args_raw(message)
  180. instance = None
  181. for module in self.allmodules.modules:
  182. if (
  183. module.__class__.__name__.lower() == args.lower()
  184. or module.strings["name"].lower() == args.lower()
  185. ):
  186. if os.path.isfile(
  187. os.path.join(
  188. DEBUG_MODS_DIR,
  189. f"{module.__class__.__name__}.py",
  190. )
  191. ):
  192. os.remove(
  193. os.path.join(
  194. DEBUG_MODS_DIR,
  195. f"{module.__class__.__name__}.py",
  196. )
  197. )
  198. try:
  199. delattr(module, "hikka_debug")
  200. except AttributeError:
  201. pass
  202. await utils.answer(message, self.strings("debugging_disabled"))
  203. return
  204. module.hikka_debug = True
  205. instance = module
  206. break
  207. if not instance:
  208. await utils.answer(message, self.strings("bad_module"))
  209. return
  210. with open(
  211. os.path.join(
  212. DEBUG_MODS_DIR,
  213. f"{instance.__class__.__name__}.py",
  214. ),
  215. "wb",
  216. ) as f:
  217. f.write(inspect.getmodule(instance).__loader__.data)
  218. await utils.answer(
  219. message,
  220. self.strings("debugging_enabled").format(instance.__class__.__name__),
  221. )
  222. async def logscmd(
  223. self,
  224. message: Union[Message, InlineCall],
  225. force: bool = False,
  226. lvl: Union[int, None] = None,
  227. ):
  228. """<level> - Dumps logs. Loglevels below WARNING may contain personal info."""
  229. if not isinstance(lvl, int):
  230. args = utils.get_args_raw(message)
  231. try:
  232. try:
  233. lvl = int(args.split()[0])
  234. except ValueError:
  235. lvl = getattr(logging, args.split()[0].upper(), None)
  236. except IndexError:
  237. lvl = None
  238. if not isinstance(lvl, int):
  239. try:
  240. if not self.inline.init_complete or not await self.inline.form(
  241. text=self.strings("choose_loglevel"),
  242. reply_markup=[
  243. [
  244. {
  245. "text": "🚨 Critical",
  246. "callback": self.logscmd,
  247. "args": (False, 50),
  248. },
  249. {
  250. "text": "🚫 Error",
  251. "callback": self.logscmd,
  252. "args": (False, 40),
  253. },
  254. ],
  255. [
  256. {
  257. "text": "⚠️ Warning",
  258. "callback": self.logscmd,
  259. "args": (False, 30),
  260. },
  261. {
  262. "text": "ℹ️ Info",
  263. "callback": self.logscmd,
  264. "args": (False, 20),
  265. },
  266. ],
  267. [
  268. {
  269. "text": "🧑‍💻 Debug",
  270. "callback": self.logscmd,
  271. "args": (False, 10),
  272. },
  273. {
  274. "text": "👁 All",
  275. "callback": self.logscmd,
  276. "args": (False, 0),
  277. },
  278. ],
  279. [{"text": "🚫 Cancel", "action": "close"}],
  280. ],
  281. message=message,
  282. ):
  283. raise
  284. except Exception:
  285. await utils.answer(message, self.strings("set_loglevel"))
  286. return
  287. logs = "\n\n".join(
  288. [
  289. "\n".join(
  290. handler.dumps(lvl, client_id=self._client.tg_id)
  291. if "client_id" in inspect.signature(handler.dumps).parameters
  292. else handler.dumps(lvl)
  293. )
  294. for handler in logging.getLogger().handlers
  295. ]
  296. )
  297. named_lvl = (
  298. lvl
  299. if lvl not in logging._levelToName
  300. else logging._levelToName[lvl] # skipcq: PYL-W0212
  301. )
  302. if (
  303. lvl < logging.WARNING
  304. and not force
  305. and (
  306. not isinstance(message, Message)
  307. or "force_insecure" not in message.raw_text.lower()
  308. )
  309. ):
  310. try:
  311. if not self.inline.init_complete:
  312. raise
  313. cfg = {
  314. "text": self.strings("confidential").format(named_lvl),
  315. "reply_markup": [
  316. {
  317. "text": "📤 Send anyway",
  318. "callback": self.logscmd,
  319. "args": [True, lvl],
  320. },
  321. {"text": "🚫 Cancel", "action": "close"},
  322. ],
  323. }
  324. if isinstance(message, Message):
  325. if not await self.inline.form(**cfg, message=message):
  326. raise
  327. else:
  328. await message.edit(**cfg)
  329. except Exception:
  330. await utils.answer(
  331. message,
  332. self.strings("confidential_text").format(named_lvl),
  333. )
  334. return
  335. if len(logs) <= 2:
  336. if isinstance(message, Message):
  337. await utils.answer(message, self.strings("no_logs").format(named_lvl))
  338. else:
  339. await message.edit(self.strings("no_logs").format(named_lvl))
  340. await message.unload()
  341. return
  342. if btoken := self._db.get("hikka.inline", "bot_token", False):
  343. logs = logs.replace(
  344. btoken,
  345. f'{btoken.split(":")[0]}:***************************',
  346. )
  347. if hikka_token := self._db.get("HikkaDL", "token", False):
  348. logs = logs.replace(
  349. hikka_token,
  350. f'{hikka_token.split("_")[0]}_********************************',
  351. )
  352. if hikka_token := self._db.get("Kirito", "token", False):
  353. logs = logs.replace(
  354. hikka_token,
  355. f'{hikka_token.split("_")[0]}_********************************',
  356. )
  357. if os.environ.get("DATABASE_URL"):
  358. logs = logs.replace(
  359. os.environ.get("DATABASE_URL"),
  360. "postgre://**************************",
  361. )
  362. if os.environ.get("REDIS_URL"):
  363. logs = logs.replace(
  364. os.environ.get("REDIS_URL"),
  365. "postgre://**************************",
  366. )
  367. if os.environ.get("hikka_session"):
  368. logs = logs.replace(
  369. os.environ.get("hikka_session"),
  370. "StringSession(**************************)",
  371. )
  372. logs = BytesIO(logs.encode("utf-16"))
  373. logs.name = self.strings("logs_filename")
  374. ghash = utils.get_git_hash()
  375. other = (
  376. *main.__version__,
  377. " <i><a"
  378. f' href="https://github.com/hikariatama/Hikka/commit/{ghash}">({ghash[:8]})</a></i>'
  379. if ghash
  380. else "",
  381. utils.formatted_uptime(),
  382. utils.get_named_platform(),
  383. self.strings(
  384. f"database_{'un' if self._db.get(main.__name__, 'enable_db_eval', False) else ''}locked"
  385. ),
  386. "✅" if self._db.get(main.__name__, "no_nickname", False) else "🚫",
  387. "✅" if self._db.get(main.__name__, "grep", False) else "🚫",
  388. "✅" if self._db.get(main.__name__, "inlinelogs", False) else "🚫",
  389. )
  390. if getattr(message, "out", True):
  391. await message.delete()
  392. if isinstance(message, Message):
  393. await utils.answer(
  394. message,
  395. logs,
  396. caption=self.strings("logs_caption").format(named_lvl, *other),
  397. )
  398. else:
  399. await self._client.send_file(
  400. message.form["chat"],
  401. logs,
  402. caption=self.strings("logs_caption").format(named_lvl, *other),
  403. )
  404. @loader.owner
  405. async def suspendcmd(self, message: Message):
  406. """<time> - Suspends the bot for N seconds"""
  407. try:
  408. time_sleep = float(utils.get_args_raw(message))
  409. await utils.answer(
  410. message,
  411. self.strings("suspended").format(time_sleep),
  412. )
  413. time.sleep(time_sleep)
  414. except ValueError:
  415. await utils.answer(message, self.strings("suspend_invalid_time"))
  416. async def pingcmd(self, message: Message):
  417. """Test your userbot ping"""
  418. start = time.perf_counter_ns()
  419. message = await utils.answer(message, "<code>🐻 Nofin...</code>")
  420. await utils.answer(
  421. message,
  422. self.strings("results_ping").format(
  423. round((time.perf_counter_ns() - start) / 10**6, 3),
  424. utils.formatted_uptime(),
  425. )
  426. + (
  427. ("\n\n" + self.strings("ping_hint"))
  428. if random.choice([0, 0, 1]) == 1
  429. else ""
  430. ),
  431. )
  432. async def client_ready(self, *_):
  433. chat, is_new = await utils.asset_channel(
  434. self._client,
  435. "hikka-logs",
  436. "🌘 Your Hikka logs will appear in this chat",
  437. silent=True,
  438. avatar="https://github.com/hikariatama/assets/raw/master/hikka-logs.png",
  439. )
  440. self._logchat = int(f"-100{chat.id}")
  441. if "DYNO" not in os.environ:
  442. self.watchdog.start()
  443. if not is_new and any(
  444. participant.id == self.inline.bot_id
  445. for participant in (await self._client.get_participants(chat, limit=3))
  446. ):
  447. logging.getLogger().handlers[0].install_tg_log(self)
  448. logger.debug(f"Bot logging installed for {self._logchat}")
  449. return
  450. logger.debug("New logging chat created, init setup...")
  451. try:
  452. await self._client(InviteToChannelRequest(chat, [self.inline.bot_username]))
  453. except Exception:
  454. logger.warning("Unable to invite logger to chat")
  455. try:
  456. await self._client(
  457. EditAdminRequest(
  458. channel=chat,
  459. user_id=self.inline.bot_username,
  460. admin_rights=ChatAdminRights(ban_users=True),
  461. rank="Logger",
  462. )
  463. )
  464. except Exception:
  465. pass
  466. logging.getLogger().handlers[0].install_tg_log(self)
  467. logger.debug(f"Bot logging installed for {self._logchat}")